Browse Source

feat(skills): Formalize okf-ops — Open Knowledge Format assess/validate/adopt

Promotes the two OKF scanners (built during the pilot) into a complete
skill, authored with the skill-creator guide + Skill Resource Protocol.

- assess-okf.py — read-only readiness scan of a doc tree (frontmatter
  coverage, type presence, key/value histogram, readiness %); the 'which
  of my repos are good OKF candidates?' tool
- check-okf.py — conformance validator (hard rules only, honouring OKF's
  permissive-consumption contract; --strict for CI)

SKILL.md leads with honest scope (v0.1 draft, adopt per-repo not blanket),
references/okf-spec.md distills the format, assets/concept-template.md is
copy-ready, tests/run.sh = 10-assertion offline self-test (8/8 suites).
yaml local-alias + unused-var lint nits cleaned. 90 -> 91 skills.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
0xDarkMatter 2 weeks ago
parent
commit
8349e57fc4

+ 1 - 1
AGENTS.md

@@ -5,7 +5,7 @@
 This is **claude-mods** - a collection of custom extensions for Claude Code:
 - **3 expert agents** for pure context-isolation/worker roles (git-agent, firecrawl-expert, project-organizer) - every domain-knowledge agent became an `-ops` skill (v3.0, skills-first)
 - **2 commands** for session management (/sync, /save)
-- **90 skills** for CLI tools, patterns, workflows, and development tasks (incl. `ffmpeg-ops` for probe-first media processing and EDL-driven editing, `supply-chain-defense` for behavioural-first dependency security, `prompt-injection-defense` for instruction-integrity scanning, `net-ops` for network troubleshooting, `windows-ops` / `mac-ops` for workstation diagnostics)
+- **91 skills** for CLI tools, patterns, workflows, and development tasks (incl. `ffmpeg-ops` for probe-first media processing and EDL-driven editing, `supply-chain-defense` for behavioural-first dependency security, `prompt-injection-defense` for instruction-integrity scanning, `net-ops` for network troubleshooting, `windows-ops` / `mac-ops` for workstation diagnostics)
 - **13 output styles** for response personality (Vesper, Spartan, Mentor, Executive, Pair, Atlas, Coach, Harbour, Meridian, Noir, Roast, Sage, Scout)
 - **11 hooks** for pre-commit linting, post-edit formatting, dangerous command warnings, uv enforcement, dependency-install + manifest-edit supply-chain advisories, hidden-Unicode scanning (session-start + pre-commit), live config-change + worktree guards, and pmail notifications - security set auto-wired via plugin hooks.json
 - **Pigeon** inter-session messaging (`pigeon send/read/reply`) - SQLite-backed pmail at `~/.claude/pmail.db`

+ 9 - 0
CHANGELOG.md

@@ -8,6 +8,15 @@ feature releases live in the README "Recent Updates" section.
 ## [Unreleased]
 
 ### Added
