@motujs/cli 0.0.0-stage → 0.1.1

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,304 @@
1
+ ---
2
+ name: motu-regions
3
+ description: The motu rules for DECLARING a region, in full: island vs host, the page survey, declaring only decisions, region state and ownership (writes, bind, reads, one producer per key), useRegion/seed, foreign stores (StoreAdapter), catalogue regions, an island that IS the app's component and its providers, `planned: true`, several agents on one region, staged adoption (root, slots, exclusive), sources and ports and the adapter, unit tests for sources, removal-check, and what motu does not check. Load before creating or changing an archipelago, an island's place in a region, a source, or a page that composes a region.
4
+ ---
5
+
6
+ # motu-regions — Declaring a region
7
+
8
+ The rules for declaring a region — the detail behind the short `motu:rules` block in the project's instructions. Read the part you need; each section stands alone.
9
+
10
+ ## The rules
11
+
12
+ motu is an ARCHITECTURE framework: you build a page's regions one at a time, and what keeps them
13
+ coherent is DECLARED — who owns each piece of region state, which island fills which slot, what the
14
+ region asks of a backend. The lagoon (every declared state at its own URL) is what those declarations
15
+ make possible; it is not a component gallery and it is not the product. When a rule below asks you to
16
+ declare something, the declaration IS the work — the preview is how you check it.
17
+
18
+ - What the region SHOWS or ACTS ON is an island; what merely ARRANGES it is not. A pure layout and an
19
+ overlay anchored to a DOM id anywhere on the page stay host components. A control that owns region
20
+ state is an island EVEN IF other islands sit inside it — DOM nesting says nothing about ownership,
21
+ and `slots` (prop -> slot, in the archipelago) declares which island fills which of its props, so
22
+ the region composes the same way in the page and in the lagoon.
23
+
24
+ - Before a PAGE gets islands, SURVEY it (`/island-locate`) and write the answer down: which regions
25
+ become islands, which stay host, who owns each shared key, and whether the page becomes an
26
+ archipelago. The scope of a region is the PAGE, never a DOM subtree — an archipelago drawn around
27
+ the div that happens to contain the islands cuts through the coupling it exists to declare.
28
+
29
+ - Declare only what you DECIDED. An island file is `island('x-tag', Component)` — its props, its
30
+ callback-events and the host modules it reaches are read from the component into
31
+ `islands/contracts.generated.ts` by `motu sync`, and a stale one is a build error. What stays
32
+ hand-written is the decision: an event whose region name differs from the callback's
33
+ (`{ events: { onProgress: 'week-progress' } }`), and in the archipelago the `writes`, the `slots`
34
+ and any renamed `bind`.
35
+
36
+ - Region state is DECLARED, not emergent. A key an island updates is declared in that island's
37
+ `writes` (event → key); every other bound key is host-fed, DERIVED — do not list it. `bind` is the
38
+ region's keys (`bind: ['compactMode', { stats: 'networkStats' }]`), the map form only for renames.
39
+ The page cannot assign a produced key — the host-side region type omits them, so it is a compile
40
+ error. If you find yourself computing a value in the page from what one island did and passing it to
41
+ another, that is the laundering the ownership rules exist to stop: declare the output instead.
42
+
43
+ - The page reads region state with the binding's `useRegion()` and establishes it with `seed(...)`,
44
+ both typed from the archipelago (`createRegion`). Never reach for a store directly.
45
+
46
+ - A host that ALREADY has a state architecture (Jotai, Zustand, a Redux slice) does not have to move
47
+ its state into motu's store, and should not be asked to. Declare the region over the keys it
48
+ already has, then install a `StoreAdapter` — `get(key)` and `subscribe(keys, onChange)`, about ten
49
+ lines — and `observeForeignStore(archipelago, adapter)`. motu then AUDITS what it cannot enforce:
50
+ a key an island declares it owns, moving with no declared output to account for it, shows in the
51
+ lens as "wrote outside the declaration". It reports the key and the declared owner, never the
52
+ culprit — naming that would mean being in the write path, which is the rewrite this avoids. Know
53
+ the boundary: motu ENFORCES its own store and only AUDITS someone else's, and no CLI check sees a
54
+ foreign store at all, because every runtime check drives the lagoon.
55
+
56
+ - `docs/05-archipelagos-and-regions.md` in the motu repo is the reference for how a region declares
57
+ ownership, and `docs/07-checks-and-verification.md` for the checks that enforce it; read them
58
+ before changing how a region declares anything.
59
+
60
+ ## A UI that lives in the database, not the repository
61
+
62
+ A metadata-driven app (Twenty: widgets and views are rows, dispatched on a codegen'd enum) has no
63
+ `islands: [...]` in source. Declare the region `membership: 'catalogue'` and give every data-summoned
64
+ island a `member:` — the app's own discriminator, not the motu slot. Islands WITHOUT `member` are
65
+ chrome and are still checked for placement; a catalogue region is mixed, and treating it as pure
66
+ reports the chrome as a hallucinated widget type.
67
+
68
+ Do NOT hand-write fixtures to make the lagoon render. The app already had to solve this: its tests
69
+ and stories need the metadata too, so it carries a capture in-repo plus a script that refreshes it
70
+ from a live instance (Twenty: `scripts/mock-data/generate-*.ts` → 2.3 MB under
71
+ `testing/mock-data/generated/`). Point `capture` in `<id>.evidence.ts` at THAT, with `universe` read
72
+ from the schema enum. Both sides are then the app's artifacts and motu only compares.
73
+
74
+ What the comparison buys, and why data-driven UI is an advantage rather than an obstacle: a declared
75
+ member absent from the schema enum is UNREACHABLE (it can never render — a typo or a hallucination),
76
+ a captured type nobody declares is UNCOVERED (the region renders less than the page does). Neither is
77
+ answerable when membership lives in JSX. Watch for the empty answer: Twenty's own transport serves
78
+ `FindAllRecordPageLayouts` with `[]`, which renders as an empty page and looks like a working one —
79
+ `unservedOperations()` catches the missing handler, coverage 0% catches the empty one.
80
+
81
+ ## An island can BE the application's component
82
+
83
+ If the host already owns the component, the island declares it directly — `component: FieldsWidget` —
84
+ and no wrapper is written. A wrapper that only installs providers or draws chrome is motu-only code
85
+ in the app's repository, which is what adopting motu is supposed to avoid.
86
+
87
+ Where the pieces go: what an island CANNOT RENDER WITHOUT is `providers` in the lagoon overrides
88
+ (installed in EVERY view, per island, like a channel); the ARRANGEMENT is `layout` (region view
89
+ only); the widget row or props are `seed` DATA. Getting this wrong is invisible in the region view
90
+ and fatal in the mountpoints view — which is the one the flow checks drive, so it reads as "the
91
+ region rendered nothing" rather than "no providers".
92
+
93
+ Providers must be IDEMPOTENT. The frame installs them for its own chrome and each island installs
94
+ them for itself, so they nest — fine for context providers, fatal for a `<Router>` (React Router
95
+ throws, and both cards rendered as "Invalid Configuration"). Gate the ones that cannot nest
96
+ (`useInRouterContext()`).
97
+
98
+ Evidence must not import the frame. `<id>.evidence.ts` is read by plain node where the app's `@/…`
99
+ alias does not resolve, so keep the rows in a module with no application imports and let both sides
100
+ import THAT. The failure is a silent "flows could not be read", not an error at the import.
101
+
102
+ ## An island that reads the host's store has to say so
103
+
104
+ `bind` is the prop path, and it is the only reader motu sees by itself. An app with its own state has
105
+ readers with no prop at all — Twenty's side panel subscribes to a Jotai atom and renders the settings
106
+ for whichever widget it names. Declare those with `reads: ['key']` on the island, or the key is
107
+ written by an island, read by nobody motu knows about, and `coupling` reports a real coupling as one
108
+ that escapes the archipelago. It is a CLAIM, not a wire: motu cannot enforce a store it does not own,
109
+ but the claim can be contradicted by the lens and by a flow.
110
+
111
+ **One producer per key, and a catalogue can break that.** Any widget card in Twenty writes
112
+ `pageLayoutEditingWidgetId`, so declaring `writes` on two members made the ownership guard fire — and
113
+ it was right: "either of these writes it" is not a producer. The writer is the CARD, chrome shared by
114
+ every member. When that happens the honest model is motu's own rule — a control that owns region
115
+ state is an island even when others sit inside it — so the card becomes an island containing the
116
+ widget, via nested `slots`. Until then the key is host-written, and say so.
117
+
118
+ ## One producer per key, checked in the fast loop
119
+
120
+ Two islands declaring `writes` for the same key is an error, statically — not a runtime store-guard
121
+ complaint you reach after the work is done. Grouped by ELEMENT, not by slot: one island placed in two
122
+ slots (acme's filter panel, desktop + mobile drawer) is one producer and must stay legal.
123
+
124
+ This is what makes parallel agents safe without any new mechanism. Declare the WHOLE region in the
125
+ survey — every slot, every owner — before any island is implemented. Each agent then branches from an
126
+ archipelago that already contains the other agents' ownership, so a second claim on a key fails in
127
+ that agent's own branch, on their first `motu check`. The archipelago IS the claim registry; a
128
+ separate ledger would only restate it.
129
+
130
+ ## Surveyed but not built: `planned: true`
131
+
132
+ Declaring a whole region up front is what makes parallel work safe — an agent branches from an
133
+ archipelago that already carries everyone else's ownership. Measured on a two-agent run, the cost was
134
+ a region RED in every branch until the last island landed, and a permanently red region teaches
135
+ everyone to ignore it.
136
+
137
+ `planned: true` on an island entry splits the two questions. Ownership still counts it — a second
138
+ claim on its keys fails exactly as if it were built — while the checks that ask "does it exist, does
139
+ it mount, is it placed" skip it, and a `planned` warning keeps it visible.
140
+
141
+ The flag REMOVES ITSELF: once the island is registered, `planned: true` becomes an error. A survey
142
+ that quietly turns into a list of things nobody built is worse than no survey, so the state cannot
143
+ persist past the moment it stops being true.
144
+
145
+ ## When several agents build one region at once
146
+
147
+ These bind you whether or not someone launched you as part of a fan-out.
148
+
149
+ - **You own ONE slot.** Do not edit another island's files, another island's entry in the archipelago,
150
+ or the region's shared type. If you believe the shared type is wrong, SAY SO in your report and stop
151
+ — changing it is a decision about everyone's work, made by someone who can see all of it.
152
+ - **Remove `planned: true` from your own entry only.** It is how the region learns your island exists.
153
+ - **Ship a coverage step for your slot** in the region's `<id>.evidence.ts`: `expectRender` naming text
154
+ your island alone produces. Without it, a slot wired to a neighbour's data passes every check.
155
+ - **Do not repair shared infrastructure.** A failing install, a missing browser, a path that resolves
156
+ outside the project — that is exit 2, "could not run". Report it. An agent that symlinks its way past
157
+ a broken environment inherits a problem it then has to explain, and does it in everyone's tree. This
158
+ happened: one agent stopped and reported, another improvised and produced a false runtime failure.
159
+ - **Say what got in your way.** Framework friction is the most valuable thing you can return, and it
160
+ is invisible to whoever reads only your diff.
161
+
162
+ The files several agents WILL collide in: `islands/registry.ts` and `islands/contracts.generated.ts`
163
+ (generated — regenerate with `motu sync`, never hand-merge), the shared stylesheet (append-only,
164
+ keep both sides), and the archipelago (one entry each, so a line-level merge is usually right). In a
165
+ lagoon frame, prefer an append-only LOOKUP over a ternary chain: two people extending one chain cannot
166
+ both win, and the loser's island renders someone else's data.
167
+
168
+ ## "motu is load-bearing" names a file, and the cause is above the line it points at
169
+
170
+ `removal-check` deletes every file that is 100% motu, unwraps its tags everywhere else, and asks
171
+ whether the host still typechecks. When it fails, read it in this order — the report now prints all
172
+ three, so nothing here needs digging:
173
+
174
+ - **"could not rewrite these files"** — the surgery threw and that file was left ALONE, so it proved
175
+ nothing and every error naming it is a consequence, not a finding. It means the rewriter met a JSX
176
+ shape it cannot express. Report the shape: that is motu's bug before it is yours.
177
+ - **"compose a region but are NOT deletable"** — the one that costs an afternoon if it is not said.
178
+ A composition root is deleted WHOLE only when EVERY import it makes is motu's; one application
179
+ import and it is stripped instead, leaving `createRegion(...)` with its imports gone and errors on
180
+ lines that look fine. The report names the offending imports. Whatever needs the application — a
181
+ source's PORT, a service, a browser API — belongs in an app file that renders the real components
182
+ inside `<R.Island slot="…">`, not in the composition root.
183
+ - **a dangling import of a deleted module** — never the line it points at. It is the first case,
184
+ one file earlier.
185
+
186
+ The shape that satisfies all of this is the one the annuaire page already uses: the region binding and
187
+ its wrapper in `components/motu/`, importing nothing but motu; the port, the source and the real
188
+ components in app files that wrap them in `<R.Island>`. The lagoon installs the same source through
189
+ `channelFrom`, because there is no page there to do it.
190
+
191
+ ## A source is REAL in the lagoon; its PORT is the stand-in — and the adapter is yours to prove
192
+
193
+ Worth being exact about, because it decides how much a green region actually claims.
194
+
195
+ When a flow runs, the source is the APPLICATION'S OWN OBJECT. Its timeout, its precedence rules, its
196
+ generation guard, its error mapping, its intent dispatch — all of that executes. What is swapped is
197
+ the PORT: production hands it Supabase and the services, the lagoon hands it fixtures. So a region
198
+ flow proves more than "the wiring type-checks"; it proves the page's own logic behaves, against
199
+ answers a human chose.
200
+
201
+ What it CANNOT prove is the seam on the other side of the port: that the real adapter
202
+ (`currentUser()` returning `{ email, user_metadata }`, `fetchCurrentMember()` returning
203
+ `primarySectorId`) matches what the backend actually sends. Get that shape wrong and every motu check
204
+ stays green while production breaks. The lagoon replaces the host module so completely that nothing
205
+ shows a fetch happened — that is the point of it, and it is the boundary.
206
+
207
+ ## WHAT MOTU DOES NOT CHECK, and is your job
208
+
209
+ motu checks COMPOSITION: that a region's islands are placed, that a declared coupling carries, that
210
+ every declared state renders. It runs no typecheck and no test runner, and it never will — a green
211
+ `motu check` says nothing whatever about whether your components are correct inside.
212
+
213
+ Two classes are yours, and no amount of motu makes them motu's:
214
+
215
+ - **Logic inside a component.** A filter that stops matching on one field, `some` where `every` was
216
+ meant, an off-by-one on a count. These render fine, compose fine, and pass every check here.
217
+ - **The path from a user's action to a declared output.** A flow EMITS an island's declared event —
218
+ `emit: { slot: 'search', event: 'term', detail: 'staging' }` — which enters the region *past* the
219
+ component's own handler. A handler that ignores its argument, so typing changes nothing, passes
220
+ every flow you can write. This is not a gap to be closed with more flows; it is where the model
221
+ ends and a unit test begins.
222
+
223
+ Measured, on a real screen: eight regressions injected into a motu region and into the same feature
224
+ built with plain props. The project's own unit tests caught five; motu's flows caught one — and that
225
+ one was a RENDER assertion, the class the tests missed. They are different instruments. Write both,
226
+ and do not read a green `motu check` as coverage.
227
+
228
+ So a source that carries logic gets UNIT TESTS, in the host's own runner, over a hand-made port. Not
229
+ because motu asks for a second suite, but because that is the only place the branches no rendered
230
+ state distinguishes can be reached: a lookup that THREW versus one that answered null, a deadline, the
231
+ precedence in a prefill, the slower of two submits not overwriting the faster. `lib/tests/unit/
232
+ actions-week-source.test.ts` is the shape. A flow drives a source through the screen; a unit test
233
+ drives it directly, and you want both for the same reason you want `expectRender` as well as `expect`.
234
+
235
+ THERE IS NO CHECK FOR THIS, deliberately. A `sources-tested` warning used to fire when no test file
236
+ imported a declared source, and it was removed: whether the application's own code has unit tests is
237
+ not motu's judgement to make. Nothing else in the framework opines on a host's test suite, and it
238
+ could only fire once a source already existed — so it encouraged TESTING an extraction, never the
239
+ extraction itself. What produces the pattern is structural: the lagoon renders a region WITHOUT the
240
+ page, so orchestration left in page effects cannot be previewed at all.
241
+
242
+ And the adapter itself — the few lines that turn the backend's shape into the port's — belongs to
243
+ whatever proves this app against a real backend. Where the call goes through one of the app's own
244
+ backend operations, that is ASSAY's ledger: it records the shape of every outbound payload (never the
245
+ values) and fails when it differs from `.assay/operations.json`. That is precisely the other half of
246
+ the port's assumption — the port says what the source expects, the ledger says what the operation
247
+ sends.
248
+
249
+ Know the gap that leaves, and do not repeat the mistake this paragraph used to make. A client that
250
+ talks to the database or the auth service DIRECTLY — `supabase.auth.getUser()`, a PostgREST read or
251
+ an `rpc` in a repository — is not an operation, so it has no declaration and neither tool pins its
252
+ shape. This text called that "a few lines by construction, which is the mitigation". It was wrong:
253
+ counted on the project it was written for, the direct-from-browser path was ~40 modules naming 89
254
+ Postgres functions — the app's DOMINANT data path, not an adapter. Nobody had counted it, and the
255
+ guess became a rule.
256
+
257
+ So: measure the gap in YOUR host before deciding it is small, and never read a green assay run as
258
+ covering it. What guards that path is RLS alone, which makes the review surface the POLICY rather
259
+ than a declaration. Routing the read through a backend operation is the fix where the read deserves
260
+ one; where it does not, say plainly that the path is unpinned rather than describing it as narrow.
261
+
262
+ ## Adoption is staged, and stage 1 does not touch the page
263
+
264
+ A region's arrangement lives in the archipelago's `root` — the application's own layout component,
265
+ with `slots` mapping its props to the region's islands, rendered by the page AND by the lagoon from
266
+ the same map — or in a hand-written lagoon frame, which is a second description of the page. Both are
267
+ supported, and which one you are in is a STAGE, not a verdict.
268
+
269
+ Adopt as a THIN OVERLAY first: islands, `<X.Island>` inside the page's existing JSX, a frame that
270
+ holds only the application's own components. `region-root` reports that shape as `ok` and names `root`
271
+ without failing on it, because moving a page is a real refactor of the host's own code and no project
272
+ can do ten of them before its first green run. Then migrate ONE region at a time, when you already
273
+ have a reason to open its page. Then, when the last one is done, set `"regionRoot": "required"` in
274
+ `motu.config.json` — the switch that makes the arc finish rather than stall half-done.
275
+
276
+ A MIGRATION SWEEP ACROSS EVERY REGION IS THE WRONG SHAPE, and the reason is not diff size. Two things
277
+ change in a move to `root` that no check sees:
278
+
279
+ - EXCLUSIVITY GETS DEMOTED. A ternary whose branches cannot both render becomes two independent
280
+ props, and nothing enforces that at most one is non-null — acme's actions page traded
281
+ `weeksLoaded ? <WeekNavigator/> : <Skeleton/>` for a `weekNav` prop and a `weekNavPlaceholder` prop
282
+ rendered adjacently, with a comment standing where the guarantee used to be. DECLARE the pair —
283
+ `exclusive: [['weekNav', 'weekNavPlaceholder']]` on the archipelago — and `<X.Root>` will not
284
+ compile with both: the page writes one condition, `{...(ready ? { weekNav } : { weekNavPlaceholder })}`.
285
+ Undeclared, nothing sees it.
286
+
287
+ - THE SERVER/CLIENT BOUNDARY MOVES. The root renders inside `<X.Region>`, a client component. A
288
+ layout the page rendered on the server crosses into the client bundle when it becomes the root, and
289
+ the nesting inverts: acme's `/forgot-password` went from `page → AuthLayout → Screen` to
290
+ `page → Screen → Region → AuthLayout`. Same pixels, different tree, different bundle. Read what the
291
+ extracted layout imports before moving it.
292
+
293
+ What the migration BUYS is one thing, and it is worth being exact about it: the lagoon previews the
294
+ page instead of a drawing of it. Islands, contracts, ownership, coverage and `removal-check` work the
295
+ same in either shape — `removal-check` rewrites `<X.Root>` back to the layout component and unwraps
296
+ `<X.Island>`, so both shapes survive deleting motu. What it COSTS is that the extraction is not
297
+ reversible by the tool: removal leaves the extracted layout files behind, where stage 1 returns the
298
+ page to exactly the JSX it had. So the trade is lagoon fidelity against a page you can still hand back
299
+ unchanged, and a region earns the move when someone is already editing it.
300
+
301
+ A frame that genuinely must draw its own markup says why once — `inventedArrangement('why', <…/>)`
302
+ downgrades the error to a warning, and warnings do not fail `motu check`. It is a HOLD, not an answer.
303
+ `motu archipelago adopt-root <id>` does the derivable half of a migration and refuses rather than
304
+ guessing when the frame nests two host components.
@@ -0,0 +1,43 @@
1
+ motu is an ARCHITECTURE framework: a page's regions are built one at a time and kept coherent by what is
2
+ DECLARED — who owns each key, which island fills which slot, what the region asks of a backend. The lagoon
3
+ renders every declared state at its own URL. These are the invariants; the detail is in the skills —
4
+ LOAD THE ONE THAT MATCHES YOUR TASK before you start it:
5
+
6
+ - `motu-regions` — declaring a region: islands vs host, ownership, sources/ports, adoption, parallel agents
7
+ - `motu-evidence` — scenarios, flows, worlds, the lagoon override, `motu lagoon try`, the final look
8
+ - `motu-checks` — which check when, exit codes, `integrate check`, the handover
9
+ - workflows: `island-locate` (survey a page) · `island-create` · `island-extract` · `archipelago-build`
10
+
11
+ Declaring
12
+ - What a region SHOWS or ACTS ON is an island; what merely ARRANGES it stays host. Survey the PAGE
13
+ (`island-locate`) and write down owners before any island is built.
14
+ - Declare only what you DECIDED. Every key an island updates is in its `writes`; everything else bound is
15
+ host-fed. One producer per key. Never compute in the page what one island did and pass it to another —
16
+ declare the output. The page uses the binding's `useRegion()` / `seed(...)`, never a store.
17
+
18
+ Evidence
19
+ - New UI starts as an island with a scenario in `<name>.evidence.ts`, renders from defaults alone, and is
20
+ looked at in the lagoon before it is placed. Changed UI starts with the scenario showing the state you
21
+ will change.
22
+ - Evidence data comes from the app (fixtures it already has, typed with `import type`, imported by
23
+ RELATIVE path) — never invented to make a frame render.
24
+ - Every declared state is an address (`motu lagoon states`); read `window.__motuLagoonState` before
25
+ believing a screenshot. EXPLORE before you assert: `motu lagoon try <region> [step…]` (~5s) shows what
26
+ each step moves; assertions are yours, never copied from its output.
27
+ - A flow step must be able to fail: end on what ANOTHER island renders. Adding an island means adding a
28
+ step asserting its own render.
29
+ - The final look is a FRESH agent's, given only the state URLs, where the app's vocabulary lives, and
30
+ the question "does anything render what this app never uses?" — never your diff or reasoning.
31
+
32
+ Checks
33
+ - Before saying UI work is done: the host's own typecheck and tests (motu checks neither), `motu check`,
34
+ then `motu integrate check`. Runtime checks are a punctual gate: name the region you touched
35
+ (`motu archipelago verify <id> --runtime`); `motu check --runtime` once, before handing over.
36
+ - Exit codes: 0 holds · 1 contradicted → repair · 2 could not run → retry, do NOT repair. A check that
37
+ examined nothing has not passed.
38
+ - Several agents on one region: you own ONE slot; do not repair shared infrastructure (report exit 2);
39
+ never symlink `node_modules` into a worktree.
40
+
41
+ Handover
42
+ - A person must be able to open it: `motu lagoon serve --watch --detach` (or `motu lagoon publish
43
+ --remote`), and end your report with the URL and the state to look at.
@@ -0,0 +1,19 @@
1
+ ---
2
+ description: Build a whole screen as a motu region with several agents at once — survey it, declare every slot and owner up front as `planned`, fan out one agent per island, then merge, write the region's flows and drive the result in the lagoon.
3
+ tools: ['codebase', 'editFiles', 'search', 'runCommands', 'problems', 'usages', 'changes']
4
+ ---
5
+
6
+ # Build a screen with several agents
7
+
8
+ Build one SCREEN as a motu region, with several agents working at the same time. You write the
9
+ CONTRACT they build against and integrate what comes back; you do not write the islands. This is the
10
+ workflow the [`archipelago-build` agent](../agents/archipelago-build.agent.md) drives — read that agent
11
+ file and the repo `README.md` for the full rules; do not duplicate them here.
12
+
13
+ Shape of the run: survey the page → declare every slot, every owner and `planned: true` for all of
14
+ them → verify that the region is green before anything exists → one agent per slot in its own
15
+ worktree → merge (regenerate the generated files, never hand-merge them) → write the region's flows →
16
+ open the lagoon and drive it.
17
+
18
+ Use it when a new screen has three or more islands. For a single island use `island-create`; for
19
+ reading a page without building anything use `island-locate`.
@@ -0,0 +1,37 @@
1
+ ---
2
+ description: Create a brand-new motu island (not migrating legacy) — pick/create the archipelago, build + verify in the lagoon against mock fixtures (including reactive behaviour and sibling-island connections), raise integration concerns, integrate into the ocean, validate with the ocean's own build/tests, then hand off to a human.
3
+ tools: ['codebase', 'editFiles', 'search', 'runCommands', 'problems', 'usages', 'changes']
4
+ ---
5
+
6
+ # Create a new motu island
7
+
8
+ Build a **new** UI capability as a motu island, designed fresh (not lifted from the ocean). Close the
9
+ loop in the **lagoon** first; integrate only once it passes. The `motu` CLI does the deterministic
10
+ work; you own the judgement and the sequence. This is the workflow the
11
+ [`island-create` agent](../agents/island-create.agent.md) drives — read that agent file and the repo
12
+ `README.md` for the full rules; do not duplicate them here.
13
+
14
+ ## Workflow (in order)
15
+
16
+ 1. **Archipelago first** — decide the page/region the island lives on. Reuse one under
17
+ `demo-app/src/archipelagos/`, or `pnpm motu archipelago init <id>`. Know which store keys it
18
+ reads/writes and which sibling islands it coordinates with.
19
+ 2. **Build in the lagoon:** `pnpm motu island create <name>` (scaffolds `ui/<kebab>/` component +
20
+ `islands/<kebab>/` mount point). Write the mode-agnostic component (no fetch/history/document, all
21
+ I/O via `@motujs/contract`, renders from default props), fill the `contract` (input/output), and the
22
+ mock fixtures — use a **function `response`** for reactive filtering and declare `scenarios`.
23
+ 3. **Test behaviour + sibling connections** offline: `MOTU_NO_SSL=1 pnpm dev:lagoon`; drive a store
24
+ key and watch the reader island react.
25
+ 4. **Verify:** `pnpm motu island verify <name>` (real-browser lagoon, native & legacy fit, data-flow).
26
+ Fix every `✗` until **PASS**. Then WRITE DOWN the integration risks the lagoon can't cover
27
+ (bound-empty inputs, unfired channels, host coupling, stub fixtures, legacy CSS/digest/session).
28
+ 5. **Integrate:** `pnpm motu island integrate <name> --archipelago <id>`; fill the `bind`/`on` wiring
29
+ (connect to siblings via the store), and add the `<motu-island slot="…">` marker to the legacy page
30
+ (`legacy-toggle="true"` for progressive rollout).
31
+ 6. **Validate with the ocean's own checks:** `pnpm typecheck` + `pnpm build:bridge`, then the host
32
+ app's compilation + unit + integration/e2e suites (ask the human for commands if unknown).
33
+ 7. **Hand off to a human** — present the verify PASS, the step-4 integration concerns, and the ocean
34
+ suite result. The human is the final gate; do not mark done without their validation.
35
+
36
+ Never work around a verify failure, widen backend surface beyond the needed `@BrowserCallable` method,
37
+ or add island-to-island imports / runtime module loading (see README "Non-goals").
@@ -0,0 +1,59 @@
1
+ ---
2
+ description: Make UI the app ALREADY HAS into a motu island — on a React host the island wraps the component the app owns (no component is written); on an AngularJS ocean host the legacy source is rewritten as a mode-agnostic React component. Loop against `motu island verify` until PASS, then integrate into an archipelago.
3
+ tools: ['codebase', 'editFiles', 'search', 'runCommands', 'problems', 'usages', 'changes']
4
+ ---
5
+
6
+ # Make an existing component a motu island
7
+
8
+ Take one piece of UI the application already has and make it a **motu island**. The `motu` CLI does
9
+ the deterministic work; you do the judgement. **Close the loop on `motu island verify`, not on your
10
+ own judgement.** This is the workflow the
11
+ [`island-extract` agent](../agents/island-extract.agent.md) drives; read that agent file and the
12
+ repo `README.md` for the full rules — do not duplicate them here.
13
+
14
+ **Step 0 — read `motu.config.json`'s `host`, it decides the whole shape.** `next`/`vite`/`none` → WRAP:
15
+ the island points at the component the app owns and you write no component; copying it into `ui/`
16
+ forks it. `angularjs` → REWRITE: there is no React component to wrap, so author one under `ui/`.
17
+ `isolation` is NOT this discriminator — it is shadow vs light DOM, and every reference project is
18
+ `light`, the AngularJS one included.
19
+
20
+ Ask the developer for the component (or legacy source path) and the target archipelago/page if they
21
+ are not obvious.
22
+
23
+ ## Workflow
24
+
25
+ 1. **Read what exists.** WRAP: find the component and the specifier the app imports it by, then list
26
+ what it cannot render without — providers, its own fetching, router/session, required props with no
27
+ default. REWRITE: controller/JS + template/partial + the REST/service it calls; note what it
28
+ renders, the actions it emits, and every server call.
29
+ 2. **Scaffold.** WRAP: `motu island create <name> --from '<the app's own specifier>' [--export <Name>]`.
30
+ REWRITE: `motu island create <name>`.
31
+ 3. **Data access.** WRAP: the component keeps the app's; a `channel` answers what reacts and the app's
32
+ own captured mock data answers what it reads. REWRITE: map every call to `@motujs/contract` — if a
33
+ method isn't `@BrowserCallable` yet, annotate it in the backend, rebuild, then `motu codegen`.
34
+ 4. **The component.** WRAP: write NONE. What it cannot render without goes in `providers` (every view,
35
+ idempotent — gate a `<Router>` on `useInRouterContext()`), arrangement in `layout`, data in `seed`.
36
+ A wrapper that only installs providers is motu-only code in the app's repo. REWRITE: author
37
+ `ui/<kebab>/<Pascal>.tsx` — props in / callbacks out, no fetch/history/document, all I/O via the
38
+ contract, renders from default props alone.
39
+ 5. **Evidence.** Both: scenarios whose outputs DIFFER, or `data-flow` fails. WRAP: point at the capture
40
+ the app already keeps for its own tests — never hand-write a third copy — and import shared evidence
41
+ with a RELATIVE specifier (`@/` does not resolve in the loaders, and the failure is silent).
42
+ REWRITE: fill the evidence file (shapes match the contract returns; a function `response` gives
43
+ reactive filtering) and set the required `legacy` strategy (`fill`/`inline`/`structural`).
44
+ 6. **Verify:** `motu island verify <name>` — boots the focused lagoon in a real browser
45
+ (Playwright/Chromium; one-time `cd packages/cli && npx playwright install chromium`). Fix every `✗`,
46
+ re-run until **PASS**. `--fast` uses happy-dom for quick iteration; look at it with
47
+ `motu lagoon serve --watch`.
48
+ 7. **Integrate:** `motu archipelago init <page-id>` (if needed) →
49
+ `motu island integrate <name> --archipelago <page-id>`; fill the `TODO(motu:wiring)`
50
+ bindings/handlers, place the region in the page (`createRegion` + `useRegion` on a React host, a
51
+ `<motu-island slot="…">` marker in a legacy template), then `motu integrate check`.
52
+ 8. **Finish:** `motu island verify <name>` (expect PASS) and the host's own build/typecheck.
53
+
54
+ For a BRAND-NEW island (nothing exists yet), use the `island-create` agent/prompt instead —
55
+ it adds the archipelago-first, sibling-connection, ocean-validation, and human-gate steps.
56
+
57
+ Never work around a verify failure, copy a component the app owns into `ui/` on a React host, widen
58
+ backend surface beyond the needed `@BrowserCallable` method, or add runtime module loading /
59
+ island-to-island imports (see README "Non-goals").
@@ -0,0 +1,43 @@
1
+ ---
2
+ description: Survey one page and locate its island candidates — what becomes an island, what stays host, who owns each shared key, and whether the page itself becomes an archipelago. Produces a ranked LOCATE REPORT and creates no files; island-create / island-extract execute it.
3
+ tools: ['codebase', 'search', 'usages', 'problems']
4
+ ---
5
+
6
+ # Locate the islands on a page
7
+
8
+ Read ONE page and decide what motu should be told about it. This is the reading step before
9
+ `island-create` / `island-extract` — it produces a **report**, not files. This is the workflow the
10
+ [`island-locate` agent](../agents/island-locate.agent.md) drives — read that agent file, the repo
11
+ `README.md` and the host's motu rules block for the full rules; do not duplicate them here.
12
+
13
+ ## Workflow (in order)
14
+
15
+ 0. **Learn the project's shape** — `motu.config.json`: paths, `tagPrefix`, and above all `host`, which
16
+ answers whether a React component already exists. `next`/`vite`/`none` → the island WRAPS the app's
17
+ own component; never propose lifting it into `ui/`, a copy forks it. `angularjs` → there is nothing
18
+ to wrap, so one is authored under `ui/`. (`isolation` is NOT this discriminator — it is shadow vs
19
+ light DOM, and every reference project is `light`, the AngularJS one included.) Read the existing
20
+ archipelagos and `islands/` — a candidate may already exist, or belong to a region already.
21
+ 1. **Enumerate what the page RENDERS** top-down: every panel, banner, card, navigator, control row,
22
+ empty and error state — with its file and its props. Follow every prop to its source.
23
+ 2. **Classify each region.** SHOWS content or ACTS ON the region → island (even if other islands sit
24
+ inside it — DOM nesting says nothing about ownership; `slots` declares who fills which prop). Only
25
+ ARRANGES, or is an overlay anchored to a DOM id, or is the composition root → host.
26
+ 3. **Find the real couplings.** Each value is one of three things: island-owned (`writes`, event →
27
+ key), host-fed/derived (`bind`, not a write), or page-local (not region state at all). The tell for
28
+ a laundered coupling: the page computes something out of island A's callback and hands it to island
29
+ B. Name the readers and the writer of every key; a key with no reader is not a key.
30
+ 4. **Rank** by value (carries a coupling · is the page's content · keeps breaking · reused · about to
31
+ change) ÷ cost (own fetching · host session/context/router · reaches `document` · drilled props ·
32
+ cannot render from defaults · states no input can reach). Then override the ranking with one rule:
33
+ **start with the island that carries the coupling.**
34
+ 5. **Answer the archipelago question** — does the page become one (yes as soon as it holds islands;
35
+ say whether it is genuinely coupled or a mix), and what is its SCOPE: the **page**, never a DOM
36
+ subtree. Scoping to the div that "contains the islands" cuts through the one real coupling.
37
+ 6. **Write the LOCATE REPORT**: archipelago id + verdict, the region-keys table (key | owner |
38
+ readers | seeded?), ranked candidates (tag, slot, binds, writes, slots, scenarios, next verb),
39
+ what stays host and why, the flows to declare in `<id>.evidence.ts`, and the unknowns/risks the
40
+ lagoon cannot cover. Close with **the first move**: one candidate, one verb, one command.
41
+
42
+ Read-only: no scaffolding, no refactor, no invented keys or events, no islands proposed for
43
+ arrangement to inflate the count. The report is a proposal — a human picks the order and the scope.
@@ -0,0 +1,43 @@
1
+ // The AngularJS ocean's contribution to the lagoon's Vite config.
2
+ //
3
+ // Unlike the Next host, this one adds no aliases: a workspace-linked ocean resolves @motujs/* through
4
+ // node_modules already. What it does add is the part a legacy backend forces — a single-origin dev
5
+ // proxy and HTTPS.
6
+ //
7
+ // WHAT THE PROXY IS FOR, now that the lagoon is mock-only to look at: `motu fixtures record <island>
8
+ // --transport http` boots this server against the real legacy backend and captures the responses
9
+ // into fixtures. That build is the only one that ever asks for the `http` transport — a human
10
+ // browsing the lagoon always gets mock, because an address has to resolve to a declared state.
11
+ //
12
+ // It is a dev proxy, NOT CORS. The browser talks only to https://localhost:5173; Vite forwards
13
+ // /api/* to the running app server. Auth is why this must be HTTPS: the login page is reCAPTCHA
14
+ // site-key locked, so you do NOT log in through the proxy — you log in at the real origin, and its
15
+ // session + XSRF cookies are host-only `localhost` Secure cookies, which therefore also flow to
16
+ // https://localhost:5173 (same host, port-agnostic).
17
+ //
18
+ // The proxy TARGET is a project fact, so it comes from lagoon.config.json ("proxy"), not from here.
19
+ export async function contribute({ lagoonJson, env, resolveBuildDep }) {
20
+ // MOTU_NO_SSL=1 serves plain HTTP (no dev cert) — used for mock-backed design previews and tooling
21
+ // that don't need the session-cookie flow. Default stays HTTPS for real backend proxying.
22
+ const noSsl = env.MOTU_NO_SSL === '1';
23
+ const basicSsl = noSsl ? null : await resolveBuildDep('@vitejs/plugin-basic-ssl');
24
+
25
+ const proxy = lagoonJson.proxy
26
+ ? Object.fromEntries(
27
+ Object.entries(lagoonJson.proxy).map(([path, target]) => [
28
+ path,
29
+ {
30
+ target,
31
+ changeOrigin: false,
32
+ secure: false, // self-signed dev cert
33
+ cookieDomainRewrite: '',
34
+ },
35
+ ]),
36
+ )
37
+ : undefined;
38
+
39
+ return {
40
+ plugins: basicSsl ? [basicSsl()] : [],
41
+ server: { https: !noSsl, ...(proxy ? { proxy } : {}) },
42
+ };
43
+ }