@bongos/core 1.24.27 → 1.24.29

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.
Files changed (41) hide show
  1. package/.bongos-core.json +53 -58
  2. package/.claude/skills/builder-claim/SKILL.md +1 -1
  3. package/.claude/skills/builder-ship/SKILL.md +1 -1
  4. package/docs/copy-inventory.md +173 -203
  5. package/docs/copy-registry.json +350 -622
  6. package/docs/module-api-changelog.md +4 -0
  7. package/docs/page-inventory.json +8 -8
  8. package/docs/page-readings.json +346 -331
  9. package/modules/builder-settings/interaction-prefs.js +2 -2
  10. package/modules/hall-ui/public/my-agents.css +72 -1
  11. package/modules/hall-ui/public/my-agents.html +30 -6
  12. package/modules/hall-ui/public/my-agents.js +98 -56
  13. package/modules/hall-ui/public/my-agents.states.json +18 -0
  14. package/modules/hall-ui/public/palette.js +8 -0
  15. package/modules/hall-ui/public/settings-moved.js +41 -0
  16. package/modules/hall-ui/public/settings.css +4 -158
  17. package/modules/hall-ui/public/settings.html +19 -147
  18. package/modules/hall-ui/public/settings.js +20 -215
  19. package/modules/hall-ui/public/settings.states.json +2 -4
  20. package/modules/hall-ui/public/speciality-tools.css +140 -0
  21. package/modules/hall-ui/public/{settings-specialities.js → speciality-tools.js} +83 -6
  22. package/modules/hall-ui/records/my-agents.md +5 -2
  23. package/modules/hall-ui/records/panel-family.md +4 -0
  24. package/modules/lifecycle/ship-card.js +2 -2
  25. package/package-lock.json +2 -2
  26. package/package.json +1 -1
  27. package/release-notes.json +12 -0
  28. package/scripts/hall-preview/server.js +4 -0
  29. package/src/module-api.js +1 -1
  30. package/tests/government_tidy_up.mjs +21 -24
  31. package/tests/hall_loading.mjs +3 -1
  32. package/tests/hall_my_agents.mjs +190 -2
  33. package/tests/hall_palette.mjs +12 -0
  34. package/tests/hall_settings_world.mjs +64 -100
  35. package/tests/helpers.mjs +162 -0
  36. package/tests/interaction_posture.mjs +3 -1
  37. package/tests/speciality_offer_ui.mjs +183 -149
  38. package/tests/speciality_walkthrough_ui.mjs +182 -161
  39. package/modules/hall-ui/public/settings-interaction.js +0 -132
  40. package/modules/hall-ui/public/settings-law-locks.js +0 -53
  41. package/modules/hall-ui/public/settings-render.js +0 -138
@@ -174,7 +174,7 @@ function shouldInject(behaviorPrefs) {
174
174
  return contractFor(behaviorPrefs) !== '';
175
175
  }
176
176
 
