@bongos/core 1.23.21 → 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 +117 -57
- 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 +2 -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/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 +66 -0
- package/scripts/bongos/publish-manifest.js +1 -0
- package/scripts/bongos/strand-watch.js +106 -10
- 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/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/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
|
@@ -434,40 +434,50 @@ module.exports = function buildGovernmentRouter() {
|
|
|
434
434
|
|
|
435
435
|
// rank: perm government.manage — add a RECOMMENDED RANK SET (task 1004400):
|
|
436
436
|
// the custom ranks recommendations.js names for a project type and size, each
|
|
437
|
-
// with its grants. It
|
|
438
|
-
// under the same checks: a rank that already exists is LEFT ALONE (never
|
|
437
|
+
// with its grants. It does what the create form and the grant toggle do, for
|
|
438
|
+
// every rank in the set, under the same checks: a rank that already exists is LEFT ALONE (never
|
|
439
439
|
// re-granted, never relabelled — an owner may have edited it), and every grant
|
|
440
440
|
// is re-checked with catalog.isGrantableTo, so a set can never write what the
|
|
441
441
|
// toggle would refuse. Re-applying is safe; the reply says what was added.
|
|
442
|
+
//
|
|
443
|
+
// task 1004929: the whole set is written by db.applyRankSet in ONE transaction
|
|
444
|
+
// and three statements, instead of a read + insert + one insert per grant for
|
|
445
|
+
// every rank. A rank this route creates is always a CUSTOM rank (is_system
|
|
446
|
+
// false, no seeded_rank — what createRank returns), so each grant is screened
|
|
447
|
+
// against that shape before the write, exactly as the toggle would screen it.
|
|
448
|
+
// A failure now writes nothing, so a 500 reports empty created/skipped lists.
|
|
442
449
|
router.post('/government/rank-sets/apply', api.requireBuilder, api.requirePermission('government.manage'), async (req, res) => {
|
|
443
450
|
if (validateOrRespond(req, res, {
|
|
444
451
|
type: { type: 'string', maxLength: 32 },
|
|
445
452
|
headcount: { required: true, type: 'integer', min: 0, max: 100000 },
|
|
446
453
|
})) return;
|
|
447
454
|
const set = recommendations.recommendedRankSet(req.body.type, req.body.headcount);
|
|
448
|
-
const
|
|
449
|
-
const skipped = [];
|
|
455
|
+
const customRank = { is_system: false, seeded_rank: null };
|
|
450
456
|
try {
|
|
457
|
+
const granted = await db.applyRankSet({
|
|
458
|
+
createdBy: req.builder.id,
|
|
459
|
+
ranks: set.ranks.map((r) => ({
|
|
460
|
+
rankKey: r.rank_key,
|
|
461
|
+
label: r.label,
|
|
462
|
+
description: r.description,
|
|
463
|
+
permissionKeys: r.permissions.filter((key) => catalog.byKey(key) && catalog.isGrantableTo(key, customRank)),
|
|
464
|
+
})),
|
|
465
|
+
});
|
|
466
|
+
const created = [];
|
|
467
|
+
const skipped = [];
|
|
451
468
|
for (const r of set.ranks) {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
throw err;
|
|
459
|
-
}
|
|
460
|
-
const granted = [];
|
|
461
|
-
for (const key of r.permissions) {
|
|
462
|
-
if (!catalog.byKey(key) || !catalog.isGrantableTo(key, rank)) continue;
|
|
463
|
-
if (await db.grantRankPermission({ rankKey: r.rank_key, permissionKey: key })) granted.push(key);
|
|
469
|
+
// delete-on-read: a key named twice in a set is created once, then skipped.
|
|
470
|
+
if (granted.has(r.rank_key)) {
|
|
471
|
+
created.push({ rank_key: r.rank_key, label: r.label, permissions: granted.get(r.rank_key) });
|
|
472
|
+
granted.delete(r.rank_key);
|
|
473
|
+
} else {
|
|
474
|
+
skipped.push({ rank_key: r.rank_key, label: r.label });
|
|
464
475
|
}
|
|
465
|
-
created.push({ rank_key: r.rank_key, label: r.label, permissions: granted });
|
|
466
476
|
}
|
|
467
477
|
res.json({ ok: true, type: set.type, size: set.size, created, skipped });
|
|
468
478
|
} catch (err) {
|
|
469
479
|
log.error('[gds] POST /government/rank-sets/apply', err);
|
|
470
|
-
res.fail('rank_set_apply_failed', { status: 500, message: 'internal error', details: { created, skipped } });
|
|
480
|
+
res.fail('rank_set_apply_failed', { status: 500, message: 'internal error', details: { created: [], skipped: [] } });
|
|
471
481
|
}
|
|
472
482
|
});
|
|
473
483
|
|
|
@@ -183,18 +183,23 @@ function runSubagent({ prompt, opts = {} }) {
|
|
|
183
183
|
// agentic Read turns re-opening the same changed files. Read them ONCE here
|
|
184
184
|
// and hand every worker the post-change contents inline (fenced, untrusted),
|
|
185
185
|
// so a worker's Read tool is only needed for files beyond the bundle.
|
|
186
|
+
//
|
|
187
|
+
// task 1004932: the bundle rides in EVERY worker prompt, so its total is paid
|
|
188
|
+
// once per worker; 120k chars made it the largest fixed input of a grade.
|
|
186
189
|
const CONTEXT_BUNDLE_MAX_FILES = 24;
|
|
187
|
-
const CONTEXT_BUNDLE_MAX_TOTAL_CHARS =
|
|
190
|
+
const CONTEXT_BUNDLE_MAX_TOTAL_CHARS = 60_000;
|
|
188
191
|
const CONTEXT_BUNDLE_MAX_FILE_CHARS = 16_000;
|
|
189
192
|
// Binary-ish extensions never worth inlining (the diff already names them).
|
|
190
193
|
const CONTEXT_BUNDLE_SKIP_RE = /\.(?:png|jpe?g|gif|webp|ico|svgz|woff2?|ttf|eot|zip|gz|tgz|tar|pdf|mp[34]|wav|ogg|bin|exe|dll|node)$/i;
|
|
191
194
|
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
+
// One pass over a unified diff, per file: how many lines it adds (a relevance
|
|
196
|
+
// weight), and the added lines long enough to be a meaningful fingerprint (a bare
|
|
197
|
+
// `}` or `});` matches anywhere) — used to prove a truncated bundle prefix
|
|
198
|
+
// actually SHOWS the change (task 1002905).
|
|
195
199
|
const BUNDLE_EVIDENCE_MIN_CHARS = 12;
|
|
196
|
-
function
|
|
197
|
-
const
|
|
200
|
+
function scanDiff(diff) {
|
|
201
|
+
const evidence = new Map();
|
|
202
|
+
const added = new Map();
|
|
198
203
|
let cur = null;
|
|
199
204
|
let prevWasOldHeader = false;
|
|
200
205
|
for (const line of String(diff || '').split('\n')) {
|
|
@@ -209,12 +214,26 @@ function diffAddedLinesByFile(diff) {
|
|
|
209
214
|
prevWasOldHeader = line.startsWith('--- ');
|
|
210
215
|
if (prevWasOldHeader || line.startsWith('diff --git')) continue;
|
|
211
216
|
if (!cur || !line.startsWith('+')) continue;
|
|
217
|
+
added.set(cur, (added.get(cur) || 0) + 1);
|
|
212
218
|
const t = line.slice(1).trim();
|
|
213
219
|
if (t.length < BUNDLE_EVIDENCE_MIN_CHARS) continue;
|
|
214
|
-
if (!
|
|
215
|
-
|
|
220
|
+
if (!evidence.has(cur)) evidence.set(cur, []);
|
|
221
|
+
evidence.get(cur).push(t);
|
|
216
222
|
}
|
|
217
|
-
return
|
|
223
|
+
return { evidence, added };
|
|
224
|
+
}
|
|
225
|
+
function diffAddedLinesByFile(diff) { return scanDiff(diff).evidence; }
|
|
226
|
+
|
|
227
|
+
// The order the bundle is filled in (task 1004932): the files the diff adds the
|
|
228
|
+
// most lines to first, so when the total cap bites it is the lightly touched
|
|
229
|
+
// files (a one-line doc pointer, a generated index) that fall to skipped[] for
|
|
230
|
+
// the worker to Read, not the heart of the change. Stable: ties, and every file
|
|
231
|
+
// when there is no diff, keep the caller's order.
|
|
232
|
+
function orderByChange(changedFiles, added) {
|
|
233
|
+
return changedFiles
|
|
234
|
+
.map((file, i) => ({ file, i, n: added.get(file) || 0 }))
|
|
235
|
+
.sort((a, b) => (b.n - a.n) || (a.i - b.i))
|
|
236
|
+
.map((x) => x.file);
|
|
218
237
|
}
|
|
219
238
|
|
|
220
239
|
// Pure-ish (fs reads only, injectable): returns { files: [{file, content,
|
|
@@ -241,8 +260,8 @@ function buildContextBundle({ changedFiles, cwd, diff, fsImpl = fs } = {}) {
|
|
|
241
260
|
// exactly the pre-change state). A prefix that contains none of the change is
|
|
242
261
|
// worse than no bundle at all, so refuse it: skipped[] already tells the worker
|
|
243
262
|
// to Read the file.
|
|
244
|
-
const addedByFile =
|
|
245
|
-
for (const rel of (Array.isArray(changedFiles) ? changedFiles : [])) {
|
|
263
|
+
const { evidence: addedByFile, added } = scanDiff(diff);
|
|
264
|
+
for (const rel of orderByChange(Array.isArray(changedFiles) ? changedFiles : [], added)) {
|
|
246
265
|
if (files.length >= CONTEXT_BUNDLE_MAX_FILES || total >= CONTEXT_BUNDLE_MAX_TOTAL_CHARS) {
|
|
247
266
|
skipped.push(rel);
|
|
248
267
|
continue;
|
|
@@ -106,24 +106,24 @@
|
|
|
106
106
|
</div>
|
|
107
107
|
|
|
108
108
|
<script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
|
|
109
|
-
<script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
|
|
110
|
-
<script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
|
|
111
|
-
<script src="/builders/shell.js?v=2026-10-25-escape"></script>
|
|
109
|
+
<script src="/builders/shell-trail.js?v=2026-10-08-city-tab" defer></script>
|
|
110
|
+
<script src="/builders/craft-mode.js?v=2026-10-09-living3" defer></script>
|
|
111
|
+
<script src="/builders/shell.js?v=2026-10-25-escape" defer></script>
|
|
112
112
|
<script src="/builders/palette.js" defer></script>
|
|
113
|
-
<script src="/builders/gov-living.js?v=2026-10-09-positions1"></script>
|
|
114
|
-
<script src="/builders/gov-feeds.js?v=2026-10-09-living3"></script>
|
|
115
|
-
<script src="/builders/city-draw.js?v=2026-10-17-tidy"></script>
|
|
113
|
+
<script src="/builders/gov-living.js?v=2026-10-09-positions1" defer></script>
|
|
114
|
+
<script src="/builders/gov-feeds.js?v=2026-10-09-living3" defer></script>
|
|
115
|
+
<script src="/builders/city-draw.js?v=2026-10-17-tidy" defer></script>
|
|
116
116
|
<!-- a building's card answers its first ask in place through the deck's own
|
|
117
117
|
route (task 1004702): the desk rows (street-desk.js over the street's kit)
|
|
118
118
|
and the deck's card and sender (street-deck.js) -->
|
|
119
|
-
<script src="/builders/hall-kit.js?v=2026-10-23-decide12"></script>
|
|
120
|
-
<script src="/builders/street-draw.js?v=2026-10-09-living3"></script>
|
|
121
|
-
<script src="/builders/street-card.js?v=2026-10-04-record1"></script>
|
|
122
|
-
<script src="/builders/street-inside.js?v=2026-10-09-living3"></script>
|
|
123
|
-
<script src="/builders/street-desk.js?v=2026-10-04-record1"></script>
|
|
124
|
-
<script src="/builders/street-deck.js?v=2026-10-13-glass1"></script>
|
|
125
|
-
<script src="/builders/city-decide.js?v=2026-10-23-decide12"></script>
|
|
126
|
-
<script src="/builders/city-switchboard.js?v=2026-10-23-board5"></script>
|
|
127
|
-
<script src="/builders/city.js?v=2026-10-23-decide12"></script>
|
|
119
|
+
<script src="/builders/hall-kit.js?v=2026-10-23-decide12" defer></script>
|
|
120
|
+
<script src="/builders/street-draw.js?v=2026-10-09-living3" defer></script>
|
|
121
|
+
<script src="/builders/street-card.js?v=2026-10-04-record1" defer></script>
|
|
122
|
+
<script src="/builders/street-inside.js?v=2026-10-09-living3" defer></script>
|
|
123
|
+
<script src="/builders/street-desk.js?v=2026-10-04-record1" defer></script>
|
|
124
|
+
<script src="/builders/street-deck.js?v=2026-10-13-glass1" defer></script>
|
|
125
|
+
<script src="/builders/city-decide.js?v=2026-10-23-decide12" defer></script>
|
|
126
|
+
<script src="/builders/city-switchboard.js?v=2026-10-23-board5" defer></script>
|
|
127
|
+
<script src="/builders/city.js?v=2026-10-23-decide12" defer></script>
|
|
128
128
|
</body>
|
|
129
129
|
</html>
|
|
@@ -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
|
@@ -10515,5 +10515,71 @@
|
|
|
10515
10515
|
"id": "1004904",
|
|
10516
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
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
|
+
}
|
|
10518
10584
|
]
|
|
10519
10585
|
}
|
|
@@ -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) ---
|