@agentshouse/kit 0.2.1-cbdf76b9a90a3 → 0.2.1-cbff58027f078

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 (142) hide show
  1. package/README.md +28 -28
  2. package/dist/acp.js +58 -7
  3. package/dist/afk.js +17 -0
  4. package/dist/agents.js +55 -32
  5. package/dist/api.js +1 -1
  6. package/dist/authority-main.js +2 -2
  7. package/dist/bridge.js +1 -1
  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} +28 -46
  13. package/dist/conversations.js +330 -125
  14. package/dist/copies.js +286 -149
  15. package/dist/end.js +11 -5
  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/mcp.js +6 -2
  27. package/dist/parts.js +10 -0
  28. package/dist/placement.js +10 -18
  29. package/dist/relay.js +21 -0
  30. package/dist/resident.js +49 -38
  31. package/dist/scope.js +12 -4
  32. package/dist/scripts.js +255 -0
  33. package/dist/service.js +1 -1
  34. package/dist/sign-in.js +22 -21
  35. package/dist/silence.js +28 -0
  36. package/dist/skills.js +42 -97
  37. package/dist/transcription.js +83 -43
  38. package/dist/update.js +113 -124
  39. package/dist/wire.js +23 -5
  40. package/package.json +20 -21
  41. package/dist/bootstrap.json +0 -24
  42. package/dist/clis-main.js +0 -23
  43. package/dist/inputs.js +0 -63
  44. package/how-we-work.md +0 -33
  45. package/skills/ask-matt/LICENSE +0 -21
  46. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  47. package/skills/ask-matt/SKILL.md +0 -97
  48. package/skills/ask-matt/agents/openai.yaml +0 -5
  49. package/skills/code-review/LICENSE +0 -21
  50. package/skills/code-review/SKILL.md +0 -89
  51. package/skills/code-review/agents/openai.yaml +0 -3
  52. package/skills/codebase-design/DEEPENING.md +0 -37
  53. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  54. package/skills/codebase-design/LICENSE +0 -21
  55. package/skills/codebase-design/SKILL.md +0 -114
  56. package/skills/codebase-design/agents/openai.yaml +0 -3
  57. package/skills/diagnosing-bugs/LICENSE +0 -21
  58. package/skills/diagnosing-bugs/SKILL.md +0 -138
  59. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  60. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  61. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  62. package/skills/domain-modeling/GLOSSARY-FORMAT.md +0 -60
  63. package/skills/domain-modeling/LICENSE +0 -21
  64. package/skills/domain-modeling/SKILL.md +0 -74
  65. package/skills/domain-modeling/agents/openai.yaml +0 -3
  66. package/skills/grill-me/LICENSE +0 -21
  67. package/skills/grill-me/SKILL.md +0 -7
  68. package/skills/grill-me/agents/openai.yaml +0 -5
  69. package/skills/grill-with-docs/LICENSE +0 -21
  70. package/skills/grill-with-docs/SKILL.md +0 -7
  71. package/skills/grill-with-docs/agents/openai.yaml +0 -5
  72. package/skills/grilling/LICENSE +0 -21
  73. package/skills/grilling/SKILL.md +0 -30
  74. package/skills/grilling/agents/openai.yaml +0 -3
  75. package/skills/handoff/LICENSE +0 -21
  76. package/skills/handoff/SKILL.md +0 -16
  77. package/skills/handoff/agents/openai.yaml +0 -5
  78. package/skills/implement/LICENSE +0 -21
  79. package/skills/implement/SKILL.md +0 -17
  80. package/skills/implement/agents/openai.yaml +0 -5
  81. package/skills/implement-spec/LICENSE +0 -21
  82. package/skills/implement-spec/SKILL.md +0 -40
  83. package/skills/implement-spec/agents/openai.yaml +0 -5
  84. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  85. package/skills/improve-codebase-architecture/LICENSE +0 -21
  86. package/skills/improve-codebase-architecture/SKILL.md +0 -71
  87. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
  88. package/skills/pr/CREDITS.md +0 -3
  89. package/skills/pr/LICENSE +0 -21
  90. package/skills/pr/SKILL.md +0 -170
  91. package/skills/pr/agents/openai.yaml +0 -3
  92. package/skills/prototype/LICENSE +0 -21
  93. package/skills/prototype/LOGIC.md +0 -67
  94. package/skills/prototype/SKILL.md +0 -26
  95. package/skills/prototype/UI.md +0 -112
  96. package/skills/prototype/agents/openai.yaml +0 -3
  97. package/skills/research/LICENSE +0 -21
  98. package/skills/research/SKILL.md +0 -12
  99. package/skills/research/agents/openai.yaml +0 -3
  100. package/skills/retro/LICENSE +0 -21
  101. package/skills/retro/SKILL.md +0 -44
  102. package/skills/retro/agents/openai.yaml +0 -5
  103. package/skills/tdd/LICENSE +0 -21
  104. package/skills/tdd/SKILL.md +0 -38
  105. package/skills/tdd/agents/openai.yaml +0 -3
  106. package/skills/tdd/mocking.md +0 -59
  107. package/skills/tdd/tests.md +0 -77
  108. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  109. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  110. package/skills/teach/LICENSE +0 -21
  111. package/skills/teach/MISSION-FORMAT.md +0 -31
  112. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  113. package/skills/teach/SKILL.md +0 -140
  114. package/skills/teach/agents/openai.yaml +0 -5
  115. package/skills/to-questionnaire/LICENSE +0 -21
  116. package/skills/to-questionnaire/SKILL.md +0 -54
  117. package/skills/to-questionnaire/agents/openai.yaml +0 -5
  118. package/skills/to-spec/LICENSE +0 -21
  119. package/skills/to-spec/SKILL.md +0 -75
  120. package/skills/to-spec/agents/openai.yaml +0 -5
  121. package/skills/to-tickets/LICENSE +0 -21
  122. package/skills/to-tickets/SKILL.md +0 -105
  123. package/skills/to-tickets/agents/openai.yaml +0 -5
  124. package/skills/triage/AGENT-BRIEF.md +0 -207
  125. package/skills/triage/LICENSE +0 -21
  126. package/skills/triage/OUT-OF-SCOPE.md +0 -105
  127. package/skills/triage/SKILL.md +0 -112
  128. package/skills/triage/agents/openai.yaml +0 -5
  129. package/skills/wait-what/LICENSE +0 -21
  130. package/skills/wait-what/SKILL.md +0 -7
  131. package/skills/wait-what/agents/openai.yaml +0 -5
  132. package/skills/wayfinder/LICENSE +0 -21
  133. package/skills/wayfinder/SKILL.md +0 -128
  134. package/skills/wayfinder/agents/openai.yaml +0 -5
  135. package/skills/wizard/LICENSE +0 -21
  136. package/skills/wizard/SKILL.md +0 -44
  137. package/skills/wizard/agents/openai.yaml +0 -3
  138. package/skills/wizard/template.sh +0 -223
  139. package/skills/writing-for-agents/LICENSE +0 -21
  140. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  141. package/skills/writing-for-agents/SKILL.md +0 -81
  142. 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/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
  }