177
- // The full catalog the /settings UI renders.
177
+ // The full catalog the My agents page renders (it left /settings, task 1005044).
178
178
  function fieldCatalog() {
179
179
  return FIELD_KEYS.map((f) => ({
180
180
  field: f,
@@ -185,7 +185,7 @@ function fieldCatalog() {
185
185
  }));
186
186
  }
187
187
 
188
- // Everything the /me payload + the /settings UI need in one object.
188
+ // Everything the /me payload + the My agents page need in one object.
189
189
  function describeForApi(behaviorPrefs) {
190
190
  return {
191
191
  fields: fieldCatalog(),
@@ -164,9 +164,12 @@ html[data-theme="dark"] body[data-page="my-agents"] {
164
164
  .ma-group { margin-top: var(--sp-md); }
165
165
  .ma-group:first-child { margin-top: 4px; }
166
166
  .ma-rows { list-style: none; margin: 0; padding: 0; }
167
+ /* the fact keeps a floor (task 1005052): a row with more chips than fit beside it
168
+ — session cards, with its way back to the profile — wraps its chips instead of
169
+ squeezing the fact to a word a line */
167
170
  .ma-row {
168
171
  display: grid;
169
- grid-template-columns: minmax(118px, 160px) minmax(0, 1fr) auto;
172
+ grid-template-columns: minmax(118px, 160px) minmax(132px, 1fr) auto;
170
173
  gap: 6px 18px;
171
174
  align-items: center;
172
175
  min-height: 48px;
@@ -241,6 +244,74 @@ html[data-theme="dark"] body[data-page="my-agents"] {
241
244
  .ma-row--sub { min-height: 40px; padding: 4px 0; }
242
245
  .ma-row--sub .ma-row__fact { font-size: 13px; }
243
246
 
247
+ /* ---- what it knows: the speciality tools (task 1005052) ----
248
+ The SAME component Settings mounts (speciality-tools.js; its structure in
249
+ speciality-tools.css, linked before this sheet). Its markup is named in the
250
+ panel family's words (.prow, .btn, .pbtn) and this page has no panel.css, so
251
+ this skin draws those names in this page's own look: a hairline row like the
252
+ desk's, ghost pills, a chosen walkthrough answer as ink on glass. No accent —
253
+ on this page salmon would mean "needs you", and nothing here does. */
254
+ .ma-col { display: flex; flex-direction: column; gap: var(--sp-md); min-width: 0; }
255
+ .ma-know .prows { list-style: none; margin: 4px 0 0; padding: 0; }
256
+ .ma-know .prow { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 8px 18px; padding: 12px 0; border-top: var(--line) solid var(--rule-soft); }
257
+ .ma-know .prows > .prow:first-child { border-top: 0; }
258
+ .ma-know .prow__main { flex: 1 1 280px; min-width: 0; }
259
+ .ma-know .prow__label { display: flex; flex-wrap: wrap; align-items: baseline; margin: 0; font-size: 14.5px; font-weight: 500; letter-spacing: -0.01em; color: var(--ink); }
260
+ .ma-know .prow__hint { margin: 4px 0 0; max-width: var(--measure); font-size: 13px; line-height: 1.5; color: var(--ink-soft); }
261
+ .ma-know .prow__actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 6px; }
262
+ .ma-know .speciality-create__h { margin: 4px 0 0; font-size: 14px; font-weight: 500; color: var(--ink); }
263
+ .ma-know .btn,
264
+ .ma-know .pbtn {
265
+ display: inline-flex;
266
+ align-items: center;
267
+ justify-content: center;
268
+ min-height: 30px;
269
+ min-width: 30px;
270
+ padding: 0 14px;
271
+ border: var(--line) solid var(--rule);
272
+ border-radius: var(--radius-pill);
273
+ background: none;
274
+ color: var(--ink-soft);
275
+ font: inherit;
276
+ font-size: 13px;
277
+ font-weight: 400;
278
+ text-transform: lowercase;
279
+ white-space: nowrap;
280
+ cursor: pointer;
281
+ }
282
+ .ma-know .btn:hover:not(:disabled),
283
+ .ma-know .pbtn:hover:not(:disabled) { background: none; color: var(--ink); border-color: var(--ink-faint); }
284
+ .ma-know .btn:disabled,
285
+ .ma-know .pbtn:disabled { background: none; color: var(--ink-faint); border-color: var(--rule-soft); cursor: default; }
286
+ /* the walkthrough's answer you gave, and its one commit, read as a chosen chip */
287
+ .ma-know .pbtn[aria-pressed="true"],
288
+ .ma-know .pbtn--save:not(:disabled) { background: var(--glass-strong); color: var(--ink); border-color: var(--ink-soft); }
289
+ /* the 24px floor, drawn in ink: the create form's "for sale" box and the skill picker */
290
+ .ma-know input[type="checkbox"] { width: 24px; height: 24px; margin: 0; flex: none; accent-color: var(--ink); cursor: pointer; }
291
+ .ma-know .speciality-field > span { color: var(--ink-soft); }
292
+ /* "create a speciality": the form folds away until asked for, so the section opens on what you have */
293
+ .ma-know__new { margin-top: 10px; padding-top: 12px; border-top: var(--line) solid var(--rule-soft); }
294
+ .ma-know__new > summary {
295
+ display: inline-flex;
296
+ align-items: center;
297
+ min-height: 30px;
298
+ padding: 0 14px;
299
+ border: var(--line) solid var(--rule);
300
+ border-radius: var(--radius-pill);
301
+ font-size: 13px;
302
+ color: var(--ink-soft);
303
+ list-style: none;
304
+ cursor: pointer;
305
+ }
306
+ .ma-know__new > summary::-webkit-details-marker { display: none; }
307
+ .ma-know__new > summary:hover { color: var(--ink); border-color: var(--ink-faint); }
308
+ .ma-know__new[open] > summary { color: var(--ink); background: var(--glass-strong); border-color: var(--ink-soft); }
309
+ .ma-know__new .speciality-create { margin-top: 4px; padding-top: 0; border-top: 0; }
310
+ /* the fold's own label already names it, so the form's "Create one" would say it twice */
311
+ .ma-know__new .speciality-create > .speciality-create__h:first-child { display: none; }
312
+ .ma-know__status { margin: 8px 0 0; font-size: 13px; color: var(--ink-soft); }
313
+ .ma-know__status:empty { display: none; }
314
+
244
315
  /* ---- what the agent reads first ---- */
245
316
  .ma-read { margin: 0 0 12px; }
246
317
  .ma-read--project { padding: 12px 14px; background: var(--ma-law-wash); border-radius: var(--radius-sm); }
@@ -13,6 +13,9 @@
13
13
  group head and the foot line (records/oversight-family.md). This page's
14
14
  repeated thing is that row at the desk's one-line density. -->
15
15
  <link rel="stylesheet" href="/builders/oversight.css">
16
+ <!-- the speciality tools' structural sheet, shared with Settings (task 1005052);
17
+ before this page's own, whose .ma-know skin draws them in this page's look -->
18
+ <link rel="stylesheet" href="/builders/speciality-tools.css">
16
19
  <link rel="stylesheet" href="/builders/my-agents.css">
17
20
  </head>
18
21
  <body data-page="my-agents" data-page-title="My agents">
@@ -38,11 +41,30 @@
38
41
  <section class="ma-strict" id="ma-strict" aria-labelledby="ma-strict-h" hidden></section>
39
42
 
40
43
  <div class="ma-grid">
41
- <section class="scroll ma-desk" id="ma-desk" aria-labelledby="ma-desk-h">
42
- <h2 class="scroll__h" id="ma-desk-h">Your agents</h2>
43
- <p class="scroll__lede" id="ma-desk-lede">One line each. Tap to change; it saves straight away.</p>
44
- <div id="ma-rows" aria-live="polite"><p class="hall-loading" role="status">Reading your settings…</p></div>
45
- </section>
44
+ <div class="ma-col">
45
+ <section class="scroll ma-desk" id="ma-desk" aria-labelledby="ma-desk-h">
46
+ <h2 class="scroll__h" id="ma-desk-h">Your agents</h2>
47
+ <p class="scroll__lede" id="ma-desk-lede">One line each. Tap to change; it saves straight away.</p>
48
+ <div id="ma-rows" aria-live="polite"><p class="hall-loading" role="status">Reading your settings…</p></div>
49
+ </section>
50
+
51
+ <!-- task 1005052: WHAT IT KNOWS — every speciality tool Settings has, from
52
+ the SAME component (speciality-tools.js fills these hosts by id). A
53
+ section of its own, not desk rows: a desk row holds at most two
54
+ buttons, and my-agents.js redraws the desk on every change. Ships
55
+ hidden; the component reveals it once its reads answer. -->
56
+ <section class="scroll ma-know" id="specialities-scroll" aria-labelledby="specialities-h" hidden>
57
+ <h2 class="scroll__h" id="specialities-h">What it knows</h2>
58
+ <p class="scroll__lede">A speciality is what you work on, and how you want to be asked about it. The one you use reaches every session where you hold work in its role.</p>
59
+ <div id="speciality-suggestions"></div>
60
+ <ul class="prows" id="specialities-rows"></ul>
61
+ <details class="ma-know__new">
62
+ <summary>create a speciality</summary>
63
+ <form id="speciality-create" class="speciality-create"></form>
64
+ </details>
65
+ <p class="ma-know__status" id="specialities-status" role="status" aria-live="polite"></p>
66
+ </section>
67
+ </div>
46
68
 
47
69
  <aside class="scroll ma-reads" id="ma-reads" aria-labelledby="ma-reads-h">
48
70
  <h2 class="scroll__h" id="ma-reads-h">What your agent reads first</h2>
@@ -67,6 +89,8 @@
67
89
  <script src="/builders/palette.js" defer></script>
68
90
  <script src="/builders/gov-living.js?v=2026-10-10-real1"></script>
69
91
  <script src="/builders/gov-positions.js?v=2026-10-09-positions1"></script>
70
- <script src="/builders/my-agents.js?v=2026-10-17-tidy"></script>
92
+ <script src="/builders/my-agents.js?v=2026-11-03-defaults"></script>
93
+ <!-- the speciality tools (task 1005052): the one component Settings mounts too -->
94
+ <script src="/builders/speciality-tools.js?v=2026-11-03-shared"></script>
71
95
  </body>
72
96
  </html>
@@ -7,12 +7,19 @@
7
7
  // yours. Every row is one of your agent settings, and a project law can LOCK it.
8
8
  // Every part of it is a real read (task 1004717: the prototype overlay is gone):
9
9
  // real EVERY ROW (task 1004714). GET /me's interaction profile, wandering,
10
- // model allocation and session cards; GET /me/skill-prefs; GET
11
- // /specialities; the sounds switch (off by the owner's call,
12
- // .claude/sounds/play.js). Writes go to the real routes (PATCH
13
- // /me/interaction|wandering|model-allocation|render|skill-prefs, POST
14
- // /specialities/:id/adopt), so in the preview harness they reach its
15
- // recorder (scripts/hall-preview/pref-writes.js).
10
+ // model allocation and session cards; GET /me/skill-prefs; the sounds
11
+ // switch (off by the owner's call, .claude/sounds/play.js). Writes go to
12
+ // the real routes (PATCH /me/interaction|wandering|model-allocation|
13
+ // render|skill-prefs), so in the preview harness they reach its recorder
14
+ // (scripts/hall-preview/pref-writes.js). Session cards and each skill
15
+ // voice can go BACK to the default (task 1005052): the first chip clears
16
+ // your own pick (PATCH ... null), as Settings' "default" choices do.
17
+ // real WHAT IT KNOWS (task 1005052): every speciality tool Settings has —
18
+ // create, train, choose what it offers, adopt (its instructions read in
19
+ // full first), the skills walkthrough, stop using. Not this file: the
20
+ // SAME component Settings mounts (speciality-tools.js) fills its own
21
+ // section; this page only re-reads /me when it says a speciality changed,
22
+ // so "What your agent reads first" stays true.
16
23
  // real the project's laws and the locks they put on your settings (task
17
24
  // 1004707): GET /me's `laws` block — the very words every session reads —
18
25
  // and `laws.locks`, what each lock did to you, enforced by the server. A
@@ -57,7 +64,7 @@
57
64
  'model.main': { name: 'Main session' },
58
65
  'model.subagentDefault': { name: 'Helpers' },
59
66
  'model.subagentRoutine': { name: 'Routine helpers' },
60
- render: { name: 'Session cards', words: { minimal: 'one line', text: 'plain text', standard: 'rich card', rich: 'rich, open' } },
67
+ render: { name: 'Session cards', words: { '': 'follow my profile', minimal: 'one line', text: 'plain text', standard: 'rich card', rich: 'rich, open' } },
61
68
  };
62
69
  // The law a habit would become, in the ask-format voice: a short plain sentence.
63
70
  const LAW_LINES = {
@@ -70,8 +77,26 @@
70
77
  { key: 'talk', title: 'How it talks to you', keys: ['technical_level', 'toolchain_familiarity', 'verbosity', 'machinery', 'render', 'sounds'] },
71
78
  { key: 'work', title: 'How your agents work', keys: ['wandering'] },
72
79
  { key: 'models', title: 'Which models run', keys: ['model.main', 'model.subagentDefault', 'model.subagentRoutine', 'skills'] },
73
- { key: 'know', title: 'What it knows', keys: ['specialities'] },
80
+ // 'What it knows' (the specialities) is a section of its own since task
81
+ // 1005052: its tools are more than a desk row's two buttons, and they are the
82
+ // shared speciality-tools.js component, which this desk's redraw would wipe.
74
83
  ];
84
+ // Deep links (task 1005044): #<anchor> lands on a group this page draws, or on
85
+ // the speciality section. Settings' old links arrive here (settings-moved.js):
86
+ // #talk holds how Claude talks to you and session cards, #work wandering,
87
+ // #skills opens the per-skill voices, #knows is What it knows.
88
+ const ANCHORS = {
89
+ talk: { group: 'talk' },
90
+ work: { group: 'work' },
91
+ models: { group: 'models' },
92
+ skills: { group: 'models', openSkills: true },
93
+ knows: { id: 'specialities-scroll' },
94
+ };
95
+ function anchorFor(hash) {
96
+ const key = String(hash || '').replace(/^#/, '');
97
+ const a = Object.prototype.hasOwnProperty.call(ANCHORS, key) ? ANCHORS[key] : null;
98
+ return a ? { id: a.id || 'ma-group-' + a.group, openSkills: !!a.openSkills } : null;
99
+ }
75
100
  // the permission atom that may file a laws vote (POST /government/board/laws)
76
101
  const PROPOSE_ATOM = 'board.item.open';
77
102
 
@@ -86,8 +111,8 @@
86
111
  const nameFor = (key) => (SETTING[key] ? SETTING[key].name : key);
87
112
 
88
113
  // ---------- the pure view ----------
89
- // buildView({ me, constitution, presets, skills, specialities, preview,
90
- // proposed, switched, adopted, as, project, now }, { GovLiving, GovPositions })
114
+ // buildView({ me, constitution, presets, skills, preview,
115
+ // proposed, switched, as, project, now }, { GovLiving, GovPositions })
91
116
  // → { project, positions, strict, groups[], habits[], canPropose, reads, counts }
92
117
  // `preview` is a strictness preset key to look at instead of the project's own;
93
118
  // `presets` is GET /government/presets' body (task 1004715); `switched` maps a
@@ -132,9 +157,16 @@
132
157
  if (!r) return null;
133
158
  const levels = arr(r.levels).map((l) => l.value);
134
159
  const level = str(r.level || r.derived_level);
160
+ // task 1005052: your own pick, or none — then the cards follow the level
161
+ // your interaction profile derives, and "follow my profile" (value '',
162
+ // written as null) is the chip pressed. A pinned level survives a later
163
+ // profile change; following does not pin today's.
164
+ const own = r.override_level == null ? null : str(r.override_level);
135
165
  return {
136
- key, name: nameFor(key), fact: wordFor(key, level), locked: false, lock: null, cap: null, control: 'chips',
137
- value: level, effective: level, options: levels.map((v) => ({ value: v, label: wordFor(key, v), pressed: v === level })),
166
+ key, name: nameFor(key), fact: wordFor(key, level), note: own === null ? 'follows your profile' : null,
167
+ locked: false, lock: null, cap: null, control: 'chips', value: own === null ? '' : own, effective: level,
168
+ options: [{ value: '', label: wordFor(key, ''), pressed: own === null }]
169
+ .concat(levels.map((v) => ({ value: v, label: wordFor(key, v), pressed: v === own }))),
138
170
  };
139
171
  }
140
172
  if (key === 'sounds') {
@@ -149,19 +181,17 @@
149
181
  return {
150
182
  key, name: 'Per skill', fact: eff.length + ' skills' + (changed ? ' · ' + changed + ' yours' : ' · all default'),
151
183
  locked: false, lock: null, cap: null, control: 'expand', options: [],
152
- skills: eff.map((e) => ({
153
- key: 'skill.' + e.skill, skill: e.skill, value: e.effective_model,
154
- options: arr(sk.allowed_models).map((v) => ({ value: v, label: v, pressed: v === e.effective_model })),
155
- })),
156
- };
157
- }
158
- if (key === 'specialities') {
159
- const adopted = arr(me.specialities).map((x) => str(x.name)).filter(Boolean)
160
- .concat(arr(o.adopted).map(str)).filter((v, i, a) => a.indexOf(v) === i);
161
- const offer = arr(o.specialities && o.specialities.specialities).filter((x) => x && x.adoptable !== false).slice(0, 3);
162
- return {
163
- key, name: 'Specialities', fact: adopted.length ? adopted.join(', ') : 'none yet', locked: false, lock: null, cap: null, control: 'adopt',
164
- options: offer.map((x) => ({ value: String(x.id), label: str(x.name).toLowerCase(), pressed: adopted.indexOf(str(x.name)) >= 0 })),
184
+ // task 1005052: "default" (value '', written as null) clears your own
185
+ // pick, as Settings' Default does; a model is pressed only if you chose it.
186
+ skills: eff.map((e) => {
187
+ const own = e.override_model == null ? null : str(e.override_model);
188
+ return {
189
+ key: 'skill.' + e.skill, skill: e.skill, value: e.effective_model,
190
+ note: own === null ? null : 'default: ' + str(e.default_model),
191
+ options: [{ value: '', label: 'default', pressed: own === null }]
192
+ .concat(arr(sk.allowed_models).map((v) => ({ value: v, label: v, pressed: v === own }))),
193
+ };
194
+ }),
165
195
  };
166
196
  }
167
197
  return null;
@@ -170,7 +200,7 @@
170
200
  const rows = g.keys.map((k) => settingRow(k) || extraRow(k)).filter(Boolean);
171
201
  return { key: g.key, title: g.title, rows, source: 'real', locked: rows.filter((r) => r.locked).length };
172
202
  }).filter((g) => g.rows.length);
173
- const lockableKeys = groups.reduce((a, g) => a.concat(g.rows.filter((r) => r.key !== 'sounds' && r.key !== 'specialities').map((r) => r.key)), []);
203
+ const lockableKeys = groups.reduce((a, g) => a.concat(g.rows.filter((r) => r.key !== 'sounds').map((r) => r.key)), []);
174
204
  const total = lockableKeys.length;
175
205
  const lockedNow = groups.reduce((n, g) => n + g.locked, 0);
176
206
 
@@ -273,8 +303,9 @@
273
303
  return n < 0 ? null : { method: 'PATCH', path: '/me/wandering', body: { wandering: n }, slice: 'wandering' };
274
304
  }
275
305
  if (key.indexOf('model.') === 0) return { method: 'PATCH', path: '/me/model-allocation', body: { [key.slice(6)]: value }, slice: 'model_allocation' };
276
- if (key === 'render') return { method: 'PATCH', path: '/me/render', body: { widgets: value }, slice: 'render' };
277
- if (key.indexOf('skill.') === 0) return { method: 'PATCH', path: '/me/skill-prefs', body: { [key.slice(6)]: value }, slice: 'skills' };
306
+ // '' is the way back to the default: null clears your own pick (task 1005052)
307
+ if (key === 'render') return { method: 'PATCH', path: '/me/render', body: { widgets: value === '' ? null : value }, slice: 'render' };
308
+ if (key.indexOf('skill.') === 0) return { method: 'PATCH', path: '/me/skill-prefs', body: { [key.slice(6)]: value === '' ? null : value }, slice: 'skills' };
278
309
  return null;
279
310
  }
