@agentshouse/kit 0.2.1-cca9e1f43de44 → 0.2.1-ccf188e02985d

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 (98) hide show
  1. package/README.md +30 -28
  2. package/dist/acp.js +41 -111
  3. package/dist/agents.js +110 -22
  4. package/dist/api.js +9 -5
  5. package/dist/app-git.js +10 -26
  6. package/dist/bootstrap.json +4 -4
  7. package/dist/bridge.js +10 -24
  8. package/dist/chatgpt.js +32 -0
  9. package/dist/clis.js +95 -14
  10. package/dist/codex-main.js +58 -0
  11. package/dist/conversations.js +181 -114
  12. package/dist/copies.js +188 -119
  13. package/dist/end.js +19 -4
  14. package/dist/enrol-main.js +7 -1
  15. package/dist/events.js +14 -76
  16. package/dist/files.js +8 -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 -177
  21. package/dist/inputs.js +37 -0
  22. package/dist/instructions.js +10 -32
  23. package/dist/kit-cli.js +68 -0
  24. package/dist/kit-main.js +8 -22
  25. package/dist/login.js +117 -106
  26. package/dist/logout.js +14 -16
  27. package/dist/parts.js +34 -16
  28. package/dist/placement.js +30 -0
  29. package/dist/relay.js +12 -2
  30. package/dist/resident.js +89 -33
  31. package/dist/scope.js +203 -0
  32. package/dist/service-main.js +9 -0
  33. package/dist/service.js +55 -0
  34. package/dist/sign-in.js +6 -24
  35. package/dist/skills.js +81 -12
  36. package/dist/stream.js +68 -17
  37. package/dist/transcription.js +3 -3
  38. package/dist/update.js +48 -29
  39. package/dist/wire.js +6 -0
  40. package/dist/work.js +346 -0
  41. package/how-we-work.md +13 -3
  42. package/package.json +3 -2
  43. package/skills/ask-matt/SKILL.md +17 -11
  44. package/skills/ask-matt/agents/openai.yaml +1 -0
  45. package/skills/code-review/SKILL.md +5 -5
  46. package/skills/codebase-design/DESIGN-IT-TWICE.md +1 -1
  47. package/skills/codebase-design/SKILL.md +0 -2
  48. package/skills/diagnosing-bugs/SKILL.md +2 -4
  49. package/skills/domain-modeling/{CONTEXT-FORMAT.md → GLOSSARY-FORMAT.md} +10 -10
  50. package/skills/domain-modeling/SKILL.md +12 -14
  51. package/skills/grill-me/SKILL.md +1 -2
  52. package/skills/grill-me/agents/openai.yaml +1 -0
  53. package/skills/grill-with-docs/SKILL.md +1 -2
  54. package/skills/grill-with-docs/agents/openai.yaml +1 -0
  55. package/skills/grilling/SKILL.md +2 -2
  56. package/skills/handoff/SKILL.md +2 -3
  57. package/skills/handoff/agents/openai.yaml +1 -0
  58. package/skills/implement/SKILL.md +5 -4
  59. package/skills/implement/agents/openai.yaml +1 -0
  60. package/skills/implement-spec/SKILL.md +40 -0
  61. package/skills/implement-spec/agents/openai.yaml +5 -0
  62. package/skills/improve-codebase-architecture/SKILL.md +6 -7
  63. package/skills/improve-codebase-architecture/agents/openai.yaml +1 -0
  64. package/skills/pr/CREDITS.md +3 -0
  65. package/skills/pr/LICENSE +21 -0
  66. package/skills/pr/SKILL.md +170 -0
  67. package/skills/pr/agents/openai.yaml +3 -0
  68. package/skills/prototype/SKILL.md +0 -2
  69. package/skills/research/SKILL.md +0 -2
  70. package/skills/retro/LICENSE +21 -0
  71. package/skills/retro/SKILL.md +44 -0
  72. package/skills/retro/agents/openai.yaml +5 -0
  73. package/skills/tdd/SKILL.md +2 -4
  74. package/skills/teach/SKILL.md +3 -4
  75. package/skills/teach/agents/openai.yaml +1 -0
  76. package/skills/to-questionnaire/SKILL.md +1 -2
  77. package/skills/to-questionnaire/agents/openai.yaml +1 -0
  78. package/skills/to-spec/SKILL.md +1 -2
  79. package/skills/to-spec/agents/openai.yaml +1 -0
  80. package/skills/to-tickets/SKILL.md +3 -4
  81. package/skills/to-tickets/agents/openai.yaml +1 -0
  82. package/skills/triage/AGENT-BRIEF.md +207 -0
  83. package/skills/triage/LICENSE +21 -0
  84. package/skills/triage/OUT-OF-SCOPE.md +105 -0
  85. package/skills/triage/SKILL.md +112 -0
  86. package/skills/triage/agents/openai.yaml +5 -0
  87. package/skills/wait-what/SKILL.md +2 -3
  88. package/skills/wait-what/agents/openai.yaml +1 -0
  89. package/skills/wayfinder/SKILL.md +5 -6
  90. package/skills/wayfinder/agents/openai.yaml +1 -0
  91. package/skills/wizard/LICENSE +21 -0
  92. package/skills/wizard/SKILL.md +44 -0
  93. package/skills/wizard/agents/openai.yaml +3 -0
  94. package/skills/wizard/template.sh +223 -0
  95. package/skills/writing-for-agents/SKILL.md +0 -2
  96. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  97. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  98. /package/skills/{resolving-merge-conflicts → implement-spec}/LICENSE +0 -0
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';
@@ -15,6 +15,9 @@ const IDENTITY = { GIT_COMMITTER_NAME: 'House Kit', GIT_COMMITTER_EMAIL: 'house-
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,32 @@ 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
+ }
35
61
  function covers(path, prefix) {
36
62
  return prefix === '' || path === prefix || path.startsWith(`${prefix}/`);
37
63
  }
