Fix #29, #30, #31: codev update stops reporting things it never checked - #48
Merged
Conversation
Three bugs, one defect wearing three hats. Each states a conclusion nothing established. #31 --dry-run printed "no files will be changed" and then wrote CLAUDE.md.codev-new and AGENTS.md.codev-new. Only the skills call was guarded; copyRootFiles was not. A dry run that writes is worse than no dry run, because it is the one mode an operator trusts specifically on the promise that it touches nothing. copyRootFiles now takes dryRun and reports what it WOULD do. #30 A conflict was declared whenever the destination merely existed, with the reason "Content differs from template" -- while the only test performed was fs.existsSync. Every update handed over a merge task that was usually a no-op. It now compares, after template substitution, and returns `unchanged` for a byte-identical file. An unreadable destination counts as differing: that direction surfaces the file for a human instead of calling it clean. #29 needed more than a comparison. Once a skill directory existed its contents were frozen at install time forever, and --force never helped (that branch only wrapped copyRootFiles). The comment said "without replacing customizations", but with nothing compared the code could not tell a customization from a stale copy, so it preserved both -- which in practice preserved rot. Real cost: a vendored afx skill claiming --branch does not exist, and agents burning turns on it. Comparing against the CURRENT skeleton does not fix that; it only says "same or different", not "customized or stale". Answering the actual question needs provenance, so copySkills now keeps a per-provider .codev-skill-manifest.json of the hash it installed: dest == skeleton -> already current, backfill the hash dest == installed hash -> unmodified but stale, REFRESH dest != installed hash -> local edits, leave alone and SAY SO no manifest entry / unread -> cannot tell, leave alone and SAY SO The last row is the one that matters. "I cannot tell" must not be spelled the same way as "safe to overwrite", or the first update after this ships eats a year of someone's local edits. Customized skills are now reported rather than silently skipped: a customization that is quietly blocking every update is exactly what an operator needs told. init/adopt keep the old semantics -- only update opts in via refreshUnmodified. 15 tests. The #30 and #31 ones fail against the old implementation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…jects The review found a path that re-creates the exact freeze #29 exists to fix, and verified it by running the code rather than reading it. copyDirRecursive is additive -- it never removes destination files absent from the source. So after a refresh where the skeleton DELETED a file: - the deleted file survives in the vendored skill, which is the stale-doc symptom #29 is about, surviving the fix meant to remove it - the recorded hash is the skeleton's, but the destination now hashes to src+leftover, so the NEXT update reads destHash != installedHash, calls the skill customized, and freezes it forever with zero local edits Silent, unrecoverable without deleting the directory by hand, and it lands on the first release that removes a skill file. The refresh branch now rmSync's the destination first -- safe there and only there, because destHash === installedHash was just proven, so nothing local is being discarded. Also from the review: --dry-run --force still wrote. I guarded the conflict branch and left the force branch calling copyRootFiles with no options at all, so the exact combination of two real CLI flags still printed "no files will be changed" and then created the file. init and adopt never wrote a manifest, so a project installed at v1 that never happened to run `update` at v1 arrived at v2 as unknown provenance and was held back from every future refresh -- the fix would never have reached the projects most in need of it. A separate recordManifest option (implied by refreshUnmodified) leaves provenance without changing init/adopt semantics: adopt still refuses to rewrite an existing project's skills. hashSkillDir read files as utf-8, which collapses invalid bytes to U+FFFD, so two different binary assets hashed identically and a modified one would have read as unmodified and been overwritten. Now hashes raw Buffers with length-framed fields, which also stops a+bc colliding with ab+c. The held-back message now names the remedy, since --force does not reach skills. 9 more tests, including the two-update sequence that proves a skeleton deletion no longer poisons provenance. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #29, closes #30, closes #31.
Three issues filed separately; one defect wearing three hats. Each states a conclusion nothing established.
#31 —
--dry-runwrote to diskIt printed
(dry run - no files will be changed)and then createdCLAUDE.md.codev-newandAGENTS.md.codev-new. Only thecopySkillscall was guarded bydryRun;copyRootFileswas not.A dry run that writes is worse than no dry run at all, because it is the one mode an operator trusts specifically on the promise that it touches nothing.
copyRootFilesnow takesdryRunand reports what it would do.#30 — "Content differs from template", where nothing compared content
A conflict was declared whenever the destination merely existed:
...and reported as
reason: "Content differs from template". Every update handed the operator a merge task that was usually a no-op.It now compares — after template substitution, so a correctly-substituted
{{PROJECT_NAME}}is not a false conflict — and returnsunchangedfor a byte-identical file. An unreadable destination counts as differing: that direction surfaces the file for a human rather than silently calling it clean.#29 — needed more than a comparison
Once
.claude/skills/<name>/existed, its contents were frozen at install time forever.--forcenever helped; that branch only wrappedcopyRootFiles.The comment said "without replacing customizations". With nothing compared, the code could not tell a customization from a stale copy, so it preserved both — which in practice means it preserved rot. The issue's examples: a vendored
afxskill claiming there is no--branchflag (there is), and anarch-saveskill claiming delayed sends are not persisted (they are).Comparing against the current skeleton does not fix this. It answers "same or different", not "customized or stale" — which is the actual question, and re-guessing it would repeat the original sin. So
copySkillsnow keeps a per-provider.codev-skill-manifest.jsonrecording the hash it installed:The last row is the one that matters.
"I cannot tell"must not be spelled the same way as"safe to overwrite", or the first update after this ships eats a year of someone's local edits. Projects vendored before manifests existed land there and are held back on purpose.Customized skills are now reported, not silently skipped — a customization quietly blocking every update is exactly what an operator needs told:
initandadoptkeep the old semantics. Onlyupdateopts in viarefreshUnmodified.Tests
15 new. The #30 and #31 cases fail against the old implementation directly; the #29 ones exercise the new API.
Worth calling out three:
{{PROJECT_NAME}}expanded to the same value must read as unchanged, not as a conflict.copied/conflicts/unchangedfrom a dry run must equal the real one. That is the entire point of the mode.update+scaffold+template-delivery+ new: 83 passed.adopt: 8.doctor: 25.(Note:
src/__tests__/init.test.tsis in the vitestexcludelist, socodev inithas no suite running in CI. Not touched here, but worth knowing.)🤖 Generated with Claude Code