Summary
Dogfooding a manual sequant update in a downstream project surfaced two correctness bugs in the update/sync flow. Both lead a user to a wrong outcome on the default path: either silently miss real drift, or silently overwrite a file they customized in place.
Scope note: the headless/TTY ergonomics of update (crashes without a TTY, no --yes) are out of scope here — these two are the correctness bugs. Filing them together because they share one theme: the manual update flow can't be trusted to do the right thing by default.
Bug 1 — sync reports "up to date" while real content drift exists
sequant sync decides up-to-date status purely from the version marker:
src/commands/sync.ts:106
if (!force && skillsVersion === packageVersion) {
console.log(chalk.green("✔ Skills are already up to date!"));
Version equality ≠ content equality. On a tree where update (which diffs content) found 5 modified + 1 new file, sync printed ✔ Skills are already up to date! and exited a no-op — same 2.6.1 marker on both sides.
This matters because sync is the non-interactive / automation-recommended path (it even prints "Plugin users get auto-updates without running sync manually"). The path we point CI and scripts at is the one that declares success while leaving drift in place. The only escape is sync --force, which is undiscoverable from the "up to date" message.
Acceptance criteria
Bug 2 — In-place customizations (e.g. constitution.md) are classified modified and overwritten on the default Y
Override protection only fires when a separate .claude/.local/<file> exists:
src/commands/update.ts:151-159
// Check if there's a local override
const localOverridePath = localPath.replace(".claude/", ".claude/.local/");
const hasLocalOverride = await fileExists(localOverridePath);
if (hasLocalOverride) {
changes.push({ path: localPath, status: "local-override" });
} else {
... status: "modified"
}
The constitution is the one file explicitly meant to be edited in place per project. A user who customizes it (custom principles/patterns) — without creating a parallel .claude/.local/ file they have no reason to know about — gets it listed under Modified, with Local overrides: 0, and the default Apply updates? (Y/n) overwrites their content. That's silent data loss of intentional customization.
Compounding it, the diff compares against the raw template:
src/commands/update.ts:148
if (localContent === templateContent) { ... unchanged }
But {{PROJECT_NAME}} is only substituted at write time (update.ts:242). So constitution.md shows as modified for every initialized project even with zero user edits (installed # matcha-maps Constitution vs template # {{PROJECT_NAME}} Constitution). This trains users to hit Y on a "modified" constitution as routine — exactly when it might contain real edits.
Repro
sequant init in a project; edit .claude/memory/constitution.md in place (add a custom principle).
sequant update.
- Observe constitution under Modified, Local overrides: 0; default
Y reverts custom content to template (+ name substitution).
Acceptance criteria
Evidence (observed this session)
sequant sync → ✔ Skills are already up to date! while sequant update --dry-run on the same tree reported New files: 1 / Modified: 5 / Local overrides: 0.
update Modified list included .claude/memory/constitution.md, shown as # matcha-maps Constitution → # {{PROJECT_NAME}} Constitution. Had to restore from git after applying.
Suggested approach
sync: fall back to (or always use) content comparison; treat version marker as a fast-path hint, not the source of truth.
update: (a) render template tokens before the localContent === templateContent check; (b) treat a customizable file diverging in place as local-override (skip-by-default), not modified.
Summary
Dogfooding a manual
sequant updatein a downstream project surfaced two correctness bugs in the update/sync flow. Both lead a user to a wrong outcome on the default path: either silently miss real drift, or silently overwrite a file they customized in place.Scope note: the headless/TTY ergonomics of
update(crashes without a TTY, no--yes) are out of scope here — these two are the correctness bugs. Filing them together because they share one theme: the manual update flow can't be trusted to do the right thing by default.Bug 1 —
syncreports "up to date" while real content drift existssequant syncdecides up-to-date status purely from the version marker:Version equality ≠ content equality. On a tree where
update(which diffs content) found 5 modified + 1 new file,syncprinted✔ Skills are already up to date!and exited a no-op — same2.6.1marker on both sides.This matters because
syncis the non-interactive / automation-recommended path (it even prints "Plugin users get auto-updates without running sync manually"). The path we point CI and scripts at is the one that declares success while leaving drift in place. The only escape issync --force, which is undiscoverable from the "up to date" message.Acceptance criteria
syncdoes not claim "up to date" — it reports the drift (e.g.version current, but N files differ — run \update` or `sync --force``) and/or applies it.synconly prints "already up to date" when content is actually identical.Bug 2 — In-place customizations (e.g.
constitution.md) are classifiedmodifiedand overwritten on the defaultYOverride protection only fires when a separate
.claude/.local/<file>exists:The constitution is the one file explicitly meant to be edited in place per project. A user who customizes it (custom principles/patterns) — without creating a parallel
.claude/.local/file they have no reason to know about — gets it listed under Modified, with Local overrides: 0, and the defaultApply updates? (Y/n)overwrites their content. That's silent data loss of intentional customization.Compounding it, the diff compares against the raw template:
But
{{PROJECT_NAME}}is only substituted at write time (update.ts:242). Soconstitution.mdshows asmodifiedfor every initialized project even with zero user edits (installed# matcha-maps Constitutionvs template# {{PROJECT_NAME}} Constitution). This trains users to hitYon a "modified" constitution as routine — exactly when it might contain real edits.Repro
sequant initin a project; edit.claude/memory/constitution.mdin place (add a custom principle).sequant update.Yreverts custom content to template (+ name substitution).Acceptance criteria
--force.{{PROJECT_NAME}}(and other tokens) before diffing, so an unmodified constitution reads asunchanged, notmodified.--force) path.Evidence (observed this session)
sequant sync→✔ Skills are already up to date!whilesequant update --dry-runon the same tree reportedNew files: 1 / Modified: 5 / Local overrides: 0.updateModified list included.claude/memory/constitution.md, shown as# matcha-maps Constitution→# {{PROJECT_NAME}} Constitution. Had to restore from git after applying.Suggested approach
sync: fall back to (or always use) content comparison; treat version marker as a fast-path hint, not the source of truth.update: (a) render template tokens before thelocalContent === templateContentcheck; (b) treat a customizable file diverging in place aslocal-override(skip-by-default), notmodified.