Skip to content

--install writes a receipt of the files it placed, and a retirement reads that before it reads a header substring - #103

Merged
brettheap merged 14 commits into
mainfrom
feat/install-receipt
Oct 3, 2026
Merged

brettheap merged 14 commits into
mainfrom
feat/install-receipt

Conversation

@brettheap

@brettheap brettheap commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

What this is

openRepoTools --install retires a word by REMOVING the copy it once placed
(retire_commands, landed in #45), and the only evidence it had that a file at
~/.local/bin/<name> is one it wrote was a CONTENT SUBSTRING: the sentence
Installed on PATH by + this installer's own name, which its own commands
carry in their header. Copilot's round-3 review of #45 (openRepoTools:263)
named that — "a user-owned restart script copied from an old installation or
containing the same banner will satisfy grep and be deleted on the next
install"
— and it was DECLINED for restart, because no installer that ever
placed restart wrote a receipt and the marker is therefore the only evidence
that exists for the copies on people's PATHs today.

But every retirement after this one can have better, which is #57. So
--install now writes down what it placed, and a later retirement reads that
first.

What changes

  • The receipt. ${OPENREPOTOOLS_DATA_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/openRepoTools}/installed.tsv,
    mode 0600, one row per REGULAR FILE this run placed:
    name<TAB>destination<TAB>sha256<TAB>UTC. Written whole through mktemp in
    the same directory + mv -f, which is the shape place_skill_and_hook
    already writes settings.json with, minus its chmod: the 0600 is the one
    mktemp gives the temporary, and there is deliberately no mode stamp. --install's mode-stamp targets prove -w, not that a chmod will succeed (ownership) #48
    rules that every chmod this command performs fails through die, and --install writes a receipt of what it placed, so a later retirement has ownership evidence stronger than a header substring #57
    rules that a receipt it cannot write is a note and never a refused install;
    a stamp that may neither die nor fall silent is one that must not exist, and
    0600 wants none (one account's evidence, read only by that account's own
    retirements). The code, test_install_writes_a_receipt_of_every_file_it_placed,
    --help and the README all say 0600.
  • It covers every placed regular file, not only the thirteen in the bin
    directory: the SKILL.mds and the command files are regular files this run
    placed too. The two hook entries get no row — an entry inside
    somebody else's JSON file is not a file this command placed — and the receipt
    carries no row for itself, so it is not an extra artifact: today that is
    27 artifacts, 25 of them files, 25 rows (13 + 6 + 6 since Amendment 16 added
    lane-rename), and the tests derive the count rather than pin it.
  • A row's subject is its DESTINATION, not its name. The lane's decision was
    "rows for names placed this run replace that name's earlier row"; the name is
    not unique once the skills and the command files are covered (handoff is a
    skill at two paths AND a command file at two more), and retire_commands
    looks a path up rather than a name. So the key is the path, the name column
    is the word --install PRINTS beside that placement (park, handoff,
    /ctx), and a run replaces the row of every destination it placed and keeps
    every other row — which also means moving $OPENREPOTOOLS_BIN_DIR does not
    throw away the evidence for the copies still in the old directory.
  • retire_commands consults it FIRST. A row whose sha256 still matches the
    file on disk is this installer's copy beyond argument → removed, and its row
    dropped. A row whose sha256 has MOVED is a person's edit of an installed
    command → named, left, the rm printed for them, row kept — and that is true
    even where the header marker is still in the file, because the receipt is
    the stronger evidence and that is the whole point of --install writes a receipt of what it placed, so a later retirement has ownership evidence stronger than a header substring #57. No row at all is
    today's header-marker fallback, byte for byte, so nothing that works now
    stops working. A symlink is still refused outright, before the receipt is
    asked at all.
  • A receipt that cannot be written is a NOTE, never a refused install. One
    line on stdout: what it could not write, where, why, that the install itself
    is complete, and that a retirement falls back to the header. The exit belongs
    to the reason rather than to the line — a path it may not write names
    $OPENREPOTOOLS_DATA_DIR, a machine with neither digest tool is told that
    instead. Fail closed, out loud, carry on.
  • sha256sum, else shasum -a 256 — one helper, bash-3.2 clean, because a
    stock macOS ships no sha256sum. The file is fed on stdin rather than
    named as an operand: GNU sha256sum escapes a filename containing a
    backslash and prefixes the line with \, which would put a \ on the front
    of the digest for a bin directory somebody spelled with one.
  • A destination carrying a TAB or a NEWLINE writes NO receipt at all and
    says so, rather than a row that is silently two columns or two rows. A TSV
    has no escape, $OPENREPOTOOLS_BIN_DIR is a path a person chooses, and a
    receipt missing exactly the file a retirement will ask about is worse than
    none.
  • The receipt path is never written through a symlink. mv -f replaces the
    link itself, so an install over one would swap a person's link for a regular
    file; path_kind — the same helper both planners use — is asked first, and
    anything that is not a regular file there is named and left.
  • The receipt is only ever asked about a path this command computed. It is
    never read as a list of paths to remove, so a receipt somebody else authored
    can change one answer and can never name a file for removal.
  • usage() gains the paragraph (where it lives, what it is for) and both
    $OPENREPOTOOLS_DATA_DIR and $XDG_DATA_HOME in the variable block, which
    test_help_names_every_variable_it_reads now pins; the README gains the same
    in its install section and a row in its variable table (the README is 484 lines and the cap in
    tests/test_repo_hygiene.py moved 472 -> 484 on 2026-10-02 with a dated entry
    for --install writes a receipt of what it placed, so a later retirement has ownership evidence stronger than a header substring #57, after main itself had reached 472).

The decision on test_installing_twice_changes_nothing

"Changes nothing" is about the bin directory and the artifacts — the lines
each placement prints — and that test asserts exactly those, so it is untouched
and still honest. The receipt's UTC is the stamp of the run that RECORDED the
row and does change on a re-install; that is the honest reading of a column
written by a run that verified a file it did not rewrite, and it is held by
test_installing_twice_leaves_no_duplicate_rows, which pins that the rows
themselves (name, destination, digest) do not move and that no destination
gains a second row.

How it was tested

tests/run.sh tests/test_openrepotools_command.py tests/test_install_skill_and_hook.py tests/test_repo_hygiene.py
on Linux, and eight new cases in tests/test_openrepotools_command.py (seven
more from the 2026-10-02 fix round are listed at the end):

  1. test_install_writes_a_receipt_of_every_file_it_placed — one row per placed
    file (ARTIFACTS - HOOK_ENTRIES, derived from the three lists), each digest
    recomputed with hashlib against the bytes at that destination, each stamp
    parsed, mode 0600, and the line that says so.
  2. test_a_retirement_reads_the_receipt_before_the_header — a file with no
    marker at all
    is removed on the strength of a matching row, and its row is
    dropped.
  3. test_a_file_the_receipt_no_longer_recognises_is_named_and_left — a file
    with the marker whose row's digest has moved is named, left byte for
    byte, printed with its rm, and its row is kept.
  4. test_a_receipt_it_cannot_write_is_a_note_and_never_a_refusal — a regular
    file where the data directory goes (a mkdir -p no user can win, root
    included): exit 0, thirteen of thirteen placed, and the note naming the path,
    the reason and the fallback.
  5. test_a_receipt_that_is_a_symlink_is_not_written_through — the link and
    what it points at are both untouched.
  6. test_a_digest_it_cannot_take_is_a_note_too — sha256sum and shasum
    shims that exit 1, first on $PATH: the install still succeeds and no
    receipt is written.
  7. test_installing_twice_leaves_no_duplicate_rows.
  8. test_the_receipt_keeps_the_rows_of_a_directory_it_no_longer_writes.

The fourth arm's sibling — a machine with NEITHER sha256sum NOR shasum on
its PATH at all — is a one-line guard checked by hand against a $PATH built
without them (it names the missing tools rather than blaming the first file it
could not digest); building that $PATH inside a test would not survive the
macOS job, so the shim test above covers the same receipt_note path.

command_env now clears $OPENREPOTOOLS_DATA_DIR and $XDG_DATA_HOME, so
a developer who exports the latter — normal on a Linux desktop — cannot have
this suite writing a receipt into their real data directory however carefully
$HOME is redirected.

Merge note

The receipt is its own section of helper functions with ONE call line in
install_commands (between place_skill_and_hook and retire_commands) and a
four-arm if in retire_commands, so the hunks inside those two functions are
as small as they can be. Measured against the other branches touching this
file: #81 changes the INSTALLABLES= line and the counts around it — this
section is inserted after the blank line below that assignment, so the two
changed ranges are separated and do not overlap — and the sibling branch's
chmod lines in the install loop are untouched here.

Closes #57

Lane: openRepoTools-1

After the 2026-10-02 resume (lane openRepoTools-1)

🤖 Generated with Claude Code

Summary by Sourcery

Add receipt-based ownership tracking to --install so retirements can safely distinguish unchanged installed files from user-owned edits.

New Features:

  • Record every regular file placed by --install in a secure, atomically written receipt containing its destination, digest, and timestamp.
  • Use receipt evidence during retirement to remove only unchanged installed files while preserving modified or unverifiable files and retaining the existing header fallback when no receipt row exists.

Bug Fixes:

  • Prevent receipt data loss and unsafe removals when receipt files, paths, digests, or destination types cannot be read, verified, or represented safely.
  • Preserve receipt entries for files in directories no longer targeted and avoid duplicate or relative-path entries across repeated or relocated installations.

Enhancements:

  • Add clear install and retirement notes for receipt failures, missing digest tools, unreadable records, and unsupported paths.
  • Canonicalize receipt and destination paths and protect receipt symlinks from being overwritten.

Documentation:

  • Document the receipt location, format, permissions, and retirement behavior in the README and command help.

Tests:

  • Add comprehensive coverage for receipt creation, retirement precedence, digest failures, unreadable and unwritable receipts, symlink and TSV safeguards, repeated installs, relocated directories, and relative paths.

… reads that before it reads a header substring

#57. `retire_commands` retires a word by REMOVING the copy
this installer once placed, and the only evidence it had that a file at
`~/.local/bin/<name>` is one it wrote was a CONTENT SUBSTRING — the
`Installed on PATH by ` header its own commands carry. Copilot's round-3 review
of #45 (`openRepoTools:263`) named that: a person's own script copied from an
old installation carries the same banner and would be deleted. It was DECLINED
for `restart`, and the reason does not generalise — no installer that ever
placed `restart` wrote a receipt, so for the copies on people's PATHs today the
marker is the only evidence there is — but every retirement AFTER this one can
have better, which costs nothing except that a run write down what it did.

So `--install` writes one row per regular file it placed —
`name<TAB>destination<TAB>sha256<TAB>UTC` — to
`${OPENREPOTOOLS_DATA_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/openRepoTools}/installed.tsv`
at 0644, through `mktemp` in the same directory + `chmod` + `mv -f`, which is
the shape `place_skill_and_hook` already writes `settings.json` with. It covers
all twenty-four files rather than only the twelve in the bin directory: the six
`SKILL.md`s and the six command files are regular files this run placed too.
The two hook entries get no row, because an entry inside somebody else's JSON
file is not a file this command placed, and the receipt carries no row for
itself — it is not a twenty-seventh artifact.

A ROW'S SUBJECT IS ITS DESTINATION AND NOT ITS NAME. The name is not unique
once the skills and the command files are covered (`handoff` is a skill at two
paths and a command file at two more) and `retire_commands` looks a PATH up, so
the key is the path and the `name` column is the word `--install` prints beside
that placement. A run replaces the row of every destination it placed and keeps
every other row, which is also why moving `$OPENREPOTOOLS_BIN_DIR` does not
throw away the evidence for the copies still in the old one.

`retire_commands` asks the receipt first. A row whose digest still matches is
this installer's copy beyond argument and is removed, and its row goes with it;
a row whose digest has MOVED is a person's edit of an installed command, named
and left with the `rm` printed for them — and that is true even where the
header marker is still in the file, because the receipt is the stronger
evidence and that is the whole of what #57 asks for. No row at all is today's
header-marker fallback byte for byte, so nothing that works now stops working,
and a symlink is still refused outright before the receipt is asked at all.

A RECEIPT THAT CANNOT BE WRITTEN IS A NOTE AND NEVER A REFUSED INSTALL: one
line saying what it could not write, where, why, that the install itself is
complete, and that a retirement falls back to the header. The digest is
`sha256sum` else `shasum -a 256`, because a stock macOS ships no `sha256sum`,
and the file is fed on stdin rather than named as an operand because GNU
`sha256sum` escapes a filename carrying a backslash and puts a `\` on the front
of the line. A destination carrying a TAB or a NEWLINE writes no receipt at all
and says so, rather than a row that is silently two columns or two rows; and
the receipt path is never written through a symlink, because `mv -f` replaces
the link itself.

THE UTC IS THE STAMP OF THE RUN THAT RECORDED THE ROW, which does change on a
re-install. `test_installing_twice_changes_nothing` is about the bin directory
and the artifacts — the lines each placement prints — and is untouched and
still honest; `test_installing_twice_leaves_no_duplicate_rows` is what pins
that the rows themselves do not move and that no destination gains a second
row.

Seven cases in `tests/test_openrepotools_command.py` cover the receipt's shape
and digests, a retirement by receipt of a file carrying no marker, a file
carrying the marker whose digest has moved being named and left, an unwritable
receipt directory being a note beside a successful install, a symlinked receipt
being left alone, the absence of duplicate rows, and the rows of a bin
directory this run no longer writes being kept. `command_env` clears
`$XDG_DATA_HOME` as well as `$OPENREPOTOOLS_DATA_DIR`, so a developer who
exports the former cannot have the suite writing into their real data
directory. `usage()` and the README say where the receipt lives and what it is
for, and name the new variable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 16, 2026 05:49

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 1 day and 20 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@sourcery-ai

sourcery-ai Bot commented Sep 16, 2026

Copy link
Copy Markdown

Reviewer's Guide

The installer now atomically records SHA-256 ownership receipts for all regular files it places, while retirement trusts matching receipt evidence before falling back to the legacy header marker; failures remain non-fatal, behavior is documented, and comprehensive tests cover precedence, portability, safety, and retention.

Sequence diagram for receipt-backed installation and retirement

sequenceDiagram
    participant User
    participant Installer as openRepoTools
    participant Receipt as installed.tsv
    participant Filesystem

    User->>Installer: --install
    Installer->>Filesystem: Place regular files
    Installer->>Installer: sha256sum or shasum -a 256
    Installer->>Receipt: Write complete receipt via temp file, chmod, mv -f
    Installer-->>User: Installation complete

    User->>Installer: --install word
    Installer->>Filesystem: Check computed destination
    Installer->>Receipt: Look up destination row first
    alt Receipt digest matches
        Installer->>Filesystem: Remove installed copy
        Installer->>Receipt: Drop row
    else Receipt digest differs
        Installer-->>User: Name edited file and print rm
    else No receipt row
        Installer->>Filesystem: Check Installed on PATH by header
        Installer-->>User: Apply legacy retirement behavior
    end
Loading

Flow diagram for safe receipt writing

flowchart TD
    A[Install places regular files] --> B{Receipt path is regular or absent?}
    B -- No, symlink or other object --> C[Print non-fatal receipt note]
    B -- Yes --> D{Destination paths contain TAB or NEWLINE?}
    D -- Yes --> C
    D -- No --> E{sha256sum or shasum available and succeeds?}
    E -- No --> C
    E -- Yes --> F[Write rows to temporary file]
    F --> G[chmod 0644]
    G --> H[mv -f into installed.tsv]
    C --> I[Keep installation successful]
    H --> I
Loading

File-Level Changes

Change Details Files
Add atomic, destination-keyed installation receipts covering every regular file placed by an install.
  • Resolve the receipt under the configurable data directory.
  • Record destination, printed name, SHA-256 digest, and UTC timestamp for each placed regular file.
  • Replace receipt contents atomically with mode 0644, preserving rows for destinations not placed by the current run.
  • Skip hook entries and the receipt itself.
openRepoTools
Use receipt ownership evidence before the existing header-marker fallback during retirement.
  • Remove a file only when its receipt digest still matches.
  • Leave edited files in place, retain their rows, and print a removal command.
  • Drop rows for successfully retired files and restrict receipt lookups to computed destinations.
  • Refuse to write through a symlink and retain the existing symlink protection.
openRepoTools
Make receipt failures non-blocking and portable across digest-tool environments.
  • Fall back from sha256sum to shasum -a 256 using stdin.
  • Emit an explanatory note and continue installation when receipt storage or digesting fails.
  • Decline to write receipts for destinations containing tabs or newlines.
openRepoTools
Document the receipt configuration and retirement behavior.
  • Describe receipt format, scope, atomic replacement, and fallback behavior in the install documentation.
  • Document OPENREPOTOOLS_DATA_DIR and expose both data-directory variables in help output.
README.md
openRepoTools
Expand test coverage for receipt creation, precedence, failure modes, and row retention.
  • Validate row count, destinations, names, digests, timestamps, permissions, and output.
  • Cover matching and changed digests, unwritable or symlinked receipts, unavailable digest tools, duplicate prevention, and moved bin directories.
  • Clear receipt-related environment variables from test subprocesses and pin help-variable coverage.
tests/test_openrepotools_command.py

Assessment against linked issues

Issue Objective Addressed Explanation
#57 Have --install write durable ownership receipts recording each regular file it places, including its destination, SHA-256 digest, and UTC timestamp, with a documented data-directory choice and safe atomic replacement. ✅
#57 Make retirement consult receipt evidence before the legacy header marker: remove files whose recorded digest still matches, leave and report edited files whose digest differs, and retain the header-marker fallback for files without receipt rows. ✅
#57 Ensure receipt failures do not refuse installation, avoid writing through symlinks or producing malformed records, and add coverage and documentation for the receipt behavior and failure cases. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Receipt error handling, concurrent updates, and deletion/update consistency have unresolved issues.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds installation receipts so retirements can distinguish unchanged installer files from edited or user-owned files.

Changes:

  • Records destination, digest, and timestamp for installed files.
  • Uses receipts during retirement with header fallback.
  • Adds tests and documents receipt storage and configuration.
File summaries
File Summary
tests/test_openrepotools_command.py Tests receipt creation, retention, retirement, and failure handling.
README.md Documents receipt storage and related variables.
openRepoTools Implements receipt creation, merging, verification, and retirement integration.
Review details

Suppressed comments (7)

README.md:289

  • The receipt is TSV, but this documentation shows spaces between fields. Destinations may contain spaces, so this can lead readers to produce or interpret invalid receipt rows; document the tab separators explicitly.
`<name> <destination> <sha256> <UTC>` per regular file, at

openRepoTools:574

  • When a receipt row exists but sha256_of cannot read the file or run the digest tool, this branch returns unknown, and retire_commands then falls through to the header-marker fallback. That can delete an edited retired script carrying the marker after the digest tools disappear, even though the receipt explicitly owns the path; the fallback should be limited to a missing receipt/row, while an existing but unverifiable row is left in place and reported as unverifiable.
		disk="$(sha256_of "$1")" || { printf 'unknown\n'; return 0; }

openRepoTools:456

  • If an existing receipt is a regular file but cannot be read, the input redirection on this while fails and the function still reaches its unconditional return 0. Because both callers invoke it in an || context, set -e will not stop that path, so receipt_record can overwrite the unreadable receipt with only this run's rows and silently discard the old-directory evidence it promises to keep. Propagate the loop/redirection failure so the old receipt is left untouched and the install emits the existing note.
	done <"$1"

openRepoTools:535

  • The receipt merge is a read/modify/write with no lock: two concurrent installs (for example, targeting different OPENREPOTOOLS_BIN_DIR values) can both read the same old receipt, stage their own rows, and let the last mv overwrite the other's rows. The atomic rename prevents a torn file but not this lost update, so the promised evidence for one directory can disappear. Serialize the full receipt merge (and the retirement-side update) on a lock in the data directory.
	# THIS RUN'S ROWS FIRST, THEN EVERY EARLIER ROW IT DID NOT REPLACE.
	receipt_rows_except "$file" "$RECEIPT_DESTS" "$RECEIPT_ROWS" ||
		{ receipt_note "$file" "the rows already there could not be read"; return 0; }
	if receipt_replace "$RECEIPT_ROWS"; then

openRepoTools:140

  • This is a tab-separated file, but the help renders the fields as space-separated. Since destinations are allowed to contain spaces, this notation is ambiguous and does not describe a row that can actually be parsed; show the literal tab separators.
`<name> <destination> <sha256> <UTC>` each — at

openRepoTools:418

  • The new fail-closed branch for a destination containing a TAB or newline is not covered by the added tests. Since this is the safeguard that prevents malformed TSV rows, add a regression test using a bin/data path with each character and assert that installation succeeds without creating or replacing the receipt.
	case "$dest" in
	*"$RECEIPT_TAB"*|*"$RECEIPT_NEWLINE"*)
		RECEIPT_WHY="$dest has a tab or a newline in it and this file is tab-separated"
		return 1
		;;
	esac
	digest="$(sha256_of "$dest")" || {
		RECEIPT_WHY="$dest could not be digested"
		return 1

openRepoTools:454

  • Use command grep for this lookup. The exit status controls whether an older receipt row is retained, and this repository documents that its test/runtime harness can provide a shell function named grep with nonstandard status; a bare call can therefore keep rows that should be replaced (or vice versa).
		if grep -qFx -e "$dest" -- "$2"; then continue; fi
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread openRepoTools
…etirement's row-drop is silent

Copilot round 1 on #103, `openRepoTools:660`. `receipt_forget` runs AFTER the
`rm` it accompanies and returned 0 on three write failures of its own — staging
the kept rows, staging the destination, and reading the rows already there — so
a full disk or a data directory that went away in that instant left the receipt
naming a path this installer's copy had just left, with nothing on screen to say
so. Every one of those four steps now reaches one note, and the note says what
the stale row is and what clears it.

AND THE WINDOW CLOSES BY ITSELF, which is the half that does not depend on a
write succeeding: the merge no longer keeps a row whose file is not at that path
any more, so the next `--install` drops a stale row whatever happened during the
retirement — a `receipt_forget` that could not write, a kill between the `rm` and
the rewrite, or a receipt somebody hand-edited. It also stops the file growing
for ever with the bin directories and profile roots people delete. `-L` beside
`-e`, for the reason `retire_commands` gives one function down: `-e` is false for
a dangling symlink, and a link is something at that path whatever it points at.

`test_a_row_whose_file_is_gone_is_not_kept` holds the new half, and the rows of
the files that ARE there are asserted untouched beside it.

WHAT IS NOT CHANGED, and the reason. The finding's stated consequence — that a
later install could "classify a newly created file at that path as `placed` and
delete it when its bytes happen to match" — is a file whose sha256 EQUALS the
digest of the bytes this installer placed, which makes it that file restored
from a backup or an older install rather than an unrelated file of somebody's.
Removing it is the retirement doing its work, and catching an unrelated file
would take a sha256 collision. The receipt is also never read as a list of paths
to remove: `retire_commands` walks `RETIRED` against its own `$dir` and asks the
receipt one question about a target it already has in hand.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 16, 2026 06:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical and moderate findings remain in receipt cleanup, diagnostics, and concurrent receipt updates.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (6)

openRepoTools:691

  • If receipt_verdict returns unknown because the digest tool or receipt is unavailable, this branch still removes a marker-bearing file via the legacy fallback but never calls receipt_forget. A receipt row for that path therefore survives after the file is gone and can be consulted for a later file at the same destination; drop the row after this removal as well.
		elif [ ! -L "$target" ] && [ -f "$target" ] && grep -qF -e 'Installed on PATH by `openRepoTools --install`' -- "$target" 2>/dev/null; then
			rm -f -- "$target"
			say "$name: RETIRED, removed from $target (lane-collision-protocol Amendment 18 Addendum 2; \`lane <name>\` is that act now)"

openRepoTools:179

  • The help text gives the receipt default as $XDG_DATA_HOME/openRepoTools, but receipt_file() falls back to $HOME/.local/share when XDG_DATA_HOME is unset. This makes the documented default point at /openRepoTools or an unset variable instead of the actual per-user location; show the ${XDG_DATA_HOME:-$HOME/.local/share} fallback here.
  $OPENREPOTOOLS_DATA_DIR   where the receipt goes    (default
                            $XDG_DATA_HOME/openRepoTools)
  $XDG_DATA_HOME            the data root it falls back to (default ~/.local/share)

openRepoTools:413

  • The TAB/newline guard is the only protection against producing an invalid TSV, but none of the added tests exercises this branch. Since the PR explicitly promises a successful install with no receipt for such destinations, add a case with a OPENREPOTOOLS_BIN_DIR containing a TAB or newline and assert the note, installed artifacts, and absent receipt.
	*"$RECEIPT_TAB"*|*"$RECEIPT_NEWLINE"*)
		RECEIPT_WHY="$dest has a tab or a newline in it and this file is tab-separated"
		return 1

openRepoTools:412

  • dest is user-controlled and this branch explicitly handles destinations containing a newline, but it copies that value verbatim into RECEIPT_WHY. receipt_note is documented as a one-line diagnostic, so a bin/profile path containing LF produces a multi-line note and an ambiguous message. Avoid embedding the raw destination here (or escape control characters before reporting it).
		RECEIPT_WHY="$dest has a tab or a newline in it and this file is tab-separated"

openRepoTools:470

  • If an existing receipt is a regular file but cannot be read, the input redirection on this while fails. Because this function is called inside ||/&& lists, set -e is suppressed and the unconditional return 0 below treats that failure as success; receipt_record then replaces the receipt and silently discards the older rows instead of emitting its read-failure note. Propagate the loop status to the caller.
	done <"$1"

openRepoTools:549

  • The temp-file rename makes each replacement atomic, but the read/merge/write sequence is not serialized. Two concurrent installs (for example, one using each of two bin directories) can both read the same old receipt, stage their own rows, and let the last mv discard the other run's destinations, defeating the documented preservation of old-directory evidence. Serialize this merge (and the retirement rewrite) with a receipt lock.
	# THIS RUN'S ROWS FIRST, THEN EVERY EARLIER ROW IT DID NOT REPLACE.
	receipt_rows_except "$file" "$RECEIPT_DESTS" "$RECEIPT_ROWS" ||
		{ receipt_note "$file" "the rows already there could not be read"; return 0; }
	if receipt_replace "$RECEIPT_ROWS"; then
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread openRepoTools Outdated
Copilot AI review requested due to automatic review settings September 16, 2026 07:16
… is the same test the row was written under

Copilot round 2 on #103, `openRepoTools:468`. The rule round 1 added asked
`[ -e "$dest" ] || [ -L "$dest" ]` — "is there anything at all at that path" —
so a destination that had become a DIRECTORY, a socket, a FIFO or a SYMLINK kept
its row. A row is evidence about a regular file this installer placed, and where
one of those sits at that path the file the row is about is not there.

So the keep test is now `receipt_add`'s own write test, spelled the same way:
`[ -f "$dest" ] && [ ! -L "$dest" ]`. Two halves of one rule in one sentence
each rather than two different ones — which is the same argument
`unplaceable_kind` and `path_kind` are single implementations for, one function
group along.

`test_a_row_whose_path_is_no_longer_a_regular_file_is_not_kept` holds it for
both shapes a person actually makes, a directory and a symlink, and asserts the
twenty-four rows of the files that ARE there are untouched beside it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical receipt-update race and moderate correctness and safety issues remain.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (5)

openRepoTools:393

  • receipt_note interpolates the receipt path and reason verbatim. If a user-selected OPENREPOTOOLS_BIN_DIR contains a newline, the dest included in RECEIPT_WHY makes this supposedly single-line failure note span multiple physical lines and become ambiguous; escape these values or omit the raw path before printing.
	say "receipt: NOT written to $1 — $2. Everything above is installed; a later retirement falls back to the \`Installed on PATH by\` header, which is the evidence it has today."

openRepoTools:413

  • The TSV-safety branch is not exercised by the added tests: none runs --install with an OPENREPOTOOLS_BIN_DIR containing a tab or newline and asserts that no receipt is created. Add a parameterized case for both characters, since a regression here would produce rows that retirement cannot parse safely.
	*"$RECEIPT_TAB"*|*"$RECEIPT_NEWLINE"*)
		RECEIPT_WHY="$dest has a tab or a newline in it and this file is tab-separated"
		return 1

openRepoTools:425

  • receipt_add unconditionally appends to RECEIPT_DESTS and the rows file. The two placement loops can produce the same destination when the configurable roots overlap (for example CLAUDE_PROFILES_HOME=$HOME and CLAUDE_USER_DIR=$HOME/shared), so one physical SKILL.md or command file is recorded twice. That breaks the destination-key invariant and the no-duplicate-row guarantee; skip destinations already staged before hashing/appending.
	printf '%s\t%s\t%s\t%s\n' "$name" "$dest" "$digest" "$RECEIPT_STAMP" \
		>>"$RECEIPT_ROWS" || {
		RECEIPT_WHY="the rows could not be staged in $WORKDIR"
		return 1
	}
	printf '%s\n' "$dest" >>"$RECEIPT_DESTS" || {

openRepoTools:339

  • OPENREPOTOOLS_DATA_DIR is used verbatim here, so a relative value makes the receipt location depend on the caller's current directory. An install from A with OPENREPOTOOLS_DATA_DIR=state writes A/state/installed.tsv, while a later install from B reads B/state/installed.tsv and silently loses the ownership records (and falls back to the header during retirement). Normalize this configured path to an absolute path, or reject relative values, before recording and reading the receipt.
receipt_file() {
	printf '%s/installed.tsv\n' \
		"${OPENREPOTOOLS_DATA_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/openRepoTools}"
}

openRepoTools:508

  • The receipt records $OPENREPOTOOLS_BIN_DIR/$name without normalizing it. Relative bin directories are accepted by the existing planner, so the same bin/park row refers to different physical files after running --install from another working directory; the merge then replaces the old row and loses evidence for the first directory. Normalize the bin destination (and use the same canonical form in retirement), or reject relative bin-directory values.
	local dir="${OPENREPOTOOLS_BIN_DIR:-$HOME/.local/bin}"
	local file="" kind="" name="" target="" why=""
	file="$(receipt_file)"
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread openRepoTools

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

One critical, two moderate, and two nit findings remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (4)

README.md:297

  • This describes the receipt fields with spaces, but the implementation is a TSV and explicitly rejects tabs/newlines in destinations. That notation is ambiguous for anyone consuming installed.tsv; use the repository's established <TAB> notation so the on-disk format is documented accurately.
`<name> <destination> <sha256> <UTC>` per regular file, at

openRepoTools:412

  • When OPENREPOTOOLS_BIN_DIR contains a newline, this embeds that raw newline in RECEIPT_WHY, and receipt_note prints it verbatim. The documented one-line receipt failure note is therefore split into multiple lines for one of the explicitly rejected TSV destinations; use an escaped/display-safe path or a fixed reason instead.
		RECEIPT_WHY="$dest has a tab or a newline in it and this file is tab-separated"

openRepoTools:338

  • The new ${XDG_DATA_HOME:-...} branch is not exercised by the receipt tests: command_env removes XDG_DATA_HOME for every run and receipt_path() always asserts the $HOME/.local/share default. Add a case with OPENREPOTOOLS_DATA_DIR unset and XDG_DATA_HOME set, so a regression in the documented XDG location or its precedence cannot pass unnoticed.
		"${OPENREPOTOOLS_DATA_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/openRepoTools}"

openRepoTools:140

  • The usage text has the same format ambiguity: it shows spaces between fields even though installed.tsv is tab-separated and destinations containing tabs/newlines are rejected. Document the literal tab separators here, matching the established <TAB> notation used throughout this repository.
`<name> <destination> <sha256> <UTC>` each — at
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread openRepoTools
…s rule that every mode stamp fails through die and #57's rule that a receipt failure is a note both hold

Merging main c1531eb (PR #102) into this branch brought `test_every_mode_stamp_in_this_command_fails_through_die`, which reads the script's text and holds every `chmod` to `|| die`. The receipt's `chmod 644 "$tmp" … || { rm -f; return 1; }` was the one stamp that could not obey it: #57 rules that a receipt this command cannot write is a note and never a refused install. A stamp that may neither die nor fall silent is a stamp that must not exist. The temporary `mktemp` makes is 0600 whatever the umask, which is `settings.json`'s mode too and enough for one account's evidence read by that account's own retirements, so the `chmod` is removed and the test asserts 0600 with both rules named.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 16, 2026 11:05

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved receipt permission and failure-handling issues could cause unsafe deletion or lost receipt data.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (5)

openRepoTools:485

  • The implementation deliberately leaves the receipt at mktemp's 0600 mode, but the PR contract and the updated README document this file as 0644, while the new test also enforces 0600. These surfaces currently describe and verify different behavior; please make the mode choice consistent, including the non-fatal failure handling required if a mode change is made.
# AND NO `chmod`: the receipt's mode is the 0600 `mktemp` gives the temporary,
# whatever the umask, which is `settings.json`'s mode too. Two rules meet here
# and this is how both are kept. #48 (PR #102) rules that EVERY `chmod` this
# command performs fails through `die` — a mode-stamp failure is a refusal —

openRepoTools:598

  • When a matching receipt row exists but sha256_of cannot run (for example, both digest tools are missing or a tool temporarily fails), this returns unknown, and retire_commands then falls through to the header-marker removal arm. That can delete an edited file carrying the marker even though the receipt is present and the digest was never shown to match; the header fallback should apply only when no matching row exists, while an unverifiable row should be left/named or otherwise fail closed.
		disk="$(sha256_of "$1")" || { printf 'unknown\n'; return 0; }

openRepoTools:623

  • This second unguarded workdir call has the same failure mode after the retired file has already been removed: a temporary-directory failure exits the install without reporting that the receipt row could not be dropped. Receipt bookkeeping must remain a non-fatal diagnostic even on the retirement path.
	workdir

openRepoTools:531

  • Receipt failures are supposed to be non-fatal, but workdir is unguarded here. If mktemp -d fails (for example because the temporary filesystem is unavailable), set -e aborts the install after the artifacts are already placed and no receipt_note is emitted. Route this failure through the same note-and-continue path.
	workdir

openRepoTools:476

  • The caller relies on this helper returning nonzero when the existing receipt cannot be read, but the unconditional return 0 masks a failed input redirection. If installed.tsv is unreadable, the loop is skipped and the staged receipt can overwrite it with only the current rows (and receipt_forget can discard surviving rows) instead of taking the failure-note path. Propagate the loop/redirection failure before replacing the receipt.
	done <"$1"
	return 0
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread README.md Outdated
brettheap and others added 2 commits October 2, 2026 20:26
…es to ten, its stale "0644" corrected to the 0600 the receipt is born at, and the README cap raised 472 -> 484 with a dated entry for #57 saying what the twelve lines buy

The paragraph carried the twenty-six/twenty-four artifact arithmetic and the
list of ways a receipt cannot be written; it now keeps only the facts a person
meets: the row format and path, the mode, what a later `--install` does with a
matching, a moved and an absent row, rows keyed by destination, the two things
that get no row, and that an unwritable receipt is one line and never a
refused install.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings October 2, 2026 20:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread openRepoTools
Comment thread openRepoTools Outdated
Comment thread openRepoTools
Comment thread openRepoTools Outdated
Comment thread tests/test_openrepotools_command.py Outdated
@brettheap

Copy link
Copy Markdown
Contributor Author

Landing note from lane openRepoTools-1 (2026-10-02). CI on 26f1d95: tests (Linux, full suite incl. the shell suite), tests-no-submodule, tests-windows, guard-launch-mode, SonarCloud and Sourcery pass; tests-macos fails inside tests/test_lane_helpers.sh with 9 cases. That job is red on main itself: main's last macOS run (69bf48d, 2026-09-30) failed the same suite with 19 cases, and 7 of this PR's 9 are in that set. The other two (an answer wider than the shell's integers refuses with 2, …saying it is not in the range the question offered) are lane picker cases; main's #135/#136 landed after that run with no macOS run of their own. This branch changes only the installer, README.md, tests/test_openrepotools_command.py and the README cap in tests/test_repo_hygiene.py — none of the lane helpers or their suite — so it cannot have introduced them. The macOS state belongs to the suite's owner (see #77). A fix round for Copilot's six threads of 2026-10-02 follows before the squash.

brettheap and others added 5 commits October 2, 2026 21:47
… on the strength of the header: `receipt_verdict` answers `unverified` when a row names the path and no digest could be taken, and the retirement keeps the header fallback for "no matching row" only (Copilot on #103 at `openRepoTools:711`, #57)

The first shape folded "no row" and "a row I cannot check" into one word, `unknown`, so on a host whose `sha256sum`/`shasum` was missing or failing the marker removed an edited, receipt-tracked `restart` — the ownership mistake the receipt exists to prevent. The new arm prints one line: the destination, that its row could not be checked because no digest tool answered, and the `rm -f --` for the person to run. Tested both with a `$PATH` that has neither tool at all (a symlink farm of the real one) and with shims that fail, and the receipt is asserted untouched.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…so a relative `$OPENREPOTOOLS_BIN_DIR` means the same file to the run that wrote a row and to a later run from any other directory (Copilot on #103 at `openRepoTools:524`, #57)

`receipt_dest` resolves a path's directory with `(cd -- "$dir" && pwd -P)` — no `realpath` on macOS or bash 3.2 — with `CDPATH` emptied and a trailing `X` so a directory name ending in a newline is not shortened past the TSV guard; `receipt_add` (record), `receipt_verdict` and `receipt_forget` all go through it, and the line a person reads still prints the path as they spelled it. `receipt_rows_except` no longer keeps a row whose destination is relative, because its `-f` answered for whichever directory the next run stood in. Two tests drive it from two working directories: an install with `bin` leaves only absolute rows and a run from elsewhere retires the right `restart`, and an old-shape `bin/restart` row can never remove another directory's file.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…NREPOTOOLS_BIN_DIR` with a TAB or a NEWLINE in its name installs everything, leaves the receipt byte for byte as it was, and prints the reason (Copilot on #103 at `openRepoTools:425`, #57)

The planner accepts both characters, so both are driven; the receipt is seeded with a row for a file that does not exist, which any rewrite would drop, and the data directory is asserted to hold nothing but `installed.tsv` so no temporary was left behind. Checked to have teeth by disabling the guard in a scratch edit, which turns both cases red on "the receipt was replaced".

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…-16 totals: a row goes to every placed regular file (the installables plus the skill and command files, with no row for the two hook entries or for the receipt), and the number is derived as `ARTIFACTS - HOOK_ENTRIES` and named as 27 artifacts and 25 rows only where it must appear (Copilot on #103 at `openRepoTools:325` and `tests/test_openrepotools_command.py:822`, #57)

`INSTALLABLES` has thirteen files since `lane-rename` (Amendment 16, #81), so "twenty-six artifacts, twenty-four of them files, twenty-four rows" and "twenty-four rows, not twenty-six" were each two short; the three other places that carried the old totals ("beside the twelve", "twelve of the twenty-four", "twelve more rows every run", "twenty-four plus one more bin directory") now say what they mean without a number that the next list change would strand again.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…test reads each of them, and the pull request description is corrected to match (Copilot on #103 at `openRepoTools:503`, #57)

The description said 0644 while the code, the test and the README said 0600 — on purpose: #48 rules that every `chmod` this command performs fails through `die` and #57 rules that a receipt it cannot write is a note, so a stamp that may neither die nor stay silent must not exist, and `mktemp`'s 0600 stands. The usage paragraph named no mode at all, so it now states it and the clause a row it cannot check is named and left; the README paragraph says the same in the ten lines it already had, so its 484-line cap does not move; and `test_the_help_and_the_readme_say_the_mode_the_receipt_is_born_at` holds both so a fifth place cannot drift from the four.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings October 2, 2026 22:10

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread openRepoTools
Comment thread openRepoTools
Comment thread openRepoTools
Comment thread openRepoTools
brettheap and others added 2 commits October 2, 2026 22:49
… the configured data directory with the same `pwd -P` the row destinations go through, every read, write and printed line uses that path, and a RELATIVE data directory is said out loud with where it resolved to (Copilot round 4 on #103 at `openRepoTools:365`, #57)

`receipt_file` handed the configured string to every caller, so a relative `$OPENREPOTOOLS_DATA_DIR` was `data/installed.tsv` in every line and every `[ -f ]`. It now goes through `receipt_dest`: canonical wherever the directory exists to resolve, the configured spelling where it does not (a getter that created directories would make a retirement's read a write), and `receipt_replace` makes the directory first and asks again, so a first install writes to and names the resolved path. What resolving cannot do is make `data` mean one directory from two working directories — a relative value names a different one from each — so `receipt_record` adds one line beside the one naming the receipt: the value is a RELATIVE path, where this run resolved it to, and to name an absolute one. The test installs from `here` with `data`, then runs from `there` with `../here/data` and checks the same receipt is read, the row is found, the file is removed and no second receipt appears anywhere.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…r a license for the header: `receipt_rows_except` fails instead of reporting a clean scan, so the install leaves the old receipt byte for byte and prints its "could not be read" note, and `receipt_verdict` answers `unreadable`, so the retirement names and leaves the file with the `rm` printed (Copilot round 4 on #103 at `openRepoTools:575` and `openRepoTools:716`, #57)

`receipt_rows_except` ended in an unconditional `return 0`, so a failed `done <"$1"` read as no rows and `receipt_record` replaced the receipt with this run's rows alone, losing every row about a copy it did not place; `receipt_verdict` read the same failure as `unknown`, which the header fallback answered by removing an edited file. Both now ask the question the loop is about to get — can the file be opened — before the loop; `unknown` is left for a scan that ran and found no row, and the new `unreadable` arm names the receipt and the destination. Two tests make the receipt mode 000 (skipped as root, the suite's own idiom): the install is whole, the note is printed, the seeded row survives byte for byte with no temporary left behind; and an edited, header-carrying `restart` is left intact with the line naming the unreadable receipt.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings October 2, 2026 22:55
@sonarqubecloud

sonarqubecloud Bot commented Oct 2, 2026

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Existing non-regular receipts can trigger the legacy fallback and potentially remove edited files.

Review effort: Lite
Findings: None

Resolved since last review (4)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Fail closed for non-regular receipt paths during retirement

openRepoTools:747

A receipt symlink (or directory) is treated as unknown here, so retire_commands falls through to the legacy header check. If installed.tsv is a user-owned symlink/directory and an edited retired file still carries the marker, this can delete that file even though the receipt path is unusable; the fail-closed unreadable arm only covers regular files that cannot be opened. Distinguish a truly absent receipt from any existing non-regular receipt and return unreadable so the target is named and left.

@brettheap
brettheap merged commit a44a4ed into main Oct 3, 2026
7 of 8 checks passed
brettheap added a commit that referenced this pull request Oct 3, 2026
…cap carries both dated entries, 472 -> 484 for #57 and 484 -> 486 for #64

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 3, 2026
…ve-the-listing

#83 (Amendment 18, one binding per lane) and this branch both added
columns to the end of `lanes-edit.sh lanes`'s row, and both changed what
column 10 says for a LIVE lane.

THE COLUMNS. #83 landed first, so its locality keeps column 13, and
Amendment 19's four move to 14-17 (class, started, head, flags) in every
reader: `lane_groups` (the class test was still reading $13, which is now
#83's `here`/`elsewhere`), `lane`'s split_row, `lanes`'s loop, the sweep's
row reader in `lane-end`, the manual's table, the field count (17) and
the eighteen `a19_field` cases. Column 13 is now printed `none` where no
binding stands rather than left empty: those three readers split the row
with a tab-IFS `read`, and a tab is IFS whitespace, so an empty 13 would
have handed the class to the variable named for the locality and shifted
every field after it. `lane_groups` files `none` exactly as it filed
empty (only `elsewhere` changes the group); #83's one assertion of the
empty value now asserts `none`.

COLUMN 10 FOR A LIVE ROW is #83's: the attach filled in (`tmux
switch-client`/`tmux attach -t <session>:<@id>`), rendered by `lanes` as
`attach:`. It supersedes this branch's `lane <name>` for the same row,
which was only ever there because the column said `none`; the two A19
assertions of that word now assert only that a LIVE row is never offered
a launch.

THE LIVENESS FIXTURE is #83's derived form (TIMEOUT_SECONDS + 600, read
out of the wrapper), which at this branch's 5400 is 6000. The wrapper's
pair test now accepts that form, proving the sed read finds the number
the wrapper runs with.

The rest is unions: both facts reads, both sections of the suite (A19
then A18, each restoring its own fixtures), both writers in the
workstation guard, both subcommand lists, and the AGENTS.md cap at the
merged count, 316.

Lane: openRepoTools-3
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 3, 2026
Bring in #119, #103, #83, #139 and #93 so #61 lands last in the queue
on the main it will actually land on. One textual conflict:

- lanes-edit.sh, the `*)` arm's unknown-subcommand list: main added
  `binding`, `request-handoff`, `register-row-local`, `retired-identity`
  (#83) and `retire-rows`, `archive-rows` (#93); this branch added
  `duplicate-holder`. Every name is kept, in main's order with
  `duplicate-holder` after `forks` as before, and the list still equals
  the dispatcher's arms exactly (test_repo_hygiene's derived check).

Checked against #61's code, nothing else needed a change: the suite's
fake-command block (tmux, claude, pgrep, ps, gh) is intact and no new
main code calls pgrep or ps; #93's columns 14-17 and #83's column 13
are in the `lanes` listing rows, which #61 never reads; LOG_AWK's
fields, live_holder's, transcript_holders' and lane_forks' output
formats are unchanged; #83's HANDOFF-REQUESTED is a lane-kind verb that
changes no state and holder_is_dead's five-verb whitelist skips it as
every last-line reader must; and lane-end's --retire-dormant sweep is
refused beside --retire and exits before the fork/duplicate retire path.

Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap
brettheap deleted the feat/install-receipt branch October 3, 2026 18:49
brettheap added a commit that referenced this pull request Oct 4, 2026
Bring #97 onto a current base before the seam rework Brett Heap ruled on
2026-10-04 ("managed ledger owns enrolled lanes; #97 owns legacy — rework
both"). Main moved 14 commits past 957a26f, to daed209: #81 lane-rename,
#83 Amendment 18, #93 Amendment 19, #61 claim --force takeover, #146,
#119, #101-#103, #129, #131, #133, #135, #136, #139 and #134.

Four files conflicted, and in every one both intents survive:

- lanes-edit.sh, the function sections: both sides appended after
  migrate_state_cells. Main's Amendment 19(c)/(d) sweep and archive stay
  directly under it, because its header says "the shape
  `migrate-state-cells` has one screen up"; #97's lifecycle and
  inventory section follows them.
- lanes-edit.sh, the unknown-subcommand refusal: the union of both lists,
  56 names, the same set as the dispatcher's 56 arms.
- lanes-edit.sh, the exit-code table, and the manual's copy of it: #97's
  7 row ("another act got there first", in three verbs) is kept, and exit
  9 now has two meanings, one per verb. #61 spent 9 on `claim --force`'s
  abandoned takeover and #97 spent it on `lane-state`'s unreadable
  snapshot, and each PR took it as unused. Neither verb can return the
  other's 9, so the table names both and nothing is renumbered in a
  merge. The manual's 9 row said "(`claim` only)", which the merge would
  have made false; its 7 row now names #97's two fenced writers too, as
  the code table already did. The next commit moves #97's 9 to 10.
- lane-handoff, the late-record restart line: #129's
  `$lane_profile_word` (lclaude) with #97's lifecycle line under it.
- docs/README-lanes.md: Amendment 19's section, then #97's #91 section,
  so the amendment sections stay in number order.
- tests/test_lane_helpers.sh: main's Amendment 19 and Amendment 18
  sections run first, in main's order (A18's restores its fixtures), and
  #97's self-contained #91 section follows them, before the
  workstation-seam section, as it did on #97's branch.

lane-start auto-merged around #134's rewrite. #97's section 4a still
prints the reconciliation after Amendment 18's binding gate and before
section 5 and the STARTED/RESUMED write in 5b. AGENTS.md, README.md,
tests/test_repo_hygiene.py and skills/ are byte-identical to main, so
the 316- and 486-line caps already match the merged counts.

Locally, tests/run.sh -k 'lane_helpers_suite or repo_hygiene or
lane_start_claude_current or guard_launch_mode or install_skill_and_hook':
274 passed, 600 deselected, in 1560 s.

Lane: openRepoTools-3
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.

--install writes a receipt of what it placed, so a later retirement has ownership evidence stronger than a header substring

2 participants