Skill: daniel-refahi-ikara/dr-context-pipeline (version 1.8.0)

--- name: dr-context-pipeline description: "Deterministic memory/context pipeline for agents: route a user message, retrieve relevant memory snippets, compress into a cited Context Pack (sources are snippet IDs), lint, and fall back safely. Prerequisite: a file-based memory layout with memory/always_on.md + topic files (works out-of-the-box with dr-memory-foundation). Use when building or standardizing agent memory, reducing prompt bloat, implementing retrieval+compression, creating a context pack, designing a memory pipeline, adding lint gates, or setting up golden regression tests for agent context. After install, users can simply say: Apply dr-context-pipeline as default behavior." --- # DR Context Pipeline (retrieval + compression + lint) Use this skill to standardize how an agent loads memory into its prompt **for correctness**. ## Prerequisites - A file-based memory layout that includes `memory/always_on.md` (policy header + topic catalog) and topic files under `memory/topics/`. - Recommended: install **dr-memory-foundation** (or implement an equivalent structure). ## Quick install commands (copy/paste) These commands work from anywhere because they set `$WORKSPACE` (defaults to `~/.openclaw/workspace`). Rerun them whenever you need to re-apply the pipeline: ```bash export WORKSPACE=${WORKSPACE:-~/.openclaw/workspace} cd "$WORKSPACE" clawhub install dr-context-pipeline --version X.Y.Z --dir skills python3 ./skills/dr-context-pipeline/scripts/install_pipeline.py --target "$WORKSPACE/context_pipeline" ls -1 "$WORKSPACE/context_pipeline" git -C "$WORKSPACE" diff -U20 AGENTS.md | cat python3 ./skills/dr-context-pipeline/scripts/validate_pipeline.py --context-root "$WORKSPACE/context_pipeline" python3 ./skills/dr-context-pipeline/scripts/memory_watchdog.py --freshness-minutes 240 --min-bytes 200 git -C "$WORKSPACE" status -sb context_pipeline AGENTS.md echo "CONTEXT PIPELINE APPLY COMPLETE" ``` Paste every command’s output. If any step fails or the watchdog reports a gap, stop and reply `NOT EXECUTED: <reason>`. ## Apply to this workspace When the user asks to **apply** this skill (for example: `Apply dr-context-pipeline as default behavior`), follow this exact order and paste the evidence for every step: All commands assume you’ve set `WORKSPACE=${WORKSPACE:-~/.openclaw/workspace}` and are running from that directory. 1. **Lay down the files** — run `python3 ./skills/dr-context-pipeline/scripts/install_pipeline.py --target context_pipeline` (adds/updates the workspace copy from `assets/context_pipeline/`). Keep the script output in the transcript (hash summary + file count). 2. **Show the tree** — `ls -1 context_pipeline` so the user sees which files are now present. 3. **Patch `AGENTS.md`** — read the file, insert/refresh the “Context Pipeline” instructions, and include a `git diff -U20 AGENTS.md` (or equivalent) snippet in your reply. Preserve everything else. 4. **Validate** — run `python3 ./skills/dr-context-pipeline/scripts/validate_pipeline.py --context-root context_pipeline` and paste the PASS/FAIL summary. If it fails, stop and report `NOT EXECUTED` with the error. 5. **Run the memory watchdog** — `python3 ./skills/dr-context-pipeline/scripts/memory_watchdog.py --freshness-minutes 240 --min-bytes 200` (tune as needed). Paste the JSON output; if status ≠ OK, stop and reply `NOT EXECUTED: memory gap` after quoting the issues. 6. **Final state** — show `git status -sb context_pipeline AGENTS.md` (or `git status -sb` if cleaner) so the user can see what changed. 7. **Success banner** — `echo "CONTEXT PIPELINE APPLY COMPLETE"` so the transcript clearly shows the run finished cleanly. This apply flow must be idempotent: if the files already match and `AGENTS.md` already contains the section, the diff should be empty but you still run the installer + validator and show their outputs. ## Memory commit / continue workflow - When Daniel says “memorize this” (or similar), run the checklist in `references/MEMORY_COMMIT.md` (daily log, now, open-loops, topic file, MEMORY.md) and confirm which files changed. - When he says “let’s continue” after a reset, reload `memory/now.md`, `open-loops`, and the relevant topic files so you can summarize where things left off before acting. ## Runtime evidence contract (non-optional) Every single task must follow the Runtime Evidence Checklist in `references/RUNTIME_CHECKLIST.md`: - Emit the **Retrieval Bundle JSON** that conforms to `references/schemas/retrieval_bundle.schema.json`. - Emit the **Context Pack JSON** that conforms to `references/schemas/context_pack.schema.json` (or explicitly state the lint failure and fall back to raw snippets). - List which snippet IDs are being passed forward. - Only then provide the user-facing reasoning/result. - If any step fails, reply `NOT EXECUTED: <reason>` and stop. Casual prompts (“go for it”, “review it”, etc.) do **not** suspend this contract. If the user wants a lighter interaction they can explicitly waive it. ## Operating procedure (default) 1) Load the always-on policy + topic catalog (your `memory/always_on.md`). 2) Route the message deterministically (task type + caps) using `references/router.yml`. 3) Retrieve top relevant snippets from your memory store; emit a **Retrieval Bundle JSON** (see schema). 4) Compress Retrieval Bundle → **Context Pack JSON** using `references/compressor_prompt.txt`. - **IMPORTANT:** Context Pack `sources` MUST be **snippet IDs only** (`S1`, `S2`, …). 5) Lint the Context Pack. If lint fails, **skip compression** and fall back to raw retrieved snippets. 6) Call the main reasoning model with: always-on policy header + Context Pack (+ raw snippets for high-stakes tasks) + user message. ## What to read / use - Router + caps: `references/router.yml` - Compressor prompt: `references/compressor_prompt.txt` - Retrieval Bundle schema: `references/schemas/retrieval_bundle.schema.json` - Context Pack schema: `references/schemas/context_pack.schema.json` - Runtime checklist: `references/RUNTIME_CHECKLIST.md` - Golden tests starter suite: `references/tests/golden.json` - Installer/validator/watchdog scripts: `scripts/install_pipeline.py`, `scripts/validate_pipeline.py`, `scripts/memory_watchdog.py` ## Notes - Keep “always-on policy header” tiny (invariants only). Put everything else behind retrieval. - If you need deterministic snippet IDs, follow the stable ordering guidance in `references/deterministic_ids.md`.

path: assets/context_pipeline/README.md
content: # Context Pipeline v1 (retrieval + compression)

Portable spec for agents:

1) Always include `always_on.md`.
2) Route message → task_type using `router.yml`.
3) Retrieve snippets from memory according to router policy; emit Retrieval Bundle JSON.
4) Compress Retrieval Bundle → Context Pack JSON using `compressor_prompt.txt`.
5) Lint Context Pack; if it fails, fall back to raw snippets.
6) Feed Always-on policy + Context Pack (+ optional raw snippets) + user message to the reasoning model.

Files:
- `always_on.md` — tiny always-on policy + topic catalog (template).
- `router.yml` — deterministic task router + retrieval caps.
- `compressor_prompt.txt` — prompt for cheap compressor model.
- `schemas/retrieval_bundle.schema.json` — JSON Schema for retrieval bundle.
- `schemas/context_pack.schema.json` — JSON Schema for context pack.
- `tests/golden.json` — starter golden tests.

Version: v1.0


path: assets/context_pipeline/always_on.md
content: # always_on.md — Always-on policy + topic catalog (TEMPLATE)

## Always-on policy header (keep tiny)
- Communication policy: (fill in)
- Truthfulness: do not invent facts; if not in retrieved snippets or the current message, ask.
- Use memory responsibly: prefer cited snippets over freeform recall when using persistent info.
- Output conventions: (fill in)

## Topic catalog (what memory exists)

### Index / dashboards
- `MEMORY.md` — preferences + indexes. Keywords: preferences, standing rules, indexes, topic map.
- `memory/now.md` — what matters right now. Keywords: priorities, current context, quick links, recent changes.
- `memory/open-loops.md` — unresolved items. Keywords: follow-ups, blockers, TODOs, waiting on.
- `memory/automation.md` — automation registry. Keywords: cron jobs, schedules, delivery targets, reliability notes.

### Core topics
- `memory/topics/discord-rules.md` — communication rules. Keywords: DM-only, addressing, triggering, channel policy.
- `memory/topics/doc-review-mode.md` — doc review posture. Keywords: untrusted claims, validation questions, customer-facing wording, risk.
- `memory/topics/openclaw-ops.md` — OpenClaw ops notes. Keywords: config, providers, delivery debugging, cron, troubleshooting.
- `memory/topics/people.md` — people/accounts. Keywords: identities, user IDs, roles, contacts.
- `memory/topics/glossary.md` — authoritative terms. Keywords: definitions, canonical names, acronyms.

### Domain topics (example)
- `memory/topics/<domain>-*.md` — domain knowledge. Keywords: (fill in)


path: assets/context_pipeline/compressor_prompt.txt
content: SYSTEM (compressor)
You are a Context Pack compressor. You receive a Retrieval Bundle JSON containing:
- the user message
- always-on policy/topic catalog
- retrieved memory snippets

Your job is to output a Context Pack JSON object that helps a downstream reasoning model answer correctly.

HARD RULES (non-negotiable):
1) You may ONLY use information present in the Retrieval Bundle JSON. Do not invent facts.
2) Every item in must_follow, relevant_context, conflicts_or_ambiguities, and exclude_as_irrelevant MUST include sources.
3) sources MUST be an array of snippet IDs only, like ["S1","S3"]. Do NOT use file paths or line ranges in sources.
4) If something is not explicitly supported by snippets, put it in unknowns_to_confirm.
5) If snippets conflict, report it in conflicts_or_ambiguities and cite both sides. Do not resolve conflicts.
6) Output MUST be valid JSON and MUST match the Context Pack schema exactly. No extra keys. No commentary.

Optimization target: correctness and constraint retention over brevity.

USER
Here is the Retrieval Bundle JSON. Produce the Context Pack JSON now:
<RETRIEVAL_BUNDLE_JSON>


path: assets/context_pipeline/router.yml
content: version: "1.0"

# Deterministic router: first match wins.
# Matching is case-insensitive substring against the user message.

router:
  - task_type: ops
    match_any:
      - "cron"
      - "schedule"
      - "job"
      - "automation"
      - "reminder"
      - "didn't receive"
      - "didnt receive"
      - "not delivered"
      - "missed"
      - "failed"
      - "error"
      - "401"
      - "unauthorized"
      - "dm not working"
      - "bot not responding"

  - task_type: briefing
    match_any:
      - "daily briefing"
      - "digest"
      - "ai briefing"
      - "briefing"

  - task_type: doc_review
    match_any:
      - "review this"
      - "proofread"
      - "customer-facing"
      - "documentation"
      - "release notes"
      - "rewrite"
      - "tone"
      - "wording"
      - "claim"
      - "accuracy"

  - task_type: debugging
    match_any:
      - "bug"
      - "stack trace"
      - "exception"
      - "not working"
      - "fails when"
      - "repro"
      - "logs"
      - "trace"
      - "latency"
      - "timeout"

  - task_type: planning
    match_any:
      - "design"
      - "architecture"
      - "implement"
      - "rollout"
      - "plan"
      - "proposal"
      - "what should we do"
      - "recommend"
      - "strategy"

  - task_type: qna
    match_any:
      - "what is"
      - "how do"
      - "why"
      - "explain"
      - "difference between"

  - task_type: other
    match_any: []

# Derived queries are rules-based additions to improve retrieval.
derived_queries:
  when_contains:
    discord:
      match_any: ["discord", "dm", "channel"]
      add:
        - "discord rules DM-only communication policy"

    memory_pipeline:
      match_any: ["memory", "retrieval", "compression", "context pack", "always-on"]
      add:
        - "always_on topic catalog retrieval compression context pack"
        - "memory design now open-loops automation topics"

    briefing_delivery:
      match_any: ["daily briefing", "digest", "briefing", "didn't receive", "not delivered", "missed"]
      add:
        - "Daily AI Briefing cron delivery discord"
        - "openclaw ops discord provider delivery"
        - "automation registry daily briefing"

    doc_review:
      match_any: ["doc", "documentation", "customer-facing", "review", "rewrite"]
      add:
        - "doc review mode untrusted claims validation questions"

    ikara:
      match_any: ["ikara"]
      add:
        - "ikara platform ops access security controls integrations"

# Retrieval policy: correctness-first defaults.
retrieval_policy:
  always_include_paths:
    - "memory/always_on.md"

  sources_priority:
    - "memory/topics/*.md"
    - "memory/now.md"
    - "memory/open-loops.md"
    - "memory/automation.md"
    - "MEMORY.md"
    - "memory/YYYY-MM-DD.md" # conditional

  caps_by_task_type:
    ops:
      top_k_snippets: 14
      max_lines_per_snippet: 60
      max_snippets_per_file: 2
      include_daily_logs: true
      max_daily_log_snippets: 3
      daily_log_lookback_days: 7

    briefing:
      top_k_snippets: 12
      max_lines_per_snippet: 60
      max_snippets_per_file: 2
      include_daily_logs: true
      max_daily_log_snippets: 1
      daily_log_lookback_days: 3

    doc_review:
      top_k_snippets: 10
      max_lines_per_snippet: 60
      max_snippets_per_file: 2
      include_daily_logs: false
      max_daily_log_snippets: 0

    planning:
      top_k_snippets: 12
      max_lines_per_snippet: 60
      max_snippets_per_file: 2
      include_daily_logs: false
      max_daily_log_snippets: 0

    qna:
      top_k_snippets: 10
      max_lines_per_snippet: 60
      max_snippets_per_file: 2
      include_daily_logs: false
      max_daily_log_snippets: 0

    other:
      top_k_snippets: 10
      max_lines_per_snippet: 60
      max_snippets_per_file: 2
      include_daily_logs: false
      max_daily_log_snippets: 0

# Daily-log switch: only enable daily logs when user asks explicitly about time/recency.
daily_log_trigger:
  match_any: ["yesterday", "today", "last time", "previously", "what did we decide", "did we"]


path: assets/context_pipeline/schemas/context_pack.schema.json
content: {
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "context-pack.schema.json",
  "title": "Context Pack",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "version",
    "task_type",
    "must_follow",
    "relevant_context",
    "conflicts_or_ambiguities",
    "unknowns_to_confirm",
    "exclude_as_irrelevant",
    "retrieval_notes"
  ],
  "properties": {
    "version": { "type": "string", "const": "1.0" },
    "task_type": {
      "type": "string",
      "enum": ["qna", "planning", "debugging", "doc_review", "ops", "briefing", "other"]
    },
    "must_follow": {
      "type": "array",
      "items": { "$ref": "#/$defs/sourcedRule" }
    },
    "relevant_context": {
      "type": "array",
      "items": { "$ref": "#/$defs/sourcedContext" }
    },
    "conflicts_or_ambiguities": {
      "type": "array",
      "items": { "$ref": "#/$defs/sourcedConflict" }
    },
    "unknowns_to_confirm": {
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["question", "why_needed"],
        "properties": {
          "question": { "type": "string", "minLength": 1 },
          "why_needed": { "type": "string", "minLength": 1 }
        }
      }
    },
    "exclude_as_irrelevant": {
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["item", "why", "sources"],
        "properties": {
          "item": { "type": "string", "minLength": 1 },
          "why": { "type": "string", "minLength": 1 },
          "sources": { "$ref": "#/$defs/sources" }
        }
      }
    },
    "retrieval_notes": {
      "type": "object",
      "additionalProperties": false,
      "required": ["included_sources", "dropped_due_to_limits"],
      "properties": {
        "included_sources": { "type": "array", "items": { "type": "string" } },
        "dropped_due_to_limits": { "type": "array", "items": { "type": "string" } }
      }
    }
  },
  "$defs": {
    "sources": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "string",
        "pattern": "^S[0-9]+$"
      }
    },
    "sourcedRule": {
      "type": "object",
      "additionalProperties": false,
      "required": ["rule", "why", "sources"],
      "properties": {
        "rule": { "type": "string", "minLength": 1 },
        "why": { "type": "string", "minLength": 1 },
        "sources": { "$ref": "#/$defs/sources" }
      }
    },
    "sourcedContext": {
      "type": "object",
      "additionalProperties": false,
      "required": ["fact_or_context", "why", "sources"],
      "properties": {
        "fact_or_context": { "type": "string", "minLength": 1 },
        "why": { "type": "string", "minLength": 1 },
        "sources": { "$ref": "#/$defs/sources" }
      }
    },
    "sourcedConflict": {
      "type": "object",
      "additionalProperties": false,
      "required": ["issue", "options", "sources"],
      "properties": {
        "issue": { "type": "string", "minLength": 1 },
        "options": {
          "type": "array",
          "minItems": 2,
          "maxItems": 2,
          "items": { "type": "string", "minLength": 1 }
        },
        "sources": {
          "type": "array",
          "minItems": 2,
          "items": { "type": "string", "pattern": "^S[0-9]+$" }
        }
      }
    }
  }
}


path: assets/context_pipeline/schemas/retrieval_bundle.schema.json
content: {
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "retrieval-bundle.schema.json",
  "title": "Retrieval Bundle",
  "type": "object",
  "additionalProperties": false,
  "required": ["retrieval_version", "user_message", "always_on", "query_plan", "snippets", "dropped"],
  "properties": {
    "retrieval_version": { "type": "string", "const": "1.0" },
    "user_message": {
      "type": "object",
      "additionalProperties": false,
      "required": ["text"],
      "properties": {
        "text": { "type": "string", "minLength": 1 },
        "timestamp_utc": { "type": "string" }
      }
    },
    "always_on": {
      "type": "object",
      "additionalProperties": false,
      "required": ["path", "content"],
      "properties": {
        "path": { "type": "string" },
        "content": { "type": "string" },
        "content_hash": { "type": "string" }
      }
    },
    "query_plan": {
      "type": "object",
      "additionalProperties": true
    },
    "snippets": {
      "type": "array",
      "items": { "$ref": "#/$defs/snippet" }
    },
    "dropped": {
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["path", "reason"],
        "properties": {
          "path": { "type": "string" },
          "reason": { "type": "string" }
        }
      }
    }
  },
  "$defs": {
    "snippet": {
      "type": "object",
      "additionalProperties": false,
      "required": ["snippet_id", "path", "start_line", "end_line", "citation", "text"],
      "properties": {
        "snippet_id": { "type": "string", "pattern": "^S[0-9]+$" },
        "stable_id": { "type": "string" },
        "path": { "type": "string" },
        "start_line": { "type": "integer", "minimum": 1 },
        "end_line": { "type": "integer", "minimum": 1 },
        "citation": { "type": "string" },
        "text": { "type": "string" },
        "score": { "type": "number" },
        "tags": { "type": "array", "items": { "type": "string" } },
        "source_group": { "type": "string" }
      }
    }
  }
}


path: assets/context_pipeline/tests/golden.json
content: [
  {
    "id": "discord_dm_only",
    "user_message": "Can you reply in #general?",
    "expect": {
      "retrieval": {
        "must_include_paths": ["memory/always_on.md", "memory/topics/discord-rules.md"],
        "max_daily_log_snippets": 0
      },
      "context_pack": {
        "task_type": "ops",
        "must_follow_contains": ["Discord DM-only"],
        "unknowns_count_max": 2
      }
    }
  },
  {
    "id": "briefing_missed",
    "user_message": "I didn’t receive today’s briefing again",
    "expect": {
      "retrieval": {
        "must_include_paths": ["memory/automation.md", "memory/topics/openclaw-ops.md"],
        "max_daily_log_snippets": 3
      },
      "context_pack": {
        "task_type": "ops"
      }
    }
  },
  {
    "id": "doc_review_mode",
    "user_message": "Review this customer-facing doc paragraph and tell me what claims need validation",
    "expect": {
      "retrieval": {
        "must_include_paths": ["memory/topics/doc-review-mode.md"],
        "max_daily_log_snippets": 0
      },
      "context_pack": {
        "task_type": "doc_review"
      }
    }
  },
  {
    "id": "memory_pipeline_question",
    "user_message": "How does our retrieval + compression memory pipeline work?",
    "expect": {
      "retrieval": {
        "must_include_paths": ["memory/always_on.md"],
        "max_daily_log_snippets": 0
      },
      "context_pack": {
        "task_type": "planning"
      }
    }
  }
]


path: references/APPLY.md
content: # Apply checklist — dr-context-pipeline

Use this whenever someone says things like:
- Apply dr-context-pipeline as default behavior
- Enable the context pipeline
- Make this the default context-loading workflow

## Command sequence (no shortcuts)
Set `WORKSPACE=${WORKSPACE:-~/.openclaw/workspace}` and run these from that directory (the same one that contains `AGENTS.md`).
1. `python3 ./skills/dr-context-pipeline/scripts/install_pipeline.py --target context_pipeline`
   - Lays down / refreshes the canonical files from `assets/context_pipeline/`.
   - Paste the script’s summary (file count + hash digest sample) in your reply.
2. `ls -1 context_pipeline`
   - Shows exactly which files are now present.
3. Update `AGENTS.md`
   - Insert or refresh the Context Pipeline instructions.
   - Paste a `git diff -U20 AGENTS.md` (or equivalent) so the user can see the changes.
4. `python3 ./skills/dr-context-pipeline/scripts/validate_pipeline.py --context-root context_pipeline`
   - Runs integrity checks (hash comparison, schema parse, golden test sanity).
   - Paste the PASS/FAIL block. On failure, stop and report `NOT EXECUTED: <error>`.
5. `python3 ./skills/dr-context-pipeline/scripts/memory_watchdog.py --freshness-minutes 240 --min-bytes 200`
   - Ensures today’s `memory/YYYY-MM-DD*.md` exists, is non-empty, and was updated recently.
   - Paste the JSON output. If status ≠ OK, stop immediately and reply `NOT EXECUTED: memory gap`.
6. `git status -sb context_pipeline AGENTS.md`
   - Confirms the final state.
7. `echo "CONTEXT PIPELINE APPLY COMPLETE"`
   - Print a success banner so the transcript has an explicit “done” line.

## Evidence to include in the transcript
- Raw command output for every step above (installer summary, directory listing, diff, validator summary, watchdog JSON, git status).
- If no files changed (idempotent apply), explicitly state that the installer/validator/watchdog still ran and returned clean results.

## Failure contract
If any command fails or the validator/watchdog reports an error, reply `NOT EXECUTED: <reason>` and wait for further instructions. Do **not** claim success without receipts.


path: references/MEMORY_COMMIT.md
content: # Memory commit checklist — “memorize this” workflow

Trigger phrases: “memorize this”, “store this”, “commit this session”, “we’ll continue later”.

## 1. Append to today’s daily log
```bash
cd ~/.openclaw/workspace
cat <<'EOF' >> memory/$(date -u +%Y-%m-%d).md
- <brief summary of what happened>
- Decisions:
  - <bullet>
- Follow-ups:
  - <bullet>
EOF
```
(Use UTC date; create the file if it doesn’t exist.)

## 2. Update `memory/now.md`
Keep a short, current-state list. Example:
```
## Current focus
- Manual dr-trading-system run pending (awaiting market-data-provider smoke test).
- Repo permissions: need GitHub push access for Hannah.
```

## 3. Update `memory/open-loops.md`
Add actionable follow-ups with owners/status. Example:
```
### Trading system
- [ ] Verify GitHub push permissions (blocked on access request).
- [ ] Schedule daily report once manual run validated.
```

## 4. Create/update the relevant topic file
If a topic exists, edit it; otherwise create one under `memory/topics/`. Example (`memory/topics/stock-modules.md`):
```
## Modules
- market-data-provider → fetch/normalize data only
...
## Contracts
- strategy-engine depends on market-data-provider
...
```

## 5. Record durable rules in `MEMORY.md`
Add a short bullet under the appropriate section, e.g.:
```
## Standing operating rules
- Stock-system modules are maintained independently with stable contracts (see memory/topics/stock-modules.md).
```

## 6. Confirm
- Run `python3 ./skills/dr-context-pipeline/scripts/memory_watchdog.py --freshness-minutes 240 --min-bytes 200` to ensure files exist.
- Mention in chat that the session was committed and list which files changed.


path: references/RUNTIME_CHECKLIST.md
content: # Runtime Evidence Checklist — dr-context-pipeline

Agents **must** follow this checklist on every task once the context pipeline is installed.

1. **Retrieval Bundle JSON**
   - Load `memory/always_on.md`.
   - Route + retrieve according to `context_pipeline/router.yml` caps.
   - Emit the Retrieval Bundle object (matching `references/schemas/retrieval_bundle.schema.json`) in the transcript.
   - Include `snippet_id`, `path`, `start_line`, `end_line`, `citation`, and `text` for every snippet.
   - List any `dropped` entries with reasons.

2. **Context Pack JSON**
   - Compress using `context_pipeline/compressor_prompt.txt`.
   - Emit the Context Pack object (matching `references/schemas/context_pack.schema.json`).
   - `sources` arrays **must** be snippet IDs (e.g., `["S1","S3"]`).
   - If lint fails, explicitly state `CONTEXT PACK LINT FAILED` and paste the lint error.

3. **Snippet Summary + Pass-through**
   - Provide a short bullet list of which snippets (by ID) you are passing into the reasoning step and why.
   - Note any snippets intentionally excluded via `exclude_as_irrelevant`.

4. **Reasoning Output**
   - Only after the artifacts above, proceed with the user-facing answer.
   - Reference snippet IDs in-line (e.g., `Source: S2`).

5. **Failure Contract**
   - If any step above cannot be completed, reply exactly `NOT EXECUTED: <reason>` and stop. Do **not** invent bundles or packs.

This checklist is non-optional. If the user issues a casual instruction like “go for it,” you still must emit the Retrieval Bundle + Context Pack or return `NOT EXECUTED`.


path: references/compressor_prompt.txt
content: SYSTEM (compressor)
You are a Context Pack compressor. You receive a Retrieval Bundle JSON containing:
- the user message
- always-on policy/topic catalog
- retrieved memory snippets

Your job is to output a Context Pack JSON object that helps a downstream reasoning model answer correctly.

HARD RULES (non-negotiable):
1) You may ONLY use information present in the Retrieval Bundle JSON. Do not invent facts.
2) Every item in must_follow, relevant_context, conflicts_or_ambiguities, and exclude_as_irrelevant MUST include sources.
3) sources MUST be an array of snippet IDs only, like ["S1","S3"]. Do NOT use file paths or line ranges in sources.
4) If something is not explicitly supported by snippets, put it in unknowns_to_confirm.
5) If snippets conflict, report it in conflicts_or_ambiguities and cite both sides. Do not resolve conflicts.
6) Output MUST be valid JSON and MUST match the Context Pack schema exactly. No extra keys. No commentary.

Optimization target: correctness and constraint retention over brevity.

USER
Here is the Retrieval Bundle JSON. Produce the Context Pack JSON now:
<RETRIEVAL_BUNDLE_JSON>


path: references/configure_prompt.md
content: # Activation prompt (copy/paste fallback)

Preferred activation phrase:
`Apply dr-context-pipeline as default behavior`

If you need a longer prompt that bakes in the evidence contract, use this:

---
Use **DR Context Pipeline** as your default context-loading and memory protocol.

**Spec files** live under `context_pipeline/` (installed via `skills/dr-context-pipeline/scripts/install_pipeline.py`).

For every user message you must:
1. Load `memory/always_on.md` verbatim.
2. Route deterministically using `context_pipeline/router.yml` (task_type + derived queries + caps).
3. Retrieve memory snippets and build a Retrieval Bundle JSON that matches `context_pipeline/schemas/retrieval_bundle.schema.json`. Include snippet IDs (S1..Sn), citations, and dropped snippets.
4. Compress to a Context Pack JSON using `context_pipeline/compressor_prompt.txt` and `context_pipeline/schemas/context_pack.schema.json`. `sources` arrays must list snippet IDs only.
5. Lint the Context Pack. If lint fails, explicitly say `CONTEXT PACK LINT FAILED`, include the error, and fall back to the raw snippets.
6. Emit (in this order) → Retrieval Bundle JSON, Context Pack JSON (or lint failure), snippet summary, then the user-facing reasoning.
7. If any step cannot be completed exactly, reply `NOT EXECUTED: <reason>`.

**Never** claim you performed a step without pasting the artifact. Casual prompts do not waive this requirement.

To install/refresh the files and validate the setup, run:
```
export WORKSPACE=${WORKSPACE:-~/.openclaw/workspace}
cd "$WORKSPACE"
clawhub install dr-context-pipeline --version X.Y.Z --dir skills
python3 ./skills/dr-context-pipeline/scripts/install_pipeline.py --target "$WORKSPACE/context_pipeline"
ls -1 "$WORKSPACE/context_pipeline"
git -C "$WORKSPACE" diff -U20 AGENTS.md | cat
python3 ./skills/dr-context-pipeline/scripts/validate_pipeline.py --context-root "$WORKSPACE/context_pipeline"
python3 ./skills/dr-context-pipeline/scripts/memory_watchdog.py --freshness-minutes 240 --min-bytes 200
git -C "$WORKSPACE" status -sb context_pipeline AGENTS.md
echo "CONTEXT PIPELINE APPLY COMPLETE"
```
(Include the command outputs in the confirmation.)
---


path: references/deterministic_ids.md
content: # Deterministic snippet ID assignment (Context Pipeline v1)

Goal: keep snippet IDs (`S1..Sn`) stable across runs for easier debugging/testing.

Recommended procedure:

1) Collect candidate snippets with scores.
2) Apply deterministic caps in order:
   - per-file cap
   - daily-log cap
   - global top-K cap
3) Dedupe:
   - exact dedupe key: `path:start_line-end_line`
   - if semantic/overlap dedupe is used, resolve ties deterministically (higher score → path → start_line).
4) Sort final snippets by stable comparator:
   - source group priority (topics → dashboards → daily logs)
   - score (desc)
   - path (asc)
   - start_line (asc)
5) Assign IDs in that final order: `S1..Sn`.

Optional: add `stable_id` (hash) per snippet for resilience to ordering shifts.


path: references/router.yml
content: version: "1.0"

# Deterministic router: first match wins.
# Matching is case-insensitive substring against the user message.

router:
  - task_type: ops
    match_any:
      - "cron"
      - "schedule"
      - "job"
      - "automation"
      - "reminder"
      - "didn't receive"
      - "didnt receive"
      - "not delivered"
      - "missed"
      - "failed"
      - "error"
      - "401"
      - "unauthorized"
      - "dm not working"
      - "bot not responding"

  - task_type: briefing
    match_any:
      - "daily briefing"
      - "digest"
      - "ai briefing"
      - "briefing"

  - task_type: doc_review
    match_any:
      - "review this"
      - "proofread"
      - "customer-facing"
      - "documentation"
      - "release notes"
      - "rewrite"
      - "tone"
      - "wording"
      - "claim"
      - "accuracy"

  - task_type: debugging
    match_any:
      - "bug"
      - "stack trace"
      - "exception"
      - "not working"
      - "fails when"
      - "repro"
      - "logs"
      - "trace"
      - "latency"
      - "timeout"

  - task_type: planning
    match_any:
      - "design"
      - "architecture"
      - "implement"
      - "rollout"
      - "plan"
      - "proposal"
      - "what should we do"
      - "recommend"
      - "strategy"

  - task_type: qna
    match_any:
      - "what is"
      - "how do"
      - "why"
      - "explain"
      - "difference between"

  - task_type: other
    match_any: []

