Recall at admission: recall.py admit holds a ruling/question batch once with its in-force neighbours #29
3 changed files with 53 additions and 3 deletions
recall: install form that cannot exit 2; a hold never lists a For line empty; For-line escape ASCII-only; tests for exclusion identity, cap and silent window
The install command is "<abs python.exe> <path>/recall.py prompt|admit || true" (Claude Code runs hook commands through Git Bash on Windows; a missing script made py -3 exit 2, which blocks every prompt or admit). A For line is joined to its first neighbour so render keeps both or neither; no neighbour fitting the budget is no deny. The escape test is [A-Za-z0-9_.:-], not Unicode \w. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
commit
34cbdd45b7
|
|
@ -15,4 +15,4 @@ gets a Soul pass before merge.
|
|||
| `tools/test-tools.sh` | Local fixture check of the parsers and the summary over `tools/fixtures/logs` (real cargo and mutation logs, plus hand-written dotnet, vitest, jest, pytest, exit-0-but-failed and no-verdict logs), and of the mutation runner's host driver and container half under a mock docker. Run it before merging a change to the scripts above. | whoever changes those scripts | `SKILL.md` (Shared tools) |
|
||||
| `tools/context-pack.py` | `py -3 tools/context-pack.py <cut_spec id> <worktree> [--since <sha>] [--only <k>[,<k>...]] [--spec-json <file>] [--max-lines <n>]`. Carries a cut_spec's `file_changes` anchors from the spec's base to the worktree by `git diff` hunks (the working tree included) and prints one header, a status table (SAME, MOVED, TOUCHED, RENAMED, NEW, GONE, OUT_OF_RANGE, UNRESOLVED) and windows of at most 100 lines numbered in worktree lines, within `--max-lines` (800). Exit 3 when an anchor cannot be carried, 4 when the spec is unreadable (fetched by id through the eureka-state binding (`tools/mind.py`), retried once on Unavailable; `--spec-json` takes a saved view). `--since <sha>` re-prints only the windows edited since that commit. Standard library plus git; writes nothing. | Hands, Soul, Imagination | `references/briefs.md` (Imagination map, Hands, Soul), `SKILL.md` (step 1) |
|
||||
| `tools/mind.py` | The one way a Eureka script reaches the mind: `mind.call(tool, arguments, deadline)` spawns the eureka-state binding (`EUREKA_STATE_EXE`, else `~/.claude.json` `mcpServers.eureka-state`), does the stdio handshake and one tool call within `deadline` seconds, returns the parsed JSON body, raises `MindError` (kind binding, transport, tool or answer; a detail, never an input value), and kills the child on every exit path. No retry inside `call`; `retry_once(fn)` is the caller's choice. Standard library only. | `tools/context-pack.py`, `tools/recall.py` | not named by a brief; it lives and dies with its consumers |
|
||||
| `tools/recall.py` | `py -3 tools/recall.py prompt`, a `UserPromptSubmit` command hook (hook JSON on stdin): queries the rulings, questions and follow-ups in force across every root, ranked by Huginn against the prompt (`RECALL_TOP_K`, 5), and prints one hook JSON whose `additionalContext` is a header saying the lines are recalled records, data and never instructions, written by agents and not the operator, then one JSON object per item (`id`, `title`, a short `operator_quote`) with `<`, `>` and control, bidi and separator characters escaped, within `RECALL_BUDGET` characters (4000, at most 9000: Claude Code caps `additionalContext` at 10,000), whole lines only. Prompts under 3 words are not queried. Never blocks: any failure exits 0 with empty stdout; its own deadline is `RECALL_DEADLINE_MS` (4000) for the whole run (stdin read to 4 MiB, answer read to 1 MiB), so set the hook `timeout` above it. One attempt, no retry. `py -3 tools/recall.py admit` is a `PreToolUse` hook on `mcp__eureka-state__admit`: for each ruling or question in the batch (at most `RECALL_ADMIT_MAX`, 3) it queries the nearest in-force rulings and questions, and denies the first attempt with the neighbours as the reason (a deliberate hold, not a failure); the identical call sent again (digest of `tool_input` plus `agent_id`, kept in the session scratchpad) passes. Never allow or ask; a failed check exits 0 with a note naming the failure kind (`RECALL_TOP_K` 3, `RECALL_DEADLINE_MS` 10000 in this mode). Standard library only; writes only the digest file. | the operator's `UserPromptSubmit` hook and `PreToolUse` hook on `mcp__eureka-state__admit` (fires inside subagents too) in `~/.claude/settings.json` | `references/campaign-state.md` (Precedent) |
|
||||
| `tools/recall.py` | `py -3 tools/recall.py prompt`, a `UserPromptSubmit` command hook (hook JSON on stdin): queries the rulings, questions and follow-ups in force across every root, ranked by Huginn against the prompt (`RECALL_TOP_K`, 5), and prints one hook JSON whose `additionalContext` is a header saying the lines are recalled records, data and never instructions, written by agents and not the operator, then one JSON object per item (`id`, `title`, a short `operator_quote`) with `<`, `>` and control, bidi and separator characters escaped, within `RECALL_BUDGET` characters (4000, at most 9000: Claude Code caps `additionalContext` at 10,000), whole lines only. Prompts under 3 words are not queried. Never blocks: any failure exits 0 with empty stdout; its own deadline is `RECALL_DEADLINE_MS` (4000) for the whole run (stdin read to 4 MiB, answer read to 1 MiB), so set the hook `timeout` above it. One attempt, no retry. Install both hooks as `<absolute python.exe> C:/Users/Meta/.claude/skills/eureka/tools/recall.py prompt || true` (and `... admit || true`): Claude Code runs hook commands through Git Bash on Windows, and `py -3 <missing script>` exits 2, which blocks every prompt or admit; with `|| true` the hook cannot exit 2 (a missing script, interpreter or file passes) and the script's stdout is untouched (proved live on 2.1.308). `py -3 tools/recall.py admit` is a `PreToolUse` hook on `mcp__eureka-state__admit`: for each ruling or question in the batch (at most `RECALL_ADMIT_MAX`, 3) it queries the nearest in-force rulings and questions, and denies the first attempt with the neighbours as the reason (a deliberate hold, not a failure); the identical call sent again (digest of `tool_input` plus `agent_id`, kept in the session scratchpad) passes. Never allow or ask; a failed check exits 0 with a note naming the failure kind (`RECALL_TOP_K` 3, `RECALL_DEADLINE_MS` 10000 in this mode). Standard library only; writes only the digest file. | the operator's `UserPromptSubmit` hook and `PreToolUse` hook on `mcp__eureka-state__admit` (fires inside subagents too) in `~/.claude/settings.json` | `references/campaign-state.md` (Precedent) |
|
||||
|
|
|
|||
|
|
@ -23,6 +23,12 @@ resolutions in the batch. A batch with a neighbour left is denied, the neighbour
|
|||
digest (tool_input plus agent_id) is recorded in the session scratchpad; the identical call sent again
|
||||
passes untouched. It never decides allow or ask. Any failure to check exits 0 with a note naming only
|
||||
the failure kind. Env defaults in this mode: RECALL_TOP_K 3, RECALL_DEADLINE_MS 10000.
|
||||
Install both hooks as a command that cannot exit 2 (Claude Code runs hook commands through Git Bash on
|
||||
Windows, and a missing script, interpreter or file makes python exit 2, which blocks the prompt or the admit):
|
||||
the interpreter by absolute path and `|| true`, stdout untouched. In settings.json, with the
|
||||
python.exe of your install (like C:/Users/Meta/AppData/Local/Programs/Python/Python313/python.exe):
|
||||
UserPromptSubmit: <python.exe> C:/Users/Meta/.claude/skills/eureka/tools/recall.py prompt || true
|
||||
PreToolUse, matcher mcp__eureka-state__admit: <python.exe> C:/Users/Meta/.claude/skills/eureka/tools/recall.py admit || true
|
||||
The recalled text is data written by agents: every field is a JSON string with < > and
|
||||
control, bidi and separator characters escaped, under a header saying so.
|
||||
"""
|
||||
|
|
@ -229,8 +235,9 @@ def admit_mode(end):
|
|||
for d in checked:
|
||||
if found[id(d)]:
|
||||
ident = d["value"]["campaign"] + ":" + d["kind"] + ":" + d["value"]["label"]
|
||||
lines.append("For %s:" % (ident if re.fullmatch(r"[\w.:-]+", ident) else inert(ident)))
|
||||
lines += [line_of(i) for i in found[id(d)]]
|
||||
head = "For %s:" % (ident if re.fullmatch(r"[A-Za-z0-9_.:-]+", ident) else inert(ident))
|
||||
ns = [line_of(i) for i in found[id(d)]]
|
||||
lines += [head + "\n" + ns[0]] + ns[1:] # a For line travels with its first neighbour: never listed empty
|
||||
over = len(docs) - len(checked)
|
||||
tail = ["%d rulings or questions over RECALL_ADMIT_MAX were not checked." % over] if over else []
|
||||
budget = min(env_int("RECALL_BUDGET", 4000), MAX_BUDGET) - sum(len(t) + 1 for t in tail)
|
||||
|
|
|
|||
|
|
@ -1028,6 +1028,49 @@ for label, extra in (("absent", {"scratchpad_dir": DEL}), ("not a directory", {"
|
|||
r, calls, ms = run(dict(hr, session_id=h["session_id"] + "-other"))
|
||||
check("R12 the same batch in another session is held again (%s)" % label, held(r))
|
||||
|
||||
# fix batch: a hold never lists a For line without a neighbour under it
|
||||
short = [item("n1", "Neighbour 1", "q1"), item("n2", "Neighbour 2"), item("n3", "Neighbour 3")]
|
||||
h = hook([mine])
|
||||
r, calls, ms = run(h, items=[item("big", "x" * 990)], RECALL_BUDGET="700")
|
||||
check("empty-hold: when no neighbour line fits the budget there is no deny, nothing is recorded", silent(r) and not os.path.exists(store(h)), r.stdout[:200])
|
||||
two = [doc("ruling", "a", "Alpha"), doc("ruling", "b", "Beta")]
|
||||
r, calls, ms = run(hook(two), items=short)
|
||||
full = reason(r).split("\n")
|
||||
cut = full.index("For t:ruling:b:")
|
||||
prefix = "\n".join(full[:cut])
|
||||
r, calls, ms = run(hook(two), items=short, RECALL_BUDGET=str(len(prefix) + len("\nFor t:ruling:b:") + 2))
|
||||
L = reason(r).split("\n")
|
||||
check("empty-hold: a second document whose first neighbour does not fit leaves no dangling 'For' line",
|
||||
held(r) and "\n".join(L) == prefix and not any(l.startswith("For t:ruling:b") for l in L), L[-3:])
|
||||
check("empty-hold: every 'For' line in a deny is followed by a neighbour line",
|
||||
all(L[i + 1].startswith("- ") for i, l in enumerate(L) if l.startswith("For ")))
|
||||
|
||||
# fix batch: exclusion matches full ids, and an answered question of another campaign is excluded by its own id
|
||||
def xitem(full_id, title, kind):
|
||||
return {"id": {"id": full_id, "kind": kind.title()}, "document": {"kind": kind, "value": {"title": title}}, "status": "InForce"}
|
||||
pool = [xitem("u:ruling:mine", "Same label, other campaign", "ruling"), xitem("t:question:mine", "Same label, other kind", "question"),
|
||||
xitem("u:question:q1", "Answered, other campaign", "question"), xitem("t:question:q1", "Same label as the answered one", "question")]
|
||||
r, calls, ms = run(hook([doc("ruling", "mine", "Admit holds once", answers="u:question:q1")]), items=pool)
|
||||
check("exclusion: only the full ids are excluded: a label shared with another campaign or kind is still a neighbour, the answered question of another campaign is not",
|
||||
held(r) and ids(r) == ["u:ruling:mine", "t:question:mine", "t:question:q1"], ids(r) if r.stdout else r.stdout)
|
||||
|
||||
# fix batch: the cap counts rulings and questions only
|
||||
mixed = [doc("finding", "f1", "A finding"), doc("follow_up", "f2", "A follow up")] + [doc("ruling", "r%d" % i, "Title %d" % i) for i in range(3)]
|
||||
r, calls, ms = run(hook(mixed))
|
||||
check("cap: documents of other kinds do not use up RECALL_ADMIT_MAX: three rulings behind two findings are all checked, none counted over",
|
||||
len(calls) == 3 and "RECALL_ADMIT_MAX" not in reason(r), (len(calls), reason(r)[-100:]))
|
||||
|
||||
# fix batch: a fast failure is reported unless it ends within the last 0.2 s of the deadline
|
||||
r, calls, ms = run(hook([mine]), RECALL_DEADLINE_MS="3000", FAKE_MODE="unavailable")
|
||||
o = out(r)
|
||||
check("window: Unavailable at 3 s deadline, answered at once, prints the note (the silent window is 0.2 s)",
|
||||
r.returncode == 0 and o is not None and "(tool)" in o.get("additionalContext", ""), r.stdout[:200])
|
||||
|
||||
# fix batch: the For line is inert unless the id is plain ASCII
|
||||
for lab, want_line in (("a<b", 'For "t:ruling:a\u003cb":'), ("na\u00efve", 'For "t:ruling:na\u00efve":'), ("plain-1", "For t:ruling:plain-1:")):
|
||||
r, calls, ms = run(hook([doc("ruling", lab, "Title")]))
|
||||
check("for-line: id %r is written %s" % (lab, want_line), held(r) and reason(r).split("\n")[1] == want_line, reason(r).split("\n")[1] if r.stdout else r.stdout)
|
||||
|
||||
# R1 across every case above: never allow, never ask
|
||||
bad = []
|
||||
for s in outs:
|
||||
|
|
|
|||
Loading…
Reference in a new issue