@@ -40,6 +66,9 @@ function sidecar(path) {
40
66
  segments.splice(-1, 0, PROVENANCE);
41
67
  return segments.join('/');
42
68
  }
69
+ function recordOf(key) {
70
+ return join(kitHome(), 'local-copies', `${key}.json`);
71
+ }
43
72
  function rootOf(handle) {
44
73
  return `/rooms/${handle}`;
45
74
  }
@@ -152,8 +181,7 @@ export class LocalCopies {
152
181
  house;
153
182
  copies = new Map();
154
183
  queues = new Map();
155
- delivering = new Set();
156
- selecting = Promise.resolve();
184
+ answering = Promise.resolve();
157
185
  constructor(house) {
158
186
  this.house = house;
159
187
  this.owner = {
@@ -185,42 +213,82 @@ export class LocalCopies {
185
213
  }
186
214
  }
187
215
  async mapping() {
188
- await this.selecting;
216
+ await this.answering;
189
217
  return [...this.copies.values()]
190
- .filter((copy) => copy.active)
218
+ .filter((copy) => copy.erasing === undefined)
191
219
  .map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
192
220
  .sort();
193
221
  }
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);
222
+ connected() {
223
+ return this.selected();
216
224
  }
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);
225
+ async changed(rooms) {
226
+ if (rooms.length > 0)
227
+ await this.selected(rooms);
228
+ }
229
+ fetch() {
230
+ return this.answered(() => this.fetched(true));
231
+ }
232
+ async selected(rooms) {
233
+ const copied = await this.answered(() => this.fetched(false, rooms));
234
+ if (copied.refused)
235
+ logged(copied.text);
236
+ }
237
+ answered(work) {
238
+ const answered = this.answering.then(work);
239
+ this.answering = answered.catch(() => undefined);
240
+ return answered;
241
+ }
242
+ async fetched(refreshing, rooms) {
243
+ const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', rooms === undefined ? {} : { rooms });
244
+ const synced = new Map(answer.rooms.map((room) => [`rooms/${room.room_ref}`, room.room_handle]));
245
+ const named = rooms === undefined ? null : new Set(rooms.map((room) => `rooms/${room}`));
246
+ const held = () => [...this.copies.values()].filter((copy) => named === null || named.has(copy.key));
247
+ return this.holding([...new Set([...held().map((copy) => copy.key), ...synced.keys()])].sort(), async () => {
248
+ const said = [];
249
+ let refused = false;
250
+ for (const copy of held()) {
251
+ const returned = synced.has(copy.key);
252
+ if (returned && copy.erasing === undefined)
253
+ continue;
254
+ try {
255
+ await this.erase(copy);
256
+ said.push(`${rootOf(copy.room_handle)}: ${returned ? '' : 'no longer synced to this Environment; '}erased from the Local copy`);
257
+ }
258
+ catch (error) {
259
+ refused = true;
260
+ said.push(`${rootOf(copy.room_handle)}: ${causeOf(error)}`);
261
+ }
262
+ }
263
+ const current = [];
264
+ for (const [key, handle] of synced) {
265
+ try {
266
+ const held = this.copies.get(key);
267
+ if (held?.erasing)
268
+ continue;
269
+ if (held === undefined || !(await exists(held.repository))) {
270
+ said.push(await this.materialize(key, handle));
271
+ continue;
272
+ }
273
+ current.push(held.room_handle === handle ? held : await this.save({ ...held, room_handle: handle }));
274
+ }
275
+ catch (error) {
276
+ refused = true;
277
+ said.push(`${rootOf(handle)}: ${causeOf(error)}`);
278
+ }
279
+ }
280
+ if (refreshing && current.length > 0) {
281
+ try {
282
+ const refreshed = await this.refreshed(current);
283
+ current.forEach((copy, index) => said.push(...moved(copy, refreshed[index])));
284
+ }
285
+ catch (error) {
286
+ refused = true;
287
+ said.push(houseCause(error));
288
+ }
289
+ }
290
+ return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
291
+ });
224
292
  }
