@idleware/ringi 0.7.2 → 0.8.1

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 (201) hide show
  1. package/README.md +18 -11
  2. package/package.json +1 -1
  3. package/src/main/resources/skill/ringi/SKILL.md +3 -4
  4. package/target/dts/adapters/{transport-git → store-git}/branches.d.ts +1 -3
  5. package/target/dts/adapters/{transport-git → store-git}/index.d.ts +0 -1
  6. package/target/dts/adapters/store-git/marks.d.ts +21 -0
  7. package/target/dts/adapters/{transport-git → store-git}/remote/push.d.ts +0 -1
  8. package/target/dts/adapters/store-git/remote/stream-check.d.ts +12 -0
  9. package/target/dts/adapters/store-git/remote/stream-control.d.ts +8 -0
  10. package/target/dts/adapters/store-git/remote/stream-facts.d.ts +4 -0
  11. package/target/dts/adapters/store-git/remote/streams.d.ts +31 -0
  12. package/target/dts/adapters/store-git/remote/sync-streams.d.ts +2 -0
  13. package/target/dts/adapters/{transport-git → store-git}/remote/types.d.ts +5 -4
  14. package/target/dts/adapters/{transport-git → store-git}/remote.d.ts +1 -4
  15. package/target/dts/adapters/{transport-git → store-git}/repo.d.ts +3 -2
  16. package/target/dts/adapters/{transport-git → store-git}/signing.d.ts +0 -1
  17. package/target/dts/clients/cli/operator/hub-commands.d.ts +0 -2
  18. package/target/dts/clients/cli/operator/index.d.ts +1 -1
  19. package/target/dts/clients/cli/operator/sessions.d.ts +0 -1
  20. package/target/dts/clients/cli/operator/where-format.d.ts +1 -1
  21. package/target/dts/clients/cli/shared/index.d.ts +2 -2
  22. package/target/dts/clients/cli/shared/messages.d.ts +0 -1
  23. package/target/dts/clients/cli/shared/profile.d.ts +0 -1
  24. package/target/dts/clients/cli/skill/index.d.ts +2 -1
  25. package/target/dts/core/agents/overview.d.ts +4 -7
  26. package/target/dts/core/agents/sessions/index.d.ts +0 -1
  27. package/target/dts/core/agents/sessions/join.d.ts +1 -2
  28. package/target/dts/core/hub/history/index.d.ts +1 -10
  29. package/target/dts/core/hub/ports/store.d.ts +35 -23
  30. package/target/dts/core/hub/rooms/address.d.ts +6 -0
  31. package/target/dts/core/hub/rooms/adoption.d.ts +7 -0
  32. package/target/dts/core/hub/rooms/compat.d.ts +3 -1
  33. package/target/dts/core/hub/rooms/contracts.d.ts +16 -0
  34. package/target/dts/core/hub/rooms/hubs.d.ts +3 -3
  35. package/target/dts/core/hub/rooms/index.d.ts +5 -6
  36. package/target/dts/core/hub/rooms/room-catalogue.d.ts +9 -0
  37. package/target/dts/core/hub/rooms/room-life.d.ts +10 -0
  38. package/target/dts/core/hub/rooms/room-view.d.ts +3 -0
  39. package/target/dts/core/hub/rooms/rooms.d.ts +4 -2
  40. package/target/dts/core/hub/signals/check.d.ts +2 -2
  41. package/target/dts/core/hub/signals/model.d.ts +2 -10
  42. package/target/dts/core/hub/signals/pins.d.ts +2 -2
  43. package/target/dts/core/hub/usecases/config.d.ts +0 -1
  44. package/target/dts/core/hub/usecases/context.d.ts +1 -2
  45. package/target/dts/core/hub/usecases/hubs.d.ts +1 -8
  46. package/target/dts/core/hub/usecases/invite.d.ts +12 -0
  47. package/target/dts/core/hub/usecases/room.d.ts +0 -1
  48. package/target/dts/core/hub/usecases/rooms.d.ts +9 -7
  49. package/target/dts/core/hub/usecases/roster.d.ts +1 -2
  50. package/target/dts/core/hub/work/index.d.ts +0 -1
  51. package/target/dts/index.d.ts +1 -1
  52. package/target/esm/adapters/{tool-claude → agent-claude}/index.js +2 -2
  53. package/target/esm/adapters/{tool-codex → agent-codex}/index.js +2 -2
  54. package/target/esm/adapters/{tool-cursor → agent-cursor}/index.js +1 -1
  55. package/target/esm/adapters/{tool-traces → agent-traces}/index.js +1 -1
  56. package/target/esm/adapters/state-fs/index.js +2 -2
  57. package/target/esm/adapters/{transport-git → store-git}/branches.js +21 -27
  58. package/target/esm/adapters/{transport-git → store-git}/index.js +10 -28
  59. package/target/esm/adapters/store-git/marks.js +86 -0
  60. package/target/esm/adapters/{transport-git → store-git}/remote/commits.js +3 -1
  61. package/target/esm/adapters/{transport-git → store-git}/remote/git.js +8 -1
  62. package/target/esm/adapters/{transport-git → store-git}/remote/keys.js +8 -3
  63. package/target/esm/adapters/{transport-git → store-git}/remote/push-retry.js +24 -6
  64. package/target/esm/adapters/store-git/remote/push-validate.js +19 -0
  65. package/target/esm/adapters/{transport-git → store-git}/remote/push.js +13 -14
  66. package/target/esm/adapters/store-git/remote/stream-check.js +44 -0
  67. package/target/esm/adapters/store-git/remote/stream-control.js +33 -0
  68. package/target/esm/adapters/store-git/remote/stream-facts.js +40 -0
  69. package/target/esm/adapters/store-git/remote/streams.js +76 -0
  70. package/target/esm/adapters/store-git/remote/sync-streams.js +28 -0
  71. package/target/esm/adapters/store-git/remote/sync.js +59 -0
  72. package/target/esm/adapters/{transport-git → store-git}/remote/types.js +0 -3
  73. package/target/esm/adapters/{transport-git → store-git}/remote.js +1 -4
  74. package/target/esm/adapters/{transport-git → store-git}/repo.js +42 -4
  75. package/target/esm/adapters/{transport-git → store-git}/signing.js +1 -9
  76. package/target/esm/adapters/store-git/transport.js +60 -0
  77. package/target/esm/clients/cli/agent/index.js +1 -1
  78. package/target/esm/clients/cli/agent/session.js +10 -10
  79. package/target/esm/clients/cli/commands.js +5 -5
  80. package/target/esm/clients/cli/context.js +1 -1
  81. package/target/esm/clients/cli/help.js +15 -21
  82. package/target/esm/clients/cli/operator/heartbeat-format.js +1 -1
  83. package/target/esm/clients/cli/operator/heartbeat.js +1 -2
  84. package/target/esm/clients/cli/operator/hub-commands.js +6 -19
  85. package/target/esm/clients/cli/operator/hub.js +2 -2
  86. package/target/esm/clients/cli/operator/index.js +2 -2
  87. package/target/esm/clients/cli/operator/room.js +25 -32
  88. package/target/esm/clients/cli/operator/sessions.js +4 -22
  89. package/target/esm/clients/cli/operator/where-format.js +2 -1
  90. package/target/esm/clients/cli/operator/where.js +5 -7
  91. package/target/esm/clients/cli/shared/index.js +2 -2
  92. package/target/esm/clients/cli/shared/messages.js +2 -12
  93. package/target/esm/clients/cli/shared/profile.js +1 -6
  94. package/target/esm/clients/cli/skill/index.js +14 -19
  95. package/target/esm/composition/index.js +4 -4
  96. package/target/esm/core/agents/heartbeat/pulse.js +4 -4
  97. package/target/esm/core/agents/overview.js +13 -25
  98. package/target/esm/core/agents/sessions/binding.js +1 -1
  99. package/target/esm/core/agents/sessions/index.js +3 -2
  100. package/target/esm/core/agents/sessions/join.js +17 -9
  101. package/target/esm/core/agents/sessions/resume.js +3 -6
  102. package/target/esm/core/hub/capabilities.js +1 -4
  103. package/target/esm/core/hub/history/index.js +23 -13
  104. package/target/esm/core/hub/ports/store.js +2 -2
  105. package/target/esm/core/hub/rooms/address.js +18 -0
  106. package/target/esm/core/hub/rooms/adoption.js +32 -0
  107. package/target/esm/core/hub/rooms/compat.js +39 -14
  108. package/target/esm/core/hub/rooms/contracts.js +62 -0
  109. package/target/esm/core/hub/rooms/hubs.js +33 -56
  110. package/target/esm/core/hub/rooms/index.js +2 -2
  111. package/target/esm/core/hub/rooms/messages.js +6 -6
  112. package/target/esm/core/hub/rooms/room-catalogue.js +31 -0
  113. package/target/esm/core/hub/rooms/room-life.js +59 -0
  114. package/target/esm/core/hub/rooms/room-view.js +39 -0
  115. package/target/esm/core/hub/rooms/rooms.js +26 -37
  116. package/target/esm/core/hub/signals/check.js +21 -26
  117. package/target/esm/core/hub/signals/index.js +10 -10
  118. package/target/esm/core/hub/signals/model.js +0 -2
  119. package/target/esm/core/hub/signals/pins.js +44 -16
  120. package/target/esm/core/hub/signals/write.js +8 -10
  121. package/target/esm/core/hub/usecases/actor.js +1 -1
  122. package/target/esm/core/hub/usecases/config.js +2 -2
  123. package/target/esm/core/hub/usecases/context.js +5 -8
  124. package/target/esm/core/hub/usecases/hubs.js +16 -33
  125. package/target/esm/core/hub/usecases/invite.js +26 -0
  126. package/target/esm/core/hub/usecases/room.js +4 -5
  127. package/target/esm/core/hub/usecases/rooms.js +40 -35
  128. package/target/esm/core/hub/usecases/roster.js +12 -21
  129. package/target/esm/core/hub/usecases/send.js +10 -6
  130. package/target/esm/core/hub/work/parse.js +1 -1
  131. package/target/esm/lib/fail.js +2 -2
  132. package/target/dts/adapters/transport-git/marks.d.ts +0 -20
  133. package/target/dts/adapters/transport-git/remote/bootstrap.d.ts +0 -7
  134. package/target/dts/adapters/transport-git/remote/heartbeat-name.d.ts +0 -6
  135. package/target/dts/adapters/transport-git/remote/heartbeat-object.d.ts +0 -10
  136. package/target/dts/adapters/transport-git/remote/heartbeat-push.d.ts +0 -2
  137. package/target/dts/adapters/transport-git/remote/heartbeat.d.ts +0 -7
  138. package/target/dts/adapters/transport-git/remote/signals.d.ts +0 -15
  139. package/target/dts/adapters/transport-git/remote/sync-heartbeat.d.ts +0 -2
  140. package/target/dts/adapters/transport-git/remote/sync-rooms.d.ts +0 -6
  141. package/target/dts/adapters/transport-git/remote/sync-tags.d.ts +0 -8
  142. package/target/dts/adapters/transport-git/tags.d.ts +0 -15
  143. package/target/dts/clients/cli/skill/targets.d.ts +0 -3
  144. package/target/dts/core/hub/usecases/staging.d.ts +0 -8
  145. package/target/esm/adapters/transport-git/marks.js +0 -82
  146. package/target/esm/adapters/transport-git/remote/bootstrap.js +0 -47
  147. package/target/esm/adapters/transport-git/remote/heartbeat-name.js +0 -19
  148. package/target/esm/adapters/transport-git/remote/heartbeat-object.js +0 -40
  149. package/target/esm/adapters/transport-git/remote/heartbeat-push.js +0 -44
  150. package/target/esm/adapters/transport-git/remote/heartbeat.js +0 -44
  151. package/target/esm/adapters/transport-git/remote/push-validate.js +0 -39
  152. package/target/esm/adapters/transport-git/remote/signals.js +0 -63
  153. package/target/esm/adapters/transport-git/remote/sync-heartbeat.js +0 -26
  154. package/target/esm/adapters/transport-git/remote/sync-rooms.js +0 -38
  155. package/target/esm/adapters/transport-git/remote/sync-tags.js +0 -48
  156. package/target/esm/adapters/transport-git/remote/sync.js +0 -38
  157. package/target/esm/adapters/transport-git/tags.js +0 -21
  158. package/target/esm/adapters/transport-git/transport.js +0 -29
  159. package/target/esm/clients/cli/skill/targets.js +0 -20
  160. package/target/esm/core/hub/usecases/staging.js +0 -60
  161. /package/target/dts/adapters/{tool-claude → agent-claude}/index.d.ts +0 -0
  162. /package/target/dts/adapters/{tool-codex → agent-codex}/index.d.ts +0 -0
  163. /package/target/dts/adapters/{tool-cursor → agent-cursor}/index.d.ts +0 -0
  164. /package/target/dts/adapters/{tool-traces → agent-traces}/index.d.ts +0 -0
  165. /package/target/dts/adapters/{transport-git → store-git}/files.d.ts +0 -0
  166. /package/target/dts/adapters/{transport-git → store-git}/remote/admit.d.ts +0 -0
  167. /package/target/dts/adapters/{transport-git → store-git}/remote/alarms.d.ts +0 -0
  168. /package/target/dts/adapters/{transport-git → store-git}/remote/branch-commit.d.ts +0 -0
  169. /package/target/dts/adapters/{transport-git → store-git}/remote/branch-control.d.ts +0 -0
  170. /package/target/dts/adapters/{transport-git → store-git}/remote/branch-scan.d.ts +0 -0
  171. /package/target/dts/adapters/{transport-git → store-git}/remote/branch.d.ts +0 -0
  172. /package/target/dts/adapters/{transport-git → store-git}/remote/cache.d.ts +0 -0
  173. /package/target/dts/adapters/{transport-git → store-git}/remote/commits.d.ts +0 -0
  174. /package/target/dts/adapters/{transport-git → store-git}/remote/forget.d.ts +0 -0
  175. /package/target/dts/adapters/{transport-git → store-git}/remote/git.d.ts +0 -0
  176. /package/target/dts/adapters/{transport-git → store-git}/remote/heartbeat-facts.d.ts +0 -0
  177. /package/target/dts/adapters/{transport-git → store-git}/remote/keys.d.ts +0 -0
  178. /package/target/dts/adapters/{transport-git → store-git}/remote/main.d.ts +0 -0
  179. /package/target/dts/adapters/{transport-git → store-git}/remote/process.d.ts +0 -0
  180. /package/target/dts/adapters/{transport-git → store-git}/remote/push-retry.d.ts +0 -0
  181. /package/target/dts/adapters/{transport-git → store-git}/remote/push-validate.d.ts +0 -0
  182. /package/target/dts/adapters/{transport-git → store-git}/remote/roster.d.ts +0 -0
  183. /package/target/dts/adapters/{transport-git → store-git}/remote/signature.d.ts +0 -0
  184. /package/target/dts/adapters/{transport-git → store-git}/remote/sync.d.ts +0 -0
  185. /package/target/dts/adapters/{transport-git → store-git}/run.d.ts +0 -0
  186. /package/target/dts/adapters/{transport-git → store-git}/transport.d.ts +0 -0
  187. /package/target/esm/adapters/{transport-git → store-git}/files.js +0 -0
  188. /package/target/esm/adapters/{transport-git → store-git}/remote/admit.js +0 -0
  189. /package/target/esm/adapters/{transport-git → store-git}/remote/alarms.js +0 -0
  190. /package/target/esm/adapters/{transport-git → store-git}/remote/branch-commit.js +0 -0
  191. /package/target/esm/adapters/{transport-git → store-git}/remote/branch-control.js +0 -0
  192. /package/target/esm/adapters/{transport-git → store-git}/remote/branch-scan.js +0 -0
  193. /package/target/esm/adapters/{transport-git → store-git}/remote/branch.js +0 -0
  194. /package/target/esm/adapters/{transport-git → store-git}/remote/cache.js +0 -0
  195. /package/target/esm/adapters/{transport-git → store-git}/remote/forget.js +0 -0
  196. /package/target/esm/adapters/{transport-git → store-git}/remote/heartbeat-facts.js +0 -0
  197. /package/target/esm/adapters/{transport-git → store-git}/remote/main.js +0 -0
  198. /package/target/esm/adapters/{transport-git → store-git}/remote/process.js +0 -0
  199. /package/target/esm/adapters/{transport-git → store-git}/remote/roster.js +0 -0
  200. /package/target/esm/adapters/{transport-git → store-git}/remote/signature.js +0 -0
  201. /package/target/esm/adapters/{transport-git → store-git}/run.js +0 -0
