@arbiterforge/ca-pi 0.16.4 → 0.16.6

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.
package/README.md CHANGED
@@ -10,7 +10,7 @@ and durable project context. You decide. codeArbiter enforces.
10
10
  <img alt="Claude Code plugin" src="https://img.shields.io/badge/Claude_Code-plugin-d97757">
11
11
  <img alt="Codex plugin" src="https://img.shields.io/badge/OpenAI_Codex-plugin-10a37f">
12
12
  <img alt="Pi Feature Forge preview" src="https://img.shields.io/badge/ca--pi-Feature_Forge_preview-d97757">
13
- <img alt="version 2.24.4" src="https://img.shields.io/badge/version-2.24.4-2b7489">
13
+ <img alt="version 2.24.5" src="https://img.shields.io/badge/version-2.24.5-2b7489">
14
14
  <img alt="core lanes" src="https://img.shields.io/badge/core_lanes-18-555">
15
15
  <img alt="skills" src="https://img.shields.io/badge/skills-22-555">
16
16
  <img alt="agents" src="https://img.shields.io/badge/agents-20-555">
@@ -183,7 +183,7 @@ Replace `<version>` with the numeric suffix from the chosen tag while retaining
183
183
  it is a partial install that cannot author typed HTML specs and plans; use the npm channel for
184
184
  that.
185
185
 
186
- Pi 1.0.0 is the supported host for this release line. Generated aliases use `/ca-*`;
186
+ Pi 1.0.2 is the supported host for this release line. Generated aliases use `/ca-*`;
187
187
  `/skill:ca-*` is the host-native fallback. Every `ca-pi-v*` tag is also published to npm as
188
188
  `npm:@arbiterforge/ca-pi` with provenance (ADR-0029); the pinned Git tag remains the reproducible
189
189
  install. The [Pi runbook](./docs/pi-parity-testing.md) covers isolated install, trust, verification,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arbiterforge/ca-pi",
3
- "version": "0.16.4",
3
+ "version": "0.16.6",
4
4
  "license": "AGPL-3.0-only",
5
5
  "repository": {
6
6
  "type": "git",
@@ -4,6 +4,23 @@ All notable changes to `ca-pi` are documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.16.6] - 2026-10-05
8
+
9
+ ### Fixed
10
+
11
+ - Publish a review whose reply contains non-ASCII text (an em dash, accented or emoji
12
+ characters). The authority adapter hashed the observation payload and producer
13
+ result as ASCII-escaped JSON while the engine hashes raw UTF-8, so such a review
14
+ failed with `OBSERVATION_UNVERIFIED`. Observation digests now use the engine's
15
+ canonical form.
16
+
17
+ ## [0.16.5] - 2026-10-05
18
+
19
+ ### Changed
20
+
21
+ - Promote the tested Pi host window through exact Pi 1.0.2 with a reviewed
22
+ managed-install lock, clearing the three brace-expansion advisories.
23
+
7
24
  ## [0.16.4] - 2026-10-05
8
25
 
9
26
  ### Fixed
@@ -629,7 +629,7 @@ function resolvePythonCommand(platform = process.platform, probe = systemPythonP
629
629
  }
630
630
 
631
631
  // src/compatibility.ts
632
- var SUPPORTED_PI_VERSIONS = /* @__PURE__ */ new Set(["1.0.0"]);
632
+ var SUPPORTED_PI_VERSIONS = /* @__PURE__ */ new Set(["1.0.2"]);
633
633
  var MINIMUM_NODE = [22, 19, 0];
634
634
  var SEMVER_PREFIX = /^(\d+)\.(\d+)\.(\d+)(?:$|[-+])/u;
