@atlassian/chippy 0.0.0-stage → 0.0.1-beta.2

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 (133) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +276 -2
  3. package/bin/chippy.mjs +92 -0
  4. package/bin/pack.mjs +204 -0
  5. package/bin/skill-dir.mjs +15 -0
  6. package/lib/avalanche.js +2601 -0
  7. package/lib/avalanche.js.map +7 -0
  8. package/lib/bootstrap.d.ts +1 -0
  9. package/lib/bootstrap.js +70 -0
  10. package/lib/bootstrap.js.map +7 -0
  11. package/lib/browser.js +15 -0
  12. package/lib/browser.js.map +7 -0
  13. package/lib/extension/avalanche.es.js +5535 -0
  14. package/lib/extension/avalanche.es.js.map +1 -0
  15. package/lib/extension/chippy-page-bridge.js +1953 -0
  16. package/lib/extension/inspect-react.js +216 -0
  17. package/lib/extension/layout-lab.js +1453 -0
  18. package/lib/extension/panel-dom.js +90 -0
  19. package/lib/extension/resolve-probes.js +342 -0
  20. package/lib/mcp-skill/LICENSE +201 -0
  21. package/lib/mcp-skill/README.md +148 -0
  22. package/lib/mcp-skill/SKILL.md +335 -0
  23. package/lib/mcp-skill/browser-scripts/playwright-probes.js +368 -0
  24. package/lib/mcp-skill/browser-scripts/playwright-session.js +114 -0
  25. package/lib/mcp-skill/extension/armed-reload-startup.js +83 -0
  26. package/lib/mcp-skill/extension/avalanche.es.js +5535 -0
  27. package/lib/mcp-skill/extension/avalanche.es.js.map +1 -0
  28. package/lib/mcp-skill/extension/avalanche.iife.js +5536 -0
  29. package/lib/mcp-skill/extension/avalanche.iife.js.map +1 -0
  30. package/lib/mcp-skill/extension/background.js +325 -0
  31. package/lib/mcp-skill/extension/chippy-page-bridge.js +1953 -0
  32. package/lib/mcp-skill/extension/content-script.js +190 -0
  33. package/lib/mcp-skill/extension/devtools-hook-shim.js +85 -0
  34. package/lib/mcp-skill/extension/early-bootstrap.js +49 -0
  35. package/lib/mcp-skill/extension/icon-128.png +0 -0
  36. package/lib/mcp-skill/extension/icon-16.png +0 -0
  37. package/lib/mcp-skill/extension/icon-32.png +0 -0
  38. package/lib/mcp-skill/extension/icon-48.png +0 -0
  39. package/lib/mcp-skill/extension/inspect-react.js +216 -0
  40. package/lib/mcp-skill/extension/layout-lab.js +1453 -0
  41. package/lib/mcp-skill/extension/logo.svg +14 -0
  42. package/lib/mcp-skill/extension/manifest.json +69 -0
  43. package/lib/mcp-skill/extension/panel-dom.js +90 -0
  44. package/lib/mcp-skill/extension/panel-settings.js +173 -0
  45. package/lib/mcp-skill/extension/popup.html +41 -0
  46. package/lib/mcp-skill/extension/popup.js +80 -0
  47. package/lib/mcp-skill/extension/probe-descriptor.js +241 -0
  48. package/lib/mcp-skill/extension/resolve-probes.js +342 -0
  49. package/lib/mcp-skill/extension/startup-handoff.js +30 -0
  50. package/lib/mcp-skill/package.json +1 -0
  51. package/lib/mcp-skill/references/README.md +56 -0
  52. package/lib/mcp-skill/references/active-stimulus.md +85 -0
  53. package/lib/mcp-skill/references/avalanche-detector.md +731 -0
  54. package/lib/mcp-skill/references/bdd-router.md +310 -0
  55. package/lib/mcp-skill/references/browser-runtime-api.md +193 -0
  56. package/lib/mcp-skill/references/command-output-formats.md +1085 -0
  57. package/lib/mcp-skill/references/command-surface.md +183 -0
  58. package/lib/mcp-skill/references/cynefin.md +99 -0
  59. package/lib/mcp-skill/references/debug-hook.md +161 -0
  60. package/lib/mcp-skill/references/fixing-performance.md +490 -0
  61. package/lib/mcp-skill/references/hook-metrics.md +199 -0
  62. package/lib/mcp-skill/references/hooks-inspection.md +61 -0
  63. package/lib/mcp-skill/references/jira-taxonomy.md +81 -0
  64. package/lib/mcp-skill/references/mcp.md +122 -0
  65. package/lib/mcp-skill/references/mode-a-injection.md +102 -0
  66. package/lib/mcp-skill/references/operating-model.md +128 -0
  67. package/lib/mcp-skill/references/playwright-cli-setup.md +105 -0
  68. package/lib/mcp-skill/references/playwright-probes.md +205 -0
  69. package/lib/mcp-skill/references/procedures.md +545 -0
  70. package/lib/mcp-skill/references/recipes.txt +114 -0
  71. package/lib/mcp-skill/references/stability-render.md +428 -0
  72. package/lib/mcp-skill/references/test-mode-playbook.md +194 -0
  73. package/lib/mcp-skill/references/troubleshooting.md +59 -0
  74. package/lib/mcp-skill/scripts/avalanche-analyze.cjs +495 -0
  75. package/lib/mcp-skill/scripts/build-avalanche-command.js +34 -0
  76. package/lib/mcp-skill/scripts/build-fiber-state-command.js +19 -0
  77. package/lib/mcp-skill/scripts/chippy-helper-server.js +1768 -0
  78. package/lib/mcp-skill/scripts/chippy-mcp.cjs +273 -0
  79. package/lib/mcp-skill/scripts/chippy.sh +1252 -0
  80. package/lib/mcp-skill/scripts/format-probe-result.js +34 -0
  81. package/lib/mcp-skill/scripts/helper-access.js +158 -0
  82. package/lib/mcp-skill/scripts/helper-proxy.js +87 -0
  83. package/lib/mcp-skill/scripts/page-socket-trust.js +81 -0
  84. package/lib/mcp-skill/scripts/probe-annotation-channel.js +116 -0
  85. package/lib/mcp-skill/scripts/search-recipes.js +248 -0
  86. package/lib/mcp-skill/snippets/README.md +89 -0
  87. package/lib/mcp-skill/snippets/count-context-consumers.js +40 -0
  88. package/lib/mcp-skill/snippets/find-context-providers.js +38 -0
  89. package/lib/mcp-skill/snippets/find-legacy-context.js +47 -0
  90. package/lib/mcp-skill/snippets/find-prop.js +83 -0
  91. package/lib/mcp-skill/snippets/hook-metrics-sample.js +69 -0
  92. package/lib/mcp-skill/snippets/hook-metrics.js +60 -0
  93. package/lib/mcp-skill/snippets/hook-tracker.js +198 -0
  94. package/lib/mcp-skill/snippets/stability-nudge.js +101 -0
  95. package/lib/package.json +1 -0
  96. package/lib/plugin.js +119 -0
  97. package/lib/plugin.js.map +7 -0
  98. package/lib/storybook/avalanche-panel-model.js +79 -0
  99. package/lib/storybook/brand.jsx +6 -0
  100. package/lib/storybook/connection-fallback.jsx +47 -0
  101. package/lib/storybook/constants.js +15 -0
  102. package/lib/storybook/context-card.jsx +171 -0
  103. package/lib/storybook/manager-owner.js +42 -0
  104. package/lib/storybook/manager.jsx +425 -0
  105. package/lib/storybook/preset.js +7 -0
  106. package/lib/storybook/preview.js +298 -0
  107. package/lib/storybook/probe-annotation-editor.jsx +52 -0
  108. package/lib/storybook/probe-intent.js +23 -0
  109. package/lib/storybook/setup.js +104 -0
  110. package/lib/types/avalanche/commit-metrics-adapter.d.ts +76 -0
  111. package/lib/types/avalanche/commit-stats-indices.d.ts +41 -0
  112. package/lib/types/avalanche/commit-surface-ownership.d.ts +9 -0
  113. package/lib/types/avalanche/detector.d.ts +183 -0
  114. package/lib/types/avalanche/interaction-capture.d.ts +25 -0
  115. package/lib/types/avalanche/internals.d.ts +189 -0
  116. package/lib/types/avalanche/report-serializer.d.ts +283 -0
  117. package/lib/types/avalanche/reporter.d.ts +610 -0
  118. package/lib/types/chippy-bridge/fiber-source.d.ts +48 -0
  119. package/lib/types/chippy-bridge/react-renderer.d.ts +74 -0
  120. package/lib/types/chippy-bridge/recorded-source.d.ts +44 -0
  121. package/lib/types/chippy-bridge/source-map.d.ts +30 -0
  122. package/lib/types/debug-render/core.d.ts +78 -0
  123. package/lib/types/layout-lab/fiber-boundaries.d.ts +51 -0
  124. package/lib/types/layout-lab/fiber-walk.d.ts +131 -0
  125. package/lib/types/layout-lab/marker-context.d.ts +94 -0
  126. package/lib/types/package/avalanche.d.ts +6 -0
  127. package/lib/types/package/browser.d.ts +10 -0
  128. package/lib/types/package/plugin.d.ts +15 -0
  129. package/lib/types/plugins/atl/commit-metrics-adapter.d.ts +65 -0
  130. package/lib/types/plugins/atl/metrics-slot.d.ts +37 -0
  131. package/lib/types/plugins/index.d.ts +49 -0
  132. package/lib/types/plugins/jira/context-provider.d.ts +36 -0
  133. package/package.json +106 -4
