@jimhoyd/urlcode 0.4.0-alpha.2 → 0.4.0-alpha.3

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 (115) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +17 -19
  2. package/.claude/skills/urlcode-operations/SKILL.md +9 -9
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CONTRIBUTING.md +20 -16
  5. package/README.md +44 -54
  6. package/ROADMAP.md +61 -376
  7. package/dist/BUILD-MANIFEST.json +15 -15
  8. package/dist/agents-guide.js +6 -6
  9. package/dist/build-static.js +2 -0
  10. package/dist/cli.js +3 -3
  11. package/dist/explain-cli.js +4 -2
  12. package/dist/explain.js +7 -1
  13. package/dist/extensions.js +1 -1
  14. package/dist/manifest.js +7 -1
  15. package/dist/mcp-authoring.js +2 -2
  16. package/dist/mcp.js +1 -1
  17. package/dist/policies.js +3 -1
  18. package/dist/prerender.js +4 -0
  19. package/dist/readiness.js +3 -0
  20. package/dist/route-diff.js +12 -5
  21. package/dist/types/explain.d.ts +6 -0
  22. package/dist/types/extensions.d.ts +1 -1
  23. package/dist/types/manifest.d.ts +5 -1
  24. package/dist/types/readiness.d.ts +2 -0
  25. package/dist/types/types.d.ts +8 -1
  26. package/dist/types.js +8 -1
  27. package/dist/typescript-authoring.js +18 -7
  28. package/docs/AI-AUTHORING.md +10 -6
  29. package/docs/ASSETS.md +2 -1
  30. package/docs/AUTH-BACKUP.md +32 -0
  31. package/docs/BEST-PRACTICES.md +17 -9
  32. package/docs/CAPABILITIES.md +1 -1
  33. package/docs/CI-FOLLOWUP-2026-09-19.md +97 -0
  34. package/docs/CI-RELEASE-AUDIT-2026-09-19.md +322 -0
  35. package/docs/CI.md +8 -3
  36. package/docs/CODEBASE-AUDIT-2026-09-20.md +278 -0
  37. package/docs/DEVELOPMENT-PIPELINE.md +181 -0
  38. package/docs/EXTENSIONS.md +15 -5
  39. package/docs/FRAMEWORK.md +22 -18
  40. package/docs/FUNCTION-SECURITY.md +44 -0
  41. package/docs/LOCAL-DEVELOPMENT.md +3 -0
  42. package/docs/OPEN-DECISIONS.md +184 -212
  43. package/docs/OPERATIONAL-PROOF.md +3 -3
  44. package/docs/OPERATIONS.md +3 -3
  45. package/docs/POLICIES.md +13 -5
  46. package/docs/PRERENDER.md +23 -11
  47. package/docs/PROJECT-DIRECTION.md +3 -3
  48. package/docs/READINESS.md +6 -2
  49. package/docs/README.md +18 -41
  50. package/docs/RELEASE-0.4.0-alpha.3.md +50 -0
  51. package/docs/RELEASE-READINESS.md +6 -6
  52. package/docs/RELEASE-SECURITY.md +68 -168
  53. package/docs/RESILIENCE.md +3 -3
  54. package/docs/ROUTING.md +3 -4
  55. package/docs/SECURITY-AUDIT.md +2 -2
  56. package/docs/SPECIFICATION.md +7 -5
  57. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +6 -5
  58. package/docs/SPIKE-BUSINESS-SUITE.md +14 -6
  59. package/docs/SPIKE-CORE-LAYERING.md +48 -17
  60. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +7 -5
  61. package/docs/SPIKE-LAMBDA-COMPILE.md +8 -0
  62. package/docs/STATIC.md +14 -3
  63. package/docs/TOOLING.md +4 -3
  64. package/docs/TYPESCRIPT-AUTHORING.md +6 -1
  65. package/docs/VERSION-ALIGNMENT.md +46 -205
  66. package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +68 -0
  67. package/docs/archive/2026-09-19/MANAGEMENT-SECURITY.md +102 -0
  68. package/docs/{NEXT-PHASE-PLAN.md → archive/2026-09-19/NEXT-PHASE-PLAN.md} +15 -5
  69. package/docs/{NEXT-STEPS.md → archive/2026-09-19/NEXT-STEPS.md} +15 -3
  70. package/docs/archive/2026-09-19/OPEN-DECISIONS.md +277 -0
  71. package/docs/archive/2026-09-19/RELEASE-SECURITY.md +186 -0
  72. package/docs/archive/2026-09-19/ROADMAP.md +386 -0
  73. package/docs/{SPIKE-EXTENSION-MODEL.md → archive/2026-09-19/SPIKE-EXTENSION-MODEL.md} +10 -0
  74. package/docs/{SPIKE-EXTENSIONS.md → archive/2026-09-19/SPIKE-EXTENSIONS.md} +25 -14
  75. package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +776 -0
  76. package/docs/{USABILITY-REVIEW.md → archive/2026-09-19/USABILITY-REVIEW.md} +12 -2
  77. package/docs/archive/README.md +27 -0
  78. package/docs/policies/agents.md +1 -1
  79. package/docs/policies/compression.md +3 -2
  80. package/docs/policies/security.md +3 -2
  81. package/examples/assets/example.yaml +1 -1
  82. package/examples/cookbook/route-index.json +1 -1
  83. package/examples/cookbook/routes/middleware.yaml +1 -1
  84. package/examples/prerender/README.md +14 -6
  85. package/examples/prerender/functions/page.mjs +4 -2
  86. package/examples/prerender/middleware/template.mjs +1 -1
  87. package/examples/prerender/prerender.mjs +1 -1
  88. package/examples/prerender/urlcode.yaml +8 -4
  89. package/llms-full.txt +117 -45
  90. package/llms.txt +5 -4
  91. package/package.json +24 -4
  92. package/packaging/claude-plugin/.claude-plugin/plugin.json +2 -2
  93. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +17 -19
  94. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +9 -9
  95. package/recipes/authenticated-json-api/README.md +4 -3
  96. package/recipes/authenticated-json-api/functions/profile.mjs +2 -1
  97. package/recipes/authenticated-json-api/recipe.yaml +1 -1
  98. package/recipes/contact-form/functions/contact.mjs +2 -1
  99. package/recipes/contact-form/recipe.yaml +2 -2
  100. package/recipes/cors-api/README.md +2 -2
  101. package/recipes/cors-api/recipe.yaml +1 -1
  102. package/recipes/health-page/README.md +1 -1
  103. package/recipes/json-api/README.md +1 -1
  104. package/recipes/json-api/recipe.yaml +3 -3
  105. package/recipes/middleware/README.md +8 -4
  106. package/recipes/protected-download/README.md +1 -1
  107. package/recipes/protected-download/recipe.yaml +1 -1
  108. package/recipes/static-plus-api/README.md +2 -2
  109. package/recipes/static-plus-api/public/index.html +1 -1
  110. package/recipes/static-plus-api/recipe.yaml +1 -1
  111. package/recipes/static-plus-api/urlcode.yaml +1 -1
  112. package/recipes/typescript/recipe.yaml +4 -4
  113. package/skills/urlcode/SKILL.md +6 -6
  114. package/starters/default/AGENTS.md +6 -6
  115. package/docs/SPIKE-MONOREPO.md +0 -322
