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
5 changes: 4 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ Deterministic bookkeeping lives in **`scripts/`** (dependency-free bash: git +
coreutils); each script emits one JSON object on stdout (exit 0 = ran, 2 =
precondition missing). Model judgment lives in **`skills/`** (`/wijzer:init`,
`:update`, `:ask`) and **`agents/`** (`wiki-scout`, read-only fan-out). Shared
doctrine is in **`references/`**. The scripts are unit-tested against real temp
doctrine is in **`references/`**. The format side of parity is gated
deterministically too: init/update finish by running `scripts/check-format.sh`
over `openwiki/` and must fix reported problems before recording state. The scripts are unit-tested against real temp
git repos in **`tests/`** (Vitest); `tests/noop.test.ts` is a case-for-case port
of OpenWiki's `test/update-noop.test.ts` — the executable parity spec.

Expand All @@ -38,6 +40,7 @@ state file stays `openwiki/.last-update.json` with the exact
| test_pattern | `scripts/<name>.sh` → `tests/<name>.test.ts` |
| branch_pattern | `claude/<description>` (off `origin/main`) |
| pr_merge_strategy | squash + delete branch |
| pr_automerge | on green — pr-monitor merges feature PRs automatically once all CI checks pass (squash + delete branch); no human approval gate |

## Conventions

Expand Down
13 changes: 12 additions & 1 deletion PARITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ living record of what that means and how it is verified.
| `openwiki --update [msg]` | `/wijzer:update [msg]` skill | `tests/parity-crossvalidate.test.ts` + Phase-3 scenarios |
| chat mode (Q&A, no writes) | `/wijzer:ask` skill | manual: assert zero wiki writes |
| `--print` non-interactive | `claude -p "/wijzer:update"` | headless recipe (Phase 4) |
| plain-MD pages, no frontmatter, source-map at page end, ≤8 pages on init | `references/wiki-format.md` | format checklist vs upstream `openwiki/` |
| plain-MD pages, no frontmatter, source-map at page end, ≤8 pages on init | `references/wiki-format.md` + `scripts/check-format.sh` gate in init/update | `tests/check-format.test.ts` + golden run vs upstream `openwiki/` |
| `.last-update.json` = {updatedAt, command, gitHead?, model} | `scripts/write-state.sh` | `tests/state.test.ts` (CLI contract) + `tests/parity-crossvalidate.test.ts` (real-function interchange) |
| no-op: (no msg AND HEAD==state) OR only `openwiki/` changed; force when dirty | `scripts/check-noop.sh` (ports `getUpdateNoopStatus` + `shouldCheckUpdateNoop`) | `tests/parity-crossvalidate.test.ts` runs bash vs the vendored real functions; `vendor/openwiki/test/update-noop.test.ts` runs verbatim against the vendored source |
| surgical edits: ≤1–2 pages when <5 files changed | `references/disciplines.md` + `scripts/diff-summary.sh` | Phase-3 scenario |
Expand All @@ -38,6 +38,17 @@ living record of what that means and how it is verified.
| idempotent AGENTS.md/CLAUDE.md block | `scripts/inject-pointer.sh` | `tests/inject.test.ts` |
| GH Action: cron 8am → update → PR `openwiki/update` | `examples/github-action.yml` (via anthropics/claude-code-action, subscription OAuth) | Phase-4 live run |

## Watch items

- **State-file parsing seam.** `scripts/check-noop.sh` extracts `gitHead` from
`openwiki/.last-update.json` with `sed`, not a JSON parser (the scripts are
dependency-free by design). This is the one place interchangeability depends
on parsing JSON that *OpenWiki* may have written. Mitigated by
`tests/parity-crossvalidate.test.ts`, which runs `check-noop.sh` over state
files the vendored real OpenWiki functions produce; if upstream ever changes
its serializer (multi-line output, key reordering across lines), re-check this
seam first during re-validation.

## Re-validation procedure (when parity-watch fires)

