@bongos/core 1.19.494 → 1.19.496

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.494",
6
- "core_contract": "1.19.494",
7
- "source_commit": "3899dae02a9a5c15e0d7f624510c3a342f1bd256",
5
+ "core_version": "1.19.496",
6
+ "core_contract": "1.19.496",
7
+ "source_commit": "8c795e90254fbc29139c278e0fd6d890b2c32035",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-02T23:55:30.581Z",
9
+ "built_at": "2026-09-03T14:22:08.467Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 424,
12
+ "docs_redacted": 425,
13
13
  "agent_docs_stubbed": 24,
14
- "functional_verbatim": 1937,
14
+ "functional_verbatim": 1938,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2385,
20
- "tree_sha256": "76335ba0fadf649f7bd21717cae44a485ff2070a4e53410db92d38354e662913",
19
+ "file_count": 2387,
20
+ "tree_sha256": "a6e4b7c3e281c43562758626b607ca7c0fa66491a11f41433d821d9a4e94d839",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -77,7 +77,7 @@
77
77
  {
78
78
  "path": ".claude/skills/builder-redteam/SKILL.md",
79
79
  "mode": "0000644",
80
- "sha256": "8a4f589c161847dcdb7a2ca5258544cd7322154b165b85cb6ff0cc8bb1b03792"
80
+ "sha256": "2843dadae1ad9d0f9bb52813aa39504b108aa49b9bf903a9ba93214744933046"
81
81
  },
82
82
  {
83
83
  "path": ".claude/skills/builder-release/SKILL.md",
@@ -1674,10 +1674,15 @@
1674
1674
  "mode": "0000644",
1675
1675
  "sha256": "69db7f69b30eb87a8b8e921ea664344faf7b953c7a85ce9d788282a0e181dbd1"
1676
1676
  },
1677
+ {
1678
+ "path": "docs/adr/0234-idea-routing-capture-time-promotion-landing-matrix-homeless-inbox.md",
1679
+ "mode": "0000644",
1680
+ "sha256": "de04b3e3a03f66dd92003dc256c1839a4c8bcc213fb26103b60baf940046502e"
1681
+ },
1677
1682
  {
1678
1683
  "path": "docs/adr/README.md",
1679
1684
  "mode": "0000644",
1680
- "sha256": "8c8285dcd86e937c0f09b18839fe9628ae845cd4f4f9d332d723cf216e88deac"
1685
+ "sha256": "f86a0fd77bda20522e0907c607b3ef9724f092194760563c92254855b681404d"
1681
1686
  },
