Shard Layout (v6 contract)
June 1, 2026 · View on GitHub
Status: Design resolved for v0.1. All rules binding. Folds into
ARCHITECTURE.md §3+AUTHORING.md §2once implementation lands; until then this is the source of truth. Discussion thread: #70.
Guiding principle
From VISION.md:
Not dependent on ShardMind to function. A vault installed by ShardMind works exactly the same without ShardMind. Delete
.shardmind/andshard-values.yaml— the vault continues to work in Obsidian and Claude Code. ShardMind is additive, not load-bearing.
The mirror obligation: the shard repo must also work as a vault without shardmind. obsidian-mind's clone-and-open experience is the product that earned the flagship its 2k+ stars. ShardMind extends that experience with install-time personalization, safe upgrades, and modular composition — without subtracting anything.
Design posture: minimum viable sidecar
A shard is today's vault + a .shardmind/ sidecar. No wrapper directories, no partials/assembly system, no committed-rendered artifacts. The engine's full capability surface (Nunjucks rendering, merge, migrations, signals, values, modules, hooks, runtime) stays intact; only the shard contract is simplified.
Scope rule for this design: v0.1 ships what obsidian-mind v6 needs to install, configure, and upgrade cleanly. Features not exercised by obsidian-mind v6 live in the Out-of-scope section with explicit justifications showing they can be added later without retroactive redesign.
What a shard is
A shard is an Obsidian vault with a
.shardmind/directory. The vault is the product;.shardmind/is the opt-in sidecar that makes it installable, configurable, and upgradeable.
Three testable properties:
- The shard repo at HEAD opens cleanly as a vault in Obsidian with no preparation.
shardmind install <shard>with all defaults produces a vault byte-equivalent togit clone <shard>(modulo Tier 1 exclusions +.shardmind/engine metadata).- Deleting
.shardmind/on either side leaves a working vault.
Layout — source side (the shard repo)
my-shard/ ← git repo root; also opens cleanly as an Obsidian vault
│
├── .shardmind/ ← engine metadata (source-side)
│ ├── shard.yaml ← manifest (name, version, values refs, modules, agents, hooks)
│ ├── shard-schema.yaml ← values schema → zod at runtime (every value MUST have a default)
│ └── hooks/ ← source-side only; engine reads from tarball, does NOT copy to installed vault
│ ├── bootstrap.ts ← optional, non-fatal; unmanaged-path setup (git init, indexes)
│ ├── personalize.ts ← optional, non-fatal; managed-file edits (engine skips when values are defaults)
│ └── post-update.ts ← optional, non-fatal; additive managed-file edits on update
│
├── .shardmindignore ← at repo root; glob semantics (negation deferred to v0.2)
│
├── <vault content at native paths> ← brain/, work/, Home.md, bases/, etc. (v5.1's shape)
│
├── CLAUDE.md, AGENTS.md, GEMINI.md ← verbatim; included per agent selection
│
├── .claude/, .codex/, .gemini/ ← agent operational layers (dotfolders; .njk allowed inside)
├── .mcp.json, .obsidian/ ← config + Obsidian vault-shape config
│
├── README.md, LICENSE, CHANGELOG.md ← installed by default; vault-relevant docs
├── ARCHITECTURE.md, .gitignore ← installed if present
│
└── <repo-only> ← .github/, CONTRIBUTING.md, README.<lang>.md, demo media
(excluded via .shardmindignore; see §File disposition)
Layout — installed side (after shardmind install)
my-vault/
│
├── .shardmind/ ← engine metadata (installed-side)
│ ├── state.json ← ownership hashes + module/agent selections + version + resolved ref
│ ├── shard.yaml ← cached manifest (runtime reads without re-extracting the tarball)
│ ├── shard-schema.yaml ← cached values schema
│ ├── templates/ ← cached source files; merge base for three-way merge on update
│ └── logs/ ← full output of a crashed or verbose hook (<slot>.log); written
│ on demand so the Summary can truncate on screen and point here
│
├── shard-values.yaml ← user's answers from the wizard; vault-root (not under .shardmind/);
│ named separately from .shardmind/ per VISION's
│ "Delete .shardmind/ and shard-values.yaml — the vault
│ continues to work" contract (§What ShardMind Is Not)
│
├── <same vault content as source, with:>
│ ├── .njk files in dotfolders rendered with user values (suffix stripped)
│ ├── optional modules/agents included per wizard (default: all)
│ └── hook may have personalized managed files (bound by Invariants 2 + 3)
│
├── .shardmindignore ← installed verbatim (Tier 2); inert post-install
├── README.md, LICENSE, CHANGELOG.md
│
└── (no .github/, no CONTRIBUTING.md, no translations, no demo media)
The installed-side path constants are authoritative in source/runtime/vault-paths.ts: STATE_FILE, CACHED_MANIFEST, CACHED_SCHEMA, CACHED_TEMPLATES, HOOK_LOGS_DIR all live under .shardmind/; VALUES_FILE lives at vault root. Everything under .shardmind/ (including logs/) is engine metadata and is excluded from the Invariant 1 byte-equivalence comparison.
Personalization model
Three mechanisms.
-
Module / agent selection. Wizard values gate which files ship. Default wizard state is all modules enabled, all agent files shipped — per VISION's "ships complete" posture and Invariant 1. User deselects what they don't want.
-
.njkNunjucks rendering (author-explicit opt-in by suffix). Any file ending in.njkis rendered with user values and the suffix is stripped on install. Author convention is to keep.njkto dotfolder configs the user doesn't see —.claude/settings.json.njk,.mcp.json.njk— so the clone-UX cost stays zero. The engine doesn't enforce that convention because iterator templates (<dir>/_each.<ext>.njk) and other legitimate uses produce vault-visible output. Vault-visible{{ values.X }}without the.njksuffix is the deferredrendered_filesopt-in tracked under #86. The three-way merge onupdatehonors the same split: copy-origin (non-.njk) files are merged on their raw bytes and are never re-rendered, so a literal{{in a script or test fixture neither crashes the merge nor gets substituted (#132). -
Lifecycle hooks. Shard-author TypeScript split across three named slots with engine-enforced write boundaries —
bootstrap(unmanaged-path setup: QMD bootstrap,git init),personalize(managed-file edits likebrain/North Star.md, only when the user supplied non-default values),post-update(additive managed-file edits on update). Bound by Invariants 2 + 3 + 4 below. See §Hook lifecycle.
Installation invariants
Four hard rules the engine + authors uphold. Enforced by CI.
Invariant 1 — install --defaults is clone-equivalent
When a user runs shardmind install --defaults <shard>, the resulting vault stands in a precise relationship to git clone <shard>:
For every clone-side path P that survives Tier 1 exclusion + .shardmindignore filtering:
- Static file (P does NOT end in
.njk): the install has a file at the same path P with byte-identical content. Content hash + relative path are compared; modes and mtimes are not. - Renderable template (P ends in
.njk): the install has a file at the stripped path (P with.njkremoved). The rendered bytes legitimately differ from the source —install_date, value substitutions, frontmatter normalization. No byte comparison; presence-at-mapped-path is the contract.
The install additionally contains, never present in the clone:
- Engine metadata under
.shardmind/:state.json, cachedshard.yaml(manifest), cachedshard-schema.yaml, andtemplates/(merge-base cache). - Vault-root
shard-values.yamlwith default values serialized.
Any other delta — a clone path with no install counterpart, an install path with no clone source, a static-file byte mismatch, a Tier 1 entry that leaked through, a .shardmindignore-excluded file that ended up installed — is a shard-design or engine bug.
Enforced by a CI E2E test. The tests/e2e/helpers/invariant1.ts helper encapsulates the comparison; shardmind install --defaults is the deterministic mode that makes the test reproducible across runs.
Author guidance. The smaller a shard's render-delta surface, the closer the install is to a true clone byte-for-byte. Vault-visible content (Home.md, brain/*.md, …) is best authored as static .md and personalized via the personalize hook; renderable templates fit naturally in hidden dotfolders (.claude/settings.json.njk, .codex/config.json.njk) where Obsidian doesn't surface the .njk suffix to the user. See docs/AUTHORING.md §5 for the full convention.
Invariant 2 — Default-value installs touch no managed files (engine-enforced)
A --defaults install must stay byte-equivalent to clone, so no hook may edit a managed file when the user accepted every default. As of the hook lifecycle split this is engine-enforced, not hook-checked: the engine computes valuesAreDefaults (deep-equal each user value against its schema default) and, when true, does not invoke the personalize hook at all. The personalize slot is the only hook permitted to write managed files, and it runs solely on first install/adopt with non-default values — so a defaults install has no code path that can mutate a managed file. Authors no longer write if (!ctx.valuesAreDefaults) …; the gate moved into the engine. (Legacy post-install hooks keep the old self-check — see §Hook lifecycle.)
Invariant 3 — Post-update hooks are additive-only by default
The post-update hook receives ctx.newFiles: string[] — managed files added in this update. By default, hook writes are restricted to those paths. Writing to any other managed file risks clobbering user edits or the three-way-merge resolution that just ran. This remains a convention (engine-provided newFiles, as today) rather than a write-boundary check — unlike bootstrap and personalize, which are detected (see §Hook lifecycle).
Invariant 4 — Bootstrap re-runs only on fingerprint change
The bootstrap hook runs on every fresh install/adopt. On update it re-runs iff the manifest's hooks.bootstrap.fingerprint differs from the value recorded in state.json at the last successful bootstrap (state.bootstrap_fingerprint). A shard with no fingerprint never re-bootstraps on update; bumping the fingerprint ("qmd-v1" → "qmd-v2") forces every installed vault to re-run bootstrap on its next update. This lets a shard rebuild unmanaged artifacts (search indexes, caches) when their schema changes, without overloading post-update (which is additive-only over managed files) and without a one-shot migration. The engine compares the raw fingerprint strings (!==); it does not hash them.
Values, schema, and modules — spec rules
- Every value has a default.
shard-schema.yamlvalidator rejects values without adefaultfield. Makes Invariant 1 testable. Since v0.1 is the first contract, no migration cost. Authors model "required" behavior via non-empty defaults or hook validation. - Default wizard state = all modules + all agents selected. User deselects. Preserves Invariant 1 under default install.
- Agent selection is modeled as module gating. Shard declares
agentsinshard.yaml; each agent is a module with file patterns. Uniform mechanism; no per-agent engine code. - Module deselection = file-path gating, not section pruning. Files under deselected module paths don't install. CLAUDE.md / AGENTS.md / GEMINI.md stay whole. Per VISION: "empty folders cost nothing; unused commands sit silently."
Hook lifecycle, state, and re-hash semantics
Three named slots
The single post-install hook is split into three slots, each with a contract the engine enforces. Declared under hooks: in shard.yaml:
hooks:
bootstrap:
script: .shardmind/hooks/bootstrap.ts
fingerprint: "qmd-v1" # optional; bumping it re-runs bootstrap on update (Invariant 4)
personalize: .shardmind/hooks/personalize.ts
post-update: .shardmind/hooks/post-update.ts
timeout_ms: 30000 # optional, applies to every slot
bootstrap may also be the bare string form (bootstrap: .shardmind/hooks/bootstrap.ts) when no fingerprint is needed.
| Slot | Runs on | May write | Gate |
|---|---|---|---|
bootstrap | first install + adopt; on update iff fingerprint changed (Invariant 4) | unmanaged paths only (.qmd/, .git/, MCP caches) | always (no value gate) |
personalize | first install + adopt only | managed files only (tracked in state.json) | engine skips it entirely when valuesAreDefaults (Invariant 2) |
post-update | updates | managed files in ctx.newFiles only | Invariant 3 |
On a fresh install/adopt, slots fire in order: bootstrap then personalize (infrastructure before content). On update, bootstrap (if its fingerprint changed) then post-update.
Write-boundary enforcement (detect-and-warn)
A hook is an ordinary Node subprocess with full filesystem access; the engine cannot prevent an out-of-boundary write. Instead it detects one and surfaces a non-fatal warning — the install/update still succeeds, and the bytes the hook wrote are left in place (consistent with the non-fatal Helm contract: whatever a hook did, stays). The engine snapshots before each boundary-checked slot and diffs after:
bootstrapwrote a managed file →HOOK_BOOTSTRAP_MANAGED_WRITEwarning naming the paths. Detection folds into the post-hook re-hash: a managed file whose hash changed during bootstrap is a violation. Move the edit topersonalize.personalizecreated an unmanaged file →HOOK_PERSONALIZE_UNMANAGED_CREATEwarning. Detection is a path-only vault walk (ignore-filtered + Tier-1-filtered) before and after; install/adopt only. Scoped to creation — apersonalizethat modifies or deletes an already-present unmanaged file (e.g. bootstrap's.qmd/artifacts) is not detected, because the path set is unchanged and the check avoids content-hashing the whole vault. Move the artifact creation tobootstrap.
These are warnings, not thrown errors — see docs/ERRORS.md §Hook lifecycle (non-fatal warnings). They turn yesterday's comment-checked conventions into machine-checked signals an author sees during their dev loop.
Re-hash + state
- Hooks run after the state.json write. Unchanged.
- Engine re-hashes all managed files after the hook phase exits — success OR failure. State.json must reflect actual file content even if a hook partially failed (non-fatal contract preserved). Parallel hash compute; bounded cost. This is what makes a legitimate
personalizeedit produce zero spurious drift on the next status run. state.bootstrap_fingerprintrecords the manifest'shooks.bootstrap.fingerprint(raw string) at the last successful bootstrap. Drives the Invariant 4 re-run decision on update. Absent if the shard never declared one. Bumpsstate.jsonschema_versionto 2 (additive; forward-migrated bystate-migrator.ts).
Per-slot HookContext
The ctx is slotted — each hook receives only the fields meaningful to it (source/runtime/types.ts):
bootstrap→{ slot, vaultRoot, values, modules, shard, previousVersion? }. NovaluesAreDefaults(it always runs); no file lists.previousVersionset only on an update re-bootstrap.personalize→{ slot, vaultRoot, values, modules, shard }. NovaluesAreDefaults— the engine already enforced the gate; ifpersonalizeruns at all, values are non-default.post-update→{ slot, vaultRoot, values, modules, shard, previousVersion, newFiles, removedFiles }.newFiles= managed paths added (UpdateAction.kind === 'add');removedFiles= managed paths deleted. UseremovedFilesto maintain external state (QMD collection refs, MCP registrations) that referenced now-gone paths.
Other rules
.shardmind/hooks/is source-side only. The installed-side.shardmind/holdsstate.json+ cachedshard.yaml+ cachedshard-schema.yaml+templates/cache — not hooks. (User'sshard-values.yamllives at vault root, not inside.shardmind/.) Engine reads hook scripts from the extracted source tarball during install/update; hook scripts never get copied into the installed vault.- Hook timeout stays at the existing
DEFAULT_HOOK_TIMEOUT_MS(non-fatal on timeout);hooks.timeout_msapplies per slot.
Legacy post-install (deprecated)
A shard declaring the old hooks.post-install slot keeps working: the engine runs it once on install/adopt with the legacy combined context (the old flat HookContext, including valuesAreDefaults so existing if (!ctx.valuesAreDefaults) self-gating still fires) and no write-boundary enforcement (the old contract had none). Each run surfaces a HOOK_POST_INSTALL_DEPRECATED warning. Legacy post-update continues unchanged.
Declaring post-install together with bootstrap or personalize is rejected at parse time (HOOK_SLOT_CONFLICT) — a half-migrated manifest is a mistake, not a merge. The legacy slot is honored for at least one minor release (deprecated in 0.2.0; removed no earlier than 0.3.0). Migration guide: docs/AUTHORING.md §6.
Update semantics — spec rules
- Default: latest stable release.
shardmind updateresolves viaGET /repos/:o/:r/releases?per_page=100filtered forprerelease: false. Replaces the v0.1/releases/latestendpoint, which 404'd for beta-only repos. Closes theARCHITECTURE §10.7gap. - Prerelease opt-in.
--include-prereleaseflag widens resolution to all releases. Explicit opt-in matches npm tag conventions; safer default. When the default-stable filter eliminates every entry but prereleases exist,NO_RELEASES_PUBLISHED's hint points at this flag. --release <tag>flag. Pins to a specific tag (stable or prerelease). Mutually exclusive with--include-prerelease(pin already chose) and with ref installs (those track a moving ref). Named--releaserather than--versionbecause Pastel reserves the program-level--versionflag for printing the package version (shardmind --version); a per-command--versionwould silently collide.- Ref-install re-resolution. Vaults installed via
github:owner/repo#<ref>re-fetch HEAD of the ref on everyshardmind update— ref installs track the branch/ref.state.jsonrecords the user-passedrefand theresolvedSha(40-char commit hex) so status can show movement and the up-to-date short-circuit can fire on SHA equality. Enables the shard-author dev loop (install from#main, iterate, update to pull new commits). Ref-installed vaults reject--releaseand--include-prereleaseasUPDATE_FLAG_CONFLICT; reinstalling viashardmind install <source>@<version>is the explicit transition off the ref. - Update-check cache stays stable-only. The 24-hour cache backing
shardmind(status) is defined as "latest stable available".shardmind updateprimes the cache only when the run resolved through the latest-stable policy —--release,--include-prerelease, and ref installs all skip the prime so the cache doesn't drift into reporting a non-stable version as "latest stable".
Adopt semantics — shardmind adopt <shard>
For users who cloned before shardmind support (obsidian-mind v5.1 and earlier) and want to adopt the update engine retroactively.
Pre-conditions enforced before any walk:
.shardmind/state.jsonmust NOT exist. Adopt is for un-adopted vaults; an existing install routes throughshardmind update.shard-values.yamlat the vault root must NOT exist. The engine writes it at adopt-finish; a pre-existing one is an inconsistent state and surfacesVALUES_FILE_COLLISION(same code install uses).
Phases (logical order; UI may interleave loading messages):
- Fetch shard at target version into a temp directory.
- Collect values via the
AdoptValuesGateconfirm-or-override page (#104) and module selections — same value pipeline as install. Runs before classification because.njktemplates need values to render before their output bytes can be hashed. - Classify each file the shard would install at the chosen module selections, comparing the rendered (or copied) bytes against the user's vault:
- Matches shard content exactly → record hash, mark managed automatically. "Exactly" means byte-for-byte equality after the standard render pipeline (frontmatter normalized via
parseYaml → stringifyYaml, seerenderer.ts). A pristine clone with default values + clean YAML lands here for every file; non-default vaults legitimately producediffersfor any rendered output the user's bytes don't post-render-equal. This is the same equalitydrift.tsenforces on update. - Differs from shard content → resolved per a batch mode (#120). When ≥1 file differs and neither
--modenor--yesis set,AdoptModePickerprompts once for the whole set: keep all mine / use all theirs / auto-merge (best-effort) / decide per file. Eachdiffersfile ends as one of:keep_mine(record user's hash, ownershipmodified),use_shard(overwrite, ownershipmanaged), ormerged(write union bytes, ownershipmodified). There is no "leave untracked" outcome — adopt is the moment the file becomes managed; the merge engine handles later edits on update. Auto-merge is a two-way union merge (core/adopt-merge.ts): adopt has no merge base, so it keeps common + each side's unique lines, sends overlapping replacements to the per-file prompt, does not apply shard deletions, and can duplicate non-adjacent edits — merged files are flagged "review recommended". Non-interactive auto-merge keeps-mine on conflicts. - Volatile templates (carry
{# shardmind: volatile #}) skip the prompt: user's bytes are recorded as managed without a differs comparison (volatile content is never expected to match across renders, so a prompt would be meaningless). Symmetric with install, which records volatile-template outputs the same way. - Excluded modules' files are not classified. If the user's vault contains them, they stay as user content.
- User has the path but it's not a shard output → user-only, left unmanaged (not in
state.files). - Shard has the path but the user's vault doesn't → shard-only, installed fresh and recorded as managed.
- Matches shard content exactly → record hash, mark managed automatically. "Exactly" means byte-for-byte equality after the standard render pipeline (frontmatter normalized via
- For every
differsdecision, apply: write shard bytes foruse_shard, union bytes formerged, leave user bytes forkeep_mine. All three becomestate.filesentries (hashed at the user's / merged / shard bytes respectively) — adopt is the entry point into management. - Write
.shardmind/state.json+ cached.shardmind/shard.yaml+ cached.shardmind/shard-schema.yaml+ vault-rootshard-values.yaml; cache the shard source under.shardmind/templates/so futureupdateruns have a merge base. - Run the install-side hook slots via the orchestrator:
bootstrap(always), thenpersonalize(managed edits) unless the engine skips it because values are defaults (Invariant 2).newFiles= paths classified shard-only and freshly installed,removedFiles= []. - Re-hash managed files per the usual post-hook semantics.
Future shardmind update calls work normally — merge base is the adopt-time cache.
Reuses: drift detection (core/drift.ts), install-executor, value collection (AdoptValuesGate confirm page → InstallWizard on override, #104), hook runtime. New surfaces: batch mode picker (AdoptModePicker, #120), two-way union merge (core/adopt-merge.ts, #120), 2-way diff UI component (AdoptDiffView), adopt-planner, adopt-executor.
Naming decisions
| Thing | Name | Rationale |
|---|---|---|
| Engine metadata dir | .shardmind/ on both sides | Mirror; same semantics source ↔ installed |
| Exclusion file | .shardmindignore at repo root | .gitignore convention; more discoverable than nested |
| Ignore-file semantics | Glob-only in v0.1 (negation deferred to v0.2) | obsidian-mind's patterns are simple excludes; negation not exercised |
| Dotfolder render marker | .njk suffix | Obsidian hides dotfolders; no clone-UX cost |
No templates/ in vocabulary | — | Obsidian reserves templates/ for user note templates |
File disposition
Three tiers. Default is install. Engine subtracts the minimum necessary; authors use .shardmindignore for the rest.
Tier 1 — engine-enforced exclusions (always excluded)
Not author-configurable. Would break things or are meaningless off-GitHub:
.shardmind/(source-side) — installed side gets a fresh one with different contents.git/— VCS database.github/— GitHub CI, issue templates,FUNDING.yml(defensive: prevents accidental Actions activation if user git-pushes their vault).obsidian/workspace.json,.obsidian/workspace-mobile.json,.obsidian/graph.json— Obsidian ephemeral user-specific state- Symbolic links anywhere in the shard source — engine rejects with a clear error during the install walk. Security baseline: an untrusted shard could symlink outside the install target.
Other Obsidian user-state files (starred.json, bookmarks.json, backlink.json, page-preview.json) are author-controlled via .shardmindignore. obsidian-mind v5.1 commits none of these, so no practical issue.
Tier 2 — default-included
Everything else at the shard root. The annotations below cover the two audiences the layout must serve:
| File | In the vault (installed user) | In the repo (contributor) |
|---|---|---|
README.md | Instructions manual | GitHub landing page |
LICENSE | Attribution | Legal terms |
CHANGELOG.md | What changed — surfaced post-update | Release notes |
ARCHITECTURE.md (if shipped) | How the vault is structured | Design rationale |
Home.md | Obsidian landing note | Same |
.gitignore | Useful if user gits their vault | Git hygiene |
.obsidian/ (minus Tier 1) | Vault-shape config; plugins pre-enabled | Same |
.claude/, .codex/, .gemini/, .mcp.json, .claude-plugin/ | Operational layer | Same |
scripts/ | Vault-bundled scripts (QMD bootstrap) | Same |
vault-manifest.json | Shard-author config; vault content | Same |
bases/, brain/, work/, … | Vault content folders | Same |
templates/ | Obsidian's native user-templates folder | Same |
Tier 3 — author-controlled via .shardmindignore
Glob-only in v0.1. Typical obsidian-mind-shaped shard:
# Repo-meta — meaningful on GitHub, noise in a vault
CONTRIBUTING.md
README.*.md # translations (README.ja.md, README.ko.md, …)
# Marketing media — not vault content
*.gif
*.png
obsidian-mind-logo.*
Rule of thumb: if a file is a property of the GitHub repo, exclude it. If it's about the shard's content, leave it installed.
Engine change scope
Paths reference current code. Detail to land in ARCHITECTURE.md §3 + IMPLEMENTATION.md §4.*.
Walk + discovery
source/core/modules.ts— replacetemplates/walk with shard-root walk; apply Tier 1 exclusions + root-level.shardmindignore. Remove partials gating (mod.partials). Reject symlinks with a clear error.source/core/state.ts:117-119— replace "Missingtemplates/" error with.shardmind/shard.yaml-absence check.source/core/download.ts:78-79— look for manifest/schema under.shardmind/in the extracted tarball.source/core/fs-utils.ts:25-27— removestripTemplatePrefixhelper (dead under flat layout).source/runtime/vault-paths.ts— addSHARD_SOURCE_DIR = '.shardmind'; keep installed-side.shardmind/templates/cache constant.- New parser:
.shardmindignoreglob matcher (gitignore semantics minus negation). - New data: canonical Tier 1 exclusion set.
Schema + values
source/core/schema.ts:66— remove deadpartialsfield; add validation that every value has adefault(reject at parse time if missing).source/runtime/types.ts:71— removepartials?: string[]from module type.
Hooks + state
source/runtime/types.ts— extendHookContextwithvaluesAreDefaults: boolean,newFiles: string[],removedFiles: string[].- Engine plumbing for the new ctx fields + post-hook re-hash — split across:
source/core/values-defaults.ts(new) — purevaluesAreDefaults(values, schema)for Invariant 2; deep-equal user values against the would-be-default map (literal defaults + computed defaults resolved against the literal-default map).source/core/update-executor.ts— surfaceaddedFiles: string[](paths fromUpdateAction.kind === 'add') and the existingdeletedFiles: string[]onUpdateSummaryso the update machine can wirenewFiles/removedFileswithout re-deriving from the plan.source/core/state.ts::rehashManagedFiles(vaultRoot, state)(new) — parallel re-read + sha256 of every managed file; per-file ENOENT / EACCES tolerated.- Superseded by the #102 hook lifecycle split. Re-hash +
writeStatenow live insource/core/hook-orchestrator.ts(runHooks), which the three command machines call after building aHookRunPlan; the standalonepostHookRehashhelper was removed. Re-hash +writeStateare still skipped when nothing changed. See §Hook lifecycle and IMPLEMENTATION.md §4.16a.
Registry + update
source/core/registry.ts—github:owner/repo#<ref>syntax (subsumes #67); record resolved commit SHA for ref installs.source/core/update-check.ts— default resolution via/releasesfiltered non-prerelease;--include-prereleasewidens. For ref-installs, re-resolve ref HEAD on every update.source/commands/update.tsx— add--release <tag>flag (named--releaserather than--versionbecause Pastel reserves the program-level--version); add--include-prereleaseflag.source/runtime/types.ts—ShardState.ref?+ShardState.resolvedSha?for ref installs.
Adopt
- New command:
source/commands/adopt.tsx+source/commands/hooks/use-adopt-machine.ts. - New component: 2-way diff UI (
source/components/AdoptDiffView.tsx) + per-file prompt flow. source/core/adopt-planner.ts— walk existing vault, classify each file (matches-shard, differs-from-shard, user-created, shard-only), plan adoption operations.source/core/adopt-executor.ts— apply plan; write installed-side metadata (.shardmind/state.json+ cachedshard.yaml+ cachedshard-schema.yaml+.shardmind/templates/cache) and vault-rootshard-values.yaml; run post-install hook; re-hash managed files.
Install non-interactive mode
source/commands/install.tsx— add--defaultsflag that accepts all schema defaults, enables all modules + agents, skips wizard prompts. Used by Invariant 1 CI test; also useful for CI/scripting.
Testing
- Invariant 1 byte-equivalence E2E test. Clone shard repo to dir-A; run
shardmind install --defaultsto dir-B; recursively compare file trees. Expected delta: Tier 1 absent in B; engine metadata present in B (.shardmind/state.json,.shardmind/shard.yaml,.shardmind/shard-schema.yaml,.shardmind/templates/, and vault-rootshard-values.yaml); content of all other files identical (content-hash match; modes and mtimes not compared). Any other diff fails. - Unit tests:
valuesAreDefaultscomputation,newFiles/removedFilesdiff,.shardmindignoreglob matching, symlink rejection. - Migrate
examples/minimal-shard/to flat layout. - Migrate
tests/fixtures/shards/tarballs. - Verify
tests/fixtures/merge/*don't referencetemplates/prefixes.
Why shardmind install beats git clone
The adoption pitch for obsidian-mind v6 users:
- Configured on install. Wizard applies values, modules, agents; hooks finish the job. No hand-editing.
- Modular. Skip modules you don't want — vault sized to your life.
- Safe upgrades.
shardmind updatethree-way-merges your edits with upstream. Backstage has had this open for three years. This is the moat perVISION.md §The Moat. - Drift visibility.
shardmindstatus shows stale / diverged / user-created. - Retroactive adopt. Cloned v5.1 already?
shardmind adopt github:breferrari/obsidian-mindreconciles your vault in place.
Compressed: clone is free but frozen; install (or adopt) gives you a configured, upgradeable vault.
Out of scope — deferred to v0.2
Criterion: obsidian-mind v6 does not need these to install, configure, or upgrade cleanly. Each is a clean additive extension — deferring doesn't force retroactive design changes.
| Deferred | Why not needed for v6 | How it's added later without redesign |
|---|---|---|
rendered_files opt-in (Nunjucks at vault-visible paths) | obsidian-mind uses post-install hook to personalize brain/North Star.md; no {{ }} at vault-visible paths | New optional field in shard.yaml; renderer.ts extended to include files in the list during install. Existing rendered_files: undefined behavior stays |
.shardmindignore negation (!pattern) | obsidian-mind's patterns are simple excludes; no negation needed | Parser upgrade; existing glob-only files keep working |
Rename migrations + adopt --from-version | v6.0 = v5.1's structure + .shardmind/ sidecar (zero renames vs v5.1). If a future obsidian-mind release introduces renames, rename migrations must ship before that release | New migrations field in shard.yaml; update-planner + adopt-planner consume it. Missing field = no-op (current behavior) |
| Shard composition (multi-shard per vault) | One shard per vault in v0.1 | State.json extends from {shard, version} to {shards: [...]}; single-shard remains the special case. No break |
| Dependency fetching | Shards vendor deps (obsidian-mind already does this) | shard.yaml gets dependencies: []; engine fetches on install. No break |
| Structural variants | obsidian-mind is a single shard | Future feature; orthogonal to layout |
shardmind init | obsidian-mind author already has the shard; manual scaffolding works | New command; doesn't interact with existing install/update |
shardmind eject | Manual rm -rf .shardmind/ shard-values.yaml works per VISION's additive principle ("delete .shardmind/ and shard-values.yaml — the vault continues to work ... ShardMind is additive, not load-bearing") | New command; orchestrates the manual delete + optional backup |
| SOUL guided creation | Obsidian-mind product feature, not a shardmind engine concern | — |
Transition
No shard migration required — zero shards published under the v0.1 templates/ contract. obsidian-mind v6 is the first shard under this contract.
examples/minimal-shard/restructures to the flat layout during Day 1-4 build.- obsidian-mind v6 conversion (Day 5): v5.1's structure +
.shardmind/sidecar + any dotfolder.njkfor config rendering. - Research-wiki shard (Day 6): same flat layout; uses hooks for any personalization (no
rendered_filesdependency). docs/ARCHITECTURE.md §3,docs/AUTHORING.md §2+ §7,docs/IMPLEMENTATION.md §4.*rewrite once this design lands in code.- #67 (branch/ref install) and #69 (
.shardmind/source layout) subsumed by #70.