225
293
  async observed(report) {
226
294
  const key = await this.keyAt(dirname(report.common));
@@ -228,6 +296,8 @@ export class LocalCopies {
228
296
  return;
229
297
  await this.exclusive(key, async () => {
230
298
  const copy = this.copies.get(key);
299
+ if (copy === undefined || copy.erasing)
300
+ return;
231
301
  let moves = copy.moves;
232
302
  for (const { from, to } of report.moves) {
233
303
  const held = moves.find((move) => move.worktree === report.worktree && move.to === from);
@@ -250,11 +320,11 @@ export class LocalCopies {
250
320
  async push(cwd, selector, caller) {
251
321
  const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
252
322
  if (located.status !== 0)
253
- return { refused: true, text: `${cwd} is not a House Local copy; run house push inside one` };
323
+ return outside(cwd);
254
324
  const [common, worktree] = located.stdout.toString('utf8').split('\n');
255
325
  const key = await this.keyAt(dirname(common));
256
326
  if (key === undefined)
257
- return { refused: true, text: `${worktree} is not a House Local copy; run house push inside one` };
327
+ return outside(worktree);
258
328
  return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
259
329
  }
260
330
  async keyAt(repository) {
@@ -270,19 +340,12 @@ export class LocalCopies {
270
340
  this.queues.set(key, queued.catch(() => undefined));
271
341
  return queued;
272
342
  }
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);
343
+ holding(keys, work) {
344
+ const [key, ...rest] = keys;
345
+ return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
283
346
  }
284
347
  async save(copy) {
285
- const path = join(kitHome(), 'local-copies', `${copy.key}.json`);
348
+ const path = recordOf(copy.key);
286
349
  await mkdir(dirname(path), { recursive: true, mode: 0o700 });
287
350
  const temporary = `${path}.${process.pid}`;
288
351
  await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
@@ -290,6 +353,12 @@ export class LocalCopies {
290
353
  this.copies.set(copy.key, copy);
291
354
  return copy;
292
355
  }
356
+ async erase(copy) {
357
+ await this.save({ ...copy, erasing: true });
358
+ await rm(copy.repository, { recursive: true, force: true });
359
+ await rm(recordOf(copy.key), { force: true });
360
+ this.copies.delete(copy.key);
361
+ }
293
362
  async read(handle) {
294
363
  const prefixes = [rootOf(handle)];
295
364
  const discovery = await this.house.post('/kit/door/discover', { prefixes });
@@ -325,46 +394,13 @@ export class LocalCopies {
325
394
  }
326
395
  return read;
327
396
  }
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
- }
397
+ async materialize(key, handle) {
398
+ const read = await this.read(handle);
399
+ if (read === null)
400
+ throw new Error('House serves nothing of it to copy');
363
401
  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
- }
402
+ if (await exists(repository))
403
+ throw new Error(`${repository} already exists and House Kit leaves it as it is`);
368
404
  const staging = join(dirname(repository), `.${randomUUID()}`);
369
405
  await mkdir(staging, { recursive: true });
370
406
  let house;
@@ -372,7 +408,7 @@ export class LocalCopies {
372
408
  try {
373
409
  await git(staging, ['init', '-q', '-b', BRANCH]);
374
410
  house = await importCommits(staging, [
375
- { message: `House ${rootOf(scope.handle)}`, from: null, files: read.writable },
411
+ { message: `House ${rootOf(handle)}`, from: null, files: read.writable },
376
412
  ]);
377
413
  await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
378
414
  await git(staging, ['reset', '-q', '--hard']);
@@ -385,11 +421,10 @@ export class LocalCopies {
385
421
  throw error;
386
422
  }
387
423
  await this.save({
388
- key: scope.key,
424
+ key,
389
425
  room_ref: read.anchor,
390
- room_handle: scope.handle,
426
+ room_handle: handle,
391
427
  repository,
392
- active: true,
393
428
  house,
394
429
  position: read.position,
395
430
  log_epoch: read.log_epoch,
@@ -399,6 +434,7 @@ export class LocalCopies {
399
434
  manifests: { [house]: { parent: null, files: read.revisions } },
400
435
  moves: [],
401
436
  });
437
+ return `${rootOf(handle)}: copied to ${repository}`;
402
438
  }
403
439
  async received(copy) {
404
440
  const read = await this.read(copy.room_handle);
@@ -422,29 +458,52 @@ export class LocalCopies {
422
458
  await ignoreRules(copy.repository, [...read.writable.keys()], next);
423
459
  return next;
424
460
  }
425
- async caughtUp(start) {
426
- let copy = start;
427
- for (;;) {
461
+ async refreshed(copies) {
462
+ const discovery = await this.house.post('/kit/door/discover', {
463
+ prefixes: copies.map((copy) => rootOf(copy.room_handle)),
464
+ });
465
+ const regranted = new Map();
466
+ for (const copy of copies) {
467
+ const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
468
+ if (authority !== undefined &&
469
+ JSON.stringify([authority.operations.write, authority.protected_paths]) !== JSON.stringify([copy.write, copy.protected])) {
470
+ regranted.set(copy.key, await this.save(await this.received(copy)));
471
+ }
472
+ }
473
+ const caught = await this.caughtUp(copies.filter((copy) => !regranted.has(copy.key)), false);
474
+ return copies.map((copy) => regranted.get(copy.key) ?? caught.find((held) => held.key === copy.key));
475
+ }
476
+ async caughtUp(copies, whole) {
477
+ const caught = new Map(copies.map((copy) => [copy.key, copy]));
478
+ let asking = [...copies];
479
+ while (asking.length > 0) {
428
480
  let answer;
429
481
  try {
430
482
  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 }],
483
+ prefixes: asking.map((copy) => rootOf(copy.room_handle)),
484
+ positions: asking.map((copy) => ({ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch })),
433
485
  });
434
486
  }
435
487
  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')
488
+ const expired = asking.find((copy) => copy.room_ref === expiredAuthority(error));
489
+ if (expired === undefined)
440
490
  throw error;
441
- return copy;
491
+ caught.set(expired.key, await this.save(await this.received(expired)));
492
+ asking = asking.filter((copy) => copy !== expired);
493
+ continue;
494
+ }
495
+ const advanced = [];
496
+ for (const copy of asking) {
497
+ const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
498
+ if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
499
+ continue;
500
+ const next = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
501
+ caught.set(copy.key, next);
502
+ advanced.push(next);
442
503
  }
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));
504
+ asking = whole ? advanced : [];
447
505
  }
506
+ return copies.map((copy) => caught.get(copy.key));
448
507
  }
449
508
  async incorporated(copy, changes, position, logEpoch) {
450
509
  const houseTree = await tree(copy.repository, copy.house);
@@ -494,6 +553,8 @@ export class LocalCopies {
494
553
  }
495
554
  async pushed(key, worktree, selector, caller) {
496
555
  let copy = this.copies.get(key);
556
+ if (copy === undefined || copy.erasing)
557
+ return outside(worktree);
497
558
  if (copy.push?.state === 'submitted')
498
559
  return this.settle(copy, caller);
499
560
  const selected = await run(worktree, ['rev-parse', '--verify', '--quiet', '--end-of-options', `${selector}^{commit}`]);
@@ -507,9 +568,6 @@ export class LocalCopies {
507
568
  }
508
569
  copy = await this.save(without(copy));
509
570
  }
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
571
  const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
514
572
  const base = listed.find((ancestor) => ancestor in copy.manifests);
515
573
  if (base === undefined) {
@@ -520,7 +578,7 @@ export class LocalCopies {
520
578
  changes = await this.derived(copy, base, commit);
521
579
  }
522
580
  catch (error) {
523
- return { refused: true, text: causeOf(error) };
581
+ return { refused: true, text: houseCause(error) };
524
582
  }
525
583
  if (changes.length === 0)
526
584
  return { refused: false, text: `Nothing to submit: ${short(commit)} matches House.` };
@@ -547,7 +605,18 @@ export class LocalCopies {
547
605
  const after = await tree(copy.repository, commit);
548
606
  const moved = movedPaths(copy.moves, before, after);
549
607
  const arrived = new Set(moved.values());
550
- const text = (entry, path) => blobText(copy.repository, entry, path);
608
+ const arriving = [...after].filter(([path, entry]) => before.get(path) !== entry);
609
+ if (arriving.length > 0) {
610
+ const { bounds } = await this.house.post('/kit/door/discover', { prefixes: [root] });
611
+ const binary = [];
612
+ for (const [path, entry] of arriving) {
613
+ if (!(await isText(copy.repository, entry, bounds.max_authored_file_bytes)))
614
+ binary.push(path);
615
+ }
616
+ if (binary.length > 0)
617
+ throw new Error(textOnly(binary));
618
+ }
619
+ const text = (entry) => blobText(copy.repository, entry);
551
620
  const changes = [];
552
621
  for (const path of [...new Set([...before.keys(), ...after.keys()])].sort()) {
553
622
  if (arrived.has(path))
@@ -560,7 +629,7 @@ export class LocalCopies {
560
629
  changes.push({ op: 'rename', path: `${root}/${path}`, to: `${root}/${to}`, base: known });
561
630
  const moving = after.get(to);
562
631
  if (moving !== prior) {
563
- changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving, to) });
632
+ changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving) });
564
633
  }
565
634
  continue;
566
635
  }
@@ -569,9 +638,9 @@ export class LocalCopies {
569
638
  if (next === undefined)
570
639
  changes.push({ op: 'remove', path: `${root}/${path}`, base: known });
571
640
  else if (prior === undefined)
572
- changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next, path) });
641
+ changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next) });
573
642
  else
