@bongos/core 1.19.1035 → 1.19.1037

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 (81) hide show
  1. package/.bongos-core.json +102 -92
  2. package/.claude/skills/grade-recover/SKILL.md +1 -1
  3. package/clients/bongos-client/index.d.ts +1 -1
  4. package/docs/adr/0302-the-third-identical-failure-stops-the-session-and-asks.md +3 -2
  5. package/docs/adr/0329-the-rendered-fit-check-is-a-kit-floor-and-an-advisory-ship-check-row.md +2 -2
  6. package/docs/adr/0339-a-projects-owner-upgrades-it-and-a-hosted-project-hands-off-to-the-hub-door.md +84 -0
  7. package/docs/adr/README.md +2 -1
  8. package/docs/api/openapi.json +7 -5
  9. package/docs/copy-inventory.md +10 -10
  10. package/docs/copy-registry.json +13 -13
  11. package/docs/module-api-changelog.md +4 -0
  12. package/modules/economy/credits.js +1 -1
  13. package/modules/grading/grader-config.js +3 -12
  14. package/modules/grading/grader-no-artifact.js +227 -2
  15. package/modules/grading/grader-prompt.js +1 -1
  16. package/modules/grading/grader-rubric.json +1 -86
  17. package/modules/grading/grader-score.js +1 -169
  18. package/modules/grading/grader-subagent.js +1 -51
  19. package/modules/grading/grader.js +26 -211
  20. package/modules/hall-ui/public/atlas.html +1 -1
  21. package/modules/hall-ui/public/blockers.html +1 -1
  22. package/modules/hall-ui/public/board-room.html +1 -1
  23. package/modules/hall-ui/public/collab.html +1 -1
  24. package/modules/hall-ui/public/copy-desk.html +1 -1
  25. package/modules/hall-ui/public/deploy.html +1 -1
  26. package/modules/hall-ui/public/diagrams.html +1 -1
  27. package/modules/hall-ui/public/drachmae.html +1 -1
  28. package/modules/hall-ui/public/fleet.html +1 -1
  29. package/modules/hall-ui/public/gate.html +1 -1
  30. package/modules/hall-ui/public/goals.html +1 -1
  31. package/modules/hall-ui/public/government.html +1 -1
  32. package/modules/hall-ui/public/idea.html +1 -1
  33. package/modules/hall-ui/public/ideas.html +1 -1
  34. package/modules/hall-ui/public/index.html +1 -1
  35. package/modules/hall-ui/public/modules.html +1 -1
  36. package/modules/hall-ui/public/people.html +1 -1
  37. package/modules/hall-ui/public/primer.html +1 -1
  38. package/modules/hall-ui/public/profile.html +1 -1
  39. package/modules/hall-ui/public/project-settings.html +1 -1
  40. package/modules/hall-ui/public/ranks.html +1 -1
  41. package/modules/hall-ui/public/roadmap.html +1 -1
  42. package/modules/hall-ui/public/roster.html +1 -1
  43. package/modules/hall-ui/public/sessions.html +1 -1
  44. package/modules/hall-ui/public/settings.html +1 -1
  45. package/modules/hall-ui/public/shell.js +36 -5
  46. package/modules/hall-ui/public/studio.html +1 -1
  47. package/modules/hall-ui/public/task.html +1 -1
  48. package/modules/hall-ui/public/watch.html +1 -1
  49. package/modules/hall-ui/public/work.html +1 -1
  50. package/modules/lifecycle/db-ship.js +3 -3
  51. package/modules/provisioning/core-upgrade-authz.js +85 -0
  52. package/modules/provisioning/core-upgrade.js +2 -1
  53. package/modules/provisioning/provisioning.js +4 -4
  54. package/modules/provisioning/routes/core-upgrade.js +12 -6
  55. package/package-lock.json +2 -2
  56. package/package.json +1 -1
  57. package/release-notes.json +20 -0
  58. package/scripts/gds/claim.js +2 -1
  59. package/scripts/gds/grade-smoke.js +3 -3
  60. package/scripts/gds/provision-core-upgrade.js +12 -4
  61. package/scripts/gds/render-check.js +1 -1
  62. package/scripts/gds/ship-check.js +1 -1
  63. package/scripts/gds/ship-flow.js +1 -1
  64. package/scripts/gds/ship-grade-steps.js +2 -2
  65. package/scripts/gds/visual-review-nudge.js +2 -1
  66. package/src/bongos/auth.js +8 -2
  67. package/src/bongos/routes/core-update.js +25 -3
  68. package/src/module-api.js +1 -1
  69. package/tests/adr_0162_gate_citation.mjs +167 -0
  70. package/tests/core_upgrade_owner_door.mjs +444 -0
  71. package/tests/grader.mjs +48 -126
  72. package/tests/grader_unbacked_score.mjs +1 -1
  73. package/tests/nav_permission_atoms.mjs +1 -1
  74. package/tests/provisioning_teardown_intent.mjs +28 -2
  75. package/tests/regrade_actual_minutes.mjs +18 -17
  76. package/tests/render_check.mjs +1 -1
  77. package/tests/scouting_page_gate.mjs +11 -3
  78. package/tests/skill_grade_audit.mjs +3 -3
  79. package/tests/skill_grader_health.mjs +2 -2
  80. package/scripts/gds/audit-ships.js +0 -319
  81. package/tests/audit_ships_git_argv.mjs +0 -173
@@ -73,7 +73,7 @@ A grade block naming protected surfaces above your rank is an authorization wall
73
73
 
74
74
  ## Constraints
75
75
 
76
- - **ADR 0162 — no new gates.** This skill describes and recovers; it never inserts a human-approval step into the ship path, and it never invents a new checkpoint.
76
+ - **It recovers; it adds nothing to pass.** This skill describes and recovers a stalled ship. It never inserts a human-approval step into the ship path (ADR 0162 retired those) and never invents a checkpoint of its own: a recovery tool that added a step would be one more thing to recover from.
77
77
  - **Diagnose before touching code.** Editing code in response to an outage (Branch A) or a guard trip (Branch B) wastes a panel round and muddies the audit trail.
78
78
  - **`--regrade` is claim-scoped**: most-recent claim holder or Archon. If you are neither, the recovery belongs to whoever is.
79
79
  - **Honest re-grade before any override.** The override-request path is for a grade that is *wrong*, not a grade that is *inconvenient*.
