@bongos/core 1.19.811 → 1.19.812

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.
Files changed (39) hide show
  1. package/.bongos-core.json +95 -35
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +24 -0
  4. package/clients/bongos-client/index.cjs +24 -0
  5. package/clients/bongos-client/index.d.ts +39 -0
  6. package/clients/bongos-client/index.mjs +24 -0
  7. package/docs/adr/0296-a-role-speciality-is-data-over-three-existing-surfaces.md +71 -0
  8. package/docs/adr/README.md +1 -0
  9. package/docs/api/openapi.json +867 -73
  10. package/docs/api-reference.md +19 -3
  11. package/docs/copy-inventory.md +48 -20
  12. package/docs/copy-registry.json +275 -23
  13. package/docs/module-api-changelog.md +2 -0
  14. package/modules/dev-box/app/src/vendor/bongos-client.cjs +24 -0
  15. package/modules/hall-ui/public/settings-specialities.js +241 -0
  16. package/modules/hall-ui/public/settings.css +56 -0
  17. package/modules/hall-ui/public/settings.html +65 -0
  18. package/modules/lifecycle/db-claim-reads.js +5 -1
  19. package/modules/memory/learnings.js +21 -2
  20. package/modules/provisioning/starter-bundles.js +1 -0
  21. package/modules/specialities/CLAUDE.md +15 -0
  22. package/modules/specialities/db.js +234 -0
  23. package/modules/specialities/migrations/specialities_001_specialities.sql +143 -0
  24. package/modules/specialities/module.json +12 -0
  25. package/modules/specialities/routes/specialities.js +379 -0
  26. package/modules/specialities/specialities.js +198 -0
  27. package/modules/ui-design/kit/serve.js +1 -1
  28. package/package-lock.json +2 -2
  29. package/package.json +1 -1
  30. package/scripts/gds/publish-manifest.js +1 -0
  31. package/scripts/gds/search-ingest-db.js +37 -0
  32. package/src/bongos/routes/me.js +17 -0
  33. package/src/module-api.js +1 -1
  34. package/tests/module_loader.mjs +1 -1
  35. package/tests/own_scope_guard.mjs +10 -0
  36. package/tests/specialities.mjs +214 -0
  37. package/tests/speciality_corpus_scope.mjs +97 -0
  38. package/tests/speciality_db_shape.mjs +124 -0
  39. package/tests/speciality_injection.mjs +107 -0
