feat: tick in-body checkboxes from a domain script
A checkbox is the one part of a body that is state and not prose.
Everything else is written once; boxes get ticked as the work goes, and
until now the only ways to tick one were a human with an editor or a
model rewriting the whole body. The second is worse: the rewrite re-flows
lines and re-words sentences, so the issue's diff swells around a change
that means one character. Progress was invisible too — issue_index.py
builds INDEX.md from metadata and never looked inside a body, so "3 of 7
done" required opening the file.
All three pieces are domain: a checkbox is body syntax, which is part of
the answer to "what is an issue". The parser goes in issue.py so the sync
layer can reuse it instead of redefining the format on its own side.
issue.py gains checkboxes(text) -> [Checkbox(index, line, end_line,
checked, text, section)], plus set_checkbox(text, item, checked) and
checkbox_progress(text). All pure, no I/O, importable from another layer.
The scan covers the whole text, in any section: the type/feature template
keeps child issues as checkboxes under `## Issues`, so binding the parser
to `## Acceptance criteria` would silently lose half of them; the heading
is recorded, never required. Only a marker line opens an item, so a
wrapped continuation line belongs to the item above it rather than
counting as one of its own. A `- [ ]` inside a code fence is an example
of the markup and is skipped. Line numbers are relative to the text
given, which is what lets a caller work on a body or on a whole file.
issue_ac.py lists the items numbered, grouped by heading, and ticks one
by number or by substring. An ambiguous substring is an error that prints
the matches — a coin flip would tick the wrong box and look like it
worked. It patches the file rather than round-tripping through
Issue.to_text(), so exactly one character changes: metadata order,
wording, wrapping, trailing whitespace and CRLF endings all come back
byte for byte, proven by a diff in the tests.
INDEX.md gains a progress column: `3/7` for an issue with checkboxes,
blank for one without. Counted off the body at build time and stored in
no field — a second copy of the state would be wrong by the next edit.
issue_check.py is unchanged and stays that way on purpose: an unticked
box is work not done yet, not a malformed issue, and validate() carries a
comment saying so.
Delivering a tick to the tracker is out of scope — that is push.py
--update in /tea:sync.
format.md gets one clarifying bullet. It said acceptance criteria are
checkboxes but never said what a checkbox is, so the parser had to settle
questions the format left open: any section, wrapped items, fenced
examples. Those rules are now written down where the parser and the sync
layer can both point at them.
tests/ is new, and is the convention: plain stdlib unittest, no pytest
and no third-party deps, since the code under test may not have
dependencies either. Scripts are imported via sys.path.insert and every
fixture is built in a TemporaryDirectory, never in tmp/.
python3 -m unittest discover -s tests -v 32 tests, OK
skills/issue/scripts/ still imports stdlib only, with no subprocess.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+39
-3
@@ -36,6 +36,7 @@ All offline, all in `<skill-base-dir>/scripts/`.
|
||||
|---|---|
|
||||
| `issue_new.py --type T --title "…"` | create `tmp/issues/<slug>.md` from the type's template |
|
||||
| `issue_check.py [id…]` | validate against the canonical format; exit 1 on errors |
|
||||
| `issue_ac.py <id> [--check N\|TEXT]` | list the body's checkboxes; tick or untick one |
|
||||
| `issue_tree.py [id…]` | draw the dependency graph from `depends:` |
|
||||
| `issue_index.py` | rebuild `tmp/issues/INDEX.md` |
|
||||
| `issue.py` | the domain module the others import — not a command |
|
||||
@@ -92,13 +93,48 @@ decision — `/tea:sync` — and does not change the file's status here.
|
||||
|
||||
## Editing an issue
|
||||
|
||||
Edit the file. Change `state:` to close it, edit `labels:`, tick checkboxes in
|
||||
`## Acceptance criteria`, add ids to `depends:`. Re-run `issue_check.py`
|
||||
afterwards, and `issue_index.py` to refresh the table.
|
||||
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.
|
||||
|
||||
## Ticking checkboxes
|
||||
|
||||
A checkbox is the one part of a body that is **state** and not prose, so it has
|
||||
a command of its own. Never rewrite a body just to tick a box: the rewrite
|
||||
re-flows lines and re-words sentences, and the issue's diff swells around a
|
||||
change that means one character.
|
||||
|
||||
```bash
|
||||
python3 <skill-base-dir>/scripts/issue_ac.py wire-sqlc-appclick
|
||||
python3 <skill-base-dir>/scripts/issue_ac.py wire-sqlc-appclick --check 3
|
||||
python3 <skill-base-dir>/scripts/issue_ac.py wire-sqlc-appclick --check "регресс"
|
||||
python3 <skill-base-dir>/scripts/issue_ac.py wire-sqlc-appclick --uncheck 3
|
||||
```
|
||||
|
||||
With no flag it prints the numbered list with each item's state, grouped by the
|
||||
heading the item sits under. `--check` / `--uncheck` take that number or a
|
||||
substring of the item's text (case-insensitive).
|
||||
|
||||
- **Every checkbox in the body counts, not just `## Acceptance criteria`.** A
|
||||
`type/feature` keeps its children as checkboxes under `## Issues`, and they
|
||||
are numbered in the same list. The script is named after the section most
|
||||
boxes live in, nothing more.
|
||||
- **A substring must match exactly one item.** Two matches is an error that
|
||||
lists them; pick by number instead. It never guesses.
|
||||
- **Exactly one character of the file changes.** Metadata, wording, wrapping
|
||||
and trailing whitespace all come back byte for byte, so `git diff` and the
|
||||
tracker's diff show the tick and nothing else.
|
||||
- Examples inside a ``` fence are markup, not state — they are skipped.
|
||||
- `INDEX.md` gains a `progress` column (`3/7`, blank when the issue has no
|
||||
boxes), recomputed from the body on every build and stored in no field.
|
||||
`issue_ac.py` rebuilds the index after a successful tick.
|
||||
|
||||
Getting the tick to the tracker is a separate step — `push.py --update` in
|
||||
`/tea:sync`.
|
||||
|
||||
## Writing a proper description
|
||||
|
||||
Issues get filed on the run — "comments aren't pulled", "the guard broke".
|
||||
|
||||
Reference in New Issue
Block a user