pyjab-mcp 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 (52) hide show
  1. pyjab_mcp-0.1.0/CHANGELOG.md +405 -0
  2. pyjab_mcp-0.1.0/LICENSE +21 -0
  3. pyjab_mcp-0.1.0/MANIFEST.in +32 -0
  4. pyjab_mcp-0.1.0/PKG-INFO +237 -0
  5. pyjab_mcp-0.1.0/README.md +201 -0
  6. pyjab_mcp-0.1.0/acknowledged.yaml +76 -0
  7. pyjab_mcp-0.1.0/docs/ARCHITECTURE.md +196 -0
  8. pyjab_mcp-0.1.0/pyproject.toml +92 -0
  9. pyjab_mcp-0.1.0/setup.cfg +4 -0
  10. pyjab_mcp-0.1.0/src/pyjab_mcp/__init__.py +18 -0
  11. pyjab_mcp-0.1.0/src/pyjab_mcp/__main__.py +42 -0
  12. pyjab_mcp-0.1.0/src/pyjab_mcp/backend.py +1094 -0
  13. pyjab_mcp-0.1.0/src/pyjab_mcp/bridge.py +377 -0
  14. pyjab_mcp-0.1.0/src/pyjab_mcp/diagnostics.py +306 -0
  15. pyjab_mcp-0.1.0/src/pyjab_mcp/handles.py +324 -0
  16. pyjab_mcp-0.1.0/src/pyjab_mcp/server.py +1254 -0
  17. pyjab_mcp-0.1.0/src/pyjab_mcp/tree.py +360 -0
  18. pyjab_mcp-0.1.0/src/pyjab_mcp/windows.py +127 -0
  19. pyjab_mcp-0.1.0/src/pyjab_mcp.egg-info/PKG-INFO +237 -0
  20. pyjab_mcp-0.1.0/src/pyjab_mcp.egg-info/SOURCES.txt +50 -0
  21. pyjab_mcp-0.1.0/src/pyjab_mcp.egg-info/dependency_links.txt +1 -0
  22. pyjab_mcp-0.1.0/src/pyjab_mcp.egg-info/entry_points.txt +2 -0
  23. pyjab_mcp-0.1.0/src/pyjab_mcp.egg-info/requires.txt +10 -0
  24. pyjab_mcp-0.1.0/src/pyjab_mcp.egg-info/top_level.txt +1 -0
  25. pyjab_mcp-0.1.0/tests/_fakebackend.py +231 -0
  26. pyjab_mcp-0.1.0/tests/_fakedriver.py +349 -0
  27. pyjab_mcp-0.1.0/tests/conftest.py +17 -0
  28. pyjab_mcp-0.1.0/tests/contract/test_pyjab_contract.py +270 -0
  29. pyjab_mcp-0.1.0/tests/e2e/test_live_window.py +534 -0
  30. pyjab_mcp-0.1.0/tests/test_backend_act.py +113 -0
  31. pyjab_mcp-0.1.0/tests/test_backend_api.py +317 -0
  32. pyjab_mcp-0.1.0/tests/test_backend_read.py +233 -0
  33. pyjab_mcp-0.1.0/tests/test_backend_table.py +147 -0
  34. pyjab_mcp-0.1.0/tests/test_backend_tree.py +143 -0
  35. pyjab_mcp-0.1.0/tests/test_bridge_thread.py +193 -0
  36. pyjab_mcp-0.1.0/tests/test_diagnostics.py +221 -0
  37. pyjab_mcp-0.1.0/tests/test_guards.py +655 -0
  38. pyjab_mcp-0.1.0/tests/test_handles.py +110 -0
  39. pyjab_mcp-0.1.0/tests/test_packaging.py +191 -0
  40. pyjab_mcp-0.1.0/tests/test_server_tools.py +705 -0
  41. pyjab_mcp-0.1.0/tests/test_stdio_server.py +80 -0
  42. pyjab_mcp-0.1.0/tests/test_tree.py +317 -0
  43. pyjab_mcp-0.1.0/tools/check_changelog_immutable.py +177 -0
  44. pyjab_mcp-0.1.0/tools/check_dist_contents.py +199 -0
  45. pyjab_mcp-0.1.0/tools/check_documented_surface.py +224 -0
  46. pyjab_mcp-0.1.0/tools/check_local_only_files.py +154 -0
  47. pyjab_mcp-0.1.0/tools/check_pyjab_api_surface.py +697 -0
  48. pyjab_mcp-0.1.0/tools/check_pyjab_changelog.py +283 -0
  49. pyjab_mcp-0.1.0/tools/check_pyjab_readiness.py +172 -0
  50. pyjab_mcp-0.1.0/tools/check_release_version.py +134 -0
  51. pyjab_mcp-0.1.0/tools/fetch_test_app.py +115 -0
  52. pyjab_mcp-0.1.0/tools/verify_diagnostics_jdk.py +111 -0
