From f6bbbdf9828e2607b792bf2c7a9b80a16131f4e4 Mon Sep 17 00:00:00 2001 From: Diogo Silva Sena <227921880+DougBTW@users.noreply.github.com> Date: Thu, 29 Oct 2026 19:43:36 -0400 Subject: [PATCH] =?UTF-8?q?release:=20v3.10.10=20=E2=80=94=20a=20commit=20?= =?UTF-8?q?missing=20from=20this=20clone=20is=20no=20longer=20taken=20as?= =?UTF-8?q?=20gone?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sync demoted (commit: ) to (commit-stale: ) whenever the SHA did not resolve locally. A pull-only clone that never fetched a commit demoted refs the author's clone still resolved, and the uncommitted ledger edit then blocked its own git pull --ff-only, after every SessionStart sync. A SHA missing locally is now demoted only when the repo has no remote, or GitHub answers HTTP 422 "No commit found" for it. A 404, a failed lookup or a non-GitHub remote leaves the annotation alone. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MVx3HWniZnrXYfPiCo5Lrs --- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- CHANGELOG.md | 7 +++ README.md | 2 +- bin/found-issues | 2 +- docs/format-spec.md | 1 + docs/found-issues.md | 1 + lib/sync.sh | 33 +++++++++++- tests/bin-shims/gh | 17 ++++++ tests/cli-sync-commit-stale.bats | 88 ++++++++++++++++++++++++++++++++ 10 files changed, 150 insertions(+), 5 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 8c37eecd..49b29f66 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "found-issues", - "version": "3.10.9", + "version": "3.10.10", "description": "Markdown-based issue tracker for AI agents. Claude logs out-of-scope issues it spots while working; entries auto-flip to fixed on PR merge or commit-fix.", "author": { "name": "AltDoug", diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 5bb65d15..806f8d73 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "found-issues", - "version": "3.10.9", + "version": "3.10.10", "description": "Markdown-based issue tracker for AI agents. The agent logs out-of-scope issues it spots while working; entries auto-flip to fixed on PR merge or commit-fix.", "author": { "name": "AltDoug", diff --git a/CHANGELOG.md b/CHANGELOG.md index dd820443..f5b70e96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.10.10] - 2026-10-29 + +A clone that never fetched a commit no longer marks its `(commit:)` annotation stale, so sync stops dirtying pull-only clones. + +### Fixed +- **A commit missing from this clone is no longer taken as gone** (agent-config ledger `docs/found-issues.md:1`). sync demoted `(commit: )` to `(commit-stale: )` whenever the SHA did not resolve locally. A pull-only clone that never fetched the branch a commit was made on (measured on a Windows box's agent-config clone: 4 entries citing `a46fa6b`, `e672651`, `d7e9245`) demoted refs the author's clone still resolved, so two clones of one ledger disagreed, and the uncommitted ledger edit made the box's next `git pull --ff-only` abort ("local changes would be overwritten"), again after every SessionStart sync. A SHA missing locally is now demoted only when the absence is evidence: the repo has no remote (unchanged), or GitHub answers that it has no such commit (`gh api repos///commits/` → HTTP 422, one call per SHA per run). A 404 (a repo the signed-in gh account cannot see), a failed lookup, or a non-GitHub remote leaves the annotation alone, as a shallow clone already did. + ## [3.10.9] - 2026-10-27 A fix PR whose landing branch was merged but kept is moved to the default branch, instead of merging into a branch nothing reads any more. diff --git a/README.md b/README.md index 590e57d9..5b3b1dd1 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ [![Claude Code plugin](https://img.shields.io/badge/Claude%20Code-plugin-orange.svg)](https://docs.claude.com/en/docs/claude-code/plugins) [![Codex plugin](https://img.shields.io/badge/Codex-plugin-black.svg)](AGENTS.md#installing-for-codex) -**7 lifecycle hooks · 14 slash commands · 2044 tests on Linux/macOS · zero manual bookkeeping** +**7 lifecycle hooks · 14 slash commands · 2048 tests on Linux/macOS · zero manual bookkeeping** ![demo](hero.gif) diff --git a/bin/found-issues b/bin/found-issues index db82cb5a..1777384a 100755 --- a/bin/found-issues +++ b/bin/found-issues @@ -28,7 +28,7 @@ set -euo pipefail -readonly FI_VERSION="3.10.9" +readonly FI_VERSION="3.10.10" # Statusline fast path (lib/segment-cache.sh): `status --format=segment` runs # every few seconds per session, so it is answered from a content-checked diff --git a/docs/format-spec.md b/docs/format-spec.md index 2558a98d..98357749 100644 --- a/docs/format-spec.md +++ b/docs/format-spec.md @@ -237,6 +237,7 @@ However, if a linked PR is closed without merge or a commit SHA becomes unreacha - `(PR: ...)` → `(PR-closed: ...)` — entry stays `[open]`, eligible for AI re-verification since the closure signal is stale - `(commit: ...)` → `(commit-stale: ...)` — entry stays `[open]`, eligible for AI re-verification since the SHA no longer resolves on default branch + - A SHA this clone cannot resolve is demoted only when that absence is evidence: the repo has no remote, or GitHub answers that it has no such commit (HTTP 422). A clone with a remote may simply never have fetched the commit, so a GitHub repo gh cannot see (404), a failed lookup, or a non-GitHub remote leaves `(commit: ...)` alone (3.10.10). These demoted forms are written **only** by `found-issues sync` during its audit phase; they are never produced by user commands or `/found-issues:annotate-*` calls. They serve as an audit trail of link staleness. diff --git a/docs/found-issues.md b/docs/found-issues.md index b46ae8c7..71e3c0b6 100644 --- a/docs/found-issues.md +++ b/docs/found-issues.md @@ -58,3 +58,4 @@ Maintained automatically by Claude. See /dev/null || true)" == "true" ]] && shallow_repo=1 + # The same holds for a clone with a remote: a SHA missing here may sit on a + # branch this clone never fetched (a pull-only clone, measured 2026-10-29: + # it demoted refs the author's clone still resolved, and the uncommitted + # ledger edit then blocked its own `git pull --ff-only`). A missing local + # object is not evidence. With no remote, this repo is the whole history. + local has_remote=0 + [[ -n "$(git remote 2>/dev/null || true)" ]] && has_remote=1 + + # Is gone upstream too? Asked only for a SHA missing locally. Yes + # only on GitHub's answer for a commit the repo does not have (HTTP 422 + # "No commit found"); a 404 is a repo gh cannot see (wrong account), and + # any other failure is "cannot tell" -- both leave the annotation alone, + # as does a non-GitHub remote, which has no cheap lookup. + # Memo lines: "\n\x1e<0|1>". + local _fi_gone_memo=$'\n' + _fi_commit_gone_upstream() { + local sha="$1" key=$'\n'"$1"$'\x1e' err ans=1 + (( has_remote )) || return 0 + [[ -n "$repo_id" ]] || return 1 + if [[ "$_fi_gone_memo" == *"$key"* ]]; then + ans="${_fi_gone_memo#*"$key"}" + return "${ans%%$'\n'*}" + fi + if ! err="$( { gh api "repos/$repo_id/commits/$sha" --jq .sha >/dev/null; } 2>&1 )" \ + && [[ "$err" == *"No commit found for SHA"* && "$err" == *"(HTTP 422)"* ]]; then + ans=0 + fi + _fi_gone_memo+="$sha"$'\x1e'"$ans"$'\n' + return "$ans" + } + # Loop-invariant; was one `git rev-parse` per path entry (audit ledger-5). local repo_root repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" @@ -355,7 +386,7 @@ cmd_sync() { || [[ "$(git cat-file -t "$sha" 2>&1 || true)" == *ambiguous* ]]; then : # Git cannot tell here (shallow history / ambiguous short SHA) — # leave the annotation for a full clone to judge. - else + elif _fi_commit_gone_upstream "$sha"; then # B1/B2: SHA doesn't resolve (squash-merge dropped it, force-push removed it) → demote demote_commit_refs+=("$sha") fi diff --git a/tests/bin-shims/gh b/tests/bin-shims/gh index 7bcc083e..7b185dcc 100755 --- a/tests/bin-shims/gh +++ b/tests/bin-shims/gh @@ -72,6 +72,23 @@ case "$cmd $sub" in fi done <<<"$GH_MOCK_PR_FILES" fi + # repos///commits/: GH_MOCK_COMMITS lists the full SHAs that + # exist upstream (newline-separated); a listed prefix answers its full SHA + # (sync asks --jq .sha). Unlisted = GitHub's real answer for a commit the + # repo does not have: HTTP 422. GH_MOCK_COMMITS_API=fail stands in for a + # repo gh cannot see (HTTP 404) or a network failure. + if [[ "$endpoint" =~ /commits/([0-9a-fA-F]+)$ ]] && [[ "${GH_MOCK_COMMITS_API:-}" != "fail" ]]; then + api_sha="${BASH_REMATCH[1]}" + while IFS= read -r mock_sha; do + if [[ -n "$mock_sha" && "$mock_sha" == "$api_sha"* ]]; then + printf '%s\n' "$mock_sha" + exit 0 + fi + done <<<"${GH_MOCK_COMMITS:-}" + printf '{"message":"No commit found for SHA: %s","status":"422"}' "$api_sha" + printf 'gh: No commit found for SHA: %s (HTTP 422)\n' "$api_sha" >&2 + exit 1 + fi printf 'gh: Not Found (HTTP 404)\n' >&2 exit 1 ;; diff --git a/tests/cli-sync-commit-stale.bats b/tests/cli-sync-commit-stale.bats index 10c58cda..76e36bcb 100644 --- a/tests/cli-sync-commit-stale.bats +++ b/tests/cli-sync-commit-stale.bats @@ -49,3 +49,91 @@ teardown() { fi_run sync grep -q '\[fixed\].*\(commit: '"$sha"'\)' docs/found-issues.md } + +# --- consumer clones (2026-10-29) --- +# A clone that never fetched a commit cannot tell "gone" from "on a branch I +# do not have". Measured on a pull-only clone: sync demoted (commit:) refs to +# (commit-stale:) because the objects were missing locally, the uncommitted +# ledger edit then made `git pull --ff-only` abort, and it recurred every +# session. A missing local object is not evidence; only the remote is. + +@test "sync: clone missing a commit that exists upstream leaves (commit:) alone" { + # Upstream (a plain git remote) holds the commit on a side branch; the + # clone fetched main only, so the object is absent locally. + local up="$TMP/up.git" + git init -q --bare -b main "$up" + git remote add origin "$up" + mkdir -p src + echo "x" > src/foo.py + git add src/foo.py + git commit -q -m "init" + git push -q origin main + git checkout -q -b side + echo "y" >> src/foo.py + git commit -q -am "fix on a side branch" + local sha + sha="$(git rev-parse --short=7 HEAD)" + git push -q origin side + git checkout -q main + local clone="$TMP/clone" + git clone -q --no-local --single-branch -b main "file://$up" "$clone" + cd "$clone" + run git cat-file -e "$sha" + [ "$status" -ne 0 ] + + fi_seed_entry "src/foo.py:1 — bug (commit: $sha)" + local before + before="$(cat docs/found-issues.md)" + fi_run sync + [ "$status" -eq 0 ] + [ "$(cat docs/found-issues.md)" = "$before" ] +} + +@test "sync: GitHub says the commit exists -> (commit:) left alone" { + fi_use_gh_shim + fi_init_github_repo foo/bar main + mkdir -p src + echo "x" > src/foo.py + git add src/foo.py + git commit -q -m "init" + export GH_MOCK_COMMITS="a46fa6bb6ce8c0b71d2691c97964fc02cc77717e" + + fi_seed_entry "src/foo.py:1 — bug (commit: a46fa6b)" + local before + before="$(cat docs/found-issues.md)" + fi_run sync + [ "$status" -eq 0 ] + [ "$(cat docs/found-issues.md)" = "$before" ] +} + +@test "sync: GitHub says no such commit (HTTP 422) -> demotes to (commit-stale:)" { + fi_use_gh_shim + fi_init_github_repo foo/bar main + mkdir -p src + echo "x" > src/foo.py + git add src/foo.py + git commit -q -m "init" + export GH_MOCK_COMMITS="" + + fi_seed_entry "src/foo.py:1 — bug (commit: deadbeef)" + fi_run sync + [ "$status" -eq 0 ] + grep -q '^- \[open\].*(commit-stale: deadbeef)' docs/found-issues.md +} + +@test "sync: GitHub lookup fails (repo not visible / offline) -> (commit:) left alone" { + fi_use_gh_shim + fi_init_github_repo foo/bar main + mkdir -p src + echo "x" > src/foo.py + git add src/foo.py + git commit -q -m "init" + export GH_MOCK_COMMITS_API=fail + + fi_seed_entry "src/foo.py:1 — bug (commit: deadbeef)" + local before + before="$(cat docs/found-issues.md)" + fi_run sync + [ "$status" -eq 0 ] + [ "$(cat docs/found-issues.md)" = "$before" ] +}