Agent skill

Preserve

by iurykrieger in iurykrieger/claude-bedrock

Single write point for the vault. An agent skill from iurykrieger/claude-bedrock.

MITAuto-check: notesKnowledge Management

Install Preserve

skills CLI
$ npx skills add iurykrieger/claude-bedrock --skill preserve -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install iurykrieger/claude-bedrock preserve --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/iurykrieger/claude-bedrock.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/preserve .claude/skills/preserve && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
preserve
GitHub stars
105
Token cost
~14k tokens
SKILL.md length
4,554 words
Files
1
Skills in repo
10
Repo updated
First seen
Licence
MIT

At a glance

Single write point for the vault. An agent skill from iurykrieger/claude-bedrock.

  • Works in 9 steps: Pre-Write Setup → Parse Input → Matching with Existing Entities → …
  • : bedrock preserve
  • SKILL.md covers Plugin Paths, Vault Resolution, Overview and Phase 0 — Pre-Write Setup, plus 5 more sections
  • Calls git, python3 and gh

What it does

Preserve is an agent skill from iurykrieger/claude-bedrock. Single write point for the vault. Centralizes entity detection, textual matching, entity creation/update, and bidirectional linking. Accepts structured input (list of entities), free-form input (text, meeting notes, session context), or graphify output (graph.json + obsidian markdown from /graphify pipeline). Use when: "bedrock preserve", "bedrock-preserve", "save to vault", "record in vault", "/bedrock:preserve", or when another skill (e.g., /bedrock:learn) needs to persist entities in the vault.

Its SKILL.md is about 14k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.

It sits in Knowledge Management, covering Meeting notes and agendas. It works with Obsidian. The repository describes itself as: Second Brain automation for Obsidian vaults — entity management, ingestion, compression, and sync via Claude Code skills. The licence is MIT.

When your agent uses it

  • : bedrock preserve
  • Bedrock-preserve
  • Record in vault
  • /bedrock:preserve

Example prompts

  • “bedrock preserve”
  • “bedrock-preserve”
  • “save to vault”
  • “/preserve”

Requirements

  • Python 3
  • Pre-approved tools (allowed-tools): Bash, Read, Write, Edit, Glob, Grep, Agent, mcp__plugin_github_github__*, mcp__plugin_atlassian_atlassian__*

Workflow steps

9 steps, taken from the step headings in SKILL.md.

  1. Pre-Write Setup
  2. Parse Input
  3. Matching with Existing Entities
  4. Change Proposal
  5. Execute Changes
  6. Bidirectional Linking
  7. 5 — Sync Graph Back-Pointers
  8. Publish
  9. Report

What it can do on your machine

Read from SKILL.md and the folder at commit 7cac16c. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves these tools, so the agent can use them without asking each time:

    • Bash
    • Read
    • Write
    • Edit
    • Glob
    • Grep
    • Agent
    • mcp__plugin_github_github__*
    • mcp__plugin_atlassian_atlassian__*

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    Shell commands in SKILL.md call:

    • git
    • python3
    • gh

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    No URLs in SKILL.md. Its commands use git and gh, which can reach the network depending on how they are called.

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Preserve loads about 14k tokens when it runs. Until then it costs about 128 tokens; SKILL.md has 4,554 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~128
When it runs · the whole SKILL.md, loaded when a task matches
~14k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check: notes

The automated check noted patterns worth knowing about, such as sudo or a known installer.

  • NotePre-approves every shell command (allowed-tools: Bash)SKILL.md
    allowed-tools: Bash, Read, Write, Edit, Glob, Grep, Agent, mcp__plugin_github_github__*, mcp__plugin_atlassian_atla

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from iurykrieger/claude-bedrock at commit 7cac16c, republished under its MIT licence (© iurykrieger). 4,554 words, ~13,566 tokens.