@@ -72,6 +86,9 @@ function sidecar(path) {
72
86
  function recordOf(key) {
73
87
  return join(kitHome(), 'local-copies', `${key}.json`);
74
88
  }
89
+ function stagingPrefix() {
90
+ return `.${createHash('sha256').update(kitHome()).digest('hex').slice(0, 16)}-`;
91
+ }
75
92
  function rootOf(handle) {
76
93
  return `/rooms/${handle}`;
77
94
  }
@@ -99,32 +116,59 @@ async function removeFile(repository, path) {
99
116
  return;
100
117
  }
101
118
  }
102
- async function readOnlyPlaced(repository, held, written, removed) {
119
+ async function readOnlyFiles(repository, held) {
103
120
  const tracked = new Set((await git(repository, ['ls-files', '-z'])).split('\0'));
104
121
  const holding = new Set(held);
105
- for (const path of removed) {
106
- if (!holding.has(path))
107
- continue;
108
- if (!tracked.has(path))
109
- await removeFile(repository, path);
110
- holding.delete(path);
111
- }
112
- for (const [path, content] of written) {
113
- if (tracked.has(path))
114
- continue;
115
- if (!holding.has(path) && (await exists(join(repository, path))))
116
- continue;
117
- await writeText(join(repository, path), content);
118
- holding.add(path);
119
- }
120
- 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 };
121
165
  }
