@bongos/core 1.18.19 → 1.19.2

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 (251) hide show
  1. package/.bongos-core.json +514 -194
  2. package/.claude/skills/builder-box/SKILL.md +1 -1
  3. package/.claude/skills/builder-claim/SKILL.md +1 -1
  4. package/.claude/skills/goal-create/SKILL.md +1 -1
  5. package/.claude/skills/idea-triage/SKILL.md +1 -1
  6. package/.claude/skills/planning-session/SKILL.md +5 -5
  7. package/.claude/skills/scan-before-install/SKILL.md +165 -0
  8. package/clients/bongos-client/README.md +1 -1
  9. package/clients/bongos-client/bongos-client.global.js +63 -41
  10. package/clients/bongos-client/index.cjs +63 -41
  11. package/clients/bongos-client/index.d.ts +78 -44
  12. package/clients/bongos-client/index.mjs +63 -41
  13. package/docs/adr/0020-security-model-public-stub.md +1 -1
  14. package/docs/adr/0097-one-active-claim-per-session-and-worktree-binding.md +2 -0
  15. package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +1 -1
  16. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +2 -1
  17. package/docs/adr/0155-adopt-private-repo-widen-oauth-scope.md +51 -0
  18. package/docs/adr/0156-user-set-display-name-wins.md +62 -0
  19. package/docs/adr/0157-archon-is-rank-and-identity-only.md +93 -0
  20. package/docs/adr/0158-choke-point-verdict-taxonomy.md +174 -0
  21. package/docs/adr/0159-merge-gate-reconciliation.md +88 -0
  22. package/docs/adr/0160-main-audit-repo-verifiable-caller.md +77 -0
  23. package/docs/adr/README.md +8 -0
  24. package/docs/api/openapi.json +1209 -233
  25. package/docs/api-reference.md +61 -46
  26. package/docs/architecture.md +1 -1
  27. package/docs/canonical-permissions.md +61 -16
  28. package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
  29. package/docs/file-map.md +4 -1
  30. package/docs/onboarding/diagrams/assertions.json +3 -3
  31. package/docs/recipes/core-release-pipeline.md +179 -0
  32. package/docs/recipes/packaging-the-core.md +2 -0
  33. package/docs/recipes/parallel-goal-waves.md +1 -3
  34. package/docs/recipes/private-npm-distribution.md +2 -0
  35. package/docs/recipes/upgrading-the-core.md +2 -0
  36. package/migrations/core_197_display_name_source.sql +56 -0
  37. package/migrations/core_204_rekey_active_claim_per_session_worktree.sql +77 -0
  38. package/modules/autonomy/routes/autonomy.js +1 -1
  39. package/modules/builder-settings/db.js +7 -1
  40. package/modules/dev-box/app/src/vendor/bongos-client.cjs +63 -41
  41. package/modules/dev-box/routes/box.js +11 -9
  42. package/modules/discord/auth.js +3 -1
  43. package/modules/discord/routes/discord.js +8 -2
  44. package/modules/discord/ship-broadcast.js +48 -1
  45. package/modules/governance/catalog.js +70 -20
  46. package/modules/governance/db.js +102 -0
  47. package/modules/governance/migrations/governance_006_version_create.sql +38 -0
  48. package/modules/governance/migrations/governance_007_delegate_archon_ops_to_metic.sql +72 -0
  49. package/modules/governance/routes/governance.js +235 -6
  50. package/modules/grading/grader-workers/index.js +18 -9
  51. package/modules/hall-ui/public/gate.js +6 -2
  52. package/modules/hall-ui/public/goals-page.js +6 -2
  53. package/modules/hall-ui/public/governance.css +100 -0
  54. package/modules/hall-ui/public/governance.html +77 -0
  55. package/modules/hall-ui/public/governance.js +353 -0
  56. package/modules/hall-ui/public/harbor.js +14 -8
  57. package/modules/hall-ui/public/instances-panel.js +3 -1
  58. package/modules/hall-ui/public/new-project-wizard.js +6 -0
  59. package/modules/hall-ui/public/people.html +51 -0
  60. package/modules/hall-ui/public/people.js +100 -0
  61. package/modules/hall-ui/public/profile.html +2 -1
  62. package/modules/hall-ui/public/profile.js +9 -3
  63. package/modules/hall-ui/public/sessions.js +4 -2
  64. package/modules/hall-ui/public/shell.js +28 -7
  65. package/modules/hall-ui/public/task-detail.js +6 -3
  66. package/modules/hall-ui/public/watch.js +17 -9
  67. package/modules/ideas/blockers.js +44 -7
  68. package/modules/ideas/routes/blockers.js +12 -7
  69. package/modules/ideas/routes/inbox.js +2 -2
  70. package/modules/lifecycle/db.js +84 -17
  71. package/modules/lifecycle/github-push.js +37 -10
  72. package/modules/lifecycle/publish-reconciler.js +128 -31
  73. package/modules/lifecycle/routes/claims.js +10 -4
  74. package/modules/lifecycle/routes/dependencies.js +2 -2
  75. package/modules/lifecycle/routes/done-when.js +4 -4
  76. package/modules/lifecycle/routes/gate-approvals.js +9 -3
  77. package/modules/lifecycle/routes/goals.js +5 -5
  78. package/modules/lifecycle/routes/tasks.js +16 -16
  79. package/modules/lifecycle/routes/versions.js +13 -2
  80. package/modules/lifecycle/ship-card.js +38 -7
  81. package/modules/memory/routes/memory.js +2 -2
  82. package/modules/onboarding/routes/access-requests.js +3 -3
  83. package/modules/platform-identity/routes/projects.js +10 -6
  84. package/modules/platform-identity/routes/scouting.js +2 -2
  85. package/modules/provisioning/migrations/provisioning_004_onboard_mode.sql +30 -0
  86. package/modules/provisioning/provisioning.js +24 -6
  87. package/modules/provisioning/routes/provisioning.js +24 -6
  88. package/modules/security/artifact-scan/aggregate.js +133 -0
  89. package/modules/security/artifact-scan/detectors/capabilities.js +53 -0
  90. package/modules/security/artifact-scan/detectors/config-obfuscation.js +41 -0
  91. package/modules/security/artifact-scan/detectors/credentials.js +112 -0
  92. package/modules/security/artifact-scan/detectors/dependencies.js +115 -0
  93. package/modules/security/artifact-scan/detectors/hooks.js +116 -0
  94. package/modules/security/artifact-scan/detectors/injection.js +118 -0
  95. package/modules/security/artifact-scan/detectors/mcp.js +102 -0
  96. package/modules/security/artifact-scan/detectors/network.js +67 -0
  97. package/modules/security/artifact-scan/detectors/npm-lifecycle.js +67 -0
  98. package/modules/security/artifact-scan/detectors/permissions.js +91 -0
  99. package/modules/security/artifact-scan/detectors/shared.js +219 -0
  100. package/modules/security/artifact-scan/fence-local.js +99 -0
  101. package/modules/security/artifact-scan/fetch.js +330 -0
  102. package/modules/security/artifact-scan/floor.js +319 -0
  103. package/modules/security/artifact-scan/limits.js +66 -0
  104. package/modules/security/artifact-scan/manifest.js +203 -0
  105. package/modules/security/artifact-scan/mirror.js +158 -0
  106. package/modules/security/artifact-scan/quarantine.js +149 -0
  107. package/modules/security/artifact-scan/rebuild-spec.js +120 -0
  108. package/modules/security/artifact-scan/report.js +155 -0
  109. package/modules/security/artifact-scan/sanitize.js +317 -0
  110. package/modules/security/migrations/security_001_tool_scans.sql +145 -0
  111. package/modules/security/module.json +2 -2
  112. package/modules/security/routes/reports.js +11 -7
  113. package/modules/security/routes/scans.js +115 -0
  114. package/modules/security/scan-store.js +214 -0
  115. package/modules/security/tests/aggregate.mjs +283 -0
  116. package/modules/security/tests/artifact_scan.mjs +591 -0
  117. package/modules/security/tests/floor.mjs +549 -0
  118. package/modules/security/tests/mirror.mjs +350 -0
  119. package/modules/security/tests/sanitize_rebuild.mjs +499 -0
  120. package/modules/security/tests/scan_store.mjs +250 -0
  121. package/modules/security/tests/tool_scans_migration.mjs +116 -0
  122. package/modules/sessions/routes/sessions.js +6 -5
  123. package/package-lock.json +2 -2
  124. package/package.json +1 -1
  125. package/public-landing/create.html +13 -3
  126. package/scripts/gds/abandon.js +1 -1
  127. package/scripts/gds/audit-authorship.sh +6 -0
  128. package/scripts/gds/box.js +1 -1
  129. package/scripts/gds/claim.js +15 -0
  130. package/scripts/gds/cli-lib.js +16 -4
  131. package/scripts/gds/gating.js +28 -13
  132. package/scripts/gds/gen-api-docs.js +1 -1
  133. package/scripts/gds/goal.js +234 -0
  134. package/scripts/gds/init.js +70 -15
  135. package/scripts/gds/main-audit.js +80 -11
  136. package/scripts/gds/mirror-redact.js +6 -0
  137. package/scripts/gds/newcomer-floor.js +2 -2
  138. package/scripts/gds/provision.js +230 -94
  139. package/scripts/gds/run-unit-tests.js +9 -1
  140. package/scripts/gds/sandbox-stage.js +66 -9
  141. package/scripts/gds/search-ingest-md.js +1 -0
  142. package/scripts/gds/sequence.js +67 -8
  143. package/scripts/gds/ship.js +183 -50
  144. package/scripts/hall-preview/server.js +1 -1
  145. package/scripts/migrate.sh +5 -5
  146. package/src/bongos/auth.js +129 -27
  147. package/src/bongos/db-kernel.js +66 -5
  148. package/src/bongos/db.js +6 -7
  149. package/src/bongos/github-repos.js +19 -7
  150. package/src/bongos/goal-scope-check.js +1 -1
  151. package/src/bongos/module-scope-map.js +3 -1
  152. package/src/bongos/permission-path-check.js +1 -1
  153. package/src/bongos/route-rank-check.js +73 -30
  154. package/src/bongos/routes/audit-log.js +4 -2
  155. package/src/bongos/routes/auth.js +8 -4
  156. package/src/bongos/routes/backup.js +7 -4
  157. package/src/bongos/routes/builders.js +23 -11
  158. package/src/bongos/routes/github.js +15 -4
  159. package/src/bongos/routes/llm-cache.js +5 -2
  160. package/src/bongos/routes/modules.js +9 -5
  161. package/src/bongos/routes/security.js +21 -12
  162. package/src/bongos/serve-internal.js +22 -7
  163. package/src/module-api.js +86 -13
  164. package/src/platform-server.js +14 -0
  165. package/tests/api_client.mjs +18 -2
  166. package/tests/api_path_404.mjs +3 -0
  167. package/tests/art_stage.mjs +2 -1
  168. package/tests/atlas.mjs +32 -4
  169. package/tests/auth_admission_gate.mjs +17 -5
  170. package/tests/auth_error_brand.mjs +41 -21
  171. package/tests/auth_resolve_failure.mjs +155 -0
  172. package/tests/blocker_link.mjs +88 -0
  173. package/tests/bongos_downloads.mjs +2 -1
  174. package/tests/box_dns_repoint.mjs +2 -0
  175. package/tests/box_ship_permissions.mjs +2 -0
  176. package/tests/box_terminal_ssrf.mjs +23 -17
  177. package/tests/claim_error_surface.mjs +23 -0
  178. package/tests/claude_materialize.mjs +4 -0
  179. package/tests/cli_box_scope.mjs +40 -0
  180. package/tests/cloud_bongos_pack.mjs +34 -4
  181. package/tests/conductor.mjs +2 -1
  182. package/tests/criterion_authoring_gate.mjs +28 -5
  183. package/tests/currency_label.mjs +10 -1
  184. package/tests/deploy_instance.mjs +2 -0
  185. package/tests/devbox_downloads.mjs +2 -1
  186. package/tests/display_name.mjs +95 -6
  187. package/tests/eviction_policy.mjs +2 -1
  188. package/tests/file_map.mjs +10 -0
  189. package/tests/first_admin_bootstrap.mjs +10 -2
  190. package/tests/first_run_engine_check.mjs +2 -1
  191. package/tests/fitness.mjs +26 -6
  192. package/tests/gate_approvals.mjs +15 -2
  193. package/tests/github_push_land_proof.mjs +50 -4
  194. package/tests/github_repos_helpers.mjs +30 -5
  195. package/tests/github_token_store.mjs +23 -0
  196. package/tests/goal_abuse_matrix.mjs +13 -3
  197. package/tests/goal_cli.mjs +89 -0
  198. package/tests/governance_abuse_matrix.mjs +386 -0
  199. package/tests/governance_fold.mjs +156 -0
  200. package/tests/governance_fold_bench.mjs +50 -0
  201. package/tests/governance_live_proof.mjs +201 -0
  202. package/tests/governance_page_gates.mjs +21 -9
  203. package/tests/governance_parity.mjs +11 -2
  204. package/tests/governance_require_permission.mjs +125 -1
  205. package/tests/governance_routes.mjs +334 -0
  206. package/tests/grader.mjs +7 -3
  207. package/tests/hall_nav.mjs +14 -4
  208. package/tests/helpers.mjs +73 -0
  209. package/tests/hook_core_resolve.mjs +2 -1
  210. package/tests/host_topology_skips.mjs +97 -0
  211. package/tests/infra_exec_paths.mjs +14 -4
  212. package/tests/init.mjs +128 -0
  213. package/tests/known_failing_ratchet.mjs +89 -0
  214. package/tests/lifecycle-card.mjs +2 -1
  215. package/tests/main_audit.mjs +72 -1
  216. package/tests/memory_isolation.mjs +56 -9
  217. package/tests/migrate_root_resolution.mjs +1 -1
  218. package/tests/mirror_redact.mjs +19 -0
  219. package/tests/module_api.mjs +3 -1
  220. package/tests/module_loader.mjs +4 -0
  221. package/tests/modules.mjs +35 -7
  222. package/tests/modules_route.mjs +29 -5
  223. package/tests/onboarding_observability.mjs +49 -11
  224. package/tests/onboarding_route_signals.mjs +1 -0
  225. package/tests/one_active_claim_per_session.mjs +80 -0
  226. package/tests/pre_compact.mjs +2 -1
  227. package/tests/preview_server.mjs +2 -1
  228. package/tests/profile_route.mjs +1 -0
  229. package/tests/provision.mjs +170 -0
  230. package/tests/provisioning_onboard_mode.mjs +180 -0
  231. package/tests/publish_manifest.mjs +16 -3
  232. package/tests/publish_reconciler.mjs +181 -10
  233. package/tests/rank_tier_single_source.mjs +205 -0
  234. package/tests/recall_hints.mjs +2 -1
  235. package/tests/repo_map.mjs +4 -0
  236. package/tests/route_permission_check.mjs +14 -5
  237. package/tests/rubric_fixtures.mjs +3 -0
  238. package/tests/run_routine_two_root.mjs +2 -0
  239. package/tests/sandbox_stage.mjs +69 -7
  240. package/tests/search_ingest.mjs +7 -0
  241. package/tests/security_docs.mjs +28 -6
  242. package/tests/sequence.mjs +64 -0
  243. package/tests/session-cards.mjs +2 -1
  244. package/tests/session_start_freshness.mjs +2 -1
  245. package/tests/ship_broadcast_server.mjs +63 -1
  246. package/tests/ship_card.mjs +66 -0
  247. package/tests/ship_ci_deploy.mjs +66 -1
  248. package/tests/ship_smoke_gate.mjs +76 -0
  249. package/tests/versions_route.mjs +15 -0
  250. package/tests/workflow_exec_paths.mjs +16 -4
  251. package/tests/worktree_guard.mjs +2 -1
