@agentshouse/kit 0.2.1-cd9cbd8b6428c → 0.2.1-cde47fd72fe59

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 (103) hide show
  1. package/README.md +32 -31
  2. package/dist/acp.js +46 -103
  3. package/dist/agents.js +178 -49
  4. package/dist/api.js +34 -2
  5. package/dist/app-git.js +48 -0
  6. package/dist/bootstrap.json +4 -4
  7. package/dist/bridge.js +38 -58
  8. package/dist/chatgpt.js +32 -0
  9. package/dist/clis.js +137 -65
  10. package/dist/codex-main.js +58 -0
  11. package/dist/conversations.js +388 -276
  12. package/dist/copies.js +213 -145
  13. package/dist/end.js +23 -0
  14. package/dist/enrol-main.js +7 -1
  15. package/dist/events.js +525 -0
  16. package/dist/files.js +22 -2
  17. package/dist/git.js +10 -8
  18. package/dist/home.js +96 -7
  19. package/dist/house-cli.js +291 -0
  20. package/dist/house-main.js +7 -159
  21. package/dist/inputs.js +37 -0
  22. package/dist/instructions.js +10 -32
  23. package/dist/kit-cli.js +68 -0
  24. package/dist/kit-main.js +8 -19
  25. package/dist/login.js +118 -103
  26. package/dist/logout.js +14 -13
  27. package/dist/parts.js +82 -0
  28. package/dist/placement.js +30 -0
  29. package/dist/refusals.js +12 -0
  30. package/dist/relay.js +12 -2
  31. package/dist/resident.js +91 -35
  32. package/dist/scope.js +203 -0
  33. package/dist/secret-input.js +4 -1
  34. package/dist/service-main.js +9 -0
  35. package/dist/service.js +55 -0
  36. package/dist/shell.js +3 -0
  37. package/dist/sign-in.js +34 -27
  38. package/dist/skills.js +81 -12
  39. package/dist/stream.js +87 -11
  40. package/dist/transcription.js +57 -9
  41. package/dist/translate.js +2 -8
  42. package/dist/update.js +50 -29
  43. package/dist/wire.js +6 -0
  44. package/dist/work.js +346 -0
  45. package/how-we-work.md +13 -3
  46. package/package.json +3 -2
  47. package/skills/ask-matt/SKILL.md +17 -11
  48. package/skills/ask-matt/agents/openai.yaml +1 -0
  49. package/skills/code-review/SKILL.md +5 -5
  50. package/skills/codebase-design/DESIGN-IT-TWICE.md +1 -1
  51. package/skills/codebase-design/SKILL.md +0 -2
  52. package/skills/diagnosing-bugs/SKILL.md +2 -4
  53. package/skills/domain-modeling/{CONTEXT-FORMAT.md → GLOSSARY-FORMAT.md} +10 -10
  54. package/skills/domain-modeling/SKILL.md +12 -14
  55. package/skills/grill-me/SKILL.md +1 -2
  56. package/skills/grill-me/agents/openai.yaml +1 -0
  57. package/skills/grill-with-docs/SKILL.md +1 -2
  58. package/skills/grill-with-docs/agents/openai.yaml +1 -0
  59. package/skills/grilling/SKILL.md +2 -2
  60. package/skills/handoff/SKILL.md +2 -3
  61. package/skills/handoff/agents/openai.yaml +1 -0
  62. package/skills/implement/SKILL.md +5 -4
  63. package/skills/implement/agents/openai.yaml +1 -0
  64. package/skills/implement-spec/SKILL.md +40 -0
  65. package/skills/implement-spec/agents/openai.yaml +5 -0
  66. package/skills/improve-codebase-architecture/SKILL.md +6 -7
  67. package/skills/improve-codebase-architecture/agents/openai.yaml +1 -0
  68. package/skills/pr/CREDITS.md +3 -0
  69. package/skills/pr/LICENSE +21 -0
  70. package/skills/pr/SKILL.md +170 -0
  71. package/skills/pr/agents/openai.yaml +3 -0
  72. package/skills/prototype/SKILL.md +0 -2
  73. package/skills/research/SKILL.md +0 -2
  74. package/skills/retro/LICENSE +21 -0
  75. package/skills/retro/SKILL.md +44 -0
  76. package/skills/retro/agents/openai.yaml +5 -0
  77. package/skills/tdd/SKILL.md +2 -4
  78. package/skills/teach/SKILL.md +3 -4
  79. package/skills/teach/agents/openai.yaml +1 -0
  80. package/skills/to-questionnaire/SKILL.md +1 -2
  81. package/skills/to-questionnaire/agents/openai.yaml +1 -0
  82. package/skills/to-spec/SKILL.md +1 -2
  83. package/skills/to-spec/agents/openai.yaml +1 -0
  84. package/skills/to-tickets/SKILL.md +3 -4
  85. package/skills/to-tickets/agents/openai.yaml +1 -0
  86. package/skills/triage/AGENT-BRIEF.md +207 -0
  87. package/skills/triage/LICENSE +21 -0
  88. package/skills/triage/OUT-OF-SCOPE.md +105 -0
  89. package/skills/triage/SKILL.md +112 -0
  90. package/skills/triage/agents/openai.yaml +5 -0
  91. package/skills/wait-what/SKILL.md +2 -3
  92. package/skills/wait-what/agents/openai.yaml +1 -0
  93. package/skills/wayfinder/SKILL.md +5 -6
  94. package/skills/wayfinder/agents/openai.yaml +1 -0
  95. package/skills/wizard/LICENSE +21 -0
  96. package/skills/wizard/SKILL.md +44 -0
  97. package/skills/wizard/agents/openai.yaml +3 -0
  98. package/skills/wizard/template.sh +223 -0
  99. package/skills/writing-for-agents/SKILL.md +0 -2
  100. package/dist/blocks.js +0 -51
  101. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  102. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  103. /package/skills/{resolving-merge-conflicts → implement-spec}/LICENSE +0 -0
