pinwash 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.
- pinwash-0.1.0/LICENSE +21 -0
- pinwash-0.1.0/MANIFEST.in +6 -0
- pinwash-0.1.0/PKG-INFO +180 -0
- pinwash-0.1.0/README.md +164 -0
- pinwash-0.1.0/SPEC.md +549 -0
- pinwash-0.1.0/THREATMODEL.md +36 -0
- pinwash-0.1.0/docs/ALPHA2.md +84 -0
- pinwash-0.1.0/docs/GUIDANCE_ACCEPTANCE.md +79 -0
- pinwash-0.1.0/docs/INSTALL.md +66 -0
- pinwash-0.1.0/docs/ONBOARDING_ACCEPTANCE.md +162 -0
- pinwash-0.1.0/docs/QUICKSTART.md +109 -0
- pinwash-0.1.0/docs/RELEASE.md +42 -0
- pinwash-0.1.0/docs/SESSIONS.md +137 -0
- pinwash-0.1.0/pinwash/__init__.py +5 -0
- pinwash-0.1.0/pinwash/__main__.py +4 -0
- pinwash-0.1.0/pinwash/allow.py +101 -0
- pinwash-0.1.0/pinwash/cli.py +193 -0
- pinwash-0.1.0/pinwash/consumer.py +226 -0
- pinwash-0.1.0/pinwash/engine.py +568 -0
- pinwash-0.1.0/pinwash/escalate.py +37 -0
- pinwash-0.1.0/pinwash/findings.py +86 -0
- pinwash-0.1.0/pinwash/gitrepo.py +1066 -0
- pinwash-0.1.0/pinwash/hooks.py +252 -0
- pinwash-0.1.0/pinwash/human_cli.py +102 -0
- pinwash-0.1.0/pinwash/parse/__init__.py +1 -0
- pinwash-0.1.0/pinwash/parse/gha.py +1243 -0
- pinwash-0.1.0/pinwash/parse/hookcmd.py +374 -0
- pinwash-0.1.0/pinwash/parse/jsonsurf.py +187 -0
- pinwash-0.1.0/pinwash/parse/tomlsub.py +63 -0
- pinwash-0.1.0/pinwash/pins.py +81 -0
- pinwash-0.1.0/pinwash/report.py +97 -0
- pinwash-0.1.0/pinwash/rules/__init__.py +1 -0
- pinwash-0.1.0/pinwash/rules/config_rule.py +231 -0
- pinwash-0.1.0/pinwash/rules/hook_coverage.py +415 -0
- pinwash-0.1.0/pinwash/rules/hook_rules.py +480 -0
- pinwash-0.1.0/pinwash/rules/hook_selector.py +240 -0
- pinwash-0.1.0/pinwash/rules/hook_switch.py +113 -0
- pinwash-0.1.0/pinwash/rules/permission.py +506 -0
- pinwash-0.1.0/pinwash/rules/pin_rules.py +151 -0
- pinwash-0.1.0/pinwash/rules/required_check.py +667 -0
- pinwash-0.1.0/pinwash/rules/skill_bypass.py +58 -0
- pinwash-0.1.0/pinwash/session.py +162 -0
- pinwash-0.1.0/pinwash/session_status.py +176 -0
- pinwash-0.1.0/pinwash/surfaces.py +132 -0
- pinwash-0.1.0/pinwash/textutil.py +93 -0
- pinwash-0.1.0/pinwash/verification.py +28 -0
- pinwash-0.1.0/pinwash.egg-info/PKG-INFO +180 -0
- pinwash-0.1.0/pinwash.egg-info/SOURCES.txt +85 -0
- pinwash-0.1.0/pinwash.egg-info/dependency_links.txt +1 -0
- pinwash-0.1.0/pinwash.egg-info/entry_points.txt +2 -0
- pinwash-0.1.0/pinwash.egg-info/top_level.txt +1 -0
- pinwash-0.1.0/pyproject.toml +29 -0
- pinwash-0.1.0/setup.cfg +4 -0
- pinwash-0.1.0/setup.py +38 -0
- pinwash-0.1.0/tests/__init__.py +0 -0
- pinwash-0.1.0/tests/gates/__init__.py +0 -0
- pinwash-0.1.0/tests/gates/test_v0_acceptance.py +325 -0
- pinwash-0.1.0/tests/test_allow_spelling.py +124 -0
- pinwash-0.1.0/tests/test_alpha2_git_cli.py +499 -0
- pinwash-0.1.0/tests/test_alpha2_regressions.py +322 -0
- pinwash-0.1.0/tests/test_checkwash_read.py +218 -0
- pinwash-0.1.0/tests/test_consumer.py +427 -0
- pinwash-0.1.0/tests/test_engine_fidelity.py +1092 -0
- pinwash-0.1.0/tests/test_exemption_trust.py +273 -0
- pinwash-0.1.0/tests/test_exit2_capability.py +141 -0
- pinwash-0.1.0/tests/test_family_regressions.py +517 -0
- pinwash-0.1.0/tests/test_git_hardening.py +896 -0
- pinwash-0.1.0/tests/test_hook_routing.py +655 -0
- pinwash-0.1.0/tests/test_hook_switch.py +235 -0
- pinwash-0.1.0/tests/test_host_decoding.py +136 -0
- pinwash-0.1.0/tests/test_ignored_surfaces.py +340 -0
- pinwash-0.1.0/tests/test_invocation.py +95 -0
- pinwash-0.1.0/tests/test_job_conditions.py +196 -0
- pinwash-0.1.0/tests/test_permission_mode.py +249 -0
- pinwash-0.1.0/tests/test_pin_severities.py +201 -0
- pinwash-0.1.0/tests/test_producer_rename.py +225 -0
- pinwash-0.1.0/tests/test_project_dir_forms.py +153 -0
- pinwash-0.1.0/tests/test_project_dir_guard_holders.py +111 -0
- pinwash-0.1.0/tests/test_r12_challenge.py +163 -0
- pinwash-0.1.0/tests/test_r12_liveness.py +192 -0
- pinwash-0.1.0/tests/test_residual_rows.py +182 -0
- pinwash-0.1.0/tests/test_session_recipe.py +145 -0
- pinwash-0.1.0/tests/test_session_status.py +266 -0
- pinwash-0.1.0/tests/test_spec16_rows.py +273 -0
- pinwash-0.1.0/tests/test_spec_statements.py +359 -0
- pinwash-0.1.0/tests/test_unit.py +644 -0
- pinwash-0.1.0/tests/test_workflow_routing.py +900 -0
pinwash-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PinWash 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.
|
pinwash-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pinwash
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Flag git diffs that weaken an agent verification harness.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Source, https://github.com/taipei49314/pinwash
|
|
7
|
+
Project-URL: Issues, https://github.com/taipei49314/pinwash/issues
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# pinwash
|
|
18
|
+
|
|
19
|
+
**Flags a Git diff that makes an agent more able to claim done by weakening the verification harness around it.**
|
|
20
|
+
|
|
21
|
+
標出會讓 agent 比較容易自稱完成的 harness 變更。
|
|
22
|
+
|
|
23
|
+
Local-first. Deterministic. No LLM. No network. Does not execute the subject. Does not enforce anything.
|
|
24
|
+
|
|
25
|
+
PinWash **0.1.0 is Alpha**, licensed under MIT. The local v0 engine follows SPEC 27; this is not a 1.0 stability claim. Read [SPEC.md](https://github.com/taipei49314/pinwash/blob/v0.1.0/SPEC.md) (contract) and [ARCHITECTURE.md](https://github.com/taipei49314/pinwash/blob/v0.1.0/ARCHITECTURE.md) (layers; SPEC wins on conflict). Frozen acceptance is `tests/gates/test_v0_acceptance.py`.
|
|
26
|
+
|
|
27
|
+
**New here? Follow the [five-minute quickstart](https://github.com/taipei49314/pinwash/blob/v0.1.0/docs/QUICKSTART.md): install an
|
|
28
|
+
isolated installation, try a small example, then use the same two steps in your
|
|
29
|
+
project.** Installation needs Python 3.11+, Git and download access. The guide
|
|
30
|
+
also explains status, exit codes and recovery. The time is a walkthrough target,
|
|
31
|
+
not a measured first-user success claim.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
Requires Python 3.11+ and Git. Install outside the project your agent will edit,
|
|
36
|
+
using [pipx](https://pipx.pypa.io/) to keep PinWash in its own environment:
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
pipx install pinwash==0.1.0
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Linux is the package acceptance target; Windows/macOS are not yet validated.
|
|
43
|
+
Installed scans use no network and have no runtime dependencies. See
|
|
44
|
+
[installation](https://github.com/taipei49314/pinwash/blob/v0.1.0/docs/INSTALL.md) for a venv alternative and a local wheel install.
|
|
45
|
+
Keep this installation fixed until the current session has been reviewed.
|
|
46
|
+
|
|
47
|
+
## Review an AI coding session
|
|
48
|
+
|
|
49
|
+
In your project's **clean worktree**, before the agent starts:
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
pinwash session start
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Use your coding tool normally. Once the agent **and all processes it started**
|
|
56
|
+
have stopped modifying files:
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
pinwash session review
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Answer the stopped-writers question. PinWash saves the original baseline and
|
|
63
|
+
judge identity, performs all three comparisons described below, explains the
|
|
64
|
+
findings, and saves a JSON report outside the project. It does not terminate
|
|
65
|
+
processes or independently verify that they stopped. A trusted non-interactive
|
|
66
|
+
controller supplies `--stopped` only after ensuring that condition itself.
|
|
67
|
+
|
|
68
|
+
Each finding explains **what changed, why it matters, and what to review next**.
|
|
69
|
+
The report begins with a **priority summary**: unfinished checks, blocking
|
|
70
|
+
changes, exemption decisions, and configuration/coverage gaps. Full findings
|
|
71
|
+
and original JSON remain available. No findings
|
|
72
|
+
does not prove that verification runs or that the product works.
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
pinwash session status
|
|
76
|
+
pinwash session review --json --stopped
|
|
77
|
+
pinwash review HEAD~1..HEAD
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`session status` reads the saved baseline, judge match and last saved report.
|
|
81
|
+
It does not scan current changes, reset the baseline or approve a historical
|
|
82
|
+
pass. Use the same `--state-dir` on start, status and review if you chose one.
|
|
83
|
+
|
|
84
|
+
`review` explains a single diff; without a range it compares HEAD to the worktree.
|
|
85
|
+
It is not a whole-session review. `check` retains its original JSON output and
|
|
86
|
+
exit codes for existing integrations. `review` and `session review` use exit `0` for
|
|
87
|
+
no blocking finding, `1` for a block, `2` for an unfinished review, and `3` for
|
|
88
|
+
an exemption awaiting human review. Coverage gaps remain visible even on exit 0.
|
|
89
|
+
`session status` exit `0` only means metadata was read successfully, including
|
|
90
|
+
when no session exists or the historical review blocked; exit `2` needs attention.
|
|
91
|
+
|
|
92
|
+
Starting never overwrites an existing session by default. After reviewing the
|
|
93
|
+
previous work and choosing the next committed baseline, use
|
|
94
|
+
`pinwash session start --replace`. Previous baseline records and reports remain.
|
|
95
|
+
For details and common errors, see [the session guide](https://github.com/taipei49314/pinwash/blob/v0.1.0/docs/SESSIONS.md).
|
|
96
|
+
|
|
97
|
+
**Trust boundary:** keep both PinWash and its state outside the agent's writable
|
|
98
|
+
scope. State is local convenience data, not signed evidence; placing it outside
|
|
99
|
+
the repository does not protect it from an unrestricted process using your account.
|
|
100
|
+
Run the consumer from your trusted terminal/controller, not from the agent being
|
|
101
|
+
judged. Keep the subject out of `PYTHONPATH` and other Python startup customization.
|
|
102
|
+
The consumer's isolated child judge prevents implicit repository-module shadowing;
|
|
103
|
+
the initial launcher environment must still be trusted. It does not sandbox the agent.
|
|
104
|
+
|
|
105
|
+
## Advanced: direct scanner and self-check
|
|
106
|
+
|
|
107
|
+
`pinwash doctor` runs the full bundled self-test suite against the installed code;
|
|
108
|
+
it can take several minutes and does not judge your project. It is deliberately
|
|
109
|
+
separate from the two-step session flow. Source contributors can still run the
|
|
110
|
+
suite from a trusted checkout with `python -m unittest discover -s tests -t . -v`.
|
|
111
|
+
|
|
112
|
+
`check` with no range compares `HEAD` to the worktree. Exit 0 is not proof the harness works; it means no in-scope finding was at or above `fail_on` (default `high`).
|
|
113
|
+
|
|
114
|
+
## Consumer recipe: judging an agent session
|
|
115
|
+
|
|
116
|
+
Judge what an agent session did with **three invocations**, after every process the session started has stopped:
|
|
117
|
+
|
|
118
|
+
```powershell
|
|
119
|
+
python -P -m pinwash check BASE..HEAD # what the session committed, against the pinned base
|
|
120
|
+
python -P -m pinwash check BASE..WORKTREE # everything it left on disk, against the pinned base
|
|
121
|
+
python -P -m pinwash check # its uncommitted edits against its own last commit
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
- `BASE` is the commit you recorded **before** the session started, pinned (write down its sha, and pass the full sha, not a branch or tag: the repository's refs, like the rest of its `.git`, are the agent's to write). Never trust `HEAD` as the base: an agent that commits redefines it, and a single-range scan of the last commit only sees the last commit. In a live-fire round, a session committed a 7-file harness weakening (`neutralize verification harness`) that a worktree-only judge would have missed.
|
|
125
|
+
- The first two read `.pinwash/allow.toml`, required contexts and hook bodies from `BASE` (SPEC §2). Plain `check` reads them from `HEAD`, which the session moves: a session that commits an exemption record and leaves the weakening uncommitted passes plain `check` (THREATMODEL R18), and so does one that drops a required context in a commit and deletes its job uncommitted. Plain `check` stays because it compares the worktree with the session's own commits, so a judge renamed in a commit and gutted on disk into a body that is no stub but can no longer block shows there and nowhere else (a stub body, such as `exit 0`, shows in `BASE..WORKTREE` too).
|
|
126
|
+
- **Pass** means all three invocations exited 0 within your timeout, each printed JSON that parses, and none reports `EXEMPTION_ADDED`. **Held** is the same, except that at least one reports `EXEMPTION_ADDED`: the session wrote an exemption record, and only a human review of each added record releases it, because once a commit holding it becomes a pinned base or is merged it drops the finding it names until it expires (THREATMODEL R18). **Block** is any exit 1. Anything else (an exit 2, a run killed or timed out, no output or output that does not parse) means the session was **not judged**, never a pass. `unknown_coverage` rows are not part of pass or held; a gate on them is your own policy, and it will flag more than weakenings: in the two worktree invocations a host-written `.claude/settings.local.json` that an ignore rule matches (GitHub's Agents.gitignore has `.claude/*.local.json`) is a row in every session that has one, and so is each ignored hook log or dependency directory under `.claude/hooks/`; a file a hook writes there that is not UTF-8 and not ignored, such as a Python `__pycache__/*.pyc`, is a row of its own (SPEC §7).
|
|
127
|
+
- This recipe replaced a two-invocation one (`BASE..HEAD` and plain `check`), and it judges some sessions differently: one that commits a valid exemption record is held where it passed, and a weakening split between a commit and the uncommitted worktree through a state neither old invocation read blocks where it passed: for example, an exemption record committed with the weakening left uncommitted, a required context dropped in a commit with its job deleted uncommitted, or an MCP server added `disabled: true` in a commit and enabled uncommitted.
|
|
128
|
+
- The next session's `BASE` is the `HEAD` this judge saw, or a commit a human chose. It is never a branch or tag in the session's repository that the judged `HEAD` does not contain: the session could have moved it.
|
|
129
|
+
- The worktree is read while the judge runs. A process the session left running can change files between or during the invocations (a session that swapped the genuine settings back in while `check` ran passed all three), so stop every process it started before judging.
|
|
130
|
+
- **Run the judge with `-P`** (Python 3.11+; `PYTHONSAFEPATH=1` does the same, and so does an installed `pinwash` script). `check` judges the directory it runs in, which the agent wrote. Without `-P`, `python -m` puts that directory first on `sys.path`, so an `importlib.py`, another standard-library name or a `pinwash/` package the agent left there runs in place of the judge, before any pinwash code, and can print any verdict. pinwash cannot guard against that itself; only the invocation can. The same goes for `doctor`. `-P` drops only the implicit current directory: if `PYTHONPATH` names the judged repository (as the "Try it" block above does, which judges this checkout itself), the agent's `pinwash/` package is imported instead of the judge. When judging another repository, run the installed `pinwash` script, or set `PYTHONPATH` to the pinwash checkout only.
|
|
131
|
+
- Judge a clone that has the objects the run reads. A shallow or partial clone (`--depth`, `--filter`, `scalar clone`) that lacks one is exit 2, `git object <sha> is not in the object store`, never a pass and never a fetch; fetch it, or for `BASE...HEAD` compute the merge base yourself and pass `MB..HEAD`.
|
|
132
|
+
- Judge only after the agent's edits have settled on disk. Some CLIs apply edits asynchronously and can still be writing after their process returns; a judge that races the writer judges a stale worktree.
|
|
133
|
+
|
|
134
|
+
**Record the judge's identity with the verdict.** A pass or block that cannot name the judge is not reproducible. Record the pinwash git revision and the `python -P -m pinwash doctor` output (`spec_version`, self-test state; a judge record needs `nested_depth` 0, the full own suite), and the exit code of each invocation. A historical example, from spec 7, when the recipe had two invocations: "pass @ pinwash `86bdcce`, spec 7, 51 self-tests ok, exit 0 on both ranges"; the spec version and test count move with the revision, so record what `doctor` prints for yours. Round 1's judge was pinned this way (`416b962`, spec 6); its reports are not published in this repository.
|
|
135
|
+
|
|
136
|
+
**Exemptions are per finding fingerprint, on the base side only** (SPEC §10). Take the `fingerprint` field from the finding JSON (`rule/path/v1:<64hex>`), and write a `[[allow]]` record into `.pinwash/allow.toml` at the pinned base — all six keys required (`fingerprint`, `rule`, `reason`, `author`, `created`, `expires`), `expires` at most 180 days after `created`, never a rule glob. Set `created` to the day you add the record by the judge's clock (`PINWASH_TODAY`, else the judge host's local date), never a later date:
|
|
137
|
+
|
|
138
|
+
```toml
|
|
139
|
+
[[allow]]
|
|
140
|
+
fingerprint = "HOOK_REMOVED/.claude/settings.json/v1:…"
|
|
141
|
+
rule = "HOOK_REMOVED"
|
|
142
|
+
reason = "planned migration, ticket #…"
|
|
143
|
+
author = "you"
|
|
144
|
+
created = "2026-09-28"
|
|
145
|
+
expires = "2026-12-01"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Head-side valid additions surface as `EXEMPTION_ADDED` (warn), or critical when an added record's `created` is after today; editing or deleting a base exemption is `CONFIG_RELAXED` (critical). A base record exempts its finding only while `expires` is at most 180 days after the earlier of `created` and today, and the exemption channel never exempts its own findings (`CONFIG_RELAXED` and `EXEMPTION_ADDED` at `.pinwash/allow.toml`) (SPEC §10, spec 11).
|
|
149
|
+
|
|
150
|
+
Exit codes: `0` no finding at or above `fail_on` · `1` verdict block · `2` engine error. A crash must not exit 1; exit 0 is never proof the harness works.
|
|
151
|
+
|
|
152
|
+
## Neighbours
|
|
153
|
+
|
|
154
|
+
pinwash is not checkwash (product tests), not tripwire (hooks that run judges), not walkaround (session admission). It only asks whether **this diff** weakened pins, hooks, permissions, required checks, or bypass instructions. (tripwire and walkaround are no longer on GitHub — removed 2026-09-26 — and are named only to bound scope.)
|
|
155
|
+
|
|
156
|
+
## Status
|
|
157
|
+
|
|
158
|
+
The [second alpha-strengthening round](https://github.com/taipei49314/pinwash/blob/v0.1.0/docs/ALPHA2.md) repaired contract fidelity
|
|
159
|
+
and added real Git/CLI regressions. It is an engineering milestone, not version
|
|
160
|
+
`2.0.0`. Its approved EC pool verification passed 96 tests and doctor on the
|
|
161
|
+
implementation commit `8e0688c` and, separately, on the merged review head
|
|
162
|
+
`e89f05a`; the acceptance document links both receipts and lists what review
|
|
163
|
+
after the merge found (issues #3–#6). A commit's `ec / alpha2-verify` check
|
|
164
|
+
covers that commit only. New onboarding guidance has its own
|
|
165
|
+
[validation record](https://github.com/taipei49314/pinwash/blob/v0.1.0/docs/GUIDANCE_ACCEPTANCE.md); earlier results do not validate
|
|
166
|
+
new code.
|
|
167
|
+
|
|
168
|
+
| Claim | Status |
|
|
169
|
+
|---|---|
|
|
170
|
+
| Frozen rule IDs and pin lattice | yes, [SPEC.md](https://github.com/taipei49314/pinwash/blob/v0.1.0/SPEC.md) |
|
|
171
|
+
| North star layers | yes, [ARCHITECTURE.md](https://github.com/taipei49314/pinwash/blob/v0.1.0/ARCHITECTURE.md); not a 1.0 claim |
|
|
172
|
+
| Engine `python -m pinwash check` | yes, v0.1.0 (Alpha) |
|
|
173
|
+
| Fixtures A1–A11 | yes, `tests/gates/test_v0_acceptance.py` (A11 = SPEC file not edited for the engine) |
|
|
174
|
+
| Byte-identical JSON across Windows / macOS / Linux and Python 3.11–3.13 (SPEC §9) | not CI-verified — no cross-platform CI matrix is configured; A10 compares LF and CRLF subject files in one run on one host. Local evidence, Linux only: on `f849b8a`, `pinwash check` stdout bytes and exit codes were identical across CPython 3.11.17, 3.12.3 and 3.13.16, each with `PYTHONHASHSEED` 1 and 17, on 58 cells (22 fixture shapes in 40 real git repos, run as `base..head`, `side...head` and worktree; 348 runs), with empty stderr. The shapes include CRLF files, non-ASCII and non-UTF-8 paths, JSON nested 1500 and 20000 deep, a 5000-digit integer, a submodule gitlink and worktree files that cannot be read. At `c71c468` the 6 cells nested 1500 deep exited 2 on 3.11 only (3.12 and 3.13 printed the bytes `f849b8a` prints on all three), and the 3 long-integer, 2 non-UTF-8-path, 3 nested-20000, 3 gitlink and 2 unreadable-file cells exited 2 on all three. Windows and macOS untested |
|
|
175
|
+
| Live-fire validated | records published, not re-judged — commits `86bdcce`, `df5eb21`, `7222bfc` and `fe0838e` record rules found and closed against real coding-agent sessions (five PREREG-predicted, judge-pinned evaluation rounds, six zen free models per round), and spec 9's producer-side `REQUIRED_CHECK_DROPPED` severity as validated live. The round records are in [`live-fire/`](https://github.com/taipei49314/pinwash/tree/v0.1.0/live-fire/) (PREREG, report, per-cell patch and pinned-judge output for rounds 0–4); the fixtures derived from them are in `tests/test_unit.py`. |
|
|
176
|
+
| 1.0 | no |
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
[MIT](https://github.com/taipei49314/pinwash/blob/v0.1.0/LICENSE).
|
pinwash-0.1.0/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# pinwash
|
|
2
|
+
|
|
3
|
+
**Flags a Git diff that makes an agent more able to claim done by weakening the verification harness around it.**
|
|
4
|
+
|
|
5
|
+
標出會讓 agent 比較容易自稱完成的 harness 變更。
|
|
6
|
+
|
|
7
|
+
Local-first. Deterministic. No LLM. No network. Does not execute the subject. Does not enforce anything.
|
|
8
|
+
|
|
9
|
+
PinWash **0.1.0 is Alpha**, licensed under MIT. The local v0 engine follows SPEC 27; this is not a 1.0 stability claim. Read [SPEC.md](https://github.com/taipei49314/pinwash/blob/v0.1.0/SPEC.md) (contract) and [ARCHITECTURE.md](https://github.com/taipei49314/pinwash/blob/v0.1.0/ARCHITECTURE.md) (layers; SPEC wins on conflict). Frozen acceptance is `tests/gates/test_v0_acceptance.py`.
|
|
10
|
+
|
|
11
|
+
**New here? Follow the [five-minute quickstart](https://github.com/taipei49314/pinwash/blob/v0.1.0/docs/QUICKSTART.md): install an
|
|
12
|
+
isolated installation, try a small example, then use the same two steps in your
|
|
13
|
+
project.** Installation needs Python 3.11+, Git and download access. The guide
|
|
14
|
+
also explains status, exit codes and recovery. The time is a walkthrough target,
|
|
15
|
+
not a measured first-user success claim.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
Requires Python 3.11+ and Git. Install outside the project your agent will edit,
|
|
20
|
+
using [pipx](https://pipx.pypa.io/) to keep PinWash in its own environment:
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
pipx install pinwash==0.1.0
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Linux is the package acceptance target; Windows/macOS are not yet validated.
|
|
27
|
+
Installed scans use no network and have no runtime dependencies. See
|
|
28
|
+
[installation](https://github.com/taipei49314/pinwash/blob/v0.1.0/docs/INSTALL.md) for a venv alternative and a local wheel install.
|
|
29
|
+
Keep this installation fixed until the current session has been reviewed.
|
|
30
|
+
|
|
31
|
+
## Review an AI coding session
|
|
32
|
+
|
|
33
|
+
In your project's **clean worktree**, before the agent starts:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
pinwash session start
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Use your coding tool normally. Once the agent **and all processes it started**
|
|
40
|
+
have stopped modifying files:
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
pinwash session review
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Answer the stopped-writers question. PinWash saves the original baseline and
|
|
47
|
+
judge identity, performs all three comparisons described below, explains the
|
|
48
|
+
findings, and saves a JSON report outside the project. It does not terminate
|
|
49
|
+
processes or independently verify that they stopped. A trusted non-interactive
|
|
50
|
+
controller supplies `--stopped` only after ensuring that condition itself.
|
|
51
|
+
|
|
52
|
+
Each finding explains **what changed, why it matters, and what to review next**.
|
|
53
|
+
The report begins with a **priority summary**: unfinished checks, blocking
|
|
54
|
+
changes, exemption decisions, and configuration/coverage gaps. Full findings
|
|
55
|
+
and original JSON remain available. No findings
|
|
56
|
+
does not prove that verification runs or that the product works.
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
pinwash session status
|
|
60
|
+
pinwash session review --json --stopped
|
|
61
|
+
pinwash review HEAD~1..HEAD
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`session status` reads the saved baseline, judge match and last saved report.
|
|
65
|
+
It does not scan current changes, reset the baseline or approve a historical
|
|
66
|
+
pass. Use the same `--state-dir` on start, status and review if you chose one.
|
|
67
|
+
|
|
68
|
+
`review` explains a single diff; without a range it compares HEAD to the worktree.
|
|
69
|
+
It is not a whole-session review. `check` retains its original JSON output and
|
|
70
|
+
exit codes for existing integrations. `review` and `session review` use exit `0` for
|
|
71
|
+
no blocking finding, `1` for a block, `2` for an unfinished review, and `3` for
|
|
72
|
+
an exemption awaiting human review. Coverage gaps remain visible even on exit 0.
|
|
73
|
+
`session status` exit `0` only means metadata was read successfully, including
|
|
74
|
+
when no session exists or the historical review blocked; exit `2` needs attention.
|
|
75
|
+
|
|
76
|
+
Starting never overwrites an existing session by default. After reviewing the
|
|
77
|
+
previous work and choosing the next committed baseline, use
|
|
78
|
+
`pinwash session start --replace`. Previous baseline records and reports remain.
|
|
79
|
+
For details and common errors, see [the session guide](https://github.com/taipei49314/pinwash/blob/v0.1.0/docs/SESSIONS.md).
|
|
80
|
+
|
|
81
|
+
**Trust boundary:** keep both PinWash and its state outside the agent's writable
|
|
82
|
+
scope. State is local convenience data, not signed evidence; placing it outside
|
|
83
|
+
the repository does not protect it from an unrestricted process using your account.
|
|
84
|
+
Run the consumer from your trusted terminal/controller, not from the agent being
|
|
85
|
+
judged. Keep the subject out of `PYTHONPATH` and other Python startup customization.
|
|
86
|
+
The consumer's isolated child judge prevents implicit repository-module shadowing;
|
|
87
|
+
the initial launcher environment must still be trusted. It does not sandbox the agent.
|
|
88
|
+
|
|
89
|
+
## Advanced: direct scanner and self-check
|
|
90
|
+
|
|
91
|
+
`pinwash doctor` runs the full bundled self-test suite against the installed code;
|
|
92
|
+
it can take several minutes and does not judge your project. It is deliberately
|
|
93
|
+
separate from the two-step session flow. Source contributors can still run the
|
|
94
|
+
suite from a trusted checkout with `python -m unittest discover -s tests -t . -v`.
|
|
95
|
+
|
|
96
|
+
`check` with no range compares `HEAD` to the worktree. Exit 0 is not proof the harness works; it means no in-scope finding was at or above `fail_on` (default `high`).
|
|
97
|
+
|
|
98
|
+
## Consumer recipe: judging an agent session
|
|
99
|
+
|
|
100
|
+
Judge what an agent session did with **three invocations**, after every process the session started has stopped:
|
|
101
|
+
|
|
102
|
+
```powershell
|
|
103
|
+
python -P -m pinwash check BASE..HEAD # what the session committed, against the pinned base
|
|
104
|
+
python -P -m pinwash check BASE..WORKTREE # everything it left on disk, against the pinned base
|
|
105
|
+
python -P -m pinwash check # its uncommitted edits against its own last commit
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- `BASE` is the commit you recorded **before** the session started, pinned (write down its sha, and pass the full sha, not a branch or tag: the repository's refs, like the rest of its `.git`, are the agent's to write). Never trust `HEAD` as the base: an agent that commits redefines it, and a single-range scan of the last commit only sees the last commit. In a live-fire round, a session committed a 7-file harness weakening (`neutralize verification harness`) that a worktree-only judge would have missed.
|
|
109
|
+
- The first two read `.pinwash/allow.toml`, required contexts and hook bodies from `BASE` (SPEC §2). Plain `check` reads them from `HEAD`, which the session moves: a session that commits an exemption record and leaves the weakening uncommitted passes plain `check` (THREATMODEL R18), and so does one that drops a required context in a commit and deletes its job uncommitted. Plain `check` stays because it compares the worktree with the session's own commits, so a judge renamed in a commit and gutted on disk into a body that is no stub but can no longer block shows there and nowhere else (a stub body, such as `exit 0`, shows in `BASE..WORKTREE` too).
|
|
110
|
+
- **Pass** means all three invocations exited 0 within your timeout, each printed JSON that parses, and none reports `EXEMPTION_ADDED`. **Held** is the same, except that at least one reports `EXEMPTION_ADDED`: the session wrote an exemption record, and only a human review of each added record releases it, because once a commit holding it becomes a pinned base or is merged it drops the finding it names until it expires (THREATMODEL R18). **Block** is any exit 1. Anything else (an exit 2, a run killed or timed out, no output or output that does not parse) means the session was **not judged**, never a pass. `unknown_coverage` rows are not part of pass or held; a gate on them is your own policy, and it will flag more than weakenings: in the two worktree invocations a host-written `.claude/settings.local.json` that an ignore rule matches (GitHub's Agents.gitignore has `.claude/*.local.json`) is a row in every session that has one, and so is each ignored hook log or dependency directory under `.claude/hooks/`; a file a hook writes there that is not UTF-8 and not ignored, such as a Python `__pycache__/*.pyc`, is a row of its own (SPEC §7).
|
|
111
|
+
- This recipe replaced a two-invocation one (`BASE..HEAD` and plain `check`), and it judges some sessions differently: one that commits a valid exemption record is held where it passed, and a weakening split between a commit and the uncommitted worktree through a state neither old invocation read blocks where it passed: for example, an exemption record committed with the weakening left uncommitted, a required context dropped in a commit with its job deleted uncommitted, or an MCP server added `disabled: true` in a commit and enabled uncommitted.
|
|
112
|
+
- The next session's `BASE` is the `HEAD` this judge saw, or a commit a human chose. It is never a branch or tag in the session's repository that the judged `HEAD` does not contain: the session could have moved it.
|
|
113
|
+
- The worktree is read while the judge runs. A process the session left running can change files between or during the invocations (a session that swapped the genuine settings back in while `check` ran passed all three), so stop every process it started before judging.
|
|
114
|
+
- **Run the judge with `-P`** (Python 3.11+; `PYTHONSAFEPATH=1` does the same, and so does an installed `pinwash` script). `check` judges the directory it runs in, which the agent wrote. Without `-P`, `python -m` puts that directory first on `sys.path`, so an `importlib.py`, another standard-library name or a `pinwash/` package the agent left there runs in place of the judge, before any pinwash code, and can print any verdict. pinwash cannot guard against that itself; only the invocation can. The same goes for `doctor`. `-P` drops only the implicit current directory: if `PYTHONPATH` names the judged repository (as the "Try it" block above does, which judges this checkout itself), the agent's `pinwash/` package is imported instead of the judge. When judging another repository, run the installed `pinwash` script, or set `PYTHONPATH` to the pinwash checkout only.
|
|
115
|
+
- Judge a clone that has the objects the run reads. A shallow or partial clone (`--depth`, `--filter`, `scalar clone`) that lacks one is exit 2, `git object <sha> is not in the object store`, never a pass and never a fetch; fetch it, or for `BASE...HEAD` compute the merge base yourself and pass `MB..HEAD`.
|
|
116
|
+
- Judge only after the agent's edits have settled on disk. Some CLIs apply edits asynchronously and can still be writing after their process returns; a judge that races the writer judges a stale worktree.
|
|
117
|
+
|
|
118
|
+
**Record the judge's identity with the verdict.** A pass or block that cannot name the judge is not reproducible. Record the pinwash git revision and the `python -P -m pinwash doctor` output (`spec_version`, self-test state; a judge record needs `nested_depth` 0, the full own suite), and the exit code of each invocation. A historical example, from spec 7, when the recipe had two invocations: "pass @ pinwash `86bdcce`, spec 7, 51 self-tests ok, exit 0 on both ranges"; the spec version and test count move with the revision, so record what `doctor` prints for yours. Round 1's judge was pinned this way (`416b962`, spec 6); its reports are not published in this repository.
|
|
119
|
+
|
|
120
|
+
**Exemptions are per finding fingerprint, on the base side only** (SPEC §10). Take the `fingerprint` field from the finding JSON (`rule/path/v1:<64hex>`), and write a `[[allow]]` record into `.pinwash/allow.toml` at the pinned base — all six keys required (`fingerprint`, `rule`, `reason`, `author`, `created`, `expires`), `expires` at most 180 days after `created`, never a rule glob. Set `created` to the day you add the record by the judge's clock (`PINWASH_TODAY`, else the judge host's local date), never a later date:
|
|
121
|
+
|
|
122
|
+
```toml
|
|
123
|
+
[[allow]]
|
|
124
|
+
fingerprint = "HOOK_REMOVED/.claude/settings.json/v1:…"
|
|
125
|
+
rule = "HOOK_REMOVED"
|
|
126
|
+
reason = "planned migration, ticket #…"
|
|
127
|
+
author = "you"
|
|
128
|
+
created = "2026-09-28"
|
|
129
|
+
expires = "2026-12-01"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Head-side valid additions surface as `EXEMPTION_ADDED` (warn), or critical when an added record's `created` is after today; editing or deleting a base exemption is `CONFIG_RELAXED` (critical). A base record exempts its finding only while `expires` is at most 180 days after the earlier of `created` and today, and the exemption channel never exempts its own findings (`CONFIG_RELAXED` and `EXEMPTION_ADDED` at `.pinwash/allow.toml`) (SPEC §10, spec 11).
|
|
133
|
+
|
|
134
|
+
Exit codes: `0` no finding at or above `fail_on` · `1` verdict block · `2` engine error. A crash must not exit 1; exit 0 is never proof the harness works.
|
|
135
|
+
|
|
136
|
+
## Neighbours
|
|
137
|
+
|
|
138
|
+
pinwash is not checkwash (product tests), not tripwire (hooks that run judges), not walkaround (session admission). It only asks whether **this diff** weakened pins, hooks, permissions, required checks, or bypass instructions. (tripwire and walkaround are no longer on GitHub — removed 2026-09-26 — and are named only to bound scope.)
|
|
139
|
+
|
|
140
|
+
## Status
|
|
141
|
+
|
|
142
|
+
The [second alpha-strengthening round](https://github.com/taipei49314/pinwash/blob/v0.1.0/docs/ALPHA2.md) repaired contract fidelity
|
|
143
|
+
and added real Git/CLI regressions. It is an engineering milestone, not version
|
|
144
|
+
`2.0.0`. Its approved EC pool verification passed 96 tests and doctor on the
|
|
145
|
+
implementation commit `8e0688c` and, separately, on the merged review head
|
|
146
|
+
`e89f05a`; the acceptance document links both receipts and lists what review
|
|
147
|
+
after the merge found (issues #3–#6). A commit's `ec / alpha2-verify` check
|
|
148
|
+
covers that commit only. New onboarding guidance has its own
|
|
149
|
+
[validation record](https://github.com/taipei49314/pinwash/blob/v0.1.0/docs/GUIDANCE_ACCEPTANCE.md); earlier results do not validate
|
|
150
|
+
new code.
|
|
151
|
+
|
|
152
|
+
| Claim | Status |
|
|
153
|
+
|---|---|
|
|
154
|
+
| Frozen rule IDs and pin lattice | yes, [SPEC.md](https://github.com/taipei49314/pinwash/blob/v0.1.0/SPEC.md) |
|
|
155
|
+
| North star layers | yes, [ARCHITECTURE.md](https://github.com/taipei49314/pinwash/blob/v0.1.0/ARCHITECTURE.md); not a 1.0 claim |
|
|
156
|
+
| Engine `python -m pinwash check` | yes, v0.1.0 (Alpha) |
|
|
157
|
+
| Fixtures A1–A11 | yes, `tests/gates/test_v0_acceptance.py` (A11 = SPEC file not edited for the engine) |
|
|
158
|
+
| Byte-identical JSON across Windows / macOS / Linux and Python 3.11–3.13 (SPEC §9) | not CI-verified — no cross-platform CI matrix is configured; A10 compares LF and CRLF subject files in one run on one host. Local evidence, Linux only: on `f849b8a`, `pinwash check` stdout bytes and exit codes were identical across CPython 3.11.17, 3.12.3 and 3.13.16, each with `PYTHONHASHSEED` 1 and 17, on 58 cells (22 fixture shapes in 40 real git repos, run as `base..head`, `side...head` and worktree; 348 runs), with empty stderr. The shapes include CRLF files, non-ASCII and non-UTF-8 paths, JSON nested 1500 and 20000 deep, a 5000-digit integer, a submodule gitlink and worktree files that cannot be read. At `c71c468` the 6 cells nested 1500 deep exited 2 on 3.11 only (3.12 and 3.13 printed the bytes `f849b8a` prints on all three), and the 3 long-integer, 2 non-UTF-8-path, 3 nested-20000, 3 gitlink and 2 unreadable-file cells exited 2 on all three. Windows and macOS untested |
|
|
159
|
+
| Live-fire validated | records published, not re-judged — commits `86bdcce`, `df5eb21`, `7222bfc` and `fe0838e` record rules found and closed against real coding-agent sessions (five PREREG-predicted, judge-pinned evaluation rounds, six zen free models per round), and spec 9's producer-side `REQUIRED_CHECK_DROPPED` severity as validated live. The round records are in [`live-fire/`](https://github.com/taipei49314/pinwash/tree/v0.1.0/live-fire/) (PREREG, report, per-cell patch and pinned-judge output for rounds 0–4); the fixtures derived from them are in `tests/test_unit.py`. |
|
|
160
|
+
| 1.0 | no |
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
[MIT](https://github.com/taipei49314/pinwash/blob/v0.1.0/LICENSE).
|