@bongos/core 1.19.343 → 1.19.345
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 +17 -12
- package/docs/adr/0187-collab-four-decisions.md +110 -0
- package/docs/adr/README.md +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +5 -1
- package/tests/baseline_staleness.mjs +18 -9
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.345",
|
|
6
|
+
"core_contract": "1.19.345",
|
|
7
|
+
"source_commit": "ef6d8227178f4b2a0b050772442f2440b6ad34ab",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-25T02:17:29.630Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 328,
|
|
13
13
|
"agent_docs_stubbed": 22,
|
|
14
14
|
"functional_verbatim": 1489,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1839,
|
|
20
|
+
"tree_sha256": "2405d3d77e5a22dee9b903e443cef2f24c9b91b96c890cd2ad93b26ed7e208d4",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1414,6 +1414,11 @@
|
|
|
1414
1414
|
"mode": "0000644",
|
|
1415
1415
|
"sha256": "749040f16b59cc1cd1c49ca40453fed838f31b36af8c8eed2b5de7db00ddb0d0"
|
|
1416
1416
|
},
|
|
1417
|
+
{
|
|
1418
|
+
"path": "docs/adr/0187-collab-four-decisions.md",
|
|
1419
|
+
"mode": "0000644",
|
|
1420
|
+
"sha256": "c87ed3fdfc93f9ba725327d5697e0b8cb1603d6a96a9bc68749788e8ab8a2c8a"
|
|
1421
|
+
},
|
|
1417
1422
|
{
|
|
1418
1423
|
"path": "docs/adr/0187-oauth-handshake-cookie-lifetime.md",
|
|
1419
1424
|
"mode": "0000644",
|
|
@@ -1432,7 +1437,7 @@
|
|
|
1432
1437
|
{
|
|
1433
1438
|
"path": "docs/adr/README.md",
|
|
1434
1439
|
"mode": "0000644",
|
|
1435
|
-
"sha256": "
|
|
1440
|
+
"sha256": "23ee65dab5a7e5eea9e69403f0c49f2e6ec2b37e8a5473562898b122d33d1236"
|
|
1436
1441
|
},
|
|
1437
1442
|
{
|
|
1438
1443
|
"path": "docs/api-reference.md",
|
|
@@ -4737,12 +4742,12 @@
|
|
|
4737
4742
|
{
|
|
4738
4743
|
"path": "package-lock.json",
|
|
4739
4744
|
"mode": "0000644",
|
|
4740
|
-
"sha256": "
|
|
4745
|
+
"sha256": "1f08aad9924b7023db8de3c1bb3b7212da7a826227695318d23634865f729975"
|
|
4741
4746
|
},
|
|
4742
4747
|
{
|
|
4743
4748
|
"path": "package.json",
|
|
4744
4749
|
"mode": "0000644",
|
|
4745
|
-
"sha256": "
|
|
4750
|
+
"sha256": "1790b798df1626b11f335c995d9c6e6e6c297fead47852cab15f8fcfae4f89f0"
|
|
4746
4751
|
},
|
|
4747
4752
|
{
|
|
4748
4753
|
"path": "public-docs/index.html",
|
|
@@ -6582,7 +6587,7 @@
|
|
|
6582
6587
|
{
|
|
6583
6588
|
"path": "src/module-api.js",
|
|
6584
6589
|
"mode": "0000644",
|
|
6585
|
-
"sha256": "
|
|
6590
|
+
"sha256": "3aa07baacc3e1cdc43bc310db5e0c104c931117a788879501694c9d525083634"
|
|
6586
6591
|
},
|
|
6587
6592
|
{
|
|
6588
6593
|
"path": "src/module-loader/catalog.js",
|
|
@@ -6842,7 +6847,7 @@
|
|
|
6842
6847
|
{
|
|
6843
6848
|
"path": "tests/baseline_staleness.mjs",
|
|
6844
6849
|
"mode": "0000644",
|
|
6845
|
-
"sha256": "
|
|
6850
|
+
"sha256": "2543492e930ace22ed9de1bf69c6efb5fdba265e88d322a45a35da18248a8731"
|
|
6846
6851
|
},
|
|
6847
6852
|
{
|
|
6848
6853
|
"path": "tests/bfg.mjs",
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# 0187 — Collab's four carried-forward questions, answered
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted
|
|
4
|
+
- **Date:** 2026-08-24
|
|
5
|
+
- **Tasks:** [#1003170](https://cloudbongos.com/builders#/task/1003170) (goal 1000075 — collaboration and the social hall). Settles the questions criteria `collab-pass-a-task`, `collab-closeness-consented` and `collab-mingle-two-switches` were each carrying.
|
|
6
|
+
- **Related:** [ADR 0171](<redacted>.md) (fail-closed consent in the READ path), ADR 0032 / 0033 (Discord as a one-way mirror, outside the trust boundary), [ADR 0120](<redacted>.md) (pay on land)
|
|
7
|
+
|
|
8
|
+
## 1. Why this ADR exists at all
|
|
9
|
+
|
|
10
|
+
Goal 1000075 was written with four questions left open **on purpose**, under the
|
|
11
|
+
owner's instruction to *"answer them in the goal, do not let them be decided by
|
|
12
|
+
accident"*. That phrasing is the point: each one is the kind of question an
|
|
13
|
+
implementation answers silently if nobody answers it first — by picking whichever
|
|
14
|
+
branch was easier to code, and then the choice is load-bearing and undocumented.
|
|
15
|
+
|
|
16
|
+
The answers were given on 2026-08-23. This ADR records them, and the criteria
|
|
17
|
+
they settle were rewritten from *"the goal states explicitly whether X or Y"* into
|
|
18
|
+
the decision itself, so they are testable rather than a note to decide something.
|
|
19
|
+
|
|
20
|
+
## 2. Closeness is a WEIGHTED BLEND, and it must be able to explain itself
|
|
21
|
+
|
|
22
|
+
Closeness is computed from several signals — co-claims, co-build sessions,
|
|
23
|
+
goals shipped together, and (see §4) comms and mentions once chat is mirrored in.
|
|
24
|
+
Not a single signal.
|
|
25
|
+
|
|
26
|
+
Two consequences that are part of the decision, not follow-ups:
|
|
27
|
+
|
|
28
|
+
- **The weights are data, not a magic constant.** A blend whose coefficients are
|
|
29
|
+
buried in a function is a model nobody can revise without reading code.
|
|
30
|
+
- **"Why am I close to them" must be answerable on the surface that shows it.**
|
|
31
|
+
This score describes real people to each other. A number that ranks colleagues
|
|
32
|
+
and cannot say what it is made of is the kind of thing that quietly becomes
|
|
33
|
+
unfair, and nobody can argue with it because there is nothing to point at.
|
|
34
|
+
|
|
35
|
+
`collab-closeness-consented` already required the graph to resolve through
|
|
36
|
+
ADR 0171's fail-closed consent, with the hide flag enforced in the READ path. That
|
|
37
|
+
stands: a builder who has hidden their profile is not a node in anyone's graph.
|
|
38
|
+
|
|
39
|
+
## 3. Passing a task RECOMMENDS; it never transfers the claim
|
|
40
|
+
|
|
41
|
+
The recipient still claims it themselves. Nobody acquires work without opting in,
|
|
42
|
+
`claim`/`release` remains the single path to holding a task, and declining costs
|
|
43
|
+
nothing.
|
|
44
|
+
|
|
45
|
+
The ledger records **who recommended it and why** — which was already the
|
|
46
|
+
criterion's own point: an untraced recommendation is indistinguishable from a task
|
|
47
|
+
quietly changing hands.
|
|
48
|
+
|
|
49
|
+
**Rejected:** transfer-on-pass. It would make the claim ledger stop meaning "this
|
|
50
|
+
person chose this work", which is the property the whole lifecycle rests on.
|
|
51
|
+
|
|
52
|
+
## 4. Co-build is a GOAL-level fact, not a shared claim — and the reward question was wrong-premised
|
|
53
|
+
|
|
54
|
+
The original question asked how to split a reward between co-builders. The owner's
|
|
55
|
+
answer rejects the premise. Verbatim: co-building *"is more about performing the
|
|
56
|
+
different ABI tasks together, no one is ever going to co run the task on their
|
|
57
|
+
claims. Likewise co building is more about goals etc. where people are working
|
|
58
|
+
together."*
|
|
59
|
+
|
|
60
|
+
So:
|
|
61
|
+
|
|
62
|
+
- the unit of collaboration is the **goal**, not the task;
|
|
63
|
+
- each builder holds and is paid for their **own** task exactly as today;
|
|
64
|
+
- **there is no split and no double award to design.** What co-build needs is
|
|
65
|
+
presence and visibility of people working the same goal — not a new payment rule.
|
|
66
|
+
|
|
67
|
+
This is worth recording precisely because it is a *deletion*: a reward-splitting
|
|
68
|
+
mechanism would otherwise have been designed, reviewed and shipped for a situation
|
|
69
|
+
that does not occur.
|
|
70
|
+
|
|
71
|
+
### 4a. The same answer ADDS scope: Discord mirrors INWARD
|
|
72
|
+
|
|
73
|
+
Verbatim: *"big part will be to mirror chats from discord into the social features
|
|
74
|
+
like the agora etc. into bongos that way we can actually also see comms and
|
|
75
|
+
mentions."*
|
|
76
|
+
|
|
77
|
+
Discord is **outbound-only** today and sits outside the trust boundary (ADR 0032 /
|
|
78
|
+
0033). Mirroring conversation *inward* is therefore not a plumbing change — it
|
|
79
|
+
puts attacker-authorable text on an in-hall surface, and this goal must answer the
|
|
80
|
+
trust-boundary posture for mirrored content rather than assume it.
|
|
81
|
+
|
|
82
|
+
**It is in scope for this goal and it is BLOCKED**, on [task #1002946](https://cloudbongos.com/builders#/task/1002946)
|
|
83
|
+
(BV1.R166, the Agora surface in goal 1000064), which is deliberately unstarted
|
|
84
|
+
pending team buy-in. Two things follow: the mirror cannot land before that goal
|
|
85
|
+
opens, and **this goal must not quietly reimplement the Agora to get around it.**
|
|
86
|
+
|
|
87
|
+
**No criterion was added for it, deliberately.** A criterion blocked on another
|
|
88
|
+
goal's unstarted work would make 1000075 unachievable for reasons that have
|
|
89
|
+
nothing to do with collaboration. It is recorded here as scope with a named
|
|
90
|
+
dependency; it earns its criterion when goal 1000064 opens.
|
|
91
|
+
|
|
92
|
+
## 5. Mingle pairs CROSS-CRAFT
|
|
93
|
+
|
|
94
|
+
An engineer with an artist or an ideator. This matches the goal's founding
|
|
95
|
+
premise — the person you need is often not another one of you — and keeps mingle
|
|
96
|
+
doing something the craft-matched Collab request path does not already do.
|
|
97
|
+
|
|
98
|
+
**Rejected:** same-craft pairing "to go deep". It is a reasonable feature and it
|
|
99
|
+
duplicates what `collab-page-matches-my-craft` already delivers.
|
|
100
|
+
|
|
101
|
+
The two-switch consent rule is unchanged: project-level enable **plus** per-builder
|
|
102
|
+
opt-in, pairing declinable without penalty. Auto-pairing a person with a stranger
|
|
103
|
+
is a real-world social commitment, and one switch is not consent.
|
|
104
|
+
|
|
105
|
+
## 6. What this ADR does not decide
|
|
106
|
+
|
|
107
|
+
The closeness **weights** themselves, the mirrored-content trust posture (§4a),
|
|
108
|
+
and whether mingle ever gains a same-craft mode. Each belongs to the task that
|
|
109
|
+
implements the surface, and each now has a decision above it to be consistent
|
|
110
|
+
with.
|
package/docs/adr/README.md
CHANGED
|
@@ -260,6 +260,7 @@ This keeps the decision history honest and traceable.
|
|
|
260
260
|
| 0184 | [**A ship must account for itself: `shipTask` requires a land proof or a declared no-artifact** ([task 1002483](https://cloudbongos.com/builders#/task/1002483), goal 1000024). `db.shipTask` is the confirmed→shipped flip and, since [ADR 0120](<redacted>.md), where the CREDIT is booked — yet its only precondition was `WHERE status = 'confirmed'`, nothing about whether code reached main. The two AUTOMATED paths each proved the land first; the MANUAL route passed nothing, so a task confirmed with no pushed code could be shipped by its claim holder **or by any Metic+ sweeping `/merge-mode`**, booking its `task.shipped` credit. It stayed untracked for two months because its tracking task was closed in June asserting task 1001547 had delivered the gate — it had not, blocker 1000047's own final update says so, and the code agreed with the blocker rather than the closure note: **a hole hidden by a record nobody checked**, which is the same failure the gate is about. Decision: the gate lives at the WRITE, not the route (a route gate leaves the invariant unenforced at the layer that moves the ledger, so the next caller reopens it silently), and `shipTask` is FAIL-CLOSED on a required `landAssurance`. TWO kinds, and the second is why this does not make things worse: `proof` (this task's commit is on main) and **`no_artifact`** — a deliberate DB-only ship, an explicitly supported shape (CLAUDE.md "Direct DB writes are work too") that [task 1002478](https://cloudbongos.com/builders#/task/1002478) documents as a dead end below Archon. A naive proof-only gate would have made 1002478 strictly worse; naming no-artifact as a KIND turns it from an absence the gate must be talked out of into a recorded answer. Both carry a reason, audited post-commit and best-effort (an audit miss must never roll back a landed ship). `ship.js` now declares its own `--db-only`/`--allow-empty` lands, without which this fix would have regressed the very flow CLAUDE.md calls supported. Rejected: (b) gate only the route — leaves the write unguarded; (c) record the residual as tolerable — defensible on blast radius and genuinely close, but it would have been the FOURTH record asserting this was handled with nothing checkable behind it. Does NOT gate confirm: a codeless confirm is still accepted, it simply cannot reach `shipped` or pay.](<redacted>.md) | lifecycle / integrity |
|
|
261
261
|
| 0185 | [**The spark-handoff credit split: 70% developer, 30% spark author** ([task 1002923](https://cloudbongos.com/builders#/task/1002923), BV1.R147, goal 1000062 — completing the chain [task 1002920](https://cloudbongos.com/builders#/task/1002920) mark → [task 1002921](https://cloudbongos.com/builders#/task/1002921) queue → [task 1002922](https://cloudbongos.com/builders#/task/1002922) pickup). [ADR 0172](<redacted>.md) built the ideator streams and paid `captured_by`, the only person an idea then had. The spark handoff created a second: R146 develops a spark IN PLACE, preserving the author while recording a `developed_by`. **R146 therefore shipped paying NOTHING on purpose** — not an oversight but a refusal to guess, because `awardIdeaFullPassCredit`'s idempotency predicate is `(builder_id, reason)`, so paying the DEVELOPER would not have blocked a later payment to the AUTHOR and one idea would have paid twice; the cost of that caution was that a developed Full Idea received stream 2 **never** unless its author resubmitted. Owner decision (2026-08-22): **70/30 favouring the developer**, on BOTH existing streams, one ratio so there is one rule to explain. The reasoning is behavioural, not sentimental — developing someone else's spark is the scarce act the queue depends on, and at an even split it pays HALF what authoring your own Full Idea pays for the same writing work, so the rational move is always to write your own and the R145 queue stays a list nobody reads. The author keeps 30% plus their share of stream 1 on every task shipped under the idea, forever. `IDEA_DEVELOPER_SHARE = 0.70` is a **policy constant in `credits.js`** beside the pass bar and quick factor (ADR 0172's own consequence, the ADR 0023 precedent): a share of someone's earnings deserves a review and a commit, not a runtime edit. **The structural half is what retires R146's caution:** the economy module now derives the payees itself (`ideaCreditParties` reads `captured_by` + `developed_by`), so the caller can no longer choose a recipient — the develop route hands over an idea and a score, never a payee, and books the split like any other path (fail-open; the development has already committed). Three arithmetic properties, each a real bug if missed: the total is **conserved** (developer rounded, author gets the REMAINDER — rounding both pays 6 out of a 5-credit payout); a **self-developed** idea collapses to ONE payee, because two rows for one builder would silently UNDER-pay under the per-builder idempotency predicate, not double-pay (the comparison normalises via `String()` since `bigint` arrives as a string, so a type mismatch cannot become a phantom payee); and **zero-value rows are dropped**. Ids otherwise pass through unconverted — `Number()`-ing a `bigint` risks precision. `builder_id` in the idempotency predicate, which entered as an index optimisation (the R143/IC2 grader finding), is now **load-bearing for correctness**: two payees share one `reason`, so a reason-only predicate would refuse the second, and a partially-booked split completes on the next call. **Not decided here:** stream 3's split (ships with the Board Room, task 1002999), and whether `origin='develop'` in `ideas_task_lineage` belongs to whoever promotes a DEVELOPED idea into a task — ADR 0172 §5 says the row is "set at promotion and at develop (R146)", but R146 creates no task, so that origin stays deliberately unwired rather than invented; it changes who stream 1 pays.](<redacted>.md) | economy / ideator lane |
|
|
262
262
|
| 0186 | [**Per-project idea templates: the seam is already built; the economics are the open problem** ([task 1002932](https://cloudbongos.com/builders#/task/1002932), BV1.R156, goal 1000062 — a SPIKE, output is this ADR plus a task list, no implementation). The literal question — how does a founder define their own idea shape without a schema change or forking the scorer — is **already answered by shipped work**: R141 gave every template function an optional `registry` defaulting to the built-in one, so validation and scoring both read the declaration as data; `ideas_grades.template_key` is per-row with a format CHECK whose own comment says a project key "stays addressable"; and `provisioning_instances.settings jsonb` is an existing per-project home. **No schema change, no fork.** Decision A: a **`project-settings` kernel port**, sibling of `builder-settings`, read via `resolveOptional` so a vanilla instance with no provisioning module degrades to the built-in registry; `base-v1` stays the default and the fallback for an unresolvable key. Rejected a new `ideas_templates` table (second home for data that has one) and a config file (per-instance, but one instance hosts many projects). **Decision B is the finding that matters.** Completeness is normalised 0–100 WITHIN its template (mean of per-field ratios), while karma reads it against an absolute bar (`IDEA_FULL_PASS_BAR = 60`, `IDEA_FULL_PASS_CREDIT = 5`) — so template choice becomes a credit-minting lever: `base-v1` needs ~200 characters for 5 credits, a 1-field template with `substanceMin` 10 needs ~10, a ~20× arbitrage handed to exactly the projects this ADR empowers. Shipping A without B would make the feature a way to print credits. The fix separates the two questions one number is answering: **completeness stays relative** (it is the right signal for triage ordering and the hall meter) and **karma stops reading it**, switching to an absolute substance total the scorer already computes per field and currently discards after averaging. An interim guard (≥3 required fields, ≥120 total `substanceMin`) bounds the arbitrage without fixing it. Decision C: an optional per-field `weight` (default 1) turning the unweighted mean into a weighted one — today's behaviour is the all-weights-1 case, so it is a data addition and one arithmetic line. Decision D: the five base fields are **not** mandatory (forcing them would make "define your own" cosmetic), replaced by two constraints — a template must declare ≥1 required field, and a stored template is validated at WRITE time so a malformed one is refused there rather than discovered inside the capture path, which must never throw. **Boundary note:** goal 1000062's wall is `[ideas, hall-ui]`, so the port task (provisioning + kernel) and the karma task (economy) cannot be claimed under it.](<redacted>.md) | ideas / templates |
|
|
263
|
+
| 0187 | [**Collab's four carried-forward questions, answered** ([task 1003170](https://cloudbongos.com/builders#/task/1003170), goal 1000075). The goal deliberately carried four open questions under the owner's instruction to "answer them in the goal, do not let them be decided by accident" — each being the kind an implementation settles silently by picking whichever branch was easier to code. **(1) Closeness is a WEIGHTED BLEND** of co-claims, co-build sessions, goals shipped together and (per 4a) comms/mentions — not one signal. Two parts of the decision, not follow-ups: the weights are DATA rather than a buried constant, and "why am I close to them" must be answerable on the surface that shows it, because a number that ranks colleagues and cannot say what it is made of becomes unfair with nothing to point at. Consent is unchanged — ADR 0171 fail-closed, hide enforced in the READ path, a hidden builder is not a node in anyone's graph. **(2) Passing RECOMMENDS, never transfers**: the recipient still claims it, claim/release stays the only path to holding work, declining is free, and the ledger records who recommended it and why. Transfer-on-pass was rejected because it would stop the claim ledger meaning "this person chose this work". **(3) Co-build is a GOAL-level fact, not a shared claim — and the reward question was WRONG-PREMISED.** Owner, verbatim: nobody will "co run the task on their claims"; collaboration happens at the goal. So each builder holds and is paid for their own task exactly as today and there is NO split to design — recorded because it is a deletion: a reward-splitting mechanism would otherwise have been designed, reviewed and shipped for a situation that does not occur. **(4) Mingle pairs CROSS-CRAFT** (engineer with artist or ideator), same-craft rejected as duplicating the craft-matched Collab path; the two-switch consent rule stands, since one switch is not consent for a real-world social commitment. **4a adds scope:** Discord mirrors INWARD so comms and mentions are visible in-hall — which puts attacker-authorable text on an in-hall surface, so the trust posture for mirrored content is this goal's to answer rather than assume (Discord is outbound-only and outside the boundary today, ADR 0032/0033). It is BLOCKED on task 1002946 (the Agora, goal 1000064, unstarted pending team buy-in), and this goal must not reimplement the Agora to get around it. No criterion was added for it deliberately — one blocked on another goal's unstarted work would make 1000075 unachievable for reasons unrelated to collaboration.](<redacted>.md) | collab / social hall |
|
|
263
264
|
| 0187 | [**The OAuth web-handshake cookies live 30 minutes, and move together** ([task 1003234](https://cloudbongos.com/builders#/task/1003234), goal 1000058). The three cookies `/auth/web/start` sets — the CSRF state, the validated return target, the repo-scope opt-in — were 10 minutes each, a hall builder's timescale; a founder's first sign-in can include a GitHub SIGNUP with email verification and came back past ten minutes to "State mismatch" with the return + repo scope gone, and the page's retry door was a bare start. Decision: one `HANDSHAKE_COOKIE_MS = 30 min` for all three (they MUST move together — a state outliving its return cookie lands on the default landing with no scope, a return outliving its state lands on State mismatch), and every OAuth error page plus the access-pending auto-walk-in rebuilds its door from whatever cookies are still valid (`recoveryStartHref`, through the same open-redirect allowlist). What did NOT change: the state is still single-use, still `httpOnly`/`sameSite=lax`/apex-scoped, the code still the bearer — only the window for a legitimate handshake grew. Rejected: lengthening only return + repos (keeps the dead end and breaks the one-lifetime invariant the other way), a server-side state store, sixty minutes. The Discord-link state cookie is a different flow and keeps 10.](<redacted>.md) | auth / security posture |
|
|
264
265
|
| 0188 | [**The weekly patrol runs on CI, and a declared cadence must have something that fires it** ([task 1003209](https://cloudbongos.com/builders#/task/1003209), goal 1000079). Goal 1000076 shipped a weekly red-team patrol, a routine describing how to run it, a recipe, and a manifest row — and nothing ever ran it; the follow-up audit §7 named all three halves (no committed scheduler, no cadence for a scheduler to read, and a manifest green anyway because its wiring predicate was satisfied by the SKILL.md mentioning its own scanner). The symmetric fix — an in-process timer beside `peer-vote-tally` in `routine-timers.js` — is **wrong on correctness, not cost**: all three patrol checks read things only the REPO has (full git history, `.github/workflows/`, an auditable lockfile), and a deployed instance runs a VENDORED core out of `node_modules/@bongos/core` that has none of them, so the timer would have scanned nothing and reported clean — worse than unscheduled. The runner is therefore CI: a weekly Actions lane with `fetch-depth: 0` and a pinned, checksum-verified gitleaks (the missing binary the audit listed separately), invoking `run-routine.js redteam-weekly` so the SKILL.md's steps stay the single source of truth. `cadence:` becomes a real frontmatter field — declarative only, scheduler-agnostic. The load-bearing piece is `routine-schedule.js` (fitness Check 25), whose predicate is **relational, not existential**: it classifies the cron into the period it actually realizes and fails when that disagrees with the declared cadence — the one rot mode no substring or existence check can EVER see, since editing weekly→monthly leaves every "is it wired?" predicate green. The manifest row was re-pointed to match ("wired" now means the SCHEDULER names the routine). Finally the exit code says **a finding was LOST, not that a finding exists**: findings are advisory (ADR 0162), so filing three is a SUCCESS and red is reserved for findings that cannot be recorded — failing on findings would have gone red on run one and stayed there (the unrotated staging credential in `092e473` is a standing history hit), and an always-red weekly job is one nobody reads. Filing is credential-gated but OPTIONAL — with no session secret the patrol still runs and writes its findings to the job summary, which matters because this goal's C1 is about eliminating long-lived tokens, not adding one. Rejected: the in-process timer; teaching `routine-timers.js` generic cadence discovery (a scheduler built for nobody — its only candidate user cannot run there); failing whenever findings exist; requiring the secret; a deeper manifest predicate instead of a new check.](<redacted>.md) | security / enforcement durability |
|
|
265
266
|
| 0189 | [**A frozen baseline must justify itself: declared floors and the freeze-forever detector** ([task 1003210](https://cloudbongos.com/builders#/task/1003210), goal 1000079). The shrink-only ratchet makes a quality metric unable to REGRESS and, by the same design, unable to sit still noticeably: the follow-up audit measured the whole file at "every metric equal to baseline, 0 reduced" and the ratchet reported a clean PASS, because one green covers both actively-paying-down and nobody-has-looked-in-a-year. The ratchet has no concept of time; this adds it. **Age comes from git, never a stored `since:` field** — a timestamp beside each metric would be faster and hermetic and would also be the field that silently stops being updated, leaving old debt permanently fresh-looking, which is worse than no check because it produces confident green; an unavailable history reports UNKNOWN, not fresh (the `--require-roster` doctrine). A metric's age is the date of the OLDEST commit still carrying its current value, so a baseline recorded once and never revisited dates to its introduction — exactly the case being hunted. **The half that matters is the declared floor.** The naive "flag anything unmoved for N weeks" fires forever on metrics that are FINISHED (`require_cycle_count`'s four deliberate lazy requires; the ~28 duplicate windows ADR 0091 §2 forces because a module may not import core), and a check that cries wolf weekly is one everyone learns to skip — the same alert-fatigue failure ADR 0188 §2.4 avoids. So a metric may declare `floors: {name: {value, why}}` and a metric AT its floor is done, not stale; the `why` is mandatory and length-checked, because a floor exempts a metric from the clock permanently and must therefore be a written claim someone can disagree with — **silence was the problem**, and a floor without a reason is just a mute button. A floor exempts only DOWN TO the floor, so `duplicate_window_count`'s 59-vs-28 keeps its ~31 reducible windows on the clock. **Two severities, split by whose fault it is:** declaration rot (a floor naming a dead metric, a floor with no reason, a floor the metric already dropped below) is hermetic and caused by the diff at hand, so it HARD-FAILS the merge gate; staleness is nobody's diff and would punish whoever pushed next, so it warns in fitness and is FILED weekly by the patrol — the "repeatable, scheduled version" the audit asked for. Also fixed a latent eater: `--update-baselines` wrote a fixed object literal, so one refresh destroyed the accumulated `_comment` and (once floors landed) every floor, deleting a finished metric's justification from the repo while turning it into permanent weekly noise. Nothing is flagged today — the file is six days old — so the detector is verified by shrinking the window, not by waiting. Rejected: a stored `since:`; no exemptions; a floor with no required reason; hard-failing on staleness; patrol-only placement.](<redacted>.md) | quality ratchets / debt governance |
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.345",
|
|
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.345",
|
|
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.345",
|
|
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
|
@@ -1116,8 +1116,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
1116
1116
|
// landed since 1.19.341 with no explicit bump. run 32799103427. (task 1002620)
|
|
1117
1117
|
// 1.19.343 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1118
1118
|
// landed since 1.19.342 with no explicit bump. run 32799245758. (task 1002620)
|
|
1119
|
+
// 1.19.344 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1120
|
+
// landed since 1.19.343 with no explicit bump. run 32799492873. (task 1002620)
|
|
1121
|
+
// 1.19.345 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1122
|
+
// landed since 1.19.344 with no explicit bump. run 32800819617. (task 1002620)
|
|
1119
1123
|
// ---------------------------------------------------------------------------
|
|
1120
|
-
const CORE_VERSION = '1.19.
|
|
1124
|
+
const CORE_VERSION = '1.19.345'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1121
1125
|
|
|
1122
1126
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1123
1127
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -165,18 +165,27 @@ test('LIVE: every floor in the shipped baseline file carries a reason', () => {
|
|
|
165
165
|
assert.equal(r.historyAvailable, true, 'this checkout should have git history for the baseline file');
|
|
166
166
|
});
|
|
167
167
|
|
|
168
|
-
test('LIVE: shrink the window
|
|
169
|
-
// Proof the
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
168
|
+
test('LIVE: shrink the window past zero and the real baselines DO get flagged', () => {
|
|
169
|
+
// Proof the detector is not passing merely because it cannot see. A negative
|
|
170
|
+
// window makes EVERY metric with a measurable age older than the threshold, so
|
|
171
|
+
// the flagged set is decided purely by the exemption rules.
|
|
172
|
+
//
|
|
173
|
+
// This deliberately names no individual metric. The first version asserted
|
|
174
|
+
// `doc_stale_links` would be flagged; task 1003211 then reduced that metric,
|
|
175
|
+
// resetting its age to zero, and the test went red on a tree that was perfectly
|
|
176
|
+
// healthy — it had been pinned to transient state rather than to a property.
|
|
177
|
+
// Paying debt down must never break the check that watches the debt.
|
|
178
|
+
const r = bs.run({ maxAgeDays: -1 });
|
|
174
179
|
const names = r.stale.map((s) => s.name);
|
|
175
|
-
assert.ok(names.
|
|
176
|
-
//
|
|
177
|
-
// the
|
|
180
|
+
assert.ok(names.length > 0, 'a negative window must flag every metric that is above its floor');
|
|
181
|
+
// The exemptions are the floor and zero, never the clock — so they hold even
|
|
182
|
+
// when the window guarantees everything else is flagged.
|
|
178
183
|
assert.ok(!names.includes('require_cycle_count'), 'a metric at its declared floor must never be flagged');
|
|
179
184
|
assert.ok(!names.includes('oversized_file_count'), 'a metric at zero must never be flagged');
|
|
185
|
+
assert.ok(!names.includes('excess_lines_total'), 'a metric at zero must never be flagged');
|
|
186
|
+
// Every flagged metric must genuinely be above its floor — the flag means
|
|
187
|
+
// "reducible and unmoved", not merely "old".
|
|
188
|
+
for (const s of r.stale) assert.ok(s.value > s.floor, `${s.name} flagged at or below its floor`);
|
|
180
189
|
});
|
|
181
190
|
|
|
182
191
|
test('LIVE: --update-baselines preserves the floors instead of overwriting them', () => {
|