package/dist/copies.js CHANGED
@@ -2,8 +2,8 @@ import { parse } from '@agentshouse/mdmodel';
2
2
  import { 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
- import { HouseRefusal } from './api.js';
6
- import { blobId, blobText, git, HOUSE_REF, importCommits, localCopies, revision, run, succeeded, tree, } from './git.js';
5
+ import { finalRefusal, HouseRefusal, retrying } from './api.js';
6
+ import { blobId, blobText, git, isText, HOUSE_REF, importCommits, localCopies, revision, run, succeeded, tree, } from './git.js';
7
7
  import { kitHome } from './home.js';
8
8
  import { operationId } from './operation.js';
9
9
  import { refusalOf } from './refusals.js';
@@ -11,11 +11,13 @@ import { changed } from './translate.js';
11
11
  import { relayed } from './relay.js';
12
12
  const BRANCH = 'main';
13
13
  const PROVENANCE = '_provenance';
14
- const REPLICA_RETRY_MS = 30_000;
15
14
  const IDENTITY = { GIT_COMMITTER_NAME: 'House Kit', GIT_COMMITTER_EMAIL: 'house-kit@localhost' };
16
15
  function causeOf(error) {
17
16
  return error instanceof Error ? error.message : String(error);
18
17
  }
18
+ function houseCause(error) {
19
+ return error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
20
+ }
19
21
  function logged(error) {
20
22
  process.stderr.write(`kit: ${causeOf(error)}\n`);
21
23
  }
@@ -30,9 +32,35 @@ const REBASED = new Set([
30
32
  function rebaseFixes(refusal) {
31
33
  return refusal.code === 'edit_conflict' && refusal.conflicts.some((code) => REBASED.has(code));
32
34
  }
35
+ function textOnly(paths) {
36
+ const [named, them] = paths.length === 1 ? [`${paths[0]} is`, 'it'] : [`${paths.join(', ')} are`, 'them'];
37
+ return `${named} not text, and house push sends text only; remove ${them} from the commit and place ${them} with house upload`;
38
+ }
39
+ function outside(path) {
40
+ return { refused: true, text: `${path} is not a House Local copy; run house push inside one` };
41
+ }
33
42
  function short(commit) {
34
43
  return commit.slice(0, 12);
35
44
  }
45
+ function moved(before, after) {
46
+ if (before.house !== after.house)
47
+ return [`${rootOf(after.room_handle)}: ${HOUSE_REF} is at ${short(after.house)}`];
48
+ return before.position === after.position ? [] : [`${rootOf(after.room_handle)}: read-only files refreshed`];
49
+ }
50
+ function expiredAuthority(error) {
51
+ if (!(error instanceof HouseRefusal))
52
+ return undefined;
53
+ try {
54
+ const { error: refused } = JSON.parse(error.text);
55
+ return refused?.code === 'expired' && typeof refused.authority === 'string' ? refused.authority : undefined;
56
+ }
57
+ catch {
58
+ return undefined;
59
+ }
60
+ }
61
+ function grouped(items, size) {
62
+ return items.flatMap((_, index) => (index % size === 0 ? [items.slice(index, index + size)] : []));
63
+ }
36
64
  function covers(path, prefix) {
37
65
  return prefix === '' || path === prefix || path.startsWith(`${prefix}/`);
38
66
  }
@@ -41,11 +69,14 @@ function sidecar(path) {
41
69
  segments.splice(-1, 0, PROVENANCE);
42
70
  return segments.join('/');
43
71
  }
44
- function rootOf(copy) {
45
- return copy.private ? '/private' : `/rooms/${copy.room_handle}`;
72
+ function recordOf(key) {
73
+ return join(kitHome(), 'local-copies', `${key}.json`);
74
+ }
75
+ function rootOf(handle) {
76
+ return `/rooms/${handle}`;
46
77
  }
47
78
  function relative(path) {
48
- return path.replace(/^\/private\//, '').replace(/^\/rooms\/[^/]+\//, '');
79
+ return path.replace(/^\/rooms\/[^/]+\//, '');
49
80
  }
50
81
  function declaredWritable(path, copy) {
51
82
  const deepest = (scopes) => Math.max(-1, ...scopes.filter((scope) => covers(path, scope)).map((scope) => scope.length));
@@ -153,19 +184,18 @@ export class LocalCopies {
153
184
  house;
154
185
  copies = new Map();
155
186
  queues = new Map();
156
- delivering = new Set();
157
- selecting = Promise.resolve();
187
+ answering = Promise.resolve();
158
188
  constructor(house) {
159
189
  this.house = house;
160
190
  this.owner = {
161
191
  key: 'owner',
162
192
  submit: async (operation, changes) => {
163
193
  try {
164
- const outcome = await house.post('/kit/door/batch', { operation_id: operation, changes });
194
+ const outcome = await retrying(() => house.post('/kit/door/batch', { operation_id: operation, changes }));
165
195
  return { accepted: outcome.contents };
166
196
  }
167
197
  catch (error) {
168
- if (!(error instanceof HouseRefusal) || error.status >= 500)
198
+ if (!finalRefusal(error))
169
199
  throw error;
170
200
  return {
171
201
  refused: refusalOf(error.text) ?? { code: '', text: `House refused it with ${error.status}`, conflicts: [] },
@@ -177,7 +207,7 @@ export class LocalCopies {
177
207
  async load() {
178
208
  const home = join(kitHome(), 'local-copies');
179
209
  const names = (await readdir(join(home, 'rooms')).catch(() => [])).filter((name) => name.endsWith('.json'));
180
- for (const path of [...names.map((name) => join(home, 'rooms', name)), join(home, 'private.json')]) {
210
+ for (const path of names.map((name) => join(home, 'rooms', name))) {
181
211
  const text = await readFile(path, 'utf8').catch(() => null);
182
212
  if (text === null)
183
213
  continue;
@@ -186,47 +216,81 @@ export class LocalCopies {
186
216
  }
187
217
  }
188
218
  async mapping() {
189
- await this.selecting;
219
+ await this.answering;
190
220
  return [...this.copies.values()]
191
- .filter((copy) => copy.active)
192
- .map((copy) => `${rootOf(copy)}: ${copy.repository}`)
221
+ .filter((copy) => copy.erasing === undefined)
222
+ .map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
193
223
  .sort();
194
224
  }
195
- select() {
196
- const selected = this.selecting.then(() => this.reselect());
197
- this.selecting = selected.catch(() => undefined);
198
- return selected;
199
- }
200
- async reselect() {
201
- const { local_copy: chosen } = await this.house.post('/kit/local-copy/selection', {});
202
- const selected = [
203
- ...(chosen?.rooms ?? [])
204
- .filter((room) => room.room_handle !== null)
205
- .map((room) => ({ key: `rooms/${room.room_ref}`, private: false, handle: room.room_handle })),
206
- ...(chosen?.private === true ? [{ key: 'private', private: true, handle: null }] : []),
207
- ];
208
- const keys = new Set(selected.map((scope) => scope.key));
209
- const work = [];
210
- for (const key of this.copies.keys()) {
211
- if (keys.has(key))
212
- continue;
213
- work.push(this.exclusive(key, async () => {
214
- const copy = this.copies.get(key);
215
- if (copy.active)
216
- await this.save({ ...copy, active: false });
217
- }));
218
- }
219
- for (const scope of selected)
220
- work.push(this.exclusive(scope.key, () => this.apply(scope)).catch(logged));
221
- await Promise.all(work);
225
+ connected() {
226
+ return this.selected();
222
227
  }
223
- entries(frame) {
224
- const copy = [...this.copies.values()].find((held) => held.active && held.room_ref === frame.authority);
225
- if (copy === undefined)
226
- return;
227
- if (copy.log_epoch === frame.log_epoch && BigInt(frame.position) <= BigInt(copy.position))
228
- return;
229
- this.deliver(copy.key);
228
+ async changed(rooms) {
229
+ if (rooms.length > 0)
230
+ await this.selected(rooms);
231
+ }
232
+ fetch() {
233
+ return this.answered(() => this.fetched(true));
234
+ }
235
+ async selected(rooms) {
236
+ const copied = await this.answered(() => this.fetched(false, rooms));
237
+ if (copied.refused)
238
+ logged(copied.text);
239
+ }
240
+ answered(work) {
241
+ const answered = this.answering.then(work);
242
+ this.answering = answered.catch(() => undefined);
243
+ return answered;
244
+ }
245
+ async fetched(refreshing, rooms) {
246
+ 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}`));
249
+ const held = () => [...this.copies.values()].filter((copy) => named === null || named.has(copy.key));
250
+ return this.holding([...new Set([...held().map((copy) => copy.key), ...synced.keys()])].sort(), async () => {
251
+ const said = [];
252
+ let refused = false;
253
+ for (const copy of held()) {
254
+ const returned = synced.has(copy.key);
255
+ if (returned && copy.erasing === undefined)
256
+ 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
+ }
265
+ }
266
+ const current = [];
267
+ for (const [key, handle] of synced) {
268
+ try {
269
+ const held = this.copies.get(key);
270
+ if (held?.erasing)
271
+ continue;
272
+ if (held === undefined || !(await exists(held.repository))) {
273
+ said.push(await this.materialize(key, handle));
274
+ continue;
275
+ }
276
+ current.push(held.room_handle === handle ? held : await this.save({ ...held, room_handle: handle }));
277
+ }
278
+ catch (error) {
279
+ refused = true;
280
+ said.push(`${rootOf(handle)}: ${causeOf(error)}`);
281
+ }
282
+ }
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)));
287
+ if (failed !== null) {
288
+ refused = true;
289
+ said.push(failed);
290
+ }
291
+ }
292
+ return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
293
+ });
230
294
  }
231
295
  async observed(report) {
232
296
  const key = await this.keyAt(dirname(report.common));
@@ -234,6 +298,8 @@ export class LocalCopies {
234
298
  return;
235
299
  await this.exclusive(key, async () => {
236
300
  const copy = this.copies.get(key);
301
+ if (copy === undefined || copy.erasing)
302
+ return;
237
303
  let moves = copy.moves;
238
304
  for (const { from, to } of report.moves) {
239
305
  const held = moves.find((move) => move.worktree === report.worktree && move.to === from);
@@ -256,11 +322,11 @@ export class LocalCopies {
256
322
  async push(cwd, selector, caller) {
257
323
  const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
258
324
  if (located.status !== 0)
259
- return { refused: true, text: `${cwd} is not a House Local copy; run house push inside one` };
325
+ return outside(cwd);
260
326
  const [common, worktree] = located.stdout.toString('utf8').split('\n');
261
327
  const key = await this.keyAt(dirname(common));
262
328
  if (key === undefined)
263
- return { refused: true, text: `${worktree} is not a House Local copy; run house push inside one` };
329
+ return outside(worktree);
264
330
  return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
265
331
  }
266
332
  async keyAt(repository) {
@@ -276,19 +342,12 @@ export class LocalCopies {
276
342
  this.queues.set(key, queued.catch(() => undefined));
277
343
  return queued;
278
344
  }
279
- deliver(key) {
280
- if (this.delivering.has(key))
281
- return;
282
- this.delivering.add(key);
283
- void this.exclusive(key, async () => {
284
- this.delivering.delete(key);
285
- const copy = this.copies.get(key);
286
- if (copy.active)
287
- await this.save(await this.caughtUp(copy));
288
- }).catch(logged);
345
+ holding(keys, work) {
346
+ const [key, ...rest] = keys;
347
+ return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
289
348
  }
290
349
  async save(copy) {
291
- const path = join(kitHome(), 'local-copies', copy.private ? 'private.json' : `${copy.key}.json`);
350
+ const path = recordOf(copy.key);
292
351
  await mkdir(dirname(path), { recursive: true, mode: 0o700 });
293
352
  const temporary = `${path}.${process.pid}`;
294
353
  await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
@@ -296,8 +355,14 @@ export class LocalCopies {
296
355
  this.copies.set(copy.key, copy);
297
356
  return copy;
298
357
  }
299
- async read(selected) {
300
- const prefixes = [rootOf(selected)];
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)];
301
366
  const discovery = await this.house.post('/kit/door/discover', { prefixes });
302
367
  let bundle = await this.house.post('/kit/door/bootstrap', { prefixes });
303
368
  if ('transfer' in bundle)
@@ -331,46 +396,13 @@ export class LocalCopies {
331
396
  }
332
397
  return read;
333
398
  }
334
- async apply(scope) {
335
- let copy = this.copies.get(scope.key);
336
- if (copy === undefined || !(await exists(copy.repository))) {
337
- await this.materialize(scope);
338
- return;
339
- }
340
- if (copy.room_handle !== scope.handle)
341
- copy = await this.save({ ...copy, room_handle: scope.handle });
342
- if (!copy.active) {
343
- await this.save(await this.received({ ...copy, active: true }));
344
- return;
345
- }
346
- const discovery = await this.house.post('/kit/door/discover', {
347
- prefixes: [rootOf(copy)],
348
- });
349
- const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
350
- if (authority === undefined)
351
- return;
352
- if (authority.managed_replication !== 'admitted') {
353
- logged(`operation_denied: ${rootOf(copy)} is not synchronized to this Environment`);
354
- return;
355
- }
356
- if (JSON.stringify([authority.operations.write, authority.protected_paths]) !==
357
- JSON.stringify([copy.write, copy.protected])) {
358
- await this.save(await this.received(copy));
359
- return;
360
- }
361
- await this.save(await this.caughtUp(copy));
362
- }
363
- async materialize(scope) {
364
- const read = await this.read({ private: scope.private, room_handle: scope.handle });
365
- if (read === null) {
366
- logged(`House serves nothing of ${rootOf({ private: scope.private, room_handle: scope.handle })} to copy`);
367
- return;
368
- }
369
- const repository = scope.private ? join(localCopies(), 'private') : join(localCopies(), 'rooms', read.anchor);
370
- if (await exists(repository)) {
371
- logged(`${repository} already exists and House Kit leaves it as it is`);
372
- return;
373
- }
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);
404
+ if (await exists(repository))
405
+ throw new Error(`${repository} already exists and House Kit leaves it as it is`);
374
406
  const staging = join(dirname(repository), `.${randomUUID()}`);
375
407
  await mkdir(staging, { recursive: true });
376
408
  let house;
@@ -378,7 +410,7 @@ export class LocalCopies {
378
410
  try {
379
411
  await git(staging, ['init', '-q', '-b', BRANCH]);
380
412
  house = await importCommits(staging, [
381
- { message: `House ${rootOf({ private: scope.private, room_handle: scope.handle })}`, from: null, files: read.writable },
413
+ { message: `House ${rootOf(handle)}`, from: null, files: read.writable },
382
414
  ]);
383
415
  await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
384
416
  await git(staging, ['reset', '-q', '--hard']);
@@ -391,12 +423,10 @@ export class LocalCopies {
391
423
  throw error;
392
424
  }
393
425
  await this.save({
394
- key: scope.key,
395
- private: scope.private,
426
+ key,
396
427
  room_ref: read.anchor,
397
- room_handle: scope.handle,
428
+ room_handle: handle,
398
429
  repository,
399
- active: true,
400
430
  house,
401
431
  position: read.position,
402
432
  log_epoch: read.log_epoch,
@@ -406,13 +436,14 @@ export class LocalCopies {
406
436
  manifests: { [house]: { parent: null, files: read.revisions } },
407
437
  moves: [],
408
438
  });
439
+ return `${rootOf(handle)}: copied to ${repository}`;
409
440
  }
410
441
  async received(copy) {
411
- const read = await this.read(copy);
442
+ const read = await this.read(copy.room_handle);
412
443
  if (read === null)
413
444
  return copy;
414
445
  const house = await importCommits(copy.repository, [
415
- { message: `House ${rootOf(copy)} at position ${read.position}`, from: copy.house, replaces: true, files: read.writable },
446
+ { message: `House ${rootOf(copy.room_handle)} at position ${read.position}`, from: copy.house, replaces: true, files: read.writable },
416
447
  ]);
417
448
  const removed = copy.read_only.filter((path) => !read.readOnly.has(path));
418
449
  const readOnly = await readOnlyPlaced(copy.repository, copy.read_only, read.readOnly, removed);
@@ -429,30 +460,57 @@ export class LocalCopies {
429
460
  await ignoreRules(copy.repository, [...read.writable.keys()], next);
430
461
  return next;
431
462
  }
432
- async caughtUp(start) {
433
- let copy = start;
434
- for (;;) {
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);
469
+ const unchanged = [];
470
+ for (const copy of copies) {
471
+ const authority = authorities.find((entry) => entry.room_ref === copy.room_ref);
472
+ if (authority !== undefined &&
473
+ JSON.stringify([authority.operations.write, authority.protected_paths]) !== JSON.stringify([copy.write, copy.protected])) {
474
+ await this.save(await this.received(copy));
475
+ }
476
+ else {
477
+ unchanged.push(copy);
478
+ }
479
+ }
480
+ for (const group of grouped(unchanged, bounds.max_prefixes))
481
+ await this.caughtUp(group, false);
482
+ }
483
+ async caughtUp(copies, whole) {
484
+ const caught = new Map(copies.map((copy) => [copy.key, copy]));
485
+ let asking = [...copies];
486
+ while (asking.length > 0) {
435
487
  let answer;
436
488
  try {
437
489
  answer = await this.house.post('/kit/door/enumerate', {
438
- prefixes: [rootOf(copy)],
439
- positions: [{ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch }],
490
+ prefixes: asking.map((copy) => rootOf(copy.room_handle)),
491
+ positions: asking.map((copy) => ({ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch })),
440
492
  });
441
493
  }
442
494
  catch (error) {
443
- const code = error instanceof HouseRefusal ? (refusalOf(error.text)?.code ?? null) : null;
444
- if (code === 'position_expired')
445
- return this.received(copy);
446
- if (code !== 'replica_behind')
495
+ const expired = asking.find((copy) => copy.room_ref === expiredAuthority(error));
496
+ if (expired === undefined)
447
497
  throw error;
448
- setTimeout(() => this.deliver(copy.key), REPLICA_RETRY_MS).unref();
449
- return copy;
498
+ caught.set(expired.key, await this.save(await this.received(expired)));
499
+ asking = asking.filter((copy) => copy !== expired);
500
+ continue;
501
+ }
502
+ const advanced = [];
503
+ for (const copy of asking) {
504
+ const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
505
+ if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
506
+ continue;
507
+ const next = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
508
+ caught.set(copy.key, next);
509
+ advanced.push(next);
450
510
  }
451
- const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
452
- if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
453
- return copy;
454
- copy = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
511
+ asking = whole ? advanced : [];
455
512
  }
513
+ return copies.map((copy) => caught.get(copy.key));
456
514
  }
457
515
  async incorporated(copy, changes, position, logEpoch) {
458
516
  const houseTree = await tree(copy.repository, copy.house);
@@ -485,7 +543,7 @@ export class LocalCopies {
485
543
  let { house, manifests } = copy;
486
544
  if (files.size > 0) {
487
545
  house = await importCommits(copy.repository, [
488
- { message: `House ${rootOf(copy)} at position ${position}`, from: copy.house, files },
546
+ { message: `House ${rootOf(copy.room_handle)} at position ${position}`, from: copy.house, files },
489
547
  ]);
490
548
  manifests = { ...manifests, [house]: { parent: copy.house, files: revisions } };
491
549
  }
@@ -502,6 +560,8 @@ export class LocalCopies {
502
560
  }
503
561
  async pushed(key, worktree, selector, caller) {
504
562
  let copy = this.copies.get(key);
563
+ if (copy === undefined || copy.erasing)
564
+ return outside(worktree);
505
565
  if (copy.push?.state === 'submitted')
506
566
  return this.settle(copy, caller);
507
567
  const selected = await run(worktree, ['rev-parse', '--verify', '--quiet', '--end-of-options', `${selector}^{commit}`]);
@@ -515,9 +575,6 @@ export class LocalCopies {
515
575
  }
516
576
  copy = await this.save(without(copy));
517
577
  }
518
- if (!copy.active) {
519
- return { refused: true, text: `${rootOf(copy)} is no longer synced to this Environment, so House takes no push from it` };
520
- }
521
578
  const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
522
579
  const base = listed.find((ancestor) => ancestor in copy.manifests);
523
580
  if (base === undefined) {
@@ -528,7 +585,7 @@ export class LocalCopies {
528
585
  changes = await this.derived(copy, base, commit);
529
586
  }
530
587
  catch (error) {
531
- return { refused: true, text: causeOf(error) };
588
+ return { refused: true, text: houseCause(error) };
532
589
  }
533
590
  if (changes.length === 0)
534
591
  return { refused: false, text: `Nothing to submit: ${short(commit)} matches House.` };
@@ -550,12 +607,23 @@ export class LocalCopies {
550
607
  return this.settle(submitted, caller);
551
608
  }
552
609
  async derived(copy, base, commit) {
553
- const root = rootOf(copy);
610
+ const root = rootOf(copy.room_handle);
554
611
  const before = await tree(copy.repository, base);
555
612
  const after = await tree(copy.repository, commit);
556
613
  const moved = movedPaths(copy.moves, before, after);
557
614
  const arrived = new Set(moved.values());
558
- const text = (entry, path) => blobText(copy.repository, entry, path);
615
+ const arriving = [...after].filter(([path, entry]) => before.get(path) !== entry);
616
+ if (arriving.length > 0) {
617
+ const { bounds } = await this.house.post('/kit/door/discover', { prefixes: [root] });
618
+ const binary = [];
619
+ for (const [path, entry] of arriving) {
620
+ if (!(await isText(copy.repository, entry, bounds.max_authored_file_bytes)))
621
+ binary.push(path);
622
+ }
623
+ if (binary.length > 0)
624
+ throw new Error(textOnly(binary));
625
+ }
626
+ const text = (entry) => blobText(copy.repository, entry);
559
627
  const changes = [];
560
628
  for (const path of [...new Set([...before.keys(), ...after.keys()])].sort()) {
561
629
  if (arrived.has(path))
@@ -568,7 +636,7 @@ export class LocalCopies {
568
636
  changes.push({ op: 'rename', path: `${root}/${path}`, to: `${root}/${to}`, base: known });
569
637
  const moving = after.get(to);
570
638
  if (moving !== prior) {
571
- changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving, to) });
639
+ changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving) });
572
640
  }
573
641
  continue;
574
642
  }
@@ -577,9 +645,9 @@ export class LocalCopies {
577
645
  if (next === undefined)
578
646
  changes.push({ op: 'remove', path: `${root}/${path}`, base: known });
579
647
  else if (prior === undefined)
580
- changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next, path) });
648
+ changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next) });
581
649
  else
582
- changes.push(...changed(`${root}/${path}`, path, known, await text(prior, path), await text(next, path)));
650
+ changes.push(...changed(`${root}/${path}`, path, known, await text(prior), await text(next)));
583
651
  }
584
652
  return changes;
585
653
  }
@@ -606,12 +674,12 @@ export class LocalCopies {
606
674
  const { upload, ...unstaged } = copy.push;
607
675
  return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
608
676
  }
609
- if (outcome.refused.code === 'operation_expired')
677
+ if (outcome.refused.code === 'expired')
610
678
  return this.recovered(copy);
611
679
  const rebased = rebaseFixes(outcome.refused);
612
680
  let settled = await this.save(without(copy));
613
681
  try {
614
- settled = await this.save(await this.caughtUp(settled));
682
+ settled = (await this.caughtUp([settled], true))[0];
615
683
  }
616
684
  catch {
617
685
  return {
@@ -630,9 +698,9 @@ export class LocalCopies {
630
698
  }
631
699
  async recovered(copy) {
632
700
  const push = copy.push;
633
- const current = await this.save(await this.caughtUp(copy));
701
+ const current = (await this.caughtUp([copy], true))[0];
634
702
  const house = await tree(copy.repository, current.house);
635
- const root = rootOf(current);
703
+ const root = rootOf(current.room_handle);
636
704
  if (!(await this.landed(copy.repository, push.changes, house, await tree(copy.repository, push.commit)))) {
637
705
  await this.save(without(current));
638
706
  return {
@@ -646,7 +714,7 @@ export class LocalCopies {
646
714
  written.push({
647
715
  path: `${root}/${path}`,
648
716
  revision: entry === undefined ? null : (revisionAt(current, current.house, path) ?? null),
649
- content: entry === undefined ? null : await blobText(copy.repository, entry, path),
717
+ content: entry === undefined ? null : await blobText(copy.repository, entry),
650
718
  });
651
719
  }
652
720
  return this.finished(await this.save({ ...current, push: { ...push, state: 'accepted', written } }));
@@ -674,7 +742,7 @@ export class LocalCopies {
674
742
  const push = copy.push;
675
743
  let written = push.written;
676
744
  if (push.changes.some((change) => change.op === 'rename')) {
677
- copy = await this.save(await this.caughtUp(copy));
745
+ copy = (await this.caughtUp([copy], true))[0];
678
746
  const house = await tree(copy.repository, copy.house);
679
747
  const sent = await tree(copy.repository, push.house);
680
748
  if (push.changes.some((change) => change.op === 'rename' && !house.has(relative(change.to)))) {
@@ -688,9 +756,9 @@ export class LocalCopies {
688
756
  written = [
689
757
  ...written,
690
758
  {
691
- path: `${rootOf(copy)}/${path}`,
759
+ path: `${rootOf(copy.room_handle)}/${path}`,
692
760
  revision: entry === undefined ? null : (revisionAt(copy, copy.house, path) ?? null),
693
- content: entry === undefined ? null : await blobText(copy.repository, entry, path),
761
+ content: entry === undefined ? null : await blobText(copy.repository, entry),
694
762
  },
695
763
  ];
696
764
  }
@@ -708,7 +776,7 @@ export class LocalCopies {
708
776
  }
709
777
  const edits = new Map([...files].filter(([path, content]) => (content === null ? selected.has(path) : selected.get(path) !== blobId(content))));
710
778
  const received = [...files.keys()].filter((path) => revisionAt(copy, copy.house, path) === revisionAt(copy, push.house, path));
711
- const accepted = `House ${rootOf(copy)} with ${push.commit} accepted`;
779
+ const accepted = `House ${rootOf(copy.room_handle)} with ${push.commit} accepted`;
712
780
  const house = await importCommits(copy.repository, [
713
781
  ...(edits.size === 0 ? [] : [{ message: accepted, from: push.commit, files: edits }]),
714
782
  {
@@ -735,7 +803,7 @@ export class LocalCopies {
735
803
  async landed(repository, changes, house, selected) {
736
804
  const text = async (path) => {
737
805
  const entry = house.get(relative(path));
738
- return entry === undefined ? undefined : blobText(repository, entry, path);
806
+ return entry === undefined ? undefined : blobText(repository, entry);
739
807
  };
740
808
  for (const change of changes) {
741
809
  const current = await text(change.path);
package/dist/end.js ADDED
@@ -0,0 +1,23 @@
1
+ import { forgetRejected } from './home.js';
2
+ import { installCommand } from './placement.js';
3
+ import { killDescendants } from './scope.js';
4
+ export const REPLACED = 'another Kit started for this Environment';
5
+ function exit(reason, status) {
6
+ process.stderr.write(`kit: ${reason}\n`);
7
+ killDescendants();
8
+ process.exit(status);
9
+ }
10
+ export function end(reason) {
11
+ exit(reason, 0);
12
+ }
13
+ export function restart(reason) {
14
+ exit(reason, 1);
15
+ }
16
+ export function updateRequired() {
17
+ const command = installCommand();
18
+ end(command === null ? 'This Kit is too old for House; install it again.' : `This Kit is too old for House. Update it: ${command}`);
19
+ }
20
+ export async function signInAgain(rejected) {
21
+ await forgetRejected(rejected);
22
+ restart('sign in again');
23
+ }
@@ -1,4 +1,10 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { writeEnrolment } from './home.js';
3
3
  const [house, environment] = process.argv.slice(2);
4
- await writeEnrolment({ house: house, environment: environment, credential: readFileSync(0, 'utf8').trim() });
4
+ const relay = process.env.HOUSE_KIT_RELAY || undefined;
5
+ await writeEnrolment({
6
+ house: house,
7
+ ...(relay === undefined ? {} : { relay }),
8
+ environment: environment,
9
+ credential: readFileSync(0, 'utf8').trim(),
10
+ });