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
165 changes: 0 additions & 165 deletions .claude/hooks/socraticode-health.sh

This file was deleted.

1 change: 1 addition & 0 deletions .claude/hooks/socraticode-health.sh
4 changes: 4 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,9 @@
"allow": [
"Read(/home/exedev/.claude/projects/**)"
]
},
"env": {
"SOCRATICODE_DRIVER": "skills-vendor/gregoryfoster-skills/skills/init-socraticode/scripts/mcp-driver.mjs",
"SOCRATICODE_PROBE_FILE": "src/worker/main.py"
}
}
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ Full tool table, prefetch query, per-tool guidance, cross-repo search:

**The manifest is a source, not the artifact.** Nothing re-embeds it — no hook, no CI step — so editing a `description` there changes what the repo says and not what `codebase_context_search` returns. Re-run `codebase_context_index` in the same change, or the highest-authority answer an agent gets stays the stale one (#19 CR #17).

**`mcp-driver.mjs` lies twice — silently through the `skills/` symlink (skills#177), falsely from a worktree (skills#180).** Use `"$SOCRATICODE_DRIVER"`; disbelieve health findings outside the main checkout. Both in [docs/SKILLS.md](docs/SKILLS.md).

## Project Layout

`src/worker/` is the primary process — the bus consumer, with the byte path, the
Expand Down
36 changes: 36 additions & 0 deletions docs/SKILLS.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ is accepted with a `N: fix + fitness` or bare `N: fitness` directive — without
directive fails to resolve. The daily auto-refresh hook bumps the submodule pointer but never creates
per-skill symlinks, so linking a newly published skill stays a manual step (#13).

**The table lists what is linked, which is a subset of what is vendored.** The `3fc7b71` → `2e1cf28`
bump (#41) published skills this repo has not evaluated — `vendoring-openapi-client` is the one with
an obvious claim here, since Replicator consumes sibling service APIs. `ls skills-vendor/gregoryfoster-skills/skills/`
against the table above is the diff; nothing surfaces it automatically, which is the #13 gap.

This submodule tracks upstream on the daily auto-refresh. It was held at `3fc7b71` for nine days;
that hold has ended — see [The curating-context v1.2 hold (ended)](#the-curating-context-v12-hold-ended).

Expand Down Expand Up @@ -152,6 +157,37 @@ version was current the day it was installed and drifts silently thereafter —
the whole `.skills/doctor.sh` commit path (#16). `readlink .claude/hooks/skills-submodule-update.sh`
is the check; an empty result means someone re-copied it.

**Two artifacts, and the second is the one that fails.** The symlink alone never runs — Claude Code
runs what `.claude/settings.json` names. This repo carried the link, tracked and resolving, for nine
days while the registration was absent, so the skills froze at one commit and `ls .claude/hooks/`
showed a hook that was right there and did nothing (#39). `tests/test_skills_hook.py` pins both
halves, the second via `install-refresh.sh --check`, so that state fails CI instead of going
unnoticed for another nine days.

## The other `.claude/hooks/` entries

| Hook | Event | What it does |
|---|---|---|
| `socraticode-reminder.sh` | SessionStart | Prints the `ToolSearch` prefetch string — the `codebase_*` MCP tools are deferred and their schemas do not load without it. Repo-authored (not vendored), so it is a real file, not a symlink. |
| `socraticode-health.sh` | SessionStart | Once-per-UTC-day SocratiCode infra check: graph yield, `codebase_health`, and a failed last operation. **Reports only — never re-indexes, never edits a file, never starts Docker.** Silent when clean; logs to `.git/socraticode-health.log`. Symlink into the vendored skill for the #16 reason above, against `init-socraticode`'s own instruction to copy it ([skills#179](https://github.com/gregoryfoster/skills/issues/179)). |
| `context-budget-guard.sh` | PostToolUse | `curating-context`'s write guard: warns when an edit pushes `AGENTS.md` over the token budget. Non-blocking. |

**The health hook lies in two situations, and both look like a healthy report.**

1. *Silently.* `mcp-driver.mjs` only dispatches when `process.argv[1]` resolves to its own module
path. `skills/init-socraticode` is a `managing-skills` symlink, so the two disagree and the script
**exits 0 having printed nothing** — and the hook's own resolution order tries that symlink
*before* the real `skills-vendor/` path, so it takes the broken one every time
([gregoryfoster/skills#177](https://github.com/gregoryfoster/skills/issues/177)). Worked around by
`SOCRATICODE_DRIVER` in `.claude/settings.json`, which is candidate 1 in that order. Committed
rather than left in `settings.local.json`: the value is a repo-relative path, and a machine-local
fix would leave every other checkout reporting clean without measuring anything.
2. *Falsely.* It measures `.` — the current working directory. Only the main checkout is indexed, so
from a worktree it reports `graph is not READY` and yield `UNKNOWN` against a perfectly healthy
index ([gregoryfoster/skills#180](https://github.com/gregoryfoster/skills/issues/180)). This repo
develops in worktrees by default, so that is the common case, not the edge one. Believe health
findings from the main checkout; disregard them anywhere else.

## The write-guard hook dangles on a submodule-less checkout

`curating-context` installs `.claude/hooks/context-budget-guard.sh` as a symlink into the
Expand Down
15 changes: 5 additions & 10 deletions docs/SOCRATICODE.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,22 +67,17 @@ anything — `READY` is reachable with a handful of edges across hundreds of fil
Check yield, not status:

```bash
node skills-vendor/gregoryfoster-skills/skills/init-socraticode/scripts/mcp-driver.mjs \
health-check /home/exedev/replicator --probe src/worker/main.py
node "$SOCRATICODE_DRIVER" health-check . --probe src/worker/main.py
```

**Invoke it through `skills-vendor/`, not `skills/`.** The driver only dispatches
when `process.argv[1]` resolves to its own module path; `skills/init-socraticode`
is a `managing-skills` symlink, so the two disagree and the script exits 0 having
printed nothing — a silent no-op that reads as a clean run
([gregoryfoster/skills#177](https://github.com/gregoryfoster/skills/issues/177)).

`verdict: "low"` means the graph resolved almost no edges, so `codebase_graph_query`,
`codebase_impact` and `codebase_flow` answer with an ordinary "no dependency
information found" rather than an error — which reads as *no dependents*, the
opposite of the truth. Send dependency questions to `grep` until it is rebuilt.
Last measured **2026-08-15: `ok`** — 172 edges across 73 files (2.36 edges/file),
85.1% of imports unresolved.

Two ways this check lies on this repo — both in `AGENTS.md` under
`## Code Exploration Notes (repo-specific)`, because a re-run overwrites this
file and would take them with it.

## Index scope

Expand Down
61 changes: 60 additions & 1 deletion tests/test_skills_hook.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,38 @@
"""The SessionStart skills hook must stay a symlink into the vendored skill.
"""The SessionStart skills hook must stay a symlink into the vendored skill,
and must stay registered in `.claude/settings.json`.

`managing-skills` Step 1 prescribes a symlink, not a copy, for one reason: a
copy freezes at whatever the script was the day it was installed and drifts
silently thereafter — no dangling link, no error, just a hook that stops
gaining upstream fixes. This repo's copy had missed the whole `.skills/doctor.sh`
commit path by the time it was noticed (#16). Nothing else catches a re-copy,
so it is pinned here rather than left to the next cohort sweep.

The registration is the *other* half of the same contract, and it is the half
that actually failed: the symlink was present and tracked for nine days while
`settings.json` never named it, so the hook never ran and the vendored skills
froze at one commit (#39). Listing `.claude/hooks/` showed a hook that was right
there and did nothing. A symlink-only test is green in exactly that state, which
is why the registration is pinned here too.
"""

import json
import os
import subprocess
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
HOOK = ROOT / ".claude" / "hooks" / "skills-submodule-update.sh"
SETTINGS = ROOT / ".claude" / "settings.json"
INSTALLER = (
ROOT
/ "skills-vendor"
/ "gregoryfoster-skills"
/ "skills"
/ "managing-skills"
/ "scripts"
/ "install-refresh.sh"
)


def test_the_session_start_hook_is_a_symlink_into_the_submodule():
Expand All @@ -31,3 +51,42 @@ def test_the_hook_the_symlink_points_at_actually_exists():
"or add `submodules: true` to the CI job's actions/checkout (#27)"
)
assert os.access(HOOK, os.X_OK)


def test_the_hook_is_registered_in_settings_json():
"""Claude Code runs what settings.json names, not what .claude/hooks/ holds.

Read directly rather than through the installer so the assertion still means
something on a checkout where skills-vendor/ is unpopulated.
"""
entries = json.loads(SETTINGS.read_text())["hooks"]["SessionStart"]
commands = [h.get("command", "") for e in entries for h in e.get("hooks", [])]

assert any("skills-submodule-update.sh" in c for c in commands), (
"the auto-refresh hook is not registered in .claude/settings.json — the "
"symlink alone never runs, which froze this repo's skills for nine days (#39). "
f"Repair with: bash {INSTALLER.relative_to(ROOT)}"
)


def test_the_installer_agrees_that_both_halves_are_present():
"""`--check` is the contract's own arbiter: exit 0 both present, 3 either missing.

Belt-and-braces over the two tests above — it is the vendored definition of
installed, so an upstream change to what that means reaches CI rather than
waiting for the next cohort audit.
"""
assert INSTALLER.exists(), (
"skills-vendor/ is not checked out: run .skills/doctor.sh locally, "
"or add `submodules: true` to the CI job's actions/checkout (#27)"
)
result = subprocess.run(
["bash", str(INSTALLER), "--check"],
cwd=ROOT,
capture_output=True,
text=True,
)
assert result.returncode == 0, (
"install-refresh.sh --check reported a half-installed hook:\n"
f"{result.stdout}{result.stderr}"
)
Loading