1. Re-vendor the pinned spec source: `scripts/vendor-openwiki.sh --sha <new>`.
Expand Down
8 changes: 7 additions & 1 deletion agents/wiki-scout.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
---
name: wiki-scout
description: Read-only repository discovery agent for wijzer. Fans out during /wijzer:init and /wijzer:update to inspect one domain of a codebase — source, docs, data model, API surface, integrations, tests, or git history — and returns concise findings with source paths and open questions. Never writes files.
model: claude-sonnet-4-6
model: sonnet
effort: medium
# Agent `tools:` frontmatter accepts only bare tool names — it does not support
# per-command permission specifiers (e.g. `Bash(git log:*)`), unlike a skill's
# `allowed-tools`. So Bash cannot be narrowed to a non-mutating git subset here;
# the read-only constraint below (and the plain-git commands the brief needs) is
# what keeps this scout non-mutating. If Claude Code later supports restricted
# Bash specifiers in agent `tools:`, tighten this to the git read subset.
tools: Read, Grep, Glob, Bash
---

Expand Down
242 changes: 242 additions & 0 deletions scripts/check-format.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
#!/usr/bin/env bash
# check-format.sh — validates that an openwiki/ directory conforms to the
# wiki-format.md PARITY CONTRACT. This is a read-only validation gate, not a
# feature: it enforces the exact format that references/wiki-format.md already
# mandates (entrypoint filename, H1-first pages, resolving relative links, the
# `## Source map` / `Git evidence:` shape, the init page ceiling). The init and
# update skills run it before writing state so a wiki that has drifted from the
# contract never gets committed.
#
# Emits a single JSON object on stdout:
# {"ok":bool,"pages":int,"problems":[...strings],"warnings":[...strings]}
# `ok` is true iff `problems` is empty. `pages` counts .md files under openwiki/
# (excluding .last-update.json — it is json, not md — and _plan.md if present).
# Exit codes: 0 = evaluated, 2 = precondition missing (no openwiki/ dir).
#
# Dependency-free: bash 3.2 + coreutils. No associative arrays, no ${var,,}.
set -euo pipefail

SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck source=scripts/lib/json.sh
. "$SCRIPT_DIR/lib/json.sh"

DIR="."
while [ $# -gt 0 ]; do
case "$1" in
--dir) DIR=$2; shift 2 ;;
*) printf 'check-format.sh: unknown argument: %s\n' "$1" >&2; exit 2 ;;
esac
done

WIKI="$DIR/openwiki"

if [ ! -d "$WIKI" ]; then
printf 'check-format.sh: %s/openwiki does not exist\n' "$DIR" >&2
exit 2
fi

# Accumulators. Newline-delimited so bash 3.2 can iterate them without arrays.
PROBLEMS=""
WARNINGS=""
add_problem() { PROBLEMS="${PROBLEMS}$1"$'\n'; }
add_warning() { WARNINGS="${WARNINGS}$1"$'\n'; }

# --- Enumerate documentation pages (.md), excluding _plan.md. -----------------
# .last-update.json is json, not md, so a plain *.md filter already excludes it.
# Sorted for deterministic problem ordering. Relative paths inside openwiki/.
PAGES=$(
cd "$WIKI" || exit 1
find . -type f -name '*.md' | LC_ALL=C sort | while IFS= read -r f; do
rel=${f#./}
[ "$rel" = "_plan.md" ] && continue
printf '%s\n' "$rel"
done
)

pages_count=0
if [ -n "$PAGES" ]; then
pages_count=$(printf '%s\n' "$PAGES" | grep -c '' || true)
fi

# --- Problem 1: quickstart.md must exist. --------------------------------------
if [ ! -f "$WIKI/quickstart.md" ]; then
add_problem "openwiki/quickstart.md is missing (the wiki entrypoint)"
fi

# --- Problem 5: the temporary plan must have been removed by init. -------------
if [ -f "$WIKI/_plan.md" ]; then
add_problem "openwiki/_plan.md is still present (init must delete the plan)"
fi

# --- Per-page checks. ----------------------------------------------------------
# Iterate the enumerated pages. Each check appends repo-relative page context so
# a listed problem points the skill at the file to fix.
if [ -n "$PAGES" ]; then
while IFS= read -r rel; do
[ -z "$rel" ] && continue
file="$WIKI/$rel"

# Problem 2: first line must be a level-1 ATX heading ("# ..."). A leading
# "---" (YAML frontmatter) or any non-"# " first line is a parity violation.
first_line=$(awk 'NR==1{print; exit}' "$file")
case "$first_line" in
"# "*) : ;; # ok — level-1 heading
"---")
add_problem "openwiki/$rel starts with '---' (YAML frontmatter is forbidden; first line must be a '# ' heading)"
;;
*)
add_problem "openwiki/$rel first line is not a '# ' level-1 heading"
;;
esac

