Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "okf-graph-eng",
"version": "0.7.0",
"description": "Graph engineering for OKF repositories — impact analysis, progressive disclosure, typed edges, and post-edit curation. Codex-compatible packaging for the same skills and hooks used by Claude Code and Grok Build.",
"version": "0.7.1",
"description": "Graph engineering for OKF repositories — impact analysis, progressive disclosure, typed edges, and fail-closed post-edit validate. Codex-compatible packaging for the same skills and hooks used by Claude Code and Grok Build.",
"author": {
"name": "Rick Hightower",
"url": "https://github.com/RichardHightower"
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@
Notable changes to **okf-graph-eng**. Newest first. Released sections are
frozen — corrections go in the next release's notes.

## 0.7.1 — 2026-08-16

### Changed

- Post-edit hook is **fail-closed**. `scripts/okf-curate.sh` now propagates
`okf-graph.py validate` (or `okf` / `okfcli` on PATH) instead of swallowing
the exit code.
- Matcher includes Codex `apply_patch` as well as Claude `Write|Edit|MultiEdit`.
- Hook parses `apply_patch` payloads. Writes outside an OKF bundle stay a
silent no-op. No SessionStart reminder.

## 0.7.0 — 2026-08-15

### Added
Expand Down
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,15 +95,21 @@ bin/worklog fold | python3 scripts/okf-ticket-link.py emit --bundle sample-okf -

### Hooks

Post-edit (`Write|Edit|MultiEdit`) runs `scripts/okf-curate.sh`: `okf validate`/`okf lint` when the official CLI is present, otherwise this repo's own `okf-graph.py validate`. It reads the tool payload from stdin, so it fires on every matching edit.
Post-edit (`apply_patch|Write|Edit|MultiEdit`) runs `scripts/okf-curate.sh`:
`okf validate` when the official CLI is present, otherwise this repo's own
`okf-graph.py validate`. It reads the tool payload from stdin (Claude
`file_path` or Codex `apply_patch` text), so it fires on every matching edit.

The bundle it curates is found by walking up from the edited file for an `index.md` containing `okf_version` (or a `.okf/` directory) — so a bundle rooted anywhere works, not just `.okf/`, `knowledge/` or `sample-okf/`. Edits outside any bundle are a silent no-op.
The bundle is found by walking up from the edited file for an `index.md`
containing `okf_version` (or a `.okf/` directory). Edits outside any bundle
are a silent no-op. **Validate is fail-closed:** a broken bundle exits
non-zero so the host cannot treat a bad write as success.

### Tests

```bash
python3 tests/test_okf_graph.py -q # graph engine — 25 cases
bash tests/test_okf_curate.sh # post-edit hook — 5 checks
bash tests/test_okf_curate.sh # post-edit hook — fail-closed checks
```

Plain asserts, no framework. Run in CI alongside `okf-graph.py validate sample-okf --strict`, and as a guarded pre-commit check.
Expand Down
2 changes: 1 addition & 1 deletion hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"matcher": "apply_patch|Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
Expand Down
2 changes: 1 addition & 1 deletion plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "okf-graph-eng",
"version": "0.7.0",
"version": "0.7.1",
"description": "Graph engineering for OKF repositories — impact analysis, agent/harness graphs, progressive disclosure, typed edges, TicketLink/worklog bridges, and write isolation bindings for multi-host agents.",
"author": {
"name": "Rick Hightower",
Expand Down
66 changes: 41 additions & 25 deletions scripts/okf-curate.sh
Original file line number Diff line number Diff line change
@@ -1,25 +1,43 @@
#!/usr/bin/env bash
# Post-edit hook: validate OKF concepts after Write/Edit/MultiEdit.
# Takes a file path as $1, or reads the PostToolUse payload from stdin.
# PostToolUse hook: fail-closed validate of the touched OKF bundle.
# Takes a file path as $1, or reads a Claude / Codex PostToolUse payload
# from stdin (Write/Edit file_path, or apply_patch patch text).
set -euo pipefail

# Claude Code delivers the tool payload as JSON on stdin; there is no
# $FILE_PATH in the hook environment. python3 rather than jq: the plugin
# already requires python3 everywhere, jq is not guaranteed present.
FILE="${1:-}"
if [[ -z "$FILE" ]]; then
FILE="$(python3 -c 'import json,sys
FILE="$(python3 -c '
import json, re, sys
def extract(data):
if not isinstance(data, dict):
return ""
nests = [data.get("tool_input"), data.get("arguments"), data]
for nest in nests:
if not isinstance(nest, dict):
continue
for key in ("file_path", "path", "file"):
v = nest.get(key)
if isinstance(v, str) and v.strip():
return v.strip()
for key in ("input", "patch"):
v = nest.get(key)
if not isinstance(v, str):
continue
m = re.search(r"\*\*\* (?:Add|Update|Delete) File: (.+)", v)
if m:
return m.group(1).strip()
return ""
try:
print(json.load(sys.stdin).get("tool_input", {}).get("file_path", ""))
print(extract(json.load(sys.stdin)))
except Exception:
pass' 2>/dev/null || true)"
pass
' 2>/dev/null || true)"
fi

# Cheap pre-check only: OKF bundles are Markdown, so anything else can never
# need curation and is not worth a filesystem walk. Bundle membership itself is
# decided by find_bundle_root below — a hard-coded list of path fragments
# ("knowledge/", "sample-okf/") is not a bundle test and skipped bundles rooted
# anywhere else.
# need validation and is not worth a filesystem walk. Bundle membership itself
# is decided by find_bundle_root below — a hard-coded list of path fragments
# ("knowledge/", "sample-okf/") is not a bundle test.
if [[ -z "$FILE" ]]; then
exit 0
fi
Expand All @@ -28,10 +46,14 @@ case "$FILE" in
*) exit 0 ;;
esac

if [[ "$FILE" != /* ]]; then
FILE="$(pwd)/$FILE"
fi

# Resolve bundle root: nearest ancestor containing index.md with okf_version,
# or a .okf/ bundle directory. No fallback to a repo's .okf/ or sample-okf/:
# a file that is not inside a bundle must not be curated against an unrelated
# one just because the repo happens to ship a bundle somewhere.
# a file that is not inside a bundle must not be validated against an
# unrelated one just because the repo happens to ship a bundle somewhere.
find_bundle_root() {
local dir
dir="$(cd "$(dirname "$FILE")" 2>/dev/null && pwd)" || return 1
Expand All @@ -56,20 +78,14 @@ if [[ -z "${BUNDLE_ROOT:-}" ]]; then
exit 0
fi

echo "okf-curate: validating bundle at $BUNDLE_ROOT (touched: $FILE)"
echo "okf-validate: validating bundle at $BUNDLE_ROOT (touched: $FILE)"

if command -v okf >/dev/null 2>&1; then
okf validate "$BUNDLE_ROOT" 2>&1 || true
if okf lint --help >/dev/null 2>&1; then
okf lint "$BUNDLE_ROOT" 2>&1 || true
fi
okf validate "$BUNDLE_ROOT"
elif command -v okfcli >/dev/null 2>&1; then
okfcli validate "$BUNDLE_ROOT" 2>&1 || true
okfcli validate "$BUNDLE_ROOT"
else
# No external CLI: use this repo's own validator, which sits next to us and
# understands typed edges. (The previous grep fallback also used `realpath
# -m`, absent from stock macOS.)
python3 "$(dirname "$0")/okf-graph.py" validate "$BUNDLE_ROOT" || true
# understands typed edges. Fail-closed: propagate the validator exit code.
python3 "$(dirname "$0")/okf-graph.py" validate "$BUNDLE_ROOT"
fi

exit 0
77 changes: 70 additions & 7 deletions tests/test_okf_curate.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#!/usr/bin/env bash
# Checks for scripts/okf-curate.sh — the PostToolUse curation hook.
# Checks for scripts/okf-curate.sh — the PostToolUse fail-closed validate hook.
# Plain bash, no framework: silent on success, loud + non-zero on failure.
# Run: bash tests/test_okf_curate.sh
set -uo pipefail

CURATE="$(cd "$(dirname "$0")/.." && pwd)/scripts/okf-curate.sh"
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
CURATE="$ROOT/scripts/okf-curate.sh"
HOOKS_JSON="$ROOT/hooks/hooks.json"
TMP="$(mktemp -d "${TMPDIR:-/tmp}/okf-curate-test.XXXXXX")"
trap 'rm -rf "$TMP"' EXIT
# TMPDIR may end in a slash (macOS) and may be a symlink (/var -> /private/var);
Expand All @@ -20,7 +22,7 @@ fail() {

# A bundle rooted somewhere the old hard-coded filter never matched:
# not .okf/, not knowledge/, not sample-okf/.
mkdir -p "$TMP/my-graph/agents" "$TMP/unrelated"
mkdir -p "$TMP/my-graph/agents" "$TMP/unrelated" "$TMP/broken-graph/agents"
cat > "$TMP/my-graph/index.md" <<'EOF'
---
okf_version: "0.2"
Expand All @@ -43,19 +45,44 @@ timestamp: 2026-01-01T00:00:00Z

# Agent A
EOF
cat > "$TMP/broken-graph/index.md" <<'EOF'
---
okf_version: "0.2"
title: Broken Bundle
description: Has a broken link so validate must fail-closed.
timestamp: 2026-01-01T00:00:00Z
---

# Broken Bundle

- [Missing](/agents/missing.md)
EOF
cat > "$TMP/broken-graph/agents/b.md" <<'EOF'
---
type: AgentNode
title: Agent B
description: Points at a file that does not exist.
timestamp: 2026-01-01T00:00:00Z
links:
- target: /agents/missing.md
rel: related_to
---

See [missing](/agents/missing.md).
EOF
echo "just some notes" > "$TMP/unrelated/notes.md"
echo "print('hi')" > "$TMP/my-graph/agents/script.py"

# 1. File in a non-standard bundle root IS curated, via explicit $1.
# 1. File in a non-standard bundle root IS validated, via explicit $1.
out="$("$CURATE" "$TMP/my-graph/agents/a.md" 2>&1)"
rc=$?
[[ $rc -eq 0 ]] || fail "arg invocation should exit 0, got $rc"
case "$out" in
*"validating bundle at $TMP/my-graph"*) ;;
*) fail "non-standard bundle root not curated" "$out" ;;
*) fail "non-standard bundle root not validated" "$out" ;;
esac

# 2. File in an unrelated directory is NOT curated, and says nothing about it.
# 2. File in an unrelated directory is NOT validated, and says nothing about it.
out="$("$CURATE" "$TMP/unrelated/notes.md" 2>&1)"
rc=$?
[[ $rc -eq 0 ]] || fail "unrelated file should exit 0, got $rc"
Expand All @@ -68,7 +95,7 @@ rc=$?
[[ $rc -eq 0 ]] || fail "stdin invocation should exit 0, got $rc"
case "$out" in
*"validating bundle at $TMP/my-graph"*) ;;
*) fail "stdin JSON payload not curated" "$out" ;;
*) fail "stdin JSON payload not validated" "$out" ;;
esac

# 4. Empty / malformed stdin is a no-op, not a crash.
Expand All @@ -83,4 +110,40 @@ rc=$?
[[ $rc -eq 0 ]] || fail "non-markdown should exit 0, got $rc"
[[ -z "$out" ]] || fail "non-markdown should be skipped silently" "$out"

# 6. Invalid bundle is fail-closed (non-zero).
out="$("$CURATE" "$TMP/broken-graph/agents/b.md" 2>&1)"
rc=$?
[[ $rc -ne 0 ]] || fail "invalid bundle should exit non-zero, got $rc" "$out"
case "$out" in
*"validating bundle at $TMP/broken-graph"*) ;;
*) fail "invalid bundle did not announce validate" "$out" ;;
esac

# 7. Codex apply_patch payload extracts the file and validates.
out="$(printf '{"tool_name":"apply_patch","tool_input":{"input":"*** Begin Patch\\n*** Update File: %s\\n*** End Patch\\n"}}' \
"$TMP/my-graph/agents/a.md" | "$CURATE" 2>&1)"
rc=$?
[[ $rc -eq 0 ]] || fail "apply_patch payload should exit 0, got $rc" "$out"
case "$out" in
*"validating bundle at $TMP/my-graph"*) ;;
*) fail "apply_patch payload not validated" "$out" ;;
esac

# 8. Manifest is PostToolUse fail-closed, not a SessionStart reminder.
if ! python3 - "$HOOKS_JSON" <<'PY'
import json, sys
p = sys.argv[1]
data = json.loads(open(p, encoding="utf-8").read())
hooks = data.get("hooks") or {}
if "SessionStart" in hooks:
raise SystemExit("hooks.json still has SessionStart")
post = hooks.get("PostToolUse") or []
matchers = " ".join(e.get("matcher") or "" for e in post)
if "apply_patch" not in matchers or "Write" not in matchers:
raise SystemExit("matcher must include apply_patch and Write")
PY
then
fail "hooks.json is not fail-closed PostToolUse"
fi

exit $FAILED
Loading