@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,199 @@
1
+ # Hook metrics — `setStateCount` + the hook-update tracker
2
+
3
+ Reference for the two snippets that consume the react-dom patch's hook metrics:
4
+ `hook-metrics` (read-only) and `hook-tracker` (drives the `onHookUpdate` callback).
5
+ They belong to the ATL plugin: they read the ATL adapter's `__ATL_REACT_METRICS`
6
+ slot and need Atlassian's patched `react-dom` to report anything.
7
+
8
+ These are the **why** layer under Avalanche. Avalanche tells you *what* re-rendered and
9
+ *how much collateral* a commit caused. Hook metrics tell you *why a component's work
10
+ repeated*: how many state dispatches were scheduled, and which memo/callback/effect slots
11
+ recomputed because their deps actually changed. ~99% of the time you reach for these
12
+ **after** an Avalanche capture has already named a hot component.
13
+
14
+ ## What the patch emits
15
+
16
+ The react-dom patch adds exactly two hook-related surfaces to the host metrics object
17
+ `__ATL_REACT_METRICS`:
18
+
19
+ | Surface | Kind | Meaning | Where it fires |
20
+ |---|---|---|---|
21
+ | `setStateCount` | plain counter | `useState`/`useReducer` **dispatches scheduled** | `dispatchSetState` / reducer dispatch, bumped unconditionally (`m.setStateCount++`) |
22
+ | `onHookUpdate(fiber, kind, slot)` | callback slot | one memo/callback/effect **recomputed because its deps changed** | update path only, *after* the deps-equality check — never on mount, never when deps were equal |
23
+
24
+ Hook kinds are numeric on the hot path: **`1 = memo`, `2 = callback`, `3 = effect`**.
25
+
26
+ **The metrics object is host-owned.** The patch never creates it and never stores hook
27
+ history — it only *bumps a counter* and *calls a callback if one is present*. This is the
28
+ core distinction from Avalanche:
29
+
30
+ - **Avalanche** installs everything for itself (`window.__AVALANCHE__`, its own detector).
31
+ - **Hook metrics** are emitted by the patched runtime into a slot the *app* owns. Chippy
32
+ **reads** `setStateCount`, and **installs one thing** — the `onHookUpdate` callback — the
33
+ same way the patch's own `hookTracker.ts` is meant to be installed independently.
34
+
35
+ ### `setStateCount` caveats
36
+
37
+ - The host may seed `setStateCount` as `null`. The patch then does `null++` → `NaN`. Once
38
+ numeric it is monotonic-per-run. `hook-metrics` reports `setStateCountValid:false` and a
39
+ note when it sees a non-finite value instead of pretending it is a clean counter.
40
+ - Some hosts **wipe** `__ATL_REACT_METRICS` to
41
+ `undefined` after hydration. `hook-metrics` reports `present:false, wiped:true`. The
42
+ `hook-tracker install` action **seeds a minimal object** in that case so the tracker can
43
+ still own `onHookUpdate` — it never overwrites an existing host object.
44
+
45
+ ## `hook-metrics` — read-only
46
+
47
+ ```bash
48
+ ./scripts/chippy.sh eval-page <clientId> hook-metrics
49
+ ./scripts/chippy.sh eval-page <clientId> hook-metrics --keys setStateCount,contextUpdateCount,totalCommitCount
50
+ ./scripts/chippy.sh eval-page <clientId> hook-metrics --raw # every own key, functions tagged
51
+ ```
52
+
53
+ Returns `{ present, wiped, setStateCount, setStateCountValid, onHookUpdateInstalled, counters:{…} }`.
54
+ Use it to read the current dispatch volume without installing anything. If `present:false`,
55
+ the host hasn't created the object yet (or wiped it) — use `hook-tracker install` to own the
56
+ slot regardless.
57
+
58
+ ## `hook-tracker` — drive the hook-update tracker
59
+
60
+ State persists across calls on `window.__CHIPPY_HOOK_TRACKER`, because install / reproduce /
61
+ read / uninstall are separate `eval-page` invocations.
62
+
63
+ ```bash
64
+ CID=<clientId>
65
+ ./scripts/chippy.sh eval-page $CID hook-tracker --action install # own onHookUpdate, mode=counts
66
+ ./scripts/chippy.sh eval-page $CID hook-tracker --action mode --mode debug # carry fiber refs for source locations
67
+ # ... reproduce the interaction (user gesture, or Playwright clicks) ...
68
+ ./scripts/chippy.sh eval-page $CID hook-tracker --action counts # source of truth, compact
69
+ ./scripts/chippy.sh eval-page $CID hook-tracker --action histogram --buckets 3,6,11
70
+ ./scripts/chippy.sh eval-page $CID hook-tracker --action offenders --threshold 10
71
+ ./scripts/chippy.sh eval-page $CID hook-tracker --action uninstall # restore slot (only if still owned)
72
+ ```
73
+
74
+ ### Actions
75
+
76
+ | Action | Effect |
77
+ |---|---|
78
+ | `install` | seed metrics if absent, set `onHookUpdate = record`, mode → `counts` |
79
+ | `mode --mode off\|counts\|debug` | `off` no-ops; `counts` aggregates; `debug` additionally keeps the last fiber per type so `offenders` can report source file:line |
80
+ | `status` | installed?, mode, whether we still own the slot, entry count, current `setStateCount` |
81
+ | `counts` | per component-type: `{ totalUpdates, activeSlots, slots:[{slot,count,kind}] }`, sorted |
82
+ | `histogram --buckets a,b,c` | DB-safe: for each kind, count of `type:slot` identities whose total reached each bucket |
83
+ | `offenders --threshold N [--top M]` | dev-only: slots with `count > N`, carrying component name + source (needs `debug` mode) |
84
+ | `reset` | clear counts, stay installed |
85
+ | `uninstall` | restore `onHookUpdate` **only if we still own it**; retains last counts for a final read |
86
+
87
+ ### Reading the numbers
88
+
89
+ - Counts aggregate by **component type**, not per instance — 100 rows of the same list item
90
+ collapse into one entry. This is intentional (type-level hotspots), matching `hookTracker.ts`.
91
+ - `counts[slot]` is how many times that hook slot recomputed. `type[slot]` is the numeric kind.
92
+ - **Use `counts`/`histogram` in prod-like runs; `offenders` only in dev**, since minified
93
+ bundles have no readable hook ids or source.
94
+ - A slot with a high count = a memo/callback/effect whose **deps keep changing**. That is
95
+ dependency churn, and it is usually an unstable identity upstream (a new object/array/
96
+ function created every render), not a bug in the memo itself.
97
+
98
+ ## Lighthouse numbers vs Avalanche — the cost argument
99
+
100
+ The most useful framing for these metrics is **lighthouse numbers**: cheap integer
101
+ counters you could sample on every hydration and every route transition and forward to a
102
+ dashboard, for *real users in realtime*.
103
+
104
+ - **Avalanche** hooks React at a low level (`onCommitFiberRoot` + full fiber walk per
105
+ commit). It sees the whole picture — collateral, penetration depth, context waves,
106
+ unmount branches — but that instrumentation is far too expensive to leave on for every
107
+ user. It is an *on-demand debugger*.
108
+ - **The patch's metrics** are just `count++` inline in react-dom plus one optional
109
+ callback. Reading `setStateCount` is reading an integer. That is cheap enough to ship.
110
+
111
+ So the two are not competitors — they sit at opposite ends of a cost/detail axis:
112
+
113
+ | | `setStateCount` / hook-tracker | Avalanche |
114
+ |---|---|---|
115
+ | Cost | integer read / one callback | full fiber walk per commit |
116
+ | Detail | counts only (no tree, no collateral) | complete commit graph |
117
+ | Realtime / all-users | yes (lighthouse) | no (on-demand only) |
118
+ | Answers | "how much settling happened" | "what re-rendered and how far it spread" |
119
+
120
+ ## `hook-metrics-sample` — labeled before/after delta in one call
121
+
122
+ The "lighthouse number" as a first-class read. Sample the counter, optionally click a
123
+ `data-testid`, sample again, return the delta:
124
+
125
+ ```bash
126
+ ./scripts/chippy.sh eval-page <clientId> hook-metrics-sample --label hydration --settleMs 500
127
+ ./scripts/chippy.sh eval-page <clientId> hook-metrics-sample --label tick --click tick --clicks 4
128
+ ```
129
+
130
+ Returns `{ available, label, before, after, delta, clicked, settleMs }`. **Metric-optional:**
131
+ if the patch is absent / wiped / `NaN` it returns `{ available:false, reason }` and never
132
+ throws.
133
+
134
+ ## Avalanche integration — `setStateCount` per commit
135
+
136
+ When the react-dom patch is present, Avalanche now records `setStateCount` **per commit** as
137
+ an optional field on the ATL commit delta (`ATLCommitDelta.setStateCount`,
138
+ `ATLCommitDeltaDTO.setStateCount`). Read it with:
139
+
140
+ ```bash
141
+ ./scripts/chippy.sh avalanche-commit-peek <clientId> <commitId> atl
142
+ # → "atl": { ..., "setStateCount": 12 }
143
+ ```
144
+
145
+ **Optional and self-degrading.** It is computed only when `__ATL_REACT_METRICS` is live at
146
+ commit time (`atlDeltaAvailable`), and the delta line is omitted entirely when neither
147
+ snapshot carries a numeric `setStateCount` — so on an unpatched host the field is simply
148
+ absent, never a bogus `0`. Every other ATL delta field can be `null` while `setStateCount`
149
+ is a real number; they are independent.
150
+
151
+ A sequence such as **1, 12, 1** distinguishes one top-level dispatch, a batched effect or
152
+ store wave, and another single dispatch. Treat the per-commit value as a *commit-shape*
153
+ signal: it distinguishes a burst of state dispatches from a single top-level update, but it
154
+ does not locate their source.
155
+
156
+ **Does it aid source detection?** Modestly, and only as a *disambiguator*, not a locator.
157
+ Avalanche already names the source component and `nearestTestId`. `setStateCount` per commit
158
+ adds one axis: a commit with `performedWork` high but `setStateCount` low = re-render driven
159
+ from *above* (props/context), not local state; high `setStateCount` = the work originated
160
+ from state dispatches in that commit window. It narrows *which kind* of trigger to chase; it
161
+ does not by itself point at a file. Treat it as a tie-breaker on top of the owner chain,
162
+ never a replacement for it. **And because the patch is optional, never build a source-tracing
163
+ procedure that requires it** — it is an enrichment when present, nothing when absent.
164
+
165
+ ## What this diagnoses (and how it pairs with Avalanche)
166
+
167
+ | Symptom | Metric read | Interpretation | Next |
168
+ |---|---|---|---|
169
+ | Component renders far more than its parent | `setStateCount` climbing on one interaction | high state-dispatch volume — often a store/effect firing setState in a loop | trace the dispatcher; check for setState-in-render or effect cascades |
170
+ | Avalanche shows a hot source, unclear why | `hook-tracker counts` on that type | one slot thrashing vs many slots each once | one slot → chase that dep's identity; many slots → the whole component re-runs (parent/props identity) |
171
+ | Suspected unstable memoization | `offenders` (debug) | a `memo`/`callback` slot with count ≈ render count | the memo's deps are unstable — fix identity **above** the boundary (see [fixing-performance](fixing-performance.md)) |
172
+ | Effect running too often | `histogram` effect column | count of effect identities reaching each bucket | effect deps churn → stabilize deps or split the effect |
173
+ | Type-level hotspot across many instances | `counts` aggregation | one component type dominates total updates | fix once at the type, not per instance |
174
+
175
+ **Lifecycle placement** (see [evidence and edit gates](../SKILL.md#evidence-and-edit-gates)):
176
+
177
+ 1. **Triage** with Avalanche (`avalanche-hot-sources`, `avalanche-commit-list`) → candidate hot component.
178
+ 2. **Attribution**: `hook-tracker install` → reproduce → `counts` on that component type →
179
+ is it *one slot thrashing* (dep churn) or *the whole component re-running* (identity/props)?
180
+ 3. **Experiment**: after a fix, `hook-tracker reset` → reproduce → `counts` again, or use
181
+ [stability-render](stability-render.md) to verify without a user gesture. Same signal,
182
+ before/after.
183
+ 4. **`uninstall`** to hand the slot back.
184
+
185
+ ## Anchor contract
186
+
187
+ `hook-tracker` does **not** mutate product state, hooks, or props. It only:
188
+
189
+ - reads `setStateCount` (a counter the patch already writes), and
190
+ - **owns `onHookUpdate`** — a slot the host leaves for exactly this purpose.
191
+
192
+ What becomes opaque while installed: any *other* consumer of `onHookUpdate` is displaced
193
+ until `uninstall`. `uninstall` restores the slot **only if Chippy still owns it** (it checks
194
+ `onHookUpdate === record`), so a host that re-seeded the object after install is not clobbered.
195
+ If the host wiped the object and `install` had to seed one, that seeded object persists after
196
+ uninstall with `onHookUpdate = undefined` — harmless, and re-wiped on the next host reset.
197
+
198
+ Always `uninstall` when the investigation is done, and `reset` between reproduction runs so
199
+ counts from an earlier gesture don't bleed into the next measurement.
@@ -0,0 +1,61 @@
1
+ # Hooks inspection
2
+
3
+ **To inspect hooks** — `probe-hooks <clientId> <probeId>`:
4
+ - Returns an array of hook nodes walked from `fiber.memoizedState`.
5
+ - Tags: `State`, `Reducer`, `Ref`, `Effect`, `Memo`, `Callback`, `Context?`, `Deferred`, `Unknown`.
6
+ - `State` → `value` is the current state value. `Reducer` → `value` is the last rendered state.
7
+ - `Ref` → `current` is the ref value.
8
+ - `Effect` → `deps` count + `hasCleanup`.
9
+ - `Memo` / `Callback` → `value` + `deps` count.
10
+ - Walks the full hook list up to a high safety cap (`maxHooks: 500`). If the response stops at that cap, `truncated: true` tells you.
11
+ - POD values inlined; objects/arrays/functions safe-serialized.
12
+ - If the hook list is malformed or cyclic, Chippy stops deterministically and marks the break in the output.
13
+ - Hook index is exact within the returned list.
14
+ - No hidden mini-cap for Jira-scale components.
15
+ - Hook output is still for inspection, not for counting business data.
16
+ - Re-run after code changes if you need fresh positions.
17
+ - Use `hookCount`, `truncated`, and `maxHooks` together when judging completeness.
18
+ - If `truncated` is false, the full visible hook chain was walked.
19
+ - If `truncated` is true on a real component, the component is unusually deep and worth noting explicitly.
20
+ - The cap is safety, not guidance.
21
+ - Deep Jira fibers are expected to work.
22
+ - Large outputs may still be verbose; inspect around known anchors.
23
+ - Prefer a nearby `State`/`Ref`/`useDebugValue` landmark when scanning hundreds of hooks.
24
+ - Counts can shift between renders.
25
+ - The list reflects one render snapshot only.
26
+ - This is deterministic from the exact probed fiber.
27
+ - If the page rerenders, probe again.
28
+ - The returned `hookCount` is the number of entries emitted in this result.
29
+ - For capped responses, `hookCount` equals `maxHooks`.
30
+ - For cycle breaks, one final synthetic `Unknown` entry may be appended with a note.
31
+ - This keeps failures inspectable instead of silent.
32
+ - The cap exists only to guard against broken or pathological hook lists.
33
+ - Jira with 300 hooks is fine.
34
+ - Very large components are noisy, but now observable.
35
+ - Use the index numbers as exact anchors.
36
+ - Do not infer missing hooks unless `truncated` says they are missing.
37
+ - When in doubt, call `probe-hooks` again after the next render settles.
38
+
39
+ Note: if you hit `truncated: true` at 500 in practice, that is a signal to improve the command surface further, not to guess past the boundary.
40
+
41
+ **Result shape adds**:
42
+ - `hookCount` — number of returned hook entries
43
+ - `truncated` — whether Chippy stopped at the safety cap
44
+ - `maxHooks` — current safety cap used for this walk
45
+ - `hooks[N].note` — optional note on synthetic break entries such as cycle detection
46
+
47
+ **Reading very deep results**:
48
+ - Default reading is **collapsed by repetition**: group consecutive hooks belonging to the same custom-hook family (e.g. `useSweetState [12..16]`) instead of listing each leaf. Expand a family fully when it is the only instance, or when the user explicitly asks for the raw list (`dump`, `expanded`, `every hook`).
49
+ - For "where exactly is complexity" asks, produce a concentration view first: hook ranges, top-level custom-hook/subtree label, primitive counts, cleanup counts, and landmarks such as `State`, `Ref`, or `useDebugValue`.
50
+ - find a stable hook anchor first (`State` value, `Ref.current`, `useDebugValue`)
51
+ - then use surrounding indexes to orient
52
+ - do not treat raw length alone as meaningful
53
+ - the useful signal is where a known value sits in the deterministic sequence
54
+
55
+ **Safety rule**:
56
+ - the hook walker should prefer explicit truncation over hanging, looping, or pretending the list ended naturally
57
+ - deterministic failure beats silent loss
58
+
59
+ **To locate a specific hook**, first use an existing `useDebugValue`, `State`, or `Ref` landmark and its neighboring indexes. If multiple source call sites still fit, report the ambiguity. Adding a new `useDebugValue` is a source edit and requires user intent plus the normal P-15/P-16 gate before changing the code.
60
+
61
+ **Hook index is stable within a render** but shifts if hooks are added/removed. Always re-run `probe-hooks` after code changes.
@@ -0,0 +1,81 @@
1
+ # Jira-specific component taxonomy
2
+
3
+ Signals to keep and use when probing Jira-like production pages.
4
+
5
+ The `owner` boundary and Jira ownership fields appear only when the built-in Jira plugin is enabled. The wrapper names below apply to Jira pages whether or not it is.
6
+
7
+ ## Probe Jira context
8
+
9
+ Chippy enriches a selected fiber with three collateral-scope boundaries. The extension/import panel renders them in-page; the Storybook addon renders the same three inside the matching marker card (feature parity — same data, host-specific rendering). It scans a bounded current/return/debug-owner path.
10
+
11
+ The three boundaries (`markers[id].context.boundaries = { owner, error, suspense }`) each carry a **`distance`** — the number of fiber hops from the probed fiber. Distance is the primary signal: it says how far each collateral boundary is without resolving it in full. Each block is **clickable**; a click reports the selected boundary (`{ markerId, boundaryKind, boundary, distance }`) to the backend as an `inspect-boundary` signal for later investigation (readable as `lastInspectedBoundary` on the client summary).
12
+
13
+ - **error** and **suspense** are **fiber-native React primitives**, detected generically on *any* React page (not just Jira). Error boundaries are found by React lifecycle (`getDerivedStateFromError` / `componentDidCatch`) with a name-heuristic fallback (`detectedBy: 'lifecycle' | 'name'`); Suspense by fiber tag 13. The error block shows the detailed fields; on Jira it is enriched from `JSErrorBoundary` props (`id`, `packageName`, `teamName`, `fallback`).
14
+ - **owner** is the Jira-specific semantic/team owner (`OwnerMark___<type>___<name>___<package>___<variant>`) — *not* the React render owner. It renders as a **short clickable lozenge** (name + distance only); its fields are intentionally not dumped because the OwnerMark commonly wraps the error boundary and would duplicate it.
15
+ - The nearest Suspense boundary in deep trees (e.g. Jira) can sit ~170+ fiber-hops above a leaf; the ancestry walk reaches it (ceiling 400) but early-exits once all three are found.
16
+ - This is live runtime metadata, not a repository or organizational lookup. Treat it as a production source hint and retain the existing confidence caveat when neither `_debugSource` nor a usable React 19 `_debugStack` source is available.
17
+ - Use `context-ui <clientId> off` to hide the boundary cards for the current page, and `context-ui <clientId> on` to show them again. This applies to both in-page and Storybook marker cards; generic probe data remains available in either state.
18
+
19
+ ## `OwnerMark___<type>___<name>___<package>___<variant>`
20
+ Declarative boundary marker near error/lazy boundaries. Often the closest thing to a source name in production.
21
+
22
+ ## `Connect(X)`
23
+ Redux wrapper. The wrapped name is the real component.
24
+
25
+ When reading avalanche traces:
26
+ - `Connect(X)` is usually transport, not ownership.
27
+ - Promote `X` or the nearest Jira-owned child/self-updater before ranking it as a source.
28
+ - If the trace only names `Connect(X)`, use its wrapped name as the business component label.
29
+
30
+ ## `ComponentWithCondition[]`
31
+ Feature-gate branch marker.
32
+
33
+ When reading avalanche traces:
34
+ - Ignore as a primary source.
35
+ - Treat as path metadata only: it explains which gated branch is active.
36
+ - Promote the nearest Jira-owned parent or child with concrete `whatChanged` / `hookChanges` evidence.
37
+ - Broad collateral under this node does not mean the gate owns the update.
38
+
39
+ ## `WithAnalyticsEvents(X)` / `WithAnalyticsContext(X)` / `WithAnalytics(X)`
40
+ Analytics wrappers. Usually structural noise.
41
+
42
+ ## `JiraUFOSegmentWrapper` → `SsrRenderProfiler` → `UFOSegment`
43
+ Performance instrumentation seam. Useful as a feature boundary marker.
44
+
45
+ ## `SuspensePlaceholder` → `SuspenseWrapper` → `OwnerMark___placeholder___X`
46
+ Lazy-loaded section marker.
47
+
48
+ When reading avalanche traces:
49
+ - `SuspensePlaceholder` is not a real business source.
50
+ - It artificially holds or releases child propagation during lazy/suspense transitions.
51
+ - Treat as environment/path metadata unless no Jira-owned source exists above or below it.
52
+ - Large collateral here usually means the subtree beneath it woke up; inspect the nearest Jira-owned child/self-updater next.
53
+
54
+ ## `Safe(X)`
55
+ Crash-safe wrapper.
56
+
57
+ ## `Container(X)`
58
+ SweetState/local shared state boundary. Strong hint for data origin.
59
+
60
+ When reading avalanche traces:
61
+ - Higher-value source candidate than wrappers/placeholders.
62
+ - If `Container(X)` is a self-updater with concrete field changes, keep it in the suspect list.
63
+
64
+ ## `ConnectedXxxRelayWithSuspense`
65
+ Relay/GraphQL data boundary.
66
+
67
+ When reading avalanche traces:
68
+ - Often a real data boundary, not just noise.
69
+ - Keep when export shows concrete field/store changes or repeated self-updates.
70
+ - If only suspense transport is visible, keep drilling to the Jira-owned child.
71
+
72
+ ## Avalanche ranking rules for Jira
73
+ - Filter helper nodes first: feature gates, placeholders, analytics wrappers, profiler seams.
74
+ - Do not equate blast radius with ownership. Broad hotspot can still be transport.
75
+ - Prefer the nearest Jira-owned self-updater with concrete `whatChanged` / `hookChanges` evidence.
76
+ - Repeated commits beat one broad placeholder event when picking real work.
77
+ - Parent/child environment still matters: keep helper nodes in notes to justify the branch shape, but not as the fix target.
78
+ - Before writing work items, normalize wrapper names to the business component that owns the update.
79
+
80
+ ## Noise to skip
81
+ `AnalyticsContext`, `AnalyticsListener`, `GenericAnalyticsListener`, `SsrRenderProfiler`, `ExperienceTrackingProvider`, single-character minified HOCs, and similar non-structural wrappers.
@@ -0,0 +1,122 @@
1
+ # Chippy over MCP
2
+
3
+ Use the local stdio MCP adapter when your agent host supports MCP tools. It starts
4
+ or reuses the helper automatically, so the agent does not need a shell command to
5
+ start a background server. The browser still needs the extension, module import,
6
+ or Storybook integration.
7
+
8
+ ## Connect your agent
9
+
10
+ Follow these steps when the user asks you to connect Chippy over MCP. They apply
11
+ to any agent host that supports stdio MCP servers.
12
+
13
+ 1. Resolve the absolute path of the directory that contains this skill's
14
+ `SKILL.md`. It may be a user-level or a project-local install; do not assume
15
+ `~/.agents/skills/chippy`. Confirm that `scripts/chippy-mcp.cjs` exists there. Check that `node --version` reports 22.12 or later.
16
+ 2. Register a stdio MCP server named `chippy` that runs `node` with that
17
+ absolute `scripts/chippy-mcp.cjs` path as its only argument. Use your host's
18
+ documented way to add an MCP server: its own command if it has one, otherwise
19
+ its MCP configuration file. Register it for the user, not one project, unless
20
+ the user asks otherwise.
21
+ 3. Merge the entry into the existing configuration. Keep the other servers, and
22
+ update an existing `chippy` entry instead of adding a second one.
23
+ 4. If your host loads MCP servers only at startup, tell the user to restart or
24
+ reload it.
25
+ 5. Verify the connection with `chippy_status`. If the Chippy tools do not
26
+ appear, report what you changed and use `scripts/chippy.sh` until they do.
27
+
28
+ If your host has no MCP support, skip these steps and use `scripts/chippy.sh`.
29
+
30
+ ## Server configuration
31
+
32
+ The server entry, in the common `mcpServers` JSON shape. Replace the example path
33
+ with the **absolute installed skill path**; JSON arguments do not expand `~`.
34
+
35
+ ```json
36
+ {
37
+ "mcpServers": {
38
+ "chippy": {
39
+ "command": "node",
40
+ "args": ["/absolute/path/to/chippy/scripts/chippy-mcp.cjs"]
41
+ }
42
+ }
43
+ }
44
+ ```
45
+
46
+ Node.js 22.12+ is required. The executable bundles its MCP dependencies and their
47
+ licenses; the installed skill needs no `npm install`. The Chippy npm library also
48
+ includes the `chippy-mcp` executable.
49
+ Host configuration syntax varies; the example shows the command and arguments
50
+ that the host must launch, not a file Chippy installs into your host.
51
+
52
+ Start with `chippy_status`, then `chippy_pages`. Select a page by URL and title,
53
+ read `chippy_snapshot`, and inspect the relevant probe or find components directly.
54
+ A ready helper with `browserConnected: false` means enable Chippy on a browser page,
55
+ not restart the server. The skill's replay, stimulus, evidence, and edit rules apply
56
+ to MCP calls exactly as they do to CLI calls.
57
+
58
+ ## Helper lifecycle and sandbox behavior
59
+
60
+ - New helpers bind to `127.0.0.1:7823`. Set `CHIPPY_HELPER_PORT` in the MCP process
61
+ environment to use another port (integer 1–65535); configure the browser's helper
62
+ URL to match. Set `CHIPPY_HELPER_HOST` (for example `0.0.0.0`) only when the
63
+ browser must reach the helper over the network. The adapter always connects to
64
+ loopback on that port.
65
+ - A matching helper version is reused. An incompatible response or occupied port
66
+ produces a tool error; Chippy does not replace that process. Stop or upgrade it
67
+ explicitly, or choose another port.
68
+ - The helper is a persistent shared service. Closing the MCP host/session closes
69
+ the stdio adapter but leaves the helper, probes, and other sessions available.
70
+ Default-port helpers can be stopped with the existing `chippy.sh stop` when no
71
+ session needs them. Chippy does not kill other helpers on shutdown.
72
+ - `chippy_status` returns `helperUrl`, `startedPid` (the child started by this MCP
73
+ process, otherwise null), `lifetime: "persistent"`, `browserConnected`, `pages`,
74
+ `hint`, and `ok`. An empty page list is normal setup state.
75
+ - The MCP process needs host permission to launch Node and connect to the helper.
76
+ MCP avoids reliance on the agent's shell sandbox only when the host launches
77
+ MCP servers with suitable access. It cannot bridge an isolated container to a
78
+ browser without networking/forwarding supplied by the environment.
79
+ - Logs use stderr; stdout contains only MCP messages. Requests are cancellable.
80
+ Helper errors are returned as MCP tool errors, not as successful empty evidence.
81
+
82
+ The adapter reads the helper's access token from `~/.chippy/helper-<port>.token`
83
+ (or `$CHIPPY_STATE_DIR`) on each request, so it keeps working after a helper
84
+ restart. `CHIPPY_HELPER_TOKEN` overrides the file. Reusing an existing helper does
85
+ not change its listening address.
86
+
87
+ ## Tools
88
+
89
+ All page tools require `clientId`. Resolve it afresh after navigation. Optional
90
+ `timeoutMs` defaults to 5000 and accepts integers 1–60000, except annotation waits,
91
+ which default to 30000. Structured results preserve the existing helper response
92
+ and are also included as JSON text.
93
+
94
+ | Tool | Arguments and behavior |
95
+ | --- | --- |
96
+ | `chippy_status` | No arguments; starts/reuses helper and reports browser readiness. |
97
+ | `chippy_pages` | No arguments; lists connected pages. |
98
+ | `chippy_snapshot` | `clientId`, `refresh` (default false). Reads stored evidence; true requests an asynchronous refresh, so read again after the page responds. |
99
+ | `chippy_probe` | `clientId`, `probeId`, `view`, optional `ownerIndex`, `prop`, `parentLevel`, `max`, `timeoutMs`. Views: `dom`, `parents`, `boundaries`, `owners`, `props`, `prop-value`, `hooks`, `hooks-summary`. `prop-value` requires `prop`. `owners` uses the existing live-owner route and cached-snapshot fallback. |
100
+ | `chippy_find_fibers` | `clientId`, `filters`, optional `timeoutMs`. Filters: `name`, `file`, `prop`, `testid`, `text`, `max`. Returns current fibers; refresh ids after commits or HMR. |
101
+ | `chippy_inspect_fiber` | `clientId`, `fiberId`, `view`, optional `hookIndex`, `windowSize`, `max`, `timeoutMs`. Views: `props`, `owners`, `parents`, `boundaries`, `hooks`, `hooks-summary`, `structure`, `describe`, `explain`. `structure` requires `hookIndex`. Replay-capable views can execute component/hook bodies. |
102
+ | `chippy_avalanche` | `clientId`, `action`, optional fields described below. Uses the existing Avalanche command builder. |
103
+ | `chippy_interact` | `clientId`, `action`, `target`, optional `targetKind` (default `selector`; alternatives `probeId`, `fiberId`), `propName`, `args`, `ownerIndex`, `timeoutMs`. Actions: `click`, `prop-contract`, `invoke`, `schedule-update`. `targetKind` selects click resolution; other actions take a fiber id as `target`. `prop-contract` and `invoke` require `propName`; `invoke` forwards a JSON argument array. Name the stimulus before execution and prove callback contracts before invoking them. |
104
+ | `chippy_wait_annotation` | `clientId`, optional `timeoutMs` (default 30000, maximum 60000). Consumes one UI intent/comment, waits once, or returns normal `status: "timeout"`. Repeat only while the user-requested loop remains active; cancellation closes the HTTP long poll. |
105
+
106
+ Avalanche actions: `status`, `install`, `uninstall`, `clear`, `export`,
107
+ `export-last`, `hot-sources`, `commit-list`, `commit-peek`, `debug-hook`,
108
+ `describe-action`. `install` starts capture; `clear` resets evidence;
109
+ `uninstall` stops capture. Optional `options` contains `detectionMode`
110
+ (`stats`, `sources`, `amplifiers`, `full`), `historySize`, and `captureFibers`.
111
+ Analysis fields are `limit`, `count`, `top`, `by`, `commitId`, `include` (array),
112
+ `depth`, `maxItems`, `sourceId`, `hookIndex`, `window`, `actionId`, and `timeoutMs`.
113
+ Use captured ids for commit, hook, and action analysis. See the
114
+ [Avalanche reference](avalanche-detector.md) for result semantics.
115
+
116
+ Indices are nonnegative integers; limits, counts, maxima, and window sizes are
117
+ positive integers, except Avalanche's `window`, which permits zero. Fields apply
118
+ to their selected view/action; for example, owner selection does not apply to the
119
+ owner-chain list. These nine tools cover the main inspection, capture, and UI-intent
120
+ loop. The CLI remains available for recipes, snippets, reload/arm workflows,
121
+ state comparisons, and other commands not exposed here. MCP does not change those
122
+ contracts or modify agent-host lifecycle hooks.
@@ -0,0 +1,102 @@
1
+ # Mode A injection contract — Playwright-controlled browser without the extension
2
+
3
+ Mode A is the Chippy/Avalanche injection path used when there is **no browser extension** — Playwright opens a browser, navigates a page, and the test runner is responsible for everything the extension would normally do in Mode B. Skipping a step silently degrades capture quality — usually shows up as `commitCount: 0`, `integrationMode: "none"`, or empty `commitStats`.
4
+
5
+ ## When this applies
6
+
7
+ - Headless verification under Vitest/Playwright/CI
8
+ - Agent-side automation that opens its own Chromium
9
+ - Any environment where the Chippy extension is **not** installed in the browser
10
+
11
+ If the user is using their own browser with the extension, this contract does NOT apply — the extension's `webNavigation.onCommitted` + `chrome.scripting.executeScript` path handles everything below.
12
+
13
+ ## Required bootstrap and install order
14
+
15
+ Mode A has two runtime-loading variants that end with the same explicit detector install. To record the initial load, use [pre-page start-up](#pre-page-start-up) instead; it installs the detector before React on every document.
16
+
17
+ 1. **Install the DevTools hook shim before navigation.** Use `page.addInitScript({ content: DEVTOOLS_HOOK_SHIM })` before `page.goto(...)`.
18
+ React-DOM checks `__REACT_DEVTOOLS_GLOBAL_HOOK__` at module-evaluation time. Installing the shim after navigation = no `currentDispatcherRef` = `replayComponentHooks` collapses to fiber-only fallback. The shim file is shipped with the Chippy skill at `extension/devtools-hook-shim.js`; the helper serves it at `/__chippy/devtools-hook-shim.js` (load that contents into the init script).
19
+ 2. **Load the Chippy and Avalanche runtimes using one variant:**
20
+ - **Packaged session loader:** call `installHelpers(page, { init: true })` before `page.goto(...)`. It registers the shipped helper scripts as init scripts, so they return on every navigation.
21
+ - **Ad-hoc / playwright-cli:** navigate first, then import `http://127.0.0.1:7823/__chippy/inject.mjs` (preferred), or add `inject.js` followed by `avalanche.iife.js`. The classic `inject.js` is intentionally split; bridge commands return `bridge module not loaded` until the IIFE is present.
22
+ 3. **Wait for the page client in `chippy.sh pages`.** A loaded global is not connection proof.
23
+ 4. **Run `chippy.sh avalanche-install <clientId>`.** Loading either runtime does not auto-install the detector in Mode A.
24
+ Auto-install is gated on `__CHIPPY_STARTUP_CONFIG.avalanche.autoStart`, which is written by the extension's MAIN-world script in Mode B. Neither loader above writes that startup configuration, so both must trigger install explicitly.
25
+
26
+ ## Pre-page start-up
27
+
28
+ Use this variant when the agent must record the initial page load without the extension. It writes the startup configuration that the extension writes in Mode B, then adds the early scripts and the page bridge as init scripts. Every document, including each reload, installs Avalanche before React DOM evaluates.
29
+
30
+ Register the init scripts before `page.goto(...)`, in this order. The files ship in this skill's `extension/` folder:
31
+
32
+ ```js
33
+ const read = (file) => fs.readFileSync(path.join(skillDir, 'extension', file), 'utf8');
34
+
35
+ await page.addInitScript(() => {
36
+ globalThis.__CHIPPY_STARTUP_CONFIG = {
37
+ enabled: true,
38
+ helperBaseUrl: 'http://127.0.0.1:7823',
39
+ avalanche: { autoStart: true },
40
+ };
41
+ });
42
+ await page.addInitScript({ content: read('devtools-hook-shim.js') });
43
+ await page.addInitScript({ content: read('avalanche.iife.js') });
44
+ await page.addInitScript({ content: read('early-bootstrap.js') });
45
+ await page.addInitScript({
46
+ content: ['inspect-react.js', 'panel-dom.js', 'layout-lab.js', 'resolve-probes.js', 'chippy-page-bridge.js']
47
+ .map(read)
48
+ .join('\n'),
49
+ });
50
+ await page.goto(url);
51
+ ```
52
+
53
+ - Skip the explicit `avalanche-install`; the detector is already installed and recording. `avalanche.options` accepts `detectionMode` and `captureFibers`, and `avalanche.max` sets the history size.
54
+ - To capture a load, call `page.reload()` and let the page settle. Do not call `avalanche-reset` between the reload and the read; it clears the boot commits.
55
+ - Read the capture with `chippy.sh avalanche-status <clientId>` and `avalanche-export` once the reloaded page appears in `chippy.sh pages`. If the client has not reconnected yet, read `window.__AVALANCHE__.exportLast()` in the page.
56
+
57
+ ## On reload (`page.reload(...)`)
58
+
59
+ - Init scripts survive reload. The packaged loader restores its runtimes automatically; the ad-hoc variant must load them again.
60
+ - The client id is kept in the tab's `sessionStorage`, so the reloaded page normally reconnects under the same id (`page-wait-reload` reports `reloaded: true`). The connection and the detector are new: repeat the explicit install.
61
+ - If the runtimes are not loaded again, the page never reconnects: `page-wait-reload` reports `reloaded: false`, and `reload-with-avalanche` exits with an error instead of reading status from a client that no longer exists.
62
+
63
+ ## Boot-time commit loss in the packaged and ad-hoc variants
64
+
65
+ This limit does not apply to [pre-page start-up](#pre-page-start-up). The ad-hoc `addScriptTag` variant loads the runtimes after page code. The packaged init-script variant loads them before page code, but its detector still remains uninstalled until the helper observes the new client and sends the explicit install.
66
+
67
+ Any React commit that fires between page mount and your `avalanche-install` call is **invisible to Avalanche**. This includes:
68
+ - initial mount
69
+ - `useEffect` cleanup teardowns
70
+ - Suspense fallback → content swaps
71
+ - route transitions on first paint
72
+
73
+ **Workaround:** use [pre-page start-up](#pre-page-start-up), or trigger the transition from the page after `avalanche-install`.
74
+
75
+ ## Failure signatures
76
+
77
+ | Symptom | Likely cause |
78
+ |---|---|
79
+ | `commitCount: 0`, `integrationMode: "none"` | explicit detector install was skipped, or no commit occurred afterward |
80
+ | `commitCount > 0` but `unmountedComponents: 0` despite expected unmounts | unmounts happened before detector install; trigger them post-install |
81
+ | `bridge module not loaded` from `find-fibers` / `describe-component` | classic bootstrap omitted `avalanche.iife.js` |
82
+ | `replayComponentHooks` returns flat fiber-only data, no custom-hook folders | DevTools hook shim was skipped or installed after navigation |
83
+ | `coverage > 100%` in `meta.memoizedUpdatersSupport` | early commits missed (this is the boot-time gap, not a wiring bug) |
84
+
85
+ ## Verifying that capture worked
86
+
87
+ - `chippy.sh avalanche-status <clientId>` → expect `installed: true`, `commitCount > 0`, and `integrationMode` matching the host (`adapter:atl` for a patched Atlassian host, `devtools` otherwise; earlier releases report `atl`), never `none` after a captured commit.
88
+ - In `avalanche-export`, compare `meta.commits` vs `meta.memoizedUpdatersSupport.commitsWithData` — they should match. If `commitsWithData > meta.commits`, early commits were missed.
89
+ - To tell the boot-time gap from a wiring fault, repeat the interaction after install. If that capture is also empty, the injection is broken; if only the pre-install interaction is missing, use [pre-page start-up](#pre-page-start-up).
90
+
91
+ ## Mode B reference (for contrast)
92
+
93
+ In Mode B, the extension does the following automatically and silently for every navigation:
94
+ 1. `webNavigation.onCommitted` fires
95
+ 2. Background fetches `armedAvalanche` config from helper
96
+ 3. `chrome.scripting.executeScript({ injectImmediately: true, world: 'MAIN' })` runs the early scripts (`devtools-hook-shim.js`, `avalanche.iife.js`, `early-bootstrap.js`). Because steps 2–3 wait on network lookups, a fast page can evaluate React first.
97
+ 4. iife.ts reads `__CHIPPY_STARTUP_CONFIG.avalanche.autoStart` and self-installs
98
+ 5. `chrome.tabs.onUpdated` (`status === 'complete'`) injects late bundle scripts (bridge + probes)
99
+
100
+ For a helper reload of an armed page (`page-reload` after `avalanche-arm`, as in `reload-with-avalanche`), the page bridge first stores the arm in `sessionStorage` and the extension registers the early scripts plus `startup-handoff.js` as a MAIN-world `document_start` content script for that one navigation. The next document installs Avalanche before any page script runs, so boot commits are captured. The reload ack's `earlyStartup` is `registered` when this path was prepared.
101
+
102
+ The packaged and ad-hoc Mode A loaders do not provide the extension's pre-page `autoStart` configuration. They install only after client connection; that post-connection detector start is the structural reason for their boot-time gap. Pre-page start-up writes the configuration itself and has no such gap.