@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
@@ -102,7 +102,7 @@ function createClient(opts = {}) {
102
102
  getAnalyticsBuilderId: (args) => request("GET", "/analytics/builder/{id}", { hasBody: false }, args),
103
103
  },
104
104
  "auditLog": {
105
- // GET /audit-log — rank: archon — GET /audit-log
105
+ // GET /audit-log — rank: metic+archon — GET /audit-log
106
106
  getAuditLog: (args) => request("GET", "/audit-log", { hasBody: false }, args),
107
107
  },
108
108
  "auth": {
@@ -144,7 +144,7 @@ function createClient(opts = {}) {
144
144
  getAuthWebStart: (args) => request("GET", "/auth/web/start", { hasBody: false }, args),
145
145
  },
146
146
  "autonomy": {
147
- // GET /autonomy/precheck — rank: archon — GET /autonomy/precheck
147
+ // GET /autonomy/precheck — rank: metic+archon — GET /autonomy/precheck
148
148
  getAutonomyPrecheck: (args) => request("GET", "/autonomy/precheck", { hasBody: false }, args),
149
149
  },
150
150
  "backup": {
@@ -204,13 +204,13 @@ function createClient(opts = {}) {
204
204
  postBoxVersion: (args) => request("POST", "/box/version", { hasBody: true }, args),
205
205
  },
206
206
  "boxes": {
207
- // GET /boxes — rank: archon — GET /boxes
207
+ // GET /boxes — rank: metic+archon — GET /boxes
208
208
  getBoxes: (args) => request("GET", "/boxes", { hasBody: false }, args),
209
- // PATCH /boxes/{builderId}/block — rank: archon — PATCH /boxes/:builderId/block
209
+ // PATCH /boxes/{builderId}/block — rank: metic+archon — PATCH /boxes/:builderId/block
210
210
  patchBoxesBuilderIdBlock: (args) => request("PATCH", "/boxes/{builderId}/block", { hasBody: true }, args),
211
- // POST /boxes/{builderId}/close — rank: archon — POST /boxes/:builderId/close
211
+ // POST /boxes/{builderId}/close — rank: metic+archon — POST /boxes/:builderId/close
212
212
  postBoxesBuilderIdClose: (args) => request("POST", "/boxes/{builderId}/close", { hasBody: true }, args),
213
- // GET /boxes/cost-ledger — rank: archon — GET /boxes/cost-ledger
213
+ // GET /boxes/cost-ledger — rank: metic+archon — GET /boxes/cost-ledger
214
214
  getBoxesCostLedger: (args) => request("GET", "/boxes/cost-ledger", { hasBody: false }, args),
215
215
  },
216
216
  "bugAttachments": {
@@ -218,9 +218,9 @@ function createClient(opts = {}) {
218
218
  getBugAttachmentsName: (args) => request("GET", "/bug-attachments/{name}", { hasBody: false }, args),
219
219
  },
220
220
  "builders": {
221
- // GET /builders/{id}/active-claims — rank: archon — GET /builders/:id/active-claims
221
+ // GET /builders/{id}/active-claims — rank: metic+archon — GET /builders/:id/active-claims
222
222
  getBuildersIdActiveClaims: (args) => request("GET", "/builders/{id}/active-claims", { hasBody: false }, args),
223
- // PATCH /builders/{id}/budget — rank: archon — PATCH /builders/:id/budget
223
+ // PATCH /builders/{id}/budget — rank: metic+archon — PATCH /builders/:id/budget
224
224
  patchBuildersIdBudget: (args) => request("PATCH", "/builders/{id}/budget", { hasBody: true }, args),
225
225
  // GET /builders/{id}/onboarding — rank: public — GET /builders/:id/onboarding
226
226
  getBuildersIdOnboarding: (args) => request("GET", "/builders/{id}/onboarding", { hasBody: false }, args),
@@ -234,13 +234,13 @@ function createClient(opts = {}) {
234
234
  getBuildersDirectory: (args) => request("GET", "/builders/directory", { hasBody: false }, args),
235
235
  // GET /builders/me/pending-rank-change — rank: any-builder — GET /builders/me/pending-rank-change
236
236
  getBuildersMePendingRankChange: (args) => request("GET", "/builders/me/pending-rank-change", { hasBody: false }, args),
237
- // GET /builders/roster — rank: archon — GET /builders/roster
237
+ // GET /builders/roster — rank: metic+archon — GET /builders/roster
238
238
  getBuildersRoster: (args) => request("GET", "/builders/roster", { hasBody: false }, args),
239
239
  },
240
240
  "claims": {
241
241
  // POST /claims — rank: any-builder — POST /claims
242
242
  postClaims: (args) => request("POST", "/claims", { hasBody: true }, args),
243
- // POST /claims/{id}/release-on-behalf — rank: archon — POST /claims/:id/release-on-behalf
243
+ // POST /claims/{id}/release-on-behalf — rank: metic+archon — POST /claims/:id/release-on-behalf
244
244
  postClaimsIdReleaseOnBehalf: (args) => request("POST", "/claims/{id}/release-on-behalf", { hasBody: true }, args),
245
245
  // POST /claims/{id}/resolve — rank: any-builder — POST /claims/:id/resolve
246
246
  postClaimsIdResolve: (args) => request("POST", "/claims/{id}/resolve", { hasBody: true }, args),
@@ -262,21 +262,21 @@ function createClient(opts = {}) {
262
262
  postDependencies: (args) => request("POST", "/dependencies", { hasBody: true }, args),
263
263
  },
264
264
  "discord": {
265
- // GET /discord/channels/plan — rank: archon — GET /discord/channels/plan
265
+ // GET /discord/channels/plan — rank: metic+archon — GET /discord/channels/plan
266
266
  getDiscordChannelsPlan: (args) => request("GET", "/discord/channels/plan", { hasBody: false }, args),
267
- // POST /discord/channels/reconcile — rank: archon — POST /discord/channels/reconcile
267
+ // POST /discord/channels/reconcile — rank: metic+archon — POST /discord/channels/reconcile
268
268
  postDiscordChannelsReconcile: (args) => request("POST", "/discord/channels/reconcile", { hasBody: true }, args),
269
269
  },
270
270
  "doneWhen": {
271
271
  // POST /done-when/{criterionId}/satisfy — rank: metic+archon — POST /done-when/:criterionId/satisfy
272
272
  postDoneWhenCriterionIdSatisfy: (args) => request("POST", "/done-when/{criterionId}/satisfy", { hasBody: true }, args),
273
- // POST /done-when/{criterionId}/unsatisfy — rank: archon — POST /done-when/:criterionId/unsatisfy
273
+ // POST /done-when/{criterionId}/unsatisfy — rank: metic+archon — POST /done-when/:criterionId/unsatisfy
274
274
  postDoneWhenCriterionIdUnsatisfy: (args) => request("POST", "/done-when/{criterionId}/unsatisfy", { hasBody: true }, args),
275
275
  // GET /done-when/pending-review — rank: metic+archon — GET /done-when/pending-review
276
276
  getDoneWhenPendingReview: (args) => request("GET", "/done-when/pending-review", { hasBody: false }, args),
277
277
  },
278
278
  "gateApprovals": {
279
- // GET /gate-approvals — rank: archon — GET /gate-approvals
279
+ // GET /gate-approvals — rank: metic+archon — GET /gate-approvals
280
280
  getGateApprovals: (args) => request("GET", "/gate-approvals", { hasBody: false }, args),
281
281
  // POST /gate-approvals/{pr}/approve — rank: archon — POST /gate-approvals/:pr/approve
282
282
  postGateApprovalsPrApprove: (args) => request("POST", "/gate-approvals/{pr}/approve", { hasBody: true }, args),
@@ -334,11 +334,29 @@ function createClient(opts = {}) {
334
334
  patchGoalsIdVisibility: (args) => request("PATCH", "/goals/{id}/visibility", { hasBody: true }, args),
335
335
  // GET /goals/inbox — rank: any-builder — GET /goals/inbox
336
336
  getGoalsInbox: (args) => request("GET", "/goals/inbox", { hasBody: false }, args),
337
- // POST /goals/reorder — rank: archon — POST /goals/reorder
337
+ // POST /goals/reorder — rank: metic+archon — POST /goals/reorder
338
338
  postGoalsReorder: (args) => request("POST", "/goals/reorder", { hasBody: true }, args),
339
339
  },
340
+ "governance": {
341
+ // GET /governance/assignments — rank: archon — GET /governance/assignments
342
+ getGovernanceAssignments: (args) => request("GET", "/governance/assignments", { hasBody: false }, args),
343
+ // POST /governance/assignments — rank: archon — POST /governance/assignments
344
+ postGovernanceAssignments: (args) => request("POST", "/governance/assignments", { hasBody: true }, args),
345
+ // DELETE /governance/assignments/{builderId}/{roleKey} — rank: archon — DELETE /governance/assignments/:builderId/:roleKey
346
+ deleteGovernanceAssignmentsBuilderIdRoleKey: (args) => request("DELETE", "/governance/assignments/{builderId}/{roleKey}", { hasBody: false }, args),
347
+ // GET /governance/permissions — rank: archon — GET /governance/permissions
348
+ getGovernancePermissions: (args) => request("GET", "/governance/permissions", { hasBody: false }, args),
349
+ // GET /governance/roles — rank: archon — GET /governance/roles
350
+ getGovernanceRoles: (args) => request("GET", "/governance/roles", { hasBody: false }, args),
351
+ // POST /governance/roles — rank: archon — POST /governance/roles
352
+ postGovernanceRoles: (args) => request("POST", "/governance/roles", { hasBody: true }, args),
353
+ // DELETE /governance/roles/{roleKey}/permissions/{permissionKey} — rank: archon — DELETE /governance/roles/:roleKey/permissions/:permissionKey
354
+ deleteGovernanceRolesRoleKeyPermissionsPermissionKey: (args) => request("DELETE", "/governance/roles/{roleKey}/permissions/{permissionKey}", { hasBody: false }, args),
355
+ // PUT /governance/roles/{roleKey}/permissions/{permissionKey} — rank: archon — PUT /governance/roles/:roleKey/permissions/:permissionKey
356
+ putGovernanceRolesRoleKeyPermissionsPermissionKey: (args) => request("PUT", "/governance/roles/{roleKey}/permissions/{permissionKey}", { hasBody: true }, args),
357
+ },
340
358
  "grades": {
341
- // GET /grades/by-builder — rank: archon — GET /grades/by-builder
359
+ // GET /grades/by-builder — rank: metic+archon — GET /grades/by-builder
342
360
  getGradesByBuilder: (args) => request("GET", "/grades/by-builder", { hasBody: false }, args),
343
361
  },
344
362
  "healthz": {
@@ -354,7 +372,7 @@ function createClient(opts = {}) {
354
372
  getInboxId: (args) => request("GET", "/inbox/{id}", { hasBody: false }, args),
355
373
  // PATCH /inbox/{id} — rank: metic+archon — PATCH /inbox/:id
356
374
  patchInboxId: (args) => request("PATCH", "/inbox/{id}", { hasBody: true }, args),
357
- // POST /inbox/{id}/ratify — rank: archon — POST /inbox/:id/ratify
375
+ // POST /inbox/{id}/ratify — rank: metic+archon — POST /inbox/:id/ratify
358
376
  postInboxIdRatify: (args) => request("POST", "/inbox/{id}/ratify", { hasBody: true }, args),
359
377
  },
360
378
  "instance": {
@@ -432,9 +450,9 @@ function createClient(opts = {}) {
432
450
  getMemoryBfgBuildersBuilderIdFiles: (args) => request("GET", "/memory/bfg/builders/{builderId}/files", { hasBody: false }, args),
433
451
  // POST /memory/builders/{builderId}/bfg-write — rank: bfg-principal — POST /memory/builders/:builderId/bfg-write
434
452
  postMemoryBuildersBuilderIdBfgWrite: (args) => request("POST", "/memory/builders/{builderId}/bfg-write", { hasBody: true }, args),
435
- // GET /memory/builders/{builderId}/file — rank: archon — GET /memory/builders/:builderId/file
453
+ // GET /memory/builders/{builderId}/file — rank: metic+archon — GET /memory/builders/:builderId/file
436
454
  getMemoryBuildersBuilderIdFile: (args) => request("GET", "/memory/builders/{builderId}/file", { hasBody: false }, args),
437
- // GET /memory/builders/{builderId}/files — rank: archon — GET /memory/builders/:builderId/files
455
+ // GET /memory/builders/{builderId}/files — rank: metic+archon — GET /memory/builders/:builderId/files
438
456
  getMemoryBuildersBuilderIdFiles: (args) => request("GET", "/memory/builders/{builderId}/files", { hasBody: false }, args),
439
457
  // DELETE /memory/file — rank: any-builder — DELETE /memory/file
440
458
  deleteMemoryFile: (args) => request("DELETE", "/memory/file", { hasBody: true }, args),
@@ -470,27 +488,27 @@ function createClient(opts = {}) {
470
488
  postMyProjectsJoin: (args) => request("POST", "/my-projects/join", { hasBody: true }, args),
471
489
  },
472
490
  "overrideRequests": {
473
- // GET /override-requests — rank: archon — GET /override-requests
491
+ // GET /override-requests — rank: metic+archon — GET /override-requests
474
492
  getOverrideRequests: (args) => request("GET", "/override-requests", { hasBody: false }, args),
475
- // POST /override-requests/{id}/decide — rank: archon — POST /override-requests/:id/decide
493
+ // POST /override-requests/{id}/decide — rank: metic+archon — POST /override-requests/:id/decide
476
494
  postOverrideRequestsIdDecide: (args) => request("POST", "/override-requests/{id}/decide", { hasBody: true }, args),
477
495
  },
478
496
  "projects": {
479
- // GET /projects — rank: archon — GET /projects
497
+ // GET /projects — rank: metic+archon — GET /projects
480
498
  getProjects: (args) => request("GET", "/projects", { hasBody: false }, args),
481
- // POST /projects — rank: archon — POST /projects
499
+ // POST /projects — rank: metic+archon — POST /projects
482
500
  postProjects: (args) => request("POST", "/projects", { hasBody: true }, args),
483
- // PATCH /projects/{id}/featured — rank: archon — PATCH /projects/:id/featured
501
+ // PATCH /projects/{id}/featured — rank: metic+archon — PATCH /projects/:id/featured
484
502
  patchProjectsIdFeatured: (args) => request("PATCH", "/projects/{id}/featured", { hasBody: true }, args),
485
503
  // GET /projects/featured — rank: public — GET /projects/featured
486
504
  getProjectsFeatured: (args) => request("GET", "/projects/featured", { hasBody: false }, args),
487
- // POST /projects/invite — rank: archon — POST /projects/invite
505
+ // POST /projects/invite — rank: metic+archon — POST /projects/invite
488
506
  postProjectsInvite: (args) => request("POST", "/projects/invite", { hasBody: true }, args),
489
507
  },
490
508
  "provisioning": {
491
- // GET /provisioning/fleet — rank: archon — GET /provisioning/fleet
509
+ // GET /provisioning/fleet — rank: metic+archon — GET /provisioning/fleet
492
510
  getProvisioningFleet: (args) => request("GET", "/provisioning/fleet", { hasBody: false }, args),
493
- // GET /provisioning/fleet/cost-ledger — rank: archon — GET /provisioning/fleet/cost-ledger
511
+ // GET /provisioning/fleet/cost-ledger — rank: metic+archon — GET /provisioning/fleet/cost-ledger
494
512
  getProvisioningFleetCostLedger: (args) => request("GET", "/provisioning/fleet/cost-ledger", { hasBody: false }, args),
495
513
  // GET /provisioning/github-app/callback — rank: public — GET /provisioning/github-app/callback
496
514
  getProvisioningGithubAppCallback: (args) => request("GET", "/provisioning/github-app/callback", { hasBody: false }, args),
@@ -504,7 +522,7 @@ function createClient(opts = {}) {
504
522
  postProvisioningInstances: (args) => request("POST", "/provisioning/instances", { hasBody: true }, args),
505
523
  // GET /provisioning/instances/{id} — rank: any-builder — GET /provisioning/instances/:id
506
524
  getProvisioningInstancesId: (args) => request("GET", "/provisioning/instances/{id}", { hasBody: false }, args),
507
- // POST /provisioning/instances/{id}/force-teardown — rank: archon — POST /provisioning/instances/:id/force-teardown
525
+ // POST /provisioning/instances/{id}/force-teardown — rank: metic+archon — POST /provisioning/instances/:id/force-teardown
508
526
  postProvisioningInstancesIdForceTeardown: (args) => request("POST", "/provisioning/instances/{id}/force-teardown", { hasBody: true }, args),
509
527
  // POST /provisioning/instances/{id}/github-app — rank: any-builder — POST /provisioning/instances/:id/github-app
510
528
  postProvisioningInstancesIdGithubApp: (args) => request("POST", "/provisioning/instances/{id}/github-app", { hasBody: true }, args),
@@ -554,7 +572,7 @@ function createClient(opts = {}) {
554
572
  getPublicVersions: (args) => request("GET", "/public/versions", { hasBody: false }, args),
555
573
  },
556
574
  "scouting": {
557
- // GET /scouting — rank: archon — GET /scouting
575
+ // GET /scouting — rank: metic+archon — GET /scouting
558
576
  getScouting: (args) => request("GET", "/scouting", { hasBody: false }, args),
559
577
  },
560
578
  "search": {
@@ -566,24 +584,28 @@ function createClient(opts = {}) {
566
584
  "security": {
567
585
  // GET /security/adr — rank: metic+archon — GET /security/adr
568
586
  getSecurityAdr: (args) => request("GET", "/security/adr", { hasBody: false }, args),
569
- // GET /security/docs — rank: archon — GET /security/docs
587
+ // GET /security/docs — rank: metic+archon — GET /security/docs
570
588
  getSecurityDocs: (args) => request("GET", "/security/docs", { hasBody: false }, args),
571
- // POST /security/docs — rank: archon — POST /security/docs
589
+ // POST /security/docs — rank: metic+archon — POST /security/docs
572
590
  postSecurityDocs: (args) => request("POST", "/security/docs", { hasBody: true }, args),
573
- // GET /security/docs/{slug} — rank: archon — GET /security/docs/:slug
591
+ // GET /security/docs/{slug} — rank: metic+archon — GET /security/docs/:slug
574
592
  getSecurityDocsSlug: (args) => request("GET", "/security/docs/{slug}", { hasBody: false }, args),
575
- // GET /security/reports — rank: archon — GET /security/reports
593
+ // GET /security/reports — rank: metic+archon — GET /security/reports
576
594
  getSecurityReports: (args) => request("GET", "/security/reports", { hasBody: false }, args),
577
595
  // POST /security/reports — rank: metic+archon — POST /security/reports
578
596
  postSecurityReports: (args) => request("POST", "/security/reports", { hasBody: true }, args),
579
- // POST /security/reports/{id}/confirm — rank: archon — POST /security/reports/:id/confirm
597
+ // POST /security/reports/{id}/confirm — rank: metic+archon — POST /security/reports/:id/confirm
580
598
  postSecurityReportsIdConfirm: (args) => request("POST", "/security/reports/{id}/confirm", { hasBody: true }, args),
581
- // POST /security/reports/{id}/dispute — rank: archon — POST /security/reports/:id/dispute
599
+ // POST /security/reports/{id}/dispute — rank: metic+archon — POST /security/reports/:id/dispute
582
600
  postSecurityReportsIdDispute: (args) => request("POST", "/security/reports/{id}/dispute", { hasBody: true }, args),
583
- // POST /security/reports/{id}/fix — rank: archon — POST /security/reports/:id/fix
601
+ // POST /security/reports/{id}/fix — rank: metic+archon — POST /security/reports/:id/fix
584
602
  postSecurityReportsIdFix: (args) => request("POST", "/security/reports/{id}/fix", { hasBody: true }, args),
585
- // POST /security/reports/{id}/link-fix — rank: archon — POST /security/reports/:id/link-fix
603
+ // POST /security/reports/{id}/link-fix — rank: metic+archon — POST /security/reports/:id/link-fix
586
604
  postSecurityReportsIdLinkFix: (args) => request("POST", "/security/reports/{id}/link-fix", { hasBody: true }, args),
605
+ // GET /security/scans — rank: any-builder — GET /security/scans
606
+ getSecurityScans: (args) => request("GET", "/security/scans", { hasBody: false }, args),
607
+ // POST /security/scans — rank: metic+archon — POST /security/scans
608
+ postSecurityScans: (args) => request("POST", "/security/scans", { hasBody: true }, args),
587
609
  },
588
610
  "session": {
589
611
  // POST /session/optimize — rank: any-builder — POST /session/optimize
@@ -596,7 +618,7 @@ function createClient(opts = {}) {
596
618
  getSessionsId: (args) => request("GET", "/sessions/{id}", { hasBody: false }, args),
597
619
  // GET /sessions/mine — rank: any-builder — GET /sessions/mine
598
620
  getSessionsMine: (args) => request("GET", "/sessions/mine", { hasBody: false }, args),
599
- // GET /sessions/search — rank: archon — GET /sessions/search
621
+ // GET /sessions/search — rank: metic+archon — GET /sessions/search
600
622
  getSessionsSearch: (args) => request("GET", "/sessions/search", { hasBody: false }, args),
601
623
  },
602
624
  "sso": {
@@ -664,17 +686,17 @@ function createClient(opts = {}) {
664
686
  getTasksClaimable: (args) => request("GET", "/tasks/claimable", { hasBody: false }, args),
665
687
  // GET /tasks/newcomer-floor — rank: any-builder — GET /tasks/newcomer-floor
666
688
  getTasksNewcomerFloor: (args) => request("GET", "/tasks/newcomer-floor", { hasBody: false }, args),
667
- // POST /tasks/newcomer-restock — rank: archon — POST /tasks/newcomer-restock
689
+ // POST /tasks/newcomer-restock — rank: metic+archon — POST /tasks/newcomer-restock
668
690
  postTasksNewcomerRestock: (args) => request("POST", "/tasks/newcomer-restock", { hasBody: true }, args),
669
691
  // GET /tasks/peer-votes/feed — rank: any-builder — GET /tasks/peer-votes/feed
670
692
  getTasksPeerVotesFeed: (args) => request("GET", "/tasks/peer-votes/feed", { hasBody: false }, args),
671
- // POST /tasks/peer-votes/tally — rank: archon — POST /tasks/peer-votes/tally
693
+ // POST /tasks/peer-votes/tally — rank: metic+archon — POST /tasks/peer-votes/tally
672
694
  postTasksPeerVotesTally: (args) => request("POST", "/tasks/peer-votes/tally", { hasBody: true }, args),
673
695
  },
674
696
  "versions": {
675
697
  // GET /versions — rank: public — GET /versions
676
698
  getVersions: (args) => request("GET", "/versions", { hasBody: false }, args),
677
- // POST /versions — rank: archon — POST /versions
699
+ // POST /versions — rank: metic+archon — POST /versions
678
700
  postVersions: (args) => request("POST", "/versions", { hasBody: true }, args),
679
701
  // GET /versions/{id}/done-when — rank: public — GET /versions/:id/done-when
680
702
  getVersionsIdDoneWhen: (args) => request("GET", "/versions/{id}/done-when", { hasBody: false }, args),
@@ -214,7 +214,7 @@ module.exports = function buildBoxRouter() {
214
214
 
215
215
  // GET /boxes — full roster across all builders. rank: archon (exposes every
216
216
  // builder's box + IP + cost; same gate as the rest of roster management).
217
- router.get('/boxes', auth.requireBuilder, auth.requireRank('archon'), async (req, res) => {
217
+ router.get('/boxes', auth.requireBuilder, auth.requirePermission('box.fleet.manage'), async (req, res) => {
218
218
  try {
219
219
  const state = req.query.state || undefined;
220
220
  const rows = await boxes.listBoxes(pool, { state });
@@ -249,7 +249,7 @@ module.exports = function buildBoxRouter() {
249
249
  // (#602). rank: archon — cost oversight across every builder without a DB query.
250
250
  // Declared among the archon routes (archon-first) so the route-rank auditor's
251
251
  // 20-line lookahead classifies it correctly.
252
- router.get('/boxes/cost-ledger', auth.requireBuilder, auth.requireRank('archon'), async (req, res) => {
252
+ router.get('/boxes/cost-ledger', auth.requireBuilder, auth.requirePermission('box.fleet.manage'), async (req, res) => {
253
253
  try {
254
254
  // R14 (#2001 / ADR 0119): shared helper; per-endpoint cap preserved (default 500, max 2000).
255
255
  const { limit } = api.parsePagination(req, { defaultLimit: 500, maxLimit: 2000 });
@@ -268,7 +268,7 @@ module.exports = function buildBoxRouter() {
268
268
  // §9.4). rank: archon (acts on another builder's paid infrastructure).
269
269
  // Declared among the archon routes (archon-first) so the route-rank auditor's
270
270
  // 20-line lookahead classifies it correctly.
271
- router.post('/boxes/:builderId/close', auth.requireBuilder, auth.requireRank('archon'), async (req, res) => {
271
+ router.post('/boxes/:builderId/close', auth.requireBuilder, auth.requirePermission('box.fleet.manage'), async (req, res) => {
272
272
  const builderId = Number(req.params.builderId);
273
273
  if (!Number.isInteger(builderId) || builderId < 1) {
274
274
  return res.fail('bad_builder_id', 400);
@@ -315,7 +315,7 @@ module.exports = function buildBoxRouter() {
315
315
  // it does NOT touch a box that is already active. close_current adds the
316
316
  // explicit teardown the owner asked be optional ("options for both"). rank:
317
317
  // archon. Archon-first ordering (see above).
318
- router.patch('/boxes/:builderId/block', auth.requireBuilder, auth.requireRank('archon'), async (req, res) => {
318
+ router.patch('/boxes/:builderId/block', auth.requireBuilder, auth.requirePermission('box.fleet.manage'), async (req, res) => {
319
319
  const builderId = Number(req.params.builderId);
320
320
  if (!Number.isInteger(builderId) || builderId < 1) {
321
321
  return res.fail('bad_builder_id', 400);
@@ -610,7 +610,7 @@ module.exports = function buildBoxRouter() {
610
610
  if (state !== 'active') {
611
611
  return res.fail('box_not_active', { status: 409, message: state === 'none' || state === 'destroyed'
612
612
  ? 'No active box to close.'
613
- : `Box is currently '${state}' — wait for it to finish or contact an Archon.`, details: { state } });
613
+ : `Box is currently '${state}' — wait for it to finish or ask a Metic+ operator.`, details: { state } });
614
614
  }
615
615
  const { intent, created } = await boxes.enqueueIntent(
616
616
  pool, req.builder.id, 'deprovision', 'api:self'
@@ -802,13 +802,15 @@ module.exports = function buildBoxRouter() {
802
802
  error: 'BOX_PROVISION_DENIED', reason: decision.reason,
803
803
  required: decision.required ?? null, actual: decision.actual ?? null,
804
804
  };
805
- // idea 257 / task 1163: when an Archon block is the cause, tell the
805
+ // idea 257 / task 1163: when an operator block is the cause, tell the
806
806
  // builder WHY (the recorded reason) rather than a bare denial — this is
807
- // the `message` the box-panel surfaces to the builder verbatim.
807
+ // the `message` the box-panel surfaces to the builder verbatim. The block
808
+ // is written by a Metic+ operator (box.fleet.manage, ADR 0157), so don't
809
+ // attribute it to an Archon specifically.
808
810
  if (decision.reason === 'BOX_PROVISION_BLOCKED') {
809
811
  payload.message = builder && builder.box_blocked_reason
810
- ? `An Archon has paused thy dev-box access: ${builder.box_blocked_reason}`
811
- : 'An Archon has paused thy dev-box access. Ask in Discord #support.';
812
+ ? `An operator has paused thy dev-box access: ${builder.box_blocked_reason}`
813
+ : 'An operator has paused thy dev-box access. Ask in Discord #support.';
812
814
  }
813
815
  return res.status(403).json(payload);
814
816
  }
@@ -4,7 +4,8 @@
4
4
  // `const auth = require('./auth')` call sites unchanged:
5
5
  //
6
6
  // • KERNEL gates, re-exported off the published doorway — requireBuilder /
7
- // requireRank (the same rank machinery every route uses; ADR 0016).
7
+ // requireRank / requirePermission (the same authority machinery every route
8
+ // uses; ADR 0016, ADR 0151).
8
9
  //
9
10
  // • DISCORD-SPECIFIC helpers VENDORED here — the snowflake validator + the bot-
10
11
  // token loader. These are deliberately NOT on the kernel doorway: the doorway
@@ -48,6 +49,7 @@ module.exports = {
48
49
  // kernel gates (doorway)
49
50
  requireBuilder: api.requireBuilder,
50
51
  requireRank: api.requireRank,
52
+ requirePermission: api.requirePermission,
51
53
  // vendored discord-specific helpers
52
54
  isValidDiscordUserId,
53
55
  loadDiscordBotToken,
@@ -31,6 +31,9 @@ const roleSync = require('../discord-role-sync');
31
31
  // unlink; degrades to { guildId: null } so the callback simply skips the join.
32
32
  // • EVENT task.shipped — core (publish-reconciler / routes/tasks) emits it on the
33
33
  // confirmed→shipped transition; we post the #ship-news / #ship-feed broadcast.
34
+ // • EVENT task.strand-flagged — the publish-reconciler emits it the moment a
35
+ // confirmed task is newly flagged needs_rebase (task 1002606); we post the
36
+ // strand notice so the builder hears about it before their next session.
34
37
  // • EVENT builder.role-sync-needed { builderId } — core emits it on a rank change
35
38
  // (routes/builders.js) + after a link (routes/auth.js); we repair the Discord
36
39
  // rank-role badge. Fire-and-forget; the listener owns its own error handling.
@@ -38,6 +41,7 @@ const roleSync = require('../discord-role-sync');
38
41
  // hasProvider-guarded; listeners are replaced (off→on) so a test re-instantiating
39
42
  // the router never accumulates duplicates.
40
43
  let _shipListenerOff = null;
44
+ let _strandListenerOff = null;
41
45
  let _roleSyncListenerOff = null;
42
46
  function registerSeams() {
43
47
  if (!api.hasProvider('discord.isLinked')) {
@@ -52,6 +56,8 @@ function registerSeams() {
52
56
  }
53
57
  if (_shipListenerOff) _shipListenerOff();
54
58
  _shipListenerOff = api.on('task.shipped', (task) => shipBroadcast.postShipBroadcast(task));
59
+ if (_strandListenerOff) _strandListenerOff();
60
+ _strandListenerOff = api.on('task.strand-flagged', (info) => shipBroadcast.postStrandNotice(info));
55
61
  if (_roleSyncListenerOff) _roleSyncListenerOff();
56
62
  _roleSyncListenerOff = api.on('builder.role-sync-needed', ({ builderId }) =>
57
63
  roleSync.syncBuilderRole(builderId).catch((e) =>
@@ -66,7 +72,7 @@ module.exports = function buildDiscordRouter() {
66
72
  // apply to make Discord match docs/discord/channels.json, plus warnings. No
67
73
  // writes. 503 when the bot is disabled / not connected to a guild.
68
74
  // rank: archon — channel administration command.
69
- router.get('/discord/channels/plan', auth.requireBuilder, auth.requireRank('archon'), async (req, res) => {
75
+ router.get('/discord/channels/plan', auth.requireBuilder, auth.requirePermission('discord.channels.manage'), async (req, res) => {
70
76
  try {
71
77
  const channels = require('../discord-channels');
72
78
  const plan = channels.computePlan();
@@ -84,7 +90,7 @@ module.exports = function buildDiscordRouter() {
84
90
  // Best-effort; returns how many ops applied/failed. 503 when the bot is
85
91
  // disabled / not connected.
86
92
  // rank: archon — channel administration command.
87
- router.post('/discord/channels/reconcile', auth.requireBuilder, auth.requireRank('archon'), async (req, res) => {
93
+ router.post('/discord/channels/reconcile', auth.requireBuilder, auth.requirePermission('discord.channels.manage'), async (req, res) => {
88
94
  try {
89
95
  const channels = require('../discord-channels');
90
96
  const result = await channels.reconcileChannels();
@@ -75,4 +75,51 @@ async function postShipBroadcast(task, deps = {}) {
75
75
  return result;
76
76
  }
77
77
 
78
- module.exports = { chooseBroadcastTargets, postShipBroadcast };
78
+ // task 1002606 (finding P-3) — the strand notice. Until now Discord fired only on
79
+ // task.shipped, so a STRAND (needs_rebase flagged by the publish-reconciler) had no
80
+ // push channel — the builder discovered it at their next /builder-start, up to a
81
+ // whole session later. The reconciler now emits `task.strand-flagged` the moment a
82
+ // strand is detected; this poster carries it to the builder channel.
83
+ //
84
+ // PURE routing, same discipline as chooseBroadcastTargets: a security-sensitive
85
+ // task's strand notice goes ONLY to the Archon-only #security-ship-feed (the reason
86
+ // string can name conflicted files of an unpatched fix). The line deliberately does
87
+ // NOT start with `#<taskId>` — a LEADING task marker is what the applause bot maps
88
+ // 👍 reactions onto (discord-reaction.parseShipMarker), and a strand is not
89
+ // applaudable shipped work.
90
+ function chooseStrandTargets({ securitySensitive, taskId, reason }) {
91
+ if (taskId == null) return [];
92
+ const line = `⚠️ task ${taskId} stranded at 'confirmed' (${reason || 'unknown'}) — its builder must fix + re-land it, or release it.`;
93
+ if (securitySensitive) {
94
+ return [{ channel: 'security_ship_feed', line }];
95
+ }
96
+ return [{ channel: 'ship_feed', line }];
97
+ }
98
+
99
+ // Best-effort server-side strand poster — fired by the `task.strand-flagged` seam
100
+ // listener. `info` is the reconciler's event payload: { id, builder_id, title,
101
+ // security_sensitive, reason }. FAIL CLOSED on the security flag exactly like
102
+ // postShipBroadcast: if it is not a definite boolean the notice is suppressed
103
+ // entirely rather than risk naming an unpatched fix on a builder channel. Never
104
+ // throws — a channel failure is logged and swallowed.
105
+ async function postStrandNotice(info, deps = {}) {
106
+ const result = { posted: [], skipped: null };
107
+ if (!info || info.id == null) { result.skipped = 'no_task'; return result; }
108
+ const sec = info.security_sensitive;
109
+ if (sec !== true && sec !== false) { result.skipped = 'security_flag_indeterminate'; return result; }
110
+
111
+ const postWebhook = deps.postWebhook || require('./webhook').postWebhook;
112
+ const log = deps.log || console.log;
113
+ const targets = chooseStrandTargets({ securitySensitive: sec, taskId: info.id, reason: info.reason });
114
+ for (const { channel, line } of targets) {
115
+ try {
116
+ await postWebhook(channel, line);
117
+ result.posted.push(channel);
118
+ } catch (e) {
119
+ log(`[gds] strand notice: #${channel.replace(/_/g, '-')} post failed (non-blocking): ${e && e.message ? e.message : e}`);
120
+ }
121
+ }
122
+ return result;
123
+ }
124
+
125
+ module.exports = { chooseBroadcastTargets, postShipBroadcast, chooseStrandTargets, postStrandNotice };
@@ -90,7 +90,7 @@ const PERMISSIONS = [
90
90
  { key: 'task.confirm.grade_bypass', system: true, floor: 'archon', guards: 'POST /tasks/:id/confirm — ratify past a failed grade (env-relaxable, ADR 0041)' },
91
91
  { key: 'dependency.cross_tier.manage', system: true, floor: 'archon', guards: 'POST|DELETE /dependencies — cross-tier structural edges (ADR 0086)' },
92
92
  { key: 'goal.scope.manage', system: true, floor: 'archon', guards: 'cross-tier goal-scope edges + protected-scope admission (ADR 0086)' },
93
- { key: 'governance.manage', system: true, floor: 'archon', guards: 'the governance tab write routes (roles/perms/assignments) — meta-escalation' },
93
+ { key: 'governance.manage', system: true, floor: 'archon', guards: 'the governance tab routes (roles/perms/assignments CRUD, reads included — R110) — meta-escalation' },
94
94
  { key: 'gate_approval.decide', system: true, floor: 'archon', guards: 'POST /gate-approvals/:pr/approve — CI deploy-gate override' },
95
95
  { key: 'deploy.push_main', system: true, floor: 'archon', guards: 'git push origin main / the deploy — substrate-enforced (pre-push, main-audit)' },
96
96
  { key: 'path.protected.modify', system: true, floor: 'archon', guards: 'modifying a PROTECTED_GLOBS path below the Metic floor — substrate (ADR 0043)' },
@@ -99,27 +99,21 @@ const PERMISSIONS = [
99
99
  { key: 'principal.discord_bot', system: true, floor: null, principal: 'discord_bot', guards: 'Discord bot routes — requireDiscordBot (exact system_role; dormant)' },
100
100
 
101
101
  // ── operational — floor archon (admin ops; grantable to a custom role) ────────
102
+ //
103
+ // ADR 0157 (owner decision, 2026-08-05) narrowed this block to IDENTITY: the Archon
104
+ // rank is responsible for rank and identity only. Nineteen keys that sat here for
105
+ // administrative convenience moved to floor `metic` below; each carries a
106
+ // "(ADR 0157, was archon)" note there. What remains answers "who exists, who is
107
+ // admitted, and whose sessions are valid" — the identity half of the rank/identity
108
+ // mandate, with rank itself covered by `builder.rank.set` (system, above).
109
+ //
110
+ // The four trust-boundary keys that ALSO stay Archon are system:true above
111
+ // (`task.confirm.grade_bypass`, `gate_approval.decide`, `goal.scope.manage`,
112
+ // `governance.manage`) — see the ADR's escalation-chain argument for why moving any
113
+ // one of them would make this delegation self-reversible.
102
114
  { key: 'builder.status.set', system: false, floor: 'archon', guards: 'PATCH /builders/:id/status — offboard / (de)activate' },
103
- { key: 'builder.budget.set', system: false, floor: 'archon', guards: 'PATCH /builders/:id/budget' },
104
- { key: 'criterion.unsatisfy', system: false, floor: 'archon', guards: 'POST /done-when/:id/unsatisfy' },
105
115
  { key: 'auth.session.revoke', system: false, floor: 'archon', guards: 'POST /auth/revoke, /auth/revoke-all' },
106
116
  { key: 'access_request.review', system: false, floor: 'archon', guards: 'GET/PATCH /access-requests/:id — admit a builder' },
107
- { key: 'security.report.adjudicate', system: false, floor: 'archon', guards: 'POST /security/reports/:id/{confirm,dispute,fix,link-fix}, /security/docs CRUD' },
108
- { key: 'provisioning.fleet.manage', system: false, floor: 'archon', guards: 'GET /provisioning/fleet[/cost-ledger], POST .../force-teardown' },
109
- { key: 'box.fleet.manage', system: false, floor: 'archon', scope: 'any', resource: 'box', guards: 'GET /boxes[/cost-ledger], POST /boxes/:b/close, PATCH .../block' },
110
- { key: 'autonomy.precheck', system: false, floor: 'archon', guards: 'GET /autonomy/precheck — unsupervised-runner gate' },
111
- { key: 'discord.channels.manage', system: false, floor: 'archon', guards: 'GET /discord/channels/plan, POST .../reconcile' },
112
- { key: 'project.curate', system: false, floor: 'archon', guards: 'platform-identity: POST /projects, /projects/invite, PATCH /projects/:id/featured, GET /scouting' },
113
- { key: 'override_request.decide', system: false, floor: 'archon', guards: 'GET /override-requests, POST .../:id/decide' },
114
- { key: 'claim.release.any', system: false, floor: 'archon', scope: 'any', resource: 'claim', guards: 'POST /claims/:id/release-on-behalf' },
115
- { key: 'idea.ratify', system: false, floor: 'archon', guards: 'POST /inbox/:id/ratify' },
116
- { key: 'task.newcomer_restock', system: false, floor: 'archon', guards: 'POST /tasks/newcomer-restock' },
117
- { key: 'task.peer_votes.tally', system: false, floor: 'archon', guards: 'POST /tasks/peer-votes/tally' },
118
- { key: 'goal.reorder', system: false, floor: 'archon', guards: 'POST /goals/reorder' },
119
- { key: 'builder.roster.read', system: false, floor: 'archon', guards: 'GET /builders/roster, /grades/by-builder, /builders/:id/active-claims' },
120
- { key: 'session.search', system: false, floor: 'archon', guards: 'GET /sessions/search' },
121
- { key: 'memory.read.any', system: false, floor: 'archon', scope: 'any', resource: 'memory', guards: 'GET /memory/builders/:id/file[s] — cross-builder memory read' },
122
- { key: 'audit_log.read', system: false, floor: 'archon', guards: 'GET /audit-log' },
123
117
 
124
118
  // ── operational — floor metic (the working rank's review/triage/authoring) ────
125
119
  { key: 'idea.triage', system: false, floor: 'metic', guards: 'PATCH /inbox/:id' },
@@ -152,6 +146,57 @@ const PERMISSIONS = [
152
146
  { key: 'source.clone.local', system: false, floor: 'metic', guards: 'Metic+ local clone / collaborator invite (ADR 0035)' },
153
147
  { key: 'box.source.full', system: false, floor: 'metic', guards: 'GET /box/source-access → full (vs starter); boxScopeForRank' },
154
148
 
149
+ // ── delegated archon→metic by ADR 0157 (owner decision, 2026-08-05) ───────────
150
+ //
151
+ // "Archon is responsible for rank and identity." These nineteen are administration
152
+ // and operations: they spend, read, adjudicate, and shuffle work, but none of them
153
+ // decides WHO HOLDS AUTHORITY, so the trusted working rank carries them. All were
154
+ // floor 'archon' before this ADR; none is system:true, so all were already
155
+ // delegable to a custom role — this only lowers the seeded floor.
156
+ //
157
+ // Deliberately NOT in this block, though the owner's rule would otherwise reach them:
158
+ // • `version.close` — retained at archon; it is the DISPOSITION that finalises a
159
+ // version's scope-truth, and canonical-permissions has long paired it with rank
160
+ // as the two powers that never delegate. ADR 0157 keeps that pairing.
161
+ // • `dependency.cross_tier.manage` — system:true, and the catalog's own invariant
162
+ // (tests/governance_catalog.mjs) forbids granting a system key to metic. Moving
163
+ // it would mean clearing the system flag, which widens what any CUSTOM role can
164
+ // be granted — a second-order change beyond the delegation that was asked for.
165
+ { key: 'builder.budget.set', system: false, floor: 'metic', guards: 'PATCH /builders/:id/budget (ADR 0157, was archon)' },
166
+ { key: 'criterion.unsatisfy', system: false, floor: 'metic', guards: 'POST /done-when/:id/unsatisfy (ADR 0157, was archon)' },
167
+ // BV1.R105. system:false DELIBERATELY, despite `version.close` being system:true.
168
+ // The `system` flag marks TRUST-BOUNDARY power — things that escalate or bypass the
169
+ // boundary (deploy, protected-path, rank-set, grade-bypass, cross-tier, principals).
170
+ // Creating a version is AUTHORING: it confers no authority on anyone. Its real
171
+ // siblings are `goal.create` and `task.create`, both operational. Close is the
172
+ // DISPOSITION that finalises a version's scope-truth; create opens it — the symmetry
173
+ // argument was considered and rejected, because system:true would permanently bar
174
+ // delegating version authoring to a custom role for no trust-boundary reason.
175
+ // ADR 0157 then lowered create's floor to metic while close stayed archon, which is
176
+ // the same asymmetry read from the other side: authoring delegates, disposition does not.
177
+ { key: 'version.create', system: false, floor: 'metic', guards: 'POST /versions — create a version + its done-when criteria (ADR 0157, was archon)' },
178
+ { key: 'security.report.adjudicate', system: false, floor: 'metic', guards: 'GET /security/reports, POST /security/reports/:id/{confirm,dispute,fix,link-fix}, /security/docs CRUD (ADR 0157, was archon)' },
179
+ { key: 'provisioning.fleet.manage', system: false, floor: 'metic', guards: 'GET /provisioning/fleet[/cost-ledger], POST .../force-teardown (ADR 0157, was archon)' },
180
+ { key: 'box.fleet.manage', system: false, floor: 'metic', scope: 'any', resource: 'box', guards: 'GET /boxes[/cost-ledger], POST /boxes/:b/close, PATCH .../block (ADR 0157, was archon)' },
181
+ { key: 'autonomy.precheck', system: false, floor: 'metic', guards: 'GET /autonomy/precheck — unsupervised-runner gate (ADR 0157, was archon)' },
182
+ { key: 'discord.channels.manage', system: false, floor: 'metic', guards: 'GET /discord/channels/plan, POST .../reconcile (ADR 0157, was archon)' },
183
+ { key: 'project.curate', system: false, floor: 'metic', guards: 'platform-identity: GET/POST /projects, /projects/invite, PATCH /projects/:id/featured, GET /scouting (ADR 0157, was archon)' },
184
+ { key: 'override_request.decide', system: false, floor: 'metic', guards: 'GET /override-requests, POST .../:id/decide (ADR 0157, was archon)' },
185
+ { key: 'claim.release.any', system: false, floor: 'metic', scope: 'any', resource: 'claim', guards: 'POST /claims/:id/release-on-behalf (ADR 0157, was archon)' },
186
+ { key: 'idea.ratify', system: false, floor: 'metic', guards: 'POST /inbox/:id/ratify (ADR 0157, was archon)' },
187
+ { key: 'task.newcomer_restock', system: false, floor: 'metic', guards: 'POST /tasks/newcomer-restock (ADR 0157, was archon)' },
188
+ { key: 'task.peer_votes.tally', system: false, floor: 'metic', guards: 'POST /tasks/peer-votes/tally (ADR 0157, was archon)' },
189
+ { key: 'goal.reorder', system: false, floor: 'metic', guards: 'POST /goals/reorder (ADR 0157, was archon)' },
190
+ { key: 'builder.roster.read', system: false, floor: 'metic', guards: 'GET /builders/roster, /grades/by-builder, /builders/:id/active-claims (ADR 0157, was archon)' },
191
+ { key: 'session.search', system: false, floor: 'metic', guards: 'GET /sessions/search (ADR 0157, was archon)' },
192
+ { key: 'memory.read.any', system: false, floor: 'metic', scope: 'any', resource: 'memory', guards: 'GET /memory/builders/:id/file[s] — cross-builder memory read (ADR 0157, was archon)' },
193
+ { key: 'audit_log.read', system: false, floor: 'metic', guards: 'GET /audit-log (ADR 0157, was archon)' },
194
+ // ADR 0157 §4. Split from `gate_approval.decide` (system, archon) so the Gate PAGE
195
+ // can open to Metic while APPROVING a held PR stays Archon. Without the split, a
196
+ // Metic could reach /gate via `page.view.governance` and then 403 on its only data
197
+ // endpoint — a page that renders nothing but an error.
198
+ { key: 'gate_approval.read', system: false, floor: 'metic', guards: 'GET /gate-approvals — the Gate page read side (approve = gate_approval.decide, archon)' },
199
+
155
200
  // ── ownership-scoped + baseline own-work (floor thetes / xenos) ───────────────
156
201
  { key: 'task.claim.any', system: false, floor: 'thetes', guards: 'claim the general queue (per-task requires_rank floor still applies)' },
157
202
  { key: 'page.view.builder', system: false, floor: 'thetes', guards: 'authenticated hall pages (requireNonXenosPage / requireBuilderPage)' },
@@ -174,7 +219,12 @@ const PERMISSIONS = [
174
219
  { key: 'blocker.file', system: false, floor: 'xenos', guards: 'POST /blockers' },
175
220
  { key: 'idea.file', system: false, floor: 'xenos', guards: 'POST /inbox (own idea capture)' },
176
221
  { key: 'page.view.public', system: false, floor: 'xenos', guards: 'public read surfaces (incl. xenos)' },
177
- { key: 'page.view.governance', system: false, floor: 'archon', guards: 'the Archon governance page (requireArchonPage; write = governance.manage)' },
222
+ // ADR 0157: floor archon→metic, which is what opens /watch, /harbor and /gate to the
223
+ // working rank. The gate FUNCTION was renamed to match (`requireArchonPage` →
224
+ // `requireGovernancePage`) once the floor moved. Viewing is all this grants: every
225
+ // write those pages reach is its own atom, and `governance.manage` (the
226
+ // roles/permissions tab) stayed archon.
227
+ { key: 'page.view.governance', system: false, floor: 'metic', guards: 'the governance/monitoring pages — /watch, /harbor, /gate (requireGovernancePage; write = governance.manage, archon)' },
178
228
  ];
179
229
 
180
230
  // ── invariants asserted at module load (fail loud on an authoring typo) ─────────