280
311
 
@@ -302,7 +333,7 @@
302
333
  };
303
334
  }
304
335
 
305
- const pure = { buildView, writeFor, proposalFor, pendingLaws, SETTING, LAW_LINES, GROUPS };
336
+ const pure = { buildView, writeFor, proposalFor, pendingLaws, anchorFor, ANCHORS, SETTING, LAW_LINES, GROUPS };
306
337
  root.OTBMyAgents = pure;
307
338
  if (typeof module !== 'undefined' && module.exports) { module.exports = pure; return; }
308
339
  if (typeof document === 'undefined' || !root.BongosClient) return;
@@ -322,7 +353,7 @@
322
353
  const LOCK = '<svg class="ma-lock__icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3.5" y="7" width="9" height="6.5" rx="1.5"/><path d="M5.5 7V5.2a2.5 2.5 0 0 1 5 0V7"/></svg>';
323
354
  const TICK = '<svg class="ma-tick" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>';
324
355
 
325
- const S = { me: null, constitution: null, presets: null, skills: null, specialities: null, preview: null, proposed: {}, pending: {}, switched: {}, adopted: [], skillsOpen: false, readAll: false, view: null };
356
+ const S = { me: null, constitution: null, presets: null, skills: null, preview: null, proposed: {}, pending: {}, switched: {}, skillsOpen: false, readAll: false, view: null };
326
357
 
