@bongos/core 1.23.20 → 1.23.22
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 +137 -67
- package/.claude/skills/goal-create/SKILL.md +5 -2
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +5 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/adr/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md +87 -0
- package/docs/adr/0386-the-funding-wall-is-a-per-project-rule-claim-time-hold-by-default.md +37 -0
- package/docs/adr/0387-two-ledgers-credits-measure-work-money-stays-with-the-processor.md +39 -0
- package/docs/adr/0388-a-module-sale-is-split-by-the-project-the-buyer-pays-the-platform-fee.md +42 -0
- package/docs/adr/0392-ai-generation-runs-on-a-processor-held-balance-and-the-builders-own-keys.md +33 -0
- package/docs/adr/README.md +5 -0
- package/docs/api/openapi.json +59 -4
- package/docs/api-reference.md +7 -1
- package/docs/file-map.md +3 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +1 -1
- package/modules/ai-studio/CLAUDE.md +15 -0
- package/modules/ai-studio/module.json +26 -0
- package/modules/ai-studio/routes/ai-studio.js +22 -0
- package/modules/copy-desk/chrome-pages.js +178 -0
- package/modules/copy-desk/page-data.js +34 -2
- package/modules/copy-desk/tests/copy_chrome_pages.mjs +127 -0
- package/modules/copy-desk/tests/copy_no_cms.mjs +1 -1
- package/modules/government/db.js +74 -0
- package/modules/government/routes/government.js +28 -18
- package/modules/grading/grader-subagent.js +30 -11
- package/modules/hall-ui/public/city.html +15 -15
- package/modules/lifecycle/db-analytics.js +10 -3
- package/modules/lifecycle/db-goals.js +30 -13
- package/modules/lifecycle/db-shared.js +9 -0
- package/modules/lifecycle/db-versions.js +21 -8
- package/modules/lifecycle/ship-assurance.js +4 -1
- package/modules/lifecycle/ship-card.js +8 -2
- package/modules/onboarding/howto-moments.js +8 -1
- package/modules/provisioning/starter-bundles.js +4 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +72 -0
- package/scripts/bongos/copy-apply.js +3 -1
- package/scripts/bongos/publish-manifest.js +1 -0
- package/scripts/bongos/strand-watch.js +106 -10
- package/scripts/bongos/tweak-renders.js +9 -2
- package/src/bongos/module-scope-map.js +4 -0
- package/src/module-api.js +1 -1
- package/src/platform-server.js +27 -1
- package/tests/batched_writes_parity.mjs +340 -0
- package/tests/conductor_main_e2e.mjs +4 -0
- package/tests/copy_desk_page_docx.mjs +3 -3
- package/tests/copy_desk_page_editor.mjs +6 -6
- package/tests/copy_desk_page_next.mjs +3 -2
- package/tests/copy_desk_page_reads.mjs +2 -1
- package/tests/copy_desk_page_submit.mjs +30 -2
- package/tests/fixtures/per-row-loops-legacy.cjs +245 -0
- package/tests/goal_archive_disposition.mjs +5 -5
- package/tests/government_routes.mjs +145 -22
- package/tests/grader.mjs +31 -0
- package/tests/hall_city.mjs +8 -0
- package/tests/hook_shell_tools.mjs +8 -0
- package/tests/hooks_drain.mjs +1 -1
- package/tests/host_topology_skips.mjs +2 -0
- package/tests/howto_moments.mjs +17 -0
- package/tests/module_loader.mjs +1 -1
- package/tests/module_skills_hook.mjs +69 -0
- package/tests/profile_role_stats.mjs +2 -1
- package/tests/public_landing_projects.mjs +37 -0
- package/tests/session_start_freshness.mjs +66 -1
- package/tests/shell_hooks.mjs +209 -0
- package/tests/ship_assurance.mjs +28 -0
- package/tests/ship_card.mjs +20 -5
- package/tests/strand_watch.mjs +48 -0
- package/tests/version_close_route.mjs +1 -1
|
@@ -860,12 +860,19 @@ async function tallyPeerVotes() {
|
|
|
860
860
|
const karmaRows = [];
|
|
861
861
|
for (const [builderId, net] of netByBuilder) {
|
|
862
862
|
if (net === 0) continue; // no net signal → no karma row, but still mark tallied
|
|
863
|
+
karmaRows.push({ builder_id: builderId, delta: net });
|
|
864
|
+
}
|
|
865
|
+
// task 1004929: ONE insert for every credited builder, not one per builder.
|
|
866
|
+
// WITH ORDINALITY + ORDER BY keeps the karma_log ids in the order the loop
|
|
867
|
+
// inserted them (first vote seen first).
|
|
868
|
+
if (karmaRows.length) {
|
|
863
869
|
await client.query(
|
|
864
870
|
`INSERT INTO karma_log (builder_id, delta, reason, granted_by)
|
|
865
|
-
|
|
866
|
-
|
|
871
|
+
SELECT k.builder_id, k.delta, 'peer_vote_weekly', NULL::bigint
|
|
872
|
+
FROM unnest($1::bigint[], $2::int[]) WITH ORDINALITY AS k(builder_id, delta, ord)
|
|
873
|
+
ORDER BY k.ord`,
|
|
874
|
+
[karmaRows.map((r) => r.builder_id), karmaRows.map((r) => r.delta)]
|
|
867
875
|
);
|
|
868
|
-
karmaRows.push({ builder_id: builderId, delta: net });
|
|
869
876
|
}
|
|
870
877
|
if (voteIds.length) {
|
|
871
878
|
await client.query(
|
|
@@ -16,7 +16,7 @@ const doneWhen = require('./done-when.js');
|
|
|
16
16
|
const { deriveRequiredRank, highestRank } = require('./db-rank-authz.js');
|
|
17
17
|
const { nonTerminalSql, isTerminalStatus } = require('./task-status');
|
|
18
18
|
const { maintenanceGoalTitle } = require('./goal-advisory.js');
|
|
19
|
-
const { SMOKE_NOT_LIKE } = require('./db-shared.js');
|
|
19
|
+
const { SMOKE_NOT_LIKE, idKey } = require('./db-shared.js');
|
|
20
20
|
const { executorFor, inTransaction } = require('./db-executor.js');
|
|
21
21
|
// Task 1003660 — closing a goal cancels its pending invites + join requests.
|
|
22
22
|
const { cancelPendingMembershipRequests } = require('./goal-close-sweep.js');
|
|
@@ -733,28 +733,43 @@ async function archiveGoalWithDispositions({ goalId, plan = [], reason }, deps =
|
|
|
733
733
|
return { goal: goalRows[0], applied: [], alreadyArchived: true };
|
|
734
734
|
}
|
|
735
735
|
|
|
736
|
-
|
|
737
|
-
|
|
736
|
+
// task 1004929: every plan task is locked and read in ONE statement, and the
|
|
737
|
+
// active claims on them in one more, instead of two round trips per step. The
|
|
738
|
+
// locks are taken in id order (no deadlock against another batch), then the
|
|
739
|
+
// plan is walked in memory in PLAN order, so the applied list and the first
|
|
740
|
+
// error are what the per-step loop produced. `inGoal` tracks this call's own
|
|
741
|
+
// writes, so a task named twice reads the state the first step left behind.
|
|
742
|
+
const inGoal = new Map();
|
|
743
|
+
const claimedIds = new Set();
|
|
744
|
+
if (plan.length > 0) {
|
|
745
|
+
const ids = plan.map((step) => step.taskId);
|
|
738
746
|
const { rows: tRows } = await client.query(
|
|
739
|
-
`SELECT id, title, status FROM tasks WHERE id = $1 AND goal_id = $2 FOR UPDATE`,
|
|
740
|
-
[
|
|
747
|
+
`SELECT id, title, status FROM tasks WHERE id = ANY($1::bigint[]) AND goal_id = $2 ORDER BY id FOR UPDATE`,
|
|
748
|
+
[ids, goalId]
|
|
741
749
|
);
|
|
750
|
+
for (const row of tRows) inGoal.set(idKey(row.id), row);
|
|
751
|
+
const { rows: claimed } = await client.query(
|
|
752
|
+
`SELECT DISTINCT task_id FROM claims WHERE task_id = ANY($1::bigint[]) AND released_at IS NULL`, [ids]
|
|
753
|
+
);
|
|
754
|
+
for (const row of claimed) claimedIds.add(idKey(row.task_id));
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const applied = [];
|
|
758
|
+
for (const step of plan) {
|
|
759
|
+
const task = inGoal.get(idKey(step.taskId));
|
|
742
760
|
// The task left this goal (or shipped) while the caller was deciding. Not an
|
|
743
761
|
// error: the disposition is moot, and the final re-count is what actually
|
|
744
762
|
// guards the invariant. Recorded as 'skipped' so the response tells the
|
|
745
763
|
// truth about what this call did rather than claiming a write it never made.
|
|
746
|
-
if (!
|
|
747
|
-
if (isTerminalStatus(
|
|
748
|
-
applied.push({ task_id: String(step.taskId), verb: step.verb, result: 'skipped_already_terminal', status:
|
|
764
|
+
if (!task) { applied.push({ task_id: String(step.taskId), verb: step.verb, result: 'skipped_not_in_goal' }); continue; }
|
|
765
|
+
if (isTerminalStatus(task.status)) {
|
|
766
|
+
applied.push({ task_id: String(step.taskId), verb: step.verb, result: 'skipped_already_terminal', status: task.status });
|
|
749
767
|
continue;
|
|
750
768
|
}
|
|
751
769
|
|
|
752
|
-
|
|
753
|
-
`SELECT id FROM claims WHERE task_id = $1 AND released_at IS NULL LIMIT 1`, [step.taskId]
|
|
754
|
-
);
|
|
755
|
-
if (claimed[0]) {
|
|
770
|
+
if (claimedIds.has(idKey(step.taskId))) {
|
|
756
771
|
throw Object.assign(new Error('task has an active claim'), {
|
|
757
|
-
code: 'TASK_HAS_ACTIVE_CLAIM', taskId: String(step.taskId), taskTitle:
|
|
772
|
+
code: 'TASK_HAS_ACTIVE_CLAIM', taskId: String(step.taskId), taskTitle: task.title,
|
|
758
773
|
});
|
|
759
774
|
}
|
|
760
775
|
|
|
@@ -766,6 +781,7 @@ async function archiveGoalWithDispositions({ goalId, plan = [], reason }, deps =
|
|
|
766
781
|
WHERE id = $1`,
|
|
767
782
|
[step.taskId, `Abandoned: ${reason}`]
|
|
768
783
|
);
|
|
784
|
+
task.status = 'abandoned';
|
|
769
785
|
applied.push({ task_id: String(step.taskId), verb: 'abandon', result: 'abandoned' });
|
|
770
786
|
} else {
|
|
771
787
|
const { rows: dest } = await client.query(
|
|
@@ -780,6 +796,7 @@ async function archiveGoalWithDispositions({ goalId, plan = [], reason }, deps =
|
|
|
780
796
|
});
|
|
781
797
|
}
|
|
782
798
|
await client.query(`UPDATE tasks SET goal_id = $2, updated_at = now() WHERE id = $1`, [step.taskId, step.toGoalId]);
|
|
799
|
+
if (idKey(step.toGoalId) !== idKey(goalId)) inGoal.delete(idKey(step.taskId));
|
|
783
800
|
applied.push({ task_id: String(step.taskId), verb: 'move', result: 'moved', to_goal_id: String(step.toGoalId) });
|
|
784
801
|
}
|
|
785
802
|
}
|
|
@@ -104,10 +104,19 @@ async function mostRecentClaimHolderId(client, taskId) {
|
|
|
104
104
|
return rows[0]?.builder_id ?? null;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
// One spelling of a bigint id as a Map key (task 1004929). pg returns bigint
|
|
108
|
+
// columns as strings while a plan may carry numbers, so a batched locked read
|
|
109
|
+
// (the goal archive, the version close) matches its rows back to the plan steps
|
|
110
|
+
// through this on both sides. A value BigInt cannot read keeps its own string.
|
|
111
|
+
function idKey(v) {
|
|
112
|
+
try { return BigInt(v).toString(); } catch { return String(v); }
|
|
113
|
+
}
|
|
114
|
+
|
|
107
115
|
module.exports = {
|
|
108
116
|
SMOKE_NOT_LIKE,
|
|
109
117
|
gradePassCount,
|
|
110
118
|
gradeQualityWhere,
|
|
119
|
+
idKey,
|
|
111
120
|
median,
|
|
112
121
|
mostRecentClaimHolderId,
|
|
113
122
|
sealSessionLog,
|
|
@@ -14,7 +14,7 @@ const api = require('../../src/module-api');
|
|
|
14
14
|
const { pool, withTx } = api;
|
|
15
15
|
const doneWhen = require('./done-when.js');
|
|
16
16
|
const { nonTerminalSql } = require('./task-status');
|
|
17
|
-
const { SMOKE_NOT_LIKE } = require('./db-shared.js');
|
|
17
|
+
const { SMOKE_NOT_LIKE, idKey } = require('./db-shared.js');
|
|
18
18
|
const { executorFor, inTransaction } = require('./db-executor.js');
|
|
19
19
|
// db-goals.js does NOT require this file, so this direction adds no require cycle
|
|
20
20
|
// (fitness pins the count). createVersion needs it to seed a version's FIRST goal
|
|
@@ -372,18 +372,29 @@ async function closeVersion({ versionId, plan = [], reason, planningVersionId =
|
|
|
372
372
|
});
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
-
|
|
376
|
-
|
|
375
|
+
// task 1004929: every plan goal is locked and read in ONE statement instead of
|
|
376
|
+
// one round trip per step. Locks are taken in id order (no deadlock against
|
|
377
|
+
// another batch); the plan is then walked in memory in PLAN order, so the
|
|
378
|
+
// applied list is what the per-step loop produced. Each step's own archive is
|
|
379
|
+
// mirrored into `onVersion`, so a goal named twice reads 'archived' second time.
|
|
380
|
+
const onVersion = new Map();
|
|
381
|
+
if (plan.length > 0) {
|
|
377
382
|
const { rows: gRows } = await client.query(
|
|
378
|
-
`SELECT id, title, status FROM goals WHERE id = $1 AND version_id = $2 FOR UPDATE`,
|
|
379
|
-
[step.goalId, versionId]
|
|
383
|
+
`SELECT id, title, status FROM goals WHERE id = ANY($1::bigint[]) AND version_id = $2 ORDER BY id FOR UPDATE`,
|
|
384
|
+
[plan.map((step) => step.goalId), versionId]
|
|
380
385
|
);
|
|
386
|
+
for (const row of gRows) onVersion.set(idKey(row.id), row);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const applied = [];
|
|
390
|
+
for (const step of plan) {
|
|
391
|
+
const goal = onVersion.get(idKey(step.goalId));
|
|
381
392
|
// The goal moved or closed itself while the caller was deciding. Not an
|
|
382
393
|
// error — the final re-count is what guards the invariant — but recorded so
|
|
383
394
|
// the response says what this call actually did.
|
|
384
|
-
if (!
|
|
385
|
-
if (
|
|
386
|
-
applied.push({ goal_id: String(step.goalId), verb: step.verb, result: 'skipped_already_closed', status:
|
|
395
|
+
if (!goal) { applied.push({ goal_id: String(step.goalId), verb: step.verb, result: 'skipped_not_on_version' }); continue; }
|
|
396
|
+
if (goal.status !== 'open') {
|
|
397
|
+
applied.push({ goal_id: String(step.goalId), verb: step.verb, result: 'skipped_already_closed', status: goal.status });
|
|
387
398
|
continue;
|
|
388
399
|
}
|
|
389
400
|
|
|
@@ -410,6 +421,7 @@ async function closeVersion({ versionId, plan = [], reason, planningVersionId =
|
|
|
410
421
|
[step.goalId, `Abandoned: ${reason}`]
|
|
411
422
|
);
|
|
412
423
|
await client.query(`UPDATE goals SET status = 'archived', updated_at = now() WHERE id = $1`, [step.goalId]);
|
|
424
|
+
Object.assign(goal, { status: 'archived' }); // mirror the write above for a goal named twice
|
|
413
425
|
await cancelPendingMembershipRequests(client, step.goalId);
|
|
414
426
|
applied.push({ goal_id: String(step.goalId), verb: 'abandon', result: 'archived', tasks_abandoned: killed.length });
|
|
415
427
|
} else {
|
|
@@ -502,6 +514,7 @@ async function closeVersion({ versionId, plan = [], reason, planningVersionId =
|
|
|
502
514
|
// on the way out rather than being special-cased around it (the brief's
|
|
503
515
|
// explicit ask).
|
|
504
516
|
await client.query(`UPDATE goals SET status = 'archived', updated_at = now() WHERE id = $1`, [step.goalId]);
|
|
517
|
+
Object.assign(goal, { status: 'archived' }); // mirror the write above for a goal named twice
|
|
505
518
|
await cancelPendingMembershipRequests(client, step.goalId);
|
|
506
519
|
|
|
507
520
|
applied.push({
|
|
@@ -194,12 +194,15 @@ function githubAccessCause(errors) {
|
|
|
194
194
|
// best-effort by design: insertAuditLog uses the module pool rather than the
|
|
195
195
|
// ship's transaction client, and an audit miss must never roll back a landed ship
|
|
196
196
|
// (the posture src/bongos/middleware/audit.js already takes).
|
|
197
|
+
// method is 'POST' — the ship route's own verb. audit_log's CHECK admits only
|
|
198
|
+
// POST/PATCH/PUT/DELETE (migration 033), so the old 'SHIP' failed every write
|
|
199
|
+
// and the record never existed (task 1004879).
|
|
197
200
|
async function recordShipAssurance({ insertAuditLog, taskId, builderId, assurance, log }) {
|
|
198
201
|
try {
|
|
199
202
|
await insertAuditLog({
|
|
200
203
|
builderId,
|
|
201
204
|
route: `/tasks/${taskId}/ship`,
|
|
202
|
-
method: '
|
|
205
|
+
method: 'POST',
|
|
203
206
|
requestBodyRedacted: { ship_assurance: assurance },
|
|
204
207
|
responseStatus: 200,
|
|
205
208
|
surface: 'lifecycle',
|
|
@@ -447,10 +447,15 @@ function buildCardModel(input = {}) {
|
|
|
447
447
|
let deployText;
|
|
448
448
|
let deployCommit = null;
|
|
449
449
|
let hostedText = null;
|
|
450
|
+
// task 1004741: "live" only when a reading SAYS the live build carries this
|
|
451
|
+
// merge. Shipped means merged; a merge reaches no instance until a published
|
|
452
|
+
// release carries it and the pin moves (merge-reach.js).
|
|
453
|
+
let liveConfirmed = false;
|
|
450
454
|
if (stage === 'shipped') {
|
|
451
455
|
deployCommit = input.deploy && input.deploy.commit ? String(input.deploy.commit) : null;
|
|
452
456
|
const reach = boundReading(input.deploy && input.deploy.reach);
|
|
453
457
|
if (reach) {
|
|
458
|
+
liveConfirmed = reach.includes === true;
|
|
454
459
|
// task 1004682: who sees this merge, in the one shared wording
|
|
455
460
|
// (merge-reach.js) — the hub's live build as read, then the hosted rule.
|
|
456
461
|
// A bare "live · <sha>" read as "every user has it", which is only true
|
|
@@ -458,7 +463,8 @@ function buildCardModel(input = {}) {
|
|
|
458
463
|
// boundReading re-bounds it (host-shaped, hex, boolean) before printing.
|
|
459
464
|
[deployText, hostedText] = whoSeesAMerge(reach);
|
|
460
465
|
} else {
|
|
461
|
-
|
|
466
|
+
// No reading of the live build: say what is known (it merged), never "live".
|
|
467
|
+
deployText = `merged${deployCommit ? ` · ${deployCommit}` : ''} — not live until a published release carries it and the pin moves`;
|
|
462
468
|
}
|
|
463
469
|
} else if (stage === 'confirmed') {
|
|
464
470
|
// task 1002541: this line used to be unconditional, driven by the STATUS
|
|
@@ -491,7 +497,7 @@ function buildCardModel(input = {}) {
|
|
|
491
497
|
|
|
492
498
|
addFact('', changed);
|
|
493
499
|
if (gradeText) addFact('grade', gradeText, { cls: gradeCls });
|
|
494
|
-
if (stage === 'shipped') addFact('live', deployCommit, { mono: true });
|
|
500
|
+
if (stage === 'shipped') addFact(liveConfirmed ? 'live' : 'merged', deployCommit, { mono: true });
|
|
495
501
|
// The reason can be a full sentence — too long for a fact chip, so the chip
|
|
496
502
|
// carries the alarm and the reason rides in its tooltip (the Deploy row above
|
|
497
503
|
// always shows it in full).
|
|
@@ -46,9 +46,16 @@ const REQUESTS = [
|
|
|
46
46
|
['show', new RegExp(String.raw`\b(?:show|open|reopen|see|view)\b.{0,24}\b${GUIDE}\b|\b${GUIDE}\b.{0,12}\bprogress\b`, 'i')],
|
|
47
47
|
];
|
|
48
48
|
|
|
49
|
+
// Only a prompt that IS the request counts (task 1004744): one short line. A pasted
|
|
50
|
+
// handoff or a long message that merely QUOTES "skip the tutorial" is about
|
|
51
|
+
// something else, and running `howto skip` on it would change the person's guide
|
|
52
|
+
// state unasked.
|
|
53
|
+
const REQUEST_MAX_CHARS = 100;
|
|
54
|
+
|
|
49
55
|
// 'show' | 'next' | 'skip' | 'reset' when the prompt asks for the guide itself, else null.
|
|
50
56
|
function guideRequest(prompt) {
|
|
51
|
-
const p = typeof prompt === 'string' ? prompt : '';
|
|
57
|
+
const p = typeof prompt === 'string' ? prompt.trim() : '';
|
|
58
|
+
if (p.length > REQUEST_MAX_CHARS || /[\r\n]/.test(p)) return null;
|
|
52
59
|
for (const [kind, re] of REQUESTS) if (re.test(p)) return kind;
|
|
53
60
|
return null;
|
|
54
61
|
}
|
|
@@ -78,8 +78,10 @@ const ALWAYS_ON_CORE = Object.freeze([
|
|
|
78
78
|
// Render: it is turned on by the connect-Render step, never chosen in this picker, so it is
|
|
79
79
|
// kept out of the opt-in catalog by the same list. `raise` (task 1004847) is the hub's own
|
|
80
80
|
// investor page: its copy is Cloud Bongos's raise, so offering it to a new project would hand
|
|
81
|
-
// that project someone else's fundraise (task 1004852).
|
|
82
|
-
|
|
81
|
+
// that project someone else's fundraise (task 1004852). `ai-studio` (task 1004916, ADR 0392)
|
|
82
|
+
// bills AI generation through Cloud Bongos's own payment processor account, so it stays on
|
|
83
|
+
// the hub until a project can bring its own billing.
|
|
84
|
+
const HUB_ONLY_MODULES = Object.freeze(['platform-identity', 'provisioning', 'npm-release', 'render-deploy', 'raise', 'ai-studio']);
|
|
83
85
|
|
|
84
86
|
// The OPT-IN catalog as the creation picker renders it (BV1.R20, task 1002339):
|
|
85
87
|
// every module an owner may actually choose, in the order the picker lists them,
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.23.
|
|
3
|
+
"version": "1.23.22",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.23.
|
|
9
|
+
"version": "1.23.22",
|
|
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.23.
|
|
3
|
+
"version": "1.23.22",
|
|
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/release-notes.json
CHANGED
|
@@ -10509,5 +10509,77 @@
|
|
|
10509
10509
|
"id": "1002983",
|
|
10510
10510
|
"text": "The Community home now shows a rising builder and the projects on the move this month, under the builders of the week, month and year."
|
|
10511
10511
|
}
|
|
10512
|
+
],
|
|
10513
|
+
"1.23.21": [
|
|
10514
|
+
{
|
|
10515
|
+
"id": "1004904",
|
|
10516
|
+
"text": "In Tweak Mode, the shared top bar, navigation, footer and skip link no longer clutter every page; each is now its own page to review once."
|
|
10517
|
+
}
|
|
10518
|
+
],
|
|
10519
|
+
"1.23.22": [
|
|
10520
|
+
{
|
|
10521
|
+
"id": "1004916",
|
|
10522
|
+
"text": "Groundwork for letting artists generate images with AI: the design for pricing and key storage is written down and the new module has its place in the project."
|
|
10523
|
+
},
|
|
10524
|
+
{
|
|
10525
|
+
"id": "1004742",
|
|
10526
|
+
"text": "Asking the session guide for the next tutorial step a second time now shows just that step, instead of repeating every earlier reminder."
|
|
10527
|
+
},
|
|
10528
|
+
{
|
|
10529
|
+
"id": "1004929",
|
|
10530
|
+
"text": "Four database jobs that used to ask the database one question per item now ask once for the whole batch: adding a recommended set of ranks, archiving a goal, closing a version, and the weekly karma count. Results are the same,"
|
|
10531
|
+
},
|
|
10532
|
+
{
|
|
10533
|
+
"id": "1004944",
|
|
10534
|
+
"text": "The goal-planning guide no longer tells people to send a setting the server rejects, and now reminds them to name each task's role."
|
|
10535
|
+
},
|
|
10536
|
+
{
|
|
10537
|
+
"id": "1004931",
|
|
10538
|
+
"text": "AI sessions run faster. The safety checks that run around every command the AI types used to start four or five separate small programs each time; they now share two, cutting that overhead about in half with the same checks an"
|
|
10539
|
+
},
|
|
10540
|
+
{
|
|
10541
|
+
"id": "1004909",
|
|
10542
|
+
"text": "How module sales are shared is now decided. The buyer pays a small platform fee on top of the price (processing cost plus 1 percent), so builders lose nothing to it. Each project sets how its module money is split between the"
|
|
10543
|
+
},
|
|
10544
|
+
{
|
|
10545
|
+
"id": "1004741",
|
|
10546
|
+
"text": "The finished-task card no longer says a change is \"live\" just because it was merged. It now says \"merged\" until the live site is actually confirmed to be running it, so nobody acts on work that has not reached the site yet."
|
|
10547
|
+
},
|
|
10548
|
+
{
|
|
10549
|
+
"id": "1004928",
|
|
10550
|
+
"text": "AI sessions now start several seconds faster: the stuck-work check reuses its recent answer instead of asking the server every time, and the check for newer code no longer waits on the network while still saying how up to date"
|
|
10551
|
+
},
|
|
10552
|
+
{
|
|
10553
|
+
"id": "1004925",
|
|
10554
|
+
"text": "Pages load a little faster: the city page no longer waits on each of its scripts in turn, and returning visitors to the landing keep its styles and scripts for a year instead of fetching them again every few hours."
|
|
10555
|
+
},
|
|
10556
|
+
{
|
|
10557
|
+
"id": "1004932",
|
|
10558
|
+
"text": "Quality reviews now cost less. Each reviewer used to be handed up to twice as much file text as it needs; it now gets half, starting with the files the change is mostly about, and can still open the rest itself."
|
|
10559
|
+
},
|
|
10560
|
+
{
|
|
10561
|
+
"id": "1003180",
|
|
10562
|
+
"text": "Credits and money are now two separate records. Credits keep measuring who did how much work, and money lives in its own record tied to the payment processor, so a payout can always be explained from one place. Old credit reco"
|
|
10563
|
+
},
|
|
10564
|
+
{
|
|
10565
|
+
"id": "1004744",
|
|
10566
|
+
"text": "Pasting a long message that happens to mention \"skip the tutorial\" no longer switches off your how-to guide. Only a short request that actually asks for it does."
|
|
10567
|
+
},
|
|
10568
|
+
{
|
|
10569
|
+
"id": "1003181",
|
|
10570
|
+
"text": "Each project now decides what happens when it runs short of money: refuse new work, refuse completion, or carry on and record what it owes. A project that picks nothing gets the safest default, which stops a builder from start"
|
|
10571
|
+
},
|
|
10572
|
+
{
|
|
10573
|
+
"id": "1004915",
|
|
10574
|
+
"text": "Artists now get /tweak (and any other enabled module skill) automatically when a session starts, instead of needing a manual copy step. Other builders see no change."
|
|
10575
|
+
},
|
|
10576
|
+
{
|
|
10577
|
+
"id": "1004879",
|
|
10578
|
+
"text": "Every ship now keeps its record of why it was allowed to land. Before, that record was silently thrown away on every ship because the database refused how it was labelled."
|
|
10579
|
+
},
|
|
10580
|
+
{
|
|
10581
|
+
"id": "1004905",
|
|
10582
|
+
"text": "The money plan is now decided on paper: a licensed payment processor holds all user and project money, never Bongos. Credits stay a per-project measure of contribution and are never cash. Each project sets what its work pays a"
|
|
10583
|
+
}
|
|
10512
10584
|
]
|
|
10513
10585
|
}
|
|
@@ -62,6 +62,7 @@ const ci = require('./copy-inventory.js');
|
|
|
62
62
|
const proposals = require('../../modules/copy-desk/proposals.js');
|
|
63
63
|
// The page tweak format's one reader and writer (ADR 0341 D4), for --batch.
|
|
64
64
|
const pages = require('../../modules/copy-desk/pages.js');
|
|
65
|
+
const chromePages = require('../../modules/copy-desk/chrome-pages.js');
|
|
65
66
|
const { apiCall, arg, hasFlag, cliExit } = require('./cli-lib.js');
|
|
66
67
|
|
|
67
68
|
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
@@ -559,7 +560,8 @@ function rereadStalePages({ spawn = spawnSync } = {}) {
|
|
|
559
560
|
function readingFor(pageId, { readFile = (p) => fs.readFileSync(p, 'utf8') } = {}) {
|
|
560
561
|
try {
|
|
561
562
|
const doc = JSON.parse(readFile(path.join(REPO_ROOT, 'docs', 'page-readings.json')));
|
|
562
|
-
|
|
563
|
+
// A shared-chrome page (task 1004904) exists only in the derived reading.
|
|
564
|
+
return (chromePages.splitReadings(doc).readings.pages || []).find((p) => p.id === pageId) || null;
|
|
563
565
|
} catch { return null; }
|
|
564
566
|
}
|
|
565
567
|
|
|
@@ -76,6 +76,7 @@ const PUBLISH_ALLOWLIST = [
|
|
|
76
76
|
'modules/agents/', // the declarative agent registry + spawn seam (goal 1000038) — CORE: its own manifest declares maintenance.status 'core-maintained', and it consumes the core `grade` port, so a released core without it cannot run the MAS machinery the methodology describes. default:false — shipped is not enabled (ADR 0083), exactly like provisioning + platform-identity below. Omitted at module-creation time (tasks 1002485/1002486): the omission was invisible until tests/agents_spawn.mjs became the first PUBLISHABLE file to import it and the R89 boundary guard caught it (task 1002489).
|
|
77
77
|
'modules/npm-release/', // the deploy page's "where is the work" + version history (task 1002622). default:false — on only in the hall that publishes the core. MUST be here: modules/hall-ui/ ships as one subtree, so a released core without this carries deploy.js's loader and the #nr-work mounts with no routes behind them — the copy-desk asymmetry above.
|
|
78
78
|
'modules/render-deploy/', // the deploy page of a project whose app deploys through Render (task 1004270). default:false. MUST be here for the npm-release reason: modules/hall-ui/ ships as one subtree, so a released core without it carries deploy.js's loader for #rd-panel with no routes behind it.
|
|
79
|
+
'modules/ai-studio/', // AI generation for artists (task 1004916, ADR 0392): keys, prepaid balance, generate. default:false.
|
|
79
80
|
'modules/raise/', // the /raise investor page (task 1004847, ADR 0384) — ships in core, enabled ONLY on the hub, like platform-identity. Owner decision 2026-10-25: the whole page, its copy and starting numbers included, lives in the repo so builders can contribute to it, accepting that it publishes with the core. Secrets never do: the investor password is RAISE_PASSWORD_HASH in the instance env. default:false.
|
|
80
81
|
'modules/platform-identity/', // hub identity provider (ADR 0141) — ships in core, enabled ONLY on the hub (cloudbongos.com). MUST be here or the hub's enabledModules() throws "unknown module" and provision.js's federation load MODULE_NOT_FOUNDs (task 1002235, same rationale as provisioning above). default:false — shipped ≠ enabled; a vanilla/self-hosted instance never turns it on.
|
|
81
82
|
// --- neutral (customer-0) config starters + their schemas (the filled-in *.json twins are denied) ---
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
//
|
|
32
32
|
// Usage:
|
|
33
33
|
// node scripts/bongos/strand-watch.js # human-readable report
|
|
34
|
-
// node scripts/bongos/strand-watch.js --line # ONE line, or nothing; always exit 0
|
|
34
|
+
// node scripts/bongos/strand-watch.js --line # ONE line, or nothing; always exit 0 (cached; see linePlan)
|
|
35
|
+
// node scripts/bongos/strand-watch.js --refresh-line # refresh that cache; prints nothing (spawned detached by --line)
|
|
35
36
|
// node scripts/bongos/strand-watch.js --json # machine shape
|
|
36
37
|
// node scripts/bongos/strand-watch.js --hours N # override the `completed` threshold
|
|
37
38
|
//
|
|
@@ -273,6 +274,77 @@ function report(r) {
|
|
|
273
274
|
console.log('');
|
|
274
275
|
}
|
|
275
276
|
|
|
277
|
+
// ---- the SessionStart line, kept off the critical path (task 1004928) --------
|
|
278
|
+
//
|
|
279
|
+
// THE MEASUREMENT. The credit-burn audit timed this hook at 8.2 s — the slowest SessionStart
|
|
280
|
+
// producer, and so the floor for every session start. The shared cache it used (task 1003689)
|
|
281
|
+
// lived 60 s, which only helps sessions opened together; the next session an hour later paid
|
|
282
|
+
// the whole ledger read again, up to 8 s per call (and the held-tweak pass can add another).
|
|
283
|
+
//
|
|
284
|
+
// THE FIX. A strand is measured in hours (one hour for `confirmed`, twelve for `completed`), so
|
|
285
|
+
// an answer a few minutes old is as true as a fresh one. So: keep the answer LINE_FRESH_MS and
|
|
286
|
+
// print it at once; past that, up to LINE_STALE_MAX_MS, print it at once LABELLED WITH ITS AGE
|
|
287
|
+
// and refresh the cache in a detached child for the next session; with nothing usable, read the
|
|
288
|
+
// ledger but give up after LINE_DEADLINE_MS. An undetermined read is never cached — a stored
|
|
289
|
+
// silence would pass for a clean board for the whole fresh window.
|
|
290
|
+
const LINE_CACHE_KEY = 'strand-watch-line';
|
|
291
|
+
const LINE_FRESH_MS = 15 * 60 * 1000;
|
|
292
|
+
const LINE_STALE_MAX_MS = 2 * 60 * 60 * 1000;
|
|
293
|
+
const LINE_DEADLINE_MS = 4000;
|
|
294
|
+
|
|
295
|
+
// PURE — what to do with the cached record: 'fresh' (print it), 'stale' (print it with its age
|
|
296
|
+
// and refresh in the background) or 'miss' (read the ledger now, time-boxed). A record from the
|
|
297
|
+
// future means the clock moved, and is treated as a miss rather than trusted.
|
|
298
|
+
function linePlan(record, nowMs, { freshMs = LINE_FRESH_MS, staleMaxMs = LINE_STALE_MAX_MS } = {}) {
|
|
299
|
+
if (!record || typeof record.at !== 'number' || !Number.isFinite(nowMs)) return 'miss';
|
|
300
|
+
const age = nowMs - record.at;
|
|
301
|
+
if (age < 0) return 'miss';
|
|
302
|
+
if (age < freshMs) return 'fresh';
|
|
303
|
+
if (age < staleMaxMs) return 'stale';
|
|
304
|
+
return 'miss';
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// PURE — the line as printed from a stale cache: the same words, plus how old they are, so a
|
|
308
|
+
// reader can weigh them. A clean (null) answer stays silent: there is nothing to label.
|
|
309
|
+
function labelStale(text, ageMs) {
|
|
310
|
+
if (!text) return null;
|
|
311
|
+
return `${text} (Checked ${humanAge(ageMs)} ago; refreshing for the next session.)`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// The ledger read for the line, bounded as a WHOLE (check() bounds each call, not the sum).
|
|
315
|
+
// Throws on an undetermined read so probe-cache stores nothing.
|
|
316
|
+
async function produceLine(opts, deadlineMs) {
|
|
317
|
+
let timer;
|
|
318
|
+
const deadline = new Promise((_, rej) => { timer = setTimeout(() => rej(new Error('deadline')), deadlineMs); });
|
|
319
|
+
try {
|
|
320
|
+
const r = await Promise.race([check({ ...opts, timeoutMs: Math.min(deadlineMs, DEFAULT_TIMEOUT_MS) }), deadline]);
|
|
321
|
+
if (!r || r.undetermined) throw new Error('undetermined');
|
|
322
|
+
return line(r);
|
|
323
|
+
} finally { clearTimeout(timer); }
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function readLineRecord() {
|
|
327
|
+
try {
|
|
328
|
+
const { cachePath } = require('./probe-cache.js');
|
|
329
|
+
return JSON.parse(require('node:fs').readFileSync(cachePath(LINE_CACHE_KEY), 'utf8'));
|
|
330
|
+
} catch { return null; }
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Detached + unref'd + hidden, so the session start never waits on it (windowsHide: a detached
|
|
334
|
+
// child on Windows otherwise flashes a console window). --hours is passed through so the
|
|
335
|
+
// refreshed answer uses the same threshold.
|
|
336
|
+
function spawnRefresh() {
|
|
337
|
+
try {
|
|
338
|
+
const args = [__filename, '--refresh-line'];
|
|
339
|
+
const hours = arg('--hours');
|
|
340
|
+
if (hours) args.push('--hours', String(hours));
|
|
341
|
+
const child = require('node:child_process').spawn(process.execPath, args, {
|
|
342
|
+
detached: true, stdio: 'ignore', windowsHide: true, cwd: process.cwd(),
|
|
343
|
+
});
|
|
344
|
+
child.unref();
|
|
345
|
+
} catch { /* the next session reads the ledger itself */ }
|
|
346
|
+
}
|
|
347
|
+
|
|
276
348
|
async function main() {
|
|
277
349
|
const hours = Number(arg('--hours'));
|
|
278
350
|
const opts = Number.isFinite(hours) && hours > 0 ? { completedStaleMs: hours * 3600000 } : {};
|
|
@@ -285,16 +357,39 @@ async function main() {
|
|
|
285
357
|
// deploy-divergence's identical comment).
|
|
286
358
|
if (hasFlag('--line')) {
|
|
287
359
|
try {
|
|
288
|
-
// SHARED ACROSS
|
|
289
|
-
// within three seconds — one worktree per claim, the normal pattern here — used
|
|
290
|
-
// to make this same call four times and ALL of them timed out, which produces no
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
|
|
360
|
+
// SHARED ACROSS SESSION STARTS (task 1003689, widened by task 1004928). Four sessions
|
|
361
|
+
// opened within three seconds — one worktree per claim, the normal pattern here — used
|
|
362
|
+
// to make this same call four times and ALL of them timed out, which produces no line
|
|
363
|
+
// at all. The first session to ask answers for the rest; see linePlan for how long.
|
|
364
|
+
const rec = readLineRecord();
|
|
365
|
+
const plan = linePlan(rec, Date.now());
|
|
366
|
+
if (plan === 'fresh') {
|
|
367
|
+
if (rec.value) console.log(rec.value);
|
|
368
|
+
} else if (plan === 'stale') {
|
|
369
|
+
const one = labelStale(rec.value, Date.now() - rec.at);
|
|
370
|
+
if (one) console.log(one);
|
|
371
|
+
spawnRefresh();
|
|
372
|
+
} else {
|
|
373
|
+
const { cached } = require('./probe-cache.js');
|
|
374
|
+
const { value: one, from } = await cached(LINE_CACHE_KEY, () => produceLine(opts, LINE_DEADLINE_MS), { ttlMs: LINE_FRESH_MS });
|
|
375
|
+
// A sibling holds the lock and the cache handed back its last answer, however old:
|
|
376
|
+
// say how old, so a long-expired line is never passed off as current.
|
|
377
|
+
const shown = from === 'stale-while-locked' && rec && typeof rec.at === 'number'
|
|
378
|
+
? labelStale(one, Date.now() - rec.at) : one;
|
|
379
|
+
if (shown) console.log(shown);
|
|
380
|
+
}
|
|
381
|
+
} catch { /* no session, offline, unreadable, past the deadline — all silence */ }
|
|
382
|
+
process.exitCode = 0;
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// --refresh-line: the detached child spawnRefresh starts. ttlMs 0 forces a fresh read; a
|
|
387
|
+
// sibling already refreshing holds the lock, and then this child simply stops. Prints nothing.
|
|
388
|
+
if (hasFlag('--refresh-line')) {
|
|
389
|
+
try {
|
|
294
390
|
const { cached } = require('./probe-cache.js');
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
} catch { /* no session, offline, unreadable — all silence */ }
|
|
391
|
+
await cached(LINE_CACHE_KEY, () => produceLine(opts, DEFAULT_TIMEOUT_MS * 2), { ttlMs: 0 });
|
|
392
|
+
} catch { /* the next session reads the ledger itself */ }
|
|
298
393
|
process.exitCode = 0;
|
|
299
394
|
return;
|
|
300
395
|
}
|
|
@@ -311,5 +406,6 @@ if (require.main === module) {
|
|
|
311
406
|
|
|
312
407
|
module.exports = {
|
|
313
408
|
check, line, report, classifyStrand, rankStrands, humanAge,
|
|
409
|
+
linePlan, labelStale, produceLine, LINE_FRESH_MS, LINE_STALE_MAX_MS, LINE_DEADLINE_MS,
|
|
314
410
|
CONFIRMED_STALE_MS, COMPLETED_STALE_MS, STRAND_REASON_PREFIX, WATCHED, PAGE_LIMIT,
|
|
315
411
|
};
|
|
@@ -87,10 +87,17 @@ function slotFilesIn(dir, { exists = (p) => fs.existsSync(p) } = {}) {
|
|
|
87
87
|
|
|
88
88
|
// ---- the browser half --------------------------------------------------------
|
|
89
89
|
|
|
90
|
+
// The inventory as Tweak Mode sees it, shared-chrome pages included (task 1004904):
|
|
91
|
+
// the committed file does not list them, so planning from it alone is unknown_page.
|
|
92
|
+
function derivedInventory() {
|
|
93
|
+
const r = require('../../modules/copy-desk/page-data.js').loadInventory();
|
|
94
|
+
return r.ok ? r.data : { pages: [] };
|
|
95
|
+
}
|
|
96
|
+
|
|
90
97
|
async function shoot({ pageId, phase, out }) {
|
|
91
98
|
const reader = require('./page-reader.js');
|
|
92
99
|
const kit = require('../../modules/ui-design/kit/lib');
|
|
93
|
-
const inventory =
|
|
100
|
+
const inventory = derivedInventory();
|
|
94
101
|
const planned = planForPage(pageId, { inventory, planRenders: reader.planRenders });
|
|
95
102
|
if (!planned.ok) { console.error(`tweak-renders: ${planned.code} (${pageId})`); return 2; }
|
|
96
103
|
const { page, plan } = planned;
|
|
@@ -172,7 +179,7 @@ async function main(args) {
|
|
|
172
179
|
if (!pageId) { console.error('usage: node scripts/bongos/tweak-renders.js shoot --page <id> --phase before|after --out <dir>'); return 2; }
|
|
173
180
|
if (cmd === 'plan') {
|
|
174
181
|
const reader = require('./page-reader.js');
|
|
175
|
-
const inventory =
|
|
182
|
+
const inventory = derivedInventory();
|
|
176
183
|
const planned = planForPage(pageId, { inventory, planRenders: reader.planRenders });
|
|
177
184
|
if (!planned.ok) { console.error(`tweak-renders: ${planned.code} (${pageId})`); return 2; }
|
|
178
185
|
for (const phase of taskVisuals.TWEAK_RENDER_PHASES) for (const s of shotsFor(phase, planned.plan)) console.log(`${s.slot}\t${s.width}px\t${s.renderMode}\t${planned.plan.url}`);
|
|
@@ -182,6 +182,10 @@ const MODULE_GLOBS = {
|
|
|
182
182
|
'page-notes': [
|
|
183
183
|
'modules/page-notes/',
|
|
184
184
|
],
|
|
185
|
+
// AI generation for artists (task 1004916, ADR 0392): keys, prepaid balance, generate.
|
|
186
|
+
'ai-studio': [
|
|
187
|
+
'modules/ai-studio/',
|
|
188
|
+
],
|
|
185
189
|
// The /raise investor page (task 1004847, ADR 0384): hub-only.
|
|
186
190
|
raise: [
|
|
187
191
|
'modules/raise/',
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.23.
|
|
84
|
+
const CORE_VERSION = '1.23.22'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|