@motujs/cli 0.0.0-stage → 0.1.0

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 (106) hide show
  1. package/LICENSE +21 -0
  2. package/bundled/github/agents/archipelago-build.agent.md +298 -0
  3. package/bundled/github/agents/island-create.agent.md +261 -0
  4. package/bundled/github/agents/island-extract.agent.md +360 -0
  5. package/bundled/github/agents/island-locate.agent.md +208 -0
  6. package/bundled/github/agents/motu-checks.agent.md +158 -0
  7. package/bundled/github/agents/motu-evidence.agent.md +267 -0
  8. package/bundled/github/agents/motu-regions.agent.md +304 -0
  9. package/bundled/github/host-rules.md +43 -0
  10. package/bundled/github/prompts/archipelago-build.prompt.md +19 -0
  11. package/bundled/github/prompts/island-create.prompt.md +37 -0
  12. package/bundled/github/prompts/island-extract.prompt.md +59 -0
  13. package/bundled/github/prompts/island-locate.prompt.md +43 -0
  14. package/bundled/packages/adapters/angularjs/vite.mjs +43 -0
  15. package/bundled/packages/adapters/next/lagoon/next-stubs.tsx +108 -0
  16. package/bundled/packages/adapters/next/vite.mjs +121 -0
  17. package/bundled/packages/adapters/vite/vite.mjs +179 -0
  18. package/package.json +64 -3
  19. package/src/alias-loader.mjs +63 -0
  20. package/src/alias-register.mjs +28 -0
  21. package/src/asset-stub.cjs +3 -0
  22. package/src/asset-stub.mjs +5 -0
  23. package/src/cli.mjs +78 -0
  24. package/src/commands/adopt-root.mjs +194 -0
  25. package/src/commands/archipelago-init.mjs +314 -0
  26. package/src/commands/archipelago.mjs +138 -0
  27. package/src/commands/assurance.mjs +40 -0
  28. package/src/commands/check.mjs +346 -0
  29. package/src/commands/codegen.mjs +29 -0
  30. package/src/commands/coverage.mjs +358 -0
  31. package/src/commands/create.mjs +277 -0
  32. package/src/commands/defaults.mjs +129 -0
  33. package/src/commands/diff.mjs +595 -0
  34. package/src/commands/fixtures.mjs +153 -0
  35. package/src/commands/init.mjs +473 -0
  36. package/src/commands/integrate.mjs +208 -0
  37. package/src/commands/integration.mjs +1349 -0
  38. package/src/commands/lagoon-try.mjs +272 -0
  39. package/src/commands/lagoon.mjs +1419 -0
  40. package/src/commands/link.mjs +110 -0
  41. package/src/commands/region-coverage.mjs +604 -0
  42. package/src/commands/removal-check.mjs +878 -0
  43. package/src/commands/skills.mjs +156 -0
  44. package/src/commands/snapshot.mjs +456 -0
  45. package/src/commands/sync.mjs +38 -0
  46. package/src/commands/verify/island-runtime.mjs +581 -0
  47. package/src/commands/verify/island-static.mjs +586 -0
  48. package/src/commands/verify/island.mjs +135 -0
  49. package/src/commands/verify/reach.mjs +426 -0
  50. package/src/commands/verify/region-composition.mjs +391 -0
  51. package/src/commands/verify/region-config.mjs +330 -0
  52. package/src/commands/verify/region-flows.mjs +760 -0
  53. package/src/commands/verify/region-sources.mjs +516 -0
  54. package/src/commands/verify/region.mjs +271 -0
  55. package/src/commands/verify/report.mjs +338 -0
  56. package/src/commands/verify/screens-runtime.mjs +123 -0
  57. package/src/commands/verify.mjs +18 -0
  58. package/src/lagoon-build.mjs +25 -0
  59. package/src/lagoon-dev.mjs +32 -0
  60. package/src/lib/alias-core.mjs +49 -0
  61. package/src/lib/archipelagos.mjs +111 -0
  62. package/src/lib/assurance.mjs +144 -0
  63. package/src/lib/baselines.mjs +107 -0
  64. package/src/lib/bundlability.mjs +123 -0
  65. package/src/lib/changed.mjs +157 -0
  66. package/src/lib/component-props.mjs +145 -0
  67. package/src/lib/config.mjs +451 -0
  68. package/src/lib/contracts.mjs +244 -0
  69. package/src/lib/data-flow.mjs +47 -0
  70. package/src/lib/declared-sources.mjs +101 -0
  71. package/src/lib/dock-inject.mjs +47 -0
  72. package/src/lib/effects.mjs +97 -0
  73. package/src/lib/eject.mjs +715 -0
  74. package/src/lib/evidence-json.mjs +74 -0
  75. package/src/lib/expect-calls.mjs +53 -0
  76. package/src/lib/finding-memory.mjs +0 -0
  77. package/src/lib/flow-calls.mjs +53 -0
  78. package/src/lib/framework.mjs +98 -0
  79. package/src/lib/host-rules.mjs +55 -0
  80. package/src/lib/host-sources.mjs +121 -0
  81. package/src/lib/island-placement.mjs +64 -0
  82. package/src/lib/island-render.mjs +105 -0
  83. package/src/lib/islands.mjs +112 -0
  84. package/src/lib/lagoon-bundle.mjs +138 -0
  85. package/src/lib/lagoon-declares.mjs +553 -0
  86. package/src/lib/lagoon-materialize.mjs +217 -0
  87. package/src/lib/lagoon-vite.mjs +788 -0
  88. package/src/lib/live-url.mjs +53 -0
  89. package/src/lib/node-aliases.mjs +73 -0
  90. package/src/lib/operations.mjs +145 -0
  91. package/src/lib/provenance-plugin.mjs +74 -0
  92. package/src/lib/region-refs.mjs +40 -0
  93. package/src/lib/remote.mjs +171 -0
  94. package/src/lib/scaffold.mjs +606 -0
  95. package/src/lib/screens.mjs +266 -0
  96. package/src/lib/snapshots.mjs +85 -0
  97. package/src/lib/stubs.mjs +114 -0
  98. package/src/lib/ts-project.mjs +42 -0
  99. package/src/lib/util.mjs +626 -0
  100. package/src/lib/worlds.mjs +236 -0
  101. package/src/lib/write-if-changed.mjs +35 -0
  102. package/src/lib/writes-coverage.mjs +32 -0
  103. package/src/playwright-lagoon.mjs +2305 -0
  104. package/src/run.mjs +385 -0
  105. package/src/runtime-harness.mjs +225 -0
  106. package/README.md +0 -3
