-
Notifications
You must be signed in to change notification settings - Fork 2
Design Doc
Current — this is the living version; regenerated at 2026-08-01T22:10:50Z. Historical snapshots are linked from Index-Releases.
Generated against the v0.3.0 tag. Every code claim below cites
path — symbol(), lines N–Mand was read from the tree at6bf5d65. Claims are labelled Confirmed (read in the code), Assumption, Recommendation, or Open Question.
Purpose. Describe what okf-graph-eng is, how it is built, and what a
developer must understand before changing it.
Audience. Contributors to this repository, and plugin users who need to know what the tooling guarantees.
Scope. The plugin surface (.claude-plugin/, skills/, commands/,
agents/, hooks/), the graph engine (scripts/okf-graph.py), the two
auxiliary scripts, the worked example (sample-okf/), and the CI/hook gates that
protect them.
Out of scope. The vendored WikiTicket SDD / worklog tooling under bin/ and
.work/ is project-management plumbing, orthogonal to the plugin's logic. It is
described only where the plugin depends on it (§15, §28).
| Term | Meaning |
|---|---|
| OKF | Open Knowledge Format — knowledge as Markdown files with YAML frontmatter, linked by Markdown links |
| Bundle | A directory tree of OKF Markdown with a root index.md carrying okf_version
|
| Concept | One .md file in a bundle; a node in the graph |
| Typed edge | A link that carries a relation name (depends_on, routes_to, …) rather than a bare hyperlink |
| Knowledge graph | Concepts describing a domain (Dataset, Metric, Reference, …) |
| Agent graph / harness graph | Concepts describing the automation itself (AgentNode, Workflow, SharedState, …) |
| Dual graph | Both of the above in one bundle — the plugin's core premise |
| Progressive disclosure | Shipping a bounded, ranked subgraph ("pack") instead of a whole tree |
| Blast radius / impact | The transitive set of concepts affected by changing one concept |
- Code Walkthrough — the guided tour of the same code
- CLI Reference
- User Guide · Plugin Guide
- CHANGELOG · Roadmap
- Plan: v0.3.0 — fix the plumbing, add the net
What it is. okf-graph-eng is a Claude Code plugin that turns a directory of
Markdown into a queryable graph, so an agent can answer "what breaks if I change
this?" and "what is the smallest set of documents you need to read?" without
loading the whole tree.
Business problem. Agent context is finite and expensive. Documentation trees are neither structured nor bounded. The plugin makes the structure explicit (typed edges), then uses it to bound what gets read.
Two graphs, one bundle. The differentiator is that the same mechanism models
the domain (datasets, metrics, references) and the harness (agents,
workflows, shared state). Asking for the blast radius of an AgentNode is the
same operation as asking for the blast radius of a Table.
Major components.
| Component | What it is |
|---|---|
Seven skills (skills/*/SKILL.md) |
The portable intelligence — Markdown instructions the host model follows |
Seven slash commands (commands/*.md) |
Thin wrappers so a user can type /okf-impact
|
One agent (agents/graph-engineer.md) |
Specialist subagent for multi-hop graph work |
Graph engine (scripts/okf-graph.py, 924 lines) |
Deterministic CLI: eight subcommands over a bundle |
Post-edit hook (hooks/hooks.json → scripts/okf-curate.sh) |
Validates the touched bundle after every Write/Edit/MultiEdit |
sample-okf/ |
22-concept, 83-edge self-describing bundle used as a worked example and a CI tripwire |
tests/test_okf_graph.py |
16 plain-assert cases; the only automated coverage in the repo |
External dependencies. Python 3 and Bash — nothing else at runtime.
Confirmed: scripts/okf-graph.py imports only the standard library
(argparse, html, json, re, sys, collections, dataclasses,
datetime, pathlib, typing — lines 17–26). No package manifest, no lockfile,
no install step.
Key architectural decisions. Package once as a Claude Code plugin and let
Grok Build read it natively (§6.1). Keep Markdown links canonical and treat
frontmatter typed edges as enrichment (§6.2). Ship a hand-rolled frontmatter
parser rather than depend on PyYAML (§6.3). Default the validator to lenient and
gate CI with --strict (§6.4).
Primary risks. The frontmatter parser is a hand-rolled approximation of YAML
and will mis-handle constructs it has never seen (§31 R1). The sample-okf
concept/edge counts are asserted as literals in the test suite, so any
legitimate edit to the sample fails CI until the numbers are updated (§31 R3).
Derived from README.md, CLAUDE.md, docs/plans/2026-08-01-v030-plumbing-and-tests.md,
and the CHANGELOG.md release notes.
| # | Requirement | Where satisfied |
|---|---|---|
| F1 | Compute transitive impact (inbound + outbound) of a concept | scripts/okf-graph.py — cmd_impact(), lines 375–416 |
| F2 | List direct inbound references to a concept | cmd_backlinks(), lines 419–438 |
| F3 | Extract an N-hop neighborhood | cmd_subgraph(), lines 441–486 |
| F4 | Emit a bounded, ranked context pack | cmd_pack(), lines 489–590 |
| F5 | List edges, filterable by source concept and relation | cmd_edges(), lines 593–606 |
| F6 | Render the graph as Mermaid, JSON, or standalone HTML | cmd_graph(), lines 690–761 |
| F7 | Validate structure, links, and graph hygiene | cmd_validate(), lines 764–835 |
| F8 | Report disconnected concepts | cmd_orphans(), lines 838–848 |
| F9 | Support typed edges without breaking plain Markdown links | merge_edges(), lines 251–261 |
| F10 | Bridge worklog items / GitHub issues into the graph as TicketLink concepts |
scripts/okf-ticket-link.py — emit(), lines 114–195 |
| F11 | Curate automatically after an agent edits a bundle |
hooks/hooks.json + scripts/okf-curate.sh
|
| F12 | Work in Claude Code and Grok Build from one package |
.claude-plugin/plugin.json, .grok-plugin/marketplace.json
|
| # | Requirement | Where satisfied |
|---|---|---|
| N1 | Zero runtime dependencies beyond python3 + bash
|
stdlib-only imports, scripts/okf-graph.py:17–26
|
| N2 | Portable across macOS and Linux without coreutils |
okf-curate.sh fallback uses okf-graph.py, not realpath -m (lines 71–76) |
| N3 | Every subcommand is scriptable | JSON on stdout for six of eight; graph prints the artifact by design (cmd_graph() docstring, lines 691–699) |
| N4 | HTML output must open offline and inside a locked-down viewer |
render_html(), lines 609–687; asserted by tests/test_okf_graph.py — test_graph_html_is_self_contained(), lines 201–214
|
| N5 | The graph engine is covered by tests that run in CI |
.github/workflows/worklog.yml, "graph engine tests" step |
| N6 | The plugin version cannot drift across its four manifests | test_version_is_consistent_across_manifests(), lines 238–255 |
| N7 | Skills must keep working when the validator emits warnings |
cmd_validate() returns 0 on warnings unless --strict, lines 833–835 |
Omitted NFR classes: availability, scalability, disaster recovery, data retention and compliance — there is no service, no tenancy, and no hosted data. The tool is a local CLI over files in a git repository.
Actors.
-
Plugin user — a developer in Claude Code or Grok Build who types
/okf-impactor asks a question that triggers a skill. -
Host model — Claude Code / Grok Build. It reads
SKILL.mdfiles and decides when to shell out to the CLI. - Post-edit hook — the host, acting automatically after each file write.
- CI — GitHub Actions, running the same checks non-bypassably.
Trust boundaries. One: the local filesystem. The graph engine reads *.md
under a bundle root and writes nothing. The only outbound network call in the
repository is scripts/substack_okf.py — http_get(), lines 77–97, and that
script is a local integration harness, not part of the plugin surface (§20).
flowchart TB
User["Plugin user"]
subgraph Host["Host — Claude Code / Grok Build"]
Model["Host model"]
Skills["skills/*/SKILL.md<br/>(7 skills)"]
Cmds["commands/*.md<br/>(7 slash commands)"]
Agent["agents/graph-engineer.md"]
Hook["PostToolUse hook"]
end
subgraph Local["Local filesystem — the only trust boundary"]
Graph["scripts/okf-graph.py<br/>(graph engine)"]
Curate["scripts/okf-curate.sh"]
Ticket["scripts/okf-ticket-link.py"]
Bundle[("OKF bundle<br/>*.md + frontmatter")]
Work[(".work/*.jsonl<br/>worklog event log")]
end
CI["GitHub Actions<br/>worklog-invariants"]
User -->|"slash command or<br/>natural language"| Model
Model --> Cmds --> Skills
Model --> Agent
Skills -->|"prefer okf/okfcli,<br/>else python3"| Graph
Hook -->|"stdin JSON:<br/>.tool_input.file_path"| Curate
Curate -->|"fallback validator"| Graph
Graph -->|"read only"| Bundle
Ticket -->|"writes TicketLink concepts"| Bundle
Ticket -.->|"reads worklog fold"| Work
CI --> Graph
CI --> Work
How to read it. Everything above the Local box is instructions the host
model interprets; everything inside it is deterministic code. The plugin's
working rule (CLAUDE.md, "Deterministic tools first") is exactly this boundary:
prose decides when, code decides what.
Failure behavior. If okf/okfcli is absent the skills fall back to
python3 scripts/okf-graph.py (skills/okf-impact/SKILL.md and siblings). If
okf-curate.sh cannot find a bundle root it prints a note and exits 0
(scripts/okf-curate.sh:57–60) — curation never blocks an edit.
flowchart LR
subgraph Intelligence["Portable intelligence (Markdown)"]
S1["okf-init-graph"]
S2["okf-author"]
S3["okf-impact"]
S4["okf-query"]
S5["okf-maintain"]
S6["okf-validate"]
S7["okf-visualize"]
end
subgraph Engine["Graph engine — scripts/okf-graph.py"]
Load["load_bundle()<br/>lines 264–293"]
Parse["parse_frontmatter()<br/>lines 72–156"]
LinkX["extract_markdown_links()<br/>extract_frontmatter_links()<br/>lines 220–248"]
Merge["merge_edges()<br/>lines 251–261"]
BFS["bfs_closure()<br/>lines 318–332"]
Render["mermaid_id() / render_mermaid()<br/>lines 159–187"]
HTML["render_html()<br/>lines 609–687"]
end
subgraph Cmds["Eight subcommands"]
C1["impact"]; C2["backlinks"]; C3["subgraph"]; C4["pack"]
C5["edges"]; C6["graph"]; C7["validate"]; C8["orphans"]
end
S3 --> C1; S4 --> C3; S4 --> C4; S6 --> C7; S5 --> C7; S5 --> C8; S7 --> C6; S2 --> C5
Load --> Parse --> LinkX --> Merge
C1 --> BFS; C3 --> BFS; C4 --> BFS; C6 --> BFS
C4 --> Render; C6 --> Render; C6 --> HTML
How to read it. Every subcommand goes through the same ingest pipeline —
load_bundle() is called first in all eight (cmd_impact():376, cmd_backlinks():420,
cmd_subgraph():442, cmd_pack():496, cmd_edges():594, cmd_graph():700,
cmd_validate():765, cmd_orphans():839). There is no cache and no incremental
mode: each invocation re-reads the bundle from disk. For a bundle of a few
hundred files this is milliseconds; see §31 R5.
Confirmed: render_mermaid() is shared by exactly two callers — cmd_pack()
line 569 and cmd_graph() line 745. It was extracted in v0.3.0 so the pack
diagram and the standalone graph diagram cannot drift apart.
flowchart TD
A["bundle.rglob('*.md')<br/>sorted, dotfiles skipped<br/>load_bundle():266–268"]
B["read_text(errors='replace')<br/>line 270"]
C["parse_frontmatter(text)<br/>→ meta dict"]
D["extract_markdown_links()<br/>regex over body + frontmatter text"]
E["extract_frontmatter_links()<br/>from meta['links']"]
F["merge_edges()<br/>typed rel beats links_to"]
G["Concept(path, rel, title, type,<br/>status, verified, tags, meta,<br/>outbound, edges)"]
H["outbound filtered to loaded concepts<br/>lines 291–292"]
I["edges kept unfiltered<br/>→ validate can see broken links"]
J["build_inbound()<br/>reverse adjacency"]
K["subcommand"]
A --> B --> C --> D --> F
C --> E --> F --> G --> H --> J --> K
G --> I --> K
The load-bearing subtlety. Concept.outbound is pruned to targets that
actually exist, but Concept.edges is not (load_bundle():288–292). Traversal
therefore never walks into a void, while cmd_validate() can still report the
dangling target as a broken link (cmd_validate():776–778). Conflating the two
lists would either crash the BFS or blind the validator.
There is no runtime to deploy. Installation is a marketplace entry pointing at
this repository (marketplace.json, .claude-plugin/marketplace.json,
.grok-plugin/marketplace.json). The host clones or links the tree and resolves
intra-plugin paths through ${CLAUDE_PLUGIN_ROOT} — used by hooks/hooks.json:9
and by every commands/*.md file (v0.3.0 fixed bare relative paths that did not
resolve from a consuming project; see CHANGELOG "Fixed").
Decision. Ship a single Claude Code plugin; do not maintain Grok-specific
packaging beyond a thin marketplace pin.
Context. Grok Build reads Claude plugins, skills, agents, and hooks natively.
Alternatives. Separate Grok package (drift risk); Grok-only features (breaks
Claude).
Consequences. One install path, two hosts. No feature may depend on a
Grok-only capability.
Recorded as an in-repo ADR: sample-okf/decisions/single-claude-plugin.md
(status: accepted, verified: true). docs/adr/ exists but is empty — the
decision records live in the sample bundle, which is itself the plugin's
self-description.
Decision. A plain [Title](/path.md) link is a real edge with relation
links_to. Frontmatter links: [{target, rel}] entries add a relation name.
Implementation. merge_edges(), lines 251–261 — frontmatter wins for the
same target, so a concept can upgrade a prose link to depends_on without
duplicating it.
Rationale. A bundle stays readable and useful in any Markdown viewer; typing
is optional and additive.
Tradeoff. Two edge sources means two parsers and a merge rule to keep
correct. Covered by test_merge_edges_frontmatter_wins(), lines 89–93.
Revisit if: relations ever need attributes beyond a name (weight, direction,
validity window) — a dict-per-edge merge would no longer suffice.
Decision. Parse frontmatter with a line-oriented state machine
(parse_frontmatter(), lines 72–156).
Rationale. PyYAML is not in the standard library. Requiring it would turn a
copy-the-file plugin into a package with an install step, on two hosts.
Tradeoff. It is not YAML. It handles scalars, booleans, inline lists
([a, b]), block sequences (- a under a bare key), and the links: list of
mappings — and nothing else. Nested maps, multi-line scalars, anchors, and
comments-after-values are unsupported.
History. v0.2.0 shipped without block-sequence support: tags: followed by
- item returned '', which load_bundle()'s isinstance guard (line 282)
then coerced to [] with no warning. Every generator in this repo emits inline
lists, so sample-okf looked clean and only user-authored bundles were affected.
Fixed in v0.3.0 by the pending_list_key mechanism (lines 83, 121–130) and
pinned by test_frontmatter_block_sequence(), lines 47–56.
Revisit if: users start hitting the parser's edges. The honest upgrade is an
optional PyYAML path with this parser as the fallback.
Decision. validate exits 0 on warnings; --strict makes warnings
non-zero (cmd_validate(), lines 833–835).
Rationale. The skills call validate mid-conversation and treat non-zero as
failure. A bundle with an unverified AgentNode is not broken — it is
in-progress. CI, however, needs warnings to actually gate.
Consequence. The one place strictness is applied is
.github/workflows/worklog.yml ("sample bundle stays valid"). The post-edit hook
deliberately uses the lenient default (okf-curate.sh:75).
Pinned by: test_strict_validate_flags_warnings(), lines 217–235.
Decision. mermaid_id(rel) sanitizes the whole path, not the stem
(lines 159–168).
Context. v0.2.0 derived ids from Path(rel).stem, so all seven index.md
files in sample-okf collapsed into one node and agents/foo.md merged with
docs/foo.md.
Rationale. Path is the concept's identity everywhere else in the engine
(dict keys in load_bundle(), edge endpoints, resolve_concept() output). The
renderer had been the only component using a different identity.
Pinned by: test_mermaid_ids_are_unique_per_path(), lines 96–108.
Decision. --format mermaid|html write the raw artifact to stdout;
--format json writes JSON.
Rationale, quoted from cmd_graph() lines 694–698: "a rendered graph is the
only product here, so wrapping it would force every caller through jq -r
before it could be pasted into a doc or written to a file."
Contrast with pack, whose JSON envelope carries included / excluded /
edges alongside its markdown byproduct (cmd_pack():580–588) — there the
structured data is the point and the Markdown is a convenience.
Tradeoff. One subcommand breaks the "always JSON" uniformity. Callers must
know which mode they are in. Documented in the CLI reference and in --help.
Decision. render_html() emits one file with inlined CSS, no <script>, no
src=, no @import, no url(), no http(s):// (lines 609–687).
Rationale, quoted from the docstring (lines 616–621): "No CDN, no JS, no
network fetches — the file must open from disk and from a locked-down viewer.
Renderers that understand pre.mermaid draw the diagram; everywhere else the
tables carry the same information."
Consequence. The diagram degrades to two tables rather than to nothing. This
is a security property as much as a portability one (§22).
Pinned by: test_graph_html_is_self_contained(), lines 201–214, which
asserts each forbidden token is absent.
| Component | Type | Responsibility | Inputs | Outputs | Depends on | Failure impact |
|---|---|---|---|---|---|---|
skills/*/SKILL.md (7) |
Markdown instructions | Tell the host model when and how to do graph work | User intent | Tool calls, prose | Graph engine (optional) | Model falls back to ad-hoc reasoning |
commands/*.md (7) |
Slash-command wrappers | Give each skill an explicit entry point | $ARGUMENTS |
Delegation to a skill | ${CLAUDE_PLUGIN_ROOT} |
Slash command unavailable; skill still auto-triggers |
agents/graph-engineer.md |
Subagent definition | Multi-hop graph work in an isolated context | Task prompt | Report | Skills + engine | Main thread does the work inline |
scripts/okf-graph.py |
Python CLI, 924 lines | All deterministic graph operations | Bundle path, concept path, flags | JSON / Mermaid / HTML | stdlib only | Every skill degrades to manual file crawling |
scripts/okf-curate.sh |
Bash, 78 lines | Post-edit validation of the touched bundle |
$1 or PostToolUse stdin JSON |
stdout report |
python3, okf-graph.py
|
Silent drift after agent edits |
scripts/okf-ticket-link.py |
Python CLI, 220 lines | Render worklog items / GitHub issues as TicketLink concepts |
worklog fold JSON or flags |
<bundle>/tickets/*.md + index |
worklog (optional) | Tickets absent from the graph |
scripts/substack_okf.py |
Python CLI, 917 lines | Local end-to-end integration harness (§20) | Substack archive API | Gitignored bundle under integration/
|
network, okf-graph.py
|
No end-to-end signal; unit tests unaffected |
hooks/hooks.json |
Host hook config | Bind PostToolUse → curate | Write/Edit/MultiEdit events | Command invocation | ${CLAUDE_PLUGIN_ROOT} |
Curation never runs (this was the v0.2.0 defect) |
sample-okf/ |
22 concepts, 83 edges | Worked example and CI drift tripwire | — | — | — | Demos break; CI fails loudly |
tests/test_okf_graph.py |
16 plain-assert cases | Regression net for the engine | — | Exit code |
okf-graph.py, sample-okf, 4 manifests |
Regressions ship silently (the v0.2.0 condition) |
hooks/pre-commit, hooks/commit-msg
|
Git hooks (vendored worklog) | Log invariants, roadmap freshness, graph tests, ULID in message | Staged tree | Exit code |
bin/, python3
|
Bad commits reach the branch; CI still catches them |
Trigger. User asks "what depends on X?" or is about to edit a high-impact
concept. CLAUDE.md working rule 2 makes this mandatory before structural edits
to AgentNode, Workflow, or SharedState.
sequenceDiagram
actor User
participant Model as Host model
participant Skill as skills/okf-impact/SKILL.md
participant CLI as okf-graph.py impact
participant FS as Bundle (*.md)
User->>Model: "what breaks if I change the graph-engineer agent?"
Model->>Skill: auto-trigger on "blast radius / depends on"
Skill->>CLI: python3 okf-graph.py impact sample-okf agents/graph-engineer.md
CLI->>FS: rglob("*.md"), read each
FS-->>CLI: 22 concepts
CLI->>CLI: resolve_concept() → exact, stem, or suffix match
alt not found
CLI-->>Skill: error payload, exit 1
Skill-->>User: ask for a valid path
else found
CLI->>CLI: bfs_closure over inbound + outbound
CLI->>CLI: enrich_nodes() → criticality, sort
CLI-->>Skill: JSON: inbound, outbound, direct_edges, suggested_order, stats
Skill-->>Model: rank + narrate
Model-->>User: affected concepts in update order
end
Main flow (cited). cmd_impact(), lines 375–416: load (376) → build reverse
adjacency (377) → resolve (379) → BFS both directions (383–384) → collect direct
typed edges in and out (386–395) → emit payload (396–415).
Failure flows. Unresolvable concept → {"error": …} and exit 1 (380–382).
Broken outbound links are invisible here: outbound was pruned at load
(load_bundle():291–292), which is why validate is the tool that reports them.
Idempotency / state. Read-only. No writes, no retries, no timeouts.
suggested_order is inbound-only (line 407) — the things that reference
the target, ordered by criticality then depth then title (enrich_nodes():362–363).
Callers wanting a full ordering must merge in outbound themselves. Confirmed
by reading the field construction; the field name does not make this obvious.
Trigger. A long-running agent needs context on a concept without reading the
tree. /okf-query or the okf-query skill.
flowchart TD
A["pack bundle concept<br/>--hops 2 --max-nodes 20"] --> B["load_bundle()"]
B --> C{"--undirected?"}
C -->|no, default| D["outbound-only adjacency<br/>cmd_pack():512"]
C -->|yes| E["symmetric adjacency<br/>cmd_pack():504–510"]
D --> F["bfs_closure(hops)"]
E --> F
F --> G["score(): root first,<br/>verified first,<br/>high-impact first,<br/>then title<br/>lines 518–528"]
G --> H["included = first max_nodes of ranked<br/>excluded = the rest"]
H --> I["edges among included only<br/>lines 534–538"]
I --> J["read_order: root, high-impact,<br/>SharedState, title<br/>lines 542–549"]
J --> K["Markdown pack + Mermaid<br/>via render_mermaid()"]
K --> L["JSON envelope: included,<br/>excluded, edges, markdown"]
Why outbound-only is the default. Quoted from cmd_pack() lines 491–494:
"Outbound-only keeps packs inside a theme (e.g. group → members) instead of
flooding through hub catalogs that link everything." A bundle's index.md links
to everything; an undirected 2-hop walk from any leaf reaches the index and then
the entire bundle. --undirected exists for deliberate neighborhood exploration
(main():870–874).
Two different orderings, deliberately. score() (518–528) decides what
survives the cut; read_order (542–549) decides what order the survivor list is
presented in, and additionally promotes SharedState. They are not the same
sort and should not be collapsed.
Unverified high-impact concepts are flagged inline in the Markdown with
⚠ unverified high-impact (lines 563–566) rather than dropped — the reader is
told the node is untrusted, not denied it.
Truncation is disclosed, never silent: up to 15 excluded titles are listed, then "… and N more" (lines 572–577).
Trigger. /okf-visualize, or a request for a diagram or an HTML map.
cmd_graph(), lines 690–761. Without --focus, nodes are every concept
(sorted(concepts), line 702). With --focus, an undirected neighborhood BFS
bounded by --hops (704–715). Edges are then restricted to pairs where both
endpoints survived (line 718).
The isolated-node repair. render_mermaid() only declares nodes it sees on
an edge. cmd_graph() therefore appends a node line for every unlinked concept
(lines 746–752), with the reason in the comment: "isolated concepts would
otherwise disappear from the diagram but stay in the JSON view." This was a
v0.3.0 fix; test_graph_mermaid_is_a_fenced_block(), lines 151–159 asserts every
node from the JSON view appears in the Mermaid view.
--hops is reported as null when there is no focus (line 726) — hops are
meaningless for a whole-bundle render, and test_graph_json_shape(), line 165
asserts it.
sequenceDiagram
participant Host as Claude Code
participant Hook as okf-curate.sh
participant Py as okf-graph.py validate
Host->>Hook: PostToolUse (Write|Edit|MultiEdit), payload on stdin
Hook->>Hook: FILE from $1, else tool_input.file_path on stdin
alt FILE empty
Hook-->>Host: exit 0, silent
else path not OKF-shaped
Note over Hook: case filter on .okf/, knowledge/, sample-okf/
Hook-->>Host: exit 0, silent
else
Hook->>Hook: find_bundle_root walks up for index.md with okf_version,<br/>then .okf/, then repo fallbacks
alt no root
Hook-->>Host: "no OKF bundle root found — skipping", exit 0
else okf / okfcli present
Hook->>Hook: okf validate (+ okf lint if supported)
else
Hook->>Py: python3 okf-graph.py validate BUNDLE_ROOT
Py-->>Hook: JSON report
end
Hook-->>Host: report, exit 0 always
end
The v0.2.0 defect, in full. hooks/hooks.json passed "$FILE_PATH", but the
host delivers the PostToolUse payload as JSON on stdin — there is no such
environment variable. okf-curate.sh bound an empty string and hit its first
guard on every edit. The hook had never run in any install. It now reads
.tool_input.file_path from stdin (okf-curate.sh:9–16), and the matcher covers
MultiEdit, which previously bypassed curation entirely (hooks/hooks.json:5).
Why python3 and not jq — stated in the comment at lines 6–8: "the plugin
already requires python3 everywhere, jq is not guaranteed present."
The hook never fails a build. Every branch exits 0, and each validator call
is suffixed || true (lines 65, 67, 70, 75). Curation reports; it does not block.
Limitation (Confirmed). The path filter at lines 22–25 keys on .okf/,
knowledge/, or sample-okf/ in the path. A bundle rooted elsewhere — say
integration/substack-okf/index.md — is skipped unless the edited file happens to
sit under a knowledge/ subdirectory. See §31 R4.
scripts/okf-ticket-link.py — emit(), lines 114–195. Reads bin/worklog fold
JSON from stdin (or --worklog-fold, or a single --id), maps worklog status to
ticket status (status_map(), lines 32–39), renders one TicketLink concept per
item into <bundle>/tickets/<slug>.md, and rewrites tickets/index.md including
pre-existing entries it did not just write (lines 184–191). --dry-run reports
paths without writing (159–161); --open-only skips done and cancelled (125–126).
Idempotency. Re-running overwrites by slug. A retitled item produces a new file and leaves the old one — Confirmed by reading lines 147–163, which derive the filename from the current title with no cleanup of prior slugs.
This is the most intricate logic in the repository and the source of a shipped defect, so it gets a state diagram.
stateDiagram-v2
[*] --> NoMatch: no leading fence
NoMatch --> [*]: return empty dict
[*] --> Normal: frontmatter fence matched
Normal --> InLinks: bare links key
Normal --> PendingList: key with empty value, arm pending_list_key
Normal --> Normal: key with scalar, bool, or inline list value
Normal --> Normal: blank or comment line, skip
PendingList --> PendingList: list item, append to the pending key
PendingList --> Normal: any other line, disarm and re-handle as key
InLinks --> InLinks: list item, flush previous and start new mapping
InLinks --> InLinks: indented key, add field to current mapping
InLinks --> Normal: unindented key, flush current and leave block
Normal --> [*]: end of block, flush and attach links
InLinks --> [*]: end of block, flush and attach links
Decision table — value coercion (parse_frontmatter(), lines 132–150):
Input after key:
|
Result | Line |
|---|---|---|
| (empty) |
"", and pending_list_key = key
|
137–140 |
true / false (any case) |
Python bool
|
142–143 |
[a, b] |
list[str], quotes and whitespace stripped |
144–148 |
[] |
[] |
146–147 |
| anything else |
str, outer " or ' stripped |
149–150 |
Edge cases.
- A bare key followed by no list keeps
""(comment, line 129: "no list materialized; the bare key keeps its empty-string value").load_bundle()'sisinstance(..., list)guard at line 282 then yields[]fortags. Confirmed — and this is precisely the path that hid the v0.2.0 bug. - A line with no
:outside a list context is skipped entirely (line 132–133). - Inside
links:, a list item whose text starts with{is not split on:(line 104) — flow-style mappings are recognized as "not my problem" rather than mis-parsed into a wrong key. - The
linkskey only appears in the returned dict if at least one item was collected (lines 154–155).
Invalid input is not reported. The parser has no error channel; malformed
frontmatter degrades to a partial dict. cmd_validate() catches the consequences
(missing title, missing type) but never the cause. See §31 R1.
criticality_of(), lines 335–343:
| Type in | Verified | Result |
|---|---|---|
HIGH_IMPACT_TYPES (AgentNode, Workflow, Harness, SharedState) |
true | high |
HIGH_IMPACT_TYPES |
false | critical |
MEDIUM_IMPACT_TYPES (Dataset, Table, Metric, API, ToolCapability) |
either | medium |
| anything else | either | low |
Confirmed dead branch. Line 342 reads
criticality = "critical" if criticality == "high" else criticality. Inside the
criticality != "low" guard the only other reachable value is "medium", for
which the expression assigns the variable to itself. Medium-impact concepts never
escalate on being unverified. Whether that is the intent is an Open Question
(§34 Q2); the code as written is a no-op, not a bug that changes output.
Sort order (enrich_nodes(), lines 362–363): criticality rank
(critical 0, high 1, medium 2, low 3), then BFS depth, then title. Unknown
criticality sorts last via the 9 default.
_normalize_target(), lines 190–217 — every edge target passes through here.
| Input | Behavior | Line |
|---|---|---|
#anchor suffix |
stripped before resolution | 191 |
empty, http…, mailto:
|
not an edge → None
|
192–193 |
/abs/path.md |
resolved against the bundle root | 194–195 |
relative.md |
resolved against the source file's directory | 196–197 |
| a directory | rewritten to <dir>/index.md when that file exists |
198–201 |
| extension-less non-file | tried as <path>/index.md, then <path>.md
|
202–210 |
| resolves outside the bundle |
None (the ValueError branch) |
211–214 |
Invariant: a target that escapes the bundle root is not an edge. relative_to()
raising ValueError is the enforcement (lines 211–214), pinned by
test_normalize_target(), lines 72–86 ("../../elsewhere.md" → None).
classDiagram
class Concept {
+Path path
+str rel
+str title
+str type
+str status
+bool verified
+list~str~ tags
+dict meta
+list~str~ outbound
+list~TypedEdge~ edges
}
class TypedEdge {
+str target
+str rel
+str source
}
Concept "1" --> "0..*" TypedEdge : edges
Concept — scripts/okf-graph.py, lines 58–69. One per .md file.
rel (the bundle-relative POSIX path) is the identity used as the dict key in
load_bundle(), as both endpoints of every edge, and as the input to
mermaid_id(). Invariant: rel is the only identity; title and stem are
convenience lookups in resolve_concept() and must never become identity.
Defaults on load (load_bundle(), lines 275–286):
| Field | Fallback |
|---|---|
title |
meta["title"], else the file stem |
type |
meta["type"], else "Index" for index.md, else "Unknown"
|
verified |
bool(meta.get("verified", False)) — absent means untrusted |
tags |
meta["tags"] only if it is already a list, else []
|
TypedEdge — lines 51–55. source records provenance (markdown or
frontmatter) and is what merge_edges() and cmd_validate() reason over.
Concept types. Not an enum in code — type is a free string. The two frozen
sets that carry behavior are HIGH_IMPACT_TYPES and MEDIUM_IMPACT_TYPES
(lines 47–48). The wider vocabulary lives in the skills and templates:
| Kind | Types |
|---|---|
| Knowledge | Dataset, Table, Metric, Playbook, Runbook, API, Reference |
| Harness | AgentNode, Workflow, Harness, DecisionRecord, SharedState, ToolCapability, TicketLink |
| Structural | Index (auto-assigned to index.md) |
Relations. KNOWN_RELS (lines 32–45) holds ten: depends_on, routes_to,
implements, documents, uses, owns, supersedes, related_to, tracks,
maps_to. An unlisted relation is kept, not rewritten —
extract_frontmatter_links() line 242 only substitutes related_to when the
value is empty — and cmd_validate() then reports it at severity info as
"non-standard rel … (allowed but uncommon)" (lines 779–786). The vocabulary is
advisory by design.
scripts/okf-graph.py is one flat module, deliberately: it is a single file a
user can copy next to a bundle. Its internal layering is nonetheless strict, and
there are no cycles.
flowchart TD
subgraph L1["Layer 1 — parsing"]
PF["parse_frontmatter()"]
NT["_normalize_target()"]
EML["extract_markdown_links()"]
EFL["extract_frontmatter_links()"]
ME["merge_edges()"]
end
subgraph L2["Layer 2 — model"]
LB["load_bundle()"]
BI["build_inbound()"]
RC["resolve_concept()"]
EI["edge_index()"]
end
subgraph L3["Layer 3 — algorithms"]
BFS["bfs_closure()"]
CR["criticality_of()"]
EN["enrich_nodes()"]
end
subgraph L4["Layer 4 — rendering"]
MI["mermaid_id()"]
RM["render_mermaid()"]
RH["render_html()"]
end
subgraph L5["Layer 5 — subcommands"]
CMDS["cmd_impact · cmd_backlinks · cmd_subgraph<br/>cmd_pack · cmd_edges · cmd_graph<br/>cmd_validate · cmd_orphans"]
end
MAIN["main() — argparse"]
EML --> NT
EFL --> NT
LB --> PF
LB --> EML
LB --> EFL
LB --> ME
EN --> CR
RM --> MI
L5 --> L2
L5 --> L3
L5 --> L4
MAIN --> L5
Confirmed: no circular dependencies. Every arrow points down a layer.
render_html() takes already-rendered Mermaid lines as a parameter (line 614)
rather than calling render_mermaid() itself, which keeps Layer 4 internally
acyclic and lets cmd_graph() apply its isolated-node repair before handing
lines to either renderer.
Extension points.
- New subcommand — add a
cmd_*function plus a subparser inmain()(lines 851–920) and a dispatch line (904–919). No other file changes. - New relation — add to
KNOWN_RELS(lines 32–45). Anything not listed still works, it just gets aninfonote fromvalidate. - New impact type — add to
HIGH_IMPACT_TYPES/MEDIUM_IMPACT_TYPES(lines 47–48). This changes pack ranking, criticality, and validate warnings in one edit. - New output format — extend
--formatchoices (line 883) and branch incmd_graph()(720–760). - New skill — a directory under
skills/with aSKILL.md, plus a matchingcommands/*.md. The seven-and-seven parity is a convention, not enforced.
Coupling risks. cmd_pack() (102 lines) and cmd_graph() (72 lines) both
do selection, ranking, and rendering inline. They share render_mermaid() and
bfs_closure() but each carries its own adjacency construction —
cmd_subgraph():449–458, cmd_pack():503–512, cmd_graph():709–714 are three
separate hand-built undirected maps. Recommendation: extract one
undirected_adjacency(concepts) helper if a fourth caller appears. Two of the
three are already provably redundant in their construction: cmd_subgraph()
adds edges from outbound_map in both directions (450–453) and from
inbound_map in both directions (454–457), which is the same edge set twice —
harmless only because line 458 deduplicates with sorted(set(v)).
One subcommand, emit. Bridges WikiTicket SDD / worklog items and GitHub issues
into the graph as TicketLink concepts. Rendering is a single f-string template
(render_ticket(), lines 42–91) that always emits verified: true and a
documents edge to /knowledge/okf-conventions.md. GitHub URLs are synthesized
only when the external key is all digits (emit():141–146).
The repository's end-to-end integration harness, and — until this document — its
least-documented component. It is not part of the plugin surface: no skill,
command, or hook references it, and its output tree is gitignored
(.gitignore: integration/, integration-okf/).
What it does. Fetches the archive of a Substack publication, classifies each post by type and subject with deterministic regex rules, emits a complete OKF v0.2 bundle from the result, then runs the plugin's own CLI against that bundle and asserts the answers are sane. It is the only test in the repository that exercises the engine against a bundle it did not hand-author.
| Subcommand | Function | Behavior |
|---|---|---|
fetch |
cmd_fetch(), lines 221–262 |
Pages /api/v1/archive 50 at a time to --limit; snapshots /feed; writes articles.json + taxonomy.json
|
classify |
cmd_classify(), lines 265–290 |
Re-runs the taxonomy over cached articles.json without refetching |
emit |
cmd_emit(), lines 452–717 |
Writes the bundle: article concepts, four type hubs, N subject hubs, knowledge/agents/workflows indexes, root index.md, log.md
|
verify |
cmd_verify(), lines 737–820 |
Shells out to okf-graph.py and asserts the results |
run |
cmd_run(), lines 863–873 |
fetch → emit → verify
|
Taxonomy. Four article types (ARTICLE_TYPES, line 32: news, tutorial,
guide, one-off) assigned by ordered regex in classify_type(), lines 134–151,
falling through to one-off. Twelve subject rules (SUBJECT_RULES, lines 34–47)
are all-match, not first-match (classify_subjects(), lines 154–160), defaulting
to ["general"]. Both are pure functions of title + subtitle, so classification
is reproducible from cached JSON with no network.
What verify asserts (lines 737–820) — this is the integration contract:
-
articles.jsonexists and holds at leastmin_countarticles. - Every article has a type in
ARTICLE_TYPESand a non-empty subject list. - The bundle exists and has a root
index.md. - The count of files in
knowledge/articles/equals the count in JSON. -
okf-graph.py validateexits 0 witherror_count == 0. -
impacton the largest non-ai-newssubject hub returns at least one neighbor underknowledge/articles/. -
pack --hops 2 --max-nodes 30on that hub returns at least 2 nodes. -
orphansruns (reported, not asserted).
Network resilience. http_get(), lines 77–97 tries urllib with a custom
User-Agent, then falls back to a curl -sL subprocess. The comment at line 78
gives the reason: "Substack often 403s". The /feed snapshot is best-effort and
its failure is caught and printed, not raised (lines 229–233).
Emit is destructive by design. cmd_emit() unlinks every *.md under the
target bundle before writing (lines 470–473), so a shrinking article set cannot
leave stale concepts behind. It deletes only *.md, leaving the raw JSON
alongside. Confirmed — and the reason the output path is gitignored and
defaulted to integration/ (line 29) rather than anywhere a user keeps work.
All four type hubs are always emitted, even when empty (lines 488–490), "so catalogs are stable" — an empty hub renders a placeholder bullet rather than vanishing from the index and breaking inbound links.
All eight subcommands take a bundle path as the first positional argument.
main() resolves it and exits 1 with {"error": "bundle not found: …"} if it is
not a directory (lines 899–902).
| Subcommand | Args | Output | Exit 0 | Exit 1 |
|---|---|---|---|---|
impact |
<bundle> <concept> |
JSON: target, inbound, outbound, direct_edges, suggested_order, stats | always when resolved | concept not found |
backlinks |
<bundle> <concept> |
JSON: target, backlinks[] with rels
|
resolved | concept not found |
subgraph |
<bundle> <concept> [--hops 2] |
JSON: root, hops, nodes, edges | resolved | concept not found |
pack |
<bundle> <concept> [--hops 2] [--max-nodes 20] [--undirected] |
JSON: root, hops, max_nodes, included, excluded, edges, markdown | resolved | concept not found |
edges |
<bundle> [--from PATH] [--rel REL] |
JSON: edges, count, typed_count | always |
--from not found |
graph |
<bundle> [--format mermaid|json|html] [--focus PATH] [--hops 2] |
raw artifact for mermaid/html; JSON for json | always |
--focus not found |
validate |
<bundle> [--strict] |
JSON: concept_count, edge_count, issues[], error_count, warn_count, strict | no errors (and no warnings under --strict) |
any error; any warning under --strict
|
orphans |
<bundle> |
JSON: orphans[], count | always | — |
Concept resolution (resolve_concept(), lines 305–315) accepts, in order:
the exact bundle-relative path; a leading / (stripped); a case-insensitive file
stem; a case-insensitive title; or any path suffix, with or without .md. First
match wins in dict iteration order — Open Question Q3: ambiguous stems
resolve non-obviously.
Validation rules (cmd_validate(), lines 764–835):
| Severity | Condition | Line |
|---|---|---|
| error | bundle has no root index.md
|
767–768 |
| error | edge target is not a loaded concept (broken link) | 776–778 |
| warn | non-index concept with type "" or Unknown
|
772–773 |
| warn | non-index concept with no title in frontmatter |
774–775 |
| warn |
TicketLink with neither external_id nor worklog_id
|
788–796 |
| warn | concept in HIGH_IMPACT_TYPES with verified: false
|
808–816 |
| info | frontmatter relation outside KNOWN_RELS
|
779–786 |
| info | orphan — no inbound and no outbound links | 797–807 |
index.md and log.md are exempt from the per-concept checks (line 770), and
any file named index.md at any depth is exempt from the type and title warnings
(lines 772, 774).
Exit-code contract. return 1 if errors or (strict and warnings) else 0
(line 835). The comment above it states the constraint explicitly: "the skills
call validate and expect 0 on warnings."
The plugin writes nothing. Two stores exist in the repository and both belong to the vendored worklog tooling:
| Store | Format | Owner | Notes |
|---|---|---|---|
.work/todo.jsonl, .work/done.jsonl
|
Append-only JSONL event log | bin/worklog |
Union-merge friendly. Never hand-edited (CLAUDE.md policy). hooks/pre-commit enforces a trailing newline and a per-event schema |
docs/.index/ |
Generated JSON + rendered Markdown | worklog ia-* |
Inventory, graph, aliases, publish manifest |
The trailing-newline invariant is the reason hooks/pre-commit exists, and
its own comment says so: without it "union merge fuses the last line of one side
with the first line of the other and you lose two events to one unparseable
line."
Generated files that must never be hand-edited: docs/roadmap.md
(pre-commit regenerates and diffs it), docs/.index/**, and roadmap snapshots
under docs/roadmap/.
Exactly one, and it is not in the plugin surface.
| Service | Used by | Protocol | Auth | Timeout | Failure handling |
|---|---|---|---|---|---|
Substack archive API (<pub>/api/v1/archive, <pub>/feed) |
scripts/substack_okf.py |
HTTPS GET | none | 60 s urllib, 90 s curl | urllib failure falls back to curl; curl failure raises RuntimeError; /feed failure is caught and skipped |
No retries and no backoff (http_get(), lines 77–97). Paging stops on an empty
batch, a short batch, or reaching --limit (fetch_archive(), lines 177–191).
Recommendation: if this harness ever runs in CI, add a cached-fixture mode so
a Substack outage cannot fail the build.
Small surface, but three properties are load-bearing.
| # | Property | Enforcement | Threat if violated |
|---|---|---|---|
| S1 | The HTML map makes no network requests and executes no script |
render_html() inlines all CSS and emits no <script>/src=/@import/url(); test_graph_html_is_self_contained(), lines 201–214 asserts each token is absent |
A generated artifact could exfiltrate or execute when opened; the file is explicitly meant to open in a locked-down viewer |
| S2 | Concept titles and paths are HTML-escaped before rendering |
render_html(), line 622 binds e = html.escape and applies it to every interpolated value (626–635) |
A crafted title: in frontmatter injects markup into the map |
| S3 | Graph traversal cannot escape the bundle root |
_normalize_target(), lines 211–214 returns None on relative_to() failure |
../../ links would pull files outside the bundle into the graph and into packs |
Command execution. okf-curate.sh runs on every Write/Edit/MultiEdit. It
reads a path from host-supplied stdin JSON and uses it in dirname and shell
case matching. It never evals it, and every validator call goes through
command -v guards (lines 64–75). Both python3 invocations use -c with a
fixed program and no interpolation (lines 11–15).
Subprocess use. substack_okf.py calls curl and sys.executable with
argument lists, never shell=True (lines 89–94, 726–727). No injection path
from the fetched content.
Secrets. None in the repository. No credential is read, written, or required
by any script. .gitignore covers .env and .env.*.
Mermaid label escaping (render_mermaid(), line 181): double quotes in a
title are replaced with single quotes before being wrapped in "…". Sufficient
for well-formed titles; a title containing ] or a newline is untested.
Open Question Q4.
| Class | Handling | Cited |
|---|---|---|
| Concept not resolvable |
{"error": …} on stdout, exit 1 |
cmd_impact():380–382 and the same pattern in backlinks, subgraph, pack, edges, graph |
| Bundle path not a directory |
{"error": "bundle not found: …"}, exit 1 |
main():900–902 |
| Unreadable bytes in a file |
read_text(errors="replace") — never raises |
load_bundle():270 |
| Broken link | Reported as a validation error; excluded from traversal |
load_bundle():291–292, cmd_validate():776–778
|
| Malformed frontmatter | Silently partial — no error channel |
parse_frontmatter() throughout |
| Curation cannot find a bundle | Message, exit 0 | okf-curate.sh:57–60 |
| External CLI missing | Fall back to okf-graph.py
|
okf-curate.sh:64–76 |
| Validator non-zero inside the hook | Suppressed with || true
|
okf-curate.sh:65,67,70,75 |
| Substack fetch failure | urllib → curl → RuntimeError
|
substack_okf.py:88–97 |
No retries, no timeouts, no circuit breakers anywhere in the plugin surface — every operation is a local read that either succeeds or fails immediately. This is a deliberate consequence of §6 (stdlib-only, filesystem-only).
Graceful degradation is the recurring pattern: the HTML map degrades to
tables, curation degrades to skipping, the skills degrade to manual crawling, and
validate degrades from gate to advisory. Nothing in the plugin ever hard-fails
a user's edit.
tests/test_okf_graph.py — 16 cases, plain assert, no framework, no fixtures.
Its own docstring (lines 6–9) states the scope: "Kept deliberately small: it
exists to catch the defects that shipped in v0.2.0 … and to stop sample-okf and
the four version manifests from drifting."
| Group | Cases | Boundary |
|---|---|---|
| Frontmatter parsing |
inline_list, block_sequence, block_sequence_then_links, booleans_survive (41–69) |
Pure function, in-process |
| Link handling |
normalize_target (72–86, uses a tempfile bundle), merge_edges_frontmatter_wins (89–93) |
Pure functions |
| Rendering |
mermaid_ids_are_unique_per_path (96–108) |
Pure function |
CLI over sample-okf
|
sample_bundle_validates, pack_mermaid_has_no_collapsed_nodes, graph_mermaid_is_a_fenced_block, graph_json_shape, graph_focus_narrows_the_node_set, graph_focus_unknown_concept_errors, graph_html_is_self_contained (121–214) |
Subprocess, real bundle |
| CLI over a temp bundle |
strict_validate_flags_warnings (217–235) |
Subprocess, synthetic bundle |
| Repo invariants |
version_is_consistent_across_manifests (238–255) |
File reads |
The module-loading trick. load_graph_module(), lines 24–35 imports
okf-graph.py by path — the filename is not a valid identifier. The
sys.modules["okf_graph"] = mod line is load-bearing and the docstring says why:
"without it the @dataclass decorators raise AttributeError on Python 3.13,
because dataclasses looks the class's module up in sys.modules and gets None."
Tripwires, not assertions about behavior. test_sample_bundle_validates()
asserts concept_count == 22 and edge_count == 83 (lines 127–128), with the
rationale in a comment: "sample-okf is the plugin's worked example, and the
skills quote these numbers. A surprise change here means an unreviewed edit."
Verified against the tree at v0.3.0: validate reports exactly 22 / 83, 0 errors,
0 warnings, 0 issues. Of the 83 edges, 13 are typed (edges subcommand).
| Gate | Command | Bypassable |
|---|---|---|
hooks/pre-commit |
python3 tests/test_okf_graph.py -q (guarded on the file existing) |
yes, --no-verify
|
| GitHub Actions "graph engine tests" | python3 tests/test_okf_graph.py -q |
no |
| GitHub Actions "sample bundle stays valid" | python3 scripts/okf-graph.py validate sample-okf --strict |
no |
The CI workflow comment states the intent directly: "The graph engine is what the plugin exists to do; nothing exercised it before v0.3.0."
- No test for
cmd_impact,cmd_backlinks,cmd_subgraph,cmd_edges, orcmd_orphansoutput shape. Five of eight subcommands are exercised only transitively. - No test for
criticality_of()orenrich_nodes()ordering. - No test for
scripts/okf-ticket-link.pyat all. - No test for
scripts/okf-curate.sh— the v0.2.0 hook defect would not be caught today by anything except a human readinghooks.json. -
scripts/substack_okf.pyis an integration harness requiring network; it is not run by CI or by the pre-commit hook.
Prerequisites. python3 and bash. Nothing to install.
# graph engine tests
python3 tests/test_okf_graph.py # verbose
python3 tests/test_okf_graph.py -q # quiet, CI mode
# the same checks CI runs
WORKLOG_SKIP_BRANCH_GUARD=1 hooks/pre-commit
python3 scripts/okf-graph.py validate sample-okf --strict
# exercise the engine
python3 scripts/okf-graph.py impact sample-okf agents/graph-engineer.md
python3 scripts/okf-graph.py pack sample-okf agents/graph-engineer.md --hops 2
python3 scripts/okf-graph.py graph sample-okf --format mermaid
python3 scripts/okf-graph.py graph sample-okf --format html --focus agents/graph-engineer.md > /tmp/map.html
python3 scripts/okf-graph.py edges sample-okf --rel routes_to
python3 scripts/okf-graph.py orphans sample-okf
# ticket bridge
bin/worklog fold | python3 scripts/okf-ticket-link.py emit --bundle sample-okf --open-only --dry-run
# end-to-end integration (network; writes to gitignored integration/)
python3 scripts/substack_okf.py run --limit 20Git hooks. git config core.hooksPath hooks (stated in hooks/pre-commit:3).
Common setup failures.
-
Commit rejected on
main. The branch guard refuses commits authored directly onmain/master(hooks/pre-commit:22–39). Branch first.WORKLOG_SKIP_BRANCH_GUARD=1is only for non-commit callers running the script standalone. -
"docs/roadmap.md is stale or hand-edited". It is generated. Run
worklog roadmap-render(hooks/pre-commit, roadmap block). -
Commit message rejected.
hooks/commit-msgrequires a 26-character ULID or a#123ticket reference. Merge commits are exempt viaMERGE_HEAD. -
__pycache__collisions during merges.hooks/pre-commit:12exportsPYTHONDONTWRITEBYTECODE=1specifically to prevent this.
| # | Item | Area | Probability | Impact | Mitigation | Trigger to act |
|---|---|---|---|---|---|---|
| R1 | The frontmatter parser is not YAML and reports nothing on malformed input | parse_frontmatter() |
high | medium | Four parser tests pin known shapes; validate catches consequences |
A user reports a silently-dropped key |
| R2 | Five of eight subcommands have no direct output-shape test | tests/ |
medium | medium | CI runs what exists | Any change to enrich_nodes() or a payload shape |
| R3 |
sample-okf counts (22 / 83) are asserted as literals |
test_sample_bundle_validates() |
high | low | Deliberate tripwire; the failure message is clear | Every legitimate sample edit must update two numbers |
| R4 | Curation's path filter misses bundles not under .okf/, knowledge/, or sample-okf/
|
okf-curate.sh:22–25 |
medium | medium | Manual validate
|
A user roots a bundle elsewhere and reports no curation |
| R5 | The bundle is fully re-read on every invocation; no cache, no incremental mode | load_bundle() |
low | low | Bundles are small; the cost is milliseconds | A bundle in the thousands of files |
| R6 | Three separate hand-built undirected adjacency maps | subgraph / pack / graph | medium | low | All three are dedup-guarded | A fourth caller appears |
| R7 |
okf-ticket-link.py leaves an orphaned file when an item is retitled |
emit():147–163 |
medium | low |
--dry-run before real runs |
Ticket directories accumulate stale slugs |
| R8 |
substack_okf.py (917 lines) has no test and no cached-fixture mode |
integration | medium | low | Not on any gate | It is ever added to CI |
| R9 |
docs/adr/ is empty while hooks/pre-commit runs worklog adr check on it |
docs | low | low | Decision records live in sample-okf/decisions/
|
Contributors look for ADRs and find nothing |
The build is done; this is the ordered list of what to do next.
| Order | Work | Rationale | Depends on |
|---|---|---|---|
| 1 | Output-shape tests for impact, backlinks, subgraph, edges, orphans
|
Closes R2; these are the payloads the skills parse | — |
| 2 | Decide and fix the criticality_of() medium branch (§34 Q2) |
A dead line invites a wrong "fix" later | — |
| 3 | Widen or make configurable the okf-curate.sh path filter |
Closes R4; the hook only just started working | — |
| 4 | Extract one undirected_adjacency() helper |
Closes R6; removes the duplicate construction in cmd_subgraph()
|
— |
| 5 | Optional PyYAML path with the hand parser as fallback | Closes R1 without adding a hard dependency | Evidence that users are hitting the edges |
| 6 | Cached-fixture mode for substack_okf.py
|
Makes the end-to-end harness CI-safe | — |
| 7 | Real docs/adr/ records, or drop the empty directory |
Closes R9 | Decision on where ADRs live |
Out of scope, unchanged from prior design: replacing okfcli, embeddings /
semantic search, and any Grok-only feature that breaks Claude Code.
| # | Question | Why it matters | Options | Recommendation |
|---|---|---|---|---|
| Q1 | Should validate gain an error class for unparseable frontmatter? |
Today a malformed block degrades to a partial dict and only its symptoms are reported | (a) leave it; (b) surface a warn when a frontmatter block is present but yields no keys |
(b) — cheap, and it turns R1 from silent to visible |
| Q2 | Is criticality_of()'s medium branch (line 342) intended to escalate? |
The line is currently a no-op; a future reader will "fix" it one way or the other | (a) delete the branch; (b) escalate unverified medium to high
|
(a) unless there is a ranking reason for (b); either way, add a test |
| Q3 | Should ambiguous resolve_concept() matches be an error? |
First-match-wins over dict order makes stem and title lookups non-deterministic across bundles |
(a) leave; (b) error on multiple matches; (c) prefer exact-path, warn otherwise | (c) |
| Q4 | Does Mermaid label escaping need to handle ] and newlines? |
render_mermaid():181 only substitutes double quotes |
(a) leave; (b) strip or escape the full unsafe set | (b) — one re.sub, removes a whole class of broken diagrams |
Per the template's menu rule, sections whose subject does not exist here, each with its reason:
| Template section | Reason omitted |
|---|---|
| 12 Package-by-Package | No packages. Three independent single-file scripts, no imports between them |
| 16 Cache Design | Nothing caches. Every invocation re-reads the bundle |
| 17 MCP Server Integration | The plugin neither ships nor consumes an MCP server |
| 18 AI Endpoint Design | No code in this repository calls a model. The host model reads the skills; the plugin never makes an inference request |
| 19 Managed AI Platform | No Bedrock / Vertex / Azure OpenAI integration |
| 21 Event-Driven Processing | No queues, topics, or async consumers. The .work/*.jsonl event log is append-only project state, covered in §14 |
| 24 Performance and Scalability | No load model applies. Single-user CLI over a local directory; see R5 for the one scaling note |
| 25 Observability | No logs, metrics, traces, or dashboards. The subcommands print JSON to stdout and exit; that is the entire observability surface |
| 26 Configuration and Secrets | No plugin configuration and no secrets. .work/config.yml configures the vendored worklog tooling, not the plugin |
| 27 Deployment Architecture | No deployment. Installation is a marketplace entry pointing at this repository (§5.3) |
| 30 Operations and Support | No running system to operate. Recovery is git checkout
|
| 33 Traceability Matrix | Folded into §3.1 and §3.2, which map each requirement directly to its implementing function and line range |
| § | Diagram | Type |
|---|---|---|
| 4 | System context and trust boundary | flowchart |
| 5.1 | Logical architecture | flowchart |
| 5.2 | Ingest data flow | flowchart |
| 8.1 | Impact-first change | sequenceDiagram |
| 8.2 | Progressive-disclosure pack | flowchart |
| 8.4 | Post-edit curation | sequenceDiagram |
| 9.1 | Frontmatter parser states | stateDiagram-v2 |
| 10 | Domain model | classDiagram |
| 11 | Module layering | flowchart |
Top architectural risks. (1) The hand-rolled frontmatter parser is the single point of silent failure for user-authored bundles — R1. (2) Five of eight subcommand payloads have no direct test, and those payloads are exactly what the skills parse — R2. (3) The curation hook, working for the first time as of v0.3.0, only fires on three path shapes — R4.
Immediate decisions required. Q2 (the dead criticality branch) and Q3 (ambiguous concept resolution). Both are small, both get worse with time.
Recommended implementation order. §21, items 1–4 — all are additive, none changes a shipped output shape.
Information still needed. Real usage data on user-authored bundles. Every known parser defect so far was found by reading, not by a report, because the repository's own generators emit only the shapes the parser already handles.
- Index-Releases
- Latest snapshot: Roadmap-2026-08-03_v0.3.2-release
- Index-Status
- Index-Decisions