Skip to content
Merged
58 changes: 52 additions & 6 deletions docs/README-lanes.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,11 @@ grep -n 'my-lane' LANES.md
LANES_LANE=my-lane ./lanes-edit.sh commit "row rewritten by hand"
```

Every mutating subcommand does the same five things:
Every mutating subcommand FIRST resolves its `<lane>` — and `LANES_LANE` — to
the register row's own spelling, case-insensitively, before it reads or writes
anything, so a name typed in another case edits the row that is there instead of
creating a second one; two rows differing only by case are a refusal naming both
(Amendment 15). Then it does the same five things:

1. takes a `mkdir` lock in `.lanes/` (so two helper runs never interleave);
2. checks whether LANES.md is already dirty before making its own edit. If it
Expand Down Expand Up @@ -199,6 +203,34 @@ still called `claude` has no lane at all; and whether a name is free is
decided by whether the row's recorded session id is **live**, never by whether
a row exists.

**A lane name is ONE name under any case, and the register row's spelling is
canonical** (Amendment 15, in force 2026-09-14T00:07:18Z). `lane-start
openxfactory 2` and `lane-start openXfactory 2` are the same lane: every lookup
of a name — the row, the object log, the tmux window, the session name,
`LANES_LANE`, and every `<lane>` argument of the four commands — compares
case-insensitively, and the row's own spelling is what the window is renamed to,
what the launch is `--name`d with, what the log file is called, and what every
line and stamp writes. A name typed in another case RESOLVES to it and is never
written as typed. A lane with no row yet takes the checkout's REAL directory
name for its `<repo>` half, resolved under the projects root case-insensitively;
two directories differing only by case are a refusal naming both, because
choosing one of them is choosing which repository the lane is in. Where the
register already holds two rows differing only by case, **every writer refuses,
naming both**, until they are merged by hand — 15(d): the newer row's session
id(s) appended to the older row's session cell, in order, and the newer row
removed in the SAME commit, whose message names both spellings. `lanes-edit.sh
canon-lane <name>` is that resolver as a read, and the one all four commands
share: **0** with the canonical spelling (the typed one where no row matches, so
`add-row` can still create a lane), **2** for the pair, **64** usage — and that
2 is RELAYED by `lane-start`, `lane-end` and `restart` rather than carried past,
because the pair may be on `origin/<branch>` while this checkout still holds one
row. `add-row` asks the published register as well as this copy of it for the
same reason: a row a peer pushed is a refusal here, naming it and the `git pull
--rebase` that settles it, and never a second row. The LOG is held to the same
rule — two files under `lanes/log/` whose names differ only by case are two logs
for one lane, and every read and every write under that name refuses, naming
both, until they are merged.

Honouring that by hand is five acts at one end of a lane and one at the other,
and this register records where they get dropped: rows added hours late (`ROW
ADDED LATE 2026-09-09T18:05Z — the lane worked from 00:10Z without a row, a
Expand Down Expand Up @@ -235,7 +267,9 @@ manual.
### `lane-start <repo> <n>`

1. derives `LANE=<repo>-<n>` and the lane's directory — `~/projects/<repo>`,
or `--dir <path>`;
or `--dir <path>`. Both halves resolve case-insensitively under Amendment
15: `<repo>` to the checkout's real directory name, and the lane to the
register row's own spelling, which then wins over everything typed;
2. **refuses unless it is already inside tmux.** It renames the window it is
run in and never creates a tmux session: the window is the lane;
3. **liveness check, before it takes the name — and it is always asked.** It
Expand Down Expand Up @@ -286,8 +320,11 @@ manual.
lane came from the swap *record* rather than from the window name or a flag;
4. `tmux rename-window <LANE>` — which also turns automatic-rename **off** for
that window, so the name survives the next command it runs;
5. the row, through `lanes-edit.sh`: `add-row` when there is none — state
`STARTING`, session id `pending — set by the session's first act`,
5. the row, through `lanes-edit.sh`: `add-row` when there is none under ANY
case — it refuses one that exists under another and names it, and the
liveness check above reaches the RESUME branch for that row rather than this
one (Amendment 15(a)) — state `STARTING`, session id `pending — set by the
session's first act`,
`<workstation> / <profile> / <user>` per Rule 10, handoff path
`handoffs/<estate>/session-handoff-<date>-lane-<LANE>.md` — and
`append-row-status` when there is one. Never a hand edit. **That status is
Expand Down Expand Up @@ -415,7 +452,7 @@ This is the tooling half of that answer.

