@bongos/core 1.24.27 → 1.24.28

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.
@@ -18,6 +18,8 @@
18
18
  has — the rail layout, the group head, the token ladder, the voices grid. -->
19
19
  <link rel="stylesheet" href="/builders/panel.css">
20
20
  <link rel="stylesheet" href="/builders/settings.css">
21
+ <!-- the speciality tools sheet (task 1005052), shared with My agents -->
22
+ <link rel="stylesheet" href="/builders/speciality-tools.css">
21
23
  </head>
22
24
  <body data-page="settings" data-page-title="Settings">
23
25
 
@@ -305,53 +307,9 @@
305
307
  <div id="speciality-suggestions"></div>
306
308
  <ul class="prows" id="specialities-rows"></ul>
307
309
 
308
- <form id="speciality-create" class="speciality-create">
309
- <h3 class="speciality-create__h">Create one</h3>
310
- <label class="speciality-field">
311
- <span>Name</span>
312
- <input type="text" name="name" maxlength="80" required placeholder="Economics" />
313
- </label>
314
- <label class="speciality-field">
315
- <span>Role it sits under</span>
316
- <select name="discipline" required>
317
- <option value="">Pick one…</option>
318
- <option value="engineer">Engineer</option>
319
- <option value="ideator">Ideator</option>
320
- <option value="artist">Artist</option>
321
- <option value="ui">UI design</option>
322
- </select>
323
- </label>
324
- <label class="speciality-field">
325
- <span>Summary <small>(optional)</small></span>
326
- <input type="text" name="summary" maxlength="500" placeholder="Markets, incentives, and how to price them." />
327
- </label>
328
- <label class="speciality-field">
329
- <span>How Claude should engage you in it <small>(optional)</small></span>
330
- <textarea name="contract_md" rows="4" maxlength="4000" placeholder="Argue from mechanism, not vibes. Always name the assumption a number rests on."></textarea>
331
- </label>
332
- <label class="speciality-field">
333
- <span>Who can use it</span>
334
- <select name="visibility">
335
- <option value="private">Only me</option>
336
- <option value="project">Anyone in the owning project</option>
337
- <option value="public">Anyone on this instance</option>
338
- </select>
339
- </label>
340
- <label class="speciality-field speciality-field--check">
341
- <input type="checkbox" name="sellable" />
342
- <span>
343
- May be offered for sale later
344
- <small>Records your decision only — there is no marketplace yet, and nothing is listed anywhere.</small>
345
- </span>
346
- </label>
347
- <details id="speciality-create-offer" class="speciality-offer-box">
348
- <summary>Skills it offers <small>(optional)</small></summary>
349
- <div id="speciality-create-skills"><p class="hall-loading hall-loading--inline" role="status">Loading this project’s skills…</p></div>
350
- </details>
351
- <div class="speciality-actions">
352
- <button type="submit" class="btn">Create speciality</button>
353
- </div>
354
- </form>
310
+ <!-- The create form is rendered by speciality-tools.js (task 1005052), the
311
+ one copy Settings and My agents share; this is only its host. -->
312
+ <form id="speciality-create" class="speciality-create"></form>
355
313
 
356
314
  <p class="settings-status" id="specialities-status" role="status" aria-live="polite"></p>
357
315
  </section>
@@ -534,7 +492,9 @@
534
492
  the same precedent and for the same reason — settings.js is against the
535
493
  1500-line ratchet, and this panel shares nothing with the rest of the page. -->
536
494
  <script src="/builders/settings-interaction.js?v=2026-10-17-laws"></script>
537
- <script src="/builders/settings-specialities.js?v=2026-09-17-specialities"></script>
495
+ <!-- task 1005052: the speciality tools are ONE component Settings and My agents
496
+ both mount (it was settings-specialities.js). Task 1005044 removes this mount. -->
497
+ <script src="/builders/speciality-tools.js?v=2026-11-03-shared"></script>
538
498
  <!-- task 1002518: the card-richness ladder. Own file on the same precedent —
539
499
  the panel grew from one switch to a rung picker with a derived default, and
540
500
  settings.js is against the 1500-line ratchet. -->
