fix: resolve the issue store from the project, not the plugin

`issue.store_root` and `_gitea.PAYLOAD_ROOT` were anchored on `__file__`, on
the reasoning that where an installation keeps its files is a fact about the
installation. That holds for an installation and not for a store.

Installed, the plugin therefore resolved every project's issues inside its own
directory — and a plugin cache is versioned, so the store moved on each
update:

    ~/.claude/plugins/cache/tea/tea/2.0.0/tmp/issues   5 files, 2 origin: local
    ~/.claude/plugins/cache/tea/tea/2.1.0/tmp/issues   12 files
    ~/.claude/plugins/cache/claude-skills/tea/2.2.0/   empty, the current one

Issues written from one project were invisible from the next, and an `origin:
local` file — which IS the issue, the only copy — was stranded a version bump
at a time. Two of them were.

The store is a fact about the project, exactly as the login pin is. So the
anchor is now an explicit marker an operator creates, `.tea/`, searched for up
from $CLAUDE_PROJECT_DIR and then cwd — the pin's order, so the two cannot
disagree about which project this is. Inferred markers were tried and are worse
than useless here: `.git` is in every clone including this plugin's own, and
the agents-sync hook writes an AGENTS.md next to every AGENTS.md, so the plugin
root always carried one and cwd never got a turn.

With no marker anywhere, `store_root()` is None and every entry point reports
which directories it searched. A store in a plausible-looking directory is the
failure this replaces, so nothing falls back to one.

- `.tea/` holds the store and the transport's scratchpad: `.tea/issues`,
  `.tea/payload`. One marker, one walk, one gitignore line.
- `issue_init.py` creates it, moves an old `tmp/issues` store in rather than
  copying, adds `.tea/` to `.gitignore`, and refuses to pick a winner when both
  sides hold the same file name.
- A linked worktree has no marker — it is gitignored — and reaches the main
  checkout's store by the hop the pin already took.
- `parents`, `gitdir_of` and `main_worktree` move from `pin.py` into the domain
  and `pin.py` imports them. The domain depends on nothing, so it is the layer
  all three callers can borrow from, and the walk stays written once: the
  guard, the transport and the store cannot disagree about a directory.

The suite stopped copying the script layers into its fixtures. That is what hid
this: with the scripts inside the fixture, the installation and the project
were the same directory. They are now deliberately far apart, and a regression
test asserts the plugin tree gains no files when commands run against a project
somewhere else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
naudachu
2026-08-11 13:38:39 +05:00
parent 27e4b6b1da
commit fb5445915f
30 changed files with 1193 additions and 430 deletions
+22 -6
View File
@@ -141,14 +141,16 @@ skills/
issue_ac.py list the body's checkboxes; tick one
issue_tree.py draw the dependency graph
issue_evict.py drop closed issues the tracker also has
issue_index.py rebuild tmp/issues/INDEX.md
issue_index.py rebuild .tea/issues/INDEX.md
issue_init.py create the .tea/ marker that makes a
directory a project; migrates tmp/issues in
sync/ /tea:sync — the bridge to Gitea
SKILL.md
scripts/
map.py md <-> Gitea JSON, pure functions, no I/O
_gitea.py transport: login pin, tea api, pagination, filters
pull.py Gitea -> tmp/issues/
push.py tmp/issues/ -> Gitea, then drops the local file
pull.py Gitea -> .tea/issues/
push.py .tea/issues/ -> Gitea, then drops the local file
remote.py discovery listing to stdout
comment.py post or edit a comment
close.py the state field, both ways
@@ -164,9 +166,23 @@ ever disagree, `AGENTS.md` is the one being worked from.
## Local issue store
Issues live in `tmp/issues/` (gitignore it) as flat markdown with one metadata
field per line — so `grep -l 'labels:.*type/bug' tmp/issues/*.md` works without
a parser.
Issues live in `.tea/issues/` as flat markdown with one metadata field per line
— so `grep -l 'labels:.*type/bug' .tea/issues/*.md` works without a parser.
**Run `issue_init.py` once per project.** It creates the `.tea/` marker, which
is what every script resolves the store from: they walk up from the working
directory to the nearest one. The marker is never inferred from the tree, and
with none anywhere the commands stop and name the directories they searched
rather than picking a plausible one.
```bash
python3 <plugin>/skills/issue/scripts/issue_init.py
```
It is idempotent, adds `.tea/` to `.gitignore`, and moves an older
`tmp/issues` store in if it finds one. Don't run it inside a git worktree: the
marker is gitignored, so a worktree has none by design and reaches the main
checkout's store on its own — exactly like the login pin.
An `origin: local` file **is** the issue — the store, and the only copy.
Anything with `origin: gitea` is a working copy of something the tracker