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, authenticated against the target Gitea instance;- PyYAML 6 or newer when using
.ymlor.yamlconfiguration.
The repository must use story titles in this form:
US01-01 — Short imperative title
The two numeric groups define epic and story order. The helper recognizes these status labels:
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:
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.
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:
.work-item.toml.work-item.yml.work-item.yaml.work-item.env
Then run:
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
[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
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:
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.
Kanban / project board position
work-item tracks each story's stage with the status/* labels
(status/backlog → status/in-progress → status/review → status/done, plus
status/blocked) and by opening/closing the issue. These labels are the board.
It does not move cards between columns of a Gitea project (Kanban) board, and this is a Gitea limitation rather than a gap in the helper:
- Gitea's REST API (checked against 1.25) exposes no project/column/board
endpoints — the swagger spec has none, and
teahas no project command. - Card movement uses Gitea's internal web endpoints, which require a logged-in
browser session (session cookie + CSRF). An API token authenticates only
/api/*, not web routes (a token request to a web page redirects to/user/login), so the helper — which is token-only by design — cannot reach them. - Gitea does not auto-move a closed issue into a "Done" column either. A closed story keeps whichever column it was in.
Recommended: use the labels as the board. Filter the issue list by label to get an equivalent view without touching the unreachable board, e.g.:
/domverse/photoanalyzer/issues?labels=status/in-progress
/domverse/photoanalyzer/issues?labels=status/review
/domverse/photoanalyzer/issues?q=&type=all&labels=status/done&state=closed
If a project board must show card positions, move the cards manually in the web UI, or drive the web endpoints from a real browser session (out of scope for this token-only helper). Do not store web login credentials in the helper to work around this.
Safety model
Before submission, the helper:
- enumerates tracked and untracked changes;
- rejects denied paths unless an allow pattern explicitly overrides them;
- rejects files larger than
max_file_bytes; - scans readable text for common credentials, authorization headers, and private keys;
- runs
git diff --checkbefore staging and again against the staged diff; - runs every required and story-specific automated test;
- requires a separate
--yesinvocation 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
claimon the story branch preserves dirty implementation work. - Re-running
claimfrom another clean branch switches back to the claimed branch. - Re-running
claimafterblockreturns the same story tostatus/in-progress. - If push succeeds but PR creation or review labeling fails,
submit --yesreuses 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:
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.