@agentshouse/kit 0.2.1-c7f53ff9eaf98 → 0.2.1-c83d468784cc9

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 (144) hide show
  1. package/README.md +28 -28
  2. package/dist/acp.js +90 -19
  3. package/dist/afk.js +17 -0
  4. package/dist/agents.js +87 -44
  5. package/dist/api.js +5 -5
  6. package/dist/authority-main.js +3 -3
  7. package/dist/bridge.js +5 -5
  8. package/dist/building.js +50 -0
  9. package/dist/calls.js +71 -0
  10. package/dist/chunks.js +49 -0
  11. package/dist/clis.js +45 -40
  12. package/dist/{stream.js → connection.js} +34 -52
  13. package/dist/conversations.js +498 -178
  14. package/dist/copies.js +315 -162
  15. package/dist/end.js +14 -4
  16. package/dist/events.js +66 -8
  17. package/dist/files.js +139 -37
  18. package/dist/git.js +47 -13
  19. package/dist/home.js +0 -7
  20. package/dist/house-cli.js +95 -87
  21. package/dist/house-main.js +3 -3
  22. package/dist/instructions.js +29 -9
  23. package/dist/kit-cli.js +4 -4
  24. package/dist/kit-main.js +3 -3
  25. package/dist/login.js +4 -4
  26. package/dist/logout.js +1 -1
  27. package/dist/mcp.js +6 -2
  28. package/dist/parts.js +10 -0
  29. package/dist/placement.js +21 -20
  30. package/dist/relay.js +21 -0
  31. package/dist/resident.js +51 -38
  32. package/dist/scope.js +15 -4
  33. package/dist/scripts.js +255 -0
  34. package/dist/secret-input.js +1 -1
  35. package/dist/service.js +23 -1
  36. package/dist/sign-in.js +22 -39
  37. package/dist/silence.js +28 -0
  38. package/dist/skills.js +42 -97
  39. package/dist/transcription.js +83 -43
  40. package/dist/translate.js +2 -8
  41. package/dist/update.js +113 -127
  42. package/dist/wire.js +37 -3
  43. package/package.json +20 -21
  44. package/dist/bootstrap.json +0 -24
  45. package/dist/clis-main.js +0 -23
  46. package/how-we-work.md +0 -33
  47. package/skills/ask-matt/LICENSE +0 -21
  48. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  49. package/skills/ask-matt/SKILL.md +0 -97
  50. package/skills/ask-matt/agents/openai.yaml +0 -5
  51. package/skills/code-review/LICENSE +0 -21
  52. package/skills/code-review/SKILL.md +0 -89
  53. package/skills/code-review/agents/openai.yaml +0 -3
  54. package/skills/codebase-design/DEEPENING.md +0 -37
  55. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  56. package/skills/codebase-design/LICENSE +0 -21
  57. package/skills/codebase-design/SKILL.md +0 -114
  58. package/skills/codebase-design/agents/openai.yaml +0 -3
  59. package/skills/diagnosing-bugs/LICENSE +0 -21
  60. package/skills/diagnosing-bugs/SKILL.md +0 -138
  61. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  62. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  63. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  64. package/skills/domain-modeling/GLOSSARY-FORMAT.md +0 -60
  65. package/skills/domain-modeling/LICENSE +0 -21
  66. package/skills/domain-modeling/SKILL.md +0 -74
  67. package/skills/domain-modeling/agents/openai.yaml +0 -3
  68. package/skills/grill-me/LICENSE +0 -21
  69. package/skills/grill-me/SKILL.md +0 -7
  70. package/skills/grill-me/agents/openai.yaml +0 -5
  71. package/skills/grill-with-docs/LICENSE +0 -21
  72. package/skills/grill-with-docs/SKILL.md +0 -7
  73. package/skills/grill-with-docs/agents/openai.yaml +0 -5
  74. package/skills/grilling/LICENSE +0 -21
  75. package/skills/grilling/SKILL.md +0 -30
  76. package/skills/grilling/agents/openai.yaml +0 -3
  77. package/skills/handoff/LICENSE +0 -21
  78. package/skills/handoff/SKILL.md +0 -16
  79. package/skills/handoff/agents/openai.yaml +0 -5
  80. package/skills/implement/LICENSE +0 -21
  81. package/skills/implement/SKILL.md +0 -17
  82. package/skills/implement/agents/openai.yaml +0 -5
  83. package/skills/implement-spec/LICENSE +0 -21
  84. package/skills/implement-spec/SKILL.md +0 -40
  85. package/skills/implement-spec/agents/openai.yaml +0 -5
  86. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  87. package/skills/improve-codebase-architecture/LICENSE +0 -21
  88. package/skills/improve-codebase-architecture/SKILL.md +0 -71
  89. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
  90. package/skills/pr/CREDITS.md +0 -3
  91. package/skills/pr/LICENSE +0 -21
  92. package/skills/pr/SKILL.md +0 -170
  93. package/skills/pr/agents/openai.yaml +0 -3
  94. package/skills/prototype/LICENSE +0 -21
  95. package/skills/prototype/LOGIC.md +0 -67
  96. package/skills/prototype/SKILL.md +0 -26
  97. package/skills/prototype/UI.md +0 -112
  98. package/skills/prototype/agents/openai.yaml +0 -3
  99. package/skills/research/LICENSE +0 -21
  100. package/skills/research/SKILL.md +0 -12
  101. package/skills/research/agents/openai.yaml +0 -3
  102. package/skills/retro/LICENSE +0 -21
  103. package/skills/retro/SKILL.md +0 -44
  104. package/skills/retro/agents/openai.yaml +0 -5
  105. package/skills/tdd/LICENSE +0 -21
  106. package/skills/tdd/SKILL.md +0 -38
  107. package/skills/tdd/agents/openai.yaml +0 -3
  108. package/skills/tdd/mocking.md +0 -59
  109. package/skills/tdd/tests.md +0 -77
  110. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  111. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  112. package/skills/teach/LICENSE +0 -21
  113. package/skills/teach/MISSION-FORMAT.md +0 -31
  114. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  115. package/skills/teach/SKILL.md +0 -140
  116. package/skills/teach/agents/openai.yaml +0 -5
  117. package/skills/to-questionnaire/LICENSE +0 -21
  118. package/skills/to-questionnaire/SKILL.md +0 -54
  119. package/skills/to-questionnaire/agents/openai.yaml +0 -5
  120. package/skills/to-spec/LICENSE +0 -21
  121. package/skills/to-spec/SKILL.md +0 -75
  122. package/skills/to-spec/agents/openai.yaml +0 -5
  123. package/skills/to-tickets/LICENSE +0 -21
  124. package/skills/to-tickets/SKILL.md +0 -105
  125. package/skills/to-tickets/agents/openai.yaml +0 -5
  126. package/skills/triage/AGENT-BRIEF.md +0 -207
  127. package/skills/triage/LICENSE +0 -21
  128. package/skills/triage/OUT-OF-SCOPE.md +0 -105
  129. package/skills/triage/SKILL.md +0 -112
  130. package/skills/triage/agents/openai.yaml +0 -5
  131. package/skills/wait-what/LICENSE +0 -21
  132. package/skills/wait-what/SKILL.md +0 -7
  133. package/skills/wait-what/agents/openai.yaml +0 -5
  134. package/skills/wayfinder/LICENSE +0 -21
  135. package/skills/wayfinder/SKILL.md +0 -128
  136. package/skills/wayfinder/agents/openai.yaml +0 -5
  137. package/skills/wizard/LICENSE +0 -21
  138. package/skills/wizard/SKILL.md +0 -44
  139. package/skills/wizard/agents/openai.yaml +0 -3
  140. package/skills/wizard/template.sh +0 -223
  141. package/skills/writing-for-agents/LICENSE +0 -21
  142. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  143. package/skills/writing-for-agents/SKILL.md +0 -81
  144. package/skills/writing-for-agents/agents/openai.yaml +0 -3
