@m-kopa/launchpad-cli 0.26.1 → 0.27.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.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: launchpad-destroy
3
- description: Tear down a Launchpad app end-to-end via `launchpad destroy` — Cloudflare Pages project, Access app, custom hostname, platform-repo TF entries, and the app repo (archive-renamed). Owner-only verb with a two-step destructive confirmation. Use when someone says "destroy this app", "/launchpad-destroy", "tear down `<slug>`", "delete the app", or asks to clean up a smoke-test / orphan / retired app.
4
- version: 0.26.1
3
+ description: Tear down a Launchpad app end-to-end via `launchpad destroy` — Cloudflare Pages project, edge-auth wiring (gateway KV/audience entries, or the Access app for `auth: access` apps), custom hostname, platform-repo TF, and the app repo (archive-renamed). Owner-only verb with a two-step destructive confirmation. Use when someone says "destroy this app", "/launchpad-destroy", "tear down `<slug>`", "delete the app", or asks to clean up a smoke-test / orphan / retired app.
4
+ version: 0.27.1
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
@@ -31,9 +31,11 @@ esac
31
31
  # /launchpad-destroy
32
32
 
33
33
  Destructive, owner-only verb. Tears down a Launchpad app end-to-end:
34
- the Cloudflare Pages project, Access app, custom hostname,
35
- launchpad-platform TF entries, and the app repo (archive-renamed,
36
- not deleted). Zero local platform-repo / terraform / Cloudflare /
34
+ the Cloudflare Pages project, the app's edge-auth wiring (gateway
35
+ KV/audience entries for gateway-fronted apps the default; the
36
+ Access app for `auth: access` apps), custom hostname,
37
+ launchpad-platform TF, and the app repo (archive-renamed, not
38
+ deleted). Zero local platform-repo / terraform / Cloudflare /
37
39
  GitHub credentials needed — the bot owns all of it. CLI is a thin
38
40
  client.
39
41
 
@@ -44,8 +46,8 @@ production app, get a second pair of eyes first.
44
46
 
45
47
  ## Pre-flight
46
48
 
47
- You need a current Cf Access session and OWNER role on the app.
48
- Editors cannot destroy.
49
+ You need a current session (`launchpad login`) and OWNER role on the
50
+ app. Editors cannot destroy.
49
51
 
50
52
  ```bash
51
53
  launchpad whoami
@@ -64,8 +66,8 @@ exits 1 with `no app with slug <slug>`.
64
66
  removes the app entirely.
65
67
  - **Re-creating an app under the same name.** Run `launchpad destroy
