95 lines
5.3 KiB
Markdown
95 lines
5.3 KiB
Markdown
# ADR-003 — Ground-truth enforcement: hooks, not advice
|
|
|
|
Date: 2026-09-04 · Status: Accepted
|
|
|
|
## Context
|
|
|
|
The old estate proved that advisory guidance fails: an MCP routing table, skills,
|
|
and operating rules existed for months, yet the servers and skills were never
|
|
consulted during development — even on the CAP packages where they obviously
|
|
applied. Causes: tool-selection dilution under a saturated loadout, zero
|
|
enforcement (prose loses to momentum), friction (cold starts, deferred tool
|
|
loading), and model overconfidence in trained SAP knowledge. The requirement for
|
|
this repo: consultation must be structural — the agent must forget what it
|
|
thinks it knows, consult the MCP servers and context7 before work starts, and be
|
|
able to cite sources on demand.
|
|
|
|
## Decision
|
|
|
|
Four mechanisms in `.claude/settings.json` + `.claude/hooks/` (project scope,
|
|
part of the founding kit for every future package):
|
|
|
|
1. **SessionStart injection** — every session opens with the grounding charter:
|
|
training knowledge is not a citable source; the five MCP servers + context7
|
|
are; if tools are deferred, loading them is the first action.
|
|
2. **Per-prompt reflex (UserPromptSubmit)** — a SAP-scoped analogue of the
|
|
machine-global TOOL-REFLEX hook: never classify whether a prompt "needs" a
|
|
consult; the classification step is the known failure.
|
|
3. **Consultation gate (PreToolUse deny)** — every successful `mcp__*`,
|
|
WebSearch, or WebFetch call stamps `.claude/.last-mcp-consult`; a Write/Edit
|
|
on a SAP artifact (`.cds`, `.abap`, `.bdef`, annotations, views,
|
|
`manifest.json`, `srv/`, `app/`, `worker/`) is **denied** unless a
|
|
consultation happened within 30 minutes, with the remedy stated in the denial
|
|
reason. Governance `.md`, `docs/`, and `.claude/` paths are exempt so
|
|
documentation never jams.
|
|
4. **Citation duty** — every SAP behavioral claim recorded in PRD/ADR/KNOWLEDGE
|
|
carries `[source: <server> <doc-id/URL>, verified YYYY-MM-DD]` (CLAUDE.md
|
|
rule 6); a PostToolUse reminder fires on governance-doc edits. Uncited =
|
|
hypothesis.
|
|
|
|
## On "load tools automatically at session start"
|
|
|
|
**Corrected 2026-09-04** after reading the 2026-09-03 "Investigate performance
|
|
lag issue" session transcript: my original claim ("no setting exists to preload
|
|
deferred tools") was true of settings *keys* but wrong in practice. The
|
|
`ENABLE_TOOL_SEARCH=false` environment variable disables deferral entirely
|
|
(verified against the binary in that session). It must be set before process
|
|
bootstrap — the settings.json `env` block loads too late, per that session's
|
|
finding — so it lives in the `claude()` wrapper in `~/.bashrc`, where it is
|
|
**already active machine-wide**, including for sessions started in this repo.
|
|
History: preloading was first measured at ~98k tokens/turn at the old ~950-tool
|
|
surface and rejected in favor of the TOOL-REFLEX hook; once plugin localization
|
|
cut the surface to ~40 tools, the same session adopted preloading via the
|
|
wrapper. This repo's slim 5-server loadout keeps the preload cost small.
|
|
Mechanism 1 (SessionStart load-first order) remains as the fallback for launch
|
|
paths that bypass the wrapper (e.g. IDE-spawned sessions).
|
|
|
|
## Consequences
|
|
|
|
- Consultation stops being a virtue and becomes a precondition; skipping it now
|
|
produces a denied edit instead of silent drift.
|
|
- The 30-minute freshness window is a tunable; if it proves too tight or too
|
|
loose in practice, adjust it in `sap-consult-gate.sh` and note it here.
|
|
- The gate measures that *a* consultation happened, not that the *right* one
|
|
did — citation duty and review cover relevance.
|
|
- Hooks activate on the first session started inside this repo (project hooks
|
|
are read at session start).
|
|
|
|
## Amendment 2026-09-14 — gate hardened after code review
|
|
|
|
The 2026-09-04 gate was advisory in practice: the `Write|Edit` matcher missed
|
|
Bash writes (sed -i, heredocs, tee, cp/mv, interpreter one-liners), MultiEdit,
|
|
NotebookEdit and filesystem-MCP edits; the `mcp__.*` PostToolUse matcher let
|
|
any MCP call (memory, chrome, mcp-manage) refresh the 30-minute marker; hook
|
|
commands hard-coded this checkout's absolute path; and a missing `jq` produced
|
|
a silent allow. I changed the mechanism, not the prose:
|
|
|
|
- PreToolUse gate matches `Write|Edit|MultiEdit|NotebookEdit|Bash|
|
|
mcp__filesystem__write_file|mcp__filesystem__edit_file|mcp__filesystem__move_file`;
|
|
for Bash the script gates only commands that combine a write indicator with
|
|
a gated path (documented heuristic, known blind spots: variable-built paths,
|
|
`cd`-relative paths, writes by invoked scripts).
|
|
- The consult marker is refreshed only by the grounding servers (sap-docs,
|
|
abap-docs, sap-released-objects, the Fiori/UI5 servers, context7, CAP) and
|
|
WebSearch/WebFetch — anchored regex in `settings.json`.
|
|
- Hook commands resolve via `"${CLAUDE_PROJECT_DIR}"`; scripts prefer that
|
|
variable over `$0` [source: https://code.claude.com/docs/en/hooks —
|
|
"`${CLAUDE_PROJECT_DIR}`: the project root where the session started",
|
|
verified 2026-09-14].
|
|
- Missing `jq` or an unresolvable marker now denies (fail closed); `jq` is a
|
|
hard dependency of any session in this repo.
|
|
- `*/worker/*` left the gated set: the TypeScript worker is governed by
|
|
context7 and the OpenAPI contract, not the SAP gate.
|
|
- `.claude/hooks/tests/run.sh` pipe-tests both scripts (37 cases) against an
|
|
override marker so the real marker is never touched.
|