@bongos/core 1.24.45 → 1.24.47

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 (109) hide show
  1. package/.bongos-core.json +151 -106
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +4 -0
  4. package/clients/bongos-client/index.cjs +4 -0
  5. package/clients/bongos-client/index.d.ts +8 -0
  6. package/clients/bongos-client/index.mjs +4 -0
  7. package/config/design-tokens.neutral.json +10 -10
  8. package/docs/adr/0307-autobongos-a-deterministic-supervisor-and-agentic-workers.md +15 -0
  9. package/docs/api/openapi.json +152 -3
  10. package/docs/api-reference.md +4 -2
  11. package/docs/copy-inventory.md +1102 -1102
  12. package/docs/copy-registry.json +1538 -1538
  13. package/docs/module-api-changelog.md +4 -0
  14. package/docs/onboarding/diagrams/assertions.json +2 -2
  15. package/docs/page-inventory.json +11 -0
  16. package/docs/page-readings.json +724 -724
  17. package/migrations/core_295_runner_logins.sql +50 -0
  18. package/modules/government/session-scopes.json +5 -0
  19. package/modules/hall-ui/public/ai-studio.states.json +2 -0
  20. package/modules/hall-ui/public/atlas.states.json +2 -0
  21. package/modules/hall-ui/public/board-room.states.json +2 -0
  22. package/modules/hall-ui/public/city.states.json +2 -0
  23. package/modules/hall-ui/public/collab.states.json +2 -0
  24. package/modules/hall-ui/public/commands.states.json +2 -0
  25. package/modules/hall-ui/public/copy-desk.states.json +2 -0
  26. package/modules/hall-ui/public/deploy.states.json +2 -0
  27. package/modules/hall-ui/public/goal-map.states.json +2 -0
  28. package/modules/hall-ui/public/goals.css +1 -0
  29. package/modules/hall-ui/public/goals.states.json +2 -0
  30. package/modules/hall-ui/public/government.states.json +2 -0
  31. package/modules/hall-ui/public/idea.states.json +2 -0
  32. package/modules/hall-ui/public/ideas.states.json +2 -0
  33. package/modules/hall-ui/public/index-founding.states.json +2 -0
  34. package/modules/hall-ui/public/index-genesis-decide.states.json +2 -0
  35. package/modules/hall-ui/public/index-genesis-demo.states.json +2 -0
  36. package/modules/hall-ui/public/index-genesis.states.json +2 -0
  37. package/modules/hall-ui/public/index.states.json +5 -0
  38. package/modules/hall-ui/public/market-watch-item.states.json +2 -0
  39. package/modules/hall-ui/public/market-watch-rules.states.json +2 -0
  40. package/modules/hall-ui/public/market-watch.states.json +2 -0
  41. package/modules/hall-ui/public/modules.states.json +2 -0
  42. package/modules/hall-ui/public/money.states.json +2 -0
  43. package/modules/hall-ui/public/my-agents.states.json +2 -0
  44. package/modules/hall-ui/public/notes-panel.css +1 -1
  45. package/modules/hall-ui/public/primer.states.json +2 -0
  46. package/modules/hall-ui/public/profile.states.json +2 -0
  47. package/modules/hall-ui/public/roster.states.json +2 -0
  48. package/modules/hall-ui/public/settings.states.json +2 -0
  49. package/modules/hall-ui/public/street-card.states.json +2 -0
  50. package/modules/hall-ui/public/street.states.json +2 -0
  51. package/modules/hall-ui/public/studio.states.json +2 -0
  52. package/modules/hall-ui/public/style.css +27 -24
  53. package/modules/hall-ui/public/task.states.json +2 -0
  54. package/modules/hall-ui/public/thinking.states.json +2 -0
  55. package/modules/hall-ui/public/tweak-editor.states.json +2 -0
  56. package/modules/hall-ui/public/watch.states.json +2 -0
  57. package/modules/hall-ui/public/welcome-boot.js +6 -2
  58. package/modules/hall-ui/public/work-founding.states.json +2 -0
  59. package/modules/hall-ui/public/work.states.json +3 -1
  60. package/modules/lifecycle/db-claims.js +6 -3
  61. package/modules/lifecycle/routes/claims.js +10 -0
  62. package/modules/lifecycle/routes/tasks.js +1 -1
  63. package/modules/lifecycle/runner-self-guard.js +19 -10
  64. package/modules/platform-identity/platform-identity.js +1 -1
  65. package/modules/platform-identity/tests/platform-identity.mjs +13 -2
  66. package/modules/provisioning/core-upgrade-authz.js +8 -0
  67. package/modules/public-landing/public/account.html +1 -0
  68. package/modules/public-landing/public/community.html +1 -0
  69. package/modules/public-landing/public/connections.html +1 -0
  70. package/modules/public-landing/public/contact.html +1 -0
  71. package/modules/public-landing/public/guild.html +1 -0
  72. package/modules/public-landing/public/index.html +1 -0
  73. package/modules/public-landing/public/invitations.html +1 -0
  74. package/modules/public-landing/public/privacy.html +1 -0
  75. package/modules/public-landing/public/projects.html +1 -0
  76. package/modules/public-landing/public/terms.html +1 -0
  77. package/modules/raise/views/gate.html +1 -0
  78. package/modules/raise/views/nda.html +1 -0
  79. package/modules/raise/views/page.html +1 -0
  80. package/modules/status-ui/public/index.html +1 -0
  81. package/modules/ui-design/kit/README.md +1 -1
  82. package/modules/ui-design/kit/fixtures/versions.json +1 -0
  83. package/modules/ui-design/kit/lib.js +48 -3
  84. package/modules/ui-design/kit/probe.js +1 -0
  85. package/modules/ui-design/kit/render.js +2 -0
  86. package/modules/ui-design/kit/serve.js +1 -0
  87. package/modules/ui-design/module.json +1 -1
  88. package/modules/ui-design/public/tokens.css +125 -0
  89. package/modules/ui-design/scripts/gen-design-tokens.js +102 -0
  90. package/package-lock.json +2 -2
  91. package/package.json +1 -1
  92. package/release-notes.json +20 -0
  93. package/scripts/bongos/fitness.js +1 -1
  94. package/scripts/bongos/run-unit-tests.js +3 -0
  95. package/src/bongos/auth.js +25 -4
  96. package/src/bongos/route-rank-check.js +5 -0
  97. package/src/bongos/routes/auth.js +48 -0
  98. package/src/bongos/runner-login.js +151 -0
  99. package/src/bongos/serve-internal.js +6 -1
  100. package/src/module-api.js +1 -1
  101. package/tests/autobongos_runner_login.mjs +296 -0
  102. package/tests/autobongos_runner_login_db.mjs +105 -0
  103. package/tests/design_tokens_generated.mjs +56 -0
  104. package/tests/design_tokens_shared.mjs +183 -0
  105. package/tests/government_session_scope.mjs +2 -1
  106. package/tests/hall_reading_world.mjs +5 -1
  107. package/tests/hall_tokens.mjs +22 -3
  108. package/tests/ui_design_kit.mjs +30 -0
  109. package/tests/welcome_boot_browser.mjs +4 -0