@@ -0,0 +1,776 @@
1
+ # Spike: consolidating core, auth, admin and ui into one repo
2
+
3
+ > **Closed 2026-09-19: the migration is done, and this document is history.**
4
+ >
5
+ > `urlcode-ui`, `urlcode-auth` and `urlcode-admin` are workspace packages under
6
+ > `packages/`, and all three have been released from this repository:
7
+ > `@jimhoyd/urlcode-ui@0.1.0-alpha.6`, `@jimhoyd/urlcode-auth@0.1.0-alpha.6`,
8
+ > `@jimhoyd/urlcode-admin@0.1.0-alpha.4`, each on the `alpha` dist-tag with
9
+ > `latest` deliberately held behind. Core's tags are unchanged. The three
10
+ > source repositories are gone.
11
+ >
12
+ > **The operational runbook is now
13
+ > [docs/DEVELOPMENT-PIPELINE.md](../../DEVELOPMENT-PIPELINE.md) and
14
+ > [docs/RELEASE-SECURITY.md](../../RELEASE-SECURITY.md).** Read those, not this.
15
+ > Nothing below is an instruction.
16
+ >
17
+ > ### What the plan got wrong, for the next migration
18
+ >
19
+ > 1. **Its strongest argument was overstated.** "What consolidation would newly
20
+ > enforce" assumed the enforcing checks would follow the code. They did not:
21
+ > `check-guidance-claims.ts` had a hardcoded target list that could never
22
+ > match `packages/`, and `check-trust-model-prose.ts` was root-anchored, so
23
+ > package source comments and each package's `llms.txt` stayed unscanned.
24
+ > Consolidation bought Markdown coverage until both scripts were taught to
25
+ > discover workspace packages.
26
+ > 2. **The real wins were the ones it never claimed.** CodeQL had never run on
27
+ > any of the three repositories, despite each `GOVERNANCE.md` saying it did;
28
+ > the first scan produced a high-severity finding in auth (a false positive,
29
+ > HIBP k-anonymity rather than password storage). Windows had never run
30
+ > either, and produced four genuine latent bugs.
31
+ > 3. **Layout A has a cost the comparison missed.** Core is the repository root
32
+ > rather than a workspace member, so npm resolved the packages' core peer
33
+ > **from the registry** until each declared `file:../..`. Everything passed
34
+ > while building against a published core.
35
+ > 4. **Mechanics #1 did not survive.** The repository allows only squash
36
+ > merges, so the ~130 imported commits collapsed into one and `git blame` on
37
+ > `main` resolves to the merge. That made archiving the source repositories
38
+ > load-bearing — and they were deleted instead. Their history survives only
39
+ > as verified bundles beside the earlier retirements.
40
+ > 5. **Nothing was mentioned about releases, which is where the work was.**
41
+ > Four distinct release-path defects surfaced only by releasing: `--prefix`
42
+ > not isolating from workspace links, Changesets narrowing a peer range,
43
+ > the prepare script not building what it typechecks against, and the
44
+ > coordinator racing npm's propagation.
45
+ >
46
+ > The three open questions at the end were answered by doing it: `peers.json`
47
+ > was deleted outright, `git subtree` was used over `filter-repo`, and the
48
+ > core/monorepo naming overlap was never a problem in practice.
49
+
50
+
51
+ > Maintainer update: monorepo work is starting now. The older proposal-only
52
+ > status and instruction to postpone repository changes below are superseded.
53
+ > Retired short-link packages and the deleted docs repository are historical
54
+ > entries, not migration scope.
55
+ >
56
+ > **Correction (2026-09-19, later the same day): middleware is no longer
57
+ > migration scope either.** The instruction this note used to carry —
58
+ > "middleware moves in as its own package with its existing behavior; folding
59
+ > it into core comes afterward" — was overtaken by events.
60
+ > `jimhoyd-com/urlcode-middleware` has been **deleted**, and
61
+ > `@jimhoyd/urlcode-middleware` unpublished from npm at `0.1.0-alpha.2`. There
62
+ > is no `packages/middleware` to create and nothing to fold into core
63
+ > afterward: per-route middleware was already native to core all along
64
+ > (`docs/MIDDLEWARE.md`), and the deleted package only ever offered the same
65
+ > behavior through the extension seam. Every "five repositories" count below
66
+ > is now **four**: core, `urlcode-auth`, `urlcode-admin`, `urlcode-ui`.
67
+
68
+ Status: migration direction accepted and work starting; completion is not claimed.
69
+ The updated analysis from main is retained below.
70
+
71
+ **Precondition re-surveyed 2026-09-19, after the cleanup PRs landed.** An
72
+ earlier revision of this header said the coordinated cleanup PRs were open
73
+ across core, auth, admin, UI and middleware, and told the reader to settle them
74
+ before each package moves. They have since settled — core merged `#175` and
75
+ `#176`, and a fresh survey reports **zero open pull requests across all four**
76
+ in-scope repositories. Core holds open issues; `auth`, `admin` and `ui` report
77
+ zero. Mechanics #0 is therefore satisfied again and mechanics #7 is again a
78
+ no-op. Treat that as perishable and re-run it per repository immediately before
79
+ that repository moves, exactly as mechanics #0 says. Trust by default and
80
+ explicit sandbox choices remain unchanged.
81
+
82
+ > **Update (2026-09-19) — reviewed against the live repositories, npm and the
83
+ > checks that have landed since. Four things changed; the recommendation did
84
+ > not.**
85
+ >
86
+ > 1. **Scope is five repositories, not six.** `urlcode-dynamic-link` was
87
+ > deleted rather than migrated, along with `urlcode-short` and
88
+ > `urlcode-docs` — all three unpublished from npm and their GitHub
89
+ > repositories removed. In scope: core, `urlcode-auth`, `urlcode-admin`,
90
+ > `urlcode-ui`, `urlcode-middleware`. **Superseded later the same day —
91
+ > `urlcode-middleware` was deleted too, making it four. See the correction
92
+ > at the top.**
93
+ > 2. **The hard precondition is currently met.** Zero open pull requests *and*
94
+ > zero open issues across all five (surveyed 2026-09-19), so mechanics #0 is
95
+ > satisfied and mechanics #7 is a no-op. This window closes on its own.
96
+ > **Re-surveyed after the cleanup PRs landed: still zero open PRs, now
97
+ > across four repositories; core has since accumulated open issues. See the
98
+ > top of this document.**
99
+ > 3. **The drift this plan opened with has already recurred** — see "The
100
+ > problem this is answering" below. It is now an observation, not a forecast.
101
+ > 4. **A second argument exists that this document does not make:** the
102
+ > enforcing trust-model check cannot reach the four downstream repositories.
103
+ > See "What consolidation would newly enforce".
104
+ >
105
+ > Sections below carry their own dated notes where the text they replace is
106
+ > kept for the record. Anything not marked still reads as originally written.
107
+
108
+ Treat this the same way as the other `SPIKE-*.md` documents in this
109
+ directory: a recorded decision trail for the maintainer to accept, amend or
110
+ reject. **Superseded in part — the maintainer has since accepted the
111
+ direction (see the top), so the plan below is committed scope, not a
112
+ proposal.**
113
+
114
+ ## What this is not
115
+
116
+ > **Superseded, and now factually false.** The paragraph below is kept because
117
+ > it describes the state the plan was drafted in. It said "no git history has
118
+ > been merged"; that stopped being true when `urlcode-ui` was merged in as
119
+ > `packages/ui`. See "What has landed" immediately below for the current
120
+ > state.
121
+
122
+ This is not a recommendation to touch any of `urlcode`, `urlcode-auth`,
123
+ `urlcode-admin` or `urlcode-ui` tonight. No git history has been merged, no
124
+ package has been moved, no CI has been reconfigured. Everything below is a
125
+ sequenced plan to review, not a changelog of what happened.
126
+
127
+ ## What has landed
128
+
129
+ > **Added 2026-09-19.** This section is a changelog, not a plan. Everything
130
+ > above it that reads as a proposal should be checked against this first.
131
+
132
+ **All three extensions are in: `packages/ui`, `packages/auth`,
133
+ `packages/admin`.** Sequencing steps 1-5 are done -- step 5 being void, since
134
+ the package it named was deleted rather than migrated. Step 6 and the
135
+ npm re-registrations have not happened, and nothing outward-facing has been
136
+ done at all. The precondition was re-verified immediately before each move and
137
+ held every time: zero open PRs, zero open issues.
138
+
139
+ Suite sizes after the move, all green against core's working tree: core
140
+ 512/514 (2 pre-existing skips), auth 204, admin 65, ui 57 with none skipped.
141
+ Auth and admin each lost their `peers.test.ts` along with the file it tested.
142
+
143
+ Specifically:
144
+
145
+ **A cost of layout A that this document does not mention, found on auth's
146
+ move.** Core is the repository root rather than a workspace member, so npm does
147
+ not link it as a workspace sibling: it resolved auth's `@jimhoyd/urlcode` peer
148
+ from the **registry** instead, at the published `0.4.0-alpha.2`, which is 24
149
+ commits behind this tree. Auth would have been built and tested against a
150
+ published core while sitting next to the real one -- the exact drift this
151
+ consolidation exists to remove, reintroduced by the consolidation itself, and
152
+ silently. The fix took two attempts, and the first one failed in a way worth recording.
153
+ A root `"overrides": {"@jimhoyd/urlcode": "file:."}` produced the right symlink
154
+ on disk but wrote a lockfile entry resolving it to `packages/auth`, so
155
+ `npm ci` rejected the tree outright -- caught by CI, not locally, because
156
+ `npm install` tolerates the drift that `npm ci` refuses. Each package now
157
+ declares `"@jimhoyd/urlcode": "file:../.."` as a devDependency instead, which
158
+ is unambiguous.
159
+
160
+ Both failure modes are invisible to the test suites: in the first, everything
161
+ passed against a published core. So
162
+ [`scripts/check-workspace-links.ts`](../scripts/check-workspace-links.ts)
163
+ asserts in `npm run check` that every package declaring core as a peer
164
+ resolves it to this repository's own `package.json`. Layout B would not have
165
+ had this problem at all; it is a real, if small, entry on layout A's side of
166
+ the ledger that the original comparison missed.
167
+
168
+ Auth's own numbers, for the record: 206 tests passed against core's working
169
+ tree at HEAD, so the 24-commit pin gap was stale bookkeeping and nothing more.
170
+ 204 remain after `peers.test.ts` was deleted with the file it tested.
171
+
172
+ - `git subtree add --prefix=packages/<name>` for each, with the precondition
173
+ re-verified immediately before every move (zero open PRs, zero open issues).
174
+ On the branch, authorship history was preserved and `git blame` resolved
175
+ through the move. **It does not survive the merge -- see below.**
176
+
177
+ ### The merge squashed, and mechanics #1 did not survive it
178
+
179
+ > **Decided 2026-09-19.** `jimhoyd-com/urlcode` allows squash merges only:
180
+ > merge commits and rebase are both disabled, and the `Protect main` ruleset
181
+ > requires linear history, which is *why* they are disabled. A
182
+ > history-preserving merge was therefore not available without suspending that
183
+ > rule, and the maintainer chose to keep linear history and accept the loss.
184
+
185
+ Mechanics #1 says to preserve history "so `git log`/`git blame` on
186
+ `packages/auth/src/auth.ts` still resolves to the real authorship history."
187
+ On `main`, **it does not.** The 142 commits on the branch -- including roughly
188
+ 130 imported from the three source repositories -- collapsed into one squash
189
+ commit, so every migrated file blames to that single commit.
190
+
191
+ Two consequences follow, and the second is the important one:
192
+
193
+ 1. `git subtree add` bought nothing that a file copy would not have, *for
194
+ `main`*. It was not wasted: the branch history is what made each move
195
+ reviewable, and it is why the imported commits exist anywhere at all.
196
+ 2. **Archiving the three source repositories rather than deleting them is now
197
+ load-bearing, not a preference.** They are the only remaining copy of the
198
+ authorship history for `packages/ui`, `packages/auth` and `packages/admin`.
199
+ Step 6 already said archive rather than delete; that instruction has
200
+ stopped being about inbound links and redirects and become the retention
201
+ policy for the history itself. Deleting them would destroy it outright, the
202
+ way the September retirements did for the packages they withdrew -- and
203
+ unlike those, this code is still shipping.
204
+ - The root `package.json` declares `"workspaces": ["packages/*"]`, and the root
205
+ `verify` script now runs each workspace's own `verify` — without that, ui's
206
+ 57 tests silently stop running the moment it becomes a workspace.
207
+ - Changesets is configured in `.changeset/`, with `fixed` and `linked` empty so
208
+ independent versioning is preserved. Core is not covered by it, because under
209
+ layout A core is the repository root rather than a workspace member.
210
+ - **Mechanics #3 is done for ui: `peers.json` is gone**, along with
211
+ `scripts/peer-revisions.mjs` and the workflow that read them. ui's
212
+ cross-repository test resolves core from the repository root instead, so it
213
+ runs by default rather than skipping. There is no pinned peer revision left
214
+ to go stale -- a workspace package and its sibling are the same commit by
215
+ construction. ui had no `peers.test.ts`; `auth` and `admin` do, and theirs
216
+ will need deleting with the file.
217
+ - **Mechanics #4 and #5 are done for ui.** `packages/ui/.github/` has been
218
+ removed rather than left inert: GitHub reads workflows, `CODEOWNERS`,
219
+ `dependabot.yml` and issue templates only from the repository root, so every
220
+ file in it was dead where it sat. Verification moved to core's `ci.yml`,
221
+ which now covers ui through the root `verify` on a 3x3 OS/Node matrix --
222
+ wider than the ubuntu-only workflow ui had of its own. Releases moved to
223
+ [`.github/workflows/release-ui.yml`](../.github/workflows/release-ui.yml).
224
+ Root `CODEOWNERS` already matched ui's (`* @jimhoyd`) so nothing was lost,
225
+ and Dependabot's npm entry at `/` covers workspaces from the root. The
226
+ cross-repository links in ui's docs are now relative, including
227
+ `docs/SPIKE-UI.md`'s link to the extension model review, which had been a
228
+ 404 since `98b5659` archived its target -- independent of this migration.
229
+ ui's `package.json` `repository`/`homepage`/`bugs` name this repository, with
230
+ `repository.directory` set to `packages/ui`, since those ship to npm.
231
+
232
+ **`pack-sources.mjs` is decided and done.** The two near-identical copies in
233
+ `packages/auth/scripts/` and `packages/admin/scripts/` are replaced by one
234
+ [`scripts/pack-sources.mjs`](../scripts/pack-sources.mjs) at the root. Its four
235
+ repository paths and its `peers.json`-derived core pin are replaced by a single
236
+ required `--revision`. The guarantee an operator verifies got *shorter and
237
+ stronger*: one commit identifies core, ui, auth and admin simultaneously, where
238
+ before it took four revisions plus trust that the `peers.json` pins agreed. What
239
+ is given up is building a mix of revisions across packages, which was the drift
240
+ vector this consolidation exists to remove. The step that installed each freshly
241
+ built tarball as the next package's peer is gone too: the workspace resolves
242
+ every sibling to this tree by construction, which is a stronger guarantee than
243
+ installing tarballs built from it moments earlier, and
244
+ `scripts/check-workspace-links.ts` enforces it.
245
+
246
+ **Not done, and outward-facing -- the maintainer's to do:**
247
+
248
+ 1. **Re-register the npm trusted publishers**, all three under
249
+ `jimhoyd-com/urlcode`: `@jimhoyd/urlcode-ui` against
250
+ `.github/workflows/release-ui.yml`, `@jimhoyd/urlcode-auth` against
251
+ `release-auth.yml`, `@jimhoyd/urlcode-admin` against `release-admin.yml`
252
+ (mechanics #6).
253
+ The entry is pinned to a repository *and a workflow filename*, and the
254
+ filename had to change because core already owns `release.yml`. Until this
255
+ is done the publish step fails closed, which is correct behavior rather
256
+ than a bug: **ui cannot be released from here yet.**
257
+ 2. **Archive `jimhoyd-com/urlcode-ui`, `-auth` and `-admin`** (step 6)
258
+ -- but only after a release
259
+ from the new location has actually worked. Archive, do not delete: unlike
260
+ the September retirements, this code continues to live at a new path, so
261
+ the clone-URL redirect is the entire point.
262
+ 3. **Retag.** `git subtree add` did not carry ui's four `v0.1.0-alpha.*` tags,
263
+ and they are not re-creatable under the scheme decided above anyway. They
264
+ remain in the source repository until it is archived.
265
+
266
+ ### Corrections this migration forced on the plan
267
+
268
+ Four claims above did not survive contact, and one of them was the document's
269
+ strongest argument.
270
+
271
+ 1. **"What consolidation would newly enforce" was substantially overstated, and
272
+ is now true only because the checks were changed to make it true.** As
273
+ written, `check-trust-model-prose.ts` matched `PROJECT_ROOTS` against
274
+ root-relative prefixes, so `packages/ui/src/*.ts` comments were **not**
275
+ scanned; its `EXTRA_FILES` was root-only, so `packages/ui/llms.txt` -- the
276
+ most agent-facing file the package ships -- was scanned by neither check;
277
+ and `check-guidance-claims.ts` used a hardcoded ten-path `TARGETS` list that
278
+ could never reach a package at all. Consolidation on its own would have
279
+ bought Markdown coverage and nothing else. Both scripts now discover
280
+ workspace packages from disk, which was verified by planting violations in
281
+ `packages/ui/src/kit.ts` and `packages/ui/llms.txt` and confirming a
282
+ non-zero exit. File counts went 417 -> 467 and targets 10 -> 12.
283
+ 2. **"21 commits behind" was 24** by the time the move happened, and would have
284
+ kept drifting. Figures in this document go stale within a day; re-measure
285
+ rather than cite.
286
+ 3. **`git log <new path>` does not show pre-move history**, contrary to
287
+ mechanics #1. `git blame` does, and nothing is lost, but `git log
288
+ packages/ui/src/kit.ts` returns only the subtree-add commit because the
289
+ original 47 commits record the path as `src/kit.ts`. Use
290
+ `git log <old-sha> -- src/kit.ts`.
291
+ 4. **Two day-one breakages the plan did not anticipate.** Core's `eslint .`
292
+ reaches `packages/` immediately, and ui had never been linted: 10 errors on
293
+ the merge commit, plus more from generated `dist/` output once built,
294
+ because the root eslint ignores were root-anchored rather than `**/`-
295
+ anchored. Separately, `scripts/build-styles.mjs` hardcoded a package-local
296
+ `node_modules` path that does not exist once npm hoists devDependencies to
297
+ the workspace root. Both are fixed. Expect the same class of breakage from
298
+ `auth` and `admin`, which have 41 and 7 lint errors respectively and have
299
+ also never been linted.
300
+
301
+ **A collision this document does not mention at all -- now settled.** Core and
302
+ every extension trigger releases on `tags: ['v*']`, and their alpha tags
303
+ literally overlap: ui carries `v0.1.0-alpha.2` through `-alpha.5`, admin
304
+ `v0.1.0-alpha.1` and `-alpha.3`, auth `v0.1.0-alpha.1` through `-alpha.3`. In
305
+ one repository, pushing a bare `v*` tag fires more than one release workflow.
306
+
307
+ Decided: workspace packages release on Changesets' `<package name>@<version>`
308
+ form, core keeps `v*`, and the two cannot collide because a scoped name starts
309
+ with `@`. See [open decisions, "Accepted: per-package release
310
+ tags"](OPEN-DECISIONS.md) for the reasoning and
311
+ [`scripts/check-release-tags.ts`](../scripts/check-release-tags.ts), which
312
+ fails `npm run check` if a future package workflow breaks the scheme. `ui`'s
313
+ workflow has been moved onto it already, including the tag-to-version parsing
314
+ that depended on the old `v` prefix.
315
+
316
+ `git subtree add` does not carry tags, so none of ui's four came across. They
317
+ would not be re-creatable under the old scheme anyway.
318
+
319
+ ## The problem this is answering
320
+
321
+ Four repos (`urlcode`, `urlcode-auth`, `urlcode-admin`, `urlcode-ui`) already
322
+ coordinate tightly — `auth`/`admin`/`ui` each pin an exact core revision in
323
+ their own `peers.json`, and `docs/FRAMEWORK.md` describes them as one
324
+ composed product, not four independent ones. Concretely observed cost of that
325
+ coordination happening across four repos, from an evening spent reading all
326
+ four:
327
+
328
+ - **Observed and since fixed, which is the point rather than a counterpoint.**
329
+ When core landed trusted-by-default execution (`b3bde4e`), `urlcode-auth`
330
+ and `urlcode-admin` were both still pinning core at `50790d3a`
331
+ (`0.4.0-alpha.1`), predating it, and `urlcode-auth/SECURITY.md` still
332
+ carried a sentence ("sandboxed guest code") that assumed the old model.
333
+ Both have since been corrected — both repos now pin `d5e86017`, and that
334
+ sentence is gone. Nothing was ever broken in production by either.
335
+ The cost this plan is describing is not "drift goes unnoticed forever"; it
336
+ is that catching and fixing it took a manual pass across three separate
337
+ repositories, with nothing structural to catch it automatically — no
338
+ mechanism flags a downstream repo's prose or pin as stale when an upstream
339
+ contract changes underneath it. That pass has to be repeated by hand on
340
+ every future contract change, for every downstream repo, indefinitely.
341
+ Consolidation removes the class of work, not just this instance of it.
342
+
343
+ > **Update (2026-09-19): it recurred, which settles the argument.** The
344
+ > correction recorded above held for roughly one day. `urlcode-auth`,
345
+ > `urlcode-admin` and `urlcode-ui` all still pin core at `d5e86017`
346
+ > (2026-09-18), now **21 commits behind core's `main`** — a span that
347
+ > includes the trusted-by-default propagation in `db375bf` and the
348
+ > retirements in `10c2439`. Nothing is broken in production again, and that
349
+ > is again beside the point: the manual pass this document described as
350
+ > repeating indefinitely repeated within twenty-four hours of being
351
+ > performed. This is no longer a predicted cost.
352
+ - Two more repos, planned in `docs/SPIKE-CORE-LAYERING.md` and originally
353
+ drafted here as "not yet created," turned out to already exist by the time
354
+ this doc was reviewed: `urlcode-dynamic-link` (7 commits, Phase 2 already
355
+ implemented, `v0.1.0-alpha.1` released) and `urlcode-middleware` (5 commits,
356
+ implemented, `v0.1.0-alpha.1` released), each with its own real commit
357
+ history, release workflow and open issues. That raises the
358
+ actively-coordinated repo count from four to six today, not hypothetically
359
+ — before this plan even accounts for `urlcode-template`, `urlcode-short`,
360
+ `urlcode-docs`, `urlcode-cloud` and `homebrew-urlcode`. It also means
361
+ "create them directly in the monorepo" (this doc's original framing) is no
362
+ longer available for these two — they now need the same history-preserving
363
+ migration as `auth`/`admin`/`ui`, covered in "Migration mechanics" below.
364
+
365
+ None of this is a defect in any one repo. It's the accumulating tax of
366
+ coordinating tightly-coupled, independently-versioned packages across
367
+ separate git histories, issue trackers and CI pipelines by hand.
368
+
369
+ ## Scope: what moves, what doesn't
370
+
371
+ Decided (see conversation this spike is drafted from):
372
+
373
+ **In scope — originally six existing repos, all with real history, folded
374
+ into one repo as workspace packages. Two of the six were deleted instead of
375
+ migrated, leaving four:**
376
+
377
+ | Repo today | Becomes |
378
+ |---|---|
379
+ | `urlcode` (core) | `packages/core` (or repo root stays core-shaped, TBD in "Layout options" below) |
380
+ | `urlcode-auth` | `packages/auth` |
381
+ | `urlcode-admin` | `packages/admin` |
382
+ | `urlcode-ui` | `packages/ui` |
383
+ | ~~`urlcode-dynamic-link`~~ | **No longer applicable — repository deleted 2026-09-19, not migrated.** See the note below. |
384
+ | ~~`urlcode-middleware`~~ | **No longer applicable — repository deleted 2026-09-19 at `0.1.0-alpha.2`, not migrated.** Per-route middleware is native to core; see the correction at the top. |
385
+
386
+ > **Update (2026-09-19): five, not six.** `urlcode-dynamic-link` was created,
387
+ > released `v0.1.0-alpha.1`, and deleted within days. Read as evidence rather
388
+ > than as a lost migration target, it is the sharpest data point this document
389
+ > has: standing up a repository per extension was costly enough that one of
390
+ > them was unwound outright rather than maintained. The section "Why six, and
391
+ > not four" below should be read as "why five, and not four"; its argument
392
+ > about `middleware` having already paid the coordination cost was unaffected
393
+ > at the time — though `middleware` was itself deleted later the same day, so
394
+ > the pattern this note reads as a one-off turned out to repeat.
395
+
396
+ **Explicitly out of scope — three live repositories, each for a distinct, real
397
+ reason, not just "left for later":**
398
+
399
+ > **Update (2026-09-19):** this list was four. Two of its entries no longer
400
+ > exist: `urlcode-short` and `urlcode-docs` were both deleted, so neither is a
401
+ > candidate for anything. The three that remain — `homebrew-urlcode`,
402
+ > `urlcode-cloud`, `urlcode-template` — are unaffected, and their reasons hold
403
+ > exactly as written.
404
+
405
+ - **`homebrew-urlcode`** — cannot move. Homebrew tap conventions require a
406
+ repo literally named `homebrew-<name>`; this is an external platform
407
+ constraint, not a project choice.
408
+ - **`urlcode-docs`** — `AGENTS.md` is explicit that public documentation is
409
+ "authored there directly," deliberately separate from code, "no longer
410
+ generated from this repository." Folding it in would reverse a stated,
411
+ recent decision, not follow one.
412
+
413
+ > **Update (2026-09-19): resolved — the repository is deleted.** The decision
414
+ > quoted above was reversed, and then `urlcode-docs` was unpublished and
415
+ > **removed from GitHub**, not merely archived. Documentation is authored in
416
+ > this repository's `docs/`, and `AGENTS.md` no longer sends pages anywhere
417
+ > else — see [open decisions, item 6](OPEN-DECISIONS.md). This entry is kept
418
+ > only so the reversal is legible; there is nothing left to include or
419
+ > exclude.
420
+ - **`urlcode-cloud`** — a separately-lifecycled hosted product (private
421
+ repo); its release cadence and access model have no reason to match a
422
+ library monorepo's.
423
+ - **`urlcode-template`** (and, when it existed, `urlcode-short`) — an
424
+ example/starter project, not a library package. Mixing "things you
425
+ `npm install`" with "things you `git clone` as a starting point" in one
426
+ workspace is a different kind of repo than what this spike is solving for.
427
+ `urlcode-short` was deleted on 2026-09-19; the reasoning survives it and
428
+ still governs `urlcode-template`, which remains out of scope.
429
+
430
+ ## Why five, and not four — resolved: it is four
431
+
432
+ > **Update (2026-09-19):** written as "why six", then narrowed to five when
433
+ > `dynamic-link` was deleted. `middleware` has since been deleted as well, so
434
+ > the answer is **four**, and this section is now entirely historical. It is
435
+ > kept because the reasoning is what the outcome refutes: the argument below
436
+ > was that `link` and `middleware` had already paid the coordination cost and
437
+ > should therefore be folded in rather than left outside the fix. Both were
438
+ > instead withdrawn altogether. That is a third possible response to the
439
+ > coordination cost this document is about — not "consolidate it" and not
440
+ > "keep paying it", but "stop shipping the thing" — and it is the one that
441
+ > actually happened, twice. Worth weighing before the next extension gets its
442
+ > own repository.
443
+
444
+ `link` and `middleware` were extracted *out* of core specifically so core
445
+ stays "the smallest thing that is still a complete product on its own"
446
+ (`docs/SPIKE-CORE-LAYERING.md`). Both are now real, shipped repos: they
447
+ already paid the coordination cost this spike is trying to remove —
448
+ `urlcode-dynamic-link`'s and `urlcode-middleware`'s own `peers.json`-style
449
+ pins against core, their own CI, their own docs that can drift the same way
450
+ `urlcode-auth/SECURITY.md` already did. Folding them into this consolidation
451
+ alongside `auth`/`admin`/`ui` stops that from compounding further, rather
452
+ than leaving two more repos outside the fix.
453
+
454
+ ## Layout: decided — option A
455
+
456
+ **A. Root repo is core, extensions live under `packages/`.**
457
+ ```
458
+ urlcode/
459
+ src/ # core, unchanged in place
460
+ packages/
461
+ auth/
462
+ admin/
463
+ ui/
464
+ ```
465
+ Lowest-friction for core's own history (nothing moves), but makes "core" and
466
+ "the monorepo" the same name, which may read as core absorbing the
467
+ extensions rather than the extensions and core coexisting as peers — worth a
468
+ naming discussion given `AGENTS.md`'s "Core never imports them" independence
469
+ framing.
470
+
471
+ **B. Everything moves under `packages/`, including core — considered, not
472
+ chosen.** Would have been symmetric and avoided the naming overlap noted
473
+ above, at real cost: core's own history would need to move too, and every
474
+ external reference to `urlcode`'s current repo path (`docs/`, READMEs
475
+ elsewhere, the `@jimhoyd/urlcode` package's repository field, CI badges,
476
+ this evening's own `peer-camera`/`peer-eyes` citations) would need updating.
477
+ Decided against for exactly that reason.
478
+
479
+ **Decided: (A).** Core's repo and history stay exactly where they are; the
480
+ extension packages move to it — three of them, `auth`, `admin` and `ui`,
481
+ after the `dynamic-link` and `middleware` deletions. The one open item this
482
+ still leaves,
483
+ worth a short naming discussion rather than blocking anything: "core" and
484
+ "the consolidated repo" now share a name, which could read as core absorbing
485
+ the extensions rather than the two coexisting as independent packages
486
+ (`AGENTS.md`'s "Core never imports them" framing still holds in code either
487
+ way — this is a naming-perception question, not a contract question).
488
+
489
+ ## Migration mechanics, per repo
490
+
491
+ For each of `urlcode-auth`, `urlcode-admin` and `urlcode-ui` — three repos
492
+ with real history, joining core, which stays in place (as of 2026-09-19;
493
+ drafted as five, before `urlcode-dynamic-link` and then `urlcode-middleware`
494
+ were deleted):
495
+
496
+ 0. **Drain open pull requests first — a hard precondition, not a courtesy.**
497
+ Before a repo is migrated, it must have zero open PRs (and no unmerged
498
+ release branch). A PR open against the source repo at the moment its code
499
+ moves is stranded: its branch targets a `main` that no longer receives
500
+ code, its diff is written against paths (`src/…`) that no longer exist at
501
+ that location, and re-creating it against the consolidated repo means
502
+ rebasing onto a different repository and a new path prefix
503
+ (`packages/<name>/src/…`) by hand. GitHub cannot retarget a PR across
504
+ repositories. So for each repo, in order: stop merging new work, merge or
505
+ close what is open, confirm `gh pr list`/the API reports none, then
506
+ migrate. Any PR that cannot be merged in time should be closed with its
507
+ branch preserved and re-opened against the consolidated repo afterwards —
508
+ a deliberate choice recorded on the PR, not an accident discovered later.
509
+ This is also the real reason to pick a quiet window for the migration
510
+ rather than a busy one: the cost of this step scales with how much is
511
+ in flight.
512
+
513
+ > **Update (2026-09-19): this precondition is met right now.** Surveyed
514
+ > across all five in-scope repositories: `urlcode`, `urlcode-auth`,
515
+ > `urlcode-admin`, `urlcode-ui` and `urlcode-middleware` each report **zero
516
+ > open pull requests**, and all but core report zero open issues (core holds
517
+ > `#168` and `#58`, neither of which is a migration blocker). Nothing is in
518
+ > flight anywhere. This is the quiet window this step asks for, and it is not
519
+ > a stable state — it closes the moment work resumes on any of the five.
520
+ >
521
+ > **Re-surveyed later the same day, after the cleanup PRs opened and
522
+ > merged:** four in-scope repositories now (`urlcode-middleware` is
523
+ > deleted), still **zero open pull requests across all four**. `auth`,
524
+ > `admin` and `ui` report zero open issues; core's open-issue count has
525
+ > grown past the two named above and none of them block migration either.
526
+ > The window described as closing on its own has so far reopened each time
527
+ > — which is an argument for re-running the survey, not for trusting any
528
+ > recorded figure in this document.
529
+ 1. **Preserve history with `git subtree add` or `git filter-repo` +
530
+ merge**, not a fresh copy — so `git log`/`git blame` on
531
+ `packages/auth/src/auth.ts` still resolves to the real authorship history
532
+ from `urlcode-auth`, and so a future "actually, let's give this its own
533
+ repo back" is a clean `git filter-repo` extraction, not archaeology.
534
+ `git subtree` is the lower-risk default (reversible, no force-push
535
+ required on the source repos); `git filter-repo` gives cleaner resulting
536
+ history at the cost of being a one-way rewrite of the joining repo's
537
+ local copy (the original `urlcode-auth` GitHub repo is untouched either
538
+ way — this only rewrites what gets pulled in).
539
+ 2. **npm workspace restructuring**: `package.json` at the monorepo root gets
540
+ `"workspaces": ["packages/*"]` (the same shape `peer-camera` already
541
+ uses); each `packages/<name>/package.json` keeps its own name/version,
542
+ independently publishable — this is what preserves "independently
543
+ versioned packages" as a property, not something this migration gives up.
544
+ **Decided: [Changesets](https://github.com/changesets/changesets) for the
545
+ release flow, not Nx or Turborepo.** A changeset is a small, bounded,
546
+ git-diffable markdown file (package name + semver bump + description) —
547
+ cheap and low-risk for an agent or a human to generate correctly, easy
548
+ for CI to verify mechanically ("does every touched package have one"),
549
+ and it's the deliberate checkpoint that stops local workspace-linked
550
+ development (testing against a sibling package's unreleased state, which
551
+ is now the default once auth/admin/ui sit next to
552
+ core) from silently becoming a real release. Nx/Turborepo were considered
553
+ and set aside: both add a much larger, more inference-heavy configuration
554
+ surface (task graphs, remote caching semantics) that's a bigger, more
555
+ opaque thing to get wrong than this repo's four packages currently need —
556
+ plain `npm test -w packages/auth`-style workspace scoping already covers
557
+ what this size of repo actually requires. Revisit only if the package
558
+ count grows enough that rebuild/retest time becomes a real problem.
559
+ 3. **`peers.json` becomes unnecessary for the three that moved** — a
560
+ workspace package can depend on a sibling workspace package directly
561
+ (`"@jimhoyd/urlcode": "workspace:*"` or npm's equivalent), which is
562
+ inherently always in sync, no separate pin file, no drift possible by
563
+ construction. `peers.json`-the-mechanism might still matter if any
564
+ *external* consumer needs a reviewed-revision pin story — worth deciding
565
+ explicitly rather than silently dropping the safeguard.
566
+ 4. **CI consolidation**: one `verify.yml` (or similar) with
567
+ path-filtered jobs per package, replacing four separate workflow files.
568
+ `CODEOWNERS` can still express per-package ownership within one repo
569
+ (path-scoped rules), so "who reviews auth changes" doesn't have to
570
+ become "everyone reviews everything."
571
+ 5. **Docs cross-references**: every `EXTENSIONS.md` links into another repository-
572
+ style cross-repo link in `auth`/`admin`/`ui`'s current docs becomes a
573
+ same-repo relative link once consolidated — this is a real cleanup
574
+ opportunity, not just migration overhead, since it directly targets the
575
+ "docs silently drifted apart" problem this spike opened with.
576
+ 6. **Re-register npm Trusted Publishing per package.** Every repo's
577
+ release workflow publishes via OIDC trusted publishing, no long-lived npm
578
+ token (`docs/SPIKE-CORE-LAYERING.md`'s governance section, confirmed by
579
+ `urlcode-middleware`'s own "Add trusted-publishing release workflow"
580
+ commit). That trust is registered on npmjs.com per package, pinned to an
581
+ exact GitHub repo + workflow filename (+ optional environment) — it does
582
+ not follow the code when the repo path changes. Each of
583
+ `@jimhoyd/urlcode-auth`, `-admin` and `-ui` needs its
584
+ npmjs.com trusted-publisher entry updated to the new repo and new workflow
585
+ path *before* that package's first release from the consolidated location,
586
+ or the publish step fails closed (correctly — not a security gap, just an
587
+ ordering dependency this plan needs to carry explicitly rather than
588
+ discover at release time).
589
+ 7. **Issue migration — decided: recreate open issues in the consolidated
590
+ repo, not leave-and-link.** GitHub doesn't move issues across repos
591
+ natively, so this means bulk-recreating each open issue at the new
592
+ location with a back-link to the original (closed with a pointer) rather
593
+ than leaving it where it is.
594
+
595
+ > **Update (2026-09-19): currently a no-op — there is nothing to
596
+ > recreate.** `urlcode-auth`, `urlcode-admin` and `urlcode-ui` all report
597
+ > **zero open issues**. Middleware's two, which this step was written
598
+ > around, were both closed before its repository was deleted (`#1`,
599
+ > `sandbox: true` unsupported, and `#3`, the vendored core tarball), along
600
+ > with a later `#4`. Those issue links no longer resolve — the deletion
601
+ > took the tracker with it — but the issue bodies were captured to
602
+ > `urlcode-middleware-issues.json` alongside the code bundle, so the
603
+ > content survives even though the URLs do not. `urlcode-dynamic-link`'s
604
+ > tally is equally moot: that repository is gone too. The decision above
605
+ > stands as policy for whatever is open at migration time; the concrete
606
+ > scope it enumerated has emptied out. Re-survey immediately before
607
+ > migrating rather than trusting this line.
608
+
609
+ The original scope, for the record:
610
+
611
+ > `auth`, `admin` and `ui`'s own open-issue counts weren't re-audited here,
612
+ > but `urlcode-dynamic-link` and `urlcode-middleware` were, since they're
613
+ > the two repos whose "does this even apply" status changed mid-conversation:
614
+ > `urlcode-dynamic-link` had 0 open issues — nothing to migrate.
615
+ > `urlcode-middleware` had 2 open issues to recreate, `#1`
616
+ > ("`sandbox: true` is not supported — needs its own QuickJS/WASM worker
617
+ > pool") and `#3` ("Remove vendored core tarball once `@jimhoyd/urlcode`
618
+ > 0.4.0-alpha.2+ is published to npm"). Both were to move to the
619
+ > consolidated repo's tracker when the merge actually happened, each closed
620
+ > in its original location with a link to the new issue.
621
+
622
+ ## What consolidation would newly enforce
623
+
624
+ > **Added 2026-09-19.** This section did not exist when the spike was drafted,
625
+ > because the checks it describes did not exist either. It is the strongest
626
+ > argument in the document.
627
+
628
+ Since this plan was written, two checks landed in `npm run check`, and both
629
+ **fail CI** rather than reporting:
630
+
631
+ - [`scripts/check-trust-model-prose.ts`](../scripts/check-trust-model-prose.ts)
632
+ rejects prose describing the pre-`0.4.0-alpha.2` trust model as current.
633
+ Since `db375bf` it reaches well past Markdown: comments in `src/`,
634
+ `scripts/`, `examples/`, `starters/`, `recipes/` and `benchmarks/`, plus
635
+ `llms.txt`/`llms-full.txt`, and it cross-checks that a project whose prose
636
+ claims isolation actually declares `sandbox: true` somewhere in its YAML.
637
+ - [`scripts/check-guidance-claims.ts`](../scripts/check-guidance-claims.ts)
638
+ rejects agent-facing guidance that contradicts
639
+ `schemas/urlcode.schema.json` — including the inverse case, guidance calling
640
+ a field invented when the schema defines it
641
+ ([historical decisions, item 7](archive/2026-09-19/OPEN-DECISIONS.md)).
642
+
643
+ **Both stop at this checkout.** The specific failure this document opens with —
644
+ `urlcode-auth/SECURITY.md` asserting "sandboxed guest code" after core inverted
645
+ the default — sits in a file that neither check can see, and cannot see while
646
+ `auth` lives in its own repository. The same is true of `admin` and `ui`.
647
+ It was also true of `middleware`, whose repository was deleted before the
648
+ question could be settled either way.
649
+
650
+ That reframes what consolidation buys. The original case was that it removes a
651
+ class of manual coordination work. The stronger case, available only now, is
652
+ that it places three packages' prose under an **existing, working, enforcing
653
+ correctness gate** for the contract most likely to be misdescribed downstream —
654
+ trusted-by-default execution, which is precisely where the observed drift
655
+ happened. No other proposal on the table extends that check's reach; writing a
656
+ cross-repository variant of it would mean building and maintaining a CI job that
657
+ clones three repositories on every core change, which is the coordination cost
658
+ again wearing a different hat.
659
+
660
+ One related gap, unchanged: `npm run check:downstream-skills` is advisory and
661
+ sits outside both `check` and `verify`, consistent with
662
+ [historical decisions, item 9](archive/2026-09-19/OPEN-DECISIONS.md).
663
+ The cleanup review has since adjudicated the template's skill drift and prepared
664
+ aligned copies in its draft PR; the report itself remains advisory.
665
+
666
+ ## What this preserves, unchanged
667
+
668
+ - **The trust/extension model itself.** `packages/auth` published from the
669
+ monorepo is exactly as separate a package, with exactly the same
670
+ `RuntimeExtension` contract, revision-pinning and operator-registration
671
+ requirements, as `urlcode-auth` published from its own repo today. This
672
+ spike changes where the source lives, not what the extension mechanism
673
+ guarantees.
674
+ - **Independent versioning and release cadence per package** — a monorepo
675
+ with workspaces is not "one version number for everything."
676
+
677
+ ## What this gives up, honestly
678
+
679
+ - **Per-repo maturity gating.** `docs/SPIKE-CORE-LAYERING.md` records that
680
+ `auth`/`admin`/`ui` used a "`private: true` until reviewed" pattern before
681
+ their first public release, and that the two new repos are deliberately
682
+ *not* following that pattern ("published public from the start"). A
683
+ monorepo can't easily make one folder private and another public — the
684
+ repo-level visibility setting is all-or-nothing on GitHub. Once
685
+ consolidated, "private until reviewed" stops being available as a pattern
686
+ for whatever the next extension after `middleware`/`dynamic-link` turns
687
+ out to be, unless it's built in yet another separate private repo first
688
+ and merged in later — which reintroduces a version of the coordination
689
+ cost this spike is trying to remove, just for pre-release work instead of
690
+ ongoing maintenance.
691
+ - **"Fork just one piece" stops being a plain `git clone` — but scoped to a
692
+ narrow audience, not every auth user.** `SPIKE-AUTH.md` names forkability
693
+ as a deliberate design goal specifically for `auth`. It's important not to
694
+ overstate who this actually affects: a developer customizing auth's look
695
+ or copy (theme, relabeling, `extra.css`, a shadowed template) works
696
+ entirely inside *their own* project repo via the `ui` extension's layering
697
+ system (`ui/copy`, `ui/extra.css`, `ui/templates`) — they never clone or
698
+ fork `urlcode-auth` at all, install it from npm like any dependency, and
699
+ this migration changes nothing for them. The friction increase applies
700
+ only to the much narrower case of someone changing auth's actual *logic*
701
+ (a new sign-in method, different session semantics) — something the
702
+ layering system can't express because it's behavior, not presentation.
703
+ For that persona, forking just the auth package post-consolidation means a
704
+ `git filter-repo`-style history extraction instead of `git clone
705
+ jimhoyd-com/urlcode-auth` — solvable, but a real step up in friction, for
706
+ a small population, not the common path.
707
+ - **Blast radius of a bad CI run.** One consolidated CI means a
708
+ misconfigured job can, in principle, block merges across all four
709
+ packages at once, where today a broken `urlcode-ui` pipeline can't stop an
710
+ unrelated `urlcode-auth` merge. Path-filtered jobs mitigate this but don't
711
+ eliminate it the way full repo separation does.
712
+
713
+ ## Sequencing, if this is accepted
714
+
715
+ 1. Decide layout (A vs. B above) and confirm the out-of-scope list.
716
+ 2. **Check open pull requests across all three joining repos before starting,
717
+ and again per repo immediately before its own migration** (mechanics #0). A
718
+ repo with anything open is not ready to move. Doing this as a survey first
719
+ also sizes the whole migration honestly: the number of in-flight PRs is the
720
+ real scheduling constraint, not the git mechanics.
721
+
722
+ > **Update (2026-09-19):** surveyed — zero open pull requests across all
723
+ > five repositories including core, and zero open issues outside core. The
724
+ > survey this step asks for has been done once and came back clean. Re-run
725
+ > it rather than relying on that, since it goes stale the moment work
726
+ > resumes. **It did go stale, twice over, within the same day:** the cleanup
727
+ > PRs opened and merged, and `urlcode-middleware` stopped existing. The
728
+ > current figure is zero open PRs across four repositories — see the top
729
+ > of this document, and re-run it again anyway.
730
+ 3. Migrate `urlcode-ui` first (fewest inbound dependents — `auth`/`admin`
731
+ both depend on it, nothing depends on them), proving the subtree +
732
+ workspace mechanics on the lowest-risk package. Re-register its npm
733
+ trusted publisher (mechanics #6) before cutting its first release from
734
+ the new location — treat this as part of "done," not a follow-up.
735
+ 4. Migrate `urlcode-auth`, then `urlcode-admin` — same re-registration step
736
+ each time.
737
+ 5. ~~Migrate `urlcode-middleware`.~~ **Void — nothing to migrate.**
738
+
739
+ > **Update (2026-09-19):** this step read "migrate `urlcode-dynamic-link`,
740
+ > then `urlcode-middleware`," and carried their issue tallies. Both halves
741
+ > are now void: each repository was deleted rather than migrated. The
742
+ > migration therefore ends at step 4, with `ui`, `auth` and `admin` moved
743
+ > and core in place. No `packages/middleware` is created, and no
744
+ > trusted-publisher entry is re-registered for `@jimhoyd/urlcode-middleware`
745
+ > — that package is unpublished.
746
+ 6. Retire (archive, don't delete — GitHub redirects an archived repo's clone
747
+ URL) the three now-empty source repos, with their READMEs pointing at the
748
+ new location.
749
+
750
+ > **Update (2026-09-19): "archive, don't delete" now has a counter-example
751
+ > in this project's own history, and it is worth weighing rather than
752
+ > restating.** `urlcode-dynamic-link`, `urlcode-short` and `urlcode-docs`
753
+ > were all **deleted outright**, so their clone URLs and every inbound link
754
+ > to them 404 with no redirect; the only surviving copies are verified
755
+ > `git bundle`s held locally (`fd9dc84`), which preserve the code but not
756
+ > the issues, the pull request history or any inbound URL. That was
757
+ > defensible for repositories whose code was being withdrawn entirely — and
758
+ > it still cost a citation: `urlcode-docs#17` is quoted as evidence in
759
+ > [historical decisions, item 7](archive/2026-09-19/OPEN-DECISIONS.md) and no longer resolves. It is a different case from a
760
+ > repository whose code continues to live at a new path, which is what this
761
+ > step covers and where the redirect is the entire point. Keep the
762
+ > archive-don't-delete rule here, and note explicitly that it diverges from
763
+ > what was done during the September 2026 retirements — the two situations
764
+ > are not the same and the precedent should not be read across.
765
+
766
+ ## Open questions for the maintainer, not answered here
767
+
768
+ - Does `peers.json`'s reviewed-pin discipline need an equivalent for any
769
+ external (non-workspace) consumer, or does workspace-linking fully replace
770
+ its purpose?
771
+ - `git subtree` vs. `git filter-repo` for history preservation — a real
772
+ tradeoff between migration safety and final history cleanliness, worth a
773
+ deliberate call rather than defaulting.
774
+ - The naming-perception question from "Layout: decided — option A" above
775
+ (core's repo and the consolidated repo sharing a name) — worth a short
776
+ discussion, not blocking.