@agentshouse/kit 0.2.1-ce145e9aadbf6 → 0.2.1-cf6326dfa5c3e

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 +151 -41
  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 +36 -57
  8. package/dist/chatgpt.js +32 -0
  9. package/dist/clis.js +126 -69
  10. package/dist/codex-main.js +58 -0
  11. package/dist/conversations.js +457 -277
  12. package/dist/copies.js +198 -124
  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 +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/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 +3 -2
  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 -28
  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,6 +69,9 @@ function sidecar(path) {
41
69
  segments.splice(-1, 0, PROVENANCE);
42
70
  return segments.join('/');
43
71
  }
72
+ function recordOf(key) {
73
+ return join(kitHome(), 'local-copies', `${key}.json`);
74
+ }
44
75
  function rootOf(handle) {
45
76
  return `/rooms/${handle}`;
46
77
  }
@@ -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: [] },
@@ -186,42 +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)
221
+ .filter((copy) => copy.erasing === undefined)
192
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 = (chosen?.rooms ?? []).flatMap((room) => room.room_handle === null ? [] : [{ key: `rooms/${room.room_ref}`, handle: room.room_handle }]);
203
- const keys = new Set(selected.map((scope) => scope.key));
204
- const work = [];
205
- for (const key of this.copies.keys()) {
206
- if (keys.has(key))
207
- continue;
208
- work.push(this.exclusive(key, async () => {
209
- const copy = this.copies.get(key);
210
- if (copy.active)
211
- await this.save({ ...copy, active: false });
212
- }));
213
- }
214
- for (const scope of selected)
215
- work.push(this.exclusive(scope.key, () => this.apply(scope)).catch(logged));
216
- await Promise.all(work);
225
+ connected() {
226
+ return this.selected();
217
227
  }
218
- entries(frame) {
219
- const copy = [...this.copies.values()].find((held) => held.active && held.room_ref === frame.authority);
220
- if (copy === undefined)
221
- return;
222
- if (copy.log_epoch === frame.log_epoch && BigInt(frame.position) <= BigInt(copy.position))
223
- return;
224
- 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
+ });
225
294
  }
