Uninstall must leave the repo as it found it
TASK-IMP-121: Uninstall must leave the repo as it found it
Summary
A sibling task, TASK-IMP-126, has shipped (status done) and made uninstall the inverse of install for three artifacts: the dangling skill symlinks across every agent family, the two dead MCP registrations, and the pre-commit hook's leading blank separator. This task is what 126 did NOT fix - the uninstall-completeness remainder - and it leads with a live DATA-LOSS bug 126 never touched.
uninstall.sh:141-142 rmdirs .agents/skills and then .agents for emptiness alone, with no ownership test, so an operator's PRE-EXISTING empty .agents/skills is destroyed on uninstall - reproduced end to end at HEAD, and .agents with it when CYBEROS_AGENTS filters the antigravity pointer off. Two smaller defects are byte-level and remain after 126: uninstall's .gitignore strip (:106, still the old sed) leaves the one separator byte install writes, and 126's shipped hook-strip - byte-exact for a newline-terminated hook - still adds one byte to a foreign hook that has no trailing newline. So a directory an operator owns does not come back at all, and two operator files do not come back byte-identical.
Problem
Measured at HEAD (payload built with build.sh; real install.sh/uninstall.sh; GNU awk 5.1.0, bash 5.1). TASK-IMP-126 already removed every dangling skill symlink (its §1.2, uninstall.sh:122-166), both MCP registrations (its §1.1, uninstall.sh:168-186), and the hook separator for a newline-terminated hook (its §1.3, the awk at uninstall.sh:87-93). Those three are OUT of this task. What is left is one data-loss bug and two byte-leaks.
1. A directory an operator owns is destroyed - the lead, and a live data-loss bug. uninstall.sh:141-142 runs rmdir "$root/.agents/skills" then rmdir "$root/.agents", each 2>/dev/null || true. Neither carries an ownership test; rmdir's refusal on a non-empty directory is the ONLY guard, and it does not fire when the directory is an operator's and happens to be empty. Reproduced end to end: a fresh git repo with an operator's pre-existing empty .agents/skills, default install (which drops three symlinks into it), then uninstall - the symlinks are removed and .agents/skills is rmdir'd GONE. In the default config .agents survives, but only incidentally: the :597 antigravity pointer writes .agents/rules/cyberos.md, no uninstall path removes it, so .agents is never empty and :142 no-ops. Filter antigravity off (CYBEROS_AGENTS=agents) and find .agents after uninstall returns nothing - the operator's .agents goes too. This is a casualty a byte-level harness that never seeds a pre-existing .agents/skills cannot see.
Install already records ownership, one directory too low. The discipline uninstall uses everywhere else is: read the artifact, decide from what it says. For a copy-fallback skill dir that discipline is a .cyberos-owned marker: install.sh:684 writes one into each .agents/skills/<cmd> copy dir, and uninstall.sh:130/:161 read it to remove only installer copies. But that marker sits on the CHILD skill dir ($_sdest, install.sh:662), never on the parent .agents/skills or on .agents - and in a default (symlink) install it is not written at all. So the two directories :141-142 can delete carry no proof of who created them, which is exactly why the rmdir is blind. The fix has two halves: install writes a parent marker when - and only when - install itself creates the directory; uninstall then removes those two dirs only when they carry that marker and are empty.
The create-test must precede the mkdir, and each level needs its own. Measured: mkdir -p on an already-existing directory exits 0 silently, so a marker written after mkdir -p "$d" would stamp an operator's pre-existing directory as ours - the current bug inverted, and worse, because it would manufacture a licence to delete. And mkdir -p a/b creates both levels, so a marker in .agents/skills proves nothing about .agents; compounding it, .agents has a SECOND creator at install.sh:575 (the :597 antigravity pointer, mkdir -p on dirname .agents/rules) which runs before :669's mkdir -p "$root/.agents/skills". Each directory must be tested for its own prior existence before the first line that could create it.
Two dirs get markers; five get their errant rmdir removed. grep -n rmdir uninstall.sh returns :141, :142, :165 and :226 (:226 is the BRAIN-restore tmpdir, unrelated). :141-142 are the .agents pair, which install CREATES and wants to reclaim cleanly, so they get the marker mechanism (§1.1-1.3): delete only what we proved we created. :165 is the per-family loop's rmdir "$root/$_sd" over the five native channel parents (.claude/skills, .grok/skills, .commandcode/skills, .codex/skills, .opencode/skill) - added by TASK-IMP-126 and absent pre-126, and reproduced destroying an operator's pre-existing empty .claude/skills end to end at HEAD. Those parents were never reclaimed pre-126, so the fix is not a marker but the REMOVAL of the blind rmdir (§1.6), restoring the pre-126 leave-in-place. A marker there would be a file nothing reads - a new permanent leftover; removing the rmdir is both smaller and the pre-126 status quo. So the same data-loss class (rmdir on emptiness with no ownership test) is closed at all four sites: the .agents pair by marker gate, the five native parents by deleting the rmdir IMP-126 added.
2. The .gitignore separator leak. install.sh:756 writes [ -s "$gi.cyberos.tmp" ] && printf '\n' before the managed block, and uninstall.sh:106's sed range starts AT the # >>> cyberos marker, so the separator above it survives. IMP-126 fixed the hook this way but not .gitignore. Measured by replaying real install+uninstall on a pre-existing newline-terminated .gitignore: 20 -> 21 bytes, holding at 21 across three cycles (install.sh:742-754's awk trims the trailing blank run on re-install, so the leak is one-time, not accumulating). Still a mutation of an operator's file, and the fix belongs to the strip.
3. The hook, only for content with no trailing newline. IMP-126's awk (uninstall.sh:87-93) is byte-exact for a foreign hook that ends in a newline (measured 34 -> 34), which is what its arm t_hook_strip_byte_identical_across_cycles proves. It is NOT byte-exact for a hook with no trailing newline: measured 33 -> 34 bytes (+1). Install's cat >> appends its leading separator directly after the last unterminated line, so there is no blank LINE for the awk to hold, and awk's print re-terminates that last surviving line. The information is not destroyed - install's append stays invertible - but only a byte-level strip (delete the block plus the one newline byte immediately before the # >>> marker) recovers it; the line-oriented awk cannot.
Proposed Solution
Four independent, testable changes, each grounded in a re-verified HEAD citation.
Gate the container rmdir on a parent marker install writes on creation (the data-loss fix). install.sh writes a .cyberos-owned marker into .agents/skills, and separately into .agents, when and only when install itself creates that directory - deciding creation by testing the directory's own existence BEFORE the mkdir -p that could create it (:669 for .agents/skills; :575-via-:597, which runs first, for .agents). uninstall.sh:141-142's unconditional rmdir is replaced by: remove the parent marker, then rmdir only if the directory now carries our marker and is empty; a directory with no marker is kept and reported as kept. Where install created the directory, uninstall takes it back and leaves nothing behind; where the operator's directory predated install, it survives. This is TASK-IMP-094's mechanism applied one directory up.
| directory | proof it is ours | consequence |
|---|---|---|
operator's pre-existing .agents/skills | no parent marker | kept + reported |
install-created .agents/skills | parent marker (§1.1) | marker removed, then rmdir if empty |
operator's pre-existing .agents | no parent marker | kept + reported |
install-created .agents (marker kept dir) | parent marker (§1.1) | marker removed even though the dir is kept (rules pointer survives) |
Delete the native-parent rmdir IMP-126 added (the other half of the data-loss close). uninstall.sh:165's per-family rmdir "$root/$_sd" removes an operator's pre-existing empty .claude/skills (and the other four native parents) once our managed link is cleared - the same blind-emptiness deletion as :141-142, introduced by IMP-126 and absent pre-126. Remove that one line (§1.6), so an emptied native channel dir is left in place like any other inert container. No marker is written or read there: pre-126 uninstall never reclaimed these, so leaving them is both correct and the status quo, and it keeps IMP-126's link removal intact.
Strip the .gitignore separator with the block, by the byte. The .gitignore strip must also delete the single \n byte install writes immediately before the # >>> cyberos marker (install.sh:756), restoring a pre-existing newline-terminated .gitignore byte-identical across any number of cycles. Same look-behind shape IMP-126 already uses for the hook, ported to the .gitignore strip.
Make the hook strip byte-exact for a hook with no trailing newline. Refine the strip so a foreign hook is byte-identical to its pre-install content even when that content has no trailing newline: delete the block together with the one separator byte and do not re-terminate the last surviving line. The newline-terminated case (already byte-exact, IMP-126) must stay byte-exact.
Alternatives Considered
- Fold this back into TASK-IMP-126. Impossible: 126 is
doneand shipped. This is the remainder task, and it leads with a defect 126's cone (symlinks/MCP/hook-core) never addressed. - Option A - never delete the parent (remove
:141-142outright). Safe and honest, but where install CREATED.agents/skillsit leaves an empty directory behind in a repo it promised to leave as it found it - a permanent false positive traded for a transient one. Rejected: measurement shows creation is knowable (install already writes a child marker), so the blanket refusal is not forced. - Option B - leave the bug, scope the clause around
:141-142. Rejected by the operator, and it fails on its own terms: a path present before install still goes missing, so re-scoping the clause hides the casualty rather than closing it. - Have install write a RECEIPT/manifest of what it created. Rejected in favour of the marker: the marker is local to the artifact, needs no parser, survives the operator moving the repo, and install ALREADY writes one at
:684. Both share the one hazard - a record that wrongly claims creation would delete an operator's dir - and both close it the same way: test existence BEFOREmkdir, never after. - Fix the
.gitignoreand hook leaks on the install side (drop the separator). Rejected for the same reason IMP-126 rejected it for the hook: the separator is a deliberate readability break in files an operator reads, and the defect is that uninstall does not undo what install does - the inverse belongs in uninstall, next to the strip that already exists. Rewriting install's.gitignoreawk (:742-754) is also a strictly larger change than the two-line strip refinement. - Trim trailing blank lines after the strip instead of consuming exactly one byte. Rejected: an operator hook or
.gitignorewhose pre-install content legitimately ends in blanks would come back with fewer. The correct rule removes exactly one byte immediately above the marker. - Mark all seven
rmdir'd dirs, not just the.agentspair. The five native channel parents ARErmdir'd - at:165, by IMP-126's per-family loop - so they are in the data-loss class too (the audit-caught correction). But marking them is the wrong fix: pre-126 uninstall never removed them, so the minimal correct close is to DELETE thermdirIMP-126 added (§1.6), not to add five markers that read to nothing and survive as new leftovers. Markers are reserved for the.agentspair, which install genuinely creates and wants to reclaim; the native parents just stop being pruned. - Effort 9 hours. install-side create-detection + the two parent markers (two dirs, two creation sites, pre-
mkdirtests) 1.5h; uninstall-side marker-gated prune + marker-removal sequencing 1h; delete the native-parentrmdirat:165(§1.6) 0.5h;.gitignorebyte-exact strip 1h; hook no-trailing-newline refinement 1h; six new suite arms (each a distinct fixture) 4h. Sum 9. Lower than the pre-126 draft's 12h because the symlink-loop rewrite, the MCP partition and the hook separator core all shipped with 126.
Success Metrics
- Primary: every container that existed BEFORE install still exists after uninstall - including an operator's pre-existing empty
.agents/skills,.agents, and any native channel dir (.claude/skills,.grok/skills,.commandcode/skills,.codex/skills,.opencode/skill) - and every.agents-pair container install created and marked is either removed with its marker or kept and reported. Baseline measured at HEAD: an operator's pre-existing empty.agents/skillsis DELETED by:141and their empty.claude/skillsby:165(both reproduced end to end); withCYBEROS_AGENTS=agentstheir.agentsis deleted by:142as well. - Primary: an operator's pre-existing newline-terminated
.gitignore, and a foreign pre-commit hook with no trailing newline, are byte-identical to their pre-install bytes after 1 and after 3 cycles, asserted withcmp. Baseline:.gitignore20 -> 21 (one-time); hook 33 -> 34 (one-time +1, stays 34 across cycles). - Guardrail: no
.cyberos-ownedfile exists anywhere under the repo after uninstall, including on an install-created.agentsthat is KEPT because its rules pointer survives. Baseline: not applicable today (install writes markers only into copy dirs uninstall removes wholesale); it becomes load-bearing the moment §1.1 writes markers into two dirs uninstall may keep. - Guardrail: the create-detection marks NO pre-existing directory - a marker on an operator's dir would satisfy the removal half while manufacturing the casualty this task exists to remove.
- Guardrail: the three IMP-126 artifacts stay fixed - zero surviving skill symlink targets naming
.cyberos/, zero cyberos MCP registrations, and a newline-terminated foreign hook still byte-identical across cycles. This task must not regress 126.
Scope
In scope: uninstall.sh:141-142's container rmdir, now gated on a parent .cyberos-owned marker with the marker removed before the rmdir; install.sh's create-detection and the two parent markers it writes (.agents/skills at :669; .agents at :575-via-:597); uninstall.sh:165's per-family rmdir over the five native channel parents, REMOVED so an operator's pre-existing empty native dir survives; uninstall.sh:106's .gitignore strip, made byte-exact for the separator; uninstall.sh:87-93's hook strip, made byte-exact for a no-trailing-newline hook; and new arms in test_install_hygiene.sh for each.
Out of scope / Non-Goals
- The three artifacts TASK-IMP-126 shipped, explicitly out. (a) The dangling skill symlinks across all agent families (IMP-126 §1.2,
uninstall.sh:122-166). (b) The two dead MCP registrations.mcp.json/.cursor/mcp.json(IMP-126 §1.1,uninstall.sh:168-186). (c) The pre-commit hook separator leak for a NEWLINE-TERMINATED foreign hook (IMP-126 §1.3). This task inherits (c)'s awk and only closes its no-trailing-newline remainder; it does not re-spec the separator core. - The child copy-fallback marker at
install.sh:684. It stays where it is and keeps proving the ENTRY copy is ours (IMP-094, read byuninstall.sh:130/:161). This task's parent marker is additional and distinct. .gitignoreshapes other than newline-terminated. A.gitignorewith no trailing newline, or one ending in a blank run, is collapsed to one post-install state byinstall.sh:742-754's awk BEFORE uninstall runs - a true pigeonhole no uninstall-side rule can invert. §1.4 is scoped to the shape where install is injective; closing the rest means changing install's rewrite, a larger change (§3).CYBEROS_COPY_SKILLS=1copies under the five native channels are entry-level and carry IMP-126's.cyberos-ownedmarker, removed by IMP-126's family loop; §1.6 changes only the parent-dirrmdir, not that entry cleanup, so folding entry-level copy policy in would change no deletion this task authorises.t22_uninstall_behavior_unchangedis NOT re-pointed. Measured self-relative (it compares the real uninstall against a summary-stripped copy of the same script) and immune to this task's edits, which sit before the:230summary anchor. Its baseline does not move and its name stays honest.- What uninstall keeps by design (
docs/tasks/,docs/status/,CHANGELOG.md, the agent/rules pointer files, BRAIN) and the$HOMEglobal skills behindCYBEROS_GLOBAL_SKILLS=1(outside the repo, outside the "leave the repo as it found it" contract).
Dependencies
depends_on TASK-IMP-106 (status done): 106 landed the uninstall summary, the _removed_list/_kept reporters, and the t22 arm on the same uninstall.sh + test_install_hygiene.sh surface this task edits, so 106's file state is the base these edits build on. The pre-126 justification for the dependency - "121 must re-point 106's t22" - is RETIRED: re-verified at HEAD, t22 is self-relative and unaffected by this task, so no re-point is authored. Shares surface with TASK-IMP-094 (the .cyberos-owned marker mechanism this task lifts one directory up) and TASK-IMP-126 (the sibling that shipped the symlink/MCP/hook-core; this task is its remainder).
AI Authorship Disclosure
- Tools used: Claude (Opus 4.8) running the CyberOS task-author skill inside Cowork.
- Scope: whole-document rewrite to a NARROWED scope after TASK-IMP-126 shipped. IMP-126 (
done) made uninstall the inverse of install for the skill symlinks, the MCP registrations, and the newline-terminated hook separator; those three are removed from this task, which becomes the remainder and leads with the data-lossrmdir. EVERYfile:linecitation was re-derived at HEAD by this author - IMP-126 added ~52 lines touninstall.sh(230 -> 282), so every pre-126uninstall.shnumber was stale (thermdirmoved :118-119 -> :141-142; the.gitignorestrip is now :106; the hook awk is :87-93; and IMP-126 also ADDED a new native-parentrmdirat :165). Facts re-measured by this author against the real payload: the data-loss deletion reproduced end to end in bothCYBEROS_AGENTSconfigs; the two.agentscreators (install.sh:575-via-:597and:669) and their order; the marker written one level too low (:684into$_sdest); bothmkdir -ptraps; the.gitignoreleak (20 -> 21 bytes, one-time); the required HOOK byte-exactness test (newline-terminated 34 -> 34 EXACT, no-trailing-newline 33 -> 34 DIFFERS, so the hook stays in scope as §1.5); andmcp_json's 105 bytes. One pre-126 claim did not survive re-verification and is dropped rather than carried: thatt22pins the leftover set as "today's behavior" and must be re-pointed - at HEADt22compares the real uninstall against a summary-stripped copy of itself, is immune to edits before the:230anchor, and needs no change. - Human review: the option-C decision (delete a container only when we can prove we created it) is a recorded operator verdict carried from the pre-126 lineage and not relitigated. The scope split against IMP-126 was set at that task's PLAN gate. Both HITL gates remain recorded human verdicts.
1. Description (normative)
- 1.1
install.shMUST write a.cyberos-ownedmarker into.agents/skillsand, separately, into.agents, when and only when install itself CREATES that directory, deciding creation by testing that directory's own existence BEFORE anymkdir -pthat could create it (:669for.agents/skills;:575reached via the:597antigravity pointer, which runs first, for.agents). A directory that existed before install MUST NOT be marked -mkdir -psucceeds silently on an existing directory and creates every level of a chain, so a post-mkdirtest would falsely mark an operator's directory. Each marker's text MUST name the directory it marks and MUST carry the adoption sentenceinstall.sh:681-684already gives the child copy dirs. - 1.2
uninstall.shMUST replace the unconditionalrmdirat:141-142so that.agents/skillsand.agentsare removed only when the directory carries the.cyberos-ownedmarker §1.1 writes AND is empty once our content and that marker are gone; emptiness alone MUST NOT authorise removing either directory, so an unmarked.agents/skillsor.agentsMUST be kept and reported as kept. - 1.3
uninstall.shMUST remove a.cyberos-ownedmarker it finds on.agents/skillsor on.agentsBEFORE attempting that directory'srmdirand whether or not thermdirthen succeeds, so no marker survives uninstall even where the directory is kept (an install-created.agentswhose:597rules pointer keeps it non-empty); and a directory whose marker the operator has deleted MUST be kept and reported as kept, honouring the adoption promise for the parent. - 1.4
uninstall.sh's.gitignorestrip (:106) MUST also delete the single\nseparator byteinstall.sh:756writes immediately before the# >>> cyberosmarker, restoring a pre-existing newline-terminated.gitignorebyte-identical to its pre-install content, for any N >= 1 install/uninstall cycles. §3 records why the non-newline-terminated.gitignoreshapes are unrecoverable fromuninstall.shand which install-side line destroys them. - 1.5
uninstall.sh's hook strip (:87-93) MUST restore a foreign pre-commit hook byte-identical to its pre-install content even when that content has NO trailing newline - deleting the managed block together with the single separator byte install writes before the# >>>marker and NOT re-terminating the last surviving line - while keeping the newline-terminated case (already byte-exact under IMP-126) byte-exact. - 1.6
uninstall.shMUST NOTrmdirany of the five native channel parents (.claude/skills,.grok/skills,.commandcode/skills,.codex/skills,.opencode/skill): the per-family loop'srmdir "$root/$_sd"at:165(added by TASK-IMP-126, which pre-126 uninstall did not have) MUST be removed, so an operator's pre-existing EMPTY native channel dir survives uninstall - the same emptied-container-is-inert rule this task applies to.gitignoreand.mcp.json, and the same data-loss class as:141-142. These parents are NOT marked (install writes no parent marker there and none is needed once the blindrmdiris gone); removing thermdirrestores the pre-126 behavior of leaving them in place.
2. Acceptance criteria
- [ ] AC 1 (traces_to: #1.1) - after install into a repo whose
.agentsand.agents/skillsPRE-EXISTED, NEITHER carries.cyberos-owned; after install into a repo with neither directory, BOTH carry it with text naming the directory and containing the adoption sentence; the arm MUST cover.agentscreated alone via the:597pointer underCYBEROS_AGENTS=antigravityand.agentscreated as a side effect of:669'smkdir -pchain underCYBEROS_AGENTS=agents, and MUST FAIL if any pre-existing directory is marked - test:tools/install/tests/test_install_hygiene.sh::t23_parent_marker_written_only_on_create - [ ] AC 2 (traces_to: #1.2) - after install into a repo whose
.agents/skillsand.agentsPRE-EXISTED empty, both still EXIST after uninstall and are reported kept; after install into a repo with neither, both are REMOVED; the arm MUST FAIL if emptiness alone removes an unmarked dir, which today's:141-142makes true (reproduced 5a/5b) - test:tools/install/tests/test_install_hygiene.sh::t24_containers_removed_only_when_marked - [ ] AC 3 (traces_to: #1.3) - after uninstall NO
.cyberos-ownedfile exists anywhere under the repo, including on an install-created.agentsthat is KEPT because its:597rules pointer survives; and an install-created.agents/skillswhose marker the operator deleted still EXISTS and is reported kept; the arm MUST FAIL if a marker survives on a kept directory or if an adopted directory is removed - test:tools/install/tests/test_install_hygiene.sh::t25_marker_removed_and_adoption_honoured - [ ] AC 4 (traces_to: #1.4) - a pre-existing newline-terminated
.gitignoreis byte-identical to its pre-install bytes after 1 and after 3 cycles, asserted withcmp; the arm MUST FAIL on today's 20 -> 21 leak and MUST leave an install-CREATED.gitignore(empty tmp, no separator) equally clean - test:tools/install/tests/test_install_hygiene.sh::t26_gitignore_strip_byte_exact - [ ] AC 5 (traces_to: #1.5) - a foreign pre-commit hook with NO trailing newline is byte-identical to its pre-install bytes after uninstall and across 3 cycles, asserted with
cmp, AND the newline-terminated control stays byte-identical; the arm MUST FAIL on today's 33 -> 34 leak and on any rule that adds a byte to the no-trailing-newline shape - test:tools/install/tests/test_install_hygiene.sh::t27_hook_strip_byte_exact_no_trailing_newline - [ ] AC 6 (traces_to: #1.6) - after install into a repo whose native channel dir (e.g.
.claude/skills) PRE-EXISTED empty, that directory still EXISTS after uninstall; the arm MUST FAIL on today's:165rmdir, which destroys it (reproduced end to end at HEAD), and MUST confirm the managed skill link inside it is still removed (this task must not regress IMP-126's link cleanup) - test:tools/install/tests/test_install_hygiene.sh::t28_native_channel_parent_survives
3. Edge cases
mkdir -psucceeds silently on an existing directory, so a marker written after themkdirwould stamp an operator's pre-existing dir as ours - the bug inverted, and worse, because it manufactures a licence to delete. Measured exit 0. The create-test in §1.1 therefore runs BEFORE themkdir, in the shape[ -d "$d" ] || { mkdir -p "$d"; marker "$d"; }.mkdir -p a/bcreates both levels, so.agentscannot inherit.agents/skills's proof; and.agentshas a second creator atinstall.sh:575(via the:597pointer, dirname.agents/rules) that runs before:669. Measured withCYBEROS_AGENTS=antigravity:.agents+.agents/rules/cyberos.mdcreated with NO.agents/skills. §1.1 tests each directory's own existence before anymkdirthat could create it.:142is reachable, so gating it is not dead code. In the default config.agentssurvives regardless of any marker, because the:597pointer's.agents/rules/cyberos.mdkeeps it non-empty and no uninstall path removes it. But measured withCYBEROS_AGENTS=agents(antigravity filtered off),find .agentsafter uninstall returns NOTHING - the operator's own.agentsand.agents/skillsare both destroyed. Both dirs need their own marker and their own gate.- The marker on a KEPT directory would be a new leftover, so §1.3 removes it either way. Where install created
.agentsand the antigravity pointer is installed, uninstall keeps.agents(its rules pointer survives) - a marker left inside would be a permanent file this task's title forbids. The sequencing is forced in the other direction too: the marker is a file, so while it is present the dir is never empty and thermdircould never fire. - Re-install after adoption keeps the adoption. If the operator deletes
.agents/skills/.cyberos-ownedto adopt the dir and later re-installs, §1.1's[ -d "$d" ]test is TRUE, so install neithermkdirs nor re-marks, and the dir stays adopted. The same test makes the marker write idempotent. - The
.gitignorepigeonhole is install's, not uninstall's.install.sh:742-754's awk REWRITES.gitignoreand trims the trailing blank run, so*.log\n,*.log\n\n\nand*.log(no trailing newline) all collapse to one post-install state that no uninstall-side rule can tell apart. §1.4 is scoped to the newline-terminated, no-trailing-blank-run shape - the measured domain where install is injective. Closing the rest means changinginstall.sh:744-754, out of scope. - The
.gitignoreinstall CREATED (empty tmp) has no separator to leak.install.sh:715seeds it,install.sh:756's[ -s tmp ]guard writes no separator, and the strip leaves it clean. AC 4 pins that this stays true. - A hook whose pre-install content already ended in multiple newlines must come back byte-exact to THAT content - §1.5 consumes exactly one byte, never normalises. (This does NOT hold for
.gitignore, whereinstall.sh:754already trimmed trailing blanks at install time.) - The native parent that held ONLY our link becomes an inert empty dir, and that is correct. IMP-126's family loop still removes the managed skill link inside a native channel dir; §1.6 stops only the PARENT
rmdir, so a.claude/skillsthat contained nothing but our link is left behind empty. That is the same emptied-container-is-inert outcome as an emptied.gitignoreor.mcp.json- a dir we no longer own the contents of, left in place because we cannot prove we created the dir itself. AC 6 pins that the operator's pre-existing native dir survives AND that the link inside is still gone (no IMP-126 regression). - Security-class. Uninstall removes a container only when it carries OUR
.cyberos-ownedmarker AND is empty; ownership is decided by the presence of that marker FILE (content install wrote), never by a path pattern or a directory NAME an operator could forge - so naming a directory.agents/skillsorship-taskscannot by itself induce a deletion. No operator-supplied string is interpolated into thermdir/rmtargets; the two paths are fixed literals under$root, on the same in-repo confinement the other helpers use; nothing read from disk is executed. An operator who plants a marker in their own directory adopts the install semantics deliberately; §1.1 never writes one into a directory that predated install.
Audit
TASK-IMP-121 - audit record
§0 - Provenance, and why this file had to be reconstructed
This page was rebuilt on 2026-07-18. Read this section before trusting any finding id on it.
The 6/10 audit was written into a COMMIT MESSAGE and never into this file, which sat at the 4/10 version (6a146869) while two rewrites happened against findings no one could open. The defect is recorded as NEW5-007 on TASK-IMP-122's audit page. This file is the remedy for 121's half of it.
§0.1 - What kind of section you are reading
| kind | sections | what you are reading |
|---|---|---|
| written at the time | §1 (audit 1, 4/10) | the auditor's own audit file, committed at 6a146869, preserved verbatim |
| reconstructed from commit | §2 (audit 2, 6/10) | the orchestrator's summary of an audit. The audit's own text does not exist. |
| not an audit at all | §3 (the third author's fix) | the author's closure claims. Nobody audited this. |
§0.2 - The rounds, mapped
| section | what it is | source | persisted at the time? | verdict |
|---|---|---|---|---|
| §1 | audit 1, of the first draft | 6a146869 (this file) | YES | FAIL 4/10 |
| §2 | audit 2, of the first rewrite | 15894b1e message | no | FAIL 6/10 |
| §3 | the third author's fix - the current spec.md | 1f8143cf message | no | NONE. UNAUDITED. |
All six ids audit 2 raised - NEW-001 through NEW-006 - resolve to §2. Neither spec.md cites a finding id by name (verified: grep -oE 'NEW-[0-9]{3}|ISS-[0-9]{3}' on both specs returns nothing for 121), so no citation in the task depends on this page. source_decisions refers to the rounds by score - "2026-07-18 audit FAIL 4/10" (§1) and "2026-07-18 audit FAIL 6/10" (§2) - and both now resolve.
§0.3 - A citation hazard a reader will otherwise walk into
Audit 2's clause and AC numbers do not resolve against HEAD's spec.md.
Audit 2 examined the first rewrite: 9 clauses, 9 ACs, effort 5 -> 8 (per 7309cd80). HEAD is the third author's whole-document rewrite: 10 clauses, 10 ACs, effort 8 -> 9. The renumbering between them was never mapped, and §2's citations are preserved as the auditor wrote them rather than silently re-pointed - re-pointing them would be a reconstruction of a mapping nobody recorded.
Concretely: NEW-002 is about "§1.5's six channel dirs" and "AC 5". In HEAD, the six-channel container rule is §1.6 and its test is AC 6; HEAD's §1.5 is the operator-edited-.mcp.json rule and its AC 5 is that rule's test. Reading NEW-002 against HEAD's §1.5/AC 5 will mislead you. The same hazard applies to NEW-001 (§1.6/AC 6/AC 7 as numbered in the first rewrite; the byte-exact strip rule is §1.7 in HEAD) and NEW-004 (§1.3 vs §1.8).
§1 - AUDIT 1: audit of the first draft - FAIL 4/10
Provenance: WRITTEN AT THE TIME. This is the audit file as committed at
6a146869(2026-07-18). It is the only section of this page that is an auditor's own artefact. Preserved verbatim; nothing below this line in §1 has been edited.Status: superseded as a verdict, and its findings are resolved - audit 2 (§2) records ISS-001/003/004/005 as DISSOLVED by the frame replacement rather than patched, and all six of ISS-006's citations as fixed. ISS-002's shape RECURRED on a new input as NEW-003 (§2.4).
Audit 1's own header block, as written:
task_id: TASK-IMP-121
audited: 2026-07-18
verdict: FAIL
score: 4/10
issues_open: 6
issues_resolved: 0
template: task@1
audit_rubric_version: audit_rubric@2.0
machine_floor: task-lint clean (exit 0) - run FIRST per TASK-IMP-084. The floor passing is why this
audit matters: every finding below is a JUDGMENT defect the mechanical check cannot see.
auditor: independent subagent (had not seen the author's reasoning) + author verification of the
two load-bearing claims against source
§1.1 - Verdict summary
FAIL at 4/10. The four underlying defects are REAL and reproduced by the harness. The spec diagnoses two of them from a false premise, carries the TASK-IMP-118 defect class in its own AC 1, and cites six line numbers that do not resolve. Not a patch: a rewrite against the true mechanism.
§1.2 - Findings (ALL OPEN)
ISS-001 (CRITICAL) - the premise ".cyberos is removed" is false in the default path
§1.1/§1.3 are written against "the removed .cyberos/". VERIFIED against source: uninstall.sh:151 runs rm -rf "$CY" and :155-158 then runs mkdir -p "$root/.cyberos/memory" + mv "$KEEP_BRAIN_STASH" "$root/.cyberos/memory/store". .cyberos/ survives with BRAIN inside unless CYBEROS_UNINSTALL_KEEP_BRAIN=0. The harness's own fresh-git run listed ./.cyberos/memory/store/* among the leftovers - the author had the evidence and wrote the clause against the opposite state.
ISS-002 (CRITICAL) - AC 1 asserts a different predicate than §1.1 (the TASK-IMP-118 defect)
§1.1 verb: a symlink "may remain [only if its] readlink target [does not resolve] inside the removed .cyberos/". AC 1 asserts: "zero unresolvable targets". These diverge BOTH ways:
- false negative: with BRAIN restored (ISS-001), a symlink into a SURVIVING
.cyberos/path resolves fine -> passes AC 1 -> violates §1.1. - false positive: an operator's own broken symlink pointing OUTSIDE
.cyberos/is unresolvable -> fails AC 1 -> but §1.2 requires it be KEPT. AC 1 and AC 2 contradict on that input. The author wrote both the clause and its test, and the test is weaker. This is precisely §15.2.
ISS-003 (CRITICAL) - §1.3/§1.4 are undecidable; the enabling mechanism is rejected in-spec
Both condition on "if install created it". Nothing on disk records that: install.sh tracks creation in a shell variable that dies with the process; the only ownership marker (.cyberos-owned) is written ONLY into skill copy dirs, never onto .gitignore or .mcp.json. The spec's Proposed Solution permits only "readlink target inside .cyberos/, or our marker" - readlink is meaningless on a regular file and no marker exists - while Alternatives REJECTS "Track install's creations in a manifest" as scope creep. The spec forbids the only mechanism that would make its own clauses decidable.
ISS-004 (CRITICAL) - §1.4 contradicts §1.7 and the zero-casualties guardrail
An operator who ran touch .gitignore before install is byte-indistinguishable from install's : > "$gi". After block-strip the file is empty; §1.4 says it MUST NOT survive -> uninstall deletes an operator's file -> §1.7 ("No path present before install may be missing") violated. §1.7 admits no exception and §3 does not cover the case. Same hazard for .mcp.json: install points operators at .cyberos/mcp/README.md for hand-registration, so an operator-authored .mcp.json is a supported path.
ISS-005 (MAJOR) - section 6 already ACCEPTS the dangling links; the spec frames it as oversight
VERIFIED: uninstall.sh:162 reads # 6. skill symlinks into .cyberos (dangling) - leave dirs; operator cleans. The dangling is KNOWN and DELIBERATE in source. The spec's Problem section says four channels "are never looked at" - true of the code path, false of the intent. This is a recorded decision to OVERTURN with reasons, not a gap to fill. The operator approved overturning it (2026-07-18) on the readlink-proves-ownership argument, which stands - but the spec must argue against section 6 explicitly rather than not notice it.
ISS-006 (MAJOR) - six citations do not resolve
:70cited as "(block strip)" and as the proven newline leak ->:70isecho " stripped cyberos block from pre-commit". The sed is:68. This is the spec's single most load-bearing citation and it points at the echo, not the mechanism.:81cited as the .gitignore strip ->:81is the echo; the strip is:80.:114cited BOTH as the ship-tasks exemption AND as the readlink test it exempts from. One line cannot be both. The[ "$_sc" != "ship-tasks" ]guard is:112.:111cited as an exemption whose rationale is "avoid clobbering operator files" ->:111is a comment; it records no such rationale.:105-107cited as a prune precedent -> it is the "kept unmarked skill dir" echo branch. The actual prune is:118-119and has NO install-created check.:92-93resolves but is a comment naming 3 paths, cited to cover 7.
§1.3 - Verified accurate (credit)
uninstall.shnever mentions mcp/codex/grok/commandcode/opencode - grep returns zero. The five-channel and dead-registration findings are REAL.- The +1-newline-per-cycle diagnosis is mechanically correct and fixable from
uninstall.shalone: install appends\n# >>> cyberos-status-hook v2, and:68's sed range starts AT the marker, so the blank separator above it survives. - 121's reading of TASK-IMP-106 AC 3 is accurate;
depends_on: [TASK-IMP-106]correctly serialises. - TRACE-003 passes:
test_install_hygiene.shexists and is declared in modified_files.
§1.4 - Required before re-audit
Rewrite §1.1/§1.3 against the surviving-.cyberos reality; re-derive AC 1 to assert §1.1's actual predicate; resolve ISS-003 by either scoping the creation-manifest IN or narrowing the clauses to what readlink/marker can decide; reconcile §1.4 with §1.7; argue against section 6 explicitly; fix all six citations. Residual noted: after 121 re-points t22, TASK-IMP-106 §1.5 has no test left that verifies it, and a test named t22_uninstall_behavior_unchanged whose baseline moved asserts the opposite of its name.
§2 - AUDIT 2: audit of the first rewrite - FAIL 6/10
Provenance: RECONSTRUCTED FROM COMMIT
15894b1e(2026-07-18); no contemporaneous audit file exists. What follows is the orchestrator's summary of an independent audit. The auditor's own text is gone. NEW-001 carries a note that the orchestrator verified it against source independently ("I verified this myself, it is exact"); the other five carry no such note.The subject was the FIRST rewrite (9 clauses / 9 ACs,
7309cd80), written by a fresh author who had written none of the failing draft. It is not HEAD. See §0.3 before resolving any clause number below.
§2.1 - Verdict summary
FAIL 6/10 (from 4/10). The frame-replacement does real work: ISS-001/003/004/005 are DISSOLVED not patched, all six inherited citations fixed, every inherited number reproduced exactly. It fails on what the author ORIGINATED.
§2.2 - NEW-001 (CRITICAL) - the impossibility proof is FALSE
The orchestrator's note records this as independently verified: "I verified this myself, it is exact."
§3 claims an operator hook with no trailing newline "cannot be restored byte-exact - the information is destroyed at append time and no uninstall-side rule inverts it". FALSE. The information survives as the presence/absence of the blank line above the marker, and install ALWAYS prepends exactly one \n, so the byte before the marker is always ours. Measured:
line-oriented strip (the spec's): 6B -> 7B DIFFERS
byte-oriented rule (awk, no perl): 6B -> 6B BYTE-EXACT <- inverts it
same rule, newline-terminated: 7B -> 7B BYTE-EXACT
A LEVEL CONFUSION: the author reasons in LINES, and in line-space the no-newline case has no blank line to consume - so they generalised "my line-oriented rule cannot" to "no rule can".
§1.6 is NARROWED on that false proof, which excuses a live §1.8 violation (an operator's hook permanently mutated +1 byte) and scopes AC 6/7 so the suite can never catch it.
§2.3 - NEW-002 (MAJOR) - AC 5 is UNSATISFIABLE
§1.5 requires each of §1.1's six channel dirs to survive and forbids removing a container for emptiness. uninstall.sh:118 rmdirs .agents/skills - the FIRST of the six - for exactly that. Verified. §3 calls it "mild tension" and re-scopes §1.5 to five IN PROSE ONLY; the clause and the AC still say six.
Aggravating: an operator's pre-existing EMPTY .agents/skills is deleted - a §1.8 violation AC 8's four cases miss.
§2.4 - NEW-003 (MAJOR) - AC 1 and AC 2 contradict: ISS-002's exact shape on a new input
§1.1's chained-form pattern widens the hazard from the 3 names install writes to EVERY entry in the dir, destroying the ours-by-construction justification while keeping the pattern. §3's "carried forward unchanged" is false.
ISS-002 (§1.2) is the same defect class - clause and test diverge, author wrote both - recurring after being dissolved once.
§2.5 - NEW-004 (MAJOR) - §1.3 vs §1.8: ISS-004's shape relocated
From container-DELETION to content-MUTATION. The §1.3/§1.4 split fixed the drafting, not the substance: an operator's hand-registered .mcp.json byte-identical to :685's form triggers §1.3, which then mutates a path present before install.
§2.6 - NEW-005 (MINOR) - a SEVENTH mis-citation, ORIGINATED
:684 is an if-guard; the summary is :689 - under a disclosure reading "every line number was re-verified". The six inherited citations were all genuinely fixed; the seventh is the author's own.
§2.7 - NEW-006 (MINOR) - the receipt rejection's conclusion stands but one universal is false
Under CYBEROS_COPY_SKILLS=1 the copy is decidable from a receipt and from nothing else - which is §3's own conceded gap. The conclusion (reject the receipt) survives; the universal supporting it does not.
§2.8 - THE PATTERN, ACROSS SEVEN AUDITS AND THREE AUTHORS
Recorded by the orchestrator alongside this audit, because 121 is the round that made it legible:
round 3 (me): patched what the audit NAMED, did not re-read -> 4 findings survived verbatim
round 4: re-read everything, false NUMBER of their own -> 1525
121 rewrite: fixed every citation, false REASONING of their own -> the impossibility proof
One rule underneath all three: AUTHORS DO NOT CHECK WHAT THEY ORIGINATE. An inherited claim carries a provenance that invites scrutiny; an originated one arrives already believed. It is TASK-IMP-118's defect class one level up - there, an author's test asserts something weaker than their own clause; here, an author's evidence never gets tested at all. Both are the same failure: the author is the wrong reader of their own work.
This became TASK-IMP-124. Note that 124 rejected rotate-the-author using the evidence offered FOR it: both fresh-author rounds shipped an originated falsehood. "Rotation changes who INHERITS; it does not make anyone check what they ORIGINATE."
§3 - THE THIRD AUTHOR'S FIX: CLAIMED CLOSED, UNAUDITED
Provenance: RECONSTRUCTED FROM COMMIT
1f8143cf(2026-07-18); no contemporaneous file exists.THIS IS NOT AN AUDIT AND THIS SECTION RECORDS NO VERDICT. It is the third author's account of closing audit 2's six findings, relayed through the orchestrator's commit message. HEAD's
spec.mdis this rewrite. No independent auditor has read it.
§3.1 - The status, stated exactly
The six findings NEW-001..006 are CLAIMED CLOSED. The fix is UNAUDITED.
- No third audit exists. Not in this file, not in any commit message, not anywhere.
- The closure claims are the author's own - which is precisely the category audit 2's own §2.8 identifies as the least-checked thing in any document. The rule the round established applies to the round's own output.
score_historytherefore stops at 6/10. The 6/10 is audit 2's verdict on the draft that PRECEDED this fix. It is not HEAD's score, and this file does not assign HEAD one. A reader who needs a verdict on HEAD must commission an audit; there is nothing here to read one off.
Two things partially corroborate the claims, and neither is an audit:
- The orchestrator reports reproducing NEW-001's disproof independently: "line-strip 6B->7B DIFFERS, byte-strip 6B->6B EXACT, controls 7B/9B exact." That is one finding's evidence re-measured, not a review of the document.
- The machine floor passes:
task-lintclean, 10 clauses / 10 ACs / 9h. Audit 1's own header says why that is worth little here: "The floor passing is why this audit matters: every finding below is a JUDGMENT defect the mechanical check cannot see."
§3.2 - What the third author claims, per 1f8143cf
All six findings closed. Specifically recorded:
- NEW-001. Reproduced the disproof independently and retracted §3's impossibility. Then found the impossibility IS REAL - on the OTHER FILE:
install.sh:733-746's awk collapses*.log\n(6B),*.log\n\n\n(8B) and*.log(5B) all to 6B. Three distinct pre-install states, one post state. A true pigeonhole, install-side, correctly scoped out. - It tested its own new rule and found its limit - operator content BELOW our block with no trailing newline: want 17B, got 18B - and recorded it rather than papering it. (This is the §2.8 rule being applied by an author to their own work, which is the first instance of it in the session.)
- §1.9's carve-out was FORCED, not chosen: §1.9 contradicted §1.1, not only §1.4 - an operator's pre-existing symlink whose target names our machine is a path present before install that §1.1 removes. The 6/10 audit found the §1.4 collision and MISSED the §1.1 one.
- effort 8 -> 9.
§3.3 - OPEN: needs an operator verdict, not an auditor's
Bringing uninstall.sh:118-119 in scope - keeping six channel dirs rather than scoping §1.6 to five - is the author's decision and is NOT covered by the recorded 2026-07-18 PLAN gate. Flagged by the author in the spec's own AI Authorship Disclosure. This is a scope question for the operator and an audit cannot settle it.
§3.4 - Also filed against this round
1f8143cf records a finding about TASK-IMP-118, raised while filing TASK-IMP-124 and belonging to neither 121 nor 122: TASK-IMP-118 declares modified_files: tools/install/docs-tools/templates/ task-audit/RUBRIC.md - WHICH DOES NOT EXIST (verified). TRACE-003 covers test paths only, so an originated citation about the author's own cone is checked by nobody. Recorded here only so it is not lost with the commit message; it needs its own task and is not 121's.
§3.5 - Required before this task can be trusted
- Audit HEAD. The six closures are unverified. This is the whole of what is missing.
- Get the operator's verdict on §3.3 -
:118-119's scope is outside the PLAN gate. - Note for whoever audits: §2's clause numbers do not resolve against HEAD (§0.3). The mapping from the 9-clause draft to HEAD's 10 was never recorded and is not reconstructed here.
Not promoted; no BACKLOG row.