@@ -0,0 +1,241 @@
1
+ // modules/hall-ui/public/settings-specialities.js — the "Your specialities" panel
2
+ // on /builders/settings (task 1003855; ADR 0296).
3
+ //
4
+ // WHAT A SPECIALITY IS, for anyone reading this file first: a craft says HOW you
5
+ // work (engineer / ideator / artist / ui); a speciality says what you work ON and
6
+ // how you, as a subject-matter expert, want to be engaged inside that craft. The
7
+ // contract you write here is injected into every session where you hold a claim
8
+ // in that craft.
9
+ //
10
+ // Its own file rather than more of settings.js, on the settings-interaction.js /
11
+ // settings-sessions.js precedent and for the same stated reason: settings.js sits
12
+ // against the 1500-line size ratchet. Self-contained — it shares nothing with the
13
+ // rest of the page but `window.OTB` and the generated client, both loaded before it.
14
+ //
15
+ // TRAINING IS TWO-FOLD, and only half of it is visible here. Documents you add are
16
+ // the half you drive; the other half accumulates on its own from learnings captured
17
+ // while you work in the speciality. The counts row says which is which, so "it
18
+ // knows 4 things" is never mistaken for "I typed 4 things".
19
+
20
+ (() => {
21
+ 'use strict';
22
+
23
+ const API = '/api/bongos';
24
+ const { escapeHtml, $ } = window.OTB;
25
+ const toast = (msg) => window.OTB.toast(msg);
26
+
27
+ const state = { rows: [], openId: null, busy: false };
28
+
29
+ const el = (id) => document.getElementById(id);
30
+ const statusLine = (msg) => { const s = el('specialities-status'); if (s) s.textContent = msg || ''; };
31
+
32
+ async function call(method, path, body) {
33
+ const res = await fetch(`${API}${path}`, {
34
+ method,
35
+ credentials: 'same-origin',
36
+ headers: body ? { 'Content-Type': 'application/json', Accept: 'application/json' } : { Accept: 'application/json' },
37
+ body: body ? JSON.stringify(body) : undefined,
38
+ });
39
+ if (res.status === 401) { window.location.assign(`${API}/auth/web/start`); return null; }
40
+ let data = null;
41
+ try { data = await res.json(); } catch { /* an empty body is legal on some paths */ }
42
+ if (!res.ok) {
43
+ // Surface the field-level reason when the server sent one — a bare
44
+ // "failed" on a form with five fields is useless.
45
+ const detail = data && data.details && Array.isArray(data.details.errors)
46
+ ? data.details.errors.map((e) => `${e.field}: ${e.code}`).join(', ')
47
+ : (data && data.message) || `HTTP ${res.status}`;
48
+ throw new Error(detail);
49
+ }
50
+ return data;
51
+ }
52
+
53
+ const VISIBILITY_COPY = {
54
+ private: 'Only you',
55
+ project: 'Anyone in the owning project',
56
+ public: 'Anyone on this instance',
57
+ };
58
+
59
+
60
+ // THE INSTRUCTION THE ADOPTER IS AGREEING TO. A speciality someone else wrote
61
+ // puts their prose into YOUR sessions, which is the only place in this product
62
+ // where that happens — so it is shown in full before "Use this", never folded
63
+ // away behind a summary. Shown for your own too, because seeing what actually
64
+ // reaches a session is the point either way.
65
+ function contractHtml(s) {
66
+ if (!s.contract_md) return '';
67
+ const theirs = !s.mine;
68
+ return `
69
+ <details class="speciality-contract"${theirs ? ' open' : ''}>
70
+ <summary>${theirs ? 'What this adds to your sessions — written by someone else' : 'What this adds to your sessions'}</summary>
71
+ <pre class="speciality-contract__body">${escapeHtml(s.contract_md)}</pre>
72
+ ${theirs ? '<p class="prow__hint">You did not write this. Read it before using the speciality — it reaches every session where you hold work in that craft, and it is passed to Claude as this author\'s stated preferences, not as instructions it must obey.</p>' : ''}
73
+ </details>`;
74
+ }
75
+
76
+ function cardHtml(s) {
77
+ const trained = Number(s.documentCount || 0);
78
+ const learned = Number(s.learningCount || 0);
79
+ const open = String(state.openId) === String(s.id);
80
+ return `
81
+ <li class="prow speciality-card" data-id="${escapeHtml(String(s.id))}">
82
+ <div class="prow__main">
83
+ <div class="prow__label">
84
+ <strong>${escapeHtml(s.name)}</strong>
85
+ <span class="speciality-craft">${escapeHtml(s.discipline)}</span>
86
+ ${s.active ? '<span class="speciality-badge speciality-badge--on">active</span>' : ''}
87
+ </div>
88
+ <p class="prow__hint">${escapeHtml(s.summary || 'No summary yet.')}</p>
89
+ ${contractHtml(s)}
90
+ <p class="prow__hint speciality-counts">
91
+ Trained on <strong>${trained}</strong> document${trained === 1 ? '' : 's'} you added
92
+ · learned <strong>${learned}</strong> thing${learned === 1 ? '' : 's'} from your sessions
93
+ · shared with: ${escapeHtml(VISIBILITY_COPY[s.visibility] || s.visibility || 'only you')}
94
+ </p>
95
+ </div>
96
+ <div class="prow__actions">
97
+ ${s.mine ? `<button type="button" class="btn btn--quiet" data-act="train" data-id="${escapeHtml(String(s.id))}">${open ? 'Close' : 'Train'}</button>` : ''}
98
+ ${s.active
99
+ ? `<button type="button" class="btn btn--quiet" data-act="abandon" data-id="${escapeHtml(String(s.id))}">Stop using</button>`
100
+ : `<button type="button" class="btn" data-act="adopt" data-id="${escapeHtml(String(s.id))}" ${s.adoptable ? '' : 'disabled'}>Use this</button>`}
101
+ </div>
102
+ ${open ? trainHtml(s) : ''}
103
+ </li>`;
104
+ }
105
+
106
+ function trainHtml(s) {
107
+ return `
108
+ <form class="speciality-train" data-id="${escapeHtml(String(s.id))}">
109
+ <p class="prow__hint">
110
+ Add something you wrote — notes, a standard, a worked example. It is indexed
111
+ for recall, not pasted into every session, so length costs you nothing.
112
+ </p>
113
+ <label class="speciality-field">
114
+ <span>Title</span>
115
+ <input type="text" name="title" maxlength="200" required placeholder="How we price a swap" />
116
+ </label>
117
+ <label class="speciality-field">
118
+ <span>The document</span>
119
+ <textarea name="body_md" rows="6" required placeholder="Paste or write it here."></textarea>
120
+ </label>
121
+ <div class="speciality-actions">
122
+ <button type="submit" class="btn">Add to what it knows</button>
123
+ </div>
124
+ </form>`;
125
+ }
126
+
127
+ function render() {
128
+ const host = el('specialities-rows');
129
+ if (!host) return;
130
+ if (!state.rows.length) {
131
+ host.innerHTML = `<li class="prow"><div class="prow__main">
132
+ <p class="prow__hint">No specialities yet. Create one below — it takes a name and a craft, and you can train it afterwards.</p>
133
+ </div></li>`;
134
+ return;
135
+ }
136
+ host.innerHTML = state.rows.map(cardHtml).join('');
137
+ }
138
+
139
+ async function load() {
140
+ try {
141
+ const [cat, mine] = await Promise.all([call('GET', '/specialities'), call('GET', '/me/specialities')]);
142
+ if (!cat) return;
143
+ const activeIds = new Set(((mine && mine.adoptions) || []).filter((a) => a.active).map((a) => String(a.id)));
144
+ state.rows = ((cat && cat.specialities) || []).map((s) => ({ ...s, active: activeIds.has(String(s.id)) }));
145
+ render();
146
+ statusLine('');
147
+ } catch (err) {
148
+ statusLine(`Could not load specialities — ${err.message}`);
149
+ }
150
+ }
151
+
152
+ async function act(action, id) {
153
+ if (state.busy) return;
154
+ state.busy = true;
155
+ try {
156
+ if (action === 'adopt') { await call('POST', `/specialities/${id}/adopt`); toast('Using this speciality.'); }
157
+ if (action === 'abandon') { await call('POST', `/specialities/${id}/abandon`); toast('Stopped using it.'); }
158
+ await load();
159
+ } catch (err) {
160
+ statusLine(err.message);
161
+ } finally {
162
+ state.busy = false;
163
+ }
164
+ }
165
+
166
+ function wire() {
167
+ const host = el('specialities-rows');
168
+ if (host) {
169
+ host.addEventListener('click', (e) => {
170
+ const btn = e.target.closest('button[data-act]');
171
+ if (!btn) return;
172
+ const { act: action, id } = btn.dataset;
173
+ if (action === 'train') { state.openId = String(state.openId) === String(id) ? null : id; render(); return; }
174
+ act(action, id);
175
+ });
176
+
177
+ host.addEventListener('submit', async (e) => {
178
+ const form = e.target.closest('form.speciality-train');
179
+ if (!form) return;
180
+ e.preventDefault();
181
+ const title = form.elements.title.value.trim();
182
+ const body = form.elements.body_md.value.trim();
183
+ // Validate before calling: an empty field should say so inline rather
184
+ // than round-trip to a 400.
185
+ if (!title || !body) { statusLine('A document needs both a title and some content.'); return; }
186
+ try {
187
+ const out = await call('POST', `/specialities/${form.dataset.id}/documents`, { title, body_md: body });
188
+ toast(out && out.skipped ? 'It already knew that one.' : 'Added.');
189
+ state.openId = null;
190
+ await load();
191
+ } catch (err) { statusLine(err.message); }
192
+ });
193
+ }
194
+
195
+ const create = el('speciality-create');
196
+ if (create) {
197
+ create.addEventListener('submit', async (e) => {
198
+ e.preventDefault();
199
+ const name = create.elements.name.value.trim();
200
+ const discipline = create.elements.discipline.value;
201
+ if (!name) { statusLine('Give it a name.'); return; }
202
+ if (!discipline) { statusLine('Pick which craft it sits under.'); return; }
203
+ try {
204
+ await call('POST', '/specialities', {
205
+ name,
206
+ discipline,
207
+ summary: create.elements.summary.value.trim(),
208
+ contract_md: create.elements.contract_md.value.trim(),
209
+ visibility: create.elements.visibility.value,
210
+ sellable: create.elements.sellable.checked,
211
+ });
212
+ create.reset();
213
+ toast('Speciality created.');
214
+ await load();
215
+ } catch (err) { statusLine(err.message); }
216
+ });
217
+
218
+ // The server refuses private+sellable (there is nobody it could be sold
219
+ // to). Mirror that here so the checkbox cannot be left in a state the save
220
+ // will reject.
221
+ const vis = create.elements.visibility;
222
+ const sell = create.elements.sellable;
223
+ const sync = () => {
224
+ const isPrivate = vis.value === 'private';
225
+ sell.disabled = isPrivate;
226
+ if (isPrivate) sell.checked = false;
227
+ };
228
+ vis.addEventListener('change', sync);
229
+ sync();
230
+ }
231
+ }
232
+
233
+ function boot() {
234
+ if (!el('specialities-rows')) return; // panel not on this page
235
+ wire();
236
+ load();
237
+ }
238
+
239
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
240
+ else boot();
241
+ })();
@@ -161,3 +161,59 @@ select.voices__select { min-width: 140px; }
161
161
  /* the box panel is the shell's (.setup__box, box-panel.js); inside a settings
162
162
  panel it sits on the glass rather than a second card */
