@bongos/core 1.19.587 → 1.19.588

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.
package/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.587",
6
- "core_contract": "1.19.587",
7
- "source_commit": "cd407ba54d0bdbe3638949084ba731c21179c811",
5
+ "core_version": "1.19.588",
6
+ "core_contract": "1.19.588",
7
+ "source_commit": "1833a4d18085bddf05b12c89aea4381e5ac5207e",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-08T00:16:55.401Z",
9
+ "built_at": "2026-09-08T02:03:26.993Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 455,
12
+ "docs_redacted": 456,
13
13
  "agent_docs_stubbed": 24,
14
14
  "functional_verbatim": 2060,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2539,
20
- "tree_sha256": "20c7a1a223a182078ca9a6e5dc0b6ca8d715a6e8b0a47b08a9c4dcb8dac2c74a",
19
+ "file_count": 2540,
20
+ "tree_sha256": "c1fc62840116ec8d78c3841c8a17e3172d902aadea6e084717199ab6bf5af95c",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -1829,10 +1829,15 @@
1829
1829
  "mode": "0000644",
1830
1830
  "sha256": "cc30ad9de02409b6e3db26517bef2966133b3362903a984b65d94ea289fd0d88"
1831
1831
  },
1832
+ {
1833
+ "path": "docs/adr/0263-how-a-version-closes.md",
1834
+ "mode": "0000644",
1835
+ "sha256": "07a3561c7bc7f55bb2077925df4df6a24b68a1f0c8d4052ac50278edd9d4eba1"
1836
+ },
1832
1837
  {
1833
1838
  "path": "docs/adr/README.md",
1834
1839
  "mode": "0000644",
1835
- "sha256": "9d58f1a823a763141df272149e58b2182aa2289dd94d69ba0b3999789d8fc743"
1840
+ "sha256": "fce8c6fab919a332b83cd467b2c0548499c58a2e032ca8e8d4da4b06d35bb715"
1836
1841
  },