Download SKILL.mdSave it as .claude/skills/preserve/SKILL.md (or your agent's skills folder).
name
preserve
description
Single write point for the vault. Centralizes entity detection, textual matching, entity creation/update, and bidirectional linking. Accepts structured input (list of entities), free-form input (text, meeting notes, session context), or graphify output (graph.json + obsidian markdown from /graphify pipeline). Use when: "bedrock preserve", "bedrock-preserve", "save to vault", "record in vault", "/bedrock:preserve", or when another skill (e.g., /bedrock:learn) needs to persist entities in the vault.
allowed-tools
Bash, Read, Write, Edit, Glob, Grep, Agent, mcp__plugin_github_github__*, mcp__plugin_atlassian_atlassian__*
user_invocable
true

/bedrock:preserve — Single Write Point for the Vault

Plugin Paths

Entity definitions and templates are in the plugin directory, not in the vault root. Use the "Base directory for this skill" provided at invocation to resolve paths:

  • Entity definitions: <base_dir>/../../entities/
  • Templates: <base_dir>/../../templates/{type}/_template.md
  • Plugin CLAUDE.md: <base_dir>/../../CLAUDE.md (already injected automatically into context)

Where <base_dir> is the path provided in "Base directory for this skill".


Vault Resolution

Resolve which vault to operate on. This skill can be invoked from any directory.

Step 1 — Parse --vault flag: Check if the input arguments include --vault <name>. If found, extract the vault name and remove it from the arguments before further parsing.

Step 2 — Resolve vault path:

  1. If --vault <name> was provided: Read the vault registry at <base_dir>/../../vaults.json. Find the entry matching the name. If not found: error — "Vault <name> is not registered. Run /bedrock:vaults to see available vaults." If found: set VAULT_PATH to the entry's path value.

  2. If no --vault flag — CWD detection: Read <base_dir>/../../vaults.json. Check if the current working directory is inside any registered vault path (CWD starts with a registered vault's absolute path). If multiple match, use the longest path (most specific). If found: set VAULT_PATH to the matching vault's path.

  3. If CWD detection fails — default vault: From the registry, find the vault with "default": true. If found: set VAULT_PATH to the default vault's path.

  4. If no resolution: Error — "No vault resolved. Available vaults:" followed by the registry listing. "Use --vault <name> to specify, or run /bedrock:setup to register a vault."

Step 3 — Validate vault path:

bash
test -d "<VAULT_PATH>" && echo "exists" || echo "missing"

If missing: error — "Vault path <VAULT_PATH> does not exist on disk. Run /bedrock:setup to re-register."

Step 4 — Read vault config:

bash
cat <VAULT_PATH>/.bedrock/config.json 2>/dev/null

Extract language, git.strategy, and other relevant fields for use in later phases.

From this point forward, ALL vault file operations use <VAULT_PATH> as the root.

  • Entity directories: <VAULT_PATH>/actors/, <VAULT_PATH>/people/, etc.
  • Vault config: <VAULT_PATH>/.bedrock/config.json
  • Git operations: git -C <VAULT_PATH> <command>

Overview

This skill centralizes ALL write logic for the vault. It receives input (structured, free-form, or graphify output), identifies entities, correlates with the existing vault, proposes changes to the user, and executes after confirmation. It is the only path to create or update entities in the vault (except /sync-people which handles people/teams via GitHub API).

You are an execution agent. Follow the phases below in order, without skipping steps.


Phase 0 — Pre-Write Setup

Two pre-flight steps run before any input parsing: synchronize the vault with its remote, then (when applicable) merge an incoming graphify output directory into the vault's cumulative graphify-out/.

0.1 Vault Sync

Execute:

bash
git -C <VAULT_PATH> pull --rebase origin main

If the pull fails:

  • No remote configured: warn "No remote configured. Working locally." and proceed.
  • Pull conflict: git -C <VAULT_PATH> rebase --abort and warn the user. DO NOT proceed without resolving.
  • Otherwise: proceed.
0.2 Merge Incoming Graphify Output

When this runs: Only when the skill was invoked with a graphify_output_path argument pointing at a graphify output directory (e.g., /bedrock:learn passes $TEACH_TMP/graphify-out-new/). Free-form text input and structured entity-list input skip this sub-phase entirely.

Skip condition (backward compat): If the input's graphify_output_path resolves to the same absolute path as <VAULT_PATH>/graphify-out/, skip this sub-phase. Legacy callers (and /bedrock:sync in its current form) point at the vault's own output directory — there is nothing to merge. Use realpath (or equivalent) to compare:

bash
incoming_real=$(cd "<graphify_output_path>" 2>/dev/null && pwd -P)
vault_real=$(cd "<VAULT_PATH>/graphify-out" 2>/dev/null && pwd -P)
if [ "$incoming_real" = "$vault_real" ]; then
  echo "Phase 0.2: graphify_output_path already points at the vault — skipping merge."
  # proceed to Phase 1 with graphify_output_path unchanged
fi

Skip condition (no graphify input): If the input is free-form text, structured entity list, or otherwise does not include graphify_output_path, skip.


Step 1 — Validate incoming directory. Verify that <graphify_output_path>/graph.json exists, is non-empty, and parses as valid JSON. If invalid, abort with a clear error and do NOT mutate the vault:

bash
if [ ! -s "<graphify_output_path>/graph.json" ]; then
  echo "ERROR: graph.json missing or empty in <graphify_output_path>. Aborting before vault mutation."
  exit 1
fi
python3 -c "import json,sys; json.load(open('<graphify_output_path>/graph.json'))" || { echo "ERROR: graph.json is not valid JSON."; exit 1; }

Step 2 — First-ingestion edge case. If <VAULT_PATH>/graphify-out/ does not exist, promote the incoming directory wholesale (no re-merge pass) and record stats, then skip to Step 7:

bash
if [ ! -d "<VAULT_PATH>/graphify-out" ]; then
  mkdir -p "<VAULT_PATH>"
  cp -R "<graphify_output_path>" "<VAULT_PATH>/graphify-out"
  echo "Phase 0.2: first ingestion — promoted incoming graphify output to <VAULT_PATH>/graphify-out/."
  # record: nodes_added = <count of nodes in graph.json>, nodes_merged = 0, edges_added = <count of edges>, stale_flag_set = false
  # skip to Step 7 (record stats) then exit sub-phase
fi

Step 3 — Merge graph.json (nodes + edges). Both files follow NetworkX node-link format ({"nodes": [...], "edges": [...]} or "links" — accept either key). Run the merge via an inline Python block to avoid hand-merging JSON in the prompt. Write the merged graph to a staging file, then atomically swap:

bash
python3 - <<'PY'
import json, os, pathlib, shutil, sys

existing_path = pathlib.Path("<VAULT_PATH>/graphify-out/graph.json")
incoming_path = pathlib.Path("<graphify_output_path>/graph.json")
staging_path = existing_path.with_suffix(".json.staging")

with existing_path.open() as f:
    existing = json.load(f)
with incoming_path.open() as f:
    incoming = json.load(f)

# Accept both "edges" and "links" keys — normalize to "edges".
def _edges(g):
    return g.get("edges", g.get("links", []))

# --- Node merge keyed by id ---
def _union(a, b):
    # Preserve order; dedup by string representation.
    seen, out = set(), []
    for item in (a or []) + (b or []):
        key = json.dumps(item, sort_keys=True) if not isinstance(item, str) else item
        if key not in seen:
            seen.add(key)
            out.append(item)
    return out

def _dedup_sources_by_url(a, b):
    seen, out = set(), []
    for item in (a or []) + (b or []):
        if isinstance(item, dict) and "url" in item:
            if item["url"] in seen:
                continue
            seen.add(item["url"])
        out.append(item)
    return out

existing_nodes = {n["id"]: n for n in existing.get("nodes", [])}
nodes_added = 0
nodes_merged = 0
for inc in incoming.get("nodes", []):
    nid = inc["id"]
    if nid not in existing_nodes:
        existing_nodes[nid] = inc
        nodes_added += 1
    else:
        cur = existing_nodes[nid]
        # Union sources by URL
        if "sources" in inc or "sources" in cur:
            cur["sources"] = _dedup_sources_by_url(cur.get("sources"), inc.get("sources"))
        # Most-recent updated_at (YYYY-MM-DD lexical compare works)
        cur_ua, inc_ua = cur.get("updated_at"), inc.get("updated_at")
        if inc_ua and (not cur_ua or inc_ua > cur_ua):
            cur["updated_at"] = inc_ua
        # Union labels and tags
        for key in ("labels", "tags"):
            if key in inc or key in cur:
                cur[key] = _union(cur.get(key), inc.get(key))
        nodes_merged += 1

# --- Edge dedup keyed by (source, target, type/relation) ---
def _edge_key(e):
    return (e.get("source"), e.get("target"), e.get("type") or e.get("relation"))

existing_edges = _edges(existing)
seen_edges = {_edge_key(e) for e in existing_edges}
edges_added = 0
for inc_edge in _edges(incoming):
    k = _edge_key(inc_edge)
    if k in seen_edges:
        continue
    existing_edges.append(inc_edge)
    seen_edges.add(k)
    edges_added += 1

merged = dict(existing)
merged["nodes"] = list(existing_nodes.values())
# Preserve the key naming the existing file used.
merged_key = "edges" if "edges" in existing else ("links" if "links" in existing else "edges")
merged[merged_key] = existing_edges

with staging_path.open("w") as f:
    json.dump(merged, f, indent=2, ensure_ascii=False)

# Emit stats to stdout for capture.
print(json.dumps({"nodes_added": nodes_added, "nodes_merged": nodes_merged, "edges_added": edges_added}))
PY

Atomic swap after the Python block succeeds:

bash
mv "<VAULT_PATH>/graphify-out/graph.json.staging" "<VAULT_PATH>/graphify-out/graph.json"

If the Python block exits non-zero, abort without running the mv — the vault's graph.json stays untouched.

Step 4 — Append obsidian/*.md files. For each markdown file in <graphify_output_path>/obsidian/:

  • Skip any file whose source_file frontmatter value starts with /tmp/ — these are ephemeral visualization files produced by /bedrock:teach and must not accumulate in the vault.
  • If the corresponding file exists in <VAULT_PATH>/graphify-out/obsidian/: append the incoming content to the existing file, separated by \n\n---\n\n. Existing content is preserved verbatim.
  • If it does not exist: copy the file into <VAULT_PATH>/graphify-out/obsidian/.
bash
mkdir -p "<VAULT_PATH>/graphify-out/obsidian"
for src in "<graphify_output_path>/obsidian/"*.md; do
  [ -e "$src" ] || continue
  SRC_FILE=$(awk -F'"' '/^source_file:/{print $2; exit}' "$src")
  case "$SRC_FILE" in /tmp/*) continue ;; esac
  dest="<VAULT_PATH>/graphify-out/obsidian/$(basename "$src")"
  if [ -e "$dest" ]; then
    printf '\n\n---\n\n' >> "$dest"
    cat "$src" >> "$dest"
  else
    cp "$src" "$dest"
  fi
done

Step 5 — Append GRAPH_REPORT.md. If <graphify_output_path>/GRAPH_REPORT.md exists:

  • If <VAULT_PATH>/graphify-out/GRAPH_REPORT.md exists: append a new dated section.
  • If it does not exist: copy.
bash
if [ -f "<graphify_output_path>/GRAPH_REPORT.md" ]; then
  dest="<VAULT_PATH>/graphify-out/GRAPH_REPORT.md"
  if [ -e "$dest" ]; then
    {
      printf '\n\n---\n\n# Merge on %s\n\n' "$(date +%Y-%m-%d)"
      cat "<graphify_output_path>/GRAPH_REPORT.md"
    } >> "$dest"
  else
    cp "<graphify_output_path>/GRAPH_REPORT.md" "$dest"
  fi
fi

Step 6 — Mark .graphify_analysis.json stale. If <VAULT_PATH>/graphify-out/.graphify_analysis.json exists, set a top-level "stale": true field. Other content is untouched:

bash
analysis="<VAULT_PATH>/graphify-out/.graphify_analysis.json"
stale_flag_set=false
if [ -f "$analysis" ]; then
  python3 - <<PY
import json, pathlib
p = pathlib.Path("$analysis")
with p.open() as f:
    data = json.load(f)
data["stale"] = True
with p.open("w") as f:
    json.dump(data, f, indent=2, ensure_ascii=False)
PY
  stale_flag_set=true
fi

If the file does not exist, skip (nothing to mark).

Step 7 — Record merge stats for the Phase 7 report. Capture nodes_added, nodes_merged, edges_added (from Step 3's Python stdout) and stale_flag_set (from Step 6). These values are threaded through to Phase 7's report block under a new "Graphify merge" section and returned in the skill's result payload to the caller (e.g., /bedrock:learn).

Step 8 — Point subsequent phases at the merged location. After the merge succeeds, set graphify_output_path := <VAULT_PATH>/graphify-out/ for all downstream phases. Phase 1.3 (graphify-output parsing), Phase 2 (matching), and the rest of the flow read from the merged vault location — not from the original temp input.


Phase 1 — Parse Input

/bedrock:preserve accepts three input modes. Determine which to apply:

1.1 Structured input

When called by another skill (e.g., /bedrock:learn) or when the user provides an explicit list. The format is an optional top-level header followed by a list of entities:

yaml
# Optional top-level header — applies to all entities in the batch
actor_context: <actor-name>  # optional, kebab-case slug of an actor in the vault.
                             # When present, /preserve treats the batch as scoped to that actor:
                             # graphify nodes of file_type=document/paper become `code` of this actor
                             # with node_type ∈ {concept, decision} (see Phase 1.3 step 4).

entities:
  - type: actor | person | team | concept | topic | discussion | project | fleeting | code
    name: "canonical entity name"
    action: create | update
    content: "content to include in the entity body"
    relations:
      actors: ["actor-slug-1", "actor-slug-2"]
      people: ["person-slug-1"]
      teams: ["team-slug-1"]
      concepts: ["concept-slug-1"]
      topics: ["topic-slug-1"]
      discussions: ["discussion-slug-1"]
      projects: ["project-slug-1"]
      code: ["node-slug-1"]
    source: "github | confluence | jira | session | manual | gdoc | csv | graphify"
    metadata: {}  # additional frontmatter fields specific to the type

If the input is a bare list (no entities: key, no actor_context), accept it as a list of entities with actor_context = null. This preserves backward compatibility with callers that pre-date the field.

If the input follows this format (or something close): parse directly and go to Phase 2.

1.2 Free-form input

When the user provides natural text, meeting notes, session context, or any unstructured content. Analyze the text and extract:

  1. Mentioned entities — identify by name, alias, or reference:

    • People: names in "First Last" format
    • Actors: service names, APIs, repositories
    • Teams: squad names
    • Concepts: patterns, principles, techniques, protocols, abstractions
    • Topics: discussion themes, bugs, RFCs, features
    • Discussions: meetings, decisions, debates
    • Projects: initiatives, migrations, cross-team features
  2. Inferred action — for each entity:

    • If the entity already exists in the vault: update
    • If the entity does not exist: create
  3. Content — what was said about each entity in the input

  4. Relations — infer which entities relate to each other based on context

  5. Source — infer: session (conversation), meeting-notes (minutes), manual (typed text)

To classify new content, consult the plugin's entity definitions (see "Plugin Paths" section) (loaded in Phase 2.0):

  • "When to create" section → positive criteria for creating a new entity
  • "When NOT to create" section → exclusion criteria
  • "How to distinguish from other types" section → disambiguation

Convert the result to the structured format from section 1.1 and proceed.

1.3 Graphify output input

When called by /bedrock:learn (or any skill) with a graphify output reference, OR when the user invokes /bedrock:preserve directly pointing at a graphify-out/ directory:

Input format:

  • graphify_output_path: path to graphify-out/ directory
  • source_url: original external source URL/path (optional — may not be present for manual invocation)
  • source_type: type of external source (optional)
  • actor_context: kebab-case slug of an actor in the vault (optional). When present, the entire corpus is treated as belonging to that actor: file_type=document/paper nodes are classified as code of that actor with node_type ∈ {concept, decision} instead of as global concept/topic/fleeting. When absent, classification falls back to the corpus-agnostic logic (concept global / topic / fleeting).

Detection: If the input contains a path ending in graphify-out/ or graphify-out, or references graph.json, treat as graphify output input.

Processing:

  1. Read graph.json from graphify_output_path/graph.json:

    • Parse NetworkX node-link format
    • Extract all nodes with: id, label, file_type, source_file, source_location
    • Extract all edges with: source, target, relation, confidence, confidence_score
    • If graph.json is missing or empty: abort with error "No graph.json found in graphify output. Run /graphify first."
  2. Read obsidian files from graphify_output_path/obsidian/*.md:

    • For each markdown file, read frontmatter and body content
    • Correlate with graph.json by matching filename stem to node id (kebab-cased)
    • If obsidian file doesn't exist for a node: fall back to graph.json metadata alone
  3. Read analysis from graphify_output_path/.graphify_analysis.json (if exists):

    • Extract community assignments (community_id per node), god nodes (is_god_node), community labels
    • Use community labels to inform domain/* tags when creating entities
    • If absent or stale: true, downstream steps fall back to graph-only signals (no community-aware grouping)
  4. Read configuration from <VAULT_PATH>/.bedrock/config.json (best-effort):

    • code.max_per_actor: integer, default 200. Per-actor cap on the number of code candidates produced from this corpus. No absolute global cap.
    • code.cluster_threshold: float, default 0.85. Minimum semantically_similar_to.confidence_score for two nodes to be grouped into the same code candidate.
    • If .bedrock/config.json is missing or has no code block, use the defaults silently.
  5. Group nodes by semantic similarity (BEFORE filtering and classification) — produces clusters that will each become at most one code candidate:

    • Build clusters by union-find:
      • Two nodes are in the same cluster if there is a semantically_similar_to edge between them with confidence_score ≥ code.cluster_threshold.
      • OR they share the same community_id (from .graphify_analysis.json) AND at least one of them is is_god_node OR both have edge_count ≥ 2. This guards against weakly-tied community co-membership.
    • If .graphify_analysis.json is absent or stale, only the semantically_similar_to rule applies.
    • Singletons (nodes with no qualifying neighbor) form their own cluster of size 1.
    • Each cluster carries: cluster_id (synthetic), member_node_ids[] (the union of node ids — this becomes the graphify_node_ids array of the resulting code entity), representative (the node with highest degree in the cluster — used for label, source_file, etc.).
    • Hard cap: maximum 50 nodes per cluster (defensive — runaway clusters indicate bad threshold).
  6. Classify clusters into vault entity types — /preserve owns this classification. Read ALL entity definitions from the plugin (see "Plugin Paths") and apply:

    When actor_context is present (single-actor corpus):

    • file_type: code clusters → code for the actor, node_type ∈ {function, class, module, interface, endpoint} inferred from the representative node's label/edges.
    • file_type: document or file_type: paper clusters → code for the actor:
      • node_type: decision if the representative node has rationale_for edges OR the label/text contains decision markers (e.g., "ADR", "RFC", "decision", "chose", "decided").
      • node_type: concept otherwise.
    • The cluster's actor field is set to [[<actor_context>]] for all entities.

    When actor_context is absent (corpus-agnostic):

    • file_type: code clusters → code (parent actor inferred from source_file path or repo name in the path; if no actor can be inferred, classify as fleeting).
    • file_type: document or file_type: paper clusters → check for concept first: if the representative node describes a pattern, principle, technique, protocol, or abstraction AND is self-contained AND is not specific to a single actor → concept (global).
      • Non-concept document → classify using entity definitions ("When to create" / "When NOT to create" / "How to distinguish").
      • Non-concept paper → topic or fleeting depending on completeness criteria.
    • God nodes (is_god_node) → consider as actor, concept, or topic.
    • Apply Zettelkasten classification (section 1.4): if content doesn't meet completeness criteria → fleeting.
  7. Filter relevant clusters (applied to code candidates only — non-code classifications keep their existing inclusion logic):

    • Inclusion predicate: representative node's strongest edge has confidence ∈ {EXTRACTED, INFERRED} AND at least one of:
      • is_god_node (from .graphify_analysis.json),
      • degree > community_average_degree (from .graphify_analysis.json; if analysis absent, use overall graph average),
      • edge_count ≥ 2.
    • Exclusion: representative label matches trivial patterns (case-insensitive substring of Test, Tests, Mock, Fake, Builder, Stub, Fixture) or the cluster only contains nodes flagged trivial by graphify.
    • Per-actor cap: group surviving code candidates by their resolved actor. Within each actor, rank by is_god_node (true first) > degree > edge_count. Keep the top code.max_per_actor (default 200); discard the rest with a warning in the report listing how many were dropped per actor.
    • No English keyword regex. The previous label allowlist (Service|Controller|...) is removed.
    • For non-code classifications (concept global, topic, fleeting): include all that pass classification.
  8. Match against existing vault — Use the textual matching logic from Phase 2 (filename, name, aliases, graphify ids). For code entities, the graphify-id match accepts both legacy singular graphify_node_id and the new graphify_node_ids array — match if the cluster's member_node_ids set intersects the entity's existing id set. Mark matched clusters as update, unmatched as create.

  9. Build internal structured format for each classified + filtered cluster:

    • type: from classification (step 6).
    • name: kebab-cased label of the cluster's representative node.
    • action: create or update (from step 8 matching).
    • content: body of the obsidian markdown file matching the representative's id (or generate from graph.json metadata if no obsidian file). When the cluster has multiple members, include a brief "Grouped from N graphify nodes" note in the body listing the member ids — this preserves traceability.
    • relations: from graph.json edges of all member nodes (convert node ids to entity slugs via kebab-case; deduplicate).
    • source: from input source_type (or "graphify" if not provided).
    • source_url: from input source_url (if provided).
    • source_type: from input source_type (if provided).
    • metadata: for code entities, include:
      • graphify_node_ids: array — the cluster's member_node_ids (always written as array even when size is 1).
      • actor: wikilink of the parent actor ([[<actor_context>]] when set; otherwise inferred).
      • node_type: from step 6.
      • source_file: relative path from the representative node.
      • confidence: strongest edge confidence across all member nodes (EXTRACTED > INFERRED > AMBIGUOUS).
    • metadata: for concept (global) entities from graphify, include:
      • graphify_node_ids: array.
      • confidence: strongest edge confidence across all members.
  10. Proceed to Phase 3 (Change Proposal) — present the classified cluster list for user confirmation, then execute writes as normal (Phases 4-7).

Note: When invoked directly by the user (not via /learn), the user confirmation in Phase 3 is the only gate before writes. When invoked via /learn, /learn has already shown the user the graphify report (god nodes, communities) providing context for the confirmation.

1.4 Zettelkasten Classification

Before converting to structured format, classify each entity by Zettelkasten role. Consult the plugin's entity definitions ("Completeness Criteria" section) to determine the correct type:

Classification rule:

  • If the content meets the completeness criteria of a permanent type (actor, person, team) → classify as permanent
  • If the content has a non-empty graphify_node_ids array (or legacy singular graphify_node_id) and actor defined → classify as code (permanent extension, sub-entity of actor)
  • If the content defines a pattern, principle, technique, protocol, or abstraction that is self-contained and actor-independent → classify as concept (permanent)
  • If the content meets the completeness criteria of a bridge type (topic, discussion) → classify as bridge
  • If the content meets the completeness criteria of an index type (project) → classify as index
  • If the content does NOT meet the completeness criteria of any type → classify as fleeting

Heuristics for fleeting:

  • Vague mention without concrete data (no repo name, no full person name, no date, no decision)
  • Idea or hypothesis without confirmation ("it seems like...", "maybe...", "someone mentioned...")
  • Fragment of information without sufficient context to be self-contained
  • Generic TODO without assignee or deadline

When in doubt, err on the side of fleeting — it is safer to capture as fleeting and promote later than to create an incomplete permanent entity.

If the input came from another skill (e.g., /bedrock:learn) and already includes a classification suggestion (type: fleeting), respect the suggestion but validate against the criteria above.

If no input was provided: ask the user "What would you like to preserve in the vault? Provide text, meeting notes, or a list of entities."


Phase 2 — Matching with Existing Entities

Objective: Correlate entities from the input with the existing vault.

2.0 Read entity definitions

Read ALL entity definition files from the plugin (see "Plugin Paths" section): <base_dir>/../../entities/*.md These files define what each entity type is, when to create, when NOT to create, and how to distinguish between types. Internalize these definitions — you will use them to classify new content (especially in free-form mode, Phase 1.2).

2.1 Collect vault entities

List all files in each entity directory (exclude _template.md and _template_node.md):

<VAULT_PATH>/actors/*.md and <VAULT_PATH>/actors/*/*.md (actors can be folders)
<VAULT_PATH>/actors/*/nodes/*.md (code entities within actors)
<VAULT_PATH>/people/*.md
<VAULT_PATH>/teams/*.md
<VAULT_PATH>/topics/*.md
<VAULT_PATH>/discussions/*.md (if exists)
<VAULT_PATH>/projects/*.md (if exists)
<VAULT_PATH>/fleeting/*.md (if exists)

For each file found, extract:

  • filename (without extension) — canonical identifier
  • name (or title) from frontmatter — human-readable name
  • aliases from frontmatter — alternative names
  • graphify_node_ids from frontmatter — for code entities (if present, accept both the new array form and the legacy singular graphify_node_id string; normalize to a set of ids per entity)
2.2 Textual matching

For each entity from the input, check if it already exists in the vault:

Match rules (in priority order):

  1. Exact match by filename (case-insensitive): billing-api == billing-api
  2. Match by name/title field (case-insensitive): "Billing API" finds billing-api.md
  3. Match by aliases (case-insensitive): "BillingAPI" finds billing-api.md if alias contains "BillingAPI"
  4. Match by filename without hyphens (case-insensitive): billing-api → billingapi finds "BillingAPI"
  5. Match by graphify ids (for code entities): set intersection between the input cluster's graphify_node_ids (array) and the vault entity's id set, where the vault entity's id set is normalized from EITHER graphify_node_ids (array, current schema) OR the legacy singular graphify_node_id (string treated as a 1-element set). Any non-empty intersection is a match. This is the most reliable match for code entities and takes priority over the others when present.

Safety rules:

  • DO NOT match by substrings of 3 characters or fewer (e.g., "api" should not match everything)
  • Maximum 20 correlations per entity type
  • In case of ambiguity: record all candidates and resolve in Phase 3 (proposal)
Show full SKILL.md (1,787 more words)Show less
2.3 Classify actions

For each entity from the input:

  • If match found in vault: mark as update (update existing entity)
  • If no match: mark as create (new entity)
  • If the input already specified the action: respect the input's action
2.4 Enrich via external sources (best-effort)

For entities of type actor that have a repository field in frontmatter:

GitHub MCP (call directly, NOT via subagent):

  • mcp__plugin_github_github__list_pull_requests → recent PRs (5, state=all, sort=updated)
  • mcp__plugin_github_github__list_commits → recent commits (5)

Atlassian MCP:

  • Search for Jira issues from the relevant squad
  • Search for related Confluence pages

IMPORTANT: Enrichment is best-effort. If MCP is not available or fails, continue without it. Record which sources failed in the final report.

IMPORTANT: DO NOT use subagents for MCP calls. Permissions are not inherited by subagents.


Phase 3 — Change Proposal

Objective: Present to the user EVERYTHING that will be done, BEFORE executing.

3.1 Build proposal

For each entity, present:

## Change Proposal — /bedrock:preserve

### Entities to create
| # | Type | Name | File | Relations |
|---|---|---|---|---|
| 1 | actor | billing-new-api | actors/billing-new-api.md | [[squad-payments]], [[alice-smith]] |

### Entities to update
| # | Type | Name | File | Changes |
|---|---|---|---|---|
| 1 | actor | billing-api | actors/billing-api.md | Add "Recent Activity" section |

### Bidirectional links
| Source entity | Target entity | Section added |
|---|---|---|
| [[billing-new-api]] | [[squad-payments]] | "Related Actors" in squad-payments |
| [[squad-payments]] | [[billing-new-api]] | "team" in billing-new-api |

### Sources consulted
- ✅ Local vault
- ✅ / ❌ GitHub MCP
- ✅ / ❌ Atlassian MCP

Total: N entities to create, M to update, P bidirectional links.
3.2 Await confirmation

Ask: "Confirm execution? (yes/no/adjust)"

  • yes: proceed to Phase 4
  • no: abort and inform
  • adjust: ask what to adjust, modify proposal, re-present

DO NOT proceed without explicit user confirmation.


Phase 4 — Execute Changes

Objective: Create and update entities as per the approved proposal.

4.1 Create new entities

For each entity marked as create:

  1. Read the plugin template: <base_dir>/../../templates/<directory>/_template.md
  2. Fill frontmatter with data from input + matching:
    • type: entity type
    • name (or title for topics): extracted name
    • aliases: generate at least 1 alias following the convention per type (see conventions.md)
    • tags: use hierarchical tags: [type/<type>, status/<status>, domain/<domain>]
    • updated_at: today's date (YYYY-MM-DD)
    • updated_by: "preserve@agent"
    • Relation fields: wikilinks to correlated entities
    • Remaining fields: fill with data from input or leave empty
  3. Fill body following the template structure
  4. Add mandatory callouts when applicable:
    • Actors with status: deprecated → > [!warning] Deprecated
    • Actors with pci: true → > [!danger] PCI Scope
  5. Save to <directory>/<filename>.md

Rules per entity type:

TypeDirectoryFilename patternName frontmatter key
actoractors/ or actors/<name>/repo-name.mdname
codeactors/<actor>/nodes/node-slug.mdname
personpeople/first-last.mdname
teamteams/squad-name.mdname
conceptconcepts/slug.mdname
topictopics/YYYY-MM-category-slug.mdtitle
discussiondiscussions/YYYY-MM-DD-slug.mdtitle
projectprojects/project-slug.mdname
fleetingfleeting/YYYY-MM-DD-slug.mdtitle
4.1.2 Code entity specific rules

When creating a code entity:

  1. Resolve the parent actor: the actor field in the input (wikilink or slug) indicates the actor. Verify that the actor exists in actors/.
  2. Ensure folder structure: if the actor is still a flat file (actors/<name>.md):
    • Create folder actors/<name>/
    • Move actors/<name>.md → actors/<name>/<name>.md (use git mv)
    • Create subfolder actors/<name>/nodes/
    • Add "Knowledge Nodes" section to the actor body (before the "Infrastructure" section or at the end)
  3. Create code entity: use template actors/_template_node.md
    • Save to actors/<actor>/nodes/<node-slug>.md
    • Filename: kebab-case of the node's name (e.g., ProcessTransaction → process-transaction.md)
    • Fill graphify_node_ids (array — always written as a list, even of size 1), actor, node_type, source_file, confidence from the input
    • Backward compat at write time: if the input still carries the legacy singular graphify_node_id (string), normalize it to graphify_node_ids: [<id>] before writing. Never persist the singular form.
    • Inherit domain/* tags from the parent actor
    • Generate at least 1 alias (human-readable name + camelCase if applicable)
  4. Bidirectional backlink:
    • In the code entity: actor: "[[actor-name]]" in frontmatter
    • In the actor: add - [[node-slug]] — brief description in the "Knowledge Nodes" section
4.1.1 Linking rules by Zettelkasten role

When filling the entity body, apply semantic linking rules by role:

  • Permanent notes (actors, people, teams, concepts): wikilinks in the body must have textual context. E.g., "receives authorizations from [[payment-gateway]] via gRPC" — not just "[[payment-gateway]]"
  • Bridge notes (topics, discussions): wikilinks in the body explain why permanents relate. E.g., "the deprecation of [[legacy-gateway]] is blocked because clients depend on [[billing-api]]"
  • Index notes (projects): wikilinks in the body point to where the knowledge is. E.g., "progress documented in [[2026-06-deprecation-legacy-gateway]]"
  • Fleeting notes: exploratory wikilinks allowed without full textual context.
4.2 Update existing entities

For each entity marked as update:

  1. Read the existing file
  2. Frontmatter: merge — update fields with new data. NEVER delete existing fields.
    • ALWAYS update updated_at and updated_by
    • Add new wikilinks to existing arrays (do not duplicate)
    • Add new aliases if discovered
  3. Body:
    • Actors: can be modified/merged — new information replaces outdated information
    • People, Teams, Concepts, Topics: append-only — add information, NEVER delete existing content
    • Discussions, Projects: append-only for the general body; structured fields (action_items, conclusions) can be updated
    • "Recent Activity" section (actors): REPLACE content (temporal data)
  4. Wikilinks: add new ones, NEVER remove existing ones
4.3 Populate sources field (when applicable)

If the input contains source_url and source_type (provided by /bedrock:learn or another caller):

When creating an entity:

  • Add to frontmatter:
    yaml
    sources:
      - url: "<source_url>"
        type: "<source_type>"
        synced_at: "<today's date>"

When updating an entity:

  1. Read the existing sources field from frontmatter
  2. If the URL already exists in the list: update synced_at with today's date
  3. If the URL does not exist: append new entry {url, type, synced_at}
  4. Sort by synced_at descending (most recent first)
  5. NEVER remove existing entries (append-only)

If the input does NOT contain source_url: do not modify the sources field — keep the existing value (or [] if new entity).


Phase 5 — Bidirectional Linking

Objective: Ensure that every relation is reciprocal.

5.1 Linking rules

When creating/updating entity X with a reference to entity Y:

  • Check if Y already references X
  • If NOT: add reference from Y → X

Bidirectional linking graph:

Team ──members──→ Person ──team──→ Team
Team ──actors──→ Actor ──team──→ Team
Topic ──people──→ Person
Topic ──actors──→ Actor
Person ──focal_points──→ Actor
Project ──focal_points──→ Person ──projects──→ Project
Project ──related_actors──→ Actor
Project ──related_topics──→ Topic
Project ──related_teams──→ Team
Discussion ──related_actors──→ Actor
Discussion ──related_people──→ Person
Discussion ──related_projects──→ Project
Discussion ──related_topics──→ Topic
Code ──actor──→ Actor ──"Knowledge Nodes" section──→ Code
Code ──relations──→ Code (bidirectional via relations[])
5.2 Implementation

For each pair (X → Y) in the approved proposal:

  1. Read entity Y
  2. Identify the corresponding field/section in the reverse link (Y → X)
  3. In frontmatter: if the array field exists, add wikilink [[X]] if not already present
  4. In body: if there is a corresponding section (e.g., ## Discussions, ## Related Projects):
    • If section exists: add - [[X]] — brief context at the end of the list
    • If section does NOT exist: create the section in the appropriate location (before "Expected Bidirectional Links" or before the last ---)
  5. Update updated_at and updated_by of Y
  6. Save

Idempotency: if the wikilink [[X]] already exists in Y's field/section, DO NOT add it again.

5.3 Linking sections by target entity type
Target entity (Y)Body sectionFrontmatter field
Actor receiving link from Discussion## Discussions—
Actor receiving link from Project## Related Projects—
Person receiving link from Discussion## Discussions—
Person receiving link from Project## Projectsprojects (if exists)
Topic receiving link from Project## Related Projects—

For frontmatter-based links (team↔actor, team↔person, person↔team, etc.): use only the YAML field, do not create a body section.


Phase 6.5 — Sync Graph Back-Pointers

Objective: Propagate vault_entity_path to the corresponding nodes in <VAULT_PATH>/graphify-out/graph.json, closing the bidirectional bridge between each code entity touched in this run and the graphify nodes it materializes.

This phase runs AFTER bidirectional linking (Phase 5) and BEFORE the git workflow (Phase 6), so the back-pointer change lands in the same commit as the entities.

6.5.1 Defensive cleanup

Remove any orphaned staging file from a prior interrupted run before doing anything else:

bash
rm -f "<VAULT_PATH>/graphify-out/graph.json.staging" 2>/dev/null || true

This is idempotent and never fails the phase.

6.5.2 Best-effort detection

Locate the vault's cumulative graph file:

bash
GRAPH_PATH="<VAULT_PATH>/graphify-out/graph.json"
test -s "$GRAPH_PATH" && echo "EXISTS" || echo "MISSING"

If the path does NOT exist, OR the file is empty (-s returns false), OR JSON parsing fails:

  • Record phase_6_5_status = "skipped" and phase_6_5_reason ("missing", "empty", or "invalid_json").
  • Skip directly to Phase 6 — do NOT fail /preserve.
  • The Phase 7 report includes a warning surface (see §6.5.6 below).

If the file exists and parses, continue.

6.5.3 Collect target node ids from touched code entities

Iterate over every entity created or updated in Phase 4 with type: code. For each one, normalize the graphify ids to a set:

python
ids = set()
fm = entity.frontmatter
if isinstance(fm.get("graphify_node_ids"), list):
    ids.update(fm["graphify_node_ids"])
if isinstance(fm.get("graphify_node_id"), str):  # legacy singular — backward compat
    ids.add(fm["graphify_node_id"])

Build a working map: id → vault_entity_path where vault_entity_path is the path of the entity file relative to <VAULT_PATH> (e.g. actors/billing-api/nodes/process-transaction.md).

If the touched-code-entities set is empty (run touched no code entities), skip directly to Phase 6 with phase_6_5_status = "skipped", phase_6_5_reason = "no_code_entities".

6.5.4 Locate and update graph nodes (atomic write)

Read graph.json and write the updated graph to a staging file:

bash
python3 - <<'PY'
import json, pathlib, sys

graph_path = pathlib.Path("<VAULT_PATH>/graphify-out/graph.json")
staging_path = graph_path.with_suffix(".json.staging")

with graph_path.open() as f:
    graph = json.load(f)

# Map from id → vault_entity_path was prepared in §6.5.3 — passed in as JSON.
target_map = json.loads("""<TARGET_MAP_JSON>""")

nodes_updated = 0
nodes_unmatched_ids = []
for node in graph.get("nodes", []):
    nid = node.get("id")
    if nid in target_map:
        # Idempotency by overwrite: always set, even if already present.
        node["vault_entity_path"] = target_map[nid]
        nodes_updated += 1

# Detect ids that did not match any node (graph diverged from vault — possible re-run of /graphify).
matched_ids = {n["id"] for n in graph.get("nodes", []) if "id" in n}
nodes_unmatched_ids = [tid for tid in target_map.keys() if tid not in matched_ids]

with staging_path.open("w") as f:
    json.dump(graph, f, indent=2, ensure_ascii=False)

print(json.dumps({
    "nodes_updated": nodes_updated,
    "unmatched_ids": nodes_unmatched_ids,
}))
PY

If the Python block exits non-zero (parse error, write error, etc.), do NOT run the mv — the original graph.json stays intact. Capture the error message, set phase_6_5_status = "failed", phase_6_5_reason = "<error>", and skip to Phase 6 without aborting /preserve. The vault's git state remains valid because no rename occurred.

6.5.5 Atomic swap

If the Python block succeeded:

bash
mv "<VAULT_PATH>/graphify-out/graph.json.staging" "<VAULT_PATH>/graphify-out/graph.json"

mv on the same filesystem is atomic at the filesystem level — readers either see the old or the new file, never a half-written one. Set phase_6_5_status = "applied" and capture nodes_updated, unmatched_ids from the Python output.

6.5.6 Record stats for Phase 7

Capture for the Phase 7 report and the skill's return payload:

yaml
graph_back_pointers:
  status: "applied" | "skipped" | "failed"
  reason: "<reason if skipped or failed, omit if applied>"
  nodes_updated: N           # 0 when skipped
  unmatched_ids: ["..."]     # ids in touched entities but not found in graph.json

These values are threaded through to Phase 7's report block under a new "Graph back-pointers" section and returned in the skill's result payload to callers (e.g. /bedrock:learn).


Phase 6 — Publish

6.1 Prepare commit

Determine the commit message following the convention:

Single entity:

vault(<type>): <verb> <name> [source: <source>]

Types: actor, person, team, concept, topic, discussion, project, source Verbs: creates, updates, links Sources: memory, github, jira, confluence, gdoc, csv, manual, session, preserve

Multiple entities:

vault: preserves N entities [source: <sources>]

Or, if called by /bedrock:learn:

vault: teaches <source-name>, creates N updates M entities [source: <type>]
6.2 Execute git workflow
6.2.1 Stage and commit
bash
# Stage touched entities (includes actor subfolders: actors/*/nodes/)
git -C <VAULT_PATH> add actors/ people/ teams/ topics/ discussions/ projects/ fleeting/

# Stage graphify back-pointer changes when Phase 6.5 ran successfully.
# Only run when graphify-out/graph.json exists — `git add` of a non-existent path errors out.
if [ -f "<VAULT_PATH>/graphify-out/graph.json" ]; then
  git -C <VAULT_PATH> add graphify-out/graph.json
fi

# Check if there is anything to commit
git -C <VAULT_PATH> diff --cached --quiet && echo "Nothing to commit" && exit 0
6.2.2 Read git strategy

Read the vault's git strategy from .bedrock/config.json:

bash
cat <VAULT_PATH>/.bedrock/config.json 2>/dev/null

Extract the git.strategy field. If the file does not exist or has no git key, default to "commit-push".

Valid values: "commit-push", "commit-push-pr", "commit-only".

6.2.3 Dispatch by strategy

Strategy: commit-push (default)

bash
git -C <VAULT_PATH> commit -m "<message per convention>"
git -C <VAULT_PATH> push origin main

If push fails (conflict):

bash
git -C <VAULT_PATH> pull --rebase origin main
git -C <VAULT_PATH> push origin main

If it fails 2x: STOP and inform the user. If there is no remote: commit locally and warn.


Strategy: commit-push-pr

First, check that gh is available:

bash
which gh 2>/dev/null

If gh is not found: warn the user and fall back to commit-push strategy (above).

If gh is available:

  1. Create a branch. Derive the branch name from the commit message:

    • Single entity: vault/<YYYY-MM-DD>-<entity-name> (e.g., vault/2026-04-15-billing-api)
    • Multiple entities: vault/<YYYY-MM-DD>-batch-<N>-entities (e.g., vault/2026-04-15-batch-7-entities)

    Check for collisions:

    bash
    git -C <VAULT_PATH> branch --list "vault/<YYYY-MM-DD>-<slug>*"

    If the branch already exists, append a counter: vault/2026-04-15-billing-api-2.

    bash
    git -C <VAULT_PATH> checkout -b <branch-name>
  2. Commit and push the branch:

    bash
    git -C <VAULT_PATH> commit -m "<message per convention>"
    git -C <VAULT_PATH> push origin <branch-name>
  3. Open a pull request:

    bash
    cd <VAULT_PATH> && gh pr create --title "<commit message>" --body "Automated by /bedrock:preserve" --base main
  4. Return to main:

    bash
    git -C <VAULT_PATH> checkout main

Strategy: commit-only

bash
git -C <VAULT_PATH> commit -m "<message per convention>"

Do not push. Output:

Git strategy: commit-only — changes committed locally. Use `git push` manually when ready.

Phase 7 — Report

Present to the user:

## Preserve — Report

### Entities created
| Type | Name | File | Source |
|---|---|---|---|
| actor | payment-new-api | actors/payment-new-api.md | github |

### Entities updated
| Type | Name | File | Changes |
|---|---|---|---|
| actor | billing-api | actors/billing-api.md | Recent Activity, wikilinks |

### Bidirectional links applied
| Source | Target | Type |
|---|---|---|
| [[billing-new-api]] | [[squad-payments]] | frontmatter: actors[] |
| [[squad-payments]] | [[billing-new-api]] | frontmatter: team |

### Graphify merge (only when Phase 0.2 ran)
| Metric | Value |
|---|---|
| Nodes added | N |
| Nodes merged | M |
| Edges added | P |
| Analysis marked stale | true / false |

Omit this section entirely when Phase 0.2 was skipped (no `graphify_output_path`, or backward-compat path match).

The same four fields are included in the skill's return payload (e.g., consumed by `/bedrock:learn`):

```yaml
graphify_merge:
  nodes_added: N
  nodes_merged: M
  edges_added: P
  stale_flag_set: true | false
Graph back-pointers (Phase 6.5)
MetricValue
Statusapplied / skipped / failed
Reason (skipped/failed)missing / empty / invalid_json / no_code_entities / <error>
Nodes updatedN (0 when skipped)
Unmatched idslist (ids present in touched entities but absent from graph.json)

When Status = applied, the back-pointer write is part of the same git commit as the entities (Phase 6 stages graphify-out/graph.json alongside the entity directories). When Status = skipped or failed, no graph mutation occurred and the vault's graph.json is untouched.

The same fields are included in the skill's return payload:

yaml
graph_back_pointers:
  status: "applied" | "skipped" | "failed"
  reason: "..."        # omit when applied
  nodes_updated: N
  unmatched_ids: ["..."]
Sources consulted
  • ✅ Local vault
  • ✅ / ❌ GitHub MCP
  • ✅ / ❌ Atlassian MCP
Git
  • Commit: vault: preserves 2 entities [source: github]
  • Push: ✅ success / ❌ failed (reason)
Warnings
  • [orphan wikilinks, ambiguous entities, MCP unavailable, etc.]

---

## Critical Rules

| # | Rule |
|---|---|
| 1 | **NEVER delete content** written by another agent or human (except the "Recent Activity" section in actors, which is temporal) |
| 2 | **NEVER overwrite frontmatter** — only merge new fields. NEVER delete existing fields. |
| 3 | **NEVER commit sensitive data** (credentials, tokens, PANs, CVVs) |
| 4 | **ALWAYS update** `updated_at` and `updated_by` on every touched entity |
| 5 | **ALWAYS use kebab-case** without accents for filenames |
| 6 | **ALWAYS follow the templates** from `_template.md` when creating new pages |
| 7 | **ALWAYS confirm** proposal with user before executing writes |
| 8 | **Maximum 2 push attempts** — after that, abort and inform |
| 9 | **Best-effort for external sources** — never block due to unavailable MCP |
| 10 | **Idempotency in wikilinks** — do not add a link that already exists |
| 11 | **Frontmatter keys in English**, values in the vault's configured language |
| 12 | **Bare wikilinks** — `[[name]]`, never `[[dir/name]]` |
| 13 | **Hierarchical tags** — `[type/actor]`, never `[actor]` |
| 14 | **Mandatory aliases** — at least 1 alias per new entity |
| 15 | **Mandatory callouts** — `[!warning] Deprecated` for deprecated, `[!danger] PCI Scope` for PCI |
| 16 | **Vault resolution first** — resolve `VAULT_PATH` before any file operation or git command |
| 17 | **All git commands use `git -C <VAULT_PATH>`** — never assume CWD is the vault |
| 18 | **All entity paths use `<VAULT_PATH>/` prefix** — `<VAULT_PATH>/actors/`, not `actors/` |
| 19 | **Graphify merge is append-only** — Phase 0.2 never deletes nodes, edges, obsidian content, or GRAPH_REPORT sections. On node-id collision: union `sources` by URL, take most-recent `updated_at`, union labels/tags. On edge collision by `(source, target, type)`: drop the incoming duplicate. |
| 20 | **Graphify merge backward-compat** — if `graphify_output_path` resolves to the same absolute path as `<VAULT_PATH>/graphify-out/`, Phase 0.2 is a no-op. Legacy callers and `/bedrock:sync` continue to work unchanged. |
| 21 | **Graphify merge is atomic** — `graph.json` is merged into a `.staging` file and atomically renamed. If validation or merge fails, the vault's `graph.json` is untouched. |
| 22 | **`.graphify_analysis.json` is marked stale, never recomputed** — Phase 0.2 sets `stale: true` on merge. `/bedrock:compress` owns recomputation. |
| 23 | **`actor_context` scopes the corpus** — when present in Phase 1.1 / 1.3 input, `file_type=document/paper` graphify nodes become `code` of that actor with `node_type ∈ {concept, decision}`; when absent, classification falls back to corpus-agnostic logic (concept global / topic / fleeting). The caller (e.g. `/learn`) decides which mode to invoke. |
| 24 | **Semantic grouping precedes filtering** — Phase 1.3 step 5 clusters graphify nodes by `semantically_similar_to ≥ code.cluster_threshold` (default 0.85) or community co-membership BEFORE the relevance filter. Each cluster maps to at most one `code` candidate carrying every `graphify_node_ids`. |
| 25 | **Per-actor cap, no global cap** — `code.max_per_actor` (default 200, from `.bedrock/config.json`) caps `code` candidates per actor. Ranking inside the cap: `is_god_node` > `degree` > `edge_count`. There is NO absolute global cap on the number of `code` entities in the vault. |
| 26 | **`graphify_node_ids` is always written as an array** — even when the cluster has a single member. At read time, `/preserve` accepts both the array form and the legacy singular `graphify_node_id` (string); it always writes the array on output. There is no batch migration. |
| 27 | **No English keyword regex for relevance** — the previous label allowlist (`Service|Controller|Client|Factory|Handler|Mapper|Gateway|Provider`) is REMOVED. Relevance comes from confidence + community signals (`is_god_node`, `degree > community average`, `edge_count ≥ 2`). Trivial label exclusion (`Test`, `Mock`, `Builder`, etc.) is preserved. |
| 28 | **Phase 6.5 is atomic** — `graph.json` is written to a `.staging` file and atomically renamed via `mv`. If the Python block fails, `mv` does NOT run, the vault's `graph.json` stays intact. Same pattern as Phase 0.2 (Rule 21). No write to `graph.json` occurs outside Phase 0.2 and Phase 6.5. |
| 29 | **Phase 6.5 is best-effort** — when `<VAULT_PATH>/graphify-out/graph.json` is missing, empty, invalid JSON, or no `code` entities were touched, Phase 6.5 silently skips with an explicit `status` and `reason` in the Phase 7 report. `/preserve` does NOT fail. Vaults without `graphify-out/` continue to work without the bridge. |
| 30 | **Phase 6.5 is idempotent** — `vault_entity_path` is always overwritten with the current path, never appended. Running `/preserve` twice in a row over the same entities does not duplicate fields nor alter any other node attribute. Unmatched ids (graphify re-run, id changed) surface as a warning but never block. |

© iurykrieger, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

Just SKILL.md in skills/preserve of iurykrieger/claude-bedrock.

Open the folder on GitHubat commit 7cac16c

Compare with similar skills

Preserve next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Preserve compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Preserve this skilliurykrieger/claude-bedrock105—~14kAutomated safety check: NotesMIT
Vexa MeetingsVexa-ai/vexa2.9k—~631Automated safety check: PassApache-2.0
Cascading Goal Trackerballred/obsidian-claude-pkm1.9k—~1.5kAutomated safety check: PassMIT
Daily Journalravila4/claude-adhd-skills158—~2.5kAutomated safety check: PassMIT
GranolaArtemXTech/claude-code-obsidian-starter225—~905Automated safety check: PassNone
Dbrain Processorsmixs/agent-second-brain393—~3.3kAutomated safety check: PassMIT

Similar skills

  • Vexa Meetings

    Vexa-ai/vexa

    Sends a Vexa bot into a meeting, follows the live transcript and writes meeting notes with speakers, decisions and action items into Obsidian, Notion or Markdown.

    2.9k GitHub stars~631 tokensUpdated yesterday
    Productivity & AutomationAuto-check passed
  • Cascading Goal Tracker

    ballred/obsidian-claude-pkm

    Tracks three-year, yearly, monthly, and weekly goals in linked Markdown files, calculating completion percentages and flagging stalled goals.

    1.9k GitHub stars~1.5k tokensUpdated 7 mo ago
    Knowledge ManagementAuto-check passed
  • Daily Journal

    ravila4/claude-adhd-skills

    Draft, organize, or update development journal entries. An agent skill from ravila4/claude-adhd-skills.

    158 GitHub stars~2.5k tokensUpdated 7 mo ago
    Productivity & AutomationAuto-check passed
  • Granola

    ArtemXTech/claude-code-obsidian-starter

    Query and sync Granola meetings to Obsidian vault. An agent skill from ArtemXTech/claude-code-obsidian-starter.

    225 GitHub stars~905 tokensUpdated 5 days ago
    Productivity & AutomationAuto-check passed
  • Dbrain Processor

    smixs/agent-second-brain

    Personal assistant for processing daily voice/text entries from Telegram.

    393 GitHub stars~3.3k tokensUpdated 2 mo ago
    Knowledge ManagementAuto-check passed
  • Setup

    jdpolasky/ai-chief-of-staff

    First-time setup for the Chief of Staff system. An agent skill from jdpolasky/ai-chief-of-staff.

    105 GitHub stars~3.5k tokensUpdated 7 days ago
    Knowledge ManagementAuto-check passed

More from iurykrieger/claude-bedrock

All 10 skills in this repo
  • Confluence To Markdown

    iurykrieger/claude-bedrock

    Internal fetcher module for Confluence pages. An agent skill from iurykrieger/claude-bedrock.

    105 GitHub starsUsed in 1 repo~2.8k tokens
    Auto-check: notes
  • Gdoc To Markdown

    iurykrieger/claude-bedrock

    Internal fetcher module for Google Docs and Sheets. An agent skill from iurykrieger/claude-bedrock.

    105 GitHub starsUsed in 1 repo~3.8k tokens
    Auto-check: notes
  • Healthcheck

    iurykrieger/claude-bedrock

    Read-only vault health diagnostic. An agent skill from iurykrieger/claude-bedrock.

    105 GitHub starsUsed in 1 repo~3.7k tokens
    Auto-check: notes
  • Vaults

    iurykrieger/claude-bedrock

    Manage registered Bedrock vaults. An agent skill from iurykrieger/claude-bedrock.

    105 GitHub starsUsed in 1 repo~1.3k tokens
    Auto-check: notes
  • Setup

    iurykrieger/claude-bedrock

    Initialize any folder as a Bedrock-powered Obsidian vault. An agent skill from iurykrieger/claude-bedrock.

    105 GitHub starsUsed in 1 repo~11k tokens
    Auto-check: notes
  • Compress

    iurykrieger/claude-bedrock

    Vault alignment engine. An agent skill from iurykrieger/claude-bedrock.

    105 GitHub starsUsed in 1 repo~9.9k tokens
    Auto-check: warnings

Works with

Questions about Preserve

What does Preserve do?

Single write point for the vault. An agent skill from iurykrieger/claude-bedrock. Preserve is an agent skill from iurykrieger/claude-bedrock. Single write point for the vault.

When should I use Preserve?

Preserve fits situations like: : bedrock preserve; bedrock-preserve; record in vault; /bedrock:preserve.

How do I install Preserve in Claude Code?

Run `npx skills add iurykrieger/claude-bedrock --skill preserve -a claude-code`. Or copy the skill folder (skills/preserve in iurykrieger/claude-bedrock) into .claude/skills/preserve in your project. Claude Code loads it when a task matches its description.

How do I install Preserve in Codex?

Run `npx skills add iurykrieger/claude-bedrock --skill preserve -a codex`. Or copy the skill folder (skills/preserve in iurykrieger/claude-bedrock) into .agents/skills/preserve in your project. Codex loads it when a task matches its description.

Can I use Preserve in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add iurykrieger/claude-bedrock --skill preserve -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/preserve, .gemini/skills/preserve, .github/skills/preserve and .opencode/skills/preserve in your project.

What does Preserve need to run?

Going by SKILL.md and its folder, Preserve needs the command-line tools its instructions call (git, python3 and gh). Our summary lists: Python 3. Its frontmatter pre-approves these tools: Bash, Read, Write, Edit, Glob, Grep, Agent, mcp__plugin_github_github__*, mcp__plugin_atlassian_atlassian__*.

Does Preserve access the network?

SKILL.md contains no URLs. Its commands use git and gh, which can reach the network depending on how they are called. This is read from the text; nothing was executed.

Is Preserve safe to install?

Our automated static check of SKILL.md found notes only (pre-approves every shell command (allowed-tools: bash)), nothing it rates as a warning. It is not a guarantee. Review the folder before installing.

What licence does Preserve use?

Preserve is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Preserve use?

About 14k tokens (SKILL.md is roughly 54k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Preserve?

Skills that share tags, products or a category with Preserve: Vexa Meetings (Vexa-ai/vexa, 2.9k stars), Cascading Goal Tracker (ballred/obsidian-claude-pkm, 1.9k stars), Daily Journal (ravila4/claude-adhd-skills, 158 stars) and Granola (ArtemXTech/claude-code-obsidian-starter, 225 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Preserve?

iurykrieger (a GitHub user) maintains it in iurykrieger/claude-bedrock, which has 105 GitHub stars. The repository holds 10 skills in this directory. The repository was last updated on May 5, 2026.

Source: iurykrieger/claude-bedrock on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.