327
358
  function asNow() {
328
359
  try { return root.sessionStorage.getItem('hall-as') === 'nobody' || /[?&]as=nobody\b/.test(root.location.search) ? 'nobody' : null; }
@@ -330,8 +361,8 @@
330
361
  }
331
362
  function view() {
332
363
  S.view = buildView({
333
- me: S.me, constitution: S.constitution, presets: S.presets, skills: S.skills, specialities: S.specialities,
334
- preview: S.preview, proposed: S.proposed, pending: S.pending, switched: S.switched, adopted: S.adopted, as: asNow(), project: PROJECT, now: Date.now(),
364
+ me: S.me, constitution: S.constitution, presets: S.presets, skills: S.skills,
365
+ preview: S.preview, proposed: S.proposed, pending: S.pending, switched: S.switched, as: asNow(), project: PROJECT, now: Date.now(),
335
366
  }, { GovLiving: root.GovLiving, GovPositions: root.GovPositions });
336
367
  return S.view;
337
368
  }
@@ -345,10 +376,6 @@
345
376
  // every lock is a real law, enforced by the server (task 1004714)
346
377
  if (row.locked) return '<span class="ma-lock">' + LOCK + '<span>' + esc(row.lock.text) + '</span></span>';
347
378
  if (row.control === 'chips') return chipsHtml(row, row.name);
348
- if (row.control === 'adopt') {
349
- return row.options.length ? '<div class="ma-chips" role="group" aria-label="Adopt a speciality">' + row.options.map((op) =>
350
- '<button type="button" class="ma-chip" data-adopt="' + esc(op.value) + '" aria-pressed="' + (op.pressed ? 'true' : 'false') + '"' + (op.pressed ? ' disabled' : '') + '>' + esc(op.pressed ? op.label : '+ ' + op.label) + '</button>').join('') + '</div>' : '';
351
- }
352
379
  if (row.control === 'expand') {
353
380
  return '<button type="button" class="btn-ghost ma-more" data-expand="skills" aria-expanded="' + (S.skillsOpen ? 'true' : 'false') + '" aria-controls="ma-skills">' + (S.skillsOpen ? 'hide' : 'show') + '</button>';
354
381
  }
@@ -357,14 +384,15 @@
357
384
  function rowHtml(row) {
358
385
  // a cap (task 1004714): the chips offer the values under it, and the line says which law
359
386
  const sub = (row.cap ? '<span class="ma-row__yours ma-row__cap">' + LOCK + 'capped by ' + esc(row.cap.text) + '</span>' : '') +
360
- (row.yours ? '<span class="ma-row__yours">yours: ' + esc(row.yours) + '</span>' : '');
387
+ (row.yours ? '<span class="ma-row__yours">yours: ' + esc(row.yours) + '</span>' : '') +
388
+ (row.note ? '<span class="ma-row__yours">' + esc(row.note) + '</span>' : '');
361
389
  let html = '<li class="ma-row' + (row.locked ? ' ma-row--locked' : '') + '" data-row="' + esc(row.key) + '">' +
362
390
  '<span class="ma-row__name">' + esc(row.name) + TICK + '</span>' +
363
391
  '<span class="ma-row__fact"><span>' + esc(row.fact) + '</span>' + sub + '</span>' +
364
392
  '<span class="ma-row__act">' + controlHtml(row) + '</span></li>';
365
393
  if (row.control === 'expand' && S.skillsOpen && !row.locked) {
366
394
  html += '<li class="ma-sub" id="ma-skills"><ul class="ma-rows ma-rows--sub">' + row.skills.map((s) =>
367
- '<li class="ma-row ma-row--sub" data-row="' + esc(s.key) + '"><span class="ma-row__name ma-mono">' + esc(s.skill) + TICK + '</span><span class="ma-row__fact"><span>' + esc(s.value) + '</span></span>' +
395
+ '<li class="ma-row ma-row--sub" data-row="' + esc(s.key) + '"><span class="ma-row__name ma-mono">' + esc(s.skill) + TICK + '</span><span class="ma-row__fact"><span>' + esc(s.value) + '</span>' + (s.note ? '<span class="ma-row__yours">' + esc(s.note) + '</span>' : '') + '</span>' +
368
396
  '<span class="ma-row__act">' + chipsHtml({ key: s.key, options: s.options }, s.skill) + '</span></li>').join('') + '</ul></li>';
369
397
  }
370
398
  return html;
@@ -410,7 +438,7 @@
410
438
  }