| what | where |
|---|---|
| a lane's object log | `lanes/log/<lane>.md` — one file per lane, append-only |
| a lane's object log | `lanes/log/<lane>.md` — one file per lane, append-only. `<lane>` is the REGISTER ROW'S spelling (Amendment 15); a file left under another case is found by every reader and RENAMED to that spelling once, by the first write that touches it, inside that write's own commit — so git records the rename beside the line appended. Two plain `mv`s through a temporary name, because on a case-insensitive filesystem `mv a A` answers *identical* and renames nothing |
| line 1 of each | `# lane <lane> — object log (lane-collision-protocol Amendment 7)` |
| every other line | one EVENT, in the grammar below |
| the alias table | two layers (Amendment 9(b)): the shipped `repos.tsv` installed beside the commands, then `<the checkout>/lanes/repos.tsv` where you keep an override. `alias<TAB>owner/repo`, case-insensitive. An override row replaces the shipped row for the same alias and adds rows it does not carry; an alias in neither is still a refusal saying to spell it `owner/repo` |
Expand Down Expand Up @@ -473,6 +510,15 @@ STARTED — lane openRepoProject-1, session 09dd34d1-3afe-43a1-88fc-c33c92c08088
`PAUSED`, `RESUMED`, `ENDED` and `RETIRED` carry the same `lane:<name>` object
and no payload.

**The `<lane>` field and the `lane:<name>` object are matched
case-insensitively, and a writer writes the register row's spelling into both**
(Amendment 15). A log is append-only and its lines carry whatever was typed on
the day it was written: `lanes/log/openxfactory-2.md` holds `lane
openXfactory-2` lines from 2026-09-02 beside the `lane openxfactory-2` lines of
the 23:51Z incident, in one file, about one lane. `who --lane`, `lane-end`'s
refusal and the `lanes` listing read both as that lane's; the old lines are
never rewritten.

**The object kinds in scope** are an issue or PR (`owner/repo#<n>`), an
OpenSpec change directory (`owner/repo:openspec/changes/<name>`), and — for
the lane-kind lines only — `lane:<name>`. `#<n>` is shorthand for the lane's
Expand Down Expand Up @@ -1374,7 +1420,7 @@ falls to its next rung there rather than refusing.
|---|---|
| `lanes-edit.sh lane-dir <lane>` | the `dir ` of the lane's **last** lane-kind line carrying one, unquoted where it was written quoted |
| `lanes-edit.sh lane-profile <lane>` | the same read one sub-field along — the `profile ` of that last line. An addition to the amendment's own table, so that `restart <lane>` can learn a profile with one `git show` instead of a listing that reads every log on the workstation |
| `lanes-edit.sh window-lane [<ws>] <@id>\|<session>:<index>` | the lane bound to a window **of the asking workstation** — the register row whose name is the window's name, else that workstation's swap record naming that ref |
| `lanes-edit.sh window-lane [<ws>] <@id>\|<session>:<index>` | the lane bound to a window **of the asking workstation** — the register row whose name is the window's name, else that workstation's swap record naming that ref. Both rungs answer in the **register row's own spelling** (Amendment 15): what this prints is renamed to, `--name`d with and typed as `restart <lane>` |
| `lanes-edit.sh session-lane <uuid>` | the lane whose register row's **session cell** names that transcript uuid. Adoption act 0's; it is the read the `SessionStart` hook already made. **0** the lane · **8** no row's cell names it · **64** usage — like every other read in this table, by A11 Addendum 4 ruling 1, which corrects act 0's `2` · **2** a helper predating the read |
| `lanes-edit.sh last-session <lane>` | the lane's resume target: the last uuid in the published cell **whatever shape it is in**, and failing that the session of its last `PAUSED`/`RESUMED`. This is what `/restart`'s step 4 reads, by A11 Addendum 4 ruling 14, rather than clause (f)'s `register-row` alone — so a lane whose row was never stamped but whose log records the session it paused in still has a resume target |
| `lanes-edit.sh forks <lane>` | the **live forks** of the lane's transcript — never holders, and a defect to retire |
Expand Down
69 changes: 67 additions & 2 deletions lane-end
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,41 @@ if [ -z "${LANES_FILE:-}" ]; then die "$(lanes_workspace_why)" 1; fi
[ -f "$LANES_FILE" ] || die "no register at $LANES_FILE — the workspace repository $(lanes_ws_yaml) names carries no lanes/LANES.md. \`openRepoTools wip init\` seeds one (it is idempotent); or set LANES_FILE" 1
[ -x "$LANES_EDIT" ] || die "no register writer at $LANES_EDIT — \`lanes-edit.sh\` is installed beside this command by \`openRepoTools --install\`. Run that, or set LANES_EDIT" 1

# ------------------------- AMENDMENT 15: THE ROW'S SPELLING IS THE LANE'S NAME
#
# **A lane name is one name under any case** (ratified 2026-09-14T00:07:18Z),
# and `lanes-edit.sh canon-lane` is the ONE resolver all four commands share —
# for the reason Amendment 11 clause (h) gives every other read here: four
# implementations of "which row is this name's" is how the four would come to
# disagree about it.
#
# `2` IS TWO ANSWERS AND ONE OF THEM IS A REFUSAL (Copilot round 4 on
# openRepoTools#41). It is what a helper predating this amendment spends on an
# unknown subcommand, and it is also `canon-lane`'s own two-rows refusal. Both
# used to carry on with the name as typed, on the reading that this command's own
# row scan sees the pair anyway — and it does, in `$LANES_FILE`, THE WORKING
# TREE. The register the refusal is about is `origin/<branch>`: on a checkout
# that is behind, the pair is published, the local scan finds exactly one row and
# passes, and the only other place the refusal could still surface is the
# `register-row` read below, whose stderr is suppressed and whose 2 becomes a
# generic exit 1 naming neither spelling. So the refusal is identified by its own
# words — no helper predating the amendment emits them from an unknown
# subcommand — and relayed, with the old-helper fallback left as it was.

