@bongos/core 1.19.484 → 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.
Files changed (48) hide show
  1. package/.bongos-core.json +102 -52
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +6 -0
  4. package/clients/bongos-client/index.cjs +6 -0
  5. package/clients/bongos-client/index.d.ts +7 -0
  6. package/clients/bongos-client/index.mjs +6 -0
  7. package/docs/adr/0232-rot-is-derived-not-swept-and-water-is-the-only-new-verb.md +155 -0
  8. package/docs/adr/README.md +1 -0
  9. package/docs/api/openapi.json +147 -3
  10. package/docs/api-reference.md +7 -4
  11. package/docs/copy-inventory.md +169 -178
  12. package/docs/copy-registry.json +295 -405
  13. package/docs/file-map.md +1 -1
  14. package/docs/module-api-changelog.md +4 -0
  15. package/migrations/core_231_project_settings_and_attention_water.sql +163 -0
  16. package/modules/dev-box/app/src/vendor/bongos-client.cjs +6 -0
  17. package/modules/hall-ui/public/goal-inbox.js +119 -1
  18. package/modules/hall-ui/public/not-ready.html +14 -10
  19. package/modules/hall-ui/public/not-ready.states.json +14 -0
  20. package/modules/hall-ui/public/pair.css +17 -67
  21. package/modules/hall-ui/public/pair.html +8 -1
  22. package/modules/hall-ui/public/pair.states.json +14 -0
  23. package/modules/hall-ui/public/panel.css +333 -0
  24. package/modules/hall-ui/public/profile.css +85 -0
  25. package/modules/hall-ui/public/profile.html +12 -5
  26. package/modules/hall-ui/public/profile.states.json +15 -0
  27. package/modules/hall-ui/public/settings.css +119 -336
  28. package/modules/hall-ui/public/settings.html +28 -19
  29. package/modules/hall-ui/public/settings.js +44 -35
  30. package/modules/hall-ui/public/settings.states.json +16 -0
  31. package/modules/hall-ui/public/style.css +5 -2
  32. package/modules/lifecycle/module.json +2 -1
  33. package/modules/lifecycle/rot.js +210 -0
  34. package/modules/lifecycle/routes/rot.js +160 -0
  35. package/package-lock.json +2 -2
  36. package/package.json +1 -1
  37. package/scripts/gds/fitness-checks-vocabulary.js +0 -1
  38. package/scripts/gds/fitness.js +9 -0
  39. package/src/bongos/project-settings.js +193 -0
  40. package/src/module-api.js +29 -1
  41. package/tests/hall_settings_world.mjs +274 -0
  42. package/tests/join_application_note.mjs +7 -15
  43. package/tests/module-scope-map.mjs +1 -1
  44. package/tests/module_api.mjs +1 -0
  45. package/tests/rot_inbox.mjs +532 -0
  46. package/modules/hall-ui/public/my-projects.css +0 -62
  47. package/modules/hall-ui/public/my-projects.html +0 -73
  48. package/modules/hall-ui/public/my-projects.js +0 -153
@@ -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.
@@ -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.
@@ -6298,6 +6298,51 @@
6298
6298
  ]
6299
6299
  }
6300
6300
  },
6301
+ "/goals/{id}/water": {
6302
+ "post": {
6303
+ "operationId": "post_goals_id_water",
6304
+ "tags": [
6305
+ "goals"
6306
+ ],
6307
+ "summary": "POST /goals/:id/water",
6308
+ "description": "POST /goals/:id/water — reset a rotting goal's clock.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
6309
+ "x-rank": "any-builder",
6310
+ "x-source": "modules/lifecycle/routes/rot.js",
6311
+ "parameters": [
6312
+ {
6313
+ "name": "id",
6314
+ "in": "path",
6315
+ "required": true,
6316
+ "schema": {
6317
+ "type": "string"
6318
+ },
6319
+ "description": "Path parameter `id`."
6320
+ }
6321
+ ],
6322
+ "responses": {
6323
+ "200": {
6324
+ "description": "Success."
6325
+ },
6326
+ "400": {
6327
+ "$ref": "#/components/responses/BadRequest"
6328
+ },
6329
+ "401": {
6330
+ "$ref": "#/components/responses/Unauthorized"
6331
+ },
6332
+ "403": {
6333
+ "$ref": "#/components/responses/Forbidden"
6334
+ },
6335
+ "404": {
6336
+ "$ref": "#/components/responses/NotFound"
6337
+ }
6338
+ },
6339
+ "security": [
6340
+ {
6341
+ "builderSession": []
6342
+ }
6343
+ ]
6344
+ }
6345
+ },
6301
6346
  "/government/assignments": {
6302
6347
  "get": {
6303
6348
  "operationId": "get_government_assignments",
@@ -7364,6 +7409,44 @@
7364
7409
  ]
7365
7410
  }
