fb5445915f
`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>
73 lines
2.3 KiB
Python
73 lines
2.3 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
issue_check.py — validate issues against the canonical format. Offline.
|
|
|
|
The same check the sync layer runs before it pushes anything, available on its
|
|
own so a local-only issue can be held to the format without a tracker being
|
|
involved. Errors mean malformed; warnings mean it deviates from its type's
|
|
template or its graph looks suspect.
|
|
|
|
issue_check.py every issue in the store
|
|
issue_check.py wire-sqlc one issue
|
|
issue_check.py --quiet exit code only (0 clean, 1 errors)
|
|
|
|
Format reference: ../references/format.md
|
|
"""
|
|
import argparse
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
import issue # noqa: E402
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description="Validate local issues (offline)")
|
|
ap.add_argument("ids", nargs="*", help="ids to check (default: all)")
|
|
ap.add_argument("--quiet", action="store_true", help="exit code only")
|
|
ap.add_argument("--strict", action="store_true", help="treat warnings as errors")
|
|
ap.add_argument("--out", default=issue.ISSUE_ROOT,
|
|
help="store root (default: <project>/.tea/issues)")
|
|
args = ap.parse_args()
|
|
|
|
problem = issue.store_error(args.out)
|
|
if problem:
|
|
sys.exit("issue_check.py: %s" % problem)
|
|
|
|
issues = issue.load_all(args.out)
|
|
ids = args.ids or sorted(issues)
|
|
for i in ids:
|
|
if i not in issues:
|
|
sys.exit("issue_check.py: no issue %r in %s" % (i, args.out))
|
|
|
|
known = set(issues)
|
|
bad = 0
|
|
for i in ids:
|
|
err, warn = issue.validate(issues[i], known_ids=known)
|
|
if args.strict:
|
|
err, warn = err + warn, []
|
|
if err:
|
|
bad += 1
|
|
if args.quiet:
|
|
continue
|
|
if not err and not warn:
|
|
print("ok %s" % i)
|
|
continue
|
|
for e in err:
|
|
print("ERROR %s: %s" % (i, e))
|
|
for w in warn:
|
|
print("warn %s: %s" % (i, w))
|
|
|
|
for c in issue.find_cycles(issue.graph(issues)):
|
|
bad += 1
|
|
if not args.quiet:
|
|
print("ERROR cycle: %s" % " -> ".join(c))
|
|
|
|
if not args.quiet:
|
|
print("%d issue(s) checked, %d with errors" % (len(ids), bad))
|
|
return 1 if bad else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|