@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.
- package/.bongos-core.json +95 -35
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +24 -0
- package/clients/bongos-client/index.cjs +24 -0
- package/clients/bongos-client/index.d.ts +39 -0
- package/clients/bongos-client/index.mjs +24 -0
- package/docs/adr/0296-a-role-speciality-is-data-over-three-existing-surfaces.md +71 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +867 -73
- package/docs/api-reference.md +19 -3
- package/docs/copy-inventory.md +48 -20
- package/docs/copy-registry.json +275 -23
- package/docs/module-api-changelog.md +2 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +24 -0
- package/modules/hall-ui/public/settings-specialities.js +241 -0
- package/modules/hall-ui/public/settings.css +56 -0
- package/modules/hall-ui/public/settings.html +65 -0
- package/modules/lifecycle/db-claim-reads.js +5 -1
- package/modules/memory/learnings.js +21 -2
- package/modules/provisioning/starter-bundles.js +1 -0
- package/modules/specialities/CLAUDE.md +15 -0
- package/modules/specialities/db.js +234 -0
- package/modules/specialities/migrations/specialities_001_specialities.sql +143 -0
- package/modules/specialities/module.json +12 -0
- package/modules/specialities/routes/specialities.js +379 -0
- package/modules/specialities/specialities.js +198 -0
- package/modules/ui-design/kit/serve.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/publish-manifest.js +1 -0
- package/scripts/gds/search-ingest-db.js +37 -0
- package/src/bongos/routes/me.js +17 -0
- package/src/module-api.js +1 -1
- package/tests/module_loader.mjs +1 -1
- package/tests/own_scope_guard.mjs +10 -0
- package/tests/specialities.mjs +214 -0
- package/tests/speciality_corpus_scope.mjs +97 -0
- package/tests/speciality_db_shape.mjs +124 -0
- 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
|
-
|
|
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
|
// -------------------------------------------------------------------------
|
|
@@ -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._
|