@bongos/core 1.19.586 → 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.586",
6
- "core_contract": "1.19.586",
7
- "source_commit": "ac6f287b895b1259c7b9fcc6b3a13e4fe178fbc0",
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-07T22:34:04.491Z",
9
+ "built_at": "2026-09-08T02:03:26.993Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 454,
12
+ "docs_redacted": 456,
13
13
  "agent_docs_stubbed": 24,
14
- "functional_verbatim": 2058,
14
+ "functional_verbatim": 2060,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2536,
20
- "tree_sha256": "7986b516ffada3b257352c5ecedda7fbc33f7e216f58f11fd2cd8edf23920cae",
19
+ "file_count": 2540,
20
+ "tree_sha256": "c1fc62840116ec8d78c3841c8a17e3172d902aadea6e084717199ab6bf5af95c",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -182,7 +182,7 @@
182
182
  {
183
183
  "path": ".claude/skills/idea-triage/SKILL.md",
184
184
  "mode": "0000644",
185
- "sha256": "2519755269ff2d55e3c060569f7d16c440428ba005d206a3bf12fe2275830285"
185
+ "sha256": "348bb6e8d27610848fb5f31b672c2c762f56cfb17141f7abe1a8dee0b5030bdd"
186
186
  },
187
187
  {
188
188
  "path": ".claude/skills/ideate/SKILL.md",
@@ -1824,10 +1824,20 @@
1824
1824
  "mode": "0000644",
1825
1825
  "sha256": "04382d76f04c9317b565fb72951b36503e67acc95981a388d5c45efc80faaa26"
1826
1826
  },
1827
+ {
1828
+ "path": "docs/adr/0262-a-bug-never-lands-in-the-inbox.md",
1829
+ "mode": "0000644",
1830
+ "sha256": "cc30ad9de02409b6e3db26517bef2966133b3362903a984b65d94ea289fd0d88"
1831
+ },
1832
+ {
1833
+ "path": "docs/adr/0263-how-a-version-closes.md",
1834
+ "mode": "0000644",
1835
+ "sha256": "07a3561c7bc7f55bb2077925df4df6a24b68a1f0c8d4052ac50278edd9d4eba1"
1836
+ },
1827
1837
  {
1828
1838
  "path": "docs/adr/README.md",
1829
1839
  "mode": "0000644",
1830
- "sha256": "6d2df0c7c0e6cfd3381935d14a703f8cfa7854d107a6add03bc743e631823ada"
1840
+ "sha256": "fce8c6fab919a332b83cd467b2c0548499c58a2e032ca8e8d4da4b06d35bb715"
1831
1841
  },