@@ -0,0 +1,405 @@
1
+ # Changelog
2
+
3
+ All notable changes to pyjab-mcp are documented here, following
4
+ [Keep a Changelog](https://keepachangelog.com/) and [Semantic Versioning](https://semver.org/).
5
+
6
+ Releases are immutable: a published version is never edited. Anything still in flight is
7
+ under **Unreleased** until it is tagged.
8
+
9
+ ## Unreleased
10
+
11
+ Nothing yet. This repository has not shipped anything since 0.1.0.
12
+
13
+ ## [0.1.0] - 2026-10-10
14
+
15
+ **The first release.** 15 MCP tools that let an agent read and drive a Java desktop
16
+ application through the JVM's own accessibility tree: windows, a bounded snapshot with
17
+ handles, element search and inspection, text, tables, clicking, typing, selection, waiting
18
+ and screenshots — with every action verified against the element the snapshot recorded before
19
+ anything is clicked or typed into.
20
+
21
+ Requires Windows, a JDK with Java Access Bridge enabled, and `pyjab>=1.10.0`. The one tool
22
+ that cannot work is `java_send_keys`: pyjab has no public API for a key sequence, so it
23
+ reports that requirement and the way round it rather than pretending `send_text` can express
24
+ `alt+y` (tracked as gaozhao1989/pyjab#168).
25
+
26
+ ### Added
27
+
28
+ * **M9: the release pipeline, and the registry validator that caught a real mistake.**
29
+ `.github/workflows/release.yml` is tag-driven: `verify` (the version guard, the suite, every
30
+ guard, a real build and `twine check`) runs before anything is uploaded; then PyPI via OIDC
31
+ trusted publishing with no token in the repository, then the MCP Registry, then the GitHub
32
+ release with the changelog section as its notes. `workflow_dispatch` rehearses the whole
33
+ verify job without publishing.
34
+
35
+ `tools/check_release_version.py` refuses a release whose tag, package version, `server.json`
36
+ and changelog do not agree — the four places that name a release, checked before the one
37
+ action that cannot be undone. It also writes the release notes, so the notes and the check
38
+ cannot disagree about what a release contains.
39
+
40
+ **The registry's validator rejected the first `server.json`, and it was right.** The published
41
+ JSON schema at `2025-07-09` spells the package fields `registry_type`/`runtime_hint` and
42
+ mentions no other version; the registry API required `registryType`, and treated the
43
+ deprecated `$schema` as a failure to publish. Had this surfaced during a real release it would
44
+ have been discovered *after* PyPI had the package, which cannot be undone. The file now follows
45
+ the current schema (`2025-12-11`), the `registry` job runs `mcp-publisher validate` before
46
+ `publish`, and `PYJAB_MCP_REGISTRY=1` runs the same validator from the test suite. The static
47
+ schema, a published changelog and memory all disagreed with the authority; only the authority
48
+ was right.
49
+
50
+ `docs/RELEASING.md` has the procedure and the two things that must exist before the first tag:
51
+ a PyPI pending publisher for `pyjab-mcp` (workflow `release.yml`, environment `pypi`) and the
52
+ registry's GitHub namespace verification.
53
+
54
+ * **pyjab 1.9.0 is the floor, and M2 is built on it.** The release shipped the tree walk
55
+ (`JABElement.walk` → `JABTree` with a truncation flag) and `JABElement.as_record()`, which
56
+ is what the precheck called R3 and what `java_snapshot` needed. The pin moved to
57
+ `pyjab>=1.9.0` in the same change that started using them, and each API's `released_in` in
58
+ `tools/check_pyjab_api_surface.py` was verified against the 1.9.0 wheel and the 1.8.0 sdist
59
+ rather than remembered.
60
+
61
+ * **`java_snapshot`: a bounded, handle-carrying view of the tree.** `depth`, `max_nodes` and
62
+ `only_interactive` bound both the walk and the view; when either is cut short the reply
63
+ says so and names the way to narrow it, because a truncated view that does not admit it is
64
+ indistinguishable from a window with less in it. The default budget is asserted under
65
+ 4,000 estimated tokens in the tests — a release criterion, not a hope.
66
+
67
+ * **Element handles that hold no JAB references (D2).** A handle is an opaque id mapped to
68
+ `{session, locator, expected identity}`; nothing is held between calls, the element is
69
+ re-found with `find_element_by_xpath`, checked against what the snapshot recorded (role,
70
+ name, position among siblings, bounds), and released immediately — in a `finally`, because
71
+ the check runs between taking the reference and giving it back. The reference-counting fake
72
+ in `tests/_fakedriver.py` fails the tests if either half of that is wrong.
73
+
74
+ Two consequences, both deliberate: eviction releases nothing (there is nothing to release,
75
+ which is the whole argument for this design over a table of live references), and an element
76
+ whose position the application does not report gets **no handle** rather than one that might
77
+ resolve to something else.
78
+
79
+ * **`java_list_windows` works, through the documented `pyjab-inspect windows --json`.** pyjab
80
+ 1.9.0 has the function this needs (`inspector.bridge_without_window`) and does not publish
81
+ it — it is not in that module's `__all__`. The CLI built on it *is* public, so the listing
82
+ runs through that, in a subprocess, and R1 stays recorded. A cost is stated rather than
83
+ hidden: a listing costs a process, not a bridge call.
84
+
85
+ * **The pyjab drift mechanism (AGENTS.md §1a).** `tools/check_pyjab_changelog.py` fails while
86
+ a pyjab changelog line marked `Affects: pyjab-mcp` has no entry in `acknowledged.yaml`, and
87
+ fails for an entry whose marker has gone — so the answer cannot outlive the question.
88
+ `tests/contract/test_pyjab_contract.py` pins the three shapes this project actually reads
89
+ (the walk, `as_record()`'s fields, the pump), and `.github/workflows/pyjab-drift.yml` runs
90
+ the whole suite against `pyjab@master` weekly on `windows-latest`, which catches drift
91
+ before a release rather than after. `acknowledged.yaml` records the four pre-convention
92
+ commits as unenforced context, because calling them acknowledgements would be a claim the
93
+ check cannot verify.
94
+
95
+
96
+ * **Guards — five checks that make the project's rules mechanical instead of remembered.**
97
+ Modelled on pyjab's `tools/check_*.py`, and on its reasoning: a rule kept by hand is kept
98
+ until somebody is in a hurry. Each prints a report, exits non-zero on a violation, and is
99
+ pinned by `tests/test_guards.py`, which proves it **can** fail — a checker whose failure
100
+ path has never run will not run the day it matters.
101
+
102
+ * `tools/check_pyjab_api_surface.py` — every pyjab name this package touches must be
103
+ recorded in `SURFACE` with its status and the release that has it. An unrecorded
104
+ touchpoint, a private name, a stale entry, a public entry with no version, and above all
105
+ **an import of an unreleased API** each fail the build. This is the M1 precheck's lesson
106
+ (the checkout is not the wheel) made a build failure. It found three things on its first
107
+ run, one of which was this project reading `pyjab.__version__`: the version now comes
108
+ from distribution metadata, because the dunder was a touchpoint no rule allowed.
109
+ * `tools/check_documented_surface.py` — the tool table and the module map in
110
+ `docs/ARCHITECTURE.md` must match what `create_server()` registers and what is on disk.
111
+ The page gained a module map and a `ships` column so that both directions are checkable:
112
+ a tool documented as available but not registered, one registered but documented as a
113
+ later milestone, or a module missing from the map all fail.
114
+ * `tools/check_local_only_files.py` — one list of local-only paths checked against both
115
+ mechanisms that keep them out, with `git check-ignore` as the behaviour rather than
116
+ `.gitignore` as the claim.
117
+ * `tools/check_dist_contents.py` — the artifact rather than the rules: a built sdist must
118
+ contain no local-only file and must contain the package, the tests and the guards; the
119
+ wheel must contain only the package, no stale `build/` copies, and an `entry_points.txt`
120
+ naming the console script.
121
+ * `tools/check_pyjab_readiness.py` — the wait for a pyjab release, made re-checkable. It
122
+ asks the installed pyjab the same questions the code asks, reports `READY` /
123
+ `NOT READY` per requirement, and exits non-zero under `--require` while something is
124
+ missing. `UNKNOWN` counts as not ready: "I could not measure it" is not "it is there".
125
+
126
+ * **A `guards` job and a `package` job in CI.** The guards run as separate steps so a
127
+ failure names the rule it broke; the package job builds the sdist and wheel, runs
128
+ `twine check`, checks the artifact with `check_dist_contents.py`, and installs the wheel
129
+ into a clean environment to prove the console script reaches real code. `build` and
130
+ `twine` are declared in `[project.optional-dependencies].dev` rather than installed in a
131
+ workflow step, which is how pyjab's own workflows drifted apart once.
132
+
133
+ * **`java_diagnostics` now reports a fourth pyjab API fact, `non-destructive detach`.** The
134
+ R2 gap was recorded in prose and then in `REQUIREMENTS`; the report now also measures it,
135
+ so "what is pyjab still missing" has one answer in one place.
136
+
137
+ * **The M1 precheck, run against the installed wheel rather than the sibling checkout.**
138
+ Plan §11 gated M1 on answering "is pyjab's public API enough?", and the answer needed
139
+ care: the sibling checkout is 62 commits past the `v1.8.0` tag, so `JABElement.walk`,
140
+ `JABTree`, `pyjab.inspector` and `Win32Utils.java_process_image_paths` all *appear* to
141
+ exist while **none of them is in any released version**. The audit, the two gaps it
142
+ found, and what each blocks are recorded in `AGENTS.md` §6 and encoded as
143
+ `REQUIREMENTS` in `src/pyjab_mcp/backend.py` so the documentation and the message a user
144
+ sees cannot drift apart.
145
+
146
+ * **`BridgeThread` (`src/pyjab_mcp/bridge.py`) — one thread owns the bridge.** Java Access
147
+ Bridge delivers accessibility events to the thread that called `Windows_run()`, and that
148
+ thread must service its own message queue; a pump on another thread fails by never seeing
149
+ a window that opens later, with no exception. The class constructs the backend on its own
150
+ thread, pumps while idle, drains a work queue, and hands callers a `Future` (or an
151
+ awaitable, so the event loop is never blocked by a cross-process call). A backend that
152
+ cannot be opened is reported as a state: queued work is released with the reason and
153
+ later calls fail fast instead of hanging. `close()` reports whether the thread actually
154
+ joined, because a thread blocked inside a JAB call cannot be cancelled.
155
+
156
+ * **`tests/_fakebackend.py`, and the L1 suite around it.** The fake records the thread ident
157
+ of every call and raises when it is not the owner — the only way to test the
158
+ thread-affinity rule without a Windows desktop — and it also raises on use after close,
159
+ so a test that leaks a call fails here rather than on a user's machine.
160
+
161
+ * **`java_diagnostics`.** Reports the platform, interpreter, installed pyjab, which of its
162
+ public APIs are present, whether Java Access Bridge is enabled, where the bridge DLL is,
163
+ and the state of the bridge thread. Every row carries a status, and a row that could not
164
+ be measured reads `UNAVAILABLE (reason)` — never `0`, never "missing". It never writes to
165
+ the environment: in particular it does not construct pyjab's `Service`, whose constructor
166
+ writes `~/.accessibility.properties`.
167
+
168
+ * **`java_list_windows` and `java_attach_window`.** Wired end to end and exercised by tests
169
+ over a real stdio handshake. On released pyjab they report the missing public API (R1) as
170
+ an actionable error rather than reaching for a private one; `java_attach_window` holds no
171
+ JAB reference, because pyjab has no non-destructive detach (R2) and holding one would
172
+ either leak a Java object per attach or kill the user's application when the session ends.
173
+
174
+ * **Tests for the console script over the real stdio transport.** It catches the seam
175
+ between the package and the client — a module that cannot be executed, a crash before the
176
+ first request, or a stray `print` corrupting the protocol stream on stdout.
177
+
178
+ * **Local agent configuration, deliberately not published.** `.agents/skills/` holds four
179
+ skills that reference pyjab (the dependency rules and the R1/R2/R3 gaps, the
180
+ bridge-threading procedure, the test layers, and this machine's setup). They take effect
181
+ from the working tree without being versioned; `.gitignore` keeps them out of git and
182
+ `MANIFEST.in` prunes them from the sdist.
183
+
184
+
185
+ * **L3 in CI: a real window, on a real desktop, on a schedule.** `tests/e2e/` is skipped
186
+ unless `PYJAB_MCP_E2E=1`; `.github/workflows/windows-gui.yml` sets it on a hosted
187
+ `windows-latest` runner, which has an interactive desktop rather than being a service in
188
+ session 0. The application is pyjab's own test app, compiled from the sdist of the
189
+ *installed* pyjab — one version, both sides — and fetched by `tools/fetch_test_app.py`,
190
+ which is a script rather than shell in a workflow for the usual reason.
191
+
192
+ Three things it says, in order of how much it would hurt to get wrong: a snapshot of a real
193
+ accessibility tree contains the controls the application builds; a handle resolves and
194
+ verifies against the real element it was taken from; and **a window that opens after the
195
+ executor has been idle is still seen** — the D3 trap, which no L1 test can catch because
196
+ the fake would have to be wrong in the same way the real thing is.
197
+
198
+ * **`java_diagnostics` checked on JDK 8, 17 and 21.** The same workflow runs a matrix of the
199
+ three, with `tools/verify_diagnostics_jdk.py` asserting not just that a bridge DLL was found
200
+ but that it is the one **paired with that JDK** (JDK 8 keeps it under `jre\bin`, 11+ under
201
+ `bin`). A mismatched pair fails as "pyjab cannot see my window", which is the hardest
202
+ failure in this project to tell apart from a bridge that is simply not enabled.
203
+
204
+
205
+ * **M3: `java_find`, `java_get_element` and `java_get_text`.** A snapshot shows the shape of a
206
+ window; these ask questions about it.
207
+
208
+ `java_find` takes exactly one selector — `name`, `role`, `description`, `states` or `xpath`
209
+ (so substrings via pyjab's `contains()`) — and returns a handle per match. A match is only
210
+ given a handle if a locator finds *it* again: the candidates are tried in order, most
211
+ survivable first, and each is resolved and compared before it is kept. A match that no
212
+ locator can pin down is listed **without** a handle, because D2 is explicit that a handle
213
+ which might point at something else is worse than no handle. Every element the search hands
214
+ over is released, including the ones past the result limit — the shape of the leak pyjab's
215
+ own walk had to be fixed for.
216
+
217
+ `java_get_element` reports one element in full and says whether it is still the element it
218
+ was; `java_get_text` reads its text, slices it client-side (pyjab's only public text API
219
+ returns the whole string), and reports how much of the whole it carries. An element with no
220
+ text interface says so, which is a different answer from an empty string and is reported as
221
+ one.
222
+
223
+
224
+ * **M4: `java_get_table`.** A Java table read through the JVM's own `AccessibleTable`
225
+ interface — row and column counts, then cell text — which is the capability a platform
226
+ bridge does not carry at all.
227
+
228
+ It never returns the whole table: a page is bounded by `row_start`, `row_count` and a
229
+ `max_cells` budget, and the reply says what it left out and how to ask for it. Three cell
230
+ states are kept apart all the way to the reply, because collapsing them would invent data:
231
+ text, `-` for a cell with no Accessible Text interface, and `?` for a cell whose read
232
+ raised, with a count — "this cell has no text" and "I could not read this cell" are
233
+ different answers (AGENTS.md §2.4). Every cell is released as it is read, one at a time; a
234
+ 200-row table read without that is 200 Java objects left behind.
235
+
236
+ Writing it turned up two bugs in itself, both caught by the tests it came with: the page
237
+ size and the table's height shared a name, so asking for one row returned the whole table,
238
+ and the per-cell budget check was dead code because the row count had already been reduced
239
+ to fit it.
240
+
241
+
242
+ * **M5: the actions — `java_click`, `java_set_text`, `java_select`, `java_fill_form`.** Each
243
+ one verifies the element and then acts **on the element it verified**, inside one element
244
+ lifetime, so there is no window between the check and the action in which the tree could
245
+ move. When the element has changed, nothing happens and the difference is reported: the
246
+ click that would have landed on whatever moved into the position is a message instead.
247
+
248
+ `java_click` maps left/double/right onto pyjab's accessibility action, mouse
249
+ double-click and right-click, with `simulate=True` for real input where the action is
250
+ ignored — the trade pyjab documents (window comes to the front, needs real geometry, cannot
251
+ work from a service session). `java_set_text` waits for the field to report the value, so a
252
+ following read sees what was typed. `java_select` picks its shape from the arguments: a
253
+ text option for a combo box, list or tab, or a row or cell for a table, which goes through
254
+ the table's accessible selection because that is the path applications respond to.
255
+ `java_fill_form` is one call for several fields, and it does **not** stop at the first
256
+ failure: a half-filled form with a report is worth more than one without.
257
+
258
+ * **`java_send_keys` is registered and answers with the requirement it is waiting on (R4).**
259
+ Every key helper pyjab has is private (`Win32Utils._press_key`, `_send_keys`, ...) and
260
+ `send_text` types text rather than a modifier chord, so `alt+y` has no public
261
+ implementation. Driving the private helpers would mean depending on names nobody promised,
262
+ in the one area where a wrong key does something irreversible. The reply names the
263
+ requirement **and** the way round it: `java_set_text` for text, or the menu item that
264
+ exposes the same action, found with `java_find(role="menu item")` and clicked.
265
+
266
+
267
+ * **M8: client setup and the boundary of what this can do.** `docs/CLIENTS.md` has the
268
+ configuration for Claude Desktop, Claude Code, Cursor and VS Code — the file each one reads,
269
+ the JSON shape, and the two calls to make first. It leads with the failure that accounts for
270
+ most setups: a client starts the server without your `PATH`, so a virtual environment's
271
+ `pyjab-mcp` is "not found" until the absolute path is used.
272
+
273
+ A README section states what this **cannot** do, because that is the half people discover
274
+ the hard way: canvas-drawn interfaces (no accessible children at all), Java inside a browser,
275
+ applets in a plugin host, non-Java applications, and anything pixel-based. `java_screenshot`
276
+ will show a picture of a canvas; a picture is not something you can act on, and the section
277
+ says so.
278
+
279
+ **`server.json`** for the MCP Registry, with a test that checks it against the fields the
280
+ registry's schema actually requires — it uses snake_case (`registry_type`, `runtime_hint`),
281
+ which is not what an earlier draft of that schema said, so it was read rather than
282
+ remembered — and that the version it names is the release the package builds. A registry
283
+ entry pointing at a version that does not exist is worse than no entry.
284
+
285
+ A **Support** section, as `docs/PLAN.md` F2/F5 planned: free support through GitHub issues,
286
+ commercial and enterprise support through the address this package already publishes in its
287
+ metadata. The open-source package stays complete — no licence key, no quota, nothing held
288
+ back — which the section says outright.
289
+
290
+ ### Changed
291
+
292
+ * **The pin is `pyjab>=1.10.0`, and R1 and R2 are retired.** 1.10.0 added
293
+ `pyjab.list_java_windows()` — dicts with `hwnd`, `title` and `pid` — and
294
+ `JABDriver.detach()`, which releases the root element's JAB reference and forgets the window
295
+ **without terminating its process**. Those were the two gaps this project waited through M1
296
+ and M2 for: the listing no longer needs pyjab's CLI in a subprocess, and a session can be
297
+ released without killing the application it was observing.
298
+
299
+ `PyjabBackend.close()` now detaches every window instead of dropping the dict, and attaching
300
+ a window that is already bound releases the earlier binding rather than holding a second
301
+ context for it. The CLI listing stays as the fallback for an install below the pin — a floor
302
+ can be worked around — and nothing supported reaches it.
303
+
304
+ **1.10.0 was published while this project's CI was running.** The readiness check reported it
305
+ `READY` and a test that asserted the previous world failed in the same run; both were right.
306
+ It is the drift machinery doing what it was built for, and the reason the pin, the registry
307
+ and the readiness check are all verified rather than remembered: each `released_in` here was
308
+ checked against the installed wheel, and the 1.8.0 and 1.9.0 sdists for what came earlier.
309
+
310
+ * **`java_diagnostics` reports a Windows-only API call off Windows as unavailable**, rather
311
+ than letting pyjab's descriptive `ImportError` cross the tool boundary: the library is
312
+ importable everywhere and only *runs* on Windows, and that is an environment answer.
313
+
314
+
315
+ * **D2's verification is split into what blocks and what is reported, because a real run showed
316
+ why it has to be.** The first end-to-end run through the tools set a text field to `"hello"`,
317
+ then could not read it back: the edit had *renamed* the element. `JTextField("1122233455")`
318
+ with no `setName` reports the text as its accessible name — Swing's
319
+ `AccessibleJTextComponent.getName()` falls back to the content — and D2 as written compared
320
+ the name, so an edit invalidated the handle that made it.
321
+
322
+ Blocking now: role, position among siblings, and bounds when both sides report them.
323
+ Reported: the name, **for the four roles whose name is their content** (`text`,
324
+ `password text`, `label`, `table cell`). For every other role a name change still blocks,
325
+ because a renamed button at the same position is a different button — which is the case the
326
+ check exists for. The reply says which of the two happened.
327
+
328
+ * **A handle from `java_find` is now built from a walk path rather than from a name.**
329
+ Same evidence, other half: a locator containing `[@name='1122233455']` stops resolving the
330
+ moment the field's content changes, so the handle could not read back what it had written.
331
+ Search results are placed against one bounded walk of the window and given the same
332
+ position-based locators a snapshot produces; only when the walk cannot place an element does
333
+ it fall back to a name locator, which resolves now but may not survive the next edit. One
334
+ walk serves every result in the call.
335
+
336
+
337
+ * **R3 is retired, R1 and R2 are restated against 1.9.0** (`AGENTS.md` §6, and the
338
+ `REQUIREMENTS` tuple in `backend.py` that the tools' error messages come from). A
339
+ requirement that outlives its release is a lie the code tells in its own messages.
340
+
341
+ * **`tools/check_pyjab_readiness.py` distinguishes a release question from a pin question.**
342
+ The tree walk is answered by the pin (`pyjab>=1.9.0` requires the release that has it), not
343
+ by a probe that cannot run off Windows; R1 and R2 are still release questions and still
344
+ report `NOT READY`.
345
+
346
+ * **The API-surface guard follows a driver passed as an argument.** Extracting the walk and
347
+ the re-resolution into functions that take the driver — which is what makes the ownership
348
+ rule testable off Windows — moved ten touchpoints out of the analyzer's reach until they
349
+ were declared in `DRIVER_PARAMETERS`. The guard noticed the moment they changed, which is
350
+ the property that matters: it fails loudly rather than silently checking less.
351
+
352
+
353
+ * **The MCP SDK floor is `mcp>=2.0`.** The 2.x line renamed `FastMCP` to
354
+ `mcp.server.mcpserver.MCPServer` and `mcp.server.fastmcp` now raises an `ImportError`
355
+ explaining the move, so the decorator shape and the import path are both 2.x. `mcp>=1.0.0`
356
+ was a claim the code did not honour.
357
+
358
+ * **`pyjab` stays at `>=1.8.0` for M1, and that is deliberate.** M1's tools do not call the
359
+ tree-walk API; M2 raises the floor to `>=1.9.0` in the same change that first calls it,
360
+ because a floor that permits a version without the API is a lie.
361
+
362
+ * **`README.md` state is now "M1 shipped, blocked on a pyjab release"** rather than
363
+ "pre-alpha skeleton", and the honest boundary is stated: no tool here has yet driven a
364
+ live Java application.
365
+
366
+ * **`backend.probe_public_api()` reads pyjab's version from distribution metadata.** It used
367
+ `pyjab.__version__`, a dunder in somebody else's package — which the API-surface guard
368
+ refused to record, which is the guard doing its job on its first run. The version it
369
+ reports is therefore the *installed* one, which is the question diagnostics is asking.
370
+
371
+ * **`docs/ARCHITECTURE.md` gained a module map and a `ships` column** in the tool table, so
372
+ that both directions of "the page and the code agree" are checkable, and `README.md`
373
+ gained a "Working on it" section listing the guards.
374
+
375
+ ### Fixed
376
+
377
+ * **A real table's cells report no text, and M4 was reading `None` for all of them.** L3
378
+ found it: the table's shape came through (4 columns, ≥5 rows) and every cell read as
379
+ `None`. Swing's `AccessibleJTableCell` puts the cell's *value* in its accessible **name**,
380
+ so `java_get_table` now falls back to the name when there is no AccessibleText — and says
381
+ which cells came from where, because a value should never be presented as something it is
382
+ not. Without this, a whole table of real values would have been reported as unknown.
383
+
384
+ * **The handle path called a method ``JABDriver`` does not have.** `resolve_driver` released
385
+ the re-resolved element with `driver.release_jabelement(element)`; the method belongs to
386
+ **`JABElement`**, and `JABDriver` has never had one. It would have raised `AttributeError`
387
+ inside the `finally` of every resolution on Windows — the first real desktop run was the
388
+ first time that line executed.
389
+
390
+ Two things had to be true for this to survive the L1 suite, and both were: the adapter
391
+ guessed, and `tests/_fakedriver.py` was shaped like the guess, so the fake agreed with the
392
+ mistake instead of with pyjab. The fake now puts the release on the element, where pyjab
393
+ puts it, and `tests/contract/` asserts the API the adapter actually calls — including the
394
+ signature that lets an element release itself.
395
+
396
+ * **The diagnostics job treated "no Java windows" as a failure.** `pyjab-inspect windows`
397
+ exits 1 when JAB sees nothing, and on a bare runner that is the expected answer rather than
398
+ an error — `UNAVAILABLE` is not `0`, and "nothing is there" is a measurement (AGENTS.md
399
+ §2.4). The step reported it and failed; it now reports it and carries on, so the assertion
400
+ behind it actually runs.
401
+
402
+ * **The L3 snapshot walked three levels and the controls are seven deep.** The first real run
403
+ found the frame, the layered panes, the menu bar and "Main panel" — and nothing inside it,
404
+ so the two assertions about a button and a table failed. The test walks 12 now, and says why:
405
+ a snapshot's default depth is a budget for an agent, not a fact about the tree.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021-2026 Gary Gao and 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,32 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CHANGELOG.md
4
+ include pyproject.toml
5
+ # The record of what was done about each marked pyjab change. It ships so that a source
6
+ # checkout can run its own guards -- tools/check_pyjab_changelog.py reads it, and the guard
7
+ # is not much use if the file it checks is missing from the sdist.
8
+ include acknowledged.yaml
9
+ recursive-include src *.py
10
+ recursive-include tests *.py
11
+ # The guards and verification scripts. sdist only, like pyjab's tools/: they are maintainer
12
+ # tools, and a wheel contains only the package. tools/check_dist_contents.py enforces both
13
+ # halves of that sentence.
14
+ recursive-include tools *.py
15
+
16
+ # ARCHITECTURE.md ships; the two notes below do not. Excluded by name rather than by a
17
+ # pattern over docs/, precisely so that adding a third public page is a one-line change
18
+ # rather than a pattern that has to be reasoned about.
19
+ include docs/ARCHITECTURE.md
20
+ #
21
+ # This file exists for the same reason pyjab has one: an sdist is built from the working
22
+ # tree, so .gitignore does not protect it. A note that is ignored by git and not excluded
23
+ # here is published by the next `python -m build`.
24
+ exclude AGENTS.md
25
+ exclude docs/PLAN.md
26
+ exclude docs/DESIGN.md
27
+ exclude docs/RELEASE_READINESS.md
28
+
29
+ # The local agent configuration -- the skills that describe how this checkout is worked
30
+ # on, and the same reasoning as above. Pruned by directory rather than by file, so a new
31
+ # skill cannot be forgotten here.
32
+ prune .agents