@bongos/core 1.20.2 → 1.20.3
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 +38 -33
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +3 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0310-a-speciality-offers-skills-and-the-adopter-chooses-them.md +1 -1
- package/docs/api/openapi.json +70 -3
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +32 -18
- package/docs/copy-registry.json +153 -27
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +2 -1
- package/docs/page-readings.json +112 -72
- package/modules/hall-ui/public/settings-specialities.js +169 -5
- package/modules/hall-ui/public/settings.css +52 -1
- package/modules/hall-ui/public/settings.states.json +2 -1
- package/modules/hall-ui/records/panel-family.md +2 -0
- package/modules/specialities/routes/specialities.js +27 -0
- package/modules/specialities/skills.js +109 -13
- package/modules/specialities/specialities.js +4 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/scripts/hall-preview/server.js +4 -0
- package/src/module-api.js +1 -1
- package/tests/speciality_skills.mjs +76 -1
- package/tests/speciality_walkthrough_ui.mjs +218 -0
|
@@ -202,7 +202,9 @@ select.voices__select { min-width: 140px; }
|
|
|
202
202
|
/* The adopted contract, shown in full before "Use this" — another builder's prose
|
|
203
203
|
reaching your sessions is the one thing here that must never be folded away. */
|
|
204
204
|
.speciality-contract { flex-basis: 100%; margin-top: 0.5rem; font-size: 0.85rem; }
|
|
205
|
-
|
|
205
|
+
/* 24px tall: the panel was never revealed until task 1004073, so the kit had
|
|
206
|
+
never measured this toggle; it came back 21px. */
|
|
207
|
+
.speciality-contract > summary { cursor: pointer; opacity: 0.85; min-height: 24px; padding-block: 2px; box-sizing: border-box; }
|
|
206
208
|
.speciality-contract__body {
|
|
207
209
|
white-space: pre-wrap;
|
|
208
210
|
margin: 0.4rem 0 0;
|
|
@@ -212,6 +214,55 @@ select.voices__select { min-width: 140px; }
|
|
|
212
214
|
font-family: inherit;
|
|
213
215
|
opacity: 0.92;
|
|
214
216
|
}
|
|
217
|
+
/* The skills walkthrough (task 1004073, ADR 0310 §3): one offered skill at a
|
|
218
|
+
time, three answers as a definition list, then a decision. It sits inside the
|
|
219
|
+
card like the train form, so it inherits the card's ground in both modes. */
|
|
220
|
+
.skill-walk {
|
|
221
|
+
flex-basis: 100%;
|
|
222
|
+
margin-top: 0.75rem;
|
|
223
|
+
padding-top: 0.75rem;
|
|
224
|
+
border-top: 1px solid var(--rule);
|
|
225
|
+
display: flex;
|
|
226
|
+
flex-direction: column;
|
|
227
|
+
gap: 0.6rem;
|
|
228
|
+
}
|
|
229
|
+
/* The measure caps the reading lines, never the section: a capped flex-basis
|
|
230
|
+
lets the section fit beside the card's actions instead of wrapping under them. */
|
|
231
|
+
.skill-walk > * { max-width: var(--measure); }
|
|
232
|
+
.skill-walk__progress { margin: 0; font-size: 0.8rem; color: var(--ink-soft); }
|
|
233
|
+
.skill-walk__name { margin: 0; font-family: var(--font-mono); font-size: 1rem; color: var(--ink); }
|
|
234
|
+
.skill-walk__facts { margin: 0; display: grid; gap: 0.2rem 0; }
|
|
235
|
+
.skill-walk__facts dt { font-size: 0.8rem; color: var(--ink-soft); margin-top: 0.4rem; }
|
|
236
|
+
.skill-walk__facts dt:first-child { margin-top: 0; }
|
|
237
|
+
.skill-walk__facts dd { margin: 0; color: var(--ink); line-height: 1.5; }
|
|
238
|
+
.skill-walk__note { font-size: 0.8rem; }
|
|
239
|
+
/* "Leave it off" and "Turn it on" are the panel family's plain pill at EQUAL
|
|
240
|
+
weight: the default is off, so the page must not lean on the reader toward on.
|
|
241
|
+
Only "Save my choices" takes the accent. A decision already made reads through
|
|
242
|
+
aria-pressed when the person steps Back to it. */
|
|
243
|
+
.skill-walk__actions { flex-wrap: wrap; }
|
|
244
|
+
.skill-walk__actions .pbtn[aria-pressed="true"] { border-color: var(--ink-soft); }
|
|
245
|
+
.skill-walk__recap { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; }
|
|
246
|
+
.skill-walk__recap li { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 0.4rem; }
|
|
247
|
+
.skill-walk__recap .prow__hint { flex-basis: 100%; margin: 0; }
|
|
248
|
+
.skill-walk__recap strong { font-family: var(--font-mono); font-weight: 600; }
|
|
249
|
+
/* "Finish later" is a way out, not a choice about a skill, so it reads as a quiet
|
|
250
|
+
link below the decision rather than a third button beside it. */
|
|
251
|
+
.skill-walk__later {
|
|
252
|
+
align-self: flex-start;
|
|
253
|
+
background: none;
|
|
254
|
+
border: 0;
|
|
255
|
+
padding: 0 0.25rem;
|
|
256
|
+
margin-inline-start: -0.25rem;
|
|
257
|
+
min-height: 24px;
|
|
258
|
+
font: inherit;
|
|
259
|
+
font-size: 0.8rem;
|
|
260
|
+
color: var(--ink-soft);
|
|
261
|
+
text-decoration: underline;
|
|
262
|
+
text-underline-offset: 3px;
|
|
263
|
+
cursor: pointer;
|
|
264
|
+
}
|
|
265
|
+
.skill-walk__later:hover { color: var(--ink); }
|
|
215
266
|
|
|
216
267
|
/* ---- Software update (task 1004296) -------------------------------------- */
|
|
217
268
|
/* A version is read digit by digit and compared against another, so it takes the mono
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"states": {
|
|
10
10
|
"account": { "auth": true, "actions": [["wait", 900]], "expect": { "visible": ["#settings-h1", "#display-name-input", "#craft-chips"] } },
|
|
11
11
|
"access": { "auth": true, "url": "/settings#access", "actions": [["wait", 900]], "expect": { "visible": ["#cli-reissue-btn", "#sessions-list", "#artkey-card"] } },
|
|
12
|
-
"preferences": { "auth": true, "url": "/settings#preferences", "actions": [["wait", 900]], "expect": { "visible": ["#wander-rows", "#render-rows"] } },
|
|
12
|
+
"preferences": { "auth": true, "url": "/settings#preferences", "actions": [["wait", 900]], "expect": { "visible": ["#wander-rows", "#render-rows", "#specialities-rows"] } },
|
|
13
|
+
"skills-walkthrough": { "auth": true, "url": "/settings#preferences", "actions": [["wait", 900], ["click", "#specialities-rows button[data-act=\"adopt\"]"], ["wait", 700]], "expect": { "visible": [".skill-walk", ".skill-walk__facts", "button[data-walk=\"on\"]", "button[data-walk=\"off\"]"] } },
|
|
13
14
|
"sound": { "auth": true, "url": "/settings#sound", "actions": [["wait", 900]], "expect": { "visible": ["#voices-table", "#sound-rows", "#event-sound-rows"] } },
|
|
14
15
|
"update": { "auth": true, "url": "/settings#software-update", "actions": [["wait", 1200]], "expect": { "visible": ["#software-update-h", "#su-body", "#su-update", ".su-versions"] } }
|
|
15
16
|
},
|
|
@@ -26,3 +26,5 @@ Also fixed here, as the board task handed forward: the two sub-24px links on Hom
|
|
|
26
26
|
- **"Up to date" is said only from an answer.** A failed registry read, an unnamed running version and unread release notes each render as what they are; unread notes are never drawn as a version with no changes.
|
|
27
27
|
- **The notes are the soft ink, underlined, on the platform.** There every entry links to its task, and a list of accent-coloured sentences read as a warning. Elsewhere the ids belong to another project's ledger, so the entries are plain text.
|
|
28
28
|
- **Kit:** `settings.states.json` `update`, ALL CLEAN at 1440/390/320 × dark/light against the harness with the fixture `software-update.json` (a live reading for 1.19.1033). Tests: `tests/software_update.mjs`.
|
|
29
|
+
|
|
30
|
+
**Settings: adopting a speciality walks through its skills (task 1004073, ADR 0310 §3)** — the owner ruled an adopted speciality's skills start OFF "and you should walk through what comes with each". So adopting one that offers skills opens `.skill-walk` inside its card: one skill per step (what it does, when to reach for it, what it costs, what it needs, who can run it), "Leave it off" / "Turn it on" at EQUAL weight (plain `.pbtn`, no accent — the default is off, so the page must not lean toward on), Back, then a recap restating each skill in a line, then ONE `PUT` of the whole set. "Finish later" saves nothing. Focus moves to each new step heading. Two things found on the way: the specialities panel shipped `hidden` and nothing ever revealed it, so adoption was unreachable from the hall; and its contract `<summary>` measured 21px once visible (now 24). **Kit:** `settings.states.json` `skills-walkthrough` + `preferences`, ALL CLEAN at 1440/390/320 × dark/light against the harness, with fixtures `specialities*.json` built from the real skills on disk and canned answers for adopt and save. Tests: `tests/speciality_walkthrough_ui.mjs` (runs the real page script).
|
|
@@ -401,6 +401,33 @@ module.exports = function buildSpecialitiesRouter() {
|
|
|
401
401
|
res.json({ adoption });
|
|
402
402
|
}));
|
|
403
403
|
|
|
404
|
+
// GET /specialities/:id/skills — what the adoption WALKTHROUGH shows (task
|
|
405
|
+
// 1004073, ADR 0310 §3): each skill the speciality offers, explained for the
|
|
406
|
+
// person deciding (what it does, when to reach for it, what it costs, what it
|
|
407
|
+
// needs), plus the caller's own enabled set and whether they hold it at all.
|
|
408
|
+
// Same visibility as GET /specialities/:id — 404 for a row you cannot see. An
|
|
409
|
+
// offered skill this instance no longer has comes back `installed: false`, so
|
|
410
|
+
// the walkthrough can say so instead of offering something that would 404.
|
|
411
|
+
// Read-only; rank: any builder who may see the speciality.
|
|
412
|
+
router.get('/specialities/:id/skills', api.requireBuilder, asyncHandler('GET /specialities/:id/skills', async (req, res) => {
|
|
413
|
+
const id = parseId(req, res, { code: 'bad_id' });
|
|
414
|
+
if (id === null) return;
|
|
415
|
+
const s = await db.getSpeciality(id);
|
|
416
|
+
if (!s || !(await visibleTo(s, req.builder))) return res.fail('not_found', 404);
|
|
417
|
+
const installed = installedSkills();
|
|
418
|
+
const offered = Array.isArray(s.skills) ? s.skills : [];
|
|
419
|
+
const skills = offered.map((name) => (installed.has(name)
|
|
420
|
+
? { ...skillsLib.explainSkill({ name, ...installed.get(name) }), installed: true }
|
|
421
|
+
: { name, installed: false }));
|
|
422
|
+
const mine = (await db.listAdoptions(req.builder.id)).find((a) => String(a.id) === String(id) && a.active);
|
|
423
|
+
res.json({
|
|
424
|
+
speciality: { id: s.id, name: s.name, discipline: s.discipline },
|
|
425
|
+
adopted: !!mine,
|
|
426
|
+
enabled: mine && Array.isArray(mine.enabled_skills) ? mine.enabled_skills : [],
|
|
427
|
+
skills,
|
|
428
|
+
});
|
|
429
|
+
}));
|
|
430
|
+
|
|
404
431
|
// PUT /specialities/:id/skills — the caller's ENABLED set for a speciality they
|
|
405
432
|
// hold (task 1004072, ADR 0310 §2). Replaces the set whole: the walkthrough
|
|
406
433
|
// (task 1004073) sends the decisions it collected, so a partial patch would
|
|
@@ -35,29 +35,125 @@ function listDirs(dir) {
|
|
|
35
35
|
} catch { return []; }
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
// The
|
|
39
|
-
//
|
|
40
|
-
//
|
|
38
|
+
// The top-level keys of a SKILL.md's frontmatter, as strings (a flow list
|
|
39
|
+
// `[a, b]` as an array). Enough YAML for the grammar SKILL.md files use: a flat
|
|
40
|
+
// `key: value`, a quoted value, and a folded or literal block (`>-`, `>`, `|`,
|
|
41
|
+
// `|-`) whose indented lines follow. Part 1 read only the flat form, and almost
|
|
42
|
+
// every skill here folds its description, so each read as `>-` (task 1004073).
|
|
43
|
+
// Anything it cannot read is simply absent.
|
|
44
|
+
function parseFrontmatter(text) {
|
|
45
|
+
const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(String(text || ''));
|
|
46
|
+
if (!fm) return {};
|
|
47
|
+
const lines = fm[1].split(/\r?\n/);
|
|
48
|
+
const out = {};
|
|
49
|
+
for (let i = 0; i < lines.length; i++) {
|
|
50
|
+
const m = /^([A-Za-z][\w-]*):\s*(.*)$/.exec(lines[i]);
|
|
51
|
+
if (!m) continue;
|
|
52
|
+
const [, key, rest] = m;
|
|
53
|
+
const block = /^([>|])([+-]?)\s*$/.exec(rest);
|
|
54
|
+
if (block) {
|
|
55
|
+
const body = [];
|
|
56
|
+
while (i + 1 < lines.length && (/^\s+\S/.test(lines[i + 1]) || lines[i + 1].trim() === '')) body.push(lines[++i].trim());
|
|
57
|
+
out[key] = block[1] === '>'
|
|
58
|
+
? body.join('\n').split(/\n{2,}/).map((p) => p.split('\n').join(' ')).join('\n').trim()
|
|
59
|
+
: body.join('\n').trim();
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const list = /^\[(.*)\]$/.exec(rest.trim());
|
|
63
|
+
if (list) { out[key] = list[1].split(',').map((s) => s.trim().replace(/^(['"])(.*)\1$/, '$2')).filter(Boolean); continue; }
|
|
64
|
+
out[key] = rest.trim().replace(/^(['"])([\s\S]*)\1$/, '$2');
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function readFrontmatter(skillMd) {
|
|
70
|
+
try { return parseFrontmatter(fs.readFileSync(skillMd, 'utf8')); } catch { return {}; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// The description a SKILL.md gives, for the adoption walkthrough (task 1004073).
|
|
41
74
|
function readDescription(skillMd) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
|
|
45
|
-
if (!fm) return '';
|
|
46
|
-
const line = fm[1].split(/\r?\n/).find((l) => /^description:\s*/.test(l));
|
|
47
|
-
if (!line) return '';
|
|
48
|
-
return line.replace(/^description:\s*/, '').replace(/^(['"])([\s\S]*)\1$/, '$2').trim();
|
|
49
|
-
} catch { return ''; }
|
|
75
|
+
const d = readFrontmatter(skillMd).description;
|
|
76
|
+
return typeof d === 'string' ? d : '';
|
|
50
77
|
}
|
|
51
78
|
|
|
79
|
+
// The optional PEOPLE-FACING fields (task 1004073). `description` is written to
|
|
80
|
+
// be MATCHED by a model, so a skill's author may also say, for the person
|
|
81
|
+
// deciding whether to turn it on: what it does (`plain`), when to reach for it
|
|
82
|
+
// (`reach-for`) and what it costs (`cost`). Claude Code ignores keys it does not
|
|
83
|
+
// know, as it already does `requires:`.
|
|
84
|
+
const PEOPLE_KEYS = { plain: 'plain', 'reach-for': 'reachFor', cost: 'cost' };
|
|
85
|
+
|
|
52
86
|
function collect(out, dir, source) {
|
|
53
87
|
for (const name of listDirs(dir)) {
|
|
54
88
|
if (!SKILL_NAME_RE.test(name) || out.has(name)) continue;
|
|
55
89
|
const md = path.join(dir, name, 'SKILL.md');
|
|
56
90
|
if (!fs.existsSync(md)) continue;
|
|
57
|
-
|
|
91
|
+
const fm = readFrontmatter(md);
|
|
92
|
+
const entry = { name, description: typeof fm.description === 'string' ? fm.description : '', source };
|
|
93
|
+
for (const [key, field] of Object.entries(PEOPLE_KEYS)) if (typeof fm[key] === 'string' && fm[key]) entry[field] = fm[key];
|
|
94
|
+
if (Array.isArray(fm.requires) && fm.requires.length) entry.requires = fm.requires;
|
|
95
|
+
out.set(name, entry);
|
|
58
96
|
}
|
|
59
97
|
}
|
|
60
98
|
|
|
99
|
+
// ONE SKILL, AS A PERSON DECIDING ABOUT IT READS IT (task 1004073; ADR 0310 §3:
|
|
100
|
+
// "what it does, when its author reaches for it, what it costs"). Pure: takes an
|
|
101
|
+
// installedSkills() entry and returns the three answers plus what the skill
|
|
102
|
+
// needs. An author's own people-facing field always wins; without one each
|
|
103
|
+
// answer is derived from the model-facing description, and `derived` says so,
|
|
104
|
+
// so the walkthrough can be honest that the words were not written for them.
|
|
105
|
+
//
|
|
106
|
+
// The rank note is kept apart on purpose: enabling a skill GRANTS NOTHING
|
|
107
|
+
// (ADR 0310 §1), so a Metic-only skill stays Metic-only after you turn it on,
|
|
108
|
+
// and the person should be told that before they choose it, not after.
|
|
109
|
+
function explainSkill(entry) {
|
|
110
|
+
const e = entry || {};
|
|
111
|
+
const desc = String(e.description || '').replace(/\s+/g, ' ').trim();
|
|
112
|
+
const cut = desc.search(/\bTriggers?:/i);
|
|
113
|
+
let does = (cut >= 0 ? desc.slice(0, cut) : desc).trim();
|
|
114
|
+
const triggers = cut >= 0 ? desc.slice(cut).replace(/^Triggers?:\s*/i, '') : '';
|
|
115
|
+
|
|
116
|
+
const rankMatch = /\b(Metic\+|Archon)[- ]only\b\.?/i.exec(does);
|
|
117
|
+
const rank = rankMatch ? (/archon/i.test(rankMatch[1]) ? 'archon' : 'metic') : null;
|
|
118
|
+
if (rankMatch) does = does.replace(rankMatch[0], '').replace(/\s{2,}/g, ' ').trim();
|
|
119
|
+
|
|
120
|
+
const quoted = [...triggers.matchAll(/"([^"]+)"/g)].map((m) => m[1].trim());
|
|
121
|
+
const commands = quoted.filter((q) => q.startsWith('/'));
|
|
122
|
+
const phrases = quoted.filter((q) => !q.startsWith('/'));
|
|
123
|
+
const otherwise = /\bor\s+(?!")([^"]+?)\.?$/i.exec(triggers);
|
|
124
|
+
|
|
125
|
+
let reachFor = e.reachFor || '';
|
|
126
|
+
if (!reachFor && (phrases.length || otherwise)) {
|
|
127
|
+
const said = phrases.slice(0, 3).map((p) => `“${p}”`).join(', ');
|
|
128
|
+
reachFor = [said && `When you would say something like ${said}`, otherwise && otherwise[1].trim()]
|
|
129
|
+
.filter(Boolean).join(' — or ');
|
|
130
|
+
reachFor = reachFor.charAt(0).toUpperCase() + reachFor.slice(1);
|
|
131
|
+
if (!/[.!?]$/.test(reachFor)) reachFor += '.';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let cost = e.cost || '';
|
|
135
|
+
if (!cost) {
|
|
136
|
+
const readOnly = /\bread-only\b|\bchanges nothing\b/i.test(desc);
|
|
137
|
+
const paid = /\bpaid\b/i.test(desc);
|
|
138
|
+
if (readOnly && paid) cost = 'Read-only as it normally runs, but its author flags a paid option — check before you use that one. Otherwise it uses your session like any other request.';
|
|
139
|
+
else if (readOnly) cost = 'Read-only: it looks things up and changes nothing. It uses your session like any other request.';
|
|
140
|
+
else if (paid) cost = 'Has a paid step — its author flags one in the description. Otherwise it uses your session like any other request.';
|
|
141
|
+
else cost = 'Its author has not written down what it costs. Running it uses your session like any other request.';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
name: e.name,
|
|
146
|
+
source: e.source || null,
|
|
147
|
+
does: e.plain || does,
|
|
148
|
+
reachFor,
|
|
149
|
+
cost,
|
|
150
|
+
commands,
|
|
151
|
+
needs: Array.isArray(e.requires) ? e.requires : [],
|
|
152
|
+
rank,
|
|
153
|
+
derived: { does: !e.plain, reachFor: !e.reachFor, cost: !e.cost },
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
61
157
|
/**
|
|
62
158
|
* The skills this instance has: Map name -> { name, description, source }.
|
|
63
159
|
* First root wins on a duplicate name (core, then instance, then modules), the
|
|
@@ -91,4 +187,4 @@ function installedSkillsCached(opts, now = Date.now()) {
|
|
|
91
187
|
}
|
|
92
188
|
function _resetCache() { cached = null; }
|
|
93
189
|
|
|
94
|
-
module.exports = { SKILL_NAME_RE, installedSkills, installedSkillsCached, readDescription, _resetCache };
|
|
190
|
+
module.exports = { SKILL_NAME_RE, installedSkills, installedSkillsCached, readDescription, parseFrontmatter, explainSkill, _resetCache };
|
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
// OFFERS skills (`skills`), each adopter's ENABLED set
|
|
19
19
|
// starts empty (`enabled_skills`), and only a skill this
|
|
20
20
|
// instance has may be named (skills.js). The walkthrough
|
|
21
|
-
// that fills the enabled set
|
|
22
|
-
//
|
|
21
|
+
// that fills the enabled set was built in task 1004073
|
|
22
|
+
// (GET /specialities/:id/skills + the hall's settings
|
|
23
|
+
// panel); the session line naming it is task 1004074.
|
|
24
|
+
// Naming a skill grants
|
|
23
25
|
// nothing.
|
|
24
26
|
// The genuinely new part is the knowledge bundle, which is why the migration adds
|
|
25
27
|
// tables for it and this file carries the rules over them.
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.3",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.20.
|
|
9
|
+
"version": "1.20.3",
|
|
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.20.
|
|
3
|
+
"version": "1.20.3",
|
|
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
|
@@ -8052,5 +8052,11 @@
|
|
|
8052
8052
|
"id": "1003631",
|
|
8053
8053
|
"text": "Closed a small consistency gap in the hall's needs-attention inbox: one link is now escaped like all the others. No visible change."
|
|
8054
8054
|
}
|
|
8055
|
+
],
|
|
8056
|
+
"1.20.3": [
|
|
8057
|
+
{
|
|
8058
|
+
"id": "1004073",
|
|
8059
|
+
"text": "Adopting a speciality now walks you through its skills one at a time: what each does, when you'd use it, and what it costs, and you choose on or off for each before seeing the next. Nothing is switched on unless you choose it."
|
|
8060
|
+
}
|
|
8055
8061
|
]
|
|
8056
8062
|
}
|
|
@@ -160,6 +160,10 @@ const CANNED_WRITES = [
|
|
|
160
160
|
// sent-back lines the artboard draws render too.
|
|
161
161
|
['POST', /^copy-desk\/pages\/[a-z0-9-]+:[a-z0-9-]+\/approve$/, 'copy-desk__pages__approve.post'],
|
|
162
162
|
['POST', /^copy-desk\/pages\/[a-z0-9-]+:[a-z0-9-]+\/send-back$/, 'copy-desk__pages__send-back.post'],
|
|
163
|
+
// a speciality's adopt and the walkthrough's save (task 1004073), so adopting
|
|
164
|
+
// opens the skills walkthrough and its recap can save in the harness.
|
|
165
|
+
['POST', /^specialities\/\d+\/adopt$/, 'specialities__9__adopt.post'],
|
|
166
|
+
['PUT', /^specialities\/\d+\/skills$/, 'specialities__9__skills.put'],
|
|
163
167
|
];
|
|
164
168
|
|
|
165
169
|
// A DATE THAT ROTS CANNOT DEMO AN AGE SIGNAL (task 1004041). The help-request
|
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.20.
|
|
78
|
+
const CORE_VERSION = '1.20.3'; // 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');
|
|
@@ -58,6 +58,46 @@ await test('the real tree resolves: the core ships skills a speciality can offer
|
|
|
58
58
|
assert.ok(got.has('recall') && got.has('status'), 'core skills are found in this checkout');
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
+
await test('a FOLDED description (`>-`) is read as its text, not as the fold marker (task 1004073)', () => {
|
|
62
|
+
const fm = skills.parseFrontmatter('---\nname: x\ndescription: >-\n One line\n and the next.\nrequires: [gh, droplet-ssh]\ncost: "Free."\n---\nbody');
|
|
63
|
+
assert.equal(fm.description, 'One line and the next.');
|
|
64
|
+
assert.deepEqual(fm.requires, ['gh', 'droplet-ssh']);
|
|
65
|
+
assert.equal(fm.cost, 'Free.');
|
|
66
|
+
assert.equal(skills.parseFrontmatter('---\ndescription: |\n a\n b\n---\n').description, 'a\nb', 'a literal block keeps its lines');
|
|
67
|
+
assert.deepEqual(skills.parseFrontmatter('no frontmatter'), {});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await test('every skill in the real tree reads a real description — none is a bare fold marker', () => {
|
|
71
|
+
const got = skills.installedSkills({ coreRoot: ROOT, isModuleEnabled: () => false });
|
|
72
|
+
const bad = [...got.values()].filter((s) => !s.description || /^[>|][+-]?$/.test(s.description)).map((s) => s.name);
|
|
73
|
+
assert.deepEqual(bad, [], `these would teach nothing in the walkthrough: ${bad.join(', ')}`);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await test('explainSkill splits a model-facing description into what it does, when, and what it costs', () => {
|
|
77
|
+
const e = skills.explainSkill({
|
|
78
|
+
name: 'goal-review', source: 'core',
|
|
79
|
+
description: 'Walk the review queue. Read-only. Metic+ only. Triggers: "/goal-review", "review goals", "confirm criteria", or a scheduled run.',
|
|
80
|
+
requires: ['gh'],
|
|
81
|
+
});
|
|
82
|
+
assert.equal(e.does, 'Walk the review queue. Read-only.', 'the trigger list and the rank note are not part of what it does');
|
|
83
|
+
assert.equal(e.rank, 'metic', 'the rank is kept apart: enabling a skill grants nothing');
|
|
84
|
+
assert.deepEqual(e.commands, ['/goal-review']);
|
|
85
|
+
assert.match(e.reachFor, /“review goals”, “confirm criteria”.*a scheduled run/);
|
|
86
|
+
assert.match(e.cost, /^Read-only/);
|
|
87
|
+
assert.deepEqual(e.needs, ['gh']);
|
|
88
|
+
assert.deepEqual(e.derived, { does: true, reachFor: true, cost: true });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await test('explainSkill prefers the author\'s people-facing words and says when it had to guess', () => {
|
|
92
|
+
const e = skills.explainSkill({ name: 'x', description: 'Model text. Triggers: "/x".', plain: 'Plain words.', cost: 'About a dollar.' });
|
|
93
|
+
assert.equal(e.does, 'Plain words.');
|
|
94
|
+
assert.equal(e.cost, 'About a dollar.');
|
|
95
|
+
assert.deepEqual(e.derived, { does: false, reachFor: true, cost: false });
|
|
96
|
+
const unknown = skills.explainSkill({ name: 'y', description: 'Does a thing.' });
|
|
97
|
+
assert.match(unknown.cost, /has not written down what it costs/, 'an unknown cost is said plainly, never invented');
|
|
98
|
+
assert.match(skills.explainSkill({ name: 'z', description: 'Read-only check. --probe runs a paid calibration.' }).cost, /paid option/);
|
|
99
|
+
});
|
|
100
|
+
|
|
61
101
|
await test('an unreadable root contributes nothing rather than throwing', () => {
|
|
62
102
|
const got = skills.installedSkills({ coreRoot: path.join(os.tmpdir(), 'no-such-root-' + Date.now()), isModuleEnabled: () => { throw new Error('boom'); } });
|
|
63
103
|
assert.equal(got.size, 0);
|
|
@@ -146,7 +186,12 @@ db.setEnabledSkills = async (b, s, list) => (store.adoption ? { ...store.adoptio
|
|
|
146
186
|
db.createSpeciality = async (row) => { store.created = row; return { id: 10, ...row }; };
|
|
147
187
|
db.updateSpeciality = async (id, patch) => { store.updated = patch; return { ...store.speciality, ...patch }; };
|
|
148
188
|
db.countTraining = async () => ({ documentCount: 0, learningCount: 0 });
|
|
149
|
-
skills.installedSkillsCached = () => new Map([
|
|
189
|
+
skills.installedSkillsCached = () => new Map([
|
|
190
|
+
['recall', { name: 'recall', description: 'Search what the project knows. Read-only. Triggers: "/recall", "what do we know about X".', source: 'core' }],
|
|
191
|
+
['status', { name: 'status', description: 'Roll up a version.', source: 'core' }],
|
|
192
|
+
['costing', { name: 'costing', description: 'Price a thing.', source: 'module:econ' }],
|
|
193
|
+
]);
|
|
194
|
+
db.listAdoptions = async () => (store.adoption ? [{ id: 9, active: store.adoption.active, enabled_skills: store.adoption.enabled_skills }] : []);
|
|
150
195
|
api.requireBuilder = (req, res, next) => { req.builder = { id: 1, rank: 'metic' }; next(); };
|
|
151
196
|
api.enabledDisciplines = () => ['ideator', 'engineer', 'artist'];
|
|
152
197
|
|
|
@@ -187,6 +232,36 @@ try {
|
|
|
187
232
|
store.speciality.skills = ['recall', 'status'];
|
|
188
233
|
});
|
|
189
234
|
|
|
235
|
+
await test('GET /specialities/:id/skills explains each OFFERED skill for the walkthrough, and the caller\'s own set', async () => {
|
|
236
|
+
store.adoption = null;
|
|
237
|
+
store.speciality.skills = ['recall', 'gone-now'];
|
|
238
|
+
const r = await call('GET', '/specialities/9/skills');
|
|
239
|
+
assert.equal(r.status, 200, JSON.stringify(r.body));
|
|
240
|
+
assert.equal(r.body.adopted, false);
|
|
241
|
+
assert.deepEqual(r.body.enabled, []);
|
|
242
|
+
assert.deepEqual(r.body.skills.map((s) => s.name), ['recall', 'gone-now'], 'the offered set, in its order — not every installed skill');
|
|
243
|
+
const recall = r.body.skills[0];
|
|
244
|
+
assert.equal(recall.installed, true);
|
|
245
|
+
assert.equal(recall.does, 'Search what the project knows. Read-only.');
|
|
246
|
+
assert.match(recall.reachFor, /what do we know about X/);
|
|
247
|
+
assert.match(recall.cost, /^Read-only/);
|
|
248
|
+
assert.deepEqual(r.body.skills[1], { name: 'gone-now', installed: false }, 'a skill removed since is named, not explained or offered');
|
|
249
|
+
|
|
250
|
+
store.adoption = { builder_id: 1, speciality_id: 9, active: true, enabled_skills: ['recall'] };
|
|
251
|
+
const held = await call('GET', '/specialities/9/skills');
|
|
252
|
+
assert.equal(held.body.adopted, true);
|
|
253
|
+
assert.deepEqual(held.body.enabled, ['recall']);
|
|
254
|
+
store.speciality.skills = ['recall', 'status'];
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
await test('GET /specialities/:id/skills is 404 for a speciality the caller cannot see', async () => {
|
|
258
|
+
const saved = store.speciality;
|
|
259
|
+
store.speciality = { ...saved, owner_builder_id: 2, visibility: 'private' };
|
|
260
|
+
const r = await call('GET', '/specialities/9/skills');
|
|
261
|
+
assert.equal(r.status, 404);
|
|
262
|
+
store.speciality = saved;
|
|
263
|
+
});
|
|
264
|
+
|
|
190
265
|
await test('PUT /specialities/:id/skills needs an active adoption', async () => {
|
|
191
266
|
store.adoption = null;
|
|
192
267
|
const r = await call('PUT', '/specialities/9/skills', { enabled_skills: ['recall'] });
|