1832
1842
  {
1833
1843
  "path": "docs/api-reference.md",
@@ -2712,7 +2722,7 @@
2712
2722
  {
2713
2723
  "path": "docs/module-api-changelog.md",
2714
2724
  "mode": "0000644",
2715
- "sha256": "3474ae19a143b381a4da0091daa41c301338dac6ffd97e7ef396079803589160"
2725
+ "sha256": "44d532bc6f634ef0730d4f5d3a778c67516143ea202f126c48aed43ffc8e8664"
2716
2726
  },
2717
2727
  {
2718
2728
  "path": "docs/modules-contract.md",
@@ -4197,7 +4207,7 @@
4197
4207
  {
4198
4208
  "path": "modules/discord/discord-bugs.js",
4199
4209
  "mode": "0000644",
4200
- "sha256": "083569e4fe2a621dc6c8663fafc5ec1d6f5e1e1893a3b4a9bdb74ecf4854cd24"
4210
+ "sha256": "52b463955cd7e57f8d194c161a76e2ade83481f3ee0a16759019a5b1f873b374"
4201
4211
  },
4202
4212
  {
4203
4213
  "path": "modules/discord/discord-channels.js",
@@ -4207,7 +4217,7 @@
4207
4217
  {
4208
4218
  "path": "modules/discord/discord-inbound.js",
4209
4219
  "mode": "0000644",
4210
- "sha256": "06937afc2ac299c8b6572cf296bbaed4e1e078802f9d3fc7a81e5851a639d5f3"
4220
+ "sha256": "86099070386ff606ec14330375ac8c3145365307ae2ac354539731aebcc16563"
4211
4221
  },
4212
4222
  {
4213
4223
  "path": "modules/discord/discord-message-buffer.js",
@@ -5364,10 +5374,15 @@
5364
5374
  "mode": "0000644",
5365
5375
  "sha256": "07e5666362c91f92d34388b4f6f4507cc5387866d317970478e6cae417c19975"
5366
5376
  },
5377
+ {
5378
+ "path": "modules/ideas/homeless-home.js",
5379
+ "mode": "0000644",
5380
+ "sha256": "363598b5905c850967b407f20110a1d9e9d5b1090a958700cb6a4ed5cea70d80"
5381
+ },
5367
5382
  {
5368
5383
  "path": "modules/ideas/inbox.js",
5369
5384
  "mode": "0000644",
5370
- "sha256": "5ebd1524a5593b70f74a83bdaa3a6d772eb820ea3d962f48dedace970efcc119"
5385
+ "sha256": "8c0792629df6d9587bce828a0f3743b8c86bdb31ba226d31211b9c6623574395"
5371
5386
  },
5372
5387
  {
5373
5388
  "path": "modules/ideas/migrations/ideas_001_grades.sql",
@@ -5412,12 +5427,12 @@
5412
5427
  {
5413
5428
  "path": "modules/ideas/routes/inbox.js",
5414
5429
  "mode": "0000644",
5415
- "sha256": "a092eea43d9030eb55a23b60bd1ad5673132b83a3a2649961413cd505fd685ef"
5430
+ "sha256": "8dec7682099d131ca050487dafe259bf4c9b511103c472cd0cbce54b4b9a898f"
5416
5431
  },
5417
5432
  {
5418
5433
  "path": "modules/ideas/routing.js",
5419
5434
  "mode": "0000644",
5420
- "sha256": "2ca0adb68b0fd44a65b9637c177848648ecd20ecd8ef67e65527c17b7a46414d"
5435
+ "sha256": "ddedb76f43edbe3e7c918578fda94b97cb3822d97074af33393791fb1ec1cbef"
5421
5436
  },
5422
5437
  {
5423
5438
  "path": "modules/ideas/templates.js",
@@ -5492,7 +5507,7 @@
5492
5507
  {
5493
5508
  "path": "modules/lifecycle/db-goals.js",
5494
5509
  "mode": "0000644",
5495
- "sha256": "31f39ea088b03feb217af4ce2fbf0b4dba0d8dad3e8eec625cf11e0dd4cb4228"
5510
+ "sha256": "f50a54a0e0f0cdeac5a2321b14757e52d9d9507417b1d1962aa09cdc6a222832"
5496
5511
  },
5497
5512
  {
5498
5513
  "path": "modules/lifecycle/db-grade.js",
@@ -5527,12 +5542,12 @@
5527
5542
  {
5528
5543
  "path": "modules/lifecycle/db-versions.js",
5529
5544
  "mode": "0000644",
5530
- "sha256": "9f777643c44ebeef8b668a8120bf74d369564f904b2cb1aff54711bd5825c9c5"
5545
+ "sha256": "8214d4a7c2e97b12e8cd9ba866b6560ac6e67446af5dcf28a0055838e32897f5"
5531
5546
  },
5532
5547
  {
5533
5548
  "path": "modules/lifecycle/db.js",
5534
5549
  "mode": "0000644",
5535
- "sha256": "61ed58e2d0dbe9aa0b889d27b6a9c38df024a40bc797f22b0ff766fedf85a9de"
5550
+ "sha256": "6d69634ad12510f81727eeed0a8fdde4eac618fedc23e43e2c1aa5835aaf0388"
5536
5551
  },
5537
5552
  {
5538
5553
  "path": "modules/lifecycle/dead-deps.js",
@@ -5557,7 +5572,7 @@
5557
5572
  {
5558
5573
  "path": "modules/lifecycle/goal-advisory.js",
5559
5574
  "mode": "0000644",
5560
- "sha256": "b9138fa63ef324b9353354e4c5398150d576dd7e3547bb6889f2f83ebd7b995f"
5575
+ "sha256": "d17e49302bde2677927467c01acc40060f0abfcb7d8361354f9b092dd9859b55"
5561
5576
  },
5562
5577
  {
5563
5578
  "path": "modules/lifecycle/goal-authz.js",
@@ -5612,7 +5627,7 @@
5612
5627
  {
5613
5628
  "path": "modules/lifecycle/lifecycle.js",
5614
5629
  "mode": "0000644",
5615
- "sha256": "6d94121968be4164151a913971f7a5e248104511ebc41ff5a95f5a1ee05af5a6"
5630
+ "sha256": "db5e823ab382d44d4ae41266cec8ff76a98d6e636fd175cea0f1a04135eec7c9"
5616
5631
  },
5617
5632
  {
5618
5633
  "path": "modules/lifecycle/merge-lock.js",
@@ -7582,12 +7597,12 @@
7582
7597
  {
7583
7598
  "path": "package-lock.json",
7584
7599
  "mode": "0000644",
7585
- "sha256": "d60a7d4e330cfd6154a5dea888aa9062d7571dee2d62aafc73cadb19b4afa604"
7600
+ "sha256": "93ff5dd9e6965597f8af514e41a02d0c5149e31e6a456576f6dc9b1bd1a773fc"
7586
7601
  },
7587
7602
  {
7588
7603
  "path": "package.json",
7589
7604
  "mode": "0000644",
7590
- "sha256": "14e7286fe47165dabdc74cb24b6bb5afdc1e7b949ae612b2f72c7a56107a246b"
7605
+ "sha256": "ed02b11ea51fdadd43ceb8fcaa9c3613ca172bf8c06caeeb0a351b8c65405900"
7591
7606
  },
7592
7607
  {
7593
7608
  "path": "public-docs/index.html",
@@ -9297,7 +9312,7 @@
9297
9312
  {
9298
9313
  "path": "src/module-api.js",
9299
9314
  "mode": "0000644",
9300
- "sha256": "b3b115c1a4652247daebb85a6c393400361c2269cb047e691121193c45a63648"
9315
+ "sha256": "d1b45d7bf6ca9667e8ef78fbb8dc255eda9bb60a27a74e48f59fb067e882d8d4"
9301
9316
  },
9302
9317
  {
9303
9318
  "path": "src/module-loader/catalog.js",
@@ -10287,7 +10302,7 @@
10287
10302
  {
10288
10303
  "path": "tests/filing_abuse_matrix.mjs",
10289
10304
  "mode": "0000644",
10290
- "sha256": "dd63216406ff8d76a6250944c03fb8e0f2028c599d01ee4720ba3dd79d547a63"
10305
+ "sha256": "2a6de0e7411bb4796fe4f301af77c33f08a99307f2298ed83cd32c38cf059dc5"
10291
10306
  },
10292
10307
  {
10293
10308
  "path": "tests/first_admin_bootstrap.mjs",
@@ -11012,7 +11027,7 @@
11012
11027
  {
11013
11028
  "path": "tests/idea_routing.mjs",
11014
11029
  "mode": "0000644",
11015
- "sha256": "39bdd91984e584f7f81f6dc096093a7a273c483e845b7c821a389472684114a3"
11030
+ "sha256": "d4d4e6ce60f1524e821e0c6c9f2844c7ba791b5502072dc00496224404afa990"
11016
11031
  },
11017
11032
  {
11018
11033
  "path": "tests/idea_spark_hall.mjs",
@@ -11234,6 +11249,11 @@
11234
11249
  "mode": "0000644",
11235
11250
  "sha256": "9a1813d6036886ea07d1563e71a466e66c28554b75b22605464c795fd9fa2ebb"
11236
11251
  },
11252
+ {
11253
+ "path": "tests/maintenance_goal.mjs",
11254
+ "mode": "0000644",
11255
+ "sha256": "645fe3e9093abcb1425a59602d6a92a6d90669775aa3503ae435a54c79eca5c1"
11256
+ },
11237
11257
  {
11238
11258
  "path": "tests/manage_manifest_shared_read.mjs",
11239
11259
  "mode": "0000644",
@@ -12477,7 +12497,7 @@
12477
12497
  {
12478
12498
  "path": "tests/task_goal_required.mjs",
12479
12499
  "mode": "0000644",
12480
- "sha256": "915711866bc194d977225326511c55ac5084b52d10a45a855bc9d6da815f4b8b"
12500
+ "sha256": "2e311d02f87c562c2e8ed7cec24100efe3d1c750cb145e9e9e6bb1dac8043228"
12481
12501
  },
12482
12502
  {
12483
12503
  "path": "tests/task_lib.mjs",
@@ -1,18 +1,38 @@
1
1
  ---
2
2
  name: idea-triage
3
- description: Daily walk through the Bongos idea_inbox, which since task 1003071 holds only HOMELESS work — bugs and goal-bound captures now route to a task at filing time and never reach the inbox. For each open idea, prompt to promote (→ task, naming a goal), discard, or merge with another idea. Triggers when the user says "/idea-triage", "triage ideas", "review the inbox", "walk the idea inbox", or runs as a scheduled daily task. Metic+ rank only (Metic and Archon; not Xenos).
3
+ description: >-
4
+ Daily walk of the Bongos idea_inbox, which holds only HOMELESS work: a capture
5
+ naming a goal, and since ADR 0262 a bug from a trusted vector, become a task at
6
+ filing time and never reach it. Promote, discard or merge each open idea.
7
+ Triggers on "/idea-triage", "triage ideas", "review the inbox", "walk the idea
8
+ inbox", or a scheduled daily run. Metic+ only.
4
9
  ---
5
10
 
6
11
  You are running the daily idea-triage session for Example. Open ideas in the Bongos `idea_inbox` table get walked one-at-a-time and verdicted, so the inbox doesn't accumulate stale entries.
7
12
 
8
- ## The inbox is HOMELESS-ONLY now (task 1003071 / R13, goal 1000071)
13
+ ## The inbox is HOMELESS-ONLY now (task 1003065 / R01 + task 1003691, goal 1000071)
9
14
 
10
- **What changed.** Since R01 (task 1003065) a bug or goal-bound capture ROUTES at
11
- filing time — it becomes a task in its goal, in one transaction, and never appears
15
+ **What changed.** Since R01 (task 1003065) a capture that NAMES A GOAL routes at
16
+ filing time — it becomes a task in that goal, in one transaction, and never appears
12
17
  here. The inbox is therefore no longer "everything anyone filed"; it holds only
13
18
  **work with no home yet**. Expect it to be smaller, and read what IS in it as
14
19
  work that genuinely needs a decision rather than a queue to clear.
15
20
 
21
+ **Bugs (task 1003691, [ADR 0262](../../../docs/adr/<redacted>.md)).**
22
+ A `kind='bug'` now routes even with NO goal named: the system files it into the
23
+ version's maintenance goal rather than making the reporter know the goal map. So a
24
+ bug should not appear in this queue — with **one deliberate exception**. Routing is
25
+ gated on the VECTOR, and Discord `#ideas` is not on the allowlist: it passes no
26
+ kind, so the classifier GUESSES one from keywords ("we should fix the copy" reads
27
+ as a bug), and a guess must not mint work (ADR 0234's owner-interview decision).
28
+ Bug reports from Discord have their own channel, `#bugs`, which does route.
29
+
30
+ > An earlier version of this section claimed bugs "never appear here" and credited
31
+ > task 1003071. That was false when written — 1003071 only touched the
32
+ > triage/promote path (commit `dab7e314`), while capture-time routing was 1003065,
33
+ > whose no-goal early return made the bug branch unreachable. If you are triaging a
34
+ > bug today, it arrived from `#ideas` or from a vector predating ADR 0262.
35
+
16
36
  **Two consequences for how you run this skill:**
17
37
 
18
38
  1. **Every promote names a goal.** The promote step now asks for one, defaulted
@@ -0,0 +1,96 @@
1
+ # ADR 0262 — A bug never lands in the inbox: the system names the home the reporter didn't
2
+
3
+ **Status:** Accepted · 2026-09-07 · task 1003691
4
+ **Supersedes nothing. Amends nothing.** It makes the explicit decision [ADR 0235](<redacted>.md) §6 said would be needed, and it keeps [ADR 0234](<redacted>.md)'s `#ideas` interview decision and [ADR 0250](<redacted>.md) D4's invariant both intact.
5
+
6
+ ## Context
7
+
8
+ A reported defect could still end up parked in `idea_inbox`, waiting for a human triage pass — the exact tax criterion C1 of goal 1000071 was opened to remove.
9
+
10
+ The cause is one line. `modules/ideas/routing.js` `decideLanding()` opened with:
11
+
12
+ ```js
13
+ if (goalId == null || goalId === '') return { route: false, reason: 'no_goal' };
14
+ ```
15
+
16
+ The `kind === 'bug'` hard-land branch sat **nine lines below** it and was therefore **unreachable without a goal**. `kind` only ever chose the routed task's *status* (`ready` for a trusted filer, `backlog` otherwise); it never chose whether the filing routed at all. So "bugs are urgent-true" was true only for a reporter who already knew which goal the defect belonged to — which is precisely the knowledge criterion C5 promises a filer never needs.
17
+
18
+ ### The conflict this task was filed over does not exist
19
+
20
+ The task was opened on the premise that two accepted ADRs contradict each other, and that one had to lose:
21
+
22
+ - **ADR 0250 D4** — "Every task belongs to a real, open goal. `goal_id` becomes required at creation." Enforced: `modules/lifecycle/db-tasks.js` throws, `POST /tasks` 400s `goal_id_required`.
23
+ - **ADR 0235** — read as exempting `kind='bug'` from needing a goal at all, on the strength of the phrase *"a fix needs no home to be filed correctly."*
24
+
25
+ **That reading of 0235 is wrong, and checking it was the first thing this task did.** ADR 0235's headline decision is the opposite:
26
+
27
+ > **A fix category is `kind='bug'` on a task that carries a real `goal_id`** — no schema change, no new column, no new goal type.
28
+
29
+ The quoted phrase describes `adviseGoal()` in `modules/lifecycle/goal-advisory.js:64`, which returns `null` — stays silent — for a `kind='bug'` task on its `no_goal`, `catch_all` and `no_criterion_link` branches. It is an **advisory suppression**: it stops the system nagging a bug about its goal. It grants nothing, and it changes no schema.
30
+
31
+ So both ADRs already require a real goal, nothing loses, and no amendment is owed. What ADR 0235 *did* anticipate is this task — §6 Consequences:
32
+
33
+ > giving `kind='bug'` more weight than an advisory exemption "is a new, explicit decision needing its own ADR."
34
+
35
+ This is that ADR.
36
+
37
+ ## Decision
38
+
39
+ **A `kind='bug'` filed from a trusted vector with no goal becomes a task in the version's maintenance goal, at the status the existing rank split already assigns.** Two halves, and both are load-bearing.
40
+
41
+ ### D1 — The home is the per-version MAINTENANCE goal, find-or-created
42
+
43
+ The reporter does not name a home; the system does. `modules/ideas/homeless-home.js` resolves the current building version (`lifecycle.currentBuildingVersionId`), then find-or-creates `"<version> — maintenance"` (`lifecycle.ensureMaintenanceGoal`).
44
+
45
+ **Not the `"<version> — general"` catch-all**, which is the obvious-looking choice and the wrong one. BV1.R11 (task 1003598) began *deleting* that fallback after 53 open tasks accumulated unread in two of them, and `db-tasks.js` warns against adding a third caller to the `allowCatchAll` opt-out it left behind. The maintenance goal is the home task 1003605 (BV1.R18) already designs for exactly this — *"where a defect against already-shipped work lands"* — so routing points at the destination the roadmap was already heading for rather than repopulating the bucket being drained. **This vector adds no `allowCatchAll` caller**, pinned by `tests/task_goal_required.mjs`.
46
+
47
+ ADR 0250 D4's invariant therefore holds exactly: the task carries a real, open `goal_id`. What changes is only *who names it*.
48
+
49
+ Three mechanics that are decisions, not implementation detail:
50
+
51
+ - **The version tiebreak is not new.** More than one version may sit at `building` — ADR 0250 D5 closes the *planning* slot only, and refusing a second building version is still unshipped (task 1003606, BV1.R19). The rule reused is `pickDefaultVersion` from `scripts/gds/version-select.js`, expressed in SQL: building only, `started_at DESC`, nulls last, `id` breaking a remaining tie.
52
+ - **Find-or-create takes a `pg_advisory_xact_lock`.** `goals` has no unique constraint on `(version_id, title)` — migration 160 used a `NOT EXISTS` guard, safe for a one-shot backfill and unsafe for a concurrent path, where two bugs reported in the same second would both insert and leave the title lookup permanently ambiguous.
53
+ - **It runs inside the caller's transaction.** A goal created on its own connection would outlive a rolled-back route as an empty goal nobody asked for.
54
+
55
+ **It fails closed.** No building version (`NO_BUILDING_VERSION`), or a version whose maintenance goal cannot be made (`NO_MAINTENANCE_GOAL`), refuses the filing and rolls back. Inventing a version is idea 1000771's exact failure — every Discord report entombed in a shipped version's catch-all — and this path exists to end that, not to repeat it.
56
+
57
+ ### D2 — The gate is the VECTOR, not the kind alone
58
+
59
+ Routing keys on an allowlist, `routing.HOMELESS_ROUTING_VECTORS = {'api', 'discord-bugs'}`, and **absent or unrecognised is untrusted**.
60
+
61
+ This is the half that keeps ADR 0234 whole. Its owner-interview decision — *"Discord `#ideas` files with HINTS only, never routes"* — is not an incidental detail: `#ideas` passes **no kind at all**, so `inbox.classify()` guesses one from keywords, and its bug pattern fires on `fix|broken|bug|regression|crash|error` anywhere in the text. *"We should fix the copy on the landing page"* classifies as a bug. Gating on kind alone would let a keyword guess mint work and would have killed an explicit owner decision as a side effect.
62
+
63
+ The two trusted members each carry a human or a channel that means *this is broken*:
64
+
65
+ | Vector | Why it is trusted |
66
+ |---|---|
67
+ | `api` | `POST /inbox`, authenticated builder, explicit `kind`. `scripts/gds/capture.js` posts to this route, so the CLI inherits it and needs no member of its own. |
68
+ | `discord-bugs` | The **channel is the classification** (`discord-bugs.js` forces `kind='bug'`), and it already carries an unlinked-author guard, a per-builder hourly rate cap, and a body screen. |
69
+
70
+ `discord-ideas` is named explicitly at its call site so its exclusion is a written fact rather than an absence someone has to notice. The vector is **server-set at every call site and never read from a request body**, which is what makes forging it a non-move.
71
+
72
+ #### A vector is claimed by a call site, so the call site must still check identity
73
+
74
+ `#bugs` sets `vector: builder ? 'discord-bugs' : null` — **conditional, not the bare string.** The first cut of this ADR set it unconditionally and that was a trust-boundary regression caught as a grader blocker, worth recording because the shape will recur.
75
+
76
+ The channel earns its place on the allowlist through two guards that are both **identity-bound**: the rank-aware landing needs a rank, and the rolling rate cap is counted per builder id. An unlinked Discord author has neither — `filerRank` is null and the goal is only sought `if (builder)` — so an unconditional vector meant they took the homeless-bug branch and minted a real `backlog` task, with the per-builder rate limiter not running at all for them. That is unattributed task creation at no cost, and it contradicts the promise at the top of `discord-bugs.js` that ADR 0047 bought: *"an unlinked author cannot inject claimable work… it no longer mints anything."*
77
+
78
+ The general rule: **being on the allowlist is a property of the DOOR, not of everyone who walks through it.** A vector attests "filings from here carry a human who means *this is broken*"; where a door admits both identified and anonymous traffic, it may only claim the vector for the identified half. With no vector the filing falls back to the inbox exactly as before — the report is kept, its images re-hosted, a human still sees it in triage. Pinned as F16, mutation-verified against the unconditional form.
79
+
80
+ Everything else the matrix already decided is unchanged and still applies to this path: the Metic+ rank split (`ready` vs `backlog`), the Full Idea refusal, and the judgment kinds. **Only bugs get a free home** — a homeless feature or cleanup is exactly the work the inbox is *for*, and fabricating homes for it would empty the inbox by lying.
81
+
82
+ ## Consequences
83
+
84
+ - **The inbox's stated identity becomes true.** `.claude/skills/idea-triage/SKILL.md` claimed bugs "never appear here" and credited task 1003071 — which only touched the triage/promote path (commit `dab7e314`); capture-time routing was task 1003065. The claim was false when written and is corrected alongside this ADR; it is now true for the `api` and `discord-bugs` vectors.
85
+ - **A bug reported in Discord `#ideas` still lands in the inbox.** That is deliberate, and it is not a gap: bug reports from Discord have their own channel, `#bugs`, which routes.
86
+ - **`#bugs`' bad-guess fallback got better.** It re-files with `goalId: null` when a suggested goal refuses the landing; that path now reaches the maintenance goal instead of the inbox, so dropping a wrong guess costs a triage pass rather than the whole report.
87
+ - **Task 1003605 (BV1.R18) is partly delivered and still owed.** This ships the maintenance goal's title, find-or-create and routing. R18 still owns auto-creating it with every version, the close-exempt flag R16 reads, and carrying open bugs forward to the successor version on close.
88
+ - **A new file on the create path** — `modules/ideas/homeless-home.js`, added to the `tests/task_goal_required.mjs` roster so the anti-drift pin covers it.
89
+
90
+ ## Alternatives rejected
91
+
92
+ - **Let a goal-less bug create a goal-less task** (an `allowCatchAll`-style escape hatch for bugs). Breaks ADR 0250 D4 one day after it was accepted, and every consumer assuming `goal_id` is non-null would need auditing. The invariant is worth more than the shortcut.
93
+ - **Reject a bug with no goal, listing open goals to pick from.** One rule, no exceptions — but a rejected report does not land *at all*, and Discord's bot fires and forgets, so every inbound bug from a channel that cannot retry would be silently lost. It also directly contradicts the thing being asked for.
94
+ - **Route into the `"<version> — general"` catch-all.** Smallest diff, reuses shipped machinery, and repopulates the exact buckets BV1.R11 was filed to drain. R18 would then have to undo it.
95
+ - **Build R18 whole first, then route.** The cleanest end state, and substantially more scope than the defect being fixed; the close-exempt flag and version-close carry-forward are independent of whether a reported bug finds a home today.
96
+ - **Gate on `kind` alone.** Cheapest fix, and it silently overturns ADR 0234's owner-interview decision by letting a keyword classifier mint tasks from an ideation channel.
@@ -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) |