@@ -0,0 +1,140 @@
1
+ /* speciality-tools.css — the SPECIALITY TOOLS sheet (task 1005052): the structural
2
+ rules of the one component Settings and My agents both mount
3
+ (speciality-tools.js) — the card, the train, offer and create forms, the
4
+ instructions block and the skills walkthrough. Moved out of settings.css
5
+ verbatim, so Settings draws exactly as it did; each page sheet adds its own
6
+ look on top (Settings: panel.css, linked before this; My agents: the `.ma-know`
7
+ skin in my-agents.css, linked after). */
8
+
9
+ /* Specialities panel (task 1003855, ADR 0296). A speciality is a named bundle of
10
+ knowledge and terms under a craft; this styles the list, the train form and the
11
+ create form. Tokens only — no literal colours, so light/dark follow the page. */
12
+ .speciality-craft {
13
+ font-size: 0.78rem;
14
+ text-transform: lowercase;
15
+ opacity: 0.72;
16
+ margin-left: 0.4rem;
17
+ }
18
+ .speciality-badge {
19
+ font-size: 0.7rem;
20
+ padding: 0.05rem 0.4rem;
21
+ border-radius: 0.5rem;
22
+ margin-left: 0.4rem;
23
+ border: 1px solid currentColor;
24
+ }
25
+ .speciality-counts { opacity: 0.82; }
26
+ .speciality-card { flex-wrap: wrap; }
27
+ .speciality-train,
28
+ .speciality-create {
29
+ flex-basis: 100%;
30
+ margin-top: 0.75rem;
31
+ padding-top: 0.75rem;
32
+ border-top: 1px solid var(--rule, rgba(128, 128, 128, 0.25));
33
+ display: flex;
34
+ flex-direction: column;
35
+ gap: 0.6rem;
36
+ }
37
+ .speciality-create__h { margin: 0; font-size: 0.95rem; }
38
+ .speciality-field { display: flex; flex-direction: column; gap: 0.25rem; }
39
+ .speciality-field > span { font-size: 0.85rem; }
40
+ .speciality-field small { display: block; opacity: 0.7; font-weight: normal; }
41
+ .speciality-field input[type='text'],
42
+ .speciality-field select,
43
+ .speciality-field textarea {
44
+ width: 100%;
45
+ box-sizing: border-box;
46
+ }
47
+ /* The checkbox row reads as one sentence, so it runs inline rather than stacked. */
48
+ .speciality-field--check { flex-direction: row; align-items: flex-start; gap: 0.5rem; }
49
+ .speciality-field--check input { margin-top: 0.2rem; }
50
+ .speciality-actions { display: flex; gap: 0.5rem; }
51
+ /* What a speciality OFFERS (task 1004404): the owner's pick-list of this
52
+ instance's skills, each with its plain line. Sixty-odd rows, so the list
53
+ scrolls inside its own box rather than pushing the form's buttons off-screen;
54
+ the reading lines keep the measure. */
55
+ .speciality-field input[type='search'] { width: 100%; box-sizing: border-box; }
56
+ .speciality-offer,
57
+ .speciality-offer-box { flex-basis: 100%; display: flex; flex-direction: column; gap: 0.6rem; }
58
+ .speciality-offer { margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid var(--rule); }
59
+ .speciality-offer > *, .speciality-offer-box > * { max-width: var(--measure); }
60
+ .speciality-offer-box > summary { cursor: pointer; font-size: 0.85rem; min-height: 24px; padding-block: 2px; box-sizing: border-box; }
61
+ .speciality-offer-box > summary small { opacity: 0.7; }
62
+ .speciality-offer__choices, #speciality-create-skills { display: flex; flex-direction: column; gap: 0.6rem; }
63
+ .speciality-offer__list {
64
+ list-style: none;
65
+ margin: 0;
66
+ padding: 0 0.25rem 0 0;
67
+ display: flex;
68
+ flex-direction: column;
69
+ gap: 0.5rem;
70
+ max-height: 24rem;
71
+ overflow-y: auto;
72
+ overflow-x: hidden;
73
+ }
74
+ .speciality-offer__list li[hidden] { display: none; }
75
+ .speciality-offer__list strong { font-family: var(--font-mono); font-weight: 600; }
76
+ .speciality-offer__list .speciality-field--check > span { font-size: 0.85rem; line-height: 1.45; }
77
+ /* The adopted contract, shown in full before "Use this" — another builder's prose
78
+ reaching your sessions is the one thing here that must never be folded away. */
79
+ .speciality-contract { flex-basis: 100%; margin-top: 0.5rem; font-size: 0.85rem; }
80
+ /* 24px tall: the panel was never revealed until task 1004073, so the kit had
81
+ never measured this toggle; it came back 21px. */
82
+ .speciality-contract > summary { cursor: pointer; opacity: 0.85; min-height: 24px; padding-block: 2px; box-sizing: border-box; }
83
+ .speciality-contract__body {
84
+ white-space: pre-wrap;
85
+ margin: 0.4rem 0 0;
86
+ padding: 0.5rem 0.65rem;
87
+ border-left: 2px solid var(--rule, rgba(128, 128, 128, 0.35));
88
+ border-radius: 0;
89
+ font-family: inherit;
90
+ opacity: 0.92;
91
+ }
92
+ /* The skills walkthrough (task 1004073, ADR 0310 §3): one offered skill at a
93
+ time, three answers as a definition list, then a decision. It sits inside the
94
+ card like the train form, so it inherits the card's ground in both modes. */
95
+ .skill-walk {
96
+ flex-basis: 100%;
97
+ margin-top: 0.75rem;
98
+ padding-top: 0.75rem;
99
+ border-top: 1px solid var(--rule);
100
+ display: flex;
101
+ flex-direction: column;
102
+ gap: 0.6rem;
103
+ }
104
+ /* The measure caps the reading lines, never the section: a capped flex-basis
105
+ lets the section fit beside the card's actions instead of wrapping under them. */
106
+ .skill-walk > * { max-width: var(--measure); }
107
+ .skill-walk__progress { margin: 0; font-size: 0.8rem; color: var(--ink-soft); }
108
+ .skill-walk__name { margin: 0; font-family: var(--font-mono); font-size: 1rem; color: var(--ink); }
109
+ .skill-walk__facts { margin: 0; display: grid; gap: 0.2rem 0; }
110
+ .skill-walk__facts dt { font-size: 0.8rem; color: var(--ink-soft); margin-top: 0.4rem; }
111
+ .skill-walk__facts dt:first-child { margin-top: 0; }
112
+ .skill-walk__facts dd { margin: 0; color: var(--ink); line-height: 1.5; }
113
+ .skill-walk__note { font-size: 0.8rem; }
114
+ /* "Leave it off" and "Turn it on" are the panel family's plain pill at EQUAL
115
+ weight: the default is off, so the page must not lean on the reader toward on.
116
+ Only "Save my choices" takes the accent. A decision already made reads through
117
+ aria-pressed when the person steps Back to it. */
118
+ .skill-walk__actions { flex-wrap: wrap; }
119
+ .skill-walk__actions .pbtn[aria-pressed="true"] { border-color: var(--ink-soft); }
120
+ .skill-walk__recap { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; }
121
+ .skill-walk__recap li { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 0.4rem; }
122
+ .skill-walk__recap .prow__hint { flex-basis: 100%; margin: 0; }
123
+ .skill-walk__recap strong { font-family: var(--font-mono); font-weight: 600; }
124
+ /* "Finish later" is a way out, not a choice about a skill, so it reads as a quiet
125
+ link below the decision rather than a third button beside it. */
126
+ .skill-walk__later {
127
+ align-self: flex-start;
128
+ background: none;
129
+ border: 0;
130
+ padding: 0 0.25rem;
131
+ margin-inline-start: -0.25rem;
132
+ min-height: 24px;
133
+ font: inherit;
134
+ font-size: 0.8rem;
135
+ color: var(--ink-soft);
136
+ text-decoration: underline;
137
+ text-underline-offset: 3px;
138
+ cursor: pointer;
139
+ }
140
+ .skill-walk__later:hover { color: var(--ink); }
@@ -1,5 +1,6 @@
1
- // modules/hall-ui/public/settings-specialities.js — the "Your specialities" panel
2
- // on /builders/settings (task 1003855; ADR 0296).
1
+ // modules/hall-ui/public/speciality-tools.js — THE SPECIALITY TOOLS, one component
2
+ // mounted by two pages: Settings' "Your specialities" panel (task 1003855; ADR
3
+ // 0296) and My agents' "What it knows" section (task 1005052).
3
4
  //
