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.
Files changed (87) hide show
  1. pinwash-0.1.0/LICENSE +21 -0
  2. pinwash-0.1.0/MANIFEST.in +6 -0
  3. pinwash-0.1.0/PKG-INFO +180 -0
  4. pinwash-0.1.0/README.md +164 -0
  5. pinwash-0.1.0/SPEC.md +549 -0
  6. pinwash-0.1.0/THREATMODEL.md +36 -0
  7. pinwash-0.1.0/docs/ALPHA2.md +84 -0
  8. pinwash-0.1.0/docs/GUIDANCE_ACCEPTANCE.md +79 -0
  9. pinwash-0.1.0/docs/INSTALL.md +66 -0
  10. pinwash-0.1.0/docs/ONBOARDING_ACCEPTANCE.md +162 -0
  11. pinwash-0.1.0/docs/QUICKSTART.md +109 -0
  12. pinwash-0.1.0/docs/RELEASE.md +42 -0
  13. pinwash-0.1.0/docs/SESSIONS.md +137 -0
  14. pinwash-0.1.0/pinwash/__init__.py +5 -0
  15. pinwash-0.1.0/pinwash/__main__.py +4 -0
  16. pinwash-0.1.0/pinwash/allow.py +101 -0
  17. pinwash-0.1.0/pinwash/cli.py +193 -0
  18. pinwash-0.1.0/pinwash/consumer.py +226 -0
  19. pinwash-0.1.0/pinwash/engine.py +568 -0
  20. pinwash-0.1.0/pinwash/escalate.py +37 -0
  21. pinwash-0.1.0/pinwash/findings.py +86 -0
  22. pinwash-0.1.0/pinwash/gitrepo.py +1066 -0
  23. pinwash-0.1.0/pinwash/hooks.py +252 -0
  24. pinwash-0.1.0/pinwash/human_cli.py +102 -0
  25. pinwash-0.1.0/pinwash/parse/__init__.py +1 -0
  26. pinwash-0.1.0/pinwash/parse/gha.py +1243 -0
  27. pinwash-0.1.0/pinwash/parse/hookcmd.py +374 -0
  28. pinwash-0.1.0/pinwash/parse/jsonsurf.py +187 -0
  29. pinwash-0.1.0/pinwash/parse/tomlsub.py +63 -0
  30. pinwash-0.1.0/pinwash/pins.py +81 -0
  31. pinwash-0.1.0/pinwash/report.py +97 -0
  32. pinwash-0.1.0/pinwash/rules/__init__.py +1 -0
  33. pinwash-0.1.0/pinwash/rules/config_rule.py +231 -0
  34. pinwash-0.1.0/pinwash/rules/hook_coverage.py +415 -0
  35. pinwash-0.1.0/pinwash/rules/hook_rules.py +480 -0
  36. pinwash-0.1.0/pinwash/rules/hook_selector.py +240 -0
  37. pinwash-0.1.0/pinwash/rules/hook_switch.py +113 -0
  38. pinwash-0.1.0/pinwash/rules/permission.py +506 -0
  39. pinwash-0.1.0/pinwash/rules/pin_rules.py +151 -0
  40. pinwash-0.1.0/pinwash/rules/required_check.py +667 -0
  41. pinwash-0.1.0/pinwash/rules/skill_bypass.py +58 -0
  42. pinwash-0.1.0/pinwash/session.py +162 -0
  43. pinwash-0.1.0/pinwash/session_status.py +176 -0
  44. pinwash-0.1.0/pinwash/surfaces.py +132 -0
  45. pinwash-0.1.0/pinwash/textutil.py +93 -0
  46. pinwash-0.1.0/pinwash/verification.py +28 -0
  47. pinwash-0.1.0/pinwash.egg-info/PKG-INFO +180 -0
  48. pinwash-0.1.0/pinwash.egg-info/SOURCES.txt +85 -0
  49. pinwash-0.1.0/pinwash.egg-info/dependency_links.txt +1 -0
  50. pinwash-0.1.0/pinwash.egg-info/entry_points.txt +2 -0
  51. pinwash-0.1.0/pinwash.egg-info/top_level.txt +1 -0
  52. pinwash-0.1.0/pyproject.toml +29 -0
  53. pinwash-0.1.0/setup.cfg +4 -0
  54. pinwash-0.1.0/setup.py +38 -0
  55. pinwash-0.1.0/tests/__init__.py +0 -0
  56. pinwash-0.1.0/tests/gates/__init__.py +0 -0
  57. pinwash-0.1.0/tests/gates/test_v0_acceptance.py +325 -0
  58. pinwash-0.1.0/tests/test_allow_spelling.py +124 -0
  59. pinwash-0.1.0/tests/test_alpha2_git_cli.py +499 -0
  60. pinwash-0.1.0/tests/test_alpha2_regressions.py +322 -0
  61. pinwash-0.1.0/tests/test_checkwash_read.py +218 -0
  62. pinwash-0.1.0/tests/test_consumer.py +427 -0
  63. pinwash-0.1.0/tests/test_engine_fidelity.py +1092 -0
  64. pinwash-0.1.0/tests/test_exemption_trust.py +273 -0
  65. pinwash-0.1.0/tests/test_exit2_capability.py +141 -0
  66. pinwash-0.1.0/tests/test_family_regressions.py +517 -0
  67. pinwash-0.1.0/tests/test_git_hardening.py +896 -0
  68. pinwash-0.1.0/tests/test_hook_routing.py +655 -0
  69. pinwash-0.1.0/tests/test_hook_switch.py +235 -0
  70. pinwash-0.1.0/tests/test_host_decoding.py +136 -0
  71. pinwash-0.1.0/tests/test_ignored_surfaces.py +340 -0
  72. pinwash-0.1.0/tests/test_invocation.py +95 -0
  73. pinwash-0.1.0/tests/test_job_conditions.py +196 -0
  74. pinwash-0.1.0/tests/test_permission_mode.py +249 -0
  75. pinwash-0.1.0/tests/test_pin_severities.py +201 -0
  76. pinwash-0.1.0/tests/test_producer_rename.py +225 -0
  77. pinwash-0.1.0/tests/test_project_dir_forms.py +153 -0
  78. pinwash-0.1.0/tests/test_project_dir_guard_holders.py +111 -0
  79. pinwash-0.1.0/tests/test_r12_challenge.py +163 -0
  80. pinwash-0.1.0/tests/test_r12_liveness.py +192 -0
  81. pinwash-0.1.0/tests/test_residual_rows.py +182 -0
  82. pinwash-0.1.0/tests/test_session_recipe.py +145 -0
  83. pinwash-0.1.0/tests/test_session_status.py +266 -0
  84. pinwash-0.1.0/tests/test_spec16_rows.py +273 -0
  85. pinwash-0.1.0/tests/test_spec_statements.py +359 -0
  86. pinwash-0.1.0/tests/test_unit.py +644 -0
  87. 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.
@@ -0,0 +1,6 @@
1
+ include LICENSE SPEC.md THREATMODEL.md README.md pyproject.toml setup.py
2
+ recursive-include tests *.py
3
+ recursive-include docs *.md
4
+ exclude ec-workloads.json
5
+ global-exclude *.py[cod]
6
+ global-exclude __pycache__
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).
@@ -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).