66
68
  <slug>`, wait for the lifecycle to reach `destroyed`, then
67
- `launchpad create`. The slug is freed by the archive-rename step
68
- in AC8.
69
+ `launchpad init` + `launchpad deploy` from the app's working
70
+ directory. The slug is freed by the archive-rename step in AC8.
69
71
  - **Cleaning up the app's GitHub history.** The destroy verb
70
72
  archive-renames the repo (preserving history); it does not
71
73
  hard-delete. Use `gh repo delete` if you genuinely need the repo
@@ -97,26 +99,78 @@ Either flag can short-circuit its respective prompt.
97
99
 
98
100
  ## What the verb does (server-side flow)
99
101
 
102
+ Every destroy starts with the same shared steps:
103
+
100
104
  1. **Server-side slug re-validation** (`checkSlug()` + `SLUG_REGEX`).
101
105
  2. **Owner-only authz** — `owner | break_glass` only; editor → 403.
102
106
  3. **Lifecycle precondition check** — see "Lifecycle states" below.
103
- 4. **main.tf carve-out check** pre-M-1182 apps that live in
104
- `main.tf` cannot be destroyed by this verb (see "Carve-outs").
105
- 5. **Per-app TF file pair existence probe** on launchpad-platform main.
106
- 6. **Lifecycle → `destroying`**.
107
- 7. **Enumerate open PRs on `launchpad-app-<slug>`.** Bot-authored PRs
108
- are closed with a comment; human-authored PRs get a heads-up
109
- comment (NOT closed — that's the human author's decision).
110
- 8. **Atomic destroy PR** opens on `launchpad-platform`, deleting
107
+ 4. **Three-way TF-shape fork.** The bot probes where the app's
108
+ Terraform lives on launchpad-platform main:
109
+ - `main.tf`-resident (pre-M-1182) **rejected** (see
110
+ "Carve-outs").
111
+ - **Per-app workspace** (`terraform/envs/prod/apps/<slug>/` with
112
+ isolated state the default shape for current apps) → the
113
+ workspace path below.
114
+ - **Legacy file pair** (`<slug>.tf` + `<slug>.allow.tf` in the
115
+ shared root) → the legacy PR path below.
116
+ 5. **Lifecycle → `destroying`**, then **enumerate open PRs on
117
+ `launchpad-app-<slug>`** (both paths): bot-authored PRs are
118
+ closed with a comment; human-authored PRs get a heads-up comment
119
+ (NOT closed — that's the human author's decision).
120
+
121
+ ### Per-app-workspace path (the current default)
122
+
123
+ There is **no destroy PR and no `destroy:confirmed` label** on this
124
+ path. Workspace apps live in isolated TF state, so there is no
125
+ shared apply to pick up a file deletion — teardown is an explicit
126
+ `terraform destroy`:
127
+
128
+ 1. The bot fires an **HMAC-signed `repository_dispatch`**
129
+ (`tf-destroy-per-app`): it signs `<slug>|<exp>` with a secret
130
+ only the bot holds (short-lived token, ~10 min). A
131
+ `repository_dispatch` already requires a repo-scoped token, so
132
+ the run cannot be fired from the Actions UI; the workflow
133
+ re-verifies the signature fail-closed AND re-checks
134
+ `lifecycle == destroying` before anything destructive runs.
135
+ 2. The workflow runs **`terraform destroy` against the app's
136
+ isolated R2 state** (while the workspace files are still
137
+ present), then deletes the state object so the slug is
138
+ reclaimable.
139
+ 3. On run success the bot opens **and auto-merges** a pure
140
+ config-cleanup PR deleting `terraform/envs/prod/apps/<slug>/`
141
+ (branch `bot/destroy-workspace/<slug>/<YYYYMMDD>`) — the
142
+ resources and state are already gone by then; merging just stops
143
+ a later per-app apply from re-creating the app.
144
+ 4. **Archive-rename + `lifecycle: destroyed` happen only after the
145
+ destroy run concludes successfully.** A failed run →
146
+ `destroy_failed`, repo untouched; re-running `launchpad destroy`
147
+ re-dispatches the teardown.
148
+
149
+ ### Legacy file-pair path (shared-root apps only)
150
+
151
+ Still shipped, for apps whose TF is the shared-root `<slug>.tf` +
152
+ `<slug>.allow.tf` pair:
153
+
154
+ 1. **Atomic destroy PR** opens on `launchpad-platform`, deleting
111
155
  `terraform/envs/prod/<slug>.tf` + `<slug>.allow.tf` in one commit.
112
- 9. **`destroy:confirmed` label gate** must be applied by a non-author
156
+ 2. **`destroy:confirmed` label gate** must be applied by a non-author
113
157
  for auto-merge.
114
- 10. **Existing `tf-apply` workflow** destroys the Cf resources on
115
- merge. There is NO new `tf-destroy` workflow — the standard
116
- apply with the removed module is the teardown.
117
- 11. **Archive-rename happens AFTER tf-apply terminal-success** — never
118
- before (AC8). If tf-apply fails, the repo is untouched and
119
- lifecycle moves to `destroy_failed` (recoverable).
158
+ 3. **Existing `tf-apply` workflow** destroys the Cf resources on
159
+ merge. There is NO `tf-destroy` workflow on this path — the
160
+ standard apply with the removed module is the teardown.
161
+ 4. **Archive-rename happens AFTER tf-apply terminal-success** — never
162
+ before (AC8). If tf-apply fails, the repo is untouched and
163
+ lifecycle moves to `destroy_failed`.
164
+
165
+ ## What destroy does NOT remove
166
+
167
+ **The app's D1 database is never dropped.** D1 databases are
168
+ bot-API-created (not in the app's TF), and no destroy path touches
169
+ them. The bot's D1 handling is create-or-adopt **by name**: the data
170
+ survives the destroy, and a later `launchpad init` + `launchpad
171
+ deploy` of the **same slug** re-adopts the existing database — your
172
+ data is still there. If you genuinely need the data gone, that is a
173
+ platform-team request, not a destroy side-effect.
120
174
 
121
175
  ## Lifecycle states
122
176
 
@@ -124,10 +178,10 @@ Either flag can short-circuit its respective prompt.
124
178
  |---|---|---|
125
179
  | `live` (or absent) | App is operational. | Destroy works. |
126
180
  | `provisioning` | Initial create still running. | Wait, then destroy. |
127
- | `failed` | Provisioning failed. | Destroy works (cleans up partial state). |
128
- | `destroying` | Destroy PR is open, tf-apply pending. | Re-running `launchpad destroy` is idempotent (returns the existing PR). |
129
- | `destroyed` | tf-apply succeeded + repo archive-renamed. | Re-running exits 0 with `already destroyed at <ts>`. Slug is free for re-use. |
130
- | `destroy_failed` | tf-apply on destroy PR failed or hung. | Re-running re-checks state; if not retriable, surfaces `platform-team intervention required`. App repo is **not** archive-renamed (recovery path stays clean). |
181
+ | `failed` | Provisioning failed. | Destroy works (cleans up partial state). If the app is actually live and serving, consider `launchpad recover <slug>` first — it repairs the record instead of tearing down. |
182
+ | `destroying` | Teardown in flight (workspace: destroy run dispatched; legacy: destroy PR open, tf-apply pending). | Re-running `launchpad destroy` is idempotent — workspace: re-dispatches the run; legacy: returns the existing PR. |
183
+ | `destroyed` | Teardown succeeded + repo archive-renamed. | Re-running exits 0 with `already destroyed at <ts>`. Slug is free for re-use. |
184
+ | `destroy_failed` | The teardown failed or hung. | **Workspace path:** re-run `launchpad destroy` — it re-dispatches from the failure point. **Legacy path:** re-run is rejected with `platform-team intervention required` (409). On both, the app repo is **not** archive-renamed (recovery path stays clean). |
131
185
 
132
186
  ## Carve-outs
133
187
 
@@ -163,10 +217,14 @@ When the destroy starts, the bot enumerates every open PR on
163
217
  >
164
218
  > Destroy PR: <url>
165
219
 
166
- The human author decides what to do (close, salvage, etc.).
220
+ The human author decides what to do (close, salvage, etc.). (The
221
+ `Destroy PR: <url>` line appears on the legacy path only — the
222
+ workspace path has no destroy PR at initiation time.)
167
223
 
168
- In TTY mode the verb prints the human-PR list at confirmation time so
169
- you can pause if you didn't realise there were in-flight PRs.
224
+ The PR split is computed server-side once the destroy is initiated;
225
+ the CLI prints the bot/human PR lists in its post-initiation render
226
+ (after confirmation, not before), so review them there if you didn't
227
+ realise there were in-flight PRs.
170
228
 
171
229
  ## Recovery — the archive-rename window
172
230
 
@@ -185,9 +243,11 @@ gh repo unarchive M-KOPA/launchpad-app-<slug>
185
243
  ```