411
439
  function rowsHtml(v) {
412
440
  let html = v.groups.map((g) =>
413
- '<div class="ov-group ma-group" data-group="' + esc(g.key) + '">' +
441
+ '<div class="ov-group ma-group" id="ma-group-' + esc(g.key) + '" data-group="' + esc(g.key) + '">' +
414
442
  '<div class="ov-group__head"><span class="ov-group__name">' + esc(g.title) + '</span>' +
415
443
  (g.locked ? '<span class="ov-group__n">' + esc(g.locked) + ' locked</span>' : '') +
416
444
  '<span class="ov-group__note">live</span></div>' +
@@ -502,7 +530,9 @@
502
530
  const row = findRow(key);
503
531
  if (!row || row.locked) return;
504
532
  const w = writeFor(key, value, S);
505
- const said = (key.indexOf('skill.') === 0 ? key.slice(6) : nameFor(key).toLowerCase()) + ' ' + wordFor(key, value);
533
+ const skill = key.indexOf('skill.') === 0 ? key.slice(6) : null;
534
+ const said = value === '' ? (skill ? skill + ' on its default model' : nameFor(key).toLowerCase() + ' follow your profile')
535
+ : (skill || nameFor(key).toLowerCase()) + ' ' + wordFor(key, value);
506
536
  if (!w) return;
507
537
  api.request(w.method, API + w.path, { body: w.body }).then((r) => {
508
538
  if (r && r.ok && r.data) {
@@ -525,8 +555,8 @@
525
555
  if (w.slice === 'interaction' && S.me.interaction) { S.me.interaction.effective = Object.assign({}, S.me.interaction.effective, { [key]: value }); S.me.interaction.stored = Object.assign({}, S.me.interaction.stored, { [key]: value }); }
526
556
  if (w.slice === 'wandering' && S.me.wandering) { S.me.wandering.effective_key = value; S.me.wandering.override_level = w.body.wandering; }
527
557
  if (w.slice === 'model_allocation' && S.me.model_allocation) S.me.model_allocation.effective = Object.assign({}, S.me.model_allocation.effective, w.body);
528
- if (w.slice === 'render' && S.me.render) S.me.render.level = value;
529
- if (w.slice === 'skills' && S.skills) arr(S.skills.effective).forEach((e) => { if (e.skill === key.slice(6)) { e.effective_model = value; e.override_model = value; } });
558
+ if (w.slice === 'render' && S.me.render) Object.assign(S.me.render, value === '' ? { level: S.me.render.derived_level, override_level: null } : { level: value, override_level: value });
559
+ if (w.slice === 'skills' && S.skills) arr(S.skills.effective).forEach((e) => { if (e.skill === key.slice(6)) { e.effective_model = value === '' ? e.default_model : value; e.override_model = value === '' ? null : value; } });
530
560
  }
531
561
  function findRow(key) {
532
562
  const v = S.view;
@@ -539,16 +569,6 @@
539
569
  }
540
570
  return null;
541
571
  }
542
- function adopt(id) {
543
- const sp = arr(S.specialities && S.specialities.specialities).find((x) => String(x.id) === String(id));
544
- if (!sp) return;
545
- api.request('POST', API + '/specialities/' + encodeURIComponent(id) + '/adopt', { body: {} }).then((r) => {
546
- if (r && r.ok) {
547
- S.adopted.push(str(sp.name)); render(); lit('specialities');
548
- toast(sp.name + ' adopted. Its skills stay off until you turn them on.');
549
- } else toast('Not adopted' + (r && r.status ? ' (' + r.status + ')' : '') + '. Nothing changed.');
550
- }, () => toast('Not adopted: no answer.'));
551
- }
552
572
  // A habit proposed as a law: a real laws vote (POST /government/board/laws,
553
573
  // task 1004706), sat under the constitution in force like any amendment.
554
574
  function propose(key) {
@@ -604,13 +624,35 @@
604
624
  if (b.hasAttribute('data-set')) { const row = findRow(b.getAttribute('data-set')); const raw = b.getAttribute('data-value'); const opt = row && row.options.find((x) => String(x.value) === raw); if (opt && !opt.pressed) choose(row.key, opt.value); return; }
605
625
  if (b.hasAttribute('data-expand')) { S.skillsOpen = !S.skillsOpen; render(); return; }
606
626
  if (b.hasAttribute('data-readall')) { S.readAll = !S.readAll; render(); return; }
607
- if (b.hasAttribute('data-adopt')) { adopt(b.getAttribute('data-adopt')); return; }
608
627
  if (b.hasAttribute('data-propose')) { propose(b.getAttribute('data-propose')); }
609
628
  });
629
+ // The speciality tools (speciality-tools.js, their own section) say when what
630
+ // a session is sent moved: an adopt, a stop, a skills save. /me carries it,
631
+ // so read it again and "What your agent reads first" stays true (task 1005052).
632
+ document.addEventListener('otb:specialities-changed', async () => {
633
+ const me = await getJSON('/me');
634
+ if (me && me.builder) { S.me = me; render(); }
635
+ });
636
+ }
637
+
638
+ // Land a deep link (task 1005044) once its target is drawn: the desk groups
639
+ // after the first render; What it knows is revealed by speciality-tools.js when
640
+ // its own read lands, so a hidden target is watched (no timer) until it shows.
641
+ function land() {
642
+ const a = anchorFor(root.location.hash);
643
+ if (!a) return;
644
+ if (a.openSkills && !S.skillsOpen && S.view) { S.skillsOpen = true; render(); }
645
+ const el = $(a.id);
646
+ if (!el) return;
647
+ if (!el.hidden) { el.scrollIntoView({ block: 'start' }); return; }
648
+ if (typeof MutationObserver !== 'function') return;
649
+ const watch = new MutationObserver(() => { if (!el.hidden) { watch.disconnect(); el.scrollIntoView({ block: 'start' }); } });
650
+ watch.observe(el, { attributes: true, attributeFilter: ['hidden'] });
610
651
  }
611
652
 
612
653
  async function boot() {
613
654
  wire();
655
+ root.addEventListener('hashchange', () => land());
614
656
  const me = await getJSON('/me');
615
657
  if (me === null || me === false || !me.builder) {
616
658
  const rows = $('ma-rows');
@@ -618,16 +660,16 @@
618
660
  return;
619
661
  }
620
662
  S.me = me;
621
- const [constitution, presets, skills, specialities, room] = await Promise.all([
622
- getJSON('/government/constitution'), getJSON('/government/presets'), getJSON('/me/skill-prefs'), getJSON('/specialities'),
663
+ const [constitution, presets, skills, room] = await Promise.all([
664
+ getJSON('/government/constitution'), getJSON('/government/presets'), getJSON('/me/skill-prefs'),
623
665
  getJSON('/government/board/items'),
624
666
  ]);
625
667
  S.pending = pendingLaws(room);
626
668
  S.presets = presets || null;
627
669
  S.constitution = constitution;
628
670
  S.skills = skills || null;
629
- S.specialities = specialities || null;
630
671
  render();
672
+ land();
631
673
  }
632
674
  boot();
633
675
  }(typeof window !== 'undefined' ? window : globalThis));
@@ -25,6 +25,24 @@
25
25
  "actions": [["wait", 900], ["clickIfVisible", "[data-craft-close]"], ["wait", 300], ["click", "button[data-set=\"verbosity\"][aria-pressed=\"false\"]"], ["wait", 400]],
26
26
  "expect": { "visible": [".ma-row[data-saved] .ma-tick"] }
27
27
  },
