Skip to content

openRepoTools becomes the openrepotools package: pipx installs the wheel with the whole payload, --install leaves pipx's link to pipx, --version names the release, and every count line says where the other commands come from (#228, PR 1 of 4) - #262

Merged
brettheap merged 56 commits into
mainfrom
006-release-pypi
Oct 20, 2026

Conversation

@brettheap

Copy link
Copy Markdown
Contributor

Part of #228: pull request 1 of 4 of feature 006-release-pypi, plan.md "Delivery". It holds User Stories 1 to 3, which are Phases 1 to 5 of specs/006-release-pypi/tasks.md (T001 to T013, and T026).

openRepoTools becomes the openrepotools package. pipx installs the wheel, and the wheel carries the installer's whole payload, so openRepoTools --install from the package needs no network.

  • Beside pipx's link, --install leaves that link to pipx and says so.
  • --version names the release from the package's own stamp.
  • Every count line says which installer the other commands now come from.

Nothing is published here. Every tag, release/v1.1, and every TestPyPI or PyPI upload waits for Brett Heap's word, through PR 3's workflow (plan.md gate G8).

What it adds

  • src/openrepotools/:
    • __init__.py: the SPDX line and a docstring, with no code, so loading the package runs nothing.
    • _release.py: the committed, unstamped stamp (0.0.0 and four empty fields). The release build writes the real one.
    • cli.py: main() replaces its own process with the first bash on PATH, running the packaged installer with the person's arguments unchanged. It reads no stamp.
      • Before the exec it puts SIGPIPE and SIGXFSZ back to their defaults, which Python had ignored (T026).
      • With no bash on PATH, or with PATH ending in a file, it refuses with exit 2.
  • pyproject.toml:
    • hatchling 1.27.0, the version read from the stamp, and one console script, openRepoTools;
    • a written-out force-include map of the 24 payload files, which a test holds equal to payload_names (design.md Decision 2).
  • The installer, openRepoTools:
    • Package mode is decided from paths alone (Decision 4).
    • stamp_read reads the stamp by its fixed grammar, and never sources it. Bash is its one reader at run time (Decision 5).
    • --version prints the five-line release block. A broken stamp refuses every command, --help included, with exit 2.
    • pipx's link: link_resolve, pipx_link_match, venv_shaped_link and pipx_own_run recognise it (contracts/pipx-link.md), and --install prints the skip line in its place.
      • A venv-shaped link that fails the pipx rule is refused in planning, before anything is placed.
    • The count lines: run from pipx's own environment, a run ends …, and 1 left to pipx. Run from anywhere else, it adds ; the other 16 now come from <source>, not from openrepotools <version> (research R5).
    • pip's console script at <dir>/openRepoTools is refused in planning, in both of pip's forms (pip_script_is).
    • path_report runs last in a package-mode --install, and keeps the exit status.

The contracts are authoritative: specs/006-release-pypi/contracts/ (cli-output.md, pipx-link.md, stamp.md, wheel.md). The OpenSpec change openspec/changes/release-tags-pypi/ holds the proposal, the design and the delta specs.

Rulings made during the build, each recorded in design.md's Open Questions:

Tests. All are new and hermetic: a sandbox HOME, no network, and LANES_* and TMUX* unset.

  • tests/package_layout.py: the fake installed layouts (pipx venvs and console scripts) that every package test builds on.
  • tests/test_package.py: pyproject.toml, the payload map, a built wheel and the console script.
  • tests/test_package_stamp.py:
    • the stamp reader, row by row of contracts/stamp.md;
    • hatch's agreement on VERSION, using hatchling 1.27.0's own pattern;
    • --version and the broken stamp.
  • tests/test_install_pipx.py: pipx's link, the skip and count lines, the offline package install, coexistence, pip's script, the PATH report, and the upgrade and uninstall.
  • tests/test_repo_hygiene.py and tests/test_openrepotools_command.py: small, named changes (T006 and T010).

Runs

Review. An independent adversarial review (Opus) gave a provisional LAND after fixes at c1c1c71, with no MUST and no defect in the code.

The reviewer's final verdict on the delta follows as a comment.

Notes for other work

Landing: Brett Heap's decision 4 on #228 ("Yes, merge when green (Recommended)"), and his word of 2026-10-18T20:3xZ, "use this as approval to merge when green for all merges thru v1.1.0".

Lane: openRepoTools-3

🤖 Generated with Claude Code