# Derived queries are rules-based additions to improve retrieval.
derived_queries:
  when_contains:
    discord:
      match_any: ["discord", "dm", "channel"]
      add:
        - "discord rules DM-only communication policy"

    memory_pipeline:
      match_any: ["memory", "retrieval", "compression", "context pack", "always-on"]
      add:
        - "always_on topic catalog retrieval compression context pack"
        - "memory design now open-loops automation topics"

    briefing_delivery:
      match_any: ["daily briefing", "digest", "briefing", "didn't receive", "not delivered", "missed"]
      add:
        - "Daily AI Briefing cron delivery discord"
        - "openclaw ops discord provider delivery"
        - "automation registry daily briefing"

    doc_review:
      match_any: ["doc", "documentation", "customer-facing", "review", "rewrite"]
      add:
        - "doc review mode untrusted claims validation questions"

    ikara:
      match_any: ["ikara"]
      add:
        - "ikara platform ops access security controls integrations"

# Retrieval policy: correctness-first defaults.
retrieval_policy:
  always_include_paths:
    - "memory/always_on.md"

  sources_priority:
    - "memory/topics/*.md"
    - "memory/now.md"
    - "memory/open-loops.md"
    - "memory/automation.md"
    - "MEMORY.md"
    - "memory/YYYY-MM-DD.md" # conditional

  caps_by_task_type:
    ops:
      top_k_snippets: 14
      max_lines_per_snippet: 60
      max_snippets_per_file: 2
      include_daily_logs: true
      max_daily_log_snippets: 3
      daily_log_lookback_days: 7

    briefing:
      top_k_snippets: 12
      max_lines_per_snippet: 60
      max_snippets_per_file: 2
      include_daily_logs: true
      max_daily_log_snippets: 1
      daily_log_lookback_days: 3

    doc_review:
      top_k_snippets: 10
      max_lines_per_snippet: 60
      max_snippets_per_file: 2
      include_daily_logs: false
      max_daily_log_snippets: 0

    planning:
      top_k_snippets: 12
      max_lines_per_snippet: 60
      max_snippets_per_file: 2
      include_daily_logs: false
      max_daily_log_snippets: 0

    qna:
      top_k_snippets: 10
      max_lines_per_snippet: 60
      max_snippets_per_file: 2
      include_daily_logs: false
      max_daily_log_snippets: 0

    other:
      top_k_snippets: 10
      max_lines_per_snippet: 60
      max_snippets_per_file: 2
      include_daily_logs: false
      max_daily_log_snippets: 0

# Daily-log switch: only enable daily logs when user asks explicitly about time/recency.
daily_log_trigger:
  match_any: ["yesterday", "today", "last time", "previously", "what did we decide", "did we"]


path: references/schemas/context_pack.schema.json
content: {
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "context-pack.schema.json",
  "title": "Context Pack",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "version",
    "task_type",
    "must_follow",
    "relevant_context",
    "conflicts_or_ambiguities",
    "unknowns_to_confirm",
    "exclude_as_irrelevant",
    "retrieval_notes"
  ],
  "properties": {
    "version": { "type": "string", "const": "1.0" },
    "task_type": {
      "type": "string",
      "enum": ["qna", "planning", "debugging", "doc_review", "ops", "briefing", "other"]
    },
    "must_follow": {
      "type": "array",
      "items": { "$ref": "#/$defs/sourcedRule" }
    },
    "relevant_context": {
      "type": "array",
      "items": { "$ref": "#/$defs/sourcedContext" }
    },
    "conflicts_or_ambiguities": {
      "type": "array",
      "items": { "$ref": "#/$defs/sourcedConflict" }
    },
    "unknowns_to_confirm": {
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["question", "why_needed"],
        "properties": {
          "question": { "type": "string", "minLength": 1 },
          "why_needed": { "type": "string", "minLength": 1 }
        }
      }
    },
    "exclude_as_irrelevant": {
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["item", "why", "sources"],
        "properties": {
          "item": { "type": "string", "minLength": 1 },
          "why": { "type": "string", "minLength": 1 },
          "sources": { "$ref": "#/$defs/sources" }
        }
      }
    },
    "retrieval_notes": {
      "type": "object",
      "additionalProperties": false,
      "required": ["included_sources", "dropped_due_to_limits"],
      "properties": {
        "included_sources": { "type": "array", "items": { "type": "string" } },
        "dropped_due_to_limits": { "type": "array", "items": { "type": "string" } }
      }
    }
  },
  "$defs": {
    "sources": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "string",
        "pattern": "^S[0-9]+$"
      }
    },
    "sourcedRule": {
      "type": "object",
      "additionalProperties": false,
      "required": ["rule", "why", "sources"],
      "properties": {
        "rule": { "type": "string", "minLength": 1 },
        "why": { "type": "string", "minLength": 1 },
        "sources": { "$ref": "#/$defs/sources" }
      }
    },
    "sourcedContext": {
      "type": "object",
      "additionalProperties": false,
      "required": ["fact_or_context", "why", "sources"],
      "properties": {
        "fact_or_context": { "type": "string", "minLength": 1 },
        "why": { "type": "string", "minLength": 1 },
        "sources": { "$ref": "#/$defs/sources" }
      }
    },
    "sourcedConflict": {
      "type": "object",
      "additionalProperties": false,
      "required": ["issue", "options", "sources"],
      "properties": {
        "issue": { "type": "string", "minLength": 1 },
        "options": {
          "type": "array",
          "minItems": 2,
          "maxItems": 2,
          "items": { "type": "string", "minLength": 1 }
        },
        "sources": {
          "type": "array",
          "minItems": 2,
          "items": { "type": "string", "pattern": "^S[0-9]+$" }
        }
      }
    }
  }
}