4
5
  // WHAT A SPECIALITY IS, for anyone reading this file first: a craft says HOW you
5
6
  // work (engineer / ideator / artist / ui); a speciality says what you work ON and
@@ -7,10 +8,21 @@
7
8
  // contract you write here is injected into every session where you hold a claim
8
9
  // in that craft.
9
10
  //
10
- // Its own file rather than more of settings.js, on the settings-interaction.js /
11
- // settings-sessions.js precedent and for the same stated reason: settings.js sits
12
- // against the 1500-line size ratchet. Self-contained — it shares nothing with the
13
- // rest of the page but `window.OTB` and the generated client, both loaded before it.
11
+ // ONE COMPONENT, TWO MOUNTS (task 1005052). This was settings-specialities.js;
12
+ // it was carved out so My agents gets every tool Settings has without a second
13
+ // copy. A page mounts it by carrying the hosts, by id, inside one section:
14
+ // #specialities-scroll the section (ships `hidden`; revealed by load())
15
+ // #speciality-suggestions what the project suggests
16
+ // #specialities-rows the catalogue, one card per speciality (a <ul>)
17
+ // #speciality-create an EMPTY <form>: createFormHtml() below fills it,
18
+ // so the create form's markup exists once, here
19
+ // #specialities-status the status line
20
+ // Nothing else is page-specific: each page's own sheet draws the same markup in
21
+ // its own look (Settings through panel.css, My agents through my-agents.css's
22
+ // `.ma-know` skin; the structural rules are speciality-tools.css). Task 1005044
23
+ // removes the Settings mount (its section and this file's script tag); the code
24
+ // stays. Self-contained — it shares nothing with either page but `window.OTB`,
25
+ // loaded before it, and one DOM event it dispatches (changed(), below).
14
26
  //
15
27
  // TRAINING IS TWO-FOLD, and only half of it is visible here. Documents you add are
16
28
  // the half you drive; the other half accumulates on its own from learnings captured
@@ -33,6 +45,13 @@
33
45
  const el = (id) => document.getElementById(id);
34
46
  const statusLine = (msg) => { const s = el('specialities-status'); if (s) s.textContent = msg || ''; };
35
47
 
48
+ // What a session is sent just moved (an adopt, a stop, a skills save): say so,
49
+ // so a page that shows it can read it again. My agents' "What your agent reads
50
+ // first" listens (task 1005052); Settings does not, and nothing here waits on it.
51
+ function changed() {
52
+ if (typeof CustomEvent === 'function' && document.dispatchEvent) document.dispatchEvent(new CustomEvent('otb:specialities-changed'));
53
+ }
54
+
36
55
  async function call(method, path, body) {
37
56
  const res = await fetch(`${API}${path}`, {
38
57
  method,
@@ -203,6 +222,7 @@
203
222
  state.walk = null;
204
223
  toast(`Saved — ${enabled.length} of ${steps.length} skill${steps.length === 1 ? '' : 's'} on.`);
205
224
  await load();
225
+ changed();
206
226
  return;
207
227
  } catch (err) {
208
228
  w.saving = false;
@@ -458,6 +478,7 @@
458
478
  if (state.walk && state.walk.id === String(id)) state.walk = null;
459
479
  }
460
480
  await load();
481
+ if (action === 'adopt' || action === 'abandon') changed();
461
482
  // Adopting is the moment the ruling is about: a speciality that offers
462
483
  // skills opens straight into the walkthrough, with every one still off.
463
484
  if (action === 'adopt') {
@@ -585,8 +606,64 @@
585
606
  });
586
607
  }
