fix(6855): the fence keeps its files where no compaction sweeps them, and builds id lists under mawk
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 1m52s
CI & Build / integration (push) Successful in 2m21s
CI & Build / Build & push image (push) Successful in 22s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 1m52s
CI & Build / integration (push) Successful in 2m21s
CI & Build / Build & push image (push) Successful in 22s
The project a session entered now lives in grimoire-fence, which the compaction clear never visits, and session end removes it. Arrays of ids in a tool's input were sent as [,"3","4"] under mawk, which creates an array element as soon as it is assigned to. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "grimoire",
|
"name": "grimoire",
|
||||||
"description": "Grimoire for Claude Code: connects the grimoire MCP server, adds the hooks that deliver live project state and relevant records at the right moment, ships the shared client-neutral Grimoire skills (using-grimoire, writing-plans, reporting-back, systematic-debugging, verification, brainstorming, reusing-code, shape-accounting, family-canon), and syncs your saved Grimoire Processes as skills (/grimoire:sync).",
|
"description": "Grimoire for Claude Code: connects the grimoire MCP server, adds the hooks that deliver live project state and relevant records at the right moment, ships the shared client-neutral Grimoire skills (using-grimoire, writing-plans, reporting-back, systematic-debugging, verification, brainstorming, reusing-code, shape-accounting, family-canon), and syncs your saved Grimoire Processes as skills (/grimoire:sync).",
|
||||||
"version": "2026.11.05.0245",
|
"version": "2026.11.05.0251",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Bryan Van Deusen"
|
"name": "Bryan Van Deusen"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ session_id=$(grimoire_json_pick "$event_flat" '.session_id')
|
|||||||
tool_name=$(grimoire_json_pick "$event_flat" '.tool_name')
|
tool_name=$(grimoire_json_pick "$event_flat" '.tool_name')
|
||||||
hook_event=$(grimoire_json_pick "$event_flat" '.hook_event_name')
|
hook_event=$(grimoire_json_pick "$event_flat" '.hook_event_name')
|
||||||
cwd=$(grimoire_json_pick "$event_flat" '.cwd')
|
cwd=$(grimoire_json_pick "$event_flat" '.cwd')
|
||||||
state_dir="${TMPDIR:-/tmp}/grimoire-priorart"
|
# Its own directory, which no compaction sweeps: the project a session
|
||||||
|
# entered stays its project for the whole session.
|
||||||
|
state_dir="${TMPDIR:-/tmp}/grimoire-fence"
|
||||||
safe_sid=$(printf '%s' "${session_id:-none}" | tr -c 'A-Za-z0-9._-' '_')
|
safe_sid=$(printf '%s' "${session_id:-none}" | tr -c 'A-Za-z0-9._-' '_')
|
||||||
entered_file="$state_dir/${safe_sid}.entered"
|
entered_file="$state_dir/${safe_sid}.entered"
|
||||||
|
|
||||||
@@ -135,7 +137,11 @@ fence_tool_input() {
|
|||||||
}
|
}
|
||||||
$2 ~ /^\.tool_input\.[A-Za-z_]+\[[0-9]+\]$/ && $3 != "null" && length($3) <= 200 {
|
$2 ~ /^\.tool_input\.[A-Za-z_]+\[[0-9]+\]$/ && $3 != "null" && length($3) <= 200 {
|
||||||
k = substr($2, 13); sub(/\[.*$/, "", k); add(k)
|
k = substr($2, 13); sub(/\[.*$/, "", k); add(k)
|
||||||
arr[k] = (k in arr) ? arr[k] ",\"" $3 "\"" : "\"" $3 "\""
|
# Tested before assigning: mawk creates arr[k] as soon as it is the
|
||||||
|
# target, so the test inside the assignment would always be true.
|
||||||
|
item = "\"" $3 "\""
|
||||||
|
if (k in arr) item = arr[k] "," item
|
||||||
|
arr[k] = item
|
||||||
}
|
}
|
||||||
END {
|
END {
|
||||||
printf "{"
|
printf "{"
|
||||||
|
|||||||
@@ -31,11 +31,13 @@ event_flat=$(printf '%s' "$event" | grimoire_json_flat)
|
|||||||
session_id=$(grimoire_json_pick "$event_flat" '.session_id')
|
session_id=$(grimoire_json_pick "$event_flat" '.session_id')
|
||||||
[ -n "$session_id" ] || exit 0
|
[ -n "$session_id" ] || exit 0
|
||||||
|
|
||||||
# The moment arm's per-session files (its tool cache and the acts ledger)
|
# The moment arm's per-session files (its tool cache and the acts ledger) and
|
||||||
# describe a session that is over, on any exit — a clear
|
# the fence's (the project the session entered, its outage marker) describe a
|
||||||
# included, since the next conversation is a new session id.
|
# session that is over, on any exit — a clear included, since the next
|
||||||
|
# conversation is a new session id.
|
||||||
safe_sid=$(printf '%s' "$session_id" | tr -c 'A-Za-z0-9._-' '_')
|
safe_sid=$(printf '%s' "$session_id" | tr -c 'A-Za-z0-9._-' '_')
|
||||||
rm -f "${TMPDIR:-/tmp}/grimoire-moment/${safe_sid}".* 2>/dev/null || true
|
rm -f "${TMPDIR:-/tmp}/grimoire-moment/${safe_sid}".* \
|
||||||
|
"${TMPDIR:-/tmp}/grimoire-fence/${safe_sid}".* 2>/dev/null || true
|
||||||
|
|
||||||
reason=$(grimoire_json_pick "$event_flat" '.reason')
|
reason=$(grimoire_json_pick "$event_flat" '.reason')
|
||||||
[ "$reason" = "clear" ] && exit 0
|
[ "$reason" = "clear" ] && exit 0
|
||||||
|
|||||||
@@ -190,3 +190,25 @@ def test_an_unanswered_check_lets_the_act_through_and_says_so_once(tmp_path):
|
|||||||
ctx = json.loads(first)["hookSpecificOutput"]["additionalContext"]
|
ctx = json.loads(first)["hookSpecificOutput"]["additionalContext"]
|
||||||
assert ctx.startswith("> Grimoire did not answer the project fence check")
|
assert ctx.startswith("> Grimoire did not answer the project fence check")
|
||||||
assert _run(tmp_path, 1, _edit(other / "b.py", home), home) == ""
|
assert _run(tmp_path, 1, _edit(other / "b.py", home), home) == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_entered_project_outlives_a_compaction_and_ends_with_the_session(tmp_path):
|
||||||
|
# A compaction loses the context, not the session: the project it entered
|
||||||
|
# stays its project. Session end clears it.
|
||||||
|
loose = tmp_path / "loose"
|
||||||
|
loose.mkdir()
|
||||||
|
_run(tmp_path, 1, {"hook_event_name": "PostToolUse", "cwd": str(loose),
|
||||||
|
"tool_name": "mcp__plugin_grimoire_grimoire__enter_project",
|
||||||
|
"tool_input": {"project_id": 7}}, loose)
|
||||||
|
entered = tmp_path / "tmp" / "grimoire-fence" / "s-fence.entered"
|
||||||
|
assert entered.read_text() == "7"
|
||||||
|
|
||||||
|
env = {"PATH": os.environ["PATH"], "TMPDIR": str(tmp_path / "tmp"), "HOME": str(tmp_path)}
|
||||||
|
hooks = HOOK.parent
|
||||||
|
for hook, event in (("grimoire_session_context.sh", {"source": "compact"}),
|
||||||
|
("grimoire_session_end.sh", {"reason": "other"})):
|
||||||
|
subprocess.run(["bash", str(hooks / hook)], input=json.dumps({"session_id": "s-fence", **event}),
|
||||||
|
capture_output=True, text=True, env=env, timeout=60, check=True)
|
||||||
|
if hook == "grimoire_session_context.sh":
|
||||||
|
assert entered.exists(), "a compaction cleared the project the session entered"
|
||||||
|
assert not entered.exists()
|
||||||
|
|||||||
Reference in New Issue
Block a user