@agentshouse/kit 0.2.1-c063e835a50ea → 0.2.1-c0b0ffa6c025b

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 (99) hide show
  1. package/README.md +27 -26
  2. package/dist/acp.js +16 -93
  3. package/dist/agents.js +138 -42
  4. package/dist/api.js +34 -2
  5. package/dist/app-git.js +64 -0
  6. package/dist/bootstrap.json +4 -4
  7. package/dist/bridge.js +33 -51
  8. package/dist/chatgpt.js +29 -0
  9. package/dist/clis.js +128 -65
  10. package/dist/codex-main.js +58 -0
  11. package/dist/conversations.js +298 -237
  12. package/dist/copies.js +165 -138
  13. package/dist/end.js +22 -0
  14. package/dist/enrol-main.js +7 -1
  15. package/dist/events.js +521 -0
  16. package/dist/files.js +21 -1
  17. package/dist/git.js +11 -8
  18. package/dist/home.js +81 -6
  19. package/dist/house-main.js +186 -56
  20. package/dist/instructions.js +9 -32
  21. package/dist/kit-main.js +54 -6
  22. package/dist/login.js +109 -103
  23. package/dist/logout.js +16 -13
  24. package/dist/parts.js +82 -0
  25. package/dist/placement.js +48 -0
  26. package/dist/refusals.js +12 -0
  27. package/dist/relay.js +12 -2
  28. package/dist/resident.js +38 -27
  29. package/dist/scope.js +203 -0
  30. package/dist/secret-input.js +4 -1
  31. package/dist/service-main.js +9 -0
  32. package/dist/service.js +33 -0
  33. package/dist/shell.js +3 -0
  34. package/dist/sign-in.js +34 -9
  35. package/dist/skills.js +81 -12
  36. package/dist/stream.js +87 -11
  37. package/dist/transcription.js +57 -9
  38. package/dist/update.js +30 -6
  39. package/dist/wire.js +6 -0
  40. package/dist/work.js +348 -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/dist/blocks.js +0 -51
  97. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  98. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  99. /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,7 +11,6 @@ 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);
