twoperson 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- twoperson-0.1.0/.github/workflows/ci.yml +33 -0
- twoperson-0.1.0/.github/workflows/publish.yml +33 -0
- twoperson-0.1.0/.gitignore +9 -0
- twoperson-0.1.0/CHANGELOG.md +31 -0
- twoperson-0.1.0/CONTRIBUTING.md +43 -0
- twoperson-0.1.0/LICENSE +21 -0
- twoperson-0.1.0/PKG-INFO +268 -0
- twoperson-0.1.0/README.md +248 -0
- twoperson-0.1.0/docs/PROTOCOL.md +169 -0
- twoperson-0.1.0/docs/demo.gif +0 -0
- twoperson-0.1.0/pyproject.toml +37 -0
- twoperson-0.1.0/src/twoperson/__init__.py +203 -0
- twoperson-0.1.0/src/twoperson/__main__.py +609 -0
- twoperson-0.1.0/src/twoperson/advice.py +249 -0
- twoperson-0.1.0/src/twoperson/consult.py +272 -0
- twoperson-0.1.0/src/twoperson/hook.py +223 -0
- twoperson-0.1.0/src/twoperson/inbox.py +999 -0
- twoperson-0.1.0/src/twoperson/packet.py +529 -0
- twoperson-0.1.0/src/twoperson/scripts/twoperson_stop_hook.sh +43 -0
- twoperson-0.1.0/src/twoperson/scripts/twoperson_watch.sh +62 -0
- twoperson-0.1.0/src/twoperson/signal.py +264 -0
- twoperson-0.1.0/src/twoperson/tier.py +166 -0
- twoperson-0.1.0/src/twoperson/verdict.py +243 -0
- twoperson-0.1.0/src/twoperson/watch.py +560 -0
- twoperson-0.1.0/src/twoperson/watchagent.py +196 -0
- twoperson-0.1.0/tests/__init__.py +0 -0
- twoperson-0.1.0/tests/fixtures.py +86 -0
- twoperson-0.1.0/tests/test_cli.py +237 -0
- twoperson-0.1.0/tests/test_consult.py +369 -0
- twoperson-0.1.0/tests/test_docs_law.py +97 -0
- twoperson-0.1.0/tests/test_gate_binding.py +153 -0
- twoperson-0.1.0/tests/test_hook_install.py +296 -0
- twoperson-0.1.0/tests/test_inbox.py +360 -0
- twoperson-0.1.0/tests/test_inbox_root.py +181 -0
- twoperson-0.1.0/tests/test_packet_schema.py +380 -0
- twoperson-0.1.0/tests/test_signal.py +354 -0
- twoperson-0.1.0/tests/test_tier.py +140 -0
- twoperson-0.1.0/tests/test_verdict.py +260 -0
- twoperson-0.1.0/tests/test_watch.py +577 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
on:
|
|
3
|
+
push: { branches: [main] }
|
|
4
|
+
pull_request:
|
|
5
|
+
|
|
6
|
+
jobs:
|
|
7
|
+
test:
|
|
8
|
+
strategy:
|
|
9
|
+
fail-fast: false
|
|
10
|
+
matrix:
|
|
11
|
+
os: [ubuntu-latest, macos-latest]
|
|
12
|
+
python: ["3.10", "3.11", "3.12", "3.13"]
|
|
13
|
+
runs-on: ${{ matrix.os }}
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python }}
|
|
19
|
+
- run: pip install -e ".[dev]"
|
|
20
|
+
- run: pytest -q
|
|
21
|
+
|
|
22
|
+
wheel:
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
- uses: actions/setup-python@v5
|
|
27
|
+
with: { python-version: "3.12" }
|
|
28
|
+
- run: pip install build && python -m build --wheel
|
|
29
|
+
# The hook scripts must survive packaging: install-hook writes their installed path into
|
|
30
|
+
# settings.json, so a wheel that drops them fails only at Stop time, silently.
|
|
31
|
+
- run: |
|
|
32
|
+
pip install dist/*.whl
|
|
33
|
+
python -c "from twoperson.hook import hook_script_path; assert hook_script_path().exists()"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
tags: ["v*"]
|
|
5
|
+
|
|
6
|
+
jobs:
|
|
7
|
+
build:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
steps:
|
|
10
|
+
- uses: actions/checkout@v4
|
|
11
|
+
- uses: actions/setup-python@v5
|
|
12
|
+
with: { python-version: "3.12" }
|
|
13
|
+
- run: pip install build && python -m build
|
|
14
|
+
- uses: actions/upload-artifact@v4
|
|
15
|
+
with:
|
|
16
|
+
name: dist
|
|
17
|
+
path: dist/
|
|
18
|
+
|
|
19
|
+
pypi:
|
|
20
|
+
needs: build
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
# Trusted publishing: PyPI trusts this repo+workflow+environment via OIDC.
|
|
23
|
+
# No API token exists anywhere. Configure the publisher at
|
|
24
|
+
# https://pypi.org/manage/account/publishing/ before the first tag.
|
|
25
|
+
environment: pypi
|
|
26
|
+
permissions:
|
|
27
|
+
id-token: write
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/download-artifact@v4
|
|
30
|
+
with:
|
|
31
|
+
name: dist
|
|
32
|
+
path: dist/
|
|
33
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-09-03
|
|
10
|
+
|
|
11
|
+
First public release.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Review packets with a strict, allow-listed schema: size cap, repo-relative path checks, a
|
|
16
|
+
credential scan that reports field paths and never values, and a rendering that fences packet
|
|
17
|
+
text as untrusted data.
|
|
18
|
+
- The four refusals that make up the gate: a verdict must answer a packet that exists in the
|
|
19
|
+
inbox; an approval must name that packet's own commit; a packet can't report a push, deploy or
|
|
20
|
+
restart without a `review_ref`; and that `review_ref` must resolve to an existing approving
|
|
21
|
+
verdict for the same commit (`verify` runs the same checks as `publish` and writes nothing).
|
|
22
|
+
- One inbox per repository, shared across `git worktree`s, resolved without spawning `git`.
|
|
23
|
+
- `install-hook`: a Claude Code `Stop` hook that drops a completion signal so a reviewer can be
|
|
24
|
+
woken by the event instead of polling. `install-watch`: a launchd agent for macOS.
|
|
25
|
+
- A consult lane (`consult-*`) that is explicitly non-gating.
|
|
26
|
+
- `tier`: a deterministic difficulty score for a packet, handed to the reviewer command as
|
|
27
|
+
`TWOPERSON_TIER` / `TWOPERSON_TIER_SCORE` / `TWOPERSON_PACKET_ID`, plus the `ESCALATE:`
|
|
28
|
+
convention for asking for a stronger reviewer.
|
|
29
|
+
|
|
30
|
+
[Unreleased]: https://github.com/ahm3dwasim/twoperson/compare/v0.1.0...HEAD
|
|
31
|
+
[0.1.0]: https://github.com/ahm3dwasim/twoperson/releases/tag/v0.1.0
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for taking a look. This is a small project with a narrow job, so the bar for changes is
|
|
4
|
+
"does it keep the gate honest", not "does it add a feature".
|
|
5
|
+
|
|
6
|
+
## Setup
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
git clone https://github.com/ahm3dwasim/twoperson
|
|
10
|
+
cd twoperson
|
|
11
|
+
python -m venv .venv && . .venv/bin/activate
|
|
12
|
+
pip install -e ".[dev]"
|
|
13
|
+
pytest -q
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The suite runs in about two seconds. Please keep it that way; a test that needs a network or a
|
|
17
|
+
real reviewer belongs in a manual checklist, not in `tests/`.
|
|
18
|
+
|
|
19
|
+
## What a good change looks like
|
|
20
|
+
|
|
21
|
+
- **Every public claim has a test behind it.** `tests/test_docs_law.py` reads the README and
|
|
22
|
+
`docs/PROTOCOL.md` and fails if they promise something the code doesn't do. If you change what
|
|
23
|
+
the tool enforces, change the docs and the drift test in the same commit.
|
|
24
|
+
- **Packet text is data.** Anything that reads a packet, verdict, consult or advice treats it as
|
|
25
|
+
hostile input. If you add a field, add its length cap, its allowed values, and a test that a
|
|
26
|
+
hostile value is refused.
|
|
27
|
+
- **Don't widen the gate to be helpful.** A refusal that exists is there because someone got burned.
|
|
28
|
+
Loosening one needs a written reason in the PR.
|
|
29
|
+
- **No git subprocesses in the gate.** The inbox resolution deliberately reads `.git` files by hand
|
|
30
|
+
so a poll stays a few `stat` calls. The only `git rev-parse` calls are for a signal's branch label
|
|
31
|
+
and, in the shell hooks, for finding a virtualenv.
|
|
32
|
+
|
|
33
|
+
## How changes get reviewed here
|
|
34
|
+
|
|
35
|
+
We use `twoperson` on itself. A change is published as a packet, a reviewer records a verdict
|
|
36
|
+
for that exact commit, and the ship report cites it. If you open a PR, expect the review comment to
|
|
37
|
+
look like a verdict: a decision, findings with file and line, and one-line note.
|
|
38
|
+
|
|
39
|
+
## Reporting a security issue
|
|
40
|
+
|
|
41
|
+
If you find a way to make a packet talk a reviewer into something, or to get a ship report past
|
|
42
|
+
the `review_ref` check, please open an issue with the reproduction. There's no bounty; there is
|
|
43
|
+
a fast fix and a test named after you if you want one.
|
twoperson-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 twoperson contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
twoperson-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: twoperson
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A two-person rule for AI coding agents: a ship report cannot be recorded without an approving verdict for that exact commit.
|
|
5
|
+
Project-URL: Homepage, https://github.com/ahm3dwasim/twoperson
|
|
6
|
+
Project-URL: Issues, https://github.com/ahm3dwasim/twoperson/issues
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: agents,ai,claude-code,code-review,codex,guardrails,llm,multi-agent
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Requires-Dist: structlog>=23.1
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=7.4; extra == 'dev'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# twoperson
|
|
22
|
+
|
|
23
|
+
[](https://github.com/ahm3dwasim/twoperson/actions/workflows/ci.yml)
|
|
24
|
+
[](https://pypi.org/project/twoperson/)
|
|
25
|
+
[](https://pypi.org/project/twoperson/)
|
|
26
|
+
[](LICENSE)
|
|
27
|
+
|
|
28
|
+
A two-person rule for AI coding agents. The agent that wrote the change can't record it as
|
|
29
|
+
shipped until a reviewer has written down a verdict for that exact commit, and that rule lives
|
|
30
|
+
in a schema rather than in a prompt.
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
builder ──publish──▶ .twoperson/pending/ ──claim──▶ reviewer
|
|
34
|
+
▲ │
|
|
35
|
+
└────────────── verdicts/ ◀──── Approve / Request changes
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
No server, no API keys, no network. Two agents share a directory. That's the whole transport.
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
## Why I built it
|
|
43
|
+
|
|
44
|
+
I run two coding agents against the same repo: one builds, the other reviews. For a while the
|
|
45
|
+
"get a review first" step was a line in a system prompt. It worked until it didn't. Left alone
|
|
46
|
+
for a few hours, the builder would decide the review was optional, or would review its own work
|
|
47
|
+
and approve it, or would get a real review and then rebase three more commits on top of the
|
|
48
|
+
approval and push the lot.
|
|
49
|
+
|
|
50
|
+
None of that was malicious. It's just what happens when a rule is advice. So I moved the rule
|
|
51
|
+
out of the prompt and into the data format the two agents use to talk to each other.
|
|
52
|
+
|
|
53
|
+
## What it actually enforces
|
|
54
|
+
|
|
55
|
+
The builder writes a **review packet**: a JSON record of what it was trying to do, the head sha,
|
|
56
|
+
which files changed, what tests it ran and what they said. `twoperson publish` validates it and
|
|
57
|
+
drops it in `.twoperson/pending/`. The reviewer claims it, reads it, and writes a **verdict**.
|
|
58
|
+
|
|
59
|
+
Four refusals do the real work. They're all real output.
|
|
60
|
+
|
|
61
|
+
A verdict has to answer a packet that actually exists in the inbox:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
$ twoperson verdict --packet made-up --decision Approve --head 0900128
|
|
65
|
+
verdict rejected — packet_id: no packet 'made-up' in this inbox (pending,
|
|
66
|
+
claimed or audited) — a verdict must answer a published packet; run `next` to claim one
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
An approval has to name the commit that packet is at, not some other commit:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
$ twoperson verdict --packet demo-1 --decision Approve --head abcdef0
|
|
73
|
+
verdict rejected — head_sha: 'Approve' names 'abcdef0' but packet 'demo-1' is at
|
|
74
|
+
'6e5acc68…' — an approval binds to the packet's own head
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
A packet can't say it was pushed, deployed, or restarted without a review reference:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
$ twoperson verify --from packet.json
|
|
81
|
+
packet rejected — push_status.review_ref: a packet may not report
|
|
82
|
+
pushed/deployed/restarted=true without a recorded Reviewer audit reference
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
And that reference has to be a verdict that exists, approves, and approves *this* commit:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
$ twoperson verify --from packet.json
|
|
89
|
+
packet rejected — push_status.review_ref: verdict 'vdt-20260902T215020Z-e2e38d43'
|
|
90
|
+
approved head '6e5acc68…', but this packet shipped 'f00dbabe…' — an approval does
|
|
91
|
+
not carry over to a different commit
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
So an approval is for one sha of one packet. Rebase, amend, or add a commit and it's stale. The
|
|
95
|
+
builder has to publish again and the reviewer has to look again. `verify` runs the same checks as
|
|
96
|
+
`publish` and writes nothing, so a builder can dry-run its own ship report.
|
|
97
|
+
|
|
98
|
+
## Try it
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pip install twoperson
|
|
102
|
+
cd your-repo
|
|
103
|
+
|
|
104
|
+
# builder
|
|
105
|
+
twoperson template > packet.json # evidence fields are "unknown"; a few fixed placeholders (see --help)
|
|
106
|
+
$EDITOR packet.json # goal, head sha, tests, evidence
|
|
107
|
+
twoperson publish --from packet.json
|
|
108
|
+
|
|
109
|
+
# reviewer (another terminal, another agent, another machine with the same checkout)
|
|
110
|
+
twoperson check # exit 0 = something's waiting. Costs a few stat() calls.
|
|
111
|
+
twoperson next # claim the oldest packet, print it for review
|
|
112
|
+
twoperson verdict --packet demo-1 --decision "Approve with nits" \
|
|
113
|
+
--note "tests cover the 503 path" --finding "add a jitter test"
|
|
114
|
+
# --head defaults to the packet's own head; pass it explicitly if you want to be sure
|
|
115
|
+
|
|
116
|
+
# builder
|
|
117
|
+
twoperson verdicts --ack
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
2026-09-02T21:50:20Z vdt-20260902T215020Z-e2e38d43
|
|
122
|
+
packet : demo-1
|
|
123
|
+
head : 6e5acc6800f849c3a4a5e5bd8edc324a6247836a
|
|
124
|
+
reviewer : reviewer
|
|
125
|
+
DECISION : Approve with nits (ship gate OPEN for this head)
|
|
126
|
+
findings :
|
|
127
|
+
- add a jitter test
|
|
128
|
+
note : tests cover the 503 path
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`check` is designed to be polled. It doesn't touch a model, so a reviewer agent can sit in a
|
|
132
|
+
loop on it and spend nothing until there's actually a packet to read.
|
|
133
|
+
|
|
134
|
+
## Hooking it into Claude Code
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
twoperson install-hook
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
That adds a `Stop` hook to `.claude/settings.json`. When a session ends it drops a small
|
|
141
|
+
signal file in the inbox, so a reviewer can be woken by the event instead of polling on a timer.
|
|
142
|
+
|
|
143
|
+
The signal is deliberately not a packet. It says "a session stopped here" and nothing else. A
|
|
144
|
+
Stop hook doesn't know what the session did or whether the tests passed, and if I let it guess
|
|
145
|
+
it would produce a nice-looking packet full of made-up facts. The whole point is to not have
|
|
146
|
+
those.
|
|
147
|
+
|
|
148
|
+
On a Mac, `twoperson install-watch` sets up a launchd agent that fires on inbox changes and runs
|
|
149
|
+
whatever command you put in `TWOPERSON_ON_PACKET`. That's where you point your reviewer.
|
|
150
|
+
|
|
151
|
+
## The packet is hostile input
|
|
152
|
+
|
|
153
|
+
The builder is a language model. Whatever it writes into the packet might be aimed at the
|
|
154
|
+
reviewer, on purpose or by accident. So the packet gets treated the way you'd treat a form
|
|
155
|
+
submission from the internet:
|
|
156
|
+
|
|
157
|
+
- Hard size cap (256 KB) before parsing.
|
|
158
|
+
- Strict schema. Unknown keys are rejected. Every string has a length limit, every list a count limit.
|
|
159
|
+
- File paths have to be repo-relative. No absolute paths, no `..`, no drive letters.
|
|
160
|
+
- A scan for things that look like credentials: Anthropic keys, OpenAI keys (legacy `sk-…`
|
|
161
|
+
and the `sk-proj-…` / `sk-admin-…` shapes), GitHub, AWS, Slack and Google keys, PEM blocks,
|
|
162
|
+
bearer tokens, and `NAME=value` where NAME ends in key/secret/token/password and the value is
|
|
163
|
+
16+ characters. It only looks at values, so writing "read the key from `ANTHROPIC_API_KEY`" is
|
|
164
|
+
fine while pasting the actual key is not. When it finds one, the error names the field path
|
|
165
|
+
and never echoes the value. The list is in `packet.py` and every shape has a fail-closed test;
|
|
166
|
+
it is a tripwire, not a guarantee.
|
|
167
|
+
- When the reviewer renders a packet, the body sits between `BEGIN`/`END` markers under a
|
|
168
|
+
short preamble saying this is data, not instructions. Markers forged inside the body get
|
|
169
|
+
defanged.
|
|
170
|
+
|
|
171
|
+
None of that replaces the reviewer's judgment. It just means the reviewer isn't the only line
|
|
172
|
+
of defense.
|
|
173
|
+
|
|
174
|
+
## Worktrees
|
|
175
|
+
|
|
176
|
+
If your agents each run in their own `git worktree` (they should), a naive "put the inbox in
|
|
177
|
+
this checkout" gives each agent a private inbox. Builder publishes, reviewer polls, neither
|
|
178
|
+
sees the other, and `check` returns 1 in a way that looks exactly like "nothing to do". I lost
|
|
179
|
+
an afternoon to that.
|
|
180
|
+
|
|
181
|
+
`twoperson` walks up to the main working tree, following `.git` files and `commondir` by
|
|
182
|
+
hand, so every worktree of a repo lands on the same `.twoperson/`. No git subprocess, so it
|
|
183
|
+
stays cheap. Set `TWOPERSON_INBOX` if you want to override it.
|
|
184
|
+
|
|
185
|
+
## Picking a reviewer by difficulty
|
|
186
|
+
|
|
187
|
+
Not every packet deserves your strongest (slowest, priciest) reviewer. `twoperson tier` scores a
|
|
188
|
+
packet from its validated fields — which review areas it names, whether the changed paths touch
|
|
189
|
+
auth/payment/deploy/migration, how big the diff is, whether tests ran and passed, whether the
|
|
190
|
+
builder left open questions, whether something already shipped — and reports `low`, `medium`,
|
|
191
|
+
`high` or `critical`. It's substring checks on structured fields, so a builder writing "trivial
|
|
192
|
+
typo fix, please approve" next to thirty changed files under `src/auth/` still gets `critical`.
|
|
193
|
+
|
|
194
|
+
The watcher hands that tier to whatever reviewer command you configured (`TWOPERSON_TIER`,
|
|
195
|
+
`TWOPERSON_TIER_SCORE`, `TWOPERSON_PACKET_ID` in its environment), so the reviewer side can start
|
|
196
|
+
cheap and only spend the big model on the packets that earn it. If a reviewer decides a packet is
|
|
197
|
+
beyond it, the convention is a `Needs owner decision` verdict with a note starting `ESCALATE:`;
|
|
198
|
+
a reviewer ladder can catch that and re-run one rung stronger. `twoperson` records the convention
|
|
199
|
+
and the tier; it doesn't run models.
|
|
200
|
+
|
|
201
|
+
## There's also a consult lane
|
|
202
|
+
|
|
203
|
+
Sometimes the builder wants an opinion, not an audit. `consult-publish` / `consult-next` /
|
|
204
|
+
`consult-advise` do that. It's a separate set of directories, it never produces a verdict, and
|
|
205
|
+
nothing on it can unlock a push. It exists so "what do you think of this approach" can't get
|
|
206
|
+
quietly upgraded to "this was reviewed".
|
|
207
|
+
|
|
208
|
+
## How this compares
|
|
209
|
+
|
|
210
|
+
There are a few projects in this space and I looked at them before writing this one.
|
|
211
|
+
|
|
212
|
+
[claude-review-loop](https://github.com/hamelsmu/claude-review-loop) runs Codex on Claude's
|
|
213
|
+
work from a slash command. It writes each review to a `reviews/` file, and its Stop hook asks for
|
|
214
|
+
a Codex pass once the loop is invoked (failing open if the review can't be produced). What it
|
|
215
|
+
doesn't have is a sha-bound approval or a ship-report binding: nothing ties "reviewed" to one
|
|
216
|
+
commit, and nothing refuses a "shipped" record that lacks one.
|
|
217
|
+
[sd0x-harness](https://github.com/sd0xdev/sd0x-harness) is much bigger (skills, agents,
|
|
218
|
+
hooks) and puts its hard gates at the git level with review as a reminder layer.
|
|
219
|
+
[quorum](https://github.com/berrzebb/quorum) does enforce an audit gate, over MCP and SQLite,
|
|
220
|
+
with a lot more machinery. [shiplog](https://github.com/devallibus/shiplog) leans on GitHub
|
|
221
|
+
branch protection and needs a remote.
|
|
222
|
+
|
|
223
|
+
`twoperson` is smaller than all of those and does one thing: the schema won't let "shipped
|
|
224
|
+
without review" or "approved without a sha" exist. It doesn't care which agents you use or
|
|
225
|
+
whether you have a GitHub remote.
|
|
226
|
+
|
|
227
|
+
## What it doesn't do
|
|
228
|
+
|
|
229
|
+
It doesn't review code. It makes sure a verdict was recorded and writes down what it said.
|
|
230
|
+
|
|
231
|
+
It doesn't know who the reviewer is. `--reviewer` is a label, not an identity. If the builder
|
|
232
|
+
and the reviewer run in the same process with the same permissions, the builder can approve
|
|
233
|
+
itself, and `twoperson` will not notice. Keeping the two apart is your deployment's job:
|
|
234
|
+
separate OS users, separate checkouts, a reviewer that only has read access to the code. What
|
|
235
|
+
`twoperson` guarantees is narrower and mechanical: an approving verdict exists for this exact
|
|
236
|
+
commit, and the ship report points at it. The binding is per commit, not per packet. A ship
|
|
237
|
+
report is its own packet, and what it has to cite is an approval of the same head; it doesn't
|
|
238
|
+
have to be the packet that was originally reviewed, because after a rebase it can't be.
|
|
239
|
+
|
|
240
|
+
It doesn't spawn agents or call models. It's a directory with a lock and a validator.
|
|
241
|
+
|
|
242
|
+
It doesn't watch your repository or your deploys. Nothing in the gate reads HEAD or the working
|
|
243
|
+
tree; the only git it touches is a `git rev-parse` to label a signal with its branch name and, in
|
|
244
|
+
the shell hooks, to find a virtualenv. It never sees a push happen. What it validates is the
|
|
245
|
+
builder's *report*:
|
|
246
|
+
a packet that says "I pushed/deployed/restarted commit X" is refused unless it cites an approving
|
|
247
|
+
verdict for X. A builder that lies in the report can lie. What can't exist is a consistent
|
|
248
|
+
report that skipped review, and an agent that has to lie to ship is a much easier thing to
|
|
249
|
+
catch than one that was never asked.
|
|
250
|
+
|
|
251
|
+
It doesn't replace CI. CI checks the code. This checks the paperwork.
|
|
252
|
+
|
|
253
|
+
## Exit codes
|
|
254
|
+
|
|
255
|
+
`0` did the thing. `1` nothing to do. `2` rejected. The one exception is `signal`: a valid
|
|
256
|
+
`signal` invocation never returns 2, and the packaged Stop-hook script always exits 0 whatever
|
|
257
|
+
happens inside it, because Claude Code treats a 2 from a Stop hook as "don't stop" and a broken
|
|
258
|
+
hook would trap the session in a loop. (Malformed flags still get argparse's usual 2, like any
|
|
259
|
+
CLI; the hook script never passes malformed flags.)
|
|
260
|
+
|
|
261
|
+
## Development
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
pip install -e ".[dev]"
|
|
265
|
+
pytest -q
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
A few hundred tests, about two seconds. MIT.
|