Brett Heap and others added 30 commits October 17, 2026 21:11
Phases 1-2 of /opsx:propose for #228: the change
scaffold and proposal.md. Annotated vX.Y.Z tags and release/vX.Y branches
after opensoft/openRepoShape#850; the openrepotools wheel carrying the whole
payload for an offline --install, with --install skipping pipx's link; a
dispatch-only release.yml adapted from openRepoShape's (#563); a TestPyPI
rehearsal now and v1.1.0 from openRepoTools-code after the cutover (#186).
Alignment review and council follow.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… answers of 2026-10-17 (#228)

Applies the /opsx:propose alignment review (stack architect and QA lead) to
proposal.md, each fix checked against the code first, and records Brett
Heap's four answers of 2026-10-17T21:51:50Z (#228 comment 6241784787) as a
dated addendum under the decisions.

- the pipx skip: the exact detection rule (link, resolved target,
  pipx_metadata.json main_package.package), the exact skip and count lines,
  28 receipt rows against 29, and the upgrade, uninstall, pip --user and
  pipx-home cases; the record's "as #563 did" corrected (#563 refuses)
- the stamp gains the source repository; how it reaches --version (five
  environment variables, bash keeps the one parser) and the exact lines
- the digest code is the pinned upstream/openRepoShape/scripts/repo_shape.py
  with submodules: true; the in-wheel layout and why the offline install
  holds (payload_select :969-984, assembly_above, wip_shape_ref :3242)
- a new standard-library hygiene test; wheel modes checked; which bash runs
- the rc upload departs from openRepoShape on Brett's word; no pypi.org
  publisher for this repository, on his word; the unsourced "Decision 1
  accepts that risk" dropped
- #852's patch-tag wording, #850's "or a merge of main" and the lanes' side
- the openRepoShape pin bump to v1.1.0 before the regenerated plan (#186
  comment 6241785035); the code leg's LICENSE from T006's code-0001
- Impact: AGENTS.md's CI paragraph and tests.yml's header comment
- the model cited as openRepoShape v1.1.0 (190133d), published by run
  39071178232; the decisions header no longer says "20:5xZ"

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ifications.md (#228)

Phase 5 of /opsx:propose for #228. The council
verdicts of lane openRepoTools-3 (2026-10-17T22:4xZ) go into proposal.md,
VALID 1-9, surgically and with the structure kept. NOTED N1-N5 and the
three dismissals go into a new clarifications.md.

proposal.md:
- VALID 1: a new "Coexistence with the other installers" section. It covers
  the move from the one-line install (pipx install --force), a checkout's
  --install meeting pipx's link and its count line, and the workBenches
  condition, stated in one bullet as position (c), pending Brett Heap's word
- VALID 2: the three README passages feature 006 corrects (:240, :251-262,
  :302); the pipx route's text, fixed now; README gains it only once PyPI
  serves 1.1.0 and the workBenches condition holds; lane 3 owns that PR
- VALID 3: a non-goal: --version names the command's release, not the
  placed files'
- VALID 4: a "Release owner" section. Brett decides every tag, cut and
  upload, and lane 3 carries them out, step 6 included. Per the lead's
  correction after #186 comment 6242560541 (D3, D4): code-0001 is the code
  leg's first commit right after execute, LICENSE is checked as blob
  261eeb9 before the first package run, and Brett's code-side environments,
  tag ruleset and both publishers come after execute and before v1.1.0
- VALID 5: the pipx skip as path and existence checks with a bash-3.2
  readlink loop, no jq and no JSON, in every mode, before any link refusal;
  pipx versions corrected (model locks 1.11.1 / 1.17.8, newest 1.18.3)
- VALID 6: the model's all-or-nothing stamp rule (standard.py:153-156);
  bash reads _release.py beside its payload with a fixed grammar; the five
  OPENREPOTOOLS_PACKAGE_* variables dropped; the entry module execs bash
- VALID 7: the cutover invariants (the gate's four job ids in the code
  leg's tests.yml, no release step reading across legs, no readme in
  pyproject.toml), and #228's PRs land before lane 1's rehearsal and plan
  (D1)
- VALID 8: the package job fails on skipped package tests; pipx-skip tests
  use fake venvs; the build asserts TREE_DIGEST_DEFINITION
- VALID 9: --install refuses pip's console script in every mode, and a
  package-mode --install reports a shadowing openRepoTools on PATH

clarifications.md: N1 detection, N2 the stamp grammar, N3 the release
notes, N4 hatchling's modes (checked: 1.27.0 normalises to 0644/0755 when
reproducible, the default), N5 the refusal and report wording. Also D1-D3,
each with the reason it was dismissed.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… build's check, not by hatchling (#228)

The lead's correction to the council verdicts' VALID 4 (after #186 comment
6242560541, D3) gives the reason for code-0001's order as
"license-files = ["LICENSE"] fails without it". Hatchling 1.27.0 does not
fail: its published wheel builds the licence list from a glob of each
license-files pattern and raises nothing when a pattern matches no file
(hatchling/metadata/core.py:757-770; add_licenses, wheel.py:714-719, only
writes what matched). A code leg without LICENSE would build a wheel without
one.

So the proposal now adopts openRepoShape's own check (release.yml:649-655 at
v1.1.0): the wheel's .dist-info holds exactly METADATA, WHEEL, RECORD,
entry_points.txt and licenses/LICENSE, and that LICENSE is the commit's.
The release build and the package job make it. The order (code-0001 first,
right after execute), lane 3's blob check before the first package run,
and "never tolerate a missing LICENSE" stand as the lead directed.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lities (#228)

Phase 6 of /opsx:propose, the first artifact: one ADDED delta per
capability the proposal lists, written against the proposal as the council
verdicts left it, and clarifications.md.

- release-tags-and-branches: annotated vX.Y.Z tags and their message form,
  release/vX.Y cut at vX.Y.0, back-ports through main (fast-forward or
  cherry-pick -x), patch tags waiting for the commit's tests.yml runs, the
  release/v* push trigger, release notes and GitHub Releases after PyPI
  serves the version, Brett Heap's word for every act with lane 3 carrying
  them out, and tags staying where they were made at the cutover
- pypi-package: the metadata (no dependencies, one script, no readme, the
  wheel's .dist-info with licenses/LICENSE), the exec'd entry point, the
  byte-for-byte payload, the offline install, the all-or-nothing stamp read
  from the package only, the exact --version lines, the pipx skip and every
  other link refused, upgrade and uninstall, coexistence (pip refused, the
  PATH report, the checkout count line), README offering only a published
  package, the package job, the release gates, the reproducible build,
  trusted-publishing uploads, the install check, rehearsals, and the
  cutover invariants

openspec validate release-tags-pypi --strict: valid.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phase 6 of /opsx:propose, the second artifact: how the change is built,
written from the proposal, the two delta specs and clarifications.md, so
that it answers N1-N5 and reopens none of D1-D3.

- the package under src/ (the case-insensitive clash with the root's bash
  file), and a written-out force-include map that a test holds to bash's
  payload_names; no build hook
- the entry module only execs bash, refusing with exit 2 when there is no
  bash, and reads no stamp: bash is the one reader
- package mode decided from $SELF's path; a broken stamp refuses every
  invocation in package mode
- N2: the stamp's fixed grammar, its refusals, writer-reader agreement, and
  no docstring line that hatch's case-insensitive VERSION pattern could
  take first (hatchling/version/core.py:6, :31 in 1.27.0); stricter than
  the model in three named places
- N1: pipx's link resolved with a 40-hop readlink loop and cd -P / pwd -P,
  quoted for macOS's "Application Support" home (pipx/paths.py:14); the jq
  check stays where it is (R-A9-12's order); a venv-shaped link that fails
  the rule is refused without rm advice; fixtures for three pipx releases
  across four homes
- the checkout count line, and the foreign stamp read beside pipx's payload
- N5: how pip's console script is recognised, and the exact refusal and
  PATH-report text
- the package job (submodules, pip wheel twice, the dist-info and LICENSE
  check, pipx 1.11.1 on 3.9, a JUnit count that fails on any skip, tomli
  pinned for 3.9) and the release.yml differences from the model
- N3: the notes stay in spec, no release step reads them, and the release
  owner creates the Release by hand; N4: the installer owns modes
- risks, including the LICENSE that hatchling would not miss and
  .gitattributes on git archive; migration, rollback and open questions

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ff record (#228)

Phase 6 of /opsx:propose, the last artifact. As propose.md requires, it is
a governance and handoff record only, with no implementation steps. It
tracks:
- the proposal, the review and the artifacts (done);
- Brett Heap's pending word on the workBenches position;
- the single Speckit feature, landing (before lane 1's rehearsal, #186
  comment 6242560541 D1), and the rehearsals on his word;
- the cutover's dependencies (lane 1's plan entries and invariants;
  code-0001 first after execute, and lane 3's LICENSE blob check);
- v1.1.0 after execute, and archive readiness.

Speckit Handoff: feature 006-release-pypi, branch 006-release-pypi, tasks
specs/006-release-pypi/tasks.md, pending /speckit.specify. That directory
is not created here.

openspec validate release-tags-pypi --strict: valid. openspec status: 4/4
artifacts complete.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gs of 2026-10-17 (#228)

design.md's Open Questions gains a "Resolved by the lead" list, each item
marked "Resolved by the lead, 2026-10-17":
1. the .dist-info check (exactly METADATA, WHEEL, RECORD, entry_points.txt
   and licenses/LICENSE, equal to the commit's) in both the release build
   and the package job is confirmed, as the way a missing LICENSE is made
   fatal; code-0001 stays the code leg's first commit after execute
2. workBenches main is noted at bdab3e2, where both of its checks still
   hold
3. the stricter stamp rules are confirmed: unstamped is VERSION 0.0.0 with
   an empty REPOSITORY, TAG is v$VERSION, REPOSITORY is a slug, and a
   broken stamp refuses every package-mode invocation, --help included,
   naming the stamp file and the fix, pipx reinstall openrepotools.
   Decision 4's refusal line now names that fix in place of
   pipx install --force openrepotools
4. the jq check staying put, the venv-looking link refused with no rm
   advice, the stamp reader's docstring guard and the pipx home with a
   space are confirmed
5. the checkout's count line is confirmed as the proposal and spec fix it

Still open: Brett Heap's choice of (a), (b) or (c) on workBenches; the
proposal's one dependent bullet stays pending his word.

openspec validate release-tags-pypi --strict: valid.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…228)

/speckit.specify for the OpenSpec change release-tags-pypi, its one linked
feature, on the existing branch (the before_specify branch hook skipped).
Written only from proposal.md, the two delta specs, design.md (with the
lead's rulings of 2026-10-17) and clarifications.md:

- six prioritised stories: install offline beside pipx (P1), know which
  release runs (P1), move between installers without being misled (P2),
  every commit proves it packages (P2), publish only through gates (P2),
  and the record tells the truth (P3)
- FR-001..FR-046 cover all 25 delta-spec requirements (17 pypi-package,
  8 release-tags-and-branches); the exact --version, skip and count lines
  are restated, and the pip refusal and PATH note are design.md
  Decision 8's
- FR-047, the workBenches condition, is the one open item: pending Brett
  Heap's choice of (a), (b) or (c), not a blocker
- edge cases, key entities, ten measurable success criteria, assumptions
  (Brett Heap's acts outside the repository, the cutover, lane 1's plan,
  the model at 190133d) and the proposal's non-goals

checklists/requirements.md: every item passes in one iteration; no
[NEEDS CLARIFICATION] marker remains.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ff (#228)

The "Speckit Handoff" section no longer says "pending /speckit.specify".
It names feature 006-release-pypi, branch 006-release-pypi, tasks
specs/006-release-pypi/tasks.md, specified on 2026-10-17 by
/speckit.specify (spec.md at eb12177). No checklist item is ticked here.

openspec validate release-tags-pypi --strict: valid.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts and quickstart (#228)

/speckit.plan (before_plan and after_plan commit hooks skipped; no
SPECKIT markers in CLAUDE.md or AGENTS.md, so no agent-context edit):

- plan.md: technical context for a bash installer, a stdlib entry module,
  declarative hatchling 1.27.0 and two GitHub Actions workflows; twelve
  gates from AGENTS.md, the Build Tiers rule and the change (the
  constitution is the unfilled template), all passing before Phase 0 and
  after Phase 1; the source tree; fixed fixture-builder and test-name
  prefixes so that tasks agree; governed files; four pull requests
- research.md R1-R18: openrepotools.cli:main; the written-out
  force-include map; the bash function names; the count line's source and
  version; pip's rule as design.md writes it (distlib's /bin/sh launcher a
  recorded gap); the PATH report's prefix; OPENREPOTOOLS_TEST_WHEEL for the
  built-wheel tests with the package job's JUnit count; workflow YAML read
  by the pinned repo_shape.load_yaml; the stamp writer a heredoc the suite
  runs; the model's hash locks re-headed; tomli by hash in the package job;
  the v1.0.0 notes checked by their task; dated cap entries
- data-model.md, quickstart.md, and contracts/ for the CLI's exact lines,
  the stamp, pipx's link and pip's script and the PATH report, the wheel,
  and both workflows

design.md Decision 8: <prefix> is five levels above payload/, the parent
of its lib/, not four; the layout it writes out has five components
(research R7, R17).

openspec validate release-tags-pypi --strict: valid.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d tier (#228)

/speckit.tasks (before_tasks and after_tasks commit hooks skipped). Every
task line carries exactly one tier tag, a files: list and a verify:
command; no [H], because AGENTS.md has no "## Governed invariants"
section. 19 [O] (governed files, the stamp, the release workflow and
trusted publishing, contracts, the shared fixture interface, the
installer's cross-cutting changes) and 6 [S].

- Phase 1-2: the stamp and __init__, pyproject.toml, and the shared
  fixture builders tests/package_layout.py
- US1-US3 (pull request 1): tests first, then cli.py and the installer's
  pipx rule, stamp reader, --version, coexistence, pip refusal and PATH
  report
- US4 (pull request 2): the model's two hash locks, tests_workflow tests,
  tests.yml's release/v* push and package job, AGENTS.md's CI paragraph
- US5 (pull request 3): release workflow tests, then release.yml's build
  job, its upload and install jobs, and its RELEASING header
- US6 (pull request 4): the v1.0.0 notes byte for byte, the v1.1.0 draft,
  README's version paragraph and three corrections
- Polish: quickstart.md end to end
- a "before Phase 1" note: merge origin/main (2732d59) into the branch
  first; a table of requirements met by acts the change's tasks.md tracks;
  FR-047 pending Brett Heap's word

Checked: a script finds 25 well-formed task lines with no [P] overlap and
every verify passing bash -n; T001's, T002's and T014's verify commands
pass on a correct sample and fail on a wrong one.

research.md R16 also records that main has moved (AGENTS.md 321/323 at
2732d59).

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… CRITICAL or HIGH left (#228)

/speckit.analyze (before_analyze and after_analyze hooks skipped), three
passes. Pass 1: 0 CRITICAL, 3 HIGH, 6 MEDIUM, 12 LOW. Pass 2: 1 more HIGH.
Pass 3: 0 CRITICAL, 0 HIGH.

HIGH, fixed:
- FR-029 refused any dispatch not on the annotated tag, against FR-033's
  branch rehearsal; now the tag and branch-membership checks stand down on
  a branch, as in the model (release.yml:200-249 at v1.1.0), any other ref
  is refused, and contracts/workflows.md steps 2-3, T018 and T019 agree
- no task ran the wheel check on a bad wheel, though a missing LICENSE is
  the lead's ruling 1: T015 and T018 now run each workflow's wheel-check
  heredoc on four synthetic wheels (as built; no licenses/LICENSE; a
  changed payload byte; a changed mode)
- the upload jobs' permissions were "the reads its re-checks need": now
  exactly id-token: write, actions: read, contents: read (the model's
  release.yml:691-694, :1133-1136), build contents+actions read, install
  contents read, held by T018 and T020
- FR-014 let --help behave as in a checkout while FR-012 refuses it on a
  broken stamp: FR-014 now applies to a stamp that passes FR-010

MEDIUM, fixed: T004's tests run the payload's bash directly, so T007 and
T008 stay parallel; FR-024's move is in the v1.1.0 notes (T023) and the
coverage table; the PATH note's "none" case has its own ending; a branch
rehearsal needs a dispatched tests.yml run first (header, T021); FR-031's
"nothing from opensoft/openRepoTools" is listed as met by the absent
pypi.org publisher; the 3.9 leg compiles the installed payload's copies.

Also: the package job takes tomli's whole entry (39+ hashes) from the
build lock instead of copying one digest (research R12); build's outputs
name `final`; main is now 83d2690 (lane-start and skills/handoff/SKILL.md
changed in content, modes and payload list unchanged).

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n the change and in feature 006 (#228)

Documents only. Each ruling is applied in the change first, then in the
feature:

1. A repository guard on the uploads, in code (FR-048; design.md
   Decision 10; research R19):
   - opensoft/openRepoTools: testpypi takes a pre-release only, and pypi
     refuses outright;
   - opensoft/openRepoTools-code: pre-release to TestPyPI only, final to
     TestPyPI then PyPI;
   - any other repository: both upload jobs refuse.
   The build decides it before any approval and records to-testpypi and
   to-pypi; each upload job's condition reads them and its first step
   checks again; the missing pypi.org publisher is the second line. The
   model's release.yml carries no such guard, so it is new here. T018 adds
   test_release_guard_build_ and _upload_, and T019-T021 build and document
   it.
2. The proposal and the pypi-package delta spec now state FR-029's rule:
   on a branch rehearsal the tag and release-branch checks stand down, as
   in openRepoShape's release.yml:200-249, and every other check runs (one
   new scenario).
3. main is not merged now: tasks.md makes that merge, as a merge commit,
   the implementation's first act.
4. pip's /bin/sh trampoline (pip 26.2.1, distlib/scripts.py:180-182) is
   recognised as pip's console script (FR-020, Decision 8, pipx-link.md,
   R6), with three refused and five placed fixtures in T012. pip's own
   template writes the import at column 0 in both forms
   (PipScriptMaker, wheel.py:417-424), so the import pattern stands.
5. A package-mode run names `openrepotools <its version>` as the count
   line's source; `<repo> @ <ref>` is only for a checkout or the one-line
   install (FR-019, Decision 7, R5).
6. Wording:
   - the unreadable-stamp clause is now "the openrepotools beside pipx's
     link, whose stamp cannot be read";
   - the broken-stamp line takes <why> as "cannot be read", "is partial:"
     or "is malformed:", one fixture each in T009;
   - "venv-shaped" is the one term;
   - the pypi job is "skipped" in an rc rehearsal;
   - a branch rehearsal builds with the version of the tag it rehearses,
     1.1.0rc1 first (R20, after the model's l3-rehearsal-v1.1.0).

The change's tasks.md ticks 2.1 (eb12177) and 2.3 (760281b), and item 4.3
now says skipped. design.md records rulings 6-11 as resolved by the lead,
2026-10-18.

/speckit.analyze, two passes:
- pass 1 found two MEDIUM, both fixed: T019's verify needed guard-test
  names that T018 had not fixed, and T009 lacked the three causes. It
  also fixed four LOW: the data model's install row, the exact-spelling
  guard, the checklist's scenario count (now 49) and FR-032's compile
  source;
- pass 2 found 0 CRITICAL, 0 HIGH and 0 MEDIUM.
25 tasks (19 [O], 6 [S]); every task line is well formed; openspec
validate --strict passes.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…to main before Phase 1

Lane: openRepoTools-3

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

src/openrepotools/__init__.py holds a docstring and nothing else (research
R1), so loading the package runs no code.

src/openrepotools/_release.py is contracts/stamp.md's committed file: a
module docstring with no line hatch could read as the version, then the
five assignments, VERSION at 0.0.0 and TAG, COMMIT, DIGEST and REPOSITORY
empty, LF only, with a final newline (#228).

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…24 carries Brett Heap's option (c) (#228)

Lane: openRepoTools-3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… ruling 2026-10-18)

src/openrepotools/__init__.py and src/openrepotools/_release.py now open
with `# SPDX-License-Identifier: Apache-2.0`, as every other tracked .py
file in this repository does and as openRepoShape v1.1.0's own package
files do. As in all of those, the docstring follows on line 2 with no
blank line between. Every other byte of both files is unchanged, so the
stamp is still the SPDX line, one module docstring and the five
assignment lines, LF only, with a final newline (#228).

T001's verify still exits 0. Hatchling 1.27.0's version pattern still
first matches `VERSION = "0.0.0"`, since a `#` line cannot match it.

Lane: openRepoTools-3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ring, five assignments (#228)

On the lead's ruling of 2026-10-18, src/openrepotools/_release.py opens
with `# SPDX-License-Identifier: Apache-2.0` (b6ff505). The documents now
say so.

- contracts/stamp.md: the committed file's block and its bullets give
  the SPDX line first, then one module docstring, then the five
  assignment lines. Grammar rule 2 already skipped every blank and `#`
  line. It now says outright that this holds before the docstring, so
  the SPDX line is skipped. Rule 3 now says the docstring need not be
  line 1, and only lines rule 2 skips may come before it. The hatch
  section notes that a `#` line can never match hatch's pattern, and
  the writer keeps the SPDX line with every other byte.
- data-model.md: the file's shape is the SPDX line, one docstring and
  the five lines, and the reader skips the SPDX line as a comment.
- research.md R1: __init__.py holds the SPDX line and a docstring and no
  code. Both package files open with that line, as every tracked .py
  file here and openRepoShape v1.1.0's package files do. The stamp is
  that line, one docstring and the five lines.

tasks.md, T001's task line included, is the lead's, and is untouched.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…soft/openBenches#165 (#228)

Brett Heap's word of 2026-10-18 was "go with (c)". Lane openRepoTools-3
has filed opensoft/openBenches#165, for workBenches'
scripts/setup-estate-commands.sh to accept pipx's openRepoTools link.

- spec.md FR-047 is decided, no longer pending. README offers
  `pipx install openrepotools` only after opensoft/openBenches#165 lands,
  and PyPI must still serve the version (FR-045). Until #165 lands,
  README says a workstation uses one install route or the other, never
  both, and cites it.
- proposal.md: the workBenches condition's position (c) bullet is
  decided and cites opensoft/openBenches#165. It is the only proposal
  bullet that changed.
- design.md: the workBenches question leaves "Still open". It is now
  item 12, "Resolved by Brett Heap, 2026-10-18: (c)".
- the change's tasks.md item 1.4 is ticked, with its evidence.

The issue is written in full as opensoft/openBenches#165 throughout,
because a bare #165 in this repository names #165.
Feature 006's tasks.md is the lead's and is untouched. T024 already
carries the README sentence (cf43b07).

`openspec validate release-tags-pypi --strict`: Change
'release-tags-pypi' is valid.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…wheel.md

The build system is pinned at hatchling==1.27.0. [project] names
openrepotools, with a dynamic version read from
src/openrepotools/_release.py, requires-python >=3.9, Apache-2.0 with
license-files ["LICENSE"], dependencies = [], and no readme. It declares
the one script, openRepoTools = openrepotools.cli:main, and the wheel's
packages, src/openrepotools.

The force-include map has 24 entries. The first 23 are the installer's
payload_names, in its own order, and the last is
contracts/openreposhape-pin.yaml. Each entry maps P to
openrepotools/payload/P.

Header comments in openRepoShape v1.1.0's style give three reasons:
- src/, because the root's openRepoTools clashes with an openrepotools/
  directory on a case-insensitive disk;
- no readme, because README.md goes to the assembly root at the cutover;
- the hatchling pin, because 1.27.0 is the newest release on Python 3.9,
  and 1.28.0 and later require 3.10, as read from PyPI.

The optional description, authors, classifiers and [project.urls] are
left out. contracts/wheel.md allows them, but this task does not ask for
them.

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

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

The four fixture builders that plan.md's "The test layout" names, for the
pipx, stamp and entry-module tests (T004, T005, T009, T012, T018). The
module imports only the standard library, never conftest, and finds the
repository as Path(__file__).resolve().parent.parent.

- package_tree(prefix, python="python3.12", stamp=None) builds
  <prefix>/lib/<python>/site-packages/openrepotools/ and returns its
  payload/. It copies every src/openrepotools/*.py that exists, at 0644,
  with the given stamp text as _release.py when one is given. payload/
  holds the 24 files of pyproject.toml's force-include map, in its order,
  byte for byte, at 0755 or 0644 by the checkout's owner-execute bit.
- pipx_venv(pipx_home, release, python=None, stamp=None) builds
  <pipx_home>/venvs/openrepotools/ around a package_tree for pipx 1.11.1,
  1.17.8 or 1.18.3, defaulting to the interpreter of that release's row in
  contracts/pipx-link.md. It writes bin/openRepoTools as pip's console
  script naming the venv's bin/python, and pipx_metadata.json and
  pyvenv.cfg with placeholder content. Under a spaced home, the console
  script is pip's /bin/sh trampoline, as pip writes it there.
- console_script(path, python, form=None) writes pip 26.2.1's
  PipScriptMaker template for openrepotools.cli:main, at 0755. It writes
  either form pip writes: form="shebang", form="trampoline", or pip's own
  choice, with distlib's quoting of a spaced interpreter and Linux's
  127-byte limit on every platform.
- stamp_text(**fields) gives the committed stamp with the given fields'
  values. Each committed line must occur exactly once, and every other
  byte is kept. Values are not checked, so broken stamps can be built.

The payload list is read from the force-include table line by line,
because Python 3.9 has no tomllib, and it is cross-checked against
tomllib where tomllib exists. The list is read when a builder is called,
not at import. A malformed map then fails that builder, and
test_payload_map_ still runs and names the differences.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Takes in #232 (tests/run.sh records its lock owner), #233 (the openRepoShape
pin at v1.1.0, 190133d) and #229 item 22 (the bench-root host-path hygiene
rule). The two sides touch disjoint files, and no file this branch adds names
a bench-root path.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…offline package install (US1)

The US1 tests of contracts/pipx-link.md and contracts/cli-output.md, written
before T008, on tests/package_layout.py's fake venvs:

- test_pipx_link_*: the twelve layouts (pipx 1.11.1, 1.17.8 and 1.18.3 under
  $PIPX_HOME, ~/.local/pipx, ~/.local/share/pipx and a home with a space,
  absolute and relative links, PIPX_HOME set only in its own column); a
  40-link chain at the hop limit; the five negative cases, from a checkout
  and from a package outside pipx's venv.
- test_pipx_skip_*: the exact skip and pipx-own count lines, 28 receipt
  rows and the link left as it was; a second link refused with today's
  rm -f for it alone; the venv-shaped entry and paragraph with no rm for
  the link, alone and beside another link; no jq; a restart retirement; a
  later --install; RETIRED never holding openRepoTools.
- test_pipx_offline_*: a package-mode --install with the sentinel
  repository and a failing gh and curl places what a checkout's install
  places, apart from the link, and calls neither.

Every run is bash <installer> --install, never the console script. Only the
lines the contracts name are asserted, never a whole output or a hook line.
Before T008, 27 fail on today's link refusal and 7 pass.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…228)

Add test_the_package_imports_only_the_standard_library. It parses every
module under src/openrepotools/ with ast and holds each import, at any
depth, to an explicit allowlist of top-level names: os and sys, the two
modules design.md Decision 3 has the entry module use (os.execvp, sys.argv,
sys.stderr). A relative import and an __import__() call are refused. Where
sys.stdlib_module_names exists the allowlist is itself checked against it;
on Python 3.9, which has none, nothing skips and the allowlist alone decides.

Widen test_the_python_files_compile to compile the same set of modules.

Lane: openRepoTools-3
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…the_ref

It now says that a checkout and the one-line install name the repository and
ref they fetch from, and that a package names its release
(contracts/cli-output.md). Every assertion of the three test_version_* tests
is unchanged.

Lane: openRepoTools-3
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…built wheel and the console script

The package's tests come before their implementation (specs/006-release-pypi,
tasks.md T005), under plan.md's four prefixes:

- test_metadata_*: contracts/wheel.md's pyproject.toml rules, read with
  tomllib or tomli, skipping by name where neither imports. The build-lock
  agreement skips until .github/release-build-requirements.txt exists.
- test_payload_map_*: the force-include map against payload_names, run from
  the installer's own text, plus the openRepoShape pin, both ways round.
- test_wheel_*: checks 2, 3, 5 and 6 of contracts/wheel.md and the member
  comparison against HEAD, on the wheel OPENREPOTOOLS_TEST_WHEEL names;
  skipped by name when it is unset. Check 6 is also held the other way, so
  a member the list has and the wheel lacks is named.
- test_entry_*: pip's console script in a venv --without-pip holding a
  package_tree: the exec of the first bash on PATH in the script's own
  process with the payload and the arguments unchanged; a refused argument
  as the payload's own refusal in both of pip's script forms; a SIGTERM
  mid-placement leaving no temporary; the exact no-bash refusal, exit 2,
  nothing run; and wip_shape_ref, cut out with installer_function, reading
  the pin packaged beside the payload.

Before T007 every test_entry_* fails on "No module named
'openrepotools.cli'", and nothing else.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… worktree and cherry-picked here (#228)

Built in parallel on Brett Heap's word of 2026-10-18T03:36:47Z, each from 4ae999d,
each accepted on model, scope and verify in its own tree, then cherry-picked with an
identical patch-id:
- T006: d5449c6 (006-release-pypi-t006) -> d57b75d, accepted 05:58:12Z (sonnet-xhigh)
- T010: 331e095 (006-release-pypi-t010) -> fa44864, accepted 08:44:12Z (sonnet-xhigh)
- T005: c862db2 (006-release-pypi-t005) -> b286953, accepted 08:46:35Z (opus-xhigh)

T004 (7921a2e) is marked once its own acceptance has run.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap

Copy link
Copy Markdown
Contributor Author

The independent review's final verdict: LAND. It covers the whole head through the fix round.

  • At c1c1c71 alone, the verdict was LAND after fixes.
  • The review fixes (b9b08ae, fd921f3, 77d04aa, on this branch as b7c111c, 65166ea and 08743b0) are exactly those fixes. No MUST or SHOULD is left.
  • The installer, src/ and pyproject.toml are unchanged since c1c1c71. The delta touches four documents and two test files.

How each finding was closed:

  • SHOULD-1: three GUARDED_LINES rows (U+017F, U+0130, U+0131), a test that hatch reads the version from each line while stamp_read refuses it, and a test that only those three letters fold.
  • SHOULD-2: three test_coexist_* tests check the exact count line for an adopted remote source, an adopted local source and a feature worktree.
  • NIT-1: the silent-skip test covers dist-packages, a pypy directory and lib64.
  • NIT-3 and NIT-4: every re-derived citation lands on its text, and research R7 names all four directories.

The reviewer's runs at 77d04aa: through tests/run.sh, with every LANES_* and TMUX* variable unset.

Run Result
-k "test_package_stamp or test_install_pipx" -rs 250 passed, 0 skipped
F01 = M01 (the fold lines removed) KILLED: exactly the 6 new cases fail
F02 = M02 (an adopted source prints $REPO @ $REF) KILLED: the two adopted-source tests
F10, F10S, F10P (each skip prints a note) KILLED: [lib64], [dist-packages] and [a-pypy-directory]
F14 (a feature worktree read as adopted) KILLED: the feature-worktree test

One new optional NIT (NIT-7): three docs carry stale test-file line citations.

  • contracts/cli-output.md:45-46 and quickstart.md:40 cite test_openrepotools_command.py:299-319 and :300-302. Those tests now sit at :323-344, with the docstring at :324-327.
  • plan.md:153 cites the cap tests at test_repo_hygiene.py:1748 and :2253, now :1963 and :2468.

Changing this PR now would make the merge queue rebuild and re-test every prefix behind it. So NIT-7 goes with T025's quickstart commit, after the four PRs.

What the review could not check:

  • a real macOS bash 3.2;
  • a real pipx, pip or PyPI install;
  • a full local suite.

Linux CI on the tree main will have after the squash is the landing gate. Here that is the queue prefix lane3-queue/q1-228pr1, run 39164234173, on lane 4's final prefix.

Lane: openRepoTools-3

Brett Heap and others added 2 commits October 19, 2026 14:08
Main now holds lane 4 queue, lane 5 queue (#259 #260 #261) and #246 (55aae16).
The one conflict is tests/test_repo_hygiene.py import block: #262 adds
import sys and #259 adds from collections import namedtuple; both are kept.
The tree is lane3-queue/q1d-228pr1-on-p246c, run 39256880821, green.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#228)

SonarCloud's pythonsecurity:S8705 flags os.execvp("bash", argv) at
src/openrepotools/cli.py:80. argv is ["bash", <payload>/openRepoTools,
*sys.argv[1:]]: the person's own command line after the packaged script's
path, so nothing reaches the exec that the caller did not type. Brett Heap
chose a NOSONAR line over marking the issue in SonarCloud ("use NOSONAR on
#262", 2026-10-19).

Lane: openRepoTools-3
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 19, 2026
…API while Eagle cannot reach git

Lane: openRepoTools-3
This was referenced Oct 19, 2026
…d reads, and its reason moves to the line above (#228)

SonarCloud's python:S7632 flagged d10cfb1's "# NOSONAR: the caller's own
arguments (#228)" as a malformed suppression comment, so pythonsecurity:S8705
stayed open on the exec line. Python's forms are a bare "# NOSONAR", which
covers every issue on its line, or "# NOSONAR(<rule>, ...)"; the bare form is
used. The comment line above it keeps the reason: argv is the person's own
command line after the packaged script's path. The module's syntax tree is
unchanged.

Lane: openRepoTools-3
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap merged commit c82578f into main Oct 20, 2026
5 checks passed
@brettheap
brettheap deleted the 006-release-pypi branch October 20, 2026 06:27
brettheap added a commit that referenced this pull request Oct 20, 2026
…ins a package job and runs on release/v* pushes, the two hash locks come from openRepoShape v1.1.0, and AGENTS.md names the four jobs (#228, PR 2 of 4) (#267)

Part of #228: **pull request 2 of 4** of feature `006-release-pypi`, plan.md "Delivery". It holds User Story 4, which is Phase 6 of `specs/006-release-pypi/tasks.md` (T014 to T017).

**Every push now proves the package builds, installs and runs.** `tests.yml` gains a fourth job, `package`, and it now also runs on pushes to `release/v*`, not only to `main`.

Nothing is published here. `release.yml` is PR 3's, and every tag and upload still waits for Brett Heap's word (plan.md gate G8).

**What it adds**
- **The two hash locks (T014):** `.github/release-build-requirements.txt` and `.github/release-pipx-requirements.txt`.
  - Below the header, each is openRepoShape v1.1.0's file byte for byte (research R11). The task's verify diffs them against `gh api …?ref=v1.1.0`.
  - Each header is rewritten for this repository and #228, and keeps the model's uv 0.12.18 command and its date, 2026-10-01T00:00:00Z.
  - The build lock serves `release.yml`'s `build` job (PR 3), and the `package` job's `tomli`. The pipx lock serves the install check (PR 3), and the `package` job.
- **`tests/test_tests_workflow.py` (T015):** the `test_tests_yml_*` tests hold contracts/workflows.md's `tests.yml` list. They read the workflow with the pinned openRepoShape's own `load_yaml` (research R9), and they skip without the submodule.
  - **The shape:** the triggers, the four job ids, and the three existing jobs exactly as they are.
  - **The `package` job:** its runner, Python 3.9 and `submodules: true`, and steps 4 to 11 in order.
    - Step 4 runs under a fake pip. It must install the build lock's whole `tomli` entry, every hash of it, and `tests.yml` itself holds no hash.
    - Step 6's wheel check runs on four synthetic wheels made from HEAD's blobs. The wheel as built passes. A wheel with no `licenses/LICENSE`, with a changed payload byte, or with a changed payload mode fails, naming the file.
    - Step 11's report check passes only when at least one test ran and none was skipped, failed or errored.
- **`.github/workflows/tests.yml` (T016):**
  - push on `[main, 'release/v*']`, and a header comment naming both push branches and the four jobs;
  - the `package` job, with contracts/workflows.md's steps 1 to 11:
    - two `pip wheel` builds at the commit's `SOURCE_DATE_EPOCH`, compared with `cmp`;
    - the inline wheel check, with the stamp equal to the committed file;
    - pipx from its hash lock, and its one link;
    - the unstamped `--version`, line for line;
    - an offline `--install` beside pipx's link, with the skip line and the count line exact, 28 receipt rows, and the link unchanged (#237's counts);
    - the three package test modules against the built wheel (`OPENREPOTOOLS_TEST_WHEEL`), with a JUnit report that fails on any skip;
  - `guard-launch-mode`, `tests` and `tests-no-submodule` byte for byte as they were.
- **AGENTS.md (T017):** point 1 of "Testing your changes to this repository" (`AGENTS.md:244-249`) names the four jobs and the two push branches. It says that the release workflow's macOS install leg, on dispatch only, runs `openRepoTools --install` under `/bin/bash` 3.2. That is the one 3.2 run anywhere, and it runs no other command (FR-025, FR-026). AGENTS.md stays at 330 lines, its cap.
  - It says "From PR 3 (#228) on", because that leg arrives with PR 3's `release.yml`.

**PR 3's files, named before they exist (review S2; the lead's ruling).** PR 3 follows this PR directly, so five places here describe PR 3's `release.yml` and `tests/test_release_workflow.py` in the present tense:
- `.github/release-build-requirements.txt:8-17`: `release.yml`'s `build` job installs this lock.
- `.github/release-build-requirements.txt:30-33`: `tests/test_release_workflow.py` holds hatchling equal to `pyproject.toml`'s pin. Until PR 3 lands, `tests/test_package.py::test_metadata_the_build_lock_names_the_same_hatchling` holds it.
- `.github/release-pipx-requirements.txt:8-11` and `:18-20`: `release.yml`'s `install` job, and the test that holds pipx 1.11.1 and 1.17.8.
- `.github/workflows/tests.yml:36-38` and `:156-158`: the release workflow builds only a commit whose push or dispatch run of the four jobs is green.
- `AGENTS.md`: the macOS install leg, now said "from PR 3 on".

Until PR 3 lands, no test reads the pipx lock's two versions.

**Runs**
- **Queue prefix q2k** (branch `lane3-queue/q2k-228pr2-on-q1k`: #262's prefix q1k + this head `fa3bd83`, on main `677acc0`, tree `75af31c`): `tests.yml` run 39373753999, green. This is the run of record for the landing.
- **An earlier read, q2f** (`aeadabe`, on main `55aae16`, tree `621b888`): run 39285234126, green on all four jobs (`guard-launch-mode`, `tests`, `tests-no-submodule` and `package`).

**Review.** An independent adversarial review (Opus) gave **LAND after fixes**, with no MUST (`briefs/rv-pr2-006-final.md` in lane 3's tree).
- **Fixed here:**
  - S1: the build lock's header, research R12, a `tests.yml` comment and a test docstring now say that the pipx lock carries the same `tomli` entry, and that the two move together;
  - S3: AGENTS.md says what the one bash 3.2 run is.
- **S2** is the list above.
- **The fix round's re-check** (lane openRepoTools-5, Opus, #228 6267650575): **LAND.**
  - S1 and S3 are fixed. The two `tomli` entries are byte-identical, and "step 7 after step 4" is right.
  - Its one SHOULD was three stale places in this body, now corrected: AGENTS.md's line count (323, now 330), the old "one place the payload runs under bash 3.2" wording, and the build lock's line citation (`:27-30`, now `:30-33`).
- **Proposed as v1.2.0 follow-ups:**
  - S4: the `package` job's stamp check has no test;
  - S5: two of step 10's static checks are labelled for more than they check;
  - from the re-check:
    - the pipx lock's header never mentions `tomli`, and no test pins the two locks' `tomli` entries equal;
    - `tests.yml:203-205` reads as if pytest were installed after step 7;
    - `tasks.md:254` (T017) keeps the old bash 3.2 wording.
- **What the reviewer ran:**
  - the job's own `run:` text on Linux under CPython 3.9.25: two builds, one file; the wheel check 32 members, 0 problems; the offline install exact, with 28 receipt rows; the package tests 285, 0 skipped;
  - the hash locks, byte for byte against openRepoShape v1.1.0, regenerated exactly by uv 0.12.18;
  - mutants: 22 of 33 killed. The 11 survivors are S4, S5 and the NITs.

**Notes for other work**
- **PR 3** (`release.yml` and `tests/test_release_workflow.py`) and **PR 4** (the release notes and README) follow, in order, each stacked on the one before.
- **The migration's single rehearsal** waits for all four PRs (D1, #186).

**Landing:** Brett Heap's decision 4 on #228 ("Yes, merge when green (Recommended)"), and his word of 2026-10-18T20:3xZ, "use this as approval to merge when green for all merges thru v1.1.0".
- Lane openRepoTools-3 lands the queue in one order: #262, PR 2, PR 3, PR 4, T025's docs PR, then #255.
- This PR squash-merges after an independent review, once Linux CI is green on the exact tree main will have after its squash. The record is a queue run of `tests.yml` on lane 3's prefix branch, under **Runs**.
- q2k is #262's prefix q1k plus this head, so the record holds #262 beneath it.
- When its turn comes, lane 3 pushes a head that merges main into `fa3bd83` with q2k's tree, as a fast-forward, so the tree under review is the tree that was tested.

Lane: openRepoTools-3

Landed by lane openRepoTools-3 as queue prefix q2k (tree 75af31c, tests.yml run 39373753999 green), on Brett Heap's word of 2026-10-18T20:37:07Z: "use this as approval to merge when green for all merges thru v1.1.0". Review: LAND after fixes, with no MUST; the fix round's re-check by lane openRepoTools-5: LAND (#228 6267650575).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 20, 2026
…cks the install on three legs and uploads it to PyPI, and tests/test_release_workflow.py holds it (#228, PR 3 of 4) (#268)

Part of #228: **pull request 3 of 4** of feature `006-release-pypi`, plan.md "Delivery". It holds User Story 5, which is Phase 7 of `specs/006-release-pypi/tasks.md` (T018 to T021).

**One dispatched workflow now builds the wheel, uploads it to TestPyPI, checks the install on three legs, and uploads it to PyPI.** `.github/workflows/release.yml` has four jobs, `build`, `testpypi`, `install` and `pypi`, and `tests/test_release_workflow.py` holds it.

Nothing is published here. `release.yml`'s `on:` block holds `workflow_dispatch` and nothing else, with one required string input, `version` (`release.yml:231-241`). No push, tag, pull request or schedule starts a run. The two upload jobs also run only on a `v*` tag, so a dispatch on a branch uploads nothing.
- **Environments:** `testpypi` runs in the GitHub environment `testpypi`, and `pypi` in the environment `pypi`.
- **Authentication:** trusted publishing (OIDC). `id-token: write` is granted to those two jobs and to no other, and the file holds no PyPI token and no `secrets.` reference (`grep -c 'secrets\.'` gives 0).

No lane pushes a tag or dispatches the workflow without Brett Heap's word (plan.md gate G8).

**What it adds**
- **`tests/test_release_workflow.py` (T018, `7b5167a`):** the tests come before the workflow, so every test that reads `release.yml` fails until T019 to T021 write it, naming the file and the tasks. They read it with the pinned openRepoShape's own `load_yaml` (research R9), and skip without the submodule.
  - **The shape:** `grep -c '^def test_'` counts 56 test functions at T018 and 66 at the fix head `9682f0b`, under ten prefixes: `test_release_trigger_`, `_lock_`, `_build_`, `_stamp_writer_`, `_guard_build_`, `_guard_upload_`, `_upload_`, `_install_check_`, `_reads_` and `_header_`. The lock tests and the digest-label test read nothing of `release.yml`, so they pass before it exists.
  - **Run, not only read:** each step is run as GitHub runs a bash step, with every `GITHUB_*`, `RUNNER_*`, `LANES_*` and `TMUX*` variable removed, and `gh` and `curl` faked.
    - That covers the repository guard under every row of contracts/workflows.md's table, each upload job's first-step guard, the version check, the ref and branch steps under the rehearsal rule, the gate on `tests.yml`'s four jobs, and the digest step, which also runs in a copy of the checkout whose standard or pin names another definition.
    - The wheel check runs on synthetic wheels made from HEAD's blobs. The wheel as built passes, as a final and as a pre-release. A wheel with no `licenses/LICENSE`, a changed payload byte, a mode 0755 made 0644, or a mode 0644 made 0755 fails, and a line of its refusal names the file.
    - The stamp writer is run on a copy of the committed stamp and read back by `--version` from a `package_tree`. It refuses a file already stamped, and a value no release carries.
- **`release.yml`'s shape and its `build` job (T019, `0de7213`):** adapted step for step from openRepoShape v1.1.0 (`190133d`), under the model's step names, with design.md Decision 10's differences.
  - **The shape:** `workflow_dispatch` with `version`, `permissions: {}`, the `release` concurrency group that never cancels, `bash` by default, and `VERSION` and `PYTHONUTF8` in `env`.
  - **`build`:** `contents: read` and `actions: read`, 16 steps (counted at `29cc1b3`), and five outputs: `wheel`, `wheel-sha256`, `digest`, `to-testpypi` and `to-pypi`. In order, it:
    - checks the version, `X.Y.Z` or `X.Y.ZrcN` and never `0.0.0`, and runs the repository guard (FR-048), which records `to-testpypi` and `to-pypi`;
    - checks the ref (on a tag, the annotated tag `v<version>` whose object names the run's commit, read from the forge and from git; on a branch, a rehearsal; any other ref is refused), that the commit is on `main` or on `release/v<major>.<minor>`, and that the checkout is that commit and clean;
    - reads both environments' rules, and the gate: `guard-launch-mode`, `tests`, `tests-no-submodule` and `package` each ran in a push or dispatch run of `tests.yml` on this commit, none is still running, and the newest to finish succeeded;
    - holds the digest: the pinned submodule's `tree_digest` equals `tree_digest_from_gh`, and its `TREE_DIGEST_DEFINITION` equals the literal `sorted-ls-tree-r-v1` and the pin's `digest_definition`;
    - installs the tools from the build lock by hash, extracts the commit twice with `git archive`, writes the five-field stamp into both (`REPOSITORY` from `GITHUB_REPOSITORY`), builds each with `python -m build --wheel --no-isolation`, and requires one file;
    - checks the wheel against the commit's own blobs, byte for byte and mode for mode, then records its name and sha256 and uploads it as an artifact.
  - Every action is pinned by a full commit, with its release tag beside it.
- **`release.yml`'s `testpypi`, `install` and `pypi` jobs (T020, `fb2249f`):** TestPyPI, then the install check, then PyPI.
  - **`testpypi` and `pypi`:** each runs in its own environment, on a `v*` tag and where the build's record says so (`to-testpypi`, `to-pypi`), so `pypi` is skipped in an rc rehearsal. Each holds exactly `id-token: write`, `actions: read` and `contents: read`.
    - The first step is the repository guard again. TestPyPI takes any release from `opensoft/openRepoTools-code` and a pre-release alone from `opensoft/openRepoTools`. PyPI takes a final from `opensoft/openRepoTools-code` and nothing else.
    - Then come the model's steps as they are: the artifact and its sha256, the environment's rules, this run's approval and the tag read again, an index that holds no file of the version but the verified wheel, the upload by `pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33`, and the served digest read up to 20 times.
  - **`install` (`contents: read`):** ubuntu and macOS on Python 3.x, and ubuntu on 3.9, with no Windows leg.
    - The wheel comes from TestPyPI by its recorded digest on a tag, and is the build's artifact on a branch. pipx comes from its hash lock and places the one link, and `--version` is checked line for line.
    - The offline `--install` beside pipx's link is set up as `tests.yml`'s `package` job sets its own: the skip line and the count line exact, 28 receipt rows, the link unchanged.
    - The macOS leg runs it again under `/bin/bash`, whose `BASH_VERSION` must begin `3.2`. The 3.9 leg compiles the installed payload's `lanes-index` and `lane-worktrees`.
- **`release.yml`'s header, RELEASING (T021, `a04b9e2`):** the header comment alone, in contracts/workflows.md's six sections, in order. The jobs are unchanged.
  - **What the workflow does** and its four jobs, with Decision 10's differences named.
  - **The procedure**, on Brett Heap's word: the CI wait, the annotated tag in `v1.0.0`'s message form, the dispatch, the two approvals, `gh release create` once PyPI serves the version, then `release/vX.Y` at `vX.Y.0`.
  - **opensoft/openRepoShape#850's branch and tag policy**, **the two rehearsals** (never `1.1.0`; `1.1.0rc1` first), **where releases come from** (the repository guard's table), and **what the workflow never reads** (FR-046).
- **The empty-index ruling (`ac2ca2b`):** the lead's ruling of 2026-10-18 on T020's first doubt, written into `openspec/changes/release-tags-pypi/design.md`, the `pypi-package` delta spec, `specs/006-release-pypi/contracts/workflows.md` and `specs/006-release-pypi/spec.md`. design.md numbers it item 15, as round 1 fixed it (a review NIT).
  - The upload jobs' empty-index check is the model's form. A 404 passes. The verified wheel already there at its digest, not yanked, passes, and the upload adds nothing new. Anything else refuses.
  - The strict form would make "Re-run failed jobs" impossible after an upload that went through but whose served-digest check timed out.
- **The marks (`29cc1b3`):** `specs/006-release-pypi/tasks.md` marks T018 to T021 done.
- **The review's two fix rounds (`c6cea2a` and `9682f0b`, 11 commits on `1b41ce7`):** they change three files and no other line of `release.yml`. `git diff --stat 1b41ce7 9682f0b` gives 3 files, 843 insertions and 56 deletions.
  - **The three files:** `tests/test_release_workflow.py`; one comment in `release.yml` (`:1381-1383`, which now says which test holds the install check's offline `--install` equal to `tests.yml`'s, instead of saying the two cannot drift); and design.md's two NITs.
  - **Round 1 (`c6cea2a`, 7 commits):**
    - `1d5d657`: a test holds the install check's offline `--install` to `tests.yml`'s, line for line but for the fingerprint, and `release.yml`'s comment says so;
    - `2ed477f`: each upload job's guard is run on refs other than the version's own tag, and refuses each;
    - `5116295`: the stamp writer writes neither copy until both are checked, and the whole step stamps the run's own repository;
    - `d0620ab`: the wheel check is run on one synthetic wheel per member check, each refused naming what is wrong;
    - `34f4239`: the sha256 checks before each upload and before the install are held by their steps' names and commands, and the install check's is run (the review's MUST, under **Review**);
    - `5bc35c3`: the build's guard is held to refuse a ref that is neither a tag nor a branch;
    - `c6cea2a`: design.md numbers the T020 ruling 15, and says the 3.9 leg compiles with `compile()`.
  - **Round 2 (`9682f0b`, 4 commits on round 1, tests only):**
    - `2a5d56c`: both upload jobs' index checks are run against a fake index, holding the empty-index ruling and the served check's 20 reads;
    - `7b1c693`: the install check's `--version` step and its Python 3.9 step are run;
    - `bc454ad`: the macOS rerun is tied to the offline `--install`, naming its ten differences;
    - `9682f0b`: the header's approval step and branch cut are pinned, and both copies of the guard's table are tied to `GUARD_ROWS`.

**The notes and the code leg, named before they exist.** PR 4 follows this PR directly, and the migration's cutover follows all four. So five places describe them in the present tense:
- `.github/workflows/release.yml:108-112`: RELEASING step 5 runs `gh release create … --notes-file <spec>/docs/release-notes/v<version>.md`. PR 4 adds `docs/release-notes/v1.0.0.md` and `v1.1.0.md`, and `<spec>` is the spec leg that the cutover makes. `tests/test_release_workflow.py:3163` (at `9682f0b`) holds that line of the header.
- `.github/workflows/release.yml:142-144`: v1.1.0 "is built later, from another commit, in opensoft/openRepoTools-code".
- `.github/workflows/release.yml:171-174`: "FROM v1.1.0 ON, release tags are made in opensoft/openRepoTools-code".
- `.github/workflows/release.yml:189-191`: the ref step fetches the tag from `opensoft/openRepoTools-code` with no credentials, and the migration creates that repository.
- `.github/workflows/release.yml:222-228`: after the cutover the workflow runs in the code leg, which holds none of `docs/`, `openspec/`, `specs/`, `README.md`, `AGENTS.md` and `CLAUDE.md`. `test_release_reads_nothing_the_code_leg_will_not_hold` holds that no step names one.

**Runs**
- **Queue prefix q3k** (branch `lane3-queue/q3k-228pr3-on-q2k`: q2k + this head `9682f0b`, on main `677acc0`, tree `294b021`): `tests.yml` run 39373756231, green. This is the run of record for the landing.
- **Lane 3's verify at `9682f0b`:** the selection `-k "test_release_ or test_tests_yml_ or repo_hygiene"` gives 646 passed, and actionlint is clean (#228 6273049912).

**Review.** An independent review by lane openRepoTools-2, from two reviewers (tests and mutants, and reading and probes), of `85a7b05..29cc1b3`, gave **LAND after fixes** (#228 6273189381). At `1b41ce7` the merge of PR 2's review fixes leaves `release.yml` and the test file byte-identical to `29cc1b3`, so that range is this PR's own.
- **No finding shows `release.yml` doing a wrong thing today.** Every finding but one false comment and some NITs is about what the tests fail to hold.
- **The MUST, M1:** the sha256 checks in front of each upload and in front of the install could be deleted with the suite green. Four mutants showed it (X4, X7, I10 and I19), and each left the suite at 312 passed and 0 failed. Round 1's `34f4239` holds the checks by their steps' names and exact commands, and runs the install check's. It should kill I10 as well, which the re-check confirms.
- **Rounds 1 and 2 kill the survivors that lane 2's verdict maps to R1 and R2.** Of its 56 surviving mutants, 16 are R1, 10 are R2, 4 are "R1, confirm" (I10, I3, I17 and I18), 25 are NEW, and 1 is right to pass. Lane 3's fix-branch notice is #228 6273049912.
- **Lane 2's re-check of `1b41ce7..9682f0b` is LAND (#228 6278141437).** Lane 2's three conditions for landing were: M1 fixed, that re-check, and Linux CI green.
- **The NEW findings go to PR 3b,** a tests-and-docs follow-up on `9682f0b`, per lane 3's ruling (#228 6273289912). Nothing more goes into PR 3 before the re-check. The re-check says which of the NEW survivors rounds 1 and 2 already kill, and whatever survives goes to PR 3b, not to this PR.
- **Six items go to v1.2.0 proposals,** each a change in behaviour or in CI: lint in CI, the two wheel checks' mode handling, the CRC traceback, the index checks' tracebacks, placement under bash 3.2, and the settings `release.yml:194-199` names.
- **What the reviewers ran, at `29cc1b3` unless named:**
  - actionlint 1.7.12 on `release.yml` and `tests.yml`, at `29cc1b3` and at `1b41ce7`: clean;
  - `-k test_release_workflow`: 312 passed, at both commits, and again offline (dead proxies and `GIT_ALLOW_PROTOCOL=file`), where decoys for `gh`, `curl`, `wget`, `pip` and git's network commands logged nothing;
  - 104 mutants of `release.yml`, 48 killed and 56 survived;
  - the repository guard's table, 931 rows (7 repositories, 19 versions and 7 refs), under bash 5.2 and 3.2.57: 0 unexpected, the two tables identical, and only the code leg, on a final at the tag `v$VERSION`, reaches PyPI;
  - the stamp writer, 29 of 29; the CI gate, 16 of 16; the wheel check on lane 3's 20 defective wheels, each named with exit 1 except the symlink and CRC NITs;
  - the empty-index and served-digest checks, probed in both upload jobs: the scripts are right today;
  - the lead's empty-index ruling (`ac2ca2b`) matches its docs, apart from the item number round 1 fixed.
- **What could not be checked:** what only GitHub runs (OIDC trusted publishing, the environments' protection and approval wait, the real API shapes, artifacts across re-run attempts, the publish action's container, the index's lag); what only macOS runs (Apple's own `/bin/bash` 3.2, BSD tools, pipx on the three legs); a real hatchling wheel; Python 3.9 itself; and a syscall-level offline proof.

**Notes for other work**
- **PR 3b** (tests, comments and docs only, on `9682f0b`) follows PR 3. Lane 3 lands it after PR 3 by cherry-picking it onto main, and lane 2 re-checks it with the same mutant set (#228 6273289912).
- **PR 4** (the release notes and README) follows, stacked on this PR.
- **The migration's single rehearsal** waits for all four PRs (D1, #186).
- **The `1.1.0rc1` rehearsal to TestPyPI** is a later step, on Brett Heap's word.

**Landing:** Brett Heap's decision 4 on #228 ("Yes, merge when green (Recommended)"), and his word of 2026-10-18T20:3xZ, "use this as approval to merge when green for all merges thru v1.1.0".
- Lane openRepoTools-3 lands the queue in one order: #262, PR 2, PR 3, PR 4, T025's docs PR, then #255.
- This PR squash-merges after an independent review, once Linux CI is green on the exact tree main will have after its squash. The record is a queue run of `tests.yml` on lane 3's prefix branch, under **Runs**.
- Lane 2's re-check of `1b41ce7..9682f0b` is LAND. q3k is q2k plus this head, so PR 2's `package` job runs on it.
- When its turn comes, lane 3 pushes a head that merges main into `9682f0b` with q3k's tree, as a fast-forward, so the tree under review is the tree that was tested.

Lane: openRepoTools-3

Landed by lane openRepoTools-3 as queue prefix q3k (tree 294b021, tests.yml run 39373756231 green), on Brett Heap's word of 2026-10-18T20:37:07Z: "use this as approval to merge when green for all merges thru v1.1.0". Review by lane openRepoTools-2: LAND once its findings were addressed (#228 6273189381); its re-check of 1b41ce7..9682f0b: LAND (#228 6278141437). The re-check's new findings went to the tests-only follow-up, PR 3b (#284).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 20, 2026
…e repository, the v1.1.0 notes are drafted, and README says what a release is, what --version prints and what a workstation with pipx's link sees (#228, PR 4 of 4) (#269)

Part of #228: **pull request 4 of 4** of feature `006-release-pypi`, plan.md "Delivery". It holds User Story 6, which is Phase 8 of `specs/006-release-pypi/tasks.md` (T022 to T024).

**The record now tells the truth about releases.** The v1.0.0 notes are in the repository, the v1.1.0 notes are drafted, and README says what a release is, what `--version` prints, and what a workstation with pipx's link sees.

Nothing is published here. The v1.1.0 notes are a draft with placeholders, and the GitHub Release is made from them only after PyPI serves 1.1.0 (`.github/workflows/release.yml:108-112`). README gives no install command for the package (FR-045). Every tag and upload still waits for Brett Heap's word (plan.md gate G8).

**What it adds**
- **`docs/release-notes/v1.0.0.md` (T022, `d637841`):** the published v1.0.0 Release body, byte for byte. It is exactly the output of `gh release view v1.0.0 --repo opensoft/openRepoTools --json body --jq .body` (research R15).
  - **How the verify proves it:** the task's verify runs that same command and pipes it into `cmp - docs/release-notes/v1.0.0.md`. One command wrote the file and checks it, so the two cannot disagree about a trailing newline. The suite does not repeat it, because it reaches no network.
  - The file is 15 lines (`git show --stat`). It is the Release body plus the one newline `--jq` adds (review N1).
- **`docs/release-notes/v1.1.0.md` (T023, `cb478cc`):** the draft v1.1.0 Release body, 90 lines. The Release is created from it after PyPI serves 1.1.0, so the notes never name a version PyPI lacks.
  - **What it holds:** `## What 1.1.0 adds` with six items (the package on PyPI as `openrepotools`; `--version` naming the release; `--install`'s rules for pipx's link and pip's console script; the hook pair that waits for its target, #237; the `package` job and the release workflow; releases as annotated tags), `## What else changed since v1.0.0`, and `## Install and upgrade` with seven items. The install section gives the move from the one-line install, `pipx install --force openrepotools` then `openRepoTools --install`, and why `--force` is needed.
  - **Placeholders:** nine, each a `<name>` in code font. Each is listed in the closing `## Before the tag` checklist, which has 11 items: eight for the placeholders (the tag time and the tag words share one) and three for facts to re-read, with no placeholder (the hook pair, the pin, the milestone).
  - **The last line** names the file's own path in the spec leg, `opensoft/openRepoTools-spec`'s `docs/release-notes/v1.1.0.md`.
- **README (T024, `ed72edf`):** what a release is, what `--version` prints, and what a workstation with pipx's link sees. README grows from 485 to 505 lines (`wc -l` at `29cc1b3` and at `9378103`), and gains no install command for the package.
  - **A release (`README.md:361-367`):** a release is an annotated tag `vX.Y.Z`, and the version comes only from the tag. The release build writes it into the `openrepotools` package, whose `--version` first line is `openRepoTools <version> (<repository> @ <commit>)`. A package built from a checkout prints `openRepoTools 0.0.0 (an unstamped package)`. A checkout and the one-line install print `openRepoTools (<repo> @ <ref>)`, as before (FR-043). This replaces "There is no version number here".
  - **One route (`:240-242`):** a workstation uses one install route or the other, never both, until opensoft/openBenches#165 lets `setup-estate-commands.sh` accept pipx's link (FR-047, Brett Heap's decision of 2026-10-18).
  - **Beside pipx's link (FR-044):**
    - the file count says SEVENTEEN, and SIXTEEN beside the link (`:244`);
    - the one link `--install` skips is pipx's, at `openRepoTools` (`:267-268`);
    - a new paragraph says the link is skipped, counted apart and given no receipt row, and that the count line reads `16 of 17 placed in <dir>, and 1 left to pipx` (`:271-276`);
    - the count invariant is 28 placed always, one left to pipx and 28 receipt rows, with the hook pair on top once its path answers (`:315-318`).
  - **`tests/test_repo_hygiene.py`:** README is now 505 lines, so `test_readme_is_short_enough_to_be_read` gets a dated entry (2026-10-18), and its cap goes from 486 to 505. The assembly's 510 does not move (review S1).
  - **`tests/test_test_roots.py`:** the lead widened T024's `files:` to it on 2026-10-18. After the fix round it changes one docstring.
- **The marks (`90794a3`):** `specs/006-release-pypi/tasks.md` marks T022 to T024 done, and adds `tests/test_test_roots.py` to T024's `files:`.

**What only becomes true at the v1.1.0 tag and the PyPI upload, or at a later step.** README gives no install command for the package. These places name the package, the release or a later step in the present tense:
- `docs/release-notes/v1.1.0.md:5-7` and `:11`: v1.1.0 "puts openRepoTools on PyPI", and is published from `opensoft/openRepoTools-code` after the migration (#186).
- `docs/release-notes/v1.1.0.md:14`, `:53` and `:57`: `pipx install openrepotools`, `pipx install openrepotools==1.1.0` and `pipx install --force openrepotools`. They work once PyPI serves 1.1.0, which is why the file is a draft.
- `docs/release-notes/v1.1.0.md:90`: the file's own path in the spec leg, which the cutover makes.
- `README.md:240-241`: names pipx's `openrepotools` as one of the two routes, and gives no command. The notes' `:7` has 1.1.0 as the first version on PyPI.
- `README.md:271-272`: "a link into pipx's `openrepotools` environment". Such a link exists on a workstation that has installed the package with pipx.
- `README.md:361-364`: "The release build writes it into the `openrepotools` package it builds". That build is PR 3's `release.yml`, and its first run is a later step, on Brett Heap's word.
- `tests/test_repo_hygiene.py:2536-2538`: the new README test says "FR-045's later PR (the change's tasks.md 6.4) rewrites this test", because that PR adds the pipx route to the assembly's README.
- `tests/test_repo_hygiene.py:2510-2513` and `tests/test_test_roots.py:367-368`: the re-cut of `assembly-0001` sets the assembly's caps. That is lane openRepoTools-1's step.

**Runs**
- **Queue prefix q4k** (branch `lane3-queue/q4k-228pr4-on-q3k`: q3k, which holds PR 3 at its fix head, + this head `26e5d9a`, on main `677acc0`, tree `4ea56ea`): `tests.yml` run 39373758353, green. This is the run of record for the landing.
- **The head:** `26e5d9a`, on PR 3's earlier head `1b41ce7`. PR 4's diff touches no file of PR 3's two fix rounds (`.github/workflows/release.yml`, `tests/test_release_workflow.py` and design.md), so the rounds and this PR share no file.

**Review.** An independent adversarial review (Opus) gave **LAND after fixes**, with no MUST and five SHOULDs (`briefs/rv-pr4-006-final.md` in lane 3's tree). It read T022 to T024 as they stood before the fix round.
- **Fixed here, in `3efcd71`:**
  - S1: the assembly's README cap stays 510, and is not 529. The README part of `assembly-0001` no longer applies (its hunks at `:237`, `:260` and `:300` fail), so 529 was arithmetic over a patch that does not apply. T024's dated entry says so, and `tests/test_test_roots.py` is back to 510 and 511 with a docstring whose numbers are true at this head;
  - S2: `test_readme_says_what_a_release_is_and_offers_no_package_route` reads README. It holds that no line matches `(?i)\bpipx?\s+install\b.*\bopenrepotools\b`, that the install section cites `opensoft/openBenches#165`, that the version paragraph holds the three `--version` first lines, and that `16 of 17 placed in <dir>, and 1 left to pipx` and `28 receipt rows` stand;
  - S3: README's one-route sentence names the two routes it rules between, pipx's `openrepotools` and workBenches' `./setup.sh`, in the same three lines, so README stays at 505 lines;
  - S4: the v1.1.0 notes fill `<#237 PR>` with `#247`, say that our exact guard entry is taken back out of a file that carries it while the pair is deferred, and turn the stale checklist item into a re-read item.
- **S5** needed nothing: `90794a3` already records T024's widened `files:`.
- **Not taken here:** the seven NITs, N1 to N7.
- **What the reviewer ran:**
  - the three verify commands at an assembled head, GitHub `main` `08e7550` with PR 1's head `08743b0` merged and the three T022 to T024 commits picked on it, each with the patch-id of its original: T022's `cmp` silent; T023's three greps pass; T024's guard passes and `bash tests/run.sh -k 'repo_hygiene or readme'` gave 207 passed;
  - `bash tests/run.sh -k test_test_roots`: 40 passed;
  - a trial of all 11 commits of PRs 2 to 4 on the same base, clean;
  - six README mutants: the pipx route offered, the FR-047 sentence dropped, "no version number" put back, and the wrong counts beside pipx's link all stayed green in the suite, which is S2. One line over the cap failed, and the verify's own grep missed `pipx install openRepoTools`;
  - not done: a full local suite (stopped at about 7%) and PR 4's Linux CI. CI's `tests` job is the suite of record.

**The fix round's re-check** (lane openRepoTools-5, Opus, #228 6267650575): **LAND after one line.** S1 to S4 are fixed. The new test fails on mutants M1, M2, M3, M4 and M6, each on the right assertion. With `26e5d9a`, PR 4 is LAND by this re-check (#228 6267768154).
- **Its SHOULD**, fixed in `26e5d9a`: T024's dated entry was headed "486 -> 505 HERE AND 510 -> 529 AT AN ASSEMBLY ROOT", against its own last paragraph. It now reads "486 -> 505 HERE, on 2026-10-18".
- **Its NIT**, fixed in the same commit: the entry named two of `assembly-0001`'s failing README hunks. It now names all three, each with what it changes and what it fails against. `git apply --check` on each hunk alone shows `:237`, `:260` and `:300` fail, and the other seven apply.
- **Its other two NITs**, left as they are: the new test's pattern lets `pip3 install`, `uv tool install` and a command wrapped across two lines through, which is no worse than before; and the count checks read the raw text, so a reflow turns them red.

**Notes for other work**
- **T025**, the quickstart run on PR 4's head, comes after this PR, as a small docs PR: lane openRepoTools-5's two commits and the `tasks.md` mark, on `26e5d9a`. It carries NIT-7's three citation fixes. #255 lands after that.

**Landing:** Brett Heap's decision 4 on #228 ("Yes, merge when green (Recommended)"), and his word of 2026-10-18T20:3xZ, "use this as approval to merge when green for all merges thru v1.1.0".
- Lane openRepoTools-3 lands the queue in one order: #262, PR 2, PR 3, PR 4, T025's docs PR, then #255.
- This PR squash-merges after an independent review, once Linux CI is green on the exact tree main will have after its squash. The record is a queue run of `tests.yml` on lane 3's prefix branch, under **Runs**.
- q4k is q3k plus this head, so PR 3's fix rounds are beneath it.
- When its turn comes, lane 3 pushes a head that merges main into `26e5d9a` with q4k's tree, as a fast-forward, so the tree under review is the tree that was tested.

Lane: openRepoTools-3

Landed by lane openRepoTools-3 as queue prefix q4k (tree 4ea56ea, tests.yml run 39373758353 green), on Brett Heap's word of 2026-10-18T20:37:07Z: "use this as approval to merge when green for all merges thru v1.1.0". Review: LAND after fixes, with no MUST; the fix round's re-check by lane openRepoTools-5 (#228 6267650575) left one SHOULD, fixed at 26e5d9a (tests/test_repo_hygiene.py:2485).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 20, 2026
…6 docs cite the tests where they now sit, and the partial-stamp example is the line stamp_read prints (#228) (#270)

Part of #228: **the small docs PR after the four.** It holds T025, Phase 9 of `specs/006-release-pypi/tasks.md`, and PR 1's NIT-7 (the final review of #262).

**quickstart.md has been run, and the three passages it found wrong are fixed.** Lane openRepoTools-5 ran its steps on PR 4's head in an isolated home. The 006 docs now cite the tests where they sit, and `contracts/cli-output.md`'s `is partial:` example is the line the reader prints.

Nothing is published here. The PR changes four documents under `specs/006-release-pypi/` and no code, test or workflow, and quickstart.md has no TestPyPI or PyPI upload step.

**What it changes** (branch `006-release-pypi-t025-pr` at `caa9c0f`, on PR 4's head `26e5d9a`: 4 files, 16 insertions and 12 deletions; its commits are lane 5's `ef6c18d` and `b3aab89`, and lane 3's `caa9c0f`)
- **`quickstart.md` (T025, `ef6c18d`):** three passages did not hold as written.
  - **The network sentence** said only steps 3 and 4 need the network, and "Nothing else here does." Step 4 builds nothing, and takes step 3's wheel. Step 5's pipx, when it installs with pip rather than uv, fetches pip into a new `PIPX_HOME`'s shared libraries from the index (pipx 1.17.12, `--backend pip`, logged). Step 8 asks GitHub. It now says "Three steps can need the network" and names them.
  - **Step 1** says the built-wheel tests skip "naming OPENREPOTOOLS_TEST_WHEEL", but pytest prints no skip reason without `-rs` (its default is `-r fE`). The command now passes `-rs`, as step 4 already does.
  - **Step 5's list of expected lines** left out the PATH note every such run ends with, because `$t/bin` is not on `PATH`. The note says the release just installed "is not the one that runs", and the list now ends with it.
- **NIT-7's stale citations (`ef6c18d`),** re-derived with `grep -n` at `26e5d9a`:
  - `quickstart.md:40` and `contracts/cli-output.md:45`: `tests/test_openrepotools_command.py:299-319` is now `:323-344`;
  - `contracts/cli-output.md:46`: the docstring at `:300-302` is now `:324-327`;
  - `plan.md:153`: `tests/test_repo_hygiene.py:1748` and `:2253` are now `:1981` and `:2516`, the AGENTS.md and README `cap =` lines (PR 4's last fix moved the second from `:2513`);
  - `plan.md:212` cites the same docstring, so it is now `:324-327` too. It was not on NIT-7's list, and lane 3 kept it;
  - `research.md` is left as written, because it dates its citations to `aaa7bc8`, where they were right.
- **`contracts/cli-output.md:67-68` (`b3aab89`):** the example of `is partial:` read "COMMIT is set and DIGEST is empty", which the reader cannot build. `openRepoTools:741` writes `is partial: <set> set, but <missing> empty`, naming every set field and every empty one. The example is now what `stamp_read` printed for a stamp whose DIGEST alone was empty: `is partial: VERSION, TAG, COMMIT and REPOSITORY are set, but DIGEST is empty`.
  - **Where it comes from:** the reader's own output, not composed by hand. A wheel built from the branch was installed with pipx in an isolated home, its stamp was given every field but DIGEST, and `openRepoTools --help` refused with exit 2. Only lines 67 and 68 change. Lane 3 ruled it (#228 6269627359).
- **`tasks.md` (`caa9c0f`):** T025 is marked done, and its `files:` now names `plan.md` and `contracts/cli-output.md` beside quickstart.md. The lead's ruling (#228 6255121743) widened it for NIT-7, so the citations in those two documents stay within T025.

**The quickstart's outcome,** which T025 asks the pull request to record (lane openRepoTools-5, #228 6269608275). Every `LANES_*` variable was cleared, `HOME` was a temporary directory, and `PATH` held only the system directories.
- **Step 0, prerequisites:** holds.
- **Step 1, the package's tests without a wheel:** 622 passed and 5 skipped, each skip saying `OPENREPOTOOLS_TEST_WHEEL is unset`.
- **Step 2, the hygiene and command tests:** 634 passed and 9 skipped.
- **Step 3, the wheel built twice:** `cmp` gives one file, with 32 members (3 modules, 24 payload files and 5 `.dist-info`).
- **Step 4, the built-wheel tests:** 175 passed.
- **Step 5, the pipx install and the offline `--install`:** holds, and the expected-output list is fixed.
- **Step 6, the broken stamp:** `--help` is refused with exit 2.
- **Step 7, the workflows:** actionlint 1.7.12 is clean, and 342 passed.
- **Step 8, the v1.0.0 notes:** byte for byte.
- **Step 9, cleanup:** holds.
- **Where each step ran:** steps 1 to 4 on `ef6c18d`, and steps 0 and 5 to 9 on `9378103`. Since `9378103` only these docs and one test docstring (PR 4's `26e5d9a`) have changed. Each test step ran as the `-k` selection quickstart.md writes, because `tests/run.sh <file>` collects the whole suite.
- **Not run locally:** T025's own verify line, `bash tests/run.sh`, the full suite. This PR's CI is its record.

**Runs**
- **Queue prefix q5k** (branch `lane3-queue/q5k-228t025-on-q4k`: q4k + this head `caa9c0f`, on main `677acc0`, tree `e27f345`): `tests.yml` run 39373760614, green. This is the run of record for the landing.
- **The two hygiene tests that read every tracked file** pass at `b3aab89`: `test_no_committed_file_names_a_host_absolute_path` and `test_adoption_act_zero_is_cited_by_the_sha_that_landed` (lane openRepoTools-5, #228 6271120043).

**Review.** Lane 5 ran the steps and wrote `ef6c18d` and `b3aab89`. Lane 3 reviewed and accepted both, and that is this PR's independent review, because lane 3 did not write them. `caa9c0f` is lane 3's own one-line `tasks.md` mark.
- **`ef6c18d`** is accepted (#228 6269627359): its scope holds, because it touches exactly the three documents T025's `files:` was widened to, and the extra `plan.md:212` fix is kept.
- **`b3aab89`** is the one commit lane 3 asked for after that, replacing the `is partial:` example with the reader's own line and changing nothing else (asked in #228 6269627359, pushed as reported in 6271120043).

**Notes for other work**
- **This PR lands right after PR 4** (#228 6269627359). Its branch is on PR 4's head, so until the four have landed, its diff against main also holds their changes.
- **#255 lands after this PR.**

**Landing:** Brett Heap's decision 4 on #228 ("Yes, merge when green (Recommended)"), and his word of 2026-10-18T20:3xZ, "use this as approval to merge when green for all merges thru v1.1.0".
- Lane openRepoTools-3 lands the queue in one order: #262, PR 2, PR 3, PR 4, T025's docs PR, then #255.
- This PR squash-merges after an independent review, once Linux CI is green on the exact tree main will have after its squash. The record is a queue run of `tests.yml` on lane 3's prefix branch, under **Runs**.
- q5k is q4k plus this head, so PRs 2 to 4 are beneath it.
- When its turn comes, lane 3 pushes a head that merges main into `caa9c0f` with q5k's tree, as a fast-forward, so the tree under review is the tree that was tested.

Lane: openRepoTools-3

Landed by lane openRepoTools-3 as queue prefix q5k (tree e27f345, tests.yml run 39373760614 green), on Brett Heap's word of 2026-10-18T20:37:07Z: "use this as approval to merge when green for all merges thru v1.1.0". T025 is lane openRepoTools-5's (#228 6269608275), accepted by lane openRepoTools-3 with one more commit, b3aab89 (#228 6269627359).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 20, 2026
… one, and a leg directory this account cannot search, or a dependency nested in one, is unreadable and never absent (fixes #255) (#271)

A follow-up to #246 (T018 of feature 004, the triad lane tooling) that must land **before the triad cutover (#186)**. It fixes the two sections of #255.

**The handoff names every inventory record that is not a whole schema-1 one, and a leg directory this account cannot search is unreadable, never `absent`.** Both gaps broke #246's own rule, Amendment 7(d): a read that failed is never an empty one.

These readers run only in an assembly, a checkout whose `project.yaml` declares a leg, so main is not exposed before the cutover.

**What it fixes**
- **Section 1, the handoff's tree inventory** (`lane-handoff`, `paired_inv` and `pi_bad`):
  - **Before:** a `lane-trees` row counted as unreadable only where its path was empty. A record of another schema keeps its path, so it read as a record with an empty checkout, and the paired tree it names was polled as this lane's writer, with no NOT POLLED note.
  - **Now:** the row's schema, field 13, decides. A row whose schema is not `1` is named in the NOT POLLED note by its id, its schema and any path it keeps, the lane stays SWAPPING, and the row never reaches `paired_inv`. The `lane-trees` row format is untouched.
- **Section 2, a leg directory with no search permission:**
  - **Before:** its `.git` could not be tested, and each reader's `.git` test failed there exactly as for a leg that is not checked out. The sweep listed it `absent` and exited 0. `lane-reconcile` gave no TREE row. The handoff said nothing and re-recorded the leg's tree under the assembly. `lane-end` ENDED the lane over the lane's unpushed commit in the leg.
  - **Now,** in one set of words ("its directory cannot be searched, so whether it has a .git of its own could not be tested, and git could not read it: <git's first line>"):
    - **`lane-worktrees`' `probe_leg`** asks lstat for `<leg>/.git`. Only ENOENT or ENOTDIR is a leg with no `.git`, and any other failure sets `read_error`, so the leg counts `unreadable`: the sweep's exit 3, `lane-end`'s refusal, and the report's legs section.
    - **`lane-handoff` and `lanes-edit.sh`'s `lane_reconcile`** take their unreadable-leg branch where the `.git` test fails and the leg directory fails `-x`. The handoff gets an UNREADABLE LEG line, a count and a withheld tree. The reconciliation gets a TREE row `unreadable` that requires recovery.
    - **A leg directory that is not there at all** is still `absent`.

**What the fold-in adds.** Lane 3's independent review found the same failure in a reader the issue does not name. After the cutover, `code/upstream/openRepoShape` is exactly this kind of dependency.
- **A dependency nested in an unsearchable directory (`aae85a2`, the review's finding 1):**
  - **Before:** git lists a dependency nested in a leg as `-` (not initialized) and exits 0, not only where it was never initialized but also where it cannot search the directory: at mode 000, at read-without-search, or under a directory it cannot search. `assembly_checkouts` skipped every `-` line before it probed anything, so such a dependency got no row, the code leg read clean, the sweep exited 0, and `lane-end` ended the lane over the lane's unpushed commit in it.
  - **Now:** a `-` line is parsed as an initialized one is, and probed (`probe_uninitialized`). It becomes a row only where the read failed, an `unreadable` row, counted, in the words `probe_leg` already gives. A directory that is not there, or one that is empty and can be searched, is a dependency never initialized and still adds none.
- **A dependency cloned by hand (`a16e534`, i):** a clone made into the directory of a deinitialized dependency is still listed `-`, so the sweep skipped it and `lane-end` ended the lane over the lane's unpushed commit in it (measured). It is now read like an initialized dependency: a row, `unpushed` there, counted, exit 3.
- **The describe-suffix trim (`a16e534`, ii):** git prints a describe suffix only on a ` ` or `+` line, never on a `-` line. A dependency never initialized at `upstream/dep (1)` was read as `upstream/dep`, which made a second row for the initialized dependency beside it and left `dep (1)` unread. The trim now applies to the initialized branch only.
- **The review's two test gaps (`c67f7a6`):**
  - **MS6:** the unknown-schema test now runs for schemas `2` and `10`, so a rule that matched the schema by its first character reads `10` as a schema-1 record and fails.
  - **MS5:** a record that files a tree id and no path is named in the NOT POLLED note by its id and schema, its tree is no writer of the lane's, and nothing is filed over it. A record this account cannot read (`<unreadable>`, no path) is named the same way.
- **The docs (`c67f7a6`, `a16e534`):** `docs/README-lanes.md` says what the sweep does with a dependency git lists as not initialized, and #255's own added text is reflowed where its insertion left ragged lines.

**The branch.** `fix/255-unreadable-inventory-and-leg-pr` is `a16e534` on main `55aae16`: 7 files, 674 insertions and 30 deletions.
- **Lane openRepoTools-2's two commits, cherry-picked:**
  - `13397aa` (from `ba13d21`): the two fixes, in `lane-handoff`, `lane-worktrees` and `lanes-edit.sh`, with four tests;
  - `e678c61` (from `523b381`): `docs/README-lanes.md`'s T018 section, `docs/triad-lane-tooling-analysis.md` section 7 item 11 naming opensoft/openBenches#166, and `lane-handoff`'s T018 section heading, now `3b`.
- **Lane 3's three fold-in commits:** `aae85a2`, `c67f7a6` and `a16e534`.
- **Why a cherry-pick:** #246 landed as the squash `55aae16`, so a merge of main into lane 2's branch conflicts in all seven files. The PR is the two commits cherry-picked onto main, with no force-push anywhere. Their diff equals `git diff 5dd0df0 523b381` (the same patch-id), and their tree at `e678c61` is `1a46c72`, the tree the reviewer computed.

**Tests.** There are 11 `test_255_` functions: lane 2's four, and the fold-in's seven. Under **Runs**, six of the seven fail under a mutant of the code they hold. The seventh, the unreadable record, needs no #252.
- **Lane 2's four,** each failing on `5dd0df0`'s tools and passing here:
  - `tests/test_triad_lane_tooling.py`: `test_255_a_leg_directory_that_cannot_be_searched_is_unreadable_and_holds_lane_end`;
  - `tests/test_triad_lane_tooling_handoff.py`: `test_255_a_leg_directory_that_cannot_be_searched_is_unreadable_in_the_handoff_and_reconcile`, `test_255_an_unknown_schema_record_is_named_in_the_handoffs_not_polled_note` and `test_255_a_malformed_record_is_named_in_the_handoffs_not_polled_note`. The `<malformed>` row comes from a fake `lanes-edit.sh`, so this test does not wait for #252.
- **The fold-in's seven:**
  - `tests/test_triad_lane_tooling.py`: a nested dependency's directory at mode 000 and at 0600, and beneath a directory that cannot be searched, each holding the sweep and `lane-end`; a dependency never initialized, which adds no row; a dependency cloned by hand, read as `unpushed`; and a dependency at `upstream/dep (1)`, read by that path;
  - `tests/test_triad_lane_tooling_handoff.py`: the tree id with no path, and the unreadable record.
- **Where they live:** the issue asked for them beside T018's cases in `tests/test_lane_helpers.sh`. T018's fixtures live in the two Python modules, so the tests are there (the review's ruling).

**The order with #252 (the review's NIT 5).** `<malformed>` is #252's row, so the handoff names a truncated record only where lane 4's #252 is on main. The review asked for #252 first, and that is what happened: #252 merged as `00d52ae` at 2026-10-19T18:14Z, and q6k is built on it. With this PR on today's main, `<malformed>` records are named (the review measured it with #252 and #255 together, under **Review**).

**Follow-ups, not in this PR:**
- a declared leg under an unsearchable parent (the review's ruling on lane 2's doubt: a follow-up);
- a leg at mode 0100, searchable and not listable, which reads `clean` (finding 4, estate-wide; lane 3 proposes it to Brett Heap for v1.2.0, #255 6272494888);
- the top block's contradiction for a paired tree that only a broken record names (finding 3; proposed for v1.2.0 in the same comment);
- the handoff's wording on #252's refusal code (NIT 8; lane 3 proposes it as a v1.2.0 item, #255 6272494888).

**Runs**
- **The record, q6k** (branch `lane3-queue/q6k-255-on-q5k`: q5k + this head `a16e534`, on main `677acc0`, tree `1149aeb`, with its conflict with #283 resolved as in `fcd3cce`): `tests.yml` run 39373762988, green. This is the run of record for the landing.
- **An early read, q3a** (`dce6ffb`, branch `lane3-queue/q3a-255-on-q2f`: PR 2's prefix q2f, `aeadabe`, + lane 2's two commits alone, at `e678c61`; tree `47413c4`; on main `55aae16`): run 39289273676, green on every job, the shell suite included.
- **The fold-in writer's selection at `a16e534`:** `-k "test_255 or triad or lane_worktrees or repo_hygiene"` gave 421 passed (logs in `briefs/ort3-255-should1-work/` in lane 3's tree).
- **The fold-in's mutants:** the writer ran seven and each fails a `test_255_` test.
  - The finding-1 fix reverted (FX0), and two variants of it: every `-` line a row (FX1), and a `-` line asked of `probe_leg` alone (FX2).
  - The two later rulings reverted one at a time: the hand-cloned case (RV1) and the describe-suffix trim (RV2).
  - The reviewer's MS5 and MS6.

**Review.** Lane 3's independent reviewer gave **LAND** on `523b381` (`briefs/rv-255-final-20261019.md` in lane 3's tree, posted on #255 as 6272494888). Both fixes do what sections 1 and 2 ask, in every reader named.
- **What it ran:**
  - the four new tests fail on `5dd0df0` for the right reason and pass on `523b381`;
  - 17 of 19 mutants are killed, and the two survivors, MS5 and MS6, are low-impact (the fold-in's tests now kill both);
  - with #252 (`3ffefce`) the fix merges cleanly, and against the real `lanes-edit.sh` the handoff names the `<malformed>` record, in 5 of 5 cases. On that tree, main + #252 + #255, the selection `-k 'test_255 or triad or lifecycle_fail_open or repo_hygiene or lane_worktrees'` gave 457 passed;
  - the shell suite on that tree gave 4889 passed and 2 failed, `test_lane_helpers.sh:1269` and `:8548`, both in `lane-start`'s row stamp. The reviewer takes them as its sandbox's 106-character path reaching the 240-character `ROW_STATE_CAP`, and neither #255 nor #252 touches `lane-start` or the cap. q3a is green on every job, the shell suite included.
- **What lane 3 did with the findings:**
  - finding 1, a SHOULD, and NITs 6 and 7: folded in, above;
  - finding 2, a SHOULD: the cherry-pick, above;
  - findings 3 and 4, and NIT 8: the follow-ups, above;
  - NIT 5: the order with #252, above.
- **The reviewer's re-check of `e678c61..a16e534` is LAND (#271 6277159862).**

**Landing:** Brett Heap's decision 4 on #228 ("Yes, merge when green (Recommended)"), and his word of 2026-10-18T20:3xZ, "use this as approval to merge when green for all merges thru v1.1.0".
- Lane openRepoTools-3 lands the queue in one order: #262, PR 2, PR 3, PR 4, T025's docs PR, then #255.
- This PR squash-merges after an independent review, once Linux CI is green on the exact tree main will have after its squash. The record is a queue run of `tests.yml` on lane 3's prefix branch, under **Runs**.
- #255 is last in the queue. q6k is q5k plus this head, so PRs 2 to 4 and T025 are beneath it.
- When its turn comes, lane 3 pushes a head that merges main into `a16e534` with q6k's tree, as a fast-forward, so the tree under review is the tree that was tested.

Lane: openRepoTools-3

Landed by lane openRepoTools-3 as queue prefix q6k (tree 1149aeb, tests.yml run 39373762988 green), on Brett Heap's word of 2026-10-18T20:37:07Z: "use this as approval to merge when green for all merges thru v1.1.0". Its conflict with #283 in tests/test_triad_lane_tooling_handoff.py is resolved as in the tested tree fcd3cce, which keeps both blocks. Independent re-check of the fold-in round e678c61..a16e534: LAND (#271 6277159862).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 20, 2026
…runs the fetch, pipx, approval, tag, ref, clean, stamp, dist and digest steps instead of reading them, and release.yml changes only in comments (#228, PR 3b) (#284)

Part of #228: **pull request 3b** of feature `006-release-pypi`, stacked on #268 (PR 3, head `9682f0b`). It is the fix round for lane openRepoTools-2's verdict on PR 3 (#228 6273189381) and its re-check (6277890612, 6278141437). Those mutants of `release.yml` survived at `9682f0b` because the steps they changed were read as text, not run.

**Nothing changes in what the workflow does.** `release.yml` changes in 9 comment lines only, and `spec.md` in 10. `tests/test_release_workflow.py` grows from 66 test functions to 79. The diff against `9682f0b` is 3 files, 1023 insertions and 90 deletions. No tag is pushed and nothing is dispatched (plan.md gate G8).

**The nine commits**
- `4daaeda` (SH2, mutants I11 and I12): the install check's fetch step is run on a tag with `curl` faked.
  - It passes the one file TestPyPI's JSON lists under the wheel's name, at the recorded digest, over https from test-files.pythonhosted.org.
  - It refuses before fetching anything: another host, plain http, a file listed twice, another digest, another file at the digest, and nothing listed.
  - The pipx step is run with pipx faked. It passes only when pipx's bin directory holds the one link `openRepoTools`.
- `2f274c4` (SH3, mutants E2 to E4): a fake `gh` answers the forge's REST reads. Under it are run:
  - the build's environments step;
  - each upload job's approval re-check;
  - each upload job's tag re-check, with the tag moved, deleted, made lightweight, or naming a tag.
- `9762479` (SH5, mutants RF1 and RF2): the ref step is run on tags that a forge fake and a `file://` origin hold, so only the step's own comparisons can refuse.
- `2a83a61` (SH7, mutants W9 to W12): one synthetic wheel each, made from HEAD's blobs:
  - METADATA naming a dependency;
  - a second console script;
  - a sixth `.dist-info` file;
  - no `licenses/LICENSE`.

  Each refusal must name what is wrong.
- `0cf8142` (NITs S7, S8, T5, BLD1, H5 and N5): a case for each:
  - a TAG of another version;
  - a read-back one character short;
  - a forge that ignores `head_sha=`;
  - the build step run with the backend faked;
  - the "STAYS PUBLIC" bullet;
  - every job's whole step list, pinned in order (`STEP_ORDER`).
- `76dd73c` (SH8, mutant N3): the clean step is run. The test file's header lists which decisive steps are run and which are still read as text.
- `5ccd66e`: the doc NITs, in comments only.
  - One spelling for the rehearsal's row.
  - FR-029's additions are named in the header and in FR-028.
  - Step 1's comment is made true.
  - US5's tenth scenario is numbered.
- `7376b34` (SH6, S5 and S5b, and NITs 1 and 3 to 6 of the PR 3 re-check):
  - the stamp step is run whole with `refs/heads/release/v1.1` and with `refs/tags/v1.1.0`, and both must stamp `TAG v1.1.0`;
  - each upload job's dist step is run whole, and so is the install check's digest step, on five cases each;
  - the fake index polls every 0.01 s;
  - `PYTHONDONTWRITEBYTECODE=1` is set in `run_index_step`;
  - a served-check case's first read is a connection closed with no response.
- `2ff7094` (the fix-up for lane 2's re-check, #228 6284132726: its N1, its question 1 and its NIT 3):
  - the test file's header names the dist step among the steps run, and says six are read only as text. Two more header sentences, untrue at `7376b34`, are corrected: how the build's record of the wheel is held, and which steps read `GITHUB_SHA` and `GITHUB_REPOSITORY`;
  - `STEP_ORDER` finds the build's guard by `guard_step()`, each upload job's guard as its first step, and the macOS and 3.9 legs by `condition_kind()`, as HOW A STEP IS FOUND has it. The count and order are unchanged;
  - W9's naming check reads only a refusal's `::error::` line.

**`7376b34`'s subject says "WIP … written and not yet run".** It was pushed at Brett Heap's account swap, before any test ran. Every result below was then taken at that exact head, on a clean tree, with nothing changed. Its message cannot be corrected without a force-push, so the squash at landing carries an accurate one.

**Results at `7376b34`**
- **Mutants:** all 37 run are killed. They ran in four batches, and each kill is credited to a test that runs only that mutant's step. The 37 are:
  - I11 and I12;
  - all 17 of group 4's survivors;
  - I3, I10, I13, I14, I17, I18, X5, X6, G7 and S4;
  - seven new ones: S5b, X8 to X12, and I20.
- `-k test_release_workflow`: 499 passed.
- `-k "test_release_ or test_tests_yml_ or repo_hygiene"`: 733 passed. That is lane 2's 646 at `9682f0b`, plus 87 new tests.
- actionlint 1.7.12 is clean.
- `git diff 9682f0b 7376b34 -- .github/workflows/release.yml` changes comment lines only.

**Results at `2ff7094`.** `-k test_release_workflow` gives 499 passed, 0 failed and 0 skipped. A step inserted after the stamp writer is killed by the pin. The macOS rerun or the build's guard, renamed, passes it. W9's refusal without its `::error::` naming line is killed. `release.yml` is byte-equal to `7376b34`.

**Review.** Lane openRepoTools-2's re-check of `9682f0b..7376b34` is LAND, with no MUST and no SHOULD (#228 6284132726): all 113 mutants are killed. Its re-check of the fix-up, `7376b34..2ff7094`, asked for in 6284529659, is LAND (#228 6288385122).

**CI record.** `lane3-queue/q7k2-228pr3b-on-q6k` (`4d6581d`, tree `7ad2bbe`) puts this PR on the main that lane 3's queue k leaves: main `677acc0`, then #262 and #267 to #271. Its `tests.yml` run 39378128111 is green.

**Left for v1.2.0** (lane 2's NITs, not taken here, filed on Brett Heap's word of 2026-10-20T09:40Z): WX1, the wheel check's bash line (#285); a `URLError` without a status (#286); the `--depth` pin on the ref step's tag fetch (#287); and a test that runs the stamp step followed by an inserted step (#288).

**Landing.** It lands after #271 in lane 3's queue, on Brett Heap's word of 2026-10-18T20:37:07Z: "use this as approval to merge when green for all merges thru v1.1.0". When #268 lands, the landing retargets this PR to main.

Lane: openRepoTools-3

Landed by lane openRepoTools-3 as queue prefix q7k2 (tree 7ad2bbe, tests.yml run 39378128111 green), on Brett Heap's word of 2026-10-18T20:37:07Z: "use this as approval to merge when green for all merges thru v1.1.0". Lane openRepoTools-2's re-check of 9682f0b..7376b34: LAND (#228 6284132726); its re-check of the fix-up 7376b34..2ff7094: LAND (#228 6288385122). 7376b34's subject says WIP because it was pushed at an account swap before its tests ran; every result above was taken afterwards, at 7376b34 or at 2ff7094.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 20, 2026
…271) into act 2's slice 1a (#198)

One conflict, in lanes-edit.sh's lane_state_follow, at the check of a
snapshot this helper cannot read: this branch releases the workstation's
sidecar lock (sidecar_release); main released W's mutex where the follow
took it (lsf_own) and gave a new note naming $LSS_WHY. Resolved as the PR
says: keep sidecar_release, take main's note. No line of main's that merged
on its own uses lsf_own or release_lock inside lane_state_follow, and main's
lanes-edit.sh changes since 55aae16 add no lock call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 21, 2026
… 1.1.0 and list, by area and from the squash titles of aaa7bc8..a42946e, what else changed on main since v1.0.0 (#228) (#293)

Part of #228. It brings `docs/release-notes/v1.1.0.md` up to date on main: first on `a42946e`, as lane 3 asked on Brett Heap's word (#228 comment 6303608345, the section for lane openRepoTools-2, task 1), then with lane 3's review of `d8a349d` (comments 6314185715 and 6314529807 on this pull request), with the lines for the three changes that landed after `a42946e`, and with the line for #296, which landed right before this pull request. It is one file, changed in two commits (`d8a349d` and `bebb77d`), 59 insertions and 4 deletions against main.

Lands last before rehearsal D's freeze; lane 3 reviews and merges it (#228 6303608345)

**What changed**
- **Lane 3's review is applied,** in the second commit:
  - **The line on #255 and #271** no longer says that `lane-reconcile` and `lane-handoff` read a dependency nested in a leg, since only the sweep and `lane-end` do. It says that a leg directory this account cannot search is unreadable and never `absent`; that a dependency nested in a leg whose directory, or one above it, it cannot search is unreadable to the sweep and `lane-end` too; and that one known gap remains, #272, where the sweep reads a declared leg beneath such a directory as `absent`, so `lane-end` does not hold on it. It also says NOT POLLED where the tool prints it, and it says that the item closes two gaps in #246, where it said "both gaps" with nothing before it to refer to.
  - **The line on #280 and #283:** an `awk` exits and does not "return", and the sentence on a status of 0 or 8 is gone, because a reader of the notes cannot act on it.
  - **The line on #214:** the fence passes a local commit only when it adds or changes a register or handoff file, and never checks whose it is, so the line no longer says "the lane tooling's own".
  - **The counts in the `<other-changes>` item:** the pin's line (#233) is in the list, so the list carries 44 of the 53, and the other 9 have their lines under "What 1.1.0 adds", not 43 and 10. The item says "the openRepoShape pin" where the runner pin of #296 could be taken for it.
  - **The milestone item** names #247, so it lists the six entries the milestone held on 2026-10-20.
  - **#209 is not tests-only.** Its `-e` fixes `lanes`' stderr filter under macOS's BSD `grep`, so it moves from the test suite to the lane area, after #208's, and its one line there also carries its test changes: the `repoG#8` assertion and the count of bash files in prose. It is cited once.
- **Lines for the changes after `a42946e`:** #263 (`d9065df`), #289 (`fb2cf36`) and #294 (`acc89ea`, which closes #292) landed on 2026-10-21, from 11:28Z to 11:30Z. Each has a line in its area, worded from its squash commit and its description: #263 and #289 under "The register and `lanes-edit.sh`", and #294 under "`lane`, `lane-start`, `lane-end`, the name guard and their helpers".
  - **#296 has its line too,** last under "The test suite and its checks": CI and the release workflow run on `ubuntu-24.04`, because `ubuntu-latest` is now Ubuntu 26.04, and `macos-latest` is unchanged. It landed right before this pull request, as `5afba4a`.
- **"What 1.1.0 adds" gains two lines,** after the annotated-tags item. They are for the three entries that closed in the v1.1.0 milestone after the draft was written:
  - **#255,** fixed by #271: the lane tools name what they could not read in a triad's legs. `lane-handoff` names every tree-inventory record that is not a whole schema-1 one, and a leg directory this account cannot search is unreadable and never `absent`. Only the sweep and `lane-end` also see a dependency nested in such a leg, since `lane-reconcile` and `lane-handoff` read declared legs only, and #272 stays open as the known gap.
  - **#280,** fixed by #283: `lane-handoff` reads the lane's lifecycle whole and parses it afterwards, so an `awk` that exits early can no longer turn `lane-state`'s SIGPIPE into a lifecycle that could not be read.
  - #283 is #280's pull request, so the three numbers make two lines. Each line cites the issue and its pull request, as the hook-pair line does for #237 and #247.
- **"What else changed since v1.0.0" is drafted** from `git log aaa7bc8..a42946e`, which is 53 squash commits, one pull request each and no merge commit. It is grouped by area, one line each, and each line ends with its pull request. It lists 44 of the 53: the pin's line (#233) at its head and 43 by area, as 4 on the estate commands, 10 on the register and `lanes-edit.sh`, 11 on `lane`, `lane-start`, `lane-end` and the name guard, 6 on the handoff and its skill, 1 on the lane tools in a triad's legs (#246), 9 on the test suite and its checks, and 2 on the triad migration's records.
  - **The other 9 are accounted for elsewhere:** the package series of #228 (#262, #267, #268, #269, #270 and #284) is covered by #228's lines, and #247, #271 and #283 have the lines above.
  - **The lines added on 2026-10-21** are for the four changes after `a42946e`: #263 and #289 on the register and `lanes-edit.sh`, #294 on `lane`, `lane-start`, `lane-end` and the name guard, and #296 on the test suite and its checks. The areas now hold 47 lines, one for each of 47 pull requests, and #233's line at the head of the list makes 48.
  - **The list is redone at the tag.** It is drafted from the squash titles only.
- **"Before the tag" is updated in three items:**
  - `<pr-count>` gives the count at `a42946e`, which is 53;
  - `<other-changes>` says what was drafted, which 9 pull requests have their lines under "What 1.1.0 adds", that the lines for #263, #289, #294 and #296 were added on 2026-10-21, and what is left to do at the tag; the placeholder's own line now stands for the changes after `a42946e` that have no line above;
  - the milestone item names the six entries the milestone held on 2026-10-20: #228 still open, #237 closed by its pull request #247, #255 closed, and #280 closed by its pull request #283.

**Not added:** nothing that has landed. #296 landed right before this pull request and has its line. This pull request itself, and whatever lands after it, is what the `<other-changes>` placeholder stands for. None of #263, #289, #294, #296 and #292 is in the v1.1.0 milestone, which still holds the six entries of 2026-10-20.

**How this keeps the draft's rules**
- **One-line paragraphs:** every added line is its own list item, and none is wrapped.
- **References:** every reference above "Before the tag" is spelled `opensoft/openRepoTools#N`, with no bare `#N`, since the notes are published from another repository after the migration.
- **Placeholders:** `<other-changes>` stays as a named placeholder in code font, as the closing line of the list and as its checklist item. `<pr-count>` is unchanged apart from the drafting-time count.
- **The file's closing lines are untouched:** the self-naming last line stays last.

**Tests**
- **T023's verify command** passes: the last line names `openRepoTools-spec` and `docs/release-notes/v1.1.0.md`, `## Before the tag` is there, and so is `pipx install --force openrepotools`.
- **Coverage:** a script over `git log aaa7bc8..acc89ea`, 56 commits and no merge, finds each pull request cited above "Before the tag" except the six of #228's package series, which #228's lines cover. That is 46 by area, #233 at the head of the list, and #247, #271 and #283 under "What 1.1.0 adds". #296 is not in that range, and its line cites it.
- **Hygiene:** by grep over `tests/`, no test names this file. The two that read its content scan every tracked file, and both are in `tests/test_repo_hygiene.py`. The only other test that names `release-notes` is in `tests/test_release_workflow.py`, and it reads `release.yml`, not the notes. All of `tests/test_repo_hygiene.py` was run on the file, through `tests/run.sh` with every `LANES_*` and `TMUX*` variable unset:

  ```text
  -k 'repo_hygiene' -v
  collected 2933 items / 2696 deselected / 237 selected
  237 passed, 2696 deselected in 2.59s
  ```
- **Not run:** the whole suite. CI's `tests` job is the suite of record. This pull request's own runs on `bebb77d` went red on `ubuntu-latest`, which is Ubuntu 26.04, for the reasons #296 gives, because #296 was not yet on main. The record is lane 3's queue run over main, #296 and `bebb77d`, on `ubuntu-24.04` (below).

**Where the draft's rules left room, and lane 3's answers (comment 6314185715)**
- **Which section holds #255 and #280:** "What 1.1.0 adds", because the milestone is Brett Heap's scope for the release and #237 with #247 already sit there.
- **`<other-changes>` stays** as a closing placeholder after the drafted list, since this pull request itself lands after `a42946e`.
- **How much to list:** the tests-only and migration-record lines stay through the tag, because the check that every pull request has a line depends on them. If Brett Heap wants a shorter body, they fold into one line that still lists every pull request number.
- **Issues and pull requests:** the milestone lines cite both, and the area lines cite the pull request only.

**Lane 3's re-check of `bebb77d`** (#293 6315668503) is MERGE AFTER FIXES.
- **What holds:** every line lane 2 listed, the recount behind line 140 (44 of the 53 pull requests in `aaa7bc8..a42946e`, and 56 in `aaa7bc8..acc89ea`), the file's rules, and T023's verify commands.
- **One SHOULD and two NITs, all wording:**
  - line 45's sentence on a dependency nested in a leg, where "one above it" reads as a directory above the leg, which is #272's case. The text was lane 3's own;
  - "One known gap" in the same line;
  - line 74's last clause.
- **Why they wait:** lane 3 merges the notes as tested and carries the three fixes into the notes' final pass at the tag. A push here would cost another CI round while lane 2 is out of usage until 15:30Z, and the notes are edited again at the tag in any case.

Lane: openRepoTools-2

Landed by lane openRepoTools-3, as queue prefix `lane3-queue-u/p2-293` (tree `9b879ca`; tests.yml run 39595166877, green on `ubuntu-24.04`), right after #296. Before the landing, lane 3 merged main into the branch, so that its own checks ran over main with #296; the content is unchanged. Brett Heap's word, of 2026-10-20T22:5xZ: "merge all when green for all merges thru v1.1.0. use this for all lanes" (#228 6303614082).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant