@bongos/core 1.20.44 → 1.20.45
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 +103 -63
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +5 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/api/openapi.json +56 -3
- package/docs/api-reference.md +7 -1
- package/docs/copy-inventory.md +46 -16
- package/docs/copy-registry.json +351 -20
- package/docs/module-api-changelog.md +2 -0
- package/docs/onboarding/slash-commands.md +12 -56
- package/docs/page-inventory.json +33 -4
- package/docs/page-readings.json +992 -957
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands-lib.js +142 -0
- package/modules/hall-ui/public/commands.css +98 -0
- package/modules/hall-ui/public/commands.html +65 -0
- package/modules/hall-ui/public/commands.js +52 -0
- package/modules/hall-ui/public/commands.states.json +14 -0
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +3 -0
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/hall-ui/records/commands.md +11 -0
- package/modules/specialities/skills.js +6 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/bongos/routes/cli-commands.js +49 -0
- package/src/bongos/routes.js +3 -0
- package/src/bongos/serve-internal.js +4 -1
- package/src/module-api.js +1 -1
- package/tests/hall_commands.mjs +162 -0
- package/tests/hall_nav.mjs +3 -1
- package/tests/hall_page_gate_map.mjs +3 -0
- package/tests/hall_tweak_editor.mjs +1 -1
- package/tests/nav_permission_atoms.mjs +1 -1
|
@@ -303,6 +303,9 @@
|
|
|
303
303
|
// stays right whatever `branding.currency.label` is set to. Page is public
|
|
304
304
|
// like /ranks — 'signed' is the same cosmetic "show it once you're in".
|
|
305
305
|
{ id: 'economy', label: 'Economy', icon: 'docs', href: '/drachmae', gate: 'signed' },
|
|
306
|
+
// task 1004473: every skill and every bongos CLI verb, read live. Its page and
|
|
307
|
+
// both reads behind it are sign-in gated, so 'signed' is the real floor here.
|
|
308
|
+
{ id: 'commands', label: 'Commands', icon: 'docs', href: '/commands', gate: 'signed' },
|
|
306
309
|
{ id: 'diagrams', label: 'Diagrams', icon: 'docs', href: '/diagrams', gate: 'signed' },
|
|
307
310
|
{ id: 'atlas', label: 'Repo Atlas', icon: 'docs', href: '/atlas', gate: 'member' },
|
|
308
311
|
{ id: 'modules', label: 'Modules', icon: 'docs', href: '/modules', gate: 'member' },
|
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
|
|
177
177
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
178
178
|
|
|
179
|
-
<script src="/builders/shell.js?v=2026-09-
|
|
179
|
+
<script src="/builders/shell.js?v=2026-09-30-commands"></script>
|
|
180
180
|
<script src="/builders/palette.js" defer></script>
|
|
181
181
|
<script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
|
|
182
182
|
<!-- The kit, for its ONE renderer: the full-size ship-time visual figure
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
|
|
58
58
|
<!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
|
|
59
59
|
time so the chrome exists before the deferred page scripts execute. -->
|
|
60
|
-
<script src="/builders/shell.js?v=2026-09-
|
|
60
|
+
<script src="/builders/shell.js?v=2026-09-30-commands"></script>
|
|
61
61
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
62
62
|
<script src="/builders/palette.js" defer></script>
|
|
63
63
|
<!-- shared frontend toolkit (window.OTB) — loaded FIRST so task.js can pull from it. -->
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
</div>
|
|
139
139
|
</div>
|
|
140
140
|
|
|
141
|
-
<script src="/builders/shell.js?v=2026-09-
|
|
141
|
+
<script src="/builders/shell.js?v=2026-09-30-commands"></script>
|
|
142
142
|
<script src="/builders/palette.js" defer></script>
|
|
143
143
|
|
|
144
144
|
<script src="/builders/idea-objects.js?v=2026-09-27-split"></script>
|
|
@@ -140,7 +140,7 @@
|
|
|
140
140
|
|
|
141
141
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
142
142
|
|
|
143
|
-
<script src="/builders/shell.js?v=2026-09-
|
|
143
|
+
<script src="/builders/shell.js?v=2026-09-30-commands"></script>
|
|
144
144
|
<script src="/builders/palette.js" defer></script>
|
|
145
145
|
<script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
|
|
146
146
|
<script src="/builders/brand-holes.js?v=2026-09-29-aqfix"></script>
|
|
@@ -162,7 +162,7 @@
|
|
|
162
162
|
|
|
163
163
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
164
164
|
chrome exists before the page script runs. -->
|
|
165
|
-
<script src="/builders/shell.js?v=2026-09-
|
|
165
|
+
<script src="/builders/shell.js?v=2026-09-30-commands"></script>
|
|
166
166
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
167
167
|
<script src="/builders/palette.js" defer></script>
|
|
168
168
|
<!-- shared frontend toolkit (window.OTB) — loaded before watch.js, which pulls from it. -->
|
|
@@ -171,7 +171,7 @@
|
|
|
171
171
|
|
|
172
172
|
<!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
|
|
173
173
|
time so the chrome exists before the deferred page scripts execute. -->
|
|
174
|
-
<script src="/builders/shell.js?v=2026-09-
|
|
174
|
+
<script src="/builders/shell.js?v=2026-09-30-commands"></script>
|
|
175
175
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
176
176
|
<script src="/builders/palette.js" defer></script>
|
|
177
177
|
<!-- shared frontend toolkit (window.OTB) — loaded FIRST so work.js can pull from it. -->
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# The Commands page, at /commands (task 1004473)
|
|
2
|
+
|
|
3
|
+
**Every skill and every `bongos` CLI verb on one Docs page, read from the live sources so it cannot drift.** The owner asked for it once task 1004471 hid the rarely used skills from Claude's menu: a skill nobody can see in the menu needs somewhere to be discovered. Five things it settled:
|
|
4
|
+
|
|
5
|
+
- **Two reads, no list in the page.** The skills come from `GET /specialities/installed-skills` (the specialities module's `installedSkills()`: core + instance + ENABLED-module skills, so a default-off module such as `pixel-art` or `design-styles` shows only where it is on); the verbs from the new core `GET /cli/commands`, which hands over `bin/bongos.js` `VERBS` grouped by its `GROUPS` — the order `bongos --help` prints. Core may not import a module, which is why the page asks for the two halves separately; each fails on its own, so a missing skills list does not take the CLI table down.
|
|
6
|
+
- **The only hand-written thing is the skill GROUP.** `commands-lib.js` `SKILL_GROUPS` places a skill by name or by the module it came from; a skill no group names lands under "Everything else", so a new skill always gets a row. Rows follow the order the group names them (the daily loop reads start → claim → ship), then A–Z.
|
|
7
|
+
- **"Shown to Claude" vs "Type it yourself" is `disable-model-invocation`.** `skills.js` now carries it (`typedOnly`), and `explainSkill()` reports `listed`. Only the literal `true` hides a skill, which is how Claude Code reads the key.
|
|
8
|
+
- **Sign-in is the whole gate.** The page joins `SIGNEDIN_BUILDERS_PAGE_RE`, its nav item is `signed` in Docs, and both reads are `requireBuilder`. Reading the list grants nothing (ADR 0310 §1): every command still meets the server's own rank checks when it runs.
|
|
9
|
+
- **At 760px and below each row is a small card.** Five columns cannot fit a phone and a sideways-scrolling table hides the costs, so `commands.css` stacks the cells under their column names (`data-label`). The CLI tables are `table-layout: fixed` with a 220px command column so the help lines align across the three groups.
|
|
10
|
+
|
|
11
|
+
Tests: `tests/hall_commands.mjs` (every SKILL.md and every VERBS entry exactly once, against the real tree; the typed-only marker per skill; the default-off modules absent; the route's gate). `docs/onboarding/slash-commands.md` now points here for the project's own list instead of keeping a second, hand-typed one.
|
|
@@ -92,6 +92,10 @@ function collect(out, dir, source) {
|
|
|
92
92
|
const entry = { name, description: typeof fm.description === 'string' ? fm.description : '', source };
|
|
93
93
|
for (const [key, field] of Object.entries(PEOPLE_KEYS)) if (typeof fm[key] === 'string' && fm[key]) entry[field] = fm[key];
|
|
94
94
|
if (Array.isArray(fm.requires) && fm.requires.length) entry.requires = fm.requires;
|
|
95
|
+
// `disable-model-invocation: true` (task 1004471) keeps a skill out of the
|
|
96
|
+
// menu Claude reads; a person still runs it by typing /name. Only the literal
|
|
97
|
+
// `true` hides it, which is how Claude Code reads the key (task 1004473).
|
|
98
|
+
if (fm['disable-model-invocation'] === 'true') entry.typedOnly = true;
|
|
95
99
|
out.set(name, entry);
|
|
96
100
|
}
|
|
97
101
|
}
|
|
@@ -150,6 +154,8 @@ function explainSkill(entry) {
|
|
|
150
154
|
commands,
|
|
151
155
|
needs: Array.isArray(e.requires) ? e.requires : [],
|
|
152
156
|
rank,
|
|
157
|
+
// Shown to Claude (it can start the skill from plain English) vs typed-only.
|
|
158
|
+
listed: e.typedOnly !== true,
|
|
153
159
|
derived: { does: !e.plain, reachFor: !e.reachFor, cost: !e.cost },
|
|
154
160
|
};
|
|
155
161
|
}
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.45",
|
|
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.45",
|
|
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.45",
|
|
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
|
@@ -8377,5 +8377,11 @@
|
|
|
8377
8377
|
"id": "1004365",
|
|
8378
8378
|
"text": "Every store module now comes with a how-to page, and the store refuses to publish one without it."
|
|
8379
8379
|
}
|
|
8380
|
+
],
|
|
8381
|
+
"1.20.45": [
|
|
8382
|
+
{
|
|
8383
|
+
"id": "1004473",
|
|
8384
|
+
"text": "The Builders Hall now has a Commands page listing every slash command and every terminal command, with what each does, when to use it and what it costs, including the ones hidden from Claude's menu."
|
|
8385
|
+
}
|
|
8380
8386
|
]
|
|
8381
8387
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// src/bongos/routes/cli-commands.js — every `bongos <verb>` the CLI answers,
|
|
2
|
+
// for the hall's Commands page (task 1004473).
|
|
3
|
+
//
|
|
4
|
+
// WHY A ROUTE. The page is the one place a builder sees every skill AND every
|
|
5
|
+
// CLI command, and both halves must be read from their live source so the list
|
|
6
|
+
// cannot drift from what the tools actually do. The verb table is bin/bongos.js
|
|
7
|
+
// `VERBS` (grouped by its `GROUPS`, the order `bongos --help` prints); the
|
|
8
|
+
// browser cannot read that file, so this hands it over as data. The skills half
|
|
9
|
+
// comes from the specialities module's GET /specialities/installed-skills —
|
|
10
|
+
// core must not import a module, so the page asks for the two halves separately.
|
|
11
|
+
//
|
|
12
|
+
// Reading the list grants nothing (ADR 0310 §1): every verb still runs against
|
|
13
|
+
// the server's own rank checks. Sign-in is the floor only because the page it
|
|
14
|
+
// feeds sits on the hall's signed-in tier.
|
|
15
|
+
|
|
16
|
+
const express = require('express');
|
|
17
|
+
const auth = require('../auth');
|
|
18
|
+
// Side-effect-free: bin/bongos.js only dispatches when it is the main module.
|
|
19
|
+
const cli = require('../../../bin/bongos.js');
|
|
20
|
+
|
|
21
|
+
/** The CLI's verbs as `bongos --help` groups them: [{ key, label, commands: [{ verb, summary }] }]. */
|
|
22
|
+
function cliCommands(source = cli) {
|
|
23
|
+
const verbs = Object.entries(source.VERBS || {});
|
|
24
|
+
const groups = (source.GROUPS || []).map(([key, label]) => ({
|
|
25
|
+
key,
|
|
26
|
+
label,
|
|
27
|
+
commands: verbs.filter(([, def]) => def.group === key).map(([verb, def]) => ({ verb, summary: def.summary })),
|
|
28
|
+
}));
|
|
29
|
+
// A verb whose group `GROUPS` does not name still exists, so it still gets a row.
|
|
30
|
+
const known = new Set(groups.map((g) => g.key));
|
|
31
|
+
const stray = verbs.filter(([, def]) => !known.has(def.group));
|
|
32
|
+
if (stray.length) groups.push({ key: 'other', label: 'Other', commands: stray.map(([verb, def]) => ({ verb, summary: def.summary })) });
|
|
33
|
+
return groups.filter((g) => g.commands.length);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildCliCommandsRouter() {
|
|
37
|
+
const router = express.Router();
|
|
38
|
+
|
|
39
|
+
// GET /cli/commands — the bongos CLI's verbs, grouped like `bongos --help`.
|
|
40
|
+
// rank: any builder — read-only reference; the hall's Commands page is sign-in gated.
|
|
41
|
+
router.get('/cli/commands', auth.requireBuilder, (_req, res) => {
|
|
42
|
+
res.json({ version: cli.VERSION, groups: cliCommands() });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return router;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = buildCliCommandsRouter;
|
|
49
|
+
module.exports.cliCommands = cliCommands;
|
package/src/bongos/routes.js
CHANGED
|
@@ -56,6 +56,8 @@ const buildSecurityRouter = require('./routes/security');
|
|
|
56
56
|
// the submit-for-review front door. Core (not module-owned): it manages
|
|
57
57
|
// EVERY module's enablement, including modules it must not import.
|
|
58
58
|
const buildModulesRouter = require('./routes/modules');
|
|
59
|
+
// The hall Commands page's CLI half (task 1004473): bin/bongos.js VERBS as data.
|
|
60
|
+
const buildCliCommandsRouter = require('./routes/cli-commands');
|
|
59
61
|
// The access-request ADMISSION routes carved to modules/onboarding/ (BV1.R81 /
|
|
60
62
|
// ADR 0093 §1); moduleLoader.mountModuleRoutes (below) mounts them via the
|
|
61
63
|
// onboarding module's route factory — no core require.
|
|
@@ -159,6 +161,7 @@ function buildGdsRouter() {
|
|
|
159
161
|
router.use(buildAuditLogRouter());
|
|
160
162
|
router.use(buildSecurityRouter());
|
|
161
163
|
router.use(buildModulesRouter());
|
|
164
|
+
router.use(buildCliCommandsRouter());
|
|
162
165
|
// access-requests (admission) is now mounted by the onboarding module via
|
|
163
166
|
// moduleLoader.mountModuleRoutes below (BV1.R81) — still BEFORE the public
|
|
164
167
|
// router, so its paths are byte-identical. The /sessions data plane is
|
|
@@ -1210,8 +1210,11 @@ function mountInternalSurfaces(app) {
|
|
|
1210
1210
|
// own room beside the studio, on the studio's tier. Its read (GET
|
|
1211
1211
|
// /copy-desk/pages/:pageId/draft) is any-builder like the .docx download, and
|
|
1212
1212
|
// every write is the page claim's holder only, checked server-side.
|
|
1213
|
+
// task 1004473: `commands`, the Commands page — every skill and every bongos CLI
|
|
1214
|
+
// verb. Both reads behind it are requireBuilder, so it sits on this tier;
|
|
1215
|
+
// reading the list grants nothing (ADR 0310 §1).
|
|
1213
1216
|
const SIGNEDIN_BUILDERS_PAGE_RE =
|
|
1214
|
-
/^\/builders\/(?:work|settings|atlas|modules|goals|goal-map|roadmap|copy-desk|studio|tweak-editor|ideas|thinking|collab|task|idea|primer(?:\.md)?|diagrams)(?:\.html)?\/?$/;
|
|
1217
|
+
/^\/builders\/(?:work|settings|atlas|modules|goals|goal-map|roadmap|copy-desk|studio|tweak-editor|ideas|thinking|collab|task|idea|primer(?:\.md)?|diagrams|commands)(?:\.html)?\/?$/;
|
|
1215
1218
|
// The diagram image assets under /builders/diagrams/ ride the same tier as the
|
|
1216
1219
|
// page that embeds them.
|
|
1217
1220
|
const SIGNEDIN_BUILDERS_PREFIX_RE = /^\/builders\/diagrams\//;
|
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.45'; // 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');
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// tests/hall_commands.mjs — the hall's Commands page (task 1004473): one table
|
|
2
|
+
// of every skill and one of every bongos CLI verb, read from live sources.
|
|
3
|
+
//
|
|
4
|
+
// The done-when pins: every SKILL.md the instance has, and every bin/bongos.js
|
|
5
|
+
// VERBS entry, appears on the page EXACTLY once. They are checked against the
|
|
6
|
+
// real tree — the same installedSkills()+explainSkill() the
|
|
7
|
+
// /specialities/installed-skills route serves, and the same cliCommands() the
|
|
8
|
+
// /cli/commands route serves — rendered through the page's own lib.
|
|
9
|
+
//
|
|
10
|
+
// Run: node --test tests/hall_commands.mjs
|
|
11
|
+
|
|
12
|
+
import { strict as assert } from 'node:assert';
|
|
13
|
+
import { test } from 'node:test';
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import http from 'node:http';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
22
|
+
const lib = require('../modules/hall-ui/public/commands-lib.js');
|
|
23
|
+
const kit = require('../modules/hall-ui/public/hall-kit.js');
|
|
24
|
+
const skills = require('../modules/specialities/skills.js');
|
|
25
|
+
const { VERBS, GROUPS } = require('../bin/bongos.js');
|
|
26
|
+
const esc = kit.esc;
|
|
27
|
+
|
|
28
|
+
// Every SKILL.md on disk, by name: core .claude/skills/ plus every module's skills/.
|
|
29
|
+
function skillDirsOnDisk({ modules = () => true } = {}) {
|
|
30
|
+
const out = [];
|
|
31
|
+
const scan = (dir) => {
|
|
32
|
+
let names = [];
|
|
33
|
+
try { names = fs.readdirSync(dir); } catch { return; }
|
|
34
|
+
for (const n of names) if (fs.existsSync(path.join(dir, n, 'SKILL.md'))) out.push({ name: n, md: path.join(dir, n, 'SKILL.md') });
|
|
35
|
+
};
|
|
36
|
+
scan(path.join(ROOT, '.claude', 'skills'));
|
|
37
|
+
for (const key of fs.readdirSync(path.join(ROOT, 'modules'))) if (modules(key)) scan(path.join(ROOT, 'modules', key, 'skills'));
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// What the route hands the page: installedSkills() explained, exactly as
|
|
42
|
+
// GET /specialities/installed-skills builds it.
|
|
43
|
+
function servedSkills(isModuleEnabled) {
|
|
44
|
+
return [...skills.installedSkills({ coreRoot: ROOT, isModuleEnabled })]
|
|
45
|
+
.map(([name, entry]) => skills.explainSkill({ name, ...entry }));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const count = (html, attr, value) => html.split(`${attr}="${value}"`).length - 1;
|
|
49
|
+
|
|
50
|
+
test('every SKILL.md in the tree appears on the page exactly once (every module on)', () => {
|
|
51
|
+
const html = lib.skillsTableHtml(lib.groupSkills(servedSkills(() => true)), { esc });
|
|
52
|
+
const onDisk = skillDirsOnDisk();
|
|
53
|
+
assert.ok(onDisk.length >= 48, `expected the real skill tree, found ${onDisk.length}`);
|
|
54
|
+
for (const { name } of onDisk) assert.equal(count(html, 'data-skill', name), 1, `/${name} should appear exactly once`);
|
|
55
|
+
assert.equal(count(html, 'data-skill', ''), 0, 'no nameless row');
|
|
56
|
+
assert.equal(html.split('data-skill="').length - 1, new Set(onDisk.map((s) => s.name)).size, 'no row that is not a SKILL.md');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('a default instance shows its enabled modules\' skills and NOT a disabled module\'s', () => {
|
|
60
|
+
const defaults = (key) => {
|
|
61
|
+
try { return JSON.parse(fs.readFileSync(path.join(ROOT, 'modules', key, 'module.json'), 'utf8')).default === true; } catch { return false; }
|
|
62
|
+
};
|
|
63
|
+
const html = lib.skillsTableHtml(lib.groupSkills(servedSkills(defaults)), { esc });
|
|
64
|
+
for (const { name } of skillDirsOnDisk({ modules: defaults })) assert.equal(count(html, 'data-skill', name), 1, `/${name} should appear exactly once`);
|
|
65
|
+
for (const { name } of skillDirsOnDisk({ modules: (k) => k === 'pixel-art' || k === 'design-styles' })) {
|
|
66
|
+
if (fs.existsSync(path.join(ROOT, '.claude', 'skills', name, 'SKILL.md'))) continue;
|
|
67
|
+
assert.equal(count(html, 'data-skill', name), 0, `/${name} ships in a default-off module, so it must not be listed`);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('each row carries the SKILL.md plain / reach-for / cost words', () => {
|
|
72
|
+
const served = servedSkills(() => true);
|
|
73
|
+
const html = lib.skillsTableHtml(lib.groupSkills(served), { esc });
|
|
74
|
+
const ship = served.find((s) => s.name === 'builder-ship');
|
|
75
|
+
const fm = skills.parseFrontmatter(fs.readFileSync(path.join(ROOT, '.claude/skills/builder-ship/SKILL.md'), 'utf8'));
|
|
76
|
+
assert.equal(ship.does, fm.plain);
|
|
77
|
+
assert.equal(ship.reachFor, fm['reach-for']);
|
|
78
|
+
assert.equal(ship.cost, fm.cost);
|
|
79
|
+
for (const v of [fm.plain, fm['reach-for'], fm.cost]) assert.ok(html.includes(esc(v)), `page shows: ${v}`);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('the "type it yourself" marker follows disable-model-invocation, skill by skill', () => {
|
|
83
|
+
const served = servedSkills(() => true);
|
|
84
|
+
const html = lib.skillsTableHtml(lib.groupSkills(served), { esc });
|
|
85
|
+
let typed = 0;
|
|
86
|
+
for (const { name, md } of skillDirsOnDisk()) {
|
|
87
|
+
const hidden = skills.parseFrontmatter(fs.readFileSync(md, 'utf8'))['disable-model-invocation'] === 'true';
|
|
88
|
+
if (hidden) typed++;
|
|
89
|
+
const row = html.slice(html.indexOf(`data-skill="${name}"`)).split('</tr>')[0];
|
|
90
|
+
assert.match(row, hidden ? /cmd-how--typed/ : /cmd-how--listed/, `/${name}: ${hidden ? 'typed-only' : 'shown to Claude'}`);
|
|
91
|
+
}
|
|
92
|
+
assert.ok(typed > 0, 'the tree has typed-only skills (task 1004471), so the marker is exercised');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('a skill no group names still gets a row, under Everything else', () => {
|
|
96
|
+
const groups = lib.groupSkills([{ name: 'brand-new-skill', source: 'core', does: 'x' }, { name: 'builder-ship', source: 'core' }]);
|
|
97
|
+
assert.deepEqual(groups.map((g) => g.key), ['loop', 'other']);
|
|
98
|
+
assert.equal(lib.groupKeyOf({ name: 'otb-tile-generate', source: 'module:pixel-art' }), 'design', 'a module\'s skills land in its group');
|
|
99
|
+
const loop = lib.groupSkills([{ name: 'builder-ship' }, { name: 'builder-claim' }, { name: 'builder-start' }].map((x) => ({ ...x, source: 'core' })));
|
|
100
|
+
assert.deepEqual(loop[0].skills.map((x) => x.name), ['builder-start', 'builder-claim', 'builder-ship'], 'the loop reads in the order you work it');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('row text is escaped — a skill cannot inject markup into the hall', () => {
|
|
104
|
+
const html = lib.skillsTableHtml(lib.groupSkills([{ name: 'x', source: 'core', does: '<img src=x onerror=alert(1)>' }]), { esc });
|
|
105
|
+
assert.ok(!html.includes('<img'), html);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const { cliCommands } = require('../src/bongos/routes/cli-commands.js');
|
|
109
|
+
|
|
110
|
+
test('every bin/bongos.js VERBS entry appears exactly once, grouped like `bongos --help`', () => {
|
|
111
|
+
const groups = cliCommands();
|
|
112
|
+
const html = lib.cliTableHtml(groups, { esc });
|
|
113
|
+
for (const verb of Object.keys(VERBS)) assert.equal(count(html, 'data-verb', verb), 1, `bongos ${verb} should appear exactly once`);
|
|
114
|
+
assert.equal(html.split('data-verb="').length - 1, Object.keys(VERBS).length, 'no row that is not a verb');
|
|
115
|
+
assert.deepEqual(groups.map((g) => g.label), GROUPS.map(([, label]) => label), 'the help\'s group order');
|
|
116
|
+
for (const g of groups) for (const c of g.commands) assert.equal(c.summary, VERBS[c.verb].summary, `${c.verb}: the help line, verbatim`);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('a verb in a group GROUPS does not name is still listed', () => {
|
|
120
|
+
const groups = cliCommands({ GROUPS: [['work', 'Work']], VERBS: { a: { group: 'work', summary: 'A' }, b: { group: 'new', summary: 'B' } } });
|
|
121
|
+
assert.deepEqual(groups.map((g) => [g.key, g.commands.map((c) => c.verb)]), [['work', ['a']], ['other', ['b']]]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('GET /cli/commands serves the verbs to a signed-in builder and refuses a stranger', async () => {
|
|
125
|
+
const auth = require('../src/bongos/auth.js');
|
|
126
|
+
const express = require('express');
|
|
127
|
+
const build = require('../src/bongos/routes/cli-commands.js');
|
|
128
|
+
const serve = async (app, fn) => {
|
|
129
|
+
const server = http.createServer(app);
|
|
130
|
+
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
131
|
+
try { return await fn(`http://127.0.0.1:${server.address().port}`); } finally { server.close(); }
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const saved = auth.requireBuilder;
|
|
135
|
+
auth.requireBuilder = (req, _res, next) => { req.builder = { id: 1, rank: 'xenos' }; next(); };
|
|
136
|
+
let signedIn;
|
|
137
|
+
try {
|
|
138
|
+
const app = express();
|
|
139
|
+
app.use(build());
|
|
140
|
+
signedIn = await serve(app, async (base) => { const r = await fetch(`${base}/cli/commands`); return { status: r.status, body: await r.json() }; });
|
|
141
|
+
} finally { auth.requireBuilder = saved; }
|
|
142
|
+
assert.equal(signedIn.status, 200);
|
|
143
|
+
assert.deepEqual(signedIn.body.groups.flatMap((g) => g.commands.map((c) => c.verb)).sort(), Object.keys(VERBS).sort());
|
|
144
|
+
|
|
145
|
+
const gated = express();
|
|
146
|
+
gated.use((req, res, next) => { res.fail = (code, st) => res.status(typeof st === 'number' ? st : 401).json({ error: code }); next(); });
|
|
147
|
+
gated.use(build());
|
|
148
|
+
const stranger = await serve(gated, async (base) => (await fetch(`${base}/cli/commands`)).status);
|
|
149
|
+
assert.ok(stranger === 401 || stranger === 403, `no session → refused (got ${stranger})`);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('the page is wired: nav entry in Docs, sign-in gated, both reads, no hand-typed command', () => {
|
|
153
|
+
const page = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/commands.html'), 'utf8');
|
|
154
|
+
const js = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/commands.js'), 'utf8');
|
|
155
|
+
assert.match(js, /\/api\/bongos\/specialities\/installed-skills/);
|
|
156
|
+
assert.match(js, /\/api\/bongos\/cli\/commands/);
|
|
157
|
+
assert.ok(!/data-skill=|data-verb=|\/builder-ship|bongos claim/.test(page), 'the HTML names no command — the rows are generated');
|
|
158
|
+
const shell = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/shell.js'), 'utf8');
|
|
159
|
+
assert.match(shell, /id: 'commands', label: 'Commands', icon: 'docs', href: '\/commands', gate: 'signed'/);
|
|
160
|
+
const serveInternal = fs.readFileSync(path.join(ROOT, 'src/bongos/serve-internal.js'), 'utf8');
|
|
161
|
+
assert.match(serveInternal.match(/const SIGNEDIN_BUILDERS_PAGE_RE =\s*(\/.*\/);/)[1], /\|commands\)/);
|
|
162
|
+
});
|
package/tests/hall_nav.mjs
CHANGED
|
@@ -285,8 +285,10 @@ test('sidebar groups read by audience, in order', () => {
|
|
|
285
285
|
// what made this group read as one real page plus a link into the home page.
|
|
286
286
|
assert.deepEqual(nav[2].items.map((i) => i.id), ['collab', 'roster'], 'Community = everyone else, in this project');
|
|
287
287
|
assert.ok(!nav.flatMap((g) => g.items).some((i) => /^\/people\b/.test(i.href)), 'no nav item points at the deleted /people');
|
|
288
|
+
// task 1004473: 'commands' — every skill and CLI verb, read live — is reference
|
|
289
|
+
// material that reads the same for every builder, so it joins Docs.
|
|
288
290
|
assert.deepEqual(nav[3].items.map((i) => i.id),
|
|
289
|
-
['primer', 'ranks', 'economy', 'diagrams', 'atlas', 'modules', 'apidocs'], 'Docs = reference material');
|
|
291
|
+
['primer', 'ranks', 'economy', 'commands', 'diagrams', 'atlas', 'modules', 'apidocs'], 'Docs = reference material');
|
|
290
292
|
// 'government' (the Permissions tab, BV1.R113) is the group's one archon-gated item.
|
|
291
293
|
// 'project-settings' joins the Government group at the metic floor (task 1003280,
|
|
292
294
|
// goal 1000072) — it sits before 'government', which is the one archon-only item.
|
|
@@ -46,6 +46,9 @@ const ORACLE = {
|
|
|
46
46
|
'/builders/settings': 'requireBuilderPage',
|
|
47
47
|
'/builders/atlas': 'requireBuilderPage',
|
|
48
48
|
'/builders/modules': 'requireBuilderPage',
|
|
49
|
+
// task 1004473: the Commands page — its two reads (installed skills, CLI verbs)
|
|
50
|
+
// are requireBuilder, so sign-in is the whole gate.
|
|
51
|
+
'/builders/commands': 'requireBuilderPage',
|
|
49
52
|
'/builders/goals': 'requireBuilderPage',
|
|
50
53
|
// task 1003479: the goal map, on the goal board's tier. task 1004232 (sky part 4):
|
|
51
54
|
// the URL now serves THE PROJECT SKY (the same page as /thinking, in its whole-project
|
|
@@ -635,7 +635,7 @@ try {
|
|
|
635
635
|
assert.match(HTML, /<body data-page="tweak-editor"[^>]*data-fullscreen-switch/);
|
|
636
636
|
assert.equal((HTML.match(/<h1\b/g) || []).length, 1);
|
|
637
637
|
assert.ok(HTML.indexOf('tweak-editor-lib.js') < HTML.indexOf('tweak-editor.js?'), 'the lib loads first');
|
|
638
|
-
assert.match(HTML, /shell\.js\?v=2026-09-
|
|
638
|
+
assert.match(HTML, /shell\.js\?v=2026-09-30-commands/);
|
|
639
639
|
assert.equal(/fonts\.googleapis\.com\/css/.test(HTML), false);
|
|
640
640
|
for (const id of ['tw-bar', 'tw-counter', 'tw-open', 'tw-open-more', 'tw-divider', 'tw-openwrap', 'tw-open-menu', 'tw-open-tab', 'tw-open-split', 'tw-frame', 'tw-frame-view', 'tw-split-close', 'tw-download', 'tw-upload', 'tw-file', 'tw-submit', 'tw-paper', 'tw-doc', 'tw-notice', 'tw-done', 'tw-take', 'tw-pick', 'tw-picks', 'tw-refused', 'tw-refused-title', 'tw-refused-list', 'tw-refused-next', 'tw-refused-download', 'tw-refused-close']) {
|
|
641
641
|
assert.ok(new RegExp(`id="${id}"`).test(HTML), `#${id} is in the markup`);
|
|
@@ -87,7 +87,7 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
|
|
|
87
87
|
// the file and left the query alone, so a returning browser kept the cached copy —
|
|
88
88
|
// a fix that reaches nobody looks identical to a fix that does not work.
|
|
89
89
|
|
|
90
|
-
const SHELL_SHA = '
|
|
90
|
+
const SHELL_SHA = '4e14115a3737db9a';
|
|
91
91
|
|
|
92
92
|
test('shell.js has not changed without its cache-bust being bumped', () => {
|
|
93
93
|
const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);
|