226
295
  async observed(report) {
227
296
  const key = await this.keyAt(dirname(report.common));
@@ -229,6 +298,8 @@ export class LocalCopies {
229
298
  return;
230
299
  await this.exclusive(key, async () => {
231
300
  const copy = this.copies.get(key);
301
+ if (copy === undefined || copy.erasing)
302
+ return;
232
303
  let moves = copy.moves;
233
304
  for (const { from, to } of report.moves) {
234
305
  const held = moves.find((move) => move.worktree === report.worktree && move.to === from);
@@ -251,11 +322,11 @@ export class LocalCopies {
251
322
  async push(cwd, selector, caller) {
252
323
  const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
253
324
  if (located.status !== 0)
254
- return { refused: true, text: `${cwd} is not a House Local copy; run house push inside one` };
325
+ return outside(cwd);
255
326
  const [common, worktree] = located.stdout.toString('utf8').split('\n');
256
327
  const key = await this.keyAt(dirname(common));
257
328
  if (key === undefined)
258
- return { refused: true, text: `${worktree} is not a House Local copy; run house push inside one` };
329
+ return outside(worktree);
259
330
  return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
260
331
  }
261
332
  async keyAt(repository) {
@@ -271,19 +342,12 @@ export class LocalCopies {
271
342
  this.queues.set(key, queued.catch(() => undefined));
272
343
  return queued;
273
344
  }
274
- deliver(key) {
275
- if (this.delivering.has(key))
276
- return;
277
- this.delivering.add(key);
278
- void this.exclusive(key, async () => {
279
- this.delivering.delete(key);
280
- const copy = this.copies.get(key);
281
- if (copy.active)
282
- await this.save(await this.caughtUp(copy));
283
- }).catch(logged);
345
+ holding(keys, work) {
346
+ const [key, ...rest] = keys;
347
+ return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
284
348
  }
285
349
  async save(copy) {
286
- const path = join(kitHome(), 'local-copies', `${copy.key}.json`);
350
+ const path = recordOf(copy.key);
287
351
  await mkdir(dirname(path), { recursive: true, mode: 0o700 });
288
352
  const temporary = `${path}.${process.pid}`;
289
353
  await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
@@ -291,6 +355,12 @@ export class LocalCopies {
291
355
  this.copies.set(copy.key, copy);
292
356
  return copy;
293
357
  }
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
+ }
294
364
  async read(handle) {
295
365
  const prefixes = [rootOf(handle)];
296
366
  const discovery = await this.house.post('/kit/door/discover', { prefixes });
@@ -326,46 +396,13 @@ export class LocalCopies {
326
396
  }
327
397
  return read;
328
398
  }
329
- async apply(scope) {
330
- let copy = this.copies.get(scope.key);
331
- if (copy === undefined || !(await exists(copy.repository))) {
332
- await this.materialize(scope);
333
- return;
334
- }
335
- if (copy.room_handle !== scope.handle)
336
- copy = await this.save({ ...copy, room_handle: scope.handle });
337
- if (!copy.active) {
338
- await this.save(await this.received({ ...copy, active: true }));
339
- return;
340
- }
341
- const discovery = await this.house.post('/kit/door/discover', {
342
- prefixes: [rootOf(copy.room_handle)],
343
- });
344
- const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
345
- if (authority === undefined)
346
- return;
347
- if (authority.managed_replication !== 'admitted') {
348
- logged(`operation_denied: ${rootOf(copy.room_handle)} is not synchronized to this Environment`);
349
- return;
350
- }
351
- if (JSON.stringify([authority.operations.write, authority.protected_paths]) !==
352
- JSON.stringify([copy.write, copy.protected])) {
353
- await this.save(await this.received(copy));
354
- return;
355
- }
356
- await this.save(await this.caughtUp(copy));
357
- }
358
- async materialize(scope) {
359
- const read = await this.read(scope.handle);
360
- if (read === null) {
361
- logged(`House serves nothing of ${rootOf(scope.handle)} to copy`);
362
- return;
363
- }
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');
364
403
  const repository = join(localCopies(), 'rooms', read.anchor);
365
- if (await exists(repository)) {
366
- logged(`${repository} already exists and House Kit leaves it as it is`);
367
- return;
368
- }
404
+ if (await exists(repository))
405
+ throw new Error(`${repository} already exists and House Kit leaves it as it is`);
369
406
  const staging = join(dirname(repository), `.${randomUUID()}`);
370
407
  await mkdir(staging, { recursive: true });
371
408
  let house;
@@ -373,7 +410,7 @@ export class LocalCopies {
373
410
  try {
374
411
  await git(staging, ['init', '-q', '-b', BRANCH]);
375
412
  house = await importCommits(staging, [
376
- { message: `House ${rootOf(scope.handle)}`, from: null, files: read.writable },
413
+ { message: `House ${rootOf(handle)}`, from: null, files: read.writable },
377
414
  ]);
378
415
  await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
379
416
  await git(staging, ['reset', '-q', '--hard']);
@@ -386,11 +423,10 @@ export class LocalCopies {
386
423
  throw error;
387
424
  }
388
425
  await this.save({
389
- key: scope.key,
426
+ key,
390
427
  room_ref: read.anchor,
391
- room_handle: scope.handle,
428
+ room_handle: handle,
392
429
  repository,
393
- active: true,
394
430
  house,
395
431
  position: read.position,
396
432
  log_epoch: read.log_epoch,
@@ -400,6 +436,7 @@ export class LocalCopies {
400
436
  manifests: { [house]: { parent: null, files: read.revisions } },
401
437
  moves: [],
402
438
  });
439
+ return `${rootOf(handle)}: copied to ${repository}`;
403
440
  }
404
441
  async received(copy) {
405
442
  const read = await this.read(copy.room_handle);
@@ -423,30 +460,57 @@ export class LocalCopies {
423
460
  await ignoreRules(copy.repository, [...read.writable.keys()], next);
424
461
  return next;
425
462
  }
426
- async caughtUp(start) {
427
- let copy = start;
428
- 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) {
429
487
  let answer;
430
488
  try {
431
489
  answer = await this.house.post('/kit/door/enumerate', {
432
- prefixes: [rootOf(copy.room_handle)],
433
- 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 })),
434
492
  });
435
493
  }
436
494
  catch (error) {
437
- const code = error instanceof HouseRefusal ? (refusalOf(error.text)?.code ?? null) : null;
438
- if (code === 'position_expired')
439
- return this.received(copy);
440
- if (code !== 'replica_behind')
495
+ const expired = asking.find((copy) => copy.room_ref === expiredAuthority(error));
496
+ if (expired === undefined)
441
497
  throw error;
442
- setTimeout(() => this.deliver(copy.key), REPLICA_RETRY_MS).unref();
443
- 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);
444
510
  }
445
- const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
446
- if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
447
- return copy;
448
- copy = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
511
+ asking = whole ? advanced : [];
449
512
  }
513
+ return copies.map((copy) => caught.get(copy.key));
450
514
  }
451
515
  async incorporated(copy, changes, position, logEpoch) {
452
516
  const houseTree = await tree(copy.repository, copy.house);
@@ -496,6 +560,8 @@ export class LocalCopies {
496
560
  }
497
561
  async pushed(key, worktree, selector, caller) {
498
562
  let copy = this.copies.get(key);
563
+ if (copy === undefined || copy.erasing)
564
+ return outside(worktree);
499
565
  if (copy.push?.state === 'submitted')
500
566
  return this.settle(copy, caller);
501
567
  const selected = await run(worktree, ['rev-parse', '--verify', '--quiet', '--end-of-options', `${selector}^{commit}`]);
@@ -509,9 +575,6 @@ export class LocalCopies {
509
575
  }
510
576
  copy = await this.save(without(copy));
511
577
  }
512
- if (!copy.active) {
513
- return { refused: true, text: `${rootOf(copy.room_handle)} is no longer synced to this Environment, so House takes no push from it` };
514
- }
515
578
  const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
516
579
  const base = listed.find((ancestor) => ancestor in copy.manifests);
517
580
  if (base === undefined) {
@@ -522,7 +585,7 @@ export class LocalCopies {
522
585
  changes = await this.derived(copy, base, commit);
523
586
  }
524
587
  catch (error) {
525
- return { refused: true, text: causeOf(error) };
588
+ return { refused: true, text: houseCause(error) };
526
589
  }
527
590
  if (changes.length === 0)
528
591
  return { refused: false, text: `Nothing to submit: ${short(commit)} matches House.` };
@@ -549,7 +612,18 @@ export class LocalCopies {
549
612
  const after = await tree(copy.repository, commit);
550
613
  const moved = movedPaths(copy.moves, before, after);
551
614
  const arrived = new Set(moved.values());
552
- 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);
553
627
  const changes = [];
554
628
  for (const path of [...new Set([...before.keys(), ...after.keys()])].sort()) {
555
629
  if (arrived.has(path))
@@ -562,7 +636,7 @@ export class LocalCopies {
562
636
  changes.push({ op: 'rename', path: `${root}/${path}`, to: `${root}/${to}`, base: known });
563
637
  const moving = after.get(to);
564
638
  if (moving !== prior) {
565
- 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) });
566
640
  }
567
641
  continue;
568
642
  }
@@ -571,9 +645,9 @@ export class LocalCopies {
571
645
  if (next === undefined)
572
646
  changes.push({ op: 'remove', path: `${root}/${path}`, base: known });
573
647
  else if (prior === undefined)
574
- changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next, path) });
648
+ changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next) });
575
649
  else
576
- 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)));
577
651
  }