@@ -0,0 +1,1085 @@
1
+ # `chippy.sh` command output formats
2
+
3
+ Exact response shapes for the commands expanded below. Start with the complete [command surface](command-surface.md) when selecting a command.
4
+
5
+ Commands return either plain text lifecycle messages on stderr or JSON on stdout. Successful JSON normally includes `"ok": true`; failures include `"ok": false` and an `error`. Command-specific references own the larger contracts:
6
+
7
+ - [Avalanche detector](avalanche-detector.md) owns capture, commit, render, unmount, action, hook-debug, and offline-analysis shapes.
8
+ - [Stability render](stability-render.md) owns the `fiber-schedule-update` / `stability-nudge` procedure and verdict shapes.
9
+ - [Playwright probes](playwright-probes.md) owns `handoff-to-playwright` and browser-session shapes.
10
+ - [Snippet README](../snippets/README.md) owns `eval-page` inputs and result envelopes.
11
+
12
+ ---
13
+
14
+ ## `start`
15
+
16
+ Starts the helper in the background. Prints to **stderr** only; then echoes health JSON to **stdout**.
17
+
18
+ ```
19
+ # stderr
20
+ Chippy helper started (pid 12345) ← fresh start
21
+ Chippy helper already up at http://… ← was already running
22
+ Failed to start Chippy helper. ← timeout (exit 1)
23
+ ```
24
+
25
+ stdout after stderr line — same shape as `health`:
26
+ ```json
27
+ { "ok": true, "now": "2026-04-16T08:39:49.493Z" }
28
+ ```
29
+
30
+ ---
31
+
32
+ ## `stop`
33
+
34
+ Prints to **stderr** only, no stdout.
35
+
36
+ ```
37
+ Chippy helper stopped
38
+ Chippy helper is not running
39
+ Timed out waiting for Chippy helper to stop ← exit 1
40
+ ```
41
+
42
+ ---
43
+
44
+ ## `health`
45
+
46
+ ```json
47
+ { "ok": true, "now": "2026-04-16T08:39:49.493Z" }
48
+ ```
49
+
50
+ Fields: `ok` (bool), `now` (ISO 8601 timestamp).
51
+
52
+ ---
53
+
54
+ ## `inject-url`
55
+
56
+ Prints a lifecycle warning and the second required asset to stderr:
57
+
58
+ ```text
59
+ Import integration is document-scoped: a full reload/navigation removes Chippy and requires the user or automation to inject it again.
60
+ The classic inject.js installs Chippy only; load http://127.0.0.1:7823/__chippy/avalanche.iife.js after it for bridge and Avalanche commands, or import http://127.0.0.1:7823/__chippy/inject.mjs instead.
61
+ ```
62
+
63
+ Then prints the classic-script URL to stdout, one line:
64
+
65
+ ```
66
+ http://127.0.0.1:7823/__chippy/inject.js
67
+ ```
68
+
69
+ ---
70
+
71
+ ## `recipes <query...> [--limit N]`
72
+
73
+ Searches the local one-record-per-line corpus without contacting the helper. Results are plain-text blocks ordered by descending BM25-style score. The default limit is five; `--limit` accepts integers from 1 through 20.
74
+
75
+ ```text
76
+ recipe: page-boundary-forest
77
+ score: 24.842
78
+ question: How do I read the whole page in large Error and Suspense boundary blocks?
79
+ answer: Run `chippy.sh page-boundaries <clientId> [max]`; ordinary fibers are filtered out while boundary nesting and root containment are preserved.
80
+ done: The boundary-only forest covers every root and reports `truncated: false`.
81
+ guard: If truncated, increase `max`; absence of boundaries is a valid complete result when truncation is false.
82
+ refs: SKILL.md#route-by-question,references/command-output-formats.md
83
+ ```
84
+
85
+ Records are separated by one blank line. `score` is useful only for ordering results from the same query; it is not a confidence value. An empty query, a limit outside 1–20, an empty corpus, or no match exits 1 with a diagnostic on stderr.
86
+
87
+ ---
88
+
89
+ ## `pages [slice]`
90
+
91
+ `slice` = `minimal` (default) | `standard` | `full`
92
+
93
+ ```json
94
+ {
95
+ "ok": true,
96
+ "count": 2,
97
+ "duplicatePageWarnings": null,
98
+ "pages": [ /* array of client objects */ ]
99
+ }
100
+ ```
101
+
102
+ `duplicatePageWarnings` is `null` when no duplicates, otherwise a map of canonical URL → array of clientIds:
103
+ ```json
104
+ {
105
+ "http://127.0.0.1:6007/iframe.html?id=labs-layoutplayground--default": ["abc-123", "def-456"]
106
+ }
107
+ ```
108
+
109
+ **Client object — fields by slice:**
110
+
111
+ | Field | minimal | standard | full |
112
+ |---|---|---|---|
113
+ | `summary` | ✓ | ✓ | ✓ |
114
+ | `clientId` | ✓ | ✓ | ✓ |
115
+ | `pageRef` | ✓ | ✓ | ✓ |
116
+ | `visibleName` | ✓ | ✓ | ✓ |
117
+ | `isLikelyCurrent` | ✓ | ✓ | ✓ |
118
+ | `connectedAt` | ✓ | ✓ | ✓ |
119
+ | `lastMessageAt` | ✓ | ✓ | ✓ |
120
+ | `lastActivityAt` | ✓ | ✓ | ✓ |
121
+ | `lastActivityReason` | ✓ | ✓ | ✓ |
122
+ | `activity` | ✓ | ✓ | ✓ |
123
+ | `latestSnapshotAt` | ✓ | ✓ | ✓ |
124
+ | `snapshotAgeMs` | ✓ | ✓ | ✓ |
125
+ | `bridgeVersion` | ✓ | ✓ | ✓ |
126
+ | `integration` | ✓ | ✓ | ✓ |
127
+ | `activated` | ✓ | ✓ | ✓ |
128
+ | `probesActive` | ✓ | ✓ | ✓ |
129
+ | `buildMode` | ✓ | ✓ | ✓ |
130
+ | `page` | ✓ | ✓ | ✓ |
131
+ | `latestSnapshot` | ✓ | ✓ | ✓ |
132
+
133
+ **`latestSnapshot.markers` fields by slice:**
134
+
135
+ | Marker field group | minimal | standard | full |
136
+ |---|---|---|---|
137
+ | `position`, `docked`, `intent`, `comment` | ✓ | ✓ | ✓ |
138
+ | `confidence`, `note`, `nearestTestId`, `dom.tag`, `dom.text`, `component.name`, `component.key` | ✓ | ✓ | ✓ |
139
+ | full `dom`, `component.props`, `component.source` | — | ✓ | ✓ |
140
+ | `component.owners` (up to 10), `component.parents` (up to 20), `componentStack`, `component.hookCount` | — | ✓ | ✓ |
141
+ | full `owners` + `parents` (all, unbounded) | — | — | ✓ |
142
+
143
+ **`nearestTestId`** is present on every marker in all slice modes (`string | null`). It is the nearest ancestor—including the probed element—with `data-testid`; it is `null` when none exists.
144
+
145
+ **Probe annotation** is present on every marker in all slice modes. `intent` is `"explain" | "investigate" | "change" | null`; `comment` is always a string (up to 2,000 characters). For P1/P2 these are developer-authored request context attached to the visible target. P3 has `intent: null` and `comment: ""` because it is computed rather than directly annotated.
146
+
147
+ **Marker `"3"` (P3)** is the auto-generated common parent when P1 and P2 are both placed and not docked. It is computed from their fiber return chains, has `position: null` and `docked: false`, and uses the same resolved shape as P1/P2.
148
+
149
+ `page` sub-object:
150
+
151
+ ```json
152
+ { "url": "http://…", "title": "Example app", "storyId": null }
153
+ ```
154
+
155
+ `buildMode`: `"dev" | "production" | "unknown"`
156
+
157
+ `dev` source evidence is either React <=18 `_debugSource` or React 19 `_debugStack`. For React 19, the first snapshot may carry the served-module URL and generated position; the bridge resolves the module's inline or external Source Map v3 payload in the page realm and republishes the authored `fileName`, `lineNumber`, and `columnNumber` with reason `<original-reason>-source-resolved`. For React <=18, the same resolution corrects a `_debugSource` line that a dev-server transform shifted; `fileName` stays the recorded path. `find-fibers` and explicit component descriptions await that resolution. If the module or map is unavailable, the recorded or served position remains the fallback.
158
+
159
+ **`integration`** — identifies who established Chippy on this page:
160
+ ```json
161
+ {
162
+ "kind": "extension | import | storybook",
163
+ "who": "human-readable integration owner",
164
+ "source": "real source file that established the integration",
165
+ "reinjectsOnReload": true,
166
+ "reloadAction": "what happens or what the user must do after reload"
167
+ }
168
+ ```
169
+
170
+ For `kind: "import"`, `reinjectsOnReload` is always `false`: full reload/navigation removes Chippy and the user or automation must run the import again.
171
+
172
+ **`summary`** — human-readable one-liner for quick page identification:
173
+ ```
174
+ <title> | <integration.who> | <url> | activity: <reason> @ <lastActivityAt> | snapshot <N>s ago
175
+ ```
176
+ `null` title/url shown as `(no title)` / `(no url)`. `snapshotAgeMs` is `null` when no snapshot exists yet.
177
+
178
+ **Empty `pages` state:**
179
+
180
+ ```json
181
+ { "ok": true, "count": 0, "hint": "No clients connected. Open a page with the Chippy extension enabled and reload it.", "duplicatePageWarnings": null, "pages": [] }
182
+ ```
183
+
184
+ ## `recent-pages`
185
+
186
+ Returns the ten newest activity records from an in-memory, 30-record history. It is diagnostic history, not proof that a page is still connected.
187
+
188
+ ```json
189
+ {
190
+ "ok": true,
191
+ "count": 2,
192
+ "pages": [
193
+ {
194
+ "clientId": "abc-123",
195
+ "at": "2026-08-03T09:30:00.000Z",
196
+ "reason": "snapshot",
197
+ "detail": null,
198
+ "integration": { "kind": "extension", "who": "Chippy browser extension" },
199
+ "pageLabel": "Layout Playground",
200
+ "title": "Layout Playground",
201
+ "url": "http://127.0.0.1:6007/iframe.html?id=labs-layoutplayground--default",
202
+ "storyId": "labs-layoutplayground--default"
203
+ }
204
+ ]
205
+ }
206
+ ```
207
+
208
+ Empty history:
209
+
210
+ ```json
211
+ { "ok": true, "count": 0, "pages": [] }
212
+ ```
213
+
214
+ ---
215
+
216
+ ## `page <clientId> [slice]`
217
+
218
+ `slice` defaults to `standard`.
219
+
220
+ ```json
221
+ {
222
+ "ok": true,
223
+ "client": { /* single client object — same shape as pages.pages entries */ }
224
+ }
225
+ ```
226
+
227
+ Client not found:
228
+ ```json
229
+ { "ok": false, "error": "Client not found" }
230
+ ```
231
+
232
+ ---
233
+
234
+ ## `snapshot <clientId> [slice]` / `request-snapshot <clientId>`
235
+
236
+ `snapshot` reads the helper's latest stored snapshot. `request-snapshot` asks the connected page to emit a fresh snapshot; read it afterward with `snapshot` or `page <clientId> full`.
237
+
238
+ **`snapshot`:**
239
+ ```json
240
+ {
241
+ "ok": true,
242
+ "targetClientId": "<clientId>",
243
+ "resolvedFrom": null,
244
+ "latestSnapshotAt": "<iso-or-null>",
245
+ "snapshot": { "page": {}, "markers": {} }
246
+ }
247
+ ```
248
+
249
+ **`request-snapshot`:**
250
+ ```json
251
+ { "ok": true, "requested": "<clientId>", "resolvedFrom": null }
252
+ ```
253
+
254
+ Client not found (both):
255
+ ```json
256
+ { "ok": false, "error": "Client not found: <clientId>. Run 'pages' to see connected clients." }
257
+ ```
258
+
259
+ ---
260
+
261
+ ## `wait <clientId> [timeoutSeconds]`
262
+
263
+ Performs one cancellable long poll for a user-attached P1/P2 annotation. `timeoutSeconds` defaults to `300` and accepts integers from `1` through `1800`. An annotation that arrived before the call remains queued in helper memory and returns immediately. Events are consumed oldest-first per connected page.
264
+
265
+ This command does not install, inspect, or modify any agent-host lifecycle hook. Repeated waiting is controlled by the current agent turn and only continues while the user-requested loop remains active.
266
+
267
+ Annotation result:
268
+
269
+ ```json
270
+ {
271
+ "ok": true,
272
+ "status": "annotation",
273
+ "targetClientId": "<clientId>",
274
+ "resolvedFrom": null,
275
+ "timeoutMs": 300000,
276
+ "event": {
277
+ "eventId": "probe-annotation:1",
278
+ "sequence": 1,
279
+ "at": "2026-09-10T07:00:00.000Z",
280
+ "clientId": "<clientId>",
281
+ "reason": "marker-annotation-change",
282
+ "page": { "url": "http://…", "title": "Example app", "storyId": null },
283
+ "integration": { "kind": "extension", "who": "Chippy browser extension" },
284
+ "changedProbeIds": ["1"],
285
+ "annotations": [
286
+ {
287
+ "probeId": "1",
288
+ "intent": "change",
289
+ "comment": "Move this below the summary.",
290
+ "marker": { "position": {}, "docked": false, "intent": "change", "comment": "Move this below the summary.", "resolved": {} }
291
+ }
292
+ ]
293
+ }
294
+ }
295
+ ```
296
+
297
+ `marker` uses the standard snapshot slice. `changedProbeIds` identifies annotations whose intent or comment differs from the preceding snapshot. When a developer re-attaches unchanged text, the event contains the currently annotated direct probes because the page snapshot does not identify the clicked probe separately.
298
+
299
+ Bounded timeout is a successful empty result:
300
+
301
+ ```json
302
+ {
303
+ "ok": true,
304
+ "status": "timeout",
305
+ "targetClientId": "<clientId>",
306
+ "resolvedFrom": null,
307
+ "timeoutMs": 300000,
308
+ "event": null
309
+ }
310
+ ```
311
+
312
+ The in-memory queue retains at most 50 events per page and does not survive a helper restart. Cancelling the HTTP/tool call removes that waiter without consuming the next event.
313
+
314
+ ---
315
+
316
+ ## `activate` / `deactivate` / `ensure-probes` / `clear-probes` / `hide-status`
317
+
318
+ All return the **command envelope**:
319
+
320
+ ```json
321
+ {
322
+ "ok": true,
323
+ "delivered": 1,
324
+ "targetClientId": "<clientId>",
325
+ "command": { "type": "activate" }
326
+ }
327
+ ```
328
+
329
+ `delivered` — integer count of clients the command was actually sent to. `0` means the clientId was not found (no error raised, just not delivered).
330
+
331
+ ---
332
+
333
+ ## `context-ui <clientId> <on|off>`
334
+
335
+ Shows or hides context-provider cards (for example Jira ownership) in the active probe UI for the current connected page, including Storybook marker cards. Enrichment is on by default. The command does not activate Chippy, place probes, mutate the inspected application, or persist across a new page realm.
336
+
337
+ ```json
338
+ {
339
+ "ok": true,
340
+ "delivered": 1,
341
+ "targetClientId": "<clientId>",
342
+ "command": { "type": "set-context-ui", "enabled": false }
343
+ }
344
+ ```
345
+
346
+ ---
347
+
348
+ ## `show-status <clientId> <text>`
349
+
350
+ Same envelope with the `text` field echoed inside `command`:
351
+
352
+ ```json
353
+ {
354
+ "ok": true,
355
+ "delivered": 1,
356
+ "targetClientId": "<clientId>",
357
+ "command": { "type": "show-status", "text": "Working…" }
358
+ }
359
+ ```
360
+
361
+ ---
362
+
363
+ ## `command <type> [clientId]`
364
+
365
+ Generic passthrough. Returns the same command envelope as above with whatever `type` was passed.
366
+
367
+ ---
368
+
369
+ ## `get-computed-style <clientId> <probeId> [parentLevel] [selector]`
370
+
371
+ Parameters:
372
+ - `probeId` — `"1"` or `"2"` (string, matches probe id)
373
+ - `parentLevel` — `0` = probe's target element, `1` = its direct DOM parent, `N` = Nth ancestor (default `0`)
374
+ - `selector` — optional CSS selector applied via `querySelector` within the resolved node
375
+
376
+ Success — `result` is a flat object of every CSS property resolved by `getComputedStyle`:
377
+
378
+ ```json
379
+ {
380
+ "ok": true,
381
+ "targetClientId": "<clientId>",
382
+ "requestId": "<uuid>",
383
+ "result": {
384
+ "display": "flex",
385
+ "flex-direction": "row",
386
+ "align-items": "center",
387
+ "color": "rgb(23, 43, 77)",
388
+ "font-size": "14px",
389
+ "margin-top": "0px"
390
+ }
391
+ }
392
+ ```
393
+
394
+ Timeout (page did not respond within 5 s):
395
+ ```json
396
+ {
397
+ "ok": false,
398
+ "error": "Timeout waiting for computed style result",
399
+ "targetClientId": "<clientId>",
400
+ "requestId": "<uuid>"
401
+ }
402
+ ```
403
+
404
+ Client not found:
405
+ ```json
406
+ { "ok": false, "error": "Client not found" }
407
+ ```
408
+
409
+ ---
410
+
411
+ ## `probe-parents <clientId> <probeId> [max]` / `inspect-fiber-parents <clientId> <fiberId> [max]`
412
+
413
+ Reads the React `fiber.return` parent chain. This is distinct from `_debugOwner` and remains useful on production builds where owner data is absent.
414
+
415
+ ```json
416
+ {
417
+ "ok": true,
418
+ "probeId": "1",
419
+ "fiberId": null,
420
+ "component": { "index": -1, "relation": "self", "name": "Link", "propNames": [], "hookCount": 13, "dom": { "tag": "a", "testId": null, "text": "" } },
421
+ "parents": [
422
+ { "index": 0, "relation": "return", "name": "div", "tag": 5, "propNames": [], "hookCount": 0, "source": null, "dom": { "tag": "div", "testId": "issue-card", "text": "MAGMA-1" } }
423
+ ],
424
+ "targetClientId": "<clientId>",
425
+ "resolvedFrom": null,
426
+ "requestId": "<uuid>"
427
+ }
428
+ ```
429
+
430
+ `max` defaults to 50 and is capped by the page bridge.
431
+
432
+ ## `probe-boundaries <clientId> <probeId> [max]` / `inspect-fiber-boundaries <clientId> <fiberId> [max]`
433
+
434
+ Reads every declarative Error/Suspense boundary on the rendered `fiber.return` path, ordered from the inspected target outward. Unlike the compact snapshot cards, this does not stop after the nearest boundary of each kind.
435
+
436
+ ```json
437
+ {
438
+ "ok": true,
439
+ "result": {
440
+ "ok": true,
441
+ "probeId": "1",
442
+ "count": 2,
443
+ "scannedFibers": 42,
444
+ "truncated": false,
445
+ "items": [
446
+ {
447
+ "fiberId": "fiber:7",
448
+ "kind": "suspense",
449
+ "componentName": "Suspense",
450
+ "detectedBy": "fiber-tag",
451
+ "status": "showing-fallback",
452
+ "source": null,
453
+ "dom": { "tag": "section", "testId": "issue-loading", "text": "Loading…" },
454
+ "distance": 5
455
+ },
456
+ {
457
+ "fiberId": "fiber:8",
458
+ "kind": "error",
459
+ "componentName": "IssueErrorBoundary",
460
+ "detectedBy": "lifecycle",
461
+ "status": "unknown",
462
+ "source": { "fileName": "…/IssueErrorBoundary.tsx", "lineNumber": 18, "columnNumber": 3 },
463
+ "dom": { "tag": "main", "testId": "issue-view", "text": "MAGMA-1" },
464
+ "distance": 11
465
+ }
466
+ ]
467
+ },
468
+ "targetClientId": "<clientId>",
469
+ "resolvedFrom": null,
470
+ "requestId": "<uuid>"
471
+ }
472
+ ```
473
+
474
+ `max` defaults to 100 and caps returned boundaries; the ancestry walk itself has a 400-fiber cycle/pathology ceiling. `truncated: true` means either cap prevented a complete inner→outer chain. `status` is `showing-content` or `showing-fallback` for Suspense based on its current fiber state; generic error boundaries report `unknown` because React has no universal error-state field. `dom` is the first host element rendered below the boundary and may be `null`.
475
+
476
+ `inspect-fiber-boundaries` has the same result shape with `fiberId` instead of `probeId` at the result root.
477
+
478
+ ## `page-boundaries <clientId> [max]`
479
+
480
+ Reads all current React roots and removes every non-boundary component while preserving Error/Suspense containment. Top-level `items` are the boundary forest roots; each node recursively contains boundary-only `children`.
481
+
482
+ ```json
483
+ {
484
+ "ok": true,
485
+ "result": {
486
+ "ok": true,
487
+ "count": 4,
488
+ "scannedFibers": 1832,
489
+ "rootCount": 2,
490
+ "rootsWithBoundaries": 1,
491
+ "truncated": false,
492
+ "items": [
493
+ {
494
+ "fiberId": "fiber:21",
495
+ "kind": "error",
496
+ "componentName": "AppErrorBoundary",
497
+ "detectedBy": "lifecycle",
498
+ "status": "unknown",
499
+ "source": { "fileName": "…/AppErrorBoundary.tsx", "lineNumber": 9, "columnNumber": 1 },
500
+ "dom": { "tag": "div", "testId": "app-shell", "text": "Projects" },
501
+ "fiberDepth": 3,
502
+ "distanceFromParent": null,
503
+ "children": [
504
+ {
505
+ "fiberId": "fiber:22",
506
+ "kind": "suspense",
507
+ "componentName": "Suspense",
508
+ "detectedBy": "fiber-tag",
509
+ "status": "showing-content",
510
+ "source": null,
511
+ "dom": { "tag": "main", "testId": "project-page", "text": "MAGMA" },
512
+ "fiberDepth": 12,
513
+ "distanceFromParent": 9,
514
+ "children": []
515
+ }
516
+ ]
517
+ }
518
+ ]
519
+ },
520
+ "targetClientId": "<clientId>",
521
+ "resolvedFrom": null,
522
+ "requestId": "<uuid>"
523
+ }
524
+ ```
525
+
526
+ `max` defaults to 1000 and is capped at 5000. `count` counts every returned node, including nested children. `distanceFromParent` is the number of omitted-or-surviving fiber edges between declarative boundaries; it is `null` for a forest root. `truncated: false` is the completion signal for a whole-page read.
527
+
528
+ ## `probe-props <clientId> <probeId> [ownerIndex]`
529
+
530
+ Fetches prop values for the probe component or one of its owners. Progressive disclosure: snapshot gives names, this gives values + non-POD traces.
531
+
532
+ Parameters:
533
+ - `probeId` — `"1"` or `"2"`
534
+ - `ownerIndex` — omit for the probe component itself; `0` = immediate owner, `1` = grandparent, etc.
535
+
536
+ Use `probe-owners` for React `_debugOwner` value flow. Then feed the numeric `ownerIndex` back into `probe-props` or `probe-prop-value`. `ownerIndex: 3` means three `_debugOwner` hops from the probe component; it is not the `fiber.return` parent chain. Use `probe-parents` for parent-chain walking.
537
+
538
+ These responses carry hook-count complexity fields next to the prop data:
539
+ - `hookCount` — counted hooks on the target component
540
+ - `hookCountTruncated` — whether counting hit the safety ceiling
541
+ - `hookCountMax` — safety ceiling used while counting
542
+
543
+ Use that as a warning label before diving into local state. Big numbers do not mean broken. They mean dense.
544
+
545
+ Notes:
546
+ - probe coordinates are exact for the landing DOM node
547
+ - owner traversal is deterministic from that exact node
548
+ - names in output are descriptive only, not command inputs
549
+
550
+ Success:
551
+ ```json
552
+ {
553
+ "ok": true,
554
+ "targetClientId": "<clientId>",
555
+ "requestId": "<uuid>",
556
+ "result": {
557
+ "name": "IssueCard",
558
+ "ownerIndex": null,
559
+ "hookCount": 37,
560
+ "hookCountTruncated": false,
561
+ "hookCountMax": 500,
562
+ "pod": {
563
+ "appearance": "compact",
564
+ "isSelected": false,
565
+ "issueKey": "ABC-123"
566
+ },
567
+ "nonPod": [
568
+ {
569
+ "prop": "issue",
570
+ "type": "object",
571
+ "ref": "REF1",
572
+ "terminatesAt": { "ownerIndex": 2, "name": "IssueList" }
573
+ },
574
+ {
575
+ "prop": "onClick",
576
+ "type": "function",
577
+ "ref": "REF2",
578
+ "terminatesAt": { "ownerIndex": 0, "name": "IssueRow" }
579
+ }
580
+ ],
581
+ "children": { "type": "element" }
582
+ }
583
+ }
584
+ ```
585
+
586
+ `terminatesAt.ownerIndex` is the index in the owner chain (matching `component.owners[N]` in snapshot) where the reference was last seen. `ownerIndex: -1` means it was created inside the probe component itself (not passed down).
587
+
588
+ Timeout / client not found: same shape as `get-computed-style`.
589
+
590
+ ---
591
+
592
+ ## `probe-hooks-summary <clientId> <probeId>`
593
+
594
+ Prepared hook summary for the exact probe fiber. Use this before raw `probe-hooks` when the question is "how many hooks / what kinds / what repeated shape?".
595
+
596
+ ```json
597
+ {
598
+ "ok": true,
599
+ "result": {
600
+ "name": "StatusServiceWithoutAriId",
601
+ "fiberId": null,
602
+ "hookCount": 102,
603
+ "truncated": false,
604
+ "maxHooks": 500,
605
+ "counts": { "Ref": 14, "Callback": 40, "State": 19 },
606
+ "cleanupEffectCount": 10,
607
+ "cleanupEffects": [5, 11],
608
+ "runs": [{ "start": 0, "end": 0, "tag": "Ref", "count": 1 }],
609
+ "valueHints": [{ "index": 43, "tag": "State", "value": { "isEnabled": true } }]
610
+ }
611
+ }
612
+ ```
613
+
614
+ ## `inspect-fiber-hooks-summary <clientId> <fiberId>`
615
+
616
+ Same prepared hook summary as `probe-hooks-summary`, but for a registered `fiber:N` from `find-fibers`.
617
+
618
+ ## `explain-component <clientId> <probeId|fiberId|name> [--text|--json|--complexity]`
619
+
620
+ Compact ASCII component overview. Default command for "inspect/explain component" because it avoids raw JSON and per-hook dumps. The shell default is `--text`: it prints `result.text` directly as semantic lines instead of embedding a huge escaped string inside JSON. Use `--json` for the full uncensored transport payload. Use `--complexity` for only the bounded `complexity:` section. The text starts with that bounded `complexity:` section before the full structure so totals, top hook families, cleanup effect indexes, and `useDebugValue` landmarks survive output truncation. The command may attempt dispatcher-based debug replay to recover hook ancestry. That replay executes component and hook bodies but does not schedule an application update; read `replayStatus` and `warnings` before trusting ancestry.
621
+
622
+ Raw `--json` response:
623
+ ```json
624
+ {
625
+ "ok": true,
626
+ "result": {
627
+ "ok": true,
628
+ "componentName": "StatusServiceWithoutAriId",
629
+ "fiberId": "fiber:7",
630
+ "resolvedBy": "probe",
631
+ "hookCount": 102,
632
+ "text": "StatusServiceWithoutAriId @ /path/StatusService.tsx:64\nhooks: 102 (Callback=40, Effect=14, Ref=14, State=19)\neffects with cleanup: 10\nstructure:\n └─ useOpenIssueTransitionModal\n └─ useResource\n ├─ hook 0 Ref\n └─ hook 1 Memo"
633
+ }
634
+ }
635
+ ```
636
+
637
+ The command always has committed fiber evidence and may augment it with dispatcher replay. Outputs separate `liveHookCount` from `replayedHookCount`. When replay cannot run, `replayStatus` and `warnings` disclose the degradation, `replayedHookCount` is `0`, and custom-hook ancestry may be partial or unavailable. Replay can execute render-time component and hook code; it does not invoke effects or schedule an application update, but unsafe render-time side effects can still run. Context values are internal replay scaffolding only: they keep provider guards from aborting structural discovery and are not included in the result.
638
+
639
+ ## `probe-hooks <clientId> <probeId>`
640
+
641
+ Walks the fiber's `memoizedState` linked list and classifies each hook.
642
+
643
+ Success:
644
+ ```json
645
+ {
646
+ "ok": true,
647
+ "targetClientId": "<clientId>",
648
+ "requestId": "<uuid>",
649
+ "result": {
650
+ "name": "IssueCard",
651
+ "hookCount": 4,
652
+ "truncated": false,
653
+ "maxHooks": 500,
654
+ "hooks": [
655
+ { "index": 0, "tag": "State", "value": false },
656
+ { "index": 1, "tag": "Ref", "current": null },
657
+ { "index": 2, "tag": "Effect", "deps": "[array:2]", "hasCleanup": false },
658
+ { "index": 3, "tag": "Memo", "value": "[object]", "deps": "[array:1]" }
659
+ ]
660
+ }
661
+ }
662
+ ```
663
+
664
+ Hook tags: `State`, `Reducer`, `Ref`, `Effect`, `Memo`, `Callback`, `Context?`, `Deferred`, `Unknown`.
665
+
666
+ To locate a specific hook, start with an existing `useDebugValue`, `State`, or `Ref` landmark and its neighboring indexes. If the call site remains ambiguous, report the gap. Adding a diagnostic label is a source edit and requires user intent plus the normal P-15/P-16 gate.
667
+
668
+ Behavior notes:
669
+ - walks the full visible hook chain up to a high safety cap
670
+ - `maxHooks` is the safety cap used for this response
671
+ - `truncated: true` means the walk stopped at the cap; no silent cutoff
672
+ - a malformed cyclic hook list yields a final synthetic entry with `tag: "Unknown"` and a `note`
673
+ - hook indexes are exact within the returned snapshot
674
+
675
+ Values are safely serialized: POD inlined, objects → `[object]`, functions → `[function:name]`, arrays → `[array:N]`, circular → `[circular]`, strings truncated at 120 chars.
676
+
677
+ Timeout / client not found: same shape as `get-computed-style`.
678
+
679
+ ---
680
+
681
+ ## `find-fibers <clientId> [--name regex] [--file regex] [--prop name] [--testid regex] [--text regex] [--max N]`
682
+
683
+ Searches the live fiber tree without probe placement. Returns remembered `fiberId` tokens for the current page realm.
684
+
685
+ ```json
686
+ {
687
+ "ok": true,
688
+ "result": {
689
+ "count": 3,
690
+ "items": [
691
+ {
692
+ "fiberId": "fiber:1",
693
+ "name": "StatusServiceWithoutAriId",
694
+ "source": { "fileName": "…/StatusService.tsx", "lineNumber": 64 },
695
+ "owner": "StatusServiceForMigrateIssueTransitionEnabled",
696
+ "domTag": null,
697
+ "domTestId": null
698
+ }
699
+ ]
700
+ }
701
+ }
702
+ ```
703
+
704
+ `fiberId` uses the canonical string form `fiber:<N>`. A bare numeric input is accepted as shorthand and normalized to that string. The registry survives commits within the current page realm, but React may replace the current fiber alternate after any commit; re-run `find-fibers` after state changes, interactions, HMR, navigation, or reload. Navigation/reload replaces the page realm and invalidates the registry entirely.
705
+
706
+ ## `fiber-state-capture <clientId> [label] [maxFibers]`
707
+
708
+ Captures the current named React fiber tree and fingerprints every prop without returning complete prop values. `maxFibers` defaults to 100000. The helper retains at most 20 captures in memory and evicts the oldest first.
709
+
710
+ ```json
711
+ {
712
+ "ok": true,
713
+ "capture": {
714
+ "captureId": "state:a1b2c3d4",
715
+ "label": "gate off",
716
+ "clientId": "<clientId>",
717
+ "page": { "url": "https://example.test/off", "title": "Example" },
718
+ "capturedAt": "2026-08-31T10:00:00.000Z",
719
+ "fiberCount": 4821,
720
+ "truncated": false
721
+ }
722
+ }
723
+ ```
724
+
725
+ Prop fingerprints contain a structural kind, deterministic hash, short preview, and truncation flag. Objects use sorted keys, arrays preserve order, React elements include element type/key/props, and cycles are stable within one prop. Functions compare by name and arity so ordinary closure recreation is not reported as a change. Capture is read-only and does not invoke components, hooks, effects, or callbacks.
726
+
727
+ ## `fiber-state-list`
728
+
729
+ Lists capture summaries only; retained fiber/prop payloads are not returned.
730
+
731
+ ```json
732
+ {
733
+ "ok": true,
734
+ "count": 2,
735
+ "captures": [{
736
+ "captureId": "state:a1b2c3d4",
737
+ "label": "gate off",
738
+ "clientId": "<clientId>",
739
+ "page": { "url": "https://example.test/off", "title": "Example" },
740
+ "capturedAt": "2026-08-31T10:00:00.000Z",
741
+ "fiberCount": 4821,
742
+ "truncated": false
743
+ }]
744
+ }
745
+ ```
746
+
747
+ Captures survive page navigation or disconnect but not helper restart.
748
+
749
+ ## `fiber-state-compare <beforeCaptureId> <afterCaptureId> [maxChanges]`
750
+
751
+ Compares captures from the same or different pages. `maxChanges` defaults to 200 diff records. A connected page is required to execute the bundled comparison, but it need not be either captured page. Fiber matching first uses the exact structural path, then a globally unique token made from fiber tag, component/host name, key, and source location. Repeated unkeyed fibers that move structurally remain added/removed rather than being guessed into a match.
752
+
753
+ ```json
754
+ {
755
+ "ok": true,
756
+ "before": { "captureId": "state:a1b2c3d4", "label": "gate off", "clientId": "client-a", "page": { "url": "https://example.test/off", "title": "Example" }, "capturedAt": "...", "fiberCount": 4821, "truncated": false },
757
+ "after": { "captureId": "state:e5f6a7b8", "label": "gate on", "clientId": "client-b", "page": { "url": "https://example.test/on", "title": "Example" }, "capturedAt": "...", "fiberCount": 4830, "truncated": false },
758
+ "diff": {
759
+ "ok": true,
760
+ "before": { "url": "https://example.test/off", "title": "Example", "capturedAt": "..." },
761
+ "after": { "url": "https://example.test/on", "title": "Example", "capturedAt": "..." },
762
+ "summary": {
763
+ "beforeFibers": 4821,
764
+ "afterFibers": 4830,
765
+ "matchedFibers": 4819,
766
+ "changedFibers": 3,
767
+ "addedFibers": 11,
768
+ "removedFibers": 2,
769
+ "propChanges": 5,
770
+ "returnedChanges": 16,
771
+ "captureTruncated": false,
772
+ "changesTruncated": false,
773
+ "truncated": false
774
+ },
775
+ "changes": [
776
+ {
777
+ "kind": "props-changed",
778
+ "match": "path",
779
+ "fiber": { "before": { "name": "FeaturePanel" }, "after": { "name": "FeaturePanel" } },
780
+ "props": {
781
+ "added": [],
782
+ "removed": [],
783
+ "changed": [{ "name": "isEnabled", "before": { "kind": "boolean", "hash": "...", "preview": "false", "truncated": false }, "after": { "kind": "boolean", "hash": "...", "preview": "true", "truncated": false } }]
784
+ }
785
+ },
786
+ { "kind": "fiber-added", "fiber": { "name": "GateContent", "key": null, "source": null }, "propNames": ["children"] }
787
+ ]
788
+ }
789
+ }
790
+ ```
791
+
792
+ `captureTruncated` means at least one source capture hit `maxFibers`; `changesTruncated` means the result hit `maxChanges`; `truncated` is their combined warning. `propChanges` counts individual added, removed, and changed props even though one `props-changed` record can contain several. Use the component source and prop preview as an index for targeted `find-fibers`, `inspect-fiber-props`, or probe work; hashes prove inequality, not causality.
793
+
794
+ ## `inspect-fiber-hooks <clientId> <fiberId>`
795
+
796
+ Deep hook list for a fiber found via `find-fibers`. Returns same shape as `probe-hooks`.
797
+
798
+ ## `inspect-fiber-props <clientId> <fiberId>`
799
+
800
+ Props inspection for a fiber found via `find-fibers`. Returns same shape as `probe-props` (no ownerIndex needed).
801
+
802
+ ## `inspect-fiber-owners <clientId> <fiberId>`
803
+
804
+ Owner chain for a fiber found via `find-fibers`.
805
+
806
+ ## `inspect-fiber-structure <clientId> <fiberId> <hookIndex> [windowSize]`
807
+
808
+ Reconstructs custom-hook folder structure for one hook in an arbitrary fiber. Does not require Avalanche or a probe. It may use dispatcher replay, which executes component and hook bodies without scheduling an application update; read `replayStatus` and `warnings`, and treat render-time side effects as the execution boundary.
809
+
810
+ Returns a nested folder `text`, a `target` block with `parents`/`parentsWithLoc`, and `warnings` when ancestry is unavailable.
811
+
812
+ ```json
813
+ {
814
+ "ok": true,
815
+ "result": {
816
+ "ok": true,
817
+ "status": "ok",
818
+ "componentName": "StatusServiceWithoutAriId",
819
+ "fiberId": "fiber:1",
820
+ "requestedHookIndex": 1,
821
+ "hookCount": 11,
822
+ "text": "StatusServiceWithoutAriId\n└─ useCloudId\n └─ useTenantContext\n └─ useSweetState\n └─ hook 1 Memo ← target",
823
+ "target": {
824
+ "hookId": 1,
825
+ "type": "Memo",
826
+ "value": "[function:DEFAULT_SELECTOR]",
827
+ "parents": ["StatusServiceWithoutAriId", "useCloudId", "useTenantContext", "useSweetState"],
828
+ "parentsWithLoc": [{ "name": "useCloudId", "loc": "17226:33" }, "…"]
829
+ },
830
+ "captureEngineOk": true,
831
+ "warnings": []
832
+ }
833
+ }
834
+ ```
835
+
836
+ `warnings` is non-empty when the dispatcher ref is unavailable (hook shim not loaded before react-dom) or when replay failed mid-component. In that case `parents` may be partial or empty.
837
+
838
+ ## `describe-component <clientId> <fiberIdOrName> [--text|--json]`
839
+
840
+ Whole-component structural map. Shell default is `--text`: print the full rendered tree as plain text, not as an escaped JSON string. Use `--json` for the full uncensored transport payload. The map contains the **full** custom-hook tree across all hooks in the component — not anchored to one `hookIndex` like `inspect-fiber-structure`. The result contains structure only: primitive kinds + folder ancestry, with no values, props, owners, or replay inputs. Building that structure may use dispatcher replay, which executes component and hook bodies without scheduling an application update. Check `replayStatus` and `warnings`; if replay is unavailable or fails, the result degrades to committed fiber evidence and ancestry may be partial.
841
+
842
+ Resolution:
843
+ - If the second arg starts with `fiber:` — resolved against the fiber registry (returned by `find-fibers`).
844
+ - Otherwise — treated as a component name. By Rules of Hooks, all instances of one component share the same hook shape, so the first matching fiber is used.
845
+
846
+ ```json
847
+ {
848
+ "ok": true,
849
+ "result": {
850
+ "ok": true,
851
+ "componentName": "ChurnLab",
852
+ "fiberId": "fiber:42",
853
+ "resolvedBy": "name",
854
+ "source": { "fileName": "/abs/path/ChurnLab.jsx", "lineNumber": 12 },
855
+ "hookCount": 7,
856
+ "tree": [
857
+ {
858
+ "path": [],
859
+ "hooks": [
860
+ { "hookId": 0, "primitive": "State" },
861
+ { "hookId": 1, "primitive": "Memo" }
862
+ ]
863
+ },
864
+ {
865
+ "path": [{ "name": "useStore", "loc": "ChurnLab.jsx:18:5" }],
866
+ "hooks": [
867
+ { "hookId": 2, "primitive": "SyncExternalStore" },
868
+ { "hookId": 3, "primitive": "Memo" }
869
+ ]
870
+ },
871
+ {
872
+ "path": [
873
+ { "name": "useStore", "loc": "ChurnLab.jsx:18:5" },
874
+ { "name": "useShallow", "loc": "ChurnLab.jsx:24:3" }
875
+ ],
876
+ "hooks": [
877
+ { "hookId": 4, "primitive": "Memo" }
878
+ ]
879
+ }
880
+ ],
881
+ "text": "ChurnLab @ /abs/path/ChurnLab.jsx:12\n├─ hook 0 State\n└─ hook 1 Memo\n└─ useStore @ ChurnLab.jsx:18:5\n ├─ hook 2 SyncExternalStore\n └─ hook 3 Memo\n └─ useShallow @ ChurnLab.jsx:24:3\n └─ hook 4 Memo",
882
+ "captureEngineOk": true,
883
+ "warnings": []
884
+ }
885
+ }
886
+ ```
887
+
888
+ Field semantics:
889
+ - `tree[]` — folders in first-seen order. Each folder has `path` (custom-hook ancestry, outer→inner) and `hooks[]` with `{ hookId, primitive, debugLabel? }`. `hookId` is `null` for `Context`, `DebugValue`, `MemoCache` (no `memoizedState` slot). For `DebugValue`, `debugLabel` carries the serialized `useDebugValue` value and should be treated as a named landmark, for example a Sweet State store label.
890
+ - `hookCount` — number of indexable primitive hooks (excludes `Context`, `DebugValue`, `MemoCache`).
891
+ - `resolvedBy` — `"fiberId"` when the registry hit, `"name"` when name-walk hit.
892
+ - `text` — pre-rendered tree for log readability. It prints `debugLabel` inline after the primitive, for example `hook · DebugValue — RSS: JWMFormStore__1xxy1q6`.
893
+ - `warnings` — same gates as `inspect-fiber-structure`: empty `parents` is normal when the hook is called directly in the component; `flat-records` warning only appears when no folder ancestry was recovered at all.
894
+
895
+ Use `describe-component` when the question is "what is this component made of"; use `inspect-fiber-structure` when the question is "what's around hook N in this component"; drill values for one hook with `avalanche-debug-hook`.
896
+
897
+ ## `page-click <clientId> <selector|probeId|fiberId>`
898
+
899
+ Success:
900
+ ```json
901
+ {
902
+ "ok": true,
903
+ "mode": "selector",
904
+ "target": { "tag": "button", "testId": "save-button", "text": "Save" },
905
+ "selector": ".save-button",
906
+ "probeId": null,
907
+ "fiberId": null,
908
+ "targetClientId": "abc123",
909
+ "resolvedFrom": null,
910
+ "requestId": "..."
911
+ }
912
+ ```
913
+
914
+ Refusal on ambiguous selector:
915
+ ```json
916
+ {
917
+ "ok": false,
918
+ "error": "selector matched 3 elements; refine the selector to one target",
919
+ "matched": 3,
920
+ "targetClientId": "abc123",
921
+ "resolvedFrom": null,
922
+ "requestId": "..."
923
+ }
924
+ ```
925
+
926
+ ## `fiber-prop-contract <clientId> <fiberId> <propName>`
927
+
928
+ Success for function prop:
929
+ ```json
930
+ {
931
+ "ok": true,
932
+ "fiberId": "fiber:42",
933
+ "fiberName": "SaveButton",
934
+ "source": { "fileName": "/path/Button.tsx", "lineNumber": 12, "columnNumber": 2 },
935
+ "propName": "onClick",
936
+ "propNames": ["children", "onClick"],
937
+ "propType": "function",
938
+ "isFunction": true,
939
+ "fnLength": 1,
940
+ "fnName": "handleClick",
941
+ "fnSourcePreview": "function handleClick(event) { … }",
942
+ "nextStep": "Read the component/owner source to determine the callback argument contract before fiber-invoke.",
943
+ "targetClientId": "abc123",
944
+ "resolvedFrom": null,
945
+ "requestId": "..."
946
+ }
947
+ ```
948
+
949
+ ## `fiber-invoke <clientId> <fiberId> <propName> [--args JSON]`
950
+
951
+ Success:
952
+ ```json
953
+ {
954
+ "ok": true,
955
+ "fiberId": "fiber:42",
956
+ "fiberName": "SaveButton",
957
+ "propName": "onClick",
958
+ "propType": "function",
959
+ "fnLength": 1,
960
+ "invoked": true,
961
+ "args": ["draft"],
962
+ "async": false,
963
+ "returnValue": null,
964
+ "targetClientId": "abc123",
965
+ "resolvedFrom": null,
966
+ "requestId": "..."
967
+ }
968
+ ```
969
+
970
+ For a thenable result, Chippy waits up to two seconds by default and returns `"async": true` with the serialized resolved value. A rejected or timed-out thenable returns `ok: false`, `async: true`, `rejected: true`, `threw: false`, plus `error` and `stack`.
971
+
972
+ A synchronously thrown callback returns:
973
+
974
+ ```json
975
+ {
976
+ "ok": false,
977
+ "threw": true,
978
+ "error": "callback failed",
979
+ "stack": "Error: callback failed ...",
980
+ "fiberId": "fiber:42",
981
+ "fiberName": "SaveButton",
982
+ "propName": "onClick",
983
+ "propType": "function",
984
+ "args": ["draft"]
985
+ }
986
+ ```
987
+
988
+ For a class component, invocation binds `this` to `fiber.stateNode`. Function components receive no synthetic `this` binding.
989
+
990
+ Refusal on non-function prop:
991
+ ```json
992
+ {
993
+ "ok": false,
994
+ "error": "prop label is not a function",
995
+ "fiberId": "fiber:42",
996
+ "fiberName": "SaveButton",
997
+ "propName": "label",
998
+ "propType": "string",
999
+ "targetClientId": "abc123",
1000
+ "resolvedFrom": null,
1001
+ "requestId": "..."
1002
+ }
1003
+ ```
1004
+
1005
+ ## `fiber-schedule-update <clientId> <fiberId> [--ownerIndex N]`
1006
+
1007
+ Success:
1008
+
1009
+ ```json
1010
+ {
1011
+ "ok": true,
1012
+ "fiberId": "fiber:42",
1013
+ "fiberName": "SavePanel",
1014
+ "source": { "fileName": "/path/SavePanel.tsx", "lineNumber": 18, "columnNumber": 2 },
1015
+ "ownerIndex": 0,
1016
+ "scheduled": true
1017
+ }
1018
+ ```
1019
+
1020
+ Failures return `ok: false`, `error`, and one stable `reason`: `no-fiberId`, `fiber-not-found`, `owner-out-of-range`, `no-scheduleUpdate`, or `scheduleUpdate-threw`. `no-scheduleUpdate` means a production React build or a DevTools-hook shim that arrived after React DOM evaluated.
1021
+
1022
+ The command schedules one render through the React DevTools renderer. It mutates no hook or prop; the extra commit and any effects it runs are the active stimulus.
1023
+
1024
+ ## `proxy-state`
1025
+
1026
+ No proxy active:
1027
+ ```json
1028
+ { "ok": true, "proxy": null }
1029
+ ```
1030
+
1031
+ Proxy active:
1032
+ ```json
1033
+ {
1034
+ "ok": true,
1035
+ "proxy": {
1036
+ "active": true,
1037
+ "targetPort": 3000,
1038
+ "targetHost": "localhost:3000",
1039
+ "originalUrl": "http://localhost:3000/issues/ABC-123",
1040
+ "injectTarget": { "pathname": "/issues/ABC-123", "search": "" },
1041
+ "startedAt": "2026-04-16T08:00:00.000Z"
1042
+ }
1043
+ }
1044
+ ```
1045
+
1046
+ `injectTarget` — the path+search the proxy will inject the Chippy bundle into. Derived from `originalUrl`. `null` if no `originalUrl` was given.
1047
+
1048
+ `targetHost` — where requests are forwarded. The proxy forwards only to this machine: it uses the loopback host of `originalUrl` (`localhost`, `127.0.0.1`, `[::1]`), or `localhost` when none is given, and reaches servers listening on either IPv4 or IPv6. Into the injected document it adds the DevTools hook shim and the Avalanche runtime before page scripts, and the page bridge at the end of the body.
1049
+
1050
+ ---
1051
+
1052
+ ## `proxy-start <targetPort> [originalUrl]`
1053
+
1054
+ Success:
1055
+ ```json
1056
+ {
1057
+ "ok": true,
1058
+ "proxy": {
1059
+ "active": true,
1060
+ "targetPort": 3000,
1061
+ "targetHost": "localhost:3000",
1062
+ "originalUrl": "http://localhost:3000/issues/ABC-123",
1063
+ "injectTarget": { "pathname": "/issues/ABC-123", "search": "" },
1064
+ "startedAt": "2026-04-16T08:00:00.000Z"
1065
+ }
1066
+ }
1067
+ ```
1068
+
1069
+ Bad port:
1070
+ ```json
1071
+ { "ok": false, "error": "Invalid target port: abc" }
1072
+ ```
1073
+
1074
+ Target off this machine:
1075
+ ```json
1076
+ { "ok": false, "error": "Proxy targets must be on this machine; originalUrl host example.com is not a loopback name" }
1077
+ ```
1078
+
1079
+ ---
1080
+
1081
+ ## `proxy-stop`
1082
+
1083
+ ```json
1084
+ { "ok": true, "proxy": { "active": false } }
1085
+ ```