587
608
 
609
+ // THE CREATE FORM, rendered into the page's empty `<form id="speciality-create">`
610
+ // (task 1005052). It lived in settings.html until a second page mounted these
611
+ // tools; here it is one copy both pages share, filled before wire() reads its
612
+ // fields. The markup is the one settings.html carried, unchanged.
613
+ function createFormHtml() {
614
+ return `
615
+ <h3 class="speciality-create__h">Create one</h3>
616
+ <label class="speciality-field">
617
+ <span>Name</span>
618
+ <input type="text" name="name" maxlength="80" required placeholder="Economics" />
619
+ </label>
620
+ <label class="speciality-field">
621
+ <span>Role it sits under</span>
622
+ <select name="discipline" required>
623
+ <option value="">Pick one…</option>
624
+ <option value="engineer">Engineer</option>
625
+ <option value="ideator">Ideator</option>
626
+ <option value="artist">Artist</option>
627
+ <option value="ui">UI design</option>
628
+ </select>
629
+ </label>
630
+ <label class="speciality-field">
631
+ <span>Summary <small>(optional)</small></span>
632
+ <input type="text" name="summary" maxlength="500" placeholder="Markets, incentives, and how to price them." />
633
+ </label>
634
+ <label class="speciality-field">
635
+ <span>How Claude should engage you in it <small>(optional)</small></span>
636
+ <textarea name="contract_md" rows="4" maxlength="4000" placeholder="Argue from mechanism, not vibes. Always name the assumption a number rests on."></textarea>
637
+ </label>
638
+ <label class="speciality-field">
639
+ <span>Who can use it</span>
640
+ <select name="visibility">
641
+ <option value="private">Only me</option>
642
+ <option value="project">Anyone in the owning project</option>
643
+ <option value="public">Anyone on this instance</option>
644
+ </select>
645
+ </label>
646
+ <label class="speciality-field speciality-field--check">
647
+ <input type="checkbox" name="sellable" />
648
+ <span>
649
+ May be offered for sale later
650
+ <small>Records your decision only — there is no marketplace yet, and nothing is listed anywhere.</small>
651
+ </span>
652
+ </label>
653
+ <details id="speciality-create-offer" class="speciality-offer-box">
654
+ <summary>Skills it offers <small>(optional)</small></summary>
655
+ <div id="speciality-create-skills"><p class="hall-loading hall-loading--inline" role="status">Loading this project’s skills…</p></div>
656
+ </details>
657
+ <div class="speciality-actions">
658
+ <button type="submit" class="btn">Create speciality</button>
659
+ </div>
660
+ `;
661
+ }
662
+
588
663
  function boot() {
589
664
  if (!el('specialities-rows')) return; // panel not on this page
665
+ const create = el('speciality-create');
666
+ if (create) create.innerHTML = createFormHtml();
590
667
  wire();
591
668
  load();
592
669
  }
@@ -1,14 +1,16 @@
1
1
  # My agents, and governing as a held seat — /my-agents and the nav (task 1004685, the living-city prototype)
2
2
 
3
- Covers: `my-agents.{html,js,css,states.json}`, `gov-positions.js`, the `position` flag in `shell.js` NAV, the seat in `craft-mode.js`, and the harness recorder `scripts/hall-preview/pref-writes.js`. Built on round-3 boards 6 ("the city and My agents"), 7 ("how strict") and 8 ("governance is a position") of the living-city design canvas. **Every row is real since task 1004714**: the settings, the laws' locks and caps, and Make it a law. The strictness preset strip is real too since task 1004715 (`GET /government/presets`, [ADR 0380](../../../docs/adr/0380-a-strictness-preset-is-a-bundle-of-laws-switched-by-one-laws-vote.md)).
3
+ Covers: `my-agents.{html,js,css,states.json}`, the speciality tools it shares with Settings (`speciality-tools.{js,css}`, task 1005052), `gov-positions.js`, the `position` flag in `shell.js` NAV, the seat in `craft-mode.js`, and the harness recorder `scripts/hall-preview/pref-writes.js`. Built on round-3 boards 6 ("the city and My agents"), 7 ("how strict") and 8 ("governance is a position") of the living-city design canvas. **Every row is real since task 1004714**: the settings, the laws' locks and caps, and Make it a law. The strictness preset strip is real too since task 1004715 (`GET /government/presets`, [ADR 0380](../../../docs/adr/0380-a-strictness-preset-is-a-bundle-of-laws-switched-by-one-laws-vote.md)).
4
4
 
5
5
  ## What it is
6
6
 
