@bongos/core 1.19.811 → 1.19.812
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 +95 -35
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +24 -0
- package/clients/bongos-client/index.cjs +24 -0
- package/clients/bongos-client/index.d.ts +39 -0
- package/clients/bongos-client/index.mjs +24 -0
- package/docs/adr/0296-a-role-speciality-is-data-over-three-existing-surfaces.md +71 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +867 -73
- package/docs/api-reference.md +19 -3
- package/docs/copy-inventory.md +48 -20
- package/docs/copy-registry.json +275 -23
- package/docs/module-api-changelog.md +2 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +24 -0
- package/modules/hall-ui/public/settings-specialities.js +241 -0
- package/modules/hall-ui/public/settings.css +56 -0
- package/modules/hall-ui/public/settings.html +65 -0
- package/modules/lifecycle/db-claim-reads.js +5 -1
- package/modules/memory/learnings.js +21 -2
- package/modules/provisioning/starter-bundles.js +1 -0
- package/modules/specialities/CLAUDE.md +15 -0
- package/modules/specialities/db.js +234 -0
- package/modules/specialities/migrations/specialities_001_specialities.sql +143 -0
- package/modules/specialities/module.json +12 -0
- package/modules/specialities/routes/specialities.js +379 -0
- package/modules/specialities/specialities.js +198 -0
- package/modules/ui-design/kit/serve.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/publish-manifest.js +1 -0
- package/scripts/gds/search-ingest-db.js +37 -0
- package/src/bongos/routes/me.js +17 -0
- package/src/module-api.js +1 -1
- package/tests/module_loader.mjs +1 -1
- package/tests/own_scope_guard.mjs +10 -0
- package/tests/specialities.mjs +214 -0
- package/tests/speciality_corpus_scope.mjs +97 -0
- package/tests/speciality_db_shape.mjs +124 -0
- package/tests/speciality_injection.mjs +107 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# ADR 0296 — A role speciality is data over three existing surfaces, not a fourth mechanism
|
|
2
|
+
|
|
3
|
+
**Status:** accepted · **Date:** 2026-09-17 · **Task:** [#1003855](https://cloudbongos.com/builders#/task/1003855) · **Goal:** 1000095
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
A craft (`discipline`) says *how* you work: engineer, ideator, artist, ui. Each maps to a playbook in [`scripts/gds/discipline-modes.json`](../../scripts/gds/discipline-modes.json). Nothing said what you work *on*.
|
|
8
|
+
|
|
9
|
+
The owner's ask (2026-08-16, expanded by direct ruling 2026-09-17) is **role sub-specialities**: an ideator might hold an Economics speciality, where what matters is the subject knowledge and the terms that expert wants to be engaged on. The constraint attached to the ask is the point of it — *"do not get too into the weeds… the deliverable is NOT a catalogue of specialities we author. It is the MECHANISM."*
|
|
10
|
+
|
|
11
|
+
The 2026-09-17 ruling settled the questions the original filing left open:
|
|
12
|
+
|
|
13
|
+
1. **Training is two-fold** — knowledge the builder *supplies* (their own documents), **and** learning from sessions. Not authored prose alone.
|
|
14
|
+
2. Specialities are for subject-matter experts, and are **both per-builder and per-project**.
|
|
15
|
+
3. Sharing is **open or closed, the owner's choice**.
|
|
16
|
+
4. A speciality is effectively a **harness**, and its owner decides whether it may be **sold on the module marketplace**.
|
|
17
|
+
|
|
18
|
+
## Decision
|
|
19
|
+
|
|
20
|
+
### A speciality is DATA, not an ADR 0083 code module
|
|
21
|
+
|
|
22
|
+
The owner's phrasing is that specialities "are more or less MODULES", and the pull toward implementing them *as* modules is real. We rejected it: a subject-matter expert authors a speciality, and requiring them to ship a code package to do so puts the mechanism out of reach of exactly the person it exists for. A speciality is a row; a module is a package; the resemblance is in what they *contribute*, not in what they *are*.
|
|
23
|
+
|
|
24
|
+
### It layers over three surfaces that already exist
|
|
25
|
+
|
|
26
|
+
| Ingredient | Where it already lives |
|
|
27
|
+
|---|---|
|
|
28
|
+
| craft → playbook | `discipline-modes.json` |
|
|
29
|
+
| the per-session interaction contract | `behavior_prefs.interaction` → `GET /me` → the Conductor hook |
|
|
30
|
+
| declared skills | the ADR 0083 manifest contribution seam |
|
|
31
|
+
| **the knowledge bundle** | **new — `speciality_documents` + `speciality_learnings`** |
|
|
32
|
+
|
|
33
|
+
Only the fourth is new machinery. In particular **a speciality contract rides the existing injection path rather than adding a second one**: `GET /me` gains a slice, the Conductor relays it, and the composition stays server-side exactly as it does for the interaction profile.
|
|
34
|
+
|
|
35
|
+
### The injected contract is craft-scoped, and silent without a claim
|
|
36
|
+
|
|
37
|
+
The hook injects only the contract whose craft matches the claim in hand — an artist's expert contract in an engineering session is noise at best. This required one small honest addition: the active-claims read now carries `t.discipline`. **With no active claim there is no craft and nothing is injected**; silence beats guessing which expertise applies.
|
|
38
|
+
|
|
39
|
+
### The corpus is inventoried, never inlined
|
|
40
|
+
|
|
41
|
+
A document may run to 100k characters. The contract names *what the speciality knows* and tells the agent to reach it with `/recall` — and `speciality_documents` is therefore added to the recall ingester, because otherwise that instruction is a lie whose failure mode is silent (the agent searches, finds nothing, and answers from general knowledge as though the expert had never trained it).
|
|
42
|
+
|
|
43
|
+
### `sellable` is recorded; no storefront is built
|
|
44
|
+
|
|
45
|
+
[ADR 0107](0107-module-upstreaming-contribution-to-core-catalog.md) states there is no submission flow and no catalog/registry, and **explicitly rejected** "a package marketplace now" as premature. There is no payments surface either. So ruling 4 is honoured as a **seam, not a storefront**: `visibility` and an owner-set `sellable` flag capture the decision at the moment the owner makes it, so the deferred marketplace reads fields that already hold honest values instead of asking every existing owner to answer retroactively. The flag grants nothing and lists nowhere.
|
|
46
|
+
|
|
47
|
+
This is a deliberate partial implementation of an owner instruction, recorded here rather than silently made. Reversing it means taking the marketplace fork, which is a larger decision than this task.
|
|
48
|
+
|
|
49
|
+
## Consequences
|
|
50
|
+
|
|
51
|
+
**Good.** The mechanism ships without a catalogue, as asked. A speciality costs one row and nothing in a session that has not adopted one. The contract path, the knowledge corpus and the learning capture are all existing machinery pointed at a new subject rather than rebuilt.
|
|
52
|
+
|
|
53
|
+
**The scoping compromise to know about, and the bug in its first implementation.** The recall index expresses two axes — `min_rank` and `owner_builder_id` — and `project` visibility is neither: it names a set of builders the index cannot. A **null owner means corpus-wide**, which is the fact everything here turns on.
|
|
54
|
+
|
|
55
|
+
So: a **public** speciality's documents are corpus-wide; a **builder-owned** non-public one is scoped to its owner; and a **project-owned** non-public one is **not indexed at all**, because there is no single builder to scope it to and the honest alternative to "the right audience" is "nobody", never "everybody".
|
|
56
|
+
|
|
57
|
+
The first implementation got that last case exactly backwards. It mapped `ownerBuilderId: visibility === 'public' ? null : owner_builder_id` — correct for a builder-owned row, and a leak for a project-owned one, because the migration's own CHECK forces `owner_builder_id IS NULL` whenever `owner_kind = 'project'`. The null flowed through and published private project material to every builder on the instance. **This paragraph previously claimed the scoping "errs toward under-sharing", so the documentation actively vouched for the leak** — which is why the correction is recorded here rather than quietly fixed. It is now guarded twice, in the ingest `WHERE` clause and again in the mapper, and every visibility/ownership combination is pinned by `tests/speciality_corpus_scope.mjs`.
|
|
58
|
+
|
|
59
|
+
A project member can therefore see and adopt a project speciality but will not get its documents in `/recall` until it is public. That is a smaller answer than the sharing rule allows, which is the right direction for an index to be wrong in.
|
|
60
|
+
|
|
61
|
+
**Project membership is resolved through an optional seam.** It belongs to `platform-identity`, which exposes no port for it. Rather than reach across a module boundary, the routes resolve `project.membership` optionally; absent means *no projects*, never *all projects*. On a non-hub instance project-owned specialities are therefore authorable only by nobody — the honest behaviour until that port exists.
|
|
62
|
+
|
|
63
|
+
**A speciality's craft is immutable.** Moving one between crafts would silently re-scope everyone who adopted it, and the adoption rows carry a denormalised copy of the craft so the "one active per craft" rule can be a real unique index rather than a documented hope. The update path rejects the change and says why.
|
|
64
|
+
|
|
65
|
+
## Alternatives rejected
|
|
66
|
+
|
|
67
|
+
- **Specialities as real modules.** Puts authoring behind shipping a package. See above.
|
|
68
|
+
- **A second injection mechanism.** A separate hook or file would have duplicated a resolve-and-inject path that already works and is already tested.
|
|
69
|
+
- **Inlining the knowledge into the contract.** Blows the session's context, and scales with the expert's diligence in exactly the wrong direction.
|
|
70
|
+
- **Structured knobs instead of prose for the contract.** A speciality is an expert saying how they want to be worked with; that is not enumerable up front, and `interaction-prefs` already owns the enumerable half.
|
|
71
|
+
- **Building the marketplace.** Reverses an accepted ADR and is a larger decision than this task. Filed as a follow-up instead.
|
package/docs/adr/README.md
CHANGED
|
@@ -387,3 +387,4 @@ This keeps the decision history honest and traceable.
|
|
|
387
387
|
| 0293 | [**The owner's deploy door is an INTENT the control plane drains, not a web tier with a shell** ([task 1003159](https://cloudbongos.com/builders#/task/1003159), goal 1000090). The owner, twice in a month: *"I don't think I can make anything live"*, then *"this is above my paygrade"*. Moving a core pin is gate 3 of the release pipeline and the ONLY act that makes shipped work visible to users — and it was reachable from exactly one place: a shell on the control-plane box holding an ssh key. No route, no hall control. An agent could not close the loop either, because the permission classifier blocks a mutating `sudo -u lars` over ssh BY DESIGN — so the gate had no owner-reachable control AND no agent-reachable one, which is a structural mismatch with a platform whose premise is that agents build and the human holds the gates. Measured cost the day it was written: cloudbongos.com served 1.19.754 while 1.19.757, carrying a shipped fix to the claimable-task feed, sat published and unreachable. **Decision: the door is an INTENT the control-plane runner drains** — the seam [ADR 0111](0111-instance-hosting-provisioning-module.md) already built and that provision/teardown/domain-attach/settings-apply/restart already ride; `enqueueIntent` already carries `requested_by`, so the actor requirement is met by the existing shape rather than bolted on. The web tier gains no sudo, no ssh key, no new capability — it gains the ability to ASK. Rejected: giving the web tier sudo (ADR 0043 makes the control plane a separate privilege domain precisely so compromising the HTTP-reachable process does not hand over the box), and a second bespoke queue (another set of claim/settle/retry semantics to get right, when this one's failure modes are already understood in production). **The control plane is enrolled as a SELF ROW** because `provisioning_intents.instance_id` is NOT NULL and the box has never had one — which is exactly why the door had nothing to point at. That same row puts a teardown button one query away from the platform, so `hosting_shape='control-plane'` is a distinct value every destructive leg can refuse BY NAME, and the rule is an ALLOW-list (`core-upgrade`, `restart`) rather than a deny-list of today's destructive actions: a deny-list would admit tomorrow's by omission, and "permitted by omission" here means the platform deleting itself. A partial unique index makes a second self row impossible. Rejected: a nullable `instance_id` meaning "the control plane" (an implicit encoding that reads as a bug at every later call site and silently widens every query assuming the FK holds). **The target version comes from the published registry, never the request** — not tidiness: the release lane mints a git tag BEFORE the npm publish completes (measured 8–13 minutes on 2026-09-16 against ~100s recorded earlier, learnings 1000263/1000266), so a door trusting "whatever main says" would routinely queue a version that does not exist and die on `npm error notarget`. **Two-step preview-then-confirm**, the owner's own call: auto-rollback already guards the bad-release case, this guards the different one — the wrong button. **Archon-gated server-side, no caching** (ADR 0016), with `core_241` giving the ledger an actor and a source, because a privileged action with an unattributed row is not acceptable and the table carried only a free-text note. **Truth is the BOX's `/version`, not the apex** (the apex sits behind pre-launch basic_auth and its HTML rides a CDN, so an apex read reports a stale answer with total confidence). Self-restart is safe for a stated reason rather than an assumed one: the runner and the web service are DIFFERENT systemd units, and a running process keeps the code it already loaded, so an upgrade rewriting the runner's own files takes effect next tick, never mid-run. Tenant instances ride the same queue and are a filed follow-up. What it does NOT do: make deploys automatic — ADR 0137's three gates stand, and this only gives the third a handle.](0293-the-owners-deploy-door-is-an-intent-the-control-plane-drains.md) | deploy / authority / provisioning |
|
|
388
388
|
| 0294 | [**CLI sessions do not expire; revocation is the whole remedy** ([task 1003956](https://cloudbongos.com/builders#/task/1003956), goal 1000110 — owner request 2026-09-16, after a three-week gap signed him out). A `bongos` sign-in died after 24h idle: `createSession` defaulted a null `expiresAt` to `now()+SESSION_TTL_MS` and `lookupSession` rewrote `expires_at = now()+24h` on every request. **This is the third argument about the number and the first about the mechanism** — V3.R38 ([task #259](https://cloudbongos.com/builders#/task/259), migration 035) set 1h, V3.R90 ([task 1000400](https://cloudbongos.com/builders#/task/1000400)) widened it to 24h because 1h "kicked builders out over lunch", and each round shortened the nuisance while leaving the mechanism alone. The hygiene case for keeping it (criterion C9 — bound a leaked token in the mode-0600 plaintext session file) **does not survive contact with refresh-on-use**: a leaked token is by definition one somebody is using, and every use rolled the expiry forward another day, so the timer could only ever fire on a session gone quiet — the legitimate holder stepping away. The control was aimed at attackers and landed exclusively on users. [ADR 0206](0206-own-scoped-session-revocation-no-absolute-ceiling.md) had already written half of this down ("a CLI bearer in daily use is, in practice, immortal") without drawing the conclusion. Decision: **CLI sessions carry no expiry and last until revoked.** `createSession` records `expires_at = NULL` when the caller names none — every such call site is `source: 'cli'` (device flow, federated sign-in, hall re-issue, dev-box pair) — and `lookupSession` still bumps `last_used_at` but rolls `expires_at` forward only via `CASE WHEN expires_at IS NULL THEN NULL ELSE …`; **without that CASE the first request after sign-in re-mints the very timeout this removes.** Migration `core_243` clears `expires_at` on live unrevoked `source='cli'` rows, so the change reaches existing sessions instead of charging every builder one last forced re-auth on deploy day — migration 035's grace inverted, for the reason 035 gave. **Browser sessions are untouched** (they pass an explicit 30 days, so the new default never reaches them, and the migration's `source='cli'` filter is its whole safety property — `lookupSession` only rolls an expiry it can already see, so a web row cleared by mistake stays cleared forever). `SESSION_TTL_MS` survives as the roll-forward window for rows that have an expiry, no longer as a default. The accepted cost, stated plainly: **a leaked CLI token is valid until revoked** — smaller in practice than it sounds, since under refresh-on-use an actively-used leak was already immortal, so what changes is the *abandoned* copy on a dead laptop. The remedy is ADR 0206's own-scoped revoke, immediate where a TTL is next-day, and that ADR's visibility half (`created_at` beside `last_used_at`) is now the primary control rather than a nice-to-have. Nothing about rank moves: `setBuilderRank` still sweeps on demotion and `requireBuilder` still refuses a deactivated builder on the hot path, neither consulting `expires_at`. Rejected: widening the TTL a fourth time; keeping it and making re-auth cheaper (already built — [task 1000401](https://cloudbongos.com/builders#/task/1000401), [task 1000504](https://cloudbongos.com/builders#/task/1000504) — the friction shrank, the interruption stayed); an absolute `created_at` ceiling (ADR 0206's reasoning holds unchanged); dropping the column (load-bearing for web); a blanket `UPDATE … SET expires_at = NULL`. Revisit for machine principals nobody will notice a leak on, a compliance maximum, or long-dead sessions accumulating faster than people prune them.](0294-cli-sessions-do-not-expire.md) | auth / account security / CLI |
|
|
389
389
|
| 0295 | [**The registry is the witness the publish lane cannot be** ([task 1003916](https://cloudbongos.com/builders#/task/1003916), goal 1000090). On 2026-09-13 eight publish runs failed over three hours and NOTHING reported it; it was found by a person idly reading the npm version list. Task 1003914 fixed the lossiness and left the silence, which is structural: a failed run is one red row nobody watches, `main` self-heals on the next merge and **erases the evidence**, and the lane's own verdict step writes to a job summary no one reads. Same shape as the nightly upgrade sweep that died reporting `failed=0`. **Decision: a separate reader compares what the lane ANNOUNCED (release commits) against what the registry HOLDS**; the difference is a phantom. The publisher infers from git because OIDC trusted publishing carries no read credential — **that never bound a reader**, and since task 1003872 the package is public, so an anonymous read needs none. Keeping witness and publisher separate IS the point. Daily not hourly (billable minutes, against a failure measured in hours), cadence pinned to the cron by `routine-schedule.js`; deliberately NOT on the session-start path (a third-party call behind a hook timeout). Healthy days are SILENT in Discord — findings to stdout, the all-clear to stderr — because a channel of reassurances stops being read. Two false-alarm sources, each guarded: a closed incident is permanent history (a 3-day window, by TIME not commit count, which a release burst would outrun), and **npm acknowledges a publish before its listing shows it** — found by RUNNING the check, which flagged 1.19.794 fifteen minutes after a publish whose log ended `published @bongos/core@1.19.794` and whose per-version endpoint answered 200; hence a 30-minute grace period plus per-version confirmation, with the listing only ever nominating suspects. A failed read throws rather than reading as "nothing missing". Proven by replaying the REAL outage from git + the live registry, not a synthetic failure. Does NOT detect a lane that stopped being triggered.](0295-the-registry-is-the-witness-the-lane-cannot-be.md) | release / observability |
|
|
390
|
+
| 0296 | [**A role speciality is data over three existing surfaces, not a fourth mechanism** ([task 1003855](https://cloudbongos.com/builders#/task/1003855), goal 1000095). A craft says HOW you work; nothing said what you work ON. A speciality is a named bundle of knowledge, skills and an interaction contract sitting under a craft — an ideator with an Economics speciality. **Decision: it is DATA, not an ADR 0083 code module** — a subject-matter expert authors one, and requiring them to ship a package puts the mechanism out of reach of exactly the person it exists for. It layers over three surfaces that already exist (discipline-modes.json, the `behavior_prefs.interaction` contract path, the manifest contribution seam); only the knowledge bundle is new. The contract **rides the existing GET /me -> Conductor injection path rather than adding a second one**, and is CRAFT-SCOPED: the hook injects only the contract matching the claim in hand (which needed `t.discipline` on the active-claims read), and with no claim there is no craft and **nothing is injected** — silence beats guessing which expertise applies. Training is two-fold per the owner's 2026-09-17 ruling: documents they supplied, plus learnings captured from sessions worked in it (pointing at existing `learnings` rows, never a second corpus). The corpus is **inventoried, never inlined** — a document may run to 100k chars — so `speciality_documents` joins the recall ingester, because otherwise the contract's "search it with /recall" is a lie whose failure is silent. **`sellable` is recorded with no storefront behind it**: ADR 0107 defers the marketplace and explicitly rejected building one, so the flag captures the owner's decision when they make it and grants nothing — a deliberate partial implementation, recorded rather than silently made. Index scoping errs toward UNDER-sharing: only a public speciality's documents are corpus-wide, because `project` names a set the index cannot express.](0296-a-role-speciality-is-data-over-three-existing-surfaces.md) | roles / modules |
|