@bongos/core 1.20.2 → 1.20.4
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 +53 -38
- package/README.md +9 -18
- 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/0073-secrets-scan-exclude-uri-detector.md +1 -1
- 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 +4 -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 +12 -0
- package/scripts/gds/control-manifest.js +9 -0
- package/scripts/gds/doc-control-claims.js +168 -0
- package/scripts/gds/fitness.js +1 -0
- package/scripts/hall-preview/server.js +4 -0
- package/src/bongos/module-scope-map.js +1 -1
- package/src/module-api.js +1 -1
- package/tests/doc_control_claims.mjs +133 -0
- 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.4",
|
|
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.4",
|
|
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.4",
|
|
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,17 @@
|
|
|
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
|
+
}
|
|
8061
|
+
],
|
|
8062
|
+
"1.20.4": [
|
|
8063
|
+
{
|
|
8064
|
+
"id": "1003418",
|
|
8065
|
+
"text": "The project's automatic checks now catch any document that describes a safety check which no longer exists. On its first run it found the README still telling people their code was scanned for leaked passwords by a tool that w"
|
|
8066
|
+
}
|
|
8055
8067
|
]
|
|
8056
8068
|
}
|
|
@@ -141,6 +141,15 @@ const CONTROLS = [
|
|
|
141
141
|
wiredIn: { file: 'scripts/gds/ship-flow.js', needle: 'ship-honesty' },
|
|
142
142
|
alsoExists: ['tests/ship_cannot_lie.mjs'],
|
|
143
143
|
},
|
|
144
|
+
{
|
|
145
|
+
// Task 1003418 (audit A14). The manifest's own blind spot: it covers the rows
|
|
146
|
+
// someone listed. This guard derives claims from the docs instead, so a
|
|
147
|
+
// described-but-absent control reds the gate with no row to add.
|
|
148
|
+
label: 'guard: a doc cannot describe a control that does not exist (A14)',
|
|
149
|
+
file: 'scripts/gds/doc-control-claims.js',
|
|
150
|
+
wiredIn: { file: 'scripts/gds/fitness.js', needle: 'doc-control-claims' },
|
|
151
|
+
alsoExists: ['tests/doc_control_claims.mjs'],
|
|
152
|
+
},
|
|
144
153
|
];
|
|
145
154
|
|
|
146
155
|
// Pure evaluation — exported for the test. deps.exists/deps.read override fs.
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/gds/doc-control-claims.js — prose cannot describe a control that does not
|
|
3
|
+
// exist (task 1003418, audit ref A14 of the 2026-08-29 security audit, goal 1000084).
|
|
4
|
+
//
|
|
5
|
+
// WHY. control-manifest.js exists because ADR 0022 named three controls that had
|
|
6
|
+
// silently stopped existing, and it works — for the 14 controls someone remembered
|
|
7
|
+
// to list. Every phantom-control finding since (A6, A13, B31, B32, the row-8 patrol
|
|
8
|
+
// that read green for months while never running) was the same failure one row
|
|
9
|
+
// outside that list. A hand list only catches what a person already thought of, so
|
|
10
|
+
// the manifest cannot be the whole answer. This check reads the claims FROM THE DOCS:
|
|
11
|
+
// a doc sentence that says an enforcement artifact does something is a claim that
|
|
12
|
+
// the artifact exists, and a claim whose artifact is gone reds the gate by
|
|
13
|
+
// construction, with nobody having to add a row. The README's secrets section was
|
|
14
|
+
// the proof on first run: it still sent readers to a TruffleHog workflow and a
|
|
15
|
+
// pre-commit hook that had not existed since the core extraction — the ADR 0022
|
|
16
|
+
// finding itself, one file over.
|
|
17
|
+
//
|
|
18
|
+
// WHAT COUNTS AS A CLAIM, deliberately narrow:
|
|
19
|
+
// · a backticked path to an ENFORCEMENT artifact — a hook, a workflow, a script
|
|
20
|
+
// under scripts/gds or a hooks dir (CONTROL_PATH_RE);
|
|
21
|
+
// · in a sentence that uses control vocabulary — enforce, gate, guard, block,
|
|
22
|
+
// refuse, scan, patrol, audit, fail the build (CONTROL_WORDS);
|
|
23
|
+
// · and does NOT speak of it in the past — removed, retired, replaced, used to,
|
|
24
|
+
// no longer (HISTORY_WORDS). An ADR recording what was deleted is history,
|
|
25
|
+
// which is not a claim; the same path in the present tense is.
|
|
26
|
+
// Superseded / deprecated / rejected / withdrawn ADRs are skipped whole, and so are
|
|
27
|
+
// the dated records (session logs, audits, limitations archives), which describe a
|
|
28
|
+
// moment rather than the tree.
|
|
29
|
+
//
|
|
30
|
+
// WHAT IT DOES NOT PROVE: that an existing artifact is WIRED. That stays the
|
|
31
|
+
// manifest's job (a file present but unhooked). The two halves are complementary:
|
|
32
|
+
// this one scales to every doc and asks only "is it there"; the manifest asks
|
|
33
|
+
// "is it connected" for the controls that matter most.
|
|
34
|
+
//
|
|
35
|
+
// ALLOWLIST. A true claim this scan cannot tell from history (an accepted ADR whose
|
|
36
|
+
// design was later retired by a newer ADR) is listed in ALLOW with the reason. It
|
|
37
|
+
// is shrink-only in practice: an entry that no longer matches anything is itself a
|
|
38
|
+
// violation, so a fixed doc cannot leave a dead exception behind to hide the next.
|
|
39
|
+
//
|
|
40
|
+
// Its own file for the size-ratchet reason (the doc-cli-guard.js precedent);
|
|
41
|
+
// fitness.js requires it in CHECKS, and control-manifest.js lists it, so this guard
|
|
42
|
+
// cannot silently stop existing either.
|
|
43
|
+
|
|
44
|
+
'use strict';
|
|
45
|
+
|
|
46
|
+
const fs = require('fs');
|
|
47
|
+
const path = require('path');
|
|
48
|
+
const { trackedMarkdown } = require('./doc-cli-guard.js');
|
|
49
|
+
|
|
50
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
51
|
+
const NAME = 'a doc cannot describe an enforcement control that does not exist (task 1003418)';
|
|
52
|
+
|
|
53
|
+
// Two shapes: a script or workflow with its extension, OR a git hook by its bare
|
|
54
|
+
// name. Hooks under .husky/, .githooks/ and scripts/hooks/ carry no extension by
|
|
55
|
+
// convention, and `.husky/pre-commit` is exactly the artifact ADR 0022 lost. The
|
|
56
|
+
// names are git's whole documented set (githooks(5)), not a curated few.
|
|
57
|
+
const GIT_HOOK_NAMES = [
|
|
58
|
+
'applypatch-msg', 'pre-applypatch', 'post-applypatch', 'pre-commit', 'pre-merge-commit',
|
|
59
|
+
'prepare-commit-msg', 'commit-msg', 'post-commit', 'pre-rebase', 'post-checkout', 'post-merge',
|
|
60
|
+
'pre-push', 'pre-receive', 'update', 'proc-receive', 'post-receive', 'post-update',
|
|
61
|
+
'reference-transaction', 'push-to-checkout', 'pre-auto-gc', 'post-rewrite', 'sendemail-validate',
|
|
62
|
+
'fsmonitor-watchman', 'p4-changelist', 'p4-prepare-changelist', 'p4-post-changelist',
|
|
63
|
+
'p4-pre-submit', 'post-index-change',
|
|
64
|
+
];
|
|
65
|
+
const CONTROL_PATH_RE = new RegExp(
|
|
66
|
+
'`((?:\\.claude/hooks|\\.github/workflows|scripts/gds|scripts/hooks|\\.githooks|\\.husky)/[A-Za-z0-9_.\\-/]+?\\.(?:js|mjs|cjs|sh|yml|yaml|ps1)'
|
|
67
|
+
+ `|(?:\\.husky|\\.githooks|scripts/hooks)/(?:${GIT_HOOK_NAMES.join('|')}))\``,
|
|
68
|
+
'g',
|
|
69
|
+
);
|
|
70
|
+
const CONTROL_WORDS = /\b(enforc\w*|gates?|gated|gating|guards?|guarded|blocks?|blocked|refuses?|refused|hard[- ]?fails?|fails? (?:the )?(?:build|ci|merge|gate)|compensating control|pre-commit|pre-push|scans?|scanned|patrol\w*|audits?)\b/i;
|
|
71
|
+
const HISTORY_WORDS = /\b(removed|deleted|retired|superseded|no longer|used to|never existed|did not exist|never survived|dropped|replaced|gone|historical|legacy|formerly|renamed)\b/i;
|
|
72
|
+
const INACTIVE_STATUS_RE = /^\s*\**\s*status\s*\**\s*:?\s*\**\s*:?\s*\**\s*(superseded|deprecated|rejected|withdrawn)/im;
|
|
73
|
+
const SKIP_PREFIXES = ['docs/session-logs/', 'docs/audits/', 'limitations/'];
|
|
74
|
+
|
|
75
|
+
// `${doc}::${artifact}` -> why this present-tense mention is not a phantom control.
|
|
76
|
+
const ALLOW = new Map([
|
|
77
|
+
['docs/adr/0025-structured-criterion-task-link.md::scripts/gds/seed-v3-tasks.js',
|
|
78
|
+
'describes the grep a session HAD to do before this ADR; a seed script, not a control'],
|
|
79
|
+
['docs/adr/0042-builder-self-deploy-ci-auto-merge.md::.github/workflows/grade-gate.yml',
|
|
80
|
+
'the CI grader gate this ADR designed; it was armed in the grader-off variant its own Status line records, and grading runs server-side (scripts/gds/ci-grade.js)'],
|
|
81
|
+
['docs/adr/0111-instance-hosting-provisioning-module.md::scripts/gds/box.js',
|
|
82
|
+
'the dev-box control-plane runner, retired with dev boxes by ADR 0346 (dev-box-guard.js keeps it out)'],
|
|
83
|
+
['docs/adr/0150-box-first-boot-bringup-vendored-instances.md::scripts/gds/box.js',
|
|
84
|
+
'the dev-box control-plane runner, retired with dev boxes by ADR 0346 (dev-box-guard.js keeps it out)'],
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
// Sentence-ish units: prose sentences, blank-line paragraphs, and list/table rows.
|
|
88
|
+
function sentencesOf(src) {
|
|
89
|
+
return String(src).split(/(?<=[.!?])\s+|\n\s*\n|\n(?=\s*(?:[-*|]|\d+\.)\s)/);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Pure over its deps: docs is a list of repo-relative .md paths.
|
|
93
|
+
function findPhantomClaims(docs, { read, exists }) {
|
|
94
|
+
const claims = [];
|
|
95
|
+
for (const doc of docs) {
|
|
96
|
+
if (SKIP_PREFIXES.some((p) => doc.startsWith(p))) continue;
|
|
97
|
+
const src = read(doc);
|
|
98
|
+
if (src == null) continue;
|
|
99
|
+
if (INACTIVE_STATUS_RE.test(src.slice(0, 2000))) continue;
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
for (const s of sentencesOf(src)) {
|
|
102
|
+
// Judge the WORDING with every code span blanked: a path's own name must not
|
|
103
|
+
// decide the tense (`gone.yml` reads as "gone", `guard.js` as "guard").
|
|
104
|
+
const prose = s.replace(/`[^`]*`/g, ' ');
|
|
105
|
+
for (const m of s.matchAll(CONTROL_PATH_RE)) {
|
|
106
|
+
const artifact = m[1];
|
|
107
|
+
if (seen.has(artifact) || exists(artifact)) continue;
|
|
108
|
+
if (!CONTROL_WORDS.test(prose) || HISTORY_WORDS.test(prose)) continue;
|
|
109
|
+
seen.add(artifact);
|
|
110
|
+
claims.push({ doc, artifact, sentence: s.replace(/\s+/g, ' ').trim().slice(0, 160) });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return claims;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function evaluate(docs, deps, allow = ALLOW) {
|
|
118
|
+
const claims = findPhantomClaims(docs, deps);
|
|
119
|
+
const hit = new Set();
|
|
120
|
+
const violations = [];
|
|
121
|
+
for (const c of claims) {
|
|
122
|
+
const key = `${c.doc}::${c.artifact}`;
|
|
123
|
+
if (allow.has(key)) { hit.add(key); continue; }
|
|
124
|
+
violations.push(`${c.doc}: names \`${c.artifact}\` as a live control, and it does not exist — "${c.sentence}"`);
|
|
125
|
+
}
|
|
126
|
+
for (const key of allow.keys()) {
|
|
127
|
+
if (!hit.has(key)) violations.push(`stale allowance: ${key} no longer matches a claim — delete it from ALLOW in scripts/gds/doc-control-claims.js`);
|
|
128
|
+
}
|
|
129
|
+
return { violations, claims };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const readRel = (rel) => { try { return fs.readFileSync(path.join(ROOT, rel), 'utf8'); } catch { return null; } };
|
|
133
|
+
const existsRel = (rel) => fs.existsSync(path.join(ROOT, rel));
|
|
134
|
+
|
|
135
|
+
function checkDocControlClaims({ files = null } = {}) {
|
|
136
|
+
const docs = files || trackedMarkdown();
|
|
137
|
+
// A lint that reports on nothing is worse than one that fails (docs-entropy).
|
|
138
|
+
if (!docs.length) {
|
|
139
|
+
return { name: NAME, ok: false, hardFail: true, warnings: [],
|
|
140
|
+
violations: ['scan defect — enumerated 0 tracked markdown files; a broken enumeration, not a clean result.'],
|
|
141
|
+
note: 'static scan of enforcement artifacts named in docs.' };
|
|
142
|
+
}
|
|
143
|
+
const { violations, claims } = evaluate(docs, { read: readRel, exists: existsRel });
|
|
144
|
+
return {
|
|
145
|
+
name: NAME,
|
|
146
|
+
ok: violations.length === 0,
|
|
147
|
+
hardFail: violations.length > 0,
|
|
148
|
+
violations,
|
|
149
|
+
warnings: [],
|
|
150
|
+
note: violations.length
|
|
151
|
+
? 'restore the control, correct the doc to name what really enforces it, or (for an accepted ADR a newer one retired) add an ALLOW entry saying which'
|
|
152
|
+
: `${docs.length} tracked .md file(s) scanned; every enforcement artifact named in a present-tense control sentence exists (${claims.length} allowlisted, each with its reason).`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function main() {
|
|
157
|
+
const r = checkDocControlClaims();
|
|
158
|
+
for (const v of r.violations) console.error(`✗ ${v}`);
|
|
159
|
+
console.log(`${r.hardFail ? 'FAIL' : 'PASS'} ${r.note}`);
|
|
160
|
+
process.exit(r.hardFail ? 1 : 0);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (require.main === module) main();
|
|
164
|
+
|
|
165
|
+
module.exports = {
|
|
166
|
+
checkDocControlClaims, findPhantomClaims, evaluate, sentencesOf,
|
|
167
|
+
ALLOW, GIT_HOOK_NAMES, CONTROL_PATH_RE, CONTROL_WORDS, HISTORY_WORDS, INACTIVE_STATUS_RE,
|
|
168
|
+
};
|
package/scripts/gds/fitness.js
CHANGED
|
@@ -1342,6 +1342,7 @@ const CHECKS = [
|
|
|
1342
1342
|
checkGovernmentRenameIdentity, checkPrelaunchVocabulary, // 19c task 1003120 (`X`→`X` rename arrow) + 19d task 1003152 ("stealth" is the product's word) — sharing a line: this file is AT the 1,500 budget (Check 23/24 precedent below; task 1003825 buys it back)
|
|
1343
1343
|
checkWriteRoutesValidated, // Check 18 — BV1 R12 / task 1999: every body-reading write route validates (ADR 0118)
|
|
1344
1344
|
require('./fitness-ratchets.js').checkQualityRatchets, // Check 20 — task 1003129: quality budgets only tighten (mechanics + knip CI step live in fitness-ratchets.js)
|
|
1345
|
+
require('./doc-control-claims.js').checkDocControlClaims, // Check 37 — task 1003418 / audit A14: a doc cannot describe an enforcement control that does not exist (derived from the docs, not a hand list)
|
|
1345
1346
|
require('./doc-cli-guard.js').checkDocNamesRealCli, // Check 22 — task 1003165 / G01: a doc that names a CLI invocation must match the real CLI
|
|
1346
1347
|
require('./skill-preflight.js').checkOpsSkillsDeclareMachine, // Check 23 — task 1003167 / G03: an ops skill declares the machine it needs
|
|
1347
1348
|
// Check 24 — task 1003166 / G02: no ops script hardcodes a version id. It landed
|
|
@@ -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
|
|
@@ -435,7 +435,7 @@ const MODULE_GLOBS = {
|
|
|
435
435
|
'scripts/gds/skill-lint.js', 'scripts/gds/run-routine.js',
|
|
436
436
|
'scripts/gds/adr-namespace.js', 'scripts/gds/baseline-staleness.js',
|
|
437
437
|
'scripts/gds/category-advisory-guard.js', 'scripts/gds/client-baseurl-guard.js',
|
|
438
|
-
'scripts/gds/doc-cli-guard.js', 'scripts/gds/doc-comment-coverage.js',
|
|
438
|
+
'scripts/gds/doc-cli-guard.js', 'scripts/gds/doc-control-claims.js', 'scripts/gds/doc-comment-coverage.js',
|
|
439
439
|
'scripts/gds/exec-path-guard.js', 'scripts/gds/fitness-lib.js',
|
|
440
440
|
'scripts/gds/fitness-checks-error-envelope.js', 'scripts/gds/fitness-checks-identity.js',
|
|
441
441
|
'scripts/gds/fitness-checks-packaging.js', 'scripts/gds/fitness-checks-route-pins.js',
|
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.4'; // 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');
|