+- **`okf-ops` skill** - assess, validate, and adopt the Open Knowledge Format
+  (OKF) across markdown+frontmatter knowledge bases. `assess-okf.py` (read-only)
+  scans a doc tree for OKF-readiness — frontmatter coverage, `type` presence, a
+  key/value histogram, and a readiness % — so you can find good adoption
+  candidates among many repos; `check-okf.py` validates a bundle for conformance
+  (hard rules only, honouring OKF's permissive-consumption contract; `--strict`
+  for CI gating). Honest scope baked in: OKF is a v0.1 draft, adopt per-repo not
+  blanket. Both tools built to the Skill Resource Protocol; OKF format reference +
+  copy-ready concept template; 10-assertion offline self-test.
 - **`adr-ops` skill** - Architecture Decision Records as a cross-project workflow,
   generalized from a mature in-house ADR protocol: when-to-write / when-NOT
   decision rule, the canonical format (BLUF-first `## Decision`, fixed section

+ 4 - 3
README.md

@@ -12,13 +12,13 @@
 
 > *A comprehensive extension toolkit that transforms Claude Code into a specialized development powerhouse.*
 
-**claude-mods** is a production-ready plugin that extends Claude Code with 90 specialized skills, 3 expert agents, 13 output styles, 11 hooks, and modern CLI tools designed for real-world development workflows. Whether you're debugging React hooks, optimizing PostgreSQL queries, or building production CLI applications, this toolkit equips Claude with the domain expertise and procedural knowledge to work at expert level across multiple technology stacks.
+**claude-mods** is a production-ready plugin that extends Claude Code with 91 specialized skills, 3 expert agents, 13 output styles, 11 hooks, and modern CLI tools designed for real-world development workflows. Whether you're debugging React hooks, optimizing PostgreSQL queries, or building production CLI applications, this toolkit equips Claude with the domain expertise and procedural knowledge to work at expert level across multiple technology stacks.
 
 Built on the [Agent Skills specification](https://agentskills.io/specification) (an open standard backed by Anthropic, Vercel, Google, Microsoft, and 40+ agent platforms), claude-mods fills critical gaps in Claude Code's capabilities: persistent session state that survives across machines, on-demand expert knowledge for specialized domains, token-efficient modern CLI tools (10-100x faster than traditional alternatives), and proven workflow patterns for TDD, code review, and feature development. The toolkit implements Anthropic's [recommended patterns for long-running agents](https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents), ensuring your development context never vanishes when sessions end.
 
 From Python async patterns to Rust ownership models, from AWS Fargate deployments to Craft CMS development - claude-mods provides the specialized knowledge and tools that transform Claude from a general-purpose assistant into a domain expert who understands your stack, remembers your workflow, and ships production code.
 
-**3 agents. 90 skills. 13 styles. 11 hooks. 7 rules. One install.**
+**3 agents. 91 skills. 13 styles. 11 hooks. 7 rules. One install.**
 
 ## Recent Updates
 
@@ -90,7 +90,7 @@ claude-mods/
 ├── .claude-plugin/     # Plugin metadata
 ├── agents/             # Expert subagents (3)
 ├── commands/           # Slash commands (2)
-├── skills/             # Custom skills (90)
+├── skills/             # Custom skills (91)
 ├── output-styles/      # Response personalities
 ├── hooks/              # Hook examples & docs
 ├── rules/              # Claude Code rules
@@ -271,6 +271,7 @@ See [skill-creator](skills/skill-creator/) for the complete guide.
 | [summon](skills/summon/) | Transfer Claude Desktop Code-tab sessions between accounts - push/pull with picker |
 | [doc-scanner](skills/doc-scanner/) | Scan and synthesize project documentation |
 | [adr-ops](skills/adr-ops/) | Architecture Decision Records - when-to-write, canonical format, supersession lifecycle, scaffold/index/lint tools |
+| [okf-ops](skills/okf-ops/) | Open Knowledge Format - assess a doc repo's frontmatter-readiness, validate a bundle for conformance, decide per-repo adoption |
 | [project-planner](skills/project-planner/) | Track stale plans, suggest session commands |
 | [python-env](skills/python-env/) | Fast Python environment management with uv |
 | [task-runner](skills/task-runner/) | Run project commands with just |

+ 1 - 1
docs/PLAN.md

@@ -16,7 +16,7 @@
 | Component | Count | Notes |
 |-----------|-------|-------|
 | Agents | 3 | Pure context-isolation/worker roles only: git-agent (background commits/PRs), firecrawl-expert (noisy scrapes), project-organizer (bulk restructure) |
-| Skills | 90 | Operational skills, CLI tools, workflows, diagnostics, security |
+| Skills | 91 | Operational skills, CLI tools, workflows, diagnostics, security |
 | Commands | 2 | Session management (sync, save) |
 | Rules | 7 | cli-tools, commit-style, naming-conventions, prompt-injection, skill-agent-updates, supply-chain, worktree-boundaries |
 | Output Styles | 13 | Vesper, Spartan, Mentor, Executive, Pair, Atlas, Coach, Harbour, Meridian, Noir, Roast, Sage, Scout |

+ 96 - 0
skills/okf-ops/SKILL.md

@@ -0,0 +1,96 @@
+---
+name: okf-ops
+description: "Assess, validate, and adopt the Open Knowledge Format (OKF) across markdown+frontmatter knowledge bases. Use to scan a doc repo for OKF-readiness (how frontmatter-consistent it already is), validate a bundle for conformance, find good OKF-adoption candidates among many repos, or migrate a frontmatter-heavy repo onto OKF. Triggers on: OKF, open knowledge format, knowledge bundle, knowledge base format, assess docs, doc repo readiness, frontmatter conformance, validate frontmatter, type frontmatter, markdown knowledge base, adopt OKF, is this repo OKF-ready, index.md log.md."
+when_to_use: "Use when deciding whether/how to adopt OKF in a repo, scanning one or many doc trees for frontmatter consistency, or validating an OKF bundle — e.g. 'how OKF-ready is this repo', 'check this bundle conforms', 'which of my repos are good OKF candidates', 'validate the frontmatter in docs/'."
+license: MIT
+compatibility: "Python 3.8+. PyYAML used if present; falls back to a built-in parser otherwise."
+allowed-tools: "Read Bash Glob Grep"
+metadata:
+  author: claude-mods
+  related-skills: "adr-ops, doc-scanner"
+---
+
+# OKF Ops
+
+The **Open Knowledge Format (OKF)** is a minimal, open convention for representing
+*knowledge* as a directory tree of markdown files with YAML frontmatter — the metadata
+and curated context that surrounds data and systems. This skill helps you **assess**
+whether a repo is a good fit, **validate** a bundle for conformance, and **adopt** OKF
+where it earns its keep.
+
+Full format rules: [references/okf-spec.md](references/okf-spec.md). Copy-ready concept
+doc: [assets/concept-template.md](assets/concept-template.md).
+
+## Honest scope (read this before adopting)
+
+OKF is a **v0.1 draft** (Google-published, platform-agnostic). It's deliberately
+minimal: one required frontmatter field (`type`), reserved `index.md`/`log.md`, and a
+*permissive-consumption* contract. Two consequences worth knowing up front:
+
+- **Adoption cost is shaped by the repo, not the size.** A repo that already uses
+  frontmatter consistently is often one mechanical `type`-derivation pass from
+  conformant. A repo of bare prose markdown needs frontmatter authored on every file —
+  often not worth it, and arguably the wrong files to make "concepts."
+- **Conformance is a weak guarantee.** "OKF-conformant" means the structural floor is
+  met, not that the content is good. Use the assessment to decide adoption per-repo;
+  don't make it a blanket mandate.
+
+The tools here are useful **regardless of OKF's trajectory** — `assess-okf.py` is a
+general "how frontmatter-consistent is this doc tree?" scanner.
+
+## Workflow: assess → decide → validate
+
+### 1. Assess (read-only) — is this repo a good candidate?
+
+```bash
+python scripts/assess-okf.py docs/                    # human summary
+python scripts/assess-okf.py --json docs/ | jq '.data.readiness_pct'
+```
+
+Reports total `.md`, how many already carry frontmatter, how many have a non-empty
+`type`, a histogram of existing frontmatter **keys** (shows what vocabulary you already
+have to derive `type` from), `type`-value distribution, reserved files present, files
+that would need a `type`, and an overall **readiness %**. Never writes.
+
+**Read the histogram, not just the %.** A repo at "0% readiness" with rich consistent
+keys (e.g. every file has `title`/`level`/`tags`) is a *cheap* migration — you derive
+`type` from an existing key. A repo at "0%" with mostly empty frontmatter is *expensive*.
+To find candidates across many repos, run assess on each and compare.
+
+### 2. Decide — adopt only where the squeeze is worth the juice
+
+- **Frontmatter-consistent repo** → adopt: derive `type`, fix any malformed files, done.
+- **Mixed prose + frontmatter repo** → usually skip, or adopt a subset (designate only
+  the real concept docs; OKF has no built-in prose exemption — that's a known rigidity).
+
+### 3. Validate — does a bundle conform?
+
+```bash
+python scripts/check-okf.py ./bundle                  # exit 0 conformant, 10 if not
+python scripts/check-okf.py --json ./bundle | jq '.data[] | select(.severity=="error")'
+python scripts/check-okf.py --strict ./bundle         # soft warnings also fail
+```
+
+Enforces only the hard rules (every non-reserved `.md` has parseable frontmatter with a
+non-empty `type`; reserved files get light sanity). Per OKF's permissive-consumption
+rule, broken links and missing optional fields are INFO, never failures (unless
+`--strict`). Wire `check-okf.py --strict` as a CI gate (exit 10 fails the build) once a
+repo has adopted OKF.
+
+## Tools
+
+Both scripts follow the Skill Resource Protocol: stdout is data-only (`--json` emits a
+`{"data":…,"meta":{"schema":…}}` envelope), framing/progress to stderr, `--help` with
+examples, semantic exit codes. Stdlib-only; PyYAML used if present, else a built-in
+frontmatter parser (announced on stderr).
+
+| Script | Role | Exit codes |
+|--------|------|-----------|
+| `scripts/assess-okf.py` | Read-only readiness scan of a doc tree | `0` scanned, `2` usage, `3` not-found |
+| `scripts/check-okf.py` | Conformance validator for a bundle | `0` conformant, `10` non-conformant, `4` unparseable frontmatter, `3` not-found, `2` usage |
+
+## See also
+
+- [references/okf-spec.md](references/okf-spec.md) — the format: frontmatter fields,
+  reserved files, conformance rules, permissive-consumption, versioning.
+- [assets/concept-template.md](assets/concept-template.md) — copy-ready OKF concept doc.

+ 29 - 0
skills/okf-ops/assets/concept-template.md

@@ -0,0 +1,29 @@
+---
+type: concept
+title: Human-readable name of this concept
+description: One sentence summarizing what this document captures.
+resource: "uri://to/the/underlying/asset"   # optional; quote it
+tags: [example, replace-me]
+timestamp: 2026-01-01T00:00:00Z
+# Add any extra keys your own schema needs — consumers must preserve them.
+---
+
+# Title
+
+A short orienting paragraph: what this asset/concept is and why it matters.
+
+## Schema
+
+Structured description of the columns / fields / shape, if applicable.
+
+| Field | Type | Notes |
+|-------|------|-------|
+| id    | string | ... |
+
+## Examples
+
+Concrete usage — a query, a sample record, a call.
+
+## Citations
+
+- [Source or upstream doc](https://example.com)

+ 99 - 0
skills/okf-ops/references/okf-spec.md

@@ -0,0 +1,99 @@
+# OKF Format Reference
+
+Distilled rules for the **Open Knowledge Format (OKF) v0.1** as enforced by
+`check-okf.py`. OKF is platform-agnostic; this is the working reference, not the
+upstream spec verbatim. When precision matters, confirm against the source.
+
+## Contents
+
+1. [Bundle layout](#bundle-layout)
+2. [Concept documents](#concept-documents)
+3. [Frontmatter fields](#frontmatter-fields)
+4. [Reserved files](#reserved-files)
+5. [Linking](#linking)
+6. [Conformance rules](#conformance-rules)
+7. [Permissive consumption](#permissive-consumption)
+8. [Versioning](#versioning)
+
+## Bundle layout
+
+A **bundle** is a directory tree of markdown files with optional subdirectories. Two
+filenames are **reserved** (`index.md`, `log.md`); every other `.md` is a **concept
+document**.
+
+```
+my-bundle/
+├── index.md            # (optional) directory listing; may carry okf_version at root
+├── log.md              # (optional) chronological update history
+├── concept-a.md        # concept document (frontmatter + body)
+└── sub/
+    └── concept-b.md
+```
+
+## Concept documents
+
+Each concept document = **YAML frontmatter** delimited by `---`, then a **markdown
+body**. Conventional (optional) body headings: `# Schema` (columns/fields), `# Examples`,
+`# Citations`.
+
+## Frontmatter fields
+
+| Field | Required | Meaning |
+|-------|----------|---------|
+| `type` | **yes** | Short string identifying the kind of concept. Consumers route/filter/present on it. **The one hard requirement.** OKF does not define a taxonomy — you choose your `type` vocabulary. |
+| `title` | recommended | Display name. |
+| `description` | recommended | One-sentence summary. |
+| `resource` | recommended | URI uniquely identifying the underlying asset. |
+| `tags` | recommended | YAML list of short strings for cross-cutting categorization. |
+| `timestamp` | recommended | ISO-8601 datetime of last change. |
+
+Producers may add **arbitrary extra keys**; consumers **must preserve** unknown keys.
+This is what lets a richer in-house schema (e.g. `level`, `children`) ride on top of an
+OKF-conformant base — be a superset, never dumb down to OKF's minimum.
+
+## Reserved files
+
+- **`index.md`** — directory listing. Normally **no frontmatter**; uses markdown sections
+  grouping links: `* [Title](url) - description`. The bundle-root `index.md` is the one
+  allowed exception — it may carry `okf_version` in frontmatter.
+- **`log.md`** — chronological history. ISO-8601 `YYYY-MM-DD` date headings grouping prose
+  entries (conventional leads: `**Update**`, `**Creation**`).
+
+## Linking
+
+- Bundle-relative (absolute): `/path/to/concept.md`
+- Relative: `./other.md`
+
+Links assert a relationship; specific semantics come from surrounding prose. **Broken
+links are tolerated** (see permissive consumption).
+
+## Conformance rules
+
+A bundle is **conformant** iff:
+
+1. Every non-reserved `.md` file has **parseable YAML frontmatter**.
+2. Every such frontmatter has a **non-empty `type`**.
+3. Reserved files follow their structure **when present**.
+
+`check-okf.py` enforces exactly these as hard failures (exit 10), plus light structural
+sanity on reserved files. Everything else is INFO/warning.
+
+## Permissive consumption
+
+Consumers **MUST NOT** reject a bundle for any of:
+
+- Missing optional frontmatter fields
+- Unknown `type` values
+- Unknown additional frontmatter keys
+- Broken cross-links
+- Missing `index.md`
+
+This is intentional: OKF stays useful as bundles grow, get refactored, and are partially
+agent-generated. `check-okf.py` honours this — these surface as INFO and never fail
+conformance unless `--strict` is passed.
+
+## Versioning
+
+`<major>.<minor>`. Minor bumps add backward-compatibly; major bumps may break. A bundle
+declares its target version with `okf_version: "0.1"` in the bundle-root `index.md`
+frontmatter. Pin it and re-check on a major bump.

+ 257 - 0
skills/okf-ops/scripts/assess-okf.py

@@ -0,0 +1,257 @@
+#!/usr/bin/env python3
+# Read-only OKF-readiness scanner for a markdown doc-tree. NEVER writes.
+#
+# Usage:   assess-okf.py [OPTIONS] [DOC_TREE]
+# Input:   argv only. DOC_TREE is a directory of markdown files (default ".").
+# Output:  stdout = data. Default = human-readable summary; --json = envelope
+#          {"data":{...},"meta":{"schema":"claude-mods.okf-ops.assess-okf/v1",...}}.
+# Stderr:  progress + the scan header.
+# Exit:    0 on a successful scan (readiness is DATA, not a failure), 2 usage, 3 not-found.
+#
+# Reports: total .md, how many have frontmatter, how many have non-empty `type`,
+# a histogram of frontmatter KEYS, a histogram of `type` VALUES, reserved files
+# present, files that would need a `type` to become conformant, OKF-readiness %,
+# and which OKF recommended fields already commonly appear.
+#
+# Examples:
+#   assess-okf.py /path/to/docs
+#   assess-okf.py --json /path/to/docs | jq '.data.readiness_pct'
+#   assess-okf.py --top 10 .
+import argparse
+import json
+import sys
+from collections import Counter
+from pathlib import Path
+
+SCHEMA = "claude-mods.okf-ops.assess-okf/v1"
+SKIP_DIRS = {".git", "node_modules", ".claude", ".venv", "dist", "build"}
+RESERVED = {"index.md", "log.md"}
+RECOMMENDED = ("title", "description", "resource", "tags", "timestamp")
+
+try:
+    import yaml  # type: ignore
+    _HAVE_YAML = True
+except Exception:
+    yaml = None
+    _HAVE_YAML = False
+
+
+def log(msg=""):
+    print(msg, file=sys.stderr)
+
+
+def split_frontmatter(text):
+    if text.startswith(""):
+        text = text[1:]
+    lines = text.splitlines()
+    i = 0
+    while i < len(lines) and lines[i].strip() == "":
+        i += 1
+    if i >= len(lines) or lines[i].strip() != "---":
+        return None
+    for j in range(i + 1, len(lines)):
+        if lines[j].strip() == "---":
+            return "\n".join(lines[i + 1:j])
+    return None
+
+
+def parse_frontmatter(fm_str):
+    """Return dict (possibly empty) or None if unparseable."""
+    _yaml = yaml  # local alias narrows cleanly (module global won't)
+    if _yaml is not None:
+        try:
+            data = _yaml.safe_load(fm_str)
+            if data is None:
+                return {}
+            if not isinstance(data, dict):
+                return None
+            return data
+        except Exception:
+            return None
+    data = {}
+    for raw in fm_str.splitlines():
+        line = raw.rstrip()
+        if not line.strip() or line.lstrip().startswith("#"):
+            continue
+        if line[0] in (" ", "\t", "-"):
+            continue
+        if ":" not in line:
+            return None
+        key, _, val = line.partition(":")
+        key = key.strip()
+        val = val.strip()
+        if len(val) >= 2 and val[0] == val[-1] and val[0] in ("'", '"'):
+            val = val[1:-1]
+        if key:
+            data[key] = val
+    return data
+
+
+def main(argv=None):
+    p = argparse.ArgumentParser(
+        prog="assess-okf.py",
+        description="Read-only OKF-readiness scanner (never writes).",
+        add_help=True,
+    )
+    p.add_argument("tree", nargs="?", default=".", help="doc-tree directory (default .)")
+    p.add_argument("--json", action="store_true", help="emit JSON envelope to stdout")
+    p.add_argument("--top", type=int, default=20, metavar="N",
+                   help="cap histogram rows (default 20)")
+    args = p.parse_args(argv)
+
+    if args.top < 1:
+        log("error: --top must be >= 1")
+        return 2
+
+    root = Path(args.tree)
+    if not root.exists() or not root.is_dir():
+        msg = f"doc-tree path not found or not a directory: {args.tree}"
+        log(f"error: {msg}")
+        if args.json:
+            print(json.dumps({"error": {"code": "NOT_FOUND", "message": msg, "details": {}}}))
+        return 3
+
+    if not _HAVE_YAML:
+        log("note: PyYAML not available — using minimal fallback frontmatter parser.")
+
+    root = root.resolve()
+    log(f"OKF-readiness scan: {root}")
+
+    md_total = 0
+    concept_total = 0
+    have_frontmatter = 0
+    have_type = 0
+    unparseable = 0
+    reserved_index = 0
+    reserved_log = 0
+    need_type = 0  # non-reserved concept docs lacking a non-empty type
+
+    key_hist = Counter()
+    type_hist = Counter()
+    recommended_present = Counter()
+
+    for path in sorted(root.rglob("*.md")):
+        if any(part in SKIP_DIRS for part in path.parts):
+            continue
+        md_total += 1
+        name = path.name.lower()
+        try:
+            text = path.read_text(encoding="utf-8", errors="replace")
+        except Exception:
+            text = ""
+
+        if name == "index.md":
+            reserved_index += 1
+        if name == "log.md":
+            reserved_log += 1
+
+        fm_str = split_frontmatter(text)
+        has_fm = fm_str is not None
+        data = parse_frontmatter(fm_str) if has_fm else None
+
+        if has_fm and data is not None:
+            have_frontmatter += 1
+            for k in data.keys():
+                key_hist[str(k)] += 1
+            for rk in RECOMMENDED:
+                if rk in data:
+                    recommended_present[rk] += 1
+            tv = data.get("type")
+            if tv is not None and not (isinstance(tv, str) and tv.strip() == ""):
+                have_type += 1
+                type_hist[str(tv).strip()] += 1
+        elif has_fm and data is None:
+            unparseable += 1
+
+        if name not in RESERVED:
+            concept_total += 1
+            tv = data.get("type") if isinstance(data, dict) else None
+            conformant = (data is not None and tv is not None
+                          and not (isinstance(tv, str) and tv.strip() == ""))
+            if not conformant:
+                need_type += 1
+
+    conformant_concepts = concept_total - need_type
+    readiness_pct = round(100.0 * conformant_concepts / concept_total, 1) if concept_total else 0.0
+
+    def top(counter):
+        return [{"key": k, "count": c} for k, c in counter.most_common(args.top)]
+
+    data_out = {
+        "md_total": md_total,
+        "concept_total": concept_total,
+        "have_frontmatter": have_frontmatter,
+        "have_frontmatter_pct": round(100.0 * have_frontmatter / md_total, 1) if md_total else 0.0,
+        "have_nonempty_type": have_type,
+        "have_type_pct": round(100.0 * have_type / md_total, 1) if md_total else 0.0,
+        "unparseable_frontmatter": unparseable,
+        "reserved_index_md": reserved_index,
+        "reserved_log_md": reserved_log,
+        "concepts_needing_type": need_type,
+        "conformant_concepts": conformant_concepts,
+        "readiness_pct": readiness_pct,
+        "key_histogram": top(key_hist),
+        "type_value_histogram": top(type_hist),
+        "recommended_fields_present": [
+            {"field": k, "count": recommended_present.get(k, 0)} for k in RECOMMENDED
+        ],
+    }
+
+    meta = {
+        "schema": SCHEMA,
+        "tree": str(root),
+        "top": args.top,
+        "yaml_parser": "PyYAML" if _HAVE_YAML else "fallback",
+        "distinct_keys": len(key_hist),
+        "distinct_type_values": len(type_hist),
+    }
+
+    if args.json:
+        print(json.dumps({"data": data_out, "meta": meta}, ensure_ascii=False))
+        return 0
+
+    # Human-readable summary to stdout.
+    out = []
+    out.append("OKF-readiness summary")
+    out.append("=" * 60)
+    out.append(f"  doc-tree                 : {root}")
+    out.append(f"  yaml parser              : {meta['yaml_parser']}")
+    out.append("")
+    out.append(f"  markdown files (.md)     : {md_total}")
+    out.append(f"  reserved index.md        : {reserved_index}")
+    out.append(f"  reserved log.md          : {reserved_log}")
+    out.append(f"  concept documents        : {concept_total}  (non-reserved)")
+    out.append("")
+    out.append(f"  with parseable frontmatter : {have_frontmatter}  "
+               f"({data_out['have_frontmatter_pct']}% of all .md)")
+    out.append(f"  with non-empty `type`      : {have_type}  "
+               f"({data_out['have_type_pct']}% of all .md)")
+    out.append(f"  unparseable frontmatter    : {unparseable}")
+    out.append("")
+    out.append(f"  concepts needing a `type`  : {need_type}")
+    out.append(f"  conformant concepts        : {conformant_concepts} / {concept_total}")
+    out.append(f"  OKF-READINESS              : {readiness_pct}%")
+    out.append("")
+    out.append(f"  Frontmatter KEYS (top {args.top}, {meta['distinct_keys']} distinct):")
+    if key_hist:
+        for k, c in key_hist.most_common(args.top):
+            out.append(f"     {c:6d}  {k}")
+    else:
+        out.append("     (none)")
+    out.append("")
+    out.append(f"  `type` VALUES (top {args.top}, {meta['distinct_type_values']} distinct):")
+    if type_hist:
+        for k, c in type_hist.most_common(args.top):
+            out.append(f"     {c:6d}  {k}")
+    else:
+        out.append("     (none — no `type` keys present yet)")
+    out.append("")
+    out.append("  OKF recommended fields already present:")
+    for k in RECOMMENDED:
+        out.append(f"     {recommended_present.get(k, 0):6d}  {k}")
+    print("\n".join(out))
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())

+ 279 - 0
skills/okf-ops/scripts/check-okf.py

@@ -0,0 +1,279 @@
+#!/usr/bin/env python3
+# Validate an Open Knowledge Format (OKF v0.1) bundle for conformance.
+#
+# Usage:   check-okf.py [OPTIONS] [BUNDLE_DIR]
+# Input:   argv only. BUNDLE_DIR is a directory of markdown files (default ".").
+# Output:  stdout = data only. Default = TSV of findings (file<TAB>severity<TAB>message);
+#          --json = envelope {"data":[...],"meta":{"schema":"claude-mods.okf-ops.check-okf/v1",...}}.
+# Stderr:  headers, progress, the human-readable verdict, errors.
+# Exit:    0 conformant, 2 usage, 3 not-found, 4 frontmatter-present-but-unparseable,
+#          10 non-conformant (hard conformance failures, or soft warnings under --strict).
+#
+# OKF rules enforced (hard): every non-reserved .md has parseable YAML frontmatter
+# with a non-empty `type`. Reserved files (index.md, log.md) get light structural
+# sanity only. Per the permissive-consumption rule, broken links / missing optional
+# fields are INFO and never cause a conformance failure (unless --strict).
+#
+# Examples:
+#   check-okf.py ./my-bundle
+#   check-okf.py --json ./my-bundle | jq '.data[] | select(.severity=="error")'
+#   check-okf.py --strict .          # soft warnings also fail (exit 10)
+import argparse
+import json
+import sys
+from pathlib import Path
+
+SCHEMA = "claude-mods.okf-ops.check-okf/v1"
+SKIP_DIRS = {".git", "node_modules", ".claude", ".venv", "dist", "build"}
+RESERVED = {"index.md", "log.md"}
+RECOMMENDED = ("title", "description", "resource", "tags", "timestamp")
+
+try:
+    import yaml  # type: ignore
+    _HAVE_YAML = True
+except Exception:
+    yaml = None
+    _HAVE_YAML = False
+
+
+def log(msg=""):
+    print(msg, file=sys.stderr)
+
+
+def split_frontmatter(text):
+    """Return (frontmatter_str_or_None, found_fences_bool).
+
+    A document has frontmatter iff it starts (after optional BOM/whitespace-free
+    leading newlines) with a line that is exactly '---' and has a closing '---'.
+    """
+    # Normalise leading BOM
+    if text.startswith(""):
+        text = text[1:]
+    lines = text.splitlines()
+    # Allow leading blank lines before the opening fence
+    i = 0
+    while i < len(lines) and lines[i].strip() == "":
+        i += 1
+    if i >= len(lines) or lines[i].strip() != "---":
+        return None, False
+    # find closing fence
+    for j in range(i + 1, len(lines)):
+        if lines[j].strip() == "---":
+            return "\n".join(lines[i + 1:j]), True
+    # opening fence with no close
+    return None, True
+
+
+def parse_frontmatter(fm_str):
+    """Parse frontmatter into a dict. Returns (dict_or_None, used_fallback_bool).
+
+    dict is None when the block is genuinely unparseable.
+    """
+    _yaml = yaml  # local alias narrows cleanly (module global won't)
+    if _yaml is not None:
+        try:
+            data = _yaml.safe_load(fm_str)
+            if data is None:
+                return {}, False
+            if not isinstance(data, dict):
+                return None, False
+            return data, False
+        except Exception:
+            return None, False
+    # Fallback: minimal key: value line parser. Good enough to detect `type`.
+    data = {}
+    for raw in fm_str.splitlines():
+        line = raw.rstrip()
+        if not line.strip() or line.lstrip().startswith("#"):
+            continue
+        # only treat top-level (non-indented) key: value lines as keys
+        if line[0] in (" ", "\t", "-"):
+            continue
+        if ":" not in line:
+            return None, True  # not a simple key:value block -> unparseable in fallback
+        key, _, val = line.partition(":")
+        key = key.strip()
+        val = val.strip()
+        # strip surrounding quotes
+        if len(val) >= 2 and val[0] == val[-1] and val[0] in ("'", '"'):
+            val = val[1:-1]
+        if key:
+            data[key] = val
+    return data, True
+
+
+def main(argv=None):
+    p = argparse.ArgumentParser(
+        prog="check-okf.py",
+        description="Validate an OKF v0.1 bundle for conformance.",
+        add_help=True,
+    )
+    p.add_argument("bundle", nargs="?", default=".", help="bundle directory (default .)")
+    p.add_argument("--json", action="store_true", help="emit JSON envelope to stdout")
+    p.add_argument("--strict", action="store_true",
+                   help="soft warnings also count toward non-conformance (exit 10)")
+    try:
+        args = p.parse_args(argv)
+    except SystemExit as e:
+        # argparse exits 0 on --help, 2 on error — both acceptable per protocol
+        raise
+
+    root = Path(args.bundle)
+    if not root.exists() or not root.is_dir():
+        msg = f"bundle path not found or not a directory: {args.bundle}"
+        log(f"error: {msg}")
+        if args.json:
+            print(json.dumps({"error": {"code": "NOT_FOUND", "message": msg, "details": {}}}))
+        return 3
+
+    if not _HAVE_YAML:
+        log("note: PyYAML not available — using minimal fallback frontmatter parser.")
+
+    root = root.resolve()
+    log(f"OKF conformance check: {root}")
+
+    findings = []          # list of {file, severity, message}
+    unparseable = False    # any frontmatter-present-but-unparseable
+    md_total = 0
+    concept_total = 0
+    okf_version = None
+
+    for path in sorted(root.rglob("*.md")):
+        # skip excluded dirs
+        if any(part in SKIP_DIRS or part == "worktrees" for part in path.parts):
+            # only skip 'worktrees' when under a .claude dir
+            if "worktrees" in path.parts:
+                idx = path.parts.index("worktrees")
+                if idx > 0 and path.parts[idx - 1] == ".claude":
+                    continue
+            if any(part in SKIP_DIRS for part in path.parts):
+                continue
+        md_total += 1
+        rel = path.relative_to(root).as_posix()
+        name = path.name.lower()
+
+        try:
+            text = path.read_text(encoding="utf-8", errors="replace")
+        except Exception as e:
+            findings.append({"file": rel, "severity": "error",
+                             "message": f"could not read file: {e}"})
+            continue
+
+        fm_str, found_fences = split_frontmatter(text)
+
+        if name in RESERVED:
+            # Light structural sanity only.
+            is_root_index = (name == "index.md" and path.parent == root)
+            if found_fences and fm_str is not None and name == "index.md":
+                # allowed exception: root index.md may declare okf_version
+                data, _ = parse_frontmatter(fm_str)
+                if data and "okf_version" in data:
+                    okf_version = data.get("okf_version")
+                if not is_root_index and data is not None:
+                    findings.append({"file": rel, "severity": "warning",
+                                     "message": "non-root index.md has frontmatter "
+                                                "(only root index.md may declare okf_version)"})
+            if found_fences and fm_str is None:
+                findings.append({"file": rel, "severity": "warning",
+                                 "message": "reserved file opens '---' fence but never closes it"})
+            # very light content sanity
+            if name == "log.md":
+                import re as _re
+                if not _re.search(r"(?m)^#{1,6}\s*\d{4}-\d{2}-\d{2}", text) and text.strip():
+                    findings.append({"file": rel, "severity": "info",
+                                     "message": "log.md has no ISO-8601 (YYYY-MM-DD) date headings"})
+            continue
+
+        # Non-reserved => concept document. Hard requirements apply.
+        concept_total += 1
+
+        if not found_fences or fm_str is None:
+            if found_fences and fm_str is None:
+                # fence opened but unparseable / unclosed
+                findings.append({"file": rel, "severity": "error",
+                                 "message": "frontmatter fence present but block is unparseable "
+                                            "(no closing '---')"})
+                unparseable = True
+            else:
+                findings.append({"file": rel, "severity": "error",
+                                 "message": "missing YAML frontmatter (no leading '---' block)"})
+            continue
+
+        data, _ = parse_frontmatter(fm_str)
+        if data is None:
+            findings.append({"file": rel, "severity": "error",
+                             "message": "frontmatter present but not parseable as YAML"})
+            unparseable = True
+            continue
+
+        type_val = data.get("type")
+        if type_val is None or (isinstance(type_val, str) and type_val.strip() == ""):
+            findings.append({"file": rel, "severity": "error",
+                             "message": "frontmatter missing non-empty `type` field"})
+            continue
+
+        # Soft INFO: note missing recommended fields (never a hard failure).
+        missing = [k for k in RECOMMENDED if k not in data]
+        if missing:
+            findings.append({"file": rel, "severity": "info",
+                             "message": "missing recommended fields: " + ", ".join(missing)})
+
+    errors = [f for f in findings if f["severity"] == "error"]
+    warnings = [f for f in findings if f["severity"] == "warning"]
+    infos = [f for f in findings if f["severity"] == "info"]
+
+    # Determine exit code.
+    if unparseable:
+        exit_code = 4
+    elif errors:
+        exit_code = 10
+    elif args.strict and warnings:
+        exit_code = 10
+    else:
+        exit_code = 0
+
+    conformant = (exit_code == 0)
+
+    meta = {
+        "schema": SCHEMA,
+        "bundle": str(root),
+        "okf_version": okf_version,
+        "md_total": md_total,
+        "concept_total": concept_total,
+        "errors": len(errors),
+        "warnings": len(warnings),
+        "infos": len(infos),
+        "conformant": conformant,
+        "yaml_parser": "PyYAML" if _HAVE_YAML else "fallback",
+        "strict": args.strict,
+    }
+
+    # Human verdict to stderr.
+    log("")
+    log(f"  markdown files scanned : {md_total}")
+    log(f"  concept documents      : {concept_total}")
+    log(f"  errors                 : {len(errors)}")
+    log(f"  warnings               : {len(warnings)}")
+    log(f"  info                   : {len(infos)}")
+    if okf_version:
+        log(f"  declared okf_version   : {okf_version}")
+    if exit_code == 0:
+        log("  verdict                : CONFORMANT")
+    elif exit_code == 4:
+        log("  verdict                : INVALID (unparseable frontmatter present)")
+    else:
+        log("  verdict                : NON-CONFORMANT")
+
+    # Data product.
+    if args.json:
+        print(json.dumps({"data": findings, "meta": meta}, ensure_ascii=False))
+    else:
+        # TSV: file<TAB>severity<TAB>message  (data only, no header line on stdout)
+        for f in findings:
+            print(f"{f['file']}\t{f['severity']}\t{f['message']}")
+
+    return exit_code
+
+
+if __name__ == "__main__":
+    sys.exit(main())

+ 80 - 0
skills/okf-ops/tests/run.sh

@@ -0,0 +1,80 @@
+#!/usr/bin/env bash
+# Offline self-test for okf-ops scripts. No network. Exits 0 if all pass.
+set -uo pipefail
+
+HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+SCRIPTS="$(cd "$HERE/.." && pwd)/scripts"
+
+# Pick a python that actually runs (the Windows Store `python3` stub exits 49).
+PYTHON=""
+for c in python python3 py; do
+    command -v "$c" >/dev/null 2>&1 || continue
+    "$c" -c 'import sys' >/dev/null 2>&1 && { PYTHON="$c"; break; }
+done
+[[ -z "$PYTHON" ]] && { echo "no working python found" >&2; exit 1; }
+
+pass=0; fail=0
+ok()  { echo "  PASS  $1"; pass=$((pass+1)); }
+no()  { echo "  FAIL  $1"; fail=$((fail+1)); }
+expect_exit() { # label want got
+    if [[ "$2" == "$3" ]]; then ok "$1 (exit $3)"; else no "$1 (want $2 got $3)"; fi
+}
+
+SB="$(mktemp -d)"
+trap 'rm -rf "$SB"' EXIT
+
+CHECK="$SCRIPTS/check-okf.py"
+ASSESS="$SCRIPTS/assess-okf.py"
+
+echo "-- check-okf.py --"
+"$PYTHON" "$CHECK" --help >/dev/null 2>&1; expect_exit "--help" 0 $?
+
+# Conformant bundle: one concept with non-empty type + an index.md
+mkdir -p "$SB/good"
+cat > "$SB/good/a.md" <<'EOF'
+---
+type: dataset
+title: A
+---
+# A
+body
+EOF
+printf '# Index\n* [A](a.md) - the a concept\n' > "$SB/good/index.md"
+"$PYTHON" "$CHECK" "$SB/good" >/dev/null 2>&1; expect_exit "conformant -> 0" 0 $?
+
+# --json envelope is valid + schema present
+"$PYTHON" "$CHECK" --json "$SB/good" 2>/dev/null \
+  | "$PYTHON" -c "import sys,json;d=json.load(sys.stdin);assert d['meta']['schema']=='claude-mods.okf-ops.check-okf/v1'" \
+  && ok "check --json envelope schema" || no "check --json envelope schema"
+
+# Missing type -> non-conformant (10)
+mkdir -p "$SB/notype"
+cat > "$SB/notype/a.md" <<'EOF'
+---
+title: no type here
+---
+# A
+EOF
+"$PYTHON" "$CHECK" "$SB/notype" >/dev/null 2>&1; expect_exit "missing type -> 10" 10 $?
+
+# Unparseable frontmatter (malformed YAML) -> 4  [only meaningful with PyYAML;
+# the fallback parser is lenient, so accept 4 OR 10]
+mkdir -p "$SB/bad"
+printf -- '---\nkey: "unterminated\n  - : :\n---\n# X\n' > "$SB/bad/a.md"
+"$PYTHON" "$CHECK" "$SB/bad" >/dev/null 2>&1; rc=$?
+if [[ "$rc" == 4 || "$rc" == 10 ]]; then ok "unparseable frontmatter -> 4/10 (got $rc)"; else no "unparseable frontmatter (want 4 or 10, got $rc)"; fi
+
+# Missing dir -> 3
+"$PYTHON" "$CHECK" "$SB/nope-xyz" >/dev/null 2>&1; expect_exit "missing dir -> 3" 3 $?
+
+echo "-- assess-okf.py --"
+"$PYTHON" "$ASSESS" --help >/dev/null 2>&1; expect_exit "--help" 0 $?
+"$PYTHON" "$ASSESS" "$SB/good" >/dev/null 2>&1; expect_exit "scan -> 0" 0 $?
+"$PYTHON" "$ASSESS" --json "$SB/good" 2>/dev/null \
+  | "$PYTHON" -c "import sys,json;d=json.load(sys.stdin);assert d['meta']['schema']=='claude-mods.okf-ops.assess-okf/v1';assert 'readiness_pct' in d['data']" \
+  && ok "assess --json schema + readiness_pct" || no "assess --json schema + readiness_pct"
+"$PYTHON" "$ASSESS" "$SB/nope-xyz" >/dev/null 2>&1; expect_exit "missing dir -> 3" 3 $?
+
+echo
+echo "=== $pass passed, $fail failed ==="
+[[ "$fail" -eq 0 ]]