@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 +26 -16
- package/.claude/skills/builder-redteam/SKILL.md +2 -2
- package/docs/adr/0234-idea-routing-capture-time-promotion-landing-matrix-homeless-inbox.md +183 -0
- package/docs/adr/README.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/modules/lifecycle/goal-advisory.js +9 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/goal_advisory.mjs +28 -0
- package/tests/redteam_followup_kind.mjs +44 -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.496",
|
|
6
|
+
"core_contract": "1.19.496",
|
|
7
|
+
"source_commit": "8c795e90254fbc29139c278e0fd6d890b2c32035",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-03T14:22:08.467Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 425,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1938,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
7215
|
+
"sha256": "cb12024e16aedcd701269af77cc73012013873d07e37301c24ae10e3c1f2d28b"
|
|
7211
7216
|
},
|
|
7212
7217
|
{
|
|
7213
7218
|
"path": "package.json",
|
|
7214
7219
|
"mode": "0000644",
|
|
7215
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
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='
|
|
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": "
|
|
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.
|
package/docs/adr/README.md
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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');
|
package/tests/goal_advisory.mjs
CHANGED
|
@@ -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
|
+
});
|