@bongos/core 1.19.483 → 1.19.485
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 +64 -44
- package/.claude/skills/builder-setup/SKILL.md +2 -0
- package/.claude/skills/design/SKILL.md +2 -1
- package/docs/copy-inventory.md +163 -175
- package/docs/copy-registry.json +265 -402
- package/docs/file-map.md +1 -1
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/not-ready.html +14 -10
- package/modules/hall-ui/public/not-ready.states.json +14 -0
- package/modules/hall-ui/public/pair.css +17 -67
- package/modules/hall-ui/public/pair.html +8 -1
- package/modules/hall-ui/public/pair.states.json +14 -0
- package/modules/hall-ui/public/panel.css +333 -0
- package/modules/hall-ui/public/profile.css +85 -0
- package/modules/hall-ui/public/profile.html +12 -5
- package/modules/hall-ui/public/profile.states.json +15 -0
- package/modules/hall-ui/public/settings.css +119 -336
- package/modules/hall-ui/public/settings.html +28 -19
- package/modules/hall-ui/public/settings.js +44 -35
- package/modules/hall-ui/public/settings.states.json +16 -0
- package/modules/hall-ui/public/style.css +5 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/claim.js +33 -2
- package/scripts/gds/claude-materialize.js +20 -1
- package/scripts/gds/discipline-modes.json +2 -1
- package/scripts/gds/fitness-checks-vocabulary.js +0 -1
- package/src/module-api.js +1 -1
- package/tests/claude_materialize.mjs +76 -0
- package/tests/hall_settings_world.mjs +274 -0
- package/tests/join_application_note.mjs +7 -15
- package/tests/module-scope-map.mjs +1 -1
- package/modules/hall-ui/public/my-projects.css +0 -62
- package/modules/hall-ui/public/my-projects.html +0 -73
- package/modules/hall-ui/public/my-projects.js +0 -153
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
10
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
11
11
|
<link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
|
|
12
|
-
<!--
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
<!-- The PANEL archetype (task 1003308): the row, the switch, the field, the
|
|
13
|
+
chip control and the family's voices, shared with profile, pair and
|
|
14
|
+
not-ready. settings.css then holds only what this page
|
|
15
|
+
has — the rail layout, the group head, the token ladder, the voices grid. -->
|
|
16
|
+
<link rel="stylesheet" href="/builders/panel.css">
|
|
15
17
|
<link rel="stylesheet" href="/builders/settings.css">
|
|
16
18
|
</head>
|
|
17
19
|
<body data-page="settings" data-page-title="Settings">
|
|
@@ -22,15 +24,22 @@
|
|
|
22
24
|
<div class="app-body">
|
|
23
25
|
<header class="app-topbar" id="app-topbar"></header>
|
|
24
26
|
<div class="app-content">
|
|
25
|
-
|
|
27
|
+
<!-- The page's ONE h1 (one-header-main-h1): the page's own name in the
|
|
28
|
+
room voice, because here the document is the page. The sub-line is
|
|
29
|
+
settings.js's — "Your preferences, <name>." and the two failure states. -->
|
|
30
|
+
<div class="page-head page-head--room page-head--panel">
|
|
31
|
+
<h1 id="settings-h1" class="page-head__h">settings</h1>
|
|
32
|
+
<p class="page-head__sub" id="settings-sub">Loading your preferences…</p>
|
|
33
|
+
</div>
|
|
26
34
|
|
|
27
35
|
<main id="hall-main">
|
|
28
36
|
<!-- Not-signed-in notice — shown by settings.js before it redirects, and
|
|
29
37
|
as a fallback if reading the viewer fails. Deliberately OUTSIDE the
|
|
30
38
|
grouped layout: full-width, ungrouped. -->
|
|
31
|
-
<section class="scroll" id="gate-scroll" aria-labelledby="gate-h" hidden>
|
|
32
|
-
<h2 id="gate-h" class="
|
|
33
|
-
<p class="
|
|
39
|
+
<section class="scroll gate" id="gate-scroll" aria-labelledby="gate-h" hidden>
|
|
40
|
+
<h2 id="gate-h" class="gate__h">Not signed in</h2>
|
|
41
|
+
<p class="gate__lede" id="gate-body">You're not signed in yet. Sign in to manage your connections, your keys, your dev box and the way the hall behaves.</p>
|
|
42
|
+
<div class="row-end"><a class="pbtn pbtn--save" href="/api/bongos/auth/web/start">Sign in</a></div>
|
|
34
43
|
</section>
|
|
35
44
|
|
|
36
45
|
<!-- Grouped layout (task 1723 → kit, task 2172): the sections sit in six
|
|
@@ -68,10 +77,11 @@
|
|
|
68
77
|
and the work feed. Your sign-in and the credit for shipped work stay
|
|
69
78
|
tied to your account, not this label.
|
|
70
79
|
</p>
|
|
80
|
+
<label class="field-label" for="display-name-input">Display name</label>
|
|
71
81
|
<div class="display-name-row">
|
|
72
82
|
<input type="text" class="display-name-input" id="display-name-input"
|
|
73
83
|
maxlength="60" autocomplete="off" spellcheck="false"
|
|
74
|
-
|
|
84
|
+
placeholder="Your display name">
|
|
75
85
|
<button type="button" class="craft-save" id="display-name-save">Save</button>
|
|
76
86
|
</div>
|
|
77
87
|
<p class="settings-status" id="display-name-status" role="status" aria-live="polite"></p>
|
|
@@ -158,7 +168,7 @@
|
|
|
158
168
|
</p>
|
|
159
169
|
<div id="sessions-list" class="sessions-list" aria-live="polite"></div>
|
|
160
170
|
<div class="settings-card">
|
|
161
|
-
<button type="button" class="settings-btn" id="sessions-revoke-all">Sign out everywhere</button>
|
|
171
|
+
<button type="button" class="settings-btn settings-btn--quiet" id="sessions-revoke-all">Sign out everywhere</button>
|
|
162
172
|
<span class="settings-card__hint">ends every session including this one — you will be asked to sign in again.</span>
|
|
163
173
|
</div>
|
|
164
174
|
<p class="section-note">Ending a session takes effect immediately; the token stops working on its very next request.</p>
|
|
@@ -213,11 +223,11 @@
|
|
|
213
223
|
<div class="settings-card">
|
|
214
224
|
<a class="settings-btn" href="/downloads/devbox/mac-arm64">Download for Mac</a>
|
|
215
225
|
<a class="settings-btn" href="/downloads/devbox/win-x64">Download for Windows</a>
|
|
216
|
-
<span class="settings-card__hint">Intel Mac? <a href="/downloads/devbox/mac-x64">This one</a>.
|
|
217
|
-
ARM Windows (Surface, Snapdragon, Parallels)? <a href="/downloads/devbox/win-arm64">This one</a>.</span>
|
|
218
|
-
<!-- #924: latest-release courtesy line; populated from /downloads/devbox/version -->
|
|
219
|
-
<span class="settings-card__hint" id="boxapp-latest" hidden></span>
|
|
220
226
|
</div>
|
|
227
|
+
<p class="panel-hint">Intel Mac? <a href="/downloads/devbox/mac-x64">This one</a>.
|
|
228
|
+
ARM Windows (Surface, Snapdragon, Parallels)? <a href="/downloads/devbox/win-arm64">This one</a>.
|
|
229
|
+
<!-- #924: latest-release courtesy line; populated from /downloads/devbox/version -->
|
|
230
|
+
<span id="boxapp-latest" hidden></span></p>
|
|
221
231
|
<p class="section-note">
|
|
222
232
|
First open: your computer warns about an unknown developer — expected
|
|
223
233
|
(we haven't bought the signing certificates yet; the app is ours).
|
|
@@ -252,7 +262,7 @@
|
|
|
252
262
|
claimed task. It never limits the work on the task itself. Your rank
|
|
253
263
|
sets the starting point; newcomers stay locked low.
|
|
254
264
|
</p>
|
|
255
|
-
<div class="
|
|
265
|
+
<div class="prows" id="wander-rows" role="radiogroup" aria-labelledby="wandering-h"></div>
|
|
256
266
|
<p class="settings-status" id="wandering-status" role="status" aria-live="polite"></p>
|
|
257
267
|
<p class="section-note" id="wandering-note"></p>
|
|
258
268
|
</section>
|
|
@@ -287,7 +297,7 @@
|
|
|
287
297
|
<div class="voices-wrap">
|
|
288
298
|
<table class="voices" id="voices-table">
|
|
289
299
|
<thead>
|
|
290
|
-
<tr><th>Skill</th><th>Default</th><th>Your choice</th></tr>
|
|
300
|
+
<tr><th scope="col">Skill</th><th scope="col">Default</th><th scope="col">Your choice</th></tr>
|
|
291
301
|
</thead>
|
|
292
302
|
<tbody id="voices-body"></tbody>
|
|
293
303
|
</table>
|
|
@@ -305,9 +315,8 @@
|
|
|
305
315
|
any you would rather not hear.
|
|
306
316
|
</p>
|
|
307
317
|
<div class="sound-volume" id="sound-volume" hidden>
|
|
308
|
-
<
|
|
309
|
-
<input type="range" id="sound-volume-range" min="0" max="100" step="5" value="100"
|
|
310
|
-
aria-label="Master sound volume">
|
|
318
|
+
<label class="field-label" for="sound-volume-range">Volume</label>
|
|
319
|
+
<input type="range" id="sound-volume-range" min="0" max="100" step="5" value="100">
|
|
311
320
|
<span class="sound-volume__pct" id="sound-volume-pct">100%</span>
|
|
312
321
|
</div>
|
|
313
322
|
<div class="sound-rows" id="sound-rows"></div>
|
|
@@ -360,7 +369,7 @@
|
|
|
360
369
|
<script src="/builders/task-detail.js?v=2026-07-03-taskdetail"></script>
|
|
361
370
|
<!-- #807: shared dev-box panel — loaded before settings.js so OTBBoxPanel exists. -->
|
|
362
371
|
<script src="/builders/box-panel.js?v=2026-06-14-domutils"></script>
|
|
363
|
-
<script src="/builders/settings.js?v=2026-
|
|
372
|
+
<script src="/builders/settings.js?v=2026-09-02-panel"></script>
|
|
364
373
|
<!-- task 1003341: the Active sessions panel. Its own file rather than more of
|
|
365
374
|
settings.js, which sits close to the 1500-line size ratchet. -->
|
|
366
375
|
<script src="/builders/settings-sessions.js?v=2026-08-29-sessions"></script>
|
|
@@ -134,6 +134,15 @@
|
|
|
134
134
|
|
|
135
135
|
// Render one row per sound from `effective`. Each row: label + description and
|
|
136
136
|
// an On/Off toggle button whose data-on reflects the effective enabled-state.
|
|
137
|
+
// The family switch (panel.css `.sw`): a role=switch button whose state is
|
|
138
|
+
// aria-checked, drawn as a track and a knob. Keeps the `.sound-toggle` class
|
|
139
|
+
// and `data-on`, which every delegated click handler on this page reads.
|
|
140
|
+
function switchHtml(on, label, extraAttrs = '') {
|
|
141
|
+
return `<button type="button" class="sound-toggle sw" role="switch" aria-checked="${on ? 'true' : 'false'}" ` +
|
|
142
|
+
`aria-label="${escapeHtml(label)}" data-on="${on ? '1' : '0'}"${extraAttrs}>` +
|
|
143
|
+
'<span class="sw__track"></span><span class="sw__knob"></span></button>';
|
|
144
|
+
}
|
|
145
|
+
|
|
137
146
|
function renderSounds(data) {
|
|
138
147
|
const wrap = $('#sound-rows');
|
|
139
148
|
if (!wrap) return;
|
|
@@ -150,9 +159,7 @@
|
|
|
150
159
|
`<div class="sound-row__label">${escapeHtml(row.label || row.sound)}</div>` +
|
|
151
160
|
`<div class="sound-row__desc">${escapeHtml(row.desc || '')}</div>` +
|
|
152
161
|
'</div>' +
|
|
153
|
-
|
|
154
|
-
`data-on="${on ? '1' : '0'}" aria-pressed="${on ? 'true' : 'false'}">` +
|
|
155
|
-
`${on ? 'On' : 'Off'}</button>` +
|
|
162
|
+
switchHtml(on, `Sound: ${row.label || row.sound}`, ` data-sound="${escapeHtml(row.sound)}"`) +
|
|
156
163
|
'</div>'
|
|
157
164
|
);
|
|
158
165
|
}).join('');
|
|
@@ -405,25 +412,23 @@
|
|
|
405
412
|
wrap.innerHTML = window.OTBKit.emptyStateHtml('No levels registered.');
|
|
406
413
|
return;
|
|
407
414
|
}
|
|
415
|
+
// One pick among four: a radio row per level (panel.css `.prow--pick`), the
|
|
416
|
+
// whole row its label. A level above the rank ceiling is a disabled radio
|
|
417
|
+
// that says so in the label voice rather than a dead "Rank-locked" button.
|
|
408
418
|
wrap.innerHTML = levels.map((lv) => {
|
|
409
419
|
const chosen = lv.n === eff;
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
} else if (chosen) {
|
|
415
|
-
btn = `<button type="button" class="sound-toggle" data-level="${lv.n}" data-on="1" aria-pressed="true">Chosen</button>`;
|
|
416
|
-
} else {
|
|
417
|
-
btn = `<button type="button" class="sound-toggle" data-level="${lv.n}" data-on="0" aria-pressed="false">Choose</button>`;
|
|
418
|
-
}
|
|
420
|
+
const locked = !lv.allowed;
|
|
421
|
+
const id = `wander-${lv.n}`;
|
|
422
|
+
const tags = (lv.n === def ? '<span class="prow__tag">default for your rank</span>' : '') +
|
|
423
|
+
(locked ? '<span class="prow__tag">rank-locked</span>' : '');
|
|
419
424
|
return (
|
|
420
|
-
`<
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
'</
|
|
425
|
+
`<label class="prow prow--pick" for="${id}" data-level="${lv.n}"${locked ? ' aria-disabled="true"' : ''}>` +
|
|
426
|
+
`<input class="pradio" type="radio" name="wandering" id="${id}" value="${lv.n}"` +
|
|
427
|
+
`${chosen ? ' checked' : ''}${locked ? ' disabled' : ''}>` +
|
|
428
|
+
'<span class="prow__main">' +
|
|
429
|
+
`<span class="prow__label">${escapeHtml(lv.label)}${tags}</span>` +
|
|
430
|
+
`<span class="prow__desc">${escapeHtml(lv.blurb || '')}</span>` +
|
|
431
|
+
'</span></label>'
|
|
427
432
|
);
|
|
428
433
|
}).join('');
|
|
429
434
|
const note = $('#wandering-note');
|
|
@@ -466,13 +471,13 @@
|
|
|
466
471
|
const data = res.data;
|
|
467
472
|
renderWandering(data);
|
|
468
473
|
$('#wandering-scroll').hidden = false;
|
|
469
|
-
// Delegate
|
|
474
|
+
// Delegate the change so post-render radios stay wired (single-select).
|
|
470
475
|
const wrap = $('#wander-rows');
|
|
471
|
-
wrap.addEventListener('
|
|
472
|
-
const
|
|
473
|
-
if (!
|
|
474
|
-
const level = Number(
|
|
475
|
-
if (Number.isInteger(level)) saveWandering(level,
|
|
476
|
+
wrap.addEventListener('change', (e) => {
|
|
477
|
+
const r = e.target.closest('.pradio');
|
|
478
|
+
if (!r || r.disabled || !r.checked) return; // ignore locked; a radio only fires on becoming checked
|
|
479
|
+
const level = Number(r.value);
|
|
480
|
+
if (Number.isInteger(level)) saveWandering(level, r);
|
|
476
481
|
});
|
|
477
482
|
} catch (err) {
|
|
478
483
|
console.error('[settings] failed to load wandering', err);
|
|
@@ -497,11 +502,11 @@
|
|
|
497
502
|
'<div class="sound-row" data-render="widgets">' +
|
|
498
503
|
'<div class="sound-row__text">' +
|
|
499
504
|
'<div class="sound-row__label">Rich in-feed cards' +
|
|
500
|
-
(isDefault ? '
|
|
505
|
+
(isDefault ? '<span class="prow__tag">default (on)</span>' : '') +
|
|
501
506
|
'</div>' +
|
|
502
507
|
'<div class="sound-row__desc">Render session cards as widgets (prettier, costs tokens) instead of plain text (cheaper, identical info).</div>' +
|
|
503
508
|
'</div>' +
|
|
504
|
-
|
|
509
|
+
switchHtml(on, 'Rich in-feed cards') +
|
|
505
510
|
'</div>';
|
|
506
511
|
}
|
|
507
512
|
|
|
@@ -579,7 +584,7 @@
|
|
|
579
584
|
'<div class="sound-row__label">Show my cross-project record</div>' +
|
|
580
585
|
`<div class="sound-row__desc">${escapeHtml(totalsLine + consentNote)} You always see your own numbers here.</div>` +
|
|
581
586
|
'</div>' +
|
|
582
|
-
|
|
587
|
+
switchHtml(on, 'Show my cross-project record') +
|
|
583
588
|
'</div>';
|
|
584
589
|
}
|
|
585
590
|
|
|
@@ -637,10 +642,10 @@
|
|
|
637
642
|
function privacyRow(key, label, state, on, desc, note) {
|
|
638
643
|
return `<div class="sound-row" data-privacy="${key}">` +
|
|
639
644
|
'<div class="sound-row__text">' +
|
|
640
|
-
`<div class="sound-row__label">${escapeHtml(label)}
|
|
645
|
+
`<div class="sound-row__label">${escapeHtml(label)}<span class="prow__tag">${escapeHtml(state)}</span>${note}</div>` +
|
|
641
646
|
`<div class="sound-row__desc">${escapeHtml(desc)}</div>` +
|
|
642
647
|
'</div>' +
|
|
643
|
-
|
|
648
|
+
switchHtml(on, `${label}: ${state}`) +
|
|
644
649
|
'</div>';
|
|
645
650
|
}
|
|
646
651
|
|
|
@@ -976,11 +981,14 @@
|
|
|
976
981
|
const body = (res.data || {});
|
|
977
982
|
if (!res.ok) throw new Error(body.message || `HTTP ${res.status}`);
|
|
978
983
|
const expires = body.expires_at ? new Date(body.expires_at).toLocaleString() : 'unknown';
|
|
984
|
+
// The secret is QUOTED (settings.css `.quote`): a 64-character token cannot
|
|
985
|
+
// be a line of prose, so it sits one tonal step down with its own foot.
|
|
979
986
|
result.innerHTML =
|
|
980
|
-
'<p>Fresh CLI token issued. Paste it in your terminal:</p>' +
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
987
|
+
'<p class="cli-result__say">Fresh CLI token issued. Paste it in your terminal:</p>' +
|
|
988
|
+
'<div class="quote"><p class="quote__eyebrow">paste command</p>' +
|
|
989
|
+
`<pre class="quote__pre">${escapeHtml(body.paste_command || '')}</pre>` +
|
|
990
|
+
'<div class="quote__foot"><button type="button" class="settings-btn settings-btn--quiet" id="cli-reissue-copy">Copy</button>' +
|
|
991
|
+
`<span class="settings-card__hint">Expires ${escapeHtml(expires)} · refreshes on use.</span></div></div>`;
|
|
984
992
|
result.hidden = false;
|
|
985
993
|
const copyBtn = $('#cli-reissue-copy');
|
|
986
994
|
if (copyBtn) copyBtn.addEventListener('click', async () => {
|
|
@@ -1037,10 +1045,11 @@
|
|
|
1037
1045
|
|
|
1038
1046
|
function artKeyEntryHtml() {
|
|
1039
1047
|
return (
|
|
1048
|
+
'<label class="field-label" for="artkey-input">Your key</label>' +
|
|
1040
1049
|
'<div class="artkey-entry-row">' +
|
|
1041
1050
|
'<input type="password" class="artkey-input" id="artkey-input" autocomplete="off" ' +
|
|
1042
1051
|
'spellcheck="false" placeholder="Paste your Google AI Studio key">' +
|
|
1043
|
-
'<button type="button" class="settings-btn" id="artkey-save">Save key</button>' +
|
|
1052
|
+
'<button type="button" class="settings-btn craft-save" id="artkey-save">Save key</button>' +
|
|
1044
1053
|
'</div>' +
|
|
1045
1054
|
`<p class="settings-card__hint">${getOneLink} · Stored encrypted; never shown again after saving.</p>`
|
|
1046
1055
|
);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "The hall's SETTINGS page through modules/ui-design/kit/render.js (task 1003308). Every panel on it is a form the page script fills from a dozen /me/* reads, so the floors run against the hall-preview harness rather than the kit's own stub:\n\n node scripts/hall-preview/server.js --port 4621 --fixture-me\n node modules/ui-design/kit/render.js --states modules/hall-ui/public/settings.states.json --base-in http://builders.localhost:4621 --base-out http://builders.localhost:4621\n\nOne group renders at a time behind the section rail, so each rail group is its own state. There is no `out` state on purpose: settings.js answers a 401 by navigating to the sign-in flow, and a state whose page navigates away measures the destination, not this page (the not-signed-in gate panel is the non-401 failure render, pinned statically in tests/hall_settings_world.mjs).",
|
|
3
|
+
"page": "/settings",
|
|
4
|
+
"surface": "hall-ui",
|
|
5
|
+
"stub": { "prefix": "/builders/" },
|
|
6
|
+
"modeQuery": false,
|
|
7
|
+
"ignoreRequests": ["/api/(gds|bongos)/", "/downloads/devbox/"],
|
|
8
|
+
"ignoreConsole": ["/api/(gds|bongos)/", "/downloads/devbox/", "\b401\b", "\b404\b", "Unauthorized", "Not Found", "load failed", "Password field"],
|
|
9
|
+
"states": {
|
|
10
|
+
"account": { "auth": true, "actions": [["wait", 900]], "expect": { "visible": ["#settings-h1", "#display-name-input", "#craft-chips", "#privacy-rows"] } },
|
|
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"] } },
|
|
13
|
+
"sound": { "auth": true, "url": "/settings#sound", "actions": [["wait", 900]], "expect": { "visible": ["#voices-table", "#sound-rows", "#event-sound-rows"] } }
|
|
14
|
+
},
|
|
15
|
+
"reducedMotion": { "auth": true, "actions": [["wait", 900]] }
|
|
16
|
+
}
|
|
@@ -1693,7 +1693,10 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
|
|
|
1693
1693
|
color: var(--ink-soft);
|
|
1694
1694
|
}
|
|
1695
1695
|
.profile__disc-label { color: var(--ink-faint); }
|
|
1696
|
-
|
|
1696
|
+
/* task 1003308: a link in the Standing card's fact line is a control and owes
|
|
1697
|
+
the 24px floor (it measured 123x19); the prose-link recipe buys the height
|
|
1698
|
+
with padding and hands the width back with the negative margin. */
|
|
1699
|
+
.profile__disc-edit { display: inline-block; min-width: 24px; min-height: 24px; padding: 3px 4px; margin: -3px -4px; font-size: 12.5px; }
|
|
1697
1700
|
.profile__credits {
|
|
1698
1701
|
display: flex;
|
|
1699
1702
|
flex-direction: column;
|
|
@@ -1731,7 +1734,7 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
|
|
|
1731
1734
|
.profile__budget--over { background: var(--danger-weak); color: var(--danger); border-color: color-mix(in srgb, var(--danger) 25%, transparent); }
|
|
1732
1735
|
.profile__needs--action { background: var(--warn-weak); color: var(--warn); border-color: color-mix(in srgb, var(--warn) 25%, transparent); }
|
|
1733
1736
|
.profile__needs--covered { background: var(--ok-weak); color: var(--ok); border-color: color-mix(in srgb, var(--ok) 22%, transparent); }
|
|
1734
|
-
.profile__needs-act { font-weight: 600; white-space: nowrap; }
|
|
1737
|
+
.profile__needs-act { display: inline-block; min-width: 24px; min-height: 24px; padding: 3px 4px; margin: -3px -4px; font-weight: 600; white-space: nowrap; }
|
|
1735
1738
|
/* ADR 0120 part 5: owed-rebase gate banner — danger-tinted (it blocks new claims). */
|
|
1736
1739
|
.profile__rebase { background: var(--danger-weak); color: var(--danger); border-color: color-mix(in srgb, var(--danger) 25%, transparent); }
|
|
1737
1740
|
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.485",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.485",
|
|
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.19.
|
|
3
|
+
"version": "1.19.485",
|
|
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/scripts/gds/claim.js
CHANGED
|
@@ -67,18 +67,49 @@ async function printRelevantLearnings(task) {
|
|
|
67
67
|
// invalid map, or a discipline with no entry (engineer/artist today), prints
|
|
68
68
|
// nothing and never breaks a successful claim.
|
|
69
69
|
function printDisciplineMode(task) {
|
|
70
|
+
let mode = null;
|
|
70
71
|
try {
|
|
71
72
|
const discipline = task && task.discipline;
|
|
72
73
|
if (!discipline) return;
|
|
73
74
|
const raw = fs.readFileSync(path.join(__dirname, 'discipline-modes.json'), 'utf8');
|
|
74
75
|
const modes = (JSON.parse(raw) || {}).modes || {};
|
|
75
|
-
|
|
76
|
+
mode = modes[discipline];
|
|
76
77
|
if (!mode || !mode.directive) return;
|
|
77
78
|
console.log('');
|
|
78
79
|
console.log(`▶ ${mode.label || 'Working mode'} — /${mode.skill}`);
|
|
79
80
|
console.log(` ${mode.directive}`);
|
|
80
81
|
} catch (_) {
|
|
81
82
|
// never let the mode hint break a successful claim
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
materializeModeSkills(mode);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// A task brings its own tools (task 1003486). A mode whose entry opts in with
|
|
89
|
+
// `materializeModuleSkills` lands the module-owned skills its playbook names — for
|
|
90
|
+
// `ui`, the ui-design taste kit /design sends the session to use — into THIS
|
|
91
|
+
// checkout's .claude/skills/, as the untracked, self-ignored copies ADR 0224 defined.
|
|
92
|
+
// Without it a builder is routed to /design, reads that impeccable and minimalist-ui
|
|
93
|
+
// are the tools, and finds none of them on the machine with no error saying why.
|
|
94
|
+
// Carried by the claim rather than a hook or a session start because a workbench is
|
|
95
|
+
// not re-provisioned every session (owner decision 2026-09-02): it runs only when
|
|
96
|
+
// someone actually claims work of that discipline.
|
|
97
|
+
//
|
|
98
|
+
// Fail-open in every branch, like the hint above it: the server already granted the
|
|
99
|
+
// claim, so nothing here may throw past this point — a failure prints the one command
|
|
100
|
+
// that does it by hand. The require is lazy so a claim of any other discipline never
|
|
101
|
+
// pays for loading the materialiser (and a broken materialiser cannot stop a claim).
|
|
102
|
+
function materializeModeSkills(mode) {
|
|
103
|
+
if (!mode || mode.materializeModuleSkills !== true) return;
|
|
104
|
+
try {
|
|
105
|
+
const { materializeModuleSkillsIntoCore, isCoreCheckoutRoot } = require('./claude-materialize');
|
|
106
|
+
const coreRoot = path.resolve(__dirname, '..', '..');
|
|
107
|
+
if (!isCoreCheckoutRoot(coreRoot)) return; // a scaffolded instance already has them from `bongos init`
|
|
108
|
+
const s = materializeModuleSkillsIntoCore({ coreRoot });
|
|
109
|
+
const n = s.materialized.length;
|
|
110
|
+
if (n) console.log(` ✓ ${n} module-owned skill(s) for this mode are in .claude/skills/ (untracked copies — ADR 0224).`);
|
|
111
|
+
} catch (e) {
|
|
112
|
+
console.log(` ⚠ could not land this mode's module-owned skills — run \`node scripts/gds/claude-materialize.js --module-skills-only\` (${e && e.message}).`);
|
|
82
113
|
}
|
|
83
114
|
}
|
|
84
115
|
|
|
@@ -638,4 +669,4 @@ if (require.main === module) {
|
|
|
638
669
|
});
|
|
639
670
|
}
|
|
640
671
|
|
|
641
|
-
module.exports = { formatClaimFailure, claimErrorInfo, goalMembershipOfferPlan };
|
|
672
|
+
module.exports = { formatClaimFailure, claimErrorInfo, goalMembershipOfferPlan, materializeModeSkills };
|
|
@@ -28,6 +28,7 @@ const path = require('node:path');
|
|
|
28
28
|
const { spawnSync } = require('node:child_process');
|
|
29
29
|
const { VERBS } = require('../../bin/bongos.js'); // side-effect-free require (guarded main)
|
|
30
30
|
const ic = require('../../src/instance-config'); // neutral configDir/envPrefix fallbacks (never a founder literal)
|
|
31
|
+
const { isCorePackageName } = require('../../src/module-loader/catalog'); // one definition of "this checkout IS the core"
|
|
31
32
|
const { renderInstanceClaudeMd } = require('./claude-md-template'); // task 2104 — the starter CLAUDE.md
|
|
32
33
|
|
|
33
34
|
// script basename (e.g. "ship.js") → dedicated bongos verb, for verbs that map to a bare script with
|
|
@@ -469,6 +470,24 @@ function materializeClaude({ coreRoot, instanceDir, dryRun = false, overwrite =
|
|
|
469
470
|
// core's own tracked .claude/skills/ carries is left to the core. Idempotent: every run first
|
|
470
471
|
// removes what the last manifest named (so a skill a module stopped declaring does not
|
|
471
472
|
// linger), then lands the current set; `clean` stops after the removal.
|
|
473
|
+
// Is `root` the core CHECKOUT — a workbench a builder edits — rather than the core
|
|
474
|
+
// package installed under a scaffolded instance's node_modules? Only a checkout's own
|
|
475
|
+
// .claude/skills/ is the directory Claude Code reads for that project, so only there
|
|
476
|
+
// does materialising the module-owned skills put them in front of anyone; an instance
|
|
477
|
+
// already received them at ITS root from `bongos init`, and a second copy under
|
|
478
|
+
// node_modules/@bongos/core would be invisible (task 1003486). Fail-soft by
|
|
479
|
+
// construction: an unreadable package.json answers "not the core checkout", which is
|
|
480
|
+
// the conservative no-op rather than a copy into a tree we could not identify.
|
|
481
|
+
function isCoreCheckoutRoot(root) {
|
|
482
|
+
try {
|
|
483
|
+
const abs = path.resolve(root);
|
|
484
|
+
if (abs.split(/[\\/]+/).includes('node_modules')) return false;
|
|
485
|
+
return isCorePackageName(JSON.parse(fs.readFileSync(path.join(abs, 'package.json'), 'utf8')).name);
|
|
486
|
+
} catch (_) {
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
472
491
|
const MODULE_SKILLS_MANIFEST = '.module-skills.json';
|
|
473
492
|
function readModuleSkillsManifest(skillsDir) {
|
|
474
493
|
const m = readJsonSafe(path.join(skillsDir, MODULE_SKILLS_MANIFEST));
|
|
@@ -535,4 +554,4 @@ if (require.main === module) {
|
|
|
535
554
|
process.exit(s.refused.length ? 1 : 0);
|
|
536
555
|
}
|
|
537
556
|
|
|
538
|
-
module.exports = { scriptToVerb, bongosify, transformSettings, copyTreeTransformed, materializeClaude, materializeModuleSkillsIntoCore, readModuleSkillsManifest, MODULE_SKILLS_MANIFEST, GDS_INVOCATION_RE, buildRebrandRules, rebrandText, disabledModuleSkills, moduleSkillRoster, moduleSkillSources, vendoredSkillGate, SKILL_NAME_RE, VENDOR_DEFAULT_POLICY };
|
|
557
|
+
module.exports = { scriptToVerb, bongosify, transformSettings, copyTreeTransformed, materializeClaude, materializeModuleSkillsIntoCore, isCoreCheckoutRoot, readModuleSkillsManifest, MODULE_SKILLS_MANIFEST, GDS_INVOCATION_RE, buildRebrandRules, rebrandText, disabledModuleSkills, moduleSkillRoster, moduleSkillSources, vendoredSkillGate, SKILL_NAME_RE, VENDOR_DEFAULT_POLICY };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_doc": "Discipline -> operating-playbook map (task 1267). A discipline whose work is best done as a *mode of working* rather than the default claim->build->ship loop names the skill that IS its playbook here. claim.js reads this after a successful claim: if the claimed task's `discipline` has an entry, it prints the entry's `directive` so the session opens the right playbook. There is NO classification step at claim time — `discipline` is already a column on the task. This is the whole discipline-fork mechanism: one carrier (the claim the builder already runs), one data lookup, no hooks, the Conductor untouched. Absence of an entry is the default: engineer and artist are intentionally NOT listed, so their claim output is unchanged and they stay in the standard build-and-ship loop. To give a discipline its own experience later, add an entry here and write the skill — no code change in claim.js. A missing or invalid version of this file must never break a claim (claim.js fails open).",
|
|
2
|
+
"_doc": "Discipline -> operating-playbook map (task 1267). A discipline whose work is best done as a *mode of working* rather than the default claim->build->ship loop names the skill that IS its playbook here. claim.js reads this after a successful claim: if the claimed task's `discipline` has an entry, it prints the entry's `directive` so the session opens the right playbook. There is NO classification step at claim time — `discipline` is already a column on the task. This is the whole discipline-fork mechanism: one carrier (the claim the builder already runs), one data lookup, no hooks, the Conductor untouched. Absence of an entry is the default: engineer and artist are intentionally NOT listed, so their claim output is unchanged and they stay in the standard build-and-ship loop. To give a discipline its own experience later, add an entry here and write the skill — no code change in claim.js. A missing or invalid version of this file must never break a claim (claim.js fails open). A mode entry may also set `\"materializeModuleSkills\": true` to have the claim land that mode's module-owned skills into this checkout's .claude/skills/ (task 1003486) — opt-in per discipline, core-checkout only, and fail-open like the hint itself, so a discipline that omits it claims exactly as before.",
|
|
3
3
|
"modes": {
|
|
4
4
|
"engineer": {
|
|
5
5
|
"skill": "dev",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"ui": {
|
|
20
20
|
"skill": "design",
|
|
21
|
+
"materializeModuleSkills": true,
|
|
21
22
|
"label": "UI design session",
|
|
22
23
|
"directive": "This is a UI task — interface DESIGN work (layout, wireframes, design systems, Figma / Claude-Design), split out of the pixel-art `artist` lane (ADR 0079). To work it, invoke the /design skill — the operating playbook for UI-design sessions, the only place the experience is defined. Like /paint it is show-first: communicate in mock-ups and rendered screens, not paragraphs about layout decisions — stage the change on your sandbox preview (/builder-stage) and look at the real interface before shipping. Keep the locked branding (config/branding.json) and the existing component conventions of the surface you touch (modules/status-ui, modules/hall-ui, public/game/ui). If no human is present (autonomous / bypass-permissions), produce the design, stage it for review, and park the screens rather than shipping interface changes blind. Open /design now rather than working this task the default way."
|
|
23
24
|
}
|
|
@@ -118,7 +118,6 @@ const GOAL_VOCAB_SURFACE = [
|
|
|
118
118
|
// The FIFTH use (task 1003090): the hub's coarse project membership.
|
|
119
119
|
'modules/platform-identity/platform-identity.js',
|
|
120
120
|
'modules/platform-identity/routes/my-projects.js',
|
|
121
|
-
'modules/hall-ui/public/my-projects.js',
|
|
122
121
|
];
|
|
123
122
|
// modules/lifecycle/db.js is 5k+ lines and legitimately says "role" about ranks
|
|
124
123
|
// (the rank-sync port, builders.system_role). Scanning it whole would need an
|
package/src/module-api.js
CHANGED
|
@@ -34,7 +34,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
34
34
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
35
35
|
// the entry to that file. Look for a version's history there, not here.
|
|
36
36
|
// ---------------------------------------------------------------------------
|
|
37
|
-
const CORE_VERSION = '1.19.
|
|
37
|
+
const CORE_VERSION = '1.19.485'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
38
38
|
|
|
39
39
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
40
40
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
// 3. transformSettings — rewrites only the scripts/gds hook commands, keeps .claude/hooks/ ones.
|
|
10
10
|
// 4. materializeClaude — copies + transforms skills into a target dir (no leftover node scripts/gds),
|
|
11
11
|
// writes settings.json, copies hooks; and NO-OPS when the target IS the core root (self-copy guard).
|
|
12
|
+
// 5. task 1003486 — a UI task brings its own tools: the `materializeModuleSkills` opt-in on a
|
|
13
|
+
// discipline-modes entry, the core-checkout probe that keeps it out of a scaffolded instance,
|
|
14
|
+
// and the fail-open contract (a materialise error never breaks a claim the server already granted).
|
|
12
15
|
//
|
|
13
16
|
// Run: node tests/claude_materialize.mjs
|
|
14
17
|
|
|
@@ -22,6 +25,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
22
25
|
const require = createRequire(import.meta.url);
|
|
23
26
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
24
27
|
const m = require('../scripts/gds/claude-materialize.js');
|
|
28
|
+
const claim = require('../scripts/gds/claim.js');
|
|
25
29
|
|
|
26
30
|
let passed = 0;
|
|
27
31
|
let failed = 0;
|
|
@@ -484,5 +488,77 @@ t('--module-skills-only: the CLI entry refuses without the flag (usage, exit 2)
|
|
|
484
488
|
rmSync(dir, { recursive: true, force: true });
|
|
485
489
|
});
|
|
486
490
|
|
|
491
|
+
// ---- task 1003486: claiming a ui task lands /design's tools in the checkout ----
|
|
492
|
+
|
|
493
|
+
t('the materialise-at-claim trigger is DATA: the ui mode opts in, every other discipline claims exactly as before', () => {
|
|
494
|
+
const modes = JSON.parse(readFileSync(join(ROOT, 'scripts', 'gds', 'discipline-modes.json'), 'utf8')).modes;
|
|
495
|
+
assert.equal(modes.ui.materializeModuleSkills, true, 'a ui claim brings the skills /design names');
|
|
496
|
+
assert.equal(modes.ui.skill, 'design', 'and still routes to /design');
|
|
497
|
+
for (const d of ['engineer', 'ideator', 'artist']) {
|
|
498
|
+
assert.ok(!modes[d].materializeModuleSkills, `${d} did not opt in, so its claim output is unchanged`);
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
t('isCoreCheckoutRoot: true for a real core checkout, false for the core installed into an instance, false for anything unreadable', () => {
|
|
503
|
+
assert.equal(m.isCoreCheckoutRoot(ROOT), true, 'this checkout names itself the core and is not under node_modules');
|
|
504
|
+
assert.equal(m.isCoreCheckoutRoot(join(ROOT, 'node_modules', '@bongos', 'core')), false,
|
|
505
|
+
'a scaffolded instance runs claim.js from node_modules — materialising there would be invisible');
|
|
506
|
+
const other = mkdtempSync(join(tmpdir(), 'cm-not-core-'));
|
|
507
|
+
writeFileSync(join(other, 'package.json'), JSON.stringify({ name: 'some-instance' }));
|
|
508
|
+
assert.equal(m.isCoreCheckoutRoot(other), false, 'another checkout is that instance, not the core');
|
|
509
|
+
assert.equal(m.isCoreCheckoutRoot(join(other, 'nope')), false, 'an unreadable tree answers no — the conservative no-op');
|
|
510
|
+
rmSync(other, { recursive: true, force: true });
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
t('materializeModeSkills: a discipline that did not opt in does nothing, and a materialise failure prints one hint instead of breaking the claim', () => {
|
|
514
|
+
const lines = [];
|
|
515
|
+
const realLog = console.log;
|
|
516
|
+
console.log = (...a) => lines.push(a.join(' '));
|
|
517
|
+
const mPath = require.resolve('../scripts/gds/claude-materialize.js');
|
|
518
|
+
const realExports = require.cache[mPath].exports;
|
|
519
|
+
try {
|
|
520
|
+
for (const mode of [null, undefined, {}, { skill: 'dev' }, { materializeModuleSkills: false }, { materializeModuleSkills: 'yes' }]) {
|
|
521
|
+
claim.materializeModeSkills(mode);
|
|
522
|
+
}
|
|
523
|
+
assert.deepEqual(lines, [], 'no opt-in => nothing printed and nothing copied');
|
|
524
|
+
|
|
525
|
+
// forced failure in the materialise step: swallowed, one line, the manual command named
|
|
526
|
+
require.cache[mPath].exports = {
|
|
527
|
+
isCoreCheckoutRoot: () => true,
|
|
528
|
+
materializeModuleSkillsIntoCore: () => { throw new Error('disk on fire'); },
|
|
529
|
+
};
|
|
530
|
+
claim.materializeModeSkills({ materializeModuleSkills: true });
|
|
531
|
+
assert.equal(lines.length, 1, 'exactly one line, not a wall');
|
|
532
|
+
assert.match(lines[0], /claude-materialize\.js --module-skills-only/, 'the hint names the command that does it by hand');
|
|
533
|
+
assert.match(lines[0], /disk on fire/, 'and why it could not');
|
|
534
|
+
|
|
535
|
+
// a tree that is not the core checkout is a no-op even with the flag on
|
|
536
|
+
lines.length = 0;
|
|
537
|
+
require.cache[mPath].exports = {
|
|
538
|
+
isCoreCheckoutRoot: () => false,
|
|
539
|
+
materializeModuleSkillsIntoCore: () => { throw new Error('must never be called in an instance'); },
|
|
540
|
+
};
|
|
541
|
+
claim.materializeModeSkills({ materializeModuleSkills: true });
|
|
542
|
+
assert.deepEqual(lines, [], 'in a scaffolded instance the claim is unchanged — init already landed them');
|
|
543
|
+
} finally {
|
|
544
|
+
require.cache[mPath].exports = realExports;
|
|
545
|
+
console.log = realLog;
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
t('materializeModeSkills lands the real module-owned skills, self-ignored, and is byte-identical on a re-claim', () => {
|
|
550
|
+
const core = fakeCore();
|
|
551
|
+
// the real function resolves its own coreRoot, so drive the library the way it does
|
|
552
|
+
const first = m.materializeModuleSkillsIntoCore({ coreRoot: core });
|
|
553
|
+
assert.deepEqual(first.materialized, ['alpha']);
|
|
554
|
+
const manifest1 = readFileSync(join(core, '.claude', 'skills', m.MODULE_SKILLS_MANIFEST), 'utf8');
|
|
555
|
+
const ignore = readFileSync(join(core, '.claude', 'skills', '.gitignore'), 'utf8');
|
|
556
|
+
assert.ok(ignore.split('\n').includes('/alpha/'), 'a re-claim leaves git status clean: the copy ignores itself');
|
|
557
|
+
const second = m.materializeModuleSkillsIntoCore({ coreRoot: core });
|
|
558
|
+
assert.deepEqual(second.materialized, ['alpha'], 'idempotent: the re-claim lands the same set');
|
|
559
|
+
assert.equal(readFileSync(join(core, '.claude', 'skills', m.MODULE_SKILLS_MANIFEST), 'utf8'), manifest1, 'byte-identical manifest');
|
|
560
|
+
rmSync(core, { recursive: true, force: true });
|
|
561
|
+
});
|
|
562
|
+
|
|
487
563
|
console.log(`\nclaude_materialize: ${passed} passed, ${failed} failed`);
|
|
488
564
|
process.exit(failed ? 1 : 0);
|