Uninstall MUST preserve .cyberos/config.yaml, the override home
TASK-IMP-129: Uninstall MUST preserve .cyberos/config.yaml, the override home
Summary
Three shipped surfaces tell the operator that durable overrides belong in .cyberos/config.yaml, and install.sh scaffolds it exactly once and never clobbers it. uninstall.sh then removes it with the rest of .cyberos/. The one file the system promises is durable does not survive the operation most likely to be followed by a reinstall.
Problem
gates.env is machine-owned by design. install.sh:299 regenerates it on every install from autodetection, and install.sh:326 says so plainly - when regeneration changes the file it points the operator at the durable alternative: "durable overrides belong in .cyberos/config.yaml". That design is coherent and this task does not disturb it.
The durable alternative is not durable. Three surfaces name config.yaml as the override home:
install.sh:326directs the operator to it when gates.env is regenerated.install.sh:328scaffolds it exactly once, explicitly never clobbering (TASK-CUO-207 §1 #3) - a never-clobber promise only means something if the file persists.run-gates.sh:24-25reads it as the override layer, andrun-gates.sh:75instructs the operator to "Set commands in.cyberos/config.yaml(gates.build/lint/test/coverage)".
uninstall.sh contains zero occurrences of config.yaml. It removes .cyberos/ wholesale, keeping only the BRAIN store under CYBEROS_UNINSTALL_KEEP_BRAIN. An operator who follows the instruction printed by install and by the gate runner loses their overrides at the next uninstall, and the reinstall scaffolds a fresh commented-out file as though they had never configured anything.
This is the same principle TASK-IMP-126 §1.4 already established - "None of 1.1-1.3 may remove an operator file" - scoped there to .mcp.json, unmarked skill dirs, and foreign hook lines. config.yaml is operator content by construction: install writes it once and never again, so every subsequent byte in it is the operator's.
Observed on this repo on 2026-07-20: an uninstall/install cycle regenerated gates.env with an empty TEST_CMD, because autodetect reported ecosystem "unknown" for a repo whose suite is bash scripts/tests/run_all.sh rather than an npm script. That silently reverted a same-day fix that had made the machine gates non-vacuous, and the gates would have reported GREEN while running no tests. Node repos were unaffected - autodetect populated build, lint and test correctly there - so the failure is specific to repos whose test entrypoint autodetect cannot name, which is exactly the population that most needs a durable override.
Proposed Solution
Add config.yaml to the set uninstall preserves, alongside the BRAIN store, and report it in the "kept on purpose" banner so the operator can see what survived. Separately, teach autodetect to recognise a shell test entrypoint so a bash-suite repo gets a populated gates.env rather than a silent empty one.
Alternatives Considered
- Preserve
gates.envinstead. Rejected: it contradicts TASK-CUO-207, which makes gates.env machine-owned and regenerated on purpose. Preserving it would carry stale autodetect provenance across installs and make the regeneration message a lie. - Move
config.yamloutside.cyberos/so uninstall cannot reach it. Rejected: it is per-repo CyberOS configuration and belongs with the machine; the fix is for uninstall to honour the promise, not to relocate the file to route around uninstall. - Document that operators should back up
config.yamlbefore uninstalling. Rejected: a durability promise discharged by asking the operator to do it themselves is not a promise. - Fix only autodetect and leave uninstall as-is. Rejected: autodetect will always have gaps, which is precisely why an override layer exists. The override layer must survive.
Success Metrics
- Primary: an operator-edited
config.yamlsurvives uninstall and its overrides are in effect after reinstall, with no manual restoration. Baseline today: it is deleted and reinstall scaffolds a fresh commented-out file. - Guardrail: a repo whose only test entrypoint is a shell script gets a populated
TEST_CMDfrom autodetect rather than an empty one, so the vacuous-gate outcome is not reachable by default.
Scope
In scope: the uninstall preserve set and its banner, the autodetect rule for a shell test entrypoint, and arms in test_install_hygiene.sh.
Out of scope / Non-Goals
- The machine-owned status of
gates.env(TASK-CUO-207) - unchanged. - The
config.yamlschema or the set of overridable keys. - Re-running install across the fleet after the fix (an operator-gated action).
- The BRAIN store's existing preservation behaviour and its
CYBEROS_UNINSTALL_KEEP_BRAINswitch.
Dependencies
None blocking. Extends the operator-file principle established by TASK-IMP-126 §1.4 to a file that task did not enumerate.
Corroboration from TASK-IMP-122 §1.5. That task, reasoning about the fingerprint cone rather than about uninstall, independently classifies config.yaml as install-generated-or-operator-owned and declares exempt:config.yaml so its contents never enter rules_sha. Two tasks reaching the same classification from unrelated directions is the strongest available evidence that the file is operator content; this task is the one that makes the classification survive an uninstall.
Boundary against TASK-IMP-121 (done). That task made uninstall leave the repo as it found it for .agents markers, .gitignore, the pre-commit hook, and the five native channel parents. It never reaches inside .cyberos/, because everything there was assumed to be machine-owned and disposable. config.yaml is the counterexample that assumption missed. This task does not revisit any surface 121 settled.
AI Authorship Disclosure
- Tools used: Claude (Fable 5) running the CyberOS task-author skill inside Cowork.
- Scope: the empty-TEST_CMD regression was observed directly during the 2026-07-20 estate sweep, on this repo. The initial framing - that uninstall wrongly destroys operator gate configuration in
gates.env- was wrong and was corrected before drafting: readinginstall.sh:324-326showed gates.env is machine-owned by design, and the real defect is the destruction ofconfig.yaml. Every source_pages line was read at HEAD; the "zero occurrences" claim is a grep count againstuninstall.sh. - Human review: scope and granularity approved at the 2026-07-20 PLAN gate, where the relationship to TASK-IMP-126 was surfaced before writing; both HITL gates are recorded human verdicts.
1. Description (normative)
- 1.1
uninstall.shMUST preserve.cyberos/config.yamlwhen it exists, and MUST report it in the "kept on purpose" banner alongside the other preserved paths. - 1.2 After uninstall then install, an operator-set override in
config.yamlMUST still be in effect -run-gates.shMUST resolve the same gate commands it resolved before the cycle. - 1.3 Gate-command autodetect MUST populate
TEST_CMDfor a repo whose test entrypoint is a shell script at a conventional path, rather than leaving it empty with ecosystem "unknown". - 1.4 Preserving
config.yamlMUST NOT keep any machine-owned file:gates.envMUST still be regenerated on install, and the rest of.cyberos/outside the documented preserve set MUST still be removed.
2. Acceptance criteria
- [x] AC 1 (traces_to: #1.1) - a fixture writes an override into
config.yaml, runs uninstall, and the file is present afterwards and named in the banner output - test:tools/install/tests/test_install_hygiene.sh::t_config_yaml_preserved - [x] AC 2 (traces_to: #1.2) - after uninstall then install,
run-gates.shresolves the gate command set by the operator'sconfig.yaml, not the autodetected default - test:tools/install/tests/test_install_hygiene.sh::t_overrides_survive_reinstall - [x] AC 3 (traces_to: #1.3) - installing into a fixture whose only test entrypoint is a shell script produces a non-empty
TEST_CMD- test:tools/install/tests/test_install_hygiene.sh::t_autodetect_shell_suite - [x] AC 4 (traces_to: #1.4) - after the same cycle,
gates.envis a freshly regenerated file and no other machine-owned path under.cyberos/survived uninstall - test:tools/install/tests/test_install_hygiene.sh::t_machine_files_still_removed
3. Edge cases
config.yamlabsent at uninstall (never scaffolded, or deleted by the operator) MUST NOT fail the uninstall, and MUST NOT be reported as kept.- A
config.yamlleft entirely at defaults (every line commented) is still preserved - uninstall cannot distinguish "unedited" from "deliberately reset to defaults", and guessing would reintroduce the silent-loss failure. - After preservation,
.cyberos/is non-empty when the machine is removed; the container-reclaim logic MUST report it kept rather than attempting to remove a non-empty directory. - A repo with both an npm test script and a shell entrypoint MUST keep the existing precedence rather than switch to the shell one - 1.3 fills a gap and MUST NOT re-rank the cases autodetect already resolves.
- A shell entrypoint that exists but is not executable MUST still be detected; the gate invokes it via
bash, so the executable bit is not the test. - Security-class: uninstall reads and removes paths under the repo root. Adding a path to the preserve set narrows what is removed and grants no new capability; the preserved file is data read by
run-gates.sh, which already treats it as untrusted configuration.
Audit
§1 — Verdict summary
Four §1 clauses, four ACs, six edge cases including one security-class row. Machine floor clean after one title-length fix. This spec scored lowest pre-revision of the batch because its founding premise was wrong and had to be replaced before anything else could be judged — see ISS-001, which is the most consequential finding in this batch. TRACE-006 compared per clause: all four cited tests meet their clause's verb.
§2 — Findings (all resolved)
ISS-001 — the task's original premise was false and would have shipped a wrong fix
The task was commissioned as "uninstall destroys operator gate configuration in gates.env", from a directly observed regression: an uninstall/install cycle on this repo blanked TEST_CMD and silently reverted a same-day non-vacuous-gates fix. The observation was correct; the diagnosis was not. Reading install.sh:324-326 before drafting showed gates.env is machine-owned by design under TASK-CUO-207, regenerated on every install on purpose, with the operator explicitly directed elsewhere for durable overrides. A spec written to the original premise would have required uninstall to preserve a file the system deliberately regenerates, contradicting a shipped task and making install's own regeneration message a lie. Material: would have failed at review against TASK-CUO-207. Resolved: reframed to the actual defect — config.yaml, the file all three surfaces name as the durable alternative, is itself deleted by uninstall. The correction is recorded in source_decisions rather than silently absorbed.
ISS-002 — the "documented home" claim needed more than one citation to stand
The reframed premise rests on the system telling the operator that config.yaml is durable. One citation would make that an interpretation. Resolved: three independent surfaces cited and read at HEAD — install.sh:326 (directs the operator there), install.sh:328 (scaffolds once, never clobbers), run-gates.sh:24-25 and :75 (reads it as the override layer and instructs the operator to use it). A never-clobber promise is only meaningful if the file persists, which is the contradiction.
ISS-003 — the deletion claim was asserted where it could be measured
"Uninstall removes it" is the load-bearing fact. Resolved: stated as a grep count (zero occurrences of config.yaml in uninstall.sh) plus the preserve-list citation at :4 showing only the BRAIN store is kept, so the claim is checkable by a reader without running an uninstall.
ISS-004 — XCHAIN: two adjacent tasks were undeclared
TASK-IMP-121 (done) settled uninstall's repo-restoration for .agents, .gitignore, hooks and native channel parents; TASK-IMP-122 (p1, on_hold) independently classifies config.yaml as operator-owned via exempt:config.yaml. Neither was in related_tasks. Material: unrecorded cross-task constraints, and in 122's case a corroboration that materially strengthens this task's premise. Resolved: both added, with Dependencies stating the 121 boundary (121 never reaches inside .cyberos/, because everything there was assumed disposable — this file is the counterexample) and the 122 corroboration (two tasks reaching the same classification from unrelated directions).
ISS-005 — §1.4 was needed to stop the fix over-preserving
A clause requiring preservation, unbounded, invites an implementation that keeps more of .cyberos/ than intended and quietly ends the machine-owned discipline TASK-CUO-207 established. Resolved: §1.4 binds the inverse — gates.env MUST still be regenerated and everything outside the documented preserve set MUST still be removed — traced by AC 4.
ISS-006 — the autodetect half needed to be scoped so it cannot re-rank existing cases
§1.3 asks autodetect to recognise a shell test entrypoint. Unbounded, an implementer could reasonably make the shell path win over an npm script, changing behaviour for every node repo in the fleet — none of which have the defect. Resolved: §3 edge case requires existing precedence to be preserved; 1.3 fills a gap and MUST NOT re-rank cases autodetect already resolves.
ISS-007 — "unedited config.yaml" is not distinguishable from "reset to defaults"
An implementation that preserves only modified config files would look like a reasonable optimisation and would silently discard a file an operator deliberately reset. Resolved: §3 edge case requires preservation regardless of edit state, naming the reason — guessing reintroduces the silent-loss failure this task exists to fix.
§3 — TRACE-006 semantic sufficiency (per clause)
| Clause | Verb demands | Cited test asserts | Verdict |
|---|---|---|---|
| 1.1 MUST preserve AND MUST report in banner | two observables — file survives, and is named in output | AC 1: asserts file present after uninstall AND named in banner output | sufficient on both halves |
| 1.2 override MUST still be in effect | behavioural evidence after a full cycle, not mere file presence | AC 2: asserts run-gates.sh resolves the operator's command, not the autodetected default | sufficient — resolution is stronger than presence |
| 1.3 MUST populate TEST_CMD for a shell entrypoint | a non-empty value where "unknown" is produced today | AC 3: asserts non-empty TEST_CMD on a shell-only fixture | sufficient |
| 1.4 MUST still remove machine-owned paths | regeneration of gates.env and absence of other machine files | AC 4: asserts gates.env freshly regenerated and no other machine path survived | sufficient on both halves |
§4 — Resolution
Seven findings, all material, all resolved in the audited revision. Machine floor clean. Score = 10/10.
Status transition draft -> ready_to_implement is authorised by this verdict per STATUS-REFERENCE.md §1.1. The two human-acceptance gates downstream are unchanged and remain recorded human verdicts.
End of TASK-IMP-129 audit.