578
652
  return changes;
579
653
  }
@@ -600,12 +674,12 @@ export class LocalCopies {
600
674
  const { upload, ...unstaged } = copy.push;
601
675
  return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
602
676
  }
603
- if (outcome.refused.code === 'operation_expired')
677
+ if (outcome.refused.code === 'expired')
604
678
  return this.recovered(copy);
605
679
  const rebased = rebaseFixes(outcome.refused);
606
680
  let settled = await this.save(without(copy));
607
681
  try {
608
- settled = await this.save(await this.caughtUp(settled));
682
+ settled = (await this.caughtUp([settled], true))[0];
609
683
  }
610
684
  catch {
611
685
  return {
@@ -624,7 +698,7 @@ export class LocalCopies {
624
698
  }
625
699
  async recovered(copy) {
626
700
  const push = copy.push;
627
- const current = await this.save(await this.caughtUp(copy));
701
+ const current = (await this.caughtUp([copy], true))[0];
628
702
  const house = await tree(copy.repository, current.house);
629
703
  const root = rootOf(current.room_handle);
630
704
  if (!(await this.landed(copy.repository, push.changes, house, await tree(copy.repository, push.commit)))) {
@@ -640,7 +714,7 @@ export class LocalCopies {
640
714
  written.push({
641
715
  path: `${root}/${path}`,
642
716
  revision: entry === undefined ? null : (revisionAt(current, current.house, path) ?? null),
643
- content: entry === undefined ? null : await blobText(copy.repository, entry, path),
717
+ content: entry === undefined ? null : await blobText(copy.repository, entry),
644
718
  });
645
719
  }
646
720
  return this.finished(await this.save({ ...current, push: { ...push, state: 'accepted', written } }));
@@ -668,7 +742,7 @@ export class LocalCopies {
668
742
  const push = copy.push;
669
743
  let written = push.written;
670
744
  if (push.changes.some((change) => change.op === 'rename')) {
671
- copy = await this.save(await this.caughtUp(copy));
745
+ copy = (await this.caughtUp([copy], true))[0];
672
746
  const house = await tree(copy.repository, copy.house);
673
747
  const sent = await tree(copy.repository, push.house);
674
748
  if (push.changes.some((change) => change.op === 'rename' && !house.has(relative(change.to)))) {
@@ -684,7 +758,7 @@ export class LocalCopies {
684
758
  {
685
759
  path: `${rootOf(copy.room_handle)}/${path}`,
686
760
  revision: entry === undefined ? null : (revisionAt(copy, copy.house, path) ?? null),
687
- content: entry === undefined ? null : await blobText(copy.repository, entry, path),
761
+ content: entry === undefined ? null : await blobText(copy.repository, entry),
688
762
  },
689
763
  ];
690
764
  }
@@ -729,7 +803,7 @@ export class LocalCopies {
729
803
  async landed(repository, changes, house, selected) {
730
804
  const text = async (path) => {
731
805
  const entry = house.get(relative(path));
732
- return entry === undefined ? undefined : blobText(repository, entry, path);
806
+ return entry === undefined ? undefined : blobText(repository, entry);
733
807
  };
734
808
  for (const change of changes) {
735
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
+ });