@agentshouse/kit 0.2.1-cb6679d048e60 → 0.2.1-cbdf76b9a90a3

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