28
+ "what-it-knows": {
29
+ "auth": true,
30
+ "_": "Task 1005052: the speciality tools, the same component Settings mounts. Someone else's speciality (Economics) shows its full instructions open beside its one adopt control; your own (Pricing, in use) carries choose its skills, train, offered skills and stop using. Session cards above follow the profile.",
31
+ "actions": [["wait", 900], ["clickIfVisible", "[data-craft-close]"], ["wait", 300], ["focus", "#specialities-rows button[data-act=\"adopt\"]"], ["wait", 300]],
32
+ "expect": { "visible": ["#specialities-scroll", ".speciality-contract[open]", "#specialities-rows button[data-act=\"abandon\"]", "#specialities-rows button[data-act=\"walk\"]", "button[data-set=\"render\"][data-value=\"\"][aria-pressed=\"true\"]"] }
33
+ },
34
+ "speciality-walk": {
35
+ "auth": true,
36
+ "_": "Adopting Economics opens the skills walkthrough in its card: one skill per step, leave it off or turn it on at equal weight.",
37
+ "actions": [["wait", 900], ["clickIfVisible", "[data-craft-close]"], ["wait", 300], ["click", "#specialities-rows button[data-act=\"adopt\"]"], ["wait", 700]],
38
+ "expect": { "visible": [".skill-walk", ".skill-walk__facts", "button[data-walk=\"on\"]", "button[data-walk=\"off\"]"] }
39
+ },
40
+ "speciality-forms": {
41
+ "auth": true,
42
+ "_": "The owner's tools open in place: Train on Pricing, and the folded create form with its skills picker.",
43
+ "actions": [["wait", 900], ["clickIfVisible", "[data-craft-close]"], ["wait", 300], ["click", "#specialities-rows button[data-act=\"train\"]"], ["wait", 300], ["click", ".ma-know__new > summary"], ["wait", 300], ["click", "#speciality-create-offer > summary"], ["wait", 900]],
44
+ "expect": { "visible": ["form.speciality-train", "#speciality-create input[name=\"name\"]", "#speciality-create-skills .speciality-offer__list"] }
45
+ },
28
46
  "nobody": {
29
47
  "auth": true,
30
48
  "url": "/my-agents?as=nobody",
@@ -53,6 +53,7 @@
53
53
  // work → work.js makeTabs ids
54
54
  // government → government.js makeTabs ids (two rooms since ADR 0266)
55
55
  // settings → settings.js RAIL_GROUPS
56
+ // my-agents → my-agents.js ANCHORS (the agent settings left Settings, task 1005044)
56
57
  var SECTIONS = [
57
58
  { parent: 'Accomplishments', gate: 'nav-standing', items: [
58
59
  { label: 'Work', href: '/#accomplishments-work' },
@@ -74,6 +75,13 @@
74
75
  { label: 'Constitution', href: '/government#constitution' },
75
76
  { label: 'Ranks & Permissions', href: '/government#ranks' },
76
77
  ] },
78
+ { parent: 'My agents', gate: 'nav-my-agents', items: [
79
+ { label: 'How it talks to you', href: '/my-agents#talk' },
80
+ { label: 'How your agents work', href: '/my-agents#work' },
81
+ { label: 'Which models run', href: '/my-agents#models' },
82
+ { label: 'Skill voices', href: '/my-agents#skills' },
83
+ { label: 'What it knows', href: '/my-agents#knows' },
84
+ ] },
77
85
  { parent: 'Settings', gate: 'shell-user', items: [
78
86
  { label: 'Account', href: '/settings#account' },
79
87
  { label: 'Access', href: '/settings#access' },
@@ -0,0 +1,41 @@
1
+ // modules/hall-ui/public/settings-moved.js — where the agent settings went
2
+ // (task 1005044).
3
+ //
4
+ // How Claude talks to you, Wandering, Rendering (session cards), Skill voices and
5
+ // Your specialities were edited on Settings AND on My agents, on the same routes,
6
+ // with nothing saying the two were one setting. The owner kept them on My agents
7
+ // only. Their old deep links (/settings#interaction, #voices, … and the section
8
+ // ids, which settings.js also resolved) still circulate in docs, skills and old
9
+ // messages, so settings.js asks this file first and sends a moved link on to the
10
+ // My agents anchor that holds the same setting (my-agents.js ANCHORS).
11
+ //
12
+ // PURE: no DOM, no fetch. tests/hall_settings_world.mjs requires it in node.
13
+
14
+ (function (root) {
15
+ 'use strict';
16
+
17
+ // old Settings anchor (and the old section id) → the My agents anchor
18
+ const MOVED = {
19
+ 'interaction': 'talk',
20
+ 'interaction-scroll': 'talk',
21
+ 'rendering': 'talk',
22
+ 'render': 'talk',
23
+ 'render-scroll': 'talk',
24
+ 'wandering': 'work',
25
+ 'wandering-scroll': 'work',
26
+ 'voices': 'skills',
27
+ 'voices-scroll': 'skills',
28
+ 'specialities': 'knows',
29
+ 'specialities-scroll': 'knows',
30
+ };
31
+
32
+ // '/my-agents#<anchor>' for a hash that left Settings, or null to stay.
33
+ function movedTarget(hash) {
34
+ const id = String(hash || '').replace(/^#/, '');
35
+ return Object.prototype.hasOwnProperty.call(MOVED, id) ? '/my-agents#' + MOVED[id] : null;
36
+ }
37
+
38
+ const api = { MOVED, movedTarget };
39
+ root.OTBSettingsMoved = api;
40
+ if (typeof module !== 'undefined' && module.exports) module.exports = api;
41
+ }(typeof window !== 'undefined' ? window : globalThis));