@@ -30,9 +29,29 @@ const REBASED = new Set([
30
29
  function rebaseFixes(refusal) {
31
30
  return refusal.code === 'edit_conflict' && refusal.conflicts.some((code) => REBASED.has(code));
32
31
  }
32
+ function textOnly(paths) {
33
+ const [named, them] = paths.length === 1 ? [`${paths[0]} is`, 'it'] : [`${paths.join(', ')} are`, 'them'];
34
+ return `${named} not text, and house push sends text only; remove ${them} from the commit and place ${them} with house upload`;
35
+ }
33
36
  function short(commit) {
34
37
  return commit.slice(0, 12);
35
38
  }
39
+ function moved(before, after) {
40
+ if (before.house !== after.house)
41
+ return [`${rootOf(after.room_handle)}: ${HOUSE_REF} is at ${short(after.house)}`];
42
+ return before.position === after.position ? [] : [`${rootOf(after.room_handle)}: read-only files refreshed`];
43
+ }
44
+ function expiredAuthority(error) {
45
+ if (!(error instanceof HouseRefusal))
46
+ return undefined;
47
+ try {
48
+ const { error: refused } = JSON.parse(error.text);
49
+ return refused?.code === 'expired' && typeof refused.authority === 'string' ? refused.authority : undefined;
50
+ }
51
+ catch {
52
+ return undefined;
53
+ }
54
+ }
36
55
  function covers(path, prefix) {
37
56
  return prefix === '' || path === prefix || path.startsWith(`${prefix}/`);
38
57
  }
@@ -41,11 +60,11 @@ function sidecar(path) {
41
60
  segments.splice(-1, 0, PROVENANCE);
42
61
  return segments.join('/');
43
62
  }
44
- function rootOf(copy) {
45
- return copy.private ? '/private' : `/rooms/${copy.room_handle}`;
63
+ function rootOf(handle) {
64
+ return `/rooms/${handle}`;
46
65
  }
47
66
  function relative(path) {
48
- return path.replace(/^\/private\//, '').replace(/^\/rooms\/[^/]+\//, '');
67
+ return path.replace(/^\/rooms\/[^/]+\//, '');
49
68
  }
50
69
  function declaredWritable(path, copy) {
51
70
  const deepest = (scopes) => Math.max(-1, ...scopes.filter((scope) => covers(path, scope)).map((scope) => scope.length));
@@ -153,19 +172,18 @@ export class LocalCopies {
153
172
  house;
154
173
  copies = new Map();
155
174
  queues = new Map();
156
- delivering = new Set();
157
- selecting = Promise.resolve();
175
+ answering = Promise.resolve();
158
176
  constructor(house) {
159
177
  this.house = house;
160
178
  this.owner = {
161
179
  key: 'owner',
162
180
  submit: async (operation, changes) => {
163
181
  try {
164
- const outcome = await house.post('/kit/door/batch', { operation_id: operation, changes });
182
+ const outcome = await retrying(() => house.post('/kit/door/batch', { operation_id: operation, changes }));
165
183
  return { accepted: outcome.contents };
166
184
  }
167
185
  catch (error) {
168
- if (!(error instanceof HouseRefusal) || error.status >= 500)
186
+ if (!finalRefusal(error))
169
187
  throw error;
170
188
  return {
171
189
  refused: refusalOf(error.text) ?? { code: '', text: `House refused it with ${error.status}`, conflicts: [] },
@@ -177,7 +195,7 @@ export class LocalCopies {
177
195
  async load() {
178
196
  const home = join(kitHome(), 'local-copies');
179
197
  const names = (await readdir(join(home, 'rooms')).catch(() => [])).filter((name) => name.endsWith('.json'));
180
- for (const path of [...names.map((name) => join(home, 'rooms', name)), join(home, 'private.json')]) {
198
+ for (const path of names.map((name) => join(home, 'rooms', name))) {
181
199
  const text = await readFile(path, 'utf8').catch(() => null);
182
200
  if (text === null)
183
201
  continue;
@@ -186,47 +204,68 @@ export class LocalCopies {
186
204
  }
187
205
  }
188
206
  async mapping() {
189
- await this.selecting;
207
+ await this.answering;
190
208
  return [...this.copies.values()]
191
209
  .filter((copy) => copy.active)
192
- .map((copy) => `${rootOf(copy)}: ${copy.repository}`)
210
+ .map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
193
211
  .sort();
194
212
  }
195
- select() {
196
- const selected = this.selecting.then(() => this.reselect());
197
- this.selecting = selected.catch(() => undefined);
198
- return selected;
199
- }
200
- async reselect() {
201
- const { local_copy: chosen } = await this.house.post('/kit/local-copy/selection', {});
202
- const selected = [
203
- ...(chosen?.rooms ?? [])
204
- .filter((room) => room.room_handle !== null)
205
- .map((room) => ({ key: `rooms/${room.room_ref}`, private: false, handle: room.room_handle })),
206
- ...(chosen?.private === true ? [{ key: 'private', private: true, handle: null }] : []),
207
- ];
208
- const keys = new Set(selected.map((scope) => scope.key));
209
- const work = [];
210
- for (const key of this.copies.keys()) {
211
- if (keys.has(key))
212
- continue;
213
- work.push(this.exclusive(key, async () => {
214
- const copy = this.copies.get(key);
215
- if (copy.active)
216
- await this.save({ ...copy, active: false });
217
- }));
218
- }
219
- for (const scope of selected)
220
- work.push(this.exclusive(scope.key, () => this.apply(scope)).catch(logged));
221
- await Promise.all(work);
213
+ async connected() {
214
+ const copied = await this.answered(() => this.fetched(false));
215
+ if (copied.refused)
216
+ logged(copied.text);
222
217
  }
223
- entries(frame) {
224
- const copy = [...this.copies.values()].find((held) => held.active && held.room_ref === frame.authority);
225
- if (copy === undefined)
226
- return;
227
- if (copy.log_epoch === frame.log_epoch && BigInt(frame.position) <= BigInt(copy.position))
228
- return;
229
- this.deliver(copy.key);
218
+ fetch() {
219
+ return this.answered(() => this.fetched(true));
220
+ }
221
+ answered(work) {
222
+ const answered = this.answering.then(work);
223
+ this.answering = answered.catch(() => undefined);
224
+ return answered;
225
+ }
226
+ async fetched(refreshing) {
227
+ const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', {});
228
+ const synced = new Map(answer.rooms.map((room) => [`rooms/${room.room_ref}`, room.room_handle]));
229
+ return this.holding([...new Set([...this.copies.keys(), ...synced.keys()])].sort(), async () => {
230
+ const said = [];
231
+ let refused = false;
232
+ for (const copy of this.copies.values()) {
233
+ if (synced.has(copy.key) || !copy.active)
234
+ continue;
235
+ await this.save({ ...copy, active: false });
236
+ said.push(`${rootOf(copy.room_handle)}: no longer synced to this Environment; its files stay`);
237
+ }
238
+ const current = [];
239
+ for (const [key, handle] of synced) {
240
+ try {
241
+ const held = this.copies.get(key);
242
+ if (held === undefined || !(await exists(held.repository))) {
243
+ said.push(await this.materialize(key, handle));
244
+ continue;
245
+ }
246
+ const copy = held.room_handle === handle ? held : await this.save({ ...held, room_handle: handle });
247
+ if (copy.active)
248
+ current.push(copy);
249
+ else
250
+ said.push(...moved(copy, await this.save(await this.received({ ...copy, active: true }))));
251
+ }
252
+ catch (error) {
253
+ refused = true;
254
+ said.push(`${rootOf(handle)}: ${causeOf(error)}`);
255
+ }
256
+ }
257
+ if (refreshing && current.length > 0) {
258
+ try {
259
+ const refreshed = await this.refreshed(current);
260
+ current.forEach((copy, index) => said.push(...moved(copy, refreshed[index])));
261
+ }
262
+ catch (error) {
263
+ refused = true;
264
+ said.push(error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error));
265
+ }
266
+ }
267
+ return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
268
+ });
230
269
  }
231
270
  async observed(report) {
232
271
  const key = await this.keyAt(dirname(report.common));
@@ -276,19 +315,12 @@ export class LocalCopies {
276
315
  this.queues.set(key, queued.catch(() => undefined));
277
316
  return queued;
278
317
  }
279
- deliver(key) {
280
- if (this.delivering.has(key))
281
- return;
282
- this.delivering.add(key);
283
- void this.exclusive(key, async () => {
284
- this.delivering.delete(key);
285
- const copy = this.copies.get(key);
286
- if (copy.active)
287
- await this.save(await this.caughtUp(copy));
288
- }).catch(logged);
318
+ holding(keys, work) {
319
+ const [key, ...rest] = keys;
320
+ return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
289
321
  }
290
322
  async save(copy) {
291
- const path = join(kitHome(), 'local-copies', copy.private ? 'private.json' : `${copy.key}.json`);
323
+ const path = join(kitHome(), 'local-copies', `${copy.key}.json`);
292
324
  await mkdir(dirname(path), { recursive: true, mode: 0o700 });
293
325
  const temporary = `${path}.${process.pid}`;
294
326
  await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
@@ -296,8 +328,8 @@ export class LocalCopies {
296
328
  this.copies.set(copy.key, copy);
297
329
  return copy;
298
330
  }
299
- async read(selected) {
300
- const prefixes = [rootOf(selected)];
331
+ async read(handle) {
332
+ const prefixes = [rootOf(handle)];
301
333
  const discovery = await this.house.post('/kit/door/discover', { prefixes });
302
334
  let bundle = await this.house.post('/kit/door/bootstrap', { prefixes });
303
335
  if ('transfer' in bundle)
@@ -331,46 +363,13 @@ export class LocalCopies {
331
363
  }
332
364
  return read;
333
365
  }
334
- async apply(scope) {
335
- let copy = this.copies.get(scope.key);
336
- if (copy === undefined || !(await exists(copy.repository))) {
337
- await this.materialize(scope);
338
- return;
339
- }
340
- if (copy.room_handle !== scope.handle)
341
- copy = await this.save({ ...copy, room_handle: scope.handle });
342
- if (!copy.active) {
343
- await this.save(await this.received({ ...copy, active: true }));
344
- return;
345
- }
346
- const discovery = await this.house.post('/kit/door/discover', {
347
- prefixes: [rootOf(copy)],
348
- });
349
- const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
350
- if (authority === undefined)
351
- return;
352
- if (authority.managed_replication !== 'admitted') {
353
- logged(`operation_denied: ${rootOf(copy)} is not synchronized to this Environment`);
354
- return;
355
- }
356
- if (JSON.stringify([authority.operations.write, authority.protected_paths]) !==
357
- JSON.stringify([copy.write, copy.protected])) {
358
- await this.save(await this.received(copy));
359
- return;
360
- }
361
- await this.save(await this.caughtUp(copy));
362
- }
363
- async materialize(scope) {
364
- const read = await this.read({ private: scope.private, room_handle: scope.handle });
365
- if (read === null) {
366
- logged(`House serves nothing of ${rootOf({ private: scope.private, room_handle: scope.handle })} to copy`);
367
- return;
368
- }
369
- const repository = scope.private ? join(localCopies(), 'private') : join(localCopies(), 'rooms', read.anchor);
370
- if (await exists(repository)) {
371
- logged(`${repository} already exists and House Kit leaves it as it is`);
372
- return;
373
- }
366
+ async materialize(key, handle) {
367
+ const read = await this.read(handle);
368
+ if (read === null)
369
+ throw new Error('House serves nothing of it to copy');
370
+ const repository = join(localCopies(), 'rooms', read.anchor);
371
+ if (await exists(repository))
372
+ throw new Error(`${repository} already exists and House Kit leaves it as it is`);
374
373
  const staging = join(dirname(repository), `.${randomUUID()}`);
375
374
  await mkdir(staging, { recursive: true });
376
375
  let house;
@@ -378,7 +377,7 @@ export class LocalCopies {
378
377
  try {
379
378
  await git(staging, ['init', '-q', '-b', BRANCH]);
380
379
  house = await importCommits(staging, [
381
- { message: `House ${rootOf({ private: scope.private, room_handle: scope.handle })}`, from: null, files: read.writable },
380
+ { message: `House ${rootOf(handle)}`, from: null, files: read.writable },
382
381
  ]);
383
382
  await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
384
383
  await git(staging, ['reset', '-q', '--hard']);
@@ -391,10 +390,9 @@ export class LocalCopies {
391
390
  throw error;
392
391
  }
393
392
  await this.save({
394
- key: scope.key,
395
- private: scope.private,
393
+ key,
396
394
  room_ref: read.anchor,
397
- room_handle: scope.handle,
395
+ room_handle: handle,
398
396
  repository,
399
397
  active: true,
400
398
  house,
@@ -406,13 +404,14 @@ export class LocalCopies {
406
404
  manifests: { [house]: { parent: null, files: read.revisions } },
407
405
  moves: [],
408
406
  });
407
+ return `${rootOf(handle)}: copied to ${repository}`;
409
408
  }
410
409
  async received(copy) {
411
- const read = await this.read(copy);
410
+ const read = await this.read(copy.room_handle);
412
411
  if (read === null)
413
412
  return copy;
414
413
  const house = await importCommits(copy.repository, [
415
- { message: `House ${rootOf(copy)} at position ${read.position}`, from: copy.house, replaces: true, files: read.writable },
414
+ { message: `House ${rootOf(copy.room_handle)} at position ${read.position}`, from: copy.house, replaces: true, files: read.writable },
416
415
  ]);
417
416
  const removed = copy.read_only.filter((path) => !read.readOnly.has(path));
418
417
  const readOnly = await readOnlyPlaced(copy.repository, copy.read_only, read.readOnly, removed);
@@ -429,30 +428,52 @@ export class LocalCopies {
429
428
  await ignoreRules(copy.repository, [...read.writable.keys()], next);
430
429
  return next;
431
430
  }
432
- async caughtUp(start) {
433
- let copy = start;
434
- for (;;) {
431
+ async refreshed(copies) {
432
+ const discovery = await this.house.post('/kit/door/discover', {
433
+ prefixes: copies.map((copy) => rootOf(copy.room_handle)),
434
+ });
435
+ const regranted = new Map();
436
+ for (const copy of copies) {
437
+ const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
438
+ if (authority !== undefined &&
439
+ JSON.stringify([authority.operations.write, authority.protected_paths]) !== JSON.stringify([copy.write, copy.protected])) {
440
+ regranted.set(copy.key, await this.save(await this.received(copy)));
441
+ }
442
+ }
443
+ const caught = await this.caughtUp(copies.filter((copy) => !regranted.has(copy.key)), false);
444
+ return copies.map((copy) => regranted.get(copy.key) ?? caught.find((held) => held.key === copy.key));
445
+ }
446
+ async caughtUp(copies, whole) {
447
+ const caught = new Map(copies.map((copy) => [copy.key, copy]));
448
+ let asking = [...copies];
449
+ while (asking.length > 0) {
435
450
  let answer;
436
451
  try {
437
452
  answer = await this.house.post('/kit/door/enumerate', {
438
- prefixes: [rootOf(copy)],
439
- positions: [{ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch }],
453
+ prefixes: asking.map((copy) => rootOf(copy.room_handle)),
454
+ positions: asking.map((copy) => ({ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch })),
440
455
  });
441
456
  }
442
457
  catch (error) {
443
- const code = error instanceof HouseRefusal ? (refusalOf(error.text)?.code ?? null) : null;
444
- if (code === 'position_expired')
445
- return this.received(copy);
446
- if (code !== 'replica_behind')
458
+ const expired = asking.find((copy) => copy.room_ref === expiredAuthority(error));
459
+ if (expired === undefined)
447
460
  throw error;
448
- setTimeout(() => this.deliver(copy.key), REPLICA_RETRY_MS).unref();
449
- return copy;
461
+ caught.set(expired.key, await this.save(await this.received(expired)));
462
+ asking = asking.filter((copy) => copy !== expired);
463
+ continue;
464
+ }
465
+ const advanced = [];
466
+ for (const copy of asking) {
467
+ const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
468
+ if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
469
+ continue;
470
+ const next = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
471
+ caught.set(copy.key, next);
472
+ advanced.push(next);
450
473
  }
451
- const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
452
- if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
453
- return copy;
454
- copy = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
474
+ asking = whole ? advanced : [];
455
475
  }
476
+ return copies.map((copy) => caught.get(copy.key));
456
477
  }
457
478
  async incorporated(copy, changes, position, logEpoch) {
458
479
  const houseTree = await tree(copy.repository, copy.house);
@@ -485,7 +506,7 @@ export class LocalCopies {
485
506
  let { house, manifests } = copy;
486
507
  if (files.size > 0) {
487
508
  house = await importCommits(copy.repository, [
488
- { message: `House ${rootOf(copy)} at position ${position}`, from: copy.house, files },
509
+ { message: `House ${rootOf(copy.room_handle)} at position ${position}`, from: copy.house, files },
489
510
  ]);
490
511
  manifests = { ...manifests, [house]: { parent: copy.house, files: revisions } };
491
512
  }
@@ -516,7 +537,7 @@ export class LocalCopies {
516
537
  copy = await this.save(without(copy));
517
538
  }
518
539
  if (!copy.active) {
519
- return { refused: true, text: `${rootOf(copy)} is no longer synced to this Environment, so House takes no push from it` };
540
+ return { refused: true, text: `${rootOf(copy.room_handle)} is no longer synced to this Environment, so House takes no push from it` };
520
541
  }
521
542
  const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
522
543
  const base = listed.find((ancestor) => ancestor in copy.manifests);
@@ -550,12 +571,18 @@ export class LocalCopies {
550
571
  return this.settle(submitted, caller);
551
572
  }
552
573
  async derived(copy, base, commit) {
553
- const root = rootOf(copy);
574
+ const root = rootOf(copy.room_handle);
554
575
  const before = await tree(copy.repository, base);
555
576
  const after = await tree(copy.repository, commit);
556
577
  const moved = movedPaths(copy.moves, before, after);
557
578
  const arrived = new Set(moved.values());
558
- const text = (entry, path) => blobText(copy.repository, entry, path);
579
+ const binary = [];
580
+ for (const [path, entry] of after)
581
+ if (before.get(path) !== entry && !(await isText(copy.repository, entry)))
582
+ binary.push(path);
583
+ if (binary.length > 0)
584
+ throw new Error(textOnly(binary));
585
+ const text = (entry) => blobText(copy.repository, entry);
559
586
  const changes = [];
560
587
  for (const path of [...new Set([...before.keys(), ...after.keys()])].sort()) {
561
588
  if (arrived.has(path))
@@ -568,7 +595,7 @@ export class LocalCopies {
568
595
  changes.push({ op: 'rename', path: `${root}/${path}`, to: `${root}/${to}`, base: known });
569
596
  const moving = after.get(to);
570
597
  if (moving !== prior) {
571
- changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving, to) });
598
+ changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving) });
572
599
  }
573
600
  continue;
574
601
  }
@@ -577,9 +604,9 @@ export class LocalCopies {
577
604
  if (next === undefined)
578
605
  changes.push({ op: 'remove', path: `${root}/${path}`, base: known });
579
606
  else if (prior === undefined)
580
- changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next, path) });
607
+ changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next) });
581
608
  else
582
- changes.push(...changed(`${root}/${path}`, path, known, await text(prior, path), await text(next, path)));
609
+ changes.push(...changed(`${root}/${path}`, path, known, await text(prior), await text(next)));
583
610
  }
584
611
  return changes;
585
612
  }
@@ -606,12 +633,12 @@ export class LocalCopies {
606
633
  const { upload, ...unstaged } = copy.push;
607
634
  return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
608
635
  }
609
- if (outcome.refused.code === 'operation_expired')
636
+ if (outcome.refused.code === 'expired')
610
637
  return this.recovered(copy);
611
638
  const rebased = rebaseFixes(outcome.refused);
612
639
  let settled = await this.save(without(copy));
613
640
  try {
614
- settled = await this.save(await this.caughtUp(settled));
641
+ settled = (await this.caughtUp([settled], true))[0];
615
642
  }
616
643
  catch {
617
644
  return {
@@ -630,9 +657,9 @@ export class LocalCopies {
630
657
  }
631
658
  async recovered(copy) {
632
659
  const push = copy.push;
633
- const current = await this.save(await this.caughtUp(copy));
660
+ const current = (await this.caughtUp([copy], true))[0];
634
661
  const house = await tree(copy.repository, current.house);
635
- const root = rootOf(current);
662
+ const root = rootOf(current.room_handle);
636
663
  if (!(await this.landed(copy.repository, push.changes, house, await tree(copy.repository, push.commit)))) {
637
664
  await this.save(without(current));
638
665
  return {
@@ -646,7 +673,7 @@ export class LocalCopies {
646
673
  written.push({
647
674
  path: `${root}/${path}`,
648
675
  revision: entry === undefined ? null : (revisionAt(current, current.house, path) ?? null),
649
- content: entry === undefined ? null : await blobText(copy.repository, entry, path),
676
+ content: entry === undefined ? null : await blobText(copy.repository, entry),
650
677
  });
651
678
  }
652
679
  return this.finished(await this.save({ ...current, push: { ...push, state: 'accepted', written } }));
@@ -674,7 +701,7 @@ export class LocalCopies {
674
701
  const push = copy.push;
675
702
  let written = push.written;
676
703
  if (push.changes.some((change) => change.op === 'rename')) {
677
- copy = await this.save(await this.caughtUp(copy));
704
+ copy = (await this.caughtUp([copy], true))[0];
678
705
  const house = await tree(copy.repository, copy.house);
679
706
  const sent = await tree(copy.repository, push.house);
680
707
  if (push.changes.some((change) => change.op === 'rename' && !house.has(relative(change.to)))) {
@@ -688,9 +715,9 @@ export class LocalCopies {
688
715
  written = [
689
716
  ...written,
690
717
  {
691
- path: `${rootOf(copy)}/${path}`,
718
+ path: `${rootOf(copy.room_handle)}/${path}`,
692
719
  revision: entry === undefined ? null : (revisionAt(copy, copy.house, path) ?? null),
693
- content: entry === undefined ? null : await blobText(copy.repository, entry, path),
720
+ content: entry === undefined ? null : await blobText(copy.repository, entry),
694
721
  },
695
722
  ];
696
723
  }
@@ -708,7 +735,7 @@ export class LocalCopies {
708
735
  }
709
736
  const edits = new Map([...files].filter(([path, content]) => (content === null ? selected.has(path) : selected.get(path) !== blobId(content))));
710
737
  const received = [...files.keys()].filter((path) => revisionAt(copy, copy.house, path) === revisionAt(copy, push.house, path));
711
- const accepted = `House ${rootOf(copy)} with ${push.commit} accepted`;
738
+ const accepted = `House ${rootOf(copy.room_handle)} with ${push.commit} accepted`;
712
739
  const house = await importCommits(copy.repository, [
713
740
  ...(edits.size === 0 ? [] : [{ message: accepted, from: push.commit, files: edits }]),
714
741
  {
@@ -735,7 +762,7 @@ export class LocalCopies {
735
762
  async landed(repository, changes, house, selected) {
736
763
  const text = async (path) => {
737
764
  const entry = house.get(relative(path));
738
- return entry === undefined ? undefined : blobText(repository, entry, path);
765
+ return entry === undefined ? undefined : blobText(repository, entry);
739
766
  };
740
767
  for (const change of changes) {
741
768
  const current = await text(change.path);
package/dist/end.js ADDED
@@ -0,0 +1,22 @@
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(house) {
17
+ end(`This Kit is too old for House. Update it: ${installCommand(house)}`);
18
+ }
19
+ export async function signInAgain(rejected) {
20
+ await forgetRejected(rejected);
21
+ restart('sign in again');
22
+ }
@@ -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
+ });