fix(hooks): raise state-line-guard MAX_LINES from 60 to 120
Operator decision 2026-08-14: the STATE.md convention's line limit moved from ~60 to ~120 (global CLAUDE.md already updated). Re-bases every selftest fixture and boundary value that encoded 60 as a literal, including section 8's ratchet fixtures, so they still exercise the ratchet rather than degenerating into a flat gate at the new threshold. Re-verified the real-tree justification at 120: 13 files over the limit, one at 1496 (was 23 over 60, one at 1405 — left as historical record). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0194eV8b6BXNv6aKLovP8TP6
This commit is contained in:
parent
09fd4b74fa
commit
44fb34ab72
3 changed files with 60 additions and 43 deletions
18
CLAUDE.md
18
CLAUDE.md
|
|
@ -52,8 +52,9 @@ marketplace plugin. Three components, one boundary:
|
|||
`coord-inbox.sh` and emits the `hookSpecificOutput.additionalContext`
|
||||
envelope. No mailbox logic lives here. Always exits 0.
|
||||
- **Hook (`hooks/scripts/pre-state-line-guard.mjs`):** a `PreToolUse` hook on
|
||||
`Write|Edit` that enforces the STATE.md convention's `maks ~60 linjer`
|
||||
(global CLAUDE.md) mechanically. It exists because the prose limit alone
|
||||
`Write|Edit` that enforces the STATE.md convention's `maks ~120 linjer`
|
||||
(global CLAUDE.md; raised from `~60` by operator decision 2026-08-14 —
|
||||
see the dated paragraph below) mechanically. It exists because the prose limit alone
|
||||
failed: a real STATE.md drifted to 155-156 lines before an /insights sweep
|
||||
of 160 sessions noticed, and one trim pass on it *increased* the line count
|
||||
instead of shrinking it. org-ops dispatched the work order
|
||||
|
|
@ -106,6 +107,19 @@ marketplace plugin. Three components, one boundary:
|
|||
shrink-while-still-over-limit allows, same-size-rewrite allows, grow-an-
|
||||
already-oversized-file still denies, and create-new-oversized-file still
|
||||
denies.
|
||||
|
||||
**`MAX_LINES` raised 60 -> 120, operator decision 2026-08-14 (evening),
|
||||
reported via coord by `.claude` after the global CLAUDE.md prose was
|
||||
already updated.** The ratchet mechanics above are unchanged - only the
|
||||
constant moved, plus every selftest fixture and boundary value that
|
||||
encoded 60 as a literal. Re-verified empirically against the real tree at
|
||||
the new threshold (2026-08-14): `wc -l ~/repos/*/STATE.md
|
||||
~/repos/*/*/STATE.md | awk '$1 > 120'` found 13 files already over 120
|
||||
lines, one at 1496 - the historical 23-files-over-60/one-at-1405 figures
|
||||
above describe the tree as it was at the moment the ratchet bug was found,
|
||||
not the current threshold, and are left as-is rather than rewritten.
|
||||
`session-start.mjs`'s 160-line injection window still covers the new
|
||||
120-line limit with room to spare, so no change was needed there.
|
||||
- **Board (`scripts/board.sh`):** cross-repo attention board. Reads STATE.md
|
||||
next-step blocks + board lines, `git status`, and mailbox pending counts, and
|
||||
prints one line per repo. Read-only by construction: it writes to no repo, no
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
// Hook: pre-state-line-guard.mjs
|
||||
// Event: PreToolUse (Write|Edit)
|
||||
// Purpose: block a Write/Edit that would push a STATE.md past the documented
|
||||
// ~60-line convention (global CLAUDE.md's Kontinuitets-system section).
|
||||
// ~120-line convention (global CLAUDE.md's Kontinuitets-system section;
|
||||
// raised from ~60 by operator decision 2026-08-14).
|
||||
//
|
||||
// PreToolUse, not PostToolUse: org-ops' work order (20260814T144553Z) asked
|
||||
// for a PostToolUse hook, but PostToolUse fires AFTER the tool already ran
|
||||
|
|
@ -41,8 +42,9 @@
|
|||
// MAX_LINES and larger than the file's CURRENT line count (0 for a file
|
||||
// that doesn't exist yet). A file already over the limit is the normal
|
||||
// starting point for a trim, not an edge case - measured on the real
|
||||
// tree 2026-08-14, 23 of the machine's STATE.md files were already over
|
||||
// 60 lines, one at 1405. Comparing only against MAX_LINES (no ratchet)
|
||||
// tree 2026-08-14 at the 120-line threshold, 13 of the machine's
|
||||
// STATE.md files were already over 120 lines, one at 1496. Comparing
|
||||
// only against MAX_LINES (no ratchet)
|
||||
// would deny every incremental trim of those files that doesn't land at
|
||||
// <=60 in one shot - the opposite of what a guard meant to make trimming
|
||||
// possible should do. The ratchet still blocks what the guard exists to
|
||||
|
|
@ -54,7 +56,7 @@
|
|||
import { readFileSync } from 'node:fs';
|
||||
import { basename } from 'node:path';
|
||||
|
||||
const MAX_LINES = 60;
|
||||
const MAX_LINES = 120;
|
||||
|
||||
function allow() {
|
||||
process.exit(0);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/bash
|
||||
# state-line-guard-selftest.sh - proves hooks/scripts/pre-state-line-guard.mjs
|
||||
# actually PREVENTS a Write/Edit that would push a STATE.md past the
|
||||
# documented ~60-line convention (global CLAUDE.md), and leaves everything
|
||||
# documented ~120-line convention (global CLAUDE.md), and leaves everything
|
||||
# else alone. ASCII only, bash 3.2 safe.
|
||||
#
|
||||
# PreToolUse, not PostToolUse: the org-ops work order (20260814T144553Z) asked
|
||||
|
|
@ -39,26 +39,26 @@ echo "state-line-guard-selftest"
|
|||
# --- 1. Write: line-count boundary ------------------------------------------
|
||||
|
||||
P="$(payload '
|
||||
const content = "x\n".repeat(60);
|
||||
const content = "x\n".repeat(120);
|
||||
process.stdout.write(JSON.stringify({
|
||||
tool_name: "Write",
|
||||
tool_input: { file_path: "/tmp/wherever/STATE.md", content }
|
||||
}));
|
||||
')"
|
||||
run_hook "$P"
|
||||
[ "$HOOK_EXIT" -eq 0 ]; check "Write: exactly 60 lines allows" $?
|
||||
[ "$HOOK_EXIT" -eq 0 ]; check "Write: exactly 120 lines allows" $?
|
||||
|
||||
P="$(payload '
|
||||
const content = "x\n".repeat(61);
|
||||
const content = "x\n".repeat(121);
|
||||
process.stdout.write(JSON.stringify({
|
||||
tool_name: "Write",
|
||||
tool_input: { file_path: "/tmp/wherever/STATE.md", content }
|
||||
}));
|
||||
')"
|
||||
run_hook "$P"
|
||||
[ "$HOOK_EXIT" -eq 2 ]; check "Write: 61 lines denies (exit 2)" $?
|
||||
printf '%s' "$HOOK_STDERR" | grep -q "61"; check "Write: denial message names the projected count" $?
|
||||
printf '%s' "$HOOK_STDERR" | grep -q "60"; check "Write: denial message names the max" $?
|
||||
[ "$HOOK_EXIT" -eq 2 ]; check "Write: 121 lines denies (exit 2)" $?
|
||||
printf '%s' "$HOOK_STDERR" | grep -q "121"; check "Write: denial message names the projected count" $?
|
||||
printf '%s' "$HOOK_STDERR" | grep -q "120"; check "Write: denial message names the max" $?
|
||||
|
||||
# --- 2. Write: only STATE.md is guarded -------------------------------------
|
||||
|
||||
|
|
@ -122,10 +122,10 @@ FIXTURE="$TMPDIR/a"
|
|||
mkdir -p "$FIXTURE"
|
||||
node -e '
|
||||
const fs = require("fs");
|
||||
fs.writeFileSync(process.argv[1], "x\n".repeat(55));
|
||||
fs.writeFileSync(process.argv[1], "x\n".repeat(115));
|
||||
' "$FIXTURE/STATE.md"
|
||||
|
||||
# 55 lines, replace one "x\n" occurrence with 6 "y\n" lines: net +5 -> 60, allow
|
||||
# 115 lines, replace one "x\n" occurrence with 6 "y\n" lines: net +5 -> 120, allow
|
||||
P="$(payload "
|
||||
process.stdout.write(JSON.stringify({
|
||||
tool_name: 'Edit',
|
||||
|
|
@ -137,9 +137,9 @@ process.stdout.write(JSON.stringify({
|
|||
}));
|
||||
")"
|
||||
run_hook "$P"
|
||||
[ "$HOOK_EXIT" -eq 0 ]; check "Edit: projected 60 lines allows" $?
|
||||
[ "$HOOK_EXIT" -eq 0 ]; check "Edit: projected 120 lines allows" $?
|
||||
|
||||
# same fixture, net +6 -> 61, deny
|
||||
# same fixture, net +6 -> 121, deny
|
||||
P="$(payload "
|
||||
process.stdout.write(JSON.stringify({
|
||||
tool_name: 'Edit',
|
||||
|
|
@ -151,8 +151,8 @@ process.stdout.write(JSON.stringify({
|
|||
}));
|
||||
")"
|
||||
run_hook "$P"
|
||||
[ "$HOOK_EXIT" -eq 2 ]; check "Edit: projected 61 lines denies" $?
|
||||
printf '%s' "$HOOK_STDERR" | grep -q "61"; check "Edit: denial message names the projected count" $?
|
||||
[ "$HOOK_EXIT" -eq 2 ]; check "Edit: projected 121 lines denies" $?
|
||||
printf '%s' "$HOOK_STDERR" | grep -q "121"; check "Edit: denial message names the projected count" $?
|
||||
|
||||
# --- 6. Edit: replace_all is honored, not just the first occurrence --------
|
||||
|
||||
|
|
@ -160,12 +160,12 @@ FIXTURE2="$TMPDIR/b"
|
|||
mkdir -p "$FIXTURE2"
|
||||
node -e '
|
||||
const fs = require("fs");
|
||||
fs.writeFileSync(process.argv[1], "a\n".repeat(50) + "b\n".repeat(5));
|
||||
fs.writeFileSync(process.argv[1], "a\n".repeat(110) + "b\n".repeat(5));
|
||||
' "$FIXTURE2/STATE.md"
|
||||
|
||||
# 55 lines total. replace_all doubles each of the 50 "a\n" occurrences
|
||||
# (a\n -> a\na\n): net +50 -> 105 lines. A hook that only replaced the FIRST
|
||||
# occurrence would project 56 lines and wrongly allow this.
|
||||
# 115 lines total. replace_all doubles each of the 110 "a\n" occurrences
|
||||
# (a\n -> a\na\n): net +110 -> 225 lines. A hook that only replaced the FIRST
|
||||
# occurrence would project 116 lines and wrongly allow this.
|
||||
P="$(payload "
|
||||
process.stdout.write(JSON.stringify({
|
||||
tool_name: 'Edit',
|
||||
|
|
@ -210,54 +210,55 @@ run_hook "$P"
|
|||
|
||||
# --- 8. Ratchet: an already-oversized file must stay editable --------------
|
||||
# The guard's job is "never let it grow past the limit", not "never let it be
|
||||
# touched again once over the limit". A file already over 60 lines is the
|
||||
# touched again once over the limit". A file already over 120 lines is the
|
||||
# NORMAL case a trim session starts from (measured on the real tree,
|
||||
# 2026-08-14: 23 of the machine's STATE.md files were over 60 lines, one at
|
||||
# 1405). Denying every write that doesn't land at <=60 in a single shot would
|
||||
# make every one of those files un-editable except by a perfect one-shot
|
||||
# rewrite - exactly backwards for a hook meant to make trimming possible.
|
||||
# 2026-08-14, at the 120-line threshold: 13 of the machine's STATE.md files
|
||||
# were over 120 lines, one at 1496). Denying every write that doesn't land at
|
||||
# <=120 in a single shot would make every one of those files un-editable
|
||||
# except by a perfect one-shot rewrite - exactly backwards for a hook meant to
|
||||
# make trimming possible.
|
||||
|
||||
FIXTURE3="$TMPDIR/c"
|
||||
mkdir -p "$FIXTURE3"
|
||||
node -e '
|
||||
const fs = require("fs");
|
||||
fs.writeFileSync(process.argv[1], "x\n".repeat(156));
|
||||
fs.writeFileSync(process.argv[1], "x\n".repeat(216));
|
||||
' "$FIXTURE3/STATE.md"
|
||||
|
||||
# Write: 156 -> 100 lines. Still over 60, but strictly smaller: allow.
|
||||
# Write: 216 -> 160 lines. Still over 120, but strictly smaller: allow.
|
||||
P="$(payload "
|
||||
process.stdout.write(JSON.stringify({
|
||||
tool_name: 'Write',
|
||||
tool_input: { file_path: '$FIXTURE3/STATE.md', content: 'x\\n'.repeat(100) }
|
||||
tool_input: { file_path: '$FIXTURE3/STATE.md', content: 'x\\n'.repeat(160) }
|
||||
}));
|
||||
")"
|
||||
run_hook "$P"
|
||||
[ "$HOOK_EXIT" -eq 0 ]; check "Write: shrinking an oversized file allows, even if still over the limit" $?
|
||||
|
||||
# Write: 156 -> 156 lines (untouched size, e.g. only prose changed): allow.
|
||||
# Write: 216 -> 216 lines (untouched size, e.g. only prose changed): allow.
|
||||
P="$(payload "
|
||||
process.stdout.write(JSON.stringify({
|
||||
tool_name: 'Write',
|
||||
tool_input: { file_path: '$FIXTURE3/STATE.md', content: 'x\\n'.repeat(156) }
|
||||
tool_input: { file_path: '$FIXTURE3/STATE.md', content: 'x\\n'.repeat(216) }
|
||||
}));
|
||||
")"
|
||||
run_hook "$P"
|
||||
[ "$HOOK_EXIT" -eq 0 ]; check "Write: same-size rewrite of an oversized file allows" $?
|
||||
|
||||
# Write: 156 -> 200 lines. Still growing an already-oversized file: deny.
|
||||
# Write: 216 -> 260 lines. Still growing an already-oversized file: deny.
|
||||
P="$(payload "
|
||||
process.stdout.write(JSON.stringify({
|
||||
tool_name: 'Write',
|
||||
tool_input: { file_path: '$FIXTURE3/STATE.md', content: 'x\\n'.repeat(200) }
|
||||
tool_input: { file_path: '$FIXTURE3/STATE.md', content: 'x\\n'.repeat(260) }
|
||||
}));
|
||||
")"
|
||||
run_hook "$P"
|
||||
[ "$HOOK_EXIT" -eq 2 ]; check "Write: growing an already-oversized file still denies" $?
|
||||
|
||||
# Write: brand-new STATE.md (no current file) at 61 lines: deny (the ratchet
|
||||
# Write: brand-new STATE.md (no current file) at 121 lines: deny (the ratchet
|
||||
# must not read "no current file" as "anything goes" -- current defaults to 0).
|
||||
P="$(payload '
|
||||
const content = "x\n".repeat(61);
|
||||
const content = "x\n".repeat(121);
|
||||
process.stdout.write(JSON.stringify({
|
||||
tool_name: "Write",
|
||||
tool_input: { file_path: "/tmp/brand-new-dir-xyz/STATE.md", content }
|
||||
|
|
@ -266,22 +267,22 @@ process.stdout.write(JSON.stringify({
|
|||
run_hook "$P"
|
||||
[ "$HOOK_EXIT" -eq 2 ]; check "Write: creating a new oversized STATE.md still denies" $?
|
||||
|
||||
# Edit: same ratchet, via the Edit path. Fixture at 156 lines; old_string is
|
||||
# the first 60 "x\n" occurrences (a contiguous substring), new_string is 4 of
|
||||
# them -> projects to 100 lines: still over 60, but smaller than 156. A
|
||||
# pre-ratchet hook denies this (100 > 60); the ratchet must allow it.
|
||||
# Edit: same ratchet, via the Edit path. Fixture at 216 lines; old_string is
|
||||
# 20 "x\n" occurrences (a contiguous substring), new_string is 4 of them ->
|
||||
# projects to 200 lines: still over 120, but smaller than 216. A pre-ratchet
|
||||
# hook denies this (200 > 120); the ratchet must allow it.
|
||||
FIXTURE4="$TMPDIR/d"
|
||||
mkdir -p "$FIXTURE4"
|
||||
node -e '
|
||||
const fs = require("fs");
|
||||
fs.writeFileSync(process.argv[1], "x\n".repeat(156));
|
||||
fs.writeFileSync(process.argv[1], "x\n".repeat(216));
|
||||
' "$FIXTURE4/STATE.md"
|
||||
P="$(payload "
|
||||
process.stdout.write(JSON.stringify({
|
||||
tool_name: 'Edit',
|
||||
tool_input: {
|
||||
file_path: '$FIXTURE4/STATE.md',
|
||||
old_string: 'x\\n'.repeat(60),
|
||||
old_string: 'x\\n'.repeat(20),
|
||||
new_string: 'x\\n'.repeat(4)
|
||||
}
|
||||
}));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue