@agentshouse/kit 0.2.1-cca9e1f43de44 → 0.2.1-ccb257fa8a318

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 (132) hide show
  1. package/README.md +42 -40
  2. package/dist/acp.js +92 -115
  3. package/dist/agents.js +112 -24
  4. package/dist/api.js +10 -6
  5. package/dist/app-git.js +10 -26
  6. package/dist/authority-main.js +2 -2
  7. package/dist/bridge.js +11 -25
  8. package/dist/building.js +46 -0
  9. package/dist/calls.js +71 -0
  10. package/dist/chatgpt.js +32 -0
  11. package/dist/clis-main.js +2 -2
  12. package/dist/clis.js +131 -27
  13. package/dist/codex-main.js +58 -0
  14. package/dist/connection.js +116 -0
  15. package/dist/conversations.js +523 -202
  16. package/dist/copies.js +185 -123
  17. package/dist/end.js +24 -5
  18. package/dist/enrol-main.js +7 -1
  19. package/dist/events.js +16 -78
  20. package/dist/files.js +139 -31
  21. package/dist/git.js +13 -11
  22. package/dist/home.js +96 -7
  23. package/dist/house-cli.js +299 -0
  24. package/dist/house-main.js +7 -177
  25. package/dist/instructions.js +8 -10
  26. package/dist/kit-cli.js +68 -0
  27. package/dist/kit-main.js +8 -22
  28. package/dist/login.js +118 -107
  29. package/dist/logout.js +14 -16
  30. package/dist/mcp.js +6 -2
  31. package/dist/parts.js +34 -16
  32. package/dist/placement.js +39 -0
  33. package/dist/relay.js +33 -2
  34. package/dist/resident.js +118 -51
  35. package/dist/scope.js +206 -0
  36. package/dist/scripts.js +255 -0
  37. package/dist/service-main.js +9 -0
  38. package/dist/service.js +55 -0
  39. package/dist/sign-in.js +23 -40
  40. package/dist/silence.js +28 -0
  41. package/dist/skills.js +49 -35
  42. package/dist/transcription.js +85 -45
  43. package/dist/translate.js +2 -8
  44. package/dist/update.js +115 -53
  45. package/dist/wire.js +22 -0
  46. package/dist/work.js +346 -0
  47. package/package.json +20 -20
  48. package/dist/bootstrap.json +0 -24
  49. package/dist/stream.js +0 -72
  50. package/how-we-work.md +0 -23
  51. package/skills/ask-matt/LICENSE +0 -21
  52. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  53. package/skills/ask-matt/SKILL.md +0 -91
  54. package/skills/ask-matt/agents/openai.yaml +0 -4
  55. package/skills/code-review/LICENSE +0 -21
  56. package/skills/code-review/SKILL.md +0 -89
  57. package/skills/code-review/agents/openai.yaml +0 -3
  58. package/skills/codebase-design/DEEPENING.md +0 -37
  59. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  60. package/skills/codebase-design/LICENSE +0 -21
  61. package/skills/codebase-design/SKILL.md +0 -116
  62. package/skills/codebase-design/agents/openai.yaml +0 -3
  63. package/skills/diagnosing-bugs/LICENSE +0 -21
  64. package/skills/diagnosing-bugs/SKILL.md +0 -140
  65. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  66. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  67. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  68. package/skills/domain-modeling/CONTEXT-FORMAT.md +0 -60
  69. package/skills/domain-modeling/LICENSE +0 -21
  70. package/skills/domain-modeling/SKILL.md +0 -76
  71. package/skills/domain-modeling/agents/openai.yaml +0 -3
  72. package/skills/grill-me/LICENSE +0 -21
  73. package/skills/grill-me/SKILL.md +0 -8
  74. package/skills/grill-me/agents/openai.yaml +0 -4
  75. package/skills/grill-with-docs/LICENSE +0 -21
  76. package/skills/grill-with-docs/SKILL.md +0 -8
  77. package/skills/grill-with-docs/agents/openai.yaml +0 -4
  78. package/skills/grilling/LICENSE +0 -21
  79. package/skills/grilling/SKILL.md +0 -30
  80. package/skills/grilling/agents/openai.yaml +0 -3
  81. package/skills/handoff/LICENSE +0 -21
  82. package/skills/handoff/SKILL.md +0 -17
  83. package/skills/handoff/agents/openai.yaml +0 -4
  84. package/skills/implement/LICENSE +0 -21
  85. package/skills/implement/SKILL.md +0 -16
  86. package/skills/implement/agents/openai.yaml +0 -4
  87. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  88. package/skills/improve-codebase-architecture/LICENSE +0 -21
  89. package/skills/improve-codebase-architecture/SKILL.md +0 -72
  90. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -4
  91. package/skills/prototype/LICENSE +0 -21
  92. package/skills/prototype/LOGIC.md +0 -67
  93. package/skills/prototype/SKILL.md +0 -28
  94. package/skills/prototype/UI.md +0 -112
  95. package/skills/prototype/agents/openai.yaml +0 -3
  96. package/skills/research/LICENSE +0 -21
  97. package/skills/research/SKILL.md +0 -14
  98. package/skills/research/agents/openai.yaml +0 -3
  99. package/skills/resolving-merge-conflicts/LICENSE +0 -21
  100. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  101. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  102. package/skills/tdd/LICENSE +0 -21
  103. package/skills/tdd/SKILL.md +0 -40
  104. package/skills/tdd/agents/openai.yaml +0 -3
  105. package/skills/tdd/mocking.md +0 -59
  106. package/skills/tdd/tests.md +0 -77
  107. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  108. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  109. package/skills/teach/LICENSE +0 -21
  110. package/skills/teach/MISSION-FORMAT.md +0 -31
  111. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  112. package/skills/teach/SKILL.md +0 -141
  113. package/skills/teach/agents/openai.yaml +0 -4
  114. package/skills/to-questionnaire/LICENSE +0 -21
  115. package/skills/to-questionnaire/SKILL.md +0 -55
  116. package/skills/to-questionnaire/agents/openai.yaml +0 -4
  117. package/skills/to-spec/LICENSE +0 -21
  118. package/skills/to-spec/SKILL.md +0 -76
  119. package/skills/to-spec/agents/openai.yaml +0 -4
  120. package/skills/to-tickets/LICENSE +0 -21
  121. package/skills/to-tickets/SKILL.md +0 -106
  122. package/skills/to-tickets/agents/openai.yaml +0 -4
  123. package/skills/wait-what/LICENSE +0 -21
  124. package/skills/wait-what/SKILL.md +0 -8
  125. package/skills/wait-what/agents/openai.yaml +0 -4
  126. package/skills/wayfinder/LICENSE +0 -21
  127. package/skills/wayfinder/SKILL.md +0 -129
  128. package/skills/wayfinder/agents/openai.yaml +0 -4
  129. package/skills/writing-for-agents/LICENSE +0 -21
  130. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  131. package/skills/writing-for-agents/SKILL.md +0 -83
  132. package/skills/writing-for-agents/agents/openai.yaml +0 -3
package/dist/copies.js CHANGED
@@ -3,7 +3,7 @@ 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
5
  import { finalRefusal, HouseRefusal, retrying } from './api.js';
6
- import { blobId, blobText, git, HOUSE_REF, importCommits, localCopies, revision, run, succeeded, tree, } from './git.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,10 +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 IDENTITY = { GIT_COMMITTER_NAME: 'House Kit', GIT_COMMITTER_EMAIL: 'house-kit@localhost' };
14
+ const IDENTITY = { GIT_COMMITTER_NAME: '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,6 +69,9 @@ function sidecar(path) {
40
69
  segments.splice(-1, 0, PROVENANCE);
41
70
  return segments.join('/');
42
71
  }
72
+ function recordOf(key) {
73
+ return join(kitHome(), 'local-copies', `${key}.json`);
74
+ }
43
75
  function rootOf(handle) {
44
76
  return `/rooms/${handle}`;
45
77
  }
@@ -101,7 +133,7 @@ async function ignoreRules(repository, writable, copy) {
101
133
  ].map(pattern);
102
134
  const granted = readOnly.filter((path) => !protectedPath(path)).map(pattern);
103
135
  await ruled(join(repository, '.gitignore'), [
104
- '# House Kit writes this file and .ignore. The paths below are read-only House',
136
+ '# Kit writes this file and .ignore. The paths below are read-only House',
105
137
  '# content: Git does not track them, and House refuses a change to them even',
106
138
  '# when this file is edited or a path is force-added.',
107
139
  '/.gitignore',
@@ -116,7 +148,7 @@ async function ignoreRules(repository, writable, copy) {
116
148
  '',
117
149
  ].join('\n'));
118
150
  await ruled(join(repository, '.ignore'), [
119
- '# House Kit writes this file so native search still reads the read-only House',
151
+ '# Kit writes this file so native search still reads the read-only House',
120
152
  '# content that .gitignore keeps out of Git.',
121
153
  `!${PROVENANCE}`,
122
154
  ...[...house, ...granted].map((rule) => `!${rule}`),
@@ -152,8 +184,7 @@ 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 = {
@@ -185,42 +216,75 @@ 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)
221
+ .filter((copy) => copy.unfollowed === undefined)
191
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 = (chosen?.rooms ?? []).flatMap((room) => room.room_handle === null ? [] : [{ key: `rooms/${room.room_ref}`, handle: room.room_handle }]);
202
- const keys = new Set(selected.map((scope) => scope.key));
203
- const work = [];
204
- for (const key of this.copies.keys()) {
205
- if (keys.has(key))
206
- continue;
207
- work.push(this.exclusive(key, async () => {
208
- const copy = this.copies.get(key);
209
- if (copy.active)
210
- await this.save({ ...copy, active: false });
211
- }));
212
- }
213
- for (const scope of selected)
214
- work.push(this.exclusive(scope.key, () => this.apply(scope)).catch(logged));
215
- await Promise.all(work);
225
+ connected() {
226
+ return this.selected();
216
227
  }
217
- entries(frame) {
218
- const copy = [...this.copies.values()].find((held) => held.active && held.room_ref === frame.authority);
219
- if (copy === undefined)
220
- return;
221
- if (copy.log_epoch === frame.log_epoch && BigInt(frame.position) <= BigInt(copy.position))
222
- return;
223
- 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
+ const mark = refreshing ? 'said' : 'unsaid';
254
+ for (const copy of held()) {
255
+ if (synced.has(copy.key) || copy.unfollowed === 'said' || copy.unfollowed === mark)
256
+ continue;
257
+ await this.save({ ...copy, unfollowed: mark });
258
+ if (refreshing)
259
+ said.push(`${rootOf(copy.room_handle)}: no longer followed; kept as it is`);
260
+ }
261
+ const current = [];
262
+ for (const [key, handle] of synced) {
263
+ try {
264
+ const held = this.copies.get(key);
265
+ if (held === undefined || !(await exists(held.repository))) {
266
+ said.push(await this.materialize(key, handle));
267
+ continue;
268
+ }
269
+ const { unfollowed, ...followed } = held;
270
+ current.push(unfollowed === undefined && held.room_handle === handle ? held : await this.save({ ...followed, room_handle: handle }));
271
+ }
272
+ catch (error) {
273
+ refused = true;
274
+ said.push(`${rootOf(handle)}: ${causeOf(error)}`);
275
+ }
276
+ }
277
+ if (refreshing && current.length > 0) {
278
+ const failed = await this.refreshed(current).then(() => null, houseCause);
279
+ for (const copy of current)
280
+ said.push(...moved(copy, this.copies.get(copy.key)));
281
+ if (failed !== null) {
282
+ refused = true;
283
+ said.push(failed);
284
+ }
285
+ }
286
+ return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
287
+ });
224
288
  }
225
289
  async observed(report) {
226
290
  const key = await this.keyAt(dirname(report.common));
@@ -250,11 +314,11 @@ export class LocalCopies {
250
314
  async push(cwd, selector, caller) {
251
315
  const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
252
316
  if (located.status !== 0)
253
- return { refused: true, text: `${cwd} is not a House Local copy; run house push inside one` };
317
+ return outside(cwd);
254
318
  const [common, worktree] = located.stdout.toString('utf8').split('\n');
255
319
  const key = await this.keyAt(dirname(common));
256
320
  if (key === undefined)
257
- return { refused: true, text: `${worktree} is not a House Local copy; run house push inside one` };
321
+ return outside(worktree);
258
322
  return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
259
323
  }
260
324
  async keyAt(repository) {
@@ -270,19 +334,12 @@ export class LocalCopies {
270
334
  this.queues.set(key, queued.catch(() => undefined));
271
335
  return queued;
272
336
  }
273
- deliver(key) {
274
- if (this.delivering.has(key))
275
- return;
276
- this.delivering.add(key);
277
- void this.exclusive(key, async () => {
278
- this.delivering.delete(key);
279
- const copy = this.copies.get(key);
280
- if (copy.active)
281
- await this.save(await this.caughtUp(copy));
282
- }).catch(logged);
337
+ holding(keys, work) {
338
+ const [key, ...rest] = keys;
339
+ return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
283
340
  }
284
341
  async save(copy) {
285
- const path = join(kitHome(), 'local-copies', `${copy.key}.json`);
342
+ const path = recordOf(copy.key);
286
343
  await mkdir(dirname(path), { recursive: true, mode: 0o700 });
287
344
  const temporary = `${path}.${process.pid}`;
288
345
  await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
@@ -325,46 +382,13 @@ export class LocalCopies {
325
382
  }
326
383
  return read;
327
384
  }
328
- async apply(scope) {
329
- let copy = this.copies.get(scope.key);
330
- if (copy === undefined || !(await exists(copy.repository))) {
331
- await this.materialize(scope);
332
- return;
333
- }
334
- if (copy.room_handle !== scope.handle)
335
- copy = await this.save({ ...copy, room_handle: scope.handle });
336
- if (!copy.active) {
337
- await this.save(await this.received({ ...copy, active: true }));
338
- return;
339
- }
340
- const discovery = await this.house.post('/kit/door/discover', {
341
- prefixes: [rootOf(copy.room_handle)],
342
- });
343
- const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
344
- if (authority === undefined)
345
- return;
346
- if (authority.managed_replication !== 'admitted') {
347
- logged(`operation_denied: ${rootOf(copy.room_handle)} is not synchronized to this Environment`);
348
- return;
349
- }
350
- if (JSON.stringify([authority.operations.write, authority.protected_paths]) !==
351
- JSON.stringify([copy.write, copy.protected])) {
352
- await this.save(await this.received(copy));
353
- return;
354
- }
355
- await this.save(await this.caughtUp(copy));
356
- }
357
- async materialize(scope) {
358
- const read = await this.read(scope.handle);
359
- if (read === null) {
360
- logged(`House serves nothing of ${rootOf(scope.handle)} to copy`);
361
- return;
362
- }
385
+ async materialize(key, handle) {
386
+ const read = await this.read(handle);
387
+ if (read === null)
388
+ throw new Error('House serves nothing of it to copy');
363
389
  const repository = join(localCopies(), 'rooms', read.anchor);
364
- if (await exists(repository)) {
365
- logged(`${repository} already exists and House Kit leaves it as it is`);
366
- return;
367
- }
390
+ if (await exists(repository))
391
+ throw new Error(`${repository} already exists and Kit leaves it as it is`);
368
392
  const staging = join(dirname(repository), `.${randomUUID()}`);
369
393
  await mkdir(staging, { recursive: true });
370
394
  let house;
@@ -372,7 +396,7 @@ export class LocalCopies {
372
396
  try {
373
397
  await git(staging, ['init', '-q', '-b', BRANCH]);
374
398
  house = await importCommits(staging, [
375
- { message: `House ${rootOf(scope.handle)}`, from: null, files: read.writable },
399
+ { message: `House ${rootOf(handle)}`, from: null, files: read.writable },
376
400
  ]);
377
401
  await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
378
402
  await git(staging, ['reset', '-q', '--hard']);
@@ -385,11 +409,10 @@ export class LocalCopies {
385
409
  throw error;
386
410
  }
387
411
  await this.save({
388
- key: scope.key,
412
+ key,
389
413
  room_ref: read.anchor,
390
- room_handle: scope.handle,
414
+ room_handle: handle,
391
415
  repository,
392
- active: true,
393
416
  house,
394
417
  position: read.position,
395
418
  log_epoch: read.log_epoch,
@@ -399,6 +422,7 @@ export class LocalCopies {
399
422
  manifests: { [house]: { parent: null, files: read.revisions } },
400
423
  moves: [],
401
424
  });
425
+ return `${rootOf(handle)}: copied to ${repository}`;
402
426
  }
403
427
  async received(copy) {
404
428
  const read = await this.read(copy.room_handle);
@@ -422,29 +446,57 @@ export class LocalCopies {
422
446
  await ignoreRules(copy.repository, [...read.writable.keys()], next);
423
447
  return next;
424
448
  }
425
- async caughtUp(start) {
426
- let copy = start;
427
- for (;;) {
449
+ async refreshed(copies) {
450
+ const discover = (group) => this.house.post('/kit/door/discover', { prefixes: group.map((copy) => rootOf(copy.room_handle)) });
451
+ const [first, ...rest] = copies;
452
+ const { bounds, authorities } = await discover([first]);
453
+ for (const group of grouped(rest, bounds.max_prefixes))
454
+ authorities.push(...(await discover(group)).authorities);
455
+ const unchanged = [];
456
+ for (const copy of copies) {
457
+ const authority = authorities.find((entry) => entry.room_ref === copy.room_ref);
458
+ if (authority !== undefined &&
459
+ JSON.stringify([authority.operations.write, authority.protected_paths]) !== JSON.stringify([copy.write, copy.protected])) {
460
+ await this.save(await this.received(copy));
461
+ }
462
+ else {
463
+ unchanged.push(copy);
464
+ }
465
+ }
466
+ for (const group of grouped(unchanged, bounds.max_prefixes))
467
+ await this.caughtUp(group, false);
468
+ }
469
+ async caughtUp(copies, whole) {
470
+ const caught = new Map(copies.map((copy) => [copy.key, copy]));
471
+ let asking = [...copies];
472
+ while (asking.length > 0) {
428
473
  let answer;
429
474
  try {
430
475
  answer = await this.house.post('/kit/door/enumerate', {
431
- prefixes: [rootOf(copy.room_handle)],
432
- positions: [{ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch }],
476
+ prefixes: asking.map((copy) => rootOf(copy.room_handle)),
477
+ positions: asking.map((copy) => ({ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch })),
433
478
  });
434
479
  }
435
480
  catch (error) {
436
- const code = error instanceof HouseRefusal ? (refusalOf(error.text)?.code ?? null) : null;
437
- if (code === 'position_expired')
438
- return this.received(copy);
439
- if (code !== 'replica_behind')
481
+ const expired = asking.find((copy) => copy.room_ref === expiredAuthority(error));
482
+ if (expired === undefined)
440
483
  throw error;
441
- return copy;
484
+ caught.set(expired.key, await this.save(await this.received(expired)));
485
+ asking = asking.filter((copy) => copy !== expired);
486
+ continue;
442
487
  }
443
- const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
444
- if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
445
- return copy;
446
- copy = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
488
+ const advanced = [];
489
+ for (const copy of asking) {
490
+ const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
491
+ if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
492
+ continue;
493
+ const next = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
494
+ caught.set(copy.key, next);
495
+ advanced.push(next);
496
+ }
497
+ asking = whole ? advanced : [];
447
498
  }
499
+ return copies.map((copy) => caught.get(copy.key));
448
500
  }
449
501
  async incorporated(copy, changes, position, logEpoch) {
450
502
  const houseTree = await tree(copy.repository, copy.house);
@@ -507,9 +559,6 @@ export class LocalCopies {
507
559
  }
508
560
  copy = await this.save(without(copy));
509
561
  }
510
- if (!copy.active) {
511
- return { refused: true, text: `${rootOf(copy.room_handle)} is no longer synced to this Environment, so House takes no push from it` };
512
- }
513
562
  const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
514
563
  const base = listed.find((ancestor) => ancestor in copy.manifests);
515
564
  if (base === undefined) {
@@ -520,7 +569,7 @@ export class LocalCopies {
520
569
  changes = await this.derived(copy, base, commit);
521
570
  }
522
571
  catch (error) {
523
- return { refused: true, text: causeOf(error) };
572
+ return { refused: true, text: houseCause(error) };
524
573
  }
525
574
  if (changes.length === 0)
526
575
  return { refused: false, text: `Nothing to submit: ${short(commit)} matches House.` };
@@ -547,7 +596,18 @@ export class LocalCopies {
547
596
  const after = await tree(copy.repository, commit);
548
597
  const moved = movedPaths(copy.moves, before, after);
549
598
  const arrived = new Set(moved.values());
550
- const text = (entry, path) => blobText(copy.repository, entry, path);
599
+ const arriving = [...after].filter(([path, entry]) => before.get(path) !== entry);
600
+ if (arriving.length > 0) {
601
+ const { bounds } = await this.house.post('/kit/door/discover', { prefixes: [root] });
602
+ const binary = [];
603
+ for (const [path, entry] of arriving) {
604
+ if (!(await isText(copy.repository, entry, bounds.max_authored_file_bytes)))
605
+ binary.push(path);
606
+ }
607
+ if (binary.length > 0)
608
+ throw new Error(textOnly(binary));
609
+ }
610
+ const text = (entry) => blobText(copy.repository, entry);
551
611
  const changes = [];
552
612
  for (const path of [...new Set([...before.keys(), ...after.keys()])].sort()) {
553
613
  if (arrived.has(path))
@@ -560,7 +620,7 @@ export class LocalCopies {
560
620
  changes.push({ op: 'rename', path: `${root}/${path}`, to: `${root}/${to}`, base: known });
561
621
  const moving = after.get(to);
562
622
  if (moving !== prior) {
563
- changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving, to) });
623
+ changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving) });
564
624
  }
565
625
  continue;
566
626
  }
@@ -569,9 +629,9 @@ export class LocalCopies {
569
629
  if (next === undefined)
570
630
  changes.push({ op: 'remove', path: `${root}/${path}`, base: known });
571
631
  else if (prior === undefined)
572
- changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next, path) });
632
+ changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next) });
573
633
  else
574
- changes.push(...changed(`${root}/${path}`, path, known, await text(prior, path), await text(next, path)));
634
+ changes.push(...changed(`${root}/${path}`, path, known, await text(prior), await text(next)));
575
635
  }
576
636
  return changes;
577
637
  }
@@ -598,12 +658,14 @@ export class LocalCopies {
598
658
  const { upload, ...unstaged } = copy.push;
599
659
  return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
600
660
  }
601
- if (outcome.refused.code === 'operation_expired')
661
+ if (outcome.refused.code === 'expired')
602
662
  return this.recovered(copy);
603
- const rebased = rebaseFixes(outcome.refused);
604
663
  let settled = await this.save(without(copy));
664
+ if (settled.unfollowed !== undefined)
665
+ return { refused: true, text: outcome.refused.text };
666
+ const rebased = rebaseFixes(outcome.refused);
605
667
  try {
606
- settled = await this.save(await this.caughtUp(settled));
668
+ settled = (await this.caughtUp([settled], true))[0];
607
669
  }
608
670
  catch {
609
671
  return {
@@ -622,7 +684,7 @@ export class LocalCopies {
622
684
  }
623
685
  async recovered(copy) {
624
686
  const push = copy.push;
625
- const current = await this.save(await this.caughtUp(copy));
687
+ const current = (await this.caughtUp([copy], true))[0];
626
688
  const house = await tree(copy.repository, current.house);
627
689
  const root = rootOf(current.room_handle);
628
690
  if (!(await this.landed(copy.repository, push.changes, house, await tree(copy.repository, push.commit)))) {
@@ -638,7 +700,7 @@ export class LocalCopies {
638
700
  written.push({
639
701
  path: `${root}/${path}`,
640
702
  revision: entry === undefined ? null : (revisionAt(current, current.house, path) ?? null),
641
- content: entry === undefined ? null : await blobText(copy.repository, entry, path),
703
+ content: entry === undefined ? null : await blobText(copy.repository, entry),
642
704
  });
643
705
  }
644
706
  return this.finished(await this.save({ ...current, push: { ...push, state: 'accepted', written } }));
@@ -666,7 +728,7 @@ export class LocalCopies {
666
728
  const push = copy.push;
667
729
  let written = push.written;
668
730
  if (push.changes.some((change) => change.op === 'rename')) {
669
- copy = await this.save(await this.caughtUp(copy));
731
+ copy = (await this.caughtUp([copy], true))[0];
670
732
  const house = await tree(copy.repository, copy.house);
671
733
  const sent = await tree(copy.repository, push.house);
672
734
  if (push.changes.some((change) => change.op === 'rename' && !house.has(relative(change.to)))) {
@@ -682,7 +744,7 @@ export class LocalCopies {
682
744
  {
683
745
  path: `${rootOf(copy.room_handle)}/${path}`,
684
746
  revision: entry === undefined ? null : (revisionAt(copy, copy.house, path) ?? null),
685
- content: entry === undefined ? null : await blobText(copy.repository, entry, path),
747
+ content: entry === undefined ? null : await blobText(copy.repository, entry),
686
748
  },
687
749
  ];
688
750
  }
@@ -727,7 +789,7 @@ export class LocalCopies {
727
789
  async landed(repository, changes, house, selected) {
728
790
  const text = async (path) => {
729
791
  const entry = house.get(relative(path));
730
- return entry === undefined ? undefined : blobText(repository, entry, path);
792
+ return entry === undefined ? undefined : blobText(repository, entry);
731
793
  };
732
794
  for (const change of changes) {
733
795
  const current = await text(change.path);
package/dist/end.js CHANGED
@@ -1,8 +1,27 @@
1
- import { killDescendants } from './acp.js';
2
- export const REPLACED = 'another Kit started for this Environment';
3
- export const SIGN_IN_AGAIN = 'sign in again';
4
- export function end(reason) {
1
+ import { KIT_RELEASE } 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 process started for this Environment';
6
+ function exit(reason, status) {
5
7
  process.stderr.write(`kit: ${reason}\n`);
6
8
  killDescendants();
7
- process.exit(0);
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_RELEASE} 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');
8
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
+ });