Merge origin/main into feat/close-script
Two conflicts git could see (AGENTS.md, skills/sync/SKILL.md) and one it could not: the payload-root change removed api()'s out_root parameter, so close.py stops passing it, and its payload test now asserts PAYLOAD_ROOT instead of the deleted PAYLOAD_DIR.
This commit is contained in:
@@ -24,11 +24,17 @@ skills/page DOMAIN what a page tree is: title <-> path, order, the index
|
||||
▲ offline — no tracker, no network, stdlib imports only
|
||||
│ imports
|
||||
skills/sync BRIDGE map.py md <-> Gitea issue JSON, pure, no I/O
|
||||
_gitea.py login pin, tea api, pagination, filters
|
||||
_gitea.py tea api, pagination, filters, payloads
|
||||
skills/wiki BRIDGE wikimap.py md <-> Gitea wiki JSON, pure, no I/O
|
||||
transport is _gitea.py — there is no second one
|
||||
skills/use REFERENCE tea CLI docs for everything that is not an issue
|
||||
│ imports
|
||||
▼
|
||||
skills/auth IDENTITY pin the login the whole tracker side runs under
|
||||
▲ pin.py where the pin is and how it is found —
|
||||
│ imports imported by _gitea.py AND by hooks/tea-guard.sh
|
||||
hooks/tea-guard so `tea` and the scripts cannot disagree
|
||||
|
||||
skills/use REFERENCE tea CLI docs for everything that is not an issue
|
||||
▲
|
||||
│ calls
|
||||
agents/ EXECUTION tea-runner: runs the scripts, reports a receipt
|
||||
@@ -51,6 +57,8 @@ If a tracker concept (issue number, login, HTTP call, label color, `sub_url`,
|
||||
## Repo layout
|
||||
|
||||
- `skills/auth` — pin the Gitea login used by `tea` (`/tea:auth`)
|
||||
- `scripts/pin.py` — the one written copy of the pin's location and search
|
||||
order (see "The login pin" below); stdlib, no subprocess, no network
|
||||
- `skills/issue` — issues as units of work (`/tea:issue`), entirely offline
|
||||
- `references/format.md` — canonical issue format; single source of truth
|
||||
- `scripts/issue.py` — domain module: slug identity, parse/render, validation,
|
||||
@@ -63,10 +71,12 @@ If a tracker concept (issue number, login, HTTP call, label color, `sub_url`,
|
||||
- `scripts/issue_index.py` — rebuild `tmp/issues/INDEX.md`
|
||||
- `skills/sync` — move issues between the local store and Gitea (`/tea:sync`)
|
||||
- `scripts/map.py` — md ↔ Gitea JSON, pure, no I/O; label colors live here
|
||||
- `scripts/_gitea.py` — transport: login pin, `tea api`, pagination, filters,
|
||||
label ids, the remote-id map
|
||||
- `scripts/pull.py`, `push.py`, `remote.py`, `comment.py`, `labels.py`
|
||||
- `scripts/_gitea.py` — transport: `tea api`, pagination, filters, label ids,
|
||||
the remote-id map, `tmp/payload/`; the login comes from `auth/pin.py`
|
||||
- `scripts/pull.py`, `push.py`, `remote.py`, `comment.py`
|
||||
- `scripts/close.py` — the state field, both ways; explicit ids only
|
||||
- `scripts/labels.py` — put the canonical `type/*` and `severity/*` set into a
|
||||
repository; reads the domain taxonomy, never the store
|
||||
- `skills/page` — a discussion's artifacts as a page tree (`/tea:page`),
|
||||
entirely offline
|
||||
- `references/pages.md` — canonical page-tree format; single source of truth
|
||||
@@ -90,10 +100,40 @@ If a tracker concept (issue number, login, HTTP call, label color, `sub_url`,
|
||||
Delegating a single call costs more than running it inline — the win is the
|
||||
loop, the retry, and the error triage.
|
||||
- `hooks/` — PreToolUse hooks: `tea-guard` blocks or rewrites `tea` invocations
|
||||
that don't use the pinned login; `agents-sync` keeps every directory canonical
|
||||
(`AGENTS.md` real file, `CLAUDE.md` symlink to it)
|
||||
that don't use the pinned login (resolving it through `auth/pin.py`);
|
||||
`agents-sync` keeps every directory canonical (`AGENTS.md` real file,
|
||||
`CLAUDE.md` symlink to it)
|
||||
- `tests/` — stdlib `unittest`, no third-party anything
|
||||
|
||||
## The login pin
|
||||
|
||||
`<project root>/.claude/settings.local.json` → `env.GITEA_LOGIN`, written by
|
||||
`/tea:auth` and read at call time. **The search order is written once, in
|
||||
`skills/auth/scripts/pin.py`**, and both callers import it: the transport
|
||||
(`_gitea.require_login`) and the `tea-guard` hook. Neither spells the path or
|
||||
the walk itself, and a test asserts they don't.
|
||||
|
||||
Start directories, first hit wins: `$CLAUDE_PROJECT_DIR`, then a hint the
|
||||
caller supplies (the hook passes the Bash payload's `cwd`; a script passes
|
||||
nothing), then the current directory. Each one is searched up its parent chain,
|
||||
and then — only if that found nothing — up the parent chain of the **main
|
||||
working tree of any linked worktree** met on the way, reached by reading
|
||||
`gitdir:` out of a `.git` *file* and following `commondir`.
|
||||
|
||||
**The pin is not resolved from `__file__`, and that asymmetry with
|
||||
`issue.store_root`/`page.store_root`/`_gitea.PAYLOAD_ROOT` is deliberate.**
|
||||
Where an installation keeps its files is a fact about the installation; whose
|
||||
login a project runs under is a fact about the project. A plugin installed
|
||||
outside any repository and pointed at somebody else's tree must not answer the
|
||||
second question from its own directory. So the search runs from the working
|
||||
directory upward — and reaches a worktree's main checkout by asking git.
|
||||
|
||||
Two failures this replaces, both worth remembering: a git worktree is a
|
||||
*sibling* of the main checkout, so the untracked pin is not on its parent chain
|
||||
and the whole sync layer died there while `tea` in the same directory worked;
|
||||
and the cure it invited — `/tea:auth` inside the worktree — writes a second
|
||||
settings file into a directory that is deleted with the worktree.
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
@@ -105,12 +145,18 @@ stdlib-only and the tests hold the same line. `skills/*/scripts/` are not
|
||||
packages, so a test that needs the domain module imports it with
|
||||
`sys.path.insert`.
|
||||
|
||||
**A test never touches `tmp/issues/` or `tmp/wiki/`.** Anything that needs a
|
||||
store builds a throwaway repository in a `tempfile.TemporaryDirectory()` — a
|
||||
`.git` marker, a copy of the script layers, fixture issues or artifacts — and
|
||||
runs the real scripts inside it as subprocesses. That is the only way to test
|
||||
behavior that depends on where a script is run from, and it keeps the
|
||||
developer's own store out of the blast radius.
|
||||
**A test never touches `tmp/issues/`, `tmp/wiki/` or `tmp/payload/`.** Anything
|
||||
that needs a store builds a throwaway repository in a
|
||||
`tempfile.TemporaryDirectory()` — a `.git` marker, a copy of the script layers,
|
||||
fixture issues or artifacts — and runs the real scripts inside it as
|
||||
subprocesses. That is the only way to test behavior that depends on where a
|
||||
script is run from, and it keeps the developer's own store out of the blast
|
||||
radius.
|
||||
|
||||
`tmp/payload/` is in that list because `_gitea.PAYLOAD_ROOT` is resolved once,
|
||||
from the module's own location: a test that stubs the transport *below* `api()`
|
||||
— at `subprocess`, to exercise a non-2xx — reaches the real write. Such a test
|
||||
patches `PAYLOAD_ROOT` to its own temp directory too.
|
||||
|
||||
## Local issue store
|
||||
|
||||
@@ -139,7 +185,9 @@ line so plain grep works without a parser.
|
||||
- **A successful push deletes the local file** (`<id>.md` and
|
||||
`<id>.comments.md`), and prints the number and URL the issue now lives at.
|
||||
`--update` too: one rule, no exception. What is in the store is what has not
|
||||
left. Get it back with `pull.py <n>`.
|
||||
left. Get it back with `pull.py <n>` — which brings its blockers back with it:
|
||||
a pull returns the unit of work, not one row of it. `--no-deps` narrows it to
|
||||
the one issue, and the cost of the default is in `pull.py`'s docstring.
|
||||
- Deletion happens only after a confirmed tracker response and only after
|
||||
`.remote.json` has been written. Network down, non-2xx, an answer that does
|
||||
not carry the right number: the file stays and the run stops. A never-pushed
|
||||
@@ -184,3 +232,20 @@ organized. Same stance as the issue store, resolved the same way from
|
||||
disagree, because a page tree is worked on locally and an issue is not.
|
||||
- The `tea` CLI has no wiki subcommand. `tea api` is the only route, through
|
||||
`_gitea.py`.
|
||||
|
||||
## Request payloads
|
||||
|
||||
`tmp/payload/` (gitignored) holds the JSON bodies `tea api -d @file` was given,
|
||||
one file per named request, kept after the call for a retry or a post-mortem.
|
||||
It is **not a store and holds nobody's only copy** — deleting it costs nothing.
|
||||
|
||||
- One directory for every caller — sync and wiki both — resolved from
|
||||
`_gitea.py`'s own location, so which command wrote a body does not change
|
||||
where it landed. `_gitea.api` takes no directory argument; that it once did
|
||||
is exactly how a label bootstrap came to create `tmp/issues/`.
|
||||
- It is created lazily, by the first write of a run, and only then: a `--dry-run`
|
||||
or a run with nothing to send leaves no directory behind.
|
||||
- **A scratchpad may never sit inside a store.** Store contents are the thing
|
||||
being tracked; request bodies are debris of the transport. When the two share
|
||||
a path, an operation that touches no issue at all still materializes the issue
|
||||
store, and the operator's `ls tmp/issues` starts lying about what exists.
|
||||
|
||||
Reference in New Issue
Block a user