186
244
 
187
245
  This restores the **repo**, not the Cloudflare infrastructure — you
188
- would still need a fresh `launchpad create <slug>` (or a hand-rolled
189
- platform-repo PR re-adding the per-app TF pair) to bring the app
190
- back online.
246
+ would still need a fresh `launchpad init` + `launchpad deploy` (or a
247
+ hand-rolled platform-repo PR re-adding the per-app TF) to bring the
248
+ app back online. Remember the app's D1 database survived the destroy
249
+ (see "What destroy does NOT remove") — a same-slug re-provision
250
+ re-adopts it.
191
251
 
192
252
  **The 30-day window is informational, not enforced by Launchpad** —
193
253
  the actual horizon is whatever GitHub's org policy is. If you
@@ -205,16 +265,17 @@ suspect a destroy was a mistake, recover within hours, not weeks.
205
265
 
206
266
  ### `launchpad destroy: session expired, run \`launchpad login\``
207
267
 
208
- Standard auth refresh. `launchpad login` once; the destroy command
209
- works again for the duration of the Cf Access session (~24h).
268
+ Standard auth refresh. `launchpad login` once; the session then stays
269
+ fresh by silent refresh as you use the CLI.
210
270
 
211
- ### `not authorised to destroy app X (you must be an owner — editor role is not sufficient)`
271
+ ### `not authorised to destroy app X (you must be an owner)`
212
272
 
213
- You're an editor, not the owner. Either:
273
+ You're an editor, not the owner — editor role is not sufficient for
274
+ destroy. Either:
214
275
 
215
276
  - Ask the current owner to transfer ownership via the portal admin UI:
216
- `https://launchpad.m-kopa.us/admin/apps/<slug>` (then transfer back
217
- after destroy, if you want).
277
+ `https://portal.launchpad.m-kopa.us/admin/apps/<slug>` (then
278
+ transfer back after destroy, if you want).
218
279
  - Get break-glass access from platform-team for this specific
219
280
  destroy (audited; one-shot).
220
281
 
@@ -224,11 +285,12 @@ See "Carve-outs". Platform-team manual cleanup is required.
224
285
 
225
286
  ### `no per-app TF for <slug> in launchpad-platform/main`
226
287
 
227
- The app exists in the registry but has no per-app TF pair on
228
- platform main. This usually means: (a) the app was never fully
229
- provisioned (look at `lifecycle` likely `failed`), or (b) the
230
- file pair was hand-removed outside of this verb. Either way:
231
- platform-team intervention.
288
+ The app exists in the registry but has no per-app TF on platform
289
+ main neither an `apps/<slug>/` workspace nor a `<slug>.tf` +
290
+ `<slug>.allow.tf` file pair (the bot probes both before this 409).
291
+ This usually means: (a) the app was never fully provisioned (look
292
+ at `lifecycle` — likely `failed`), or (b) the TF was hand-removed
293
+ outside of this verb. Either way: platform-team intervention.
232
294
 
233
295
  ### `slug ${urlSlug} is already being destroyed`
234
296
 
@@ -237,14 +299,17 @@ message it's from an old bot. Update the bot.)
237
299
 
238
300
  ### `slug <slug> is in destroy_failed state; platform-team intervention required`
239
301
 
240
- tf-apply on the destroy PR failed (commonly: a stuck Cf
241
- custom-hostname deletion). The app repo is NOT archive-renamed in
242
- this state the recovery path is unblocked. Options:
302
+ This 409 fires on the **legacy file-pair path only**: tf-apply on
303
+ the destroy PR failed (commonly: a stuck Cf custom-hostname
304
+ deletion), and the file-pair model has no self-service retry —
305
+ contact platform-team for manual TF state surgery.
306
+
307
+ On the **per-app-workspace path** a `destroy_failed` slug never
308
+ returns this error: re-running `launchpad destroy <slug>` simply
309
+ re-dispatches the teardown run from the failure point.
243
310
 
244
- 1. Re-run `launchpad destroy <slug>` to retry once the underlying
245
- issue is fixed (e.g. tf-apply hang resolved).
246
- 2. Contact platform-team for manual TF state surgery if re-running
247
- doesn't move past the failure.
311
+ On both paths the app repo is NOT archive-renamed in this state —
312
+ the recovery path is unblocked.
248
313
 
249
314
  ## --json output
250
315
 
@@ -254,6 +319,29 @@ For downstream scripts (CI cleanup jobs, automation):
254
319
  launchpad destroy <slug> --confirm-slug <slug> --yes --json
255
320
  ```
256
321
 
322
+ The shape is per-path. **Per-app-workspace apps** (the default) have
323
+ no destroy PR at initiation — the response carries a `path`
324
+ discriminant instead:
325
+
326
+ ```json
327
+ {
328
+ "slug": "<slug>",
329
+ "lifecycle": "destroying",
330
+ "path": "per-app-workspace",
331
+ "openBotPrs": [],
332
+ "openHumanPrs": [
333
+ { "number": 22, "title": "...", "htmlUrl": "...", "authorLogin": "alice" }
334
+ ]
335
+ }
336
+ ```
337
+
338
+ (The workspace config-cleanup PR — branch
339
+ `bot/destroy-workspace/<slug>/<YYYYMMDD>` — is opened and auto-merged
340
+ by the bot later, after the destroy run succeeds; it never appears in
341
+ this response.)
342
+
343
+ **Legacy file-pair apps** return the destroy PR:
344
+
257
345
  ```json
