Summary
UpdateChecker.checkPackUpdates() (Sources/mcs/Core/UpdateChecker.swift:173) compares the remote HEAD SHA (via git ls-remote) against the locally recorded entry.commitSHA. Any difference surfaces as "pack update available" — regardless of what actually changed.
In practice, upstream pack repos accrue commits that have zero effect on what mcs sync installs: README tweaks, LICENSE updates, CHANGELOG edits, CI workflow changes, .github/ config, etc. Users get update notifications (including the STOP. Before doing ANY work… hook banner) for commits that would produce an identical install. This trains users to ignore the notification, which is the worst outcome for an update checker.
This is also connected to a second annoyance: mcs pack validate emits is not referenced by any component or template warnings (PackHeuristics.checkUnreferencedFiles, PackHeuristics.swift:108) for exactly these same files. Today pack authors have no way to quiet those warnings short of deleting the files, which they legitimately want to keep in the repo.
Both problems share a root cause: the engine has no concept of "files that exist in the pack but aren't material to installation".
Proposed solution
Apply a deny-list filter after the SHA-change trigger, reusing heuristics that already exist in the codebase.
Phase 1 — Engine-side filter
When git ls-remote reports a new SHA, do a shallow git fetch --depth 1 + git diff --name-only localSHA..FETCH_HEAD against the existing pack checkout, then filter the changed-path list against:
PackHeuristics.ignoredDirectories (.git, .github, .gitlab, .vscode, node_modules, __pycache__, .build)
PackHeuristics.infrastructureFiles minus techpack.yaml (see invariant below) — README.md, README, LICENSE, LICENSE.md, CHANGELOG.md, CONTRIBUTING.md, .gitignore, .editorconfig, package.json, package-lock.json, requirements.txt, Makefile, Dockerfile, .dockerignore
If the filtered set is empty → suppress the notification AND advance entry.commitSHA to the new SHA so the same no-op commits don't re-trigger on every cooldown window.
If the filtered set is non-empty → show the notification as today (optionally include the material paths in the message — "Dev Essentials updated: hooks/session-start.sh").
Why deny-list, not allow-list: we considered intersecting changes with PackHeuristics.referencedPaths(from:) (PackHeuristics.swift:87), which enumerates copyPackFile.source, settingsFile.source, template.contentFile, and configureProject.script. Rejected: that set does not include shellCommand script paths or a configure script's transitive dependencies, so an allow-list would produce false negatives (hiding real updates). Deny-list treats unknown files as material — safe default.
Phase 1a — Hard invariant: techpack.yaml is always material
A manifest edit can change the install surface entirely: new copyPackFile sources, different shell: actions, added/removed hooks, different MCP server commands. Silently suppressing a manifest-only commit is a supply-chain attack vector — a pack author (malicious or compromised) could swap hook scripts or MCP commands without the user ever seeing a notification.
Therefore:
techpack.yaml is unconditionally material. Any commit that touches it surfaces the update notification, regardless of ignore lists, built-in or author-supplied.
- The update-check deny-list uses
infrastructureFiles \ {techpack.yaml}, NOT the raw set. Today PackHeuristics.infrastructureFiles at PackHeuristics.swift:171 contains Constants.ExternalPacks.manifestFilename because it is intentional infrastructure for the checkRootLevelContentFiles use case. Reusing the set as-is for update-check filtering would silently suppress manifest changes. The two call sites need distinct effective sets — worth naming explicitly in code so a future reader doesn't "deduplicate" them.
Phase 2 — Pack-author ignore: field
Add an optional ignore: list to techpack.yaml:
identifier: my-pack
displayName: My Pack
ignore:
- docs/
- examples/
- diagrams/*.png
Semantics:
- Entries merge with the built-in ignore lists (authors extend, they don't replace)
- Used by both
UpdateChecker (for the filter above) AND PackHeuristics.checkUnreferencedFiles (to silence the validation warning)
- Glob support via
fnmatch to handle docs/*.md-style patterns
Phase 2a — Validation of ignore: entries (belt-and-suspenders)
The ignore: list cannot contain anything the engine relies on. Enforce at two layers:
mcs pack validate rejects as an error (publish-time guard) — authors catch mistakes before shipping. Rejection rules:
techpack.yaml is forbidden (manifest is always tracked).
- Any path in
PackHeuristics.referencedPaths(from: manifest) is forbidden — you cannot silence a file the manifest claims to use. Error shape:
ignore: entry 'hooks/session-start.sh' is referenced by component 'session-hook'.
Remove it from ignore: or remove the component.
- Runtime loader strips forbidden entries silently with a warn log (user-safety guard) — if a malformed manifest somehow reaches a user machine (older mcs version, hand-edited file), their install doesn't break. Worst case: the author's invalid suppression silently doesn't apply; they figure it out when notifications keep firing.
Why both: blocking alone risks breaking working installs when authors publish bad manifests; stripping alone hides the error from authors who wonder why their ignore entry "doesn't work". Together: loud feedback at publish time, safe fallback at runtime.
Phase 3 — Remediation hint in mcs pack validate
When checkUnreferencedFiles emits warnings, append a hint:
Add these paths to the ignore: field in techpack.yaml if they are intentionally not installed (docs, examples, assets).
This closes the loop: users discover the field naturally through validation, and they're motivated to add entries because it quiets both the validation warning AND the spurious update notifications downstream.
Cost / tradeoffs
- One shallow
git fetch per genuinely-changed pack per cooldown window (24h). For the typical case where 0–1 packs have new commits at a time, this is negligible vs. the current ls-remote-only path.
- Network-failure fallback: if the deep check fails (offline, rate-limited), surface the notification anyway. The filter can only suppress, never manufacture silence on error — preserves the "never hide a real update" invariant.
- Cache invalidation:
UpdateChecker.loadCache() (UpdateChecker.swift:122) already invalidates on CLI version change; may also want to invalidate when a pack's manifest ignore: list changes (hash the list, store in CachedResult).
Scope
Sources/mcs/Core/UpdateChecker.swift — add post-SHA-change filter and baseline advancement
Sources/mcs/ExternalPack/ExternalPackManifest.swift — add ignore: [String]? field; validate rejects forbidden entries
Sources/mcs/ExternalPack/PackHeuristics.swift — respect manifest ignore: in checkUnreferencedFiles, add remediation hint, expose infrastructureFilesForUpdateCheck or equivalent so the \ {techpack.yaml} distinction is explicit
Sources/mcs/Commands/ValidatePackCommand.swift — wire through the new hint
Test coverage
- Unit: filter-only-noise commits → suppressed + baseline advances
- Unit: commit touching
techpack.yaml only → always surfaced, never suppressed, even if author put techpack.yaml in ignore:
- Unit: mixed commits (README + hook script) → surfaced as "hook script changed"
- Unit: manifest
ignore: entries extend built-in sets (union, not replace)
- Unit: glob matching for
ignore: patterns
- Unit:
mcs pack validate rejects ignore: containing techpack.yaml or a referenced path, with actionable error
- Unit: runtime loader strips forbidden
ignore: entries with a warn log
- Integration:
mcs pack validate on a pack with ignore: entries does not emit unreferenced-file warnings for those paths
Summary
UpdateChecker.checkPackUpdates()(Sources/mcs/Core/UpdateChecker.swift:173) compares the remote HEAD SHA (viagit ls-remote) against the locally recordedentry.commitSHA. Any difference surfaces as "pack update available" — regardless of what actually changed.In practice, upstream pack repos accrue commits that have zero effect on what
mcs syncinstalls: README tweaks, LICENSE updates, CHANGELOG edits, CI workflow changes,.github/config, etc. Users get update notifications (including theSTOP. Before doing ANY work…hook banner) for commits that would produce an identical install. This trains users to ignore the notification, which is the worst outcome for an update checker.This is also connected to a second annoyance:
mcs pack validateemitsis not referenced by any component or templatewarnings (PackHeuristics.checkUnreferencedFiles,PackHeuristics.swift:108) for exactly these same files. Today pack authors have no way to quiet those warnings short of deleting the files, which they legitimately want to keep in the repo.Both problems share a root cause: the engine has no concept of "files that exist in the pack but aren't material to installation".
Proposed solution
Apply a deny-list filter after the SHA-change trigger, reusing heuristics that already exist in the codebase.
Phase 1 — Engine-side filter
When
git ls-remotereports a new SHA, do a shallowgit fetch --depth 1+git diff --name-only localSHA..FETCH_HEADagainst the existing pack checkout, then filter the changed-path list against:PackHeuristics.ignoredDirectories(.git,.github,.gitlab,.vscode,node_modules,__pycache__,.build)PackHeuristics.infrastructureFilesminustechpack.yaml(see invariant below) —README.md,README,LICENSE,LICENSE.md,CHANGELOG.md,CONTRIBUTING.md,.gitignore,.editorconfig,package.json,package-lock.json,requirements.txt,Makefile,Dockerfile,.dockerignoreIf the filtered set is empty → suppress the notification AND advance
entry.commitSHAto the new SHA so the same no-op commits don't re-trigger on every cooldown window.If the filtered set is non-empty → show the notification as today (optionally include the material paths in the message — "Dev Essentials updated:
hooks/session-start.sh").Why deny-list, not allow-list: we considered intersecting changes with
PackHeuristics.referencedPaths(from:)(PackHeuristics.swift:87), which enumeratescopyPackFile.source,settingsFile.source,template.contentFile, andconfigureProject.script. Rejected: that set does not includeshellCommandscript paths or a configure script's transitive dependencies, so an allow-list would produce false negatives (hiding real updates). Deny-list treats unknown files as material — safe default.Phase 1a — Hard invariant:
techpack.yamlis always materialA manifest edit can change the install surface entirely: new
copyPackFilesources, differentshell:actions, added/removed hooks, different MCP server commands. Silently suppressing a manifest-only commit is a supply-chain attack vector — a pack author (malicious or compromised) could swap hook scripts or MCP commands without the user ever seeing a notification.Therefore:
techpack.yamlis unconditionally material. Any commit that touches it surfaces the update notification, regardless of ignore lists, built-in or author-supplied.infrastructureFiles \ {techpack.yaml}, NOT the raw set. TodayPackHeuristics.infrastructureFilesatPackHeuristics.swift:171containsConstants.ExternalPacks.manifestFilenamebecause it is intentional infrastructure for thecheckRootLevelContentFilesuse case. Reusing the set as-is for update-check filtering would silently suppress manifest changes. The two call sites need distinct effective sets — worth naming explicitly in code so a future reader doesn't "deduplicate" them.Phase 2 — Pack-author
ignore:fieldAdd an optional
ignore:list totechpack.yaml:Semantics:
UpdateChecker(for the filter above) ANDPackHeuristics.checkUnreferencedFiles(to silence the validation warning)fnmatchto handledocs/*.md-style patternsPhase 2a — Validation of
ignore:entries (belt-and-suspenders)The
ignore:list cannot contain anything the engine relies on. Enforce at two layers:mcs pack validaterejects as an error (publish-time guard) — authors catch mistakes before shipping. Rejection rules:techpack.yamlis forbidden (manifest is always tracked).PackHeuristics.referencedPaths(from: manifest)is forbidden — you cannot silence a file the manifest claims to use. Error shape:Why both: blocking alone risks breaking working installs when authors publish bad manifests; stripping alone hides the error from authors who wonder why their ignore entry "doesn't work". Together: loud feedback at publish time, safe fallback at runtime.
Phase 3 — Remediation hint in
mcs pack validateWhen
checkUnreferencedFilesemits warnings, append a hint:This closes the loop: users discover the field naturally through validation, and they're motivated to add entries because it quiets both the validation warning AND the spurious update notifications downstream.
Cost / tradeoffs
git fetchper genuinely-changed pack per cooldown window (24h). For the typical case where 0–1 packs have new commits at a time, this is negligible vs. the currentls-remote-only path.UpdateChecker.loadCache()(UpdateChecker.swift:122) already invalidates on CLI version change; may also want to invalidate when a pack's manifestignore:list changes (hash the list, store inCachedResult).Scope
Sources/mcs/Core/UpdateChecker.swift— add post-SHA-change filter and baseline advancementSources/mcs/ExternalPack/ExternalPackManifest.swift— addignore: [String]?field; validate rejects forbidden entriesSources/mcs/ExternalPack/PackHeuristics.swift— respect manifestignore:incheckUnreferencedFiles, add remediation hint, exposeinfrastructureFilesForUpdateCheckor equivalent so the\ {techpack.yaml}distinction is explicitSources/mcs/Commands/ValidatePackCommand.swift— wire through the new hintTest coverage
techpack.yamlonly → always surfaced, never suppressed, even if author puttechpack.yamlinignore:ignore:entries extend built-in sets (union, not replace)ignore:patternsmcs pack validaterejectsignore:containingtechpack.yamlor a referenced path, with actionable errorignore:entries with a warn logmcs pack validateon a pack withignore:entries does not emit unreferenced-file warnings for those paths