7366
7411
  },
7412
+ "/inbox/rotting": {
7413
+ "get": {
7414
+ "operationId": "get_inbox_rotting",
7415
+ "tags": [
7416
+ "inbox"
7417
+ ],
7418
+ "summary": "GET /inbox/rotting",
7419
+ "description": "GET /inbox/rotting — what is quietly rotting, for this caller. rank: any-builder — own-scoped by default. The builder id comes from the authenticated session and is never read off the request, so a caller can only ever widen the result by HOLDING an atom, never by naming a different builder. A Xenos sees exactly the things they created and nothing else. ?rot_days= is a LENS, not a setting: it answers \"what would 7 days show me?\" without reconfiguring the project, and is never written back. The response reports the timer and where the number came from, so a reader can always tell a lens apart from the project's real answer (the src/stale-timer.js receipt convention — a number with no provenance is what let 6h and 24h coexist).\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
7420
+ "x-rank": "any-builder",
7421
+ "x-source": "modules/lifecycle/routes/rot.js",
7422
+ "responses": {
7423
+ "200": {
7424
+ "description": "Success.",
7425
+ "content": {
7426
+ "application/json": {
7427
+ "schema": {
7428
+ "$ref": "#/components/schemas/GetInboxRottingResponse"
7429
+ }
7430
+ }
7431
+ }
7432
+ },
7433
+ "400": {
7434
+ "$ref": "#/components/responses/BadRequest"
7435
+ },
7436
+ "401": {
7437
+ "$ref": "#/components/responses/Unauthorized"
7438
+ },
7439
+ "403": {
7440
+ "$ref": "#/components/responses/Forbidden"
7441
+ }
7442
+ },
7443
+ "security": [
7444
+ {
7445
+ "builderSession": []
7446
+ }
7447
+ ]
7448
+ }
7449
+ },
7367
7450
  "/inbox/score": {
7368
7451
  "post": {
7369
7452
  "operationId": "post_inbox_score",
@@ -15191,6 +15274,51 @@
15191
15274
  ]
15192
15275
  }
15193
15276
  },
15277
+ "/tasks/{id}/water": {
15278
+ "post": {
15279
+ "operationId": "post_tasks_id_water",
15280
+ "tags": [
15281
+ "tasks"
15282
+ ],
15283
+ "summary": "POST /tasks/:id/water",
15284
+ "description": "POST /tasks/:id/water — reset a rotting task's clock.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
15285
+ "x-rank": "any-builder",
15286
+ "x-source": "modules/lifecycle/routes/rot.js",
15287
+ "parameters": [
15288
+ {
15289
+ "name": "id",
15290
+ "in": "path",
15291
+ "required": true,
15292
+ "schema": {
15293
+ "type": "string"
15294
+ },
15295
+ "description": "Path parameter `id`."
15296
+ }
15297
+ ],
15298
+ "responses": {
15299
+ "200": {
15300
+ "description": "Success."
15301
+ },
15302
+ "400": {
15303
+ "$ref": "#/components/responses/BadRequest"
15304
+ },
15305
+ "401": {
15306
+ "$ref": "#/components/responses/Unauthorized"
15307
+ },
15308
+ "403": {
15309
+ "$ref": "#/components/responses/Forbidden"
15310
+ },
15311
+ "404": {
15312
+ "$ref": "#/components/responses/NotFound"
15313
+ }
15314
+ },
15315
+ "security": [
15316
+ {
15317
+ "builderSession": []
15318
+ }
15319
+ ]
15320
+ }
15321
+ },
15194
15322
  "/versions": {
15195
15323
  "get": {
15196
15324
  "operationId": "get_versions",
@@ -16307,6 +16435,22 @@
16307
16435
  "open_count"
16308
16436
  ]
16309
16437
  },