163
163
  .settings-groups .setup__box { margin: var(--sp-sm) 0 0; background: var(--glass); border-color: var(--glass-edge); border-radius: var(--radius-sm); }
164
+
165
+ /* Specialities panel (task 1003855, ADR 0296). A speciality is a named bundle of
166
+ knowledge and terms under a craft; this styles the list, the train form and the
167
+ create form. Tokens only — no literal colours, so light/dark follow the page. */
168
+ .speciality-craft {
169
+ font-size: 0.78rem;
170
+ text-transform: lowercase;
171
+ opacity: 0.72;
172
+ margin-left: 0.4rem;
173
+ }
174
+ .speciality-badge {
175
+ font-size: 0.7rem;
176
+ padding: 0.05rem 0.4rem;
177
+ border-radius: 0.5rem;
178
+ margin-left: 0.4rem;
179
+ border: 1px solid currentColor;
180
+ }
181
+ .speciality-counts { opacity: 0.82; }
182
+ .speciality-card { flex-wrap: wrap; }
183
+ .speciality-train,
184
+ .speciality-create {
185
+ flex-basis: 100%;
186
+ margin-top: 0.75rem;
187
+ padding-top: 0.75rem;
188
+ border-top: 1px solid var(--rule, rgba(128, 128, 128, 0.25));
189
+ display: flex;
190
+ flex-direction: column;
191
+ gap: 0.6rem;
192
+ }
193
+ .speciality-create__h { margin: 0; font-size: 0.95rem; }
194
+ .speciality-field { display: flex; flex-direction: column; gap: 0.25rem; }
195
+ .speciality-field > span { font-size: 0.85rem; }
196
+ .speciality-field small { display: block; opacity: 0.7; font-weight: normal; }
197
+ .speciality-field input[type='text'],
198
+ .speciality-field select,
199
+ .speciality-field textarea {
200
+ width: 100%;
201
+ box-sizing: border-box;
202
+ }
203
+ /* The checkbox row reads as one sentence, so it runs inline rather than stacked. */
204
+ .speciality-field--check { flex-direction: row; align-items: flex-start; gap: 0.5rem; }
205
+ .speciality-field--check input { margin-top: 0.2rem; }
206
+ .speciality-actions { display: flex; gap: 0.5rem; }
207
+ /* The adopted contract, shown in full before "Use this" — another builder's prose
208
+ reaching your sessions is the one thing here that must never be folded away. */
209
+ .speciality-contract { flex-basis: 100%; margin-top: 0.5rem; font-size: 0.85rem; }
210
+ .speciality-contract > summary { cursor: pointer; opacity: 0.85; }
211
+ .speciality-contract__body {
212
+ white-space: pre-wrap;
213
+ margin: 0.4rem 0 0;
214
+ padding: 0.5rem 0.65rem;
215
+ border-left: 2px solid var(--rule, rgba(128, 128, 128, 0.35));
216
+ border-radius: 0;
217
+ font-family: inherit;
218
+ opacity: 0.92;
219
+ }
@@ -269,6 +269,70 @@
269
269
  <p class="settings-status" id="interaction-status" role="status" aria-live="polite"></p>
