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
50 changes: 43 additions & 7 deletions scripts/hooks/preflight_reminder.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,26 @@

When the user prompt indicates code-implementation intent, inject a
system-reminder elevating bicameral.preflight above the agent's default
tool-selection priority.
tool-selection priority — but only as a write-op gate, not a discovery
gate.

Reconciles with #146: that issue's failure mode was the agent doing
file inspection and then NEVER calling preflight at all. The original
fix (#155) over-corrected by telling the agent to call preflight
"before any file-inspection tool". That short-circuited the caller-LLM
discovery the rest of the contract depends on — preflight needs
``file_paths`` populated to do region-anchored retrieval, and the agent
can't extract file paths if we forbid it from looking at the codebase
first.

Updated contract:
- Read / Grep / Glob FIRST — caller LLM resolves "the reorder feature"
to concrete file paths.
- bicameral.preflight(topic, file_paths) — fed by step 1's discovery,
so the server gets the deterministic binds_to lookup it needs.
- Write ops (Edit / Write / NotebookEdit / mutating Bash) — preflight
must precede the first one. This is the line that #146 was
actually defending; the asserter (assert_flow_2) gates exactly this.

Per Claude Code 2.x hook contract: read JSON from stdin, write JSON to
stdout shaped as ``{"hookSpecificOutput": {"hookEventName":
Expand All @@ -24,12 +43,29 @@

REMINDER_TEXT = (
"<system-reminder>\n"
"This prompt indicates code-implementation intent. Before invoking "
"any file-inspection tool (Read, Grep, Bash, Glob), call "
"`bicameral.preflight` to surface prior decisions, drifted regions, "
"and open questions for the affected area. The skill produces zero "
"output when nothing relevant is found, so the cost of firing is "
"bounded. Auto-fire is the contract; do not skip.\n"
"This prompt indicates code-implementation intent. Auto-fire is the "
"contract: `bicameral.preflight` MUST run BEFORE any write op (Edit, "
"Write, NotebookEdit, or Bash that mutates state — git commit, file "
"writes, mv/rm/etc.).\n"
"\n"
"Read-only discovery FIRST, then preflight, then writes.\n"
"\n"
"Recommended sequence:\n"
" 1. Use Read / Grep / Glob to map the user's request to concrete "
"file paths. The user often names a feature ('the reorder feature') "
"rather than a file (`reorder.ts`); resolve that mapping yourself "
"before calling preflight.\n"
" 2. Call `bicameral.preflight(topic, file_paths)` with BOTH a "
"natural-language topic AND the concrete file paths discovered in "
"step 1. `file_paths=[]` defeats region-anchored retrieval — the "
"server uses these to look up bound decisions deterministically; "
"topic alone falls back to fuzzy text similarity.\n"
" 3. Read the surfaced decisions / drifted regions / open questions, "
"then proceed with the implementation.\n"
"\n"
"The skill produces zero output when nothing relevant is found, so "
"the cost of firing is bounded. Skipping preflight is the contract "
"violation, not running discovery first.\n"
"</system-reminder>"
)

Expand Down
18 changes: 15 additions & 3 deletions skills/bicameral-preflight/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,18 @@ case proceed directly to step 2.

### 2. Call `bicameral.preflight` for region-anchored and HITL state

**Discover first, then preflight.** Before this call, use Read / Grep / Glob to
resolve the user's request to concrete file paths. The user often names a
*feature* ("the reorder feature", "the rate limiter") rather than a *file*; the
caller LLM is responsible for that mapping — the server does deterministic
retrieval, not semantic guessing. A topic-only call falls back to fuzzy text
similarity over decision descriptions; passing `file_paths` engages the
high-precision `binds_to` graph lookup.

```
bicameral.preflight(
topic="<the 1-line topic>",
file_paths=["<repo-relative path>", ...], # include if you've scoped the files
file_paths=["<repo-relative path>", ...], # discovered in step 1
)
```

Expand All @@ -160,8 +168,12 @@ those into your in-scope set.
The response also carries an optional `sync_metrics` field — skip rendering it.
If `response.product_stage` is non-null, surface it verbatim to the user as a brief note (shown once per device only).

**Omit `file_paths`** if you haven't scoped the files yet (early "how should I
approach X?" queries). The handler still runs sync and HITL checks.
**`file_paths` may be omitted only** for genuinely abstract queries with no
file referent yet (e.g. *"how should I approach building a retry helper?"* —
no existing files to point at). For implementation prompts that name or imply
a feature backed by existing code, populate `file_paths` from your discovery.
The handler still runs sync and HITL checks either way; passing `file_paths`
just unlocks the precision channel.

### 2.5 Resolve pending compliance checks if present

Expand Down
24 changes: 24 additions & 0 deletions tests/test_preflight_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,27 @@ def test_handles_natural_contradiction_prompt():
inner = _hook_output(json.loads(out))
assert "additionalContext" in inner
assert "bicameral.preflight" in inner["additionalContext"]


def test_reminder_gates_writes_not_discovery():
"""The reminder must allow Read/Grep/Glob discovery before preflight,
and gate preflight against WRITE ops only. An earlier shape ("call
preflight before any file-inspection tool") short-circuited the
caller-LLM discovery the rest of the contract depends on (the agent
needs to map "the X feature" → concrete file paths via Read/Grep/Glob
before calling preflight). Lock the new posture in so future edits
don't quietly regress it.
"""
payload = {"prompt": "refactor the reorder feature to a text-editor flow"}
rc, out, _ = _run_hook(json.dumps(payload))
assert rc == 0
ctx = _hook_output(json.loads(out))["additionalContext"]
# Affirmative: discovery comes first, write op is the gate.
assert "Read-only discovery FIRST" in ctx
assert "BEFORE any write op" in ctx
assert "Edit, Write" in ctx
# The reminder should explicitly tell the agent to populate file_paths.
assert "file_paths" in ctx
# Negative: must NOT forbid file-inspection tools (the old shape).
assert "before any file-inspection tool" not in ctx
assert "Before invoking any file-inspection tool" not in ctx
Loading