Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when you need to populate the Meta-Vault with machine-generated notes derived from session-orchestrator JSONL records. Converts entries from `.orchestrator/metrics/sessions.jsonl` and `.orchestrator/metrics/learnings.jsonl` into vault-conformant Markdown under `50-sessions/` and `40-learnings/`. Called automatically at session-end Phase 3.7 and after evolve Phase 3.5 — only when `vault-integration.enabled=true` and `vault-integration.mode != "off"`. Idempotent: re-runs safely; skips hand-aut
.claude/skills/hashgraph-online-vault-mirror/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | 176% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 251% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 298% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 429% | 0% |
| case-16 | ✗→✓ | ▲ Improved | 255% | 0% |
> Project-instruction file resolution: CLAUDE.md and AGENTS.md (Codex CLI) are transparent aliases — see skills/_shared/instruction-file-resolution.md. When this skill mentions Session Config in CLAUDE.md, the alias rule applies.
vault-mirror populates the Meta-Vault with machine-generated notes derived from structured JSONL records. It converts entries from .orchestrator/metrics/sessions.jsonl and .orchestrator/metrics/learnings.jsonl into vault-conformant Markdown files under numeric-prefix subdirectories. This is distinct from vault-sync, which validates the vault — vault-sync validates the vault; vault-mirror populates it. The two skills are complementary: vault-mirror writes notes, vault-sync checks that the vault as a whole remains conformant.
vault-mirror is called in two places:
skills/session-end/session-metrics-write.md) — mirrors the sessions.jsonl entry for the closing session.skills/evolve/SKILL.md) — mirrors all learnings.jsonl entries added during the evolve cycle.Both call sites are conditional: vault-mirror runs only when vault-integration.enabled == true AND vault-integration.mode != "off" in the project's Session Config. When either condition is not met, the call site skips silently and vault-mirror is never invoked.
scripts/vault-mirror.mjs is the implementation. All arguments are required except --dry-run.
| Flag | Type | Required | Description | |---|---|---|---| | --vault-dir | path | yes | Absolute path to the Meta-Vault root directory. Must exist. | | --source | path | yes | Path to the JSONL file to read (one JSON object per line). Must exist. | | --kind | session or learning | yes | Determines which generator and target path are used. | | --dry-run | flag | no | Parse and resolve paths but do not write any files. Emits action lines as normal. |
Empty lines in the JSONL source are silently skipped.
One JSON line is written to stdout for each non-empty JSONL entry processed. Exit code reflects the run outcome.
| action | Meaning | |---|---| | created | Entry did not exist in the vault; file created. | | updated | Entry existed (generator marker present, same id) with an older updated date; file overwritten. | | skipped-noop | Entry existed, same id, updated date not advanced; file unchanged. | | skipped-handwritten | A file at the target path has no _generator marker (or an unknown generator); left untouched. | | skipped-collision-resolved | A file at the target path has the generator marker but a different id; a disambiguated slug was used instead. | | skipped-invalid | Entry is missing one or more required fields; entry skipped, processing continues. | | skipped-quality-low | Entry failed the quality gate (PRD F1.2): learning confidence below vault-mirror.quality.min-confidence (CLI: --quality-min-confidence, default 0.5), or session rendered-narrative length below vault-mirror.quality.min-narrative-chars (CLI: --quality-min-narrative-chars, default 400). The emitted JSON line includes a reason field describing the violated threshold and path: null (no file was created). The quality gate runs before --force; --force does not bypass it. |
json{"action":"created","path":"50-sessions/session-orchestrator/session-2026-04-13.md","kind":"session","id":"session-2026-04-13"}
path is relative to --vault-dir.
| Code | Meaning | |---|---| | 0 | Success (including idempotent no-ops and per-entry skips). | | 1 | Malformed JSON on a JSONL line — fatal, processing stops. Also returned when required CLI args are missing. | | 2 | Filesystem error: --vault-dir not found, --source not found, or an unexpected write error. |
| Kind | Target | |---|---| | session | <vault-dir>/50-sessions/<repo>/<session-id>.md | | learning | <vault-dir>/40-learnings/<repo>/<slug>.md |
Subdirectories are created automatically with mkdirSync({ recursive: true }) when writing (not in --dry-run mode).
The numeric prefix (50-sessions/, 40-learnings/) follows the vault folder ordering convention so that sessions and learnings appear in the correct position in the vault tree relative to other note types.
Per-project namespacing (#660). New writes are namespaced under a per-repo subdirectory <repo>/, so a single shared vault can hold notes from multiple projects without cross-repo slug/id collisions. <repo> is resolved by resolveRepoNamespace() (scripts/lib/vault-mirror/namespace.mjs): the optional vault-integration.vault-name Session Config key (CLI: --vault-name) when set, else the git-origin repo slug via deriveRepo(), sanitised to a single kebab segment. Owner-privacy leaks (personal home path / private project slug / personal name) are redacted to redacted-repo before any write. The legacy flat layout (40-learnings/<slug>.md) is still read — the writer dual-probes the flat path so a pre-existing flat note is never duplicated; a one-time relocation of the historical flat corpus is tracked as a follow-up.
Path contract note: Issue #187 shipped the flat numeric-prefix layout (deferring the per-<repo> subfolder it sketched). Issue #660 adds that per-repo subfolder for write-isolation (above) while keeping the numeric-prefix ordering. If you need a different layout, file a new issue — do NOT silently change the script.
Learning-coverage self-healing after a store recovery (#740). vault-mirror always mirrors the CURRENT learnings.jsonl store contents at run time — if an earlier session's store was truncated or corrupted and later recovered (e.g. from a .bak), the resulting transient coverage gap in the vault is closed automatically by the next mirror run, with no manual backfill required. Note also that vault-mirror.quality.min-narrative-chars (see Failure Modes → skipped-quality-low below) gates sessions only — processLearning never applies a narrative-length filter, so a short learning insight is never skipped for length; only the confidence gate (vault-integration.quality.min-confidence) applies to learnings.
Host-local pseudonym mapping (Epic #725 D5). Without a map, EVERY owner-leaky repo namespace collapses to a single redacted-repo/ subdir — which breaks the #660 write-isolation (N private repos share one directory → id/slug collisions) and destroys cross-repo attribution. A host-local pseudonym map fixes this: each real repo slug is assigned a stable, non-leaky pseudonym, so the vault gets 40-learnings/<pseudonym>/ (and matching source-repo frontmatter, via W2's attribution threading) instead of everyone landing in redacted-repo/. The map is a JSON object { "real-slug": "pseudonym-slug", … } at a host-local path referenced by owner.yaml → paths.namespace-map-path (env SO_NAMESPACE_MAP overrides; precedence env > owner.yaml > unset). resolveRepoNamespace() consults the map ONLY at the redaction site (i.e. only when a repo would otherwise be redacted), looking up both the sanitised segment and the raw identifier; a hit returns the pseudonym and skips redaction, so clean/public repos never touch the map (or its lazy owner.yaml read). Privacy guarantee: the map file lives OUTSIDE every repo (alongside owner.yaml under ~/.config/session-orchestrator/) and is never committed — only the pseudonyms reach the vault, the real names stay host-local. Each pseudonym is validated (must be a kebab slug AND must not itself be owner-leaky); invalid or leaky pseudonyms are dropped with a WARN. Fallback: an absent, unreadable, or malformed map — and any UNMAPPED owner-leaky repo — still redacts to redacted-repo, identical to pre-#725 behaviour. Implementation: scripts/lib/vault-mirror/pseudonym-map.mjs (loadPseudonymMap) + namespace.mjs.
vault-mirror is safe to run multiple times against the same JSONL source:
_generator marker present, same id, updated date not advanced → skipped-noop, file unchanged._generator marker present, same id, updated date advanced → updated, file overwritten._generator marker → skipped-handwritten, file untouched. The absence of the marker means the file was written by a human and must not be overwritten automatically._generator marker present, different id → slug collision. A disambiguated slug is derived by appending -<first-8-chars-of-entry-uuid> (hyphens stripped from the UUID before taking the prefix). The original file is left unchanged; the new note is written at the disambiguated path with action skipped-collision-resolved.The generator marker value is session-orchestrator-vault-mirror@1 and appears in the YAML frontmatter as _generator: session-orchestrator-vault-mirror@1.
After a successful mirror pass, scripts/lib/vault-mirror/auto-commit.mjs (autoCommitVaultMirror(vaultDirPath, sessionId, repo)) optionally commits the freshly-written mirror artifacts in 40-learnings/ and 50-sessions/ as a single chore(vault): mirror … commit. It runs unattended at session-end Phase 3.7 / evolve Phase 3.5. The phase is fail-safe: it never throws, emits one JSON action line on stdout, and aborts (unstaging everything) if any staged path is not a generator-stamped mirror artifact. When the optional repo namespace is passed (#660), staging is scoped to 40-learnings/<repo> + 50-sessions/<repo> and the phase additionally aborts with reason cross-repo-staged-changes if any staged file belongs to a different repo's namespace — so one project's mirror run can never commit another project's notes. Omitting repo preserves the legacy whole-directory behaviour.
--no-verify)The auto-commit deliberately commits with --no-verify, skipping the vault repo's pre-commit hooks. This bypass is intentional (issue #603), not gratuitous, for two reasons:
isMirrorArtifact() on every staged path — a per-file frontmatter check for the _generator: session-orchestrator-vault-mirror@1 marker. Any staged file lacking the marker triggers a full unstage and an auto-commit-skipped no-op. The committed files were written by vault-mirror's own generator, which already enforces conformant frontmatter (and the quality gate). The vault's pre-commit frontmatter / wiki-link validator would only re-check what is already guaranteed.Per .claude/rules/development.md Git Safety Protocol — "Never skip hooks (`--no-verify`) unless the user has explicitly asked for it" — the bypass is permissible here because it is explicit, documented, and committing already-validated content. A regression test in tests/lib/vault-mirror/auto-commit.test.mjs pins --no-verify into the commit git-args so a future silent removal is caught. If the vault's pre-commit hooks ever need to run on these commits, remove --no-verify together with this note and the test.
| Condition | Behaviour | |---|---| | Entry missing a required field | skipped-invalid emitted on stdout, error written to stderr, exit 0 (processing continues for remaining entries). | | Malformed JSON on a JSONL line | Error written to stderr, exit 1 (processing stops). | | --vault-dir not found | Error written to stderr, exit 2. | | --source file not found | Error written to stderr, exit 2. | | Hand-written file at target path | skipped-handwritten emitted on stdout, note written to stderr, file left unchanged. | | Unknown _generator value in existing file | Treated as hand-written: skipped-handwritten, file left unchanged. | | Unexpected filesystem write error | Error written to stderr, exit 2. |
Mirror the sessions.jsonl for the current project into a vault:
bashnode scripts/vault-mirror.mjs \ --vault-dir ~/Projects/vault \ --source .orchestrator/metrics/sessions.jsonl \ --kind session
Dry-run a learnings mirror to preview actions without writing:
bashnode scripts/vault-mirror.mjs \ --vault-dir ~/Projects/vault \ --source .orchestrator/metrics/learnings.jsonl \ --kind learning \ --dry-run
Phase 1 shipped and actively running. As of 2026-05-09, the skill has produced:
vault://40-learnings/ — each carries _generator: session-orchestrator-vault-mirror@1 in the YAML frontmatter.vault://50-sessions/ — same generator stamp.Both target directories were verified by direct ls | wc -l measurement against ~/Projects/vault/. The numeric-prefix layout (40-learnings/, 50-sessions/) confirmed correct per the Target Paths spec above.
scripts/relocate-vault-corpus.mjs is a one-time operator utility that migrates the legacy flat vault corpus (40-learnings/*.md + 50-sessions/*.md at depth 1) into the per-repo namespace subdirectories introduced by #660. Running it is never required for vault-mirror to function — the mirror writes to <repo>/ subfolders since #660 automatically. Relocation is a clean-up step for vaults that accumulated notes before per-repo namespacing existed.
The classifier (scripts/lib/vault-relocation-rules.mjs) reads each file's YAML frontmatter:
type: session): derives repo from repo: frontmatter → project/<slug> tag → (optional, --with-backfill only) backfill index → _unsorted fallback.type: learning): derives repo from project: wikilink → source: free-text field → source_session: wikilink (transitive: looks up the session's own namespace) → _unsorted fallback.Every derived value routes through resolveRepoNamespace() (the same CP1/CP6/CP10 leak-guard used by vault-mirror) so private slugs redact to redacted-repo and no personal home path leaks into a namespace.
repo: backfill (--with-backfill, #700)Most legacy session notes (~466 of 653 at last census) carry no repo: field, so they — and every learning that transitively points at them via source_session: — fall to _unsorted. --with-backfill infers the owning repo from an authoritative signal rather than a guess: the note's id: is joined against each sibling repo's own .orchestrator/metrics/sessions.jsonl session_id.
id found in exactly one repo's sessions.jsonl → that repo. (An id present in >1 repo is ambiguous → SKIP, never a guess.)id not found, but the branch+date parsed from the id matches exactly one repo → that repo. (Colliding branch+date pairs → SKIP.)_unsorted (non-destructive).The inference lives in the pure module scripts/lib/vault-repo-backfill.mjs (inferRepoForSession / buildBackfillIndex / isBackfillDerivable); the CLI builds the cross-repo sessions.jsonl index and threads it into both buildSessionRepoIndex (so backfilled sessions lift their transitive learnings) and the classify loop. Every inferred slug still passes through resolveRepoNamespace() — a mis-inferred private slug redacts to redacted-repo and is excluded from any confident move. Backfilled sessions feed --derivable-only exactly like repo:-carrying ones.
Non-derivable files (_unsorted, redacted-repo, unknown-repo) are moved to a _unsorted/ subfolder (or skipped entirely when --derivable-only is set).
| Mode | Flag | What happens | |---|---|---| | Preview (default) | --dry-run | Read-only scan — reports would-move lines, writes nothing. Always run this first. | | Apply | --apply | Executes git mv (stages only — no commit). Operator reviews diff and commits separately. | | Confident-only apply | --apply --derivable-only | Like --apply but skips files with confident===false (_unsorted, redacted-repo, unknown-repo destinations). | | Backfill (opt-in) | --with-backfill | Infer repo: for repo-less session notes via the authoritative id: → sibling-repo sessions.jsonl join (HIGH/MEDIUM/SKIP). Composes with any mode; without it, output is byte-identical to before. | | Repos root | --repos-root <dir> | Parent dir holding the sibling repos whose sessions.jsonl the backfill scans (default: parent of --vault-dir; Archiv/dot-dirs excluded). | | Rollback | --rollback <manifest> | Reverses a prior --apply run using the reverse-manifest written at <vault-dir>/.orchestrator/relocation-manifest-<ISO>.json. |
Scope filters: --learnings-only / --sessions-only restrict to one corpus root.
--apply must be explicit; there is no accidental write path.--vault-dir is required. No default prevents silently operating on a wrong directory.--apply calls git mv (which stages) but never commits. The operator owns the commit.--apply run writes <vault-dir>/.orchestrator/relocation-manifest-<ISO>.json. Pass this path to --rollback to undo.reason: dest-exists — no data is ever overwritten.resolveRepoNamespace() before use. Private slugs redact to redacted-repo; files that would land in redacted-repo/ are excluded by --derivable-only or left flat if the operator chooses.bash# Step 1: Preview (dry-run default) node scripts/relocate-vault-corpus.mjs --vault-dir ~/Projects/vault # Step 2: Move only confident files (skip _unsorted/redacted-repo) node scripts/relocate-vault-corpus.mjs --vault-dir ~/Projects/vault --apply --derivable-only # Step 2b: Lift coverage further by backfilling repo: for repo-less sessions (preview first) node scripts/relocate-vault-corpus.mjs --vault-dir ~/Projects/vault --with-backfill --derivable-only # dry-run node scripts/relocate-vault-corpus.mjs --vault-dir ~/Projects/vault --with-backfill --derivable-only --apply # then apply # Step 3 (optional): Roll back if something looks wrong node scripts/relocate-vault-corpus.mjs --rollback ~/Projects/vault/.orchestrator/relocation-manifest-<ISO>.json
Exit codes: 0 success (including dry-run); 1 input/arg error; 2 IO error. Data goes to stdout; diagnostics and summary go to stderr. Add --json for JSONL output (one record per file).
scripts/vault-mirror.mjs enforces that the --vault-dir target is a known vault (guard: git remote ends with a canonical vault suffix). Phase-2 generalises this from a single /agents/vault suffix to N named suffixes via scripts/lib/named-vault-resolver.mjs. When vaults: is declared in owner.yaml (see owner-persona.md), the guard uses .some() across all configured suffixes. When vaults: is absent, behaviour is byte-identical to the pre-#700 single-suffix path.
vault-mirror respects the vault-integration block in the project's Session Config (CLAUDE.md, or AGENTS.md on Codex CLI). The script itself does not read Session Config — the calling skill (session-end, evolve) is responsible for reading the config and deciding whether to invoke vault-mirror at all.
| Field | Type | Default | Meaning | |---|---|---|---| | vault-integration.enabled | boolean | false | When false, the calling skill skips vault-mirror entirely. | | vault-integration.vault-dir | string | — | Absolute or ~-prefixed path to the Meta-Vault root. Passed as --vault-dir. | | vault-integration.mode | strict, warn, or off | warn | When off, the calling skill skips vault-mirror. When warn, mirror failures are surfaced as warnings but do not block session close. When strict, mirror failures block session close. |
Other measured skills in the registry, with their headline benchmark lift.