270
270
  </section>
271
271
 
272
+ <!-- SPECIALITIES — a named bundle of knowledge + terms UNDER a craft
273
+ (task 1003855, ADR 0296). GET/POST /api/bongos/specialities,
274
+ POST /specialities/:id/{adopt,abandon,documents}. No rank gate:
275
+ a speciality is how an expert's knowledge enters the platform,
276
+ and gating that by rank would mean only trusted builders could
277
+ be experts. -->
278
+ <section class="scroll" id="specialities-scroll" aria-labelledby="specialities-h" data-group="preferences" hidden>
279
+ <h2 id="specialities-h" class="scroll__h">Your specialities</h2>
280
+ <p class="scroll__lede">
281
+ A craft says how you work. A speciality says what you work on — and how you,
282
+ as someone who knows the subject, want to be engaged inside it. Whichever one
283
+ you are using reaches every session where you hold a claim in that craft.
284
+ Train it by adding things you have written; it also picks things up on its own
285
+ as you work.
286
+ </p>
287
+ <ul class="prows" id="specialities-rows"></ul>
288
+
289
+ <form id="speciality-create" class="speciality-create">
290
+ <h3 class="speciality-create__h">Create one</h3>
291
+ <label class="speciality-field">
292
+ <span>Name</span>
293
+ <input type="text" name="name" maxlength="80" required placeholder="Economics" />
294
+ </label>
295
+ <label class="speciality-field">
296
+ <span>Craft it sits under</span>
297
+ <select name="discipline" required>
298
+ <option value="">Pick one…</option>
299
+ <option value="engineer">Engineer</option>
300
+ <option value="ideator">Ideator</option>
301
+ <option value="artist">Artist</option>
302
+ <option value="ui">UI design</option>
303
+ </select>
304
+ </label>
305
+ <label class="speciality-field">
306
+ <span>Summary <small>(optional)</small></span>
307
+ <input type="text" name="summary" maxlength="500" placeholder="Markets, incentives, and how to price them." />
308
+ </label>
309
+ <label class="speciality-field">
310
+ <span>How Claude should engage you in it <small>(optional)</small></span>
311
+ <textarea name="contract_md" rows="4" maxlength="4000" placeholder="Argue from mechanism, not vibes. Always name the assumption a number rests on."></textarea>
312
+ </label>
313
+ <label class="speciality-field">
314
+ <span>Who can use it</span>
315
+ <select name="visibility">
316
+ <option value="private">Only me</option>
317
+ <option value="project">Anyone in the owning project</option>
318
+ <option value="public">Anyone on this instance</option>
319
+ </select>
320
+ </label>
321
+ <label class="speciality-field speciality-field--check">
322
+ <input type="checkbox" name="sellable" />
323
+ <span>
324
+ May be offered for sale later
325
+ <small>Records your decision only — there is no marketplace yet, and nothing is listed anywhere.</small>
326
+ </span>
327
+ </label>
328
+ <div class="speciality-actions">
329
+ <button type="submit" class="btn">Create speciality</button>
330
+ </div>
331
+ </form>
332
+
333
+ <p class="settings-status" id="specialities-status" role="status" aria-live="polite"></p>
334
+ </section>
335
+
272
336
  <!-- WANDERING — how readily the agent opens off-task threads (task 1268).