# Problem 3: relative Markdown links to same-wiki targets must resolve.
# Extract link targets of the form ](target). Ignore external/anchor links.
# A target that is a wiki-relative path (./x, x.md, sub/x.md) resolves
# against openwiki/; a repo-relative target (../src/...) resolves against
# the repo dir. Only flag a missing target.
targets=$(grep -oE '\]\([^)]+\)' "$file" 2>/dev/null | sed -e 's/^](//' -e 's/)$//' || true)
if [ -n "$targets" ]; then
while IFS= read -r target; do
[ -z "$target" ] && continue
# Strip a trailing #anchor and any surrounding whitespace.
target=${target%%#*}
target=${target%"${target##*[![:space:]]}"}
target=${target#"${target%%[![:space:]]*}"}
[ -z "$target" ] && continue
case "$target" in
http://*|https://*|mailto:*|//*) continue ;; # external
/*) continue ;; # absolute path — out of scope
*:*) continue ;; # other scheme (e.g. ftp:)
esac
# Only consider Markdown-ish same-wiki targets: those ending in .md or
# beginning with ./ or ../ . Anything else (e.g. an image, a bare
# fragment already stripped) we leave alone to stay conservative.
case "$target" in
*.md|./*|../*) : ;;
*) continue ;;
esac
# Resolve against the page's own directory using real filesystem
# semantics, so `./x.md` and `../topic/y.md` (same-wiki) resolve inside
# openwiki/, while a link that escapes with enough `../` to reach repo
# files resolves against the repo. `[ -e ]` follows `..` for us; a
# missing target (broken same-wiki link, or a repo file that moved) is
# the only thing we flag.
page_dir=$(dirname "$file")
resolved="$page_dir/$target"
if [ ! -e "$resolved" ]; then
add_problem "openwiki/$rel has a broken relative link: $target"
fi
done <<EOF
$targets
EOF
fi

# Problem 4: source-map / git-evidence shape.
# 4a. A mis-cased heading ("## Source Map") is a problem — the literal is
# "## Source map" (capital S, lowercase m).
if grep -qE '^##[[:space:]]+[Ss]ource[[:space:]]+[Mm]ap[[:space:]]*$' "$file"; then
if ! grep -qE '^##[[:space:]]+Source map[[:space:]]*$' "$file"; then
add_problem "openwiki/$rel has a mis-cased source-map heading (must be exactly '## Source map')"
fi
fi

# 4b/4c. Within the '## Source map' section, validate Git evidence bullets.
# We scan the section: from a line matching the exact heading up to the next
# '## ' heading or EOF. Collect its bullet lines to check evidence position.
# Capture the scan result in a variable — never write into openwiki/ (a
# git-tracked dir); a leftover temp file would dirty the worktree and trip
# check-noop. Emits "notlast" and/or "malformed" lines for this page.
smverdict=$(awk '
BEGIN { insec=0 }
/^##[[:space:]]+Source map[[:space:]]*$/ { insec=1; next }
insec && /^##[[:space:]]/ { insec=0 }
insec {
# Track bullets (lines starting with "- ").
if ($0 ~ /^-[[:space:]]/) {
bullets = bullets $0 "\n"
}
}
END {
n = split(bullets, arr, "\n")
# arr may have a trailing empty element from the final newline.
# Find real bullet count.
cnt = 0
for (i = 1; i <= n; i++) if (length(arr[i]) > 0) { cnt++; last[cnt] = arr[i]; }
for (i = 1; i <= cnt; i++) {
line = last[i]
if (line ~ /Git evidence:/) {
# Must be the LAST bullet.
if (i != cnt) { print "notlast"; }
# Must match exactly: "- Git evidence: commits `<7hex>`" then zero or
# more ", `<7hex>`". Backticks + 7-char lowercase hex required.
if (line !~ /^-[[:space:]]Git evidence: commits `[0-9a-f]{7}`([[:space:]]*,[[:space:]]*`[0-9a-f]{7}`)*[[:space:]]*$/) {
print "malformed";
}
}
}
}
' "$file" 2>/dev/null || true)
case "$smverdict" in
*malformed*)
add_problem "openwiki/$rel has a malformed 'Git evidence:' bullet (must be \`- Git evidence: commits \`<7-hex>\`\` with 7-char backticked hashes)"
;;
esac
case "$smverdict" in
*notlast*)
add_problem "openwiki/$rel has a 'Git evidence:' bullet that is not the last bullet of its source map"
;;
esac

# Warning: a non-quickstart .md page sitting at the openwiki/ root instead of
# a topic subdirectory.
case "$rel" in
*/*) : ;; # in a subdirectory — good
quickstart.md) : ;; # the entrypoint belongs at root
*) add_warning "openwiki/$rel sits at the wiki root; section pages belong in a topic subdirectory" ;;
esac
done <<EOF
$PAGES
EOF
fi