@@ -61,7 +61,7 @@ export interface GetConnectionsDegreeHandleResponse { handle: unknown; degree: u
61
61
  export interface GetConnectionsRequestsResponse { incoming: unknown; outgoing: unknown; incoming_count: unknown; outgoing_count: unknown }
62
62
  export interface GetConnectionsResponse { connections: unknown; count: unknown }
63
63
  export interface GetConnectionsSuggestionsResponse { suggestions: unknown; count: unknown }
64
- export interface GetCoreUpdateResponse { ok: boolean; update: unknown }
64
+ export interface GetCoreUpdateResponse { ok: boolean; update: unknown; deploy_url: unknown }
65
65
  export interface GetCoreUpgradesResponse { ok: boolean; running: unknown; limit: unknown; upgrades: unknown }
66
66
  export interface GetDiscordChannelsPlanResponse { ok: boolean; guild_id: unknown; op_count: unknown; ops: unknown; warnings: unknown }
67
67
  export interface GetDiscordChannelsSnapshotResponse { ok: boolean; guild_id: unknown; config: unknown; warnings: unknown }
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Status:** accepted · **Date:** 2026-09-17 · **Task:** [task 1003990](https://cloudbongos.com/builders#/task/1003990) (goal 1000095 — BONGOS-V2)
4
4
 
5
- **Related:** [ADR 0301](0301-a-red-pr-names-its-failing-tests-in-a-commit-status.md) (a red PR names its failing tests) · [ADR 0291](0291-the-accessibility-dimension-scores-the-transcript-not-the-diff.md) (accessibility is scored after the ship) · [ADR 0162](0162-review-gates-retired-rank-consistent-ci-only.md) (no new gates on the ship path) · [ADR 0043](0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md) (cooperative, fail-open hook layer)
5
+ **Related:** [ADR 0301](0301-a-red-pr-names-its-failing-tests-in-a-commit-status.md) (a red PR names its failing tests) · [ADR 0291](0291-the-accessibility-dimension-scores-the-transcript-not-the-diff.md) (accessibility is scored after the ship) · [ADR 0162](0162-review-gates-retired-rank-consistent-ci-only.md) (review gates retired: no human approval in the land path) · [ADR 0043](0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md) (cooperative, fail-open hook layer)
6
6
 
7
7
  ## Context
8
8
 
@@ -46,7 +46,8 @@ computed after the ship.
46
46
 
47
47
  - **Count any repeated failing command.** Red → green on a test *is* the same command failing
48
48
  repeatedly; the detector would fire on ordinary work and become wallpaper.
49
- - **Block the fourth attempt.** That is a new gate on the ship path (ADR 0162), and a wrong
49
+ - **Block the fourth attempt.** That is a new gate on the ship path with no escape hatch the
50
+ stuck session can use (the admissibility test ADR 0241 draws from ADR 0162), and a wrong
50
51
  signature match would strand a builder behind the tool meant to unstick them.
51
52
  - **Server-side detection.** The server sees refused claims but not guard blocks or local red
52
53
  runs, and cannot speak into the session at the moment of the retry.
@@ -2,13 +2,13 @@
2
2
 
3
3
  - **Status:** Accepted (2026-09-23)
4
4
  - **Task:** [task 1004203](https://cloudbongos.com/builders#/task/1004203) (goal 1000095 — UI quality)
5
- - **Relates to:** [ADR 0162](0162-review-gates-retired-rank-consistent-ci-only.md) (no new gates), [ADR 0197](0197-the-ui-design-module.md) (the kit), [ADR 0223](0223-the-detector-is-two-tiers-in-the-kit.md) (floors and tells), [ADR 0241](0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md) (the artist gate holds the deploy)
5
+ - **Relates to:** [ADR 0162](0162-review-gates-retired-rank-consistent-ci-only.md) (review gates retired: no human approval in the land path), [ADR 0197](0197-the-ui-design-module.md) (the kit), [ADR 0223](0223-the-detector-is-two-tiers-in-the-kit.md) (floors and tells), [ADR 0241](0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md) (the artist gate holds the deploy)
6
6
 
7
7
  ## Context
8
8
 
9
9
  Nothing in the pipeline renders a page. The grader reads the diff, so a layout bug that looks right in code ships unseen. Two did in September 2026: goal titles clipped to a character count still ran past their cards, on the goal forest ([task 1003480](https://cloudbongos.com/builders#/task/1003480)) and on the goal map ([task 1004204](https://cloudbongos.com/builders#/task/1004204)); and a second child in a `#view-manage .mSec` card would have landed on the first, because every `.mSec > div` takes the same grid cell (caught by hand in [task 1003078](https://cloudbongos.com/builders#/task/1003078)). Both were found only by opening a browser. The owner's rule since then is that every page change gets a before/after review by a person, and the deploy holds on an open artist review (ADR 0241). This check is the machine half of that: something that looks at the rendered page before a person has to.
10
10
 
11
- The task left open where the check should live: an advisory row in `/ship-check`, a grader worker whose findings ride the panel, or both. It had to stay a signal (ADR 0162).
11
+ The task left open where the check should live: an advisory row in `/ship-check`, a grader worker whose findings ride the panel, or both. It had to stay a signal: the task asked for an advisory check, and the human look at a page change is the artist review that holds the deploy (ADR 0241). *Citation note (task 1004280): this sentence used to cite ADR 0162 for that, which 0162 does not say. 0162 retired gates that need a particular person's approval and keeps automatic gates that fail closed; why this check is not one is decisions 3 and 4 below.*
12
12
 
13
13
  ## Decision
14
14
 
@@ -0,0 +1,84 @@
1
+ # ADR 0339 — A project's owner upgrades it, and a hosted project hands off to the hub's door
2
+
3
+ - **Status:** Accepted
4
+ - **Date:** 2026-09-26
5
+ - **Task:** [task 1004174](https://cloudbongos.com/builders#/task/1004174) (goal 1000090 — BONGOS-V2)
6
+ - **Deciders:** the owner, Lars. On 2026-09-22 he said "every project has the option to upgrade its version of bongos including bongos itself", and that the control belongs in that project's own hall. On 2026-09-26 he ruled that **only a project's owner upgrades it**, adding that for him "owners and archons are the same", because he owns the platform.
7
+ - **Amends:** [ADR 0293](0293-the-owners-deploy-door-is-an-intent-the-control-plane-drains.md) D5 ("Archon-gated server-side"). The deploy door gains a second audience, as stated below. D1 (the web tier only asks, the control plane acts), D3, D4 and D6 are unchanged.
8
+ - **Related:** [ADR 0328](0328-a-hosted-projects-authority-is-its-membership-row-not-its-owner-column.md) (the owner-OR-operator gate shape this reuses), [ADR 0210](0210-scouting-graduates-on-an-instance-asserted-owner-claim.md) (the `alsoAdmits` page-gate door this reuses), [ADR 0246](0246-catalog-membership-is-a-projection-not-a-federation-side-effect.md) (a hosted project's hub client id is its slug), [ADR 0043](0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md) / 0111 §2 (the web tier gains no capability)
9
+
10
+ ## Context
11
+
12
+ The deploy door is `POST` and `GET /provisioning/instances/:id/core-upgrade` on the control plane, fronted by the hub's `/deploy` page. It was gated on `core.pin.move` alone. That atom has an archon floor and `system:true`: the catalog forbids granting it to metic, and no custom rank can hold it.
13
+
14
+ That produced two dead ends.
15
+
16
+ 1. **The owner of a hosted project below Archon could see they were behind and could never act on it.** On a hosted project the banner (task 1004045) prints "Core X is available" with no action, because that project does not run the provisioning module. The door lives on the hub, and on the hub the owner (erikwils is Xenos; willHermesLines is Metic) is refused by the atom.
17
+ 2. **A hosted project's own hall had a "Deploy" link that opened a page with nothing behind it.** The page's calls are provisioning routes, and a tenant has none.
18
+
19
+ Task 1004174's own text settled where the act must happen. It stays on the control plane, because ADR 0293 D1 and ADR 0043 both turn on the web tier never gaining a shell. The hosted project hands off rather than acting.
20
+
21
+ ## Decision
22
+
23
+ ### D1 — The owner of a hosted project may move that project's core, from the hub
24
+
25
+ The two per-project routes (the preview `GET` and the move `POST`) are gated by an OR, implemented in `modules/provisioning/core-upgrade-authz.js`:
26
+
27
+ 1. **Ownership is asked first.** The caller is admitted if the row's `owner_builder_id` is the caller and the row is a tenant shape (`co-tenant` or `cloud-host`).
28
+ 2. **Every other caller meets the unmodified `requirePermission('core.pin.move')`** and its unchanged refusal. That includes callers who are not the owner, a missing row, an unparseable id, and a database error during the ownership read.
29
+
30
+ This is the shape ADR 0328 Decision 2 and ADR 0210's `requireScoutingReader` already use. The atom is still spelled in each route's middleware list, where `route-rank-check.js` and `gen-api-docs.js` read it. The consequences:
31
+
32
+ - A non-owner below Archon gets exactly the 403 they always got, and learns nothing about whether the id exists.
33
+ - The Archon path is untouched.
34
+ - The widening can only admit.
35
+
36
+ ### D2 — The platform's own row stays on the atom
37
+
38
+ `OWNER_SHAPES` excludes `control-plane`. Moving the platform changes the code that drives every hosted project, so only a holder of `core.pin.move` may do it. `GET /provisioning/control-plane` keeps the atom alone.
39
+
40
+ For Lars this is no restriction: he is both the platform's owner and an Archon, which is his ruling that the two are the same.
41
+
42
+ ### D3 — A hosted project hands its owner to the hub, and gains nothing
43
+
44
+ `GET /api/bongos/core-update` (core) now also answers `deploy_url`: `<hub origin>/deploy?project=<slug>`.
45
+
46
+ - It is built from the two values provisioning already writes into a federated project's `web.env`: the hub origin and the hub client id, which is the slug (ADR 0246).
47
+ - It is null on an instance with no hub, such as a self-hosted instance or the hub itself.
48
+ - The client secret is never read for it.
49
+
50
+ The hall uses that address in two places, and only where it has no door of its own (the provisioning module is off):
51
+
52
+ - **The banner's "Upgrade" links there.**
53
+ - **The rail's "Deploy" item** is dropped where the provisioning module is off, and re-pointed at that address once it is known.
54
+
55
+ The hub's `/deploy?project=<slug>` page (task 1004171) was built as exactly this seam.
56
+
57
+ The hub's `/deploy` page shell is gated by `requireCorePinPage`. It now declares `alsoAdmits: 'isProjectOwner'`, the door `/people` uses: it admits the shell for an owner of any live project. Which project the owner may read or move is still the routes' decision, made per request against that project's row.
58
+
59
+ ### D4 — The actor says which door was used, and still names the builder
60
+
61
+ An owner's move is recorded as `api:owner:<id>`, and an Archon's as `api:archon:<id>`. The runner's `requestedByOf` accepts both and passes the id through as `--requested-by`, so the `core_upgrades` ledger attributes the move either way (ADR 0293 D5).
62
+
63
+ ## Rejected
64
+
65
+ - **A tenant-side API that authenticates back to the control plane and performs the upgrade.** It is rejected in writing, as the task asked. It would be a new cross-instance authentication surface on the most privileged action in the system, and it would give a tenant's web process a path to the act ADR 0293 D1 keeps on the control plane. The hand-off costs one navigation and adds no new capability anywhere.
66
+ - **Granting `core.pin.move` to owners.** The atom is `system:true` precisely so it cannot be handed down, and "may move core pins" is not "may move this one project's pin". Ownership is a separate axis, as ADR 0151 §3 keeps it.
67
+ - **Letting an owner move the platform row.** The platform row is not the owner's project in the sense the ruling means. Its only owner is the Archon who holds the atom anyway.
68
+ - **Asking the platform-identity membership ledger instead of `owner_builder_id`.** ADR 0328 Phase 2 (task 1004161) will widen project authority to members through that port. The ruling here is "owner", which the row already records, so this ADR takes the column and leaves the widening to that task.
69
+
70
+ ## Consequences
71
+
72
+ - **erikwils (hoodify-finance) and willHermesLines (mercury) can upgrade their own projects.** They open `https://cloudbongos.com/deploy?project=<slug>` on the hub. The hub half works as soon as the platform runs a core carrying this ADR. The in-hall link reaches a hosted project only once that project runs such a core, because the banner ships inside the core. Until then the owner reaches the door by its address.
73
+ - **An owner's upgrade restarts their own site.** The runner's health gate and auto-rollback apply unchanged.
74
+ - **ADR 0328 Phase 1 must absorb this gate, not route around it.** When task 1004159 lands `requireProjectOwner` for the `/provisioning/instances/:id...` routes, the two core-upgrade routes should move onto it with the atom as their override. That keeps the same owner-OR-atom answer, and the platform row stays atom-only. Until then `requireOwnProjectOrPin` is the only owner gate on them. Task 1004160's `project_scope_missing` check lists `/provisioning/instances/:id` as a prefix, so it would flag these two routes. That is correct: it is the reminder to fold them in, not a sign that they are unguarded.
75
+ - **The page shell admits any live project owner.** An owner without the atom who opens `/deploy` without `?project=` sees the sealed state, because `/provisioning/control-plane` refuses them. The page is a shell, and every datum behind it is gated per request.
76
+ - **Proof:** `tests/core_upgrade_owner_door.mjs` covers each case:
77
+ - the owner below Archon, admitted;
78
+ - the Archon on another's project, admitted by the atom;
79
+ - the non-owner below Archon, refused byte-for-byte as before, whether the id exists or not;
80
+ - the platform row, owner or not, refused without the atom;
81
+ - a failing ownership read, which falls to the atom;
82
+ - the actor labels, and the runner's attribution parse;
83
+ - the page shell admitting an owner and still refusing a stranger;
84
+ - the `deploy_url`, and the hall's banner and rail on a hosted project and on the platform.
@@ -420,7 +420,7 @@ This keeps the decision history honest and traceable.
420
420
  | 0326 | [**An upgrade may ADD a deny rule to a file the instance owns** ([task 1004156](https://cloudbongos.com/builders#/task/1004156), goal 1000090 — BONGOS-V2). Task 1003966 gave the instance starter its deny rules — no reading `.env` files, keys or session tokens, no shelving with the repo-global stash — and they reached **nobody who was already here**. The starter is written only when `.claude/settings.json` is ABSENT, and `bongos upgrade` materializes from the published package, which excludes that file on purpose; so an upgrade always takes the starter branch, always finds a settings.json already there, and left it alone. Every instance on the allow-only starter from task 1003962 — the live one included — kept it forever. That was deliberate ("rewriting a file the instance owns is the line task 1003962 drew"), and the recorded remedy was manual: re-run `bongos init`, or delete the file. **A manual remedy for a security gap protects the instances whose owners read release notes, which is not the set that needs protecting.** The owner's call: ADD the missing deny rules and NAME each one in the run output. The argument is that a deny rule is **monotonic in the safe direction** — an allow rule widens what the local agent may do, a deny rule can only narrow, so appending one cannot make any instance less safe than it was a moment earlier. That is what makes this the one merge worth doing without asking per-instance, and it extends [ADR 0121](0121-greenfield-vs-brownfield-onboarding-adopt-existing-repo.md) §2's already-licensed "merge OR leave-and-report" from `--adopt` to every materialize. "Never clobber" survives in the sense that was load-bearing: **nothing the instance wrote is removed, reordered or rewritten** — their allow rules, own denies, `ask` list, hooks, env and every key outside `permissions` come back exactly as they were, with new rules appended after. Deliberately NOT done: **no provenance**, so an owner who deletes a rule we add gets it back next upgrade and (deny beating allow in Claude Code's order) cannot override it with an allow — rejected as speculative machinery for a case nobody has hit, revisit on a real complaint; a file that will not parse, or whose `permissions`/`permissions.deny` is not the expected shape, is left BYTE-FOR-BYTE alone and reported, the posture task 1004023 took for the core's own file, because a half-understood rewrite of a permissions file is worse than the gap; and **the allow half is never topped up** — the safety argument is directionality and does not survive being applied to a rule that grants. `materializeClaude` gains `summary.settingsDenyAdded` + `settingsDenyUncheckable`, both printed through `log()` since `upgrade.js` does not iterate `summary.notes`. The test asserting an instance's settings.json comes back byte-identical is superseded by a strictly stronger one — every rule still there, unmoved, deny-only growth — the weaker spelling being what froze the gap in place. **AMENDED 2026-09-22 by [task 1004158](https://cloudbongos.com/builders#/task/1004158): the two shelf rules are now starter-only and the top-up asks for the CREDENTIAL half alone.** 0326 itself named this exclusion as the remedy if the rule ever bit; the owner took it ahead of that, because the monotonicity argument licenses a rule about reading a secret and does not reach one that guards a workflow — with no provenance and deny beating allow, an instance that loses the shelf cannot give it back. A fresh instance still gets both. The top-up only ever appends, so this is **forward-looking only**: an instance that already took them keeps them, live included.](0326-an-upgrade-may-add-a-deny-rule-to-a-file-the-instance-owns.md) | security / instance onboarding |
421
421
  | 0327 | [**A `cloud-host` runs on the owner's own account, and the key is borrowed, not stored** ([task 1004175](https://cloudbongos.com/builders#/task/1004175), goal 1000106 — BONGOS-V2). [ADR 0323](0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md) moved a `cloud-host` to Render to stop being a landlord but left **whose account** unexamined; [ADR 0325](0325-the-managed-fleet-control-plane-is-the-hub-we-already-run.md) §4 then ruled the platform token is ours and "never per-customer". Standing it up disproved the premise: a Render API key is issued against a **user** and carries their rights in **every workspace they belong to** — the owner's reached three, one of them another builder's, holding their live production. The same check found `mercury` **already on Render**, run by its own owner, which 0323 would have migrated OFF a correct setup onto our bill. The remaining landlord relationship was never the servers; it was the **account**. **Decisions:** (1) each project brings its own platform key — we hold no billing relationship for anyone's compute, and a leaked key's blast radius is exactly its own owner's account, a property that comes from the decision and not from cryptography; (2) the key is **borrowed for standup and cleared**, never stored, because nothing afterwards needs it — Render auto-deploys from Git, the deploy door ([ADR 0293](0293-the-owners-deploy-door-is-an-intent-the-control-plane-drains.md)) is a push, liveness is the [ADR 0252](0252-the-self-hosted-visibility-report-is-a-pull-and-auto-appear-buys-a-retraction-lever.md) pull — so it rides the queue as a short-lived value encrypted with the existing `secret-box.js`, the [ADR 0133](0133-github-app-manifest-onboarding-signin.md) handshake's shape; (3) we **never destroy a customer's infrastructure** — teardown disconnects, the owner deletes their own services, a safety property first and the reason (2) works; (4) **two shapes, not three** (`dedicated-cloud-host` removed — on someone's own account a bigger plan is a setting, finishing 0323 §1's own "plan size, not a second code path"), and the **free tier is free**, 0323 §7's cap retired with its premise. **Deliberately NOT built, because nothing is stored:** an asymmetric sealing module, per-row credential binding, rotation/revocation, and the separate-unix-user precondition — each designed in full, then deleted when the retention question was asked instead of assumed. **Stated honestly:** encrypting the in-transit value protects it from a database dump, NOT from a compromised web tier, because the owner's key arrives there in plaintext first; that defence was never available at any retention length. **§5 records a measurement, fixed elsewhere:** [ADR 0111](0111-instance-hosting-provisioning-module.md) §2's boundary is enforced by the CODE, not the kernel — `cloudbongos.service` runs `User=lars` with no sandboxing and `/etc/cloudbongos/box.env` is mode 600 owned by `lars`, so the web tier CAN read the DigitalOcean, Cloudflare and platform tokens; 0325 §4's "root-only" is not what is on the box. (A second finding in the same section — a "mode 777" key file — was **void and is corrected in place**: the path is a symlink, whose mode display is meaningless, to a 600 target. It was inferred from an `ls` rather than an attempted read.) **Resolved 2026-09-23 by [task 1004176](https://cloudbongos.com/builders#/task/1004176)**, and more narrowly than §5 proposed — not the unix split (which would mean relocating the checkout, config dir, `web.env` and the secret-box master key the web tier legitimately needs), but a systemd drop-in with `InaccessiblePaths`, verified as the service user; proving it as **root** reports "no effect" against a working config, because `CAP_DAC_OVERRIDE` bypasses the mode-000 over-mount. It is emphatically NOT a precondition here — that is the point of storing nothing, and had we sealed per-project keys with a private key in that same file the two would have had to land together. **Consequences:** the migration shrinks to an **adoption**; the cost ledger's question inverts (our spend on a `cloud-host` is ~\$0 and correct — what an owner is worth telling is THEIR Render cost, a different column, [task 1004148](https://cloudbongos.com/builders#/task/1004148)); and metering loses its forcing function, cost *visibility* surviving as a feature while billing for hosting does not ([task 1001949](https://cloudbongos.com/builders#/task/1001949)). Rejected: storing each key sealed with a key pair (three days plus a live-site infra change, to keep a value needed once); one platform account holding every project (0325 §4 as written — keeps us the billing landlord and makes one credential reach every customer); generating a Blueprint and having the owner click once in Render (simplest, but the click is the exact friction `cloud-host` exists to remove); and keeping the key in the browser to call Render directly (standup is a runner job with retries and an audit trail, not a page that stays open).](0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md) | hosting / provisioning / credentials |
422
422
  | 0328 | [**A hosted project's authority is its membership row, not its owner column** ([task 1003882](https://cloudbongos.com/builders#/task/1003882), goal 1000094 — BONGOS-V2). The owner's decision (2026-09-13): every API action must name its project, and a non-member may only apply. Two "project" concepts exist and answer different questions: `provisioning_instances` (the hub's fleet registry, single `owner_builder_id`, [ADR 0325](0325-the-managed-fleet-control-plane-is-the-hub-we-already-run.md)) and `<redacted>` (`client_id, github_id, membership_kind ∈ {owner,member,visitor,pending}`, already correct, already backing `GET /my-projects`). The composition this needs already exists once, in the opposite direction: `provisioning.projectOwnedBy` is a published port (task 1003580) that `platform-identity/invite-authz.js` already consumes via `resolveOptional` to ask "does this builder own this project" without importing provisioning (ADR 0083). **The gap is that the module which owns the fact does not use it on itself, and has grown three separate local copies of it instead of one** — `instanceAccessDenied` (6 routes), `resolveOwnInstance` (4 routes) and `disconnect.js`'s own inline copy (1 route) all implement "owner OR archon", while 3 routes (`teardown`, `domain`, `github-app`) implement only "owner" with no archon override at all — an existing inconsistency between sibling routes in the same module, not just duplication, and none of the three implementations is visible to `route-rank-check.js`. Decision: (1) project scope stays the existing path segment, resolved through `slug` (the `client_id = slug` join key [ADR 0246](0246-catalog-membership-is-a-projection-not-a-federation-side-effect.md) already establishes); (2) Phase 1 extracts one local `requireProjectOwner` helper replacing all three implementations — a behavior-preserving swap for 11 routes and a stated, intentional archon-override widening for the 3 that lacked it — no new port needed since provisioning already holds the column; Phase 2 adds a new port `platform-identity.projectMembershipKind` and widens the same helper to admit `owner` OR `member`, falling back to the Phase 1 column check when platform-identity isn't installed (ADR 0083's degrade-gracefully contract) — a NEW parallel axis, not a fifth branch on `requirePermission`, mirroring why the ownership axis ([ADR 0151](0151-governance-permissions-as-atom-ranks-as-roles.md) §3, R100) is kept separate; (3) a caller with NO membership row still gets 404 `instance_not_found` (existence-hidden, unchanged), but a `pending`/`visitor` row gets 403 `project_membership_required` naming `POST /my-projects/join` (ADR 0141/0201/0247's existing apply flow) — the "apply to join it" the owner's decision names, routed into the flow that already ships rather than a second one; (4) `route-rank-check.js` gains a `project_scope_missing` finding (blocker on write, concerns on read) for any `/provisioning/instances/:id...` route missing the gate, the same structural read it already gives `requirePermission`; (5) migration order is the 14 evidenced routes only — the task's own "96 write routes" figure is the whole app's census, and the other ~82 act on a single-tenant instance's own Bongos board, which [ADR 0100](0100-two-repo-instance-model.md) §3 already isolates one-database-per-repo and has no project dimension to name. Filed: [task 1004159](https://cloudbongos.com/builders#/task/1004159) (the helper) · [task 1004160](https://cloudbongos.com/builders#/task/1004160) (route-rank-check.js, depends on 1004159) · [task 1004161](https://cloudbongos.com/builders#/task/1004161) (the membership port, depends on 1004159) · [task 1004162](https://cloudbongos.com/builders#/task/1004162) (the 403/404 split, depends on 1004161). Rejected: a second `provisioning_instance_members` table (the membership table already exists and joins on the same key — a second one repeats the mistake ADR 0325 already refused); folding project authority into `requirePermission` as a fifth axis (same reasoning R100 already applied to `.own` keys); migrating all 96 write routes (re-litigates ADR 0100's single-tenant model, not this decision); dropping 404 existence-hiding entirely (a total stranger should learn nothing, exactly as `invite-authz.js` already guarantees).](0328-a-hosted-projects-authority-is-its-membership-row-not-its-owner-column.md) | security / authorization / provisioning |
423
- | 0329 | [**The rendered fit check is a kit floor and an advisory ship-check row, not a grader worker** ([task 1004203](https://cloudbongos.com/builders#/task/1004203), goal 1000095 — UI quality). Nothing in the pipeline rendered a page, so titles clipped by character count ran past their cards (task 1003480, task 1004204) and a second `.mSec` child would have landed on the first (task 1003078), each found only in a browser. Two new kit floors, `text-fit` (text measured word by word past its box; SVG text past its card rect) and `sibling-overlap` (in-flow, block-level, text-bearing siblings), run in every `render.js` render; `scripts/gds/render-check.js` renders the pages a branch changed at 1280 + 390 and `/ship-check` shows them as an ADVISORY row that never flips its exit (ADR 0162). Not a grader worker: that would put a browser, fixtures and a harness on the grader host for signal the builder already saw; the artist gate (ADR 0241) stays the human look before deploy.](0329-the-rendered-fit-check-is-a-kit-floor-and-an-advisory-ship-check-row.md) | ui-design / kit + ship-check |
423
+ | 0329 | [**The rendered fit check is a kit floor and an advisory ship-check row, not a grader worker** ([task 1004203](https://cloudbongos.com/builders#/task/1004203), goal 1000095 — UI quality). Nothing in the pipeline rendered a page, so titles clipped by character count ran past their cards (task 1003480, task 1004204) and a second `.mSec` child would have landed on the first (task 1003078), each found only in a browser. Two new kit floors, `text-fit` (text measured word by word past its box; SVG text past its card rect) and `sibling-overlap` (in-flow, block-level, text-bearing siblings), run in every `render.js` render; `scripts/gds/render-check.js` renders the pages a branch changed at 1280 + 390 and `/ship-check` shows them as an ADVISORY row that never flips its exit. Not a grader worker: that would put a browser, fixtures and a harness on the grader host for signal the builder already saw; the artist gate (ADR 0241) stays the human look before deploy.](0329-the-rendered-fit-check-is-a-kit-floor-and-an-advisory-ship-check-row.md) | ui-design / kit + ship-check |
424
424
  | 0330 | [**An ask is answered where it was asked** ([task 1004216](https://cloudbongos.com/builders#/task/1004216), goal 1000095 — BONGOS-V2), amending [ADR 0187](0187-collab-four-decisions.md) §4a. `lifecycle_004` let a builder ASK for help and let the **asker** mark their own row answered; the person being **asked** had no write at all, so the answer happened somewhere Bongos cannot see and the row sat open until its author remembered it — six asks addressed to the owner had been open between four and thirteen days on 2026-09-23, every one of them answerable. That was not an oversight: §4a deferred conversation to the Agora and a test named *"there is deliberately NO reply thread"* enforced it. **The deferral lost its addressee** — task 1002946 is ABANDONED and goal 1000064 ARCHIVED (verified against the live DB), so the rule had stopped meaning "wait for the Agora" and started meaning "an ask stays unanswerable forever to keep a promise to something that does not exist". **Decisions:** (1) an ask carries a thread of replies, APPEND-ONLY in the SCHEMA rather than by convention — no `updated_at`, `edited_by`, `deleted_at` or `parent_reply_id`, no reactions, no mentions, and a trigger that refuses UPDATE and DELETE outright, because a thing you can edit, nest, react to and address at people is a chat and that is what §4a declined; any builder may reply, since the board read is already project-wide and the good case is somebody you did not ask knowing the answer; (2) the **ADDRESSEE** may close an ask (`answered`), widening an asker-only UPDATE that was coherent only while nothing could be said on the row — but `withdrawn` does NOT widen, since "I no longer need this" is the asker's sentence alone and widening it would be a way to make an ask vanish unanswered; `settled_by` records which of the two closed it, and ADR 0187 §3's consent posture is untouched — closing ends a question, it does not accept work; (3) `/collab-review` posts the reply and **leaves the ask open** (owner decision), since an answer the reader has not read is exactly what an unattended run must halt on. **Consequences:** a reply cannot be taken back, by design; the FK **RESTRICTS rather than cascades** — the first draft cascaded, which reads right and was wrong, because a cascade is a plain DELETE the trigger refuses, so deleting an answered ask failed outright (caught by applying the migration to a real Postgres, not by reading it); the old test was REPLACED, not deleted, so the record that the restraint was deliberate survives; and replies inherit the pull-not-push posture, so nothing still tells an asker they were answered. Rejected: a single `answer` column (cheapest and unambiguously not a chat, but a follow-up would have nowhere to go except a second ask, splitting one conversation across two rows); waiting for the Agora (abandoned — waiting means forever); letting anyone close any ask (an open question is somebody's, and a stranger ending it should not be one click away).](0330-an-ask-is-answered-where-it-was-asked.md) | collab / lifecycle / schema |
425
425
  | 0331 | [**The sky is the ideator's home, and the project sky takes the goal map's page** ([task 1004229](https://cloudbongos.com/builders#/task/1004229), goal 1000095 — BONGOS-V2), amending [ADR 0227](0227-a-task-network-map-is-refused-the-goal-graph-is-the-map.md) and decision 11 of `docs/specs/<redacted>.md`. The owner reviewed an interactive prototype of the ideator sky and the project sky over five rounds on 2026-09-24 and adopted it AS IS — copy, layout, colours, tour text, toggles, interactions — which contradicts two things already written down, so they are rewritten rather than leaving the build graded against a contract nobody means any more. **Decisions:** (1) the sky is the ideator's HOME surface — it replaces *Your thinking*, and the project sky replaces the goal map page — so "never load-bearing" and "arrives last" are DROPPED as a sequencing hedge from before there was a design to look at; the rule that survives at full force is the accessibility half, that the sky is never the ONLY path to anything it shows (the list drawer — the room — is one key away, and every drawn thing is a row in it); (2) ADR 0227 is AMENDED, not reversed — its measured refusal of a whole-corpus task map stands, and the project sky is §1's own prescription rather than its refusal: the goal graph is the entry point, tasks are drawn only INSIDE one goal past a zoom threshold (the per-goal drill-down §10.5 measured readable at 85.2px pitch worst case), below that they are dust density on the rings with NO identities, and the goal map's table survives as the sky's ledger drawer (§6's table equivalent kept); the nav slot the goal map was deliberately denied is GRANTED to the project sky, under Goals (owner, 2026-09-24), so part 4 changes the page and the test asserting no slot together; (3) the five states and the two channels are UNTOUCHED — criterion `ideator-sky-tells-the-truth` names a model, not a picture, and this amendment changes the picture's standing only; (4) the two HEURISTICS in the prototype — an idea's origin guessed from its text, and "born of a thought" found by scanning text for idea ids — must not ship as ledger facts: parts 3 and 4 hide both layers, part 5 replaces them with a `filed_via` column and a real born-of read. **Consequences:** `docs/design/mocks/sky/sky.template.html` is the PIXEL CONTRACT for parts 2–5, and every deviation a gate or the file ratchet forces is listed in that part's ship notes; no snapshot JSON or built page is ever committed (it carries real task and idea titles and this repo is public); ADR 0264's deleted goal "Living project sky" is superseded by this work under Working area 3. Rejected: shipping the sky as an extra and keeping "never load-bearing" (makes the adopted home surface unroutable); reversing ADR 0227 (its refusal is measured and still correct — the sky needs its drill-down shape, not its repeal); shipping the heuristics behind a disclaimer (a labelled guess is still a guess in a picture whose promise is that it cannot be faked).](0331-the-sky-is-the-ideators-home.md) | design / ideator / sky |
426
426
  | 0332 | [**Offboarding scrubs stored secrets: an event, not a direct write, and reactivation restores neither** (spike [task 1003338](https://cloudbongos.com/builders#/task/1003338), goal 1000110 — BONGOS-V2). `deactivateBuilder` revokes sessions but never scrubs the two third-party credentials the platform holds on a departing builder's behalf: the `github_oauth_tokens` row and the per-builder Gemini BYOK key (`gemini_key_enc`/`last4`/`set_at`, module-owned by `art-pipeline` per [ADR 0102](0102-finish-bounding-the-kernel.md)). **Decisions:** (1) `deleteGithubToken` is called synchronously inside `deactivateBuilder`'s existing transaction — the table is core's own; (2) a new best-effort event, `builder.secrets-scrub-needed`, is emitted post-commit (mirroring the existing `builder.box-reconcile-needed` pattern) so `art-pipeline` — or any future module storing a builder-keyed secret — can clear its own store without core reaching across the module boundary ADR 0102 drew; (3) reactivation restores NEITHER secret, symmetric with rank ("not auto-restored… re-promotes deliberately") — a returning builder re-enters their own key and re-derives a fresh GitHub token. **Rejected:** a direct `UPDATE` nulling the Gemini-key columns from core (re-opens the ADR 0102 leak); deleting the `builders` row to get `ON DELETE CASCADE` for free (breaks the explicit lossless design of graceful exit); a cron sweep (a new mechanism where an event already fits); auto-restoring the key on reactivation (a stored credential is not earned state). The core-side change (GitHub-token delete, event emission, contract doc, tests) shipped with this task; follow-up [task 1004239](https://cloudbongos.com/builders#/task/1004239) is narrowed to the `art-pipeline` module-side listener plus a periodic reconcile backstop.](0332-offboarding-secret-scrub-semantics.md) | security / privacy / modules |
@@ -430,3 +430,4 @@ This keeps the decision history honest and traceable.
430
430
  | 0336 | [**A guild is a hub-side group whose members each consent twice, and its collective record is only ever the sum of what those members already show** ([task 1002299](https://cloudbongos.com/builders#/task/1002299), goal 1000110 — the R23 design spike for goal 1000045's C7/C8/C9). No platform guild existed (every "guild" in the tree is Discord's). Decisions: (1) three hub tables in `platform-identity`, in their own `guilds.js` (the 1,500-line ratchet), distinct from instance-local `goal_members`: guilds (slug, name, `visibility ∈ public|unlisted` with **no default**), members (`membership_kind ∈ owner|member`, never `role` per ADR 0174; a partial unique index holds exactly one owner; a per-member `shown_publicly`), and requests; (2) two consents to join, no self-join, through ADR 0202's connections lifecycle verbatim (silent decline, 90-day swallowed re-ask, 30-day read-time expiry); `accountActiveSql` floors the feature; an **unlisted guild 404s to everyone on the public read, members included**, and takes invites only, because a slug is guessable; (3) the public roster is `shown_publicly AND accountVisibleSql`, and widening a guild to public changes no member's flag but the flipping owner's, so consent given to a small audience is never reused for a large one; (4) **the privacy resolution — NOT ratified; superseded by [ADR 0337](0337-the-hub-account-is-the-person-a-hall-account-is-their-seat-in-one-project.md) D4.4:** it proposed that a member is counted iff shown AND `rollupVisibleSql`, read-time, `no-store`, never stored, and N and M are both lengths of lists the reader can see. The guild record is therefore always a sum of numbers already on members' own public profiles, so nothing can be subtracted out. All-or-nothing is rejected (one member blanks everyone and hiding becomes costly), as is per-member consent to count hidden numbers (it breaks "hide_stats beats everything"). The trade-off: guilds of privacy-minded builders look smaller and rank lower. A union count of all members' projects is refused because it reveals shared private projects; (5) the namespace is `/g/<slug>`, `g` is reserved in the SEO spec's project-slug list, the slug is fixed in V1, and `tests/guild_namespace_reserved.mjs` pins the two documents together; (6) a guild engages a project only through the individual door: a guild application is each member's own vouched application, labelled through ADR 0260's live applicant-profile read (no new instance field); inviting a guild fans out ordinary individual invites to its public roster at that moment, **never a guild-level pre-approval** (that would let a guild owner decide admission to someone else's project); (7) the map ranks public guilds by size and counted works shipped, read-time, with a slug tiebreak, and R29 narrows to the visual. Scope changes for R24–R37 are tabled in the ADR, including R28's dependency on the abandoned R11 and R29/R31's archived orb goals.](0336-a-guilds-record-is-the-sum-of-what-its-members-already-show.md) | platform identity / privacy / social graph |
431
431
  | 0337 | [**The hub account is the person, a hall account is their seat in one project, so anything that spans projects lives on the hub** ([task 1004255](https://cloudbongos.com/builders#/task/1004255), goal 1000110 — the owner's hub/hall re-scope, 2026-09-24/25). Extends [ADR 0141](<redacted>.md) from identity to placement: (1) a surface about the person across projects, or about more than one project, goes on the HUB (`public-landing`, `platform-identity`): people directory, community, guilds, connections, recruiting, platform-account settings; one about this project goes in the HALL (`hall-ui`); "Builders you may know" is on both; `status-ui` is the status dashboard, not the hall; (2) no authority moves, and hub actions still land as the project's own rows; (3) hall `/people` retires, ADR 0334's recruiting directory moves unchanged to the hub community page, `/roster` is the in-project directory; (4) owner calls: the hidden-builder recruiter panel is recruiter-only while public search is unchanged (amends community-tab spec D8); platform-account settings get a new hub page; **guild totals count hidden and private members by default, with a notice on the join card and a per-guild off switch (`counted_in_totals`), rounded to a 1–2–5 band, only once a guild has at least 3 of them, refreshed weekly; size stays the public roster length** (supersedes ADR 0336 D5; relaxes the privacy spec's "`hide_stats` beats everything" for guild totals only, for a member who was told), with the fake-account padding limit stated and accepted.](0337-the-hub-account-is-the-person-a-hall-account-is-their-seat-in-one-project.md) | platform identity / placement / privacy |
432
432
  | 0338 | [**Modules travel through a Bongos-hosted store, and an acquired module keeps working forever** ([task 1003782](https://cloudbongos.com/builders#/task/1003782), goal 1000091 — working area 5, owner Will). A module had no transport of its own: it reached an instance only inside the core (ADR 0107 §6) or in the instance's repo. Weighed npm packages, a Bongos-hosted store and git refs against the four things area 5's criteria require — entitlement before install, a per-version score, a tester channel, and a delist that stops distribution. npm and git refs fail entitlement and delist by construction. **D1:** Cloud Bongos hosts the store; a module version is a tarball + hash manifest in the same format `package-core.js` already emits (same `isPublishable()` gate), registry rows on the control-plane DB, artifacts in the already-paid DO Spaces bucket, served by the control plane only after an entitlement + channel check; free modules take the same path at price zero; install/update verify hashes on arrival; modules' own npm deps still travel via npm (ADR 0138). No new fixed cost. **D2 (owner):** delisting, author withdrawal or deletion removes a module from the catalog and stops new acquisitions and updates, but **an acquired module keeps working forever** — the store never disables or removes an installed copy and a delist never revokes an entitlement; the holder is told no more updates will come. Security issues are not a special case; a remote kill switch would need a superseding ADR. Rejected: npm, git refs, remote disable on delist.](0338-modules-travel-through-a-bongos-hosted-store.md) | modules / distribution / economy |
433
+ | 0339 | [**A project's owner upgrades it, and a hosted project hands off to the hub's door** ([task 1004174](https://cloudbongos.com/builders#/task/1004174), goal 1000090 — the owner's ruling of 2026-09-26 that only a project's owner upgrades it). Amends ADR 0293 D5. **D1:** the deploy door's preview and move routes admit the OWNER of a tenant row (`co-tenant`/`cloud-host`) for that row, asked first; every other caller meets the unmodified `core.pin.move` gate, so a non-owner's refusal is unchanged whether or not the id exists and the widening can only admit. **D2:** the platform's own row stays atom-only. **D3:** `GET /core-update` names `<hub>/deploy?project=<slug>` on a federated project, and the hosted hall's banner and rail hand off there (the dead local Deploy item is gone); the hub's `/deploy` shell admits a project owner through `alsoAdmits: 'isProjectOwner'`. **D4:** the intent's actor records the door (`api:owner:<id>` / `api:archon:<id>`), both attributed on the `core_upgrades` ledger. Rejected: a tenant-side API that performs the upgrade, granting the system atom to owners, owners moving the platform row. ADR 0328 Phase 1 (task 1004159) should fold this gate into `requireProjectOwner`.](0339-a-projects-owner-upgrades-it-and-a-hosted-project-hands-off-to-the-hub-door.md) | provisioning / deploy door / permissions |
@@ -13915,7 +13915,7 @@
13915
13915
  "provisioning"
13916
13916
  ],
13917
13917
  "summary": "GET /provisioning/control-plane",
13918
- "description": "GET /provisioning/control-plane — \"which row is this platform, and where does it stand?\" The deploy page's first call: the self row is created once by hand at install time, so its id is per-install and nobody's to memorise. Same atom again, so the entrance is no wider than the door it leads to.\n\n**Rank:** `archon` — Archon only (rank and identity management + the escalation keys — the trust boundary).\n\n**Permissions:** `core.pin.move` (all required).",
13918
+ "description": "GET /provisioning/control-plane — \"which row is this platform, and where does it stand?\" The deploy page's first call: the self row is created once by hand at install time, so its id is per-install and nobody's to memorise. The atom ALONE here, with no owner door: this is the platform's own row (ADR 0339).\n\n**Rank:** `archon` — Archon only (rank and identity management + the escalation keys — the trust boundary).\n\n**Permissions:** `core.pin.move` (all required).",
13919
13919
  "x-rank": "archon",
13920
13920
  "x-source": "modules/provisioning/routes/core-upgrade.js",
13921
13921
  "x-permissions": [
@@ -14231,7 +14231,7 @@
14231
14231
  "provisioning"
14232
14232
  ],
14233
14233
  "summary": "GET /provisioning/instances/:id/core-upgrade",
14234
- "description": "GET the same path — step ONE of the two-step (ADR 0293 D4): what a move would do. GATED THE SAME AS THE POST, deliberately. Reading it discloses the instance's exact core version and how far behind it is, which is a vulnerability-window disclosure — precisely the sort of thing ADR 0043 keeps off unauthenticated surfaces. A preview anyone could read would also let an attacker time a request against a known-stale window. The read being no cheaper than the write costs the owner nothing: they hold the atom by construction, because they are the only one who can press the button.\n\n**Rank:** `archon` — Archon only (rank and identity management + the escalation keys — the trust boundary).\n\n**Permissions:** `core.pin.move` (all required).",
14234
+ "description": "GET the same path — step ONE of the two-step (ADR 0293 D4): what a move would do. GATED THE SAME AS THE POST, deliberately. Reading it discloses the instance's exact core version and how far behind it is, which is a vulnerability-window disclosure — precisely the sort of thing ADR 0043 keeps off unauthenticated surfaces. A preview anyone could read would also let an attacker time a request against a known-stale window. The read being no cheaper than the write costs the owner nothing: whoever may press the button may read what it would do — the same two doors as the POST.\n\n**Rank:** `archon` — Archon only (rank and identity management + the escalation keys — the trust boundary).\n\n**Permissions:** `core.pin.move` (all required).",
14235
14235
  "x-rank": "archon",
14236
14236
  "x-source": "modules/provisioning/routes/core-upgrade.js",
14237
14237
  "x-permissions": [
@@ -14277,7 +14277,7 @@
14277
14277
  "provisioning"
14278
14278
  ],
14279
14279
  "summary": "POST /provisioning/instances/:id/core-upgrade",
14280
- "description": "POST /provisioning/instances/:id/core-upgrade — move the core pin. rank: `core.pin.move` — archon floor AND system:true, so the catalog's own invariant forbids granting it to metic and no custom rank can be handed it without a deliberate, visible edit. BV1.R105 / ADR 0151 is why this is an atom and not `requireRank`: tests/government_require_permission.mjs fails the build on a new requireRank in a modules/*/routes gate. Body: { to: \"1.19.757\" } — and requiring it IS the second step of the two-step the owner asked for (ADR 0293 D4). A request that cannot fire without naming its target cannot fire from a stray click, and \"move to whatever is newest\", resolved at drain time, would be a different act from the one previewed: main moves between the preview and the confirm. What this does NOT check: whether the version is PUBLISHED, and whether the channel policy admits it. Both need the registry, and the only tool that reads it (update-channel.js `listAvailableVersions`) shells out synchronously — it would block the event loop for every other request on this process. It also lives in scripts/gds/, which a module may not require (ADR 0083). The runner enforces both, where it is a CLI and both are free. See ../core-upgrade.js.\n\n**Rank:** `archon` — Archon only (rank and identity management + the escalation keys — the trust boundary).\n\n**Permissions:** `core.pin.move` (all required).",
14280
+ "description": "POST /provisioning/instances/:id/core-upgrade — move the core pin. rank: `core.pin.move` — archon floor AND system:true, so the catalog's own invariant forbids granting it to metic and no custom rank can be handed it without a deliberate, visible edit. BV1.R105 / ADR 0151 is why this is an atom and not `requireRank`: tests/government_require_permission.mjs fails the build on a new requireRank in a modules/*/routes gate. OR THE PROJECT'S OWN OWNER (task 1004174, ADR 0339 — the owner's ruling that only a project's owner upgrades it). Ownership of a TENANT row is asked first; any other caller meets the unmodified `core.pin.move` gate and its unchanged refusal. The platform's own row is not an owner's to move here — it stays on the atom. Body: { to: \"1.19.757\" } — and requiring it IS the second step of the two-step the owner asked for (ADR 0293 D4). A request that cannot fire without naming its target cannot fire from a stray click, and \"move to whatever is newest\", resolved at drain time, would be a different act from the one previewed: main moves between the preview and the confirm. What this does NOT check: whether the version is PUBLISHED, and whether the channel policy admits it. Both need the registry, and the only tool that reads it (update-channel.js `listAvailableVersions`) shells out synchronously — it would block the event loop for every other request on this process. It also lives in scripts/gds/, which a module may not require (ADR 0083). The runner enforces both, where it is a CLI and both are free. See ../core-upgrade.js.\n\n**Rank:** `archon` — Archon only (rank and identity management + the escalation keys — the trust boundary).\n\n**Permissions:** `core.pin.move` (all required).",
14281
14281
  "x-rank": "archon",
14282
14282
  "x-source": "modules/provisioning/routes/core-upgrade.js",
14283
14283
  "x-permissions": [
@@ -21091,11 +21091,13 @@
21091
21091
  "ok": {
21092
21092
  "type": "boolean"
21093
21093
  },
21094
- "update": {}
21094
+ "update": {},
21095
+ "deploy_url": {}
21095
21096
  },
21096
21097
  "required": [
21097
21098
  "ok",
21098
- "update"
21099
+ "update",
21100
+ "deploy_url"
21099
21101
  ]
21100
21102
  },
21101
21103
  "GetCoreUpgradesResponse": {
@@ -594,8 +594,8 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
594
594
  | `d305f0df6f25` | certain | Jump to | `modules/hall-ui/public/palette.js:226` |
595
595
  | `01176c156112` | certain | Jump to a goal or a thought | `modules/hall-ui/public/goal-map.html:81` |
596
596
  | `01176c156112` | certain | Jump to a goal or a thought | `modules/hall-ui/public/thinking.html:80` |
597
- | `2bfc5a50ce88` | certain | Jump to a page or record | `modules/hall-ui/public/shell.js:495` |
598
- | `eba610d25523` | certain | Jump to… | `modules/hall-ui/public/shell.js:497` |
597
+ | `2bfc5a50ce88` | certain | Jump to a page or record | `modules/hall-ui/public/shell.js:500` |
598
+ | `eba610d25523` | certain | Jump to… | `modules/hall-ui/public/shell.js:502` |
599
599
  | `bc779353aa0c` | certain | Karma — peer recognition and approvals, a separate counter from {…}. | `modules/hall-ui/public/profile.js:94` |
600
600
  | `de0e1590667f` | certain | Keep all {…} {…} — they are the values the code drops in, and the change cannot land without them. | `modules/hall-ui/public/copy-desk.js:442` |
601
601
  | `fbe40b5818a3` | certain | Key removed. | `modules/hall-ui/public/settings.js:1128` |
@@ -776,7 +776,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
776
776
  | `261d123970e8` | certain | Open goal #{…} | `modules/hall-ui/public/roadmap.js:112` |
777
777
  | `5f3e0b179839` | certain | Open in Sessions | `modules/hall-ui/public/task.js:483` |
778
778
  | `9f55e4674ebb` | certain | Open in the copy desk | `modules/hall-ui/public/studio.js:219` |
779
- | `3366b115217b` | certain | Open navigation | `modules/hall-ui/public/shell.js:486` |
779
+ | `3366b115217b` | certain | Open navigation | `modules/hall-ui/public/shell.js:491` |
780
780
  | `855277dff074` | certain | Open task #{…} | `modules/hall-ui/public/goals-render.js:415` |
781
781
  | `855277dff074` | certain | Open task #{…} | `modules/hall-ui/public/work.js:1124` |
782
782
  | `99c630009152` | certain | Open tasks whose declared files overlap — sequence these before two builders collide. | `modules/hall-ui/public/goals.html:125` |
@@ -984,12 +984,12 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
984
984
  | `e57ec24e13ef` | certain | Sign in with GitHub | `modules/hall-ui/public/hall-render.js:367` |
985
985
  | `e57ec24e13ef` | certain | Sign in with GitHub | `modules/hall-ui/public/hall-render.js:391` |
986
986
  | `e57ec24e13ef` | certain | Sign in with GitHub | `modules/hall-ui/public/hall-render.js:461` |
987
- | `e57ec24e13ef` | certain | Sign in with GitHub | `modules/hall-ui/public/shell.js:456` |
988
- | `1883ad7f07fc` | certain | Sign out | `modules/hall-ui/public/shell.js:474` |
989
- | `1883ad7f07fc` | certain | Sign out | `modules/hall-ui/public/shell.js:707` |
987
+ | `e57ec24e13ef` | certain | Sign in with GitHub | `modules/hall-ui/public/shell.js:461` |
988
+ | `1883ad7f07fc` | certain | Sign out | `modules/hall-ui/public/shell.js:479` |
989
+ | `1883ad7f07fc` | certain | Sign out | `modules/hall-ui/public/shell.js:738` |
990
990
  | `8ad669ab2565` | certain | Sign out everywhere | `modules/hall-ui/public/settings.html:171` |
991
991
  | `ecc12c1dd4de` | certain | Skill voices | `modules/hall-ui/public/settings.html:350` |
992
- | `87cee78ca134` | certain | Skip to content | `modules/hall-ui/public/shell.js:698` |
992
+ | `87cee78ca134` | certain | Skip to content | `modules/hall-ui/public/shell.js:729` |
993
993
  | `688ca7b6e30f` | certain | Slop — reads as machine-written | `modules/hall-ui/public/copy-desk.html:90` |
994
994
  | `82df09a08476` | certain | So the Archon knows who you are | `modules/hall-ui/public/hall-render.js:294` |
995
995
  | `7da6a848cd26` | certain | Someone helped | `modules/hall-ui/public/collab.js:296` |
@@ -1016,7 +1016,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1016
1016
  | `5831de503015` | certain | Studio — {{worldName}} | `modules/hall-ui/public/studio.html:10` |
1017
1017
  | `f79e1fb8ebd4` | certain | Suggested as you type; leave it on "no goal" and it waits for triage. | `modules/hall-ui/public/ideas.html:84` |
1018
1018
  | `a1482eff4ead` | certain | Suggested for you | `modules/hall-ui/public/settings-specialities.js:137` |
1019
- | `1e40f33483eb` | certain | Switch theme | `modules/hall-ui/public/shell.js:501` |
1019
+ | `1e40f33483eb` | certain | Switch theme | `modules/hall-ui/public/shell.js:506` |
1020
1020
  | `93fa2e97dd35` | certain | System default for this skill | `modules/hall-ui/public/settings.js:72` |
1021
1021
  | `4894b1d9b0de` | certain | System oversight: people, operations and security. | `modules/hall-ui/public/watch.js:1335` |
1022
1022
  | `e604fc3ace79` | certain | Task # | `modules/hall-ui/public/blockers.js:139` |
@@ -1037,7 +1037,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1037
1037
  | `2fc66e722e33` | certain | Tasks open once you have read the builder primer. | `modules/hall-ui/public/work.html:43` |
1038
1038
  | `44579eef0c8d` | certain | Tasks — {{worldName}} | `modules/hall-ui/public/work.html:6` |
1039
1039
  | `44579eef0c8d` | certain | Tasks — {{worldName}} | `modules/hall-ui/public/work.html:6` |
1040
- | `2f4f047dc1b4` | certain | Text size | `modules/hall-ui/public/shell.js:500` |
1040
+ | `2f4f047dc1b4` | certain | Text size | `modules/hall-ui/public/shell.js:505` |
1041
1041
  | `f420429f942e` | certain | That builder no longer exists. | `modules/hall-ui/public/goals-render.js:1305` |
1042
1042
  | `545c59c1deb7` | certain | That is the session you are using right now. Ending it signs you out of this page. | `modules/hall-ui/public/settings-sessions.js:74` |
1043
1043
  | `282c3fa2d456` | certain | That is the wording it already has. Change something, or file a flag instead if you only want to mark it. | `modules/hall-ui/public/copy-desk.js:438` |
@@ -1169,7 +1169,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1169
1169
  | `2c1c20785a6e` | certain | Unclear — a person would not know what to do | `modules/hall-ui/public/copy-desk.html:91` |
1170
1170
  | `aa445f06cc5e` | certain | Unlink failed: {…} | `modules/hall-ui/public/settings.js:856` |
1171
1171
  | `67fbc770b3b3` | certain | Upvote (+1) | `modules/hall-ui/public/builders.js:777` |
1172
- | `16d7f809be8a` | certain | Use the control below to take it. | `modules/hall-ui/public/shell.js:665` |
1172
+ | `16d7f809be8a` | certain | Use the control below to take it. | `modules/hall-ui/public/shell.js:696` |
1173
1173
  | `e7e2a99a2bea` | certain | Use this | `modules/hall-ui/public/settings-specialities.js:148` |
1174
1174
  | `e1ecd543a2ca` | certain | Using this speciality. | `modules/hall-ui/public/settings-specialities.js:199` |
1175
1175
  | `6200bac6bdca` | certain | View all sessions | `modules/hall-ui/public/watch.html:99` |
@@ -192,7 +192,7 @@
192
192
  "modules/hall-ui/public/hall-render.js:367",
193
193
  "modules/hall-ui/public/hall-render.js:391",
194
194
  "modules/hall-ui/public/hall-render.js:461",
195
- "modules/hall-ui/public/shell.js:456"
195
+ "modules/hall-ui/public/shell.js:461"
196
196
  ]
197
197
  },
198
198
  {
@@ -1581,8 +1581,8 @@
1581
1581
  "text": "Sign out",
1582
1582
  "occurrences": 2,
1583
1583
  "at": [
1584
- "modules/hall-ui/public/shell.js:474",
1585
- "modules/hall-ui/public/shell.js:707"
1584
+ "modules/hall-ui/public/shell.js:479",
1585
+ "modules/hall-ui/public/shell.js:738"
1586
1586
  ]
1587
1587
  },
1588
1588
  {
@@ -6850,7 +6850,7 @@
6850
6850
  "surface": "builders-hall",
6851
6851
  "text": "Jump to a page or record",
6852
6852
  "file": "modules/hall-ui/public/shell.js",
6853
- "line": 495,
6853
+ "line": 500,
6854
6854
  "origin": "js-markup",
6855
6855
  "confidence": "certain"
6856
6856
  },
@@ -6859,7 +6859,7 @@
6859
6859
  "surface": "builders-hall",
6860
6860
  "text": "Jump to…",
6861
6861
  "file": "modules/hall-ui/public/shell.js",
6862
- "line": 497,
6862
+ "line": 502,
6863
6863
  "origin": "js-markup",
6864
6864
  "confidence": "certain"
6865
6865
  },
@@ -8488,7 +8488,7 @@
8488
8488
  "surface": "builders-hall",
8489
8489
  "text": "Open navigation",
8490
8490
  "file": "modules/hall-ui/public/shell.js",
8491
- "line": 486,
8491
+ "line": 491,
8492
8492
  "origin": "js-markup",
8493
8493
  "confidence": "certain"
8494
8494
  },
@@ -10360,7 +10360,7 @@
10360
10360
  "surface": "builders-hall",
10361
10361
  "text": "Sign in with GitHub",
10362
10362
  "file": "modules/hall-ui/public/shell.js",
10363
- "line": 456,
10363
+ "line": 461,
10364
10364
  "origin": "js-markup",
10365
10365
  "confidence": "certain"
10366
10366
  },
@@ -10369,7 +10369,7 @@
10369
10369
  "surface": "builders-hall",
10370
10370
  "text": "Sign out",
10371
10371
  "file": "modules/hall-ui/public/shell.js",
10372
- "line": 474,
10372
+ "line": 479,
10373
10373
  "origin": "js-markup",
10374
10374
  "confidence": "certain"
10375
10375
  },
@@ -10378,7 +10378,7 @@
10378
10378
  "surface": "builders-hall",
10379
10379
  "text": "Sign out",
10380
10380
  "file": "modules/hall-ui/public/shell.js",
10381
- "line": 707,
10381
+ "line": 738,
10382
10382
  "origin": "js-markup",
10383
10383
  "confidence": "certain"
10384
10384
  },
@@ -10405,7 +10405,7 @@
10405
10405
  "surface": "builders-hall",
10406
10406
  "text": "Skip to content",
10407
10407
  "file": "modules/hall-ui/public/shell.js",
10408
- "line": 698,
10408
+ "line": 729,
10409
10409
  "origin": "js-text-assign",
10410
10410
  "confidence": "certain"
10411
10411
  },
@@ -10648,7 +10648,7 @@
10648
10648
  "surface": "builders-hall",
10649
10649
  "text": "Switch theme",
10650
10650
  "file": "modules/hall-ui/public/shell.js",
10651
- "line": 501,
10651
+ "line": 506,
10652
10652
  "origin": "js-markup",
10653
10653
  "confidence": "certain"
10654
10654
  },
@@ -10837,7 +10837,7 @@
10837
10837
  "surface": "builders-hall",
10838
10838
  "text": "Text size",
10839
10839
  "file": "modules/hall-ui/public/shell.js",
10840
- "line": 500,
10840
+ "line": 505,
10841
10841
  "origin": "js-markup",
10842
10842
  "confidence": "certain"
10843
10843
  },
@@ -12025,7 +12025,7 @@
12025
12025
  "surface": "builders-hall",
12026
12026
  "text": "Use the control below to take it.",
12027
12027
  "file": "modules/hall-ui/public/shell.js",
12028
- "line": 665,
12028
+ "line": 696,
12029
12029
  "origin": "js-markup",
12030
12030
  "confidence": "certain"
12031
12031
  },
@@ -2549,5 +2549,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
2549
2549
  landed since 1.19.1033 with no explicit bump. run 36215757254. (task 1002620)
2550
2550
  1.19.1035 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2551
2551
  landed since 1.19.1034 with no explicit bump. run 36226356336. (task 1002620)
2552
+ 1.19.1036 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2553
+ landed since 1.19.1035 with no explicit bump. run 36226955285. (task 1002620)
2554
+ 1.19.1037 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2555
+ landed since 1.19.1036 with no explicit bump. run 36227523642. (task 1002620)
2552
2556
  ---------------------------------------------------------------------------
2553
2557
  ```
@@ -1048,7 +1048,7 @@ async function awardSessionTokenReward(
1048
1048
 
1049
1049
  // net-negative ship bonus — V3.R93 (#354). Simplification reward.
1050
1050
  //
1051
- // The v1 grader's scoreLocNetNegative concept (a ship that removes more lines
1051
+ // The retired v1 grader's loc-net-negative criterion (a ship that removes more lines
1052
1052
  // than it adds is a win) was only ever expressed as a rubric SCORE in the
1053
1053
  // deprecated deterministic grader — it never moved credits. Here we revive the
1054
1054
  // idea as a small, real credit bonus routed through the SAME credit_log path
@@ -16,19 +16,13 @@ const RUBRIC = JSON.parse(
16
16
  fs.readFileSync(path.join(__dirname, 'grader-rubric.json'), 'utf8')
17
17
  );
18
18
 
19
- // V4 = the active panel-based gate. V3 / V1 are kept under their fields for
20
- // the two paths that still read them: V3 = the trivial-diff multi-shot grader
21
- // (ship.js #513), V1 = the audit-ships.js retroactive deterministic path. The
22
- // v2 binding + its rubric block were dead (no reader) and removed in task 466.
19
+ // V4 = the active panel-based gate, and the only rubric block left. The v2 block
20
+ // was removed in task 466; v1 (with audit-ships.js, its only reader) and v3 (whose
21
+ // only live field was the unused 3-shot default) in task 1002842.
23
22
  const V4 = RUBRIC.v4;
24
- const V3 = RUBRIC.v3;
25
- const V1 = RUBRIC.v1;
26
23
  const THRESHOLD = V4.threshold;
27
24
  const VERSION = V4.version;
28
25
  const LOW_CONFIDENCE = V4.low_confidence_threshold;
29
- // DEFAULT_SHOTS is kept for the v3 multi-shot path (still exported for
30
- // diagnostics); v4 doesn't use shots — it uses workers.
31
- const DEFAULT_SHOTS = V3.shots || 3;
32
26
  const ASSUMED_BUILDER_MODEL = V4.assumed_builder_model || 'claude-fable-5';
33
27
  const DEFAULT_GRADER_MODEL = V4.default_model || 'claude-sonnet-5';
34
28
  const FALLTHROUGH_CONFIDENCE = V4.fallthrough_confidence ?? 0.3;
@@ -80,13 +74,10 @@ function routeRankPrePass(changedFiles) {
80
74
  module.exports = {
81
75
  ASSUMED_BUILDER_MODEL,
82
76
  DEFAULT_GRADER_MODEL,
83
- DEFAULT_SHOTS,
84
77
  FALLTHROUGH_CONFIDENCE,
85
78
  LOW_CONFIDENCE,
86
79
  RUBRIC,
87
80
  THRESHOLD,
88
- V1,
89
- V3,
90
81
  V4,
91
82
  VERSION,
92
83
  WORKER_MODEL_TIERS,