273
337
  GET/PATCH /api/bongos/me/wandering. Rank sets the default + ceiling; the
274
338
  server clamps, so options above the rank cap render disabled. -->
@@ -398,6 +462,7 @@
398
462
  the same precedent and for the same reason — settings.js is against the
399
463
  1500-line ratchet, and this panel shares nothing with the rest of the page. -->
400
464
  <script src="/builders/settings-interaction.js?v=2026-09-15-interaction"></script>
465
+ <script src="/builders/settings-specialities.js?v=2026-09-17-specialities"></script>
401
466
  <!-- task 1002518: the card-richness ladder. Own file on the same precedent —
402
467
  the panel grew from one switch to a rung picker with a derived default, and
403
468
  settings.js is against the 1500-line ratchet. -->
@@ -21,7 +21,11 @@ async function getActiveClaimsForBuilder(builderId) {
21
21
  `SELECT c.id AS claim_id, c.task_id, c.worktree_name, c.claimed_at,
22
22
  c.head_sha, c.reserved_migration_number,
23
23
  c.release_requested_by, c.release_requested_at, c.release_requested_note,
24
- t.title, t.touches, t.priority, t.credits_reward, t.version_id, t.module_key
24
+ t.title, t.touches, t.priority, t.credits_reward, t.version_id, t.module_key,
25
+ -- task 1003855: the craft of the claimed work. A speciality contract is
26
+ -- craft-scoped, so the Conductor needs to know which craft is in play
27
+ -- before it can pick the right one to inject.
28
+ t.discipline
25
29
  FROM claims c JOIN tasks t ON t.id = c.task_id
26
30
  WHERE c.builder_id = $1 AND c.released_at IS NULL
27
31
  ORDER BY c.claimed_at`,
@@ -13,7 +13,7 @@
13
13
 
14
14
  // Carved into modules/memory/ (BV1.R72 / ADR 0091 §2): pool via the doorway.
15
15
  const crypto = require('crypto');
16
- const { pool } = require('../../src/module-api');
16
+ const { pool, resolveOptional } = require('../../src/module-api');
17
17
  // ADR 0095 C4: <private>…</private>-fenced content is stripped at ingest so it
18
18
  // never lands in the learnings store (nor the recall index that reads it).
19
19
  const { stripPrivate } = require('./search-chunker');
@@ -111,6 +111,20 @@ async function createLearning({
111
111
  }
112
112
  }
113
113
 
114
+ // task 1003855: once the row lands, offer it to the builder's active speciality
115
+ // for that task's craft (training half (b)). Through the PORT, so this module
116
+ // never requires the specialities module and a disabled one simply means no
117
+ // link. Best-effort by construction: a capture must never fail over it.
118
+ const offerToSpeciality = async (row) => {
119
+ if (!row || !row.id) return;
120
+ try {
121
+ const sp = typeof resolveOptional === 'function' ? resolveOptional('specialities') : null;
122
+ if (sp && typeof sp.recordLearningForTask === 'function') {
123
+ await sp.recordLearningForTask({ learningId: row.id, builderId: row.captured_by, taskId: row.task_id });
124
+ }
125
+ } catch { /* never fail a capture over speciality bookkeeping */ }
126
+ };
127
+
114
128
  const { rows } = await pool.query(
115
129
  `INSERT INTO learnings (title, body_md, tags, related_files, captured_by,
116
130
  source, source_ref, task_id)
@@ -131,7 +145,12 @@ async function createLearning({
131
145
  taskId,
132
146
  ]
133
147
  );
134
- return rows[0] ?? null;
148
+ const created = rows[0] ?? null;
149
+ // Awaited, not fire-and-forget: an unawaited promise here would let the process
150
+ // exit mid-write in a CLI capture, and the helper already swallows its own
151
+ // failures, so awaiting costs one round trip and never fails the capture.
152
+ await offerToSpeciality(created);
153
+ return created;
135
154
  }
136
155
 
137
156
  // -------------------------------------------------------------------------
@@ -59,6 +59,7 @@ const ALWAYS_ON_CORE = Object.freeze([
59
59
  'public-landing',
60
60
  'security',
61
61
  'sessions',
62
+ 'specialities',
62
63
  'status-ui',
63
64
  'ui-design',
64
65
  ]);
@@ -0,0 +1,15 @@
1
+ # @bongos/core — installed platform dependency (not your project's CLAUDE.md)
2
+
3
+ This file is a placeholder shipped inside the **@bongos/core** package. The core's own
4
+ internal agent notes are intentionally omitted from the published artifact so they cannot
5
+ load into your instance's context.
6
+
7
+ **It does not govern this repository.** Your own root `CLAUDE.md`, your `.claude/`
8
+ directory, and the live Bongos database are the source of truth for how work happens here
9
+ and who may do what.
10
+
11
+ You are seeing this only because an agent opened a file under `node_modules/@bongos/core/`.
12
+ Nothing in here needs to be read to use the platform — run `bongos help` for commands.
13
+
14
+ _To work ON the platform core itself, use the standalone @bongos/core source repository,
15
+ not this vendored copy._