@@ -0,0 +1,100 @@
1
+ /* governance.css — Page-local styles for the Governance page (roles /
2
+ permissions / assignments — BV1.R111, ADR 0151). Built on the shared design
3
+ tokens (style.css), mirroring gate.css's card + pill idiom; the ?v= cache-bust
4
+ is stamped from this file's content hash at boot (src/asset-version.js). */
5
+
6
+ .gov-h { color: var(--ink); font-size: 16px; font-weight: 600; margin: 22px 0 8px; }
7
+ .gov-h--minor { font-size: 13px; margin: 18px 0 6px; }
8
+ .gov-grid-note { color: var(--ink-soft); font-size: 12.5px; margin: 0 0 12px; }
9
+ .gov-sealed, .gov-empty { color: var(--ink-soft); padding: 16px 0; }
10
+
11
+ /* Two columns on wide screens: role rail left, permission grid right. */
12
+ .gov-layout { display: grid; grid-template-columns: 260px 1fr; gap: 22px; align-items: start; }
13
+ @media (max-width: 760px) { .gov-layout { grid-template-columns: 1fr; } }
14
+
15
+ /* ---- role list ---------------------------------------------------------- */
16
+ .gov-role-list { display: flex; flex-direction: column; gap: 8px; }
17
+ .gov-role {
18
+ text-align: left; font: inherit; cursor: pointer;
19
+ border: 1px solid var(--border); border-radius: var(--radius-sm);
20
+ background: var(--surface); padding: 10px 12px;
21
+ display: flex; flex-direction: column; gap: 4px;
22
+ }
23
+ .gov-role:hover { background: var(--surface-2); }
24
+ .gov-role[aria-pressed="true"] { border-color: var(--accent); box-shadow: inset 2px 0 0 var(--accent); }
25
+ .gov-role__head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
26
+ .gov-role__label { font-weight: 600; color: var(--ink); font-size: 13.5px; }
27
+ .gov-role__meta { color: var(--ink-faint); font-size: 11.5px; }
28
+
29
+ .gov-pill {
30
+ font-size: 10.5px; font-weight: 600;
31
+ letter-spacing: 0.04em; text-transform: uppercase;
32
+ padding: 1px 7px; border-radius: 999px; white-space: nowrap;
33
+ border: 1px solid var(--border); color: var(--ink-soft); background: var(--surface-2);
34
+ }
35
+ .gov-pill--seeded { background: var(--info-weak); border-color: color-mix(in srgb, var(--info) 30%, transparent); color: var(--info); }
36
+ .gov-pill--custom { background: var(--ok-weak, var(--surface-2)); border-color: color-mix(in srgb, var(--ok) 30%, transparent); color: var(--ok); }
37
+ .gov-pill--locked { background: var(--danger-weak); border-color: color-mix(in srgb, var(--danger) 30%, transparent); color: var(--danger); }
38
+ .gov-pill--floor { font-variant-numeric: tabular-nums; }
39
+
40
+ /* ---- permission grid ---------------------------------------------------- */
41
+ .gov-section { margin: 0 0 18px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); overflow: hidden; }
42
+ .gov-section__head {
43
+ padding: 8px 14px; font-size: 11.5px; font-weight: 600;
44
+ letter-spacing: 0.04em; text-transform: uppercase; color: var(--ink-soft);
45
+ background: var(--surface-2); border-bottom: 1px solid var(--border);
46
+ }
47
+ .gov-perm { display: flex; align-items: center; gap: 10px; padding: 8px 14px; border-bottom: 1px solid var(--border); }
48
+ .gov-perm:last-child { border-bottom: 0; }
49
+ .gov-perm__main { flex: 1 1 auto; min-width: 0; }
50
+ .gov-perm__key {
51
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px;
52
+ color: var(--accent-text); background: var(--surface-2);
53
+ padding: 1px 5px; border-radius: 4px;
54
+ }
55
+ .gov-perm__guards { color: var(--ink-soft); font-size: 12px; margin-top: 2px; overflow-wrap: anywhere; }
56
+
57
+ /* The toggle: a small pill button. Held = filled; not held = outline. */
58
+ .gov-toggle {
59
+ font: inherit; font-size: 12px; font-weight: 600; cursor: pointer;
60
+ padding: 4px 12px; border-radius: 999px; white-space: nowrap;
61
+ border: 1px solid var(--border-strong); background: var(--surface); color: var(--ink-soft);
62
+ }
63
+ .gov-toggle[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: #fff; }
64
+ .gov-toggle:hover:not(:disabled) { border-color: var(--accent); }
65
+ .gov-toggle:disabled { opacity: 0.55; cursor: default; }
66
+ .gov-held-note { color: var(--ok); font-size: 12px; white-space: nowrap; }
67
+
68
+ /* ---- create / assign forms ---------------------------------------------- */
69
+ .gov-create, .gov-assign { display: flex; flex-direction: column; gap: 8px; margin-top: 8px; }
70
+ .gov-assign { flex-direction: row; flex-wrap: wrap; align-items: center; margin-bottom: 14px; }
71
+ .gov-input {
72
+ font: inherit; font-size: 13px; color: var(--ink);
73
+ background: var(--surface); border: 1px solid var(--border-strong);
74
+ border-radius: var(--radius-sm); padding: 7px 10px;
75
+ }
76
+ .gov-input:focus { outline: none; border-color: var(--accent); }
77
+ .gov-input--id { width: 110px; }
78
+ .gov-btn {
79
+ font: inherit; font-size: 13px; font-weight: 600;
80
+ padding: 7px 16px; border: 1px solid var(--accent);
81
+ background: var(--accent); color: #fff;
82
+ border-radius: var(--radius-sm); cursor: pointer; align-self: flex-start;
83
+ }
84
+ .gov-btn:hover:not(:disabled) { background: var(--accent-deep); border-color: var(--accent-deep); }
85
+ .gov-btn:disabled { opacity: 0.45; cursor: default; }
86
+ .gov-btn--ghost { background: var(--surface); color: var(--ink); border-color: var(--border-strong); }
87
+ .gov-btn--ghost:hover:not(:disabled) { background: var(--surface-2); }
88
+ .gov-btn--small { font-size: 12px; padding: 4px 10px; }
89
+
90
+ /* ---- assignments + principals ------------------------------------------ */
91
+ .gov-assign-row { display: flex; align-items: center; gap: 10px; padding: 8px 14px; border-bottom: 1px solid var(--border); }
92
+ .gov-assign-row:last-child { border-bottom: 0; }
93
+ .gov-assign-row__who { flex: 1 1 auto; color: var(--ink); font-size: 13px; min-width: 0; overflow-wrap: anywhere; }
94
+ .gov-assign-row__who .muted { color: var(--ink-faint); }
95
+ .gov-principal { padding: 8px 14px; border-bottom: 1px solid var(--border); font-size: 12.5px; color: var(--ink-soft); }
96
+ .gov-principal:last-child { border-bottom: 0; }
97
+ .gov-principal code {
98
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px;
99
+ color: var(--accent-text); background: var(--surface-2); padding: 1px 5px; border-radius: 4px;
100
+ }
@@ -0,0 +1,77 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Governance — {{worldName}}</title>
7
+ <meta name="description" content="Roles and permissions — create a custom role, grant everyday permissions, assign it to a builder. Trust-boundary powers stay locked.">
8
+ <link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
12
+ <link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
13
+ <link rel="stylesheet" href="/builders/governance.css">
14
+ </head>
15
+ <body data-page="governance" data-page-title="Governance">
16
+
17
+ <div class="app">
18
+ <aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
19
+ <div class="app-scrim" id="app-scrim" hidden></div>
20
+ <div class="app-body">
21
+ <header class="app-topbar" id="app-topbar"></header>
22
+ <div class="app-content">
23
+ <div class="page-head"><p class="page-head__sub" id="gov-sub">Loading…</p></div>
24
+
25
+ <main>
26
+ <p class="gov-sealed" id="gov-sealed" hidden>Archons only.</p>
27
+
28
+ <section id="gov-body" hidden>
29
+ <div class="gov-layout">
30
+ <div class="gov-roles-col">
31
+ <h2 class="gov-h">Roles</h2>
32
+ <div class="gov-role-list" id="gov-role-list"></div>
33
+ <form class="gov-create" id="gov-create-form" autocomplete="off">
34
+ <h3 class="gov-h gov-h--minor">New custom role</h3>
35
+ <input class="gov-input" id="gov-create-key" placeholder="role-key (kebab-case)" maxlength="64" required>
36
+ <input class="gov-input" id="gov-create-label" placeholder="Label" maxlength="200" required>
37
+ <input class="gov-input" id="gov-create-desc" placeholder="Description (optional)" maxlength="2000">
38
+ <button class="gov-btn" type="submit">Create role</button>
39
+ </form>
40
+ </div>
41
+
42
+ <div class="gov-grid-col">
43
+ <h2 class="gov-h" id="gov-grid-title">Permissions</h2>
44
+ <p class="gov-grid-note" id="gov-grid-note"></p>
45
+ <div id="gov-grid"></div>
46
+ </div>
47
+ </div>
48
+
49
+ <h2 class="gov-h">Assignments</h2>
50
+ <p class="gov-grid-note">Hand a custom role to a builder. Rank roles are assigned by rank itself and cannot be granted here.</p>
51
+ <form class="gov-assign" id="gov-assign-form" autocomplete="off">
52
+ <input class="gov-input gov-input--id" id="gov-assign-builder" type="number" min="1" placeholder="Builder id" required>
53
+ <select class="gov-input" id="gov-assign-role" required></select>
54
+ <button class="gov-btn" type="submit">Assign</button>
55
+ </form>
56
+ <div id="gov-assignments"></div>
57
+
58
+ <h2 class="gov-h">Machine principals</h2>
59
+ <p class="gov-grid-note">Capabilities held by a machine identity, never by a person or a role. Shown for completeness — nothing here can be granted.</p>
60
+ <div id="gov-principals"></div>
61
+ </section>
62
+ </main>
63
+
64
+ <footer class="app-foot" id="app-foot"></footer>
65
+ </div>
66
+ </div>
67
+ </div>
68
+
69
+ <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
70
+
71
+ <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
72
+ <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
73
+ <script src="/builders/palette.js" defer></script>
74
+ <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
75
+ <script src="/builders/governance.js?v=2026-08-06-governance"></script>
76
+ </body>
77
+ </html>
@@ -0,0 +1,353 @@
1
+ // public-builders/governance.js — Governance (/builders/governance).
2
+ //
3
+ // The roles/permissions/assignments tab (BV1.R111, ADR 0151 §2) over the R110
4
+ // CRUD routes. Discord-style: pick a role on the left, toggle its OPERATIONAL
5
+ // permissions in the grid, assign custom roles to builders below. Two walls the
6
+ // SERVER enforces and this page only renders honestly (ADR 0016 — nothing here
7
+ // grants anything):
8
+ // • `system` (trust-boundary) permissions render LOCKED — visible, never
9
+ // togglable; the API refuses them as `permission_not_grantable`.
10
+ // • the four seeded rank-roles are read-only — their grants derive from
11
+ // catalog floors and their assignment from builders.rank (R95b), so both
12
+ // toggle + assign are disabled and the API refuses `seeded_role_immutable`
13
+ // / `seeded_role_assignment_forbidden`.
14
+ //
15
+ // Like The Gate, this reads /api/bongos/me for a friendly Sealed notice —
16
+ // DEFENCE IN DEPTH only. Every /governance/* endpoint (reads included) is ALSO
17
+ // gated server-side on the `governance.manage` atom (archon floor). On 401 →
18
+ // web sign-in.
19
+ //
20
+ // DELIBERATELY NOT IN THE SIDEBAR YET: the nav entry + the server page gate are
21
+ // R113 (the next slice) — tests/hall_nav.mjs pins the Governance nav group and
22
+ // grows with that slice, not this one. Until then the page is reached by URL.
23
+
24
+ (() => {
25
+ 'use strict';
26
+
27
+ const API = '/api/bongos';
28
+ // Generated typed client (task 1996 / R09), served as window.BongosClient by
29
+ // /lib/bongos-client.js. baseUrl:'' passes the full `${API}${path}` URLs below
30
+ // through unchanged; result-mode ({ status, ok, data }) preserves control flow 1:1.
31
+ const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
32
+ const { escapeHtml } = window.OTB;
33
+ const toast = (msg) => window.OTB.toast(msg);
34
+
35
+ // ---- fetch helpers (mirror gate.js) --------------------------------------
36
+
37
+ async function fetchJson(path) {
38
+ const res = await api.request('GET', `${API}${path}`);
39
+ if (res.status === 401) { window.location.assign(`${API}/auth/web/start`); throw { kind: '401' }; }
40
+ if (res.status === 403) { throw { kind: '403' }; }
41
+ if (!res.ok) throw new Error(`${path} → ${res.status}`);
42
+ return res.data;
43
+ }
44
+
45
+ async function sendJson(method, path, body) {
46
+ const res = await api.request(method, `${API}${path}`, { body: body || {} });
47
+ if (res.status === 401) { window.location.assign(`${API}/auth/web/start`); throw { kind: '401' }; }
48
+ const data = res.data || {};
49
+ if (!res.ok) {
50
+ const err = data.error || {};
51
+ throw new Error(err.message && err.message !== err.code ? err.message : friendlyError(err.code) || `${path} → ${res.status}`);
52
+ }
53
+ return data;
54
+ }
55
+
56
+ // The R110 refusal codes, translated for the person clicking.
57
+ const ERROR_COPY = {
58
+ permission_not_grantable: 'That is a trust-boundary permission — it can never be granted to a custom role.',
59
+ seeded_role_immutable: 'Rank roles are read-only: their permissions derive from the catalog, not from this page.',
60
+ seeded_role_assignment_forbidden: 'Rank roles are assigned by rank itself — change the builder’s rank instead.',
61
+ role_exists: 'A role with that key already exists.',
62
+ role_not_found: 'That role no longer exists — refreshing.',
63
+ builder_not_found: 'No builder with that id.',
64
+ unknown_permission: 'The permission catalog does not define that key.',
65
+ bad_role_key: 'Role keys are kebab-case: lowercase letters, digits, dashes.',
66
+ validation_failed: 'Missing or invalid fields.',
67
+ };
68
+ const friendlyError = (code) => ERROR_COPY[code] || null;
69
+
70
+ // ---- state ---------------------------------------------------------------
71
+
72
+ let roles = []; // [{ role_key, label, description, is_system, seeded_rank, permissions: [] }]
73
+ let permissions = []; // catalog rows: [{ key, system, floor, guards, grantable }]
74
+ let principals = []; // [{ system_role, permission, guards }]
75
+ let assignments = []; // [{ builder_id, github_login, display_name, rank, role_key, role_label, is_system }]
76
+ let selectedRoleKey = null;
77
+
78
+ const selectedRole = () => roles.find((r) => r.role_key === selectedRoleKey) || null;
79
+
80
+ // ---- render: role list ---------------------------------------------------
81
+
82
+ function renderRoles() {
83
+ const list = document.getElementById('gov-role-list');
84
+ list.innerHTML = '';
85
+ for (const role of roles) {
86
+ const pill = role.is_system
87
+ ? `<span class="gov-pill gov-pill--seeded">rank role</span>`
88
+ : `<span class="gov-pill gov-pill--custom">custom</span>`;
89
+ const btn = document.createElement('button');
90
+ btn.type = 'button';
91
+ btn.className = 'gov-role';
92
+ btn.setAttribute('aria-pressed', role.role_key === selectedRoleKey ? 'true' : 'false');
93
+ btn.innerHTML =
94
+ `<span class="gov-role__head"><span class="gov-role__label">${escapeHtml(role.label || role.role_key)}</span>${pill}</span>
95
+ <span class="gov-role__meta"><code>${escapeHtml(role.role_key)}</code> · ${(role.permissions || []).length} permission${(role.permissions || []).length === 1 ? '' : 's'}</span>`;
96
+ btn.addEventListener('click', () => { selectedRoleKey = role.role_key; renderRoles(); renderGrid(); });
97
+ list.appendChild(btn);
98
+ }
99
+ }
100
+
101
+ // ---- render: permission grid --------------------------------------------
102
+
103
+ function permRow(p, role) {
104
+ const held = !!role && (role.permissions || []).includes(p.key);
105
+ const row = document.createElement('div');
106
+ row.className = 'gov-perm';
107
+
108
+ let control;
109
+ if (!p.grantable) {
110
+ // The R110 wall, rendered: visible, never togglable.
111
+ control = `<span class="gov-pill gov-pill--locked" title="Trust-boundary permission — non-grantable (ADR 0151)">locked</span>`;
112
+ } else if (!role) {
113
+ control = '';
114
+ } else if (role.is_system) {
115
+ // Seeded rank-role: show what it holds, allow nothing.
116
+ control = held ? `<span class="gov-held-note">held via rank</span>` : '';
117
+ } else {
118
+ control = `<button type="button" class="gov-toggle" aria-pressed="${held ? 'true' : 'false'}" data-perm="${escapeHtml(p.key)}">${held ? 'granted' : 'grant'}</button>`;
119
+ }
120
+
121
+ row.innerHTML =
122
+ `<div class="gov-perm__main">
123
+ <code class="gov-perm__key">${escapeHtml(p.key)}</code>
124
+ <span class="gov-pill gov-pill--floor">${escapeHtml(p.floor || 'principal')}</span>
125
+ <div class="gov-perm__guards">${escapeHtml(p.guards || '')}</div>
126
+ </div>
127
+ ${control}`;
128
+
129
+ const btn = row.querySelector('button.gov-toggle');
130
+ if (btn) btn.addEventListener('click', () => toggleGrant(role, p.key, !held, btn));
131
+ return row;
132
+ }
133
+
134
+ function section(title, rows) {
135
+ const el = document.createElement('section');
136
+ el.className = 'gov-section';
137
+ el.innerHTML = `<div class="gov-section__head">${escapeHtml(title)}</div>`;
138
+ for (const r of rows) el.appendChild(r);
139
+ return el;
140
+ }
141
+
142
+ function renderGrid() {
143
+ const grid = document.getElementById('gov-grid');
144
+ const role = selectedRole();
145
+ document.getElementById('gov-grid-title').textContent = role
146
+ ? `Permissions — ${role.label || role.role_key}` : 'Permissions';
147
+ document.getElementById('gov-grid-note').textContent = !role
148
+ ? 'Pick a role to see and edit what it may do.'
149
+ : role.is_system
150
+ ? 'A rank role’s permissions derive from the catalog and cannot be edited here.'
151
+ : 'Toggle the everyday permissions this role carries. Locked rows are trust-boundary powers no custom role can ever hold.';
152
+
153
+ grid.innerHTML = '';
154
+ const humanPerms = permissions.filter((p) => !principals.some((pr) => pr.permission === p.key));
155
+ const locked = humanPerms.filter((p) => !p.grantable);
156
+ const operational = humanPerms.filter((p) => p.grantable);
157
+ grid.appendChild(section(`Trust boundary — locked (${locked.length})`, locked.map((p) => permRow(p, role))));
158
+ grid.appendChild(section(`Operational (${operational.length})`, operational.map((p) => permRow(p, role))));
159
+ }
160
+
161
+ async function toggleGrant(role, permissionKey, grant, btn) {
162
+ btn.disabled = true;
163
+ try {
164
+ await sendJson(grant ? 'PUT' : 'DELETE', `/governance/roles/${encodeURIComponent(role.role_key)}/permissions/${encodeURIComponent(permissionKey)}`);
165
+ toast(grant ? `Granted ${permissionKey}.` : `Revoked ${permissionKey}.`);
166
+ await reloadRoles();
167
+ } catch (err) {
168
+ if (err && err.kind === '401') return;
169
+ toast(`Could not ${grant ? 'grant' : 'revoke'}: ${err.message || err}`);
170
+ btn.disabled = false;
171
+ }
172
+ }
173
+
174
+ // ---- render: assignments -------------------------------------------------
175
+
176
+ function renderAssignments() {
177
+ const box = document.getElementById('gov-assignments');
178
+ box.innerHTML = '';
179
+ if (assignments.length === 0) {
180
+ box.innerHTML = `<p class="gov-empty">No role assignments yet.</p>`;
181
+ return;
182
+ }
183
+ const wrap = document.createElement('section');
184
+ wrap.className = 'gov-section';
185
+ for (const a of assignments) {
186
+ const row = document.createElement('div');
187
+ row.className = 'gov-assign-row';
188
+ const who = `${escapeHtml(a.display_name || a.github_login || `builder ${a.builder_id}`)} <span class="muted">(#${escapeHtml(String(a.builder_id))} · ${escapeHtml(a.rank || '')})</span>`;
189
+ const rolePill = a.is_system
190
+ ? `<span class="gov-pill gov-pill--seeded">${escapeHtml(a.role_label || a.role_key)} · via rank</span>`
191
+ : `<span class="gov-pill gov-pill--custom">${escapeHtml(a.role_label || a.role_key)}</span>`;
192
+ const action = a.is_system ? '' : `<button type="button" class="gov-btn gov-btn--ghost gov-btn--small" data-act="unassign">Remove</button>`;
193
+ row.innerHTML = `<span class="gov-assign-row__who">${who}</span>${rolePill}${action}`;
194
+ const btn = row.querySelector('[data-act=unassign]');
195
+ if (btn) btn.addEventListener('click', () => unassign(a, btn));
196
+ wrap.appendChild(row);
197
+ }
198
+ box.appendChild(wrap);
199
+ }
200
+
201
+ function renderAssignSelect() {
202
+ const sel = document.getElementById('gov-assign-role');
203
+ const custom = roles.filter((r) => !r.is_system);
204
+ sel.innerHTML = custom.length
205
+ ? custom.map((r) => `<option value="${escapeHtml(r.role_key)}">${escapeHtml(r.label || r.role_key)}</option>`).join('')
206
+ : `<option value="" disabled selected>No custom roles yet</option>`;
207
+ }
208
+
209
+ async function unassign(a, btn) {
210
+ btn.disabled = true;
211
+ try {
212
+ await sendJson('DELETE', `/governance/assignments/${encodeURIComponent(a.builder_id)}/${encodeURIComponent(a.role_key)}`);
213
+ toast(`Removed ${a.role_label || a.role_key} from builder ${a.builder_id}.`);
214
+ await reloadAssignments();
215
+ } catch (err) {
216
+ if (err && err.kind === '401') return;
217
+ toast(`Could not remove: ${err.message || err}`);
218
+ btn.disabled = false;
219
+ }
220
+ }
221
+
222
+ // ---- render: principals --------------------------------------------------
223
+
224
+ function renderPrincipals() {
225
+ const box = document.getElementById('gov-principals');
226
+ box.innerHTML = '';
227
+ if (principals.length === 0) {
228
+ box.innerHTML = `<p class="gov-empty">No machine principals declared.</p>`;
229
+ return;
230
+ }
231
+ const wrap = document.createElement('section');
232
+ wrap.className = 'gov-section';
233
+ for (const p of principals) {
234
+ const row = document.createElement('div');
235
+ row.className = 'gov-principal';
236
+ row.innerHTML = `<code>${escapeHtml(p.system_role)}</code> holds <code>${escapeHtml(p.permission)}</code> — ${escapeHtml(p.guards || '')} <span class="gov-pill gov-pill--locked">locked</span>`;
237
+ wrap.appendChild(row);
238
+ }
239
+ box.appendChild(wrap);
240
+ }
241
+
242
+ // ---- loads ---------------------------------------------------------------
243
+
244
+ async function reloadRoles() {
245
+ const data = await fetchJson('/governance/roles');
246
+ roles = data.roles || [];
247
+ if (!roles.some((r) => r.role_key === selectedRoleKey)) selectedRoleKey = roles.length ? roles[0].role_key : null;
248
+ renderRoles();
249
+ renderGrid();
250
+ renderAssignSelect();
251
+ }
252
+
253
+ async function reloadAssignments() {
254
+ const data = await fetchJson('/governance/assignments');
255
+ assignments = data.assignments || [];
256
+ renderAssignments();
257
+ }
258
+
259
+ async function load() {
260
+ const perms = await fetchJson('/governance/permissions');
261
+ permissions = perms.permissions || [];
262
+ principals = perms.principals || [];
263
+ await reloadRoles();
264
+ await reloadAssignments();
265
+ renderPrincipals();
266
+ }
267
+
268
+ // ---- forms ---------------------------------------------------------------
269
+
270
+ function wireForms() {
271
+ document.getElementById('gov-create-form').addEventListener('submit', async (e) => {
272
+ e.preventDefault();
273
+ const btn = e.target.querySelector('button');
274
+ btn.disabled = true;
275
+ const body = {
276
+ role_key: document.getElementById('gov-create-key').value.trim(),
277
+ label: document.getElementById('gov-create-label').value.trim(),
278
+ };
279
+ const desc = document.getElementById('gov-create-desc').value.trim();
280
+ if (desc) body.description = desc;
281
+ try {
282
+ const r = await sendJson('POST', '/governance/roles', body);
283
+ toast(`Created role ${r.role ? r.role.role_key : body.role_key}.`);
284
+ e.target.reset();
285
+ selectedRoleKey = body.role_key;
286
+ await reloadRoles();
287
+ } catch (err) {
288
+ if (err && err.kind === '401') return;
289
+ toast(`Could not create role: ${err.message || err}`);
290
+ } finally {
291
+ btn.disabled = false;
292
+ }
293
+ });
294
+
295
+ document.getElementById('gov-assign-form').addEventListener('submit', async (e) => {
296
+ e.preventDefault();
297
+ const btn = e.target.querySelector('button');
298
+ const builderId = Number(document.getElementById('gov-assign-builder').value);
299
+ const roleKey = document.getElementById('gov-assign-role').value;
300
+ if (!roleKey) { toast('Create a custom role first.'); return; }
301
+ btn.disabled = true;
302
+ try {
303
+ await sendJson('POST', '/governance/assignments', { builder_id: builderId, role_key: roleKey });
304
+ toast(`Assigned ${roleKey} to builder ${builderId}.`);
305
+ e.target.reset();
306
+ await reloadAssignments();
307
+ } catch (err) {
308
+ if (err && err.kind === '401') return;
309
+ toast(`Could not assign: ${err.message || err}`);
310
+ } finally {
311
+ btn.disabled = false;
312
+ }
313
+ });
314
+ }
315
+
316
+ // ---- init ----------------------------------------------------------------
317
+
318
+ function showSealed() {
319
+ document.getElementById('gov-sealed').hidden = false;
320
+ document.getElementById('gov-body').hidden = true;
321
+ document.getElementById('gov-sub').textContent = 'Restricted.';
322
+ }
323
+
324
+ async function init() {
325
+ // Confirm rank for a friendly sealed notice; the server gates the page and
326
+ // every /governance endpoint on `governance.manage` regardless (cosmetic
327
+ // defence-in-depth). governance.manage is system:true, so only the archon
328
+ // rank-role can hold it — rank is an accurate client-side proxy.
329
+ let me;
330
+ try {
331
+ me = await fetchJson('/me');
332
+ } catch (err) {
333
+ if (err && err.kind === '401') return;
334
+ if (err && err.kind === '403') { showSealed(); return; }
335
+ me = null;
336
+ }
337
+ const rank = (me && (me.builder || me).rank || '').toLowerCase();
338
+ if (rank !== 'archon') { showSealed(); return; }
339
+
340
+ document.getElementById('gov-sub').textContent = 'Roles, permissions, and who holds them — the trust boundary stays locked.';
341
+ document.getElementById('gov-body').hidden = false;
342
+ wireForms();
343
+ try {
344
+ await load();
345
+ } catch (err) {
346
+ if (err && err.kind === '401') return;
347
+ if (err && err.kind === '403') { showSealed(); return; }
348
+ document.getElementById('gov-grid').innerHTML = `<p class="gov-empty">Could not load governance data just now.</p>`;
349
+ }
350
+ }
351
+
352
+ init();
353
+ })();
@@ -1,14 +1,16 @@
1
1
  // public-builders/harbor.js — the Harbor (/builders/harbor).
2
2
  //
3
- // Archon-only dev-box management view (idea 257 / task 1163). Every builder's
4
- // dev box is rendered as a card on the dock; an Archon can close any box and
5
- // block a builder from opening new ones.
3
+ // Metic+ dev-box management view (idea 257 / task 1163; Archon-only until ADR 0157
4
+ // moved `box.fleet.manage` to the working rank). Every builder's dev box is rendered
5
+ // as a card on the dock; a Metic or Archon can close any box and block a builder from
6
+ // opening new ones.
6
7
  //
7
8
  // Like the Watch page, this reads /api/bongos/me to learn the viewer's rank and
8
- // shows an Archons-only notice otherwise — DEFENCE IN DEPTH only. The data + mutation
9
- // endpoints (/boxes, /boxes/:id/close, /boxes/:id/block) are ALSO archon-gated
10
- // server-side (ADR 0016), and the page itself is gated by server.js
11
- // requireArchonPage. On a 401 it redirects into the web sign-in flow.
9
+ // shows a restricted notice below the Metic floor — DEFENCE IN DEPTH only. The data +
10
+ // mutation endpoints (/boxes, /boxes/:id/close, /boxes/:id/block) are ALSO gated
11
+ // server-side on `box.fleet.manage` (ADR 0016), and the page itself is gated by
12
+ // serve-internal.js requireGovernancePage — whose own floor is `page.view.governance`,
13
+ // metic since ADR 0157. On a 401 it redirects into the web sign-in flow.
12
14
 
13
15
  (() => {
14
16
  'use strict';
@@ -324,7 +326,11 @@
324
326
  me = null;
325
327
  }
326
328
  const rank = (me && (me.builder || me).rank || '').toLowerCase();
327
- if (rank !== 'archon') { showSealed(); return; }
329
+ // ADR 0157: Harbor sits at the `page.view.governance` floor (metic), and the fleet
330
+ // data it reads is `box.fleet.manage` (also metic now). Cosmetic —
331
+ // requireGovernancePage + the route gates are the wall; this just picks body vs
332
+ // sealed notice.
333
+ if (rank !== 'metic' && rank !== 'archon') { showSealed(); return; }
328
334
 
329
335
  document.getElementById('harbor-sub').textContent = 'Each builder’s dev box at a glance.';
330
336
  document.getElementById('harbor-body').hidden = false;
@@ -137,7 +137,9 @@
137
137
  try {
138
138
  const me = await getJSON(`${API}/me`, { softAuth: true }).catch(() => null);
139
139
  const rank = me && me.builder ? me.builder.rank : null;
140
- if (rank !== 'archon') return;
140
+ // ADR 0157: `provisioning.fleet.manage` moved archon→metic, so the fleet section
141
+ // reveals for Metic+ too. Cosmetic — the route gates are the wall.
142
+ if (rank !== 'metic' && rank !== 'archon') return;
141
143
  const [fleet, ledger] = await Promise.all([
142
144
  getJSON(`${API}/provisioning/fleet`).catch(() => ({ instances: [] })),
143
145
  getJSON(`${API}/provisioning/fleet/cost-ledger`).catch(() => null),
@@ -137,6 +137,12 @@
137
137
  hosting_shape: 'standalone',
138
138
  domain: domain || undefined,
139
139
  target_ref: `${owner}/${repo}`,
140
+ // The mode is LOAD-BEARING for the control-plane runner, not just for which
141
+ // checklist we render (task 1002562): 'adopt' makes it clone + layer onto the
142
+ // owner's existing history instead of pushing a fresh tree, which an existing
143
+ // repo rejects. This form already tracked `mode` purely for renderNextSteps —
144
+ // it has to reach the server too.
145
+ onboard_mode: mode,
140
146
  });
141
147
  const instanceId = data && data.instance ? data.instance.id : null;
142
148
  q('#np-form').hidden = true;
@@ -0,0 +1,51 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>People — {{worldName}}</title>
7
+ <meta name="description" content="The builder directory: everyone building across {{worldName}} projects, searchable and ranked by track record.">
8
+ <link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
12
+ <link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
13
+ </head>
14
+ <body data-page="people" data-page-title="People">
15
+
16
+ <div class="app">
17
+ <aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
18
+ <div class="app-scrim" id="app-scrim" hidden></div>
19
+ <div class="app-body">
20
+ <header class="app-topbar" id="app-topbar"></header>
21
+ <div class="app-content">
22
+ <div class="page-head">
23
+ <p class="page-head__sub">Everyone building across hosted projects, ranked by track record. Descriptive only — cross-project numbers never carry authority here.</p>
24
+ </div>
25
+
26
+ <main>
27
+ <section class="scroll" id="people-scroll" aria-labelledby="people-h">
28
+ <h2 id="people-h" class="scroll__h">Builder directory</h2>
29
+ <div id="people-filters"></div>
30
+ <div id="people-body">
31
+ <div class="profile__placeholder">Loading the directory…</div>
32
+ </div>
33
+ </section>
34
+ </main>
35
+
36
+ <footer class="app-foot" id="app-foot"></footer>
37
+ </div>
38
+ </div>
39
+ </div>
40
+
41
+ <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
42
+
43
+ <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
44
+ chrome exists before the page script runs. -->
45
+ <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
46
+ <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
47
+ <script src="/builders/palette.js" defer></script>
48
+ <script src="/builders/hall-kit.js"></script>
49
+ <script src="/builders/people.js"></script>
50
+ </body>
51
+ </html>