7
- - **My agents** (in You, after Profile): every setting that shapes how your own Claude works, one line each — a name of at most three words, one fact, one-tap chips (the owner's desk line rule). Real rows come from `GET /me` (interaction profile, wandering, model allocation, session cards), `GET /me/skill-prefs`, `GET /specialities` and the sounds switch; they write to the real routes. The five settings no table held (when stuck, when it asks, how it asks, at once, going live) were dropped by task 1004714 rather than shown as settings nothing stores or obeys; how an agent asks is a project rule now (the ask shape, ADR 0372-0373).
7
+ - **My agents** (in You, after Profile): every setting that shapes how your own Claude works, one line each — a name of at most three words, one fact, one-tap chips (the owner's desk line rule). Real rows come from `GET /me` (interaction profile, wandering, model allocation, session cards), `GET /me/skill-prefs` and the sounds switch; they write to the real routes. Session cards and each skill voice offer their way back to the default as the FIRST chip (`follow my profile`, `default`; task 1005052): it writes `null`, which clears your own pick rather than pinning today's value, and a level or model is pressed only when it is your own pick. The five settings no table held (when stuck, when it asks, how it asks, at once, going live) were dropped by task 1004714 rather than shown as settings nothing stores or obeys; how an agent asks is a project rule now (the ask shape, ADR 0372-0373).
8
8
  - **Only a law in force locks a row** (task 1004714). The rows read gov-living's model, which holds nothing but real reads (task 1004717 deleted the prototype overlay), so nothing else locks, caps or advises one. The strictness strip (real, task 1004715) names the preset the laws in force amount to ("No preset yet" on the hub) and previews what each WOULD lock of these rows — the server's locks after a switch, fixed or capped (Open workshop none, Guild off-task, Factory off-task and the three model slots) — with a tile and one pip per lockable row; it never locks a row. Previewing another preset offers "put it to a vote" to a builder who may open a board item: one real laws vote (`POST /government/board/presets`).
9
9
  - **What your agent reads first** quotes the REAL contract text `/me` carries (the Conductor relays it), says plainly when a knob is not sent (permissive wandering is `inject: false`), and puts the project's REAL laws above it: `/me`'s `laws` block, the very words every session gets first (task 1004707). With no law in force that block is not drawn.
10
10
  - **A real law's lock is real.** `/me`'s `laws.locks` says what each law in force did to your settings; the server already applied it (the slice's effective value obeys it), so the row is locked, naming the law and its number in the real book, with your own choice underneath. A law that only CAPS a setting (`{ setting, max }`) does not lock the row: the server names every value it still allows (`allowed` on each lock, `law-locks.js` allowedFor — the same list a refused PATCH carries), the chips offer those, and the line says `capped by <law> · Law N`. A cap that leaves one value, or a server that names none, reads as the whole-row lock. A lock the server could not enforce (a setting no table holds yet) locks nothing. A write the law forbids answers 409 `locked_by_law`, and the toast says the law.
11
11
  - **Make it a law** lists your own non-default choices that no law already locks or caps; proposing one files a real laws vote (`POST /government/board/laws`, task 1004706): one law, in the desk's voice, locking the setting to your value, sat under the constitution in force. It is offered only to a builder `/me` says may open a board item (`permissions['board.item.open']`, one of `NAV_PERMISSION_ATOMS`); an unreadable map offers nothing. A filed habit links to its vote in the Board Room.
12
+ - **What it knows is the speciality tools, not desk rows** (task 1005052). The section `#specialities-scroll` under the desk is filled by `speciality-tools.js` — the SAME component Settings mounts (it was `settings-specialities.js`), finding its hosts by id: create (it renders the create form into the page's empty `<form id="speciality-create">`, so the markup exists once), train, offered skills, adopt with someone else's full instructions open in the same card, the skills walkthrough, stop using, and the project's suggestions. It is its own section because a desk row holds at most two buttons and `my-agents.js` redraws `#ma-rows` on every change, which would wipe a half-filled form. Its markup is in the panel family's words (`.prow`, `.btn`, `.pbtn`); this page has no `panel.css`, so `my-agents.css`'s `.ma-know` skin draws them here as hairline rows and ghost pills, no accent (the structure is `speciality-tools.css`, shared). The desk's old one-tap adopt chips are gone: they adopted someone else's speciality without showing its instructions. After an adopt, a stop or a skills save the component dispatches `otb:specialities-changed` and this page re-reads `/me`, so "What your agent reads first" stays true. Task 1005044 removes the Settings mount; the code stays. Tests: `tests/speciality_offer_ui.mjs` and `tests/speciality_walkthrough_ui.mjs` run every tool on BOTH pages' own markup (`tests/helpers.mjs` `bootSpecialityTools`). Kit states `what-it-knows`, `speciality-walk`, `speciality-forms`; the harness serves an in-use Pricing (`me__specialities.json`) and canned stop-using and train answers.
13
+ - **The fact keeps a floor.** The row grid is `minmax(118px, 160px) minmax(132px, 1fr) auto` since task 1005052: session cards' five chips wrap rather than squeeze its fact to one word a line.
12
14
  - **Governing is a held seat** (real since task 1004713, ADR 0377). `GET /me` carries `positions`, resolved by the server (`modules/government/positions.js`: the owner seat, a Lead in office, a seat on a building's sitting, the board under the constitution in force). `gov-positions.js` only reads that block — no constitution or rank. Items flagged `position: true` (the City, the Board Room, the Government group) hide for a builder the server says holds none, and the craft mode names Governing by the seat ("Planning Lead") or drops it. A `/me` with no block is unknown and keeps today's nav.
13
15
 
14
16
  ## The rules an edit here must keep
@@ -29,4 +29,6 @@ Also fixed here, as the board task handed forward: the two sub-24px links on Hom
29
29
 
30
30
  **Settings: adopting a speciality walks through its skills (task 1004073, ADR 0310 §3)** — the owner ruled an adopted speciality's skills start OFF "and you should walk through what comes with each". So adopting one that offers skills opens `.skill-walk` inside its card: one skill per step (what it does, when to reach for it, what it costs, what it needs, who can run it), "Leave it off" / "Turn it on" at EQUAL weight (plain `.pbtn`, no accent — the default is off, so the page must not lean toward on), Back, then a recap restating each skill in a line, then ONE `PUT` of the whole set. "Finish later" saves nothing. Focus moves to each new step heading. Two things found on the way: the specialities panel shipped `hidden` and nothing ever revealed it, so adoption was unreachable from the hall; and its contract `<summary>` measured 21px once visible (now 24). **Kit:** `settings.states.json` `skills-walkthrough` + `preferences`, ALL CLEAN at 1440/390/320 × dark/light against the harness, with fixtures `specialities*.json` built from the real skills on disk and canned answers for adopt and save. Tests: `tests/speciality_walkthrough_ui.mjs` (runs the real page script).
31
31
 
32
+ **Settings: the speciality panel became a shared component (task 1005052)** — `settings-specialities.js` is now `speciality-tools.js`, mounted by Settings and by My agents (records/my-agents.md); it renders the create form into settings.html's now-empty `<form id="speciality-create">`, and its rules moved verbatim from `settings.css` to `speciality-tools.css`, linked right after it. Settings was verified unchanged against a before-capture: the same elements, every computed style and the same text in five states (rest, train, offered skills, the create picker, the walkthrough). **Known gap, older than this:** the `skills-walkthrough` and `offered-skills` states never close the first-visit craft picker, so their clicks time out under the kit and the page reader (both notes are on main); with a `clickIfVisible [data-craft-close]` step they are ALL CLEAN.
33
+
32
34
  **Settings: a speciality's owner chooses what it offers (task 1004404, ADR 0310 §1)** — until this only the API could set an offer, so the walkthrough above never appeared for a speciality made in the hall. The create form gains a `<details>` "Skills it offers" picker (filled on first open, so a builder who never offers one never pays for the list), and an owned card gains "Offered skills", an inline `form.speciality-offer` like Train. Both list `GET /specialities/installed-skills` — only what this instance has — each row a checkbox with the skill's plain line and cost, the words adopters will see, under a line saying offering turns nothing on. A "Find a skill" box narrows the sixty-odd rows (Enter never submits), the list scrolls in its own box, an offered skill removed since is named and dropped on save rather than silently kept, and saving is ONE `PATCH { skills }`. **The checkboxes take the shared 24px rule in `panel.css`** (the `.chip-ctl input` / `.craft-pref` block, one selector added) rather than a fourth copy of the `::before` box; without it 128 targets measured 13px. **Kit:** `settings.states.json` `offered-skills` + `preferences`, ALL CLEAN at 1440/390/320 × dark/light, with fixtures `specialities__installed-skills.json` (the real 64 skills, explained) and an owned speciality in `specialities.json`, plus canned POST/PATCH answers. Tests: `tests/speciality_offer_ui.mjs` (runs the real page script).
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.27",
3
+ "version": "1.24.28",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.24.27",
9
+ "version": "1.24.28",
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.24.27",
3
+ "version": "1.24.28",
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",
@@ -11095,5 +11095,11 @@
11095
11095
  "id": "1005053",
11096
11096
  "text": "The account, projects, community and status pages now show the Bongos logo turning while they wait for data, instead of a plain \"Reading…\" line."
11097
11097
  }
11098
+ ],
11099
+ "1.24.28": [
11100
+ {
11101
+ "id": "1005052",
11102
+ "text": "My agents now has every speciality tool Settings has (create one, train it, choose what it offers, take one on after reading its full instructions, pick its skills, stop using it), and session cards and each skill's model can"
11103
+ }
11098
11104
  ]
11099
11105
  }
@@ -195,6 +195,10 @@ const CANNED_WRITES = [
195
195
  // the Market Watch weekly ask's answer (task 1004940), so its "sent." state renders.
196
196
  ['POST', /^market-watch\/ask\/answer$/, 'market-watch__ask__answer.post'],
197
197
  ['PATCH', /^specialities\/\d+$/, 'specialities__12.patch'],
198
+ // the rest of the speciality tools, now on My agents too (task 1005052): stop
199
+ // using one, and train your own, so their toasts and redraws render here.
200
+ ['POST', /^specialities\/\d+\/abandon$/, 'specialities__12__abandon.post'],
201
+ ['POST', /^specialities\/\d+\/documents$/, 'specialities__12__documents.post'],
198
202
  // a vote cast from the street's matter card (task 1004528), so the card's
199
203
  // answer (the toast, the building lighting) renders here too.
200
204
  ['POST', /^government\/board\/items\/\d+\/votes$/, 'government__board__items__votes.post'],
package/src/module-api.js CHANGED
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
81
81
  // MAJOR (see allowBoxScope below): passes the request through untouched.
82
82
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
83
83
 
84
- const CORE_VERSION = '1.24.27'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.24.28'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
85
85
 
86
86
  // A namespaced logger so a module's log lines are attributable + consistent.
87
87
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -136,7 +136,9 @@ test('the placeholders the page scripts write wear it too, and a failure stops i
136
136
  'criterion-uat-ui.js': '<p class="hall-loading hall-loading--inline" role="status">Loading…</p>',
137
137
  'watch.js': '<p class="hall-loading" role="status">Reading…</p>',
138
138
  'studio.js': '<p class="hall-loading" role="status">Reading the reviews…</p>',
139
- 'settings-specialities.js': '<p class="hall-loading hall-loading--inline" role="status">Loading this project’s skills…</p>',
139
+ // the speciality tools, Settings' and My agents' one copy (task 1005052): the offer
140
+ // editor's and the create form's skills picker both write it
141
+ 'speciality-tools.js': '<p class="hall-loading hall-loading--inline" role="status">Loading this project’s skills…</p>',
140
142
  'settings-software-update.js': '<p class="hall-loading hall-loading--inline" role="status">Checking for a newer version…</p>',
141
143
  'goals-page.js': '<div class="hall-loading" role="status">Loading…</div>',
142
144
  };
@@ -440,10 +440,11 @@ test('PAGE: in the hall family, with no prototype key', () => {
440
440
  const html = read('my-agents.html');
441
441
  assert.match(html, /<body data-page="my-agents"/);
442
442
  const sheets = [...html.matchAll(/href="\/builders\/([a-z-]+\.css)/g)].map((m) => m[1]);
443
- assert.deepEqual(sheets, ['style.css', 'oversight.css', 'my-agents.css'], 'rides the oversight archetype, linked before its own sheet');
443
+ assert.deepEqual(sheets, ['style.css', 'oversight.css', 'speciality-tools.css', 'my-agents.css'],
444
+ 'rides the oversight archetype; the shared speciality sheet (task 1005052) before its own, so the page skin wins');
444
445
  const scripts = [...html.matchAll(/src="\/builders\/([a-z-]+)\.js/g)].map((m) => m[1]);
445
446
  assert.deepEqual(scripts.slice(0, 4), ['dom-utils', 'shell-trail', 'craft-mode', 'shell']); // dom-utils first: the shell escapes through it (task 1004876)
446
- for (const s of ['gov-living', 'gov-positions', 'my-agents']) assert.ok(scripts.includes(s), `loads ${s}.js`);
447
+ for (const s of ['gov-living', 'gov-positions', 'my-agents', 'speciality-tools']) assert.ok(scripts.includes(s), `loads ${s}.js`);
447
448
  assert.doesNotMatch(html, /ma-key|data-proto|Prototype data/, 'nothing on the page is prototype data, so there is no key');
448
449
  assert.doesNotMatch(read('my-agents.js'), /governor-overlay/, 'and it reads no overlay');
449
450
  assert.match(html, /id="hall-main"/);
@@ -480,3 +481,104 @@ test('PAGE: no animation loop or timer-driven motion in the page script', () =>
480
481
  // the one timer takes the saved tick's attribute back off; CSS plays the motion
481
482
  assert.equal((code.match(/setTimeout\(/g) || []).length, 1);
482
483
  });
484
+
485
+ // ---------------------------------------------------------------------------
486
+ // 6. everything Settings can do here too (task 1005052)
487
+ // ---------------------------------------------------------------------------
488
+
489
+ const { createPrefRecorder: recorder } = require(path.join(ROOT, 'scripts', 'hall-preview', 'pref-writes.js'));
490
+
491
+ test('DEFAULTS: session cards can go back to following your interaction profile', () => {
492
+ const me = fx('me');
493
+ assert.equal(me.render.override_level, null, 'the fixture viewer follows the profile');
494
+ const r = row(viewOf(), 'render');
495
+ assert.deepEqual(r.options[0], { value: '', label: 'follow my profile', pressed: true }, 'the way back comes first, and is the choice in force');
496
+ assert.deepEqual(r.options.slice(1).map((o) => o.value), me.render.levels.map((l) => l.value), 'then the four levels');
497
+ assert.ok(r.options.slice(1).every((o) => !o.pressed), 'a level is pressed only when it is your own pick');
498
+ assert.equal(r.fact, MyAgents.SETTING.render.words[me.render.derived_level], 'the fact is the level the profile gives');
499
+ assert.equal(r.note, 'follows your profile');
500
+ const pinned = fx('me');
501
+ pinned.render = { ...pinned.render, override_level: 'minimal', level: 'minimal' };
502
+ const p = row(viewOf({ me: pinned }), 'render');
503
+ assert.equal(p.options[0].pressed, false, 'a pinned level is not "follow my profile"');
504
+ assert.deepEqual(p.options.filter((o) => o.pressed).map((o) => o.value), ['minimal']);
505
+ assert.equal(p.fact, 'one line');
506
+ assert.equal(p.note, null);
507
+ assert.deepEqual(MyAgents.writeFor('render', '', { me }), { method: 'PATCH', path: '/me/render', body: { widgets: null }, slice: 'render' }, 'the way back CLEARS the pick (null), it does not pin today\'s level');
508
+ assert.deepEqual(MyAgents.writeFor('render', 'minimal', { me }).body, { widgets: 'minimal' });
509
+ });
510
+
511
+ test('DEFAULTS: the real shapers take the way back for session cards, and the row reads it as chosen', () => {
512
+ const rec = recorder({ log() {} });
513
+ const me = fx('me');
514
+ assert.equal(rec.write('me/render', MyAgents.writeFor('render', 'minimal', { me }).body, { me }).body.override_level, 'minimal');
515
+ const back = rec.write('me/render', MyAgents.writeFor('render', '', { me }).body, { me });
516
+ assert.equal(back.status, 200, 'the real route accepts it');
517
+ assert.equal(back.body.override_level, null);
518
+ assert.equal(back.body.level, back.body.derived_level, 'the cards follow the profile again');
519
+ const after = { ...me, render: back.body };
520
+ assert.equal(row(viewOf({ me: after }), 'render').options[0].pressed, true);
521
+ });
522
+
523
+ test('DEFAULTS: each skill voice can go back to its default model', () => {
524
+ const sk = fx('me__skill-prefs');
525
+ const subs = row(viewOf(), 'skills').skills;
526
+ assert.equal(subs.length, sk.effective.length);
527
+ for (const s of subs) {
528
+ assert.deepEqual(s.options[0], { value: '', label: 'default', pressed: true }, `${s.skill}: the default comes first, and is in force`);
529
+ assert.deepEqual(s.options.slice(1).map((o) => o.value), sk.allowed_models);
530
+ assert.ok(s.options.slice(1).every((o) => !o.pressed), `${s.skill}: no model is pressed you did not pick`);
531
+ assert.equal(s.note, null);
532
+ }
533
+ const own = JSON.parse(JSON.stringify(sk));
534
+ own.effective[0] = { ...own.effective[0], override_model: 'opus', effective_model: 'opus' };
535
+ const s0 = row(viewOf({ skills: own }), 'skills').skills[0];
536
+ assert.equal(s0.options[0].pressed, false);
537
+ assert.deepEqual(s0.options.filter((o) => o.pressed).map((o) => o.value), ['opus']);
538
+ assert.equal(s0.value, 'opus');
539
+ assert.equal(s0.note, 'default: ' + sk.effective[0].default_model, 'with your own pick, the default it replaces is said');
540
+ assert.match(row(viewOf({ skills: own }), 'skills').fact, /1 yours$/);
541
+ assert.deepEqual(MyAgents.writeFor('skill.' + sk.effective[0].skill, '', {}).body, { [sk.effective[0].skill]: null }, 'the way back clears the pick');
542
+ });
543
+
544
+ test('DEFAULTS: the real shapers take the way back for a skill voice', () => {
545
+ const rec = recorder({ log() {} });
546
+ const me = fx('me');
547
+ const sk = fx('me__skill-prefs');
548
+ const skill = sk.effective[0].skill;
549
+ rec.write('me/skill-prefs', { [skill]: 'opus' }, { me, skillFixture: sk });
550
+ const back = rec.write('me/skill-prefs', MyAgents.writeFor('skill.' + skill, '', {}).body, { me, skillFixture: sk });
551
+ assert.equal(back.status, 200);
552
+ assert.equal(skill in back.body.prefs, false, 'the override is gone');
553
+ const e = back.body.effective.find((x) => x.skill === skill);
554
+ assert.equal(e.override_model, null);
555
+ assert.equal(e.effective_model, e.default_model);
556
+ assert.equal(row(viewOf({ skills: back.body }), 'skills').skills[0].options[0].pressed, true);
557
+ });
558
+
559
+ test('PAGE: My agents mounts the SAME speciality component Settings does, in a section of its own', () => {
560
+ const html = read('my-agents.html');
561
+ assert.ok(!fs.existsSync(path.join(HALL, 'settings-specialities.js')), 'one copy: the Settings file was carved into the shared one');
562
+ for (const page of ['my-agents.html', 'settings.html']) assert.match(read(page), /src="\/builders\/speciality-tools\.js/, `${page} loads it`);
563
+ const at = html.indexOf('id="specialities-scroll"');
564
+ assert.ok(at > 0, 'the section is on the page');
565
+ assert.match(html.slice(html.lastIndexOf('<section', at), at), /class="scroll ma-know"/, 'a panel of its own');
566
+ const desk = html.slice(html.indexOf('id="ma-desk"'), html.indexOf('</section>', html.indexOf('id="ma-desk"')));
567
+ assert.doesNotMatch(desk, /specialities-rows/, 'not inside the desk, which my-agents.js redraws on every change and would wipe a form half-filled');
568
+ // tests/speciality_offer_ui.mjs and tests/speciality_walkthrough_ui.mjs drive every tool on this page's markup
569
+ });
570
+
571
+ test('PAGE: the desk adopts nothing in one tap — taking a speciality on goes through its instructions, read in full', () => {
572
+ const v = viewOf();
573
+ assert.equal(row(v, 'specialities'), undefined, 'no desk row adopts');
574
+ assert.equal(v.groups.some((g) => g.key === 'know'), false, 'the specialities are their own section');
575
+ assert.doesNotMatch(codeOf(read('my-agents.js')), /\/adopt'|data-adopt/, 'my-agents.js posts no adopt of its own');
576
+ });
577
+
578
+ test('PAGE: when a speciality changes, what your agent reads first is read again', () => {
579
+ // speciality-tools.js dispatches it after an adopt, a stop or a skills save
580
+ // (tests/speciality_walkthrough_ui.mjs); the page listens and re-reads /me.
581
+ const code = codeOf(read('my-agents.js'));
582
+ assert.match(code, /document\.addEventListener\('otb:specialities-changed', /);
583
+ assert.match(codeOf(read('speciality-tools.js')), /new CustomEvent\('otb:specialities-changed'\)/);
584
+ });