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
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <sha>)` to `(commit-stale: <sha>)` 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/<o>/<r>/commits/<sha>` → 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.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion bin/found-issues
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/format-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
1 change: 1 addition & 0 deletions docs/found-issues.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,4 @@ Maintained automatically by Claude. See <https://github.com/AltDoug/found-issues
- [fixed] 2026-10-27 lib/autofix-ship.sh:511 — merge-when-green gives up on the first failed check ('checks failed — not merging') and nothing re-runs it, so a one-off CI infrastructure failure strands the fix PR for good; the message reaches only spawn.log while the item's run log says 'shipped'. Seen 2026-10-24 on ByteTechSoftwares/sayciao#320: supabase-legs failed on 'toomanyrequests: Data limit exceeded' pulling ECR images, app/zapier/static green, PR still open 2026-10-27 (suggested: on the first 'fail' re-run the head's failed GitHub Actions runs once with gh run rerun --failed and keep polling; a second fail stops as now; the guard does the same for an armed auto-merge) (fix: medium) (PR: AltDoug/found-issues#290) (fixed: 2026-10-27)
- [fixed] 2026-10-27 lib/autofix-ship.sh:658 — the 3.10.7 squash rebuild runs only after GitHub's AutomaticBaseChangeSucceededEvent, which needs the landing branch deleted; in a repo with delete_branch_on_merge off the landing branch survives its squash merge, so a fix PR still merges into that dead branch and never reaches the default branch. Seen on ByteTechSoftwares/sayciao#320 (base gsd/phase-24-resume squash-merged into main as #318 on 2026-10-25, branch kept, 49 ahead / 6 behind main) (suggested: before merging, if the base has a merged PR into the default branch whose squash is not an ancestor-carrier of the base tip, retarget with gh pr edit --base and reuse _fi_af_mwg_squashed with that PR as #M) (decide: When a fix PR's landing branch was squash-merged into the default branch but not deleted [no GitHub retarget], should merge-when-green retarget the PR to the default branch and rebuild it there, or stop and report?) (PR: AltDoug/found-issues#292) (fixed: 2026-10-27)
- [fixed] 2026-10-27 tests/autofix-sweep.bats:1067 — 'a continuation's PR leaves the PR branch ledger alone' flaked on the Mac 2026-10-27 with status 141: git log ... | grep -q under pipefail, grep -q exits on the first match and git log dies of SIGPIPE (suggested: capture the log into a variable first, then grep it, as other tests do) (fix: small) (PR: AltDoug/found-issues#292) (fixed: 2026-10-27)
- [open] 2026-10-29 lib/sync.sh:770 — SessionStart sync still writes the ledger on a pull-only clone through auto-archive (and merged-PR closures): measured 2026-10-29 on a main-only agent-config clone, sync moved 5 [fixed] entries to found-issues-archive.md; git refuses a later pull --ff-only even when upstream made the byte-identical edit, so the clone's update path breaks whenever upstream commits the same archive (suggested: read-only sync mode for consumer clones) (decide: should sync get a read-only mode for pull-only clones [e.g. a git config key or FOUND_ISSUES_SYNC_WRITE=off that skips every ledger write], or should such clones keep a deploy-side workaround [FOUND_ISSUES_AUTO_ARCHIVE=off plus a checkout of docs/ before pull]?)
33 changes: 32 additions & 1 deletion lib/sync.sh
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,37 @@ cmd_sync() {
local shallow_repo=0
[[ "$(git rev-parse --is-shallow-repository 2>/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 <sha> 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<sha>\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)"
Expand Down Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions tests/bin-shims/gh
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,23 @@ case "$cmd $sub" in
fi
done <<<"$GH_MOCK_PR_FILES"
fi
# repos/<o>/<r>/commits/<sha>: 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
;;
Expand Down
88 changes: 88 additions & 0 deletions tests/cli-sync-commit-stale.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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" ]
}
Loading