From c8562718d99c893591ac2ab325718e17f77998a0 Mon Sep 17 00:00:00 2001 From: Negrescu Alexandru Date: Sun, 25 Oct 2026 12:58:10 +0200 Subject: [PATCH 01/14] test(worktree-audit): count sessions started in a worktree subdirectory (fails until the fix) omp files a session under the bucket of its own cwd, so a chat started in /src never reaches the audit and a merged worktree reads as safe. The live probe asks omp for that path from a home and an absolute subdirectory. --- omp-port/test-worktree-audit.sh | 49 +++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/omp-port/test-worktree-audit.sh b/omp-port/test-worktree-audit.sh index ab5732f..b019ce6 100755 --- a/omp-port/test-worktree-audit.sh +++ b/omp-port/test-worktree-audit.sh @@ -64,7 +64,7 @@ commit() { git -C "$1" rev-parse HEAD } for name in open draft closed-pushed closed-unpushed squash local-only untracked ignored tracked \ - locked broken sibling unrelated plain; do + locked broken sibling unrelated plain subdir; do wt "$name" "$CH/$name" done RECENT='recent [one].worktree' @@ -72,6 +72,7 @@ wt recent "$CH/$RECENT" wt nested "$CH/nest/deep:wt" wt temp "$CT/temp:wt" wt absolute "$FIXTURE/abs:wt" +wt sub-absolute "$FIXTURE/sub:abs" g "$REPO" worktree add -q --detach "$CH/detached" main OPEN_HEAD=$(git -C "$CH/open" rev-parse HEAD) @@ -96,7 +97,7 @@ printf 'log\n' >"$CH/ignored/run.log" printf 'edit\n' >>"$CH/tracked/tracked" g "$REPO" worktree lock --reason 'keep this worktree' "$CH/locked" printf 'gitdir: %s/missing\n' "$FIXTURE" >"$CH/broken/.git" -ROWS=19 +ROWS=21 # A closed PR on a reused branch name sits first, so the audit has to prefer the PR whose head is # HEAD. gh reports uppercase states; the audit lowercases them. @@ -153,6 +154,11 @@ printf '{"type":"message","path":"%s/notes.md"}\n' "$CH/sibling" >>"$SESSIONS/-r chat "$SESSIONS/-nest-deep-wt" own "$CH/nest/deep:wt" chat "$SESSIONS/-tmp-temp-wt" own "$CT/temp:wt" chat "$SESSIONS/$ABS_BUCKET" own "$FIXTURE/abs:wt" +# A session started in a subdirectory sits in that subdirectory's bucket, for a home and an +# absolute worktree alike. A bucket that only shares a name prefix with a worktree is no chat in it. +chat "$SESSIONS/-subdir-src" sub "$CH/subdir/src" +chat "$SESSIONS/--$(printf '%s' "${FIXTURE#/}" | tr '/' '-')-sub-abs-src--" sub "$FIXTURE/sub:abs/src" +chat "$SESSIONS/-plain-old" old "$CH/plain-old" chat "$SESSIONS/-another-project" other "$CH/unrelated" TODAY=$(date '+%Y-%m-%d') @@ -217,7 +223,7 @@ expect plain LOCKED - expect plain INSPECT ok expect plain LAST_CHAT - expect plain BUCKET safe -for name in "$RECENT" sibling deep:wt temp:wt abs:wt; do +for name in "$RECENT" sibling deep:wt temp:wt abs:wt subdir sub:abs; do expect "$name" LAST_CHAT "$TODAY" expect "$name" BUCKET verify-recent-chat done @@ -304,28 +310,35 @@ audit no-tmpdir -u TMPDIR [ ! -e "$REPO/.pstack-scratch" ] || fail "scratch written inside the repo: $REPO/.pstack-scratch" [ -z "$(git -C "$REPO" status --porcelain --ignored)" ] || fail "the audit left files in the repo" -# omp names the transcript it would write for a worktree without a model turn. A chat written at -# that path has to reach the audit for a home, a temp and an absolute worktree. +# omp names the transcript it would write for a cwd without a model turn. A chat written at that +# path has to reach the audit for a home, a temp and an absolute worktree, and for a session started +# in a subdirectory of one. +probe() { + local file + wt "$1" "$2" + mkdir -p "$3" + if ! file=$(cd "$3" && printf '%s\n' '{"id":"s","type":"get_state"}' | + env -i HOME="$HOME" TMPDIR="$TMPDIR" PATH="$PATH" omp --mode rpc --no-title --no-lsp --no-tools \ + --no-rules --no-extensions --no-skills --model probe/no-inference "${@:4}" 2>"$FIXTURE/live-omp.err" | + jq -r 'select(.type == "response" and .id == "s") | .data.sessionFile // empty') || [ -z "$file" ]; then + fail "omp reported no session file for $3: $(tr '\n' ' ' <"$FIXTURE/live-omp.err")" + return + fi + chat "$(dirname "$file")" "$(basename "$file" .jsonl)" "$3" +} if ! command -v omp >/dev/null 2>&1; then printf 'worktree audit: live omp probe SKIPPED, omp is not on PATH\n' else LABEL=live-omp printf '%s\n' providers: ' probe:' ' baseUrl: http://127.0.0.1:9/v1' ' auth: none' \ ' api: openai-completions' ' models:' ' - id: no-inference' >"$HOME/.omp/agent/models.yml" - for spec in "probe-home:$CH/probe home" "probe-temp:$CT/probe:temp" "probe-abs:$FIXTURE/probe:abs"; do - path=${spec#*:} - wt "${spec%%:*}" "$path" - if ! file=$(cd "$path" && printf '%s\n' '{"id":"s","type":"get_state"}' | - env -i HOME="$HOME" TMPDIR="$TMPDIR" PATH="$PATH" omp --mode rpc --no-title --no-lsp \ - --no-tools --no-rules --no-extensions --no-skills --model probe/no-inference 2>"$FIXTURE/live-omp.err" | - jq -r 'select(.type == "response" and .id == "s") | .data.sessionFile // empty') || [ -z "$file" ]; then - fail "omp reported no session file for $path: $(tr '\n' ' ' <"$FIXTURE/live-omp.err")" - continue - fi - chat "$(dirname "$file")" "$(basename "$file" .jsonl)" "$path" - done + probe probe-home "$CH/probe home" "$CH/probe home" + probe probe-temp "$CT/probe:temp" "$CT/probe:temp" + probe probe-abs "$FIXTURE/probe:abs" "$FIXTURE/probe:abs" + probe probe-sub "$CH/probe sub" "$CH/probe sub/src" + probe probe-sub-abs "$FIXTURE/probe:sub" "$FIXTURE/probe:sub/src" audit live-omp - for name in "probe home" probe:temp probe:abs; do + for name in "probe home" probe:temp probe:abs "probe sub" probe:sub; do expect "$name" LAST_CHAT "$TODAY" done fi From 09f4c8e59554973c41ac15e2139d925bb23098c6 Mon Sep 17 00:00:00 2001 From: Negrescu Alexandru Date: Sun, 25 Oct 2026 12:59:38 +0200 Subject: [PATCH 02/14] fix(worktree-audit): scan the buckets of a worktree's subdirectories omp names a session's bucket after its own cwd, so a session started in /src sits in -src, or ---src-- outside home and temp. The audit now also scans every existing bucket that matches the worktree's encoded name with a subdirectory appended, built by the same encoder. A transcript must still name the worktree path, so a sibling such as foo-bar that shares foo's prefix costs one more grep and never a match. The main worktree is skipped, since a repo at $HOME would turn the prefix into the whole store. --- omp-port/patches/worktree-audit-omp.patch | 29 ++++++++++++++----- .../poteto-mode/scripts/worktree-audit.sh | 21 +++++++++++--- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/omp-port/patches/worktree-audit-omp.patch b/omp-port/patches/worktree-audit-omp.patch index 12f4218..c9eddf8 100644 --- a/omp-port/patches/worktree-audit-omp.patch +++ b/omp-port/patches/worktree-audit-omp.patch @@ -10,6 +10,8 @@ # root, checked first, - under home, ---- otherwise, / \ : mapped to -) # - scan only the buckets of this repo's worktrees, never the whole store, with grep -F, so # ripgrep is not required and a path with [ or spaces matches literally +# - scan each linked worktree's subdirectory buckets too, found by the bucket-name prefix, since +# omp files a session started in /src under that cwd's own bucket # - fail closed: a store that cannot be found or read makes LAST_CHAT "?" and holds the row at # verify-recent-chat, the bucket playbooks/worktree-cleanup.md already names, never `safe` # - fail closed the same way when a .env file omp reads (each worktree's, the agent dir's, the @@ -54,7 +56,7 @@ if [ -z "$base" ]; then for cand in "$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null)" origin/main origin/master; do -@@ -55,33 +60,105 @@ +@@ -55,33 +60,118 @@ stat_fmt=(-f '%m %N'); fmt_day() { date -r "$1" '+%Y-%m-%d'; } fi @@ -89,9 +91,10 @@ +# PI_PROFILE) under ~/${PI_CONFIG_DIR:-.omp}, PI_CODING_AGENT_DIR when no +# profile is set, or an existing XDG_DATA_HOME/omp when the agent dir is the +# default, and it holds one bucket per canonical cwd. Only the buckets of this -+# repo's worktrees are scanned; walking every bucket would scale with total -+# session history. A store that cannot be read makes LAST_CHAT "?" and holds -+# the row at verify-recent-chat instead of reading as no chat at all. ++# repo's worktrees and their subdirectories are scanned; walking every bucket ++# would scale with total session history. A store that cannot be read makes ++# LAST_CHAT "?" and holds the row at verify-recent-chat instead of reading as ++# no chat at all. +# omp also fills each unset variable from the .env in the cwd it launched in, +# then the agent dir's, the config root's and home's, reading an OMP_ key there +# as its PI_ alias, while the profile comes from the environment alone. The @@ -136,8 +139,20 @@ + temp_root=$(canon "${TMPDIR:-${TMP:-${TEMP:-/tmp}}}") + dotenvs=("$agent/.env" "$config/.env" "$HOME/.env") + while IFS= read -r path; do -+ dir="$sessions/$(bucket_name "$(canon "$path")")" ++ cpath=$(canon "$path") ++ dir="$sessions/$(bucket_name "$cpath")" + [ -d "$dir" ] && session_dirs+=("$dir/") ++ # A session started in a subdirectory has that subdirectory's bucket, ++ # the worktree's name plus "-" ahead of any closing "--". A ++ # transcript still has to name the worktree path, so a bucket that ++ # only shares the prefix costs time, never a match. The main worktree ++ # is skipped, since a repo at $HOME would make this the whole store. ++ if [ "$path" != "$main_wt" ]; then ++ sub=$(bucket_name "$cpath/*") ++ for dir in "$sessions/${sub%\**}"*"${sub##*\*}"; do ++ [ -d "$dir" ] && session_dirs+=("$dir/") ++ done ++ fi + dotenvs+=("$path/.env") + done < <(printf '%s\n' "$worktree_records" | awk '/^worktree /{sub(/^worktree /, ""); print}') + moved=$(grep -lsE '^[[:space:]]*(export[[:space:]]+)?((PI|OMP)_(CODING_AGENT_SESSION_DIR|CODING_AGENT_DIR|CONFIG_DIR)|XDG_DATA_HOME)[[:space:]]*[=:]' \ @@ -174,7 +189,7 @@ head_ts=$(git -C "$wt" log -1 --format='%ct' HEAD 2>/dev/null || echo 0) age=$([ "$head_ts" -gt 0 ] 2>/dev/null && echo "$(( (now - head_ts) / 86400 ))d" || echo "?") -@@ -89,12 +166,12 @@ +@@ -89,12 +179,12 @@ # real signal; merge-base only catches fast-forward/rebase merges. git merge-base --is-ancestor "$head" "$base" 2>/dev/null && merged=YES || merged=no @@ -193,7 +208,7 @@ branch=$(git -C "$wt" symbolic-ref --quiet --short HEAD 2>/dev/null || echo "") if [ -z "$branch" ]; then remote=detached -@@ -102,33 +179,57 @@ +@@ -102,33 +192,57 @@ [ "$(git -C "$wt" rev-parse "origin/$branch" 2>/dev/null)" = "$head" ] \ && remote=pushed \ || remote="ahead$(git -C "$wt" rev-list --count "origin/$branch..HEAD" 2>/dev/null)" diff --git a/plugins/pstack/skills/poteto-mode/scripts/worktree-audit.sh b/plugins/pstack/skills/poteto-mode/scripts/worktree-audit.sh index dc3dbd7..edf5d3d 100755 --- a/plugins/pstack/skills/poteto-mode/scripts/worktree-audit.sh +++ b/plugins/pstack/skills/poteto-mode/scripts/worktree-audit.sh @@ -80,9 +80,10 @@ fi # PI_PROFILE) under ~/${PI_CONFIG_DIR:-.omp}, PI_CODING_AGENT_DIR when no # profile is set, or an existing XDG_DATA_HOME/omp when the agent dir is the # default, and it holds one bucket per canonical cwd. Only the buckets of this -# repo's worktrees are scanned; walking every bucket would scale with total -# session history. A store that cannot be read makes LAST_CHAT "?" and holds -# the row at verify-recent-chat instead of reading as no chat at all. +# repo's worktrees and their subdirectories are scanned; walking every bucket +# would scale with total session history. A store that cannot be read makes +# LAST_CHAT "?" and holds the row at verify-recent-chat instead of reading as +# no chat at all. # omp also fills each unset variable from the .env in the cwd it launched in, # then the agent dir's, the config root's and home's, reading an OMP_ key there # as its PI_ alias, while the profile comes from the environment alone. The @@ -127,8 +128,20 @@ else temp_root=$(canon "${TMPDIR:-${TMP:-${TEMP:-/tmp}}}") dotenvs=("$agent/.env" "$config/.env" "$HOME/.env") while IFS= read -r path; do - dir="$sessions/$(bucket_name "$(canon "$path")")" + cpath=$(canon "$path") + dir="$sessions/$(bucket_name "$cpath")" [ -d "$dir" ] && session_dirs+=("$dir/") + # A session started in a subdirectory has that subdirectory's bucket, + # the worktree's name plus "-" ahead of any closing "--". A + # transcript still has to name the worktree path, so a bucket that + # only shares the prefix costs time, never a match. The main worktree + # is skipped, since a repo at $HOME would make this the whole store. + if [ "$path" != "$main_wt" ]; then + sub=$(bucket_name "$cpath/*") + for dir in "$sessions/${sub%\**}"*"${sub##*\*}"; do + [ -d "$dir" ] && session_dirs+=("$dir/") + done + fi dotenvs+=("$path/.env") done < <(printf '%s\n' "$worktree_records" | awk '/^worktree /{sub(/^worktree /, ""); print}') moved=$(grep -lsE '^[[:space:]]*(export[[:space:]]+)?((PI|OMP)_(CODING_AGENT_SESSION_DIR|CODING_AGENT_DIR|CONFIG_DIR)|XDG_DATA_HOME)[[:space:]]*[=:]' \ From f697a93debe1b2b2557d4d2952640666cfd77b4b Mon Sep 17 00:00:00 2001 From: Negrescu Alexandru Date: Sun, 25 Oct 2026 13:01:27 +0200 Subject: [PATCH 03/14] test(worktree-audit): count sessions launched with --profile or --session-dir (fails until the fix) Both flags move a transcript out of the store the audit resolves from its own environment. Each leaves an on-disk trace: a profile writes under /profiles/, and omp records the absolute path of every transcript outside the bucket store in the agent dir's custom-session-files. The live probe launches omp with each flag. The stores the audit's environment replaces still count, so the RECENT assertions under an explicit dir, a profile and XDG_DATA_HOME flip. --- omp-port/test-worktree-audit.sh | 56 +++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/omp-port/test-worktree-audit.sh b/omp-port/test-worktree-audit.sh index b019ce6..4dc7bcf 100755 --- a/omp-port/test-worktree-audit.sh +++ b/omp-port/test-worktree-audit.sh @@ -244,25 +244,57 @@ for name in "$RECENT" deep:wt temp:wt abs:wt; do expect "$name" BUCKET verify-recent-chat done -# An explicit session directory holds transcripts directly, with no cwd bucket under it. +# An explicit session directory holds transcripts directly, with no cwd bucket under it. The bucket +# store still counts, since a session launched without the variable lands there. chat "$FIXTURE/direct sessions" direct "$CH/plain" audit explicit-dir PI_CODING_AGENT_SESSION_DIR="$FIXTURE/direct sessions" expect plain LAST_CHAT "$TODAY" expect plain BUCKET verify-recent-chat -expect "$RECENT" LAST_CHAT - +expect "$RECENT" LAST_CHAT "$TODAY" -# A profile owns its own store and outranks PI_CODING_AGENT_DIR. +# A session launched with --profile work sits in that profile's store, which an audit under any +# profile reads. The audit's own profile names the store that has to exist, and under a profile +# PI_CODING_AGENT_DIR does not name it. chat "$HOME/.omp/profiles/work/agent/sessions/-plain" profile "$CH/plain" -mkdir -p "$FIXTURE/decoy/sessions" +audit other-profile +expect plain LAST_CHAT "$TODAY" +expect plain BUCKET verify-recent-chat audit profile OMP_PROFILE=work PI_CODING_AGENT_DIR="$FIXTURE/decoy" expect plain LAST_CHAT "$TODAY" -expect "$RECENT" LAST_CHAT - +expect "$RECENT" LAST_CHAT "$TODAY" +rm -r "$HOME/.omp/profiles" -# An existing XDG_DATA_HOME/omp replaces the default data root, with no agent/ under it. +# An existing XDG_DATA_HOME/omp replaces the default data root, with no agent/ under it. The store +# it replaced still counts. chat "$FIXTURE/xdg/omp/sessions/-plain" xdg "$CH/plain" audit xdg XDG_DATA_HOME="$FIXTURE/xdg" expect plain LAST_CHAT "$TODAY" -expect "$RECENT" LAST_CHAT - +expect "$RECENT" LAST_CHAT "$TODAY" + +# omp lists each transcript it writes outside the bucket store, from --session-dir, an explicit +# PI_CODING_AGENT_SESSION_DIR or a --resume of a moved file, as one absolute path per file in the +# agent dir's custom-session-files, or XDG_STATE_HOME/omp's when that exists. A subagent transcript +# sits in the directory named after its parent. A listed transcript that is gone is no chat, and an +# index that cannot be read is unknown. +INDEX="$HOME/.omp/agent/custom-session-files" +chat "$FIXTURE/custom dir" custom "$FIXTURE" +chat "$FIXTURE/custom dir/custom" Child "$CH/plain" +mkdir -p "$INDEX" +printf '%s' "$FIXTURE/custom dir/custom.jsonl" >"$INDEX/0a1b2c3" +printf '%s' "$FIXTURE/gone/x.jsonl" >"$INDEX/4d5e6f7" +audit custom-dir +expect plain LAST_CHAT "$TODAY" +expect plain BUCKET verify-recent-chat +if [ "$(id -u)" != 0 ]; then + chmod 000 "$INDEX" + audit unreadable-index + expect plain LAST_CHAT '?' + chmod 755 "$INDEX" +fi +mkdir -p "$FIXTURE/state/omp" +mv "$INDEX" "$FIXTURE/state/omp/" +audit state-index XDG_STATE_HOME="$FIXTURE/state" +expect plain LAST_CHAT "$TODAY" # omp fills each unset variable from four .env files, the launch directory's, the agent # directory's, the config root's and the home directory's, and reads an OMP_ key in one as its PI_ @@ -311,8 +343,8 @@ audit no-tmpdir -u TMPDIR [ -z "$(git -C "$REPO" status --porcelain --ignored)" ] || fail "the audit left files in the repo" # omp names the transcript it would write for a cwd without a model turn. A chat written at that -# path has to reach the audit for a home, a temp and an absolute worktree, and for a session started -# in a subdirectory of one. +# path has to reach the audit for a home, a temp and an absolute worktree, for a session started in +# a subdirectory of one, and for one launched with --profile or --session-dir. probe() { local file wt "$1" "$2" @@ -332,13 +364,17 @@ else LABEL=live-omp printf '%s\n' providers: ' probe:' ' baseUrl: http://127.0.0.1:9/v1' ' auth: none' \ ' api: openai-completions' ' models:' ' - id: no-inference' >"$HOME/.omp/agent/models.yml" + mkdir -p "$HOME/.omp/profiles/live/agent" + cp "$HOME/.omp/agent/models.yml" "$HOME/.omp/profiles/live/agent/" probe probe-home "$CH/probe home" "$CH/probe home" probe probe-temp "$CT/probe:temp" "$CT/probe:temp" probe probe-abs "$FIXTURE/probe:abs" "$FIXTURE/probe:abs" probe probe-sub "$CH/probe sub" "$CH/probe sub/src" probe probe-sub-abs "$FIXTURE/probe:sub" "$FIXTURE/probe:sub/src" + probe probe-profile "$CH/probe profile" "$CH/probe profile" --profile live + probe probe-session-dir "$CH/probe sdir" "$CH/probe sdir" --session-dir "$FIXTURE/live sessions" audit live-omp - for name in "probe home" probe:temp probe:abs "probe sub" probe:sub; do + for name in "probe home" probe:temp probe:abs "probe sub" probe:sub "probe profile" "probe sdir"; do expect "$name" LAST_CHAT "$TODAY" done fi From 1a2f7ae35c7e1760c22dd0f85ba6e98af7ac4529 Mon Sep 17 00:00:00 2001 From: Negrescu Alexandru Date: Sun, 25 Oct 2026 13:07:26 +0200 Subject: [PATCH 04/14] test(worktree-audit): keep omp's own index out of the live bucket checks Under a symlinked HOME omp lists every transcript, bucket ones included, in custom-session-files, so an audit that reads the index found the subdirectory and profile probes without scanning their buckets. The live probe drops that index before the bucket audit and runs the --session-dir probe after it, so each rule is proven alone. --- omp-port/test-worktree-audit.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/omp-port/test-worktree-audit.sh b/omp-port/test-worktree-audit.sh index 4dc7bcf..02c127b 100755 --- a/omp-port/test-worktree-audit.sh +++ b/omp-port/test-worktree-audit.sh @@ -372,11 +372,16 @@ else probe probe-sub "$CH/probe sub" "$CH/probe sub/src" probe probe-sub-abs "$FIXTURE/probe:sub" "$FIXTURE/probe:sub/src" probe probe-profile "$CH/probe profile" "$CH/probe profile" --profile live - probe probe-session-dir "$CH/probe sdir" "$CH/probe sdir" --session-dir "$FIXTURE/live sessions" + # Under a symlinked HOME omp also lists each bucket transcript in custom-session-files, which would + # hide whether the audit finds the bucket itself. + rm -rf "$HOME/.omp/agent/custom-session-files" "$HOME/.omp/profiles/live/agent/custom-session-files" audit live-omp - for name in "probe home" probe:temp probe:abs "probe sub" probe:sub "probe profile" "probe sdir"; do + for name in "probe home" probe:temp probe:abs "probe sub" probe:sub "probe profile"; do expect "$name" LAST_CHAT "$TODAY" done + probe probe-session-dir "$CH/probe sdir" "$CH/probe sdir" --session-dir "$FIXTURE/live sessions" + audit live-omp-session-dir + expect "probe sdir" LAST_CHAT "$TODAY" fi if [ "$FAILS" -gt 0 ]; then From 7859feb430cd87d98bef26bdee80ed6290b5076f Mon Sep 17 00:00:00 2001 From: Negrescu Alexandru Date: Sun, 25 Oct 2026 13:08:07 +0200 Subject: [PATCH 05/14] fix(worktree-audit): count sessions launched with --profile or --session-dir Neither flag is visible to the audit's environment, but both leave a bounded trace on disk. Each profile's store sits under /profiles/, and omp lists the absolute path of every transcript it writes outside the bucket store, from --session-dir, PI_CODING_AGENT_SESSION_DIR or a --resume of a moved file, in the agent dir's custom-session-files, or XDG_STATE_HOME/omp's when that exists. The audit now scans the worktree buckets of every store under the config root and the XDG roots and every transcript the indexes list, with its subagent directory. The store the environment resolves still has to exist. A listed transcript that is gone holds no chat, and an index that cannot be read makes LAST_CHAT ?. --- omp-port/patches/worktree-audit-omp.patch | 91 +++++++++++++------ .../poteto-mode/scripts/worktree-audit.sh | 74 ++++++++++----- 2 files changed, 116 insertions(+), 49 deletions(-) diff --git a/omp-port/patches/worktree-audit-omp.patch b/omp-port/patches/worktree-audit-omp.patch index c9eddf8..c716c70 100644 --- a/omp-port/patches/worktree-audit-omp.patch +++ b/omp-port/patches/worktree-audit-omp.patch @@ -12,8 +12,15 @@ # ripgrep is not required and a path with [ or spaces matches literally # - scan each linked worktree's subdirectory buckets too, found by the bucket-name prefix, since # omp files a session started in /src under that cwd's own bucket -# - fail closed: a store that cannot be found or read makes LAST_CHAT "?" and holds the row at -# verify-recent-chat, the bucket playbooks/worktree-cleanup.md already names, never `safe` +# - scan those buckets in every store on disk, not only the one the audit's environment names: +# the default agent dir, each profile's, PI_CODING_AGENT_DIR, and the XDG data and state +# roots, so a session launched with --profile still counts +# - read each store's custom-session-files, where omp lists the absolute path of every +# transcript written outside the bucket store (--session-dir, PI_CODING_AGENT_SESSION_DIR, +# --resume of a moved file), and scan those transcripts and their subagent directories +# - fail closed: a store that cannot be found or read, or an index that cannot be read, makes +# LAST_CHAT "?" and holds the row at verify-recent-chat, the bucket +# playbooks/worktree-cleanup.md already names, never `safe` # - fail closed the same way when a .env file omp reads (each worktree's, the agent dir's, the # config root's, home's) sets a key that moves the store, OMP_ spellings included, rather # than replaying omp's .env loader @@ -56,7 +63,7 @@ if [ -z "$base" ]; then for cand in "$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null)" origin/main origin/master; do -@@ -55,33 +60,118 @@ +@@ -55,33 +60,148 @@ stat_fmt=(-f '%m %N'); fmt_day() { date -r "$1" '+%Y-%m-%d'; } fi @@ -121,43 +128,70 @@ + else printf '%s' "--$(dash "${1#/}")--"; fi +} +session_dirs=() -+moved="" ++dotenvs=() ++config_root="$HOME/${PI_CONFIG_DIR:-.omp}" +if [ -n "${PI_CODING_AGENT_SESSION_DIR:-}" ]; then + sessions=$PI_CODING_AGENT_SESSION_DIR + session_dirs=("$sessions/") +else + profile=$(printf '%s' "${OMP_PROFILE-${PI_PROFILE-}}" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + [ "$profile" = default ] && profile="" -+ config="$HOME/${PI_CONFIG_DIR:-.omp}${profile:+/profiles/$profile}" ++ config="$config_root${profile:+/profiles/$profile}" + agent="$config/agent" + [ -z "$profile" ] && [ -n "${PI_CODING_AGENT_DIR:-}" ] && agent=$PI_CODING_AGENT_DIR + data=$agent + xdg="${XDG_DATA_HOME:-}/omp${profile:+/profiles/$profile}" + [ "$agent" = "$config/agent" ] && [ -n "${XDG_DATA_HOME:-}" ] && [ -e "$xdg" ] && data=$xdg + sessions="$data/sessions" -+ home_root=$(canon "$HOME") -+ temp_root=$(canon "${TMPDIR:-${TMP:-${TEMP:-/tmp}}}") + dotenvs=("$agent/.env" "$config/.env" "$HOME/.env") -+ while IFS= read -r path; do -+ cpath=$(canon "$path") -+ dir="$sessions/$(bucket_name "$cpath")" -+ [ -d "$dir" ] && session_dirs+=("$dir/") -+ # A session started in a subdirectory has that subdirectory's bucket, -+ # the worktree's name plus "-" ahead of any closing "--". A -+ # transcript still has to name the worktree path, so a bucket that -+ # only shares the prefix costs time, never a match. The main worktree -+ # is skipped, since a repo at $HOME would make this the whole store. -+ if [ "$path" != "$main_wt" ]; then -+ sub=$(bucket_name "$cpath/*") -+ for dir in "$sessions/${sub%\**}"*"${sub##*\*}"; do -+ [ -d "$dir" ] && session_dirs+=("$dir/") -+ done -+ fi -+ dotenvs+=("$path/.env") -+ done < <(printf '%s\n' "$worktree_records" | awk '/^worktree /{sub(/^worktree /, ""); print}') ++fi ++# A session launched with --profile, --session-dir or another environment is ++# not in that store. Every store under the config root and the XDG roots counts ++# too, and each one's custom-session-files lists the absolute path of every ++# transcript omp wrote outside the bucket store. Precedence does not matter ++# for these, since a transcript only counts when it names the worktree. ++home_root=$(canon "$HOME") ++temp_root=$(canon "${TMPDIR:-${TMP:-${TEMP:-/tmp}}}") ++roots=("$config_root/agent" "$config_root"/profiles/*/agent) ++for x in "${XDG_DATA_HOME:-}" "${XDG_STATE_HOME:-}"; do ++ [ -n "$x" ] && roots+=("$x/omp" "$x"/omp/profiles/*) ++done ++[ -n "${PI_CODING_AGENT_DIR:-}" ] && roots+=("$PI_CODING_AGENT_DIR") ++indexes=() ++for root in "${roots[@]}"; do ++ [ -d "$root/custom-session-files" ] && indexes+=("$root/custom-session-files") ++done ++while IFS= read -r path; do ++ cpath=$(canon "$path") ++ own=$(bucket_name "$cpath") ++ # A session started in a subdirectory has that subdirectory's bucket, the ++ # worktree's name plus "-" ahead of any closing "--". A transcript ++ # still has to name the worktree path, so a bucket that only shares the ++ # prefix costs time, never a match. The main worktree is skipped, since a ++ # repo at $HOME would make this the whole store. ++ sub=$(bucket_name "$cpath/*") ++ for root in "${roots[@]}"; do ++ [ -d "$root/sessions/$own" ] && session_dirs+=("$root/sessions/$own/") ++ [ "$path" = "$main_wt" ] && continue ++ for dir in "$root/sessions/${sub%\**}"*"${sub##*\*}"; do ++ [ -d "$dir" ] && session_dirs+=("$dir/") ++ done ++ done ++ dotenvs+=("$path/.env") ++done < <(printf '%s\n' "$worktree_records" | awk '/^worktree /{sub(/^worktree /, ""); print}') ++# A listed transcript that is gone holds no chat; its subagents sit in the ++# directory named after it. ++listed=$([ "${#indexes[@]}" -eq 0 ] || grep -rhs '' -- "${indexes[@]}") ++index_status=$? ++while IFS= read -r f; do ++ [ -f "$f" ] || continue ++ session_dirs+=("$f") ++ [ -d "${f%.jsonl}" ] && session_dirs+=("${f%.jsonl}/") ++done <<<"$listed" ++moved="" ++[ -n "${PI_CODING_AGENT_SESSION_DIR:-}" ] || + moved=$(grep -lsE '^[[:space:]]*(export[[:space:]]+)?((PI|OMP)_(CODING_AGENT_SESSION_DIR|CODING_AGENT_DIR|CONFIG_DIR)|XDG_DATA_HOME)[[:space:]]*[=:]' \ + -- "${dotenvs[@]}") -+fi +chat_lookup=ok +if [ -n "$moved" ]; then + chat_lookup=failed @@ -165,6 +199,9 @@ +elif [ ! -d "$sessions" ]; then + chat_lookup=failed + echo "warn: no omp session store at $sessions; LAST_CHAT is ? and rows hold at verify-recent-chat" >&2 ++elif [ "$index_status" -gt 1 ]; then ++ chat_lookup=failed ++ echo "warn: cannot read an omp custom-session-files index; LAST_CHAT is ? and rows hold at verify-recent-chat" >&2 +fi now=$(date +%s) @@ -189,7 +226,7 @@ head_ts=$(git -C "$wt" log -1 --format='%ct' HEAD 2>/dev/null || echo 0) age=$([ "$head_ts" -gt 0 ] 2>/dev/null && echo "$(( (now - head_ts) / 86400 ))d" || echo "?") -@@ -89,12 +179,12 @@ +@@ -89,12 +209,12 @@ # real signal; merge-base only catches fast-forward/rebase merges. git merge-base --is-ancestor "$head" "$base" 2>/dev/null && merged=YES || merged=no @@ -208,7 +245,7 @@ branch=$(git -C "$wt" symbolic-ref --quiet --short HEAD 2>/dev/null || echo "") if [ -z "$branch" ]; then remote=detached -@@ -102,33 +192,57 @@ +@@ -102,33 +222,57 @@ [ "$(git -C "$wt" rev-parse "origin/$branch" 2>/dev/null)" = "$head" ] \ && remote=pushed \ || remote="ahead$(git -C "$wt" rev-list --count "origin/$branch..HEAD" 2>/dev/null)" diff --git a/plugins/pstack/skills/poteto-mode/scripts/worktree-audit.sh b/plugins/pstack/skills/poteto-mode/scripts/worktree-audit.sh index edf5d3d..231fbc6 100755 --- a/plugins/pstack/skills/poteto-mode/scripts/worktree-audit.sh +++ b/plugins/pstack/skills/poteto-mode/scripts/worktree-audit.sh @@ -110,43 +110,70 @@ bucket_name() { else printf '%s' "--$(dash "${1#/}")--"; fi } session_dirs=() -moved="" +dotenvs=() +config_root="$HOME/${PI_CONFIG_DIR:-.omp}" if [ -n "${PI_CODING_AGENT_SESSION_DIR:-}" ]; then sessions=$PI_CODING_AGENT_SESSION_DIR session_dirs=("$sessions/") else profile=$(printf '%s' "${OMP_PROFILE-${PI_PROFILE-}}" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') [ "$profile" = default ] && profile="" - config="$HOME/${PI_CONFIG_DIR:-.omp}${profile:+/profiles/$profile}" + config="$config_root${profile:+/profiles/$profile}" agent="$config/agent" [ -z "$profile" ] && [ -n "${PI_CODING_AGENT_DIR:-}" ] && agent=$PI_CODING_AGENT_DIR data=$agent xdg="${XDG_DATA_HOME:-}/omp${profile:+/profiles/$profile}" [ "$agent" = "$config/agent" ] && [ -n "${XDG_DATA_HOME:-}" ] && [ -e "$xdg" ] && data=$xdg sessions="$data/sessions" - home_root=$(canon "$HOME") - temp_root=$(canon "${TMPDIR:-${TMP:-${TEMP:-/tmp}}}") dotenvs=("$agent/.env" "$config/.env" "$HOME/.env") - while IFS= read -r path; do - cpath=$(canon "$path") - dir="$sessions/$(bucket_name "$cpath")" - [ -d "$dir" ] && session_dirs+=("$dir/") - # A session started in a subdirectory has that subdirectory's bucket, - # the worktree's name plus "-" ahead of any closing "--". A - # transcript still has to name the worktree path, so a bucket that - # only shares the prefix costs time, never a match. The main worktree - # is skipped, since a repo at $HOME would make this the whole store. - if [ "$path" != "$main_wt" ]; then - sub=$(bucket_name "$cpath/*") - for dir in "$sessions/${sub%\**}"*"${sub##*\*}"; do - [ -d "$dir" ] && session_dirs+=("$dir/") - done - fi - dotenvs+=("$path/.env") - done < <(printf '%s\n' "$worktree_records" | awk '/^worktree /{sub(/^worktree /, ""); print}') +fi +# A session launched with --profile, --session-dir or another environment is +# not in that store. Every store under the config root and the XDG roots counts +# too, and each one's custom-session-files lists the absolute path of every +# transcript omp wrote outside the bucket store. Precedence does not matter +# for these, since a transcript only counts when it names the worktree. +home_root=$(canon "$HOME") +temp_root=$(canon "${TMPDIR:-${TMP:-${TEMP:-/tmp}}}") +roots=("$config_root/agent" "$config_root"/profiles/*/agent) +for x in "${XDG_DATA_HOME:-}" "${XDG_STATE_HOME:-}"; do + [ -n "$x" ] && roots+=("$x/omp" "$x"/omp/profiles/*) +done +[ -n "${PI_CODING_AGENT_DIR:-}" ] && roots+=("$PI_CODING_AGENT_DIR") +indexes=() +for root in "${roots[@]}"; do + [ -d "$root/custom-session-files" ] && indexes+=("$root/custom-session-files") +done +while IFS= read -r path; do + cpath=$(canon "$path") + own=$(bucket_name "$cpath") + # A session started in a subdirectory has that subdirectory's bucket, the + # worktree's name plus "-" ahead of any closing "--". A transcript + # still has to name the worktree path, so a bucket that only shares the + # prefix costs time, never a match. The main worktree is skipped, since a + # repo at $HOME would make this the whole store. + sub=$(bucket_name "$cpath/*") + for root in "${roots[@]}"; do + [ -d "$root/sessions/$own" ] && session_dirs+=("$root/sessions/$own/") + [ "$path" = "$main_wt" ] && continue + for dir in "$root/sessions/${sub%\**}"*"${sub##*\*}"; do + [ -d "$dir" ] && session_dirs+=("$dir/") + done + done + dotenvs+=("$path/.env") +done < <(printf '%s\n' "$worktree_records" | awk '/^worktree /{sub(/^worktree /, ""); print}') +# A listed transcript that is gone holds no chat; its subagents sit in the +# directory named after it. +listed=$([ "${#indexes[@]}" -eq 0 ] || grep -rhs '' -- "${indexes[@]}") +index_status=$? +while IFS= read -r f; do + [ -f "$f" ] || continue + session_dirs+=("$f") + [ -d "${f%.jsonl}" ] && session_dirs+=("${f%.jsonl}/") +done <<<"$listed" +moved="" +[ -n "${PI_CODING_AGENT_SESSION_DIR:-}" ] || moved=$(grep -lsE '^[[:space:]]*(export[[:space:]]+)?((PI|OMP)_(CODING_AGENT_SESSION_DIR|CODING_AGENT_DIR|CONFIG_DIR)|XDG_DATA_HOME)[[:space:]]*[=:]' \ -- "${dotenvs[@]}") -fi chat_lookup=ok if [ -n "$moved" ]; then chat_lookup=failed @@ -154,6 +181,9 @@ if [ -n "$moved" ]; then elif [ ! -d "$sessions" ]; then chat_lookup=failed echo "warn: no omp session store at $sessions; LAST_CHAT is ? and rows hold at verify-recent-chat" >&2 +elif [ "$index_status" -gt 1 ]; then + chat_lookup=failed + echo "warn: cannot read an omp custom-session-files index; LAST_CHAT is ? and rows hold at verify-recent-chat" >&2 fi now=$(date +%s) From e674518a30bb043e79128a5e78429ee3f59c7c8a Mon Sep 17 00:00:00 2001 From: Negrescu Alexandru Date: Sun, 25 Oct 2026 13:09:30 +0200 Subject: [PATCH 06/14] docs(omp-mechanics): record subdirectory buckets, custom-session-files, and the store no reader can see A session's bucket follows its launch cwd, not the git root. omp lists every transcript it writes outside the buckets in the agent dir's custom-session-files. A store moved at launch by the process environment leaves no list entry a reader can find, so the worktree audit cannot see it, and the doc says what an operator does instead. --- .../references/session-store-on-omp.md | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/plugins/pstack/skills/omp-mechanics/references/session-store-on-omp.md b/plugins/pstack/skills/omp-mechanics/references/session-store-on-omp.md index 0b7db11..d58e328 100644 --- a/plugins/pstack/skills/omp-mechanics/references/session-store-on-omp.md +++ b/plugins/pstack/skills/omp-mechanics/references/session-store-on-omp.md @@ -56,10 +56,36 @@ home and temp roots are canonicalized too, and `/`, `\`, and `:` all become `-` - Anything else drops its leading `/` and becomes `----`, so `/srv/a:b` is `--srv-a-b--`. +The cwd is the launch directory, not its git root. A session started in `/home/you/proj/src` sits in +`-proj-src`, and one in `/srv/a/src` in `--srv-a-src--`, beside the project's own bucket. + Two distinct paths that encode identically share one bucket unless the live runtime separates them. Probe the current store after an upgrade. Older stores may contain a short-lived hashed form, `--`, which is migrated back to path-encoded names on access. +## Transcripts outside the buckets + +`--session-dir`, `PI_CODING_AGENT_SESSION_DIR`, and a `--resume` of a transcript outside the store +write wherever they point. omp lists each such transcript in `custom-session-files/` under the +active agent directory, or under `$XDG_STATE_HOME/omp` when that exists, so each profile keeps its +own list. One file per session holds one absolute transcript path with no newline. omp writes it +when the session starts, before any message. Under a `$HOME` reached through a symlink it lists +bucket transcripts there too. Measured on omp 18.12.6 in a throwaway `HOME`. + +To find every transcript of a project, read its buckets in every store, `~/.omp/agent` and each +`~/.omp/profiles//agent`, then every path those lists name. A listed file that is gone was +deleted or moved. + +A store moved at launch by the process environment has no list entry a reader can find. A session +started with `PI_CODING_AGENT_DIR`, `PI_CONFIG_DIR`, `XDG_DATA_HOME`, or `XDG_STATE_HOME` set writes +its transcript and its list entry under a root that a reader without the same variable never +resolves. Under `PI_CODING_AGENT_DIR` only omp's per-process debug log in `~/.omp/logs` still names +the file, and a log is not an index. A `sessionDir` key in `config.yml` or a `--config` overlay +moves nothing. `--no-session` writes no transcript at all. The worktree audit cannot see these +sessions either. Rerun it with the variables those sessions launched with, or read +`get_state.sessionFile` from the live session, before you trust a `safe` row for a worktree you +used that way. + ## The files One flat transcript per session, `_.jsonl`. The timestamp is ISO-8601 with From 8fcfdd97354ecf4280c6e1f060bfe333cac54064 Mon Sep 17 00:00:00 2001 From: Negrescu Alexandru Date: Sun, 25 Oct 2026 13:11:09 +0200 Subject: [PATCH 07/14] test(worktree-audit): fail the live probe instead of skipping when OMP_AUDIT_LIVE_REQUIRED=1 Without omp the probe prints SKIPPED and the run passes, which is right on a developer machine and wrong in a job that installed omp to run it. --- omp-port/test-worktree-audit.sh | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/omp-port/test-worktree-audit.sh b/omp-port/test-worktree-audit.sh index 02c127b..5062141 100755 --- a/omp-port/test-worktree-audit.sh +++ b/omp-port/test-worktree-audit.sh @@ -9,8 +9,8 @@ # Builds a throwaway repo with a local bare origin, a fake gh and origin CLI, and fixture omp # session stores, then runs the audit under each store layout omp resolves. Needs git and jq. It # never reaches the network, the real gh, or the real session store. When omp is on PATH it also -# asks that omp, under the fixture HOME and with no model turn, which transcript it would write; -# without omp that probe prints SKIPPED. +# asks that omp, under the fixture HOME and with no model turn, which transcript it would write. +# Without omp that probe prints SKIPPED, or fails when OMP_AUDIT_LIVE_REQUIRED=1. set -euo pipefail PORT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P) @@ -359,7 +359,12 @@ probe() { chat "$(dirname "$file")" "$(basename "$file" .jsonl)" "$3" } if ! command -v omp >/dev/null 2>&1; then - printf 'worktree audit: live omp probe SKIPPED, omp is not on PATH\n' + if [ "${OMP_AUDIT_LIVE_REQUIRED:-}" = 1 ]; then + LABEL=live-omp + fail "omp is not on PATH and OMP_AUDIT_LIVE_REQUIRED=1" + else + printf 'worktree audit: live omp probe SKIPPED, omp is not on PATH; set OMP_AUDIT_LIVE_REQUIRED=1 to fail instead\n' + fi else LABEL=live-omp printf '%s\n' providers: ' probe:' ' baseUrl: http://127.0.0.1:9/v1' ' auth: none' \ From 054c8899793e6fc6598d9a234ca7aa8c044ca4e6 Mon Sep 17 00:00:00 2001 From: Negrescu Alexandru Date: Sun, 25 Oct 2026 13:11:09 +0200 Subject: [PATCH 08/14] ci(gate): run the worktree audit against the pinned omp in the smoke job The gate job has no omp, so the live probe in test-worktree-audit.sh never ran in CI. The smoke job installs omp by sha256, and the new step runs the test there with OMP_AUDIT_LIVE_REQUIRED=1, so a missing omp fails the step. --- .github/workflows/gate.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml index 15e16a8..415ade6 100644 --- a/.github/workflows/gate.yml +++ b/.github/workflows/gate.yml @@ -73,3 +73,5 @@ jobs: echo "$dir" >> "$GITHUB_PATH" - name: Plugin smoke run: OMP_SMOKE_REQUIRED=1 bun omp-port/smoke-plugin.mjs + - name: Worktree audit against live omp + run: OMP_AUDIT_LIVE_REQUIRED=1 bash omp-port/test-worktree-audit.sh From b77a2e239e6dbdc8490ca4f03979c638098b78e0 Mon Sep 17 00:00:00 2001 From: Negrescu Alexandru Date: Sun, 25 Oct 2026 13:13:56 +0200 Subject: [PATCH 09/14] docs(omp-mechanics): drop the omp version from the custom-session-files note The version-agnostic gate rejects a pinned omp version in owned prose. The note now tells the reader to list a real store after an upgrade, as the rest of the file does. --- .../skills/omp-mechanics/references/session-store-on-omp.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/pstack/skills/omp-mechanics/references/session-store-on-omp.md b/plugins/pstack/skills/omp-mechanics/references/session-store-on-omp.md index d58e328..db8559a 100644 --- a/plugins/pstack/skills/omp-mechanics/references/session-store-on-omp.md +++ b/plugins/pstack/skills/omp-mechanics/references/session-store-on-omp.md @@ -70,7 +70,7 @@ write wherever they point. omp lists each such transcript in `custom-session-fil active agent directory, or under `$XDG_STATE_HOME/omp` when that exists, so each profile keeps its own list. One file per session holds one absolute transcript path with no newline. omp writes it when the session starts, before any message. Under a `$HOME` reached through a symlink it lists -bucket transcripts there too. Measured on omp 18.12.6 in a throwaway `HOME`. +bucket transcripts there too. Measured in a throwaway `HOME`, so list a real one after an upgrade. To find every transcript of a project, read its buckets in every store, `~/.omp/agent` and each `~/.omp/profiles//agent`, then every path those lists name. A listed file that is gone was From 3466de867a3bbe0ad65f4278f71f4a4ac4507423 Mon Sep 17 00:00:00 2001 From: Negrescu Alexandru Date: Sun, 25 Oct 2026 20:41:24 +0200 Subject: [PATCH 10/14] test(worktree-audit): unset XDG_STATE_HOME so fixture audits never read the developer's index --- omp-port/test-worktree-audit.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/omp-port/test-worktree-audit.sh b/omp-port/test-worktree-audit.sh index 5062141..799fba7 100755 --- a/omp-port/test-worktree-audit.sh +++ b/omp-port/test-worktree-audit.sh @@ -34,7 +34,8 @@ ln -s "real home" "$FIXTURE/home" export HOME="$FIXTURE/home" TMPDIR="$FIXTURE/home/scratch" CT="$CH/scratch" unset TMP TEMP CDPATH GIT_DIR GIT_WORK_TREE GIT_COMMON_DIR GIT_INDEX_FILE \ - PI_CODING_AGENT_SESSION_DIR PI_CODING_AGENT_DIR PI_CONFIG_DIR OMP_PROFILE PI_PROFILE XDG_DATA_HOME + PI_CODING_AGENT_SESSION_DIR PI_CODING_AGENT_DIR PI_CONFIG_DIR OMP_PROFILE PI_PROFILE XDG_DATA_HOME \ + XDG_STATE_HOME export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_TERMINAL_PROMPT=0 export GIT_AUTHOR_NAME=audit GIT_AUTHOR_EMAIL=audit@example.invalid export GIT_COMMITTER_NAME=audit GIT_COMMITTER_EMAIL=audit@example.invalid From c6d46853a48bfc28d031f2fa7171ffe10710f830 Mon Sep 17 00:00:00 2001 From: Negrescu Alexandru Date: Sun, 25 Oct 2026 20:42:14 +0200 Subject: [PATCH 11/14] test(worktree-audit): keep listed bucket transcripts out of the scan (fails until the fix) --- omp-port/test-worktree-audit.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/omp-port/test-worktree-audit.sh b/omp-port/test-worktree-audit.sh index 799fba7..7511089 100755 --- a/omp-port/test-worktree-audit.sh +++ b/omp-port/test-worktree-audit.sh @@ -286,6 +286,14 @@ printf '%s' "$FIXTURE/gone/x.jsonl" >"$INDEX/4d5e6f7" audit custom-dir expect plain LAST_CHAT "$TODAY" expect plain BUCKET verify-recent-chat +# Under a symlinked HOME omp lists every bucket transcript as well, under the HOME it was given. +# The bucket scan already covers those, so one in another project's bucket stays out of the scan. +printf '%s' "$SESSIONS/-another-project/other.jsonl" >"$INDEX/8a9b0c1" +printf '%s' "$SESSIONS/-recent [one].worktree/own.jsonl" >"$INDEX/2d3e4f5" +audit listed-buckets +expect unrelated LAST_CHAT - +expect unrelated BUCKET safe +expect "$RECENT" LAST_CHAT "$TODAY" if [ "$(id -u)" != 0 ]; then chmod 000 "$INDEX" audit unreadable-index From f09beff3637002c65a338f35060eedc9810cf596 Mon Sep 17 00:00:00 2001 From: Negrescu Alexandru Date: Sun, 25 Oct 2026 20:46:07 +0200 Subject: [PATCH 12/14] fix(worktree-audit): skip listed bucket transcripts so the scan stays bounded Under a symlinked HOME omp lists every bucket transcript in custom-session-files, so each row grepped the whole store. A listed path whose directory's parent is a store's sessions directory is already covered by the bucket scan and is skipped. Each distinct parent is canonicalized once. --- omp-port/patches/worktree-audit-omp.patch | 19 ++++++++++++++----- .../poteto-mode/scripts/worktree-audit.sh | 9 ++++++++- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/omp-port/patches/worktree-audit-omp.patch b/omp-port/patches/worktree-audit-omp.patch index c716c70..072cfa9 100644 --- a/omp-port/patches/worktree-audit-omp.patch +++ b/omp-port/patches/worktree-audit-omp.patch @@ -17,7 +17,9 @@ # roots, so a session launched with --profile still counts # - read each store's custom-session-files, where omp lists the absolute path of every # transcript written outside the bucket store (--session-dir, PI_CODING_AGENT_SESSION_DIR, -# --resume of a moved file), and scan those transcripts and their subagent directories +# --resume of a moved file), and scan those transcripts and their subagent directories, +# skipping the bucket transcripts omp also lists under a symlinked home, which the bucket +# scan already bounds to this repo's worktrees # - fail closed: a store that cannot be found or read, or an index that cannot be read, makes # LAST_CHAT "?" and holds the row at verify-recent-chat, the bucket # playbooks/worktree-cleanup.md already names, never `safe` @@ -63,7 +65,7 @@ if [ -z "$base" ]; then for cand in "$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null)" origin/main origin/master; do -@@ -55,33 +60,148 @@ +@@ -55,33 +60,155 @@ stat_fmt=(-f '%m %N'); fmt_day() { date -r "$1" '+%Y-%m-%d'; } fi @@ -158,8 +160,10 @@ +done +[ -n "${PI_CODING_AGENT_DIR:-}" ] && roots+=("$PI_CODING_AGENT_DIR") +indexes=() ++stores=$'\n' +for root in "${roots[@]}"; do + [ -d "$root/custom-session-files" ] && indexes+=("$root/custom-session-files") ++ [ -d "$root/sessions" ] && stores+="$(canon "$root/sessions")"$'\n' +done +while IFS= read -r path; do + cpath=$(canon "$path") @@ -180,11 +184,16 @@ + dotenvs+=("$path/.env") +done < <(printf '%s\n' "$worktree_records" | awk '/^worktree /{sub(/^worktree /, ""); print}') +# A listed transcript that is gone holds no chat; its subagents sit in the -+# directory named after it. ++# directory named after it. Under a symlinked home omp lists every bucket ++# transcript too, and the bucket scan already covers those. They share one ++# parent, so it is canonicalized once rather than once per transcript. +listed=$([ "${#indexes[@]}" -eq 0 ] || grep -rhs '' -- "${indexes[@]}") +index_status=$? ++seen="" store="" +while IFS= read -r f; do + [ -f "$f" ] || continue ++ [ "${f%/*/*}" = "$seen" ] || { seen=${f%/*/*}; store=$(canon "$seen"); } ++ case "$stores" in *$'\n'"$store"$'\n'*) continue ;; esac + session_dirs+=("$f") + [ -d "${f%.jsonl}" ] && session_dirs+=("${f%.jsonl}/") +done <<<"$listed" @@ -226,7 +235,7 @@ head_ts=$(git -C "$wt" log -1 --format='%ct' HEAD 2>/dev/null || echo 0) age=$([ "$head_ts" -gt 0 ] 2>/dev/null && echo "$(( (now - head_ts) / 86400 ))d" || echo "?") -@@ -89,12 +209,12 @@ +@@ -89,12 +216,12 @@ # real signal; merge-base only catches fast-forward/rebase merges. git merge-base --is-ancestor "$head" "$base" 2>/dev/null && merged=YES || merged=no @@ -245,7 +254,7 @@ branch=$(git -C "$wt" symbolic-ref --quiet --short HEAD 2>/dev/null || echo "") if [ -z "$branch" ]; then remote=detached -@@ -102,33 +222,57 @@ +@@ -102,33 +229,57 @@ [ "$(git -C "$wt" rev-parse "origin/$branch" 2>/dev/null)" = "$head" ] \ && remote=pushed \ || remote="ahead$(git -C "$wt" rev-list --count "origin/$branch..HEAD" 2>/dev/null)" diff --git a/plugins/pstack/skills/poteto-mode/scripts/worktree-audit.sh b/plugins/pstack/skills/poteto-mode/scripts/worktree-audit.sh index 231fbc6..cb9f476 100755 --- a/plugins/pstack/skills/poteto-mode/scripts/worktree-audit.sh +++ b/plugins/pstack/skills/poteto-mode/scripts/worktree-audit.sh @@ -140,8 +140,10 @@ for x in "${XDG_DATA_HOME:-}" "${XDG_STATE_HOME:-}"; do done [ -n "${PI_CODING_AGENT_DIR:-}" ] && roots+=("$PI_CODING_AGENT_DIR") indexes=() +stores=$'\n' for root in "${roots[@]}"; do [ -d "$root/custom-session-files" ] && indexes+=("$root/custom-session-files") + [ -d "$root/sessions" ] && stores+="$(canon "$root/sessions")"$'\n' done while IFS= read -r path; do cpath=$(canon "$path") @@ -162,11 +164,16 @@ while IFS= read -r path; do dotenvs+=("$path/.env") done < <(printf '%s\n' "$worktree_records" | awk '/^worktree /{sub(/^worktree /, ""); print}') # A listed transcript that is gone holds no chat; its subagents sit in the -# directory named after it. +# directory named after it. Under a symlinked home omp lists every bucket +# transcript too, and the bucket scan already covers those. They share one +# parent, so it is canonicalized once rather than once per transcript. listed=$([ "${#indexes[@]}" -eq 0 ] || grep -rhs '' -- "${indexes[@]}") index_status=$? +seen="" store="" while IFS= read -r f; do [ -f "$f" ] || continue + [ "${f%/*/*}" = "$seen" ] || { seen=${f%/*/*}; store=$(canon "$seen"); } + case "$stores" in *$'\n'"$store"$'\n'*) continue ;; esac session_dirs+=("$f") [ -d "${f%.jsonl}" ] && session_dirs+=("${f%.jsonl}/") done <<<"$listed" From 46a581e634fd9be1ac501498b38219d8199c913d Mon Sep 17 00:00:00 2001 From: Negrescu Alexandru Date: Sun, 25 Oct 2026 20:47:32 +0200 Subject: [PATCH 13/14] test(worktree-audit): read a listed transcript behind a closed directory as unknown (fails until the fix) --- omp-port/test-worktree-audit.sh | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/omp-port/test-worktree-audit.sh b/omp-port/test-worktree-audit.sh index 7511089..11d4d04 100755 --- a/omp-port/test-worktree-audit.sh +++ b/omp-port/test-worktree-audit.sh @@ -275,8 +275,9 @@ expect "$RECENT" LAST_CHAT "$TODAY" # omp lists each transcript it writes outside the bucket store, from --session-dir, an explicit # PI_CODING_AGENT_SESSION_DIR or a --resume of a moved file, as one absolute path per file in the # agent dir's custom-session-files, or XDG_STATE_HOME/omp's when that exists. A subagent transcript -# sits in the directory named after its parent. A listed transcript that is gone is no chat, and an -# index that cannot be read is unknown. +# sits in the directory named after its parent. A listed transcript that is gone is no chat. One +# behind a directory the audit cannot search may still exist, so it is unknown, and so is an index +# that cannot be read. INDEX="$HOME/.omp/agent/custom-session-files" chat "$FIXTURE/custom dir" custom "$FIXTURE" chat "$FIXTURE/custom dir/custom" Child "$CH/plain" @@ -295,10 +296,23 @@ expect unrelated LAST_CHAT - expect unrelated BUCKET safe expect "$RECENT" LAST_CHAT "$TODAY" if [ "$(id -u)" != 0 ]; then + chmod 000 "$FIXTURE/custom dir" + audit closed-transcript-dir + expect plain LAST_CHAT '?' + expect plain BUCKET verify-recent-chat + chmod 755 "$FIXTURE/custom dir" + chat "$FIXTURE/vault/inner" s "$CH/plain" + printf '%s' "$FIXTURE/vault/inner/s.jsonl" >"$INDEX/6e7f809" + chmod 000 "$FIXTURE/vault" + audit closed-ancestor + expect plain LAST_CHAT '?' + chmod 755 "$FIXTURE/vault" chmod 000 "$INDEX" audit unreadable-index expect plain LAST_CHAT '?' chmod 755 "$INDEX" +else + printf 'worktree audit: closed directory and index cases SKIPPED, root searches every directory\n' fi mkdir -p "$FIXTURE/state/omp" mv "$INDEX" "$FIXTURE/state/omp/" From d454f8fa03aa30a9dfa1216fd4e7ab7e3e6469d6 Mon Sep 17 00:00:00 2001 From: Negrescu Alexandru Date: Sun, 25 Oct 2026 20:49:13 +0200 Subject: [PATCH 14/14] fix(worktree-audit): hold a row when a listed transcript sits behind a closed directory [ -f ] cannot tell a deleted transcript from one the audit cannot reach, so a listed transcript under an unsearchable directory read as no chat and the row could be safe. The audit now walks up to the nearest existing directory and, when that directory cannot be searched, makes LAST_CHAT ? and holds every row at verify-recent-chat. --- omp-port/patches/worktree-audit-omp.patch | 34 ++++++++++++------- .../poteto-mode/scripts/worktree-audit.sh | 21 ++++++++---- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/omp-port/patches/worktree-audit-omp.patch b/omp-port/patches/worktree-audit-omp.patch index 072cfa9..05bbb82 100644 --- a/omp-port/patches/worktree-audit-omp.patch +++ b/omp-port/patches/worktree-audit-omp.patch @@ -20,9 +20,10 @@ # --resume of a moved file), and scan those transcripts and their subagent directories, # skipping the bucket transcripts omp also lists under a symlinked home, which the bucket # scan already bounds to this repo's worktrees -# - fail closed: a store that cannot be found or read, or an index that cannot be read, makes -# LAST_CHAT "?" and holds the row at verify-recent-chat, the bucket -# playbooks/worktree-cleanup.md already names, never `safe` +# - fail closed: a store that cannot be found or read, an index that cannot be read, or a +# listed transcript behind a directory the audit cannot search makes LAST_CHAT "?" and holds +# the row at verify-recent-chat, the bucket playbooks/worktree-cleanup.md already names, +# never `safe` # - fail closed the same way when a .env file omp reads (each worktree's, the agent dir's, the # config root's, home's) sets a key that moves the store, OMP_ spellings included, rather # than replaying omp's .env loader @@ -65,7 +66,7 @@ if [ -z "$base" ]; then for cand in "$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null)" origin/main origin/master; do -@@ -55,33 +60,155 @@ +@@ -55,33 +60,164 @@ stat_fmt=(-f '%m %N'); fmt_day() { date -r "$1" '+%Y-%m-%d'; } fi @@ -183,17 +184,23 @@ + done + dotenvs+=("$path/.env") +done < <(printf '%s\n' "$worktree_records" | awk '/^worktree /{sub(/^worktree /, ""); print}') -+# A listed transcript that is gone holds no chat; its subagents sit in the -+# directory named after it. Under a symlinked home omp lists every bucket -+# transcript too, and the bucket scan already covers those. They share one -+# parent, so it is canonicalized once rather than once per transcript. ++# Under a symlinked home omp lists every bucket transcript too, and the bucket ++# scan already covers those. They share one parent, so it is canonicalized ++# once rather than once per transcript. Any other listed transcript that is ++# gone holds no chat, but one behind a directory the audit cannot search may ++# still exist. Its subagents sit in the directory named after it. +listed=$([ "${#indexes[@]}" -eq 0 ] || grep -rhs '' -- "${indexes[@]}") +index_status=$? -+seen="" store="" ++seen="" store="" closed="" +while IFS= read -r f; do -+ [ -f "$f" ] || continue + [ "${f%/*/*}" = "$seen" ] || { seen=${f%/*/*}; store=$(canon "$seen"); } + case "$stores" in *$'\n'"$store"$'\n'*) continue ;; esac ++ if [ ! -f "$f" ]; then ++ dir=${f%/*} ++ while [ ! -e "$dir" ] && [ "$dir" != "${dir%/*}" ]; do dir=${dir%/*}; done ++ [ -d "$dir" ] && [ ! -x "$dir" ] && closed=$dir ++ continue ++ fi + session_dirs+=("$f") + [ -d "${f%.jsonl}" ] && session_dirs+=("${f%.jsonl}/") +done <<<"$listed" @@ -211,6 +218,9 @@ +elif [ "$index_status" -gt 1 ]; then + chat_lookup=failed + echo "warn: cannot read an omp custom-session-files index; LAST_CHAT is ? and rows hold at verify-recent-chat" >&2 ++elif [ -n "$closed" ]; then ++ chat_lookup=failed ++ echo "warn: cannot search $closed for a listed omp transcript; LAST_CHAT is ? and rows hold at verify-recent-chat" >&2 +fi now=$(date +%s) @@ -235,7 +245,7 @@ head_ts=$(git -C "$wt" log -1 --format='%ct' HEAD 2>/dev/null || echo 0) age=$([ "$head_ts" -gt 0 ] 2>/dev/null && echo "$(( (now - head_ts) / 86400 ))d" || echo "?") -@@ -89,12 +216,12 @@ +@@ -89,12 +225,12 @@ # real signal; merge-base only catches fast-forward/rebase merges. git merge-base --is-ancestor "$head" "$base" 2>/dev/null && merged=YES || merged=no @@ -254,7 +264,7 @@ branch=$(git -C "$wt" symbolic-ref --quiet --short HEAD 2>/dev/null || echo "") if [ -z "$branch" ]; then remote=detached -@@ -102,33 +229,57 @@ +@@ -102,33 +238,57 @@ [ "$(git -C "$wt" rev-parse "origin/$branch" 2>/dev/null)" = "$head" ] \ && remote=pushed \ || remote="ahead$(git -C "$wt" rev-list --count "origin/$branch..HEAD" 2>/dev/null)" diff --git a/plugins/pstack/skills/poteto-mode/scripts/worktree-audit.sh b/plugins/pstack/skills/poteto-mode/scripts/worktree-audit.sh index cb9f476..ed7c540 100755 --- a/plugins/pstack/skills/poteto-mode/scripts/worktree-audit.sh +++ b/plugins/pstack/skills/poteto-mode/scripts/worktree-audit.sh @@ -163,17 +163,23 @@ while IFS= read -r path; do done dotenvs+=("$path/.env") done < <(printf '%s\n' "$worktree_records" | awk '/^worktree /{sub(/^worktree /, ""); print}') -# A listed transcript that is gone holds no chat; its subagents sit in the -# directory named after it. Under a symlinked home omp lists every bucket -# transcript too, and the bucket scan already covers those. They share one -# parent, so it is canonicalized once rather than once per transcript. +# Under a symlinked home omp lists every bucket transcript too, and the bucket +# scan already covers those. They share one parent, so it is canonicalized +# once rather than once per transcript. Any other listed transcript that is +# gone holds no chat, but one behind a directory the audit cannot search may +# still exist. Its subagents sit in the directory named after it. listed=$([ "${#indexes[@]}" -eq 0 ] || grep -rhs '' -- "${indexes[@]}") index_status=$? -seen="" store="" +seen="" store="" closed="" while IFS= read -r f; do - [ -f "$f" ] || continue [ "${f%/*/*}" = "$seen" ] || { seen=${f%/*/*}; store=$(canon "$seen"); } case "$stores" in *$'\n'"$store"$'\n'*) continue ;; esac + if [ ! -f "$f" ]; then + dir=${f%/*} + while [ ! -e "$dir" ] && [ "$dir" != "${dir%/*}" ]; do dir=${dir%/*}; done + [ -d "$dir" ] && [ ! -x "$dir" ] && closed=$dir + continue + fi session_dirs+=("$f") [ -d "${f%.jsonl}" ] && session_dirs+=("${f%.jsonl}/") done <<<"$listed" @@ -191,6 +197,9 @@ elif [ ! -d "$sessions" ]; then elif [ "$index_status" -gt 1 ]; then chat_lookup=failed echo "warn: cannot read an omp custom-session-files index; LAST_CHAT is ? and rows hold at verify-recent-chat" >&2 +elif [ -n "$closed" ]; then + chat_lookup=failed + echo "warn: cannot search $closed for a listed omp transcript; LAST_CHAT is ? and rows hold at verify-recent-chat" >&2 fi now=$(date +%s)