diff --git a/README.md b/README.md index 8e8fbff..7d0554a 100644 --- a/README.md +++ b/README.md @@ -110,10 +110,9 @@ Start Claude Code in the repository you are changing. The build's lock and recor `.review/` of the session's working directory. A build whose session makes or tries a Write, Edit or NotebookEdit in another repository records its gates and tokens as `not recorded` rather than as counts, naming that repository by its directory's name. Not seen: a change made there only -through shell commands, and edits made after the session's working directory moves into it. While the build's session also holds the lock, the build hook refuses -such edits in each other repository, so the person can stop or go on unrecorded, until a refused -edit there is made again; that edit and every later one there are allowed. A `pr-fix` session, with no build open, -is not refused. A repository inside the session's, such as a submodule, one the session's sits in, +through shell commands, and edits made after the session's working directory moves into it. The +build hook only marks such an edit; it never refuses one. A `pr-fix` session, with no build open, +marks nothing. A repository inside the session's, such as a submodule, one the session's sits in, a worktree of the same repository, and a repository holding Claude Code's own directory (`~/.claude`, or `CLAUDE_CONFIG_DIR`), for a file in that directory, are not another repository; a repository cloned inside that directory, such as an installed plugin, is. diff --git a/docs/contracts.md b/docs/contracts.md index eb9b85f..8bded5e 100644 --- a/docs/contracts.md +++ b/docs/contracts.md @@ -102,8 +102,7 @@ standard, its profile and the default values are both refused, or a gate pattern tried a Write, Edit or NotebookEdit in another repository while it was recorded, with the session's working directory still in the build's repository, reads `Gates: not recorded, this session also while its build was recorded here`, where `` joins with -` and ` one `edited `, or `tried to edit ` when every attempt there was -refused, per repository, named by its top-level directory's name and never by a path or remote, +` and ` one `edited ` per repository, an attempt included, named by its top-level directory's name and never by a path or remote, and `Tokens since the build started: not recorded` (version 10). A pattern a profile leaves out takes the default profile's value as every left-out key does. Optional: `Depth:`, one of light, standard or deep, read into the record's `inner.depth` (version 2); `Reopened:`, one line per reopened thread or `session--` reopen, read into the list diff --git a/docs/design.md b/docs/design.md index 1b62a3f..a80b3f9 100644 --- a/docs/design.md +++ b/docs/design.md @@ -133,6 +133,8 @@ Libera's standard. **The controlled-file check asks only on a file it finds.** `hooks/controlled-gate.sh` asked, as the secret scan once did, whenever it could not finish: a `.review/` tree past its walk limit, a glob past its limit, a payload or script output it could not read, or a machine with no `python3`. Those asks named no item, so the person had nothing to judge and approved them unread. So it asks only when it has found a `.review/` file opening with `Controlled:`, naming the item and its warning, and what it cannot reach passes. (`scripts/controlled_gate.py`; the README, "Permissions".) +**A build in another repository is marked, not stopped.** The build hook's record lives in the `.review/` of the repository the session started in, so an edit in another repository would count that work as this build's. The hook once refused the first such edit and had the person choose between stopping and going on unrecorded; the answer was always to go on, often after the person had already said the work belonged in this session. So the hook marks the build and lets the edit through, and the Already checked block reads `not recorded`, naming the repository: the record stays honest without a stop. (`hooks/build-record.sh`; the README, "Install".) + **The first merge changes nothing for a teammate who does not use the plugin.** The long pull request template, a version check requiring a bump, the measurement narrative and American spelling in libera_utils' pre-commit were kept out of it. The measurement narrative was dropped with the record store. **The corpus is private, beside public repositories.** `libera_utils` is public, and R-014 keeps internal links and internal document content out of it, while an archive entry says what went wrong, on which mission, and what the team decided. So the archive, the context links and the records live in the private `dev-tools`, and the code repository names its context entries rather than copying them. (The corpus README and the archive README in Libera's standard; R-014.) diff --git a/hooks/build-record.sh b/hooks/build-record.sh index bfbb45a..455bb8d 100755 --- a/hooks/build-record.sh +++ b/hooks/build-record.sh @@ -3,10 +3,9 @@ # rather than remembered. # # build-record.sh guard PreToolUse on Bash|Write|Edit|NotebookEdit: refuses, once, a call -# from a session whose lock was taken; refuses a write in another -# repository while this session holds the lock and its build is open, -# until a refused write there is made again; a write under .review/ -# keeps it ignored +# from a session whose lock was taken; marks the open build when its +# session writes in another repository; a write under .review/ keeps +# it ignored # build-record.sh event PostToolUse: classify one tool call, append it # build-record.sh prompt UserPromptSubmit: a typed /implement-change starts a build # build-record.sh start by hand: start or resume a build for this session, or `manual`; @@ -36,8 +35,7 @@ # repository holding Claude Code's own directory, and neither holding this one nor held by it nor # sharing its git directory as a worktree does, marks that build `elsewhere`, naming the # repository by its directory's name, and its summary then records its gates and tokens as not -# recorded. While the session also holds the lock, such a call is refused until one refused call -# there is made again, which the person's going on unrecorded is. +# recorded. The call itself goes ahead. # Events and summaries are inert until that directory has a .review/ directory; a phase # skill's start, or a write under it, creates it, and scripts/review_dir.sh keeps it out of git # where the repository's ignore rules do not. A build starts when implement-change is invoked, @@ -262,9 +260,6 @@ repo_of() { # The git directory a repository's worktrees share, symlinks resolved. common_of() { (cd "$1" 2>/dev/null && cd "$(git rev-parse --git-common-dir 2>/dev/null)" 2>/dev/null && pwd -P); } -# True when session $1 holds the lock here. -holds_lock() { [ -s .review/lock ] && [ "$(jq -r '.session // empty' .review/lock 2>/dev/null)" = "$1" ]; } - # True when session $1 is the last to start or resume the open build here. open_build_of() { [ -s "$log" ] && [ ! -e "$dir/outcome.md" ] && @@ -302,33 +297,19 @@ run_line() { printf "printf '%%s' '%s' | bash '%s' %s" "${j//\'/$r}" "${h//\'/$r}" "$2" } -# The `elsewhere` marks for top level $1, one per line: `u` for an edit allowed, `r ` for -# one refused. A line that is not JSON is skipped, so it hides no later mark. -marks_of() { - jq -Rr --arg p "$1" 'fromjson? | select(.kind == "elsewhere" and .top == $p) | if .refused then "r " + .file else "u" end' "$log" 2>/dev/null -} +# True when the log holds a readable `elsewhere` mark for top level $1. +marked() { jq -Rr --arg p "$1" 'fromjson? | select(.kind == "elsewhere" and .top == $p) | "m"' "$log" 2>/dev/null | grep -q m; } -# Session $1, whose build is open here, is writing file $3 in repository $2, not this one. While -# the session holds the lock and no edit there has been allowed, a file not refused before is -# refused: it marks the build `elsewhere` with `refused` and that file, and prints the refusal -# only once the mark reads back. Any other call, one whose refused mark could not be written or -# read back included, is allowed, and marks the build once for that repository. A mark names the repository by its directory's name for the summary, and records -# its top level and the file only in the local log. True only when it refuses. +# The session whose build is open here is writing in repository $1, not this one: mark the build +# `elsewhere` once for that repository, naming it by its directory's name for the summary and +# recording its top level only in the local log. A mark that does not read back, as one appended +# to a line cut short does not, is written once more, on the fresh line the first write ended. elsewhere() { - local marks f - f=$(resolved "$3") - marks=$(marks_of "$2") - printf '%s\n' "$marks" | grep -qx u && return 1 - if holds_lock "$1" && ! printf '%s\n' "$marks" | grep -qxF "r $f" && - { jq -cn --arg t "$(now)" --arg r "$(printf %s "${2##*/}" | tr -c 'A-Za-z0-9._-' _)" --arg p "$2" --arg f "$f" \ - '{t:$t,kind:"elsewhere",repo:$r,top:$p,file:$f,refused:true}' >>"$log"; } 2>/dev/null && - marks_of "$2" | grep -qxF "r $f"; then - jq -cn --arg c "This session holds \`.review/lock\` in $top for $(jq -r .skill .review/lock) and records its build there, but this edit is in $2, so the build's gates and tokens would count work done in another repository. Its summary now records them as not recorded. Do not go on yet: ask the person in one AskUserQuestion whether to Stop and start a session in $2 (recommended), Go on unrecorded, or discuss it. On Go on unrecorded, make this same edit again: it is not refused a second time, and later edits there are allowed." \ - '{hookSpecificOutput: {hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: $c}}' - return 0 - fi - append "$(jq -cn --arg t "$(now)" --arg r "$(printf %s "${2##*/}" | tr -c 'A-Za-z0-9._-' _)" --arg p "$2" '{t:$t,kind:"elsewhere",repo:$r,top:$p}')" - return 1 + local mark + marked "$1" && return 0 + mark=$(jq -cn --arg t "$(now)" --arg r "$(printf %s "${1##*/}" | tr -c 'A-Za-z0-9._-' _)" --arg p "$1" '{t:$t,kind:"elsewhere",repo:$r,top:$p}') + append "$mark" + marked "$1" || append "$mark" } # A hook's payload; `start`, `release` and `take` run by hand may have none. @@ -351,7 +332,7 @@ if [ "$mode" = "guard" ]; then git rev-parse --git-dir >/dev/null 2>&1 || exit 0 if [ -n "$session" ] && open_build_of "$session"; then there=$(repo_of "$file") - [ -n "$there" ] && ! config_repo "$there" "$file" && another "$there" && elsewhere "$session" "$there" "$file" && exit 0 + [ -n "$there" ] && ! config_repo "$there" "$file" && another "$there" && elsewhere "$there" fi case "$(resolved "$file")" in "$(resolved "$top")"/.review/*) bash "$review_dir" ;; @@ -563,9 +544,9 @@ if [ -f "$helper" ]; then fi gates="lint $lint · types $types · tests $tests · reviewer rounds $rounds · reviewers $reviewers · wall clock $mins min$patterns" -# A build whose session also wrote, or tried to write, in another repository may have its gates +# A build whose session also wrote in another repository may have its gates # and tokens counted here, so neither is a measurement of this build. -other=$(jq -Rc 'fromjson? | select(.kind == "elsewhere")' "$log" 2>/dev/null | jq -nr '[inputs] | group_by(.top) | map((if all(.[]; .refused) then "tried to edit " else "edited " end) + .[0].repo) | join(" and ")' 2>/dev/null) +other=$(jq -Rc 'fromjson? | select(.kind == "elsewhere")' "$log" 2>/dev/null | jq -nr '[inputs] | group_by(.top) | map("edited " + .[0].repo) | join(" and ")' 2>/dev/null) if [ -n "$other" ]; then gates="not recorded, this session also $other while its build was recorded here" tokens="not recorded" diff --git a/skills/implement-change/SKILL.md b/skills/implement-change/SKILL.md index 1011ba3..95e62fb 100644 --- a/skills/implement-change/SKILL.md +++ b/skills/implement-change/SKILL.md @@ -99,13 +99,10 @@ Before reading the source, say in a line when the person is needed (the brief, t the pull request) and that the branch, build and review loop run without them unless a stop needs them. -**Run from the repository being changed.** When the change is to another repository, say so: the -hooks record under this session's `.review/`, so the Already checked block would count the other -repository's work as this one's. Ask: **Stop**, to start a session in that repository -(recommended); **Go on unrecorded**, where the block's gates and tokens read `not recorded` once an -edit there is made; or **Let's discuss**. The build hook refuses the build's edits in each other -repository and asks the same; on **Go on unrecorded**, make the refused edit again, and it and -every later edit there are allowed. +**Run from the repository being changed.** The hooks record under this session's `.review/`, so +once the build edits another repository, the Already checked block's gates and tokens read `not +recorded`, naming that repository. When the change is to another repository, say so once and go +on; a session started in that repository records its build there. The request comes from one of these. Find a remote source's tools with a `ToolSearch` keyword search for what the tool does, never a server prefix. diff --git a/tests/test_build_record.sh b/tests/test_build_record.sh index 790f678..4420794 100644 --- a/tests/test_build_record.sh +++ b/tests/test_build_record.sh @@ -474,37 +474,33 @@ check "a write outside any repository, in a directory that exists or not, is not '[ -z "$out" ] && ! grep -q "\"kind\":\"elsewhere\"" .review/build/events.jsonl' bn=${b_top##*/} out=$(write_in "$b/src/new/x.py" s1) -check "a write in another repository while holding the lock is refused, naming it and to make the edit again" \ - 'printf "%s" "$out" | jq -e ".hookSpecificOutput.permissionDecision == \"deny\" and (.hookSpecificOutput.permissionDecisionReason | test(\"this edit is in '"$b_top"'\") and test(\"Stop\") and test(\"make this same edit again\") and (test(\"release\") | not))" >/dev/null' -want=$(jq -cn --arg r "$bn" --arg p "$b_top" --arg f "$b_top/src/new/x.py" '[$r, $p, $f, true]') -check "and marks the build here as refused, with the repository's name, top level and the file, writing no record in the other repository" \ - '[ "$(jq -c "select(.kind == \"elsewhere\") | [.repo, .top, .file, .refused]" .review/build/events.jsonl)" = "$want" ] && [ ! -e "$b/.review/build" ] && [ ! -e "$b/.review/lock" ]' +want=$(jq -cn --arg r "$bn" --arg p "$b_top" '[$r, $p, null]') +check "a write in another repository while holding the lock is allowed, marking the build here with the repository's name and top level, writing no record in the other repository" \ + '[ -z "$out" ] && [ "$(jq -c "select(.kind == \"elsewhere\") | [.repo, .top, .refused]" .review/build/events.jsonl)" = "$want" ] && [ ! -e "$b/.review/build" ] && [ ! -e "$b/.review/lock" ]' jq -cn '{tool_name:"Bash", session_id:"s1", tool_input:{command:"cd b && pytest"}}' | at_a event jq -cn '{session_id:"s1"}' | at_a summary -check "so the build's block records its gates and tokens as not recorded, naming the refused edit as tried" \ - 'grep -qx "Gates: not recorded, this session also tried to edit $bn while its build was recorded here" .review/build/already-checked.md && grep -qx "Tokens since the build started: not recorded" .review/build/already-checked.md' +check "so the build's block records its gates and tokens as not recorded, naming the edit as made" \ + 'grep -qx "Gates: not recorded, this session also edited $bn while its build was recorded here" .review/build/already-checked.md && grep -qx "Tokens since the build started: not recorded" .review/build/already-checked.md' check "and the block holds no local path of the other repository" '! grep -qF "$b_top" .review/build/already-checked.md' out=$(write_in "$b/src/y.py" s1) -check "another file there, written before the refused edit is made again, is refused too" \ - 'printf "%s" "$out" | jq -e ".hookSpecificOutput.permissionDecision == \"deny\"" >/dev/null && [ "$(grep -c "\"kind\":\"elsewhere\"" .review/build/events.jsonl)" = 2 ]' -out=$(write_in "$b/src/new/x.py" s1) -check "the refused edit made again is allowed, keeping the lock, and marks the build once more as made" \ - '[ -z "$out" ] && [ "$(jq -r .session .review/lock)" = s1 ] && [ "$(jq -c "select(.kind == \"elsewhere\") | .refused" .review/build/events.jsonl | tr "\n" " ")" = "true true null " ]' -out=$(write_in "$b/src/z.py" s1) -check "then a later edit of another file there is allowed and marks nothing more" \ - '[ -z "$out" ] && [ "$(grep -c "\"kind\":\"elsewhere\"" .review/build/events.jsonl)" = 3 ]' +check "a later edit of another file there is allowed, keeping the lock, and marks nothing more" \ + '[ -z "$out" ] && [ "$(jq -r .session .review/lock)" = s1 ] && [ "$(grep -c "\"kind\":\"elsewhere\"" .review/build/events.jsonl)" = 1 ]' +n=$(wc -l <.review/build/events.jsonl) out=$(jq -cn '{prompt:"/lasp-dev:implement-change PROJ-1", session_id:"s2"}' | at_a prompt) check "so another session's implement-change is still told the lock is held, appending nothing" \ - '[ "$(jq -r .session .review/lock)" = s1 ] && printf "%s" "$out" | grep -q "held by implement-change" && [ "$(tail -1 .review/build/events.jsonl | jq -r .kind)" = elsewhere ]' -jq -cn '{session_id:"s1"}' | at_a summary -check "and the block then names the edit as made" \ - 'grep -qx "Gates: not recorded, this session also edited $bn while its build was recorded here" .review/build/already-checked.md' + '[ "$(jq -r .session .review/lock)" = s1 ] && printf "%s" "$out" | grep -q "held by implement-change" && [ "$(wc -l <.review/build/events.jsonl)" = "$n" ]' +o=$(scratch) && made+=("$o") +o_top=$(cd "$o" && pwd -P) +jq -cn --arg t "$o_top" --arg r "${o_top##*/}" '{t:"2026-01-01T00:00:00Z",kind:"elsewhere",repo:$r,top:$t,file:"x",refused:true}' >>.review/build/events.jsonl +out=$(write_in "$o/x.py" s1) +check "an edit in a repository an older log marks as refused is allowed and marks nothing more" \ + '[ -z "$out" ] && [ "$(grep -c "\"top\":\"$o_top\"" .review/build/events.jsonl)" = 1 ]' check "the guard runs on NotebookEdit calls" \ 'jq -e "[.hooks.PreToolUse[] | select(.hooks[].command | test(\"build-record.sh.* guard\")) | .matcher | split(\"|\") | index(\"NotebookEdit\")] | .[0] != null" "$root/hooks/hooks.json" >/dev/null' nb=$(scratch) && made+=("$nb") out=$(jq -cn --arg f "$nb/n.ipynb" '{tool_name:"NotebookEdit", session_id:"s1", tool_input:{notebook_path:$f}}' | at_a guard) -check "a NotebookEdit in another repository is refused, and marks it by its directory's name" \ - 'printf "%s" "$out" | jq -e ".hookSpecificOutput.permissionDecision == \"deny\"" >/dev/null && [ "$(jq -r "select(.kind == \"elsewhere\") | .repo" .review/build/events.jsonl | tail -1)" = "${nb##*/}" ]' +check "a NotebookEdit in another repository is allowed, and marks it by its directory's name" \ + '[ -z "$out" ] && [ "$(jq -r "select(.kind == \"elsewhere\") | .repo" .review/build/events.jsonl | tail -1)" = "${nb##*/}" ]' f=$(scratch) && made+=("$f") jq -cn --arg c "$f" '{prompt:"/lasp-dev:pr-fix 1", session_id:"s3", cwd:$c}' | "$hook" prompt @@ -533,8 +529,8 @@ mkdir -p src out=$(write_at "$c/src" "$c/src/x.py") check "a session in a subdirectory writing in its own repository is not refused" '[ -z "$out" ]' out=$(write_at "$c/src" "$b/x.py") -check "and its write in another repository is refused" \ - 'printf "%s" "$out" | jq -e ".hookSpecificOutput.permissionDecision == \"deny\"" >/dev/null && grep -q "\"kind\":\"elsewhere\"" .review/build/events.jsonl' +check "and its write in another repository is allowed and marks the build" \ + '[ -z "$out" ] && grep -q "\"kind\":\"elsewhere\"" .review/build/events.jsonl' e=$(scratch) && made+=("$e") && cd "$e" || exit 1 jq -cn --arg c "$e" '{prompt:"/lasp-dev:implement-change PROJ-1", session_id:"s1", cwd:$c}' | "$hook" prompt @@ -564,8 +560,8 @@ check "and named by CLAUDE_CONFIG_DIR with a trailing slash and no HOME, it is n git init -q "$home/.claude/plugins/p" out=$(jq -cn --arg c "$e" --arg f "$home/.claude/plugins/p/x.md" '{tool_name:"Write", session_id:"s1", cwd:$c, tool_input:{file_path:$f}}' | HOME=$home CLAUDE_CONFIG_DIR="" "$hook" guard) -check "but a repository cloned inside it, as a plugin is, is another repository and refused" \ - 'printf "%s" "$out" | jq -e ".hookSpecificOutput.permissionDecision == \"deny\"" >/dev/null' +check "but a repository cloned inside it, as a plugin is, is another repository and marks the build" \ + '[ -z "$out" ] && [ "$(jq -r "select(.kind == \"elsewhere\") | .repo" .review/build/events.jsonl)" = p ]' e=$(scratch) && made+=("$e") && cd "$e" || exit 1 jq -cn --arg c "$e" '{prompt:"/lasp-dev:implement-change PROJ-1", session_id:"s1", cwd:$c}' | "$hook" prompt @@ -573,8 +569,8 @@ jq -cn --arg c "$e" '{session_id:"s1", cwd:$c}' | "$hook" release jq -cn --arg c "$e" '{prompt:"/lasp-dev:implement-change PROJ-1", session_id:"s2", cwd:$c}' | "$hook" prompt out=$(jq -cn --arg c "$e" --arg f "$b/x.py" '{tool_name:"Write", session_id:"s2", cwd:$c, tool_input:{file_path:$f}}' | "$hook" guard) jq -cn --arg c "$e" '{session_id:"s2", cwd:$c}' | "$hook" summary -check "a second session that resumed the build is refused in another repository, and the build reads not recorded" \ - '[ "$(tail -2 .review/build/events.jsonl | head -1 | jq -r ".kind + \" \" + .session")" = "resume s2" ] && printf "%s" "$out" | jq -e ".hookSpecificOutput.permissionDecision == \"deny\"" >/dev/null && grep -q "^Gates: not recorded" .review/build/already-checked.md' +check "a second session that resumed the build marks it from another repository, and the build reads not recorded" \ + '[ "$(tail -2 .review/build/events.jsonl | head -1 | jq -r ".kind + \" \" + .session")" = "resume s2" ] && [ -z "$out" ] && grep -q "^Gates: not recorded" .review/build/already-checked.md' # A build's repository names, from remotes that carry a credential, a user name or a local path. e=$(scratch) && made+=("$e") && cd "$e" || exit 1 @@ -589,7 +585,7 @@ for u in "https://user:ghp_SECRET@github.com/owner/name/" "https://github.com/ow done jq -cn --arg c "$e" '{session_id:"s1", cwd:$c}' | "$hook" summary check "a repository is named by its directory alone, never a remote's credential, user name or a local path" \ - '! grep -qE "ghp_SECRET|tok@|alicex9|ownerlogin|owner/name|github" .review/build/already-checked.md && ! printf "%s" "$tops" | grep -F -f - .review/build/already-checked.md >/dev/null && printf "%s" "$tops" | while read -r t; do grep -qF "tried to edit ${t##*/}" .review/build/already-checked.md || exit 1; done' + '! grep -qE "ghp_SECRET|tok@|alicex9|ownerlogin|owner/name|github" .review/build/already-checked.md && ! printf "%s" "$tops" | grep -F -f - .review/build/already-checked.md >/dev/null && printf "%s" "$tops" | while read -r t; do grep -qF "edited ${t##*/}" .review/build/already-checked.md || exit 1; done' e=$(scratch) && made+=("$e") && cd "$e" || exit 1 jq -cn --arg c "$e" '{prompt:"/lasp-dev:implement-change PROJ-1", session_id:"s1", cwd:$c}' | "$hook" prompt @@ -597,22 +593,21 @@ q=$(mktemp -d) && made+=("$q") && git init -q "$q/odd " 2>/dev/null write_at "$e" "$q/odd /x.py" >/dev/null jq -cn --arg c "$e" '{session_id:"s1", cwd:$c}' | "$hook" summary check "a repository's name keeps only letters, digits, dot, dash and underscore in the block" \ - 'grep -qx "Gates: not recorded, this session also tried to edit odd__name_ while its build was recorded here" .review/build/already-checked.md' + 'grep -qx "Gates: not recorded, this session also edited odd__name_ while its build was recorded here" .review/build/already-checked.md' e=$(scratch) && made+=("$e") && cd "$e" || exit 1 q=$(mktemp -d) && made+=("$q") && git init -q "$q/one/same" && git init -q "$q/two/same" jq -cn --arg c "$e" '{prompt:"/lasp-dev:implement-change PROJ-1", session_id:"s1", cwd:$c}' | "$hook" prompt -write_at "$e" "$q/one/same/x.py" >/dev/null out=$(write_at "$e" "$q/one/same/x.py")$(write_at "$e" "$q/two/same/x.py") -check "a refusal made good in one repository lets nothing through in another with the same name" \ - 'printf "%s" "$out" | jq -e ".hookSpecificOutput.permissionDecision == \"deny\"" >/dev/null' +check "two repositories with the same name are each marked" \ + '[ -z "$out" ] && [ "$(jq -r "select(.kind == \"elsewhere\") | .top" .review/build/events.jsonl | sort -u | wc -l | tr -d " ")" = 2 ]' e=$(scratch) && made+=("$e") && cd "$e" || exit 1 jq -cn --arg c "$e" '{prompt:"/lasp-dev:implement-change PROJ-1", session_id:"s1", cwd:$c}' | "$hook" prompt chmod 444 .review/build/events.jsonl out=$(write_at "$e" "$b/x.py" 2>/dev/null)$(write_at "$e" "$b/x.py" 2>/dev/null) chmod 644 .review/build/events.jsonl -check "with a log that cannot be written, a write in another repository is allowed, never refused unrecorded" '[ -z "$out" ]' +check "with a log that cannot be written, a write in another repository is allowed" '[ -z "$out" ]' e=$(scratch) && made+=("$e") && cd "$e" || exit 1 jq -cn --arg c "$e" '{prompt:"/lasp-dev:implement-change PROJ-1", session_id:"s1", cwd:$c}' | "$hook" prompt @@ -620,15 +615,15 @@ printf '{"t":\n' >>.review/build/events.jsonl out=$(write_at "$e" "$b/x.py") out2=$(write_at "$e" "$b/x.py") jq -cn --arg c "$e" '{session_id:"s1", cwd:$c}' | "$hook" summary -check "a line that is not JSON hides no later mark: the refused edit made again is allowed, and the block reads it" \ - 'printf "%s" "$out" | jq -e ".hookSpecificOutput.permissionDecision == \"deny\"" >/dev/null && [ -z "$out2" ] && grep -q "^Gates: not recorded, this session also edited $bn " .review/build/already-checked.md' +check "a line that is not JSON hides no later mark: both edits are allowed, marked once, and the block reads it" \ + '[ -z "$out$out2" ] && [ "$(grep -c "\"kind\":\"elsewhere\"" .review/build/events.jsonl)" = 1 ] && grep -q "^Gates: not recorded, this session also edited $bn " .review/build/already-checked.md' e=$(scratch) && made+=("$e") && cd "$e" || exit 1 jq -cn --arg c "$e" '{prompt:"/lasp-dev:implement-change PROJ-1", session_id:"s1", cwd:$c}' | "$hook" prompt printf '{"t":' >>.review/build/events.jsonl out=$(write_at "$e" "$b/x.py") jq -cn --arg c "$e" '{session_id:"s1", cwd:$c}' | "$hook" summary -check "a refusal whose mark cannot be read back is not printed, and the write is marked as made" \ +check "a mark appended to a line cut short is written again, and the block reads the edit as made" \ '[ -z "$out" ] && grep -q "^Gates: not recorded, this session also edited $bn " .review/build/already-checked.md' # The take-over and release lines run as given from a plugin and a repository whose paths hold a