Metadata-Version: 2.5
Name: h2h-dev-tools
Version: 0.10.4
Summary: Shared development tools and reproducible repository snapshots for H2H projects
Project-URL: Homepage, https://github.com/Kuan-Lun/h2h-dev-tools
Project-URL: Source, https://github.com/Kuan-Lun/h2h-dev-tools
Project-URL: Tracker, https://github.com/Kuan-Lun/h2h-dev-tools/issues
Author: Kuan-Lun Wang
License-Expression: GPL-3.0-only
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.14
Requires-Dist: distlib>=0.4.4; sys_platform == 'win32'
Requires-Dist: packaging>=26.0
Provides-Extra: dev
Requires-Dist: build>=1.5.0; extra == 'dev'
Requires-Dist: distlib>=0.4.4; extra == 'dev'
Requires-Dist: hatchling>=1.32.0; extra == 'dev'
Requires-Dist: mypy>=2.3.1; extra == 'dev'
Requires-Dist: pytest-xdist>=3.8.0; extra == 'dev'
Requires-Dist: pytest>=9.1.1; extra == 'dev'
Requires-Dist: ruff>=0.16.4; extra == 'dev'
Description-Content-Type: text/markdown

# h2h-dev-tools

Shared development tools for H2H repositories. The Python package owns common
implementations; each consumer commits a verified source snapshot so its hooks
run offline against the code present in that repository's Git tree.

## Install and synchronize

Use Python 3.14 or newer. When version 0.9.0 is available on your selected
index, install it in a development environment:

```bash
python -m pip install h2h-dev-tools==0.9.0
```

For candidate validation, install an explicitly supplied wheel instead. These
examples do not assert publication status:

```bash
python -m pip install /absolute/path/to/h2h_dev_tools-0.9.0-py3-none-any.whl
```

Declare the components the consumer actually uses in its `pyproject.toml`:

```toml
[tool.h2h-dev-tools]
components = ["change-scope", "dependencies", "documentation", "git-flow"]
```

Then generate or update the snapshot:

```bash
h2h-dev-tools sync --repository /absolute/path/to/consumer
h2h-dev-tools verify --repository /absolute/path/to/consumer
```

Selection is explicit. The generator includes each selected component's complete
dependency closure and removes previously verified files that are no longer
selected. Verification rejects stale selection, modified files and unknown files.
Python modules and shell resources share the same hash-bound manifest.

When upgrading across releases that change a component's dependency closure,
supply the wheel that produced the existing snapshot explicitly:

```bash
h2h-dev-tools sync --repository /absolute/path/to/consumer \
  --previous-wheel /absolute/path/to/h2h_dev_tools-0.2.2-py3-none-any.whl
```

Use a trusted wheel from the exact previous build; matching version strings alone
are insufficient. The updater reads its distribution metadata and source bytes
without executing its code. Every declared existing file must match that wheel
and its manifest hash. Unknown files, local modifications, symlinks and missing
declared files block replacement. The tool rechecks ownership before writing.
The supplied wheel proves which existing files may be replaced or removed; it
does not certify that the old component graph is complete or passed a gate.
An absent file also omitted from the old manifest may be restored by this explicit
repair. The new snapshot must always pass the current complete closure verifier.
Normal `verify`, hooks and `sync` without `--previous-wheel` retain their strict
current-closure checks; none executes a compatibility fallback or downloads a wheel.

| Component | Responsibility |
| --- | --- |
| `atomic-files` | Atomic byte publication with explicit replace or no-replace semantics; callers own JSON and directory policy |
| `change-scope`, `documentation` | Exact Git candidate classification and documentation checks |
| `dependencies`, `dependency-audit` | Declared dependencies, registry candidates and reviewed audit receipts |
| `version-policy` | Task-level release impact, version and audit validation |
| `primary-branch`, `git-flow`, `hooks`, `environment` | Local branch selection, safe merge lifecycle, hook installation, commit-message validation and environment rebuild |
| `publish-version` | Version transition across a complete push |
| `distribution-check` | Installed requirements, RECORD and import-origin validation |
| `offline-wheel` | Temporary candidate-wheel environment with development dependencies and a local smoke callback |
| `process` | Owned process trees, deadlines, signal deferral and Windows Job start gates |
| `git-index` | Reject hidden index entries without changing their flags |
| `git-environment` | Isolate test children from inherited Git hook repository authority |
| `pytest` | Fresh source and assertion caches with explicit import roots for pytest and local xdist workers |
| `formal` | Pinned TLC download and offline host/Docker execution |
| `code-review` | Exact-candidate Codex review and offline evidence verification |
| `test-support` | Portable executable Python command fixtures |
| `review-test-support` | Explicitly synthetic V3 responses for disposable workflow fixtures |

Disposable command fixtures may select `test-support` and call
`executable_fixture.write_python_executable(path, source, python=...)`; always use its returned
executable path. POSIX uses a quoted shell `exec` launcher and adjacent `.py`
payload located relative to the actual launcher, preserving the selected interpreter
(including venv symlinks), stdin, arguments and PID. Windows returns a native
`.exe` console launcher generated by
[distlib's documented ScriptMaker API](https://distlib.readthedocs.io/en/stable/tutorial.html#generating-scripts).
Consumers selecting this component must declare `distlib>=0.4.4` in their
development dependencies on every platform: strict static analysis also resolves
the Windows import on POSIX. This test-only dependency does not belong in consumer
runtime requirements.
The helper uses the locally installed dependency offline; it does not download
or retain native launcher binaries in generated source snapshots. Windows source
is embedded in the executable's `__main__.py`; `sys.argv[0]` is the executable,
while POSIX uses the adjacent payload for both `sys.argv[0]` and `__file__`.
Callers must not infer command names from these platform-specific identities.
POSIX launcher/payload pairs can move together; standalone launcher symlinks are
not supported. npm-style entry fixtures may explicitly pass `windows_cmd=True`:
Windows then adds and returns an ASCII `.cmd` wrapper for the same-stem native
executable, using normal batch argument semantics instead of the default native
argument contract. For example, documentation lint stubs use
`write_python_executable(tools / "markdownlint-cli2", body, windows_cmd=True)` to
match npm's `.cmd` discovery contract. POSIX behavior is unchanged by this flag.
Only the explicit launcher, POSIX payload and optional wrapper files are replaced.
The helper does not execute fixtures, change console code pages, add Python startup
flags or change the selected interpreter. Native Windows path, argument and stdio behavior
is checked by the Windows platform workflow; POSIX checks do not prove it.

Disposable workflow tests may explicitly select `review-test-support`. After
bootstrapping that repository's verified snapshot, call
`review_test_support.synthetic_review_response_source()` and include the returned
self-contained module in a fake CLI script. Its
`synthetic_review_response(prompt)` projects the V3 assignment into synthetic raw
output; local fixtures retain CLI arguments, failure injection, event tracking and
output-file handling. Serialize the result with compact UTF-8 JSON
(`json.dumps(result, ensure_ascii=False, separators=(",", ":"))`) to preserve the
assignment's wire bound. The builder rejects malformed or legacy prompts and does
not perform code review or write receipts. Production `code-review` and `git-flow`
components do not depend on this test support. Never use a fake CLI for actual
candidate review; genuine review and coverage validators remain unchanged.

Schema-1 snapshots from 0.1 use a fixed payload and are not accepted by normal
schema-2 verification or sync. After declaring components, convert once:

```bash
h2h-dev-tools migrate-v1 --repository /absolute/path/to/consumer
```

The converter verifies the complete old manifest, file set and hashes before
updating generated files. It preserves unrelated files and rejects edited or
incomplete snapshots. This conversion changes development tools only; application
databases, archives and runtime environments do not need conversion.

Review the resulting consumer diff, run that repository's checks and commit the
snapshot update on its task branch. Sync is an explicit source update; it does
not commit, merge, push or publish. Consumers do not need this installed package
to run their committed snapshots, and do not add it to runtime dependencies.

The consumer snapshot lives in `scripts/_shared/h2h_dev_tools/`. Its tracked
`manifest.json` identifies the toolkit version and source payload hashes; these
are payload identities, not the wheel archive hash. Generated snapshots must
never be edited manually. A consumer checks its local snapshot without network
or an installed copy of the tool:

```bash
python scripts/_shared/h2h_dev_tools/bundle_verify.py --repository .
```

The standalone `bundle_verify.py` command above performs verification itself.
Other shared Python modules must be called through repository-owned entries;
direct execution of their generated `.py` files does not verify the snapshot.
Python consumer entry points must source-bootstrap before their first shared
import (also before `runpy.run_module`):

```python
import runpy
import sys
from pathlib import Path

repository = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(repository / "scripts"))
package = repository / "scripts/_shared/h2h_dev_tools"
runpy.run_path(str(package / "source_loader.py"))["load_package"](
    package, "_shared.h2h_dev_tools"
)
```

This entry verifies the complete snapshot and directly compiles the verified
source bytes, including the package initializer and relative imports. Python
bytecode caches may exist but are never executed by this loader; `-B` alone only
prevents new cache writes. Repeated bootstrap of the same namespace, directory
and bytes preserves existing module and class identity. A changed source set,
conflicting directory or package imported before bootstrap is rejected; start a
fresh process after an update. Review fingerprints use that original loader
registration, rechecking the complete source set before candidate capture and
final verification. Updating and staging source after import cannot relabel old
code as the new candidate; a fresh process is required. Historical Git candidate
blobs remain distinct from the executed source identity.

Qualified namespaces also pin every parent package to its explicit directory.
Parent initializers remain repository-owned and outside the snapshot manifest;
the loader records their source hashes and compiles source without reading
bytecode. A namespace parent records the absence of an initializer. An earlier
ordinary parent import, changed initializer, changed parent path, or newly added
initializer requires a fresh process. Parent initializers may import the verified
child package. Their unrelated imports are outside this flat-package guarantee.

Version 0.7 requires bootstrap before any ordinary import of a qualified parent.
Update generated snapshots with the existing `h2h-dev-tools sync --repository ROOT`
command, start a fresh process, and keep bootstrap ahead of parent or shared imports
in handwritten entries. There is no fallback to an already imported parent.
An automatic entry rewriter cannot infer arbitrary initializer side effects or
safe import ordering. Application data is unchanged and needs no conversion.
Existing review evidence is invalidated; establish a new full-repository review
through the normal merge workflow rather than converting old receipts.

Toolkit checkout development uses its `scripts/*.py` source entries, including
editable environments. Snapshot synchronization uses a CLI installed from an
explicit immutable release or candidate wheel, not an editable CLI. Ordinary
installed-wheel imports retain Python's standard import/cache behavior.

The manifest detects changed or incomplete source files; it is not a signature
or proof of the publisher's identity. Review the package source and consumer diff
before adopting an update.

Sync writes individual files before replacing the manifest. An interrupted update
can therefore leave a bundle that verification and later sync deliberately reject.
Inspect and preserve any pending edits first. If this consumer already has a
committed bundle, restore only that generated directory from the intended known
good commit, then run sync again; do not restore the whole repository or overwrite
unrelated changes. For example, after confirming `HEAD` contains the bundle you
intend to recover:

```bash
git diff -- scripts/_shared/h2h_dev_tools
git diff --cached -- scripts/_shared/h2h_dev_tools
git restore --source=HEAD --worktree -- scripts/_shared/h2h_dev_tools
h2h-dev-tools sync --repository .
```

The restore command changes only working files; after sync, review and stage the
complete updated bundle using the consumer's normal workflow.

For an interrupted first sync with no committed bundle, preserve the incomplete
directory outside `scripts/_shared/` after inspecting its contents, then sync into
the vacant destination. The tool never treats unknown files as disposable.

Thin repository entry points supply their own root and policies. Core schema
checks, adapter tests, pytest selectors, timeout budgets, formal targets and
deployment acceptance remain owned by their original repositories. Primary-branch
detection, hook installation and environment rebuild run before a `.venv` exists;
they do not import the Python package.

Commit-message hooks select the `hooks` component and invoke
`bash scripts/_shared/h2h_dev_tools/commit-message.sh ROOT MESSAGE_FILE PATTERN`.
Each repository declares its own Bash regular expression. The shared helper reads
the first line and rejects unreadable messages and invalid or empty patterns.
A subject starting with `Merge` followed by a space is exempt only while Git
verifies `MERGE_HEAD`. Relative message paths resolve against the caller's working
directory; the helper does not change it.

Test launchers select the `git-environment` component and call
`without_local_git_environment(environment, repository=repository_root)` when
building a test child's environment. Shell entry points use a repository-owned
Python entry, which verifies and source-loads the complete selected snapshot
before the Git query or test child can run. For example, create
`scripts/with-git-environment.py`:

```python
# scripts/with-git-environment.py
import runpy
import sys
from pathlib import Path

repository = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(repository / "scripts"))
package = repository / "scripts/_shared/h2h_dev_tools"
runpy.run_path(str(package / "source_loader.py"))["load_package"](
    package, "_shared.h2h_dev_tools"
)

from _shared.h2h_dev_tools.git_environment import main

raise SystemExit(main())
```

Then invoke that entry with the existing CLI arguments:

```bash
python scripts/with-git-environment.py --repository ROOT -- COMMAND
```

Both use Git's `rev-parse --local-env-vars` inventory, reject failed or malformed
queries and preserve other environment values. On POSIX the CLI replaces itself
with the test command; on Windows it waits for the child and returns its exit code
inside the existing Job. Neither path creates another process owner.
Apply this only to test children:
gate checks before and after pytest must retain the real candidate's Git environment.

Pytest entry points select the `pytest` component, source-bootstrap their shared
package as above, then call `pytest_source.main(runtime_sources=...)` before
importing pytest, plugins or repository runtime. The required mapping explicitly
names this repository's pure Python runtime packages and their absolute source
directories. For example, after the verified shared-source bootstrap:

```python
from pathlib import Path
from _shared.h2h_dev_tools.pytest_source import main

repository = Path(__file__).resolve().parents[1]
main(runtime_sources={"h2hdb": repository / "src/h2hdb"})
```

The repository entry owns these declarations; the runner does not infer packages,
search sibling repositories or bind installed dependencies. The six H2H entries
use the following explicit values:

| Repository | Runtime mapping relative to its own root |
| --- | --- |
| Core | `{"h2hdb": root / "src/h2hdb"}` |
| Ingest | `{"h2hdb_ingest": root / "src/h2hdb_ingest"}` |
| OPDS | `{"h2hdb_opds": root / "src/h2hdb_opds"}` |
| Komga | `{"h2hdb_komga": root / "src/h2hdb_komga"}` |
| Downloader | `{"h2hdb_downloader": root / "src/h2hdb_downloader"}` |
| hbrowser | `{"hbrowser": root / "hbrowser"}` |

hbrowser keeps its existing runtime owner and passes a zero-argument callback,
`adapter.run(lambda: main(runtime_sources={"hbrowser": root / "hbrowser"}), pytest_entry=True)`.
The toolkit's own `scripts/run-pytest.py` explicitly passes `{}` because its only
owned runtime package is already covered by the verified toolkit source loader.

The controller captures the complete Python source inventory and directory
topology before pytest loads.
Each local xdist worker must match that same inventory before loading plugins or
conftests. Imports under declared names use candidate source even if `PYTHONPATH`
points at another checkout; source bytes are checked again when each module is
first loaded. Earlier ordinary imports, changed or added source, source symlinks,
redirected package paths and falling back to another installation are rejected.
Roots must be regular Python packages. Nested namespace directories, including
empty and resource-only directories, are retained and checked against the same
controller inventory. Namespace resource access uses ordinary file reads; resource
bytes are outside the Python source hash guarantee.
Native extension modules and installed dependency packages are outside this
pure-Python source declaration. Start a fresh process for each invocation.

The verified toolkit package and declared runtime packages are compiled directly
from verified source, even when a plugin registers them for assertion rewriting.
The runner wraps pytest's
`assertion.install_importhook` only for this invocation to keep the runtime finder
and this invocation's unique registered toolkit finder before the newly installed
rewrite hook, including an empty runtime mapping. An unsupported installer or registration
fails explicitly. Tests, conftests and other packages retain normal assertion
rewriting; `--assert=plain` keeps its ordinary meaning. The installer is restored
when the controller exits.

The helper preserves pytest arguments and uses a unique empty bytecode-cache
prefix with cache writes disabled. It prepends the verified toolkit import root
to `sys.path` and `PYTHONPATH`, preserving the existing environment suffix.
The local xdist gateway hook revalidates and source-loads the same toolkit package
using the controller's original hashes and complete parent directory/initializer
authority, captured before pytest or plugins are imported. A missing parent
initializer is explicit authority, so adding one later is also rejected.
Worker bootstrap and snapshot verifier code are compiled from the bytes checked
against those hashes. Workers explicitly
set the same cache prefix and write policy, including interpreters started with
`-I` or `-E`, which ignore inherited `PYTHON*` environment variables.
Settings and the runtime finder are restored when pytest exits; imported modules
are not unloaded. Remote or chained workers need their own explicit source
distribution and are outside this entry's guarantee. Selectors, dependencies,
process ownership and deadlines remain repository-owned.

Version 0.5 removes the no-argument `main()` call; there is no fallback that silently
leaves runtime imports unbound. To update a consumer, install an explicit release
or candidate wheel, regenerate its snapshot with the existing `h2h-dev-tools sync
--repository ROOT` command, and update its thin entry with the mapping above in
the same task. Existing snapshots continue using their own recorded version until
that explicit update. Application data needs no conversion. No automatic source
rewriter is provided: package names and source paths remain the entry's declared
contract, and installed-wheel smoke keeps ordinary imports.

Version paths are declared in `[tool.h2h.version]`; dependency inventory comes
from the project manifests. Adding, changing or removing only the `dev` optional
extra does not require a release; other named extras, including empty extras,
remain release metadata. Shared version evaluation considers only commits that
actually change release surface. Commit type and `!` signals come only from each
commit's subject; quoted subjects in its body do not affect the version.
An explicit `BREAKING CHANGE:` footer remains an independent breaking signal.
Where enabled, `Version-Impact: none` and its
nonempty `Version-Reason` exempt only their own commit; later release changes
still require the task's single version bump, and extra candidate changes cannot
borrow that exemption. Exemptions that cannot be established from a clean
automatic three-way merge are rejected. Hook installation refuses to replace an
existing third-party hook directory. Rebuild validates explicit source overrides before
replacing the environment. Relative `--source PACKAGE=SOURCE` paths are resolved
from the caller's working directory, including when the rebuild script is invoked
from outside its repository. Local wrappers must preserve that working directory
while locating the repository. The shared rebuild captures it before changing to
the repository, then passes it as `--source-base` to the source resolver. The
repository's `.[dev]` installation, npm and after-build callback still run from
the repository root; absolute paths, URLs and index sources keep their meaning.
Environment rebuilds require uv 0.13.0 or newer, verified before creating or
replacing an environment. uv 0.10.12 mishandles encoded local project paths
containing `#` or `%`; 0.13.0 is the verified baseline for this rebuild workflow.
Upgrade uv before rerunning the rebuild command.
Local dependency checkouts supplied through `--source` are built and installed
as ordinary distributions, so their imports and metadata satisfy installed-wheel
verification. Unlike the editable installation of this repository itself, edits
to a dependency checkout take effect only after rebuilding the environment.
This replaces the automatic editable dependency installation from versions before
0.4; existing environments can be converted by rerunning the same rebuild command.
No application data conversion or deletion is needed.
Repository path changes use command-local `CDPATH=` so an interactive shell's
search paths cannot redirect tooling or contaminate machine-readable output.
The caller's environment remains available to invoked commands and callbacks.
Merge checks dirty worktrees, in-progress Git operations
and stable refs; it runs the local candidate review/gate and aborts on failure.
Primary worktree discovery queries the exact branch with NUL-delimited fields;
paths containing line breaks are preserved. Repository-local merge, gate and
hook entries must likewise preserve path bytes when determining the Git root.
Merge always creates a pending two-parent candidate with `--no-ff --no-commit`,
using the captured task commit and preserving its branch name in the merge message.
Review and version checks require the actual `MERGE_HEAD`; the direct strategy and
`WORKFLOW_MERGE_TASK_REF` fallback are removed. When updating the `git-flow`
snapshot, remove the old `candidate` or `direct` argument from the consumer wrapper:

```bash
exec bash "$repository_root/scripts/_shared/h2h_dev_tools/git-flow-merge.sh" \
  "$repository_root" scripts/review-merge-candidate.sh "$@"
```

The consumer's pre-commit hook must delegate pending merges to its pre-merge gate.
There is no automatic wrapper conversion because that gate remains repository-owned.
Merge, review and release gates reject `assume-unchanged` and `skip-worktree`
index entries before trusting workspace cleanliness. They preserve these flags and
local contents; inspect and explicitly resolve them before retrying. Cleanliness
queries use NUL-delimited raw path bytes, including whitespace-only filenames.
Switches and recovery use Git's `--no-overwrite-ignore` protection. Before a
merge, the owner checks task and predicted merge destinations for untracked or
ignored obstructions, including directory/file transitions and paths created by
directory renames. This requires Git 2.38 or newer: `merge-tree --write-tree`
previews the captured parents without changing the index or worktree, and a
conflicting preview stops before the real merge. Both stages use the `ort`
strategy and the same repository merge configuration. Primary
`branch.<primary>.mergeOptions` may be empty or the single exact `--no-ff` value
set by the canonical hook installer. Other custom, combined or repeated values
are rejected explicitly; resolve them before retrying. The actual merge pins empty
branch options for that command without changing persistent configuration. Git's no-overwrite
option alone does not protect every `--no-ff` merge implementation. Planning is
a normal owned stage under the existing outer lifetime; failure recovery keeps
its separate ten-second limits.

Already-integrated tasks use checked cleanup without another merge or gate.
Cleanup rechecks that the original task tip remains unchanged and is contained in
primary before removing a linked worktree. Branch deletion uses primary's checked
out HEAD as its ancestry authority, independent of the task's configured upstream.
A temporary `reference-transaction` hook rechecks the actual task tip while Git
holds its deletion lock, forwarding every event and its unchanged input to the
existing hook. A task advanced during review or
cleanup is retained, and cleanup failure never rolls back an already successful
merge.
Workflow commands that change Git state use repository-local Python to own their
hook process trees, including checkout, merge, failure recovery and task-branch
deletion. Starting with 0.3.0, consumers of the `git-flow` component must create
their local `.venv` before merging or cleaning up an already-merged task. Run
the repository's `scripts/rebuild-env.sh` once if it does not exist; branch
detection and bootstrap still work without it. Linked-worktree cleanup also
requires the primary checkout's `.venv`, so removing the task worktree cannot
remove the interpreter needed to finish cleanup. Worktree removal refuses any
untracked or ignored material, including empty directories, symlinks and `.venv`;
there is no environment or ignore-pattern deletion allowlist. A successful merge
therefore retains a linked task containing its local environment and reports
cleanup as incomplete. It preserves the merge commit, task branch and worktree.
Inspect and save or remove the retained material yourself, then run
`scripts/git-flow-merge.sh --cleanup-only task-branch` from the primary checkout.
This named form uses primary's local `.venv` and does not require one in the task
worktree; it acquires admission before resolving the task refs and worktree.
If the task worktree was already removed, it still checks ancestry and safely
deletes the unchanged task branch. The original task-checkout `--cleanup-only`
form remains available. Neither form repeats review for an integrated task.
Existing application data does not need conversion.
This contract does not cover custom helpers used by separate read-only Git
queries, such as an external filesystem monitor, or deliberately detached
processes outside the owned POSIX group.
An interruption waits for that cleanup before aborting a pending candidate; a
commit already completed is retained. Unproven process cleanup retains the
candidate and task for inspection instead of letting a surviving hook race an abort.
Normal workflow commands have no automatic timeout and can be interrupted through
the outer wrapper. During failure recovery, each abort or checkout command has
a separate 10-second hard budget including the existing 6-second teardown reserve.
Recovery Git output is captured and replayed after its owner finishes, up to
64 KiB with a truncation notice when the destination accepts it. Recovery owner
startup errors are suppressed; a failed startup has no cleanup receipt and blocks
further mutation. Owner progress, replay and shell failure messages use the shared
best-effort diagnostic writer described below, so a closed or stalled pipe cannot
delay recovery cleanup or receipt publication. Once the owner is prepared,
this also covers ordinary failures before the shell enters its recovery trap.
All shell launches of the stdlib process owner, including source-closure discovery,
normal commands, review, recovery and diagnostics, use `-I -S -B` to exclude site
startup code before ownership and deadlines exist. The owned Git/classifier/reviewer
children retain their normal environment and installed dependencies.
The diagnostic launcher shares the workflow's tracked PID, signal handoff and
cleanup-receipt checks.
Missing diagnostic cleanup proof retains the candidate and owner files and
stops both further diagnostics and Git recovery.
Preflight messages before an owner exists retain normal stdio behavior; no Git
mutation or owned child has started at that point.
A failed abort prevents the recovery checkout; recovery never rolls back an
already completed merge.

Every merge or `--cleanup-only` workflow first acquires an atomic admission
in Git common metadata (`h2h-merge-workflow.lock`), before sampling mutable refs
or worktrees. This excludes cooperating workflows across linked worktrees,
including repeated invocations for the same task. The copied bootstrap retains
its source authority through task worktree removal. Recovery captures the owned
merge's exact refs, index, worktree and operation state before later checks;
it aborts only that unchanged pending candidate. `AUTO_MERGE` without the owned
`MERGE_HEAD` is insufficient evidence and is retained. Recovery checks tracked,
untracked and ignored obstructions before restoring primary contents.
Incomplete or inconsistent intermediate-state evidence is retained for inspection.
Snapshot reads and abort share the
existing ten-second recovery deadline, including cleanup. An exact committed
merge is never rolled back; after proven process cleanup, admission is released
so `--cleanup-only` can retry remaining task cleanup.

The owner performs obstruction and complete removal-inventory checks before the
native Git mutation under the same workflow admission. These checks do not form
an atomic filesystem transaction with Git: an unrelated process can create a
file after the check. Stop external writers before merge or cleanup; admission
coordinates participating workflows and does not isolate arbitrary filesystem
writers. Git's own tracked-change and ordinary untracked-file protections remain
active, and removal never uses `--force`.

Admission is not a lock against manual Git commands. Observable external drift,
unknown recovery ownership, unproven process cleanup or abrupt owner death
retains the candidate, task and admission; there is no automatic stale reclaim.
Before manually removing retained admission, verify its `owner.json` identity,
the recorded workflow state in the retained `h2h-merge-owner.*` directory,
all owned processes and nested cleanup obligations, and the actual primary,
index and pending operation. Preserve unknown or externally modified state;
resolve it explicitly before retrying. Partial admission initialization is
removed only when this invocation proved it created that directory; failed
cleanup retains the barrier. This local coordination is not tamper protection.

The `code-review` component owns the reviewer implementation; each repository's
`scripts/review-code.py` calls `code_review.main(repository_root)`, and its merge
entry point delegates to the shared `review-merge-candidate.sh` with that root.
Full changes require online review of the staged two-parent candidate before
commit, then offline verification before and after the existing full gate.
Documentation-only changes retain their documentation gate without online review.
Documentation export rejects backslashes and colons in Git path components on
every platform: those otherwise valid POSIX names have Windows separator, drive
or alternate-stream semantics. Export also checks resolved destination containment
and refuses filesystem name collisions instead of overwriting another document.
Review reads the repository's root `AGENTS.md`; findings, incomplete results,
timeouts and invalid output block the merge and preserve the task branch.
Each review invocation sets `notify=[]`: asynchronous completion notifications
can otherwise outlive the CLI and violate its process ownership contract.
This override applies only to that invocation. Model preferences, authentication,
plugins, MCP servers and security hooks retain their existing configuration;
their descendants remain subject to the same cleanup requirements.
Reviews use normal persistent CLI sessions rather than forcing `--ephemeral`,
so subagents can resolve their parent thread. Normal CLI session rollouts are
retained and are not automatically deleted by the reviewer. The raw review
results, diagnostics and exact-candidate evidence in Git metadata remain separate
and continue to follow the same verification rules.
The merge launcher first owns classification in a dedicated process group and
proves that group empty. Only a full-profile candidate then requires the local
review entry and starts the reviewer in its next owned group. The reviewer registers
nested cleanup obligations before starting CLI-version, shard or integration owners.
Documentation needs neither the review entry nor Codex.
The owner is exported from the actual staged candidate after the three-way merge,
so an independently updated primary does not require rebasing the task. The owner
holds the candidate review lock through cleanup and publication. Managed reviewers
write only pending evidence; the owner publishes it at the terminal commit described
below, after successful group exit and fresh candidate/coverage validation.
The managed reviewer validates its direct
parent, group leader and complete owner source closure before sharing ownership.
Every separate Codex session has a registered canonical owner. Unexpected owner
death cannot turn an empty outer group into proof of nested cleanup. The launcher
proves all groups and obligations empty before Git aborts; missing cleanup evidence
retains the candidate and scope for inspection. Standalone
review instead owns its CLI-version and Codex command groups directly. Even a
successful leader exit requires bounded proof that its group is empty before
standalone review evidence can be published.

For long-running review automation, redirect output to a log file and cancel with
TERM sent to the outer merge wrapper. A terminal executor that forcibly kills the
owner cannot run cleanup handlers. Retain the candidate and inspect the surviving
processes; prove their cleanup before manually aborting that candidate.

The first review covers the complete candidate repository, including unchanged
code, tests and configuration. Only a passing full-repository candidate that
actually becomes a commit on the primary's first-parent history establishes a
baseline. Later reviews use the primary-to-candidate diff only while that baseline
and the intervening review chain remain valid. Documentation transitions are
checked by the exact-tree classifier. Missing evidence, unreviewed code in that
history, or changed review policy/implementation requires a new repository review.
Aborted candidates and old difference-only receipts cannot establish a baseline.

Review evidence in Git common metadata `h2h-review` binds the exact tree, ordered
parents, scope, baseline, root policy, local review entry points and shared reviewer,
process and classification sources. Snapshot reviewer parent initializers also
bind their candidate Git mode/blob (or explicit absence) and their current source
mode/hash independently. Managed owners pre-read the same repository dependency;
qualified reviewers additionally verify it against their loaded parent authority.
These identities use repository-relative paths, not checkout locations. Changing
parent source, mode, or presence invalidates earlier evidence, including offline
revision verification. A new review also rejects a current initializer that differs
from the exact candidate, even if Git ignores that file. Historical tree identities
do not claim that the current initializer was executed in that historical review.
Review evidence is separate from test evidence. Run needs an authenticated `codex`
CLI and POSIX process-group support; offline verify also works on Windows when
the recorded source bytes, initializer presence and observed modes are preserved.
A checkout that cannot preserve an executable parent initializer's mode cannot
reuse that receipt; verify it in a supported POSIX checkout instead. This does not
normalize filesystem modes or claim native Windows validation of executable
initializers. The default timeout is 900 seconds
(`--timeout-seconds` accepts 1..14400). Set `H2H_CODE_REVIEW_TIMEOUT_SECONDS=14400`
when invoking `scripts/git-flow-merge.sh` to allow a longer first repository
review; the same CLI validation applies. Old Core `h2hdb-review` receipts are not
converted: run the new review to establish evidence for the current implementation.
V3 also rejects V2 evidence and partial-run continuation. Updating the shared reviewer
closure requires a fresh full-repository baseline in every updated consumer; historical
receipts remain historical evidence, and are never rewritten into new coverage.

Before starting Codex, review records its unit count, assigned bytes, shard count,
concurrency, queued waves and remaining execution capacity in `time-budget.json`
inside the Git metadata run directory. `--min-shard-seconds` optionally declares a
per-shard planning estimate (0..14400; default 0 means no estimate). A plan needs
`ceil(shards / 4) * estimate` seconds after elapsed preparation and the existing
integration, cleanup and publication reservations. Insufficient capacity fails
before any reviewer is admitted. The check runs again after CLI probing and prompt
preparation, using the same absolute deadline. It neither extends the deadline nor
sets a separate timeout per shard, and it does not guarantee completion or infer
model throughput from byte counts. With no estimate, capacity is still recorded;
the existing aggregate deadline remains the execution limit.

For a large first review, supply an estimate based on completed-run observations:

```bash
H2H_CODE_REVIEW_TIMEOUT_SECONDS=14400 \
H2H_CODE_REVIEW_MIN_SHARD_SECONDS=900 scripts/git-flow-merge.sh
```

The merge entry forwards the estimate to the same reviewer CLI. For example,
54 shards require 14 waves and at least 12600 seconds with this estimate; a
one-hour budget is rejected before spending review tokens. These diagnostics are
planning data, not passing review evidence; partial runs are still not resumed.

A deterministic plan binds every tracked path, mode, blob and text byte range.
Fresh-context shards contain at most 32 units, each at most 64 KiB, packed into
384 KiB assignments. Up to four registered owners run concurrently. Every shard
can read any raw candidate source; all shards must pass, followed by a separate
cross-module integration review (a single shard performs both roles). Coverage is
reviewer attestation, not machine proof that a model read or understood the bytes.
The aggregate deadline includes planning, preflight, output validation and cleanup;
it is never multiplied by shard count. Before review starts, the full inventory and
id/path/origin/member/range map determine each shard's UTF-8 JSON handoff budget.
Every accepted handoff, together with the policy and integration assignment,
must fit the declared prompt bound; no contracts or coverage are truncated.
Each assignment also declares its response limits. Shard-only output is bounded
to 512 KiB; integration and combined single-shard output to 1 MiB, including JSON
escaping and whitespace. Packing reserves exact keys, identities and every required
related source, plus up to 2 KiB of decoded UTF-8 per unit method, 8 KiB for the
summary and 8 KiB per integration analysis, allowing six wire bytes per text byte.
Each of the six integration obligations can cite every known source once.
Contracts retain their per-shard wire budget, at most eight 1 KiB strings;
integration output has a fixed 8256-byte contracts cap with no downstream handoff.
Repeated paths within one source list are rejected. Optional extra references,
findings and pretty-printing share the same response bound. A combined response
that cannot fit is split into shard and integration stages; if one unit or the
complete integration still cannot fit, planning fails before review admission.
Planning admits only bounded inputs before retaining review content. Each Git
inventory, diff or configuration output is limited to 2 MiB and each inventory
or selected path set to 4096 entries. Git metadata is spooled to a temporary file
under the same command deadline before a bounded read; this controls planner
memory, not a filesystem disk quota. Selected raw blob sizes total at most
64 MiB, counting every path even when multiple paths share one blob. Actual gzip
expansion totals at most 128 MiB across the plan, including tar headers and
padding; each read is bounded by both the artifact envelope and the remaining
aggregate allowance. Declared maxima are not charged as actual expanded bytes.

Retained examination views total at most 32 MiB. Before decoding and appending
units, planning also limits the count to 4096 and their cumulative serialized
metadata to 8 MiB; repeated reasons and related paths count for every unit.
Each view is hashed once, and every unit retains that same complete-view hash.
Sampled JSON members are limited before DOM parsing to 16 MiB, a conservative
1,000,000 structural slots and nesting depth 64. The preflight uses the standard
JSON byte-encoding detection, preserving UTF-8/16/32 input and ignoring quoted
and escaped punctuation. Complete syntax is still validated by the standard
parser. Each member's DOM is released before parsing the next member. These are
explicit planner memory and work admission limits, not an operating-system RSS
limit. Exceeding any limit rejects the plan without truncating coverage, sampling
additional source or publishing a partial receipt.

Multi-shard review reserves 20% of that budget,
up to 600 seconds, for integration. Managed review also reserves the final 5%
(up to 30 seconds, within that same total) for owner verification and publication.
Any failing/incomplete/missing shard, candidate
drift, malformed range, late output or unproven owner blocks publication and stops
admission; all active owners are cleaned under the same remaining deadline.

Review evidence becomes canonical at one terminal commit: the final atomic rename.
Complete the review, exact candidate/coverage revalidation, cleanup of every review
owned process group and nested obligation, and evidence preparation first. Then
check the original absolute deadline and pending signals immediately before
attempting the rename; this does not extend any budget. A failure before commit
must not create a canonical receipt. A successful rename retains the valid committed
receipt; a later error still blocks that merge attempt and does not delete the
receipt to undo the commit. The final filesystem operation is not guaranteed to
return within the deadline. The clock check and rename are separate operations,
so the actual start of rename has no strict deadline guarantee either.

Tracked binary artifacts require repository-owned declarations, for example:

```toml
[[tool.h2h-dev-tools.review.artifacts]]
path = "evidence/results.tar.gz"
role = "historical-evidence"
format = "tar-gzip"
related-paths = ["scripts/collect.py", "tests/test_evidence.py"]
reason = "Inspect the archived collector and report structure; do not claim numerical reruns."
max-expanded-bytes = 33554432
max-members = 64
sampled-data-members = ["reports/numeric-results.json"]
```

Roles are `generated`, `fixture` or `historical-evidence`; formats are `opaque`,
`gzip` or `tar-gzip`. ZIP artifact declarations are unsupported and rejected before
archive parsing. This review-format restriction does not affect wheel packaging
or consumer CBZ support. Every related path must exist in the same authority tree.
Expanded bytes are bounded by 128 MiB and member count by 4096; single-payload
formats require one member. Inventory covers every archive member's path, type,
mode, size and hash without extracting, following links or executing/deserializing
payloads. Tar inventory requires a complete two-zero-block end marker followed
only by whole zero-padding blocks. Concatenated streams, non-padding trailing
bytes and invalid or truncated later headers fail closed, even when the standard
reader would stop successfully at an earlier member. USTAR, GNU and PAX metadata
remain supported within the same limits. Unsafe names, links, special files,
undeclared binary and exceeded limits fail closed. Embedded source is always examined
in full. Only exact nonexecutable
JSON names in a historical archive's optional `sampled-data-members` roster use
complete syntax parsing plus bounded structure/value samples; unlisted JSON remains
full text. Samples and hashes do not prove historical numerical correctness.
Generated opaque payloads require examination of their declared generator, restricted
decoder, validator, consumer and source correspondence. Binary deletions and rename
sources use the primary parent's declaration and related-source identities.

Process-owner and code-review human diagnostics use a small, finite writer
process for POSIX descriptors and Windows pipes. It never changes inherited FD
flags. The POSIX writer stays in the caller's process group. When the caller
inherits an owner scope, the writer registers its own cleanup obligation under
that parent's admission lock before spawn and releases it only after actual
reaping and complete local scope removal. Scope allocation, initialization
rollback and removal remain inside the parent's admission lock; only fully
removed local metadata permits releasing a completed obligation. Uncertain
registration or rollback retains responsibility and fails closed. A verified
closed parent allocates no diagnostic scope or writer; malformed scope,
lock failure or unproven cleanup instead remains an ownership error. This also
protects output emitted after a main command has closed its own scope. Windows uses the
base interpreter with isolated startup and a Job-assigned start gate. The fixed
stdlib writer starts no descendants and has no dependency on a consumer runtime.
Spawn, writing, termination and reaping share the caller's original absolute
deadline; an expired or insufficient budget admits no writer. Diagnostic output
uses at most 0.25 seconds within that bound, including cleanup. Only a standalone
CLI without an outer deadline receives that explicit default budget. Failure to
prove helper cleanup remains an ownership error, even after the main command is
empty. No additional cleanup grace, process session or recursive command runner is created.
Automatic diagnostic signal handling requires the main thread; callers already
controlling signals pass their existing controller. Parallel cleanup workers only
collect text; the controlling thread emits it after every worker has joined.

A writable pipe retains normal human output. Discarding text for a closed or
stalled pipe does not change the command result. A writer whose cleanup cannot be
proven instead fails with an ownership error. Human `owned_tree_empty` output is
not a cleanup receipt: only successful owner return and the canonical receipt
prove completion, including diagnostic writers and scope closure. Payloads are
limited to 20 KiB of complete encoded characters with a visible truncation marker;
Windows also checks the complete command-line UTF-16 length before spawn.
Recovery replays at most its existing 64 KiB raw prefix using incremental UTF-8
decoding and exact text chunks, inside the same recovery deadline. Process stdout
and other command data streams keep their original semantics. Known in-memory
capture and Windows native console/regular-file streams retain their existing
TextIOWrapper encoding and newline behavior; these direct native writes can still
block in the operating system's console or filesystem. Unknown no-descriptor
writers are not invoked. Timeout and interruption messages wait until primary command-tree
teardown, so optional output does not spend the main cleanup reserve first.

`close_owner(owner, *, deadline, diagnostic=None)` requires the caller's existing
absolute cleanup deadline. It no longer reuses a prior short polling deadline.
Callers that already own cleanup collect diagnostic text until termination and
closure succeed; an unproven owner must not start a diagnostic helper. Update
explicit callers when syncing the snapshot; no application-data conversion is
needed. `run_command` keeps its existing API.

The low-level process launch API is silent and accepts file-backed stdout/stderr.
Its caller must defer signals until it owns the returned process. `run_command`
provides that lifecycle, reserves cleanup time, rejects surviving descendants and
verifies the final absolute deadline. Local callers select their budgets and
signals. POSIX children that deliberately detach and Docker-daemon-owned resources
are outside the owned-tree guarantee. Canonical nested commands are registered
before their separate POSIX sessions start. Closing admission prevents a new
phase from racing cleanup; an outer owner cannot send SIGKILL to its inner owner
or claim an empty tree until that owner's recursive cleanup obligations are released.
Scope locks share the caller's deadline. A killed inner owner, stale capability,
damaged ledger or exhausted deadline fails closed and retains the scope path for
inspection; a merge then retains its candidate and task instead of aborting while
unproven children may remain. The private inherited scope capability is local
coordination state, not a persistent receipt or protection against malicious
same-user modification. Windows behavior also runs in the dedicated
Windows workflow; a POSIX test run does not substitute for it.

The `offline-wheel` component owns temporary build/install directories for
consumer wheel smoke checks. A consumer supplies its own validation script and a
repository-owned entry such as `scripts/check-offline-wheel.py`:

```python
# scripts/check-offline-wheel.py
import runpy
import sys
from pathlib import Path

repository = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(repository / "scripts"))
package = repository / "scripts/_shared/h2h_dev_tools"
runpy.run_path(str(package / "source_loader.py"))["load_package"](
    package, "_shared.h2h_dev_tools"
)

from _shared.h2h_dev_tools.offline_wheel import main

raise SystemExit(main())
```

Run that entry, which verifies the complete selected snapshot before starting any
build, install or callback command:

```bash
.venv/bin/python scripts/check-offline-wheel.py \
  --repository . --smoke-script scripts/check-installed-wheel.py
```

Arguments following `--` are forwarded unchanged to that script. The helper
builds fresh distributions with the repository's development Python and build
backend, requires exactly one wheel, then installs only that wheel in a temporary
venv with `uv` in offline mode. It explicitly adds the development site for the
callback's dependencies and runs the callback with `python -I` from the temporary
directory. The callback owns package import/metadata checks, CLI checks and domain
oracles; arguments naming repository files must therefore be absolute paths.
Success and ordinary failure both remove the temporary environment. A surrounding
runner still owns the aggregate deadline and process-tree termination policy.
This checks the candidate artifact with development dependencies; it does **not**
prove clean dependency resolution or compatibility of an index-selected cohort.
The toolkit's own isolated resolver smoke and hbrowser's target-directory smoke
have different contracts and do not use this component.

## Develop this repository

```bash
scripts/rebuild-env.sh
scripts/install-git-hooks.sh
scripts/check-fast.sh
scripts/check-full.sh
```

For focused tests, use `.venv/bin/python scripts/run-pytest.py <pytest arguments>`.
The full gate and Windows CI use this same source entry. It loads the checkout's
toolkit source before pytest, plugins or conftest can import the package, so a
matching timestamp/size `.pyc` cannot replace the candidate implementation.
The shared `pytest` component also isolates pytest assertion caches and local
xdist worker imports for the full invocation.
Python's direct `-m pytest` entry does not provide this source guarantee; `-B`
only prevents cache writes. Installed wheel smoke keeps ordinary isolated imports.
The full gate still removes Git-local authority only from its pytest child;
the surrounding checks retain the candidate's Git environment.

The expected primary branch is `master`. Create a dedicated task branch before
changing tracked files, then finish with `scripts/git-flow-merge.sh`. The merge
flow reviews full candidates before the hook verifies review evidence, version
impact and the applicable documentation or full gate. `scripts/format.sh` applies formatters;
the check commands are read-only. Ruff and strict mypy use `pyproject.toml`.

The full gate builds fresh distributions, then installs the sdist-derived wheel
in a new environment with normal, offline pip resolution. Only the installed
`packaging` distribution and, on Windows, `distlib` are copied into that environment
as dependency evidence, with each distribution's complete file inventory checked;
no editable project or development-site fallback is used. The smoke verifies
package and metadata locations, runtime requirements, both CLI entry points, and
real snapshot export plus standalone verification.
Console smoke isolates Python environment overrides and compares every exported
snapshot file with the package bytes installed in the smoke environment.
This is an offline distribution check, not a registry-backed clean-resolution test.

## Releases

`pyproject.toml` is the sole project-version source. The initial version is
`0.1.0`; subsequent compatible releases increment the third component, and
breaking changes increment the second component while it is below 1.0.
The shipped development tools and snapshot format are release surface even
though consumers use them only for development.

Before committing a release version, review dependency candidates and generate
the version-bound audit evidence:

```bash
.venv/bin/python scripts/audit-dependencies.py \
  --review-note "Release notes and compatibility validation results"
```

Node audits resolve npm from `PATH`. Standard Windows `npm.cmd` installations
use their adjacent Node executable (or Node from `PATH`), `npm-prefix.js` and
`npm-cli.js` directly, including npm's configured global-prefix selection.
Package specifications remain literal arguments without shell interpretation.
A batch launcher missing these standard JS entries is rejected explicitly.

GitHub's `publish.yml` workflow uses PyPI Trusted Publishing with environment
`pypi`. Normal default-branch pushes publish only when the version increases over
the whole push. Creating the branch does not publish. The first release can use
an explicitly authorized workflow dispatch with the exact project version; it
still requires the default branch and builds and verifies distributions first.
The PyPI publisher must match owner `Kuan-Lun`, repository `h2h-dev-tools`, workflow
`publish.yml` and environment `pypi`. Configuring this workflow does not execute a
release or validate the remote publisher configuration.

Licensed under GPL-3.0-only; see [LICENSE](LICENSE).