@@ -0,0 +1,360 @@
1
+ ---
2
+ name: island-extract
3
+ description: Turn UI the application ALREADY HAS into a motu island, and iterate against `motu island verify` until it passes. Two shapes, chosen by the project's `host` — on a React host (next/vite/none) the island WRAPS the component the app owns, and no component is written; on an AngularJS ocean host there is nothing to wrap, so the legacy source (controller + template, JSP) is rewritten as a mode-agnostic React component. The CLI owns everything deterministic (scaffolding, config edits, verification); this agent owns the judgement. Invoke by asking to "make <component> an island", "extract <component> into a motu island", or `copilot --agent=island-extract`.
4
+ ---
5
+
6
+ # island-extract — Custom Agent
7
+
8
+ You take one piece of UI the application **already has** and make it a **motu island**: something
9
+ that renders in the *lagoon* against fixtures, alone, and only later gets integrated into the real
10
+ page. You do the *judgement* work; the `motu` CLI does the *deterministic* work — scaffolding, config
11
+ edits, and verification. **Lean on `motu island verify`: it is the loop you close on, not your own
12
+ eyeballing.**
13
+
14
+ Which judgement that is depends on Step 0, and getting it wrong is the one mistake this agent can make
15
+ that no check will catch — a forked component drifts silently, and a rewritten one is a second copy
16
+ nobody diffs.
17
+
18
+ Iterate with `--fast` (happy-dom, no browser); spend the browser once at the end with `--runtime`, and
19
+ `--audit` before integrating. See "Which level, when" in `island-create.agent.md`.
20
+
21
+ Read `README.md` first (the loop), then `docs/01-concepts.md` (terminology) and `docs/15-design-and-limits.md` (non-goals, limits).
22
+ Do **not** invent rules — the authoritative, mechanical rule set is whatever `motu island verify`
23
+ enforces. Never work around a verify failure; fix the island.
24
+
25
+ ## Step 0 — WRAP or REWRITE? Read it from the config, do not judge it
26
+
27
+ `motu.config.json`'s **`host`** decides, because it answers whether a React component for this region
28
+ already exists. Read it before anything else; the rest of this agent forks on the answer.
29
+
30
+ | `host` | shape | what you produce |
31
+ | --- | --- | --- |
32
+ | `next` / `vite` / `none` | **WRAP** | no component at all — the island points at the one the app owns |
33
+ | `angularjs` | **REWRITE** | a mode-agnostic React component under `ui/`, authored from legacy source |
34
+
35
+ **WRAP is the default on any React host, and copying is the error there.** `island('x-tag', Component)`
36
+ points at the application's own component; duplicating it into `ui/` forks it and lets the two drift,
37
+ which is the opposite of migrating incrementally. A wrapper that exists only to install providers or
38
+ draw chrome is motu-only code sitting in someone's repository — exactly what adopting motu is supposed
39
+ to avoid. If you believe a wrap genuinely needs a wrapper, say why in your report before writing one.
40
+
41
+ **REWRITE only applies where there is nothing to wrap.** An AngularJS controller plus a JSP partial is
42
+ not a React component, so one has to be authored. `demo-app/src/ui/` is what that looks like.
43
+
44
+ `isolation` is NOT this discriminator, however it reads: it selects shadow vs light DOM, a styling
45
+ decision, and all three reference projects are `light` — including the AngularJS one.
46
+
47
+ ## The anatomy you are producing
48
+
49
+ An island is a **mount point** plus, only in REWRITE, a **ui component**:
50
+
51
+ 1. `src/islands/<kebab>.island.ts` — the mount-point registry row: `tag` → component + the `contract`
52
+ (input / output / coupling), plus the `legacy` fit strategy where the host has a legacy skin. In
53
+ WRAP this is the ONLY code file you produce.
54
+ 2. `src/ui/<kebab>/<Pascal>.tsx` — **REWRITE only.** The plain, mode-agnostic component (props in,
55
+ callbacks out). Lives in `ui/` (the "mainland") so mount points can never import each other — `ui/`
56
+ may import `@motujs/contract`, `shared/`, and other `ui/`, but never `islands/` or `archipelagos/`.
57
+ 3. `src/islands/<kebab>.evidence.ts` — the island's scenarios, and its fixtures where it fetches.
58
+ 4. `src/archipelagos/<id>/<id>.archipelago.ts` — archipelago membership (only at integrate).
59
+
60
+ ## Step 1 — Read what exists
61
+
62
+ ### WRAP — find the component, and what it cannot render without
63
+
64
+ Locate the component the app owns and the specifier the app itself imports it by (`@/components/foo`,
65
+ a relative path) — that string is what the island will point at, so use the app's own spelling.
66
+
67
+ Then answer the question the lagoon will ask, which is the whole cost of this step: **what does it need
68
+ that a bare mount does not give it?** Walk the component and everything it renders for:
69
+
70
+ - **Providers** — a context, a theme, a query client, an i18n root, a store `<Provider>`. These are what
71
+ it *cannot render without*.
72
+ - **Its own data access** — a fetch, a hook that fetches, a GraphQL query. In the lagoon that is answered
73
+ by a channel or the mock transport, never by letting it reach the network.
74
+ - **Host ambient** — router, session, `document` reach-out, a store read with no prop.
75
+ - **Required props with no default.** An island must render from defaults alone; a required prop is a
76
+ default you have to add or a value that belongs in `seed`.
77
+
78
+ Write these down as a list. It is the input to Step 5, and it is the honest answer to "how much does
79
+ adopting motu cost on this component" — usually a few lines, occasionally a reason not to start here.
80
+
81
+ ### REWRITE — read the legacy component
82
+
83
+ Locate the ocean source (ask the developer for the path if unclear). For an ocean-app AngularJS
84
+ screen that is typically:
85
+
86
+ - the controller/JS: `~/dev/ocean/web-console/src/main/webapp/**/<name>.js` (or `test-console/...`),
87
+ - the template/partial: `~/dev/ocean/web-console/src/main/webapp/dist/partials/**/<name>.html`,
88
+ - the backing REST/service Java it calls.
89
+
90
+ Identify: what the component renders, what user actions it emits, and **every server call** it makes
91
+ (`$http`, `$resource`, a REST endpoint). Write these down — they drive the contract mapping.
92
+
93
+ ## Step 2 — Scaffold
94
+
95
+ ```bash
96
+ # WRAP — point at the component the app owns; nothing is written under ui/
97
+ motu island create <name> --from '@/components/members/MemberNotes' [--export MemberNotes]
98
+
99
+ # REWRITE — scaffold a component stub under ui/ to author into
100
+ motu island create <name> # kebab, e.g. member-notes
101
+ ```
102
+
103
+ `--from` takes the specifier **the application itself uses**; `--export` names the component inside
104
+ that module when it is not the island's Pascal name. Either way this writes the registry row and the
105
+ evidence stub and prints the paths. If you find yourself scaffolding a `ui/` component on a React host
106
+ and then importing the app's component into it, stop: that is the fork, and `--from` is the answer.
107
+
108
+ ## Step 3 — Data access
109
+
110
+ **WRAP:** the component keeps whatever data access the app gave it — you are not rewriting it. What
111
+ changes is who ANSWERS in the lagoon: a `channel` for anything that reacts, the app's own captured
112
+ mock data for what it reads. If the app's transport is not motu's, `observeForeignTransport` lets the
113
+ lens show what actually fired, and `unservedOperations()` catches the handler you forgot — the failure
114
+ that renders an empty page and looks like a working one. Skip the rest of this step; there is no
115
+ generated contract on a React host.
116
+
117
+ **REWRITE (ocean host):** islands never do bare `fetch`/`$http`. All server I/O goes through
118
+ `@motujs/contract` (`contract/src/index.ts`). For each legacy call:
119
+
120
+ - If a matching `@BrowserCallable` method already exists in the contract, use it.
121
+ - If not, annotate the CDI service method in the backend with `@BrowserCallable` (and `@Roles` as
122
+ appropriate — motu never reimplements authorization), rebuild the backend so `dev.motu:apt` emits a
123
+ fresh `motu-manifest.json`, then regenerate:
124
+
125
+ ```bash
126
+ motu codegen # regenerates contract/src from the built manifest
127
+ ```
128
+
129
+ Prefer reusing an existing browser-callable method over exposing new surface. Only `@BrowserCallable`
130
+ methods are ever reachable — everything else stays invisible by design.
131
+
132
+ ## Step 4 — The component
133
+
134
+ ### WRAP — write NO component
135
+
136
+ There is nothing to author here, and that is the point: the island IS the application's component.
137
+ What Step 1 catalogued does not go into a wrapper, it goes into the lagoon override, and each piece has
138
+ exactly one right home:
139
+
140
+ - what the component **cannot render without** → `providers` in the lagoon overrides, installed in
141
+ EVERY view, per island;
142
+ - the **arrangement** of a region → the archipelago's `root` where the region has one, otherwise
143
+ `layout` in the overrides, which points at the application's own layout component and is region-view
144
+ only (see "Two ways a region composes" below — an extraction normally uses `layout`);
145
+ - the **data** — a widget row, props, a record → `seed`;
146
+ - anything that **reacts** — the stand-in for the page's fetch → a `channel`, installed in every view,
147
+ so the checks that drive the region see the same answers a human does.
148
+
149
+ Get this split wrong and the failure is invisible in the region view and fatal in the mountpoints view
150
+ — the one the flow checks drive — so it reads as "the region rendered nothing" rather than "no
151
+ providers". If a region renders in one view and not the other, suspect this before anything else.
152
+
153
+ **Providers must be IDEMPOTENT.** The frame installs them for its own chrome and each island installs
154
+ them for itself, so they nest. Fine for context providers, fatal for a `<Router>` — React Router throws
155
+ and every card renders as "Invalid Configuration". Gate the ones that cannot nest
156
+ (`useInRouterContext()`).
157
+
158
+ Two things the component itself may still need, and they are edits to the APP, not to a copy of it:
159
+ a **default for every prop** (an island must render from defaults alone), and `reads: ['key']` on the
160
+ island when it subscribes to a host store with no prop — otherwise the key is written by an island,
161
+ read by nobody motu knows about, and `coupling` reports a real coupling as one that escapes.
162
+
163
+ ### REWRITE — write the component body
164
+
165
+ Translate the legacy template + behaviour into `src/ui/<kebab>/<Pascal>.tsx`. It MUST obey
166
+ the island rules (verify enforces them):
167
+
168
+ - **Mode-agnostic:** no `fetch`/`XMLHttpRequest`, no `history`/`pushState`/`location`, no
169
+ `document.querySelector` into the host. All server I/O via `@motujs/contract`.
170
+ - **Props in, callbacks out.** Outward navigation/actions are *intents*: expose an `onXxx` callback
171
+ (wired later to `host.navigate` / `host.action`), never touch the URL yourself.
172
+ - **Renders from default props alone** — give the props param a default (or destructure with defaults),
173
+ so the lagoon can mount it with nothing. This is what makes the lagoon a real test.
174
+ - Handle the `403` (`MotuError`) case like `CompanyLookup` does when a `@Roles` method is denied.
175
+
176
+ Use the existing islands (`CompanyLookup`, `MemberResults`, `MemberSearch`) as the reference for style,
177
+ error handling, and the debounce/sequence pattern.
178
+
179
+ ## Step 5 — Evidence (and, on an ocean host, the legacy fit)
180
+
181
+ Both shapes need **scenarios**: two or more seeds whose output DIFFERS, or `motu island verify`'s
182
+ data-flow check fails — that check exists because fake evidence is worse than none. An island with no
183
+ props at all still gets a scenario; drive it through a channel or a stubbed host module instead.
184
+
185
+ **WRAP: do not hand-write fixtures the app already has.** Its own tests and stories need the same data,
186
+ so a repo usually carries a capture plus a script that refreshes it (Twenty: `scripts/mock-data/` →
187
+ `testing/mock-data/generated/`). Point at THAT. Both sides are then the app's artifacts and motu only
188
+ compares; invented data in a lagoon frame is a third copy nobody diffs. Type any shared evidence module
189
+ against the app's own types with `import type`, and import it with a RELATIVE specifier — `@/` does not
190
+ resolve in the loaders that read evidence files, and that failure is silent.
191
+
192
+ On an ocean host, additionally:
193
+
194
+ - Fill `src/islands/<kebab>.evidence.ts` with responses whose **shape matches the
195
+ contract return types** (see the method's `call<…>()` return in `@motujs/contract`). Include the
196
+ `roles` a caller needs so role-gated paths can be demoed offline.
197
+ - For REACTIVE behaviour (e.g. type a filter → results narrow), a fixture `response` may be a
198
+ **function of the call args** — `response: (args) => filter(dataset, args[1])` — a client-side STUB
199
+ (not backend fidelity) so ANY input works offline, not just recorded ones. Declare `scenarios` (two+
200
+ seeds whose output differs) so `motu island verify`'s data-flow check proves the seam.
201
+ - The island's `contract` declares INPUT (`input` props), OUTPUT (`output` events), and COUPLING
202
+ (`coupling.hostScope`/`hostScopeKey`/`adopt`/`inheritScope` — only for AngularJS islands reaching
203
+ host scope). Set the required `legacy` strategy: `fill`/`inline` are CSS-only (most islands);
204
+ `structural` means the component reads the injected `fit` prop and branches (see `MemberActions`).
205
+
206
+ ## Step 6 — Close the loop in the lagoon
207
+
208
+ ```bash
209
+ motu island verify <name> # static + config + REAL-browser lagoon mount
210
+ ```
211
+
212
+ The runtime layer boots the focused lagoon and mounts the island in **Playwright/Chromium**, so real
213
+ layout/CSS/paint are checked — not just an in-process DOM. One-time setup for the browser:
214
+ `cd packages/cli && npx playwright install chromium`. Use `--fast` for a quicker in-process happy-dom
215
+ mount when iterating, but let the real-browser check be the gate before you integrate.
216
+
217
+ Fix every `✗` and re-run until it prints **PASS**. For visual iteration, run the lagoon and open it:
218
+
219
+ ```bash
220
+ motu lagoon serve --watch # the gallery: every archipelago + a switcher, mock data
221
+ motu lagoon serve --watch --host # ...as one self-contained page, reloading, openable on a phone
222
+ # the single-island focus (what verify drives) is lagoon.html with MOTU_TARGET=island:x-<name>
223
+ ```
224
+
225
+ Do not proceed while verify is red. `--json` gives a machine-readable report if you are scripting.
226
+
227
+ ## Step 7 — Integrate
228
+
229
+ Pick (or create) the archipelago for the page:
230
+
231
+ ```bash
232
+ motu archipelago init <page-id> # if the page has no archipelago yet
233
+ motu island integrate <name> --archipelago <page-id> [--slot <slot>]
234
+ ```
235
+
236
+ Then:
237
+
238
+ - Fill the `TODO(motu:wiring)` in the new `IslandSpec`: `bind` element props to shared store keys, and
239
+ handle the island's events (`on`) by writing the store or firing a `host` intent.
240
+ - Place the island where the page should mount it — the one stack-specific edit the CLI does not do.
241
+ On a React host the page composes the region with `createRegion`, places each declared slot and reads
242
+ it back with `useRegion()`; on an ocean host it is a `<motu-island slot="…">` marker in the legacy
243
+ template, and for progressive replacement you wrap the original fragment with `legacy-toggle="true"`
244
+ so it defaults to legacy and can be toggled per slot.
245
+ - Then run `motu integrate check` — the last mile the lagoon cannot answer. The one that bites on a
246
+ React host: has the page stopped keeping its own `useState` of a key the island now produces?
247
+ - If the archipelago layout is an imported constant, add the marker to that layout too (integrate prints
248
+ the exact marker when it can't edit it automatically).
249
+
250
+ **Say what the change did to the flows.** `motu diff --base <branch point> --json` replays the
251
+ flows of every region you touched, at the base and now, and lists what moved (keys, what each slot
252
+ renders, backend calls, declarations). For each finding, fix it or explain it in an `--explain` file
253
+ (`{ "<id>": "<one line: why it was intended>" }`) until it exits 0, then quote the explanations in your
254
+ report. A reader of your diff sees code; this is the only place they learn which behaviour you meant to
255
+ change.
256
+
257
+ ## Step 8 — Verify green + typecheck
258
+
259
+ ```bash
260
+ motu island verify <name> # expect PASS (membership now satisfied)
261
+ motu integrate check <region> # does the HOST compose, place and read the region?
262
+ motu island snapshot --all --remote --changed # did anything you touched MOVE? (~11s scoped)
263
+ motu archipelago snapshot --all --remote --changed # did the composed page move? (~15s scoped)
264
+ ```
265
+
266
+ The two snapshot runs are the last thing you do. Wrapping an existing component often touches shared
267
+ code on the way — a default, a class, a token — and that lands on islands you never opened. LOOK at any
268
+ `.motu/snapshots/*.diff.png`, then `--accept` only what you meant.
269
+
270
+ Then the host's OWN build or typecheck (`pnpm typecheck` in this monorepo). motu checks its own
271
+ declarations; only the app's build can tell you the app still compiles.
272
+
273
+ ## Two ways a region composes, and which one an extraction uses
274
+
275
+ A region's arrangement lives in ONE of two places, and both are supported:
276
+
277
+ - **`root` on the archipelago** — the application's own layout component, with `slots` mapping its
278
+ props to this region's islands. The page renders `<X.Root results={…} />` using its own prop names
279
+ and never writes a slot; the lagoon renders the SAME component from the SAME map. There is no second
280
+ description, so the two cannot differ. Safe by construction.
281
+ - **A hand-written lagoon frame** (`layout` in the overrides) — a second description of the page,
282
+ checked but not eliminated: `island-composition` compares WHICH islands the region is made of
283
+ against what the page places, and `region-root` refuses arrangement the frame invented. Nothing
284
+ compares the ARRANGEMENT itself.
285
+
286
+ **An extraction uses the frame, and that is correct rather than a concession.** You are working on a
287
+ page that already exists and already expresses its own arrangement in JSX. Moving it to `root` is a
288
+ region-level refactor of the host's own code; doing it in the same step as pulling out one island
289
+ couples two changes and hides the risky one. Extract the island, look at it in the lagoon, then decide
290
+ about the region.
291
+
292
+ THAT DECISION IS A STAGE, NOT A VERDICT: adopt as a thin overlay (islands in the page's own JSX +
293
+ a frame), migrate one region to `root` when you already have a reason to open its page, then set
294
+ `"regionRoot": "required"` when the last one is done. The archipelago-build skill has the arc and
295
+ the two hazards a migration carries that no check sees. A frame that genuinely must draw says so
296
+ once with `inventedArrangement('why', <…/>)` — a warning, and warnings do not fail `motu check`.
297
+
298
+ `motu archipelago init <id>` without `--page` is the opposite case and scaffolds `root` first: a NEW region has no page to
299
+ restructure, so the safe shape is free there.
300
+
301
+ **If the region ALREADY declares a `root`**, `motu island integrate` adds the slot to `slots` for you
302
+ and prints the two things it cannot derive — the prop to add to the root component, and the line the
303
+ page must pass. Do both, or the island is declared and never placed.
304
+
305
+ `region-root` reports which shape a region is in on every run. A project that has finished migrating
306
+ sets `"regionRoot": "required"` in `motu.config.json`, and a frame becomes an error from then on.
307
+
308
+
309
+ ## Guardrails
310
+
311
+ - **On a React host, never copy the component.** `--from` exists so the island points at what the app
312
+ owns. A `ui/` copy forks it, and nothing checks that the two still agree.
313
+ - **Do not write a wrapper to install providers or draw chrome.** Providers go in the lagoon override,
314
+ arrangement in the region's `root` or its `layout`, data in `seed`. A wrapper is motu-only code in
315
+ the app's repository.
316
+ - **Never invent arrangement in a frame.** A frame may hold only the application's own components,
317
+ fragments and `island(slot)`. `region-root` fails on an intrinsic element or a literal string,
318
+ because a frame that draws its own version of the page drifts from it — acme shipped a lagoon saying
319
+ "On récupère ton accès" over a page saying "Mot de passe oublié ?" for weeks, entirely green.
320
+ - Never add runtime module loading, federation, per-island versioning, or island-to-island imports
321
+ (see README "Non-goals"). Islands coordinate only through the archipelago store and DOM events.
322
+ - Never widen backend surface beyond the specific `@BrowserCallable` method you need.
323
+ - Keep the motu terminology (island/ocean/archipelago/lagoon/mainland) in prose only — imports and
324
+ type names stay literal.
325
+ - REWRITE only: the end state is the mainland — the component should de-wrap trivially, so keep it a
326
+ plain component. In WRAP the component is already the app's own and there is nothing to de-wrap.
327
+
328
+ ## Before you hand it over: motu did not test your logic
329
+
330
+ `motu check` is green when the region COMPOSES — islands placed, declared couplings carrying, every
331
+ state rendering. It runs no typecheck and no test runner. Two things are yours:
332
+
333
+ - **logic inside the component** (a filter that stops matching a field, `some` for `every`, an
334
+ off-by-one) — renders fine, composes fine, passes everything here;
335
+ - **the path from a user's action to a declared output** — a flow that EMITS the island's declared
336
+ event enters the region past the component's own handler, so a handler that drops its argument
337
+ passes it. Write `{ click: '<accessible name>' }` instead when the control has one: the component
338
+ fires its own output, by the path a person takes. Accessible name only — no selectors, no `fill`,
339
+ no waits. Anything beyond one click is Playwright's job, not motu's.
340
+
341
+ Measured on a real screen: of eight injected regressions, the project's unit tests caught five and
342
+ motu's flows caught one — and that one was a render assertion the tests missed. Different
343
+ instruments. If the component you just wrote or wrapped carries logic, say in your handover that it
344
+ needs unit tests in the host's own runner, and do not present a green `motu check` as coverage.
345
+
346
+
347
+ ## Before you hand this over
348
+
349
+ A person has to be able to OPEN what you built. Start (or confirm) the detached lagoon, which
350
+ announces itself to the shared host under a slug scoped to your branch:
351
+
352
+ motu lagoon serve --watch --detach
353
+ motu lagoon states # the address of each declared state
354
+
355
+ **End your report with the live URL and the one or two states you want looked at.** A passing check
356
+ describes files; the work is a rendered region, and until someone opens it nobody has seen it. Every
357
+ `motu check` verdict now prints the live URL, or how to start one if there is none.
358
+
359
+ And say plainly what motu did not check: the host's typecheck and its test runner, both of which are
360
+ yours to run.
@@ -0,0 +1,208 @@
1
+ ---
2
+ name: island-locate
3
+ description: Survey ONE page and locate its island candidates — which regions should become islands, which stay host, what the page's shared state really is, and whether the page itself becomes an archipelago. Produces a LOCATE REPORT (ranked candidates, region keys with owners, flows to declare, what integration must still confirm) and creates no files — it is the reading step that island-create / island-extract execute. Invoke by asking to "locate the islands on <page>", "is this page an archipelago?", or `copilot --agent=island-locate`.
4
+ ---
5
+
6
+ # island-locate — Custom Agent
7
+
8
+ You read **one page** and decide what motu should be told about it: which of its regions are
9
+ **islands**, which stay **host**, what the page's **shared state** actually is and who owns each key,
10
+ and whether the page becomes an **archipelago**. You produce a **report**, not files. `island-create`
11
+ and `island-extract` are the verbs that act on it; this agent is the judgement that comes first, and it
12
+ is the step people skip — which is how a page ends up with an archipelago drawn around a DOM subtree,
13
+ a key nobody reads, or a "coupling" that was only ever prop drilling.
14
+
15
+ Read `README.md`, `docs/01-concepts.md` (terminology) and `docs/15-design-and-limits.md` (non-goals, limits) and
16
+ the host's `CLAUDE.md` motu rules block, with the `motu-regions` skill for the full rules on island vs
17
+ host, ownership and the survey. Do not invent rules: the mechanical rule set is whatever
18
+ `motu check` enforces. Nothing here creates, moves or refactors code — **locating is read-only**.
19
+
20
+ ## Step 0 — Learn this project's shape before reading the page
21
+
22
+ **If the project has no `motu.config.json` yet, that is normal — surveying is what happens BEFORE
23
+ adoption.** Locating a page is read-only and decides whether adopting is worth it, so it is routinely
24
+ run against a repo motu has never touched. Measured on two cold-start adoptions: this step said "read
25
+ `motu.config.json`", the file did not exist, and the agent improvised the whole survey from the page's
26
+ file layout without knowing that was allowed.
27
+
28
+ When there is no config, answer the same questions from the repository itself, and say in the report
29
+ that you did:
30
+
31
+ - the paths — from where the app already keeps its components. There is no `islands`/`ui`/
32
+ `archipelagos` yet; name where they WOULD go and let `motu init` settle it.
33
+ - `tagPrefix` — assume `x-`, the default `init` writes.
34
+ - **`host`** — from the app's own build: a `next.config.*` means `next`, a `vite.config.*` means
35
+ `vite`, an AngularJS app means `angularjs`, otherwise `none`. This is the one answer that changes
36
+ the recommendation, so derive it rather than guess it.
37
+ - `isolation` — irrelevant to this decision either way (see below).
38
+
39
+ With a config present, it is the answer to "where does anything go here", and it differs per repo:
40
+
41
+ - `islands` / `ui` / `archipelagos` / `shared` / `contract` — the paths you will name in the report.
42
+ - `tagPrefix` — the tag namespace (`x-` → `x-week-actions`).
43
+ - `host` — **this changes the recommendation you make**, because it answers whether a React component
44
+ for this region ALREADY EXISTS:
45
+ - `next` / `vite` / `none` → the app owns its components. The island WRAPS one
46
+ (`island('x-week-actions', WeekActionsView)`) and it stays where it lives. Never propose lifting it
47
+ into `ui/`: a copy forks it, and the two drift — which is the opposite of migrating incrementally.
48
+ - `angularjs` → there is no React component to wrap, so one has to be authored under `ui/` (the
49
+ mainland). That is the reference ocean's shape, and `demo-app/src/ui/` is what it looks like.
50
+ - `isolation` is NOT this discriminator, however it reads. It selects shadow vs light DOM — a styling
51
+ decision — and all three reference projects are `light`, including the AngularJS one. Branching on
52
+ it told an agent surveying `demo-app` never to lift a component into `ui/`, in the project whose
53
+ every island is exactly that.
54
+ - `hostRoot` — the app the page belongs to.
55
+
56
+ Then read what already exists: the archipelago registry, the sibling regions (they are the style guide
57
+ for the report you are about to write), and `islands/` — a candidate may already be an island, or a
58
+ component next door may already be one you can reuse.
59
+
60
+ ## Step 1 — Enumerate what the page renders
61
+
62
+ Read the page top-down and list every region a user could point at: a panel, a banner, a card, a
63
+ navigator, a row of controls, an empty state, an error state. Name each one, with its file and the
64
+ props it receives. Two rules while you enumerate:
65
+
66
+ - **Enumerate what is RENDERED, not what is defined.** A region that only ever appears inside a
67
+ conditional is still a region (its empty/error state is usually a scenario, sometimes its own island).
68
+ - **Follow every prop to its source.** You cannot classify what you have not traced. If the repo ships
69
+ a flow-mapping tool, use it to map what feeds the page before you read the props one at a time.
70
+
71
+ ## Step 2 — Classify: island, or host?
72
+
73
+ The discriminator is **what the region does**, not where it sits in the DOM:
74
+
75
+ | Verdict | The tell |
76
+ | --- | --- |
77
+ | **Island** | It SHOWS content or ACTS ON the region — it has a state a user would recognise. |
78
+ | **Island** | It OWNS region state, *even if other islands sit inside it* (a navigator that holds the selected week). DOM nesting says nothing about ownership; `slots` declares which island fills which of its props. |
79
+ | **Host** | It only ARRANGES — a grid, a column, a card shell, a `<section>` wrapper. |
80
+ | **Host** | An overlay/portal anchored to a DOM id anywhere on the page. |
81
+ | **Host** | The composition root itself: the fetching, the seeding, the routing. |
82
+
83
+ A region you cannot state a *scenario* for — "with data / empty / error" — is usually arrangement.
84
+ When you hesitate between "one island" and "three", ask which unit a human would look at in the lagoon
85
+ and say "that is wrong": that is the island.
86
+
87
+ ## Step 3 — Find the real couplings (this is the whole job)
88
+
89
+ For every value the page holds, decide which of **three** things it is. Most pages are a MIX, and the
90
+ report is worthless if it does not say which is which:
91
+
92
+ 1. **Island-owned** — an island's act updates it. → the island's `writes` (event → key). The tell you
93
+ are looking for: *the page computes something out of what island A reported and hands it to island
94
+ B*. That is the laundering the ownership rules exist to stop — declare the output instead.
95
+ 2. **Host-fed (derived)** — the page fetches/computes it and the island renders it. → a `bind`, and it
96
+ is NOT a `writes`. Ownership is about updates, not first paint: a key the page seeds and an island
97
+ updates afterwards is still the island's.
98
+ 3. **Page-local** — nothing else reads it. → it is not region state. Do not put it in the region.
99
+
100
+ Then, per candidate, write the pairs down: *who reads it* and *who writes it*. A key with one reader
101
+ and one writer that are the same island is not shared state. A key with no reader is not a key.
102
+ Say explicitly which couplings are genuine **island-to-island**, and which are just the host feeding
103
+ the region — a page with zero island-to-island keys is perfectly ordinary, not a failure.
104
+
105
+ Watch for the couplings that are invisible in the JSX because they travel through a context, a hook, a
106
+ store or a URL param. Those are exactly the ones an archipelago is supposed to make visible.
107
+
108
+ ## Step 4 — Rank the candidates
109
+
110
+ Score each candidate on two axes and rank by both. Cite the evidence for every score — a line number,
111
+ an import, a prop — never an impression.
112
+
113
+ **Value (why bother):** it carries a real coupling · it is the page's actual content · it has states
114
+ that keep breaking · it is reused on other pages · it is about to change.
115
+
116
+ **Cost (what it will fight you on):** does its own data fetching · reads host session/context/router ·
117
+ reaches `document` outside itself · takes a prop drilled from far above · cannot render from defaults
118
+ alone (every input must be optional, with a default) · one component doing four things · states that
119
+ cannot be reached from inputs (so no honest scenario set).
120
+
121
+ Rank by value ÷ cost, then **override the ranking with one rule: start with the island that carries
122
+ the coupling.** A structural island is easy and proves nothing; the coupled one is where the
123
+ declaration earns its keep, and it is the one that decides the region's keys.
124
+
125
+ ## Step 5 — Answer the archipelago question
126
+
127
+ Two distinct questions, and the report must answer both:
128
+
129
+ - **Does this page become an archipelago?** Yes as soon as it holds islands — the region is the unit
130
+ the lagoon previews, the unit an agent drives, and the place a future coupling has somewhere to
131
+ land. Say honestly whether it is a genuinely coupled region or a page-shaped grouping of independent
132
+ islands; both are legitimate, and the difference is the report's most useful sentence.
133
+ - **What is its SCOPE?** The archipelago is the **PAGE**, never a DOM subtree. Scoping it to the
134
+ two-column div that "contains the islands" is the classic mistake: it draws a boundary through the
135
+ middle of the one real coupling on the page and leaves a key written for a reader outside the
136
+ region. If two candidates share a key, they are in the same archipelago even if they sit at opposite
137
+ ends of the screen.
138
+
139
+ If the page already has an archipelago, this step becomes: which candidates JOIN it, does any of them
140
+ change who owns an existing key, and does any of them belong in another island's `slots`?
141
+
142
+ ## Step 6 — Write the LOCATE REPORT
143
+
144
+ The deliverable. Keep it in this order; keep the evidence attached.
145
+
146
+ ```
147
+ ## Page: <route> (<file>)
148
+ Archipelago: <id> — NEW | EXISTS | none needed (+ one sentence: genuinely coupled, or a mix?)
149
+ Host: next|vite|none (wrap the app's own components) | angularjs (author them under ui/)
150
+
151
+ ### Region keys
152
+ | key | owner (island `writes` / host-fed) | readers | seeded by the page? | evidence | provenance |
153
+
154
+ ### Island candidates (ranked, best first)
155
+ 1. <name> → tag `<prefix><name>`, slot `<slot>`
156
+ what it shows/acts on · value · cost · binds · writes (event → key) · slots it fills or offers
157
+ scenarios worth having · the verb to run next (island-create | island-extract)
158
+ ...
159
+
160
+ ### Stays host (with the reason)
161
+ - <name> — arrangement / overlay anchored to #<id> / composition root
162
+
163
+ ### Flows to declare (<id>.evidence.ts)
164
+ - seed <keys> → island <x> emits <event> → region must hold <keys>
165
+
166
+ ### Unknowns and risks
167
+ - what you could NOT trace, host coupling the lagoon can only stub, states with no reachable input
168
+
169
+ ### Decisions for the human (every `inferred` and `default` row above, as a question)
170
+ - <key/slot/owner/flow>: I chose <X> because <reason> — or <alternative>?
171
+ ```
172
+
173
+ **Provenance, on every key, owner, slot and flow** — the survey IS the step that turns what a person
174
+ wants into the spec everything after it is proved against, and a plausible guess here becomes a
175
+ "requirement" nobody stated:
176
+
177
+ - **stated** — quote it: the user's words, or the code line that already does exactly this
178
+ (`ManageServers.tsx:27 holds searchTerm in useState and filters with it`);
179
+ - **inferred** — reasoned from the code, not written anywhere (`the list reads it, so the search must
180
+ own it`); say what it was inferred from;
181
+ - **default** — nobody decided; you picked something plausible. Say so plainly.
182
+
183
+ Stated rows get a skim. Inferred and default rows ARE the review, so every one of them is repeated
184
+ under "Decisions for the human" as a question with the alternative named. A report with no inferred
185
+ or default rows on a page nobody has described to you is not finished — it is overconfident.
186
+
187
+ When two runs of this survey (or two agents) would plausibly answer a row differently, that row is
188
+ ambiguous whatever its provenance says: mark it **default**.
189
+
190
+ Close with **the first move**: one candidate, one verb, one command. A report that ranks eleven
191
+ candidates and does not say where to start has not finished the job.
192
+
193
+ ## Guardrails
194
+
195
+ - **Read-only.** Locating creates no files and refactors nothing. If a candidate needs a prop split to
196
+ become an island, that is a line in the report, not an edit you make now.
197
+ - **No speculative region.** Do not propose a key without naming its reader, or a `writes` without
198
+ naming the event that fires it. An undeclared coupling is a bug; an invented one is worse.
199
+ - **Never upgrade provenance.** A guess written as `stated` is the invention this whole method exists
200
+ to catch, moved upstream where everything built afterwards is proved against it.
201
+ - Do not propose islands for arrangement to inflate the count. A page whose honest answer is "one
202
+ island and a layout" gets that answer.
203
+ - Respect `host`: on a React host (`next`/`vite`/`none`) the recommendation is to WRAP the component
204
+ the app owns, not to copy it into `ui/`. Only `angularjs` has nothing to wrap.
205
+ - Never widen backend surface, add island-to-island imports, or invent rules `motu check` does not
206
+ enforce (README "Non-goals").
207
+ - The report is a proposal. A human picks the order and the scope; hand it to them, and only then to
208
+ `island-create` / `island-extract`.