16438
+ "GetInboxRottingResponse": {
16439
+ "type": "object",
16440
+ "properties": {
16441
+ "rows": {},
16442
+ "count": {},
16443
+ "scope": {},
16444
+ "timer": {},
16445
+ "cutoff": {},
16446
+ "ok": {
16447
+ "type": "boolean"
16448
+ },
16449
+ "tier": {},
16450
+ "id": {},
16451
+ "watered_at": {}
16452
+ }
16453
+ },
16310
16454
  "GetInboxSparksResponse": {
16311
16455
  "type": "object",
16312
16456
  "properties": {
@@ -21505,9 +21649,9 @@
21505
21649
  "description": "A required dependency/feature is not configured or is temporarily down."
21506
21650
  }
21507
21651
  },
21508
- "x-endpoint-count": 341,
21509
- "x-schema-count": 374,
21652
+ "x-endpoint-count": 344,
21653
+ "x-schema-count": 375,
21510
21654
  "x-undocumented-bodies": 9,
21511
- "x-response-schemas": 255,
21655
+ "x-response-schemas": 256,
21512
21656
  "x-generated-by": "scripts/gds/gen-api-docs.js"
21513
21657
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Bongos API reference
4
4
 
5
- > **Generated from the live route files** — the route file is authoritative. 341 endpoints across 55 route files.
5
+ > **Generated from the live route files** — the route file is authoritative. 344 endpoints across 56 route files.
6
6
  > Machine-readable spec: [`docs/api/openapi.json`](api/openapi.json) (OpenAPI 3.1). Rendered docs site: **`/docs`** (e.g. `cloudbongos.com/docs`).
7
7
 
8
8
  Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>.md)): `public` < `any-builder` < `metic+archon` < `archon`.
@@ -220,7 +220,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
220
220
  | GET | `/api/bongos/github/repos` | `any-builder` | — | acts only on the caller's OWN GitHub identity (the token they granted). |
221
221
  | POST | `/api/bongos/github/repos` | `any-builder` | `name` | creates a repo in the caller's OWN account. |
222
222
 
223
- ## `goals` (30)
223
+ ## `goals` (31)
224
224
 
225
225
  | Method | Path | Rank | Body | Description |
226
226
  |---|---|---|---|---|
@@ -250,6 +250,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
250
250
  | POST | `/api/bongos/goals/:id/tasks` | `any-builder` | `title`, `description`, `touches`, `est_minutes`, `priority`, `kind`, `discipline`, `value_summary`, `parent_task_id` | POST /goals/:id/tasks — bounded task authoring within a goal's scope (BV1.R60 / ADR 0086 §3). |
251
251
  | POST | `/api/bongos/goals/:id/transfer` | `any-builder` | `builder_id` | POST /goals/:id/transfer — hand the goal to a new owner (task 1735). |
252
252
  | PATCH | `/api/bongos/goals/:id/visibility` | `any-builder` | — | PATCH /goals/:id/visibility — make a goal public\|private (task 1952 / ADR 0112). |
253
+ | POST | `/api/bongos/goals/:id/water` | `any-builder` | — | POST /goals/:id/water — reset a rotting goal's clock. |
253
254
  | GET | `/api/bongos/goals/inbox` | `any-builder` | — | GET /goals/inbox — the caller's private-goals inbox (task 1952 / ADR 0112): { invitations: invites addressed to me (accept/decline); join… |