1682
1687
  {
1683
1688
  "path": "docs/api-reference.md",
@@ -2562,7 +2567,7 @@
2562
2567
  {
2563
2568
  "path": "docs/module-api-changelog.md",
2564
2569
  "mode": "0000644",
2565
- "sha256": "f861f82c27495f73e7117109ce43d137d6bc3da9628d63905bdc679ab4b71ffc"
2570
+ "sha256": "f7ca8307d5cae80f0602080d5d3fc9944cc15d9fc5d2fdf43f45d89626edd039"
2566
2571
  },
2567
2572
  {
2568
2573
  "path": "docs/modules-contract.md",
@@ -5307,7 +5312,7 @@
5307
5312
  {
5308
5313
  "path": "modules/lifecycle/goal-advisory.js",
5309
5314
  "mode": "0000644",
5310
- "sha256": "9f1a2543b4e575f024fd5dd561c47fd804907a1f8db10ace5b552bdccfe5365e"
5315
+ "sha256": "494c93f4cc64eb8dbc23039fa50a27b8242ba09b5afa0843ca985c807e312b6c"
5311
5316
  },
5312
5317
  {
5313
5318
  "path": "modules/lifecycle/goal-authz.js",
@@ -7207,12 +7212,12 @@
7207
7212
  {
7208
7213
  "path": "package-lock.json",
7209
7214
  "mode": "0000644",
7210
- "sha256": "501ef2c960aec4a20d26d149fabb74417deb4f46d5da85d110d6dad0f861ff22"
7215
+ "sha256": "cb12024e16aedcd701269af77cc73012013873d07e37301c24ae10e3c1f2d28b"
7211
7216
  },
7212
7217
  {
7213
7218
  "path": "package.json",
7214
7219
  "mode": "0000644",
7215
- "sha256": "70d72840d1b8edcf74d56407f9446ecc10ffec95d18a2d2921d1b616747ecf4e"
7220
+ "sha256": "ccfb94ab67d5acffc5c910496496a9cfbe44343ff52b90098857338dda375bae"
7216
7221
  },
7217
7222
  {
7218
7223
  "path": "public-docs/index.html",
@@ -8877,7 +8882,7 @@
8877
8882
  {
8878
8883
  "path": "src/module-api.js",
8879
8884
  "mode": "0000644",
8880
- "sha256": "36694adf0d4cc5c15b9193853b2089a2b533573d7ea2f68cb5f88ac16cf50629"
8885
+ "sha256": "2b403360f3b811aecd3de800ebb5cb87dbc5df36e045754184dd3924abc187f9"
8881
8886
  },
8882
8887
  {
8883
8888
  "path": "src/module-loader/catalog.js",
@@ -9912,7 +9917,7 @@
9912
9917
  {
9913
9918
  "path": "tests/goal_advisory.mjs",
9914
9919
  "mode": "0000644",
9915
- "sha256": "1b1c403574132def4c4beb98595987e90b720a851f29e42f643f1f00f285da5c"
9920
+ "sha256": "1a7498e0a1927485db9a9e99b19012dc7df91825b11077b283a95b23971fe86e"
9916
9921
  },
9917
9922
  {
9918
9923
  "path": "tests/goal_category.mjs",
@@ -11229,6 +11234,11 @@
11229
11234
  "mode": "0000644",
11230
11235
  "sha256": "cad269f556293e0caa234561f5764f90998a5d133b1e0147a5bbf22403c55e3a"
11231
11236
  },
11237
+ {
11238
+ "path": "tests/redteam_followup_kind.mjs",
11239
+ "mode": "0000644",
11240
+ "sha256": "01c34b417bd36ba9676d53c025d6457ffcc949edaac55f1bbe5a3234b23d2801"
11241
+ },
11232
11242
  {
11233
11243
  "path": "tests/redteam_patrol.mjs",
11234
11244
  "mode": "0000644",
@@ -28,7 +28,7 @@ The reporter does NOT need Archon attention to file. The Archon comes to the que
28
28
  3. Walks the user through five questions (see below).
29
29
  4. POSTs to `/api/gds/security/reports`.
30
30
  5. Echoes the report id back + a note that the auto-repro subagent will attempt reproduction within the hour.
31
- 6. (Optional) Offers to file an `idea_inbox` entry with `kind='security-finding'` pointing back at the report id, so the fix can be triaged into a normal task on the next `/idea-triage` pass.
31
+ 6. (Optional) Offers to file an `idea_inbox` entry with `kind='bug'` pointing back at the report id, so the fix can be triaged into a normal task on the next `/idea-triage` pass.
32
32
 
33
33
  ## The five questions
34
34
 
@@ -92,7 +92,7 @@ node scripts/gds/api.js POST /api/gds/inbox --body-file <path-to-idea.json>
92
92
  where `idea.json` is:
93
93
 
94
94
  ```json
95
- { "title": "Fix: <TITLE>", "body_md": "Originates from vulnerability_report #<REPORT_ID> (severity=<SEVERITY>). Target: <TARGET>.\n\n<DESCRIPTION>", "kind": "security-finding" }
95
+ { "title": "Fix: <TITLE>", "body_md": "Originates from vulnerability_report #<REPORT_ID> (severity=<SEVERITY>). Target: <TARGET>.\n\n<DESCRIPTION>", "kind": "bug" }
96
96
  ```
97
97
 
98
98
  This is optional because some findings are "the system is fine, my mental model was wrong" — the report itself is the durable signal; an idea entry is only useful when there's actual remediation work to schedule.
@@ -0,0 +1,183 @@
1
+ # 0234 — Idea routing: capture-time promotion, the landing matrix, and the homeless-only inbox
2
+
3
+ - **Status:** Accepted
4
+ - **Date:** 2026-09-03
5
+ - **Tasks:** [#1003064](https://cloudbongos.com/builders#/task/1003064) (R14 of goal 1000071, "Idea routing") — records the mechanism R01/R02/R05/R07 built and the identity R13 gave the inbox
6
+ - **Spec:** [`docs/specs/<redacted>.md`](../specs/<redacted>.md) — "Design decisions & assumptions" + "Interview decisions"
7
+ - **Related:** ADR 0084 (auto-derived per-task rank floor), ADR 0086 (goal-scoped work hierarchy), ADR 0016 (trust boundary, server-enforced), ADR 0172 (ideator credit lane, the lineage this feeds)
8
+
9
+ ## Context
10
+
11
+ Idea 1000771 named the failure this goal exists to close: Discord `#bugs` created tasks **directly**
12
+ (`createTask`, no `idea_inbox` row) against a **hardcoded, already-shipped** version id. Every bug
13
+ reported through that channel landed in a version catch-all nobody would ever reopen, and — the worse
14
+ half — no `captured_by`, no `ideas_task_lineage` row, so the ideator-credit lane (goal 1000067) had
15
+ nothing to ever pay against. The reporter vanished from the record the moment their report became work.
16
+
17
+ Meanwhile the *other* 49 open `idea_inbox` rows sat waiting for a human triage pass that, on
18
+ promotion, dropped the resulting task into "`<version> — general`" — because nothing in the capture
19
+ path had ever named a goal. Work that plainly had a home (a bug in a live feature, a feature request
20
+ against an open goal) got no closer to that home than a homeless idea did.
21
+
22
+ This ADR records what shipped to fix both: five tasks (R01–R03, R05, R07) plus the promote-path change
23
+ (R13), all now `shipped`.
24
+
25
+ ## Decision
26
+
27
+ ### 1. Routing is capture-time promotion, inside `captureIdea` — never a second door
28
+
29
+ `inbox.captureIdea` (`modules/ideas/inbox.js:225`, task 1003065 / R01) is documented in its own header
30
+ as **"THE ONE CAPTURE CHOKEPOINT"** — every filing vector (hall, `capture.js`, the Discord `#ideas`
31
+ inbound via the `ideas.capture` port, agents, seeds) already funnels through it, so routing was added
32
+ as one optional argument (`goalId`) rather than a sibling `captureAndRoute` function. The module's own
33
+ comment names the reason explicitly: *"A `captureAndRoute` sibling would have been the same code with
34
+ an invitation to bypass it."* That is the direct, structural answer to the Discord `#bugs` lesson — a
35
+ second creation path is exactly the shape idea 1000771 was.
36
+
37
+ When the landing matrix (below) says **don't route**, `captureIdea` runs the pre-routing INSERT
38
+ unchanged — `idea_inbox` status `'open'`, no task. This is the default path and is what every
39
+ goal-unaware caller still gets (routing is strictly additive).
40
+
41
+ When the matrix says **route**, `routeIdeaToGoal` (`modules/ideas/inbox.js:303`) runs **one
42
+ transaction**: the idea row (`status='promoted'`, `reviewed_at` stamped — routing's whole claim is
43
+ that this row never needed a triage pass), the task via the `lifecycle.createTask` kernel port, and an
44
+ `ideas_task_lineage` row with `origin='route'`. The order is forced by foreign keys (idea → task →
45
+ lineage), and the module's own comment states the stakes: *"THE TRANSACTION IS THE POINT, not an
46
+ optimisation... A route that committed the task but lost the lineage row would manufacture exactly
47
+ that bug, at scale, on the path whose whole promise is that attribution survives (criterion C3)."* If
48
+ any step fails, the whole thing rolls back and the filer's report is never silently lost mid-flight.
49
+
50
+ `captureIdea` treats `filerRank`/`grade` as **read-only inputs to the decision** — it performs no
51
+ authority check itself; that is R05's job (below), a deliberately separate, load-bearing gate.
52
+
53
+ ### 2. The landing matrix, as shipped (`modules/ideas/routing.js`, task 1003065 / R01 + task 1003066 / R05)
54
+
55
+ `decideLanding()` is a pure, DB-free function — no clock, no rank lookup — so every cell is pinned by
56
+ `tests/idea_routing.mjs` without touching Postgres. Its posture is explicitly **fail-closed**: every
57
+ uncertainty resolves toward "don't route" or "route to backlog", never toward `'ready'`.
58
+
59
+ | Filed thing | Filer rank | Landing (as shipped) |
60
+ |---|---|---|
61
+ | No goal named | any | `idea_inbox`, `status='open'` — the default path (reason: `no_goal`) |
62
+ | `grade='full'` (Full Idea) | any | Never routes — the Board Room's subject (reason: `full_idea_never_routes`) |
63
+ | `kind` ∈ {`criterion-proposal`, `decision`} | any | Never routes — judgment work (reason: `judgment_kind`) |
64
+ | `kind='bug'` with a goal | Metic or Archon | Task in the goal, `status='ready'` (reason: `bug_trusted_filer`) |
65
+ | `kind='bug'` with a goal | sub-Metic / unknown | Task in the goal, `status='backlog'` (reason: `bug_awaiting_nod`) |
66
+ | Anything else with a goal (feature, cleanup, refactor, infra, or an unrecognised kind) | any | Task in the goal, `status='backlog'` (reason: `soft_landing`) |
67
+
68
+ This matches the spec's table (`docs/specs/<redacted>.md` "Landing matrix") exactly for the
69
+ hard-land/soft-land split. Priority on a routed task defaults to the idea's own `suggested_priority`,
70
+ clamped to 1–5, else `3` (`routedPriority`) — precisely the "decided without interview" line the spec
71
+ records.
72
+
73
+ `decideLanding` only decides the **status**; it grants nothing. `authorizeRoutedLanding`
74
+ (`modules/ideas/routing.js:169`, task 1003066 / R05) is the **binding** gate `routeIdeaToGoal` calls
75
+ before it writes anything, and it closes a hole the matrix alone left open: a routed task declares no
76
+ `touches[]`, so the ordinary ADR 0084/0085 rank-floor derivation would land it at `xenos` even when the
77
+ goal's `scope_modules` reach protected territory (the permission/ship-grade/deploy/migration core).
78
+ Two rules, ordered and fail-closed (the `authorizeGoalTaskCreate` precedent):
79
+
80
+ - **Touches declared** (a programmatic filer that knows its own blast radius) → `goalScopeCheck.check`
81
+ runs verbatim, refusals and all — the same wall `POST /goals/:id/tasks` applies to an authored task.
82
+ - **Touches absent** (every human filing — demanding touches[] as the price of reporting a bug is
83
+ exactly the triage tax this goal removes) → protection moves to the **claim side**: a goal whose
84
+ scope reaches protected territory raises the task's `requires_rank` floor to `metic`
85
+ (`PROTECTED_SCOPE_FLOOR`), and — the part R05 tightened after its first cut — a **sub-Metic filer is
86
+ refused outright** (`ROUTE_NOT_AUTHORIZED` / `protected_scope`, HTTP 403) rather than merely having
87
+ the floor stamped on afterward. The refusal costs the filer nothing they had before: omit `goal_id`
88
+ and the report files to the inbox exactly as it always did.
89
+
90
+ **Membership is deliberately not required to route into a goal**, and the module's comment names this
91
+ as the one place the routed path diverges from the authored one on purpose: `authorizeGoalTaskCreate`
92
+ refuses a non-member (`not_a_member`) because *authoring* inside someone's workspace is a member's
93
+ privilege, but *reporting* is not — criterion C5's promise ("the filer never has to know the goal map")
94
+ and C1's ("a bug filed anywhere lands in its home") both require that a non-member can still route a
95
+ report in. The safety membership would have carried moves instead to the rank floor (claim side) and
96
+ the matrix (ready side), and is pinned by a test rather than left to erode quietly.
97
+
98
+ ### 3. The schema: one advisory hint column, one widened CHECK (task 1003060 / R02)
99
+
100
+ `migrations/<redacted>.sql` adds `idea_inbox.suggested_goal_id` (nullable FK
101
+ `ON DELETE SET NULL`) — **not** where a routed idea's goal lives (that is `tasks.goal_id`, set by
102
+ construction), but the leftover hint for rows that did **not** convert: soft-landed/homeless ideas, a
103
+ Discord classifier's guess, Full Ideas and criterion-proposals. The migration's own comment is explicit
104
+ that the column is "load-bearing for NOTHING — no rank check, no scope wall, and no payout may read
105
+ it." It lives in **core** migrations, not a module migration, because `idea_inbox` is a core table and
106
+ `fitness.js`'s `checkModuleMigrationsAdditive` forbids a module `ALTER TABLE` against it.
107
+
108
+ `modules/ideas/migrations/<redacted>.sql` widens `ideas_task_lineage`'s CHECK to
109
+ admit a third `origin`: `'route'`, alongside the pre-existing `'promote'` (a human triaged it) and
110
+ `'develop'` (a spark became a Full Idea). The distinction is deliberate — "how much triage routing
111
+ actually saved" (criterion C6) is only answerable if capture-time conversions are tagged differently
112
+ from later human promotions in the ledger.
113
+
114
+ ### 4. The suggester answers "which goal" *before* anything is created (task 1003062 / R07)
115
+
116
+ `modules/lifecycle/goal-suggest.js` lifts the candidate logic `buildGoalAdvisory` already had (goal
117
+ 1000071's "Reuse, do not rebuild" table) into a **pure, creation-free read**: `suggestGoals(goals,
118
+ draft, opts)` ranks open goals against a draft title/body by weighted token overlap (title terms
119
+ weight 2, body terms weight 1 — the `criterion-suggest.js` precedent), returning `[]` rather than five
120
+ arbitrary goals when nothing scores (the `includeZero` default). `GET /goals/suggestions` exposes it to
121
+ a human picker (the hall filing surface, the CLI prompt); `suggestGoalForCapture({ title, body })`
122
+ collapses the same ranking to a **single confident answer or null**, for a vector with nobody to choose
123
+ — currently Discord `#bugs` (below). It ranks across **every** version still open to work
124
+ (`versionAcceptsNewWork`, task 1003061 / R03), not one, because an earlier "decline if more than one
125
+ version is open" design would have returned null on every report this instance (which runs two
126
+ building versions concurrently) ever files.
127
+
128
+ ### 5. The inbox's new identity: homeless-only (task 1003071 / R13)
129
+
130
+ `scripts/gds/triage.js`'s `resolveGoalDefault(idea, versionId)` gives the promote path a goal by a
131
+ strict precedence — `idea.suggested_goal_id` (a human or filing surface already named this goal; a
132
+ heuristic must not overrule it) **before** the R07 suggester (only when there is no stored hint) — and
133
+ `createTask({ ..., goalId })` finally sends `goal_id` on `POST /tasks`, a field the route has accepted
134
+ since task 1763 with no client ever sending it. `.claude/skills/idea-triage/SKILL.md` states the
135
+ contract plainly: *"the inbox holds only HOMELESS work — bugs and goal-bound captures now route to a
136
+ task at filing time and never reach the inbox."* Triage shrinks to the judgment calls a routing rule
137
+ cannot make for itself: Full Ideas (Board Room material), `criterion-proposal`/`decision` rows, and
138
+ anything nobody could suggest a home for.
139
+
140
+ ## Divergence from the spec, recorded rather than silently reconciled
141
+
142
+ The spec's interview decision 2 says: *"Discord-filed bugs soft-land with a suggested goal. The report
143
+ files as an idea with a classifier-suggested goal attached and surfaces in the pending queue for a
144
+ one-click route. Nothing lands unvetted."* **That is not what R11 shipped.** `modules/discord/discord-bugs.js`
145
+ (task 1003067 / R11, retiring the hardcoded-`GDS-V3` path idea 1000771 named) instead resolves a
146
+ suggested goal via `suggestGoalForCapture` and hands it straight into `captureIdea` as `goalId` — so a
147
+ **linked Metic+ builder's Discord bug hard-lands `'ready'` immediately**, through the *same* landing
148
+ matrix every other filing vector uses, rather than always parking at `'backlog'` pending a one-click
149
+ nod. The file's own header explains the choice: a local copy of the rank split would only ever drift
150
+ from the one definition in `modules/ideas/routing.js` (`tests/filing_abuse_matrix.mjs` exists precisely
151
+ to prevent that divergence), and "same outcomes as before" (the pre-routing #bugs behaviour already
152
+ hard-landed a trusted linked builder's report). An **unlinked** Discord author still gets no goal and
153
+ no rank, so the matrix cannot route their report into work at all — it lands as a homeless idea, same
154
+ as `#ideas` has always given that person, which is the actual "nothing lands unvetted" guarantee that
155
+ survived.
156
+
157
+ The companion interview decision — **"Discord `#ideas` files with HINTS only (`suggested_goal_id`),
158
+ never routes"** — is recorded here as the design intent, but **is not yet built**: R12 (task 1003068,
159
+ "Discord `#ideas` carries routing hints") is still `status='ready'`, not shipped. Today,
160
+ `modules/discord/discord-inbound.js`'s `#ideas` capture carries no goal hint at all; every `#ideas`
161
+ filing lands exactly as it did before this goal, an ordinary homeless `idea_inbox` row. When R12 ships,
162
+ its hint must land as `suggested_goal_id` only (never `goal_id`) — consistent with every other
163
+ "classifier guess, not a route" vector this ADR documents — or it would silently convert `#ideas` into
164
+ a second bypass of the kind idea 1000771 already named once.
165
+
166
+ ## Consequences
167
+
168
+ - **Attribution survives routing by construction, not by discipline.** `tests/idea_routing.mjs` pins
169
+ the matrix; `tests/filing_abuse_matrix.mjs` (task 1003073 / R06) pins that a Xenos mass-filing bugs
170
+ never lands anything claimable, never lands in protected scope, and never escalates via filing;
171
+ task 1003072 / R04 is the proof that a routed bug's lineage + `captured_by` survive end-to-end to a
172
+ shipped task.
173
+ - **A caller lying about `filerRank` moves a bug from `'backlog'` to `'ready'` and nothing else** — the
174
+ matrix's own header states this as the reason R06's abuse matrix tests the *route* (server-derived
175
+ rank), not `captureIdea` (which trusts whatever rank it is handed, by design, because the route is
176
+ the only legitimate caller).
177
+ - **The inbox count is now a real signal.** With bugs and goal-bound features routed away at filing
178
+ time, `idea_inbox`'s open count should trend toward "things that genuinely have no home yet" —
179
+ measured by task 1003074 / R15's before/after sweep of the ~50 rows open when the spec was written
180
+ (not yet shipped at the time of this ADR).
181
+ - **R08–R10, R12, R15 remain open** (hall filing surface, CLI `--goal`, hall reassignment affordance,
182
+ Discord `#ideas` hints, the reclassification-sweep proof). This ADR documents the mechanism and the
183
+ matrix those tasks build against; it does not claim the whole spec is delivered.
@@ -321,5 +321,6 @@ This keeps the decision history honest and traceable.
321
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 |
322
322
 
323
323
  | 0233 | [**A copy proposal is a task carrying a patch, and the applier is the only writer** ([task 1003114](https://cloudbongos.com/builders#/task/1003114) · goal 1000074, criterion `artist-edits-land-as-tasks`). Goal 1000074 asks for two things that pull against each other in its own text: the artist must change copy WITHOUT an engineer, and there must be NO LIVE CMS because every copy change is a claimed, graded, shipped task. R02 shipped the half that needs no reconciliation (the flag) and [ADR 0178](<redacted>.md) §3 pinned the absence of any column that could hold replacement text, naming this task as the one that would have to keep it true. Decisions: (1) the shape is **borrowed from the design half, not invented** — ADR 0081 already has a tool authoring and an `adapter.export()` landing the output as a committed diff under a claim inside a path guard, so the artist is the tool, the copy desk is where they author, and the proposal is the export. (2) A proposal is a **TASK carrying a fenced `copy-proposal` patch**, never a row: the ledger is where a proposal belongs anyway, nothing renders a task description as product copy, a reviewer can read the change without running anything, and `copy-desk` gains NO migration (the obvious-but-wrong R03 is a `copy_desk_proposals` table, which is the forbidden column with extra steps). The module gains `consumes: ["lifecycle"]` and calls `createTask` through the port — it can put a request INTO the ledger and nothing can pull copy OUT of it. (3) `scripts/gds/copy-apply.js` **re-derives the position** rather than trusting the patch, which required teaching R01s extractor to record a source SPAN and deliberately dropping it from the committed registry: an offset goes stale the moment anything above it changes, while the id is designed to survive exactly that movement, so the span is recomputed at the moment of the edit by the same code that decided the string was copy. (4) The **placeholder arithmetic** is what makes a patch mechanical rather than a note for an engineer — the applier puts the sources own `${expr}` back, k-th hole to k-th expression, so an artist may MOVE a value within a sentence but never add or drop one, checked at proposal time and again against the real source. (5) Every ambiguity is a **named refusal** (`target_gone`, `ambiguous_target`, `no_exact_span`, `placeholder_mismatch`, `path_outside_surface` — the design adapters path guard, derived from R01s own SURFACES), because a wrong edit is worse than no edit; after writing, the applier regenerates the registry and checks that what it wrote is what the inventory now reads. (6) Proposing is **open to every rank** (an artist is a craft, not a rank) and lands at `backlog`, so a proposal is a request that passes ordinary triage. Rejected: a proposals table, a column beside the flag, trusting the patchs line number, an escape-aware index map to buy spans for nine strings, auto-closing the flag a proposal answers.](<redacted>.md) | copy-desk / the artist loop |
324
+ | 0234 | [**Idea routing: capture-time promotion, the landing matrix, and the homeless-only inbox** ([task 1003064](https://cloudbongos.com/builders#/task/1003064) · goal 1000071, "Idea routing"). Idea 1000771 named the failure: Discord `#bugs` created tasks DIRECTLY against a hardcoded, already-shipped version — no `captured_by`, no lineage, so the ideator-credit lane had nothing to pay against — while every other open idea sat waiting for a human triage pass that dropped its promotion into a version catch-all, because nothing in capture ever named a goal. Records what R01/R02/R05/R07/R13 shipped. `captureIdea` (`modules/ideas/inbox.js`) stays the ONE capture chokepoint — routing is one optional `goalId` argument, never a second creation door — and when the matrix routes, `routeIdeaToGoal` writes the idea row, the task (via the `lifecycle.createTask` port) and an `ideas_task_lineage` row (`origin='route'`) in ONE transaction, so a route can never mint attribution-free work the way `#bugs` did. `modules/ideas/routing.js`'s `decideLanding` is PURE and fail-closed: a bug from a Metic+ filer hard-lands `ready`, a sub-Metic bug or any other kind with a goal soft-lands `backlog`, a Full Idea or `criterion-proposal`/`decision` never routes, no goal named stays the pre-routing inbox path. `authorizeRoutedLanding` is the BINDING gate underneath the matrix's decision — a goal reaching protected scope raises the task's rank floor to Metic and REFUSES a sub-Metic filer outright, while membership is deliberately NOT required (reporting isn't authoring). `core_222`'s `suggested_goal_id` is an advisory hint only — the real home is `tasks.goal_id`, set by construction — and `/idea-triage` (`scripts/gds/triage.js`) now sends `goal_id` on promotion, so the inbox is homeless-only. Divergence recorded: R11 (Discord `#bugs`) hard-lands a linked Metic+ builder's bug immediately through the SAME shared matrix rather than the spec's "always soft-land, pending queue" design, because a local copy of the rank split would only drift from the one definition; and R12 (Discord `#ideas` hints) is NOT YET SHIPPED, so `#ideas` carries no goal hint today despite the interview decision that it should, hint-only, never route.](<redacted>.md) | ideas / lifecycle |
324
325
 
325
326
  > ⚠️ **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.
@@ -1425,5 +1425,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
1425
1425
  landed since 1.19.492 with no explicit bump. run 33694576680. (task 1002620)
1426
1426
  1.19.494 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1427
1427
  landed since 1.19.493 with no explicit bump. run 33697218855. (task 1002620)
1428
+ 1.19.495 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1429
+ landed since 1.19.494 with no explicit bump. run 33766103175. (task 1002620)
1430
+ 1.19.496 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1431
+ landed since 1.19.495 with no explicit bump. run 33766363834. (task 1002620)
1428
1432
  ---------------------------------------------------------------------------
1429
1433
  ```
@@ -40,8 +40,14 @@ function generalGoalTitle(versionId) {
40
40
  // authorization gate. This is advice appended to a request that ALREADY SUCCEEDED;
41
41
  // a wrong guess here nags a builder about a task that is fine, which is worse than
42
42
  // staying quiet. A missing versionId or an unreadable goal title yields null.
43
- function adviseGoal({ versionId, goalId, goalTitle } = {}) {
43
+ //
44
+ // kind='bug' is a THIRD silent case (task 1003214, idea 1000828 — the owner
45
+ // directive against manufacturing small goals): a fix sitting in the catch-all,
46
+ // or with no goal at all, is already where it belongs, so both reasons above
47
+ // collapse to null for a bug. The nudge is for homeless FEATURE work only.
48
+ function adviseGoal({ versionId, goalId, goalTitle, kind } = {}) {
44
49
  if (goalId == null) {
50
+ if (kind === 'bug') return null;
45
51
  return {
46
52
  reason: 'no_goal',
47
53
  message: 'This task has no goal to live in — no scope wall, no done-when criteria gating it, '
@@ -53,6 +59,7 @@ function adviseGoal({ versionId, goalId, goalTitle } = {}) {
53
59
  }
54
60
  if (versionId == null || typeof goalTitle !== 'string') return null;
55
61
  if (goalTitle !== generalGoalTitle(versionId)) return null;
62
+ if (kind === 'bug') return null;
56
63
  return {
57
64
  reason: 'catch_all',
58
65
  message: `This task landed in "${goalTitle}", the version's catch-all bucket rather than a real goal — `
@@ -93,6 +100,7 @@ async function buildGoalAdvisory({ db, versionId, task }) {
93
100
  versionId,
94
101
  goalId: task.goal_id,
95
102
  goalTitle: homeGoal && homeGoal.title,
103
+ kind: task.kind,
96
104
  });
97
105
  if (!verdict) return null;
98
106
  // LIMIT 6, not 6-in-JS: Postgres truncates instead of shipping every open goal
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.494",
3
+ "version": "1.19.496",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.494",
9
+ "version": "1.19.496",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.494",
3
+ "version": "1.19.496",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
package/src/module-api.js CHANGED
@@ -45,7 +45,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
45
45
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
46
46
  // the entry to that file. Look for a version's history there, not here.
47
47
  // ---------------------------------------------------------------------------
48
- const CORE_VERSION = '1.19.494'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
48
+ const CORE_VERSION = '1.19.496'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
49
49
 
50
50
  // A namespaced logger so a module's log lines are attributable + consistent.
51
51
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -55,10 +55,30 @@ await test('catch_all — landing in "<version> — general" is a bucket, not a
55
55
  assert.equal(a.goal_id, 3, 'names the catch-all so a caller can PATCH away from it');
56
56
  });
57
57
 
58
+ // task 1003214 (idea 1000828): a bug in the catch-all is where it belongs, so the
59
+ // advisory goes silent (reuses the same null the module already uses for "no
60
+ // advice needed").
61
+ await test("kind='bug' in the catch-all is quiet — a fix is where it belongs", () => {
62
+ assert.equal(adviseGoal({ versionId: V, goalId: 3, goalTitle: generalGoalTitle(V), kind: 'bug' }), null);
63
+ });
64
+
65
+ await test("kind='feature' in the catch-all still gets the nudge — quiet is bug-only", () => {
66
+ const a = adviseGoal({ versionId: V, goalId: 3, goalTitle: generalGoalTitle(V), kind: 'feature' });
67
+ assert.equal(a.reason, 'catch_all');
68
+ });
69
+
70
+ await test("kind='bug' with no goal at all is quiet too — a fix needs no home to be filed correctly", () => {
71
+ assert.equal(adviseGoal({ versionId: V, goalId: null, kind: 'bug' }), null);
72
+ });
73
+
58
74
  await test('a REAL goal is silent — null, not an empty object', () => {
59
75
  assert.equal(adviseGoal({ versionId: V, goalId: 1000025, goalTitle: 'Builder experience — hall, dev box, and skills' }), null);
60
76
  });
61
77
 
78
+ await test("kind='bug' under a real goal is silent too — already silent, kind changes nothing", () => {
79
+ assert.equal(adviseGoal({ versionId: V, goalId: 1000025, goalTitle: 'Builder experience — hall, dev box, and skills', kind: 'bug' }), null);
80
+ });
81
+
62
82
  // The bug this guards: a near-miss title must not be mistaken for the catch-all.
63
83
  // db.generalGoalIdForVersion resolves the SAME string, so if these two ever disagree
64
84
  // a task would be filed into a goal the advisory then calls orphaned (or vice versa).
@@ -164,6 +184,14 @@ await test('buildGoalAdvisory: the catch-all is detected and named', async () =>
164
184
  assert.equal(a.goal_id, 3);
165
185
  });
166
186
 
187
+ // task 1003214: the full POST /tasks path (buildGoalAdvisory reads task.kind
188
+ // off the already-committed row) for a bug landed in the catch-all — silent.
189
+ await test("buildGoalAdvisory: kind='bug' in the catch-all yields null end to end", async () => {
190
+ const db = stubDb({ goal: { id: 3, title: generalGoalTitle(V) }, goals: [] });
191
+ const a = await buildGoalAdvisory({ db, versionId: V, task: { id: 7, goal_id: 3, kind: 'bug' } });
192
+ assert.equal(a, null);
193
+ });
194
+
167
195
  // The invariant a future refactor will quietly break. Both DB calls are covered:
168
196
  // getGoal is only reached when goal_id is set, listGoals only on the unhappy path.
169
197
  await test('NON-BLOCKING: buildGoalAdvisory returns null when getGoal throws', async () => {
@@ -0,0 +1,44 @@
1
+ // tests/redteam_followup_kind.mjs — task 1003452.
2
+ //
3
+ // /builder-redteam's optional follow-up idea_inbox POST filed kind:'security-finding',
4
+ // which is not in idea_inbox's CHECK constraint (migrations/042_criterion_proposal_workflow.sql
5
+ // mirrored at modules/ideas/inbox.js VALID_KINDS) — the POST 400s every time, and because
6
+ // the primary vulnerability_reports write already succeeded, the failure is silent (the
7
+ // report lands, the fix idea never files). Fixed by filing kind:'bug' instead — a red-team
8
+ // finding is a bug in the platform's own walls.
9
+ //
10
+ // Static: reads the skill's own committed idea.json shape (CRLF-normalised) and asserts the
11
+ // kind it files is a real member of the enum, imported (not duplicated) from the module that
12
+ // owns it — so a future enum change or a re-broken skill both fail this test honestly.
13
+ //
14
+ // Run: node --test --test-reporter=tap tests/redteam_followup_kind.mjs
15
+
16
+ import { test } from 'node:test';
17
+ import assert from 'node:assert/strict';
18
+ import fs from 'node:fs';
19
+ import path from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
21
+ import { VALID_KINDS } from '../modules/ideas/inbox.js';
22
+
23
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
24
+ const SKILL_MD = path.join(ROOT, '.claude', 'skills', 'builder-redteam', 'SKILL.md');
25
+
26
+ function readNormalized(p) {
27
+ return fs.readFileSync(p, 'utf8').replace(/\r\n/g, '\n');
28
+ }
29
+
30
+ test('the follow-up idea.json files a kind that is a real member of VALID_KINDS', () => {
31
+ const md = readNormalized(SKILL_MD);
32
+ const m = md.match(/"kind":\s*"([^"]+)"/);
33
+ assert.ok(m, 'SKILL.md must show the literal idea.json shape with a "kind" field');
34
+ const filedKind = m[1];
35
+ assert.ok(
36
+ VALID_KINDS.includes(filedKind),
37
+ `SKILL.md files kind:'${filedKind}', which is not in VALID_KINDS (${VALID_KINDS.join(', ')})`
38
+ );
39
+ });
40
+
41
+ test('SKILL.md does not still describe the dead kind=\'security-finding\' value anywhere', () => {
42
+ const md = readNormalized(SKILL_MD);
43
+ assert.doesNotMatch(md, /security-finding/, 'the invalid kind must not linger in prose either');
44
+ });