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
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 1 addition & 2 deletions docs/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <edits> while its build was recorded here`, where `<edits>` joins with
` and ` one `edited <repository>`, or `tried to edit <repository>` 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 <repository>` 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-<pr>-<n>` reopen, read into the list
Expand Down
2 changes: 2 additions & 0 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.)
Expand Down
55 changes: 18 additions & 37 deletions hooks/build-record.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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" ] &&
Expand Down Expand Up @@ -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 <file>` 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.
Expand All @@ -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" ;;
Expand Down Expand Up @@ -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"
Expand Down
11 changes: 4 additions & 7 deletions skills/implement-change/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading
Loading