chore: make work-item a reusable subproject

This commit is contained in:
2026-07-13 16:21:28 +02:00
parent e35e5040a3
commit 43ffde71da
15 changed files with 1740 additions and 620 deletions

227
work_item/README.md Normal file
View File

@@ -0,0 +1,227 @@
# work-item
Safe, deterministic Git and Gitea workflow automation for a single agent working an
ordered user-story backlog.
`work-item` selects the first eligible story, creates its feature branch, runs the
configured automated tests, checks changes for unsafe content, pushes the commit,
opens a pull request, and closes the issue only after the merge is verified.
## Highlights
- deterministic numeric story selection with dependency checks;
- one active story at a time, with resume-safe local state;
- idempotent resume after blocked work or partially completed remote operations;
- mandatory automated tests before commit and push;
- configurable file, size, secret, and private-data safety checks;
- pull-request creation, optional CI gating, merge verification, and issue closure;
- TOML, YAML, dotenv, and process-environment configuration;
- no workflow state committed to the repository.
## Requirements
- Python 3.12 or newer;
- Git;
- [`tea`](https://gitea.com/gitea/tea), authenticated against the target Gitea
instance;
- PyYAML 6 or newer when using `.yml` or `.yaml` configuration.
The repository must use story titles in this form:
```text
US01-01 — Short imperative title
```
The two numeric groups define epic and story order. The helper recognizes these
status labels:
```text
status/backlog status/ready status/in-progress
status/review status/blocked status/done
```
Dependencies must be configured through Gitea's issue-dependency feature.
## Installation
The directory is a self-contained Python subproject. From the host repository root:
```bash
python -m pip install -e work_item
work-item --help
```
For a repository-local installation, copy the complete `work_item/` directory into the
target repository and run `work_item/scripts/work-item`. Its Python wrapper adds the
bundled `src/` directory to `PYTHONPATH`, prefers a compatible Conda base interpreter,
and otherwise uses `python3.13` or `python3.12`.
```text
work_item/
├── .work-item.yml # host-project configuration
├── README.md
├── pyproject.toml # independently installable package
├── scripts/ # repository-local launchers
├── src/work_item/ # implementation
└── tests/ # unit and fake-Gitea end-to-end tests
```
## Quick start
Create one supported configuration file either in the repository root or inside the
`work_item/` directory. The repository root takes precedence; within each location,
automatic discovery uses this order:
1. `.work-item.toml`
2. `.work-item.yml`
3. `.work-item.yaml`
4. `.work-item.env`
Then run:
```bash
work_item/scripts/work-item next
work_item/scripts/work-item claim
# Implement the claimed story and its tests.
work_item/scripts/work-item status
work_item/scripts/work-item submit --test "pytest -q"
# Review the displayed diff, then authorize Git/PR creation.
work_item/scripts/work-item submit --test "pytest -q" --yes
# After review and successful CI:
work_item/scripts/work-item complete --merge
```
When installed, the shorter `work-item` command is equivalent. Use a non-default file
explicitly with `work-item --config path/to/config.yml next`.
## Configuration
### TOML
```toml
[repository]
slug = "owner/project"
login = "my-tea-login"
assignee = "agent-user"
remote = "origin"
main_branch = "main"
[workflow]
branch_prefix = "us"
require_ci = true
required_tests = ["pytest -q", "ruff check ."]
[safety]
max_file_bytes = 5000000
allow = ["tests/fixtures/**"]
deny = ["*.env", "*.pem", "data/**", "photos/**", "*.jpg"]
```
### YAML
```yaml
repository:
slug: owner/project
login: my-tea-login
assignee: agent-user
remote: origin
main_branch: main
workflow:
branch_prefix: us
require_ci: true
required_tests:
- pytest -q
- ruff check .
safety:
max_file_bytes: 5000000
allow:
- tests/fixtures/**
deny:
- "*.env"
- "*.pem"
- data/**
- photos/**
```
### dotenv
Lists use JSON arrays so test commands and glob patterns remain unambiguous:
```dotenv
WORK_ITEM_REPO_SLUG=owner/project
WORK_ITEM_LOGIN=my-tea-login
WORK_ITEM_ASSIGNEE=agent-user
WORK_ITEM_REMOTE=origin
WORK_ITEM_MAIN_BRANCH=main
WORK_ITEM_BRANCH_PREFIX=us
WORK_ITEM_REQUIRE_CI=true
WORK_ITEM_REQUIRED_TESTS='["pytest -q", "ruff check ."]'
WORK_ITEM_MAX_FILE_BYTES=5000000
WORK_ITEM_ALLOW='[".work-item.env", "tests/fixtures/**"]'
WORK_ITEM_DENY='["*.env", "*.pem", "data/**", "photos/**"]'
```
The same `WORK_ITEM_*` variables may be exported by the calling process. Process
environment values override values from any configuration file, which is useful for
CI or per-machine `tea` login and assignee settings.
Only repository slug, login, and assignee are required. Other fields use the defaults
shown above.
## Commands
| Command | Behavior |
|---|---|
| `next` | Shows the lowest-numbered unassigned story whose dependencies are closed. |
| `claim` | Claims the next story, or safely resumes the existing in-progress/blocked story. |
| `status` | Shows local workflow state, current branch, and changed files. |
| `submit` | Runs tests and safety checks; without `--yes`, stops after the review summary. |
| `complete` | Verifies CI/merge state, closes the issue, updates main, and removes the branch. |
| `block` | Records a precise blocking reason without discarding the branch or local work. |
`--test` is repeatable. Configured `required_tests` are always included, and duplicate
commands are run only once.
## Safety model
Before submission, the helper:
1. enumerates tracked and untracked changes;
2. rejects denied paths unless an allow pattern explicitly overrides them;
3. rejects files larger than `max_file_bytes`;
4. scans readable text for common credentials, authorization headers, and private keys;
5. runs `git diff --check` before staging and again against the staged diff;
6. runs every required and story-specific automated test;
7. requires a separate `--yes` invocation before commit and push.
Keep real media, databases, runtime data, credentials, and generated artifacts in the
deny list. Allow only deterministic, non-private test fixtures.
## Resume and recovery
State is stored at `.git/work-item-state.json`, so it is local and never committed.
- Re-running `claim` on the story branch preserves dirty implementation work.
- Re-running `claim` from another clean branch switches back to the claimed branch.
- Re-running `claim` after `block` returns the same story to `status/in-progress`.
- If push succeeds but PR creation or review labeling fails, `submit --yes` reuses the
pushed commit and existing PR instead of committing twice.
- A story in review cannot be replaced by a new claim; complete or block it first.
## Development
From this repository:
```bash
ruff check --no-cache work_item/src work_item/tests
ruff format --check --no-cache work_item/src work_item/tests
work_item/scripts/python -m unittest discover -s work_item/tests -v
```
The end-to-end tests use temporary Git repositories and a deterministic fake `tea`
binary. They do not mutate a live Gitea project.