@bongos/core 1.19.154 → 1.19.156
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 +29 -19
- package/config/branding.neutral.json +1 -1
- package/modules/dev-box/app/src/brand.generated.json +1 -1
- package/modules/sessions/db.js +133 -4
- package/modules/sessions/pollers/session-reward-reconcile.js +13 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/verro-medallion.png +0 -0
- package/public-landing/index.html +167 -69
- package/src/module-api.js +5 -1
- package/tests/cloud_bongos_pack.mjs +2 -1
- package/tests/landing_page.mjs +29 -24
- package/tests/platform_boot.mjs +3 -1
- package/tests/session_attach_from_claim.mjs +188 -0
- package/tests/session_reward_on_land.mjs +3 -1
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.156",
|
|
6
|
+
"core_contract": "1.19.156",
|
|
7
|
+
"source_commit": "3eb459c447a131f66f5d8297ca621cca2bee58a2",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-16T16:14:04.323Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 304,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1256,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1581,
|
|
20
|
+
"tree_sha256": "482b7fbbf94668a5a66dfbbf5f2450699c1670c04b8199d6bf3529b0853cfe31",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -382,7 +382,7 @@
|
|
|
382
382
|
{
|
|
383
383
|
"path": "config/branding.neutral.json",
|
|
384
384
|
"mode": "0000644",
|
|
385
|
-
"sha256": "
|
|
385
|
+
"sha256": "c3d785e319252965f8b5f338908d1dd66bd56985dafacb5d2ab8e5095d2515b3"
|
|
386
386
|
},
|
|
387
387
|
{
|
|
388
388
|
"path": "config/design-tokens.neutral.json",
|
|
@@ -2597,7 +2597,7 @@
|
|
|
2597
2597
|
{
|
|
2598
2598
|
"path": "modules/dev-box/app/src/brand.generated.json",
|
|
2599
2599
|
"mode": "0000644",
|
|
2600
|
-
"sha256": "
|
|
2600
|
+
"sha256": "516593196fc4d75a44325e478690485d91f1d293387a541df571b4bccf6e3360"
|
|
2601
2601
|
},
|
|
2602
2602
|
{
|
|
2603
2603
|
"path": "modules/dev-box/app/src/brand.js",
|
|
@@ -4107,7 +4107,7 @@
|
|
|
4107
4107
|
{
|
|
4108
4108
|
"path": "modules/sessions/db.js",
|
|
4109
4109
|
"mode": "0000644",
|
|
4110
|
-
"sha256": "
|
|
4110
|
+
"sha256": "65cb78d4a3e0507df5d2b37ac99cdb3d762b98de5c34996f64c6de7ad38dc71a"
|
|
4111
4111
|
},
|
|
4112
4112
|
{
|
|
4113
4113
|
"path": "modules/sessions/module.json",
|
|
@@ -4117,7 +4117,7 @@
|
|
|
4117
4117
|
{
|
|
4118
4118
|
"path": "modules/sessions/pollers/session-reward-reconcile.js",
|
|
4119
4119
|
"mode": "0000644",
|
|
4120
|
-
"sha256": "
|
|
4120
|
+
"sha256": "32942d1169776ff1b2a32b85269c285f52167a6f3a7add7e38f74f108a9f3f42"
|
|
4121
4121
|
},
|
|
4122
4122
|
{
|
|
4123
4123
|
"path": "modules/sessions/pulse.js",
|
|
@@ -4187,12 +4187,12 @@
|
|
|
4187
4187
|
{
|
|
4188
4188
|
"path": "package-lock.json",
|
|
4189
4189
|
"mode": "0000644",
|
|
4190
|
-
"sha256": "
|
|
4190
|
+
"sha256": "288b7b3c867bd31f3753a058d01234d02a39d422cc9196569620b3609ca67ba8"
|
|
4191
4191
|
},
|
|
4192
4192
|
{
|
|
4193
4193
|
"path": "package.json",
|
|
4194
4194
|
"mode": "0000644",
|
|
4195
|
-
"sha256": "
|
|
4195
|
+
"sha256": "2892b26243a2c662e0e772a9f8a2a3e7b1ae63bcc046f1d797ac2cab597d7f7e"
|
|
4196
4196
|
},
|
|
4197
4197
|
{
|
|
4198
4198
|
"path": "public-docs/index.html",
|
|
@@ -4434,6 +4434,11 @@
|
|
|
4434
4434
|
"mode": "0000644",
|
|
4435
4435
|
"sha256": "611050e8599be5791e89f9711b76ef6efc5b1e8bd703e2badc6a6418a9ed432b"
|
|
4436
4436
|
},
|
|
4437
|
+
{
|
|
4438
|
+
"path": "public-landing/assets/verro-medallion.png",
|
|
4439
|
+
"mode": "0000644",
|
|
4440
|
+
"sha256": "f374d042a0428f956212a11ca9e86870232ab46af4010bd710a15affd374ab45"
|
|
4441
|
+
},
|
|
4437
4442
|
{
|
|
4438
4443
|
"path": "public-landing/buddha.png",
|
|
4439
4444
|
"mode": "0000644",
|
|
@@ -4442,7 +4447,7 @@
|
|
|
4442
4447
|
{
|
|
4443
4448
|
"path": "public-landing/index.html",
|
|
4444
4449
|
"mode": "0000644",
|
|
4445
|
-
"sha256": "
|
|
4450
|
+
"sha256": "ab299596b6c514ac044b2471330026e1516f6878e02b9045afae0ddc1f5fbfd9"
|
|
4446
4451
|
},
|
|
4447
4452
|
{
|
|
4448
4453
|
"path": "public-landing/projects.html",
|
|
@@ -5782,7 +5787,7 @@
|
|
|
5782
5787
|
{
|
|
5783
5788
|
"path": "src/module-api.js",
|
|
5784
5789
|
"mode": "0000644",
|
|
5785
|
-
"sha256": "
|
|
5790
|
+
"sha256": "7e9d6b73af3f2c1ebb9aa216c63f0049ce892c85297ff2d5f00cc02aa9e6a1cb"
|
|
5786
5791
|
},
|
|
5787
5792
|
{
|
|
5788
5793
|
"path": "src/module-loader/catalog.js",
|
|
@@ -6262,7 +6267,7 @@
|
|
|
6262
6267
|
{
|
|
6263
6268
|
"path": "tests/cloud_bongos_pack.mjs",
|
|
6264
6269
|
"mode": "0000644",
|
|
6265
|
-
"sha256": "
|
|
6270
|
+
"sha256": "9f225b5d4db7e73fbd35bea68ab6c8c64b72bac79daa7be0219e796dc758376a"
|
|
6266
6271
|
},
|
|
6267
6272
|
{
|
|
6268
6273
|
"path": "tests/conductor.mjs",
|
|
@@ -6967,7 +6972,7 @@
|
|
|
6967
6972
|
{
|
|
6968
6973
|
"path": "tests/landing_page.mjs",
|
|
6969
6974
|
"mode": "0000644",
|
|
6970
|
-
"sha256": "
|
|
6975
|
+
"sha256": "2efddbab276f2ef38dbcf09ef6b76d4c1ff58f68c8b11a8803c7a27b605b385f"
|
|
6971
6976
|
},
|
|
6972
6977
|
{
|
|
6973
6978
|
"path": "tests/lib_sh_resolution.mjs",
|
|
@@ -7292,7 +7297,7 @@
|
|
|
7292
7297
|
{
|
|
7293
7298
|
"path": "tests/platform_boot.mjs",
|
|
7294
7299
|
"mode": "0000644",
|
|
7295
|
-
"sha256": "
|
|
7300
|
+
"sha256": "3552e9e4e947c21770d734b340893db576ea42c7b5de9d2822021a03e1345810"
|
|
7296
7301
|
},
|
|
7297
7302
|
{
|
|
7298
7303
|
"path": "tests/post_login_landing.mjs",
|
|
@@ -7579,6 +7584,11 @@
|
|
|
7579
7584
|
"mode": "0000644",
|
|
7580
7585
|
"sha256": "921c24a356d0fc1f398563bab3e8bb6f0831919a6954d86b6d74fdc60ff3b5f9"
|
|
7581
7586
|
},
|
|
7587
|
+
{
|
|
7588
|
+
"path": "tests/session_attach_from_claim.mjs",
|
|
7589
|
+
"mode": "0000644",
|
|
7590
|
+
"sha256": "e76a3baf5fb2cdee08ffe8d80c55630b5ab4f708046f3c63c07c480926ccda31"
|
|
7591
|
+
},
|
|
7582
7592
|
{
|
|
7583
7593
|
"path": "tests/session_caps.mjs",
|
|
7584
7594
|
"mode": "0000644",
|
|
@@ -7612,7 +7622,7 @@
|
|
|
7612
7622
|
{
|
|
7613
7623
|
"path": "tests/session_reward_on_land.mjs",
|
|
7614
7624
|
"mode": "0000644",
|
|
7615
|
-
"sha256": "
|
|
7625
|
+
"sha256": "52be7bd7afd17f7f7d1c93efb9be32902d08062cd65f35e3e3709a0e3669050f"
|
|
7616
7626
|
},
|
|
7617
7627
|
{
|
|
7618
7628
|
"path": "tests/session_reward_reconcile.mjs",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"rankLabels": { "xenos": "Newcomer", "thetes": "Contributor", "metic": "Trusted", "archon": "Owner" },
|
|
53
53
|
"copy": {
|
|
54
|
-
"tagline": "Building
|
|
54
|
+
"tagline": "Building Digitally, as simple as Bongos.",
|
|
55
55
|
"hallName": "Builders' Hall",
|
|
56
56
|
"notFound": "This page skipped a beat.",
|
|
57
57
|
"authError": {
|
package/modules/sessions/db.js
CHANGED
|
@@ -191,6 +191,67 @@ async function upsertSessionRecordWithReward(opts) {
|
|
|
191
191
|
});
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
// attachClaimSessionsToTaskTx — SERVER-SIDE task↔session attribution (task
|
|
195
|
+
// 1002964). Every payout path keys on session_records.task_ids, and until this
|
|
196
|
+
// existed exactly ONE thing ever populated that column: ship.js's client-side
|
|
197
|
+
// upload (`task_ids: [taskId]`). When that upload skipped — no
|
|
198
|
+
// CLAUDE_CODE_SESSION_ID, an unsafe id, or a transcript the client couldn't find
|
|
199
|
+
// (classifyUploadSkip, scripts/gds/ship.js) — the session was left with an EMPTY
|
|
200
|
+
// task_ids, which matches NOTHING in awardDeferredSessionRewardForTask,
|
|
201
|
+
// reconcileUnbookedSessionRewards, or the upload-side gate. The reward then
|
|
202
|
+
// stranded PERMANENTLY, not late: even the state-based safety net could not see
|
|
203
|
+
// the row. Builder IAMMASTERQUA hit exactly this — 19 sessions, 19 empty, ~$730
|
|
204
|
+
// of recorded session cost, zero paid, for ten weeks, with the only diagnostic
|
|
205
|
+
// (recordUploadSkip's trace) written to their own laptop.
|
|
206
|
+
//
|
|
207
|
+
// The server does not need the client's help here: a claim already records WHO
|
|
208
|
+
// shipped WHAT and WHEN. So we recover the link from durable state — the
|
|
209
|
+
// builder's session_records whose activity window overlaps the claim window.
|
|
210
|
+
//
|
|
211
|
+
// Design notes:
|
|
212
|
+
// • OVERLAP, not containment: a session that was already running when the
|
|
213
|
+
// claim opened (or outlived it) still did that work. started_at/ended_at
|
|
214
|
+
// fall back to uploaded_at when the transcript carried no span.
|
|
215
|
+
// • OVER-ATTACHMENT IS FINANCIALLY SAFE. The reward is idempotent per
|
|
216
|
+
// session_id (the watermark SELECT + uniq_credit_session_token_reward_basis),
|
|
217
|
+
// so linking one session to several tasks pays it ONCE, never per task. The
|
|
218
|
+
// bounded downside is a longer task_ids list, not double-pay.
|
|
219
|
+
// • Bounded by `limit` so a pathological long-running session can't fan out
|
|
220
|
+
// an unbounded UPDATE.
|
|
221
|
+
// • Does NOT touch the landing gate. It only answers "which session did this
|
|
222
|
+
// task come from"; whether the task LANDED is still checked by every caller
|
|
223
|
+
// (ADR 0120 — never pay for code that didn't land).
|
|
224
|
+
// Returns { attached } (rows freshly linked).
|
|
225
|
+
async function attachClaimSessionsToTaskTx(client, taskId, { limit = 50 } = {}) {
|
|
226
|
+
const id = Number(taskId);
|
|
227
|
+
if (!Number.isFinite(id)) return { attached: 0 };
|
|
228
|
+
const n = Number(limit);
|
|
229
|
+
const cap = Number.isFinite(n) ? Math.max(1, Math.min(500, Math.trunc(n))) : 50;
|
|
230
|
+
const { rows } = await client.query(
|
|
231
|
+
`WITH cand AS (
|
|
232
|
+
SELECT DISTINCT sr.id
|
|
233
|
+
FROM claims c
|
|
234
|
+
JOIN session_records sr ON sr.builder_id = c.builder_id
|
|
235
|
+
WHERE c.task_id = $1
|
|
236
|
+
AND NOT (sr.task_ids @> ARRAY[$1]::bigint[])
|
|
237
|
+
AND COALESCE(sr.started_at, sr.uploaded_at) <= COALESCE(c.released_at, now())
|
|
238
|
+
AND COALESCE(sr.ended_at, sr.uploaded_at) >= c.claimed_at
|
|
239
|
+
ORDER BY sr.id
|
|
240
|
+
LIMIT $2
|
|
241
|
+
)
|
|
242
|
+
UPDATE session_records sr
|
|
243
|
+
SET task_ids = (
|
|
244
|
+
SELECT COALESCE(array_agg(DISTINCT e ORDER BY e), '{}')
|
|
245
|
+
FROM unnest(sr.task_ids || ARRAY[$1]::bigint[]) AS e
|
|
246
|
+
)
|
|
247
|
+
FROM cand
|
|
248
|
+
WHERE cand.id = sr.id
|
|
249
|
+
RETURNING sr.session_id`,
|
|
250
|
+
[id, cap]
|
|
251
|
+
);
|
|
252
|
+
return { attached: rows.length };
|
|
253
|
+
}
|
|
254
|
+
|
|
194
255
|
// awardDeferredSessionRewardForTask — the LAND-side half of ADR 0120 "pay on
|
|
195
256
|
// land". When a task reaches status='shipped', routes/sessions.js's task.shipped
|
|
196
257
|
// listener calls this with the shipped task id. It finds every session_record
|
|
@@ -198,16 +259,24 @@ async function upsertSessionRecordWithReward(opts) {
|
|
|
198
259
|
// ci/async path, where the digest was uploaded (and DEFERRED) before the code
|
|
199
260
|
// landed. Opens its own transaction.
|
|
200
261
|
//
|
|
262
|
+
// It first runs attachClaimSessionsToTaskTx (task 1002964) so a session whose
|
|
263
|
+
// client-side upload skipped is still found: before that, an unsent task_ids
|
|
264
|
+
// made the reward unreachable by every path, permanently.
|
|
265
|
+
//
|
|
201
266
|
// We do NOT pre-filter on token_reward_drachmae: awardSessionTokenReward is
|
|
202
267
|
// idempotent per session_id, so a session that already collected at upload time
|
|
203
268
|
// (sync mode) — or a re-fired event — is a clean no-op. The credit_log is the
|
|
204
269
|
// single source of truth for "already paid"; the mirror column is only
|
|
205
|
-
// observability, so it is never the idempotency key.
|
|
206
|
-
// (rows scanned, rewards freshly booked
|
|
270
|
+
// observability, so it is never the idempotency key.
|
|
271
|
+
// Returns { sessions, awarded, attached } (rows scanned, rewards freshly booked,
|
|
272
|
+
// links freshly recovered).
|
|
207
273
|
async function awardDeferredSessionRewardForTask(taskId) {
|
|
208
274
|
const id = Number(taskId);
|
|
209
|
-
if (!Number.isFinite(id)) return { sessions: 0, awarded: 0 };
|
|
275
|
+
if (!Number.isFinite(id)) return { sessions: 0, awarded: 0, attached: 0 };
|
|
210
276
|
return api.withTx(async (client) => {
|
|
277
|
+
// Recover any link the client-side upload failed to send BEFORE looking for
|
|
278
|
+
// payable sessions — otherwise a skipped upload is unpayable forever.
|
|
279
|
+
const { attached } = await attachClaimSessionsToTaskTx(client, id);
|
|
211
280
|
const { rows } = await client.query(
|
|
212
281
|
`SELECT session_id, builder_id, model_usage, total_tokens
|
|
213
282
|
FROM session_records
|
|
@@ -224,7 +293,7 @@ async function awardDeferredSessionRewardForTask(taskId) {
|
|
|
224
293
|
});
|
|
225
294
|
if (tokenReward && tokenReward.awarded && tokenReward.drachmae > 0) awarded += 1;
|
|
226
295
|
}
|
|
227
|
-
return { sessions: rows.length, awarded };
|
|
296
|
+
return { sessions: rows.length, awarded, attached };
|
|
228
297
|
});
|
|
229
298
|
}
|
|
230
299
|
|
|
@@ -287,6 +356,64 @@ async function reconcileUnbookedSessionRewards({ limit = 200 } = {}) {
|
|
|
287
356
|
});
|
|
288
357
|
}
|
|
289
358
|
|
|
359
|
+
// reconcileUnattachedClaimSessions — the ATTRIBUTION half of the safety net
|
|
360
|
+
// (task 1002964), and the reason reconcileUnbookedSessionRewards alone was not
|
|
361
|
+
// enough. That sweep starts from session_records.task_ids, so a session the
|
|
362
|
+
// client never linked is invisible to it — the very rows most in need of rescue
|
|
363
|
+
// are the ones it cannot see. This sweep runs FIRST and comes at it from the
|
|
364
|
+
// other side: start from LANDED tasks, walk their claims, and link the claiming
|
|
365
|
+
// builder's overlapping sessions. Once linked, the existing sweep pays them.
|
|
366
|
+
//
|
|
367
|
+
// Ordering matters: the poller calls this, then reconcileUnbookedSessionRewards.
|
|
368
|
+
// Same idempotency story as attachClaimSessionsToTaskTx — a session already
|
|
369
|
+
// carrying the task is skipped by the NOT (task_ids @> …) guard, and the payout
|
|
370
|
+
// stays exactly-once per session regardless of how many tasks it links to.
|
|
371
|
+
// Strictly pay-on-land: only tasks at status='shipped' are considered.
|
|
372
|
+
//
|
|
373
|
+
// ⚠️ LOOKBACK WINDOW — this is a SAFETY NET, NOT A BACKFILL. The reward sweep
|
|
374
|
+
// runs immediately after this one, so every task linked here becomes payable on
|
|
375
|
+
// the same tick. Unbounded, the first run would retro-link the ENTIRE history
|
|
376
|
+
// (measured on cloudbongos.com 2026-08-16: 1383 tasks / 1157 sessions for one
|
|
377
|
+
// builder alone) and mint a mass retroactive payout nobody authorised. Deliberate
|
|
378
|
+
// historical back-pay is an Archon decision made with eyes open via
|
|
379
|
+
// scripts/gds/backfill-task-rewards.js (dry-run by default, reviewed amounts) —
|
|
380
|
+
// never a side effect of a poller starting up. So we only consider tasks that
|
|
381
|
+
// landed inside the window: recent enough that a live client-side upload skip is
|
|
382
|
+
// what we are recovering from. Widen it deliberately, never casually.
|
|
383
|
+
// Returns { scanned, attached }.
|
|
384
|
+
const ATTACH_LOOKBACK_DAYS = Number(process.env.SESSION_ATTACH_LOOKBACK_DAYS) || 7;
|
|
385
|
+
|
|
386
|
+
async function reconcileUnattachedClaimSessions({ limit = 200, lookbackDays } = {}) {
|
|
387
|
+
const n = Number(limit);
|
|
388
|
+
const cap = Number.isFinite(n) ? Math.max(1, Math.min(1000, Math.trunc(n))) : 200;
|
|
389
|
+
const d = Number(lookbackDays ?? ATTACH_LOOKBACK_DAYS);
|
|
390
|
+
const days = Number.isFinite(d) ? Math.max(1, Math.min(365, Math.trunc(d))) : 7;
|
|
391
|
+
return api.withTx(async (client) => {
|
|
392
|
+
// Candidate LANDED tasks whose claiming builder has at least one overlapping
|
|
393
|
+
// session that is not yet linked. Bounded, newest first — a backlog drains
|
|
394
|
+
// over successive sweeps rather than in one unbounded UPDATE.
|
|
395
|
+
const { rows } = await client.query(
|
|
396
|
+
`SELECT DISTINCT c.task_id
|
|
397
|
+
FROM claims c
|
|
398
|
+
JOIN tasks t ON t.id = c.task_id AND t.status = 'shipped'
|
|
399
|
+
JOIN session_records sr ON sr.builder_id = c.builder_id
|
|
400
|
+
WHERE NOT (sr.task_ids @> ARRAY[c.task_id]::bigint[])
|
|
401
|
+
AND COALESCE(t.shipped_at, t.updated_at) >= now() - ($2 || ' days')::interval
|
|
402
|
+
AND COALESCE(sr.started_at, sr.uploaded_at) <= COALESCE(c.released_at, now())
|
|
403
|
+
AND COALESCE(sr.ended_at, sr.uploaded_at) >= c.claimed_at
|
|
404
|
+
ORDER BY c.task_id DESC
|
|
405
|
+
LIMIT $1`,
|
|
406
|
+
[cap, String(days)]
|
|
407
|
+
);
|
|
408
|
+
let attached = 0;
|
|
409
|
+
for (const r of rows) {
|
|
410
|
+
const res = await attachClaimSessionsToTaskTx(client, r.task_id);
|
|
411
|
+
attached += res.attached;
|
|
412
|
+
}
|
|
413
|
+
return { scanned: rows.length, attached };
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
290
417
|
// ---- 6D.2: session search + fetch (read side, ADR 0027) -------------------
|
|
291
418
|
|
|
292
419
|
// Safe ORDER BY whitelist — the ONLY value interpolated into the search SQL, so
|
|
@@ -459,6 +586,8 @@ module.exports = {
|
|
|
459
586
|
upsertSessionRecordWithReward, // route entry point — upsert + token reward (task 1005)
|
|
460
587
|
awardDeferredSessionRewardForTask, // ADR 0120 land-side trigger — task.shipped listener calls it
|
|
461
588
|
reconcileUnbookedSessionRewards, // ADR 0120 state-based safety net — books rewards for out-of-band ships
|
|
589
|
+
attachClaimSessionsToTaskTx, // task 1002964 — server-side task↔session link, recovered from the claim
|
|
590
|
+
reconcileUnattachedClaimSessions, // task 1002964 — attribution sweep; must run BEFORE the reward sweep
|
|
462
591
|
// search + fetch (6D.2)
|
|
463
592
|
searchSessions,
|
|
464
593
|
summarizeSessions,
|
|
@@ -29,13 +29,25 @@ let intervalTimer = null;
|
|
|
29
29
|
let firstTimer = null;
|
|
30
30
|
|
|
31
31
|
async function runSweep() {
|
|
32
|
+
// ATTRIBUTION FIRST (task 1002964). reconcileUnbookedSessionRewards starts from
|
|
33
|
+
// session_records.task_ids, so a session whose client-side upload skipped — and
|
|
34
|
+
// which therefore carries an EMPTY task_ids — is invisible to it. Those are
|
|
35
|
+
// precisely the stranded rows worth rescuing, so recover the task↔session link
|
|
36
|
+
// from the claim before looking for unbooked rewards. Both halves are
|
|
37
|
+
// idempotent, so a re-sweep neither re-links nor double-pays.
|
|
38
|
+
const attach = await db.reconcileUnattachedClaimSessions({ limit: 200 });
|
|
39
|
+
if (attach && attach.attached > 0) {
|
|
40
|
+
console.log(
|
|
41
|
+
`[session-reward-reconcile] linked ${attach.attached} session(s) to landed tasks from claim state (scanned ${attach.scanned})`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
32
44
|
const res = await db.reconcileUnbookedSessionRewards({ limit: 200 });
|
|
33
45
|
if (res && res.awarded > 0) {
|
|
34
46
|
console.log(
|
|
35
47
|
`[session-reward-reconcile] booked ${res.awarded} stranded session reward(s) (scanned ${res.scanned})`
|
|
36
48
|
);
|
|
37
49
|
}
|
|
38
|
-
return res;
|
|
50
|
+
return { ...res, attached: attach ? attach.attached : 0 };
|
|
39
51
|
}
|
|
40
52
|
|
|
41
53
|
// Start the periodic sweep. Idempotent (a second call is a no-op). Kill-switched by
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.156",
|
|
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.156",
|
|
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.156",
|
|
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",
|
|
Binary file
|
|
@@ -144,11 +144,62 @@ h1.big{font-family:var(--font-display);font-weight:700;font-size:clamp(33px,5.4v
|
|
|
144
144
|
at rest and would otherwise sit visible at full stroke. */
|
|
145
145
|
@media (prefers-reduced-motion:reduce){.bongos .ring{display:none;}}
|
|
146
146
|
|
|
147
|
-
/* ── S3 · the
|
|
148
|
-
.
|
|
149
|
-
|
|
150
|
-
.
|
|
151
|
-
.
|
|
147
|
+
/* ── S3 · the builder-credit strip (apex-only content; classes .verro*) ──
|
|
148
|
+
Deliberately a different house from the rest of the page. Its palette is a
|
|
149
|
+
LOCAL token set, not the Bongos one: cool greys that hold in both schemes,
|
|
150
|
+
so the block reads as an inset plaque rather than another warm card. Only
|
|
151
|
+
these five tokens are local — everything else still comes from cosmos.css. */
|
|
152
|
+
.verro{
|
|
153
|
+
--v-ground:#e9ecec; --v-surface:#f3f5f5; --v-ink:#4e5457;
|
|
154
|
+
--v-ink-strong:#33383b; --v-hair:#aeb6b8;
|
|
155
|
+
background:var(--v-ground);border-radius:20px;
|
|
156
|
+
padding:clamp(18px,2.4vw,26px) clamp(20px,2.8vw,30px);
|
|
157
|
+
box-shadow:var(--shadow-card);overflow:hidden;
|
|
158
|
+
}
|
|
159
|
+
@media (prefers-color-scheme:dark){
|
|
160
|
+
.verro{--v-ground:#15181a;--v-surface:#1b1f21;--v-ink:#aeb6b9;--v-ink-strong:#d6dcde;--v-hair:#3d4548;}
|
|
161
|
+
}
|
|
162
|
+
:root[data-theme="dark"] .verro{--v-ground:#15181a;--v-surface:#1b1f21;--v-ink:#aeb6b9;--v-ink-strong:#d6dcde;--v-hair:#3d4548;}
|
|
163
|
+
:root[data-theme="light"] .verro{--v-ground:#e9ecec;--v-surface:#f3f5f5;--v-ink:#4e5457;--v-ink-strong:#33383b;--v-hair:#aeb6b8;}
|
|
164
|
+
/* mark LEFT, text RIGHT — a fixed logo column, the copy takes the rest */
|
|
165
|
+
.verroInner{position:relative;display:grid;grid-template-columns:124px 1fr;gap:clamp(18px,2.6vw,30px);align-items:center;}
|
|
166
|
+
.verroMark{
|
|
167
|
+
font-family:system-ui,-apple-system,"Segoe UI",Arial,sans-serif;font-weight:400;
|
|
168
|
+
font-size:clamp(19px,2.6vw,26px);letter-spacing:.075em;line-height:1;
|
|
169
|
+
color:var(--v-ink-strong);margin-bottom:11px;
|
|
170
|
+
text-shadow:-1px 0 rgba(214,120,40,.26),1px 0 rgba(60,120,190,.26);
|
|
171
|
+
}
|
|
172
|
+
.verroRule{width:40px;height:1px;background:var(--v-hair);margin-bottom:12px;}
|
|
173
|
+
.verro p{
|
|
174
|
+
position:relative;font-family:system-ui,-apple-system,"Segoe UI",Arial,sans-serif;
|
|
175
|
+
font-size:clamp(14px,1.5vw,15.5px);font-weight:400;line-height:1.58;
|
|
176
|
+
color:var(--v-ink);max-width:62ch;
|
|
177
|
+
}
|
|
178
|
+
.verroMedal{justify-self:center;}
|
|
179
|
+
/* No border-radius needed: the PNG is already masked to a transparent-cornered
|
|
180
|
+
disc, so it sits on the grey block with nothing to clip. Width/height are
|
|
181
|
+
also set as attributes on the element so the box is reserved before the
|
|
182
|
+
image loads and the block does not reflow. */
|
|
183
|
+
.verroMedal .verroDisc{width:124px;height:124px;display:block;}
|
|
184
|
+
@media (max-width:760px){
|
|
185
|
+
.verroInner{grid-template-columns:1fr;justify-items:center;text-align:center;}
|
|
186
|
+
.verroRule{margin-left:auto;margin-right:auto;}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
/* ── S6b · the basics ── */
|
|
191
|
+
.basics{--col:300px;align-items:stretch;}
|
|
192
|
+
.basic{background:var(--card);border-radius:22px;box-shadow:var(--shadow-card);padding:clamp(20px,2.8vw,28px);}
|
|
193
|
+
.basicWho{font-family:var(--font-mono);font-size:12px;font-weight:500;color:var(--accent);letter-spacing:1.3px;text-transform:uppercase;margin-bottom:11px;}
|
|
194
|
+
.basic ul{list-style:none;margin:14px 0 0;padding:0;display:flex;flex-direction:column;gap:13px;}
|
|
195
|
+
.basic li{position:relative;padding-left:19px;font-size:var(--fs-body);font-weight:500;color:var(--ink-soft);line-height:1.55;}
|
|
196
|
+
.basic li::before{content:"";position:absolute;left:0;top:.62em;width:7px;height:7px;border-radius:50%;background:var(--accent);}
|
|
197
|
+
.basic li b{font-weight:700;color:var(--ink);}
|
|
198
|
+
/* second level: the points that fold up under a tier. Hollow marker so the
|
|
199
|
+
nesting reads as subordinate rather than as another top-level choice. */
|
|
200
|
+
.basic li ul{margin:9px 0 0;gap:7px;}
|
|
201
|
+
.basic li li{padding-left:16px;font-size:calc(var(--fs-body) - .5px);}
|
|
202
|
+
.basic li li::before{top:.66em;width:5px;height:5px;background:transparent;border:1.5px solid var(--accent);}
|
|
152
203
|
|
|
153
204
|
/* ── S4 · the lifecycle: cosmos.css's .strip, unmodified, doing marketing work.
|
|
154
205
|
The marketing page and the working page share the component rather than
|
|
@@ -179,10 +230,10 @@ h1.big{font-family:var(--font-display);font-weight:700;font-size:clamp(33px,5.4v
|
|
|
179
230
|
.conTag{font-size:12.5px;font-weight:600;color:rgba(255,246,230,.58);margin-top:3px;line-height:1.35;display:block;}
|
|
180
231
|
.conItem .chip{margin-top:8px;white-space:nowrap;}
|
|
181
232
|
|
|
182
|
-
/* ──
|
|
183
|
-
|
|
184
|
-
.door
|
|
185
|
-
.
|
|
233
|
+
/* ── S7 ── */
|
|
234
|
+
/* S7 closes on two bare buttons — the door cards are gone (task 1002934), and
|
|
235
|
+
.door/.door p went with them rather than lingering as dead rules. */
|
|
236
|
+
.closing .cta-row{justify-content:center;}
|
|
186
237
|
|
|
187
238
|
@media (max-width:900px){
|
|
188
239
|
.lead{grid-template-columns:1fr;text-align:center;}
|
|
@@ -238,11 +289,16 @@ h1.big{font-family:var(--font-display);font-weight:700;font-size:clamp(33px,5.4v
|
|
|
238
289
|
<section class="lead">
|
|
239
290
|
<div>
|
|
240
291
|
<p class="eyebrow">type <b>bongos</b> in <span class="typed" id="typed">any AI</span><span class="caret" aria-hidden="true">▌</span></p>
|
|
241
|
-
<h1 class="big" data-brand-tagline>Building
|
|
242
|
-
<p class="lede">AI agents claim tasks and ship
|
|
292
|
+
<h1 class="big" data-brand-tagline>Building Digitally, as simple as Bongos.</h1>
|
|
293
|
+
<p class="lede">AI agents claim tasks and ship code. You direct them.</p>
|
|
294
|
+
<!-- ONE button by owner direction (task 1002934). The former ghost CTA
|
|
295
|
+
("See what's building") is gone; the survivor keeps .btn.lg exactly as
|
|
296
|
+
it was — a deletion, not a restyle. It now points at /projects rather
|
|
297
|
+
than /projects?view=new because the label is an invitation to browse,
|
|
298
|
+
not to create. NOTE for a child instance: /projects there means that
|
|
299
|
+
instance's own provisioning rows, not the hub catalog. -->
|
|
243
300
|
<div class="cta-row">
|
|
244
|
-
<a class="btn lg" href="/projects
|
|
245
|
-
<!--apex-only:start--><a class="btn ghost lg" href="/projects">See what's building</a><!--apex-only:end-->
|
|
301
|
+
<a class="btn lg" href="/projects">Explore Bongo Projects</a>
|
|
246
302
|
</div>
|
|
247
303
|
</div>
|
|
248
304
|
<div class="mascot">
|
|
@@ -329,41 +385,64 @@ h1.big{font-family:var(--font-display);font-weight:700;font-size:clamp(33px,5.4v
|
|
|
329
385
|
</section>
|
|
330
386
|
</div>
|
|
331
387
|
|
|
332
|
-
<!--
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
388
|
+
<!--apex-only:start-->
|
|
389
|
+
<!-- ══ S3 · VERRO LABS ═════════════════════════════════════════════════════
|
|
390
|
+
Replaces the three mechanism cards (the ledger / the gates / the grade) by
|
|
391
|
+
owner direction, task 1002934.
|
|
392
|
+
|
|
393
|
+
APEX-ONLY, and it must stay that way: a child instance is NOT built by
|
|
394
|
+
Verro Labs and must never say so. The block the beats used to occupy was
|
|
395
|
+
deliberately portable; this one cannot be. Consequence a child instance
|
|
396
|
+
now has nothing between the hero and the lifecycle — flagged to the owner
|
|
397
|
+
as a follow-up decision, not silently absorbed.
|
|
398
|
+
|
|
399
|
+
The design is drawn FROM the Verro Labs logo so it reads as a different
|
|
400
|
+
house inside the warm Bongos page: cool grey monochrome, a wide-tracked
|
|
401
|
+
uppercase wordmark, and a dialled-down chromatic fringe.
|
|
402
|
+
|
|
403
|
+
NO HAIRLINE FRAME. An earlier pass added one, reading the thin rectangle
|
|
404
|
+
in the logo lockup as part of the mark; the owner confirmed it was not
|
|
405
|
+
meant to be there. Do not reintroduce it.
|
|
337
406
|
═══════════════════════════════════════════════════════════════════════ -->
|
|
338
407
|
<div class="wrap">
|
|
339
|
-
<section class="sec
|
|
340
|
-
<div class="
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
408
|
+
<section class="sec verro">
|
|
409
|
+
<div class="verroInner">
|
|
410
|
+
<!-- mark first in source order as well as visually, so it reads the same
|
|
411
|
+
way to a screen reader and stacks logo-above-text on a narrow screen
|
|
412
|
+
without needing an order override -->
|
|
413
|
+
<div class="verroMedal">
|
|
414
|
+
<!-- The Verro Labs medallion. Cropped from the supplied artwork to the
|
|
415
|
+
circle only and masked to a transparent-cornered disc, so there is
|
|
416
|
+
no white plate and no square frame to hide with CSS. The file is
|
|
417
|
+
384px against a 124px box — comfortably past 2x, so it stays sharp
|
|
418
|
+
if the block is ever scaled back up.
|
|
419
|
+
|
|
420
|
+
NOTE ON SHARPNESS: the source was a 217x210 screenshot and the disc
|
|
421
|
+
within it only 193x197, so this is upscaled and will look soft on a
|
|
422
|
+
high-DPI screen. If a higher-resolution original (or a vector)
|
|
423
|
+
exists, re-crop from that — the recipe is in the task notes. -->
|
|
424
|
+
<img class="verroDisc" src="assets/verro-medallion.png"
|
|
425
|
+
width="124" height="124" loading="lazy" decoding="async"
|
|
426
|
+
alt="The Verro Labs mark: a galaxy and constellation above a river valley threaded with circuitry">
|
|
427
|
+
</div>
|
|
428
|
+
<div>
|
|
429
|
+
<div class="verroMark">VERRO LABS</div>
|
|
430
|
+
<div class="verroRule"></div>
|
|
431
|
+
<p>Built by Verro Labs, Cloud Bongos is our way of making AI co-building usable by entire
|
|
432
|
+
project groups — whether that's a 5,000-person company, a 3,000-person department inside
|
|
433
|
+
one, a college research project, or a solo dev.</p>
|
|
434
|
+
</div>
|
|
357
435
|
</div>
|
|
358
436
|
</section>
|
|
359
437
|
</div>
|
|
360
438
|
|
|
439
|
+
<!--apex-only:end-->
|
|
440
|
+
|
|
361
441
|
<!-- ══ S4 · THE LIFECYCLE ══════════════════════════════════════════════════ -->
|
|
362
442
|
<div class="wrap">
|
|
363
443
|
<section class="sec railWrap">
|
|
364
|
-
<h2 class="sec-h">
|
|
365
|
-
<p class="sec-p">
|
|
366
|
-
projects page.</p>
|
|
444
|
+
<h2 class="sec-h">A project as a codebase, regardless of function.</h2>
|
|
445
|
+
<p class="sec-p">A laddered beat progresses all tasks.</p>
|
|
367
446
|
<div class="strip isLive" role="img" aria-label="claim, build, grade, ship, credit">
|
|
368
447
|
<ul class="stages">
|
|
369
448
|
<li class="done"><span class="sdot"></span><span class="slbl">claim</span></li>
|
|
@@ -391,7 +470,8 @@ h1.big{font-family:var(--font-display);font-weight:700;font-size:clamp(33px,5.4v
|
|
|
391
470
|
<div class="bandHead">
|
|
392
471
|
<div>
|
|
393
472
|
<h2 class="sec-h">Built on Cloud Bongos</h2>
|
|
394
|
-
<p class="bandSub">
|
|
473
|
+
<p class="bandSub">At present, Bongos projects are all held by Verro Labs or its pilot
|
|
474
|
+
clients.</p>
|
|
395
475
|
</div>
|
|
396
476
|
<a class="linklike" href="/projects">Open the map →</a>
|
|
397
477
|
</div>
|
|
@@ -401,43 +481,61 @@ h1.big{font-family:var(--font-display);font-weight:700;font-size:clamp(33px,5.4v
|
|
|
401
481
|
<!--apex-only:end-->
|
|
402
482
|
|
|
403
483
|
<!--apex-only:start-->
|
|
404
|
-
<!-- ══
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
484
|
+
<!-- ══ S6b · THE BASICS ═════════════════════════════════════════════════════
|
|
485
|
+
New dual block, owner direction (task 1002934). Inside the existing
|
|
486
|
+
apex-only run: it describes Verro Labs' commercial offering, which is not
|
|
487
|
+
true of a child instance.
|
|
488
|
+
|
|
489
|
+
Placement is a planner's call — the last block before the closing
|
|
490
|
+
doors, so the practical explainer sits just ahead of the final ask.
|
|
408
491
|
═══════════════════════════════════════════════════════════════════════ -->
|
|
409
492
|
<div class="wrap">
|
|
410
|
-
<section class="sec
|
|
411
|
-
<
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
493
|
+
<section class="sec">
|
|
494
|
+
<div class="grid pair basics">
|
|
495
|
+
<div class="basic">
|
|
496
|
+
<div class="basicWho">For founders</div>
|
|
497
|
+
<h3 class="sec-h3">Types of projects</h3>
|
|
498
|
+
<ul>
|
|
499
|
+
<li><b>Open source.</b> Download our core code and protocols, and build instantly with
|
|
500
|
+
your team.</li>
|
|
501
|
+
<!-- "Bongos Managed" and "Cloud Bongos Managed" were two separate tiers with
|
|
502
|
+
near-identical names, which read as the same thing said twice. Consolidated
|
|
503
|
+
into one tier by owner direction; every point from both now sits under it. -->
|
|
504
|
+
<li><b>Bongos Managed.</b>
|
|
505
|
+
<ul>
|
|
506
|
+
<li>Buy microtransaction packages of code modules on our marketplace.</li>
|
|
507
|
+
<li>Have us handle your hosting.</li>
|
|
508
|
+
<li>Get access to our advanced project presets and module packages.</li>
|
|
509
|
+
<li>Equity issuance, business filing, and builder compensation.</li>
|
|
510
|
+
</ul>
|
|
511
|
+
</li>
|
|
512
|
+
</ul>
|
|
513
|
+
</div>
|
|
514
|
+
<div class="basic">
|
|
515
|
+
<div class="basicWho">For builders</div>
|
|
516
|
+
<h3 class="sec-h3">Ways to get paid</h3>
|
|
517
|
+
<ul>
|
|
518
|
+
<li><b>Build equity</b> in multiple projects.</li>
|
|
519
|
+
<li><b>Work as a contractor</b> for companies big, medium and small — and get paid
|
|
520
|
+
instantly.</li>
|
|
521
|
+
<li><b>Build as a game</b> or a learning experience.</li>
|
|
522
|
+
</ul>
|
|
523
|
+
</div>
|
|
524
|
+
</div>
|
|
421
525
|
</section>
|
|
422
526
|
</div>
|
|
423
527
|
<!--apex-only:end-->
|
|
424
528
|
|
|
425
|
-
<!-- ══ S7 ·
|
|
426
|
-
|
|
427
|
-
|
|
529
|
+
<!-- ══ S7 · THE CLOSING PAIR ═══════════════════════════════════════════════
|
|
530
|
+
Was two "door" cards (Build a project / Join as a builder). Reduced to two
|
|
531
|
+
bare buttons by owner direction, task 1002934 — the hero already made the
|
|
532
|
+
argument, so the page closes on the choice rather than re-pitching it.
|
|
428
533
|
═══════════════════════════════════════════════════════════════════════ -->
|
|
429
534
|
<div class="wrap">
|
|
430
|
-
<section class="sec
|
|
431
|
-
<div class="
|
|
432
|
-
<
|
|
433
|
-
<
|
|
434
|
-
<a class="btn" href="/projects?view=new">Start a project</a>
|
|
435
|
-
</div>
|
|
436
|
-
<div class="door">
|
|
437
|
-
<h3 class="sec-h3">Join as a builder</h3>
|
|
438
|
-
<p>Claim tasks, ship code, earn credits. The hall is where the work is, and newcomers start
|
|
439
|
-
with a real task on day one.</p>
|
|
440
|
-
<a class="btn ghost" data-link-hall href="{{buildersOrigin}}">Enter the Hall →</a>
|
|
535
|
+
<section class="sec closing">
|
|
536
|
+
<div class="cta-row">
|
|
537
|
+
<a class="btn lg" href="/projects?view=new">Start a Project</a>
|
|
538
|
+
<a class="btn ghost lg" href="/projects">Find a Project</a>
|
|
441
539
|
</div>
|
|
442
540
|
</section>
|
|
443
541
|
</div>
|
package/src/module-api.js
CHANGED
|
@@ -735,8 +735,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
735
735
|
// landed since 1.19.152 with no explicit bump. run 31956560292. (task 1002620)
|
|
736
736
|
// 1.19.154 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
737
737
|
// landed since 1.19.153 with no explicit bump. run 31957052737. (task 1002620)
|
|
738
|
+
// 1.19.155 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
739
|
+
// landed since 1.19.154 with no explicit bump. run 31957556550. (task 1002620)
|
|
740
|
+
// 1.19.156 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
741
|
+
// landed since 1.19.155 with no explicit bump. run 31957976979. (task 1002620)
|
|
738
742
|
// ---------------------------------------------------------------------------
|
|
739
|
-
const CORE_VERSION = '1.19.
|
|
743
|
+
const CORE_VERSION = '1.19.156'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
740
744
|
|
|
741
745
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
742
746
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -44,7 +44,8 @@ test('R74: the vanilla pack carries the Cloud Bongos LOOK (sunrise palette, Balo
|
|
|
44
44
|
assert.match(c.theme.favicon, /circle/, 'bongo-pair favicon (circles), not the Greek Α');
|
|
45
45
|
assert.doesNotMatch(c.theme.favicon, /font-family='serif'>Α/);
|
|
46
46
|
assert.equal(c.theme.backdrop, '#f9d9a8', 'the marigold ground holds all the way down');
|
|
47
|
-
|
|
47
|
+
// task 1002934: the owner's homepage rewrite retitled the platform.
|
|
48
|
+
assert.equal(c.copy.tagline, 'Building Digitally, as simple as Bongos.');
|
|
48
49
|
assert.equal(c.rankLabels.archon, 'Owner', 'plain rank labels, not "Archon"');
|
|
49
50
|
});
|
|
50
51
|
|
package/tests/landing_page.mjs
CHANGED
|
@@ -55,20 +55,22 @@ test('the hub keeps every apex-only section', () => {
|
|
|
55
55
|
test('a child instance is served none of the platform-only claims', () => {
|
|
56
56
|
const child = stripApexOnly(HTML, false);
|
|
57
57
|
assert.doesNotMatch(child, /<!--apex-only:(start|end)-->/, 'no sentinel survives the strip');
|
|
58
|
-
// the
|
|
58
|
+
// the apex-only regions, by their load-bearing content (task 1002934: the
|
|
59
|
+
// Verro Labs strip + The Basics replaced the beats / What-you-own era)
|
|
59
60
|
assert.doesNotMatch(child, /capture-resistant commons/, 'the commons claim is the platform\'s, not a child\'s');
|
|
60
61
|
assert.doesNotMatch(child, /Built on Cloud Bongos/, 'a child has no catalog of other instances');
|
|
61
|
-
assert.doesNotMatch(child, /
|
|
62
|
-
assert.doesNotMatch(child, /
|
|
62
|
+
assert.doesNotMatch(child, /Verro Labs/, 'a child instance is not built by Verro Labs');
|
|
63
|
+
assert.doesNotMatch(child, /Bongos Managed/, 'the commercial offer is the platform\'s, not a child\'s');
|
|
64
|
+
assert.doesNotMatch(child, /thousands of builders/, 'a child has no hub builder pool to promise');
|
|
63
65
|
});
|
|
64
66
|
|
|
65
67
|
test('a child instance keeps the whole portable spine', () => {
|
|
66
68
|
const child = stripApexOnly(HTML, false);
|
|
67
69
|
for (const portable of [
|
|
68
|
-
'Building
|
|
69
|
-
'
|
|
70
|
-
'
|
|
71
|
-
'
|
|
70
|
+
'Building Digitally, as simple as', // the hero tagline (task 1002934)
|
|
71
|
+
'A project as a codebase', // S4 heading (task 1002934 rewording)
|
|
72
|
+
'A laddered beat progresses all tasks', // S4 subtitle
|
|
73
|
+
'Start a Project', 'Find a Project', // S7, the closing pair (buttons since 1002934)
|
|
72
74
|
'Built by agents, on Cloud', // the portable half of the footer
|
|
73
75
|
'id="who"', // the identity chip
|
|
74
76
|
]) {
|
|
@@ -109,7 +111,9 @@ test('every head tag a crawler reads is stamped, and no token survives', () => {
|
|
|
109
111
|
test('every cross-host link is stamped to THIS instance, never the platform', () => {
|
|
110
112
|
const out = applyBrandTokens(HTML, BRAND, 'https://spike-harbor.example', 'https://builders.spike-harbor.example');
|
|
111
113
|
const halls = [...out.matchAll(/data-link-hall href="([^"]*)"/g)].map((m) => m[1]);
|
|
112
|
-
|
|
114
|
+
// task 1002934: the S7 door cards (which carried the second hall link) became
|
|
115
|
+
// two bare project buttons — the header Hall entry is the one that remains.
|
|
116
|
+
assert.ok(halls.length >= 1, 'the header Hall entry points at the hall');
|
|
113
117
|
for (const href of halls) assert.equal(href, 'https://builders.spike-harbor.example');
|
|
114
118
|
// C6: this is the bug the stamping fixes — clientBranding() omits domains, so a
|
|
115
119
|
// client fill can never re-point these and every child sent visitors to the platform.
|
|
@@ -121,19 +125,21 @@ test('a pack with no hall of its own falls back to its own apex, not the platfor
|
|
|
121
125
|
assert.match(out, /data-link-hall href="https:\/\/spike-harbor\.example"/);
|
|
122
126
|
});
|
|
123
127
|
|
|
124
|
-
test('
|
|
125
|
-
//
|
|
126
|
-
//
|
|
128
|
+
test('no repo path is baked into the page', () => {
|
|
129
|
+
// task 1002934 removed the "What you own" section and with it the page's only
|
|
130
|
+
// {{sourceRepoUrl}} link (owner-accepted). What must still hold: no HARDCODED
|
|
131
|
+
// owner/name — a baked one would point every instance at whoever authored
|
|
132
|
+
// this file, and the authorship audit fails it.
|
|
127
133
|
assert.doesNotMatch(HTML, /github\.com\/[a-z0-9-]+\/[a-z0-9-]+/i, 'no baked repo path');
|
|
128
|
-
const out = applyBrandTokens(HTML, BRAND, 'https://o.example', '', 'https://github.com/acme/harbor');
|
|
129
|
-
assert.match(out, /href="https:\/\/github\.com\/acme\/harbor"/);
|
|
130
134
|
});
|
|
131
135
|
|
|
132
136
|
test('an unstamped link is removed, not left dangling', () => {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
137
|
+
// the page keeps its empty-href scrubber so ANY future unstamped token link
|
|
138
|
+
// (the header hall entry today) is dropped rather than rendered dead.
|
|
139
|
+
const out = applyBrandTokens(HTML, BRAND, 'https://o.example', '');
|
|
140
|
+
assert.match(CODE, /a\[href=""\]/, 'the page drops empty-href links');
|
|
136
141
|
assert.match(CODE, /\.remove\(\)/);
|
|
142
|
+
assert.ok(out.length > 0);
|
|
137
143
|
});
|
|
138
144
|
|
|
139
145
|
// ── the branding cascade + portability ─────────────────────────────────────
|
|
@@ -238,13 +244,12 @@ test('nothing about the platform is hardcoded — the file is portable (C5)', ()
|
|
|
238
244
|
test('the page reads without JavaScript: the spine is markup, not script-built', () => {
|
|
239
245
|
const body = HTML.split('<body>')[1].split('<script>')[0];
|
|
240
246
|
for (const copy of [
|
|
241
|
-
'Building
|
|
242
|
-
'
|
|
243
|
-
'A
|
|
244
|
-
'
|
|
245
|
-
'
|
|
246
|
-
'
|
|
247
|
-
'Enter the Hall',
|
|
247
|
+
'Building Digitally, as simple as', // hero (task 1002934)
|
|
248
|
+
'Built by Verro Labs', // the Verro strip
|
|
249
|
+
'A project as a codebase', // S4 heading
|
|
250
|
+
'Explore Bongo Projects', // the hero CTA
|
|
251
|
+
'Start a Project', // the closing pair
|
|
252
|
+
'Find a Project',
|
|
248
253
|
]) {
|
|
249
254
|
assert.ok(body.includes(copy), `"${copy}" is only reachable with scripting on`);
|
|
250
255
|
}
|
|
@@ -297,7 +302,7 @@ test('the five primitives graduated into cosmos.css, not the page (§6)', () =>
|
|
|
297
302
|
|
|
298
303
|
test('the page satisfies the parked platform_boot assertions', () => {
|
|
299
304
|
assert.match(HTML, /id="who"/);
|
|
300
|
-
assert.match(HTML, /
|
|
305
|
+
assert.match(HTML, /Find a Project/); // the landing marker since task 1002934
|
|
301
306
|
// an ORG-level action that lives only on the gate — asserted absent there
|
|
302
307
|
assert.doesNotMatch(HTML, /Create a project/);
|
|
303
308
|
});
|
package/tests/platform_boot.mjs
CHANGED
|
@@ -113,7 +113,9 @@ test('the apex host serves the landing page — not the game — on a game-less
|
|
|
113
113
|
assert.doesNotMatch(res.body, /404 — not found/); // not the bare apex tail
|
|
114
114
|
// it IS the landing, with the branding pack injected (look + identity from config)
|
|
115
115
|
assert.match(res.body, /window\.__BRANDING__=/);
|
|
116
|
-
|
|
116
|
+
// task 1002934: the doors became two bare buttons; "Find a Project" is the
|
|
117
|
+
// portable landing marker now ("Enter the Hall" left with the door cards).
|
|
118
|
+
assert.match(res.body, /Find a Project/);
|
|
117
119
|
// the per-project landing carries the top-right identity chip + hall entry
|
|
118
120
|
// (goal 35, task 2094). "Create a project" is an ORG-level action that lives
|
|
119
121
|
// only on the gate now — it must NOT appear on a project's own landing.
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// tests/session_attach_from_claim.mjs
|
|
2
|
+
//
|
|
3
|
+
// Regression guard for task 1002964 — SERVER-SIDE task↔session attribution.
|
|
4
|
+
//
|
|
5
|
+
// THE BUG. Every payout path keys on session_records.task_ids, and until this
|
|
6
|
+
// fix exactly ONE thing populated that column: ship.js's client-side upload.
|
|
7
|
+
// When that upload skipped (classifyUploadSkip: no_session_id /
|
|
8
|
+
// unsafe_session_id / transcript_not_found) the session was left with an EMPTY
|
|
9
|
+
// task_ids — which matches NOTHING in the upload-side gate,
|
|
10
|
+
// awardDeferredSessionRewardForTask, or reconcileUnbookedSessionRewards. The
|
|
11
|
+
// reward stranded PERMANENTLY, invisible even to the state-based safety net.
|
|
12
|
+
// Builder IAMMASTERQUA hit exactly this: 19 sessions, 19 empty, ~$730 of
|
|
13
|
+
// recorded session cost, zero paid, for ten weeks.
|
|
14
|
+
//
|
|
15
|
+
// THE FIX. The server recovers the link from durable claim state — the claiming
|
|
16
|
+
// builder's session_records whose activity window overlaps the claim window —
|
|
17
|
+
// so a client-side skip can no longer zero a builder's pay. What this must NOT
|
|
18
|
+
// do is loosen the landing gate: ADR 0120 still pays only for code that landed.
|
|
19
|
+
//
|
|
20
|
+
// Run: node tests/session_attach_from_claim.mjs
|
|
21
|
+
|
|
22
|
+
import { strict as assert } from 'node:assert';
|
|
23
|
+
import { createRequire } from 'node:module';
|
|
24
|
+
import { makeRunner, makeSqlAwareClient } from './helpers.mjs';
|
|
25
|
+
|
|
26
|
+
process.env.NODE_ENV = 'test';
|
|
27
|
+
|
|
28
|
+
const require = createRequire(import.meta.url);
|
|
29
|
+
const seams = require('../src/module-seams.js');
|
|
30
|
+
if (!seams.hasProvider('reward')) seams.registerProvider('reward', require('../modules/economy/reward.js'));
|
|
31
|
+
const api = require('../src/module-api.js');
|
|
32
|
+
const db = require('../modules/sessions/db.js');
|
|
33
|
+
|
|
34
|
+
const { test, summary } = makeRunner();
|
|
35
|
+
|
|
36
|
+
// Same priceable fixture the sibling reward tests use: Opus 110k input →
|
|
37
|
+
// $0.55 true cost → ×1.20 → 1 drachma. A non-zero payout is what lets the
|
|
38
|
+
// idempotency watermark SELECT run at all.
|
|
39
|
+
const MODEL_USAGE = { 'claude-opus-4-8': { input: 110_000 } };
|
|
40
|
+
const WATERMARK_RE = /COALESCE\(SUM\(delta\)/;
|
|
41
|
+
|
|
42
|
+
const norm = (q) => String(q.sql).replace(/\s+/g, ' ').trim();
|
|
43
|
+
const find = (queries, re) => queries.find((q) => re.test(norm(q)));
|
|
44
|
+
const all = (queries, re) => queries.filter((q) => re.test(norm(q)));
|
|
45
|
+
|
|
46
|
+
const ATTACH_RE = /WITH cand AS .* UPDATE session_records/;
|
|
47
|
+
const PAYABLE_RE = /SELECT session_id, builder_id, model_usage, total_tokens FROM session_records WHERE task_ids @>/;
|
|
48
|
+
|
|
49
|
+
async function withFakeTx(handler, run) {
|
|
50
|
+
const fake = makeSqlAwareClient(handler);
|
|
51
|
+
const prev = api.withTx;
|
|
52
|
+
api.withTx = (fn) => fn(fake._client);
|
|
53
|
+
try {
|
|
54
|
+
const result = await run();
|
|
55
|
+
return { result, queries: fake.queries };
|
|
56
|
+
} finally {
|
|
57
|
+
api.withTx = prev;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// =========================================================================
|
|
62
|
+
// (1) The attach runs, and runs BEFORE the payable-session lookup
|
|
63
|
+
// =========================================================================
|
|
64
|
+
|
|
65
|
+
await test('awardDeferredSessionRewardForTask: attaches from the claim BEFORE selecting payable sessions', async () => {
|
|
66
|
+
const seen = [];
|
|
67
|
+
const { result, queries } = await withFakeTx(
|
|
68
|
+
(s) => {
|
|
69
|
+
seen.push(s);
|
|
70
|
+
if (ATTACH_RE.test(s)) return { rows: [{ session_id: 'sess-hook-only' }] };
|
|
71
|
+
if (PAYABLE_RE.test(s)) {
|
|
72
|
+
return { rows: [{ session_id: 'sess-hook-only', builder_id: 7, model_usage: MODEL_USAGE, total_tokens: 5000 }] };
|
|
73
|
+
}
|
|
74
|
+
if (WATERMARK_RE.test(s)) return { rows: [{ paid: 0, legacy: 0 }] };
|
|
75
|
+
if (/^INSERT INTO credit_log/.test(s)) return { rows: [{ id: 1 }] };
|
|
76
|
+
return null;
|
|
77
|
+
},
|
|
78
|
+
() => db.awardDeferredSessionRewardForTask(660)
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const attachIdx = seen.findIndex((s) => ATTACH_RE.test(s));
|
|
82
|
+
const payableIdx = seen.findIndex((s) => PAYABLE_RE.test(s));
|
|
83
|
+
assert.ok(attachIdx >= 0, 'the claim-based attach UPDATE must run');
|
|
84
|
+
assert.ok(payableIdx >= 0, 'the payable-session SELECT must run');
|
|
85
|
+
assert.ok(attachIdx < payableIdx,
|
|
86
|
+
'the attach MUST precede the payable lookup — otherwise a skipped upload is unpayable forever');
|
|
87
|
+
assert.equal(result.attached, 1, 'the freshly-linked session is reported');
|
|
88
|
+
assert.equal(result.awarded, 1, 'the hook-only session is paid once linked');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// =========================================================================
|
|
92
|
+
// (2) The end-to-end shape the bug produced: hook-only record gets paid
|
|
93
|
+
// =========================================================================
|
|
94
|
+
|
|
95
|
+
await test('a hook-only record (empty task_ids, empty model_usage) is attributed and paid from total_tokens', async () => {
|
|
96
|
+
// The exact Masterqua row shape: no task, no per-model usage — only a token
|
|
97
|
+
// total. Pricing must fall back to totalTokens (task 1676) or this still pays 0.
|
|
98
|
+
const { result, queries } = await withFakeTx(
|
|
99
|
+
(s) => {
|
|
100
|
+
if (ATTACH_RE.test(s)) return { rows: [{ session_id: 'sess-masterqua' }] };
|
|
101
|
+
if (PAYABLE_RE.test(s)) {
|
|
102
|
+
return { rows: [{ session_id: 'sess-masterqua', builder_id: 1000025, model_usage: {}, total_tokens: 77_403_545 }] };
|
|
103
|
+
}
|
|
104
|
+
if (WATERMARK_RE.test(s)) return { rows: [{ paid: 0, legacy: 0 }] };
|
|
105
|
+
if (/^INSERT INTO credit_log/.test(s)) return { rows: [{ id: 1 }] };
|
|
106
|
+
return null;
|
|
107
|
+
},
|
|
108
|
+
() => db.awardDeferredSessionRewardForTask(660)
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const insert = find(queries, /^INSERT INTO credit_log/);
|
|
112
|
+
assert.ok(insert, 'an empty model_usage must still book a reward via the totalTokens fallback');
|
|
113
|
+
assert.equal(result.awarded, 1, 'the session that was stranded for ten weeks now pays');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// =========================================================================
|
|
117
|
+
// (3) Idempotency + the landing gate (the two things this must not break)
|
|
118
|
+
// =========================================================================
|
|
119
|
+
|
|
120
|
+
await test('attach SQL skips sessions already carrying the task (no re-link, no double-pay)', async () => {
|
|
121
|
+
const { queries } = await withFakeTx(
|
|
122
|
+
(s) => {
|
|
123
|
+
if (ATTACH_RE.test(s)) return { rows: [] }; // nothing new to link
|
|
124
|
+
if (PAYABLE_RE.test(s)) return { rows: [] };
|
|
125
|
+
return null;
|
|
126
|
+
},
|
|
127
|
+
() => db.awardDeferredSessionRewardForTask(660)
|
|
128
|
+
);
|
|
129
|
+
const attach = find(queries, ATTACH_RE);
|
|
130
|
+
assert.ok(/NOT \(sr\.task_ids @> ARRAY/.test(norm(attach)),
|
|
131
|
+
'the attach must exclude sessions already linked to this task');
|
|
132
|
+
assert.equal(all(queries, /^INSERT INTO credit_log/).length, 0, 'nothing linked → nothing paid');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
await test('attach is bounded by a LIMIT so one long session cannot fan out an unbounded UPDATE', async () => {
|
|
136
|
+
const { queries } = await withFakeTx(
|
|
137
|
+
(s) => (ATTACH_RE.test(s) || PAYABLE_RE.test(s) ? { rows: [] } : null),
|
|
138
|
+
() => db.awardDeferredSessionRewardForTask(660)
|
|
139
|
+
);
|
|
140
|
+
const attach = find(queries, ATTACH_RE);
|
|
141
|
+
assert.ok(/LIMIT \$2/.test(norm(attach)), 'the candidate set must be bounded');
|
|
142
|
+
assert.equal(attach.params[1], 50, 'default cap');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
await test('reconcileUnattachedClaimSessions considers ONLY landed tasks (ADR 0120 unchanged)', async () => {
|
|
146
|
+
const { queries } = await withFakeTx(
|
|
147
|
+
(s) => (/FROM claims c JOIN tasks t/.test(s) ? { rows: [] } : null),
|
|
148
|
+
() => db.reconcileUnattachedClaimSessions({ limit: 5 })
|
|
149
|
+
);
|
|
150
|
+
const scan = find(queries, /FROM claims c JOIN tasks t/);
|
|
151
|
+
assert.ok(scan, 'the attribution sweep must scan claims joined to tasks');
|
|
152
|
+
assert.ok(/t\.status = 'shipped'/.test(norm(scan)),
|
|
153
|
+
"the sweep must require status='shipped' — never attribute-and-pay unlanded work");
|
|
154
|
+
assert.equal(scan.params[0], 5, 'the caller-supplied limit is honoured');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// The guard that keeps a safety net from becoming an unauthorised mass payout.
|
|
158
|
+
// Without the window, the first sweep on cloudbongos.com would have retro-linked
|
|
159
|
+
// 1383 tasks / 1157 sessions for a single builder and paid them on the same tick.
|
|
160
|
+
await test('the sweep is WINDOWED — it can never become a retroactive mass backfill', async () => {
|
|
161
|
+
const { queries } = await withFakeTx(
|
|
162
|
+
(s) => (/FROM claims c JOIN tasks t/.test(s) ? { rows: [] } : null),
|
|
163
|
+
() => db.reconcileUnattachedClaimSessions({ limit: 5 })
|
|
164
|
+
);
|
|
165
|
+
const scan = find(queries, /FROM claims c JOIN tasks t/);
|
|
166
|
+
assert.ok(/shipped_at, t\.updated_at\) >= now\(\) - /.test(norm(scan)),
|
|
167
|
+
'the sweep MUST bound itself to recently-landed tasks — historical back-pay is a deliberate Archon decision, not a poller side effect');
|
|
168
|
+
assert.equal(scan.params[1], '7', 'conservative default lookback');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
await test('lookbackDays is clamped to a sane range (no 0-day no-op, no 10-year backfill)', async () => {
|
|
172
|
+
for (const [given, expected] of [[0, '1'], [-5, '1'], [99999, '365'], ['junk', '7']]) {
|
|
173
|
+
const { queries } = await withFakeTx(
|
|
174
|
+
(s) => (/FROM claims c JOIN tasks t/.test(s) ? { rows: [] } : null),
|
|
175
|
+
() => db.reconcileUnattachedClaimSessions({ limit: 5, lookbackDays: given })
|
|
176
|
+
);
|
|
177
|
+
assert.equal(find(queries, /FROM claims c JOIN tasks t/).params[1], expected,
|
|
178
|
+
`lookbackDays ${given} must clamp to ${expected}`);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await test('a non-finite task id is a clean no-op (no SQL, no reward)', async () => {
|
|
183
|
+
const { result, queries } = await withFakeTx(() => null, () => db.awardDeferredSessionRewardForTask('nope'));
|
|
184
|
+
assert.equal(queries.length, 0, 'a garbage id must not reach the DB');
|
|
185
|
+
assert.deepEqual(result, { sessions: 0, awarded: 0, attached: 0 });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
summary();
|
|
@@ -224,8 +224,10 @@ await test('awardDeferredSessionRewardForTask: no session shipped this task →
|
|
|
224
224
|
|
|
225
225
|
await test('awardDeferredSessionRewardForTask: non-numeric id → immediate no-op (no DB touch)', async () => {
|
|
226
226
|
// No withTx patch — a bad id must return before opening a transaction.
|
|
227
|
+
// `attached` joined the shape in task 1002964 (the claim-based attribution
|
|
228
|
+
// recovery that now runs ahead of the payable-session lookup).
|
|
227
229
|
const out = await db.awardDeferredSessionRewardForTask('not-a-number');
|
|
228
|
-
assert.deepEqual(out, { sessions: 0, awarded: 0 });
|
|
230
|
+
assert.deepEqual(out, { sessions: 0, awarded: 0, attached: 0 });
|
|
229
231
|
});
|
|
230
232
|
|
|
231
233
|
// =========================================================================
|