merge: drop the local copy after a successful push

# Conflicts:
#	AGENTS.md
#	agents/tea-runner.md
This commit is contained in:
naudachu
2026-08-10 16:40:21 +05:00
14 changed files with 1410 additions and 118 deletions
+5 -2
View File
@@ -115,8 +115,11 @@ Edit the file. Change `state:` to close it, edit `labels:`, add ids to
`depends:`. Re-run `issue_check.py` afterwards, and `issue_index.py` to refresh
the table. Checkboxes are the exception — use `issue_ac.py`, below.
If the issue is synced (`origin: gitea`), your edit is local until you run
`push.py --update` from `/tea:sync`. Nothing tracks that drift automatically.
If the issue is synced (`origin: gitea`), the file is a working copy: your edit
is local until you run `push.py --update` from `/tea:sync`, and that push
**deletes the file** once Gitea has it. Nothing tracks drift, and with one copy
at a time there is little to track — a file that is still here has not been
pushed. Get it back with `pull.py <n>`; the slug does not change.
## Ticking checkboxes
+30 -4
View File
@@ -14,13 +14,22 @@ sync layer's business — see `/tea:sync`.
An issue is one file, `tmp/issues/<id>.md`, and `id` is a slug: lowercase
ASCII, digits, single dashes, derived from the title. **The slug is the
identity.** It is stable for the life of the issue — a retitled issue keeps its
slug, and an issue pushed to a tracker keeps it too. Tracker numbers are a
foreign key stored in a field, never the name of anything.
slug; an issue pushed to a tracker, deleted locally and fetched back a month
later keeps it too. Tracker numbers are a foreign key stored in a field, never
the name of anything.
```
tmp/issues/wire-sqlc-appclick.md
```
A slug never contains a dot, which is how the store tells an issue from the
files parked beside it (`<id>.comments.md`).
Stability is a promise the format makes, so something has to keep it once the
file is gone. That is the sync layer's problem and its answer is a marker in the
body — see `/tea:sync`; the domain neither writes nor reads it, and it never
appears in the file on disk.
## Metadata block
One field per line, lists inline, so plain `grep` works without a parser:
@@ -69,8 +78,25 @@ sync layer's business — the domain carries `gitea:` and the rest through
load/save verbatim and never reads them. That passthrough is why one file can
represent a local issue and a synced one without a second format.
`origin: local` is a **durable state, not a pending one.** An issue that never
leaves this machine is complete and valid. Pushing is optional and additive.
`origin: local` is a **complete state, not a pending one.** An issue that never
leaves this machine is valid and finished work; pushing it is optional and
nothing here treats it as a draft.
It is not a *permanent* state, and this is the one place where the file's fate
depends on it:
| `origin:` | what the file is | what a push does to it |
|---|---|---|
| `local` | the issue itself — the only copy there is | creates it in the tracker, then deletes the file |
| a tracker | a working copy of something the tracker already has | updates the tracker, then deletes the file |
**A successful push deletes `tmp/issues/<id>.md`** (and `<id>.comments.md`), on
create and on `--update` alike. What is in the store is what has not left this
machine; everything else is fetched again when it is needed. The rule, its
safety conditions, and how the slug survives are `/tea:sync`'s to state.
The `id` never changes across that round trip, which is why `depends:` in other
issues keeps working. That is the format's promise; the mechanism is not.
## Language rules
+15 -3
View File
@@ -262,8 +262,11 @@ class Issue(object):
@property
def is_local(self):
"""True while this issue exists nowhere but here — a durable state,
not a pending one."""
"""True while this issue exists nowhere but here.
A complete state, not a pending one — and the state in which this file
is the only copy of the work. An issue whose `origin` names somewhere
else can be fetched from there again; this one cannot."""
return self.origin == LOCAL
# -- taxonomy views ----------------------------------------------------
@@ -617,10 +620,19 @@ def path_of(root, id):
def all_ids(root):
"""Every issue in the store, by slug.
An issue file is named by its slug and a slug has no dot in it (SLUG_OK),
so `<id>.comments.md` — the thread the sync layer parks beside an issue —
is not one, and neither is anything else that grew a second extension.
Without that rule `wire-sqlc.comments` reads as an issue called
`wire-sqlc.comments`, and a bare `push.py` tries to file the comment thread
as a unit of work."""
if not os.path.isdir(root):
return []
return sorted(f[:-3] for f in os.listdir(root)
if f.endswith(".md") and not f.startswith((".", "INDEX", "tree-")))
if f.endswith(".md") and not f.startswith((".", "INDEX", "tree-"))
and "." not in f[:-3])
def load(root, id):
+6 -2
View File
@@ -3,8 +3,12 @@
issue_new.py — create an issue in the local store. Offline, always.
The issue is real the moment this writes the file. Nothing is pending, nothing
is a draft awaiting a tracker: `origin: local` is a durable state, and pushing
it to Gitea later (see /tea:sync) is optional and additive.
is a draft awaiting a tracker: `origin: local` is a complete state and pushing
it to Gitea later (see /tea:sync) is optional.
While it says `local`, this file is the ONLY copy of the work — the store, not
a cache of anything. That is what a push changes: it hands the issue to the
tracker and removes the file.
issue_new.py --type task --title "Wire sqlc into the appclick repo layer" \
--label tech/sql --label comp/appclick