cl_out=""; cl_rc=0
cl_out="$("$LANES_EDIT" canon-lane "$LANE" 2>/dev/null)" || cl_rc=$?
cl_why=""; cl_pair=0
[ "$cl_rc" = 2 ] && cl_why="$(LANES_NO_FETCH=1 "$LANES_EDIT" canon-lane "$LANE" 2>&1 >/dev/null || :)"
case "$cl_why" in *"Amendment 15(d)"*) cl_pair=1 ;; esac
if [ "$cl_pair" = 1 ]; then
printf '%s\n' "$cl_why" >&2
die "the PUBLISHED register spells lane $LANE two ways (above), so no read and no write under that name is unambiguous and this command will not end a lane it cannot name — Amendment 15(d): merge the newer row's session id(s) into the older row's session cell, in order, remove the newer row in the SAME commit naming both spellings, push, and re-run. The row is untouched and nothing was written." 2
fi
if [ "$cl_rc" = 0 ] && [ -n "$cl_out" ] && [ "$cl_out" != "$LANE" ]; then
note "lane $LANE → $cl_out (Amendment 15: the register row's spelling is canonical)"
LANE="$cl_out"
fi

# `--home` IS VALIDATED BEFORE ANYTHING IS WRITTEN. It is for a PRE-CUTOVER
# lane, and `lanes-edit.sh` refuses it once the lane's own log records a home —
# but the only place this script passes it is its LAST act, the ENDED/RETIRED
Expand Down Expand Up @@ -617,12 +652,33 @@ fi

# ---------------------------------------------------------------- the row

# AMENDMENT 15 — `tolower` ON BOTH SIDES, exactly as `lane-start`'s function of
# this name now compares and as `row_line` compares one file over. The resolver
# above has usually already made `$LANE` the row's own spelling, and this is
# what happens when it could NOT: a helper predating `canon-lane` spends 2 on
# the unknown subcommand, and a register holding 15(d)'s pair spends 2 as well.
# Byte for byte, `lane-end openxfactory-2` on the lane `openXfactory-2` then
# reads as NO ROW — and the refusal's own remedy is `add-row`, which is the
# 23:51:56Z incident performed by the other end of the lane. (`add-row` refuses
# it, so the register is safe either way; the message was the defect.)
rows_for_lane() {
awk -v lane="$1" '
substr($0,1,1) == "|" {
p1 = index($0, "`"); if (p1 == 0) next
rest = substr($0, p1 + 1); p2 = index(rest, "`"); if (p2 == 0) next
if (substr(rest, 1, p2 - 1) == lane) print
if (tolower(substr(rest, 1, p2 - 1)) == tolower(lane)) print
}' "$LANES_FILE"
}
# The SPELLINGS those rows carry, for a refusal that can name them — two rows
# for one lane may now be two spellings of one name, and a person sent looking
# for a duplicate that is not there to find loses the time twice.
row_names_for_lane() {
awk -v lane="$1" '
substr($0,1,1) == "|" {
p1 = index($0, "`"); if (p1 == 0) next
rest = substr($0, p1 + 1); p2 = index(rest, "`"); if (p2 == 0) next
t = substr(rest, 1, p2 - 1)
if (tolower(t) == tolower(lane)) print t
}' "$LANES_FILE"
}

Expand All @@ -633,7 +689,16 @@ row_count=0
case "$row_count" in
0) die "the register has no row for lane $LANE — nothing to end. If the lane predates the helpers, add its row first: lanes-edit.sh add-row \"| \`$LANE\` | … |\"" 2 ;;
1) : ;;
*) die "the register has $row_count rows for lane $LANE, and lanes-edit.sh can edit neither. Merge them into one row by hand in $LANES_FILE, then re-run" 2 ;;
*) rfl_names="$(row_names_for_lane "$LANE" | tr '\n' ' ')"
rfl_distinct="$(row_names_for_lane "$LANE" | LC_ALL=C sort -u | grep -c . || :)"
rfl_why="and lanes-edit.sh can edit neither. Merge them into one row by hand in $LANES_FILE, then re-run"
if [ "$rfl_distinct" -gt 1 ]; then
# BRACED: bash decides where a NAME ends with a locale-dependent
# `isalnum()`, and bash 3.2 on Darwin reads the em dash's three bytes as
# part of it (A9 Addendum 4, R-A9-11).
rfl_why="spelled ${rfl_names}— a lane name is ONE name under any case (Amendment 15), so those are two rows for ONE lane, and lanes-edit.sh can edit neither. Merge them into one row by hand in $LANES_FILE — Amendment 15(d): append the newer row's session id(s) to the older row's session cell, in order, and remove the newer row in the SAME commit, whose message names both spellings — then re-run"
fi
die "the register has $row_count rows for lane $LANE, $rfl_why" 2 ;;
esac

# The state cell is the row's SEVENTH column — awk field 8, because $1 is the
Expand Down
Loading
Loading