package/dist/copies.js CHANGED
@@ -1,17 +1,31 @@
1
1
  import { parse } from '@agentshouse/mdmodel';
2
- import { randomUUID } from 'node:crypto';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
3
  import { mkdir, readFile, readdir, realpath, rename, rm, rmdir, stat, writeFile } from 'node:fs/promises';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { finalRefusal, HouseRefusal, retrying } from './api.js';
6
+ import { chunkTexts, stagedChunks } from './chunks.js';
6
7
  import { blobId, blobText, git, isText, HOUSE_REF, importCommits, localCopies, revision, run, succeeded, tree, } from './git.js';
8
+ import { paused } from './files.js';
7
9
  import { kitHome } from './home.js';
8
10
  import { operationId } from './operation.js';
9
11
  import { refusalOf } from './refusals.js';
10
12
  import { changed } from './translate.js';
11
- import { relayed } from './relay.js';
12
13
  const BRANCH = 'main';
13
14
  const PROVENANCE = '_provenance';
14
- const IDENTITY = { GIT_COMMITTER_NAME: 'House Kit', GIT_COMMITTER_EMAIL: 'house-kit@localhost' };
15
+ const IDENTITY = { GIT_COMMITTER_NAME: 'Kit', GIT_COMMITTER_EMAIL: 'house-kit@localhost' };
16
+ const ROOMS = 'rooms/';
17
+ const ENUMERATE = '/kit/door/changes/enumerate';
18
+ const NOT_READY = 'not ready yet; Kit copies it once House has built it';
19
+ const STAGED_CHUNKS = 'house-chunks';
20
+ // A Room answered not ready is asked again on this schedule: its chunk build usually catches up within seconds, so the first asks come soon and the last repeats until it does.
21
+ const ASKED_AGAIN_MS = [1000, 2000, 3000, 5000, 8000, 11_000, 15_000];
22
+ class NotReady extends Error {
23
+ rooms;
24
+ constructor(rooms) {
25
+ super(NOT_READY);
26
+ this.rooms = rooms;
27
+ }
28
+ }
15
29
  function causeOf(error) {
16
30
  return error instanceof Error ? error.message : String(error);
17
31
  }
