@bongos/core 1.21.73 → 1.21.75
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 +59 -34
- package/bin/bongos.js +1 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +6 -0
- package/clients/bongos-client/index.cjs +6 -0
- package/clients/bongos-client/index.d.ts +9 -0
- package/clients/bongos-client/index.mjs +6 -0
- package/docs/adr/0343-a-module-score-is-a-security-gate-then-an-average-of-visible-parts.md +12 -0
- package/docs/adr/0369-ship-mode-decides-who-starts-a-ship-not-how-it-lands.md +44 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +170 -3
- package/docs/api-reference.md +6 -3
- package/docs/copy-inventory.md +18 -14
- package/docs/copy-registry.json +72 -26
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +1 -0
- package/docs/page-readings.json +8 -8
- package/modules/hall-ui/public/project-settings.css +15 -0
- package/modules/hall-ui/public/project-settings.html +4 -4
- package/modules/hall-ui/public/project-settings.js +42 -0
- package/modules/lifecycle/routes/project-settings.js +65 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/shipping-mode.js +71 -0
- package/src/bongos/module-listing.js +85 -0
- package/src/bongos/project-settings.js +67 -0
- package/src/bongos/routes/modules.js +15 -0
- package/src/module-api.js +7 -1
- package/tests/module_store_listing.mjs +156 -0
- package/tests/project_settings_page.mjs +8 -5
- package/tests/ship_mode.mjs +171 -0
package/docs/modules-contract.md
CHANGED
|
@@ -362,6 +362,7 @@ Run `bongos upgrade` to confirm coreVersion compatibility, then `node scripts/gd
|
|
|
362
362
|
- The publish denylist applies (ADR 0098), plus a refusal of credential-named files (`.env*`, `*.pem`, `*.key`, `id_rsa`, …) anywhere in the module. The store re-checks every hash and the denylist itself.
|
|
363
363
|
- Gate: Metic+ (`module.submit`) for now. This is not `bongos module submit`, which proposes a module *into* core.
|
|
364
364
|
- **A how-to is required** ([ADR 0347](adr/0347-every-store-module-ships-a-how-to.md)). `modules/<key>/HOWTO.md` is what a buyer reads — the store shows it as the module's page — so it is written for the person who installs the module, not for an AI working inside it (that is `CLAUDE.md`). It is plain Markdown: any AI or person can write it. Publish refuses the version unless it has these five level-2 headings, in any order and any case, each with some text under it: `## What it does`, `## Install and enable`, `## How to use it` (with one worked example), `## Configuration` ("None." is fine) and `## Limits and known issues` ("None known." is fine). HTML comments don't count as text, so the scaffold's prompts must be replaced. The refusal names each failing section. The check is one function, `checkHowto` in `scripts/gds/module-artifact.js`; the CLI runs it before uploading and the store runs it again on upload, so a hand-built upload can't skip it. `bongos module check` reports it as advice only — an upstream submit is not gated by it. Install and update don't re-check it, so a version published before the gate still installs. The file travels in the tarball, so its hash pins it to the version. An optional `howto.artifactUrl` in `module.json` (an `https://` link on `claude.ai`) is shown as an extra, never graded.
|
|
365
|
+
- **Getting listed** (task 1003807, [ADR 0343](adr/0343-a-module-score-is-a-security-gate-then-an-average-of-visible-parts.md)). Publishing a version does not by itself put it in the store's catalog, `GET /store/modules`. The catalog shows each module's newest version, judged by its current score. It must pass the Security gate first; a version that failed it, or has not been checked yet, is not listed. A new version with no install or reliability data yet is listed with a **New** label and is not ranked; the floor does not apply to it until it has a real score. Every other version is listed only if its overall score is **at least 50 out of 100**. Below that it is not listed at all. The floor is fixed in Bongos itself (`QUALITY_FLOOR` in `src/bongos/module-listing.js`) and returned in the catalog response; no author or instance setting moves it, and changing it is an owner-approved, reviewed task.
|
|
365
366
|
|
|
366
367
|
### 7. Install from the store
|
|
367
368
|
|
package/docs/page-readings.json
CHANGED
|
@@ -503,7 +503,7 @@
|
|
|
503
503
|
"surface": "builders",
|
|
504
504
|
"title": "Deploy",
|
|
505
505
|
"states_read": ["work","out"],
|
|
506
|
-
"files_hash": "
|
|
506
|
+
"files_hash": "535c116eea07d320",
|
|
507
507
|
"reading_hash": "b9fc5f83cc76b257",
|
|
508
508
|
"counts": {"lines":21,"placed":5,"shared":9,"unplaced":7},
|
|
509
509
|
"omitted": {"data":1},
|
|
@@ -1035,7 +1035,7 @@
|
|
|
1035
1035
|
"surface": "builders",
|
|
1036
1036
|
"title": "Fleet costs",
|
|
1037
1037
|
"states_read": ["ledger","out"],
|
|
1038
|
-
"files_hash": "
|
|
1038
|
+
"files_hash": "153446369d287bc8",
|
|
1039
1039
|
"reading_hash": "db04003c871f8317",
|
|
1040
1040
|
"counts": {"lines":36,"placed":11,"shared":9,"unplaced":16},
|
|
1041
1041
|
"omitted": {"data":11},
|
|
@@ -2586,8 +2586,8 @@
|
|
|
2586
2586
|
"surface": "builders",
|
|
2587
2587
|
"title": "Project settings",
|
|
2588
2588
|
"states_read": ["default"],
|
|
2589
|
-
"files_hash": "
|
|
2590
|
-
"reading_hash": "
|
|
2589
|
+
"files_hash": "525a8712c618456d",
|
|
2590
|
+
"reading_hash": "8bbf4ff80ec8097c",
|
|
2591
2591
|
"counts": {"lines":31,"placed":17,"shared":9,"unplaced":5},
|
|
2592
2592
|
"omitted": {"data":1},
|
|
2593
2593
|
"notes": [],
|
|
@@ -2601,13 +2601,13 @@
|
|
|
2601
2601
|
{"key":"L0007","section":"top bar","text":"Ideating","placement":"shared"},
|
|
2602
2602
|
{"key":"L0008","section":"top bar","text":"Governing","placement":"shared"},
|
|
2603
2603
|
{"key":"L0009","section":"Project settings","text":"Project settings","placement":"placed","file":"modules/hall-ui/public/project-settings.html","line":15,"string_id":"e08628967f2f"},
|
|
2604
|
-
{"key":"L0010","section":"Project settings","text":"Could not load","placement":"placed","file":"modules/hall-ui/public/project-settings.js","line":
|
|
2604
|
+
{"key":"L0010","section":"Project settings","text":"Could not load","placement":"placed","file":"modules/hall-ui/public/project-settings.js","line":284,"string_id":"90691bceaf68"},
|
|
2605
2605
|
{"key":"L0011","section":"The fundamentals","text":"The fundamentals","placement":"placed","file":"modules/hall-ui/public/project-settings.html","line":40,"string_id":"cb7c7879eeba"},
|
|
2606
|
-
{"key":"L0012","section":"The fundamentals","text":"
|
|
2606
|
+
{"key":"L0012","section":"The fundamentals","text":"Six things every project settles. Three of them are knobs you can turn here; the other three are decided in committed configuration, and each says which file decides it.","placement":"placed","file":"modules/hall-ui/public/project-settings.html","line":41,"string_id":"ce994c34804e"},
|
|
2607
2607
|
{"key":"L0013","section":"The fundamentals","text":"{…} project{…}","placement":"placed","file":"modules/hall-ui/public/fleet.js","line":39,"string_id":"771bef3d31b5"},
|
|
2608
2608
|
{"key":"L0014","section":"Danger zone","text":"Danger zone","placement":"placed","file":"modules/hall-ui/public/project-settings.html","line":57,"string_id":"13b22e48d9fb"},
|
|
2609
|
-
{"key":"L0015","section":"Danger zone","text":"This project is hosted on {…}, and that is where it is taken offline or removed. Both links below open this project’s page there.","placement":"placed","file":"modules/hall-ui/public/project-settings.js","line":
|
|
2610
|
-
{"key":"L0016","section":"Danger zone","text":"Manage this project on {…}","placement":"placed","file":"modules/hall-ui/public/project-settings.js","line":
|
|
2609
|
+
{"key":"L0015","section":"Danger zone","text":"This project is hosted on {…}, and that is where it is taken offline or removed. Both links below open this project’s page there.","placement":"placed","file":"modules/hall-ui/public/project-settings.js","line":301,"string_id":"e97b2d549f10"},
|
|
2610
|
+
{"key":"L0016","section":"Danger zone","text":"Manage this project on {…}","placement":"placed","file":"modules/hall-ui/public/project-settings.js","line":302,"string_id":"4e261fd15bd5"},
|
|
2611
2611
|
{"key":"L0017","section":"Danger zone","text":"Take it offline","placement":"placed","file":"modules/hall-ui/public/project-settings.html","line":60,"string_id":"da9da6e866bd"},
|
|
2612
2612
|
{"key":"L0018","section":"Danger zone","text":"— stops the project and its nightly backups but keeps everything: its database, its backups and your repository on GitHub. You can bring it back.","placement":"placed","file":"modules/hall-ui/public/project-settings.html","line":60,"string_id":"61ebc5d684fd"},
|
|
2613
2613
|
{"key":"L0019","section":"Danger zone","text":"Disconnect from Bongos entirely","placement":"placed","file":"modules/hall-ui/public/project-settings.html","line":61,"string_id":"d3efd513694b"},
|
|
@@ -219,3 +219,18 @@
|
|
|
219
219
|
one sentence saying what it does. Plain list, the lede's measure. */
|
|
220
220
|
.ps-danger__list { margin: 10px 0 0; padding-left: 18px; line-height: 1.6; max-width: var(--measure); }
|
|
221
221
|
.ps-danger__list li + li { margin-top: 8px; }
|
|
222
|
+
|
|
223
|
+
/* Ship mode (task 1004628): two radio choices, each a full-width row whose whole
|
|
224
|
+
line is the hit target, so the 24px floor holds on a phone without a bigger dot. */
|
|
225
|
+
.ps-ship { margin: 10px 0 0; padding: 0; border: 0; }
|
|
226
|
+
.ps-ship__legend { padding: 0; font-size: 12px; color: var(--ink-soft); }
|
|
227
|
+
.ps-ship__choice {
|
|
228
|
+
display: flex;
|
|
229
|
+
align-items: flex-start;
|
|
230
|
+
gap: 8px;
|
|
231
|
+
min-height: 24px;
|
|
232
|
+
padding: 6px 0;
|
|
233
|
+
line-height: 1.5;
|
|
234
|
+
cursor: pointer;
|
|
235
|
+
}
|
|
236
|
+
.ps-ship__choice input { margin-top: 4px; flex: 0 0 auto; }
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>Project settings — {{worldName}}</title>
|
|
7
|
-
<meta name="description" content="The fundamentals of this project in one place — the stale timer, the category set, how credits are calculated, whether equity is real, and what share of upmarket revenue is paid on credits.">
|
|
7
|
+
<meta name="description" content="The fundamentals of this project in one place — how tasks ship, the stale timer, the category set, how credits are calculated, whether equity is real, and what share of upmarket revenue is paid on credits.">
|
|
8
8
|
<link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
|
|
9
9
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
10
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
@@ -39,11 +39,11 @@
|
|
|
39
39
|
<section class="scroll" id="ps-body" aria-labelledby="ps-body-h" hidden>
|
|
40
40
|
<h2 class="scroll__h" id="ps-body-h">The fundamentals</h2>
|
|
41
41
|
<p class="scroll__lede">
|
|
42
|
-
|
|
42
|
+
Six things every project settles. Three of them are knobs you can turn here;
|
|
43
43
|
the other three are decided in committed configuration, and each says which
|
|
44
44
|
file decides it.
|
|
45
45
|
</p>
|
|
46
|
-
<!-- One mount,
|
|
46
|
+
<!-- One mount, six rows, written by project-settings.js. The rows are NOT
|
|
47
47
|
authored in this file: each carries a value plus the provenance the
|
|
48
48
|
server resolved it through, and markup that could drift from that is the
|
|
49
49
|
thing this page exists to end. -->
|
|
@@ -77,6 +77,6 @@
|
|
|
77
77
|
<script src="/builders/palette.js" defer></script>
|
|
78
78
|
<script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
|
|
79
79
|
<script src="/builders/hall-kit.js"></script>
|
|
80
|
-
<script src="/builders/project-settings.js?v=2026-
|
|
80
|
+
<script src="/builders/project-settings.js?v=2026-10-08-ship-mode"></script>
|
|
81
81
|
</body>
|
|
82
82
|
</html>
|
|
@@ -122,6 +122,30 @@
|
|
|
122
122
|
${sib}`;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
// Ship mode (task 1004628): two choices, each with the sentence the SERVER wrote,
|
|
126
|
+
// so the page never re-types what Auto or Manual means.
|
|
127
|
+
function shipBodyHtml(row) {
|
|
128
|
+
const v = row.value || {};
|
|
129
|
+
const choices = Array.isArray(row.choices) ? row.choices : [];
|
|
130
|
+
const invalid = row.invalid
|
|
131
|
+
? `<p class="ps-invalid">A configured value could not be used: <code>${escapeHtml(String(row.invalid.raw))}</code>
|
|
132
|
+
— ${escapeHtml(String(row.invalid.reason))}. The mode above is the fallback that is actually in force.</p>`
|
|
133
|
+
: '';
|
|
134
|
+
return `
|
|
135
|
+
${invalid}
|
|
136
|
+
<fieldset class="ps-ship">
|
|
137
|
+
<legend class="ps-ship__legend">How tasks ship</legend>
|
|
138
|
+
${choices.map((c) => `
|
|
139
|
+
<label class="ps-ship__choice">
|
|
140
|
+
<input type="radio" name="ps-ship-mode" value="${escapeHtml(String(c.mode))}" ${c.mode === v.mode ? 'checked' : ''}>
|
|
141
|
+
<span><strong>${escapeHtml(String(c.label))}</strong> — ${escapeHtml(String(c.sentence))}</span>
|
|
142
|
+
</label>`).join('')}
|
|
143
|
+
</fieldset>
|
|
144
|
+
<div class="ps-controls">
|
|
145
|
+
<button type="button" class="btn" id="ps-ship-save">Save</button>
|
|
146
|
+
</div>`;
|
|
147
|
+
}
|
|
148
|
+
|
|
125
149
|
function catsBodyHtml(row) {
|
|
126
150
|
const v = row.value || {};
|
|
127
151
|
const cats = Array.isArray(v.categories) ? v.categories : [];
|
|
@@ -150,6 +174,7 @@
|
|
|
150
174
|
function render() {
|
|
151
175
|
const mount = $('ps-rows');
|
|
152
176
|
mount.innerHTML = `<ul class="ov-rows">${rows.map((row) => {
|
|
177
|
+
if (row.key === 'ship_mode' && row.editable) return rowHtml(row, shipBodyHtml(row));
|
|
153
178
|
if (row.key === 'rot_timer' && row.editable) return rowHtml(row, rotBodyHtml(row));
|
|
154
179
|
if (row.key === 'work_categories' && row.editable) return rowHtml(row, catsBodyHtml(row));
|
|
155
180
|
return rowHtml(row, '');
|
|
@@ -166,6 +191,23 @@
|
|
|
166
191
|
}
|
|
167
192
|
|
|
168
193
|
function wire() {
|
|
194
|
+
const shipSave = $('ps-ship-save');
|
|
195
|
+
if (shipSave) {
|
|
196
|
+
shipSave.addEventListener('click', async () => {
|
|
197
|
+
const picked = document.querySelector('input[name="ps-ship-mode"]:checked');
|
|
198
|
+
if (!picked) { toast('Pick Auto or Manual.'); return; }
|
|
199
|
+
setBusy(true);
|
|
200
|
+
try {
|
|
201
|
+
const out = await sendJson('PATCH', '/project-settings/ship-mode', { mode: picked.value });
|
|
202
|
+
toast(out.mode === 'manual' ? 'Ship mode is now Manual.' : 'Ship mode is now Auto.');
|
|
203
|
+
await load();
|
|
204
|
+
} catch (err) {
|
|
205
|
+
toast(err.message || 'Could not save the ship mode.');
|
|
206
|
+
setBusy(false);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
169
211
|
const rotSave = $('ps-rot-save');
|
|
170
212
|
if (rotSave) {
|
|
171
213
|
rotSave.addEventListener('click', async () => {
|
|
@@ -34,6 +34,29 @@ const goalCategory = require('../goal-category');
|
|
|
34
34
|
|
|
35
35
|
const log = api.logger('lifecycle');
|
|
36
36
|
|
|
37
|
+
// --- ship mode: who starts a ship (task 1004628, ADR 0369) -------------------
|
|
38
|
+
//
|
|
39
|
+
// One row shape, two readers: the settings page (inside GET /project-settings) and
|
|
40
|
+
// any builder's CLI or AI session (GET /project-settings/ship-mode), so the sentence
|
|
41
|
+
// a person reads and the one an agent obeys are the same words.
|
|
42
|
+
function shipModeRow(resolved) {
|
|
43
|
+
return {
|
|
44
|
+
key: 'ship_mode',
|
|
45
|
+
title: 'Ship mode',
|
|
46
|
+
editable: true,
|
|
47
|
+
state: 'live',
|
|
48
|
+
value: { mode: resolved.mode, chosen: resolved.chosen, modes: api.projectSettings.SHIP_MODES },
|
|
49
|
+
sentence: api.projectSettings.describeShipMode(resolved),
|
|
50
|
+
choices: [
|
|
51
|
+
{ mode: 'auto', label: 'Auto (recommended)', sentence: 'When the AI finishes a task it ships and lands it. Nobody has to type a command.' },
|
|
52
|
+
{ mode: 'manual', label: 'Manual', sentence: 'The AI stops before each ship and asks a person to approve it.' },
|
|
53
|
+
],
|
|
54
|
+
source: resolved.source,
|
|
55
|
+
invalid: resolved.invalid,
|
|
56
|
+
reads: { file: 'project_settings[\'ship.mode\'] (migration core_231)', adr: 'ADR 0369' },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
37
60
|
// --- the three rows that are READ, never written -----------------------------
|
|
38
61
|
//
|
|
39
62
|
// Each names the committed artefact it resolves from, because "you cannot edit this"
|
|
@@ -214,12 +237,14 @@ module.exports = function buildProjectSettingsRouter() {
|
|
|
214
237
|
// Inactive included: the set's SHAPE is what this page edits, and a deactivated
|
|
215
238
|
// category the owner cannot see is one they cannot bring back.
|
|
216
239
|
const categories = await goalCategory.listCategories({ includeInactive: true });
|
|
240
|
+
const shipMode = await api.projectSettings.resolveShipMode();
|
|
217
241
|
|
|
218
242
|
// A settings read is a configuration comparison — a cached one is wrong the
|
|
219
243
|
// moment somebody turns a knob (the rot.js precedent).
|
|
220
244
|
res.set('Cache-Control', 'no-store');
|
|
221
245
|
res.json({
|
|
222
246
|
rows: [
|
|
247
|
+
shipModeRow(shipMode),
|
|
223
248
|
{
|
|
224
249
|
key: 'rot_timer',
|
|
225
250
|
title: 'Stale timer',
|
|
@@ -316,7 +341,47 @@ module.exports = function buildProjectSettingsRouter() {
|
|
|
316
341
|
}
|
|
317
342
|
});
|
|
318
343
|
|
|
344
|
+
// GET /project-settings/ship-mode — the ship mode alone, for the CLI and the AI.
|
|
345
|
+
//
|
|
346
|
+
// rank: any signed-in builder. Unlike the full settings read this carries no
|
|
347
|
+
// reward internals or file paths an agent could not already read, and every
|
|
348
|
+
// builder's AI session must know whether to ship on its own or stop and ask —
|
|
349
|
+
// gating it Metic+ would leave a newcomer's session guessing.
|
|
350
|
+
router.get('/project-settings/ship-mode', api.requireBuilder, async (req, res) => {
|
|
351
|
+
try {
|
|
352
|
+
const resolved = await api.projectSettings.resolveShipMode();
|
|
353
|
+
res.set('Cache-Control', 'no-store');
|
|
354
|
+
const row = shipModeRow(resolved);
|
|
355
|
+
res.json({ mode: resolved.mode, chosen: resolved.chosen, source: resolved.source, sentence: row.sentence, choices: row.choices });
|
|
356
|
+
} catch (err) {
|
|
357
|
+
log.error(`GET /project-settings/ship-mode: ${err && err.message}`);
|
|
358
|
+
res.fail('project_settings_read_failed', { status: 500, message: 'internal error' });
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// PATCH /project-settings/ship-mode — body { mode: 'auto' | 'manual' }.
|
|
363
|
+
//
|
|
364
|
+
// rank: project.settings.manage (metic+, government_010) — the same atom as the
|
|
365
|
+
// other writes on this page. Validated with the resolver's own parser, so the
|
|
366
|
+
// page, the CLI and the reader can never disagree about what a mode is.
|
|
367
|
+
router.patch('/project-settings/ship-mode', api.requireBuilder, api.requirePermission('project.settings.manage'), async (req, res) => {
|
|
368
|
+
if (api.validateOrRespond(req, res, { mode: { required: true, type: 'string' } })) return;
|
|
369
|
+
const parsed = api.projectSettings.parseShipMode(req.body.mode);
|
|
370
|
+
if (!parsed.mode) {
|
|
371
|
+
return res.fail('bad_ship_mode', { status: 400, message: `mode: ${parsed.reason}` });
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
await api.projectSettings.set(api.projectSettings.KEYS.SHIP_MODE, parsed.mode, req.builder.id);
|
|
375
|
+
const resolved = await api.projectSettings.resolveShipMode();
|
|
376
|
+
res.json({ ok: true, mode: resolved.mode, chosen: resolved.chosen, source: resolved.source, sentence: api.projectSettings.describeShipMode(resolved) });
|
|
377
|
+
} catch (err) {
|
|
378
|
+
log.error(`PATCH /project-settings/ship-mode: ${err && err.message}`);
|
|
379
|
+
res.fail('project_settings_write_failed', { status: 500, message: 'internal error' });
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
319
383
|
return router;
|
|
320
384
|
};
|
|
321
385
|
|
|
322
386
|
module.exports.settingsDeskRows = settingsDeskRows;
|
|
387
|
+
module.exports.shipModeRow = shipModeRow;
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.75",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.75",
|
|
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.21.
|
|
3
|
+
"version": "1.21.75",
|
|
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
|
@@ -9248,5 +9248,17 @@
|
|
|
9248
9248
|
"id": "1004625",
|
|
9249
9249
|
"text": "The governor city now has its own tab, and in Governing mode it leads the sidebar with every other governing page tucked into one dropdown under it."
|
|
9250
9250
|
}
|
|
9251
|
+
],
|
|
9252
|
+
"1.21.74": [
|
|
9253
|
+
{
|
|
9254
|
+
"id": "1004628",
|
|
9255
|
+
"text": "Projects now have a Ship mode setting. Auto, the default, means the AI ships finished work on its own. Manual means it asks first. Owners can switch it on the project settings page."
|
|
9256
|
+
}
|
|
9257
|
+
],
|
|
9258
|
+
"1.21.75": [
|
|
9259
|
+
{
|
|
9260
|
+
"id": "1003807",
|
|
9261
|
+
"text": "The module store now only shows modules that are safe and good enough: each must pass the security check, and once it has real-world data it needs a score of at least 50 out of 100. Brand-new modules show with a 'New' label un"
|
|
9262
|
+
}
|
|
9251
9263
|
]
|
|
9252
9264
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/gds/shipping-mode.js — read or set the project's ship mode (task 1004628, ADR 0369).
|
|
3
|
+
//
|
|
4
|
+
// WHY. Ship mode says WHO starts a ship: in `auto` the AI finishes a task and ships
|
|
5
|
+
// it with no commands from the person; in `manual` it stops and asks first. An AI
|
|
6
|
+
// session has to know which before it ships, and a person has to be able to change
|
|
7
|
+
// it without opening the settings page — so the mode is one command away.
|
|
8
|
+
//
|
|
9
|
+
// Usage:
|
|
10
|
+
// node scripts/gds/shipping-mode.js print the mode (any builder)
|
|
11
|
+
// node scripts/gds/shipping-mode.js auto|manual set it (needs project.settings.manage)
|
|
12
|
+
// node scripts/gds/shipping-mode.js --json print the mode as JSON
|
|
13
|
+
//
|
|
14
|
+
// Also `bongos ship-mode [auto|manual]`.
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const { apiCall, requireSession, errCode, cliExit } = require('./cli-lib');
|
|
19
|
+
|
|
20
|
+
const USAGE = 'usage: node scripts/gds/shipping-mode.js [auto|manual] [--json]';
|
|
21
|
+
|
|
22
|
+
// What a person sees. One sentence from the server, plus how to change it.
|
|
23
|
+
function describe(data) {
|
|
24
|
+
const lines = [`Ship mode: ${data.mode}${data.chosen ? '' : ' (default — not chosen yet)'}`];
|
|
25
|
+
if (data.sentence) lines.push(data.sentence);
|
|
26
|
+
lines.push(data.mode === 'manual'
|
|
27
|
+
? 'Change it: node scripts/gds/shipping-mode.js auto'
|
|
28
|
+
: 'Change it: node scripts/gds/shipping-mode.js manual');
|
|
29
|
+
return lines.join('\n');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function main(argv) {
|
|
33
|
+
const args = argv.filter((a) => a !== '--json');
|
|
34
|
+
const json = argv.includes('--json');
|
|
35
|
+
if (args.includes('--help') || args.includes('-h')) { console.log(USAGE); return 0; }
|
|
36
|
+
if (args.length > 1) { console.error(USAGE); return 2; }
|
|
37
|
+
|
|
38
|
+
await requireSession();
|
|
39
|
+
|
|
40
|
+
if (args.length === 0) {
|
|
41
|
+
const r = await apiCall('GET', '/api/bongos/project-settings/ship-mode');
|
|
42
|
+
if (!r.ok) {
|
|
43
|
+
console.error(`Could not read the ship mode (${r.status}${errCode(r.data) ? `: ${errCode(r.data)}` : ''}).`);
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
console.log(json ? JSON.stringify(r.data) : describe(r.data));
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const mode = String(args[0]).trim().toLowerCase();
|
|
51
|
+
if (mode !== 'auto' && mode !== 'manual') { console.error(USAGE); return 2; }
|
|
52
|
+
const r = await apiCall('PATCH', '/api/bongos/project-settings/ship-mode', { mode });
|
|
53
|
+
if (!r.ok) {
|
|
54
|
+
if (r.status === 403) {
|
|
55
|
+
console.error('Only the project owner (or someone with project settings access) can change the ship mode.');
|
|
56
|
+
} else {
|
|
57
|
+
console.error(`Could not set the ship mode (${r.status}${errCode(r.data) ? `: ${errCode(r.data)}` : ''}).`);
|
|
58
|
+
}
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
console.log(json ? JSON.stringify(r.data) : `Saved. ${describe(r.data)}`);
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (require.main === module) {
|
|
66
|
+
main(process.argv.slice(2))
|
|
67
|
+
.then((code) => cliExit(code))
|
|
68
|
+
.catch((err) => { console.error('fatal:', err && err.message ? err.message : err); cliExit(1); });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { describe, main };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// src/bongos/module-listing.js — which store modules the catalog lists, under the
|
|
2
|
+
// quality floor (task 1003807, criterion wa5-curated-listing, ADR 0343 D2/D5).
|
|
3
|
+
//
|
|
4
|
+
// One module appears once, as its NEWEST published version (semver order), judged by
|
|
5
|
+
// that version's CURRENT score: the newest module_assessment_scores row for it
|
|
6
|
+
// (computed_at DESC, id DESC), whether computed or a Metic+ override (core_265).
|
|
7
|
+
// placeVersion is the whole rule and is pure, so tests drive it directly:
|
|
8
|
+
//
|
|
9
|
+
// excluded the Security gate has not passed (false, or not run yet), or the
|
|
10
|
+
// version has no score row at all. The gate is a hard exclusion (D2).
|
|
11
|
+
// new passed the gate but has no real-world data yet (is_new, D5). Listed
|
|
12
|
+
// with a "New" label and never ranked: the floor does not apply until
|
|
13
|
+
// it has a real score (owner decision, 2026-10-01).
|
|
14
|
+
// listed passed the gate and its overall score is at least the floor.
|
|
15
|
+
// excluded otherwise: scored below the floor, or no overall to compare.
|
|
16
|
+
//
|
|
17
|
+
// A delisted module (ADR 0338 D2) is never in the catalog, whatever its score.
|
|
18
|
+
|
|
19
|
+
const { parseVersion, cmp } = require('../module-loader/semver');
|
|
20
|
+
|
|
21
|
+
// The quality floor, 0-100. It lives here and nowhere else: it is part of Bongos
|
|
22
|
+
// itself, not a live setting, so no author, builder or instance can move it.
|
|
23
|
+
// Changing it is an owner-approved, reviewed task (owner decision, 2026-10-01).
|
|
24
|
+
// Authors read the rule in docs/modules-contract.md ("Getting listed").
|
|
25
|
+
const QUALITY_FLOOR = 50;
|
|
26
|
+
|
|
27
|
+
// Where one version stands. `score` is its current score row, or null.
|
|
28
|
+
// Returns { placement: 'listed' | 'new' | 'excluded', reason }.
|
|
29
|
+
function placeVersion(score, floor = QUALITY_FLOOR) {
|
|
30
|
+
if (!score) return { placement: 'excluded', reason: 'not_assessed' };
|
|
31
|
+
if (score.security_passed !== true) return { placement: 'excluded', reason: 'security_not_passed' };
|
|
32
|
+
if (score.is_new) return { placement: 'new', reason: 'new' };
|
|
33
|
+
if (score.overall == null) return { placement: 'excluded', reason: 'no_score' };
|
|
34
|
+
if (score.overall < floor) return { placement: 'excluded', reason: 'below_floor' };
|
|
35
|
+
return { placement: 'listed', reason: 'at_or_above_floor' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The newest version row per module key, in semver order (a version that does not
|
|
39
|
+
// parse is skipped, as latestVersions does in module-store.js).
|
|
40
|
+
function newestPerModule(rows) {
|
|
41
|
+
const out = new Map();
|
|
42
|
+
for (const r of rows) {
|
|
43
|
+
const next = parseVersion(r.version);
|
|
44
|
+
if (!next) continue;
|
|
45
|
+
const have = out.get(r.module_key);
|
|
46
|
+
if (have && cmp(parseVersion(have.version), next) >= 0) continue;
|
|
47
|
+
out.set(r.module_key, r);
|
|
48
|
+
}
|
|
49
|
+
return [...out.values()];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The catalog: { floor, listed: [...], new: [...] }, each entry
|
|
53
|
+
// { key, title, version, published_at, overall, is_new }. Excluded versions are left
|
|
54
|
+
// out entirely: a module below the bar is not listed at all. Both lists are in key
|
|
55
|
+
// order; ranking is a separate task (1003808) and does not happen here.
|
|
56
|
+
async function listCatalog({ db, floor = QUALITY_FLOOR } = {}) {
|
|
57
|
+
const pool = db || require('./pool').pool;
|
|
58
|
+
const { rows: versions } = await pool.query(
|
|
59
|
+
`SELECT m.module_key, m.title, v.id AS version_id, v.version, v.published_at
|
|
60
|
+
FROM store_modules m JOIN store_module_versions v ON v.module_key = m.module_key
|
|
61
|
+
WHERE m.status = 'listed'`);
|
|
62
|
+
const newest = newestPerModule(versions);
|
|
63
|
+
const scores = new Map();
|
|
64
|
+
if (newest.length) {
|
|
65
|
+
const { rows } = await pool.query(
|
|
66
|
+
`SELECT DISTINCT ON (version_id) version_id, overall, security_passed, is_new
|
|
67
|
+
FROM module_assessment_scores
|
|
68
|
+
WHERE version_id = ANY($1)
|
|
69
|
+
ORDER BY version_id, computed_at DESC, id DESC`, [newest.map((r) => r.version_id)]);
|
|
70
|
+
for (const s of rows) scores.set(String(s.version_id), s);
|
|
71
|
+
}
|
|
72
|
+
const out = { floor, listed: [], new: [] };
|
|
73
|
+
for (const r of newest.sort((a, b) => a.module_key.localeCompare(b.module_key))) {
|
|
74
|
+
const score = scores.get(String(r.version_id)) || null;
|
|
75
|
+
const { placement } = placeVersion(score, floor);
|
|
76
|
+
if (placement === 'excluded') continue;
|
|
77
|
+
out[placement].push({
|
|
78
|
+
key: r.module_key, title: r.title, version: r.version, published_at: r.published_at,
|
|
79
|
+
overall: placement === 'new' ? null : score.overall, is_new: placement === 'new',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { QUALITY_FLOOR, placeVersion, listCatalog };
|
|
@@ -49,8 +49,20 @@ const { logger } = require('./logger');
|
|
|
49
49
|
const SETTING_KEYS = {
|
|
50
50
|
ROT_DAYS: 'attention.rot_days',
|
|
51
51
|
FOCUS_VERSION: 'roadmap.focus_version',
|
|
52
|
+
SHIP_MODE: 'ship.mode',
|
|
52
53
|
};
|
|
53
54
|
|
|
55
|
+
// SHIP MODE (task 1004628, ADR 0369) — WHO starts a ship, never HOW it lands.
|
|
56
|
+
// auto — the AI finishes a task, then ships and lands it with no commands from
|
|
57
|
+
// the person. The default, because a new owner who is handed terminal
|
|
58
|
+
// commands at every ship reads the project as broken (owner feedback,
|
|
59
|
+
// 2026-10-08).
|
|
60
|
+
// manual — the AI stops before shipping and the person approves each ship.
|
|
61
|
+
// It is NOT the deploy mode (config/deploy.json, ci|laptop): that one decides the
|
|
62
|
+
// mechanics of landing once a ship starts, is committed config, and is unchanged.
|
|
63
|
+
const SHIP_MODES = Object.freeze(['auto', 'manual']);
|
|
64
|
+
const DEFAULT_SHIP_MODE = 'auto';
|
|
65
|
+
|
|
54
66
|
// The last-resort rot timer, in days. 30 because that is the shortest window in
|
|
55
67
|
// which "nobody has touched this" is evidence of rot rather than evidence of a
|
|
56
68
|
// normal week — a fortnight would fire on every ordinary holiday, and a quarter is
|
|
@@ -202,6 +214,56 @@ async function setFocusVersionId(versionId, builderId = null) {
|
|
|
202
214
|
return String(versionId);
|
|
203
215
|
}
|
|
204
216
|
|
|
217
|
+
// parseShipMode(raw) — one candidate as a ship mode, or why it is unusable. Pure,
|
|
218
|
+
// never throws, case-insensitive and trimmed so a hand-typed "Manual " still reads.
|
|
219
|
+
function parseShipMode(raw) {
|
|
220
|
+
if (raw == null || String(raw).trim() === '') return { reason: 'empty' };
|
|
221
|
+
const v = String(raw).trim().toLowerCase();
|
|
222
|
+
if (!SHIP_MODES.includes(v)) return { reason: `must be one of ${SHIP_MODES.join(' | ')}` };
|
|
223
|
+
return { mode: v };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// resolveShipMode() — the project's ship mode and where it came from.
|
|
227
|
+
// Returns { mode, source, chosen, invalid }:
|
|
228
|
+
// mode — always 'auto' or 'manual'
|
|
229
|
+
// source — 'project_settings' | 'default'
|
|
230
|
+
// chosen — true only when someone actually SET a usable value. "Never chosen" is
|
|
231
|
+
// a real answer the first-ship prompt (task 1004630) asks about, so it
|
|
232
|
+
// must not be confused with "chose the default".
|
|
233
|
+
// invalid — null, or { where, raw, reason } for a stored value that is unusable.
|
|
234
|
+
// A database that cannot be read answers the default rather than failing a ship —
|
|
235
|
+
// the same reporting posture as resolveRotDays.
|
|
236
|
+
async function resolveShipMode() {
|
|
237
|
+
let row = null;
|
|
238
|
+
try {
|
|
239
|
+
row = await getSetting(SETTING_KEYS.SHIP_MODE);
|
|
240
|
+
} catch (err) {
|
|
241
|
+
logger.error(`project_settings read failed; falling back to the default ship mode: ${err && err.message}`);
|
|
242
|
+
return { mode: DEFAULT_SHIP_MODE, source: 'default', chosen: false, invalid: null };
|
|
243
|
+
}
|
|
244
|
+
if (row && row.value != null && row.value !== '') {
|
|
245
|
+
const got = parseShipMode(row.value);
|
|
246
|
+
if (got.mode) return { mode: got.mode, source: 'project_settings', chosen: true, invalid: null };
|
|
247
|
+
return {
|
|
248
|
+
mode: DEFAULT_SHIP_MODE,
|
|
249
|
+
source: 'default',
|
|
250
|
+
chosen: false,
|
|
251
|
+
invalid: { where: `project_settings['${SETTING_KEYS.SHIP_MODE}']`, raw: String(row.value), reason: got.reason },
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return { mode: DEFAULT_SHIP_MODE, source: 'default', chosen: false, invalid: null };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// describeShipMode(resolved) — one plain sentence for a person or an AI session.
|
|
258
|
+
function describeShipMode(resolved) {
|
|
259
|
+
const r = resolved || {};
|
|
260
|
+
const what = r.mode === 'manual'
|
|
261
|
+
? 'Manual: the AI stops before each ship and waits for a person to approve it.'
|
|
262
|
+
: 'Auto: when the AI finishes a task it ships and lands it with no commands from a person.';
|
|
263
|
+
const how = r.chosen ? '' : ' (the default — nobody has chosen yet)';
|
|
264
|
+
return `${what}${how}`;
|
|
265
|
+
}
|
|
266
|
+
|
|
205
267
|
// parseDays is exported for the tests only — the range check and the default are
|
|
206
268
|
// asserted against the SAME constants the resolver reads, because a test that
|
|
207
269
|
// re-typed 30 or 365 would be the second copy of the number this file exists to
|
|
@@ -218,4 +280,9 @@ module.exports = {
|
|
|
218
280
|
describeRotTimer,
|
|
219
281
|
getFocusVersionId,
|
|
220
282
|
setFocusVersionId,
|
|
283
|
+
SHIP_MODES,
|
|
284
|
+
DEFAULT_SHIP_MODE,
|
|
285
|
+
parseShipMode,
|
|
286
|
+
resolveShipMode,
|
|
287
|
+
describeShipMode,
|
|
221
288
|
};
|
|
@@ -97,6 +97,7 @@ const log = require('../logger').logger.child({ src: 'modules' });
|
|
|
97
97
|
// property access on a CommonJS namespace, so reaching these through one made them read
|
|
98
98
|
// as unused exports and tripped the dead-code ratchet that held this branch out of main.
|
|
99
99
|
const { listOverrides, setOverride } = require('../module-overrides');
|
|
100
|
+
const { listCatalog } = require('../module-listing');
|
|
100
101
|
|
|
101
102
|
// A packaged submission embeds every file's content (base64) — cap the total
|
|
102
103
|
// so a runaway/misbuilt module can't bloat the review-queue table. Generous
|
|
@@ -692,6 +693,20 @@ module.exports = function buildModulesRouter() {
|
|
|
692
693
|
res.json({ latest });
|
|
693
694
|
}));
|
|
694
695
|
|
|
696
|
+
// The store catalog under the quality floor (task 1003807, ADR 0343 D2/D5): each
|
|
697
|
+
// listed module's newest version, if it passed the Security gate and scores at or
|
|
698
|
+
// above the floor, or under "new" if it has no real-world data yet. Below the bar
|
|
699
|
+
// is not listed at all. The floor is fixed in module-listing.js and returned here.
|
|
700
|
+
//
|
|
701
|
+
// rank: any-builder — a read of public store metadata, like /store/modules/latest:
|
|
702
|
+
// no entitlement taken, nothing changed.
|
|
703
|
+
router.get('/store/modules', auth.requireBuilder,
|
|
704
|
+
asyncHandler('GET /store/modules', async (_req, res) => {
|
|
705
|
+
const listing = await listCatalog();
|
|
706
|
+
res.set('Cache-Control', 'no-store');
|
|
707
|
+
res.json(listing);
|
|
708
|
+
}));
|
|
709
|
+
|
|
695
710
|
router.post('/store/modules/:key/acquired', auth.requireBuilder,
|
|
696
711
|
asyncHandler('POST /store/modules/:key/acquired', async (req, res) => {
|
|
697
712
|
const { key } = req.params;
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.75'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -462,6 +462,12 @@ module.exports = {
|
|
|
462
462
|
// page's own anti-drift rule forbids, caught one layer up on task 1003280's grade.
|
|
463
463
|
get MIN_ROT_DAYS() { return require('./bongos/project-settings').MIN_ROT_DAYS; },
|
|
464
464
|
get MAX_ROT_DAYS() { return require('./bongos/project-settings').MAX_ROT_DAYS; },
|
|
465
|
+
// Ship mode (task 1004628, ADR 0369): who starts a ship — the AI (auto) or a
|
|
466
|
+
// person (manual). The settings page writes it; the CLI and the AI read it.
|
|
467
|
+
get SHIP_MODES() { return require('./bongos/project-settings').SHIP_MODES; },
|
|
468
|
+
get parseShipMode() { return require('./bongos/project-settings').parseShipMode; },
|
|
469
|
+
get resolveShipMode() { return require('./bongos/project-settings').resolveShipMode; },
|
|
470
|
+
get describeShipMode() { return require('./bongos/project-settings').describeShipMode; },
|
|
465
471
|
},
|
|
466
472
|
readConfigFileSync: instanceConfig.readConfigFileSync,
|
|
467
473
|
configHome: instanceConfig.configHome,
|