1837
1842
  {
1838
1843
  "path": "docs/api-reference.md",
@@ -2717,7 +2722,7 @@
2717
2722
  {
2718
2723
  "path": "docs/module-api-changelog.md",
2719
2724
  "mode": "0000644",
2720
- "sha256": "3e6075bb21834fc14b6201155cac3f4b51e262d86d6ccbed2505f593831f59f9"
2725
+ "sha256": "44d532bc6f634ef0730d4f5d3a778c67516143ea202f126c48aed43ffc8e8664"
2721
2726
  },
2722
2727
  {
2723
2728
  "path": "docs/modules-contract.md",
@@ -7592,12 +7597,12 @@
7592
7597
  {
7593
7598
  "path": "package-lock.json",
7594
7599
  "mode": "0000644",
7595
- "sha256": "2b1a6ffdd207887a187f1574571de67e3f8ba851e80e433d463e932316b36e16"
7600
+ "sha256": "93ff5dd9e6965597f8af514e41a02d0c5149e31e6a456576f6dc9b1bd1a773fc"
7596
7601
  },
7597
7602
  {
7598
7603
  "path": "package.json",
7599
7604
  "mode": "0000644",
7600
- "sha256": "0caa3d71c5b02c8a41cebc7e59a6af2d68b50eb419311c7d444b0daa30df8430"
7605
+ "sha256": "ed02b11ea51fdadd43ceb8fcaa9c3613ca172bf8c06caeeb0a351b8c65405900"
7601
7606
  },
7602
7607
  {
7603
7608
  "path": "public-docs/index.html",
@@ -9307,7 +9312,7 @@
9307
9312
  {
9308
9313
  "path": "src/module-api.js",
9309
9314
  "mode": "0000644",
9310
- "sha256": "6a4b0d97d9bc67728886e879359cf1f657dd81f865d141d8a1c6cd6e41877012"
9315
+ "sha256": "d1b45d7bf6ca9667e8ef78fbb8dc255eda9bb60a27a74e48f59fb067e882d8d4"
9311
9316
  },
9312
9317
  {
9313
9318
  "path": "src/module-loader/catalog.js",
@@ -0,0 +1,275 @@
1
+ # 0263 — How a version closes: auto, early, roll-forward, and the maintenance exemption
2
+
3
+ - **Status:** Accepted
4
+ - **Date:** 2026-09-07
5
+ - **Tasks:** [#1003593](https://cloudbongos.com/builders#/task/1003593) (this design pass, BV1.R06). Implemented by BV1.R12 ([#1003599](https://cloudbongos.com/builders#/task/1003599)), R16 ([#1003603](https://cloudbongos.com/builders#/task/1003603)), R17 ([#1003604](https://cloudbongos.com/builders#/task/1003604)), R18 ([#1003605](https://cloudbongos.com/builders#/task/1003605)), R20 ([#1003607](https://cloudbongos.com/builders#/task/1003607)) and R23 ([#1003610](https://cloudbongos.com/builders#/task/1003610)).
6
+ - **Goal:** [#1000086](https://cloudbongos.com/builders#/goal/1000086) — Strict versioning.
7
+ - **Implements:** [ADR 0250](<redacted>.md) D5. That decision says a version *can* close; this one says *how*, so the five build tasks under it are mechanical.
8
+
9
+ ## 1. Why a second ADR
10
+
11
+ [ADR 0250](<redacted>.md) is the
12
+ decision: the version boundary is the scope gate, and a version must be able to
13
+ reach done. It deliberately stops at the rule. Closing a version is the widest
14
+ write in the system — it flips a version, dispositions every goal still standing,
15
+ promotes the next version into `building`, and carries a bug queue across the
16
+ boundary — and *four* separate tasks build pieces of it. Left undesigned, each
17
+ would pick its own refusal codes, its own payload shape, and its own answer to
18
+ "what happens to the maintenance goal", and the seams would only meet in
19
+ production.
20
+
21
+ So this ADR pins the mechanics. It adds no rule ADR 0250 did not already decide.
22
+
23
+ ## 2. The state machine
24
+
25
+ `versions.status` has four values (migration 003): `planning`, `building`,
26
+ `shipped`, `frozen`. Only two transitions are in scope here:
27
+
28
+ ```
29
+ planning ──promote──> building ──close──> shipped
30
+ ```
31
+
32
+ `frozen` is archival bookkeeping and no business of this design. Nothing
33
+ un-closes a version: reopening is not a move, and a version closed in error is
34
+ corrected the way any other bad row is.
35
+
36
+ **Only a `building` version may close.** Closing a `planning` version is
37
+ meaningless (nothing was built), and closing an already-`shipped` one is a
38
+ double-close. Both are `version_not_building` (409).
39
+
40
+ ## 3. Two doors, and the asymmetry that makes the design simple
41
+
42
+ The single most useful thing to notice about D5 is that **auto-close, by
43
+ construction, has nothing to disposition.**
44
+
45
+ Auto-close fires when the last non-maintenance goal achieves. If that goal was
46
+ the last one, then every non-maintenance goal on the version is already
47
+ `achieved` or `archived` — so the set of open goals needing a decision is *empty
48
+ by definition*. The disposition machinery is not merely unnecessary on that path;
49
+ it can never have input.
50
+
51
+ That splits the feature cleanly in two:
52
+
53
+ | | **Auto-close** (R16) | **Early close** (R12) |
54
+ |---|---|---|
55
+ | Trigger | the last non-maintenance goal achieves | an Archon calls `POST /versions/:id/close` |
56
+ | Open goals at close | zero, by construction | any number |
57
+ | Disposition required | none — there is nothing to disposition | one per open non-maintenance goal |
58
+ | Actor | the system, on a ship or a satisfy | an Archon, deliberately |
59
+ | Roll-forward | the maintenance goal only | the maintenance goal, plus every goal dispositioned `roll_forward` |
60
+
61
+ Every complication in this feature — the disposition map, the refusal that
62
+ returns the open goals, the successor lineage — belongs to **early close only**.
63
+ R16 is a much smaller task than the ADR 0250 sentence makes it sound, and R12 is
64
+ where the weight is.
65
+
66
+ ## 4. Auto-close: where it fires, and why it cannot fail a ship
67
+
68
+ Goal achievement has **two writers**, and [ADR 0250](<redacted>.md) D2 already made them a deliberate,
69
+ documented duplicate rather than one helper, because collapsing them creates a
70
+ require cycle (`db-goals` → `done-when` → `db-goals`):
71
+
72
+ - [`db-goals.js` `achieveGoalIfComplete`](../../modules/lifecycle/db-goals.js) — the manual `POST /done-when/:id/satisfy` path.
73
+ - [`done-when.js` `autoSatisfyShippedCriteria`](../../modules/lifecycle/done-when.js) — the cascade that fires on **every ship** and every reconciler tick, on the ship transaction's own `exec`.
74
+
75
+ **Auto-close hangs off both, on the same exec, exactly as R08/R09's closure check
76
+ did.** The rule is written twice on purpose, and the two copies move together or
77
+ a version closes correctly through one door and wrongly through the other. That
78
+ sentence is already load-bearing in both files; this feature adds a second clause
79
+ governed by it, so it is repeated here rather than left to be rediscovered.
80
+
81
+ **Auto-close must never fail a ship.** This is not a preference — it is the
82
+ established posture of the surrounding code:
83
+ [`closeCompletedWorkForShip`](../../modules/lifecycle/done-when.js) already
84
+ swallows its own failure and reports it in a summary line, on the argument that a
85
+ criterion close must never fail a ship and the reconciler's unscoped sweep
86
+ retries every tick. Version close is a strictly wider write than a criterion
87
+ close, so it inherits the posture *a fortiori*:
88
+
89
+ - it runs inside the ship transaction so it sees the uncommitted `shipped` flip (the same reason R09's clause needs `exec`);
90
+ - it is wrapped so a throw is caught, logged, and reported — never propagated into the ship;
91
+ - it is **idempotent** (`WHERE status = 'building'` guards the flip), so the reconciler sweep retrying it is a no-op once it has run.
92
+
93
+ The consequence, stated plainly: **a version can be left un-closed by a failure,
94
+ and never half-closed.** Un-closed self-heals on the next sweep. That is the
95
+ correct trade — the opposite choice makes a bug in version-close able to block
96
+ every builder's ship.
97
+
98
+ ### The archive doc is not part of the transaction
99
+
100
+ `limitations/<version>-shipped.md` is a **repo file**, and a route cannot write
101
+ one. Auto-close therefore flips the database and nothing else; the scope archive
102
+ stays a human/agent follow-up driven by
103
+ [`scripts/gds/version-close.js`](../../scripts/gds/version-close.js) (R23). This is
104
+ worth saying because ADR 0250 §7 speaks of a version "reaching done", and a
105
+ reader could reasonably assume the archive rides along. It does not, and pretending
106
+ otherwise would put file I/O inside a ship transaction.
107
+
108
+ ## 5. Early close: the two-step, mirroring the archive refusal
109
+
110
+ `POST /versions/:id/close` is Archon-only, behind the **already-defined**
111
+ `version.close` permission ([`government/catalog.js`](../../modules/government/catalog.js)) that nothing has referenced until now.
112
+
113
+ The shape is **the R10 archive two-step, verbatim in spirit** — that precedent is
114
+ shipped, agents already know it, and inventing a second idiom for the same
115
+ interaction is how two surfaces drift:
116
+
117
+ **Step 1 — refuse, and return the work.** Called with open non-maintenance goals
118
+ and no disposition, the route refuses with `version_holds_open_goals` (409) and
119
+ **returns the goals**, not merely a count. R10's reasoning applies unchanged: the
120
+ caller's next move is to decide what happens to each, and an agent that has to go
121
+ fetch the list first will guess instead.
122
+
123
+ ```
124
+ 409 { error: "version_holds_open_goals",
125
+ message: "Version 'BONGOS-V1' still holds 12 open goal(s). …",
126
+ details: { total: 12, shown: 12,
127
+ goals: [ { id, title, open_tasks } … ] } }
128
+ ```
129
+
130
+ **Step 2 — proceed with a disposition map.** One entry per open non-maintenance
131
+ goal, keyed by goal id:
132
+
133
+ ```json
134
+ { "reason": "V1 is done; the rest is V2 scope.",
135
+ "dispositions": {
136
+ "1000044": { "verb": "roll_forward" },
137
+ "1000065": { "verb": "abandon" }
138
+ } }
139
+ ```
140
+
141
+ Two verbs, and only two:
142
+
143
+ - **`roll_forward`** — a successor goal is created in the planning version (§6).
144
+ - **`abandon`** — the goal is archived, and its open tasks are abandoned with the close's `reason`, through the same path R14 gives the archive flow. It is deliberately **not** possible to abandon a goal without a reason: `reason` is required on the close call, and it is what lands in each abandoned task's record.
145
+
146
+ **A version holding zero open goals stays a single call**, exactly as a goal
147
+ holding zero open tasks does. That is the auto-close path's shape too, so both
148
+ doors agree.
149
+
150
+ ### Refusal codes
151
+
152
+ Every one is **named**, per ADR 0250 §7 — the callers are agents, and an unnamed
153
+ 409 is an agent stopping to ask a human.
154
+
155
+ | Code | Status | When |
156
+ |---|---|---|
157
+ | `version_not_found` | 404 | no such version |
158
+ | `version_not_building` | 409 | the version is `planning`, `shipped` or `frozen` |
159
+ | `version_holds_open_goals` | 409 | open non-maintenance goals and no/partial disposition map; returns the goals |
160
+ | `bad_disposition` | 400 | a disposition names an unknown goal, a goal not on this version, or a verb outside `roll_forward`/`abandon` |
161
+ | `no_planning_version` | 409 | a `roll_forward` was asked for and no `planning` version exists to receive it |
162
+ | `close_reason_required` | 400 | `reason` missing on a close that dispositions anything |
163
+
164
+ `no_planning_version` is the one that will actually be hit in practice, and it is
165
+ a **refusal rather than an auto-create on purpose**: cutting the successor version
166
+ is a scope decision with its own criteria, and doing it implicitly inside a close
167
+ is precisely the "fake hotfix version" failure ADR 0250 §3 built the override
168
+ counter to prevent.
169
+
170
+ ## 6. Roll-forward: the lineage column already exists, pointing the other way
171
+
172
+ Migration 160 gave `goals` a **`succeeded_by_goal_id`** column — on the *old*
173
+ goal, pointing at the *new* one. Both ADR 0250 and task 1003604's brief say
174
+ `succeeds_goal_id`, which does **not** exist. The column that exists wins; there
175
+ is no migration here, and R17 must write:
176
+
177
+ ```sql
178
+ UPDATE goals SET succeeded_by_goal_id = <new.id> WHERE id = <old.id>
179
+ ```
180
+
181
+ Naming this explicitly is the whole reason this section exists — R17's brief would
182
+ otherwise send a builder to reserve a migration number for a column that has been
183
+ in the schema since the goal tier landed.
184
+
185
+ A rolled-forward goal is created on the planning version carrying the original's
186
+ `title`, `description`, `scope_modules`, `category_id` and **membership**; its
187
+ open tasks are re-pointed at it. The predecessor is then `archived` with zero
188
+ open tasks — so it passes R10's own check on the way out, rather than being
189
+ special-cased around it.
190
+
191
+ ## 7. The maintenance goal: the flag R18 owes, and the exemption
192
+
193
+ The maintenance goal is currently **matched by title** —
194
+ `ensureMaintenanceGoal` (task 1003691) looks up `"<version> — maintenance"`, and
195
+ its own comment flags this as R18's debt:
196
+
197
+ > MATCHED BY TITLE, because there is no column that says "this is the maintenance goal" … task 1003605 (BV1.R18) is where a real flag belongs when the feature gets its own column.
198
+
199
+ **R18 adds the column**: `goals.is_maintenance boolean NOT NULL DEFAULT false`,
200
+ backfilled by title for the rows that already exist, after which the title lookup
201
+ becomes a flag read. This is not cosmetic. The close count asks "are there any
202
+ open non-maintenance goals?", and answering it with a `title NOT LIKE` would make
203
+ a hand-titled goal silently exempt from the gate that decides when a **version**
204
+ closes — a much worse blast radius than the provenance wrinkle the title match
205
+ carries today.
206
+
207
+ The exemption is then one predicate, used identically by auto-close and early
208
+ close:
209
+
210
+ ```sql
211
+ WHERE version_id = $1 AND status = 'open' AND NOT is_maintenance
212
+ ```
213
+
214
+ **The maintenance goal always rolls forward, and never needs a disposition.** It
215
+ is exempt from the close count, so it cannot hold a version open; and its open
216
+ bugs move to the successor version's maintenance goal (`ensureMaintenanceGoal` on
217
+ the newly-promoted version — the find-or-create already exists and is
218
+ advisory-locked). Bugs arrive on their own schedule and must not need an override
219
+ to be filed; that is the pressure valve ADR 0250 §3 describes, and it only works
220
+ if the queue survives the boundary.
221
+
222
+ ## 8. Promotion: the close's last act
223
+
224
+ On a successful close, the single `planning` version is promoted to `building`
225
+ (R20), so the project is never without a live train.
226
+
227
+ - It runs **in the close's transaction** — a project with zero building versions is a broken state, not an intermediate one, and `currentBuildingVersionId` returns `null` there, which fails every routing path closed over it.
228
+ - **Zero planning versions is not an error.** The close succeeds and the project sits with no building version until someone cuts one. Refusing the close would trap a finished version open because nobody had scoped the next one yet.
229
+ - Exactly one can be promoted, because R04 already refuses a second `planning` version.
230
+ - The promoted version gets its maintenance goal via `ensureMaintenanceGoal`, which is what receives the carried-forward bugs in §7.
231
+
232
+ **Ordering inside the transaction is load-bearing:** promote *before* roll-forward
233
+ and before the bug carry-over, because both need a destination version that is
234
+ already `building` — `ensureMaintenanceGoal` returns `null` for a version whose
235
+ status is not `building`, so a carry-over run before the promotion silently
236
+ carries nothing.
237
+
238
+ ## 9. The race, and where the index belongs
239
+
240
+ [`versions.js`](../../modules/lifecycle/routes/versions.js) already documents R04's
241
+ unmitigated check-then-insert race and names R19 as the place to fix both halves
242
+ with one partial unique index. That still stands, and this design adds the close
243
+ side of the same exposure: two concurrent closes could both promote.
244
+
245
+ The single migration R19 reserves should therefore carry **both** guards:
246
+
247
+ ```sql
248
+ CREATE UNIQUE INDEX ON versions ((status)) WHERE status = 'planning';
249
+ CREATE UNIQUE INDEX ON versions ((status)) WHERE status = 'building';
250
+ ```
251
+
252
+ With those in place the promotion race resolves the way the version-id collision
253
+ already does — the loser gets a `23505` and the route translates it into the same
254
+ clean refusal, rather than the check-then-act prayer both rules rely on today.
255
+
256
+ ## 10. What this design does not do
257
+
258
+ - **It does not gate shipping.** Consistent with ADR 0250 §4. Auto-close rides the ship transaction and is swallowed on failure; nothing here can refuse a ship, a claim, or a grade.
259
+ - **It does not write the scope archive.** §4. The route closes the version; the doc is R23's CLI work.
260
+ - **It does not auto-create the successor version.** §5. That is a scope decision with criteria, and doing it implicitly is the escape hatch ADR 0250 exists to close.
261
+ - **It does not add a reopen.** A version does not un-close.
262
+
263
+ ## 11. The sub-tasks this implies
264
+
265
+ All five were already filed by the planning session; this ADR is what makes them
266
+ mechanical. Two briefs are **corrected** by it:
267
+
268
+ | Task | What this ADR pins |
269
+ |---|---|
270
+ | R12 [#1003599](https://cloudbongos.com/builders#/task/1003599) | the route, the six refusal codes, the two-step + disposition payload (§5) |
271
+ | R16 [#1003603](https://cloudbongos.com/builders#/task/1003603) | **corrected** — auto-close needs no disposition machinery (§3); it hangs off *both* achievement writers on the caller exec and may never fail a ship (§4) |
272
+ | R17 [#1003604](https://cloudbongos.com/builders#/task/1003604) | **corrected** — the column is `succeeded_by_goal_id` and already exists; no migration (§6) |
273
+ | R18 [#1003605](https://cloudbongos.com/builders#/task/1003605) | the `is_maintenance` flag, the backfill, the exemption predicate, the bug carry-over (§7) |
274
+ | R20 [#1003607](https://cloudbongos.com/builders#/task/1003607) | promotion runs in the close txn, before roll-forward; zero planning versions is not an error (§8) |
275
+ | R19 [#1003606](https://cloudbongos.com/builders#/task/1003606) | its migration carries the `building` partial index **and** R04's `planning` one (§9) |
@@ -354,3 +354,4 @@ This keeps the decision history honest and traceable.
354
354
  | 0260 | [**An application IS the consent, and the hub’s own echo is the gate** ([task 1002972](https://cloudbongos.com/builders#/task/1002972) · goal 1000045, criterion C4). Privacy spec **D7** asks that a reviewer see what the applicant’s OWN profile rules would already show, evaluated LIVE at review time. The rule is ONE port composing the two EXISTING views — `getPublicProfileExtras` for a public account, `recruiter-sliver`’s own `sliverShapeFor` for a private one — because a third five-key lookalike **is** the new disclosure class D7 forbids. **The one deliberate difference from the D3 sliver:** `getRecruiterSliver` floors on recruiting REACH, and reusing it verbatim would be wrong in the direction that looks safe — `recruiter_discoverable` defaults to FALSE for a private account, so a private builder who deliberately applied would show their reviewer NOTHING and C4 would be satisfied by an empty box. D7 settles it: *“applying is an explicit act”*. The swap relaxes REACH only; `accountActiveSql` (active **and** terms accepted) and `hide_stats` still floor it, and the proof asserts both directions on the same account so the tempting refactor cannot pass quietly. **The federated route’s gate is the whole security story** ([ADR 0205](<redacted>.md) / security report 1000027): client credentials name a PROJECT and never a person, so `POST /sso/applicant-profile` authenticated alone is a bulk disclosure oracle over every account on the platform, private ones included. `applicationBacksProfileRead` demands a HUB-WRITTEN echo for exactly (this account, this client) inside the same 30-day window — a row only the hub’s own join relay creates — refusing **404, never 403**, and BEFORE any account is read (asserted on the statement log, because a gate that refuses after reading has already done the disclosure work). LIVE means nothing is stored at either end; the queue keys on the VOUCH and never on `github_login` (the one PUBLIC unauthenticated write makes an unvouched login an impersonation surface); and the hub client secret stays in core behind a narrow doorway port. **The path is complete but DORMANT until R09 ([task 1002285](https://cloudbongos.com/builders#/task/1002285)) ships the vouch writer** — nothing writes `applicant_github_id` today, and that dependency was missing from 1002972’s graph. Rejected: routing the port through `getRecruiterSliver`, a snapshot at apply time, resolving by `github_login`, and putting `loadIdpConfig` on the doorway.](<redacted>.md) | platform identity / privacy |
355
355
  | 0261 | [**A preselect always carries a reason; the bundle’s summary is the floor** ([task 1003684](https://cloudbongos.com/builders#/task/1003684) · goal 1000046 — *Project creation*). Resolves a collision between two rules that were each right alone. [ADR 0243](<redacted>.md) made `adjustments` a **delta** — a team-shape rule that fires without moving anything claims nothing, because a sentence explaining a change that did not happen is a claim the owner cannot check. [ADR 0237](<redacted>.md) says a preselect the owner cannot see a reason for is one they must audit, which is worse than no preselect at all. The two collide whenever a type’s bundle **already contains** what a rule would add: `game` is `[dev-box, discord]` and the `small-team` rule adds `dev-box`, so the rule fires, moves nothing, and correctly reports `adjustments: []`. Confirmed on live core 1.19.580 — `byTeamShape[small-team]` = `{"bundle":["dev-box","discord"],"adjustments":[]}`. The owner then reached step 4 and saw **two extras switched on with no reason beside them**, `#modWhy` an empty hidden div: exactly what 0237 exists to prevent, produced by 0243 behaving correctly. Filed as a dropped adjustment; it was not — the engine is right, and the panel simply had ONE voice and fell silent when the rule had nothing to say. **Decision: the why-line has two voices and the type’s is the floor.** A real adjustment still speaks for itself and is never displaced; when none moved, the type’s own curated `summary` explains the preselect; when the owner has answered the picker themselves the panel stays silent (unchanged — a reason handed back for a toggle they just flipped reads as the panel arguing with them); a bundle with no summary still says nothing, because a floor is not an invention. **No new copy, endpoint or field** — the summary was authored for this job in `starter-bundles.js`, served on every row of `GET /provisioning/starter-bundles`, and rendered NOWHERE in the product until now. 0243 is not weakened: the delta stays a delta and no rule is credited with a change it did not make. The pin `tests/projects_hub_module_picker.mjs` carried a fixture with **no `summary` field**, which made four “no adjustment, no sentence” cases pass for the wrong reason; the fixture now carries the real summaries and those cases assert the new contract, including explicitly that no RULE sentence is invented for a change that did not happen. Rejected: making the rule claim a no-op change (re-introduces the unverifiable claim 0243 removed); widening the bundles so no rule is ever redundant (contorts curated presets for a rendering concern, and the redundancy returns on the next edit); and leaving it silent, declined by the owner once the trade-off was put to them directly.](<redacted>.md) | provisioning / starter bundles / owner-facing copy |
356
356
  | 0262 | [**A bug never lands in the inbox: the system names the home the reporter didn't** ([task 1003691](https://cloudbongos.com/builders#/task/1003691) · goal 1000086 — *Strict versioning*). `routing.js` returned early on a goal-less filing, so its own `kind='bug'` hard-land branch was UNREACHABLE without a goal — `kind` only ever chose a routed task's STATUS, never whether it routed — and a reported defect sat in `idea_inbox` awaiting the triage pass criterion C1 exists to remove. The premise that [ADR 0250](<redacted>.md) D4 and [ADR 0235](<redacted>.md) contradict each other is FALSE: 0235's exemption is an advisory silence in `goal-advisory.adviseGoal`, and its actual decision already requires a real `goal_id`. So nothing is amended; this is the explicit decision 0235 §6 said would be needed. **D1** a homeless bug gets the version's MAINTENANCE goal (R18's design), find-or-created under an advisory lock inside the route's own transaction — not the catch-all R11 is deleting, and adding no `allowCatchAll` caller. **D2** the gate is the VECTOR (`api`, `discord-bugs`), not the kind: `#ideas` passes no kind, so the classifier guesses one from keywords, and gating on kind alone would silently overturn [ADR 0234](<redacted>.md)'s owner-interview decision. Rejected: goal-less bug tasks (breaks D4 a day after it landed); rejecting the filing (Discord cannot retry, so the report is lost); the catch-all (repopulates the bucket being drained); building R18 whole first.](<redacted>.md) | work intake / idea routing / the version-goal invariant |
357
+ | 0263 | [**How a version closes: auto, early, roll-forward, and the maintenance exemption** ([task 1003593](https://cloudbongos.com/builders#/task/1003593) · goal 1000086 — *Strict versioning*). [ADR 0250](<redacted>.md) D5 decided a version *can* close; this is the design pass that makes its five build tasks mechanical, so they do not each invent their own refusal codes and payload shapes and meet only in production. **The asymmetry that shrinks the feature:** auto-close fires when the last non-maintenance goal achieves — so by construction every non-maintenance goal is already closed and the set needing a disposition is EMPTY. All the machinery (the disposition map, the refusal that returns the goals, successor lineage) belongs to EARLY close alone; R16 is small and R12 carries the weight. **Auto-close hangs off BOTH achievement writers** (`db-goals.achieveGoalIfComplete` and the `done-when` cascade — the deliberate duplicate D2 already governs, a require cycle being the reason they are not one helper), on the caller `exec` so it sees the ship's uncommitted flip, and **may never fail a ship**: wrapped and swallowed like `closeCompletedWorkForShip`, idempotent under `WHERE status='building'`, so a failure leaves a version un-closed (self-healing on the next reconciler sweep) and never half-closed. **Early close** is the R10 archive two-step verbatim in spirit — refuse with `version_holds_open_goals` **returning the goals**, then proceed with a per-goal `roll_forward`/`abandon` map and a required `reason`; six named codes, because the callers are agents. **Two briefs are corrected by measurement:** the lineage column is `succeeded_by_goal_id` (migration 160, on the OLD row pointing forward) — not the `succeeds_goal_id` both 0250 and R17's brief name, so R17 needs NO migration; and R18's `is_maintenance` flag is load-bearing rather than cosmetic, because answering the close count with a `title NOT LIKE` would let a hand-titled goal silently exempt itself from the gate that decides when a VERSION closes. Promotion runs inside the close txn and BEFORE roll-forward (`ensureMaintenanceGoal` returns null for a non-`building` version, so a carry-over run first silently carries nothing); zero planning versions is not an error; R19's one migration should carry the `building` AND `planning` partial unique indexes so the promotion race resolves like a 23505 rather than a prayer. Rejected: auto-creating the successor version inside a close (the "fake hotfix version" escape hatch 0250 built the override counter to prevent), writing `limitations/<version>-shipped.md` from the route (file I/O in a ship transaction — it stays R23's CLI work), and a version reopen.](<redacted>.md) | version lifecycle / scope closure |
@@ -1623,5 +1623,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1623
1623
  landed since 1.19.585 with no explicit bump. run 34167072966. (task 1002620)
1624
1624
  1.19.587 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1625
1625
  landed since 1.19.586 with no explicit bump. run 34172785860. (task 1002620)
1626
+ 1.19.588 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1627
+ landed since 1.19.587 with no explicit bump. run 34178696372. (task 1002620)
1626
1628
  ---------------------------------------------------------------------------
1627
1629
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.587",
3
+ "version": "1.19.588",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.587",
9
+ "version": "1.19.588",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.587",
3
+ "version": "1.19.588",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
package/src/module-api.js CHANGED
@@ -55,7 +55,7 @@ const { buildInfo } = require('./build-info');
55
55
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
56
56
  // the entry to that file. Look for a version's history there, not here.
57
57
  // ---------------------------------------------------------------------------
58
- const CORE_VERSION = '1.19.587'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
58
+ const CORE_VERSION = '1.19.588'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
59
59
 
60
60
  // A namespaced logger so a module's log lines are attributable + consistent.
61
61
  // Usage: const log = api.logger('dev-box'); log.info('mounted');