@@ -58,6 +72,9 @@ function expiredAuthority(error) {
58
72
  return undefined;
59
73
  }
60
74
  }
75
+ function grouped(items, size) {
76
+ return items.flatMap((_, index) => (index % size === 0 ? [items.slice(index, index + size)] : []));
77
+ }
61
78
  function covers(path, prefix) {
62
79
  return prefix === '' || path === prefix || path.startsWith(`${prefix}/`);
63
80
  }
@@ -69,6 +86,9 @@ function sidecar(path) {
69
86
  function recordOf(key) {
70
87
  return join(kitHome(), 'local-copies', `${key}.json`);
71
88
  }
89
+ function stagingPrefix() {
90
+ return `.${createHash('sha256').update(kitHome()).digest('hex').slice(0, 16)}-`;
91
+ }
72
92
  function rootOf(handle) {
73
93
  return `/rooms/${handle}`;
74
94
  }
@@ -96,32 +116,59 @@ async function removeFile(repository, path) {
96
116
  return;
97
117
  }
98
118
  }
99
- async function readOnlyPlaced(repository, held, written, removed) {
119
+ async function readOnlyFiles(repository, held) {
100
120
  const tracked = new Set((await git(repository, ['ls-files', '-z'])).split('\0'));
101
121
  const holding = new Set(held);
102
- for (const path of removed) {
103
- if (!holding.has(path))
104
- continue;
105
- if (!tracked.has(path))
106
- await removeFile(repository, path);
107
- holding.delete(path);
108
- }
109
- for (const [path, content] of written) {
110
- if (tracked.has(path))
111
- continue;
112
- if (!holding.has(path) && (await exists(join(repository, path))))
113
- continue;
114
- await writeText(join(repository, path), content);
115
- holding.add(path);
116
- }
117
- return [...holding].sort();
122
+ return {
123
+ remove: async (path) => {
124
+ if (!holding.has(path))
125
+ return;
126
+ if (!tracked.has(path))
127
+ await removeFile(repository, path);
128
+ holding.delete(path);
129
+ },
130
+ place: async (path, content) => {
131
+ if (tracked.has(path))
132
+ return;
133
+ if (!holding.has(path) && (await exists(join(repository, path))))
134
+ return;
135
+ await writeText(join(repository, path), content);
136
+ holding.add(path);
137
+ },
138
+ held: () => [...holding].sort(),
139
+ };
140
+ }
141
+ async function readOnlyPlaced(repository, held, written, removed) {
142
+ const files = await readOnlyFiles(repository, held);
143
+ for (const path of removed)
144
+ await files.remove(path);
145
+ for (const [path, content] of written)
146
+ await files.place(path, content);
147
+ return files.held();
148
+ }
149
+ async function importedWhole(repository, scope, texts, commit, readOnly) {
150
+ const revisions = {};
151
+ async function* tracked() {
152
+ for await (const { path, content, revision: held, provenance } of texts) {
153
+ if (declaredWritable(path, scope)) {
154
+ revisions[path] = held;
155
+ yield [path, content];
156
+ }
157
+ else {
158
+ await readOnly.place(path, content);
159
+ }
160
+ if (provenance !== undefined)
161
+ await readOnly.place(sidecar(path), provenance);
162
+ }
163
+ }
164
+ return { house: await importCommits(repository, [{ ...commit, files: tracked() }]), revisions };
118
165
  }
119
166
  async function ruled(path, rules) {
120
167
  if ((await readFile(path, 'utf8').catch(() => null)) !== rules)
121
168
  await writeFile(path, rules);
122
169
  }
123
170
  async function ignoreRules(repository, writable, copy) {
124
- const readOnly = ('read_only' in copy ? copy.read_only : [...copy.readOnly.keys()]).filter((path) => !path.split('/').includes(PROVENANCE));
171
+ const readOnly = copy.read_only.filter((path) => !path.split('/').includes(PROVENANCE));
125
172
  const protectedPath = (path) => copy.protected.some((prefix) => prefix !== '' && covers(path, prefix));
126
173
  const whole = copy.protected.filter((prefix) => prefix !== '' && !writable.some((path) => covers(path, prefix)));
127
174
  const house = [
@@ -130,7 +177,7 @@ async function ignoreRules(repository, writable, copy) {
130
177
  ].map(pattern);
131
178
  const granted = readOnly.filter((path) => !protectedPath(path)).map(pattern);
132
179
  await ruled(join(repository, '.gitignore'), [
133
- '# House Kit writes this file and .ignore. The paths below are read-only House',
180
+ '# Kit writes this file and .ignore. The paths below are read-only House',
134
181
  '# content: Git does not track them, and House refuses a change to them even',
135
182
  '# when this file is edited or a path is force-added.',
136
183
  '/.gitignore',
@@ -145,7 +192,7 @@ async function ignoreRules(repository, writable, copy) {
145
192
  '',
146
193
  ].join('\n'));
147
194
  await ruled(join(repository, '.ignore'), [
148
- '# House Kit writes this file so native search still reads the read-only House',
195
+ '# Kit writes this file so native search still reads the read-only House',
149
196
  '# content that .gitignore keeps out of Git.',
150
197
  `!${PROVENANCE}`,
151
198
  ...[...house, ...granted].map((rule) => `!${rule}`),
@@ -181,6 +228,7 @@ export class LocalCopies {
181
228
  house;
182
229
  copies = new Map();
183
230
  queues = new Map();
231
+ waiting = new Set();
184
232
  answering = Promise.resolve();
185
233
  constructor(house) {
186
234
  this.house = house;
@@ -188,7 +236,7 @@ export class LocalCopies {
188
236
  key: 'owner',
189
237
  submit: async (operation, changes) => {
190
238
  try {
191
- const outcome = await retrying(() => house.post('/kit/door/batch', { operation_id: operation, changes }));
239
+ const outcome = await retrying(() => house.post('/kit/door/files/batch', { operation_id: operation, changes }));
192
240
  return { accepted: outcome.contents };
193
241
  }
194
242
  catch (error) {
@@ -212,83 +260,135 @@ export class LocalCopies {
212
260
  this.copies.set(copy.key, copy);
213
261
  }
214
262
  }
263
+ async swept() {
264
+ const rooms = join(localCopies(), 'rooms');
265
+ for (const name of await readdir(rooms).catch(() => [])) {
266
+ if (name.startsWith(stagingPrefix()))
267
+ await rm(join(rooms, name), { recursive: true, force: true });
268
+ }
269
+ }
215
270
  async mapping() {
216
271
  await this.answering;
217
272
  return [...this.copies.values()]
218
- .filter((copy) => copy.erasing === undefined)
273
+ .filter((copy) => copy.unfollowed === undefined)
219
274
  .map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
220
275
  .sort();
221
276
  }
222
- async connected() {
223
- const copied = await this.answered(() => this.fetched(false));
224
- if (copied.refused)
225
- logged(copied.text);
277
+ connected() {
278
+ return this.selected();
279
+ }
280
+ async changed(rooms) {
281
+ if (rooms.length > 0)
282
+ await this.selected(rooms);
226
283
  }
227
284
  fetch() {
228
285
  return this.answered(() => this.fetched(true));
229
286
  }
287
+ async selected(rooms) {
288
+ const copied = await this.answered(() => this.fetched(false, rooms));
289
+ if (copied.refused)
290
+ logged(copied.text);
291
+ }
230
292
  answered(work) {
231
293
  const answered = this.answering.then(work);
232
294
  this.answering = answered.catch(() => undefined);
233
295
  return answered;
234
296
  }
235
- async fetched(refreshing) {
236
- const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', {});
237
- const synced = new Map(answer.rooms.map((room) => [`rooms/${room.room_ref}`, room.room_handle]));
238
- return this.holding([...new Set([...this.copies.keys(), ...synced.keys()])].sort(), async () => {
297
+ async fetched(refreshing, rooms) {
298
+ const { local_copy: answer } = await this.house.post('/kit/local-copies/selection/read', rooms === undefined ? {} : { rooms });
299
+ const synced = new Map(answer.rooms.map((room) => [`${ROOMS}${room.room_ref}`, room.room_handle]));
300
+ const named = rooms === undefined ? null : new Set(rooms.map((room) => `${ROOMS}${room}`));
301
+ const held = () => [...this.copies.values()].filter((copy) => named === null || named.has(copy.key));
302
+ return this.holding([...new Set([...held().map((copy) => copy.key), ...synced.keys()])].sort(), async () => {
239
303
  const said = [];
240
304
  let refused = false;
241
- for (const copy of [...this.copies.values()]) {
242
- const returned = synced.has(copy.key);
243
- if (returned && copy.erasing === undefined)
305
+ const mark = refreshing ? 'said' : 'unsaid';
306
+ for (const copy of held()) {
307
+ if (synced.has(copy.key) || copy.unfollowed === 'said' || copy.unfollowed === mark)
244
308
  continue;
245
- try {
246
- await this.erase(copy);
247
- said.push(`${rootOf(copy.room_handle)}: ${returned ? '' : 'no longer synced to this Environment; '}erased from the Local copy`);
248
- }
249
- catch (error) {
250
- refused = true;
251
- said.push(`${rootOf(copy.room_handle)}: ${causeOf(error)}`);
252
- }
309
+ await this.save({ ...copy, unfollowed: mark });
310
+ if (refreshing)
311
+ said.push(`${rootOf(copy.room_handle)}: no longer followed; kept as it is`);
253
312
  }
254
313
  const current = [];
314
+ const wanted = new Map();
255
315
  for (const [key, handle] of synced) {
256
316
  try {
257
317
  const held = this.copies.get(key);
258
- if (held?.erasing)
259
- continue;
260
318
  if (held === undefined || !(await exists(held.repository))) {
261
- said.push(await this.materialize(key, handle));
319
+ wanted.set(key, handle);
262
320
  continue;
263
321
  }
264
- current.push(held.room_handle === handle ? held : await this.save({ ...held, room_handle: handle }));
322
+ const { unfollowed, ...followed } = held;
323
+ current.push(unfollowed === undefined && held.room_handle === handle ? held : await this.save({ ...followed, room_handle: handle }));
265
324
  }
266
325
  catch (error) {
267
326
  refused = true;
268
327
  said.push(`${rootOf(handle)}: ${causeOf(error)}`);
269
328
  }
270
329
  }
271
- if (refreshing && current.length > 0) {
272
- try {
273
- const refreshed = await this.refreshed(current);
274
- current.forEach((copy, index) => said.push(...moved(copy, refreshed[index])));
330
+ const unready = [];
331
+ const unsettled = [];
332
+ if (wanted.size > 0) {
333
+ const { authorities } = await this.discovered([...wanted.values()]);
334
+ for (const [key, handle] of wanted) {
335
+ const authority = authorities.find((entry) => `${ROOMS}${entry.room_ref}` === key);
336
+ try {
337
+ if (authority === undefined)
338
+ throw new Error('House serves nothing of it to copy');
339
+ said.push(await this.copied({ key, handle, authority }));
340
+ }
341
+ catch (error) {
342
+ if (error instanceof NotReady) {
343
+ unready.push(...error.rooms);
344
+ said.push(`${rootOf(handle)}: ${NOT_READY}`);
345
+ }
346
+ else {
347
+ refused = true;
348
+ unsettled.push(key.slice(ROOMS.length));
349
+ said.push(`${rootOf(handle)}: ${causeOf(error)}`);
350
+ }
351
+ }
275
352
  }
276
- catch (error) {
353
+ }
354
+ const reading = refreshing ? current : current.filter((copy) => this.waiting.has(copy.room_ref));
355
+ if (reading.length > 0) {
356
+ const failed = await this.refreshed(reading, unready).then(() => null, houseCause);
357
+ for (const copy of reading) {
358
+ said.push(...(unready.includes(copy.room_ref) ? [`${rootOf(copy.room_handle)}: ${NOT_READY}`] : moved(copy, this.copies.get(copy.key))));
359
+ }
360
+ if (failed !== null) {
277
361
  refused = true;
278
- said.push(houseCause(error));
362
+ unsettled.push(...reading.map((copy) => copy.room_ref));
363
+ said.push(failed);
279
364
  }
280
365
  }
366
+ for (const room of rooms ?? [...this.waiting]) {
367
+ if (!unready.includes(room) && !unsettled.includes(room))
368
+ this.waiting.delete(room);
369
+ }
370
+ const due = unready.filter((room) => !this.waiting.has(room));
371
+ for (const room of due)
372
+ this.waiting.add(room);
373
+ if (due.length > 0)
374
+ void this.askedAgain(due);
281
375
  return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
282
376
  });
283
377
  }
378
+ async askedAgain(rooms) {
379
+ let asking = rooms;
380
+ for (let step = 0; asking.length > 0; step++) {
381
+ await paused(ASKED_AGAIN_MS[Math.min(step, ASKED_AGAIN_MS.length - 1)]);
382
+ await this.selected().catch(logged);
383
+ asking = asking.filter((room) => this.waiting.has(room));
384
+ }
385
+ }
284
386
  async observed(report) {
285
387
  const key = await this.keyAt(dirname(report.common));
286
388
  if (key === undefined)
287
389
  return;
288
390
  await this.exclusive(key, async () => {
289
391
  const copy = this.copies.get(key);
290
- if (copy === undefined || copy.erasing)
291
- return;
292
392
  let moves = copy.moves;
293
393
  for (const { from, to } of report.moves) {
294
394
  const held = moves.find((move) => move.worktree === report.worktree && move.to === from);
@@ -344,133 +444,170 @@ export class LocalCopies {
344
444
  this.copies.set(copy.key, copy);
345
445
  return copy;
346
446
  }
347
- async erase(copy) {
348
- await this.save({ ...copy, erasing: true });
349
- await rm(copy.repository, { recursive: true, force: true });
350
- await rm(recordOf(copy.key), { force: true });
351
- this.copies.delete(copy.key);
352
- }
353
- async read(handle) {
354
- const prefixes = [rootOf(handle)];
355
- const discovery = await this.house.post('/kit/door/discover', { prefixes });
356
- let bundle = await this.house.post('/kit/door/bootstrap', { prefixes });
357
- if ('transfer' in bundle)
358
- bundle = (await (await fetch(relayed(bundle.transfer.url))).json());
359
- const [view] = bundle.scopes;
360
- if (view === undefined)
361
- return null;
362
- const authority = discovery.authorities.find((entry) => entry.room_ref === view.manifest.anchor);
363
- const contents = new Map(view.contents.map((entry) => [entry.path, entry.content]));
364
- const read = {
365
- anchor: view.manifest.anchor,
366
- position: view.manifest.position,
367
- log_epoch: view.manifest.log_epoch,
368
- write: authority.operations.write,
369
- protected: authority.protected_paths,
370
- writable: new Map(),
371
- revisions: {},
372
- readOnly: new Map(),
373
- };
374
- for (const file of view.manifest.files) {
375
- const content = contents.get(file.path);
376
- if (file.writable) {
377
- read.writable.set(file.path, content);
378
- read.revisions[file.path] = file.revision;
379
- }
380
- else {
381
- read.readOnly.set(file.path, content);
447
+ async discovered(handles) {
448
+ const discover = (group) => this.house.post('/kit/door/rooms/discover', { prefixes: group.map(rootOf) });
449
+ const [first, ...rest] = handles;
450
+ const { bounds, authorities } = await discover([first]);
451
+ for (const group of grouped(rest, bounds.max_prefixes))
452
+ authorities.push(...(await discover(group)).authorities);
453
+ return { bounds, authorities };
454
+ }
455
+ async chunked(root, room, directory) {
456
+ await mkdir(directory, { recursive: true });
457
+ for (let renewed = false;; renewed = true) {
458
+ const { rooms } = await this.house.post('/kit/door/room-chunks/list', { prefixes: [root] });
459
+ const list = rooms.find((entry) => entry.room_ref === room);
460
+ if (list === undefined)
461
+ throw new Error('House serves nothing of it to copy');
462
+ if (!list.ready)
463
+ throw new NotReady([room]);
464
+ const held = new Set(await readdir(directory));
465
+ const failure = await stagedChunks(directory, list.chunks.filter(({ hash }) => !held.has(hash)));
466
+ if (failure === undefined) {
467
+ return { position: list.position, log_epoch: list.log_epoch, texts: chunkTexts(directory, list.chunks, list.models) };
382
468
  }
383
- if (file.provenance !== undefined)
384
- read.readOnly.set(sidecar(file.path), file.provenance);
469
+ if (renewed && (await readdir(directory)).length === held.size)
470
+ throw failure;
385
471
  }
386
- return read;
387
472
  }
388
- async materialize(key, handle) {
389
- const read = await this.read(handle);
390
- if (read === null)
391
- throw new Error('House serves nothing of it to copy');
392
- const repository = join(localCopies(), 'rooms', read.anchor);
473
+ async materialized({ key, handle, authority }, whole) {
474
+ const repository = join(localCopies(), 'rooms', authority.room_ref);
393
475
  if (await exists(repository))
394
- throw new Error(`${repository} already exists and House Kit leaves it as it is`);
395
- const staging = join(dirname(repository), `.${randomUUID()}`);
476
+ throw new Error(`${repository} already exists and Kit leaves it as it is`);
477
+ const staging = join(dirname(repository), `${stagingPrefix()}${randomUUID()}`);
478
+ const scope = { write: authority.operations.write, protected: authority.protected_paths };
396
479
  await mkdir(staging, { recursive: true });
397
- let house;
398
- let readOnly;
480
+ let copy;
399
481
  try {
400
482
  await git(staging, ['init', '-q', '-b', BRANCH]);
401
- house = await importCommits(staging, [
402
- { message: `House ${rootOf(handle)}`, from: null, files: read.writable },
403
- ]);
483
+ const read = await whole(join(staging, '.git', STAGED_CHUNKS));
484
+ const readOnly = await readOnlyFiles(staging, []);
485
+ const { house, revisions } = await importedWhole(staging, scope, read.texts, { message: `House ${rootOf(handle)}`, from: null }, readOnly);
404
486
  await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
405
487
  await git(staging, ['reset', '-q', '--hard']);
406
- readOnly = await readOnlyPlaced(staging, [], read.readOnly, []);
407
- await ignoreRules(staging, [...read.writable.keys()], read);
488
+ copy = {
489
+ key,
490
+ room_ref: authority.room_ref,
491
+ room_handle: handle,
492
+ repository,
493
+ house,
494
+ position: read.position,
495
+ log_epoch: read.log_epoch,
496
+ ...scope,
497
+ read_only: readOnly.held(),
498
+ manifests: { [house]: { parent: null, files: revisions } },
499
+ moves: [],
500
+ };
501
+ await ignoreRules(staging, Object.keys(revisions), copy);
502
+ await rm(join(staging, '.git', STAGED_CHUNKS), { recursive: true, force: true });
408
503
  await rename(staging, repository);
409
504
  }
410
505
  catch (error) {
411
506
  await rm(staging, { recursive: true, force: true });
412
507
  throw error;
413
508
  }
414
- await this.save({
415
- key,
416
- room_ref: read.anchor,
417
- room_handle: handle,
418
- repository,
419
- house,
420
- position: read.position,
421
- log_epoch: read.log_epoch,
422
- write: read.write,
423
- protected: read.protected,
424
- read_only: readOnly,
425
- manifests: { [house]: { parent: null, files: read.revisions } },
426
- moves: [],
427
- });
428
- return `${rootOf(handle)}: copied to ${repository}`;
509
+ return this.save(copy);
429
510
  }
430
- async received(copy) {
431
- const read = await this.read(copy.room_handle);
432
- if (read === null)
433
- return copy;
434
- const house = await importCommits(copy.repository, [
435
- { message: `House ${rootOf(copy.room_handle)} at position ${read.position}`, from: copy.house, replaces: true, files: read.writable },
436
- ]);
437
- const removed = copy.read_only.filter((path) => !read.readOnly.has(path));
438
- const readOnly = await readOnlyPlaced(copy.repository, copy.read_only, read.readOnly, removed);
439
- const next = {
440
- ...copy,
441
- house,
442
- position: read.position,
443
- log_epoch: read.log_epoch,
444
- write: read.write,
445
- protected: read.protected,
446
- read_only: readOnly,
447
- manifests: { ...copy.manifests, [house]: { parent: null, files: read.revisions } },
448
- };
449
- await ignoreRules(copy.repository, [...read.writable.keys()], next);
450
- return next;
511
+ async copied(wanted) {
512
+ const { handle, authority } = wanted;
513
+ const root = rootOf(handle);
514
+ let answer = null;
515
+ try {
516
+ if (!this.waiting.has(authority.room_ref)) {
517
+ answer = await this.house.post(ENUMERATE, {
518
+ prefixes: [root],
519
+ positions: [
520
+ { authority: authority.room_ref, position: '0', log_epoch: authority.log_epoch, page: { through: authority.position, read: 0 } },
521
+ ],
522
+ });
523
+ }
524
+ }
525
+ catch (error) {
526
+ if (expiredAuthority(error) !== authority.room_ref)
527
+ throw error;
528
+ }
529
+ const view = answer?.authorities.find((entry) => entry.authority === authority.room_ref);
530
+ if (answer !== null && view === undefined)
531
+ throw new Error('House serves nothing of it to copy');
532
+ if (view !== undefined && view.next === undefined) {
533
+ const texts = view.changes.filter((change) => !('removed' in change));
534
+ const copy = await this.materialized(wanted, async () => ({ position: view.position, log_epoch: view.log_epoch, texts }));
535
+ return `${root}: copied to ${copy.repository}`;
536
+ }
537
+ const copy = await this.materialized(wanted, (directory) => this.chunked(root, authority.room_ref, directory));
538
+ const [caught] = await this.caughtUp([copy], true);
539
+ await succeeded(copy.repository, ['merge', '--ff-only', '-q', caught.house]);
540
+ return `${root}: copied to ${copy.repository}`;
451
541
  }
452
- async refreshed(copies) {
453
- const discovery = await this.house.post('/kit/door/discover', {
454
- prefixes: copies.map((copy) => rootOf(copy.room_handle)),
455
- });
456
- const regranted = new Map();
542
+ async received(copy, authority) {
543
+ const root = rootOf(copy.room_handle);
544
+ const directory = join(copy.repository, '.git', STAGED_CHUNKS);
545
+ const scope = { write: authority.operations.write, protected: authority.protected_paths };
546
+ try {
547
+ const read = await this.chunked(root, copy.room_ref, directory);
548
+ const readOnly = await readOnlyFiles(copy.repository, copy.read_only);
549
+ for (const path of copy.read_only)
550
+ await readOnly.remove(path);
551
+ const commit = { message: `House ${root} at position ${read.position}`, from: copy.house, replaces: true };
552
+ const { house, revisions } = await importedWhole(copy.repository, scope, read.texts, commit, readOnly);
553
+ const next = {
554
+ ...copy,
555
+ ...scope,
556
+ house,
557
+ position: read.position,
558
+ log_epoch: read.log_epoch,
559
+ read_only: readOnly.held(),
560
+ manifests: { ...copy.manifests, [house]: { parent: null, files: revisions } },
561
+ };
562
+ await ignoreRules(copy.repository, Object.keys(revisions), next);
563
+ return next;
564
+ }
565
+ finally {
566
+ await rm(directory, { recursive: true, force: true });
567
+ }
568
+ }
569
+ async reread(copy) {
570
+ const { authorities } = await this.discovered([copy.room_handle]);
571
+ const authority = authorities.find((entry) => entry.room_ref === copy.room_ref);
572
+ if (authority === undefined)
573
+ throw new NotReady([copy.room_ref]);
574
+ return this.received(copy, authority);
575
+ }
576
+ async refreshed(copies, unready) {
577
+ const { bounds, authorities } = await this.discovered(copies.map((copy) => copy.room_handle));
578
+ const read = [];
579
+ const unchanged = [];
457
580
  for (const copy of copies) {
458
- const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
581
+ const authority = authorities.find((entry) => entry.room_ref === copy.room_ref);
459
582
  if (authority !== undefined &&
460
583
  JSON.stringify([authority.operations.write, authority.protected_paths]) !== JSON.stringify([copy.write, copy.protected])) {
461
- regranted.set(copy.key, await this.save(await this.received(copy)));
584
+ try {
585
+ read.push(await this.save(await this.received(copy, authority)));
586
+ }
587
+ catch (error) {
588
+ if (!(error instanceof NotReady))
589
+ throw error;
590
+ unready.push(...error.rooms);
591
+ }
592
+ }
593
+ else {
594
+ unchanged.push(copy);
462
595
  }
463
596
  }
464
- const caught = await this.caughtUp(copies.filter((copy) => !regranted.has(copy.key)), false);
465
- return copies.map((copy) => regranted.get(copy.key) ?? caught.find((held) => held.key === copy.key));
597
+ for (const group of grouped(unchanged, bounds.max_prefixes))
598
+ await this.caughtUp(group, false, unready);
599
+ for (const group of grouped(read, bounds.max_prefixes))
600
+ await this.caughtUp(group, true, unready);
466
601
  }
467
- async caughtUp(copies, whole) {
602
+ async caughtUp(copies, whole, unready) {
468
603
  const caught = new Map(copies.map((copy) => [copy.key, copy]));
604
+ const read = new Set();
605
+ const waiting = unready ?? [];
469
606
  let asking = [...copies];
470
607
  while (asking.length > 0) {
471
608
  let answer;
472
609
  try {
473
- answer = await this.house.post('/kit/door/enumerate', {
610
+ answer = await this.house.post(ENUMERATE, {
474
611
  prefixes: asking.map((copy) => rootOf(copy.room_handle)),
475
612
  positions: asking.map((copy) => ({ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch })),
476
613
  });
@@ -479,8 +616,22 @@ export class LocalCopies {
479
616
  const expired = asking.find((copy) => copy.room_ref === expiredAuthority(error));
480
617
  if (expired === undefined)
481
618
  throw error;
482
- caught.set(expired.key, await this.save(await this.received(expired)));
483
619
  asking = asking.filter((copy) => copy !== expired);
620
+ if (read.has(expired.key)) {
621
+ waiting.push(expired.room_ref);
622
+ continue;
623
+ }
624
+ read.add(expired.key);
625
+ try {
626
+ const next = await this.save(await this.reread(expired));
627
+ caught.set(expired.key, next);
628
+ asking.push(next);
629
+ }
630
+ catch (refused) {
631
+ if (!(refused instanceof NotReady))
632
+ throw refused;
633
+ waiting.push(expired.room_ref);
634
+ }
484
635
  continue;
485
636
  }
486
637
  const advanced = [];
@@ -492,8 +643,10 @@ export class LocalCopies {
492
643
  caught.set(copy.key, next);
493
644
  advanced.push(next);
494
645
  }
495
- asking = whole ? advanced : [];
646
+ asking = advanced.filter((copy) => whole || read.has(copy.key));
496
647
  }
648
+ if (unready === undefined && waiting.length > 0)
649
+ throw new NotReady(waiting);
497
650
  return copies.map((copy) => caught.get(copy.key));
498
651
  }
499
652
  async incorporated(copy, changes, position, logEpoch) {
@@ -544,8 +697,6 @@ export class LocalCopies {
544
697
  }
545
698
  async pushed(key, worktree, selector, caller) {
546
699
  let copy = this.copies.get(key);
547
- if (copy === undefined || copy.erasing)
548
- return outside(worktree);
549
700
  if (copy.push?.state === 'submitted')
550
701
  return this.settle(copy, caller);
551
702
  const selected = await run(worktree, ['rev-parse', '--verify', '--quiet', '--end-of-options', `${selector}^{commit}`]);
@@ -598,7 +749,7 @@ export class LocalCopies {
598
749
  const arrived = new Set(moved.values());
599
750
  const arriving = [...after].filter(([path, entry]) => before.get(path) !== entry);
600
751
  if (arriving.length > 0) {
601
- const { bounds } = await this.house.post('/kit/door/discover', { prefixes: [root] });
752
+ const { bounds } = await this.house.post('/kit/door/rooms/discover', { prefixes: [root] });
602
753
  const binary = [];
603
754
  for (const [path, entry] of arriving) {
604
755
  if (!(await isText(copy.repository, entry, bounds.max_authored_file_bytes)))
@@ -660,8 +811,10 @@ export class LocalCopies {
660
811
  }
661
812
  if (outcome.refused.code === 'expired')
662
813
  return this.recovered(copy);
663
- const rebased = rebaseFixes(outcome.refused);
664
814
  let settled = await this.save(without(copy));
815
+ if (settled.unfollowed !== undefined)
816
+ return { refused: true, text: outcome.refused.text };
817
+ const rebased = rebaseFixes(outcome.refused);
665
818
  try {
666
819
  settled = (await this.caughtUp([settled], true))[0];
667
820
  }
@@ -813,7 +966,7 @@ export class LocalCopies {
813
966
  return true;
814
967
  }
815
968
  async revisionText(path, wanted) {
816
- const page = await this.house.post('/kit/door/revisions', {
969
+ const page = await this.house.post('/kit/door/files/revisions/list', {
817
970
  path,
818
971
  revision: wanted,
819
972
  });