kpopper 0.21.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.
kpopper-0.21.0/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ilan Bar Magen
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 OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,242 @@
1
+ Metadata-Version: 2.4
2
+ Name: kpopper
3
+ Version: 0.21.0
4
+ Summary: An epistemic record for work that gets revisited: what is known, how each piece is grounded, and what would make each judgment wrong.
5
+ Author: Ilan Bar Magen
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/ilanbm/kpopper
8
+ Project-URL: Source, https://github.com/ilanbm/kpopper
9
+ Project-URL: Issues, https://github.com/ilanbm/kpopper/issues
10
+ Keywords: provenance,epistemic,falsifiability,truth-maintenance,traceability,research
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Software Development :: Documentation
17
+ Classifier: Topic :: Software Development :: Quality Assurance
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: PyYAML>=5.1
22
+ Dynamic: license-file
23
+
24
+ # kpopper
25
+
26
+ *Named for Karl Popper: nothing here is ever verified, only exposed to refutation. Every
27
+ judgment must say what would make it wrong — one that cannot be wrong is an opinion.*
28
+
29
+ The stores remember. This remembers **how you know.**
30
+
31
+ kpopper keeps an epistemic record for work that gets revisited: what was read from the world,
32
+ what was worked out from it, what was concluded — and what every conclusion is still standing
33
+ on. It is not another knowledge store, and it does not model what exists. A fact from
34
+ anywhere — a document, a database, another knowledge system — enters as an entry that says
35
+ where it came from; the record governs its life: who vouches for it, when it was last
36
+ checked, and what falls when it moves.
37
+
38
+ Two mechanics carry the whole method:
39
+
40
+ - **Every judgment states, in advance, what would make it wrong — or declares out loud why
41
+ it cannot yet.** `wrong_if` is a predicate over the things the judgment declares it rests
42
+ on. The reader refuses prose in its place, rejects a predicate that reads anything
43
+ undeclared — and when the predicate is a plain comparison, evaluates it: a judgment whose
44
+ own falsifier holds **fails the build**. Anything richer is surfaced beside exactly what
45
+ moved, never guessed at.
46
+ - **Staleness fires on reality, not on the calendar.** Every judgment carries `seen`, a
47
+ snapshot of what its dependencies held when it was last reviewed. Nothing stores a stale
48
+ flag; drift is *derived* by comparison, so it cannot be forgotten, cleared by accident, or
49
+ survive a revert. What it compares is the value the judgment used — re-reading a source is
50
+ still a human act, and a value the record never re-read cannot drift.
51
+
52
+ And one boundary: **invalidation spreads — along what each judgment declared it rests on,
53
+ and through the rules of worked-out values — but a re-derivation never applies itself.** A
54
+ change marks everything downstream of it; deciding what to do about that waits for a person.
55
+ A stale recommendation someone read beats a current one nobody did.
56
+
57
+ A content hash over a source answers a different question than the one a conclusion needs:
58
+ it fires when a file is reformatted and stays silent when the number you relied on moves
59
+ somewhere the hash never covered. Matching bytes prove a file is unchanged — never that what
60
+ you concluded from it still holds. So kpopper snapshots the *value the judgment used*, not
61
+ the bytes it came from, and states the breaking condition in advance instead of waiting for
62
+ a checksum to notice.
63
+
64
+ ## What it installs
65
+
66
+ The package is the command line: `kpopper`, with the reader and the renderer behind it. The
67
+ plugin is all of that plus the method and the session hooks:
68
+
69
+ | | |
70
+ |---|---|
71
+ | `skills/kpopper` | The method. Loads when work will be revisited, or when resuming such work. |
72
+ | `skills/kpopper/PAGE.md` | The page reference — briefs, renderers, the tree. Read only when building a page. |
73
+ | `scripts/kpopper` | One entry point: `open · check · affects · pull · page`. The dispatcher itself is `scripts/cli.py` — the same code the installed `kpopper` command runs. |
74
+ | `scripts/provenance.py` | The reader underneath. Field names are inferred by shape, so it reads records written in any vocabulary. |
75
+ | `scripts/render_page.py` | The record as one self-contained page, three tabs, no dependencies beyond the reader. |
76
+ | `scripts/verify_page.js` | Browser checks for that page, in both themes and under reduced motion. Playwright. Reached as `kpopper page --checks`. |
77
+ | `tests/` | The contract the reader and the page keep, run as `python3 -m unittest discover -s tests` against the fixture record in `tests/fixtures/page` — every field they accept, exercised once. |
78
+ | `hooks/` | A session opener and a stop gate. The opener runs `provenance.py open` when the project keeps a record — at its root, or registered with the checkout — and is silent everywhere else; the gate bounces a session once, with the failures, if it tries to finish having left `check` worse than it found it. |
79
+
80
+ ## The record
81
+
82
+ One file, one place: `PROVENANCE.yaml` at the project root — or a pointer to wherever the
83
+ content actually sits, including a mapping of several files; the reader follows it. A project
84
+ whose tree cannot hold the file keeps it elsewhere and registers the path with the checkout
85
+ (one line in the git common dir; `kpopper where` prints it), and the opener and every command
86
+ find it from any worktree. It holds three things: what was taken from a source (and where
87
+ within it), what was worked out (the rule, never the result), and what was concluded (with
88
+ what would make it wrong).
89
+
90
+ ```yaml
91
+ sources:
92
+ msa: {file: "contracts/acme-msa-2026.pdf", of: "2026-04-02"}
93
+
94
+ known:
95
+ acme.seat_price: {v: 42, from: msa, at: "Enterprise tier", name: "Acme seat price"}
96
+ acme.seats: {v: 180, from: msa, name: "Committed seats"}
97
+ acme.annual: {rule: "acme.seat_price * acme.seats * 12", name: "Annual Acme cost"}
98
+ beta.annual: {v: 84000, from: msa, name: "Annual Beta cost"}
99
+
100
+ judgments:
101
+ why_acme:
102
+ rests_on: [acme.annual, beta.annual, acme.seats]
103
+ verdict: "prefer Acme above ~150 seats"
104
+ wrong_if: "acme.seats < 150"
105
+ seen: {acme.annual: 90720, beta.annual: 84000, acme.seats: 180}
106
+ seats_fit:
107
+ rests_on: [acme.seats]
108
+ verdict: "the committed seats fit one contract tier"
109
+ wrong_if: "acme.seats > 500"
110
+ seen: {acme.seats: 180}
111
+ ```
112
+
113
+ Drop the seat count to 120 and `check` fails with `why_acme: wrong_if holds (acme.seats <
114
+ 150) - broken by its own condition`; `affects acme.seat_price` reaches `why_acme` through
115
+ the rule that computes the annual figure. The record does not wait to be asked.
116
+
117
+ Nothing is created on the first turn. The file appears when there is a first thing to put in
118
+ it, and structure is added only when something observable forces it. A project that never
119
+ grows past a single file is a correct outcome.
120
+
121
+ ## Opening a session costs what moved, not what exists
122
+
123
+ With the plugin installed, every session in a project that keeps a record opens with the
124
+ record's own head — its name, its namespace, what needs a person, the open questions — inside
125
+ a fixed character budget. Measured on a live record of 107 entries: reading it whole costs
126
+ 39,777 characters; the opener costs 948 on a clean record. The opener also re-runs after a
127
+ context compaction, which is exactly when a session most needs to be re-grounded.
128
+
129
+ The rest is pulled, never preloaded:
130
+
131
+ ```bash
132
+ kpopper open # what a session reads instead of the whole record
133
+ kpopper check # does the record still hold together
134
+ kpopper affects <entry> # what a change reaches, through intermediate judgments
135
+ kpopper pull <entry|prefix> # a subject's values with their sources - and what moved
136
+ # since each judgment last looked
137
+ kpopper where # the record this directory answers for
138
+ ```
139
+
140
+ Each command reads the record in the current directory, or the files you name. Without the
141
+ command line on your path, the same dispatcher ships inside the plugin — find it once, then run
142
+ `"$K" open`. An installed plugin sits under a version directory and may hold worktrees of its
143
+ own, so the search skips those and takes the highest version rather than the first hit:
144
+
145
+ ```bash
146
+ K=$(find ~/.claude -name worktrees -prune -o -path '*kpopper*/scripts/kpopper' -print 2>/dev/null | sort -V | tail -1)
147
+ ```
148
+
149
+ `check` exits non-zero on an undeclared gap: a dependency that is not an entry (unless the
150
+ judgment declares it missing with `blocked_on`), a dependency with no snapshot, a predicate
151
+ naming something undeclared, prose sitting in a predicate field, or a plain-comparison
152
+ predicate that currently holds — a judgment broken by its own condition. A declared hole is a
153
+ note, not a failure — a build that stays red over an honest declaration teaches records to
154
+ stop declaring. A dependency that moved since a judgment's snapshot is reported as `MOVED`
155
+ and does not fail the build either: it puts the judgment in front of a person, and it is
156
+ muted when the predicate names it and still evaluates false — moved, not across the line.
157
+
158
+ ## The page — and the tree
159
+
160
+ ```bash
161
+ kpopper page --open # the record as one page, in the browser
162
+ kpopper page --open --tree # landing on the tree
163
+ kpopper page --verify # deterministic checks, no browser
164
+ ```
165
+
166
+ Three tabs. **Now** is the arrangement this session chose, written in a brief the page keeps
167
+ honest: it may order, it cannot drop. **Record** is everything, arranged by nothing. **Tree**
168
+ is the whole record as one growing thing: what was read from the world is the root fan below
169
+ the ground line, worked-out values branch above it, and conclusions blossom in a canopy —
170
+ the three node kinds in their three colors. Everything on every tab is the same live card:
171
+ hover or tap for where a value came from, click a dependency to walk to it, and on the tree,
172
+ opening a node lights the sap — the full chain of evidence feeding it — while its neighbours
173
+ stir like branches in wind. A card's `tree` button prunes the tree to that node's world; one
174
+ chip brings the whole tree back. The tab grows in the first time it is opened, and any node can
175
+ be pulled — it resists like a branch, stretches its limbs, and springs back when you let go,
176
+ because the page keeps nothing.
177
+
178
+ ## The part that does not change
179
+
180
+ 1. **Every entry declares what it rests on.** Something with no declaration does not go in.
181
+ 2. **Store what produces an output, never the output.**
182
+ 3. **Invalidation spreads automatically; re-running a judgment never applies itself.**
183
+ 4. **A change to the shape is valid only with a migration that leaves the build green.**
184
+
185
+ Everything else — field names, sections, renderers — is open to revision. A record that can
186
+ rewrite its own rules will drift unless something in it is not up for revision. These four
187
+ are that floor.
188
+
189
+ ## Installing
190
+
191
+ The command line on its own, for any project and any editor:
192
+
193
+ ```bash
194
+ pipx install kpopper # or: pip install kpopper
195
+ ```
196
+
197
+ The browser checks come with it — they are Node, but they install where everything else does:
198
+
199
+ ```bash
200
+ kpopper page --out record.html # then: kpopper page --checks record.html
201
+ ```
202
+
203
+ The Claude Code plugin — the method as a skill, the session opener, the stop gate, and the
204
+ same commands:
205
+
206
+ ```bash
207
+ /plugin marketplace add /path/to/kpopper
208
+ /plugin install kpopper@kpopper
209
+ ```
210
+
211
+ or non-interactively:
212
+
213
+ ```bash
214
+ claude plugin install kpopper@kpopper --scope user
215
+ ```
216
+
217
+ `--scope user` makes it available in every project on the machine; `--scope project` commits
218
+ it to the repo you are in. Other editors are wired up from `adapters/`.
219
+
220
+ Where the channels overlap they are the same files rather than copies of them: the packages are
221
+ mapped onto the plugin's `scripts/`, so the reader, the renderer, the dispatcher and the browser
222
+ checks have nothing to keep in step, and one version number covers all of them. There is a third
223
+ package, on npm, and it is that same mapping: it holds the name while an open question in the
224
+ record — whether the renderer, the command line and the checks move to TypeScript — is still open,
225
+ and it carries the browser checks rather than nothing, because they are the part that is already
226
+ Node. This repository is the only place any of it is edited; every installed copy is a read-only
227
+ distribution.
228
+
229
+ ## Requirements
230
+
231
+ Python 3.9+ and PyYAML — `pipx` brings it along; a plugin-only install wants
232
+ `pip3 install pyyaml`. The browser checks additionally want Node 18+, a Chrome/Chromium
233
+ binary (`CHROME=/path/to/chrome` when it is not on a known path), and `playwright-core` — the
234
+ driver, which none of the packages ship: it is found beside the page when the project already
235
+ uses Playwright, and otherwise `npm i --no-save playwright-core` next to the page is enough.
236
+ Nothing else.
237
+
238
+ ## What would show this was not worth it
239
+
240
+ The method promises exactly one measurable thing: the opening cost of session number N. If
241
+ after five sessions the sixth does not open cheaper, the method did not return what it cost,
242
+ and you can drop it with a clear conscience.
@@ -0,0 +1,219 @@
1
+ # kpopper
2
+
3
+ *Named for Karl Popper: nothing here is ever verified, only exposed to refutation. Every
4
+ judgment must say what would make it wrong — one that cannot be wrong is an opinion.*
5
+
6
+ The stores remember. This remembers **how you know.**
7
+
8
+ kpopper keeps an epistemic record for work that gets revisited: what was read from the world,
9
+ what was worked out from it, what was concluded — and what every conclusion is still standing
10
+ on. It is not another knowledge store, and it does not model what exists. A fact from
11
+ anywhere — a document, a database, another knowledge system — enters as an entry that says
12
+ where it came from; the record governs its life: who vouches for it, when it was last
13
+ checked, and what falls when it moves.
14
+
15
+ Two mechanics carry the whole method:
16
+
17
+ - **Every judgment states, in advance, what would make it wrong — or declares out loud why
18
+ it cannot yet.** `wrong_if` is a predicate over the things the judgment declares it rests
19
+ on. The reader refuses prose in its place, rejects a predicate that reads anything
20
+ undeclared — and when the predicate is a plain comparison, evaluates it: a judgment whose
21
+ own falsifier holds **fails the build**. Anything richer is surfaced beside exactly what
22
+ moved, never guessed at.
23
+ - **Staleness fires on reality, not on the calendar.** Every judgment carries `seen`, a
24
+ snapshot of what its dependencies held when it was last reviewed. Nothing stores a stale
25
+ flag; drift is *derived* by comparison, so it cannot be forgotten, cleared by accident, or
26
+ survive a revert. What it compares is the value the judgment used — re-reading a source is
27
+ still a human act, and a value the record never re-read cannot drift.
28
+
29
+ And one boundary: **invalidation spreads — along what each judgment declared it rests on,
30
+ and through the rules of worked-out values — but a re-derivation never applies itself.** A
31
+ change marks everything downstream of it; deciding what to do about that waits for a person.
32
+ A stale recommendation someone read beats a current one nobody did.
33
+
34
+ A content hash over a source answers a different question than the one a conclusion needs:
35
+ it fires when a file is reformatted and stays silent when the number you relied on moves
36
+ somewhere the hash never covered. Matching bytes prove a file is unchanged — never that what
37
+ you concluded from it still holds. So kpopper snapshots the *value the judgment used*, not
38
+ the bytes it came from, and states the breaking condition in advance instead of waiting for
39
+ a checksum to notice.
40
+
41
+ ## What it installs
42
+
43
+ The package is the command line: `kpopper`, with the reader and the renderer behind it. The
44
+ plugin is all of that plus the method and the session hooks:
45
+
46
+ | | |
47
+ |---|---|
48
+ | `skills/kpopper` | The method. Loads when work will be revisited, or when resuming such work. |
49
+ | `skills/kpopper/PAGE.md` | The page reference — briefs, renderers, the tree. Read only when building a page. |
50
+ | `scripts/kpopper` | One entry point: `open · check · affects · pull · page`. The dispatcher itself is `scripts/cli.py` — the same code the installed `kpopper` command runs. |
51
+ | `scripts/provenance.py` | The reader underneath. Field names are inferred by shape, so it reads records written in any vocabulary. |
52
+ | `scripts/render_page.py` | The record as one self-contained page, three tabs, no dependencies beyond the reader. |
53
+ | `scripts/verify_page.js` | Browser checks for that page, in both themes and under reduced motion. Playwright. Reached as `kpopper page --checks`. |
54
+ | `tests/` | The contract the reader and the page keep, run as `python3 -m unittest discover -s tests` against the fixture record in `tests/fixtures/page` — every field they accept, exercised once. |
55
+ | `hooks/` | A session opener and a stop gate. The opener runs `provenance.py open` when the project keeps a record — at its root, or registered with the checkout — and is silent everywhere else; the gate bounces a session once, with the failures, if it tries to finish having left `check` worse than it found it. |
56
+
57
+ ## The record
58
+
59
+ One file, one place: `PROVENANCE.yaml` at the project root — or a pointer to wherever the
60
+ content actually sits, including a mapping of several files; the reader follows it. A project
61
+ whose tree cannot hold the file keeps it elsewhere and registers the path with the checkout
62
+ (one line in the git common dir; `kpopper where` prints it), and the opener and every command
63
+ find it from any worktree. It holds three things: what was taken from a source (and where
64
+ within it), what was worked out (the rule, never the result), and what was concluded (with
65
+ what would make it wrong).
66
+
67
+ ```yaml
68
+ sources:
69
+ msa: {file: "contracts/acme-msa-2026.pdf", of: "2026-04-02"}
70
+
71
+ known:
72
+ acme.seat_price: {v: 42, from: msa, at: "Enterprise tier", name: "Acme seat price"}
73
+ acme.seats: {v: 180, from: msa, name: "Committed seats"}
74
+ acme.annual: {rule: "acme.seat_price * acme.seats * 12", name: "Annual Acme cost"}
75
+ beta.annual: {v: 84000, from: msa, name: "Annual Beta cost"}
76
+
77
+ judgments:
78
+ why_acme:
79
+ rests_on: [acme.annual, beta.annual, acme.seats]
80
+ verdict: "prefer Acme above ~150 seats"
81
+ wrong_if: "acme.seats < 150"
82
+ seen: {acme.annual: 90720, beta.annual: 84000, acme.seats: 180}
83
+ seats_fit:
84
+ rests_on: [acme.seats]
85
+ verdict: "the committed seats fit one contract tier"
86
+ wrong_if: "acme.seats > 500"
87
+ seen: {acme.seats: 180}
88
+ ```
89
+
90
+ Drop the seat count to 120 and `check` fails with `why_acme: wrong_if holds (acme.seats <
91
+ 150) - broken by its own condition`; `affects acme.seat_price` reaches `why_acme` through
92
+ the rule that computes the annual figure. The record does not wait to be asked.
93
+
94
+ Nothing is created on the first turn. The file appears when there is a first thing to put in
95
+ it, and structure is added only when something observable forces it. A project that never
96
+ grows past a single file is a correct outcome.
97
+
98
+ ## Opening a session costs what moved, not what exists
99
+
100
+ With the plugin installed, every session in a project that keeps a record opens with the
101
+ record's own head — its name, its namespace, what needs a person, the open questions — inside
102
+ a fixed character budget. Measured on a live record of 107 entries: reading it whole costs
103
+ 39,777 characters; the opener costs 948 on a clean record. The opener also re-runs after a
104
+ context compaction, which is exactly when a session most needs to be re-grounded.
105
+
106
+ The rest is pulled, never preloaded:
107
+
108
+ ```bash
109
+ kpopper open # what a session reads instead of the whole record
110
+ kpopper check # does the record still hold together
111
+ kpopper affects <entry> # what a change reaches, through intermediate judgments
112
+ kpopper pull <entry|prefix> # a subject's values with their sources - and what moved
113
+ # since each judgment last looked
114
+ kpopper where # the record this directory answers for
115
+ ```
116
+
117
+ Each command reads the record in the current directory, or the files you name. Without the
118
+ command line on your path, the same dispatcher ships inside the plugin — find it once, then run
119
+ `"$K" open`. An installed plugin sits under a version directory and may hold worktrees of its
120
+ own, so the search skips those and takes the highest version rather than the first hit:
121
+
122
+ ```bash
123
+ K=$(find ~/.claude -name worktrees -prune -o -path '*kpopper*/scripts/kpopper' -print 2>/dev/null | sort -V | tail -1)
124
+ ```
125
+
126
+ `check` exits non-zero on an undeclared gap: a dependency that is not an entry (unless the
127
+ judgment declares it missing with `blocked_on`), a dependency with no snapshot, a predicate
128
+ naming something undeclared, prose sitting in a predicate field, or a plain-comparison
129
+ predicate that currently holds — a judgment broken by its own condition. A declared hole is a
130
+ note, not a failure — a build that stays red over an honest declaration teaches records to
131
+ stop declaring. A dependency that moved since a judgment's snapshot is reported as `MOVED`
132
+ and does not fail the build either: it puts the judgment in front of a person, and it is
133
+ muted when the predicate names it and still evaluates false — moved, not across the line.
134
+
135
+ ## The page — and the tree
136
+
137
+ ```bash
138
+ kpopper page --open # the record as one page, in the browser
139
+ kpopper page --open --tree # landing on the tree
140
+ kpopper page --verify # deterministic checks, no browser
141
+ ```
142
+
143
+ Three tabs. **Now** is the arrangement this session chose, written in a brief the page keeps
144
+ honest: it may order, it cannot drop. **Record** is everything, arranged by nothing. **Tree**
145
+ is the whole record as one growing thing: what was read from the world is the root fan below
146
+ the ground line, worked-out values branch above it, and conclusions blossom in a canopy —
147
+ the three node kinds in their three colors. Everything on every tab is the same live card:
148
+ hover or tap for where a value came from, click a dependency to walk to it, and on the tree,
149
+ opening a node lights the sap — the full chain of evidence feeding it — while its neighbours
150
+ stir like branches in wind. A card's `tree` button prunes the tree to that node's world; one
151
+ chip brings the whole tree back. The tab grows in the first time it is opened, and any node can
152
+ be pulled — it resists like a branch, stretches its limbs, and springs back when you let go,
153
+ because the page keeps nothing.
154
+
155
+ ## The part that does not change
156
+
157
+ 1. **Every entry declares what it rests on.** Something with no declaration does not go in.
158
+ 2. **Store what produces an output, never the output.**
159
+ 3. **Invalidation spreads automatically; re-running a judgment never applies itself.**
160
+ 4. **A change to the shape is valid only with a migration that leaves the build green.**
161
+
162
+ Everything else — field names, sections, renderers — is open to revision. A record that can
163
+ rewrite its own rules will drift unless something in it is not up for revision. These four
164
+ are that floor.
165
+
166
+ ## Installing
167
+
168
+ The command line on its own, for any project and any editor:
169
+
170
+ ```bash
171
+ pipx install kpopper # or: pip install kpopper
172
+ ```
173
+
174
+ The browser checks come with it — they are Node, but they install where everything else does:
175
+
176
+ ```bash
177
+ kpopper page --out record.html # then: kpopper page --checks record.html
178
+ ```
179
+
180
+ The Claude Code plugin — the method as a skill, the session opener, the stop gate, and the
181
+ same commands:
182
+
183
+ ```bash
184
+ /plugin marketplace add /path/to/kpopper
185
+ /plugin install kpopper@kpopper
186
+ ```
187
+
188
+ or non-interactively:
189
+
190
+ ```bash
191
+ claude plugin install kpopper@kpopper --scope user
192
+ ```
193
+
194
+ `--scope user` makes it available in every project on the machine; `--scope project` commits
195
+ it to the repo you are in. Other editors are wired up from `adapters/`.
196
+
197
+ Where the channels overlap they are the same files rather than copies of them: the packages are
198
+ mapped onto the plugin's `scripts/`, so the reader, the renderer, the dispatcher and the browser
199
+ checks have nothing to keep in step, and one version number covers all of them. There is a third
200
+ package, on npm, and it is that same mapping: it holds the name while an open question in the
201
+ record — whether the renderer, the command line and the checks move to TypeScript — is still open,
202
+ and it carries the browser checks rather than nothing, because they are the part that is already
203
+ Node. This repository is the only place any of it is edited; every installed copy is a read-only
204
+ distribution.
205
+
206
+ ## Requirements
207
+
208
+ Python 3.9+ and PyYAML — `pipx` brings it along; a plugin-only install wants
209
+ `pip3 install pyyaml`. The browser checks additionally want Node 18+, a Chrome/Chromium
210
+ binary (`CHROME=/path/to/chrome` when it is not on a known path), and `playwright-core` — the
211
+ driver, which none of the packages ship: it is found beside the page when the project already
212
+ uses Playwright, and otherwise `npm i --no-save playwright-core` next to the page is enough.
213
+ Nothing else.
214
+
215
+ ## What would show this was not worth it
216
+
217
+ The method promises exactly one measurable thing: the opening cost of session number N. If
218
+ after five sessions the sixth does not open cheaper, the method did not return what it cost,
219
+ and you can drop it with a clear conscience.