# --- Problem 6: with 2+ pages, quickstart needs the linking headings. ----------
if [ "$pages_count" -ge 2 ] && [ -f "$WIKI/quickstart.md" ]; then
if ! grep -qE '^##[[:space:]]+Start here[[:space:]]*$' "$WIKI/quickstart.md"; then
add_problem "openwiki/quickstart.md is missing the '## Start here' heading (required once 2+ pages exist)"
fi
if ! grep -qE '^##[[:space:]]+Documentation map[[:space:]]*$' "$WIKI/quickstart.md"; then
add_problem "openwiki/quickstart.md is missing the '## Documentation map' heading (required once 2+ pages exist)"
fi
fi

# --- Warning: soft init ceiling of 8 pages. ------------------------------------
if [ "$pages_count" -gt 8 ]; then
add_warning "openwiki/ has $pages_count pages (soft init ceiling is 8; consider merging thin pages)"
fi

# --- Emit. ---------------------------------------------------------------------
# Build JSON arrays from the newline-delimited accumulators.
json_array() {
# reads newline-delimited items on stdin -> [json_str,json_str,...]
local out="" first=1 item
while IFS= read -r item; do
[ -z "$item" ] && continue
if [ "$first" -eq 1 ]; then
out="$(json_str "$item")"
first=0
else
out="$out,$(json_str "$item")"
fi
done
printf '[%s]' "$out"
}

problems_json=$(printf '%s' "$PROBLEMS" | json_array)
warnings_json=$(printf '%s' "$WARNINGS" | json_array)

ok=true
[ -n "$PROBLEMS" ] && ok=false

