Files
marketplace/AGENTS.md
T
naudachu 6d01ead245 fix: resolve the issue store path independently of the working directory
ISSUE_ROOT was the relative `tmp/issues`, so "the store" was whatever
directory the shell happened to be standing in. It is the --out default
in all eight scripts of both layers, which made one `cd` — and a `cd`
outlives the command that ran it — enough for readers to report an empty
store on a full one and for writers to quietly build a second store
beside the first. `issue_index.py` run from inside tmp/issues left
tmp/issues/tmp/issues/ behind and exited 0.

The anchor is issue.py's own __file__, not cwd. A script's location is a
fact about the installation; cwd is a fact about the last `cd`, and the
scripts are invoked by path from wherever the agent happens to be. From
there `store_root()` walks up to the nearest repo marker — `.git`
(exists(), not isdir(): a worktree's .git is a file) or AGENTS.md for a
copy taken out of git — and joins tmp/issues. Markers rather than a
fixed number of `..` hops, because the layout is not a promise. cwd is
tried only if the scripts are not inside a repository at all.

The function lives in the domain layer and skills/sync imports it, so
both layers agree by construction — the direction the layering rule
allows. skills/issue stays stdlib-only.

An explicit --out still wins and is used exactly as typed: a relative
--out stays relative to cwd, because that is what the operator asked
for. No new environment surface.

Two consequences the issue also asked for:

- Missing is no longer reported as empty. `store_error()` returns one
  message for a path that is not there and another for a store with no
  issues in it.
- Nothing conjures a store as a side effect of a write. save() and
  issue_index.build() require it instead of os.makedirs'ing it; only
  issue_new.py and pull.py create one, and both say so on stderr.

Establishes tests/ — plain stdlib unittest, no pytest, no dependencies.
The store tests build a throwaway repo in a TemporaryDirectory (a .git
marker, a copy of both script layers, fixture issues) and run the real
scripts inside it as subprocesses from five different working
directories; tmp/issues/ is never touched. Against the pre-fix scripts
15 of the 21 fail, reproducing the report exactly — five stray stores,
including tmp/issues/tmp/issues.

    python3 -m unittest discover -s tests -v

Closes claude-skills/tea#15

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 15:40:07 +05:00

5.4 KiB

AGENTS.md

Project goals

  1. Unify and systematize issue workflow for the development team with minimal context usage. Issue operations are wrapped in scripts so agents spend tokens on the task, not on re-deriving commands and formats.
  2. Keep the tracker out of the work. An issue is a unit of work first and a Gitea row second. The two are separate layers, and the first one does not know the second exists.
  3. Route all Gitea interaction through the tea CLI via scripts instead of direct ad-hoc calls wherever possible. Scripts give deterministic, reviewable behavior; the tea-guard hook enforces that every tea invocation runs under the operator-pinned login.

Layers

The hard rule of this repo. Knowledge flows one way only:

skills/issue    DOMAIN     what an issue is: format, validation, dependency graph
      ▲                    offline — no tracker, no network, stdlib imports only
      │ imports
skills/sync     BRIDGE     map.py    md <-> Gitea JSON, pure functions, no I/O
                           _gitea.py login pin, tea api, pagination, filters
skills/use      REFERENCE  tea CLI docs for everything that is not an issue
skills/auth     IDENTITY   pin the login the whole tracker side runs under
      ▲
      │ calls
agents/         EXECUTION  tea-runner: runs the scripts, reports a receipt

skills/issue never imports from skills/sync. Delete skills/sync and the domain layer keeps working. The check is mechanical — every import under skills/issue/scripts/ is stdlib, and subprocess is not among them:

grep -rh '^import \|^from ' skills/issue/scripts/ | sort -u

If a tracker concept (issue number, login, HTTP call, label color) shows up in the domain layer, it is in the wrong place.

Repo layout

  • skills/auth — pin the Gitea login used by tea (/tea:auth)
  • 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, taxonomy, dependency graph
    • scripts/issue_new.py — create a local issue from its type template
    • scripts/issue_check.py — validate against the format
    • scripts/issue_tree.py — draw the dependency graph
    • 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
  • skills/usetea CLI reference for everything that is not an issue (/tea:use); references/tea/ holds the command docs
  • agents/tea-runner.md — subagent on Haiku that executes the scripts and returns a compact receipt. Delegate batches (bulk pull, push a named set, bootstrap labels, rebuild the index), never the thinking: it has no Edit and no Write, may not --force, and may not decide what an issue says. 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)
  • tests/ — stdlib unittest, no third-party anything

Tests

python3 -m unittest discover -s tests -v

Plain unittest; no pytest, no dependencies — the scripts under test are 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/. Anything that needs a store builds a throwaway repository in a tempfile.TemporaryDirectory() — a .git marker, a copy of the script layers, fixture issues — 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.

Local issue store

tmp/issues/ (gitignored) is the store, not a cache of Gitea. One flat markdown file per issue, named by its slug, with one metadata field per line so plain grep works without a parser.

  • The path is <repo root>/tmp/issues, resolved from issue.py's own location, not from cwd. issue.store_root() walks up from __file__ to the nearest .git or AGENTS.md — so every script in both layers sees one store whatever directory it is run from. An explicit --out overrides it and is used exactly as typed; a relative --out stays relative to cwd.
  • Nothing creates the store as a side effect of a write. Readers distinguish "does not exist" from "is empty"; only issue_new.py and pull.py create it, and they say so on stderr.
  • Identity is the slug (wire-sqlc-appclick.md), never a tracker number. Numbers live in the gitea: field.
  • origin: local is a durable state. An issue that never leaves this machine is complete and valid, not a draft.
  • Pushing is additive: the file is never deleted, it gains gitea: / url: / synced:.
  • Pulling overwrites the body — a fetch, not a merge.
  • No drift tracking. synced: tells you how old your copy is; re-pull when it matters.