@@ -1,44 +1,32 @@
1
1
  // overview: where things stand for the operator: the hub, the room, who is in, open work, the latest
2
- // messages, and new ones as they land; and the sessions of this machine.
3
- import { hubPorts, locked } from '../hub/ports/index.js';
2
+ // messages, and new ones as they land; and the rooms of this machine.
4
3
  import { HIDDEN, OPERATOR, openHub, room as roomOf } from '../hub/rooms/index.js';
5
- import { fail } from '../../lib/index.js';
6
4
  import { ordered } from '../hub/protocol/index.js';
7
5
  import { linked } from '../hub/history/index.js';
8
- import { allSessions, dropSession, liveness, readSession } from './sessions/index.js';
6
+ import { allSessions, liveness } from './sessions/index.js';
9
7
  import { tasks } from '../hub/usecases/room.js';
10
- import { rooms } from '../hub/usecases/rooms.js';
8
+ import { roomNamed, rooms } from '../hub/usecases/rooms.js';
11
9
  const sessionView = (s) => ({ ...s, liveness: liveness(s) });
12
- export const where = (H, name) => {
10
+ export const where = (name) => {
13
11
  if (!name)
14
- return { hub: H, room: null, sessions: [], work: [], all: new Map(), messages: [] };
15
- const R = roomOf(H, name);
12
+ return { room: null, sessions: [], work: [], all: new Map(), messages: [] };
13
+ const R = roomNamed(name);
16
14
  const { all } = linked(R, OPERATOR);
17
- return { hub: H, room: R, sessions: allSessions().filter(s => s.hub === H.name && s.genesis === R.genesis).map(sessionView),
15
+ return { room: R, sessions: allSessions().filter(s => s.genesis === R.genesis).map(sessionView),
18
16
  work: tasks({ R, me: OPERATOR }).views,
19
17
  all, messages: ordered([...all.values()].filter(m => !(m.kind && HIDDEN.has(m.kind)))) };
20
18
  };
21
19
  // The visible messages that landed since `seen` (which it extends), read afresh from the store.
22
- export const arrived = (H, R, seen) => {
23
- const { all } = linked(roomOf(openHub(H.name, { refresh: true }), R.name), OPERATOR);
20
+ export const arrived = (R, seen) => {
21
+ const { all } = linked(roomOf(openHub(R.hub.name, { refresh: true }), R.name), OPERATOR);
24
22
  const messages = ordered([...all.values()].filter(m => !seen.has(m.id) && !(m.kind && HIDDEN.has(m.kind))));
25
23
  for (const m of messages)
26
24
  seen.add(m.id);
27
25
  return { all, messages };
28
26
  };
29
- // The hub's rooms, each with how many of its sessions on this machine are active.
30
- export const roomsActive = (H) => {
31
- const ss = allSessions().filter(s => s.hub === H.name && liveness(s) === 'active');
32
- return rooms(H).map(v => ({ ...v, active: ss.filter(s => s.genesis === v.genesis).length }));
27
+ // The rooms on this machine, each with how many of its sessions here are active.
28
+ export const roomsActive = () => {
29
+ const ss = allSessions().filter(s => liveness(s) === 'active');
30
+ return rooms().map(v => ({ ...v, active: ss.filter(s => s.genesis === v.genesis).length }));
33
31
  };
34
- export const sessions = () => {
35
- hubPorts().state.ensure();
36
- return allSessions().map(sessionView);
37
- };
38
- // The seat a session held becomes free.
39
- export const forgetSession = (handle) => locked('sessions', () => {
40
- if (!readSession(handle))
41
- fail(`no session ${handle}`);
42
- dropSession(handle);
43
- });
44
32
  //# sourceMappingURL=overview.js.map
@@ -8,7 +8,7 @@ import { dropSession, readSession, saveSession, whoAmI } from './index.js';
8
8
  export const bound = () => {
9
9
  const me = whoAmI();
10
10
  const s = readSession(me.handle) ?? fail(`this session is not in a room. Take part only when the operator says so in this chat:
11
- "join <room> as <id>" runs ringi join local <room> --as <id> --scope "<what they delegate>";
11
+ "join <room> as <id>" runs ringi join <room> --as <id> --scope "<what they delegate>" (<room>: its name, or the invitation);
12
12
  after a restart, "continue" runs ringi continue.`);
13
13
  if (s.state === 'stopped')
14
14
  fail('this session is stopped: the operator paused it. It reacts to nothing until the operator says "continue" (ringi continue)');
@@ -48,8 +48,9 @@ export const liveness = (s) => s.state === 'stopped' ? 'stopped'
48
48
  : Date.now() - Date.parse(s.lastSeen) < LEASE && (!s.pid || pidAlive(s.pid)) ? 'active' : 'gone';
49
49
  export const seatHolder = (hub, genesis, agent, except) => allSessions().find(s => s.hub === hub && s.genesis === genesis && s.agent === agent && s.handle !== except);
50
50
  export const ownSession = (handle) => readSession(handle);
51
+ // A seat is named by its room and agent: a room's name is unique on a machine, whatever hub keeps it.
51
52
  export const parseSelector = (sel) => {
52
- const m = /^(?:([a-z0-9._-]+):)?([^/]+)\/([^/]+)$/.exec(sel ?? '');
53
- return m ? { hub: m[1] ?? null, room: m[2], agent: m[3] } : fail('a session is named <room>/<agent> or <hub>:<room>/<agent>');
53
+ const m = /^([^/]+)\/([^/]+)$/.exec(sel ?? '');
54
+ return m ? { room: m[1], agent: m[2] } : fail('a session is named <room>/<agent>');
54
55
  };
55
56
  //# sourceMappingURL=index.js.map
@@ -1,19 +1,27 @@
1
1
  // join: a chat takes a seat in a room as an agent, one room per chat; on a signed hub its key must be admitted first.
2
2
  import { locked } from '../../hub/ports/index.js';
3
- import { OPERATOR, controlChange, openHub, room as roomOf } from '../../hub/rooms/index.js';
3
+ import { OPERATOR, attachRoom, adoptHub, controlChange } from '../../hub/rooms/index.js';
4
+ import { roomNamed } from '../../hub/usecases/rooms.js';
5
+ import { readInvite } from '../../hub/usecases/invite.js';
4
6
  import { fail, note, stamp } from '../../../lib/index.js';
5
- import { AGENT_RE } from '../../hub/protocol/index.js';
7
+ import { agentId } from '../../hub/usecases/roster.js';
6
8
  import { dropSession, liveness, ownSession, readSession, saveSession, seatHolder, whoAmI } from './index.js';
7
- export const join = ({ hub, room, agent: id, scope }) => {
9
+ // The room an invitation names, attached on first use; or the one room of that name on this machine.
10
+ const roomFor = (target) => {
11
+ const x = readInvite(target);
12
+ if (x)
13
+ return attachRoom(adoptHub({ trust: x.trust, root: x.root, genesis: x.hubGenesis }), x.room, { schema: 'ringi-room/1', name: x.room, hubGenesis: x.hubGenesis, genesis: x.genesis, store: { path: 'store', remote: x.address } });
14
+ return roomNamed(target);
15
+ };
16
+ export const join = ({ room: target, agent: id, scope }) => {
8
17
  const me = whoAmI();
9
18
  const agent = id ?? me.product ?? fail('--as <id> is required: this session\'s product is unknown');
10
- if (!AGENT_RE.test(agent) || agent === OPERATOR)
11
- fail(`bad agent id ${agent}: lowercase letters, digits, - and _, no dot`);
12
- const H = openHub(hub), R = roomOf(H, room);
19
+ agentId(agent);
20
+ const R = roomFor(target), H = R.hub, room = R.name;
13
21
  if (H.signed) {
14
- if (!H.store.joinKey)
22
+ if (!R.store.joinKey)
15
23
  fail(`hub ${H.name}: its store cannot make keys`);
16
- const key = H.store.joinKey(R.s, agent);
24
+ const key = R.store.joinKey(R.s, agent);
17
25
  if (!key.admitted)
18
26
  return { agent, hub: H.name, room: R.name, session: null, key };
19
27
  }
@@ -26,7 +34,7 @@ const claimSession = (c) => {
26
34
  const same = !!mine && mine.hub === H.name && mine.genesis === R.genesis && mine.agent === agent;
27
35
  ensureMember(c);
28
36
  if (mine && !same)
29
- note(`left ${mine.hub}/${mine.room} as ${mine.agent}: one room per session`);
37
+ note(`left ${mine.room} as ${mine.agent}: one room per session`);
30
38
  saveSession({
31
39
  handle: me.handle, product: me.product, hub: H.name, room: R.name, genesis: R.genesis, agent,
32
40
  scope: scope ?? (same ? mine.scope : null), state: 'active', since: same ? mine.since : stamp(), lastSeen: stamp(), pid: me.pid
@@ -19,10 +19,7 @@ const selectTarget = (me, selector) => {
19
19
  }
20
20
  else {
21
21
  const sel = parseSelector(selector);
22
- const hits = allSessions().filter(s => s.room === sel.room && s.agent === sel.agent && (!sel.hub || s.hub === sel.hub));
23
- if (hits.length > 1)
24
- fail(`several hubs have ${sel.room}/${sel.agent}; name one: <hub>:${sel.room}/${sel.agent}`);
25
- return hits[0] ?? fail(`no session holds ${selector}; the operator joins this chat with ringi join`);
22
+ return allSessions().find(s => s.room === sel.room && s.agent === sel.agent) ?? fail(`no session holds ${selector}; the operator joins this chat with ringi join`);
26
23
  }
27
24
  };
28
25
  const ownTarget = (me) => {
@@ -31,7 +28,7 @@ const ownTarget = (me) => {
31
28
  const cands = allSessions().filter(s => liveness(s) !== 'active').sort((a, b) => Number(b.product === me.product) - Number(a.product === me.product));
32
29
  if (!cands.length)
33
30
  fail('nothing to continue: this chat was never joined, and no paused or gone session waits. The operator says "join <room> as <id>"');
34
- fail(`this chat has no binding of its own. Say which one to take over:\n${cands.map(s => ` ringi continue ${s.hub}:${s.room}/${s.agent} (${liveness(s)}, ${s.product ?? '?'}, ${s.handle}${s.scope ? `, scope: ${s.scope}` : ''})`).join('\n')}`);
31
+ fail(`this chat has no binding of its own. Say which one to take over:\n${cands.map(s => ` ringi continue ${s.room}/${s.agent} (${liveness(s)}, ${s.product ?? '?'}, ${s.handle}${s.scope ? `, scope: ${s.scope}` : ''})`).join('\n')}`);
35
32
  }
36
33
  return mine;
37
34
  };
@@ -40,7 +37,7 @@ const takeOver = (me, target) => {
40
37
  if (liveness(target) === 'active')
41
38
  fail(`${target.room}/${target.agent} is held by a live session (${target.handle}); it is not taken over`);
42
39
  dropSession(target.handle);
43
- note(`took over ${target.hub}/${target.room}/${target.agent} from ${target.handle}`);
40
+ note(`took over ${target.room}/${target.agent} from ${target.handle}`);
44
41
  }
45
42
  };
46
43
  const resumed = (me, target) => {
@@ -2,10 +2,7 @@
2
2
  // registered beside the old one. A hub contract selects at most one major of each name; a room applies what its
3
3
  // contract base selects (docs/ARCHITECTURE.md, Evolution).
4
4
  import * as profiles from './work/index.js';
5
- // decisions/1 (proposals, grants, budgets) is retired: nothing in this build creates, decides or spends what it
6
- // governs, so a contract that still names it restricts nothing here and is read.
7
- const RETIRED = {};
8
- export const CAPABILITIES = { [profiles.PROFILES_CAPABILITY]: profiles, 'decisions/1': RETIRED };
5
+ export const CAPABILITIES = { [profiles.PROFILES_CAPABILITY]: profiles };
9
6
  // Why a contract's capability list cannot be enforced by this build, or nothing.
10
7
  export const unenforceable = (ids) => {
11
8
  const names = ids.map(id => id.split('/')[0]);
@@ -6,20 +6,20 @@ import { toMessage } from '../rooms/index.js';
6
6
  import { fail, note, short } from '../../../lib/index.js';
7
7
  import { link } from '../protocol/index.js';
8
8
  // What one reader accepted of one room: a record of the machine's state.
9
- const readerKey = (R, reader) => ['readers', R.hub.name, R.genesis, reader];
10
- export const readerLock = (R, reader, fn) => locked(`reader-${R.hub.name}-${R.genesis.slice(0, 12)}-${reader}`, fn);
9
+ const readerKey = (R, reader) => ['readers', R.hub.d.genesis, R.genesis, reader];
10
+ export const readerLock = (R, reader, fn) => locked(`reader-${R.hub.d.genesis}-${R.genesis}-${reader}`, fn);
11
11
  export const loadState = (R, reader) => hubPorts().state.read(readerKey(R, reader), {});
12
12
  export const saveState = (R, reader, st) => hubPorts().state.write(readerKey(R, reader), st);
13
13
  // The control history must extend what this reader accepted; a rewritten one delivers nothing.
14
14
  const acceptControl = (R, st) => {
15
- if (st.control && !R.hub.store.extends(st.control, R.head)) {
15
+ if (st.control && !R.store.extends(st.control, R.head)) {
16
16
  fail(`ALARM room ${R.name}: its control history was rewritten (accepted ${short(st.control)}, now ${short(R.head)}). Nothing is delivered; the operator decides`);
17
17
  }
18
18
  st.control = R.head;
19
19
  };
20
20
  // Why a log's current head cannot be accepted after `acc`, if it cannot.
21
21
  const logProblem = (R, id, acc, cur) => {
22
- const store = R.hub.store;
22
+ const store = R.store;
23
23
  if (!cur)
24
24
  return `log ${id} was deleted after ${short(acc)}`;
25
25
  if (acc && !store.extends(acc, cur))
@@ -31,7 +31,7 @@ const logProblem = (R, id, acc, cur) => {
31
31
  };
32
32
  // Every log that still extends its accepted head, pinned at its new head; the others become alarms.
33
33
  const acceptLogs = (R, reader, st, alarms) => {
34
- const store = R.hub.store, heads = st.heads ??= {};
34
+ const store = R.store, heads = st.heads ??= {};
35
35
  const logs = new Map();
36
36
  for (const id of new Set([...store.logIds(R.s), ...Object.keys(heads)])) {
37
37
  const cur = store.logHead(R.s, id), acc = heads[id];
@@ -49,19 +49,28 @@ const acceptLogs = (R, reader, st, alarms) => {
49
49
  return logs;
50
50
  };
51
51
  // The signals that agree with their messages are pinned; the rest become alarms.
52
- const acceptSignals = (R, logs, st, alarms) => {
53
- const records = R.hub.store.signals(R.s);
52
+ const acceptSignals = (R, logs, st, alarms, reader) => {
53
+ const records = R.store.signals(R.s);
54
54
  const checked = Signals.check(R, link(logs), records);
55
55
  const signalPins = Signals.pins(records, checked, st.signals);
56
+ const heads = new Map(R.store.signalHeads(R.s).map(x => [x.actor, x.head]));
57
+ for (const [actor, old] of Object.entries(st.signals ?? {})) {
58
+ const head = heads.get(actor);
59
+ if (!head || !R.store.extends(old, head)) {
60
+ alarms.push(`signal stream ${actor} was deleted or rewritten after ${short(old)}`);
61
+ signalPins.accepted[actor] = old;
62
+ }
63
+ }
64
+ for (const [actor, head] of Object.entries(signalPins.accepted))
65
+ if (st.signals?.[actor] !== head)
66
+ R.store.pinSignals(R.s, reader, actor, head);
56
67
  st.signals = signalPins.accepted;
57
- alarms.push(...signalPins.alarms, ...checked.filter(x => !x.ok).map(x => `signal ${x.ref}: ${x.why}`));
68
+ alarms.push(...signalPins.alarms, ...checked.filter(x => !x.ok && !x.pending).map(x => `signal ${x.ref}: ${x.why}`));
58
69
  };
59
70
  // What follows from an alarm, by what it is about.
60
71
  const remedy = (R, alarm) => {
61
72
  if (alarm.startsWith('signal '))
62
- return alarm.endsWith('was removed after it was accepted')
63
- ? 'the messages stay the truth; its author restores it with ringi agent signals --repair'
64
- : 'the messages stay the truth; the signal is ignored';
73
+ return 'the affected stream stops at its accepted prefix; messages stay the truth; the operator decides';
65
74
  if (alarm.startsWith('commit '))
66
75
  return 'it is skipped; the room reads on without it';
67
76
  return `it is quarantined and withheld. Restore it, or the operator runs ringi forget ${R.name}/<log>`;
@@ -73,9 +82,10 @@ const remedy = (R, alarm) => {
73
82
  export const loadRoomUnlocked = (R, reader) => {
74
83
  const st = loadState(R, reader);
75
84
  acceptControl(R, st);
76
- const alarms = [...R.hub.store.alarms?.(R.s) ?? []];
85
+ const alarms = [...R.store.alarms?.(R.s) ?? []];
77
86
  const logs = acceptLogs(R, reader, st, alarms);
78
- acceptSignals(R, logs, st, alarms);
87
+ acceptSignals(R, logs, st, alarms, reader);
88
+ alarms.push(...R.store.alarms?.(R.s) ?? []);
79
89
  const reported = new Set(st.alarms ?? []);
80
90
  for (const a of alarms)
81
91
  if (!reported.has(a))
@@ -1,9 +1,9 @@
1
- // store: the port every storage type implements, each an adapter (adapters/transport-<type>) that composition/
1
+ // store: the port every storage type implements, each an adapter (adapters/store-<type>) that composition/
2
2
  // registers. Rooms, accepted history, profiles and signals use only this
3
3
  // interface; git is the first implementation, and a hub's descriptor names which one it uses (`store.type`). A store
4
4
  // keeps the shared contract (versioned files the participants agree on: contract.json, profiles), the
5
5
  // rooms (a control record with versioned files, members and roster, and one append-only log per member) and signals
6
- // (named markers on log entries); the port is split by what each part does (Log, Attachments, …).
6
+ // (append-only actor facts about log entries); the port is split by what each part does (Log, Attachments, …).
7
7
  // Revisions are opaque, store-assigned ids. A store says whether one revision extends another; ringi builds the
8
8
  // trust rules on that (core/hub/history).
9
9
  export {};
@@ -0,0 +1,18 @@
1
+ // A storage address names its implementation explicitly; adapters receive only the locator they understand.
2
+ import { fail } from '../../../lib/index.js';
3
+ import { hubPorts } from '../ports/index.js';
4
+ export const storeOf = (address) => {
5
+ const m = typeof address === 'string' && !/[\r\n\0]/.test(address)
6
+ && /^([a-z][a-z0-9.-]*)(?:\+([a-z][a-z0-9.-]*))?:\/\/(.+)$/i.exec(address);
7
+ if (!m)
8
+ return fail('a room address must name its store scheme, such as git+ssh://, git+https:// or git+file://');
9
+ const type = m[1].toLowerCase();
10
+ if (['ssh', 'http', 'https', 'file'].includes(type))
11
+ fail('address has no explicit store type; use git+ssh://, git+https:// or git+file://');
12
+ return { type, locator: m[2] ? `${m[2].toLowerCase()}://${m[3]}` : address };
13
+ };
14
+ export const transportOf = (name, type) => {
15
+ const { transports, build } = hubPorts();
16
+ return transports[type] ?? fail(`hub ${name} uses a ${type} store; this ringi ${build.version} knows ${Object.keys(transports).join(', ')}`);
17
+ };
18
+ //# sourceMappingURL=address.js.map
@@ -0,0 +1,32 @@
1
+ // Invitation pins adopted as a catalogue, without a contract source or a membership grant.
2
+ import { hubPorts, locked } from '../ports/index.js';
3
+ import { fail } from '../../../lib/index.js';
4
+ import { descriptorRefusal } from './compat.js';
5
+ import { hubNames, openHub } from './hubs.js';
6
+ // An invitation supplies pins, never credentials or a default repository. Adoption grants no room membership.
7
+ const matchingHub = (d) => {
8
+ for (const existing of hubNames()) {
9
+ const H = openHub(existing), known = H.d;
10
+ if (known.genesis !== d.genesis)
11
+ continue;
12
+ if (known.trust !== d.trust || known.root !== d.root)
13
+ fail('invitation changes the trust or root of an existing hub genesis');
14
+ return H;
15
+ }
16
+ return null;
17
+ };
18
+ export const adoptHub = ({ trust, root, genesis }) => locked('hub-catalogue', () => {
19
+ const st = hubPorts().state, name = genesis.slice(0, 12);
20
+ const d = { schema: 'ringi-hub/3', name, trust, genesis, store: { type: hubPorts().store }, ...(root ? { root } : {}) };
21
+ const why = descriptorRefusal(d, name);
22
+ if (why)
23
+ fail(why);
24
+ const known = matchingHub(d);
25
+ if (known)
26
+ return known;
27
+ if (st.list(['hub']).includes(name))
28
+ fail('hub genesis prefix conflicts with an existing catalogue name; nothing is replaced');
29
+ st.write(['hub', name, 'hub'], d);
30
+ return openHub(name);
31
+ });
32
+ //# sourceMappingURL=adoption.js.map
@@ -8,14 +8,25 @@ import { fail, short, strictJson } from '../../../lib/index.js';
8
8
  import { HUB_MAJOR, PROTOCOL_MAJOR } from './index.js';
9
9
  const version = () => hubPorts().build.version;
10
10
  // What to do with a hub this ringi cannot read.
11
- const beside = 'make a new hub beside it (ringi hub create <name>), or a new root branch on a forge; ringi does not migrate';
11
+ const beside = 'make a new hub beside it (ringi hub create <name>), or a new space on a forge; ringi does not migrate';
12
12
  const newer = (major, mine) => major > mine ? 'upgrade ringi' : beside;
13
13
  // Why this ringi cannot read the hub's descriptor (hub.json), or nothing.
14
- const knownSchema = (d, name) => d.schema === 'ringi-hub/2' && d.name === name;
15
- const knownStore = (d) => ['signed', 'asserted'].includes(d.trust) && !!d.store?.type && !!d.store.path;
14
+ const oid = (s) => typeof s === 'string' && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(s);
15
+ const localPath = (s) => typeof s === 'string' && /^[a-zA-Z0-9._-]+(?:\/[a-zA-Z0-9._-]+)*$/.test(s) && s.split('/').every(p => p !== '.' && p !== '..');
16
+ const knownSchema = (d, name) => d?.schema === 'ringi-hub/3' && d.name === name;
17
+ const knownTrust = (d) => d.trust === 'signed'
18
+ ? typeof d.root === 'string' && !!d.root : d.trust === 'asserted' && !d.root;
19
+ const knownStore = (d) => {
20
+ const type = typeof d.store?.type === 'string' && !!d.store.type;
21
+ const pins = oid(d.genesis) && knownTrust(d);
22
+ const source = !d.contract || localPath(d.contract.path);
23
+ return type && pins && source;
24
+ };
16
25
  export const descriptorRefusal = (d, name) => knownSchema(d, name) && knownStore(d) ? null : `hub ${name}: its descriptor is not one this ringi ${version()} reads; ${beside}`;
17
26
  // Why this ringi cannot read the contract, or nothing.
18
- const refusal = (c, name) => {
27
+ const contractIdentity = (c, name) => {
28
+ if (!c || typeof c !== 'object')
29
+ return `hub ${name}: contract must be an object`;
19
30
  if (c.schema !== 'ringi-hub')
20
31
  return `hub ${name} declares schema ${String(c.schema)}, not ringi-hub`;
21
32
  const major = Number(String(c.version).split('.')[0]);
@@ -23,23 +34,37 @@ const refusal = (c, name) => {
23
34
  return `hub ${name} follows contract version ${c.version}; this ringi ${version()} supports ${HUB_MAJOR}.x: ${newer(major, HUB_MAJOR)}`;
24
35
  if (c.protocol !== PROTOCOL_MAJOR)
25
36
  return `hub ${name} speaks protocol ${String(c.protocol)}; this ringi ${version()} speaks ${PROTOCOL_MAJOR}: ${newer(Number(c.protocol), PROTOCOL_MAJOR)}`;
37
+ return null;
38
+ };
39
+ const contractFields = (c, name) => {
40
+ if (typeof c.store !== 'string' || !c.store)
41
+ return `hub ${name}: contract must name an explicit store layout`;
42
+ if (c.trust !== 'signed' && c.trust !== 'asserted')
43
+ return `hub ${name}: unknown contract trust`;
44
+ if (c.capabilities === undefined)
45
+ return null;
46
+ const list = Array.isArray(c.capabilities);
47
+ if (!list || c.capabilities.some(x => typeof x !== 'string'))
48
+ return `hub ${name}: capabilities must be a list of names`;
49
+ return null;
50
+ };
51
+ export const contractRefusal = (c, name) => {
52
+ const shape = contractIdentity(c, name) ?? contractFields(c, name);
53
+ if (shape)
54
+ return shape;
26
55
  const why = unenforceable(c.capabilities ?? []);
27
56
  return why && `hub ${name} ${why}, which this ringi ${version()} cannot enforce. Upgrade ringi`;
28
57
  };
29
- // Old contract snapshots stay immutable after an explicit transition. Only ancestors of a readable canonical
30
- // main may use the old filename; an old current head or a room's attachment is never accepted this way.
31
- const historicalContract = (store, name, rev) => {
32
- const head = store.contractHead();
33
- if (rev === head || !store.extends(rev, head))
34
- return null;
35
- contractAt(store, name, head);
36
- return store.contractFile(rev, 'hub.json');
58
+ export const endpointRefusal = (d, name, e) => {
59
+ const identity = e?.schema === 'ringi-room/1' && e.name === name && e.hubGenesis === d.genesis;
60
+ const storage = oid(e?.genesis) && localPath(e?.store?.path);
61
+ return identity && storage ? null : `room ${name}: its endpoint or genesis pins do not match hub ${d.name}; refusing another room's history`;
37
62
  };
38
63
  // The contract at `rev`, once it passes every gate.
39
64
  export const contractAt = (store, name, rev) => {
40
- const text = store.contractFile(rev, 'contract.json') ?? historicalContract(store, name, rev) ?? fail(`hub ${name} has no contract at ${short(rev)}`);
65
+ const text = store.contractFile(rev, 'contract.json') ?? fail(`hub ${name} has no contract at ${short(rev)}`);
41
66
  const c = strictJson(text, `hub ${name}: contract`);
42
- const why = refusal(c, name);
67
+ const why = contractRefusal(c, name);
43
68
  if (why)
44
69
  fail(why);
45
70
  return c;
@@ -0,0 +1,62 @@
1
+ // The operator's explicit contract source, distinct from frozen room copies.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { DEFAULT_PROFILE, PROFILES_CAPABILITY } from '../work/index.js';
5
+ import { hubPorts, locked } from '../ports/index.js';
6
+ import { fail, strictJson } from '../../../lib/index.js';
7
+ import { contractAt, contractRefusal, layoutRefusal } from './compat.js';
8
+ import { PROTOCOL_MAJOR } from './index.js';
9
+ import { transportOf } from './address.js';
10
+ const HUB_MD = `# ringi hub
11
+
12
+ The shared contract of a ringi hub (docs/spec/hub-2.md): contract.json (schema, version, protocol, trust, capabilities, the
13
+ default profile) and profiles/ (what a piece of work is in a room: its states, and who may move it). Use \`ringi\`; never edit by hand.
14
+ `;
15
+ export const readProfileResource = (name) => fs.readFileSync(path.join(hubPorts().build.resources, 'profiles', `${name}.json`), 'utf8');
16
+ // A new hub: local (asserted senders), or signed, with the operator's root key generated on this machine and pinned.
17
+ // The files of a new hub's first contract.
18
+ export const genesisFiles = (d, layout) => {
19
+ const contract = { schema: 'ringi-hub', version: '2.0', protocol: PROTOCOL_MAJOR, trust: d.trust, store: layout, capabilities: [PROFILES_CAPABILITY], profile: DEFAULT_PROFILE, ...(d.root ? { root: d.root } : {}) };
20
+ return {
21
+ 'contract.json': JSON.stringify(contract, null, 2) + '\n',
22
+ 'HUB.md': HUB_MD,
23
+ [`profiles/${DEFAULT_PROFILE}.json`]: readProfileResource(DEFAULT_PROFILE),
24
+ };
25
+ };
26
+ export const checkedContract = (H, store) => {
27
+ const main = store.contractHead();
28
+ if (!store.extends(H.d.genesis, main))
29
+ fail(`hub ${H.name}: main does not descend from its pinned genesis`);
30
+ const contract = contractAt(store, H.name, main);
31
+ const t = transportOf(H.name, store.type);
32
+ const why = layoutRefusal(H.name, contract.store, t.layout);
33
+ if (why)
34
+ fail(why);
35
+ if (contract.trust !== H.d.trust || (H.signed && contract.root !== H.d.root))
36
+ fail(`hub ${H.name}: contract changes its pinned trust or root`);
37
+ return { main, contract };
38
+ };
39
+ export const openContract = (H, { refresh = false } = {}) => {
40
+ if (!H.d.contract)
41
+ fail(`hub ${H.name}: no operator contract source; open a named room instead`);
42
+ const store = transportOf(H.name, H.d.store.type).open(H.dir, H.d);
43
+ // Each handle syncs once; refreshing explicitly reads the source again, never an arbitrary room.
44
+ void refresh;
45
+ store.sync();
46
+ return { store, ...checkedContract(H, store) };
47
+ };
48
+ export const amendContract = (H, who, text, files) => locked(`hub-${H.name}`, () => {
49
+ const { store } = openContract(H, { refresh: true });
50
+ if (files['contract.json'] !== undefined) {
51
+ const candidate = strictJson(files['contract.json'].toString(), `hub ${H.name}: proposed contract`);
52
+ const why = contractRefusal(candidate, H.name) ?? layoutRefusal(H.name, candidate.store, transportOf(H.name, H.d.store.type).layout);
53
+ if (why)
54
+ fail(why);
55
+ if (candidate.trust !== H.d.trust || candidate.root !== H.d.root)
56
+ fail('contract amendment must retain the pinned trust and root');
57
+ }
58
+ const rev = store.amendContract(who, text, files);
59
+ checkedContract(H, store);
60
+ return rev;
61
+ });
62
+ //# sourceMappingURL=contracts.js.map
@@ -1,60 +1,48 @@
1
1
  // hubs: a hub's descriptor beside its store, the contract it carries, and opening it.
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import { DEFAULT_PROFILE, PROFILES_CAPABILITY } from '../work/index.js';
5
- import { hubPorts } from '../ports/index.js';
2
+ import { hubPorts, locked } from '../ports/index.js';
6
3
  import { fail } from '../../../lib/index.js';
7
4
  import { compose } from '../protocol/index.js';
8
- import { contractAt, descriptorRefusal, layoutRefusal } from './compat.js';
9
- import { PROTOCOL_MAJOR } from './index.js';
5
+ import { descriptorRefusal } from './compat.js';
6
+ import { storeOf, transportOf } from './address.js';
7
+ import { genesisFiles } from './contracts.js';
8
+ export { storeOf, transportOf } from './address.js';
9
+ export { checkedContract, openContract, amendContract, readProfileResource } from './contracts.js';
10
10
  // A hub on this machine is a directory under `hub` with its descriptor (`hub`) in it.
11
11
  const has = (name) => hubPorts().state.list(['hub', name]).includes('hub');
12
12
  export const hubNames = () => hubPorts().state.list(['hub']).filter(has);
13
- const VERSION = () => hubPorts().build.version;
14
- const HUB_MD = `# ringi hub
15
-
16
- The shared contract of a ringi hub (docs/spec/hub-1.md): contract.json (schema, version, protocol, trust, capabilities, the
17
- default profile) and profiles/ (what a piece of work is in a room: its states, and who may move it). Use \`ringi\`; never edit by hand.
18
- `;
19
- export const readProfileResource = (name) => fs.readFileSync(path.join(hubPorts().build.resources, 'profiles', `${name}.json`), 'utf8');
20
- // A new hub: local (asserted senders), or signed, with the operator's root key generated on this machine and pinned.
21
- // The files of a new hub's first contract.
22
- const genesisFiles = (d, layout) => {
23
- const contract = { schema: 'ringi-hub', version: '1.2', protocol: PROTOCOL_MAJOR, trust: d.trust, store: layout, capabilities: [PROFILES_CAPABILITY], profile: DEFAULT_PROFILE, ...(d.root ? { root: d.root } : {}) };
24
- return {
25
- 'contract.json': JSON.stringify(contract, null, 2) + '\n',
26
- 'HUB.md': HUB_MD,
27
- [`profiles/${DEFAULT_PROFILE}.json`]: readProfileResource(DEFAULT_PROFILE),
28
- };
29
- };
30
- const descriptor = (name, signed, url) => ({ schema: 'ringi-hub/2', name, trust: signed ? 'signed' : 'asserted', store: { type: hubPorts().store, path: 'store', ...(url ? { remote: url } : {}) } });
31
- // The store type a descriptor names, as this build registers it.
32
- export const transportOf = (name, type) => {
33
- const t = hubPorts().transports;
34
- return t[type] ?? fail(`hub ${name} uses a ${type} store; this ringi ${VERSION()} knows ${Object.keys(t).join(', ')}`);
35
- };
13
+ const descriptor = (name, signed, url) => ({ schema: 'ringi-hub/3', name, trust: signed ? 'signed' : 'asserted', genesis: '', store: { type: url ? storeOf(url).type : hubPorts().store }, contract: { path: 'contract', ...(url ? { remote: url } : {}) } });
36
14
  // A new hub: its store made, then its descriptor written; a failure in between leaves no half-made hub. `custody`: where
37
15
  // a signed hub's new root key is kept.
38
- export const createHub = (name, { signed = false, url } = {}) => {
39
- const st = hubPorts().state;
40
- st.ensure();
41
- if (st.list(['hub']).includes(name))
16
+ const makeHub = (name, d) => {
17
+ const st = hubPorts().state, t = transportOf(name, d.store.type);
18
+ const made = t.create(st.place(['hub', name]), d, x => ({ files: genesisFiles(x, t.layout), text: compose(`hub ${name}`, [['To', '*'], ['Kind', 'hub']]) }));
19
+ d.genesis = made.store.contractHead();
20
+ const why = descriptorRefusal(d, name);
21
+ if (why)
22
+ fail(why);
23
+ st.write(['hub', name, 'hub'], d);
24
+ return { custody: made.custody };
25
+ };
26
+ const newHub = (name, signed, url) => {
27
+ if (!/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/.test(name))
28
+ fail(`bad hub name ${name}`);
29
+ if (hubPorts().state.list(['hub']).includes(name))
42
30
  fail(`hub ${name} exists`);
43
31
  if (url && !signed)
44
32
  fail('--url publishes a hub; only a --signed hub may be published');
45
- const d = descriptor(name, signed, url);
33
+ return descriptor(name, signed, url);
34
+ };
35
+ export const createHub = (name, { signed = false, url } = {}) => locked('hub-catalogue', () => {
36
+ const d = newHub(name, signed, url);
46
37
  try {
47
- const t = transportOf(name, d.store.type);
48
- const made = t.create(st.place(['hub', name]), d, x => ({ files: genesisFiles(x, t.layout), text: compose(`hub ${name}`, [['To', '*'], ['Kind', 'hub']]) }));
49
- st.write(['hub', name, 'hub'], d);
50
- return { custody: made.custody };
38
+ return makeHub(name, d);
51
39
  }
52
40
  catch (e) {
53
41
  if (!has(name))
54
- st.remove(['hub', name]);
42
+ hubPorts().state.remove(['hub', name]);
55
43
  throw e;
56
44
  }
57
- };
45
+ });
58
46
  const readDescriptor = (name) => {
59
47
  const d = hubPorts().state.read(['hub', name, 'hub'], null) ?? fail(`no hub ${name}; hubs: ${hubNames().join(', ') || 'none'}`);
60
48
  const why = descriptorRefusal(d, name);
@@ -62,28 +50,17 @@ const readDescriptor = (name) => {
62
50
  fail(why);
63
51
  return d;
64
52
  };
65
- // A hub is opened once per process; its store syncs then (a signed store fetches and verifies), and again on refresh.
66
- const opened = new Map();
53
+ // Opening a catalogue never chooses a repository or requires an operator contract source.
67
54
  export const openHub = (name, { refresh = false } = {}) => {
68
55
  hubPorts().state.ensure();
69
56
  if (!/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/.test(name))
70
57
  fail(`bad hub name ${name}`);
71
- if (opened.has(name) && !refresh)
72
- return opened.get(name);
73
58
  if (name === 'local' && !has(name))
74
59
  createHub('local');
75
60
  const d = readDescriptor(name);
76
- const dir = hubPorts().state.place(['hub', name]);
77
- const t = transportOf(name, d.store.type);
78
- const store = t.open(dir, d);
79
- store.sync();
80
- const main = store.contractHead();
81
- const contract = contractAt(store, name, main);
82
- const why = layoutRefusal(name, contract.store ?? t.layoutOf?.(store) ?? t.layout, t.layout);
83
- if (why)
84
- fail(why);
85
- const H = { name, dir, d, store, main, contract, signed: d.trust === 'signed' };
86
- opened.set(name, H);
87
- return H;
61
+ // Descriptors are cheap local reads. Refresh has no implicit fan-out to rooms.
62
+ void refresh;
63
+ return { name, dir: hubPorts().state.place(['hub', name]), d, signed: d.trust === 'signed' };
88
64
  };
65
+ export { adoptHub } from './adoption.js';
89
66
  //# sourceMappingURL=hubs.js.map
@@ -5,10 +5,10 @@
5
5
  import { CAPABILITIES } from '../capabilities.js';
6
6
  // The root principal of every hub and room (hub-1): the operator of the machine that made it.
7
7
  export const OPERATOR = 'operator';
8
- export const HUB_MAJOR = 1;
8
+ export const HUB_MAJOR = 2;
9
9
  export const PROTOCOL_MAJOR = 1;
10
10
  export const CAPS = new Set(Object.keys(CAPABILITIES));
11
- export const RESERVED_ROOMS = new Set(['main']);
11
+ export const RESERVED_ROOMS = new Set(['main', 'chat']);
12
12
  // Whether the room's work has started: it has the quorum of agents its genesis asked for (`Quorum`), or asked for none.
13
13
  export const start = (R) => {
14
14
  const agents = Object.values(R.members).filter(m => m.role !== 'root').length;