@@ -1,6 +1,8 @@
1
1
  {
2
2
  "_": "Watch — the attention board behind requireGovernmentPage. The SEALED state (a viewer below the gate) is a real render, not an error page, so it is a state here. Written with the re-skin (task 1003307) so this page is MEASURED at three widths in both modes rather than looked at once.",
3
3
  "page": "/builders/watch",
4
+ "_storage": "The hall asks a first-visit question (the session mode picker, craft-mode.js) over every page; seeding \"already asked\" renders the page a returning builder sees (task 1005080).",
5
+ "storage": { "session": { "hall-craft-asked": "1" } },
4
6
  "surface": "hall-ui",
5
7
  "stub": {
6
8
  "prefix": "/builders/"
@@ -166,14 +166,18 @@
166
166
 
167
167
  var root = null, timers = [], returnFocus = null, shown = false, leaving = false;
168
168
 
169
+ function bannerTag(builder) { return builder ? 'div' : 'header'; }
169
170
  function overlayHtml(builder, now, when) {
170
171
  return '<div class="wb__art" aria-hidden="true"><div class="wb__pan">' +
171
172
  '<img class="wb__img" alt="" src="' + artFor(when) + '" decoding="async">' +
172
173
  '<div class="wb__fx" id="wb-fx"></div>' +
173
174
  '</div></div>' +
174
175
  '<div class="wb__cloud wb__cloud--a" aria-hidden="true"></div><div class="wb__cloud wb__cloud--b" aria-hidden="true"></div>' +
175
- '<div class="wb__banner">' + logoHtml(window.__BRANDING__) +
176
- '<span class="wb__version" id="wb-version" hidden></span></div>' +
176
+ // For a visitor the welcome IS the page (the shell draws no header for them), so its logo
177
+ // banner is the page's one <header>; over a signed-in hall the shell's header stands, and the
178
+ // banner stays a plain block (task 1005080).
179
+ '<' + bannerTag(builder) + ' class="wb__banner">' + logoHtml(window.__BRANDING__) +
180
+ '<span class="wb__version" id="wb-version" hidden></span></' + bannerTag(builder) + '>' +
177
181
  '<div class="wb__veil" aria-hidden="true"></div>' +
178
182
  '<button class="wb__skip" type="button" data-wb-skip>Skip</button>' +
179
183
  '<a class="wb__built" href="' + BUILT_ON.href + '" target="_blank" rel="noopener">Built on <b>' + BUILT_ON.label + '</b></a>' +
@@ -1,6 +1,8 @@
1
1
  {
2
2
  "_": "Project founding mode (task 1004422) on an ordinary hall page: the founding band above the topbar and today's nav items asleep, rendered from a /me that says founding (kit fixture me-founding.json, the real server shaper's output at stage strategize). The page itself is work.states.json's; only the identity differs. The no-founding case is every other hall states file.",
3
3
  "page": "/builders/work",
4
+ "_storage": "The hall asks a first-visit question (the session mode picker, craft-mode.js) over every page; seeding \"already asked\" renders the page a returning builder sees (task 1005080).",
5
+ "storage": { "session": { "hall-craft-asked": "1" } },
4
6
  "surface": "hall-ui",
5
7
  "stub": {
6
8
  "prefix": "/builders/",
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "_": "The hall's Work Board through modules/ui-design/kit/render.js (task 1003304). Written with the re-skin so the board's four tabs and its selection state are measured at 1440/390/320 in both modes, not just looked at. The populated look is the hall-preview harness (scripts/hall-preview/server.js); this file is the FLOORS.",
3
3
  "page": "/builders/work",
4
+ "_storage": "The hall asks a first-visit question (the session mode picker, craft-mode.js) over every page; seeding \"already asked\" renders the page a returning builder sees (task 1005080).",
5
+ "storage": { "session": { "hall-craft-asked": "1" } },
4
6
  "surface": "hall-ui",
5
7
  "stub": { "prefix": "/builders/" },
6
8
  "modeQuery": false,
7
9
  "_feeds": "The kit's stub carries a small fixture set, not the hall's task feeds, so the board renders its EMPTY and SKELETON states here — which is the point: an empty queue is a state the floors must hold in. The feed 404s are declared, never hidden.",
8
- "ignoreRequests": ["/api/(gds|bongos)/me$", "/api/(gds|bongos)/me/", "/api/(gds|bongos)/tasks", "/api/(gds|bongos)/claims", "/api/(gds|bongos)/goals", "/api/(gds|bongos)/inbox/", "/api/(gds|bongos)/versions"],
10
+ "ignoreRequests": ["/api/(gds|bongos)/live", "/api/(gds|bongos)/me$", "/api/(gds|bongos)/me/", "/api/(gds|bongos)/tasks", "/api/(gds|bongos)/claims", "/api/(gds|bongos)/goals", "/api/(gds|bongos)/inbox/", "/api/(gds|bongos)/versions"],
9
11
  "ignoreConsole": ["/api/(gds|bongos)/", "\\b401\\b", "\\b404\\b", "Unauthorized", "Not Found", "load failed"],
10
12
  "states": {
11
13
  "ready": { "auth": true, "expect": { "visible": ["#claim-scroll"], "hidden": ["#wip-scroll", "#backlog-scroll", "#history-scroll"] } },
@@ -108,7 +108,7 @@ function isRebaseGateExempt(task) {
108
108
  return Boolean(task) && task.kind === 'infra' && Number(task.priority) === 1;
109
109
  }
110
110
 
111
- async function claimTask({ taskId, builderId, worktreeName, headSha, creatorSessionId, enforceOverlap = false, autoPickup = false }) {
111
+ async function claimTask({ taskId, builderId, worktreeName, headSha, creatorSessionId, enforceOverlap = false, autoPickup = false, autonomous = false }) {
112
112
  const heldPermissions = await claimPermissionsFor(builderId); // BV1.R106 — before the tx opens
113
113
  // ADR 0371 D5: a marked claim needs the project's switch and cap, read before the
114
114
  // tx like the permissions above; the COUNT runs inside it, under the lock.
@@ -433,9 +433,12 @@ async function claimTask({ taskId, builderId, worktreeName, headSha, creatorSess
433
433
 
434
434
  // The column is named only on a marked claim, so an ordinary claim's insert is
435
435
  // byte-identical to before core_275.
436
+ // `autonomous` (core_295, task 1004547) is the server's mark that an Autobongos
437
+ // runner's own login made this claim — set by the claims route from the SESSION,
438
+ // never from the body. Like auto_pickup, the column is named only when true.
436
439
  const insertClaim = await client.query(
437
- `INSERT INTO claims (task_id, builder_id, worktree_name, requested_worktree_name, head_sha, reserved_migration_number, creator_session_id${autoPickup ? ', auto_pickup' : ''})
438
- VALUES ($1, $2, $3, $4, $5, $6, $7${autoPickup ? ', TRUE' : ''})
440
+ `INSERT INTO claims (task_id, builder_id, worktree_name, requested_worktree_name, head_sha, reserved_migration_number, creator_session_id${autoPickup ? ', auto_pickup' : ''}${autonomous ? ', autonomous' : ''})
441
+ VALUES ($1, $2, $3, $4, $5, $6, $7${autoPickup ? ', TRUE' : ''}${autonomous ? ', TRUE' : ''})
439
442
  RETURNING id, task_id, builder_id, worktree_name, requested_worktree_name, claimed_at, head_sha, reserved_migration_number`,
440
443
  [taskId, builderId, finalWorktreeName, requestedWorktreeName, headSha ?? null, reservedMigrationNumber, creatorSessionId ?? null]
441
444
  );
@@ -305,6 +305,10 @@ module.exports = function buildClaimsRouter() {
305
305
  creatorSessionId: resolveCreatorSessionId(bindSession, req.gdsSession?.session_id),
306
306
  enforceOverlap: enforceTouches === true,
307
307
  autoPickup: autoPickup === true,
308
+ // task 1004547: an Autobongos runner's own login marks its claim
309
+ // autonomous. Read from the SESSION (auth.requireBuilder), never the body,
310
+ // so a worker cannot claim its way out of the runner-only walls.
311
+ autonomous: !!(req.builder && req.builder.runner),
308
312
  });
309
313
  const { just_unlocked: justUnlocked, overlap_warning: overlapWarning, sibling_overlap_warning: siblingOverlapWarning, page_notice: pageNotice, ...claim } = claimResult;
310
314
  const task = await db.getTask(Number(taskId));
@@ -476,6 +480,12 @@ module.exports = function buildClaimsRouter() {
476
480
  if (taskIds.length === 0) {
477
481
  return res.fail('task_ids_required', 400);
478
482
  }
483
+ // task 1004547: a batch claim binds only its first row to the session, so it
484
+ // cannot carry the autonomous mark row by row. A runner works one task per
485
+ // worker and never batch-claims, so its own login is refused here outright.
486
+ if (req.builder && req.builder.runner) {
487
+ return res.fail('runner_batch_refused', { status: 403, message: 'An Autobongos runner claims one task at a time. Use POST /claims.' });
488
+ }
479
489
  if (taskIds.length > 20) {
480
490
  // Safety cap. The optimizer doesn't return more than a handful at a time,
481
491
  // and unlimited batches let one slow transaction lock up the table.
@@ -1184,7 +1184,7 @@ module.exports = function buildTasksRouter() {
1184
1184
  `Merging triggers .github/workflows/deploy-prod.yml -> ${branding().domains.publicOrigin}. ` +
1185
1185
  prMergeSentence(held);
1186
1186
  const pr = await githubPush.openOrFindPullRequest({ branch, title, body });
1187
- const prReads = sharedPrReads(); for (const [guard, refusalFor] of [[autoPickupFiles, autoPickupFiles.autoPickupFileRefusal], [runnerSelfGuard, runnerSelfGuard.runnerSelfEditRefusal]]) { const refusal = await refusalFor({ taskId: id, prNumber: pr.number, branch }, { getPullRequestFiles: prReads.getPullRequestFiles }); if (refusal) { await githubPrClose.closePullRequest({ number: pr.number }).catch(() => null); return res.fail(refusal.code, 409, { ...refusal, message: guard.refusalMessage(refusal) }); } } // the PR's own files: an auto-pickup round stays on its page (ADR 0371 D8, task 1004678); a runner never changes its own files (task 1004546)
1187
+ const prReads = sharedPrReads(); for (const [guard, refusalFor] of [[autoPickupFiles, autoPickupFiles.autoPickupFileRefusal], [runnerSelfGuard, runnerSelfGuard.runnerSelfEditRefusal]]) { const refusal = await refusalFor({ taskId: id, prNumber: pr.number, branch, publisherIsRunner: !!(req.builder && req.builder.runner) }, { getPullRequestFiles: prReads.getPullRequestFiles }); if (refusal) { await githubPrClose.closePullRequest({ number: pr.number }).catch(() => null); return res.fail(refusal.code, 409, { ...refusal, message: guard.refusalMessage(refusal) }); } } // the PR's own files: an auto-pickup round stays on its page (ADR 0371 D8, task 1004678); a runner never changes its own files (task 1004546), whoever's claim it is when a runner login publishes (task 1004547)
1188
1188
  // Best-effort by contract (pr-assign.js never rejects); the catch is for a future
1189
1189
  // edit that forgets — an assignment must not be able to fail a pushed ship.
1190
1190
  const assignment = await prAssign.assignPullRequest({ number: pr.number, login: builderLogin })
@@ -26,11 +26,18 @@
26
26
  // paths the registry has always protected, and one that does not is treated as
27
27
  // unreadable, which refuses.
28
28
  //
29
- // WHICH CLAIM IS A RUNNER'S. Its worktree name (runner-self.js RUNNER_TREE_RE),
30
- // read from the claim row. If that read FAILS, the branch name stands in (the
31
- // runner's branch is its worktree name), so a database hiccup does not switch the
32
- // wall off for a runner while an ordinary publish still never depends on the read.
33
- // Telling a runner's session from its owner's on the server is task 1004547.
29
+ // WHICH PUBLISH IS A RUNNER'S (task 1004547). Any one of three says so:
30
+ // - the claim row's `autonomous` mark, which the claims route sets from the
31
+ // claiming SESSION (a runner's own login, src/bongos/runner-login.js) and which
32
+ // no request body can set or clear. This is the reliable one: a worker that
33
+ // claims under a hand-picked worktree name is still recognised;
34
+ // - the publishing session is itself a runner's (`publisherIsRunner`), whoever
35
+ // made the claim;
36
+ // - the claim's worktree name (runner-self.js RUNNER_TREE_RE), kept for runners
37
+ // still running under their builder's CLI session until they are re-set-up.
38
+ // If the claim read FAILS, the branch name stands in (the runner's branch is its
39
+ // worktree name), so a database hiccup does not switch the wall off for a runner
40
+ // while an ordinary publish still never depends on the read.
34
41
  //
35
42
  // FAILS CLOSED on the files: a read that throws, a list at GitHub's 300-file cap,
36
43
  // or a matcher that fails its check all refuse. An unattended change that cannot
@@ -49,7 +56,7 @@ const runnerSelf = require('./runner-self');
49
56
  // resolved it. Both names are read because the server may store a disambiguated
50
57
  // worktree_name beside the one the runner asked for.
51
58
  const CLAIM_SQL = `
52
- SELECT c.worktree_name, c.requested_worktree_name
59
+ SELECT c.worktree_name, c.requested_worktree_name, c.autonomous
53
60
  FROM claims c WHERE c.task_id = $1
54
61
  ORDER BY c.claimed_at DESC, c.id DESC LIMIT 1`;
55
62
 
@@ -66,9 +73,11 @@ async function readLatestClaim(taskId, exec = api.pool) {
66
73
  }
67
74
  }
68
75
 
69
- function isRunnerPublish(claim, branch) {
76
+ function isRunnerPublish(claim, branch, publisherIsRunner = false) {
77
+ if (publisherIsRunner === true) return true;
70
78
  if (claim === CLAIM_UNREADABLE) return runnerSelf.isRunnerTree(branch);
71
- return !!claim && (runnerSelf.isRunnerTree(claim.worktree_name) || runnerSelf.isRunnerTree(claim.requested_worktree_name));
79
+ return !!claim && (claim.autonomous === true
80
+ || runnerSelf.isRunnerTree(claim.worktree_name) || runnerSelf.isRunnerTree(claim.requested_worktree_name));
72
81
  }
73
82
 
74
83
  // Paths the registry has protected since ADR 0043; a matcher that misses either
@@ -86,9 +95,9 @@ const unreadable = () => ({ code: 'self_modification_unreadable', files: [], pro
86
95
  // The route's one call. Returns null (publish as usual) or the refusal
87
96
  // { code, files, protected }. `deps` is the test seam, and the route passes a
88
97
  // shared getPullRequestFiles so this and the auto-pickup check read GitHub once.
89
- async function runnerSelfEditRefusal({ taskId, prNumber, branch = null }, deps = {}) {
98
+ async function runnerSelfEditRefusal({ taskId, prNumber, branch = null, publisherIsRunner = false }, deps = {}) {
90
99
  const claim = await (deps.readLatestClaim || readLatestClaim)(taskId);
91
- if (!isRunnerPublish(claim, branch)) return null;
100
+ if (!isRunnerPublish(claim, branch, publisherIsRunner)) return null;
92
101
  const match = verifiedMatcher(deps.matchProtected || ((files) => api.permissionPathCheck.matchProtected(files)));
93
102
  if (!match) return unreadable();
94
103
  let rows;
@@ -96,7 +96,7 @@ function clientSecretValid(client, presentedSecret) {
96
96
  // FEDERATION_SOURCES mirrors modules/government/session-scopes.json — a module may
97
97
  // not read another module's files, and the doorway exports no lookup for it — and
98
98
  // tests/platform-identity.mjs pins the two lists equal, so a source declared or
99
- // retired there without the same edit here fails the build instead of drifting.
99
+ // retired there without the same edit here fails the build instead of drifting ('runner' is left out: task 1004547).
100
100
  const FEDERATION_SOURCES = Object.freeze(['cli', 'web', null]);
101
101
  function sessionUsableForFederation(session) {
102
102
  if (!session) return { ok: false, reason: 'no_session' };
@@ -130,12 +130,23 @@ t('a session of an undeclared source (a leftover dev-box token) may NOT federate
130
130
  }
131
131
  });
132
132
 
133
- t('FEDERATION_SOURCES is exactly the kernel session-source registry', () => {
133
+ // Sources the registry declares that must NOT federate. An Autobongos runner's own
134
+ // login (task 1004547) is a machine working for its builder on ONE project; it has no
135
+ // business signing that person in to other projects through the hub.
136
+ const NEVER_FEDERATES = ['runner'];
137
+
138
+ t('FEDERATION_SOURCES is exactly the kernel session-source registry, less the machine logins', () => {
134
139
  // platform-identity may not read the government module, so it mirrors the list;
135
140
  // this pin is what stops the copy drifting from modules/government/session-scopes.json.
136
141
  const reg = JSON.parse(readFileSync(new URL('../../government/session-scopes.json', import.meta.url), 'utf8'));
137
142
  const declared = reg.scopes.map((r) => (r.source === undefined ? null : r.source));
138
- assert.deepEqual([...P.FEDERATION_SOURCES].sort(), declared.sort());
143
+ for (const s of NEVER_FEDERATES) assert.ok(declared.includes(s), `${s} is still declared, so the exclusion is still needed`);
144
+ assert.deepEqual([...P.FEDERATION_SOURCES].sort(), declared.filter((s) => !NEVER_FEDERATES.includes(s)).sort());
145
+ });
146
+
147
+ t('an Autobongos runner login may NOT federate', () => {
148
+ const r = P.sessionUsableForFederation({ github_id: 1, status: 'active', source: 'runner' });
149
+ assert.deepEqual(r, { ok: false, reason: 'session_scope' });
139
150
  });
140
151
 
141
152
  t('a service principal (system_role set) may NOT federate', () => {
@@ -54,6 +54,14 @@ function ownsUpgradableProject(inst, builder) {
54
54
  function requireOwnProjectOrPin({ pinGate, provisioning, pool, log }) {
55
55
  return async function ownProjectOrPinGate(req, res, next) {
56
56
  const raw = req.params ? req.params.id : undefined;
57
+ // An Autobongos runner's own login never takes the owner door (task 1004547):
58
+ // an unattended machine does not move a live version, even its owner's. It falls
59
+ // to the permission gate, which refuses it, because a runner session holds no
60
+ // system permission (src/bongos/auth.js resolveHeldPermissions).
61
+ if (req.builder && req.builder.runner) {
62
+ req.coreUpgradeAuthority = 'pin';
63
+ return pinGate(req, res, next);
64
+ }
57
65
  if (typeof raw === 'string' && ID_RE.test(raw)) {
58
66
  try {
59
67
  const inst = await provisioning.getInstanceById(pool, Number(raw));
@@ -19,6 +19,7 @@
19
19
  <link rel="preconnect" href="https://fonts.googleapis.com">
20
20
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
21
21
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400&family=Manrope:wght@300;400;500;600&display=swap">
22
+ <link rel="stylesheet" href="/modules/ui-design/public/tokens.css">
22
23
  <link rel="stylesheet" href="/assets/cosmos.css">
23
24
  <!-- Dark only, like every apex page (ADR 0204): data-mode="dark" is fixed on <html>. -->
24
25
  <style>
@@ -36,6 +36,7 @@
36
36
  <link rel="preconnect" href="https://fonts.googleapis.com">
37
37
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
38
38
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400&family=Manrope:wght@300;400;500;600&display=swap">
39
+ <link rel="stylesheet" href="/modules/ui-design/public/tokens.css">
39
40
  <link rel="stylesheet" href="/assets/cosmos.css">
40
41
  <!-- Dark only, like every apex page (ADR 0204): data-mode="dark" is fixed on <html>. -->
41
42
  <style>
@@ -22,6 +22,7 @@
22
22
  <link rel="preconnect" href="https://fonts.googleapis.com">
23
23
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
24
24
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400&family=Manrope:wght@300;400;500;600&display=swap">
25
+ <link rel="stylesheet" href="/modules/ui-design/public/tokens.css">
25
26
  <link rel="stylesheet" href="/assets/cosmos.css">
26
27
  <!-- Dark only, like every apex page (ADR 0204): data-mode="dark" is fixed on <html>. -->
27
28
  <style>
@@ -16,6 +16,7 @@
16
16
  <link rel="preconnect" href="https://fonts.googleapis.com">
17
17
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
18
18
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400&family=Manrope:wght@300;400;500;600&display=swap">
19
+ <link rel="stylesheet" href="/modules/ui-design/public/tokens.css">
19
20
  <link rel="stylesheet" href="/assets/cosmos.css">
20
21
  <!-- Dark only, like every apex page (task 1003336, ADR 0204): data-mode="dark" is fixed on <html>. -->
21
22
  <style>
@@ -29,6 +29,7 @@
29
29
  <link rel="preconnect" href="https://fonts.googleapis.com">
30
30
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
31
31
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400&family=Manrope:wght@300;400;500;600&display=swap">
32
+ <link rel="stylesheet" href="/modules/ui-design/public/tokens.css">
32
33
  <link rel="stylesheet" href="/assets/cosmos.css">
33
34
  <!-- Dark only, like every apex page (ADR 0204): data-mode="dark" is fixed on <html>. -->
34
35
  <style>
@@ -41,6 +41,7 @@
41
41
  <!-- The world's token layer. Root-absolute so the server's ?v= content stamp
42
42
  applies (a relative href is never stamped, and a four-hour browser cache
43
43
  then styles new markup with an old sheet). -->
44
+ <link rel="stylesheet" href="/modules/ui-design/public/tokens.css">
44
45
  <link rel="stylesheet" href="/assets/world.css">
45
46
  <style>
46
47
  /* ── PAGE-LOCAL SHEET ────────────────────────────────────────────────────
@@ -23,6 +23,7 @@
23
23
  <link rel="preconnect" href="https://fonts.googleapis.com">
24
24
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
25
25
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400&family=Manrope:wght@300;400;500;600&display=swap">
26
+ <link rel="stylesheet" href="/modules/ui-design/public/tokens.css">
26
27
  <link rel="stylesheet" href="/assets/cosmos.css">
27
28
  <!-- Dark only, like every apex page (ADR 0204): data-mode="dark" is fixed on <html>. -->
28
29
  <style>
@@ -14,6 +14,7 @@
14
14
  <link rel="preconnect" href="https://fonts.googleapis.com">
15
15
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
16
16
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400&family=Manrope:wght@300;400;500;600&display=swap">
17
+ <link rel="stylesheet" href="/modules/ui-design/public/tokens.css">
17
18
  <link rel="stylesheet" href="/assets/cosmos.css">
18
19
  <!-- Dark only, like every apex page (task 1003336, ADR 0204): data-mode="dark" is fixed on <html>. -->
19
20
  <style>
@@ -19,6 +19,7 @@
19
19
  space sky both modes ride on, light/dark governing the cards. The server
20
20
  re-injects the branding :root AFTER stylesheets, so the pack stays
21
21
  authoritative. Design contract: docs/design/projects-surfaces-redesign-direction.md -->
22
+ <link rel="stylesheet" href="/modules/ui-design/public/tokens.css">
22
23
  <link rel="stylesheet" href="/assets/cosmos.css">
23
24
  <!-- The apex is dark only (task 1003336, ADR 0204): data-mode="dark" is fixed on
24
25
  <html> above — no pre-paint script, no stored choice, no OS preference. Light
@@ -14,6 +14,7 @@
14
14
  <link rel="preconnect" href="https://fonts.googleapis.com">
15
15
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
16
16
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400&family=Manrope:wght@300;400;500;600&display=swap">
17
+ <link rel="stylesheet" href="/modules/ui-design/public/tokens.css">
17
18
  <link rel="stylesheet" href="/assets/cosmos.css">
18
19
  <!-- Dark only, like every apex page (task 1003336, ADR 0204): data-mode="dark" is fixed on <html>. -->
19
20
  <style>
@@ -10,6 +10,7 @@
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
11
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400&family=Manrope:wght@300;400;500;600&display=swap">
12
12
  <script src="{{base}}/assets/mode.js"></script>
13
+ <link rel="stylesheet" href="/modules/ui-design/public/tokens.css">
13
14
  <link rel="stylesheet" href="{{base}}/assets/raise.css">
14
15
  </head>
15
16
  <body class="gate-body">
@@ -10,6 +10,7 @@
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
11
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400&family=Manrope:wght@300;400;500;600&display=swap">
12
12
  <script src="{{base}}/assets/mode.js"></script>
13
+ <link rel="stylesheet" href="/modules/ui-design/public/tokens.css">
13
14
  <link rel="stylesheet" href="{{base}}/assets/raise.css">
14
15
  </head>
15
16
  <body>
@@ -10,6 +10,7 @@
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
11
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400&family=Manrope:wght@300;400;500;600&display=swap">
12
12
  <script src="{{base}}/assets/mode.js"></script>
13
+ <link rel="stylesheet" href="/modules/ui-design/public/tokens.css">
13
14
  <link rel="stylesheet" href="{{base}}/assets/raise.css?v={{assetVersion}}">
14
15
  <script src="{{base}}/assets/raise.js?v={{assetVersion}}" defer></script>
15
16
  {{editorAssets}}
@@ -9,6 +9,7 @@
9
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
11
  <link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;700&family=EB+Garamond:ital,wght@0,400;0,500;0,600;1,400;1,500&display=swap" rel="stylesheet">
12
+ <link rel="stylesheet" href="/modules/ui-design/public/tokens.css">
12
13
  <link rel="stylesheet" href="/style.css?v=2026-06-12-wip3">
13
14
  </head>
14
15
  <body>
@@ -14,7 +14,7 @@ A **look** is a branding pack from the module's style library (`modules/ui-desig
14
14
 
15
15
  Two of the floors are about fit ([task 1004203](https://cloudbongos.com/builders#/task/1004203), [ADR 0329](../../../docs/adr/0329-the-rendered-fit-check-is-a-kit-floor-and-an-advisory-ship-check-row.md)): `text-fit` (text past its box, SVG text past its card) and `sibling-overlap` (text-bearing siblings on top of each other). `scripts/bongos/render-check.js` runs just those two over the pages a branch changed, and `/ship-check` shows them as an advisory row.
16
16
 
17
- A surface describes itself in two JSON files beside it — `<page>.states.json` and `<page>.probes.json` — in the vocabularies `lib.js` defines and validates. The full loop, the file shapes and the failure table: [`docs/recipes/ui-look-before-you-ship.md`](../../../docs/recipes/ui-look-before-you-ship.md). Exit codes everywhere: 0 clean · 1 problems · 2 a broken file or usage · 3 no browser.
17
+ A surface describes itself in two JSON files beside it — `<page>.states.json` and `<page>.probes.json` — in the vocabularies `lib.js` defines and validates. A page served at the surface root (the hall's Home, `/builders/`) reads `index.states.json`. A file (or one state) may carry `"storage": { "session": {…}, "local": {…} }`, written into the browser before the page's own scripts run, so a first-visit question renders as a returning visitor sees it: every hall file seeds `hall-craft-asked` so the mode picker stays out of the shots ([task 1005080](https://cloudbongos.com/builders#/task/1005080)). A hall state that needs real data (a goal's detail, the Settings sections) passes against the hall-preview harness with `--base-in`/`--base-out`, one render at a time. The full loop, the file shapes and the failure table: [`docs/recipes/ui-look-before-you-ship.md`](../../../docs/recipes/ui-look-before-you-ship.md). Exit codes everywhere: 0 clean · 1 problems · 2 a broken file or usage · 3 no browser.
18
18
 
19
19
  ## The anti-pattern detector (`tells.js`, and the rendered twins in `lib.js`)
20
20
 
@@ -0,0 +1 @@
1
+ {"versions":[{"id":"BONGOS-V1","name":"Cloud Bongos — platform (V1)","status":"shipped","scope_doc_path":null,"started_at":"2026-06-01T00:00:00.000Z","shipped_at":"2026-09-01T00:00:00.000Z","done_when":"The platform builds itself."},{"id":"BONGOS-V2","name":"The ten working areas","status":"building","scope_doc_path":null,"started_at":"2026-09-01T00:00:00.000Z","shipped_at":null,"done_when":"Every working area has a goal."}],"page":{"limit":50,"offset":0,"total":2}}
@@ -79,7 +79,16 @@ function resolveSurfaceDir(surface, root = REPO_ROOT) {
79
79
  return null;
80
80
  }
81
81
  // The states/probes file for a page lives BESIDE the surface: <dir>/<page>.states.json.
82
- function sidecarPath(surfaceDir, urlPath, kind) { return path.join(surfaceDir, `${pageName(urlPath)}.${kind}.json`); }
82
+ function sidecarPath(surfaceDir, urlPath, kind) {
83
+ const named = path.join(surfaceDir, `${pageName(urlPath)}.${kind}.json`);
84
+ // A page served at the surface ROOT ("/builders/") has no last segment of its own — the
85
+ // segment is the surface's prefix — so its sidecar is index.<kind>.json (task 1005080).
86
+ if (/\/$/.test(String(urlPath || '').split('?')[0]) && !fs.existsSync(named)) {
87
+ const index = path.join(surfaceDir, `index.${kind}.json`);
88
+ if (fs.existsSync(index)) return index;
89
+ }
90
+ return named;
91
+ }
83
92
  function readJson(file) { return JSON.parse(fs.readFileSync(file, 'utf8')); }
84
93
 
85
94
  // ── the action vocabulary (states + probes) ───────────────────────────────────
@@ -332,6 +341,37 @@ function reducedMotionProblems({ running = [], frames = 0 } = {}) {
332
341
  // floor and not proof, and a reviewer verifies each tell in context (task 1003329, ADR 0223).
333
342
  function craftTells(facts) { return (facts && facts.tells ? facts.tells : []).map((t) => `tell: ${t}`); }
334
343
 
344
+ // ── storage seeded before a page's own scripts run (task 1005080) ─────────────
345
+ // { "storage"?: { "session"?: { key: "value" }, "local"?: { key: "value" } } } on a states or
346
+ // probes file — and the same object on one state — is written into the browser's storage by
347
+ // an init script, so a page that asks a one-time question on first visit (the hall's mode
348
+ // picker) renders as a returning visitor sees it. A state that MEANS the first visit leaves
349
+ // it out. A state's own storage is merged over the file's.
350
+ function validateStorage(st, at) {
351
+ if (st === undefined) return [];
352
+ if (!st || typeof st !== 'object' || Array.isArray(st)) return [`${at}: an object { session?, local? }`];
353
+ const errors = [];
354
+ for (const k of Object.keys(st)) {
355
+ if (k !== 'session' && k !== 'local') { errors.push(`${at}.${k}: unknown store (session | local)`); continue; }
356
+ const m = st[k];
357
+ if (!m || typeof m !== 'object' || Array.isArray(m) || !Object.values(m).every((v) => typeof v === 'string')) errors.push(`${at}.${k}: an object of string → string`);
358
+ }
359
+ return errors;
360
+ }
361
+ function mergeStorage(...layers) {
362
+ const out = { session: {}, local: {} };
363
+ for (const s of layers) if (s) for (const k of ['session', 'local']) Object.assign(out[k], s[k] || {});
364
+ return out;
365
+ }
366
+ async function seedStorage(ctx, ...layers) {
367
+ const st = mergeStorage(...layers);
368
+ if (!Object.keys(st.session).length && !Object.keys(st.local).length) return;
369
+ await ctx.addInitScript((s) => {
370
+ try { for (const [k, v] of Object.entries(s.session)) window.sessionStorage.setItem(k, v); } catch (_) { /* storage refused */ }
371
+ try { for (const [k, v] of Object.entries(s.local)) window.localStorage.setItem(k, v); } catch (_) { /* storage refused */ }
372
+ }, st);
373
+ }
374
+
335
375
  // ── the states file ───────────────────────────────────────────────────────────
336
376
  // { "page": "/projects", "surface"?: "public-landing" (a module key), "stub"?: { prefix?, child? },
337
377
  // "modes"?: ["dark" | "light", …] (the modes the page renders; default both — a page that pins
@@ -339,7 +379,8 @@ function craftTells(facts) { return (facts && facts.tells ? facts.tells : []).ma
339
379
  // stub.fixtures?: { "<fixture name>": "<other fixture file>" } (serve another file under a name — an empty list, a
340
380
  // failure shape — for the states in THIS file; task 1003345),
341
381
  // "ignoreControls"?: [selectors], "states": { name: { url?, auth?, actions?, expect? } },
342
- // "reducedMotion"?: { auth?, url?, actions? } }
382
+ // "reducedMotion"?: { auth?, url?, actions? },
383
+ // "storage"?: { session?, local? } (seeded before load; a state may carry its own — see seedStorage) }
343
384
  function validateStatesFile(json) {
344
385
  if (!json || typeof json !== 'object' || Array.isArray(json)) return { ok: false, errors: ['a states file is a JSON object'] };
345
386
  const errors = [];
@@ -349,6 +390,7 @@ function validateStatesFile(json) {
349
390
  if (json.modeQuery !== undefined && json.modeQuery !== false && typeof json.modeQuery !== 'string') errors.push('modeQuery: the query param the page reads for its colour mode (default "mode"), or false');
350
391
  if (json.modes !== undefined && !(Array.isArray(json.modes) && json.modes.length && json.modes.every((m) => MODES.includes(m)))) errors.push('modes: a non-empty array of "dark" | "light" — the modes the page renders (default both); a page that pins one mode declares it here');
351
392
  for (const k of ['ignoreRequests', 'ignoreConsole', 'ignoreControls']) if (json[k] !== undefined && !(Array.isArray(json[k]) && json[k].every((s) => typeof s === 'string'))) errors.push(`${k}: an array of strings`);
393
+ errors.push(...validateStorage(json.storage, 'storage'));
352
394
  const states = json.states;
353
395
  if (!states || typeof states !== 'object' || Array.isArray(states) || !Object.keys(states).length) errors.push('states: required — a non-empty object of name → { url?, auth?, actions?, expect? }');
354
396
  else {
@@ -359,6 +401,7 @@ function validateStatesFile(json) {
359
401
  if (s.auth !== undefined && typeof s.auth !== 'boolean') errors.push(`states.${name}.auth: true (signed-in stub) or false`);
360
402
  errors.push(...validateActions(s.actions, `states.${name}.actions`));
361
403
  errors.push(...validateExpect(s.expect, `states.${name}.expect`));
404
+ errors.push(...validateStorage(s.storage, `states.${name}.storage`));
362
405
  }
363
406
  }
364
407
  if (json.reducedMotion !== undefined) {
@@ -386,6 +429,7 @@ function validateProbesFile(json) {
386
429
  const errors = [];
387
430
  if (typeof json.page !== 'string' || !json.page.startsWith('/')) errors.push('page: required — the root-relative path the probes open');
388
431
  errors.push(...validateStub(json.stub, 'stub'));
432
+ errors.push(...validateStorage(json.storage, 'storage'));
389
433
  if (!Array.isArray(json.probes) || !json.probes.length) errors.push('probes: required — a non-empty array');
390
434
  else json.probes.forEach((pr, i) => {
391
435
  const at = `probes[${i}]`;
@@ -396,6 +440,7 @@ function validateProbesFile(json) {
396
440
  if (pr.url !== undefined && (typeof pr.url !== 'string' || !pr.url.startsWith('/'))) errors.push(`${at}.url: a root-relative path`);
397
441
  if (pr.continue !== undefined && typeof pr.continue !== 'boolean') errors.push(`${at}.continue: boolean (reuse the previous probe's page)`);
398
442
  if (pr.continue && i === 0) errors.push(`${at}.continue: the first probe has nothing to continue`);
443
+ errors.push(...validateStorage(pr.storage, `${at}.storage`));
399
444
  errors.push(...validateActions(pr.steps, `${at}.steps`));
400
445
  if (!pr.expect || typeof pr.expect !== 'object' || !Object.keys(pr.expect).length) errors.push(`${at}.expect: required — a probe with nothing to expect proves nothing`);
401
446
  else errors.push(...validateExpect(pr.expect, `${at}.expect`));
@@ -527,7 +572,7 @@ module.exports = {
527
572
  REPO_ROOT, KIT_DIR, STYLES_DIR, EXIT, FLOORS, SIZES, DEFAULT_SIZES, MODES, CHANNELS, SETTLE_MS, ACTIONS,
528
573
  argOf, hasFlag, listArg, pageName, normalizePageArg, resolveSurfaceDir, sidecarPath, readJson,
529
574
  runActions, factsInPage, expectProblems, auditFloors, reducedMotionProblems, craftTells,
530
- validateStatesFile, validateProbesFile, NoBrowserError, launchBrowser, attachCollectors, withMode,
575
+ validateStatesFile, validateProbesFile, mergeStorage, seedStorage, NoBrowserError, launchBrowser, attachCollectors, withMode,
531
576
  freePort, startStub, parseColor, composite, luminance, contrastRatio, instancePack, packColors,
532
577
  listStyles, resolveStyle, styleBrandingFile,
533
578
  };
@@ -72,6 +72,7 @@ async function main() {
72
72
  if (ctx) await ctx.close();
73
73
  const [W, H] = lib.SIZES[pr.width || 1440];
74
74
  ctx = await browser.newContext({ viewport: { width: W, height: H }, deviceScaleFactor: 1 });
75
+ await lib.seedStorage(ctx, json.storage, pr.storage);
75
76
  page = await ctx.newPage();
76
77
  errors = [];
77
78
  page.on('pageerror', (e) => errors.push(String(e.message).split('\n')[0]));
@@ -97,6 +97,7 @@ async function main() {
97
97
  const st = json.states[name];
98
98
  const [W, H] = lib.SIZES[w];
99
99
  const ctx = await browser.newContext({ viewport: { width: W, height: H }, deviceScaleFactor: 1, colorScheme: mode });
100
+ await lib.seedStorage(ctx, json.storage, st.storage);
100
101
  const page = await ctx.newPage();
101
102
  const logs = lib.attachCollectors(page, collectorOpts);
102
103
  const base = st.auth ? baseIn : baseOut;
@@ -121,6 +122,7 @@ async function main() {
121
122
  const rm = json.reducedMotion || {};
122
123
  for (const mode of lib.hasFlag(args, '--no-reduced-motion') ? [] : modes) {
123
124
  const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, colorScheme: mode, reducedMotion: 'reduce' });
125
+ await lib.seedStorage(ctx, json.storage, rm.storage);
124
126
  const page = await ctx.newPage();
125
127
  await page.goto((rm.auth === false ? baseOut : baseIn) + lib.withMode(rm.url || json.page, mode, json.modeQuery), { waitUntil: 'load' });
126
128
  await page.waitForTimeout(lib.SETTLE_MS);
@@ -106,6 +106,7 @@ const FEEDS = {
106
106
  '/public/cost-summary': 'cost-summary',
107
107
  '/public/recent-shipped': 'recent-shipped',
108
108
  '/public/leaderboard': 'leaderboard',
109
+ '/public/versions': 'versions', // the roadmap and goals pages' version dates (task 1005080)
109
110
  '/projects/featured': 'featured',
110
111
  };
111
112
  // /founding/builders (task 1004424) answers a never-founded project's empty list by default,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "key": "ui-design",
3
3
  "title": "UI design (the design contract, its adapters and sync, the /design playbook, the kit, the styles)",
4
- "description": "The platform capability that lets any builder on any instance create and design good-looking, functional UIs in line with that instance's world (scoped with the owner 2026-08-28; task 1003324, ADR 0197). It ships METHOD and never a world: the neutral design contract of ADR 0081 with its tool adapters (Claude Design, Figma), their sync CLIs and the validator; the /design playbook for ui-discipline work; the look-before-you-ship kit (kit/) and the style library (styles/). The thirteen design-style skills that lived in skills/ are the opt-in design-styles module since task 1004470; /design reaches them when that module is on. The instance's world — its branding pack, config/design-tokens.*, config/design-sources.json and its DESIGN.md — stays in config/ and at the repo root as the host boundary this module reads and never owns (ADR 0062 §7). Cloud Bongos's own front door is this module's first customer, not its owner. default-on because every instance has a face. No routes, no migrations, no port: declaration-only like character-anim, so disabling it withdraws only the skills it contributes and touches no served surface.",
4
+ "description": "The platform capability that lets any builder on any instance create and design good-looking, functional UIs in line with that instance's world (scoped with the owner 2026-08-28; task 1003324, ADR 0197). It ships METHOD and never a world: the neutral design contract of ADR 0081 with its tool adapters (Claude Design, Figma), their sync CLIs and the validator; the /design playbook for ui-discipline work; the look-before-you-ship kit (kit/) and the style library (styles/). The thirteen design-style skills that lived in skills/ are the opt-in design-styles module since task 1004470; /design reaches them when that module is on. The instance's world — its branding pack, config/design-tokens.*, config/design-sources.json and its DESIGN.md — stays in config/ and at the repo root as the host boundary this module reads and never owns (ADR 0062 §7). Cloud Bongos's own front door is this module's first customer, not its owner. default-on because every instance has a face. No routes, no migrations, no port. Since task 1005081 it also ships public/tokens.css, the shared token sheet (the scales: type, spacing, corners, shadows, motion, stacking) every surface loads at /modules/ui-design/public/tokens.css, so disabling it now also withdraws that sheet and the hall's scale aliases lose their values.",
5
5
  "version": "1.0.0",
6
6
  "coreVersion": "^1.19.0",
7
7
  "category": { "parent": "pages" },