574
- changes.push(...changed(`${root}/${path}`, path, known, await text(prior, path), await text(next, path)));
643
+ changes.push(...changed(`${root}/${path}`, path, known, await text(prior), await text(next)));
575
644
  }
576
645
  return changes;
577
646
  }
@@ -598,12 +667,12 @@ export class LocalCopies {
598
667
  const { upload, ...unstaged } = copy.push;
599
668
  return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
600
669
  }
601
- if (outcome.refused.code === 'operation_expired')
670
+ if (outcome.refused.code === 'expired')
602
671
  return this.recovered(copy);
603
672
  const rebased = rebaseFixes(outcome.refused);
604
673
  let settled = await this.save(without(copy));
605
674
  try {
606
- settled = await this.save(await this.caughtUp(settled));
675
+ settled = (await this.caughtUp([settled], true))[0];
607
676
  }
608
677
  catch {
609
678
  return {
@@ -622,7 +691,7 @@ export class LocalCopies {
622
691
  }
623
692
  async recovered(copy) {
624
693
  const push = copy.push;
625
- const current = await this.save(await this.caughtUp(copy));
694
+ const current = (await this.caughtUp([copy], true))[0];
626
695
  const house = await tree(copy.repository, current.house);
627
696
  const root = rootOf(current.room_handle);
628
697
  if (!(await this.landed(copy.repository, push.changes, house, await tree(copy.repository, push.commit)))) {
@@ -638,7 +707,7 @@ export class LocalCopies {
638
707
  written.push({
639
708
  path: `${root}/${path}`,
640
709
  revision: entry === undefined ? null : (revisionAt(current, current.house, path) ?? null),
641
- content: entry === undefined ? null : await blobText(copy.repository, entry, path),
710
+ content: entry === undefined ? null : await blobText(copy.repository, entry),
642
711
  });
643
712
  }
644
713
  return this.finished(await this.save({ ...current, push: { ...push, state: 'accepted', written } }));
@@ -666,7 +735,7 @@ export class LocalCopies {
666
735
  const push = copy.push;
667
736
  let written = push.written;
668
737
  if (push.changes.some((change) => change.op === 'rename')) {
669
- copy = await this.save(await this.caughtUp(copy));
738
+ copy = (await this.caughtUp([copy], true))[0];
670
739
  const house = await tree(copy.repository, copy.house);
671
740
  const sent = await tree(copy.repository, push.house);
672
741
  if (push.changes.some((change) => change.op === 'rename' && !house.has(relative(change.to)))) {
@@ -682,7 +751,7 @@ export class LocalCopies {
682
751
  {
683
752
  path: `${rootOf(copy.room_handle)}/${path}`,
684
753
  revision: entry === undefined ? null : (revisionAt(copy, copy.house, path) ?? null),
685
- content: entry === undefined ? null : await blobText(copy.repository, entry, path),
754
+ content: entry === undefined ? null : await blobText(copy.repository, entry),
686
755
  },
687
756
  ];
688
757
  }
@@ -727,7 +796,7 @@ export class LocalCopies {
727
796
  async landed(repository, changes, house, selected) {
728
797
  const text = async (path) => {
729
798
  const entry = house.get(relative(path));
730
- return entry === undefined ? undefined : blobText(repository, entry, path);
799
+ return entry === undefined ? undefined : blobText(repository, entry);
731
800
  };
732
801
  for (const change of changes) {
733
802
  const current = await text(change.path);
package/dist/end.js CHANGED
@@ -1,8 +1,23 @@
1
- import { killDescendants } from './acp.js';
1
+ import { forgetRejected } from './home.js';
2
+ import { installCommand } from './placement.js';
3
+ import { killDescendants } from './scope.js';
2
4
  export const REPLACED = 'another Kit started for this Environment';
3
- export const SIGN_IN_AGAIN = 'sign in again';
4
- export function end(reason) {
5
+ function exit(reason, status) {
5
6
  process.stderr.write(`kit: ${reason}\n`);
6
7
  killDescendants();
7
- process.exit(0);
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');
8
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
+ });