path: references/schemas/retrieval_bundle.schema.json
content: {
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "retrieval-bundle.schema.json",
  "title": "Retrieval Bundle",
  "type": "object",
  "additionalProperties": false,
  "required": ["retrieval_version", "user_message", "always_on", "query_plan", "snippets", "dropped"],
  "properties": {
    "retrieval_version": { "type": "string", "const": "1.0" },
    "user_message": {
      "type": "object",
      "additionalProperties": false,
      "required": ["text"],
      "properties": {
        "text": { "type": "string", "minLength": 1 },
        "timestamp_utc": { "type": "string" }
      }
    },
    "always_on": {
      "type": "object",
      "additionalProperties": false,
      "required": ["path", "content"],
      "properties": {
        "path": { "type": "string" },
        "content": { "type": "string" },
        "content_hash": { "type": "string" }
      }
    },
    "query_plan": {
      "type": "object",
      "additionalProperties": true
    },
    "snippets": {
      "type": "array",
      "items": { "$ref": "#/$defs/snippet" }
    },
    "dropped": {
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["path", "reason"],
        "properties": {
          "path": { "type": "string" },
          "reason": { "type": "string" }
        }
      }
    }
  },
  "$defs": {
    "snippet": {
      "type": "object",
      "additionalProperties": false,
      "required": ["snippet_id", "path", "start_line", "end_line", "citation", "text"],
      "properties": {
        "snippet_id": { "type": "string", "pattern": "^S[0-9]+$" },
        "stable_id": { "type": "string" },
        "path": { "type": "string" },
        "start_line": { "type": "integer", "minimum": 1 },
        "end_line": { "type": "integer", "minimum": 1 },
        "citation": { "type": "string" },
        "text": { "type": "string" },
        "score": { "type": "number" },
        "tags": { "type": "array", "items": { "type": "string" } },
        "source_group": { "type": "string" }
      }
    }
  }
}


path: references/tests/golden.json
content: [
  {
    "id": "discord_dm_only",
    "user_message": "Can you reply in #general?",
    "expect": {
      "retrieval": {
        "must_include_paths": [
          "memory/always_on.md",
          "memory/topics/discord-rules.md"
        ],
        "max_daily_log_snippets": 0
      },
      "context_pack": {
        "task_type": "ops",
        "must_follow_contains": [
          "Discord DM-only"
        ],
        "unknowns_count_max": 2
      }
    }
  },
  {
    "id": "briefing_missed",
    "user_message": "I didn\u2019t receive today\u2019s briefing again",
    "expect": {
      "retrieval": {
        "must_include_paths": [
          "memory/automation.md",
          "memory/topics/openclaw-ops.md"
        ],
        "max_daily_log_snippets": 3
      },
      "context_pack": {
        "task_type": "ops"
      }
    }
  },
  {
    "id": "doc_review_mode",
    "user_message": "Review this customer-facing doc paragraph and tell me what claims need validation",
    "expect": {
      "retrieval": {
        "must_include_paths": [
          "memory/topics/doc-review-mode.md"
        ],
        "max_daily_log_snippets": 0
      },
      "context_pack": {
        "task_type": "doc_review"
      }
    }
  },
  {
    "id": "memory_pipeline_question",
    "user_message": "How does our retrieval + compression memory pipeline work?",
    "expect": {
      "retrieval": {
        "must_include_paths": [
          "memory/always_on.md"
        ],
        "max_daily_log_snippets": 0
      },
      "context_pack": {
        "task_type": "planning"
      }
    }
  },
  {
    "id": "work_order_evidence_contract",
    "user_message": "WORK ORDER: Summarize the latest pipeline spec. Evidence required: Retrieval Bundle JSON with snippet IDs, Context Pack JSON, and NOT EXECUTED fallback if anything fails.",
    "expect": {
      "retrieval": {
        "must_include_paths": [
          "memory/always_on.md"
        ],
        "max_daily_log_snippets": 0
      },
      "context_pack": {
        "task_type": "other",
        "must_follow_contains": [
          "Emit the Retrieval Bundle JSON",
          "If any step cannot be completed, reply NOT EXECUTED"
        ]
      }
    }
  }
]

path: scripts/install_pipeline.py
content: #!/usr/bin/env python3
"""Install/sync the context_pipeline bundle into the workspace."""

from __future__ import annotations

import argparse
import hashlib
import shutil
from pathlib import Path


def sha256(path: Path) -> str:
    h = hashlib.sha256()
    with path.open("rb") as fh:
        for chunk in iter(lambda: fh.read(1024 * 1024), b""):
            if not chunk:
                break
            h.update(chunk)
    return h.hexdigest()


def collect_files(root: Path):
    for item in sorted(root.rglob("*")):
        if item.is_file():
            yield item


def main() -> int:
    script_path = Path(__file__).resolve()
    skill_root = script_path.parents[1]
    workspace_root = script_path.parents[3]

    default_target = workspace_root / "context_pipeline"
    assets_root = skill_root / "assets" / "context_pipeline"

    parser = argparse.ArgumentParser(description="Install/sync context_pipeline")
    parser.add_argument("--target", default=str(default_target), help="Destination folder (default: workspace/context_pipeline)")
    parser.add_argument("--assets-root", default=str(assets_root), help="Source bundle (default: skill assets)")
    args = parser.parse_args()

    target = Path(args.target).resolve()
    source = Path(args.assets_root).resolve()

    if not source.exists():
        raise SystemExit(f"Source bundle not found: {source}")

    target.parent.mkdir(parents=True, exist_ok=True)
    shutil.copytree(source, target, dirs_exist_ok=True)

    files = list(collect_files(target))
    summary = []
    for file in files:
        rel = file.relative_to(target)
        summary.append((str(rel), sha256(file)))

    print(f"Installed context pipeline files into {target}")
    print(f"Total files: {len(files)}")
    preview = summary[:5]
    for rel_path, digest in preview:
        print(f" - {rel_path}: {digest}")
    remaining = len(summary) - len(preview)
    if remaining > 0:
        print(f" ... {remaining} more files hashed")

    return 0


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/memory_watchdog.py
content: #!/usr/bin/env python3
"""Memory freshness watchdog for dr-context-pipeline."""

from __future__ import annotations

import argparse
import json
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import List


@dataclass
class CheckResult:
    status: str
    issues: List[str]
    details: dict


def iso_date(dt: datetime) -> str:
    return dt.strftime("%Y-%m-%d")


def find_daily_files(memory_dir: Path, target_date: datetime) -> List[Path]:
    prefix = iso_date(target_date)
    return sorted(memory_dir.glob(f"{prefix}*.md"))


