AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Preserve

skill-iurykrieger-claude-bedrock-preserve · by iurykrieger

>

No reviews yet
0 installs
8 views
0.0% view→install

Install

$ agentstack add skill-iurykrieger-claude-bedrock-preserve

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access Used
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-iurykrieger-claude-bedrock-preserve)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
3mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Preserve? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

/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: /../../entities/
  • Templates: /../../templates/{type}/_template.md
  • Plugin CLAUDE.md: /../../CLAUDE.md (already injected automatically into context)

Where `` 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 . If found, extract the vault name and remove it from the arguments before further parsing.

Step 2 — Resolve vault path:

  1. If --vault was provided:

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

  1. If no --vault flag — CWD detection:

Read /../../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.

  1. 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.

  1. If no resolution:

Error — "No vault resolved. Available vaults:" followed by the registry listing. "Use --vault to specify, or run /bedrock:setup to register a vault."

Step 3 — Validate vault path:

test -d "" && echo "exists" || echo "missing"

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

Step 4 — Read vault config:

cat /.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 `` as the root.

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

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:

git -C  pull --rebase origin main

If the pull fails:

  • No remote configured: warn "No remote configured. Working locally." and proceed.
  • Pull conflict: git -C 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 /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:

incoming_real=$(cd "" 2>/dev/null && pwd -P)
vault_real=$(cd "/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 /graph.json exists, is non-empty, and parses as valid JSON. If invalid, abort with a clear error and do NOT mutate the vault:

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

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

if [ ! -d "/graphify-out" ]; then
  mkdir -p ""
  cp -R "" "/graphify-out"
  echo "Phase 0.2: first ingestion — promoted incoming graphify output to /graphify-out/."
  # record: nodes_added = , nodes_merged = 0, edges_added = , 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:

python3 - /graphify-out/graph.json")
incoming_path = pathlib.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:

mv "/graphify-out/graph.json.staging" "/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 /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 /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 /graphify-out/obsidian/.
mkdir -p "/graphify-out/obsidian"
for src in "/obsidian/"*.md; do
  [ -e "$src" ] || continue
  SRC_FILE=$(awk -F'"' '/^source_file:/{print $2; exit}' "$src")
  case "$SRC_FILE" in /tmp/*) continue ;; esac
  dest="/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 /GRAPH_REPORT.md exists:

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

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

analysis="/graphify-out/.graphify_analysis.json"
stale_flag_set=false
if [ -f "$analysis" ]; then
  python3 - /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:   # 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
  1. Inferred action — for each entity:
  • If the entity already exists in the vault: update
  • If the entity does not exist: create
  1. Content — what was said about each entity in the input
  1. Relations — infer which entities relate to each other based on context
  1. 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."
  1. 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
  1. 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)
  1. Read configuration from /.bedrock/config.json (best-effort):
  • code.max_per_actor: integer, default 200. Per-actor cap on the number of code candidates prod

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.