258
346
  {
259
347
  "slug": "<slug>",
@@ -287,12 +375,18 @@ On an idempotent re-run against a `destroyed` slug:
287
375
  - **`/launchpad-status`** — read the deployed manifest, compare to
288
376
  local. Pair this with `launchpad pull <slug> --out backup.yaml`
289
377
  before destroy if you want to keep the manifest for reference.
290
- - **`/launchpad-deploy`** — provision a new app (e.g. after a destroy
291
- that frees the slug). Note the slug-lock during the destroy window:
292
- `launchpad create <slug>` rejects if the slug is `destroying` or
293
- `destroy_failed`.
378
+ - **`/launchpad-deploy`** — provision a new app via `launchpad init`
379
+ + `launchpad deploy` (e.g. after a destroy that frees the slug).
380
+ Note the slug-lock during the destroy window: creating an app is
381
+ rejected while the slug is `destroying` or `destroy_failed`.
294
382
  - **`/launchpad-deploy-status`** — diagnose `provisioning` /
295
383
  `failed` lifecycle stages. Less relevant after destroy lands.
384
+ If the app shows `failed` but is actually live and serving, reach
385
+ for `launchpad recover <slug>` (documented there) before assuming
386
+ destroy + re-create is the fix — recover repairs the registry
387
+ record against live state. (Recover refuses destroy-side states —
388
+ `destroying` / `destroyed` / `destroy_failed` are owned by this
389
+ verb.)
296
390
 
297
391
  ## Anti-patterns
298
392
 
@@ -300,17 +394,21 @@ On an idempotent re-run against a `destroyed` slug:
300
394
  running `launchpad destroy` — the destroy needs to enumerate open
301
395
  PRs on the repo and the platform PR opens fine without it, but
302
396
  you'll lose the audit trail in the app repo's history.
303
- - **Don't** hand-edit `terraform/envs/prod/<slug>.tf` to "blank out"
304
- the app the bot still considers it live and `launchpad destroy`
305
- is the only path that handles every dependent (Access app,
306
- hostname, registry record, app repo). Manual TF edits leave
307
- orphan Cloudflare state.
397
+ - **Don't** hand-edit the app's per-app TF (the
398
+ `terraform/envs/prod/apps/<slug>/` workspace, or legacy
399
+ `<slug>.tf`) to "blank out" the app the bot still considers it
400
+ live and `launchpad destroy` is the only path that handles every
401
+ dependent (edge-auth wiring, hostname, registry record, app repo).
402
+ Manual TF edits leave orphan Cloudflare state.
308
403
  - **Don't** parse the prose output of `launchpad destroy` in CI
309
404
  scripts. Use `--json`.
310
- - **Don't** rely on a destroy PR auto-merging without the
311
- `destroy:confirmed` label — a non-author must apply it. The label
312
- is the audit gate; treating it as ceremony defeats AC6's two-pair-of-
313
- eyes property.
405
+ - **Don't** rely on a **legacy-path** destroy PR auto-merging without
406
+ the `destroy:confirmed` label — a non-author must apply it. The
407
+ label is the audit gate; treating it as ceremony defeats AC6's
408
+ two-pair-of-eyes property. (The label gate does not exist on the
409
+ per-app-workspace path — there is no destroy PR there; the
410
+ equivalent control is the bot-only HMAC-signed dispatch + the
411
+ workflow's lifecycle re-check.)
314
412
 
315
413
  ## Version
316
414
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: launchpad-onboard
3
- description: One-time setup for the Launchpad CLI + Claude Code skill bundle. Verifies the `launchpad` CLI is installed and current, runs `launchpad whoami` to confirm the Cf Access session is fresh, and checks the bundled skills are installed and in lock-step with the CLI. Idempotent — safe to re-run any time. Use when someone says "set me up for Launchpad", "I just got a new machine and want to use Launchpad", "/launchpad-onboard", or any of the other launchpad-* skills fails on a prereq check.
4
- version: 0.26.1
3
+ description: One-time setup for the Launchpad CLI + Claude Code skill bundle. Verifies the `launchpad` CLI is installed and current, runs `launchpad whoami` to confirm the session is fresh, and checks the bundled skills are installed and in lock-step with the CLI. Idempotent — safe to re-run any time. Use when someone says "set me up for Launchpad", "I just got a new machine and want to use Launchpad", "/launchpad-onboard", or any of the other launchpad-* skills fails on a prereq check.
4
+ version: 0.27.1
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
@@ -45,9 +45,10 @@ missing — pointing back here.
45
45
 
46
46
  ## Zero non-Launchpad dependencies
47
47
 
48
- Under Model A (M-1216), the CLI is fully self-contained: it talks
49
- straight to the bot via Cf Access OAuth and never shells out to `gh`,
50
- `jq`, `curl`, `git`, or any other system tool. **External users with
48
+ Under Model A (M-1216), the CLI is fully self-contained: it signs in
49
+ through the platform auth gateway and talks straight to the bot with
50
+ that bearer credential, never shelling out to `gh`, `jq`, `curl`,
51
+ `git`, or any other system tool. **External users with
51
52
  no M-KOPA GitHub access are first-class.** This skill therefore
52
53
  checks only two things: the `launchpad` binary is on PATH at the
53
54
  expected version, and the bundled skills are installed and synced.
@@ -78,8 +79,14 @@ launchpad --version
78
79
  self-contained `.ps1` may trip Defender — prefer the npm install
79
80
  above.
80
81
  - **Below the version bundled with this skill** (see step 4 below):
81
- `npm update -g @m-kopa/launchpad-cli` (or re-run the platform
82
- installer). Then `launchpad skills update` to re-sync the skill bundle.
82
+ run `launchpad update` the CLI's built-in self-update. It detects
83
+ the install channel itself (public npm vs the platform channel) and
84
+ upgrades in place; `launchpad update --check` reports current vs
85
+ latest without installing anything (exit 10 = update available).
86
+ If `update` can't tell which package manager owns the global
87
+ install, it prints the explicit upgrade commands — run the right
88
+ one (e.g. `npm update -g @m-kopa/launchpad-cli`). Then
89
+ `launchpad skills update` to re-sync the skill bundle.
83
90
 
84
91
  ### 2. Session is present and fresh
85
92
 
@@ -96,22 +103,44 @@ launchpad whoami
96
103
 
97
104
  - **Exit 0** → session is valid; identity and expiry are printed.
98
105
  - **"No session — run `launchpad login`"** → no session file on
99
- disk. Run `launchpad login` (opens a browser; sign in via
100
- Cloudflare Access SSO).
101
- - **"session expired, run `launchpad login`"** → refresh tokens
102
- rotate; re-authenticate.
106
+ disk. Run `launchpad login` (opens a browser; sign in with your
107
+ M-KOPA Microsoft account via the Launchpad auth gateway).
108
+ - **"session expired, run `launchpad login`"** → re-authenticate.
109
+ Sessions normally stay fresh on their own — the CLI silently
110
+ rotates a refresh token as you use it — so this only appears after
111
+ roughly a week of not using the CLI (or a hard cap of 30 days),
112
+ not on a daily cadence. The short access-token expiry `whoami`
113
+ prints is by design; verbs refresh it automatically.
103
114
  - **Any other error** → surface the message verbatim. Do not fall
104
115
  back to inspecting `~/.launchpad/session.json` by hand — the file
105
116
  is internal to the CLI and its shape is not a stable contract.
106
117
 
118
+ **Upgrading from CLI ≤ 0.26.x:** the login flow moved from Cloudflare
119
+ Access to the platform auth gateway on 2026-06-12, and old clients are
120
+ hard-broken against the bot by design. The fix is always the same two
121
+ commands:
122
+
123
+ ```bash
124
+ launchpad update
125
+ launchpad login
126
+ ```
127
+
128
+ (If the gateway flow itself fails, the CLI auto-falls back to the
129
+ legacy flow with a deprecation notice; `LAUNCHPAD_AUTH_LEGACY=1`
130
+ forces it outright. It is a temporary escape hatch slated for
131
+ removal — don't reach for it unless platform support says so.)
132
+
107
133
  ### 3. Skill bundle installed at `$HOME/.claude/skills/launchpad-*/`
108
134
 
109
135
  ```bash
110
136
  launchpad skills list
111
137
  ```
112
138
 
113
- Expected: the launchpad-* skills are all present with a `version:`
114
- matching the CLI.
139
+ Expected: the launchpad-* skills **plus the bundled `marquee-share`
140
+ skill** are all present. The launchpad-* skills each carry a
141
+ `version:` matching the CLI; `marquee-share` is vendored from
142
+ `M-KOPA/marquee` and prints `(no version)` — that is normal, not a
143
+ drift signal.
115
144
 
116
145
  - **Any skill missing** (or `$HOME/.claude/skills/` doesn't exist):
117
146
  ```bash
@@ -166,7 +195,7 @@ Otherwise finish with:
166
195
 
167
196
  - Do **not** run `launchpad login` without telling the user first — it
168
197
  opens a browser. State that it's about to happen and what they need
169
- to do (sign in via Cloudflare Access SSO).
198
+ to do (sign in with their M-KOPA Microsoft account).
170
199
  - Do **not** run destructive ops (e.g. `npm uninstall`, `rm`) on the
171
200
  user's machine even if a check fails — only suggest the fix; the
172
201
  user runs it themselves.