printf '{"ok":%s,"pages":%s,"problems":%s,"warnings":%s}\n' \
"$ok" "$pages_count" "$problems_json" "$warnings_json"
exit 0
22 changes: 19 additions & 3 deletions skills/init/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: init
description: Generate the wijzer/OpenWiki wiki for this repository from scratch into openwiki/, then add a pointer block to AGENTS.md / CLAUDE.md. Use when the user runs /wijzer:init or asks to create/bootstrap the repository wiki.
argument-hint: [focus]
disable-model-invocation: true
allowed-tools: Bash(${CLAUDE_PLUGIN_ROOT}/scripts/*), Bash(git *), Bash(rg *), Bash(rm -f openwiki/_plan.md), Read, Grep, Glob, Write, Edit, Task
allowed-tools: Bash(${CLAUDE_PLUGIN_ROOT}/scripts/*), Bash(git log:*), Bash(git show:*), Bash(git diff:*), Bash(git status:*), Bash(git blame:*), Bash(git rev-parse:*), Bash(git rev-list:*), Bash(git cat-file:*), Bash(git ls-files:*), Bash(git shortlog:*), Bash(rg *), Bash(rm -f openwiki/_plan.md), Read, Grep, Glob, Write, Edit, Task
---

# /wijzer:init — generate the wiki from scratch
Expand Down Expand Up @@ -64,7 +64,22 @@ repo is clearly tiny; merge thin pages rather than shipping stubs.
rm -f openwiki/_plan.md
```

**7. Pointer block.** Point coding agents at the wiki idempotently:
**7. Parity gate.** Verify the wiki conforms to the format contract before doing
anything else:

```bash
"${CLAUDE_PLUGIN_ROOT}/scripts/check-format.sh" --dir .
```

If `ok` is `false`, the wiki violates `wiki-format.md`. Fix **each** string in
`problems` directly in the affected pages (broken links, missing `## Start
here` / `## Documentation map`, frontmatter, malformed `## Source map` /
`Git evidence:` bullets, a leftover `_plan.md`) and re-run until `ok` is `true`.
Entries in `warnings` (e.g. the 8-page soft ceiling, a page at the wiki root)
are judgment calls, not blockers — weigh them but you need not act on them.
**Do not proceed to the pointer or state steps while the gate reports `ok:false`.**

**8. Pointer block.** Point coding agents at the wiki idempotently:

```bash
"${CLAUDE_PLUGIN_ROOT}/scripts/inject-pointer.sh" --dir .
Expand All @@ -73,7 +88,8 @@ rm -f openwiki/_plan.md
This creates or appends a marker-delimited block in `AGENTS.md` / `CLAUDE.md`
(safe to re-run). Report its `results` to the user.

**8. Record state.** Only after the wiki content exists, write the run metadata:
**9. Record state.** Only after the wiki content exists and the parity gate
passes, write the run metadata:

```bash
"${CLAUDE_PLUGIN_ROOT}/scripts/write-state.sh" --dir . --command init --model <your-model-id>
Expand Down
20 changes: 17 additions & 3 deletions skills/update/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: update
description: Refresh the wijzer/OpenWiki wiki from what changed in the repository since the last run, making surgical edits and no-opping cleanly when nothing meaningful changed. Use when the user runs /wijzer:update or asks to refresh/sync the wiki. Supports --dry-run to preview without writing.
argument-hint: [--dry-run] [instruction]
disable-model-invocation: true
allowed-tools: Bash(${CLAUDE_PLUGIN_ROOT}/scripts/*), Bash(git *), Bash(rg *), Bash(rm -f openwiki/_plan.md), Read, Grep, Glob, Write, Edit, Task
allowed-tools: Bash(${CLAUDE_PLUGIN_ROOT}/scripts/*), Bash(git log:*), Bash(git show:*), Bash(git diff:*), Bash(git status:*), Bash(git blame:*), Bash(git rev-parse:*), Bash(git rev-list:*), Bash(git cat-file:*), Bash(git ls-files:*), Bash(git shortlog:*), Bash(rg *), Bash(rm -f openwiki/_plan.md), Read, Grep, Glob, Write, Edit, Task
---

# /wijzer:update — refresh the wiki from recent changes
Expand Down Expand Up @@ -85,14 +85,28 @@ Compare to the step-3 `digest`:
runs don't churn a PR. Report "wiki already accurate — no changes".
- **Changed** → continue to step 6.

**6. Pointer block.** Re-run the idempotent injector (picks up a newly added
**6. Parity gate.** Now that content actually changed, verify it still conforms
to the format contract:

```bash
"${CLAUDE_PLUGIN_ROOT}/scripts/check-format.sh" --dir .
```

If `ok` is `false`, fix **each** string in `problems` in the affected pages
(broken links, missing quickstart linking headings, frontmatter, malformed
`## Source map` / `Git evidence:` bullets) and re-run until `ok` is `true`;
`warnings` are judgment calls, not blockers. **Do not run the pointer or state
steps while the gate reports `ok:false`.** (This gate does not run in the
`--dry-run` or no-op paths — those already stopped above.)

**7. Pointer block.** Re-run the idempotent injector (picks up a newly added
`AGENTS.md`/`CLAUDE.md`, no-ops otherwise):

```bash
"${CLAUDE_PLUGIN_ROOT}/scripts/inject-pointer.sh" --dir .
```

**7. Record state.**
**8. Record state.**

```bash
"${CLAUDE_PLUGIN_ROOT}/scripts/write-state.sh" --dir . --command update --model <your-model-id>
Expand Down
Loading
Loading