@bongos/core 1.19.485 → 1.19.486
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 +57 -27
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +6 -0
- package/clients/bongos-client/index.cjs +6 -0
- package/clients/bongos-client/index.d.ts +7 -0
- package/clients/bongos-client/index.mjs +6 -0
- package/docs/adr/0232-rot-is-derived-not-swept-and-water-is-the-only-new-verb.md +155 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +147 -3
- package/docs/api-reference.md +7 -4
- package/docs/copy-inventory.md +9 -6
- package/docs/copy-registry.json +34 -7
- package/docs/module-api-changelog.md +2 -0
- package/migrations/core_231_project_settings_and_attention_water.sql +163 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +6 -0
- package/modules/hall-ui/public/goal-inbox.js +119 -1
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/rot.js +210 -0
- package/modules/lifecycle/routes/rot.js +160 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness.js +9 -0
- package/src/bongos/project-settings.js +193 -0
- package/src/module-api.js +29 -1
- package/tests/module_api.mjs +1 -0
- package/tests/rot_inbox.mjs +532 -0
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.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.486",
|
|
6
|
+
"core_contract": "1.19.486",
|
|
7
|
+
"source_commit": "41bd70c3df16dc2563b63d1cb7a5835234c1c9b0",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-02T21:04:03.491Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 422,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1852,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2298,
|
|
20
|
+
"tree_sha256": "445c2ba1051ff6eba8f6d44b827beafd384519e189bef09d0d797a09790e35f9",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -362,12 +362,12 @@
|
|
|
362
362
|
{
|
|
363
363
|
"path": "clients/bongos-client/README.md",
|
|
364
364
|
"mode": "0000644",
|
|
365
|
-
"sha256": "
|
|
365
|
+
"sha256": "8713ee79dc2598ff009c061fd3ba3078e26138a5ffe59377531b1fcd468bcbdc"
|
|
366
366
|
},
|
|
367
367
|
{
|
|
368
368
|
"path": "clients/bongos-client/bongos-client.global.js",
|
|
369
369
|
"mode": "0000644",
|
|
370
|
-
"sha256": "
|
|
370
|
+
"sha256": "f7eb7547af970135ddc0ba95e5fcb0d90c00c7a4d419d233bd205597ec490af6"
|
|
371
371
|
},
|
|
372
372
|
{
|
|
373
373
|
"path": "clients/bongos-client/examples/hello-world.mjs",
|
|
@@ -377,17 +377,17 @@
|
|
|
377
377
|
{
|
|
378
378
|
"path": "clients/bongos-client/index.cjs",
|
|
379
379
|
"mode": "0000644",
|
|
380
|
-
"sha256": "
|
|
380
|
+
"sha256": "1ebe647295155290431dd30de7f725b8ae14f6a0f5f992a9934fc1e9691adfaa"
|
|
381
381
|
},
|
|
382
382
|
{
|
|
383
383
|
"path": "clients/bongos-client/index.d.ts",
|
|
384
384
|
"mode": "0000644",
|
|
385
|
-
"sha256": "
|
|
385
|
+
"sha256": "c7de731aac661fc6af71177c150d0ab5c6df7d443001af6f9c493822fff50340"
|
|
386
386
|
},
|
|
387
387
|
{
|
|
388
388
|
"path": "clients/bongos-client/index.mjs",
|
|
389
389
|
"mode": "0000644",
|
|
390
|
-
"sha256": "
|
|
390
|
+
"sha256": "3403c459b72b36c20e13a9db9660da3b52bb3599617fe25d65df4b4952c46179"
|
|
391
391
|
},
|
|
392
392
|
{
|
|
393
393
|
"path": "clients/bongos-client/package.json",
|
|
@@ -1664,20 +1664,25 @@
|
|
|
1664
1664
|
"mode": "0000644",
|
|
1665
1665
|
"sha256": "6d47d3d5ee4e6683e1437142e3e2440d6009870bdb06bddc3b52ee210fdbade8"
|
|
1666
1666
|
},
|
|
1667
|
+
{
|
|
1668
|
+
"path": "docs/adr/0232-rot-is-derived-not-swept-and-water-is-the-only-new-verb.md",
|
|
1669
|
+
"mode": "0000644",
|
|
1670
|
+
"sha256": "240b71442e189fd1167ad0b339dc389d55b77d83aa45c676a7e29f573645b681"
|
|
1671
|
+
},
|
|
1667
1672
|
{
|
|
1668
1673
|
"path": "docs/adr/README.md",
|
|
1669
1674
|
"mode": "0000644",
|
|
1670
|
-
"sha256": "
|
|
1675
|
+
"sha256": "ae62470871555cc058b9045eb261c5ec27fb3a124eaa8293f47c894a7d1c4b37"
|
|
1671
1676
|
},
|
|
1672
1677
|
{
|
|
1673
1678
|
"path": "docs/api-reference.md",
|
|
1674
1679
|
"mode": "0000644",
|
|
1675
|
-
"sha256": "
|
|
1680
|
+
"sha256": "ac663f05ab9f10410c950e2dc55d5bd87f214ae641bcb6cafa4e8865f5580796"
|
|
1676
1681
|
},
|
|
1677
1682
|
{
|
|
1678
1683
|
"path": "docs/api/openapi.json",
|
|
1679
1684
|
"mode": "0000644",
|
|
1680
|
-
"sha256": "
|
|
1685
|
+
"sha256": "1b2567b2c3a5a50b447e07a46add1aedb414e04c1911c4879eff878d0144ebbe"
|
|
1681
1686
|
},
|
|
1682
1687
|
{
|
|
1683
1688
|
"path": "docs/architecture.md",
|
|
@@ -1702,12 +1707,12 @@
|
|
|
1702
1707
|
{
|
|
1703
1708
|
"path": "docs/copy-inventory.md",
|
|
1704
1709
|
"mode": "0000644",
|
|
1705
|
-
"sha256": "
|
|
1710
|
+
"sha256": "67a664891add6e7a4b3cc3d882c8b579ec31948e4c1d2842ff58a979164482fb"
|
|
1706
1711
|
},
|
|
1707
1712
|
{
|
|
1708
1713
|
"path": "docs/copy-registry.json",
|
|
1709
1714
|
"mode": "0000644",
|
|
1710
|
-
"sha256": "
|
|
1715
|
+
"sha256": "fc6ea5a69dba52644c8b45114c238d2410ba5542339e1e86783053577f829f5c"
|
|
1711
1716
|
},
|
|
1712
1717
|
{
|
|
1713
1718
|
"path": "docs/design/apex-pass-2-direction.md",
|
|
@@ -2157,7 +2162,7 @@
|
|
|
2157
2162
|
{
|
|
2158
2163
|
"path": "docs/module-api-changelog.md",
|
|
2159
2164
|
"mode": "0000644",
|
|
2160
|
-
"sha256": "
|
|
2165
|
+
"sha256": "b6572054df616752848e0b725e71aa8a38aa150ab75c0afcf814b89c4cca3bdc"
|
|
2161
2166
|
},
|
|
2162
2167
|
{
|
|
2163
2168
|
"path": "docs/modules-contract.md",
|
|
@@ -3129,6 +3134,11 @@
|
|
|
3129
3134
|
"mode": "0000644",
|
|
3130
3135
|
"sha256": "77b7a9dee4de54743ecea5e1f326521c4483928317882f9e214f3a6f7db07e8c"
|
|
3131
3136
|
},
|
|
3137
|
+
{
|
|
3138
|
+
"path": "migrations/core_231_project_settings_and_attention_water.sql",
|
|
3139
|
+
"mode": "0000644",
|
|
3140
|
+
"sha256": "8ae32be783a4ca41637f56626bd83375aa4ae0755e701a0c0170eada8d8303b8"
|
|
3141
|
+
},
|
|
3132
3142
|
{
|
|
3133
3143
|
"path": "modules/agents/migrations/agents_001_definitions.sql",
|
|
3134
3144
|
"mode": "0000644",
|
|
@@ -3512,7 +3522,7 @@
|
|
|
3512
3522
|
{
|
|
3513
3523
|
"path": "modules/dev-box/app/src/vendor/bongos-client.cjs",
|
|
3514
3524
|
"mode": "0000644",
|
|
3515
|
-
"sha256": "
|
|
3525
|
+
"sha256": "1ebe647295155290431dd30de7f725b8ae14f6a0f5f992a9934fc1e9691adfaa"
|
|
3516
3526
|
},
|
|
3517
3527
|
{
|
|
3518
3528
|
"path": "modules/dev-box/app/test/unit.mjs",
|
|
@@ -4202,7 +4212,7 @@
|
|
|
4202
4212
|
{
|
|
4203
4213
|
"path": "modules/hall-ui/public/goal-inbox.js",
|
|
4204
4214
|
"mode": "0000644",
|
|
4205
|
-
"sha256": "
|
|
4215
|
+
"sha256": "524a475dd6ab05e805e8db6baf11c90e10a3e6e76d7a423ffe571cafe61c0324"
|
|
4206
4216
|
},
|
|
4207
4217
|
{
|
|
4208
4218
|
"path": "modules/hall-ui/public/goals-lib.js",
|
|
@@ -4967,7 +4977,7 @@
|
|
|
4967
4977
|
{
|
|
4968
4978
|
"path": "modules/lifecycle/module.json",
|
|
4969
4979
|
"mode": "0000644",
|
|
4970
|
-
"sha256": "
|
|
4980
|
+
"sha256": "c18bea123294d549c14b33d121c911e737fa10d12b62d9fe25d73d2387b2f4c1"
|
|
4971
4981
|
},
|
|
4972
4982
|
{
|
|
4973
4983
|
"path": "modules/lifecycle/pollers/publish-reconciler.js",
|
|
@@ -4979,6 +4989,11 @@
|
|
|
4979
4989
|
"mode": "0000644",
|
|
4980
4990
|
"sha256": "28c61d55fe2b492f0fce4c99b60e88f0cbeb542c47c04482558818d65628e0d7"
|
|
4981
4991
|
},
|
|
4992
|
+
{
|
|
4993
|
+
"path": "modules/lifecycle/rot.js",
|
|
4994
|
+
"mode": "0000644",
|
|
4995
|
+
"sha256": "c8005a083511afacb6d9ec801050e679bf86c0a01a7d5a5fd987edf7509cb829"
|
|
4996
|
+
},
|
|
4982
4997
|
{
|
|
4983
4998
|
"path": "modules/lifecycle/routes/analytics.js",
|
|
4984
4999
|
"mode": "0000644",
|
|
@@ -5039,6 +5054,11 @@
|
|
|
5039
5054
|
"mode": "0000644",
|
|
5040
5055
|
"sha256": "a8e851ca630a24b197e7ecb8f8cd8ac3d169801bacbac72097528500f95d6cdb"
|
|
5041
5056
|
},
|
|
5057
|
+
{
|
|
5058
|
+
"path": "modules/lifecycle/routes/rot.js",
|
|
5059
|
+
"mode": "0000644",
|
|
5060
|
+
"sha256": "2ab5931bd936c88a1e3a4339d927ef537e998f9756fc3260671f2bf701cd7dfa"
|
|
5061
|
+
},
|
|
5042
5062
|
{
|
|
5043
5063
|
"path": "modules/lifecycle/routes/task-route-helpers.js",
|
|
5044
5064
|
"mode": "0000644",
|
|
@@ -6767,12 +6787,12 @@
|
|
|
6767
6787
|
{
|
|
6768
6788
|
"path": "package-lock.json",
|
|
6769
6789
|
"mode": "0000644",
|
|
6770
|
-
"sha256": "
|
|
6790
|
+
"sha256": "a8eb93a585eb4f91a9e25a291c59f74b50695164b4784695851bacc4a9efed68"
|
|
6771
6791
|
},
|
|
6772
6792
|
{
|
|
6773
6793
|
"path": "package.json",
|
|
6774
6794
|
"mode": "0000644",
|
|
6775
|
-
"sha256": "
|
|
6795
|
+
"sha256": "2a687952d76dd8206fd83ca58f13f9faf4229de2b2f0e7232d21ec82c2ac570d"
|
|
6776
6796
|
},
|
|
6777
6797
|
{
|
|
6778
6798
|
"path": "public-docs/index.html",
|
|
@@ -7202,7 +7222,7 @@
|
|
|
7202
7222
|
{
|
|
7203
7223
|
"path": "scripts/gds/fitness.js",
|
|
7204
7224
|
"mode": "0000644",
|
|
7205
|
-
"sha256": "
|
|
7225
|
+
"sha256": "4cdce5dcc1a160d1ce3c6ac04f6dd778042c115657175fbcfbb7dfcabb4aa204"
|
|
7206
7226
|
},
|
|
7207
7227
|
{
|
|
7208
7228
|
"path": "scripts/gds/gate-review.js",
|
|
@@ -8279,6 +8299,11 @@
|
|
|
8279
8299
|
"mode": "0000644",
|
|
8280
8300
|
"sha256": "c9edcf9cb1f027ca122388beabae488d2b771b9e7aee2cd12fbdb48ea6f0ded8"
|
|
8281
8301
|
},
|
|
8302
|
+
{
|
|
8303
|
+
"path": "src/bongos/project-settings.js",
|
|
8304
|
+
"mode": "0000644",
|
|
8305
|
+
"sha256": "a33925c65c1c9e909eceaf57e17834506d923b34a39aa751418dfaedc095fc65"
|
|
8306
|
+
},
|
|
8282
8307
|
{
|
|
8283
8308
|
"path": "src/bongos/repo-info.js",
|
|
8284
8309
|
"mode": "0000644",
|
|
@@ -8427,7 +8452,7 @@
|
|
|
8427
8452
|
{
|
|
8428
8453
|
"path": "src/module-api.js",
|
|
8429
8454
|
"mode": "0000644",
|
|
8430
|
-
"sha256": "
|
|
8455
|
+
"sha256": "99a228e6d22d78a725121714a65ca189608b2524d3a7175725551e9d41bd298e"
|
|
8431
8456
|
},
|
|
8432
8457
|
{
|
|
8433
8458
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10322,7 +10347,7 @@
|
|
|
10322
10347
|
{
|
|
10323
10348
|
"path": "tests/module_api.mjs",
|
|
10324
10349
|
"mode": "0000644",
|
|
10325
|
-
"sha256": "
|
|
10350
|
+
"sha256": "839cbdc51d53f6719671ff1f2caa431d52a8b76636cd7973350000510510779b"
|
|
10326
10351
|
},
|
|
10327
10352
|
{
|
|
10328
10353
|
"path": "tests/module_catalog.mjs",
|
|
@@ -10814,6 +10839,11 @@
|
|
|
10814
10839
|
"mode": "0000644",
|
|
10815
10840
|
"sha256": "34bb386f994042067d9efa33e9b8840b0764bd4046541ccff9f1c7e4723fadf0"
|
|
10816
10841
|
},
|
|
10842
|
+
{
|
|
10843
|
+
"path": "tests/rot_inbox.mjs",
|
|
10844
|
+
"mode": "0000644",
|
|
10845
|
+
"sha256": "9948b3095f6d49cf3f323aaa448825e2f48b652b330b585ebbf35e71317f71f2"
|
|
10846
|
+
},
|
|
10817
10847
|
{
|
|
10818
10848
|
"path": "tests/route_permission_check.mjs",
|
|
10819
10849
|
"mode": "0000644",
|
|
@@ -5,7 +5,7 @@ A **generated**, zero-dependency typed client for the Bongos API — produced fr
|
|
|
5
5
|
by hand; it regenerates when the spec changes, so it can never drift from the routes.
|
|
6
6
|
|
|
7
7
|
- API version: **v1** (served at `/api/bongos/v1`)
|
|
8
|
-
-
|
|
8
|
+
- 344 operations across 53 resource groups
|
|
9
9
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
@@ -387,6 +387,8 @@ function createClient(opts = {}) {
|
|
|
387
387
|
postGoalsIdTransfer: (args) => request("POST", "/goals/{id}/transfer", { hasBody: true }, args),
|
|
388
388
|
// PATCH /goals/{id}/visibility — rank: any-builder — PATCH /goals/:id/visibility
|
|
389
389
|
patchGoalsIdVisibility: (args) => request("PATCH", "/goals/{id}/visibility", { hasBody: true }, args),
|
|
390
|
+
// POST /goals/{id}/water — rank: any-builder — POST /goals/:id/water
|
|
391
|
+
postGoalsIdWater: (args) => request("POST", "/goals/{id}/water", { hasBody: true }, args),
|
|
390
392
|
// GET /goals/inbox — rank: any-builder — GET /goals/inbox
|
|
391
393
|
getGoalsInbox: (args) => request("GET", "/goals/inbox", { hasBody: false }, args),
|
|
392
394
|
// POST /goals/reorder — rank: metic+archon — POST /goals/reorder
|
|
@@ -467,6 +469,8 @@ function createClient(opts = {}) {
|
|
|
467
469
|
getInboxAwaitingNod: (args) => request("GET", "/inbox/awaiting-nod", { hasBody: false }, args),
|
|
468
470
|
// GET /inbox/by-builder/{id} — rank: any-builder — GET /inbox/by-builder/:id
|
|
469
471
|
getInboxByBuilderId: (args) => request("GET", "/inbox/by-builder/{id}", { hasBody: false }, args),
|
|
472
|
+
// GET /inbox/rotting — rank: any-builder — GET /inbox/rotting
|
|
473
|
+
getInboxRotting: (args) => request("GET", "/inbox/rotting", { hasBody: false }, args),
|
|
470
474
|
// POST /inbox/score — rank: any-builder — POST /inbox/score
|
|
471
475
|
postInboxScore: (args) => request("POST", "/inbox/score", { hasBody: true }, args),
|
|
472
476
|
// GET /inbox/sparks — rank: any-builder — GET /inbox/sparks
|
|
@@ -839,6 +843,8 @@ function createClient(opts = {}) {
|
|
|
839
843
|
postTasksIdVisual: (args) => request("POST", "/tasks/{id}/visual", { hasBody: true }, args),
|
|
840
844
|
// POST /tasks/{id}/vote — rank: metic+archon — POST /tasks/:id/vote
|
|
841
845
|
postTasksIdVote: (args) => request("POST", "/tasks/{id}/vote", { hasBody: true }, args),
|
|
846
|
+
// POST /tasks/{id}/water — rank: any-builder — POST /tasks/:id/water
|
|
847
|
+
postTasksIdWater: (args) => request("POST", "/tasks/{id}/water", { hasBody: true }, args),
|
|
842
848
|
// GET /tasks/claimable — rank: any-builder — GET /tasks/claimable
|
|
843
849
|
getTasksClaimable: (args) => request("GET", "/tasks/claimable", { hasBody: false }, args),
|
|
844
850
|
// GET /tasks/newcomer-floor — rank: any-builder — GET /tasks/newcomer-floor
|
|
@@ -386,6 +386,8 @@ function createClient(opts = {}) {
|
|
|
386
386
|
postGoalsIdTransfer: (args) => request("POST", "/goals/{id}/transfer", { hasBody: true }, args),
|
|
387
387
|
// PATCH /goals/{id}/visibility — rank: any-builder — PATCH /goals/:id/visibility
|
|
388
388
|
patchGoalsIdVisibility: (args) => request("PATCH", "/goals/{id}/visibility", { hasBody: true }, args),
|
|
389
|
+
// POST /goals/{id}/water — rank: any-builder — POST /goals/:id/water
|
|
390
|
+
postGoalsIdWater: (args) => request("POST", "/goals/{id}/water", { hasBody: true }, args),
|
|
389
391
|
// GET /goals/inbox — rank: any-builder — GET /goals/inbox
|
|
390
392
|
getGoalsInbox: (args) => request("GET", "/goals/inbox", { hasBody: false }, args),
|
|
391
393
|
// POST /goals/reorder — rank: metic+archon — POST /goals/reorder
|
|
@@ -466,6 +468,8 @@ function createClient(opts = {}) {
|
|
|
466
468
|
getInboxAwaitingNod: (args) => request("GET", "/inbox/awaiting-nod", { hasBody: false }, args),
|
|
467
469
|
// GET /inbox/by-builder/{id} — rank: any-builder — GET /inbox/by-builder/:id
|
|
468
470
|
getInboxByBuilderId: (args) => request("GET", "/inbox/by-builder/{id}", { hasBody: false }, args),
|
|
471
|
+
// GET /inbox/rotting — rank: any-builder — GET /inbox/rotting
|
|
472
|
+
getInboxRotting: (args) => request("GET", "/inbox/rotting", { hasBody: false }, args),
|
|
469
473
|
// POST /inbox/score — rank: any-builder — POST /inbox/score
|
|
470
474
|
postInboxScore: (args) => request("POST", "/inbox/score", { hasBody: true }, args),
|
|
471
475
|
// GET /inbox/sparks — rank: any-builder — GET /inbox/sparks
|
|
@@ -838,6 +842,8 @@ function createClient(opts = {}) {
|
|
|
838
842
|
postTasksIdVisual: (args) => request("POST", "/tasks/{id}/visual", { hasBody: true }, args),
|
|
839
843
|
// POST /tasks/{id}/vote — rank: metic+archon — POST /tasks/:id/vote
|
|
840
844
|
postTasksIdVote: (args) => request("POST", "/tasks/{id}/vote", { hasBody: true }, args),
|
|
845
|
+
// POST /tasks/{id}/water — rank: any-builder — POST /tasks/:id/water
|
|
846
|
+
postTasksIdWater: (args) => request("POST", "/tasks/{id}/water", { hasBody: true }, args),
|
|
841
847
|
// GET /tasks/claimable — rank: any-builder — GET /tasks/claimable
|
|
842
848
|
getTasksClaimable: (args) => request("GET", "/tasks/claimable", { hasBody: false }, args),
|
|
843
849
|
// GET /tasks/newcomer-floor — rank: any-builder — GET /tasks/newcomer-floor
|
|
@@ -63,6 +63,7 @@ export interface GetInboxAwaitingNodResponse { awaiting: unknown; count: unknown
|
|
|
63
63
|
export interface GetInboxByBuilderIdResponse { builder_id: unknown; ideas: unknown; total: unknown; limit: unknown; offset: unknown }
|
|
64
64
|
export interface GetInboxIdResponse { idea: unknown }
|
|
65
65
|
export interface GetInboxResponse { ideas: unknown; open_count: unknown }
|
|
66
|
+
export interface GetInboxRottingResponse { rows?: unknown; count?: unknown; scope?: unknown; timer?: unknown; cutoff?: unknown; ok?: boolean; tier?: unknown; id?: unknown; watered_at?: unknown }
|
|
66
67
|
export interface GetInboxSparksResponse { sparks: unknown; count: unknown }
|
|
67
68
|
export interface GetInboxTemplateResponse { template: unknown }
|
|
68
69
|
export interface GetLeaderboardResponse { leaderboard: unknown; page: unknown }
|
|
@@ -694,6 +695,8 @@ export interface BongosClient {
|
|
|
694
695
|
postGoalsIdTransfer(args?: RequestArgs & { body?: PostGoalsIdTransferRequest }): Promise<PostGoalsIdTransferResponse>;
|
|
695
696
|
/** PATCH /goals/{id}/visibility — rank: any-builder */
|
|
696
697
|
patchGoalsIdVisibility(args?: RequestArgs): Promise<ApiResponse>;
|
|
698
|
+
/** POST /goals/{id}/water — rank: any-builder */
|
|
699
|
+
postGoalsIdWater(args?: RequestArgs): Promise<ApiResponse>;
|
|
697
700
|
/** GET /goals/inbox — rank: any-builder */
|
|
698
701
|
getGoalsInbox(args?: RequestArgs): Promise<ApiResponse>;
|
|
699
702
|
/** POST /goals/reorder — rank: metic+archon */
|
|
@@ -774,6 +777,8 @@ export interface BongosClient {
|
|
|
774
777
|
getInboxAwaitingNod(args?: RequestArgs): Promise<GetInboxAwaitingNodResponse>;
|
|
775
778
|
/** GET /inbox/by-builder/{id} — rank: any-builder */
|
|
776
779
|
getInboxByBuilderId(args?: RequestArgs): Promise<GetInboxByBuilderIdResponse>;
|
|
780
|
+
/** GET /inbox/rotting — rank: any-builder */
|
|
781
|
+
getInboxRotting(args?: RequestArgs): Promise<GetInboxRottingResponse>;
|
|
777
782
|
/** POST /inbox/score — rank: any-builder */
|
|
778
783
|
postInboxScore(args: RequestArgs & { body: PostInboxScoreRequest }): Promise<PostInboxScoreResponse>;
|
|
779
784
|
/** GET /inbox/sparks — rank: any-builder */
|
|
@@ -1146,6 +1151,8 @@ export interface BongosClient {
|
|
|
1146
1151
|
postTasksIdVisual(args: RequestArgs & { body: PostTasksIdVisualRequest }): Promise<PostTasksIdVisualResponse>;
|
|
1147
1152
|
/** POST /tasks/{id}/vote — rank: metic+archon */
|
|
1148
1153
|
postTasksIdVote(args: RequestArgs & { body: PostTasksIdVoteRequest }): Promise<PostTasksIdVoteResponse>;
|
|
1154
|
+
/** POST /tasks/{id}/water — rank: any-builder */
|
|
1155
|
+
postTasksIdWater(args?: RequestArgs): Promise<ApiResponse>;
|
|
1149
1156
|
/** GET /tasks/claimable — rank: any-builder */
|
|
1150
1157
|
getTasksClaimable(args?: RequestArgs): Promise<GetTasksClaimableResponse>;
|
|
1151
1158
|
/** GET /tasks/newcomer-floor — rank: any-builder */
|
|
@@ -383,6 +383,8 @@ export function createClient(opts = {}) {
|
|
|
383
383
|
postGoalsIdTransfer: (args) => request("POST", "/goals/{id}/transfer", { hasBody: true }, args),
|
|
384
384
|
// PATCH /goals/{id}/visibility — rank: any-builder — PATCH /goals/:id/visibility
|
|
385
385
|
patchGoalsIdVisibility: (args) => request("PATCH", "/goals/{id}/visibility", { hasBody: true }, args),
|
|
386
|
+
// POST /goals/{id}/water — rank: any-builder — POST /goals/:id/water
|
|
387
|
+
postGoalsIdWater: (args) => request("POST", "/goals/{id}/water", { hasBody: true }, args),
|
|
386
388
|
// GET /goals/inbox — rank: any-builder — GET /goals/inbox
|
|
387
389
|
getGoalsInbox: (args) => request("GET", "/goals/inbox", { hasBody: false }, args),
|
|
388
390
|
// POST /goals/reorder — rank: metic+archon — POST /goals/reorder
|
|
@@ -463,6 +465,8 @@ export function createClient(opts = {}) {
|
|
|
463
465
|
getInboxAwaitingNod: (args) => request("GET", "/inbox/awaiting-nod", { hasBody: false }, args),
|
|
464
466
|
// GET /inbox/by-builder/{id} — rank: any-builder — GET /inbox/by-builder/:id
|
|
465
467
|
getInboxByBuilderId: (args) => request("GET", "/inbox/by-builder/{id}", { hasBody: false }, args),
|
|
468
|
+
// GET /inbox/rotting — rank: any-builder — GET /inbox/rotting
|
|
469
|
+
getInboxRotting: (args) => request("GET", "/inbox/rotting", { hasBody: false }, args),
|
|
466
470
|
// POST /inbox/score — rank: any-builder — POST /inbox/score
|
|
467
471
|
postInboxScore: (args) => request("POST", "/inbox/score", { hasBody: true }, args),
|
|
468
472
|
// GET /inbox/sparks — rank: any-builder — GET /inbox/sparks
|
|
@@ -835,6 +839,8 @@ export function createClient(opts = {}) {
|
|
|
835
839
|
postTasksIdVisual: (args) => request("POST", "/tasks/{id}/visual", { hasBody: true }, args),
|
|
836
840
|
// POST /tasks/{id}/vote — rank: metic+archon — POST /tasks/:id/vote
|
|
837
841
|
postTasksIdVote: (args) => request("POST", "/tasks/{id}/vote", { hasBody: true }, args),
|
|
842
|
+
// POST /tasks/{id}/water — rank: any-builder — POST /tasks/:id/water
|
|
843
|
+
postTasksIdWater: (args) => request("POST", "/tasks/{id}/water", { hasBody: true }, args),
|
|
838
844
|
// GET /tasks/claimable — rank: any-builder — GET /tasks/claimable
|
|
839
845
|
getTasksClaimable: (args) => request("GET", "/tasks/claimable", { hasBody: false }, args),
|
|
840
846
|
// GET /tasks/newcomer-floor — rank: any-builder — GET /tasks/newcomer-floor
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# 0232 — Rot is derived, not swept; and `water` is the only new verb
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted
|
|
4
|
+
- **Date:** 2026-09-02
|
|
5
|
+
- **Tasks:** [#1003279](https://cloudbongos.com/builders#/task/1003279)
|
|
6
|
+
- **Goal:** [#1000072](https://cloudbongos.com/builders#/goal/1000072) — Human legibility, criterion `legibility-sort-and-staleness`.
|
|
7
|
+
- **Completes:** [ADR 0226](<redacted>.md), which split the *claim* timer out of this task and recorded that "1003279 stays open for its inbox half". This is that half.
|
|
8
|
+
- **Builds on:** [#1003278](https://cloudbongos.com/builders#/task/1003278) (the shared *task* staleness vocabulary the boards use — §3 below is why this reuses its definition rather than writing a second one).
|
|
9
|
+
- **Unblocks:** [#1003280](https://cloudbongos.com/builders#/task/1003280), whose settings page needs the timer to be "live, DB-backed" and warns "do not fork a second store". §4 is that store.
|
|
10
|
+
|
|
11
|
+
## 1. What the owner decided, and what was rejected
|
|
12
|
+
|
|
13
|
+
Goal 1000072's rotting half asks that "untouched-for-X triggers a configured action
|
|
14
|
+
rather than a silent decay". The owner's decision (2026-08-25) fixed *what the action
|
|
15
|
+
is*: a **needs-attention item** — one line in the hall's existing inbox, addressed to
|
|
16
|
+
whoever created the thing and to the owner, offering four human verbs: **prune, kill,
|
|
17
|
+
prioritise, water** (water = leave it, and reset the clock). Nothing changes status on
|
|
18
|
+
its own; a human decides.
|
|
19
|
+
|
|
20
|
+
Two alternatives were rejected, and the reasons matter because both are the obvious
|
|
21
|
+
implementation:
|
|
22
|
+
|
|
23
|
+
| Rejected | Why |
|
|
24
|
+
|---|---|
|
|
25
|
+
| Auto-demote a stale thing to a review queue | It mutates real work with no human in the loop, and a wrong timer then silently freezes it. The failure is invisible: nobody notices work that stopped being offered. |
|
|
26
|
+
| A nightly digest | A notification is not an action. It gets scrolled past, and a digest you scroll past is indistinguishable from the silent decay it was meant to fix. |
|
|
27
|
+
|
|
28
|
+
## 2. The decision that shaped everything else: **rot is derived, not swept**
|
|
29
|
+
|
|
30
|
+
The task's own build note asked for "a poller/routine that is idempotent (one open item
|
|
31
|
+
per stale thing, not one per sweep)". That requirement is real — an inbox that accretes
|
|
32
|
+
a duplicate line per sweep is unusable within a week — but the sweep is not what
|
|
33
|
+
delivers it, and building one would have made the requirement *harder* to satisfy.
|
|
34
|
+
|
|
35
|
+
**There is no `attention_items` table and no poller.** A line is a `SELECT`: the thing's
|
|
36
|
+
rot clock compared against the timer, evaluated at read time. Everything the owner's
|
|
37
|
+
note asks for then falls out of arithmetic rather than out of code that must be written
|
|
38
|
+
correctly and then stay correct:
|
|
39
|
+
|
|
40
|
+
- *exactly one item per stale thing* — a row either satisfies the predicate or it does
|
|
41
|
+
not; there is nothing to duplicate.
|
|
42
|
+
- *the item clears when the thing is touched* — a real edit moves `updated_at`.
|
|
43
|
+
- *the item clears when it is decided* — prune/kill/prioritise change `status`, and the
|
|
44
|
+
status filter stops matching.
|
|
45
|
+
- *water resets the clock* — the water mark is a term in the clock.
|
|
46
|
+
|
|
47
|
+
There is also no window in which the inbox and the database disagree, no reconciliation,
|
|
48
|
+
and nothing to garbage-collect. **A stored item would have been a cache of a query whose
|
|
49
|
+
inputs it could not observe** — and keeping that cache honest is precisely the class of
|
|
50
|
+
bug the sweep was being asked to avoid.
|
|
51
|
+
|
|
52
|
+
The cost, stated plainly: the rot list is computed per request rather than materialised.
|
|
53
|
+
That is affordable here because the predicate is indexed (`idx_tasks_attention_rot`,
|
|
54
|
+
`idx_goals_attention_rot` in migration `core_231`) and the read is capped. If the corpus
|
|
55
|
+
ever outgrows it, the fix is a materialised view over the same predicate — still not an
|
|
56
|
+
items table, because the derivation is the part worth keeping.
|
|
57
|
+
|
|
58
|
+
## 3. What can rot — and the exclusions, which are the load-bearing part
|
|
59
|
+
|
|
60
|
+
Rot is **work nobody has picked up**, not work in flight. Tasks at `backlog` or `ready`;
|
|
61
|
+
goals at `open`. Every exclusion is excluded because *another surface already owns that
|
|
62
|
+
question*, and two surfaces asking one question with two different timers is exactly the
|
|
63
|
+
failure [ADR 0226](<redacted>.md) was
|
|
64
|
+
written to end:
|
|
65
|
+
|
|
66
|
+
| Excluded | Who owns it instead |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `active` | the **claim-silence timer** (`src/stale-timer.js`) — the sweeper releases it, the Archon roster reports it |
|
|
69
|
+
| `blocked` | the blocker, and `/blocker-review` as its cadence |
|
|
70
|
+
| `completed`, `confirmed` | the ship pipeline — the publish reconciler and the "Stranded ships" line |
|
|
71
|
+
| `shipped`, `abandoned` | terminal; nothing rots after it is done |
|
|
72
|
+
|
|
73
|
+
`active` is the one that would have been easy to get wrong, and the one that matters
|
|
74
|
+
most: a claimed-but-quiet task is *already* reported, in hours, by a surface that acts
|
|
75
|
+
on it. Reporting it again here, in days, would have put the same rot in two inboxes
|
|
76
|
+
under two numbers — re-creating the 6h-vs-24h split one tier up. `tests/rot_inbox.mjs`
|
|
77
|
+
asserts the exclusion directly, so a later "why isn't my active task listed?" cannot be
|
|
78
|
+
fixed by deleting the reason.
|
|
79
|
+
|
|
80
|
+
**The clock is not a new definition of "touched".** `modules/hall-ui/public/task-lib.js`
|
|
81
|
+
already publishes one for the boards (task 1003278): `max(updated_at, shipped_at)`,
|
|
82
|
+
folding in `shipped_at` because a ship path can stamp it without touching `updated_at`.
|
|
83
|
+
This adds a third term and nothing else:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
tasks: GREATEST(updated_at, COALESCE(shipped_at, updated_at), COALESCE(attention_watered_at, updated_at))
|
|
87
|
+
goals: GREATEST(updated_at, COALESCE(attention_watered_at, updated_at))
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Had the server invented its own two-term clock, the board and the inbox would disagree
|
|
91
|
+
about which rows are stale. Two tests pin this: one asserts the domain's expression
|
|
92
|
+
appears **verbatim** in the migration's index expressions (drift there is silent — the
|
|
93
|
+
query simply stops using the index), and one asserts `task-lib.js` still defines
|
|
94
|
+
"touched" the way the server assumes.
|
|
95
|
+
|
|
96
|
+
## 4. The timer is DB-backed, and it is a *second* timer on purpose
|
|
97
|
+
|
|
98
|
+
ADR 0226's whole subject is that one question must have one answer. So adding a second
|
|
99
|
+
timer needs a defence, and it is that these are **two questions**:
|
|
100
|
+
|
|
101
|
+
| | claim-silence (`src/stale-timer.js`) | rot (this ADR) |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| measures | a builder who went quiet | work nobody picked up |
|
|
104
|
+
| unit | hours | days |
|
|
105
|
+
| configured in | env (a redeploy) | `project_settings` (a live knob) |
|
|
106
|
+
| consequence | the machine **releases** the claim, alone | the machine **asks** a human |
|
|
107
|
+
|
|
108
|
+
The store is a generic key/value table rather than a column per knob, because task
|
|
109
|
+
1003280 puts five fundamentals on one page and a knob-per-migration would make that page
|
|
110
|
+
a schema change every time. `project_settings` is created here, unseeded: **no row means
|
|
111
|
+
"never configured, use the code default"**, which is what lets a fresh instance boot on
|
|
112
|
+
an empty table and what lets the settings page tell "never touched" apart from
|
|
113
|
+
"deliberately set to the default value".
|
|
114
|
+
|
|
115
|
+
Precedence mirrors ADR 0226 exactly — `override` (a per-request lens, never written
|
|
116
|
+
back) > the DB row > the code default — and so does the **fail posture**, on the
|
|
117
|
+
reporting side of that ADR's asymmetry: a value that fails its range check degrades to
|
|
118
|
+
the default **with a receipt**, which the inbox renders in its own heading. A typo shows
|
|
119
|
+
up on the screen of the person who can fix it instead of 500-ing the inbox they were
|
|
120
|
+
using to find the problem. One case is deliberately *not* treated as a bad value: an
|
|
121
|
+
unreachable database is a failure to read, not a misconfiguration, and it must not blame
|
|
122
|
+
the operator for something they did not type.
|
|
123
|
+
|
|
124
|
+
## 5. Three of the four verbs add no authority to the system
|
|
125
|
+
|
|
126
|
+
Only `water` is a new route. `prioritise`, `prune` and `kill` reuse `POST
|
|
127
|
+
/tasks/:id/promote`, `/demote` and `/abandon` (and `/goals/:id/archive`) — the precedent
|
|
128
|
+
the nod slice set in this same card (task 1003066), and the reason three quarters of
|
|
129
|
+
this feature introduces no new permission at all.
|
|
130
|
+
|
|
131
|
+
The verbs are **served with each row** rather than hardcoded in the client. That is not
|
|
132
|
+
decoration: the tier→verb mapping already differs (a goal gets two verbs, because
|
|
133
|
+
promote/demote are task-lifecycle transitions a goal has no equivalent of), and a
|
|
134
|
+
hardcoded list in the hall would be a second copy of that mapping, free to drift into
|
|
135
|
+
rendering a button the server refuses. A test asserts every advertised verb names a
|
|
136
|
+
route that actually exists.
|
|
137
|
+
|
|
138
|
+
`water` itself is gated on **ownership OR the atom**: the creator may always water their
|
|
139
|
+
own line, and `builder.roster.read` — "may read another builder's record off the roster",
|
|
140
|
+
the atom `/builders/:id/active-claims` already carries — covers the owner. That check
|
|
141
|
+
runs *inside* the handler rather than as middleware, because a creator holding no atom
|
|
142
|
+
must still get through, and a gate mounted in front would refuse them before ownership
|
|
143
|
+
is known. It fails closed: no governance port means no permission (ADR 0016).
|
|
144
|
+
|
|
145
|
+
## 6. Consequences
|
|
146
|
+
|
|
147
|
+
- The hall's needs-attention card gains one slice, `#attention-rot`, placed last: every
|
|
148
|
+
slice above it is somebody waiting on you *now*, while rot has already waited weeks.
|
|
149
|
+
- `project_settings` exists and has exactly one key. Task 1003280 adds rows, not tables.
|
|
150
|
+
- `tasks` and `goals` each gain two nullable columns. No backfill: never-watered is the
|
|
151
|
+
honest state for every existing row.
|
|
152
|
+
- The rot list is per-request. §2 names the escape hatch if that ever stops being cheap.
|
|
153
|
+
- **A future third timer needs its own ADR.** §4 is the template for defending one: name
|
|
154
|
+
the question, the unit, the store and the consequence, and show they differ from both
|
|
155
|
+
of the existing two. If they do not, it is not a new timer — it is a second answer.
|
package/docs/adr/README.md
CHANGED
|
@@ -318,5 +318,6 @@ This keeps the decision history honest and traceable.
|
|
|
318
318
|
| 0230 | [**A look skill is a composition grammar; the palette is the pack and the texture is a library entry** ([task 1003327](https://cloudbongos.com/builders#/task/1003327) · goal 1000083, criterion `uidesign-skills-in-house`). Four groups of the taste-skill rebuild spec ship a *look*, not a method, and upstream each one shipped a palette and a set of faces — which is the one thing the fifteen-token contract forbids (ADR 0196, 0219). Decisions: (1) a look skill ships **structure, rhythm, type roles, component anatomy, motion and bans, and no colours** — every colour a `var()` or `color-mix()` of the fifteen, the palette the instance's pack, the texture a named entry in `modules/ui-design/styles/`, and no skill adds a face. (2) The brutalist "one substrate per project" becomes an **archetype** deciding which mode the surface is composed *for*, while the other mode ships complete and measured; "never mixed in one interface" is the part that was about craft. (3) `gpt-taste`'s prompt-seeded **random** selection becomes a deterministic one derived from the route and bumped against sibling surfaces, written into a plan block before any markup with the hero arithmetic, the grid-density proof and the measured contrast pairs — a roll nobody can reproduce is not a decision. (4) `stitch-design-taste` **transcribes** the world instead of authoring one: the platform's own `DESIGN.md` frontmatter shape, the thirteen diffed from the pack rather than retyped, and its universal bans stated as defaults the instance overrules. (5) The upstream's seeded placeholder photography is **dropped, not softened** (ADR 0222); (6) motion is CSS inside the Kill Switch and what cannot live there is named *not available*; (7) the pre-flight is the kit and `tells.js` is its mechanical half, so a look skill adds no reviewer of its own. Rejected: shipping each look as a pack, a sixteenth token for a substrate, a fixed seed behind the same roll.](<redacted>.md) | ui-design / skills |
|
|
319
319
|
| 0231 | [**The `/style` session: an owner authors a look inside the contract, and a module skill declares one of two origins** ([task 1003475](https://cloudbongos.com/builders#/task/1003475) · goal 1000083, criterion `uidesign-skills-in-house`). The library ships three looks to choose from; the owner's ask was to *make* one. A look is a small, strict object (thirteen colours, two font stacks, a `DESIGN.md`, one specimen), so what was missing was the loop between the ends. Decisions: (1) `/style` is a **bounded four-step session** - SEARCH, CREATE, PROVE, ADOPT - and nothing else. (2) SEARCH **renders before it asks**: the nearest two entries shown through the kit (`/` and the hall's light tier), then two or three questions about *feel*, the light-only fact of a pack stated up front, and adapt-or-draft declared in one sentence before a file is written. (3) CREATE stays inside the contract or refuses - never a fork of a sheet, **never a sixteenth token** (a radius, a shadow, a glass are a sheet's derived tier; a look that needs one is an ADR). (4) PROVE is **arithmetic, not taste**: the pack floor, the three token suites under the pack, `check-mock`, the renders, with a failure-to-fix table so the retune is a lookup, and the four green lines pasted as evidence. (5) ADOPT hands the owner one step and **never writes `config/branding.json`** - the host boundary is theirs (ADR 0062 §7) - but does file the catalogue rows under the claimed task. (6) A module skill declares **exactly one of two origins**, `## Rebuilt from` (ADR 0198 / 0220) or `## First party` (no upstream; the authoring task, this ADR, and why it exists), and the pins branch on which - a fabricated spec citation would destroy the provenance the section exists for. (7) The session's first output is its own proof: `blueprint`, the library's first cool look, cleared the pack floor and all three suites on the first measurement. Rejected: a fabricated Rebuilt-from, a session that repaints the live instance, filing the catalogue rows "later", a palette generated from a reference image.](<redacted>.md) | ui-design / skills |
|
|
320
320
|
| 0226 | [**One stale-claim timer; reporting degrades where enforcement refuses** ([task 1003476](https://cloudbongos.com/builders#/task/1003476), goal 1000072, criterion `legibility-sort-and-staleness`). "How long may a claim go silent" had FOUR answers and none of them was the project's: the sweeper that actually releases a claim read `GDS_STALE_CLAIM_HOURS` (else 6h); `GET /builders/roster` and `GET /builders/:id/active-claims` each hardcoded 24h in byte-identical blocks; and the two `session_pulses` helpers defaulted to 30 minutes. The 6-vs-24 pair is the harm: the roster is the screen an Archon opens to decide whether to intervene, and for **eighteen hours** after the sweeper had released a claim, stamped its session log `[auto-released:stale]` and returned the task to `ready`, that screen still showed it healthy — while the roster's own comment claimed to "mirror" the sweeper, true of the definition and false of the number. Decision: ONE resolver, `src/stale-timer.js` (a KERNEL file, the `instance-config.js`/`branding.js` category — host config a module reads, never hardcodes), reached by modules through the doorway and by core routes and the CLI directly. Precedence: explicit override (`--threshold-hours`, `?stale_hours=` — a LENS, labelled `query`, never a reconfiguration) → `<PREFIX>_STALE_CLAIM_HOURS` via `instance-config.resolveEnv`, whose legacy prefix chain still honours the live `GDS_STALE_CLAIM_HOURS` so **nothing in a running deployment has to change** (and which is the channel ADR 0190's runner already patches into `web.env`) → **6**, the SWEEP's historical number and deliberately not the roster's 24, because the sweep is the surface that acts and unifying on 24 would have made every project three times more patient with a dead claim under cover of a refactor. Bound 1..720h — the range `/builders/roster` has published since V3.R14; one timer, one bound. Every surface that prints the timer prints its PROVENANCE (`stale_hours_source`, the sweep header, the auto-release note): a number without it is exactly what let 6 and 24 coexist unnoticed. **The fail posture is asymmetric and that is the decision**: the resolver reports an unusable value rather than throwing, then a surface that only READS degrades to the default and names the bad var (500-ing the roster an Archon opened to find the problem would hide the problem), while the SWEEP refuses to run at all (it releases other people's claims and stamps their logs; no reload undoes that) — a surface that only reads may guess and say so, a surface that acts may not guess. Both routes now share one `staleHoursFor(req, res)`; two copies of a literal is how the drift happened twice inside one file. Rejected: a new `config/stale.json` tier (the env route already IS the per-project channel); making 24 the shared default; throwing from the resolver (forces a second reader — the original bug); folding in the boards' 14/30/60-DAY task buckets (a different question — days, about a task, a browsing lens with no enforcement behind it); a DB row + settings UI (a reasonable next step, but the defect was four numbers, not a missing knob — a row added before the readers agreed would have been a fifth).](<redacted>.md) | GDS / claims + ops |
|
|
321
|
+
| 0232 | [**Rot is derived, not swept; and `water` is the only new verb** ([task 1003279](https://cloudbongos.com/builders#/task/1003279) · goal 1000072, criterion `legibility-sort-and-staleness`). The inbox half [ADR 0226](<redacted>.md) split out and left open. Owner decision (2026-08-25): a goal or task past the timer raises a **needs-attention line** — not an automatic status change (it mutates real work with no human in the loop, and a wrong timer then silently freezes it) and not a nightly digest (a notification is not an action, and one you scroll past is indistinguishable from the silent decay it was meant to fix). The build note asked for an idempotent poller; the decision is that **there is no poller and no items table** — a line is a `SELECT` comparing the thing's rot clock to the timer, so "exactly one item per stale thing, not one per sweep" is true by arithmetic rather than by deduplication code that must be written correctly and then stay correct, and clearing falls out for free (an edit moves `updated_at`, a decision moves `status`, watering moves the mark). A stored item would have been a cache of a query whose inputs it could not observe. **The exclusions are the load-bearing part**: rot is work nobody picked up (`backlog`/`ready`, and `open` goals), and `active` is deliberately excluded because the claim-silence timer already reports it in hours — listing it here in days would put one rot in two inboxes under two numbers, the 6h-vs-24h split one tier up; `blocked`, `completed`/`confirmed` and the terminal states are excluded for the same reason, each naming the surface that owns it. The clock is **not a new definition of touched** — it is `task-lib.js`'s published `max(updated_at, shipped_at)` (task 1003278) plus a water mark, pinned by a test asserting the domain expression appears verbatim in migration `core_231`'s index expressions, because drift there is silent. The timer is a **second** timer and defends itself as a different question (days vs hours, DB vs env, asks a human vs releases a claim), stored in a new unseeded generic `project_settings` table — no row means "never configured", which is what lets a settings page tell that apart from "set to the default" — with ADR 0226's precedence and its reporting-degrades fail posture, and an unreachable DB explicitly not treated as a bad value. Three of the four verbs (**prioritise/prune/kill**) reuse `promote`/`demote`/`abandon`/`archive`, the task-1003066 precedent, so they add no permission; only `water` is new, gated on ownership OR `builder.roster.read` inside the handler (a creator holding no atom must still get through). Verbs are **served with each row** rather than hardcoded, since a goal legitimately gets two where a task gets four. Rejected: an items table, a sweep, a digest, auto-demotion, and a fourth verb on goals that would render a dead button.](<redacted>.md) | GDS / legibility + hall |
|
|
321
322
|
|
|
322
323
|
> ⚠️ **Numbering collisions are now machine-enforced, not narrated.** Twenty numbers are shared by two ADRs each — assigned in parallel sessions before anything checked. The files keep their filenames (renumbering would break every existing citation, and a number once assigned is never reused), and this table disambiguates each pair as `NNNN-a` / `NNNN-b`. The authoritative list is `LEGACY_DUPLICATE_ADRS` in [`scripts/gds/adr-namespace.js`](../../scripts/gds/adr-namespace.js), frozen by exact filename: a **new** collision — or a third file joining a legacy number, or a rename of either half — hard-fails the `unit` CI gate, as does an index row that points at no file or an ADR with no row. The list may only shrink. This paragraph used to narrate the collisions one by one and had fallen eight behind reality; see [ADR 0195](<redacted>.md) for why the check exists and why the pairs are grandfathered rather than renumbered.
|