122
166
  async function ruled(path, rules) {
123
167
  if ((await readFile(path, 'utf8').catch(() => null)) !== rules)
124
168
  await writeFile(path, rules);
125
169
  }
126
170
  async function ignoreRules(repository, writable, copy) {
127
- 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));
128
172
  const protectedPath = (path) => copy.protected.some((prefix) => prefix !== '' && covers(path, prefix));
129
173
  const whole = copy.protected.filter((prefix) => prefix !== '' && !writable.some((path) => covers(path, prefix)));
130
174
  const house = [
@@ -133,7 +177,7 @@ async function ignoreRules(repository, writable, copy) {
133
177
  ].map(pattern);
134
178
  const granted = readOnly.filter((path) => !protectedPath(path)).map(pattern);
135
179
  await ruled(join(repository, '.gitignore'), [
136
- '# 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',
137
181
  '# content: Git does not track them, and House refuses a change to them even',
138
182
  '# when this file is edited or a path is force-added.',
139
183
  '/.gitignore',
@@ -148,7 +192,7 @@ async function ignoreRules(repository, writable, copy) {
148
192
  '',
149
193
  ].join('\n'));
150
194
  await ruled(join(repository, '.ignore'), [
151
- '# 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',
152
196
  '# content that .gitignore keeps out of Git.',
153
197
  `!${PROVENANCE}`,
154
198
  ...[...house, ...granted].map((rule) => `!${rule}`),
@@ -184,6 +228,7 @@ export class LocalCopies {
184
228
  house;
185
229
  copies = new Map();
186
230
  queues = new Map();
231
+ waiting = new Set();
187
232
  answering = Promise.resolve();
188
233
  constructor(house) {
189
234
  this.house = house;
@@ -215,10 +260,17 @@ export class LocalCopies {
215
260
  this.copies.set(copy.key, copy);
216
261
  }
217
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
+ }
218
270
  async mapping() {
219
271
  await this.answering;
220
272
  return [...this.copies.values()]
221
- .filter((copy) => copy.erasing === undefined)
273
+ .filter((copy) => copy.unfollowed === undefined)
222
274
  .map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
223
275
  .sort();
224
276
  }
@@ -244,62 +296,99 @@ export class LocalCopies {
244
296
  }
245
297
  async fetched(refreshing, rooms) {
246
298
  const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', rooms === undefined ? {} : { rooms });
247
- const synced = new Map(answer.rooms.map((room) => [`rooms/${room.room_ref}`, room.room_handle]));
248
- const named = rooms === undefined ? null : new Set(rooms.map((room) => `rooms/${room}`));
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}`));
249
301
  const held = () => [...this.copies.values()].filter((copy) => named === null || named.has(copy.key));
250
302
  return this.holding([...new Set([...held().map((copy) => copy.key), ...synced.keys()])].sort(), async () => {
251
303
  const said = [];
252
304
  let refused = false;
305
+ const mark = refreshing ? 'said' : 'unsaid';
253
306
  for (const copy of held()) {
254
- const returned = synced.has(copy.key);
255
- if (returned && copy.erasing === undefined)
307
+ if (synced.has(copy.key) || copy.unfollowed === 'said' || copy.unfollowed === mark)
256
308
  continue;
257
- try {
258
- await this.erase(copy);
259
- said.push(`${rootOf(copy.room_handle)}: ${returned ? '' : 'no longer synced to this Environment; '}erased from the Local copy`);
260
- }
261
- catch (error) {
262
- refused = true;
263
- said.push(`${rootOf(copy.room_handle)}: ${causeOf(error)}`);
264
- }
309
+ await this.save({ ...copy, unfollowed: mark });
310
+ if (refreshing)
311
+ said.push(`${rootOf(copy.room_handle)}: no longer followed; kept as it is`);
265
312
  }
266
313
  const current = [];
314
+ const wanted = new Map();
267
315
  for (const [key, handle] of synced) {
268
316
  try {
269
317
  const held = this.copies.get(key);
270
- if (held?.erasing)
271
- continue;
272
318
  if (held === undefined || !(await exists(held.repository))) {
273
- said.push(await this.materialize(key, handle));
319
+ wanted.set(key, handle);
274
320
  continue;
275
321
  }
276
- 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 }));
277
324
  }
278
325
  catch (error) {
279
326
  refused = true;
280
327
  said.push(`${rootOf(handle)}: ${causeOf(error)}`);
281
328
  }
282
329
  }
283
- if (refreshing && current.length > 0) {
284
- const failed = await this.refreshed(current).then(() => null, houseCause);
285
- for (const copy of current)
286
- said.push(...moved(copy, this.copies.get(copy.key)));
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
+ }
352
+ }
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
+ }
287
360
  if (failed !== null) {
288
361
  refused = true;
362
+ unsettled.push(...reading.map((copy) => copy.room_ref));
289
363
  said.push(failed);
290
364
  }
291
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);
292
375
  return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
293
376
  });
294
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
+ }
295
386
  async observed(report) {
296
387
  const key = await this.keyAt(dirname(report.common));
297
388
  if (key === undefined)
298
389
  return;
299
390
  await this.exclusive(key, async () => {
300
391
  const copy = this.copies.get(key);
301
- if (copy === undefined || copy.erasing)
302
- return;
303
392
  let moves = copy.moves;
304
393
  for (const { from, to } of report.moves) {
305
394
  const held = moves.find((move) => move.worktree === report.worktree && move.to === from);
@@ -355,138 +444,170 @@ export class LocalCopies {
355
444
  this.copies.set(copy.key, copy);
356
445
  return copy;
357
446
  }
358
- async erase(copy) {
359
- await this.save({ ...copy, erasing: true });
360
- await rm(copy.repository, { recursive: true, force: true });
361
- await rm(recordOf(copy.key), { force: true });
362
- this.copies.delete(copy.key);
363
- }
364
- async read(handle) {
365
- const prefixes = [rootOf(handle)];
366
- const discovery = await this.house.post('/kit/door/discover', { prefixes });
367
- let bundle = await this.house.post('/kit/door/bootstrap', { prefixes });
368
- if ('transfer' in bundle)
369
- bundle = (await (await fetch(relayed(bundle.transfer.url))).json());
370
- const [view] = bundle.scopes;
371
- if (view === undefined)
372
- return null;
373
- const authority = discovery.authorities.find((entry) => entry.room_ref === view.manifest.anchor);
374
- const contents = new Map(view.contents.map((entry) => [entry.path, entry.content]));
375
- const read = {
376
- anchor: view.manifest.anchor,
377
- position: view.manifest.position,
378
- log_epoch: view.manifest.log_epoch,
379
- write: authority.operations.write,
380
- protected: authority.protected_paths,
381
- writable: new Map(),
382
- revisions: {},
383
- readOnly: new Map(),
384
- };
385
- for (const file of view.manifest.files) {
386
- const content = contents.get(file.path);
387
- if (file.writable) {
388
- read.writable.set(file.path, content);
389
- read.revisions[file.path] = file.revision;
390
- }
391
- else {
392
- read.readOnly.set(file.path, content);
447
+ async discovered(handles) {
448
+ const discover = (group) => this.house.post('/kit/door/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) };
393
468
  }
394
- if (file.provenance !== undefined)
395
- read.readOnly.set(sidecar(file.path), file.provenance);
469
+ if (renewed && (await readdir(directory)).length === held.size)
470
+ throw failure;
396
471
  }
397
- return read;
398
472
  }
399
- async materialize(key, handle) {
400
- const read = await this.read(handle);
401
- if (read === null)
402
- throw new Error('House serves nothing of it to copy');
403
- const repository = join(localCopies(), 'rooms', read.anchor);
473
+ async materialized({ key, handle, authority }, whole) {
474
+ const repository = join(localCopies(), 'rooms', authority.room_ref);
404
475
  if (await exists(repository))
405
- throw new Error(`${repository} already exists and House Kit leaves it as it is`);
406
- 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 };
407
479
  await mkdir(staging, { recursive: true });
408
- let house;
409
- let readOnly;
480
+ let copy;
410
481
  try {
411
482
  await git(staging, ['init', '-q', '-b', BRANCH]);
412
- house = await importCommits(staging, [
413
- { message: `House ${rootOf(handle)}`, from: null, files: read.writable },
414
- ]);
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);
415
486
  await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
416
487
  await git(staging, ['reset', '-q', '--hard']);
417
- readOnly = await readOnlyPlaced(staging, [], read.readOnly, []);
418
- 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 });
419
503
  await rename(staging, repository);
420
504
  }
421
505
  catch (error) {
422
506
  await rm(staging, { recursive: true, force: true });
423
507
  throw error;
424
508
  }
425
- await this.save({
426
- key,
427
- room_ref: read.anchor,
428
- room_handle: handle,
429
- repository,
430
- house,
431
- position: read.position,
432
- log_epoch: read.log_epoch,
433
- write: read.write,
434
- protected: read.protected,
435
- read_only: readOnly,
436
- manifests: { [house]: { parent: null, files: read.revisions } },
437
- moves: [],
438
- });
439
- return `${rootOf(handle)}: copied to ${repository}`;
509
+ return this.save(copy);
440
510
  }
441
- async received(copy) {
442
- const read = await this.read(copy.room_handle);
443
- if (read === null)
444
- return copy;
445
- const house = await importCommits(copy.repository, [
446
- { message: `House ${rootOf(copy.room_handle)} at position ${read.position}`, from: copy.house, replaces: true, files: read.writable },
447
- ]);
448
- const removed = copy.read_only.filter((path) => !read.readOnly.has(path));
449
- const readOnly = await readOnlyPlaced(copy.repository, copy.read_only, read.readOnly, removed);
450
- const next = {
451
- ...copy,
452
- house,
453
- position: read.position,
454
- log_epoch: read.log_epoch,
455
- write: read.write,
456
- protected: read.protected,
457
- read_only: readOnly,
458
- manifests: { ...copy.manifests, [house]: { parent: null, files: read.revisions } },
459
- };
460
- await ignoreRules(copy.repository, [...read.writable.keys()], next);
461
- 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}`;
462
541
  }
463
- async refreshed(copies) {
464
- const discover = (group) => this.house.post('/kit/door/discover', { prefixes: group.map((copy) => rootOf(copy.room_handle)) });
465
- const [first, ...rest] = copies;
466
- const { bounds, authorities } = await discover([first]);
467
- for (const group of grouped(rest, bounds.max_prefixes))
468
- authorities.push(...(await discover(group)).authorities);
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 = [];
469
579
  const unchanged = [];
470
580
  for (const copy of copies) {
471
581
  const authority = authorities.find((entry) => entry.room_ref === copy.room_ref);
472
582
  if (authority !== undefined &&
473
583
  JSON.stringify([authority.operations.write, authority.protected_paths]) !== JSON.stringify([copy.write, copy.protected])) {
474
- 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
+ }
475
592
  }
476
593
  else {
477
594
  unchanged.push(copy);
478
595
  }
479
596
  }
480
597
  for (const group of grouped(unchanged, bounds.max_prefixes))
481
- await this.caughtUp(group, false);
598
+ await this.caughtUp(group, false, unready);
599
+ for (const group of grouped(read, bounds.max_prefixes))
600
+ await this.caughtUp(group, true, unready);
482
601
  }
483
- async caughtUp(copies, whole) {
602
+ async caughtUp(copies, whole, unready) {
484
603
  const caught = new Map(copies.map((copy) => [copy.key, copy]));
604
+ const read = new Set();
605
+ const waiting = unready ?? [];
485
606
  let asking = [...copies];
486
607
  while (asking.length > 0) {
487
608
  let answer;
488
609
  try {
489
- answer = await this.house.post('/kit/door/enumerate', {
610
+ answer = await this.house.post(ENUMERATE, {
490
611
  prefixes: asking.map((copy) => rootOf(copy.room_handle)),
491
612
  positions: asking.map((copy) => ({ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch })),
492
613
  });
@@ -495,8 +616,22 @@ export class LocalCopies {
495
616
  const expired = asking.find((copy) => copy.room_ref === expiredAuthority(error));
496
617
  if (expired === undefined)
497
618
  throw error;
498
- caught.set(expired.key, await this.save(await this.received(expired)));
499
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
+ }
500
635
  continue;
501
636
  }
502
637
  const advanced = [];
@@ -508,8 +643,10 @@ export class LocalCopies {
508
643
  caught.set(copy.key, next);
509
644
  advanced.push(next);
510
645
  }
511
- asking = whole ? advanced : [];
646
+ asking = advanced.filter((copy) => whole || read.has(copy.key));
512
647
  }
648
+ if (unready === undefined && waiting.length > 0)
649
+ throw new NotReady(waiting);
513
650
  return copies.map((copy) => caught.get(copy.key));
514
651
  }
515
652
  async incorporated(copy, changes, position, logEpoch) {
@@ -560,8 +697,6 @@ export class LocalCopies {
560
697
  }
561
698
  async pushed(key, worktree, selector, caller) {
562
699
  let copy = this.copies.get(key);
563
- if (copy === undefined || copy.erasing)
564
- return outside(worktree);
565
700
  if (copy.push?.state === 'submitted')
566
701
  return this.settle(copy, caller);
567
702
  const selected = await run(worktree, ['rev-parse', '--verify', '--quiet', '--end-of-options', `${selector}^{commit}`]);
@@ -676,8 +811,10 @@ export class LocalCopies {
676
811
  }
677
812
  if (outcome.refused.code === 'expired')
678
813
  return this.recovered(copy);
679
- const rebased = rebaseFixes(outcome.refused);
680
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);
681
818
  try {
682
819
  settled = (await this.caughtUp([settled], true))[0];
683
820
  }
package/dist/end.js CHANGED
@@ -1,8 +1,8 @@
1
- import { KIT_VERSION } from './clis.js';
1
+ import { KIT_RELEASE } from './clis.js';
2
2
  import { forgetRejected } from './home.js';
3
3
  import { installCommand } from './placement.js';
4
4
  import { killDescendants } from './scope.js';
5
- export const REPLACED = 'another Kit started for this Environment';
5
+ export const REPLACED = 'another Kit process started for this Environment';
6
6
  function exit(reason, status) {
7
7
  process.stderr.write(`kit: ${reason}\n`);
8
8
  killDescendants();
@@ -14,12 +14,18 @@ export function end(reason) {
14
14
  export function restart(reason) {
15
15
  exit(reason, 1);
16
16
  }
17
- export function updateRequired() {
17
+ export function updateLine(lead) {
18
18
  const command = installCommand();
19
- end(command === null ? 'This Kit is too old for House; install it again.' : `This Kit is too old for House. Update it: ${command}`);
19
+ return command === null ? `${lead}; install it again.` : `${lead}. Update it: ${command}`;
20
+ }
21
+ export function updateRequired() {
22
+ end(updateLine('This Kit is too old for House'));
23
+ }
24
+ export function unknownMethod(kind) {
25
+ return updateLine(`This Kit does not know ${kind}`);
20
26
  }
21
27
  export function newerThanHouse(release, command) {
22
- end(`Kit ${KIT_VERSION} is newer than this Building supports (Kit ${release}). Install Kit ${release}: ${command}`);
28
+ end(`Kit ${KIT_RELEASE} is newer than this Building supports (Kit ${release}). Install Kit ${release}: ${command}`);
23
29
  }
24
30
  export async function signInAgain(rejected) {
25
31
  await forgetRejected(rejected);