def check_daily_file(paths: List[Path], freshness_minutes: int, min_bytes: int) -> CheckResult:
    issues: List[str] = []
    now = datetime.now(timezone.utc)

    if not paths:
        issues.append("Missing daily log for today")
        return CheckResult("GAP", issues, {"files_checked": []})

    newest = None
    newest_age = None
    details = []
    for p in paths:
        stat = p.stat()
        mtime = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc)
        age_minutes = (now - mtime).total_seconds() / 60
        size = stat.st_size
        details.append({
            "path": str(p),
            "modified_utc": mtime.isoformat(),
            "age_minutes": round(age_minutes, 2),
            "bytes": size,
        })
        if newest is None or mtime > newest:
            newest = mtime
            newest_age = age_minutes
        if size < min_bytes:
            issues.append(f"File {p} is only {size} bytes (<{min_bytes})")

    if newest_age is not None and newest_age > freshness_minutes:
        issues.append(f"Newest daily note is {round(newest_age, 1)} min old (> {freshness_minutes} min)")

    status = "OK" if not issues else "GAP"
    return CheckResult(status, issues, {"files_checked": details})


def main() -> int:
    script_path = Path(__file__).resolve()
    workspace_root = script_path.parents[3]
    memory_dir_default = workspace_root / "memory"

    parser = argparse.ArgumentParser(description="Ensure memory daily logs are fresh and non-empty")
    parser.add_argument("--memory-dir", default=str(memory_dir_default), help="Path to the memory directory")
    parser.add_argument("--freshness-minutes", type=int, default=120, help="Max minutes since the latest daily log edit")
    parser.add_argument("--min-bytes", type=int, default=200, help="Minimum acceptable size for the daily log")
    parser.add_argument("--timezone", default="UTC", help="Display timezone label (informational only)")
    args = parser.parse_args()

    memory_dir = Path(args.memory_dir).resolve()
    if not memory_dir.exists():
        print(json.dumps({
            "status": "GAP",
            "issues": [f"Memory directory missing: {memory_dir}"],
            "details": {}
        }, indent=2))
        return 1

    today = datetime.now(timezone.utc)
    files = find_daily_files(memory_dir, today)
    result = check_daily_file(files, args.freshness_minutes, args.min_bytes)
    payload = {
        "status": result.status,
        "issues": result.issues,
        "details": result.details,
        "freshness_minutes": args.freshness_minutes,
        "min_bytes": args.min_bytes,
        "timezone": args.timezone,
        "today": iso_date(today),
    }
    print(json.dumps(payload, indent=2))
    return 0 if result.status == "OK" else 1


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/validate_pipeline.py
content: #!/usr/bin/env python3
"""Validate the installed context_pipeline bundle."""

from __future__ import annotations

import argparse
import json
import hashlib
from pathlib import Path


def sha256(path: Path) -> str:
    h = hashlib.sha256()
    with path.open("rb") as fh:
        for chunk in iter(lambda: fh.read(1024 * 1024), b""):
            if not chunk:
                break
            h.update(chunk)
    return h.hexdigest()


def iter_files(root: Path):
    for item in sorted(root.rglob("*")):
        if item.is_file():
            yield item


def validate_golden(golden, errors):
    if not isinstance(golden, list):
        errors.append("Golden tests file must be a list of cases")
        return
    for idx, case in enumerate(golden):
        if not isinstance(case, dict):
            errors.append(f"Golden case #{idx} is not an object")
            continue
        for key in ["id", "user_message", "expect"]:
            if key not in case:
                errors.append(f"Golden case #{idx} missing '{key}'")
        expect = case.get("expect", {})
        if not isinstance(expect, dict):
            errors.append(f"Golden case {case.get('id', idx)} expect must be object")
            continue
        # Soft checks on nested structures
        retrieval = expect.get("retrieval")
        if retrieval is not None and not isinstance(retrieval, dict):
            errors.append(f"Golden case {case.get('id', idx)} retrieval must be object")
        context_pack = expect.get("context_pack")
        if context_pack is not None and not isinstance(context_pack, dict):
            errors.append(f"Golden case {case.get('id', idx)} context_pack must be object")


def main() -> int:
    script_path = Path(__file__).resolve()
    workspace_root = script_path.parents[3]
    skill_root = script_path.parents[1]

    default_context = workspace_root / "context_pipeline"
    default_assets = skill_root / "assets" / "context_pipeline"

    parser = argparse.ArgumentParser(description="Validate context_pipeline installation")
    parser.add_argument("--context-root", default=str(default_context), help="Path to the installed context_pipeline folder")
    parser.add_argument("--assets-root", default=str(default_assets), help="Path to the source assets for comparison")
    args = parser.parse_args()

    context_root = Path(args.context_root).resolve()
    assets_root = Path(args.assets_root).resolve()

    errors = []

    if not context_root.exists():
        errors.append(f"Context root does not exist: {context_root}")
    if not assets_root.exists():
        errors.append(f"Assets root does not exist: {assets_root}")

    if errors:
        for err in errors:
            print(f"ERROR: {err}")
        return 1

    # Compare hashes
    missing = []
    mismatched = []
    for asset_file in iter_files(assets_root):
        rel = asset_file.relative_to(assets_root)
        target_file = context_root / rel
        if not target_file.exists():
            missing.append(str(rel))
            continue
        asset_hash = sha256(asset_file)
        target_hash = sha256(target_file)
        if asset_hash != target_hash:
            mismatched.append((str(rel), asset_hash, target_hash))

    extra_files = []
    asset_paths = {str(f.relative_to(assets_root)) for f in iter_files(assets_root)}
    for target_file in iter_files(context_root):
        rel = str(target_file.relative_to(context_root))
        if rel not in asset_paths:
            extra_files.append(rel)

    for rel in missing:
        errors.append(f"Missing file in context root: {rel}")
    for rel, asset_hash, target_hash in mismatched:
        errors.append(f"Hash mismatch for {rel}: expected {asset_hash}, found {target_hash}")
    if extra_files:
        errors.append("Extra files present: " + ", ".join(extra_files))

    # JSON/schema sanity checks
    json_files = [
        context_root / "schemas" / "context_pack.schema.json",
        context_root / "schemas" / "retrieval_bundle.schema.json",
        context_root / "tests" / "golden.json",
    ]
    for json_path in json_files:
        try:
            data = json.loads(json_path.read_text())
            if json_path.name == "golden.json":
                validate_golden(data, errors)
        except Exception as exc:  # noqa: BLE001
            errors.append(f"Failed to parse {json_path}: {exc}")

    if errors:
        print("VALIDATION FAILED")
        for err in errors:
            print(f" - {err}")
        return 1

    print("VALIDATION PASSED")
    print(f"Context root: {context_root}")
    print(f"Assets root:  {assets_root}")
    print(f"Total files checked: {len(list(iter_files(context_root)))}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)