254
255
  | POST | `/api/bongos/goals/reorder` | `metic+archon` | `version_id`, `ordered_ids` | POST /goals/reorder — Archon drag-to-reorder: set the priority order of every goal WITHIN one version (task 1760). |
255
256
  | GET | `/api/bongos/goals/rollups` | `any-builder` | — | GET /goals/rollups?version= — one small row per goal: readiness counters, the last-activity timestamp, the craft set, and the next claima… |
@@ -294,7 +295,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
294
295
  | GET | `/api/bongos/help-requests/for-me` | `any-builder` | — | The addressed read — C01's answer to "and fires a notification". |
295
296
  | GET | `/api/bongos/help-requests/mine` | `any-builder` | — | your own asks, so you can settle them. |
296
297
 
297
- ## `inbox` (15)
298
+ ## `inbox` (16)
298
299
 
299
300
  | Method | Path | Rank | Body | Description |
300
301
  |---|---|---|---|---|
@@ -310,6 +311,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
310
311
  | DELETE | `/api/bongos/inbox/:id/spark` | `any-builder` | — | Withdraw the Big Idea Spark mark — the reversibility half of R144. |
311
312
  | GET | `/api/bongos/inbox/awaiting-nod` | `metic+archon` | — | GET /inbox/awaiting-nod — the NOD QUEUE (task 1003066, BV1.R05). |
312
313
  | GET | `/api/bongos/inbox/by-builder/:id` | `any-builder` | — | THE IDEA TRAIL: every idea one builder filed, newest first, with what became of each (task 1003281, goal 1000072, criterion legibility-ac… |
314
+ | GET | `/api/bongos/inbox/rotting` | `any-builder` | — | GET /inbox/rotting — what is quietly rotting, for this caller. |
313
315
  | POST | `/api/bongos/inbox/score` | `any-builder` | `field_values`, `template_key` | |
314
316
  | GET | `/api/bongos/inbox/sparks` | `any-builder` | — | GET /inbox/sparks — THE SPARK QUEUE (task 1002921 / BV1.R145, goal 1000062). |
315
317
  | GET | `/api/bongos/inbox/template` | `any-builder` | — | ----------------------------------------------------------------------- The two reads the hall's Full Idea composer is built from (task 1… |
@@ -559,7 +561,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
559
561
  |---|---|---|---|---|
560
562
  | GET | `/api/bongos/task-visuals/:name` | `public` | — | the WIDEST path this route can take, declared honestly. |
561
563
 
562
- ## `tasks` (30)
564
+ ## `tasks` (31)
563
565
 
564
566
  | Method | Path | Rank | Body | Description |
565
567
  |---|---|---|---|---|
@@ -588,6 +590,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
588
590
  | POST | `/api/bongos/tasks/:id/visual` | `any-builder` | `image_b64`, `content_type`, `alt` | POST /tasks/:id/visual — attach (or replace) this task's ship-time visual. |
589
591
  | DELETE | `/api/bongos/tasks/:id/visual` | `any-builder` | — | DELETE /tasks/:id/visual — remove it again. |
590
592
  | POST | `/api/bongos/tasks/:id/vote` | `metic+archon` | `vote` | POST /tasks/:id/vote { vote: 1 \| -1 \| 0 } — cast/change/clear a peer vote on a confirmed/shipped task. |
593
+ | POST | `/api/bongos/tasks/:id/water` | `any-builder` | — | POST /tasks/:id/water — reset a rotting task's clock. |
591
594
  | GET | `/api/bongos/tasks/claimable` | `any-builder` | — | NOTE: /tasks/claimable must come BEFORE /tasks/:id or Express will route 'claimable' to the :id param. |
592
595
  | GET | `/api/bongos/tasks/newcomer-floor` | `any-builder` | — | GET /tasks/newcomer-floor — supply gauge for the newcomer queue (task 614). |
593
596
  | POST | `/api/bongos/tasks/newcomer-restock` | `metic+archon` | — | POST /tasks/newcomer-restock (task 1168) — bring every evergreen newcomer chore's standing pool up to its configured floor. |