skill: make the spool the way lessons are added, and guard it - #3323
skill: make the spool the way lessons are added, and guard it#3323gHashTag wants to merge 4 commits into
Conversation
Refs #3236 Pass 117 added `tri skill add` / `fold` so two branches cannot choose the same section number. It shipped as a tool and a habit, and a habit is what already failed here -- the next pass reads what is convenient and reaches for `cat >> SKILL.md`. The collision itself is invisible on a branch: `tri skill check` passes on both sides and fails only on the merge result. The PRACTICE that causes it is not invisible. `tri skill fold` deletes one spool file for every section it appends; a direct append deletes nothing. That difference is in the diff. `tri skill spooled --gate` compares the section TITLES on base against the titles now, and allows as many new ones as there are spool files this branch deleted. By title rather than by number, because `tri skill renumber` rewrites every number and keeps every title -- a number comparison would report the whole file as new and fire hardest on the command whose job is avoiding collisions. By parsed section rather than by diff line, because a `+## N. ` line cannot be told from a heading QUOTED inside a fence, and 3 of the 518 such lines on master are quotations; miscounting one of those has already destroyed a real section here. A branch that runs `add` AND `fold` together still chooses its number against its own base, so it collides exactly like a direct append -- and it fails this guard, correctly, because the spool file it deleted was never on base. The flow the guard leaves open is: `add` on the branch, `fold` after the merge. Also writes the rule at the top of SKILL.md, where a pass looking for how to add a lesson will read it before reaching for the shell. 6 tests. Three mutants of the decision line killed (5, 3 and 3 failures).
Refs #3236 Three things, all in service of the guard actually running. 1. The gate itself, in cli-tri.yml beside `tri skill check`. That check finds a duplicate number; it cannot find the practice that makes one, because the collision is in neither branch. This asks the question a diff can answer. 2. `tri skill spooled` exits 2, not 0, when no skill file can be read on the base. This checkout is shallow, so `origin/master` does not resolve, every file reads as absent-on-base, the population empties, and a gate that never ran prints a pass. The step fetches the ref; the exit code is there for when something else stops it resolving. Control: `--base origin/no-such-branch` exits 2. 3. `skill_files()` reads the directory entry instead of joining "SKILL.md". 5 skill files are tracked and 2 are spelled `skill.md`, so the old form read 3 of 5 -- and on a case-insensitive filesystem it read the other two under a name git does not have, so `git show origin/master:<path>` failed and they looked NEW. Neither lowercase file carries a numbered heading, so the missing population is empty and no past check was wrong; the next numbered heading added to one of them would have been. `tri skill check` now reports 5 files and still exits 0. Census: `shell` run: steps 235 -> 236, the one step added above. Blessed here. Controls, all four run: - branch adding no section -> exit 0 - `## 999.` appended by hand -> UNSPOOLED, exit 1 - spool file on base, folded now -> exit 0 - the SAME two commits, base moved back before the spool file -> exit 1 The last is the sharpest: identical trees, opposite verdicts, which is the add-and-fold-in-one-branch case being refused. A branch that does both still picks its number against its own base and collides exactly like a direct append.
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
Refs #3236 The PR body described `skill_files()` reading the directory entry instead of joining "SKILL.md". The code was written, verified, and then destroyed before it was committed -- by a control in this same branch. The control needed two commits to test a fold against a base. It built them with `git add -A`, which swept in the uncommitted `skill_files` change, and cleaned up with `git reset --hard $BASE~1`, which threw that commit away. The controls all passed and the fix was gone. Only `git cat-file` on the pushed head found it: the description said one thing and the tree said another. The change itself, again: 5 skill files are tracked and 2 are spelled `skill.md`, so joining the uppercase name reads 3 of 5 -- and on a case-insensitive filesystem `join("SKILL.md").is_file()` is TRUE for a file actually named `skill.md`, so the path pushed is one git has never heard of, `git show origin/master:<path>` fails, and a tracked file reads as NEW. That is the quiet direction to be wrong in. Neither lowercase file carries a numbered heading, so the population that was missing is empty and no past check gave a wrong answer. That is luck, not design. Controls re-run on the restored code: all 5 paths the guard names now resolve in git (previously 2 did not), `skill check` 0, `skill refs` 0, `skill spooled --gate` 0, 795 tests 0 failed.
|
Correction, pushed as a fourth commit. The body above described the It surfaced only from Restored, controls re-run: all 5 paths the guard names now resolve in git, where previously 2 did not. |
Refs #3236 While building the guard in #3323, a control built two commits with `git add -A` and cleaned up with `git reset --hard $BASE~1`. That swept an uncommitted fix into the discarded commit. Every control passed, the PR body described the fix, and the branch did not carry it -- visible only from `git cat-file` on the pushed head. Second instance this session of a git command chosen for safety destroying work, after `git checkout HEAD@{1} -- <files>`. Both were cleanup steps, both exited zero, both were invisible until the tree was read on purpose.
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
PR DashboardGenerated at: 2026-09-05 18:58:37 UTC
Summary
Seal Status
|
Pull request was closed
The branch was green with auto-merge armed and BEHIND by nine commits, and `allow_update_branch` is false, so nothing but a session could move it. The claim mechanism it carries is the one both concurrent sessions need: this pass alone duplicated an issue already open (#3323 against #3333) and nearly retook a task a neighbour had finished. Two censuses moved and are re-blessed in this same commit, as the gate demands: `quiet` 127 -> 128 (one more path named but not quiet) and `shell` `run: steps` 234 -> 235, both from the nine commits being merged in, not from this branch's own work.
Refs #3236
Pass 117 shipped
tri skill add/tri skill foldso two branches cannot choosethe same section number. It shipped as a tool and a habit. A habit is what failed
here in the first place: the next pass reads what is convenient and reaches for
cat >> SKILL.md.The rule now lives at the top of SKILL.md, where a pass looking for how to add
a lesson reads it before reaching for the shell.
And it is checked. The collision is invisible on a branch —
tri skill checkpasses on both sides and fails only on the merge result — but the practice that
causes it is not.
tri skill folddeletes one spool file for every section itappends; a direct append deletes nothing.
tri skill spooled --gatecomparessection TITLES on base against titles now and allows as many new ones as there
are spool files this branch deleted.
tri skill renumberrewrites every number and keepsevery title, so a number comparison would report the whole file as new and fire
hardest on the command whose job is avoiding collisions.
+## N.line cannot be told from aheading quoted inside a fence. 3 of the 518 such lines on master are
quotations, and miscounting one has already destroyed a real section here.
A branch that runs
addandfoldtogether still picks its number againstits own base, so it collides exactly like a direct append — and it fails this
guard, because the spool file it deleted was never on base. The flow left open is
addon the branch,foldafter the merge.Two things found while building it
origin/masterdoes not resolve; every file would read absent-on-base, thepopulation would empty, and a gate that never ran would print a pass. The step
fetches the ref, and the command now exits 2 (COULD NOT RUN) rather than 0.
skill_files()read 3 of 5 tracked skill files. Two are spelledskill.md; on a case-insensitive filesystem the old code found them under aname git does not have. Both carry zero numbered headings, so the missing
population is empty and no past check was wrong — the next numbered heading
added to one of them would have been.
Verification
6 unit tests. Three mutants of the decision line killed (5, 3 and 3 failing
tests). Five controls:
## 999.appended by hand--base origin/no-such-branchskill check0 ·skill refs0 ·census pin --gate0 · 795 tests, 0 failed.Census
shellrun: steps 235 → 236, blessed in the same commit.