635
635
  function atLeast(version, minimum) {
@@ -644,7 +644,7 @@ function atLeast(version, minimum) {
644
644
  }
645
645
  function compatibilityDirection(input) {
646
646
  if (!SUPPORTED_PI_VERSIONS.has(input.piVersion)) {
647
- return "codeArbiter requires Pi 1.0.0; install a supported Pi version and run /ca-doctor.";
647
+ return "codeArbiter requires Pi 1.0.2; install a supported Pi version and run /ca-doctor.";
648
648
  }
649
649
  if (!atLeast(input.nodeVersion, MINIMUM_NODE)) {
650
650
  return "codeArbiter requires Node >=22.19.0 for Pi; upgrade Node and run /ca-doctor.";
@@ -2,7 +2,7 @@
2
2
  var define_CODEARBITER_PI_PERMISSION_POLICY_SURFACES_default = { "ca-plan": "planning-write", codearbiter_background_bash: "background-launch" };
3
3
 
4
4
  // <define:__CODEARBITER_PI_SKILL_EXPANSION_FINGERPRINTS__>
5
- var define_CODEARBITER_PI_SKILL_EXPANSION_FINGERPRINTS_default = { "1.0.0": "12632f365440b07d5183cff871d889b796a3c711b6b49df20f95d9bc198d6c51" };
5
+ var define_CODEARBITER_PI_SKILL_EXPANSION_FINGERPRINTS_default = { "1.0.2": "12632f365440b07d5183cff871d889b796a3c711b6b49df20f95d9bc198d6c51" };
6
6
 
7
7
  // <define:__CODEARBITER_PI_TOOL_CLASSES__>
8
8
  var define_CODEARBITER_PI_TOOL_CLASSES_default = { bash: "EXEC", codearbiter_background_bash: "EXEC", codearbiter_dispatch: "EXEC", codearbiter_farm_preview: "EXEC", write: "WRITE", edit: "EDIT", read: "READ" };
@@ -16,7 +16,7 @@ import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL2 } from
16
16
  import { types as utilTypes9 } from "node:util";
17
17
 
18
18
  // src/compatibility.ts
19
- var SUPPORTED_PI_VERSIONS = /* @__PURE__ */ new Set(["1.0.0"]);
19
+ var SUPPORTED_PI_VERSIONS = /* @__PURE__ */ new Set(["1.0.2"]);
20
20
  var MINIMUM_NODE = [22, 19, 0];
21
21
  var SEMVER_PREFIX = /^(\d+)\.(\d+)\.(\d+)(?:$|[-+])/u;
22
22
  function atLeast(version, minimum) {
@@ -31,7 +31,7 @@ function atLeast(version, minimum) {
31
31
  }
32
32
  function compatibilityDirection(input) {
33
33
  if (!SUPPORTED_PI_VERSIONS.has(input.piVersion)) {
34
- return "codeArbiter requires Pi 1.0.0; install a supported Pi version and run /ca-doctor.";
34
+ return "codeArbiter requires Pi 1.0.2; install a supported Pi version and run /ca-doctor.";
35
35
  }
36
36
  if (!atLeast(input.nodeVersion, MINIMUM_NODE)) {
37
37
  return "codeArbiter requires Node >=22.19.0 for Pi; upgrade Node and run /ca-doctor.";
@@ -6881,10 +6881,10 @@ async function collectPiDoctorInput(dependencies) {
6881
6881
  var REMEDIATION = {
6882
6882
  package: "Reinstall ca-pi from the approved pinned Git tag, then restart Pi.",
6883
6883
  trust: "Run /trust in Pi, inspect the project, grant trust only if you accept it, then start a new session.",
6884
- version: "Upgrade Pi to 1.0.0 and Node to >=22.19.0, then restart Pi.",
6884
+ version: "Upgrade Pi to 1.0.2 and Node to >=22.19.0, then restart Pi.",
6885
6885
  python: "Upgrade or install Python 3, then run /ca-doctor again.",
6886
6886
  core: "Reinstall ca-pi to restore the generated shared core, then run /ca-doctor again.",
6887
- commands: "Remove conflicting command owners or run Pi 1.0.0, then restart Pi and run /ca-doctor.",
6887
+ commands: "Remove conflicting command owners or run Pi 1.0.2, then restart Pi and run /ca-doctor.",
6888
6888
  bridge: "Reinstall ca-pi and Python 3, then run /ca-doctor again.",
6889
6889
  child: "Reinstall ca-pi if the hardened child artifact is missing or tampered, then run /ca-doctor again.",
6890
6890
  "ambient-marker": "Remove CODEARBITER_SUBAGENT from the parent environment and restart Pi.",
@@ -6913,7 +6913,7 @@ function diagnosePi(input) {
6913
6913
  const packageHealthy = input.package.declared && input.package.name === "@arbiterforge/ca-pi" && existsSync(input.package.root) && existsSync(input.package.extensionPath) && samePath2(input.package.extensionPath, expectedExtension) && canonicallyInside(input.package.extensionPath, input.package.root);
6914
6914
  const trustHealthy = input.trust.inspected && (!input.trust.required || input.trust.projectTrusted);
6915
6915
  const waitingForTrust = input.trust.required && !input.trust.projectTrusted;
6916
- const versionHealthy = input.runtime.piVersion === "1.0.0" && atLeast(input.runtime.nodeVersion, [22, 19, 0]);
6916
+ const versionHealthy = input.runtime.piVersion === "1.0.2" && atLeast(input.runtime.nodeVersion, [22, 19, 0]);
6917
6917
  const piBelowMinimum = !atLeast(input.runtime.piVersion, [0, 80, 5]);
6918
6918
  const supportedExpansion = input.commands.expansionVerifiedVersions.includes(input.runtime.piVersion);
6919
6919
  const expectedDoctorSkill = resolve9(input.package.root, "skills", "ca-doctor", "SKILL.md");
@@ -7034,7 +7034,7 @@ function diagnosePi(input) {
7034
7034
  {
7035
7035
  id: "active-dispatch",
7036
7036
  state: "degraded",
7037
- message: "Supported Pi 1.0.0 public extension APIs cannot submit this deterministic self-test through the active dispatcher; the wrapper self-test does not exercise active dispatch.",
7037
+ message: "Supported Pi 1.0.2 public extension APIs cannot submit this deterministic self-test through the active dispatcher; the wrapper self-test does not exercise active dispatch.",
7038
7038
  remediation: REMEDIATION["active-dispatch"]
7039
7039
  }
7040
7040
  ];
@@ -10598,7 +10598,7 @@ async function codeArbiterPi(pi) {
10598
10598
  activeTools: pi.getActiveTools(),
10599
10599
  allTools: pi.getAllTools(),
10600
10600
  expansionFingerprints,
10601
- childFingerprint: "86a08a0d5c29aa6693a07065f4ab2d6ad1d43293c65277cee944389886363d67"
10601
+ childFingerprint: "cc75f11e8eb1df2ab70c4ae0d8c510c61fcf178d905db29a734842d8c5905752"
10602
10602
  });
10603
10603
  const wrapperSelfTest = await runPiWrapperSelfTest({
10604
10604
  enabled: enabledForDoctor,
@@ -37,11 +37,11 @@
37
37
  },
38
38
  "context_qualification": {
39
39
  "darwin/amd64": {
40
- "source_commit": "8bcb71a5edb74fa0196b7a844b23b5c1772b2d2c",
40
+ "source_commit": "49aed5b5c4f78d3e8fcf359c2b9e43472068ebb1",
41
41
  "workflow": ".github/workflows/ci.yml",
42
- "run_id": "37278290441",
42
+ "run_id": "37623035385",
43
43
  "binary_sha256": "5670d20116a3c846f539b82e8db49a68509016c3dda4a147126c0d85e63e484a",
44
- "observation_sha256": "ee7dfb6049a9aa6a96a96dc8e7b8c2f38ed9767252809ef3c0e5cda66cbe8495",
44
+ "observation_sha256": "2329112713771cc95c19bad6b0350b0a37eeebf067ab2d9c14fa6c03c2f3121b",
45
45
  "module_sha256": "f4b15732c8d1066be412718753670f3aeedc9ac668447919bb903db2eedace10",
46
46
  "native_cases": [
47
47
  "TestContextBoundedRender",
@@ -53,11 +53,11 @@
53
53
  ]
54
54
  },
55
55
  "darwin/arm64": {
56
- "source_commit": "8bcb71a5edb74fa0196b7a844b23b5c1772b2d2c",
56
+ "source_commit": "49aed5b5c4f78d3e8fcf359c2b9e43472068ebb1",
57
57
  "workflow": ".github/workflows/ci.yml",
58
- "run_id": "37278290441",
58
+ "run_id": "37623035385",
59
59
  "binary_sha256": "dbc911004289993da4d720113906baba12f1afa510894f850a042869c9af918b",
60
- "observation_sha256": "c1b24d33f08be3bc9226755b589ad8b649454686972f5b5dfd23945a034735e8",
60
+ "observation_sha256": "1bb537709ab07417e7d0a1c61527d426bf9e583db7ea939523e88c9e2b7cc97d",
61
61
  "module_sha256": "f4b15732c8d1066be412718753670f3aeedc9ac668447919bb903db2eedace10",
62
62
  "native_cases": [
63
63
  "TestContextBoundedRender",
@@ -69,11 +69,11 @@
69
69
  ]
70
70
  },
71
71
  "linux/amd64": {
72
- "source_commit": "8bcb71a5edb74fa0196b7a844b23b5c1772b2d2c",
72
+ "source_commit": "49aed5b5c4f78d3e8fcf359c2b9e43472068ebb1",
73
73
  "workflow": ".github/workflows/ci.yml",
74
- "run_id": "37278290441",
74
+ "run_id": "37623035385",
75
75
  "binary_sha256": "9e13efce9499ea895a60fe7461a6234e258ac1ad836f1e9272cce7dfc88bb0d4",
76
- "observation_sha256": "13fcb615b208702392ea689a2560e9d307c883e270a774255abf097ff03826fc",
76
+ "observation_sha256": "6f295c8569f441a8bdfc02492b2962f182be62561d6911be7e5819c114aa7a78",
77
77
  "module_sha256": "f4b15732c8d1066be412718753670f3aeedc9ac668447919bb903db2eedace10",
78
78
  "native_cases": [
79
79
  "TestContextBoundedRender",
@@ -85,11 +85,11 @@
85
85
  ]
86
86
  },
87
87
  "linux/arm64": {
88
- "source_commit": "8bcb71a5edb74fa0196b7a844b23b5c1772b2d2c",
88
+ "source_commit": "49aed5b5c4f78d3e8fcf359c2b9e43472068ebb1",
89
89
  "workflow": ".github/workflows/ci.yml",
90
- "run_id": "37278290441",
90
+ "run_id": "37623035385",
91
91
  "binary_sha256": "e527ac09363a15c2c70cfe92b1a051ee645c3ce528cafccdfc5349e16bcf34e8",
92
- "observation_sha256": "47afbbf8cfb3a2cc41c2234e1f4ac250aa7e244d5cbfcac3eb61299c85209f4a",
92
+ "observation_sha256": "7b0b251ac87d6ae31c1d9fe6622f6f316474ccde2ebbce153e380e296cb02097",
93
93
  "module_sha256": "f4b15732c8d1066be412718753670f3aeedc9ac668447919bb903db2eedace10",
94
94
  "native_cases": [
95
95
  "TestContextBoundedRender",
@@ -101,11 +101,11 @@
101
101
  ]
102
102
  },
103
103
  "windows/amd64": {
104
- "source_commit": "8bcb71a5edb74fa0196b7a844b23b5c1772b2d2c",
104
+ "source_commit": "49aed5b5c4f78d3e8fcf359c2b9e43472068ebb1",
105
105
  "workflow": ".github/workflows/ci.yml",
106
- "run_id": "37278290441",
106
+ "run_id": "37623035385",
107
107
  "binary_sha256": "c545c16d9aad8968e6616a5749500ee56e7907bab97fc9aefa7e02204162e2cb",
108
- "observation_sha256": "0664828bb79c53a36b351fe971a0dad1a17d7e0b29b92b207d49487b270103c5",
108
+ "observation_sha256": "531460c5014621590d9e5afd3c455cb66c88e5d32a137b8c8c92559c29a8523e",
109
109
  "module_sha256": "f4b15732c8d1066be412718753670f3aeedc9ac668447919bb903db2eedace10",
110
110
  "native_cases": [
111
111
  "TestContextBoundedRender",
@@ -117,11 +117,11 @@
117
117
  ]
118
118
  },
119
119
  "windows/arm64": {
120
- "source_commit": "8bcb71a5edb74fa0196b7a844b23b5c1772b2d2c",
120
+ "source_commit": "49aed5b5c4f78d3e8fcf359c2b9e43472068ebb1",
121
121
  "workflow": ".github/workflows/ci.yml",
122
- "run_id": "37278290441",
122
+ "run_id": "37623035385",
123
123
  "binary_sha256": "0bc2c45ab2eff641ae2f493016f2a0828d2757c9670be645b2c7366d3a86c9f3",
124
- "observation_sha256": "2b662148f2fac408ec6d81cef69e4e54edda2163f2577ecb8a0aafdabe80a544",
124
+ "observation_sha256": "020ff24e68e25f62d65151c8d476a3abb8a2aba5bb01fc0558e526c777461bf3",
125
125
  "module_sha256": "f4b15732c8d1066be412718753670f3aeedc9ac668447919bb903db2eedace10",
126
126
  "native_cases": [
127
127
  "TestContextBoundedRender",
@@ -141,7 +141,7 @@ def _canonical(value: Any) -> bytes:
141
141
  raise AuthorityError("INVALID_AUTHORITY_STATE", "state is not finite JSON") from exc
142
142
 
143
143
 
144
- def _native_context_canonical(value: Any) -> bytes:
144
+ def _native_context_canonical(value: Any, code: str = "INVALID_EVIDENCE_CONTEXT") -> bytes:
145
145
  """Match the artifact engine's safe-integer, UTF-16-key-order JSON bytes."""
146
146
  def ordered(item: Any, depth: int) -> Any:
147
147
  if depth > 64:
@@ -172,7 +172,13 @@ def _native_context_canonical(value: Any) -> bytes:
172
172
  raise ValueError("context exceeds native byte limit")
173
173
  return raw
174
174
  except (TypeError, ValueError, UnicodeError, RecursionError) as exc:
175
- raise AuthorityError("INVALID_EVIDENCE_CONTEXT", "context is outside native canonical JSON") from exc
175
+ raise AuthorityError(code, "value is outside native canonical JSON") from exc
176
+
177
+
178
+ def _engine_digest(value: Any) -> str:
179
+ """Digest a value exactly as the engine's canonical.Hash does: raw UTF-8,
180
+ never ASCII-escaped, so non-ASCII text binds to the same digest."""
181
+ return _digest(_native_context_canonical(value, "INVALID_AUTHORITY_STATE"))
176
182
 
177
183
 
178
184
  def _digest(data: bytes) -> str:
@@ -2106,11 +2112,11 @@ def _closed_observation(
2106
2112
  "subject": request["context"]["subject"],
2107
2113
  "context_ref": request["context_ref"],
2108
2114
  "context_sha256": request["context_sha256"],
2109
- "payload_sha256": _digest(_canonical(payload)),
2115
+ "payload_sha256": _engine_digest(payload),
2110
2116
  "producer_profile": producer_profile,
2111
2117
  "producer_run_id": producer_run_id,
2112
2118
  "producer_result": producer_result,
2113
- "producer_result_sha256": _digest(_canonical(producer_result)),
2119
+ "producer_result_sha256": _engine_digest(producer_result),
2114
2120
  }
2115
2121
 
2116
2122
 
@@ -2163,11 +2169,11 @@ def capture_user_prompt(
2163
2169
  "subject": subject,
2164
2170
  "context_ref": context_ref,
2165
2171
  "context_sha256": context_sha256,
2166
- "payload_sha256": _digest(_canonical(event.get("payload"))),
2172
+ "payload_sha256": _engine_digest(event.get("payload")),
2167
2173
  "producer_profile": "host-user-prompt/0.1.0",
2168
2174
  "producer_run_id": producer_run_id,
2169
2175
  "producer_result": producer_result,
2170
- "producer_result_sha256": _digest(_canonical(producer_result)),
2176
+ "producer_result_sha256": _engine_digest(producer_result),
2171
2177
  }
2172
2178
  observation_raw = _canonical(observation)
2173
2179
  observation_sha256 = _digest(observation_raw)
@@ -2213,12 +2219,12 @@ def _context_preview_context(root: Path, client: Any, preview: dict[str, Any], p
2213
2219
  "record_id": "CONTEXT-" + preview["document_id"].upper(),
2214
2220
  "normative_sha256": binding}
2215
2221
  if (
2216
- _digest(raw) != context_hash or raw != _canonical(context)
2222
+ _digest(raw) != context_hash or raw != _native_context_canonical(context, "INVALID_CONTEXT_AUTHORITY")
2217
2223
  or context.get("activity") != CONTEXT_ROUTE
2218
2224
  or context.get("preview") != preview
2219
2225
  or context.get("subject") != subject
2220
2226
  or context.get("record") != {"preview_binding_sha256": binding}
2221
- or context.get("record_sha256") != _digest(_canonical(context.get("record")))
2227
+ or context.get("record_sha256") != _engine_digest(context.get("record"))
2222
2228
  or context.get("input_sha256") != binding
2223
2229
  or context.get("prompt_sha256") != _digest(prompt.encode("utf-8"))
2224
2230
  or result.get("subject") != subject
@@ -2302,9 +2308,9 @@ def capture_context_preview(root: str | Path, client: Any, prompt: str, *, host:
2302
2308
  observation = {
2303
2309
  "format": OBSERVATION_FORMAT, "kind": CONTEXT_ROUTE,
2304
2310
  "subject": context["subject"], "context_ref": context_ref,
2305
- "context_sha256": context_hash, "payload_sha256": _digest(_canonical(payload)),
2311
+ "context_sha256": context_hash, "payload_sha256": _engine_digest(payload),
2306
2312
  "producer_profile": "host-user-context-preview/0.1.0", "producer_run_id": producer_run_id,
2307
- "producer_result": producer_result, "producer_result_sha256": _digest(_canonical(producer_result)),
2313
+ "producer_result": producer_result, "producer_result_sha256": _engine_digest(producer_result),
2308
2314
  }
2309
2315
  observation_raw = _canonical(observation)
2310
2316
  observation_hash = _digest(observation_raw)
@@ -11,7 +11,7 @@ import hostapi # noqa: E402
11
11
  class PiHost(hostapi.Host):
12
12
  name = "pi"
13
13
  adapter_name = "@arbiterforge/ca-pi"
14
- adapter_version = "0.16.4"
14
+ adapter_version = "0.16.6"
15
15
  update_target = "ca-pi"
16
16
  update_tag_prefix = "ca-pi-v"
17
17
  update_command = "pi update npm:@arbiterforge/ca-pi"
@@ -313,7 +313,7 @@ class Host:
313
313
 
314
314
  name = "claude"
315
315
  adapter_name = "ca"
316
- adapter_version = "2.24.4"
316
+ adapter_version = "2.24.5"
317
317
 
318
318
  # Update-notifier descriptor. Each independently versioned host overrides
319
319
  # these three values in its per-plugin _host.py. Keeping the target,
@@ -87,7 +87,7 @@ file maps those actions to Pi's extension API.
87
87
  operator-launched Pi CLI, imported module, package root, and reported version.
88
88
  It does not prove publisher authenticity. Verify the source separately with
89
89
  `pi list` and `pi config`.
90
- - The supported promotion target is exact Pi 1.0.0. The package is
90
+ - The supported promotion target is exact Pi 1.0.2. The package is
91
91
  published as `npm:@arbiterforge/ca-pi` on every `ca-pi-v*` tag (ADR-0029);
92
92
  the pinned Git tag remains the reproducible install. A Pi-native embedded
93
93
  farm worker is a future spike, not an installed dependency.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arbiterforge/ca-pi",
3
- "version": "0.16.4",
3
+ "version": "0.16.6",
4
4
  "private": true,
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",
@@ -32,7 +32,7 @@ repository binary, mutate an artifact, or silently fall back to Markdown.
32
32
  Pi bash wrapper. The exact shared-core `[H-03]` block is healthy and cannot stage files; execution
33
33
  or a different block is unhealthy. This self-test does not traverse Pi's active dispatcher. Do not
34
34
  rerun or respell it.
35
- 4. Its `active-dispatch` row remains degraded because supported Pi 1.0.0 public extension
35
+ 4. Its `active-dispatch` row remains degraded because supported Pi 1.0.2 public extension
36
36
  APIs cannot submit that deterministic call through the active dispatcher. PI-AC-28 remains blocked
37
37
  until supported-version real-host promotion/CI evidence closes the gap.
38
38
  5. Resolve Python once, run `<plugin-root>/hooks/doctor.py`, and present its mechanical report