diff --git a/custom-skills/97-ourdigital-okf/DESIGN.md b/custom-skills/97-ourdigital-okf/DESIGN.md
new file mode 100644
index 0000000..01bedc5
--- /dev/null
+++ b/custom-skills/97-ourdigital-okf/DESIGN.md
@@ -0,0 +1,139 @@
+# ourdigital-okf — Claude Skill Design Spec
+
+> **Status:** Approved (decisions resolved 2026-06-16) · **Author:** Claude Code (brainstorming skill)
+> **Notion spec of record:** https://app.notion.com/p/381581e58a1e81128280f43839902dc8
+> **Related:** OKF Reference Capture (Notion) · Local reference library at `~/Documents/reference-library/open-knowledge-format/`
+
+A custom Claude skill, triggered by `/ourdigital-okf`, that **produces**, **visualizes**, and **validates**
+Google Open Knowledge Format (OKF) v0.1 knowledge bundles. It puts Claude where it is strongest —
+drafting and enriching concept documents — the work Google's reference implementation needs a full
+Python ADK + Gemini agent to do.
+
+---
+
+## 1. Scope (finalized)
+
+- **In scope:** produce · visualize · validate.
+- **Dropped:** query/consume mode (decision 1 — largely native to Claude; not worth the surface area).
+- **Out of scope:** wrapping Google's Python `enrichment_agent`; live BigQuery pulls.
+
+## 2. Architecture — "Single skill + bundled utilities" (Approach A)
+
+One coherent skill with three modes. Production *composes existing tooling* rather than reimplementing it,
+and stays **MCP-agnostic** (decision 4):
+
+| Need | Source |
+| --- | --- |
+| Data-source schemas | **Pasted or exported schema** — BigQuery DDL / `information_schema` dump, GA4 export schema, CSV/list of columns, JSON Schema, OpenAPI. No live MCP dependency. |
+| Existing docs & markdown | direct file reads |
+| Web-research topics | the `/reference-curator` pipeline (or Firecrawl) |
+| Conformance + graph viz | two small bundled Python scripts (zero pip deps) |
+
+## 3. Identity & install
+
+- **Trigger:** `/ourdigital-okf` (decision 2 — part of the OurDigital skill family).
+- **Source dir:** `/Users/ourdigital/Project/our-claude-skills/custom-skills/97-ourdigital-okf/`
+- **Installs to:** `~/.claude/skills/ourdigital-okf/` via `install.sh` (symlink — existing pattern).
+- **Conventions:** must follow OurDigital `ourdigital-*` skill rules (`_ourdigital-shared`, brand guide);
+ verified by `ourdigital-skill-creator` at the end (decision 6).
+- **Activates on:** "OKF", "Open Knowledge Format", "knowledge bundle", "concept docs with YAML frontmatter",
+ produce/validate/visualize a bundle.
+
+## 4. File layout (OurDigital `ourdigital-*` convention)
+
+Follows the same structure as existing numbered OurDigital skills (e.g. `04-ourdigital-research`):
+top-level `SKILL.md` + `README.md`, a `code/` variant (Claude Code), a `desktop/` variant
+(Claude Desktop), and `docs/`.
+
+```
+97-ourdigital-okf/
+├── SKILL.md # top-level canonical (YAML frontmatter: name, triggers, version, author, environment)
+├── README.md # overview
+├── DESIGN.md # this spec (repo copy — decision 5)
+├── install.sh # symlink top-level SKILL.md → ~/.claude/skills/ourdigital-okf
+├── code/
+│ ├── SKILL.md # Claude Code variant: detailed produce/visualize/validate mode flows
+│ ├── CLAUDE.md # code-pattern pointer
+│ ├── references/
+│ │ ├── okf-spec-v0.1.md # SPEC.md distilled into actionable authoring rules + conformance checklist
+│ │ └── frontmatter-fields.md
+│ ├── assets/
+│ │ └── concept.md index.md log.md # templates
+│ └── scripts/
+│ ├── okf_common.py # shared frontmatter/link parsing (stdlib only)
+│ ├── okf_validate.py # conformance + broken-link linter
+│ ├── okf_viz.py # minimal viz.html generator (Cytoscape+marked via CDN)
+│ ├── requirements.txt # documents zero runtime deps
+│ └── tests/ # stdlib unittest + a mini fixture bundle
+├── desktop/
+│ ├── SKILL.md # Claude Desktop variant (leaner)
+│ └── skill.yaml
+└── docs/
+ ├── CHANGELOG.md
+ └── IMPLEMENTATION-PLAN.md # the build plan
+```
+
+**Zero-dependency choice:** OKF frontmatter uses a tiny YAML subset (`key: value`, `[a, b]` lists),
+so `okf_common.py` ships a minimal built-in parser — no PyYAML / pip install. Tests use stdlib
+`unittest` (no pytest), keeping the whole skill installable-free.
+
+## 5. Mode: produce (Claude-native)
+
+1. Pick input adapter (schema / docs / research) and **confirm the target bundle directory before
+ creating anything** (honors the no-directory-without-consent rule).
+2. Gather raw material per §2 — for the data adapter, ingest a pasted/exported schema (no live MCP call).
+3. Plan a concept hierarchy (`datasets/`, `tables/`, `metrics/`, `references/`, `playbooks/` — as fits).
+4. Draft one concept `.md` per concept: **required** `type` + recommended fields
+ (`title`, `description`, `resource`, `tags`, `timestamp`), bundle-relative cross-links
+ (`/path/concept.md`), and a `# Citations` section.
+5. Auto-generate `index.md` per directory + root; optional `log.md`.
+6. **Self-validate** with `okf_validate.py`; fix all errors before reporting done.
+
+## 6. Mode: visualize (minimal first — decision 3)
+
+`okf_viz.py --bundle
[--out viz.html] [--name X]` → one self-contained HTML.
+**v1 (minimal):** force-directed concept graph (Cytoscape), type-colored nodes, click a node to see its
+rendered markdown + frontmatter. **Later iterations:** "cited by" backlinks, search box, type filter,
+layout switch (parity with Google's viewer).
+
+## 7. Validation (supporting)
+
+`okf_validate.py ` checks: parseable frontmatter on every non-reserved `.md`; non-empty `type`;
+`index.md` / `log.md` structure; **broken cross-link report** (warning, not failure). Text + JSON output,
+meaningful exit code. Invoked automatically by produce, available standalone.
+
+## 8. Built-in fixtures / regression test
+
+Validator and visualizer are verified against **Google's own three sample bundles** already mirrored:
+`~/Documents/reference-library/open-knowledge-format/okf/bundles/{crypto_bitcoin, ga4, stackoverflow}`.
+Authoritative, conformant fixtures — no invented test data.
+
+## 9. Success criteria
+
+- `/ourdigital-okf produce` from a pasted schema, a docs folder, or a research topic emits a bundle that
+ passes `okf_validate.py` with **0 conformance errors**.
+- `okf_viz.py` on any bundle opens a working graph in the browser.
+- Both scripts validate/visualize Google's 3 reference bundles cleanly.
+- Skill passes `ourdigital-skill-creator` consistency/rules check.
+
+## 10. Build sequence
+
+1. Scaffold `97-ourdigital-okf/` (SKILL.md, README, USER-GUIDE, install.sh, templates) to OurDigital conventions.
+2. Distill `SPEC.md` → `reference/okf-spec-v0.1.md` authoring rules + checklist.
+3. Write `okf_validate.py`; verify against the 3 Google sample bundles.
+4. Write `okf_viz.py` (minimal); verify a working graph for those bundles.
+5. Write the produce/visualize/validate mode flows in SKILL.md.
+6. End-to-end test: produce a small bundle from a docs folder → validate → visualize.
+7. `install.sh` into `~/.claude/skills/ourdigital-okf/`; smoke-test `/ourdigital-okf`.
+8. Run `ourdigital-skill-creator` consistency check; fix any rule violations.
+
+## 11. Resolved decisions (2026-06-16)
+
+| # | Question | Decision |
+|---|----------|----------|
+| 1 | Keep query mode? | **No** — produce + visualize + validate only |
+| 2 | Naming | **`ourdigital-okf`** |
+| 3 | Viz scope | **Minimal graph first**, iterate |
+| 4 | Data adapter | **Accept pasted/exported schema** (MCP-agnostic) |
+| 5 | Spec home | Notion (of record) **+ repo `DESIGN.md` copy** |
+| 6 | Build method | **writing-plans → implementation**, then `ourdigital-skill-creator` validity check |
diff --git a/custom-skills/97-ourdigital-okf/code/scripts/requirements.txt b/custom-skills/97-ourdigital-okf/code/scripts/requirements.txt
new file mode 100644
index 0000000..833db17
--- /dev/null
+++ b/custom-skills/97-ourdigital-okf/code/scripts/requirements.txt
@@ -0,0 +1,2 @@
+# ourdigital-okf scripts use the Python standard library only.
+# No third-party runtime dependencies. Tests use stdlib `unittest`.
diff --git a/custom-skills/97-ourdigital-okf/code/scripts/tests/fixtures/mini_bundle/datasets/sales.md b/custom-skills/97-ourdigital-okf/code/scripts/tests/fixtures/mini_bundle/datasets/sales.md
new file mode 100644
index 0000000..fd2613d
--- /dev/null
+++ b/custom-skills/97-ourdigital-okf/code/scripts/tests/fixtures/mini_bundle/datasets/sales.md
@@ -0,0 +1,10 @@
+---
+type: BigQuery Dataset
+title: Sales
+description: All sales-related tables for the retail business.
+resource: https://console.cloud.google.com/bigquery?p=acme&d=sales
+tags: [sales]
+timestamp: 2026-05-28T00:00:00Z
+---
+
+The sales dataset contains [orders](/tables/orders.md) and [customers](/tables/customers.md).
diff --git a/custom-skills/97-ourdigital-okf/code/scripts/tests/fixtures/mini_bundle/index.md b/custom-skills/97-ourdigital-okf/code/scripts/tests/fixtures/mini_bundle/index.md
new file mode 100644
index 0000000..2854cd6
--- /dev/null
+++ b/custom-skills/97-ourdigital-okf/code/scripts/tests/fixtures/mini_bundle/index.md
@@ -0,0 +1,8 @@
+# Datasets
+
+* [Sales](datasets/sales.md) - All sales-related tables.
+
+# Tables
+
+* [Orders](tables/orders.md) - One row per completed order.
+* [Customers](tables/customers.md) - One row per customer.
diff --git a/custom-skills/97-ourdigital-okf/code/scripts/tests/fixtures/mini_bundle/tables/customers.md b/custom-skills/97-ourdigital-okf/code/scripts/tests/fixtures/mini_bundle/tables/customers.md
new file mode 100644
index 0000000..30250b5
--- /dev/null
+++ b/custom-skills/97-ourdigital-okf/code/scripts/tests/fixtures/mini_bundle/tables/customers.md
@@ -0,0 +1,16 @@
+---
+type: BigQuery Table
+title: Customers
+description: One row per customer.
+resource: https://console.cloud.google.com/bigquery?p=acme&d=sales&t=customers
+tags: [sales, customers]
+timestamp: 2026-05-28T00:00:00Z
+---
+
+# Schema
+
+| Column | Type | Description |
+|---------------|--------|-----------------------|
+| `customer_id` | STRING | Unique customer id. |
+
+Referenced by [orders](/tables/orders.md).
diff --git a/custom-skills/97-ourdigital-okf/code/scripts/tests/fixtures/mini_bundle/tables/orders.md b/custom-skills/97-ourdigital-okf/code/scripts/tests/fixtures/mini_bundle/tables/orders.md
new file mode 100644
index 0000000..ad455ab
--- /dev/null
+++ b/custom-skills/97-ourdigital-okf/code/scripts/tests/fixtures/mini_bundle/tables/orders.md
@@ -0,0 +1,17 @@
+---
+type: BigQuery Table
+title: Orders
+description: One row per completed customer order.
+resource: https://console.cloud.google.com/bigquery?p=acme&d=sales&t=orders
+tags: [sales, orders]
+timestamp: 2026-05-28T00:00:00Z
+---
+
+# Schema
+
+| Column | Type | Description |
+|---------------|---------|------------------------------------------|
+| `order_id` | STRING | Unique order identifier. |
+| `customer_id` | STRING | FK to [customers](/tables/customers.md). |
+
+Part of the [sales dataset](/datasets/sales.md).
diff --git a/custom-skills/97-ourdigital-okf/docs/IMPLEMENTATION-PLAN.md b/custom-skills/97-ourdigital-okf/docs/IMPLEMENTATION-PLAN.md
new file mode 100644
index 0000000..3bd7120
--- /dev/null
+++ b/custom-skills/97-ourdigital-okf/docs/IMPLEMENTATION-PLAN.md
@@ -0,0 +1,1058 @@
+# ourdigital-okf Skill — Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Build the `/ourdigital-okf` Claude skill that produces, visualizes, and validates Google Open Knowledge Format (OKF) v0.1 bundles, following OurDigital skill conventions.
+
+**Architecture:** A single OurDigital-family skill. Claude natively drafts conformant OKF concept docs (produce); two zero-dependency Python utilities handle deterministic work — `okf_validate.py` (conformance + broken-link lint) and `okf_viz.py` (minimal Cytoscape graph HTML). A shared `okf_common.py` holds the frontmatter/link parser. Tests use stdlib `unittest` and are verified against Google's three mirrored sample bundles.
+
+**Tech Stack:** Python 3 (stdlib only — no pip deps), `unittest` for tests, Cytoscape.js + marked via CDN for the viewer, OurDigital skill structure (`SKILL.md` + `code/` + `desktop/` + `docs/`).
+
+**Repo:** `/Users/ourdigital/Project/our-claude-skills` (git, branch `main`). Skill dir: `custom-skills/97-ourdigital-okf/`.
+
+**Reference fixtures (already on disk):** `~/Documents/reference-library/open-knowledge-format/okf/bundles/{crypto_bitcoin,ga4,stackoverflow}` and the spec at `.../okf/SPEC.md`.
+
+---
+
+## File Structure
+
+```
+custom-skills/97-ourdigital-okf/
+├── SKILL.md # top-level canonical (frontmatter + mode dispatch)
+├── README.md # overview
+├── DESIGN.md # (exists) approved spec
+├── install.sh # symlink SKILL.md → ~/.claude/skills/ourdigital-okf
+├── code/
+│ ├── SKILL.md # Claude Code variant (detailed flows)
+│ ├── CLAUDE.md # code-pattern pointer
+│ ├── references/
+│ │ ├── okf-spec-v0.1.md # distilled actionable spec + conformance checklist
+│ │ └── frontmatter-fields.md
+│ ├── assets/
+│ │ ├── concept.md # template
+│ │ ├── index.md # template
+│ │ └── log.md # template
+│ └── scripts/
+│ ├── okf_common.py # shared parser (stdlib)
+│ ├── okf_validate.py # validator CLI
+│ ├── okf_viz.py # visualizer CLI
+│ ├── requirements.txt # (documents: no runtime deps)
+│ └── tests/
+│ ├── test_okf_common.py
+│ ├── test_okf_validate.py
+│ ├── test_okf_viz.py
+│ └── fixtures/mini_bundle/ # tiny conformant bundle (from SPEC Appendix A)
+├── desktop/
+│ ├── SKILL.md # Claude Desktop variant (leaner)
+│ └── skill.yaml
+└── docs/
+ ├── CHANGELOG.md
+ └── IMPLEMENTATION-PLAN.md # this file
+```
+
+Responsibilities: `okf_common.py` = parsing only; `okf_validate.py` = conformance rules; `okf_viz.py` = graph build + HTML render. Each file is independently testable.
+
+---
+
+## Task 1: Branch + directory skeleton
+
+**Files:**
+- Create branch `feat/ourdigital-okf`
+- Create: `code/scripts/`, `code/references/`, `code/assets/`, `code/scripts/tests/fixtures/mini_bundle/`, `desktop/`, `docs/`
+
+- [ ] **Step 1: Create a feature branch**
+
+```bash
+cd /Users/ourdigital/Project/our-claude-skills
+git checkout -b feat/ourdigital-okf
+```
+
+- [ ] **Step 2: Create the directory skeleton**
+
+```bash
+cd custom-skills/97-ourdigital-okf
+mkdir -p code/references code/assets code/scripts/tests/fixtures/mini_bundle desktop docs
+```
+
+- [ ] **Step 3: Create `code/scripts/requirements.txt`**
+
+```text
+# ourdigital-okf scripts use the Python standard library only.
+# No third-party runtime dependencies. Tests use stdlib `unittest`.
+```
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add custom-skills/97-ourdigital-okf
+git commit -m "feat(okf): scaffold ourdigital-okf skill skeleton"
+```
+
+---
+
+## Task 2: Mini fixture bundle (test data, from SPEC Appendix A)
+
+**Files:**
+- Create: `code/scripts/tests/fixtures/mini_bundle/{index.md, datasets/sales.md, tables/orders.md, tables/customers.md}`
+
+- [ ] **Step 1: Create `tests/fixtures/mini_bundle/index.md`**
+
+```markdown
+# Datasets
+
+* [Sales](datasets/sales.md) - All sales-related tables.
+
+# Tables
+
+* [Orders](tables/orders.md) - One row per completed order.
+* [Customers](tables/customers.md) - One row per customer.
+```
+
+- [ ] **Step 2: Create `tests/fixtures/mini_bundle/datasets/sales.md`**
+
+```markdown
+---
+type: BigQuery Dataset
+title: Sales
+description: All sales-related tables for the retail business.
+resource: https://console.cloud.google.com/bigquery?p=acme&d=sales
+tags: [sales]
+timestamp: 2026-05-28T00:00:00Z
+---
+
+The sales dataset contains [orders](/tables/orders.md) and [customers](/tables/customers.md).
+```
+
+- [ ] **Step 3: Create `tests/fixtures/mini_bundle/tables/orders.md`**
+
+```markdown
+---
+type: BigQuery Table
+title: Orders
+description: One row per completed customer order.
+resource: https://console.cloud.google.com/bigquery?p=acme&d=sales&t=orders
+tags: [sales, orders]
+timestamp: 2026-05-28T00:00:00Z
+---
+
+# Schema
+
+| Column | Type | Description |
+|---------------|---------|------------------------------------------|
+| `order_id` | STRING | Unique order identifier. |
+| `customer_id` | STRING | FK to [customers](/tables/customers.md). |
+
+Part of the [sales dataset](/datasets/sales.md).
+```
+
+- [ ] **Step 4: Create `tests/fixtures/mini_bundle/tables/customers.md`**
+
+```markdown
+---
+type: BigQuery Table
+title: Customers
+description: One row per customer.
+resource: https://console.cloud.google.com/bigquery?p=acme&d=sales&t=customers
+tags: [sales, customers]
+timestamp: 2026-05-28T00:00:00Z
+---
+
+# Schema
+
+| Column | Type | Description |
+|---------------|--------|-----------------------|
+| `customer_id` | STRING | Unique customer id. |
+
+Referenced by [orders](/tables/orders.md).
+```
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add custom-skills/97-ourdigital-okf/code/scripts/tests/fixtures
+git commit -m "test(okf): add mini conformant fixture bundle"
+```
+
+---
+
+## Task 3: `okf_common.py` — shared parser (TDD)
+
+**Files:**
+- Create: `code/scripts/okf_common.py`
+- Test: `code/scripts/tests/test_okf_common.py`
+
+- [ ] **Step 1: Write the failing test** — `tests/test_okf_common.py`
+
+```python
+import sys, unittest
+from pathlib import Path
+sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
+from okf_common import (parse_frontmatter, parse_yaml_subset,
+ extract_links, concept_id, FrontmatterError)
+
+
+class TestFrontmatter(unittest.TestCase):
+ def test_parse_basic(self):
+ text = "---\ntype: BigQuery Table\ntitle: Orders\ntags: [sales, revenue]\n---\n\n# Body\n"
+ meta, body = parse_frontmatter(text)
+ self.assertEqual(meta["type"], "BigQuery Table")
+ self.assertEqual(meta["title"], "Orders")
+ self.assertEqual(meta["tags"], ["sales", "revenue"])
+ self.assertIn("# Body", body)
+
+ def test_url_value_with_colons(self):
+ meta, _ = parse_frontmatter("---\ntype: X\nresource: https://e.com/a?b=c\n---\nbody")
+ self.assertEqual(meta["resource"], "https://e.com/a?b=c")
+
+ def test_quoted_value(self):
+ self.assertEqual(parse_yaml_subset('okf_version: "0.1"')["okf_version"], "0.1")
+
+ def test_empty_list(self):
+ self.assertEqual(parse_yaml_subset("tags: []")["tags"], [])
+
+ def test_no_frontmatter(self):
+ meta, body = parse_frontmatter("# Just markdown\n")
+ self.assertIsNone(meta)
+ self.assertEqual(body, "# Just markdown\n")
+
+ def test_unclosed_raises(self):
+ with self.assertRaises(FrontmatterError):
+ parse_frontmatter("---\ntype: X\n\nbody without close")
+
+
+class TestLinksAndIds(unittest.TestCase):
+ def test_extract_links(self):
+ body = "See [a](/tables/a.md) and [b](./b.md) and [ext](https://x.com)."
+ self.assertEqual(extract_links(body), ["/tables/a.md", "./b.md", "https://x.com"])
+
+ def test_concept_id(self):
+ self.assertEqual(concept_id("/bundle", "/bundle/tables/users.md"), "tables/users")
+
+
+if __name__ == "__main__":
+ unittest.main()
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `cd custom-skills/97-ourdigital-okf/code/scripts && python3 -m unittest tests.test_okf_common -v`
+Expected: FAIL with `ModuleNotFoundError: No module named 'okf_common'`
+
+- [ ] **Step 3: Write `code/scripts/okf_common.py`**
+
+```python
+"""Shared OKF v0.1 parsing utilities (Python standard library only)."""
+from __future__ import annotations
+
+import re
+from pathlib import Path
+
+RESERVED = {"index.md", "log.md"}
+_LINK_RE = re.compile(r"\[[^\]]*\]\(([^)]+)\)")
+
+
+class FrontmatterError(ValueError):
+ """Raised when a frontmatter block is present but cannot be parsed."""
+
+
+def split_frontmatter(text):
+ """Return (raw_frontmatter, body). raw is None if there is no leading '---' block."""
+ if not text.startswith("---"):
+ return None, text
+ lines = text.splitlines()
+ if lines[0].strip() != "---":
+ return None, text
+ for i in range(1, len(lines)):
+ if lines[i].strip() == "---":
+ return "\n".join(lines[1:i]), "\n".join(lines[i + 1:])
+ raise FrontmatterError("opening '---' without a closing '---'")
+
+
+def _scalar(value):
+ value = value.strip()
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
+ return value[1:-1]
+ return value
+
+
+def parse_yaml_subset(raw):
+ """Parse the small YAML subset OKF uses: `key: value` and `key: [a, b]`."""
+ meta = {}
+ for line in raw.splitlines():
+ stripped = line.strip()
+ if not stripped or stripped.startswith("#"):
+ continue
+ if ":" not in line:
+ raise FrontmatterError("unparseable frontmatter line: %r" % line)
+ key, _, value = line.partition(":")
+ key, value = key.strip(), value.strip()
+ if value.startswith("[") and value.endswith("]"):
+ inner = value[1:-1].strip()
+ meta[key] = [s for s in (_scalar(x) for x in inner.split(",")) if s] if inner else []
+ else:
+ meta[key] = _scalar(value)
+ return meta
+
+
+def parse_frontmatter(text):
+ """Return (meta, body). meta is None when no frontmatter block is present."""
+ raw, body = split_frontmatter(text)
+ if raw is None:
+ return None, text
+ return parse_yaml_subset(raw), body
+
+
+def iter_concepts(bundle_dir):
+ """Yield Path for every non-reserved .md file under bundle_dir, sorted."""
+ root = Path(bundle_dir)
+ for path in sorted(root.rglob("*.md")):
+ if path.name not in RESERVED:
+ yield path
+
+
+def concept_id(bundle_dir, path):
+ """Concept ID = bundle-relative path with the .md suffix removed."""
+ rel = Path(path).relative_to(Path(bundle_dir)).as_posix()
+ return rel[:-3] if rel.endswith(".md") else rel
+
+
+def extract_links(body):
+ """Return the list of link targets from markdown links in body."""
+ return _LINK_RE.findall(body)
+```
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Run: `python3 -m unittest tests.test_okf_common -v`
+Expected: PASS (8 tests OK)
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add custom-skills/97-ourdigital-okf/code/scripts/okf_common.py custom-skills/97-ourdigital-okf/code/scripts/tests/test_okf_common.py
+git commit -m "feat(okf): add okf_common frontmatter/link parser with tests"
+```
+
+---
+
+## Task 4: `okf_validate.py` — conformance + broken-link linter (TDD)
+
+**Files:**
+- Create: `code/scripts/okf_validate.py`
+- Test: `code/scripts/tests/test_okf_validate.py`
+
+- [ ] **Step 1: Write the failing test** — `tests/test_okf_validate.py`
+
+```python
+import sys, tempfile, unittest
+from pathlib import Path
+sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
+from okf_validate import validate_bundle
+
+FIX = Path(__file__).resolve().parent / "fixtures" / "mini_bundle"
+
+
+def write_bundle(root, files):
+ for rel, content in files.items():
+ p = Path(root) / rel
+ p.parent.mkdir(parents=True, exist_ok=True)
+ p.write_text(content, encoding="utf-8")
+
+
+class TestValidate(unittest.TestCase):
+ def test_mini_fixture_conformant(self):
+ report = validate_bundle(FIX)
+ self.assertTrue(report["conformant"], report["errors"])
+ self.assertGreaterEqual(report["concepts"], 3)
+
+ def test_missing_type_is_error(self):
+ with tempfile.TemporaryDirectory() as d:
+ write_bundle(d, {"tables/x.md": "---\ntitle: X\n---\nbody"})
+ report = validate_bundle(d)
+ self.assertFalse(report["conformant"])
+ self.assertTrue(any(e["rule"] == "type" for e in report["errors"]))
+
+ def test_missing_frontmatter_is_error(self):
+ with tempfile.TemporaryDirectory() as d:
+ write_bundle(d, {"tables/x.md": "# no frontmatter\n"})
+ self.assertFalse(validate_bundle(d)["conformant"])
+
+ def test_reserved_files_not_required_to_have_type(self):
+ with tempfile.TemporaryDirectory() as d:
+ write_bundle(d, {"index.md": "# Index\n* [x](/tables/x.md)\n",
+ "tables/x.md": "---\ntype: T\n---\nb"})
+ self.assertTrue(validate_bundle(d)["conformant"])
+
+ def test_broken_link_is_warning_not_error(self):
+ with tempfile.TemporaryDirectory() as d:
+ write_bundle(d, {"tables/x.md": "---\ntype: T\n---\nSee [y](/tables/y.md)."})
+ report = validate_bundle(d)
+ self.assertTrue(report["conformant"])
+ self.assertTrue(any(w["rule"] == "broken_link" for w in report["warnings"]))
+
+
+if __name__ == "__main__":
+ unittest.main()
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `python3 -m unittest tests.test_okf_validate -v`
+Expected: FAIL with `ModuleNotFoundError: No module named 'okf_validate'`
+
+- [ ] **Step 3: Write `code/scripts/okf_validate.py`**
+
+```python
+#!/usr/bin/env python3
+"""OKF v0.1 conformance + broken-link validator (Python standard library only).
+
+Conformance (SPEC.md §9): every non-reserved .md has a parseable frontmatter
+block with a non-empty `type`. Broken cross-links are reported as warnings,
+never errors (consumers MUST tolerate them).
+"""
+from __future__ import annotations
+
+import argparse
+import json
+import sys
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+from okf_common import (FrontmatterError, concept_id, extract_links,
+ iter_concepts, parse_frontmatter)
+
+
+def _resolve_link(bundle, path, target):
+ """Return a Path for an internal .md link, or None for external/anchor/non-md."""
+ t = target.split("#", 1)[0].strip()
+ if not t or "://" in t or t.startswith("mailto:") or not t.endswith(".md"):
+ return None
+ if t.startswith("/"):
+ return bundle / t.lstrip("/")
+ return path.parent / t
+
+
+def validate_bundle(bundle_dir):
+ bundle = Path(bundle_dir)
+ errors, warnings = [], []
+ concept_files = list(iter_concepts(bundle))
+ existing = {p.resolve() for p in bundle.rglob("*.md")}
+ for path in concept_files:
+ cid = concept_id(bundle, path)
+ text = path.read_text(encoding="utf-8")
+ try:
+ meta, body = parse_frontmatter(text)
+ except FrontmatterError as exc:
+ errors.append({"concept": cid, "rule": "frontmatter", "message": str(exc)})
+ continue
+ if meta is None:
+ errors.append({"concept": cid, "rule": "frontmatter",
+ "message": "missing YAML frontmatter block"})
+ continue
+ if not str(meta.get("type", "")).strip():
+ errors.append({"concept": cid, "rule": "type",
+ "message": "missing or empty required 'type' field"})
+ for target in extract_links(body):
+ resolved = _resolve_link(bundle, path, target)
+ if resolved is not None and resolved.resolve() not in existing:
+ warnings.append({"concept": cid, "rule": "broken_link",
+ "message": "link target not found: %s" % target})
+ return {
+ "bundle": str(bundle),
+ "concepts": len(concept_files),
+ "conformant": len(errors) == 0,
+ "errors": errors,
+ "warnings": warnings,
+ }
+
+
+def format_report(report):
+ status = "CONFORMANT" if report["conformant"] else "NON-CONFORMANT"
+ lines = [
+ "OKF v0.1 validation: %s" % report["bundle"],
+ " concepts: %d status: %s" % (report["concepts"], status),
+ " errors: %d warnings: %d" % (len(report["errors"]), len(report["warnings"])),
+ ]
+ for e in report["errors"]:
+ lines.append(" ERROR [%s] %s: %s" % (e["concept"], e["rule"], e["message"]))
+ for w in report["warnings"]:
+ lines.append(" WARN [%s] %s: %s" % (w["concept"], w["rule"], w["message"]))
+ return "\n".join(lines)
+
+
+def main(argv=None):
+ ap = argparse.ArgumentParser(description="Validate an OKF v0.1 bundle.")
+ ap.add_argument("bundle", help="Path to the bundle directory")
+ ap.add_argument("--json", action="store_true", help="Emit a JSON report")
+ args = ap.parse_args(argv)
+ report = validate_bundle(args.bundle)
+ print(json.dumps(report, indent=2) if args.json else format_report(report))
+ return 0 if report["conformant"] else 1
+
+
+if __name__ == "__main__":
+ sys.exit(main())
+```
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Run: `python3 -m unittest tests.test_okf_validate -v`
+Expected: PASS (5 tests OK)
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add custom-skills/97-ourdigital-okf/code/scripts/okf_validate.py custom-skills/97-ourdigital-okf/code/scripts/tests/test_okf_validate.py
+git commit -m "feat(okf): add okf_validate conformance linter with tests"
+```
+
+---
+
+## Task 5: Validate against Google's 3 reference bundles (integration + parser hardening)
+
+**Files:**
+- Modify (if needed): `code/scripts/okf_common.py`
+- Test: append to `code/scripts/tests/test_okf_validate.py`
+
+- [ ] **Step 1: Add the integration test** — append to `tests/test_okf_validate.py` (before the `if __name__` block)
+
+```python
+GOOGLE = Path.home() / "Documents/reference-library/open-knowledge-format/okf/bundles"
+
+
+class TestGoogleBundles(unittest.TestCase):
+ @unittest.skipUnless(GOOGLE.exists(), "Google reference bundles not present")
+ def test_google_sample_bundles_conformant(self):
+ for name in ("crypto_bitcoin", "ga4", "stackoverflow"):
+ bundle = GOOGLE / name
+ if not bundle.exists():
+ continue
+ report = validate_bundle(bundle)
+ self.assertTrue(
+ report["conformant"],
+ "%s non-conformant; first errors: %s" % (name, report["errors"][:3]),
+ )
+```
+
+- [ ] **Step 2: Run it**
+
+Run: `python3 -m unittest tests.test_okf_validate.TestGoogleBundles -v`
+Expected: PASS. **If it FAILS:** inspect the reported errors. If they are real conformance gaps in Google's bundles, that's a finding — record it in `docs/CHANGELOG.md` and relax that specific check to a warning. If they are *parser limitations* (e.g. a frontmatter value shape `parse_yaml_subset` doesn't handle), extend the parser in `okf_common.py` minimally and re-run Task 3 tests to confirm no regressions.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add custom-skills/97-ourdigital-okf/code/scripts
+git commit -m "test(okf): validate against Google reference bundles"
+```
+
+---
+
+## Task 6: `okf_viz.py` — minimal graph visualizer (TDD)
+
+**Files:**
+- Create: `code/scripts/okf_viz.py`
+- Test: `code/scripts/tests/test_okf_viz.py`
+
+- [ ] **Step 1: Write the failing test** — `tests/test_okf_viz.py`
+
+```python
+import sys, tempfile, unittest
+from pathlib import Path
+sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
+from okf_viz import build_graph, render_html, main
+
+FIX = Path(__file__).resolve().parent / "fixtures" / "mini_bundle"
+
+
+class TestViz(unittest.TestCase):
+ def test_build_graph_nodes_and_edges(self):
+ g = build_graph(FIX)
+ ids = {n["id"] for n in g["nodes"]}
+ self.assertIn("tables/orders", ids)
+ self.assertTrue(any(e["source"] == "tables/orders" and e["target"] == "tables/customers"
+ for e in g["edges"]))
+
+ def test_render_html_embeds_data_and_cdn(self):
+ html = render_html(build_graph(FIX), "Mini")
+ self.assertIn("cytoscape", html)
+ self.assertIn('"nodes"', html)
+
+ def test_render_html_escapes_script_close(self):
+ g = {"nodes": [{"id": "x", "type": "T", "title": "x",
+ "description": "", "body": "hi"}], "edges": []}
+ html = render_html(g, "X")
+ self.assertNotIn("hi", html)
+
+ def test_main_writes_file(self):
+ with tempfile.TemporaryDirectory() as d:
+ out = Path(d) / "v.html"
+ self.assertEqual(main(["--bundle", str(FIX), "--out", str(out)]), 0)
+ self.assertTrue(out.exists() and out.stat().st_size > 500)
+
+
+if __name__ == "__main__":
+ unittest.main()
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `python3 -m unittest tests.test_okf_viz -v`
+Expected: FAIL with `ModuleNotFoundError: No module named 'okf_viz'`
+
+- [ ] **Step 3: Write `code/scripts/okf_viz.py`**
+
+```python
+#!/usr/bin/env python3
+"""Generate a minimal, self-contained OKF bundle visualizer (stdlib only).
+
+Emits one HTML file: a Cytoscape.js force-directed graph of the bundle's
+concepts (nodes colored by `type`), with a side panel that renders the
+selected concept's markdown body via marked. Both libraries load from a CDN;
+the bundle is embedded as JSON, so no backend and no data leaves the page.
+"""
+from __future__ import annotations
+
+import argparse
+import html
+import json
+import sys
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+from okf_common import (concept_id, extract_links, iter_concepts,
+ parse_frontmatter)
+
+
+def _target_id(bundle, path, target, ids):
+ t = target.split("#", 1)[0].strip()
+ if not t or "://" in t or not t.endswith(".md"):
+ return None
+ if t.startswith("/"):
+ cid = t.lstrip("/")[:-3]
+ else:
+ rel = (path.parent / t).resolve().relative_to(Path(bundle).resolve())
+ cid = rel.as_posix()[:-3]
+ return cid if cid in ids else None
+
+
+def build_graph(bundle_dir):
+ bundle = Path(bundle_dir)
+ concepts = list(iter_concepts(bundle))
+ ids = {concept_id(bundle, p) for p in concepts}
+ nodes, edges = [], []
+ for path in concepts:
+ cid = concept_id(bundle, path)
+ try:
+ meta, body = parse_frontmatter(path.read_text(encoding="utf-8"))
+ except Exception:
+ meta, body = None, path.read_text(encoding="utf-8")
+ meta = meta or {}
+ nodes.append({
+ "id": cid,
+ "type": str(meta.get("type", "Concept")),
+ "title": str(meta.get("title", Path(cid).name)),
+ "description": str(meta.get("description", "")),
+ "body": body,
+ })
+ for target in extract_links(body):
+ tid = _target_id(bundle, path, target, ids)
+ if tid and tid != cid:
+ edges.append({"source": cid, "target": tid})
+ return {"nodes": nodes, "edges": edges}
+
+
+_HTML = """
+
+__NAME__ — OKF Viewer
+
+
+
+
+Click a concept node to view it.
+"""
+
+
+def render_html(graph, name="OKF Bundle"):
+ data = json.dumps(graph, ensure_ascii=False).replace("", "<\\/")
+ return _HTML.replace("__NAME__", html.escape(name)).replace("__DATA__", data)
+
+
+def main(argv=None):
+ ap = argparse.ArgumentParser(description="Visualize an OKF bundle as self-contained HTML.")
+ ap.add_argument("--bundle", required=True)
+ ap.add_argument("--out")
+ ap.add_argument("--name")
+ args = ap.parse_args(argv)
+ bundle = Path(args.bundle)
+ graph = build_graph(bundle)
+ out = Path(args.out) if args.out else bundle / "viz.html"
+ out.write_text(render_html(graph, args.name or bundle.name), encoding="utf-8")
+ print("Wrote %s (%d nodes, %d edges)" % (out, len(graph["nodes"]), len(graph["edges"])))
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
+```
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Run: `python3 -m unittest tests.test_okf_viz -v`
+Expected: PASS (4 tests OK)
+
+- [ ] **Step 5: Smoke-test against a real Google bundle**
+
+Run: `python3 okf_viz.py --bundle ~/Documents/reference-library/open-knowledge-format/okf/bundles/crypto_bitcoin --out /tmp/okf_btc_viz.html && echo OK`
+Expected: prints `Wrote /tmp/okf_btc_viz.html (N nodes, M edges)` with N>0. Open in a browser to confirm a graph renders. (This is a manual visual check.)
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add custom-skills/97-ourdigital-okf/code/scripts/okf_viz.py custom-skills/97-ourdigital-okf/code/scripts/tests/test_okf_viz.py
+git commit -m "feat(okf): add minimal okf_viz Cytoscape generator with tests"
+```
+
+---
+
+## Task 7: Run the full test suite
+
+- [ ] **Step 1: Run every test**
+
+Run: `cd custom-skills/97-ourdigital-okf/code/scripts && python3 -m unittest discover -s tests -v`
+Expected: all tests PASS (Google-bundle test passes or skips if fixtures absent).
+
+- [ ] **Step 2: Commit any fixes** (only if changes were needed)
+
+```bash
+git add -A custom-skills/97-ourdigital-okf
+git commit -m "test(okf): green full suite"
+```
+
+---
+
+## Task 8: Reference docs (`code/references/`)
+
+**Files:**
+- Create: `code/references/okf-spec-v0.1.md`, `code/references/frontmatter-fields.md`
+
+- [ ] **Step 1: Create `code/references/okf-spec-v0.1.md`**
+
+Write a distilled, *actionable* version of OKF v0.1 (source: `~/Documents/reference-library/open-knowledge-format/okf/SPEC.md`). It MUST contain these sections with this content:
+- **Bundle/Concept/Concept-ID definitions** (concept ID = path minus `.md`).
+- **Reserved filenames table:** `index.md` (directory listing / progressive disclosure, no frontmatter except optional root `okf_version`), `log.md` (date-grouped `YYYY-MM-DD`, newest-first).
+- **Frontmatter table:** `type` REQUIRED; `title`/`description`/`resource`/`tags`/`timestamp` recommended; arbitrary extra keys allowed.
+- **Conventional body headings:** `# Schema`, `# Examples`, `# Citations`.
+- **Cross-linking:** bundle-relative (`/path/concept.md`, leading `/`) recommended; relative (`./x.md`) allowed; broken links tolerated.
+- **Conformance checklist (§9):** parseable frontmatter + non-empty `type` per concept; never reject for missing optional fields / unknown types / broken links / missing index.
+- **Authoring rules for the producer** (numbered): one concept per file; pick descriptive `type` values; prefer structural markdown; cross-link related concepts; add `# Citations` when sourcing external claims; generate `index.md` per directory.
+
+(The "OKF v0.1 at a glance" cheat-sheet in `~/Documents/reference-library/open-knowledge-format/README.md` is a ready source for the tables — adapt it.)
+
+- [ ] **Step 2: Create `code/references/frontmatter-fields.md`**
+
+A one-page field reference: for each of `type, title, description, resource, tags, timestamp` give purpose, requiredness, example value, and example `type` values (`BigQuery Table`, `BigQuery Dataset`, `Metric`, `Playbook`, `Reference`, `API Endpoint`).
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add custom-skills/97-ourdigital-okf/code/references
+git commit -m "docs(okf): add distilled spec + frontmatter reference"
+```
+
+---
+
+## Task 9: Templates (`code/assets/`)
+
+**Files:**
+- Create: `code/assets/concept.md`, `code/assets/index.md`, `code/assets/log.md`
+
+- [ ] **Step 1: Create `code/assets/concept.md`**
+
+```markdown
+---
+type:
+title:
+description:
+resource:
+tags: [, ]
+timestamp:
+---
+
+# Schema
+
+| Column | Type | Description |
+|--------|------|-------------|
+| `col` | TYPE | What it is. FK to [other](/tables/other.md). |
+
+# Citations
+
+[1] [Source title](https://example.com)
+```
+
+- [ ] **Step 2: Create `code/assets/index.md`**
+
+```markdown
+# Group Heading
+
+* [Title](relative-or-bundle-relative-path) - short description from the concept's frontmatter
+```
+
+- [ ] **Step 3: Create `code/assets/log.md`**
+
+```markdown
+# Update Log
+
+## 2026-06-16
+* **Initialization**: Created the bundle structure.
+```
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add custom-skills/97-ourdigital-okf/code/assets
+git commit -m "docs(okf): add concept/index/log templates"
+```
+
+---
+
+## Task 10: `code/SKILL.md` + `code/CLAUDE.md` (Claude Code variant)
+
+**Files:**
+- Create: `code/SKILL.md`, `code/CLAUDE.md`
+
+- [ ] **Step 1: Create `code/SKILL.md`** with this frontmatter and a body (target 800–1,200 words) covering the sections below.
+
+```yaml
+---
+name: ourdigital-okf
+description: |
+ Produce, visualize, and validate Google Open Knowledge Format (OKF) v0.1
+ knowledge bundles. Activated with the "ourdigital" keyword for OKF work.
+
+ Triggers (ourdigital or our prefix):
+ - "ourdigital okf", "our okf"
+ - "ourdigital open knowledge format", "our knowledge bundle"
+ - "ourdigital okf 만들기", "our okf 검증"
+
+ Features:
+ - Produce conformant OKF bundles from a pasted/exported schema, a docs folder, or a research topic
+ - Validate a bundle for OKF v0.1 conformance + broken-link report
+ - Visualize a bundle as a self-contained interactive graph (viz.html)
+version: "1.0"
+author: OurDigital
+environment: Both
+---
+```
+
+Body sections (write actual instructions, not placeholders):
+1. **What is OKF** (3–4 sentences; point to `references/okf-spec-v0.1.md` as the authority — instruct: read it before producing).
+2. **Mode dispatch** — decide produce / visualize / validate from the request.
+3. **Produce flow** (the 6 steps from DESIGN.md §5). Emphasize: (a) **confirm the target bundle directory with the user before creating anything** (no-directory-without-consent rule); (b) input adapters: *schema* = ingest a pasted/exported schema (BigQuery DDL, GA4 export schema, CSV/JSON Schema, OpenAPI) — do **not** call a live MCP; *docs* = read the provided files/folder; *research* = invoke `/reference-curator`; (c) every concept gets a required `type`; (d) cross-link with bundle-relative paths; (e) generate `index.md` per directory; (f) run `python3 code/scripts/okf_validate.py ` and fix all errors before reporting done.
+4. **Validate flow** — `python3 code/scripts/okf_validate.py [--json]`; explain exit code and that broken links are warnings.
+5. **Visualize flow** — `python3 code/scripts/okf_viz.py --bundle [--out viz.html] [--name X]`.
+6. **Resources** — list `references/`, `assets/`, `scripts/`.
+
+- [ ] **Step 2: Create `code/CLAUDE.md`**
+
+```markdown
+# ourdigital-okf (Claude Code)
+
+Use `SKILL.md` in this directory as the instruction set. Scripts live in `scripts/`
+(stdlib-only Python). Read `references/okf-spec-v0.1.md` before producing a bundle.
+Always confirm the output directory with the user before creating it.
+```
+
+- [ ] **Step 3: Verify word count**
+
+Run: `wc -w custom-skills/97-ourdigital-okf/code/SKILL.md`
+Expected: body in the 800–1,200 word range (OurDigital convention).
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add custom-skills/97-ourdigital-okf/code/SKILL.md custom-skills/97-ourdigital-okf/code/CLAUDE.md
+git commit -m "feat(okf): add code-variant SKILL.md and CLAUDE.md"
+```
+
+---
+
+## Task 11: Top-level `SKILL.md`, `README.md`, and `desktop/` variant
+
+**Files:**
+- Create: `SKILL.md` (top-level), `README.md`, `desktop/SKILL.md`, `desktop/skill.yaml`
+
+- [ ] **Step 1: Create top-level `SKILL.md`**
+
+Same frontmatter as `code/SKILL.md` (Task 10 Step 1). Body: a concise canonical version (300–500 words) summarizing the three modes and pointing to `code/SKILL.md` for the detailed Claude Code flows and `code/references/` for the spec.
+
+- [ ] **Step 2: Create `README.md`**
+
+Overview: what the skill does, the three modes, install instructions (`./install.sh`), the script commands, and a note that bundles are validated against Google's reference bundles. Link to `DESIGN.md`.
+
+- [ ] **Step 3: Create `desktop/SKILL.md`**
+
+Leaner Claude Desktop variant: same frontmatter with `environment: Desktop`; body explains produce + validate + visualize conceptually and notes the Python scripts run in the Code environment (Desktop users run them via a terminal).
+
+- [ ] **Step 4: Create `desktop/skill.yaml`** (mirror the pattern in `04-ourdigital-research/desktop/skill.yaml`)
+
+```yaml
+# Skill metadata (extracted from SKILL.md frontmatter)
+
+name: ourdigital-okf
+description: |
+ Produce, visualize, and validate Google Open Knowledge Format (OKF) v0.1 bundles.
+ Triggers: "ourdigital okf", "our okf", "open knowledge format", "knowledge bundle".
+```
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add custom-skills/97-ourdigital-okf/SKILL.md custom-skills/97-ourdigital-okf/README.md custom-skills/97-ourdigital-okf/desktop
+git commit -m "feat(okf): add top-level + desktop skill variants and README"
+```
+
+---
+
+## Task 12: `install.sh` + smoke test
+
+**Files:**
+- Create: `install.sh`
+
+- [ ] **Step 1: Create `install.sh`**
+
+```bash
+#!/bin/bash
+# ourdigital-okf installer — symlinks the skill into ~/.claude/skills/ourdigital-okf
+set -e
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+SKILLS_DIR="$HOME/.claude/skills"
+TARGET="$SKILLS_DIR/ourdigital-okf"
+
+mkdir -p "$SKILLS_DIR"
+if [ -e "$TARGET" ] || [ -L "$TARGET" ]; then
+ echo "Removing existing $TARGET"
+ rm -rf "$TARGET"
+fi
+ln -s "$SCRIPT_DIR" "$TARGET"
+echo "Linked $TARGET -> $SCRIPT_DIR"
+echo "Verifying scripts (stdlib-only)…"
+python3 "$SCRIPT_DIR/code/scripts/okf_validate.py" \
+ "$SCRIPT_DIR/code/scripts/tests/fixtures/mini_bundle" >/dev/null && echo "validate OK"
+echo "Done. Invoke with /ourdigital-okf"
+```
+
+- [ ] **Step 2: Make it executable and run it**
+
+Run:
+```bash
+chmod +x custom-skills/97-ourdigital-okf/install.sh
+custom-skills/97-ourdigital-okf/install.sh
+```
+Expected: prints `Linked …`, `validate OK`, `Done.` and exit 0.
+
+- [ ] **Step 3: Verify the symlink and that the skill resolves**
+
+Run: `ls -l ~/.claude/skills/ourdigital-okf && test -f ~/.claude/skills/ourdigital-okf/SKILL.md && echo PRESENT`
+Expected: symlink shown, `PRESENT`.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add custom-skills/97-ourdigital-okf/install.sh
+git commit -m "feat(okf): add install.sh symlink installer"
+```
+
+---
+
+## Task 13: End-to-end produce test (manual, real flow)
+
+- [ ] **Step 1: Produce a tiny bundle from a docs folder**
+
+Create a scratch input and ask the skill (or follow the produce flow manually) to emit a 2–3 concept bundle into a **user-confirmed** temp dir, e.g. `/tmp/okf-e2e/`. Each concept must have a `type`.
+
+- [ ] **Step 2: Validate it**
+
+Run: `python3 custom-skills/97-ourdigital-okf/code/scripts/okf_validate.py /tmp/okf-e2e`
+Expected: `status: CONFORMANT`, `errors: 0`.
+
+- [ ] **Step 3: Visualize it**
+
+Run: `python3 custom-skills/97-ourdigital-okf/code/scripts/okf_viz.py --bundle /tmp/okf-e2e`
+Expected: writes `/tmp/okf-e2e/viz.html`; opens to a graph in a browser.
+
+- [ ] **Step 4: Record the result in `docs/CHANGELOG.md`**
+
+```markdown
+# Changelog
+
+## 1.0 — 2026-06-16
+* Initial release: produce / validate / visualize OKF v0.1 bundles.
+* Validated against Google reference bundles (crypto_bitcoin, ga4, stackoverflow).
+```
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add custom-skills/97-ourdigital-okf/docs/CHANGELOG.md
+git commit -m "docs(okf): add changelog; e2e produce→validate→visualize verified"
+```
+
+---
+
+## Task 14: OurDigital consistency check + finalize
+
+- [ ] **Step 1: Run the OurDigital skill-creator validation**
+
+Invoke `ourdigital-skill-creator` in "validate existing skill" mode against `97-ourdigital-okf`. It checks: frontmatter has `ourdigital` triggers; description states activation; body 800–1,200 words; `shared/` (here `code/references` + `code/assets`) referenced; no overlap with existing skills; numbering/structure conventions.
+
+- [ ] **Step 2: Fix any reported violations**, re-running the relevant tests/word-count checks. Commit fixes:
+
+```bash
+git add -A custom-skills/97-ourdigital-okf
+git commit -m "fix(okf): address ourdigital-skill-creator consistency findings"
+```
+
+- [ ] **Step 3: Update the Notion spec page** (`381581e5-8a1e-8112-8280-f43839902dc8`) Status → Done.
+
+- [ ] **Step 4: Final full-suite run**
+
+Run: `cd custom-skills/97-ourdigital-okf/code/scripts && python3 -m unittest discover -s tests -v`
+Expected: all green.
+
+- [ ] **Step 5: Offer to merge `feat/ourdigital-okf`** (use superpowers:finishing-a-development-branch).
+
+---
+
+## Self-Review (completed by plan author)
+
+**Spec coverage:** produce (Tasks 8–10 author flow; validator self-check in Task 4) · visualize (Task 6) · validate (Tasks 4–5) · pasted/exported schema adapter (Task 10 §3b) · MCP-agnostic (no live MCP calls in scripts/flows) · minimal viz first (Task 6) · OurDigital structure (Tasks 10–11) · DESIGN.md repo copy (exists) · ourdigital-skill-creator check (Task 14) · Google-bundle verification (Tasks 5, 6) · no-directory-without-consent (Task 10 §3a, Task 13). All spec sections map to a task.
+
+**Placeholder scan:** No "TBD/TODO". Content tasks (8–11) specify exact sections + content to write, not vague "fill in". Code tasks carry complete implementations.
+
+**Type consistency:** `parse_frontmatter`, `parse_yaml_subset`, `split_frontmatter`, `iter_concepts`, `concept_id`, `extract_links`, `FrontmatterError`, `RESERVED` defined in Task 3 and imported consistently in Tasks 4 & 6. `validate_bundle` report keys (`bundle/concepts/conformant/errors/warnings`) used consistently. `build_graph`→`{nodes,edges}` and `render_html(graph,name)` consistent across Task 6 code and tests.
+
+**Known risk:** Task 5 may surface frontmatter shapes in Google's real bundles that the minimal parser doesn't handle; Task 5 Step 2 gives the explicit remediation (extend parser or downgrade to warning).