@agentshouse/kit 0.2.1-alpha.9 → 0.2.1-c073f4e9391b2

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 (48) hide show
  1. package/README.md +44 -26
  2. package/dist/abort.js +9 -0
  3. package/dist/acp.js +18 -75
  4. package/dist/agents.js +266 -53
  5. package/dist/api.js +40 -4
  6. package/dist/app-git.js +64 -0
  7. package/dist/bootstrap.json +10 -14
  8. package/dist/bridge.js +89 -71
  9. package/dist/chatgpt.js +25 -0
  10. package/dist/clis-main.js +23 -0
  11. package/dist/clis.js +203 -71
  12. package/dist/codex-main.js +58 -0
  13. package/dist/conversations.js +328 -199
  14. package/dist/copies.js +188 -161
  15. package/dist/end.js +22 -0
  16. package/dist/enrol-main.js +7 -1
  17. package/dist/events.js +521 -0
  18. package/dist/files.js +16 -1
  19. package/dist/git-main.js +2 -2
  20. package/dist/git.js +6 -2
  21. package/dist/home.js +72 -5
  22. package/dist/house-main.js +181 -57
  23. package/dist/instructions.js +16 -13
  24. package/dist/kit-main.js +55 -4
  25. package/dist/login.js +108 -97
  26. package/dist/logout.js +21 -0
  27. package/dist/mcp.js +39 -0
  28. package/dist/operation.js +7 -4
  29. package/dist/parts.js +82 -0
  30. package/dist/placement.js +48 -0
  31. package/dist/refusals.js +53 -0
  32. package/dist/relay.js +20 -0
  33. package/dist/resident.js +70 -34
  34. package/dist/scope.js +203 -0
  35. package/dist/secret-input.js +7 -2
  36. package/dist/service-main.js +9 -0
  37. package/dist/service.js +33 -0
  38. package/dist/shell.js +3 -0
  39. package/dist/sign-in.js +48 -11
  40. package/dist/skills.js +5 -10
  41. package/dist/stream.js +37 -7
  42. package/dist/transcription.js +170 -0
  43. package/dist/update.js +140 -0
  44. package/dist/wire.js +6 -0
  45. package/dist/work.js +348 -0
  46. package/how-we-work.md +10 -12
  47. package/package.json +4 -7
  48. package/dist/blocks.js +0 -51
package/dist/copies.js CHANGED
@@ -2,15 +2,15 @@ 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, revision, run, succeeded, tree, WORKING_COPIES, } from './git.js';
5
+ import { finalRefusal, HouseRefusal, retrying } from './api.js';
6
+ import { blobId, blobText, git, 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
+ import { refusalOf } from './refusals.js';
9
10
  import { changed } from './translate.js';
11
+ import { relayed } from './relay.js';
10
12
  const BRANCH = 'main';
11
- const ROOM_HOMES = ['capture', 'library', 'collaboration'];
12
13
  const PROVENANCE = '_provenance';
13
- const REPLICA_RETRY_MS = 30_000;
14
14
  const IDENTITY = { GIT_COMMITTER_NAME: 'House Kit', GIT_COMMITTER_EMAIL: 'house-kit@localhost' };
15
15
  function causeOf(error) {
16
16
  return error instanceof Error ? error.message : String(error);
@@ -18,17 +18,36 @@ function causeOf(error) {
18
18
  function logged(error) {
19
19
  process.stderr.write(`kit: ${causeOf(error)}\n`);
20
20
  }
21
- export function refusalCode(text) {
21
+ const REBASED = new Set([
22
+ 'field_base_stale',
23
+ 'section_base_stale',
24
+ 'preamble_base_stale',
25
+ 'base_revision_stale',
26
+ 'path_exists',
27
+ 'path_absent',
28
+ ]);
29
+ function rebaseFixes(refusal) {
30
+ return refusal.code === 'edit_conflict' && refusal.conflicts.some((code) => REBASED.has(code));
31
+ }
32
+ function short(commit) {
33
+ return commit.slice(0, 12);
34
+ }
35
+ function moved(before, after) {
36
+ if (before.house !== after.house)
37
+ return [`${rootOf(after.room_handle)}: ${HOUSE_REF} is at ${short(after.house)}`];
38
+ return before.position === after.position ? [] : [`${rootOf(after.room_handle)}: read-only files refreshed`];
39
+ }
40
+ function expiredAuthority(error) {
41
+ if (!(error instanceof HouseRefusal))
42
+ return undefined;
22
43
  try {
23
- return String(JSON.parse(text).error.code);
44
+ const { error: refused } = JSON.parse(error.text);
45
+ return refused?.code === 'expired' && typeof refused.authority === 'string' ? refused.authority : undefined;
24
46
  }
25
47
  catch {
26
- return text.split(':')[0].trim();
48
+ return undefined;
27
49
  }
28
50
  }
29
- function short(commit) {
30
- return commit.slice(0, 12);
31
- }
32
51
  function covers(path, prefix) {
33
52
  return prefix === '' || path === prefix || path.startsWith(`${prefix}/`);
34
53
  }
@@ -37,14 +56,11 @@ function sidecar(path) {
37
56
  segments.splice(-1, 0, PROVENANCE);
38
57
  return segments.join('/');
39
58
  }
40
- function rootOf(copy) {
41
- return copy.private ? '/private' : `/rooms/${copy.room_handle}`;
59
+ function rootOf(handle) {
60
+ return `/rooms/${handle}`;
42
61
  }
43
62
  function relative(path) {
44
- return path.replace(/^\/private\//, '').replace(/^\/rooms\/[^/]+\//, '');
45
- }
46
- function prefixesOf(selected) {
47
- return selected.private ? ['/private'] : ROOM_HOMES.map((home) => `${rootOf(selected)}/${home}`);
63
+ return path.replace(/^\/rooms\/[^/]+\//, '');
48
64
  }
49
65
  function declaredWritable(path, copy) {
50
66
  const deepest = (scopes) => Math.max(-1, ...scopes.filter((scope) => covers(path, scope)).map((scope) => scope.length));
@@ -147,34 +163,35 @@ function movedPaths(moves, before, after) {
147
163
  .filter((pair) => pairs.filter((other) => other.from === pair.from || other.to === pair.to).length === 1)
148
164
  .map(({ from, to }) => [from, to]));
149
165
  }
150
- export class WorkingCopies {
166
+ export class LocalCopies {
151
167
  owner;
152
168
  house;
153
169
  copies = new Map();
154
170
  queues = new Map();
155
- delivering = new Set();
156
- selecting = Promise.resolve();
171
+ answering = Promise.resolve();
157
172
  constructor(house) {
158
173
  this.house = house;
159
174
  this.owner = {
160
175
  key: 'owner',
161
176
  submit: async (operation, changes) => {
162
177
  try {
163
- const outcome = await house.post('/kit/door/batch', { operation_id: operation, changes });
178
+ const outcome = await retrying(() => house.post('/kit/door/batch', { operation_id: operation, changes }));
164
179
  return { accepted: outcome.contents };
165
180
  }
166
181
  catch (error) {
167
- if (!(error instanceof HouseRefusal) || error.status >= 500)
182
+ if (!finalRefusal(error))
168
183
  throw error;
169
- return { refused: { code: refusalCode(error.text), text: error.message } };
184
+ return {
185
+ refused: refusalOf(error.text) ?? { code: '', text: `House refused it with ${error.status}`, conflicts: [] },
186
+ };
170
187
  }
171
188
  },
172
189
  };
173
190
  }
174
191
  async load() {
175
- const home = join(kitHome(), 'working-copies');
192
+ const home = join(kitHome(), 'local-copies');
176
193
  const names = (await readdir(join(home, 'rooms')).catch(() => [])).filter((name) => name.endsWith('.json'));
177
- for (const path of [...names.map((name) => join(home, 'rooms', name)), join(home, 'private.json')]) {
194
+ for (const path of names.map((name) => join(home, 'rooms', name))) {
178
195
  const text = await readFile(path, 'utf8').catch(() => null);
179
196
  if (text === null)
180
197
  continue;
@@ -183,47 +200,68 @@ export class WorkingCopies {
183
200
  }
184
201
  }
185
202
  async mapping() {
186
- await this.selecting;
203
+ await this.answering;
187
204
  return [...this.copies.values()]
188
205
  .filter((copy) => copy.active)
189
- .map((copy) => `${rootOf(copy)}: ${copy.repository}`)
206
+ .map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
190
207
  .sort();
191
208
  }
192
- select() {
193
- const selected = this.selecting.then(() => this.reselect());
194
- this.selecting = selected.catch(() => undefined);
195
- return selected;
196
- }
197
- async reselect() {
198
- const { working_copy: chosen } = await this.house.post('/kit/working-copy/selection', {});
199
- const selected = [
200
- ...(chosen?.rooms ?? [])
201
- .filter((room) => room.room_handle !== null)
202
- .map((room) => ({ key: `rooms/${room.room_ref}`, private: false, handle: room.room_handle })),
203
- ...(chosen?.private === true ? [{ key: 'private', private: true, handle: null }] : []),
204
- ];
205
- const keys = new Set(selected.map((scope) => scope.key));
206
- const work = [];
207
- for (const key of this.copies.keys()) {
208
- if (keys.has(key))
209
- continue;
210
- work.push(this.exclusive(key, async () => {
211
- const copy = this.copies.get(key);
212
- if (copy.active)
213
- await this.save({ ...copy, active: false });
214
- }));
215
- }
216
- for (const scope of selected)
217
- work.push(this.exclusive(scope.key, () => this.apply(scope)).catch(logged));
218
- await Promise.all(work);
219
- }
220
- entries(frame) {
221
- const copy = [...this.copies.values()].find((held) => held.active && held.room_ref === frame.authority);
222
- if (copy === undefined)
223
- return;
224
- if (copy.log_epoch === frame.log_epoch && BigInt(frame.position) <= BigInt(copy.position))
225
- return;
226
- this.deliver(copy.key);
209
+ async connected() {
210
+ const copied = await this.answered(() => this.fetched(false));
211
+ if (copied.refused)
212
+ logged(copied.text);
213
+ }
214
+ fetch() {
215
+ return this.answered(() => this.fetched(true));
216
+ }
217
+ answered(work) {
218
+ const answered = this.answering.then(work);
219
+ this.answering = answered.catch(() => undefined);
220
+ return answered;
221
+ }
222
+ async fetched(refreshing) {
223
+ const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', {});
224
+ const synced = new Map(answer.rooms.map((room) => [`rooms/${room.room_ref}`, room.room_handle]));
225
+ return this.holding([...new Set([...this.copies.keys(), ...synced.keys()])].sort(), async () => {
226
+ const said = [];
227
+ let refused = false;
228
+ for (const copy of this.copies.values()) {
229
+ if (synced.has(copy.key) || !copy.active)
230
+ continue;
231
+ await this.save({ ...copy, active: false });
232
+ said.push(`${rootOf(copy.room_handle)}: no longer synced to this Environment; its files stay`);
233
+ }
234
+ const current = [];
235
+ for (const [key, handle] of synced) {
236
+ try {
237
+ const held = this.copies.get(key);
238
+ if (held === undefined || !(await exists(held.repository))) {
239
+ said.push(await this.materialize(key, handle));
240
+ continue;
241
+ }
242
+ const copy = held.room_handle === handle ? held : await this.save({ ...held, room_handle: handle });
243
+ if (copy.active)
244
+ current.push(copy);
245
+ else
246
+ said.push(...moved(copy, await this.save(await this.received({ ...copy, active: true }))));
247
+ }
248
+ catch (error) {
249
+ refused = true;
250
+ said.push(`${rootOf(handle)}: ${causeOf(error)}`);
251
+ }
252
+ }
253
+ if (refreshing && current.length > 0) {
254
+ try {
255
+ const refreshed = await this.refreshed(current);
256
+ current.forEach((copy, index) => said.push(...moved(copy, refreshed[index])));
257
+ }
258
+ catch (error) {
259
+ refused = true;
260
+ said.push(error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error));
261
+ }
262
+ }
263
+ return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
264
+ });
227
265
  }
228
266
  async observed(report) {
229
267
  const key = await this.keyAt(dirname(report.common));
@@ -253,11 +291,11 @@ export class WorkingCopies {
253
291
  async push(cwd, selector, caller) {
254
292
  const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
255
293
  if (located.status !== 0)
256
- return { refused: true, text: `${cwd} is not in a Git repository` };
294
+ return { refused: true, text: `${cwd} is not a House Local copy; run house push inside one` };
257
295
  const [common, worktree] = located.stdout.toString('utf8').split('\n');
258
296
  const key = await this.keyAt(dirname(common));
259
297
  if (key === undefined)
260
- return { refused: true, text: `${worktree} is not a House working copy` };
298
+ return { refused: true, text: `${worktree} is not a House Local copy; run house push inside one` };
261
299
  return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
262
300
  }
263
301
  async keyAt(repository) {
@@ -273,19 +311,12 @@ export class WorkingCopies {
273
311
  this.queues.set(key, queued.catch(() => undefined));
274
312
  return queued;
275
313
  }
276
- deliver(key) {
277
- if (this.delivering.has(key))
278
- return;
279
- this.delivering.add(key);
280
- void this.exclusive(key, async () => {
281
- this.delivering.delete(key);
282
- const copy = this.copies.get(key);
283
- if (copy.active)
284
- await this.save(await this.caughtUp(copy));
285
- }).catch(logged);
314
+ holding(keys, work) {
315
+ const [key, ...rest] = keys;
316
+ return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
286
317
  }
287
318
  async save(copy) {
288
- const path = join(kitHome(), 'working-copies', copy.private ? 'private.json' : `${copy.key}.json`);
319
+ const path = join(kitHome(), 'local-copies', `${copy.key}.json`);
289
320
  await mkdir(dirname(path), { recursive: true, mode: 0o700 });
290
321
  const temporary = `${path}.${process.pid}`;
291
322
  await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
@@ -293,12 +324,12 @@ export class WorkingCopies {
293
324
  this.copies.set(copy.key, copy);
294
325
  return copy;
295
326
  }
296
- async read(selected) {
297
- const prefixes = prefixesOf(selected);
327
+ async read(handle) {
328
+ const prefixes = [rootOf(handle)];
298
329
  const discovery = await this.house.post('/kit/door/discover', { prefixes });
299
330
  let bundle = await this.house.post('/kit/door/bootstrap', { prefixes });
300
331
  if ('transfer' in bundle)
301
- bundle = (await (await fetch(bundle.transfer.url)).json());
332
+ bundle = (await (await fetch(relayed(bundle.transfer.url))).json());
302
333
  const [view] = bundle.scopes;
303
334
  if (view === undefined)
304
335
  return null;
@@ -328,46 +359,13 @@ export class WorkingCopies {
328
359
  }
329
360
  return read;
330
361
  }
331
- async apply(scope) {
332
- let copy = this.copies.get(scope.key);
333
- if (copy === undefined || !(await exists(copy.repository))) {
334
- await this.materialize(scope);
335
- return;
336
- }
337
- if (copy.room_handle !== scope.handle)
338
- copy = await this.save({ ...copy, room_handle: scope.handle });
339
- if (!copy.active) {
340
- await this.save(await this.received({ ...copy, active: true }));
341
- return;
342
- }
343
- const discovery = await this.house.post('/kit/door/discover', {
344
- prefixes: prefixesOf(copy),
345
- });
346
- const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
347
- if (authority === undefined)
348
- return;
349
- if (authority.managed_replication !== 'admitted') {
350
- logged(`operation_denied: ${rootOf(copy)} is not synchronized to this Environment`);
351
- return;
352
- }
353
- if (JSON.stringify([authority.operations.write, authority.protected_paths]) !==
354
- JSON.stringify([copy.write, copy.protected])) {
355
- await this.save(await this.received(copy));
356
- return;
357
- }
358
- await this.save(await this.caughtUp(copy));
359
- }
360
- async materialize(scope) {
361
- const read = await this.read({ private: scope.private, room_handle: scope.handle });
362
- if (read === null) {
363
- logged(`House serves nothing of ${rootOf({ private: scope.private, room_handle: scope.handle })} to copy`);
364
- return;
365
- }
366
- const repository = scope.private ? join(WORKING_COPIES, 'private') : join(WORKING_COPIES, 'rooms', read.anchor);
367
- if (await exists(repository)) {
368
- logged(`${repository} already exists and House Kit leaves it as it is`);
369
- return;
370
- }
362
+ async materialize(key, handle) {
363
+ const read = await this.read(handle);
364
+ if (read === null)
365
+ throw new Error('House serves nothing of it to copy');
366
+ const repository = join(localCopies(), 'rooms', read.anchor);
367
+ if (await exists(repository))
368
+ throw new Error(`${repository} already exists and House Kit leaves it as it is`);
371
369
  const staging = join(dirname(repository), `.${randomUUID()}`);
372
370
  await mkdir(staging, { recursive: true });
373
371
  let house;
@@ -375,7 +373,7 @@ export class WorkingCopies {
375
373
  try {
376
374
  await git(staging, ['init', '-q', '-b', BRANCH]);
377
375
  house = await importCommits(staging, [
378
- { message: `House ${rootOf({ private: scope.private, room_handle: scope.handle })}`, from: null, files: read.writable },
376
+ { message: `House ${rootOf(handle)}`, from: null, files: read.writable },
379
377
  ]);
380
378
  await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
381
379
  await git(staging, ['reset', '-q', '--hard']);
@@ -388,10 +386,9 @@ export class WorkingCopies {
388
386
  throw error;
389
387
  }
390
388
  await this.save({
391
- key: scope.key,
392
- private: scope.private,
389
+ key,
393
390
  room_ref: read.anchor,
394
- room_handle: scope.handle,
391
+ room_handle: handle,
395
392
  repository,
396
393
  active: true,
397
394
  house,
@@ -403,13 +400,14 @@ export class WorkingCopies {
403
400
  manifests: { [house]: { parent: null, files: read.revisions } },
404
401
  moves: [],
405
402
  });
403
+ return `${rootOf(handle)}: copied to ${repository}`;
406
404
  }
407
405
  async received(copy) {
408
- const read = await this.read(copy);
406
+ const read = await this.read(copy.room_handle);
409
407
  if (read === null)
410
408
  return copy;
411
409
  const house = await importCommits(copy.repository, [
412
- { message: `House ${rootOf(copy)} at position ${read.position}`, from: copy.house, replaces: true, files: read.writable },
410
+ { message: `House ${rootOf(copy.room_handle)} at position ${read.position}`, from: copy.house, replaces: true, files: read.writable },
413
411
  ]);
414
412
  const removed = copy.read_only.filter((path) => !read.readOnly.has(path));
415
413
  const readOnly = await readOnlyPlaced(copy.repository, copy.read_only, read.readOnly, removed);
@@ -426,30 +424,52 @@ export class WorkingCopies {
426
424
  await ignoreRules(copy.repository, [...read.writable.keys()], next);
427
425
  return next;
428
426
  }
429
- async caughtUp(start) {
430
- let copy = start;
431
- for (;;) {
427
+ async refreshed(copies) {
428
+ const discovery = await this.house.post('/kit/door/discover', {
429
+ prefixes: copies.map((copy) => rootOf(copy.room_handle)),
430
+ });
431
+ const regranted = new Map();
432
+ for (const copy of copies) {
433
+ const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
434
+ if (authority !== undefined &&
435
+ JSON.stringify([authority.operations.write, authority.protected_paths]) !== JSON.stringify([copy.write, copy.protected])) {
436
+ regranted.set(copy.key, await this.save(await this.received(copy)));
437
+ }
438
+ }
439
+ const caught = await this.caughtUp(copies.filter((copy) => !regranted.has(copy.key)), false);
440
+ return copies.map((copy) => regranted.get(copy.key) ?? caught.find((held) => held.key === copy.key));
441
+ }
442
+ async caughtUp(copies, whole) {
443
+ const caught = new Map(copies.map((copy) => [copy.key, copy]));
444
+ let asking = [...copies];
445
+ while (asking.length > 0) {
432
446
  let answer;
433
447
  try {
434
448
  answer = await this.house.post('/kit/door/enumerate', {
435
- prefixes: prefixesOf(copy),
436
- positions: [{ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch }],
449
+ prefixes: asking.map((copy) => rootOf(copy.room_handle)),
450
+ positions: asking.map((copy) => ({ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch })),
437
451
  });
438
452
  }
439
453
  catch (error) {
440
- const code = error instanceof HouseRefusal ? refusalCode(error.text) : null;
441
- if (code === 'position_expired')
442
- return this.received(copy);
443
- if (code !== 'replica_behind')
454
+ const expired = asking.find((copy) => copy.room_ref === expiredAuthority(error));
455
+ if (expired === undefined)
444
456
  throw error;
445
- setTimeout(() => this.deliver(copy.key), REPLICA_RETRY_MS).unref();
446
- return copy;
457
+ caught.set(expired.key, await this.save(await this.received(expired)));
458
+ asking = asking.filter((copy) => copy !== expired);
459
+ continue;
447
460
  }
448
- const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
449
- if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
450
- return copy;
451
- copy = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
461
+ const advanced = [];
462
+ for (const copy of asking) {
463
+ const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
464
+ if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
465
+ continue;
466
+ const next = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
467
+ caught.set(copy.key, next);
468
+ advanced.push(next);
469
+ }
470
+ asking = whole ? advanced : [];
452
471
  }
472
+ return copies.map((copy) => caught.get(copy.key));
453
473
  }
454
474
  async incorporated(copy, changes, position, logEpoch) {
455
475
  const houseTree = await tree(copy.repository, copy.house);
@@ -482,7 +502,7 @@ export class WorkingCopies {
482
502
  let { house, manifests } = copy;
483
503
  if (files.size > 0) {
484
504
  house = await importCommits(copy.repository, [
485
- { message: `House ${rootOf(copy)} at position ${position}`, from: copy.house, files },
505
+ { message: `House ${rootOf(copy.room_handle)} at position ${position}`, from: copy.house, files },
486
506
  ]);
487
507
  manifests = { ...manifests, [house]: { parent: copy.house, files: revisions } };
488
508
  }
@@ -512,8 +532,9 @@ export class WorkingCopies {
512
532
  }
513
533
  copy = await this.save(without(copy));
514
534
  }
515
- if (!copy.active)
516
- return { refused: true, text: `${rootOf(copy)} is no longer kept in this Environment` };
535
+ if (!copy.active) {
536
+ return { refused: true, text: `${rootOf(copy.room_handle)} is no longer synced to this Environment, so House takes no push from it` };
537
+ }
517
538
  const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
518
539
  const base = listed.find((ancestor) => ancestor in copy.manifests);
519
540
  if (base === undefined) {
@@ -546,7 +567,7 @@ export class WorkingCopies {
546
567
  return this.settle(submitted, caller);
547
568
  }
548
569
  async derived(copy, base, commit) {
549
- const root = rootOf(copy);
570
+ const root = rootOf(copy.room_handle);
550
571
  const before = await tree(copy.repository, base);
551
572
  const after = await tree(copy.repository, commit);
552
573
  const moved = movedPaths(copy.moves, before, after);
@@ -582,9 +603,8 @@ export class WorkingCopies {
582
603
  async settle(start, caller, restaged = false) {
583
604
  let copy = start;
584
605
  const push = copy.push;
585
- if (push.caller !== caller.key) {
586
- return this.recovered(copy, `${short(push.commit)} was pushed by another caller and awaited its outcome`);
587
- }
606
+ if (push.caller !== caller.key)
607
+ return this.recovered(copy);
588
608
  let outcome;
589
609
  try {
590
610
  outcome = await caller.submit(push.operation_id, push.changes, push.upload, async (upload) => {
@@ -592,9 +612,10 @@ export class WorkingCopies {
592
612
  });
593
613
  }
594
614
  catch (error) {
615
+ const refusal = error instanceof HouseRefusal ? refusalOf(error.text) : null;
595
616
  return {
596
617
  refused: true,
597
- text: `House's answer to ${short(push.commit)} did not arrive (${causeOf(error)}); run house git push again to recover its outcome.`,
618
+ text: refusal?.text ?? `House's answer to ${short(push.commit)} did not arrive; run house push again`,
598
619
  };
599
620
  }
600
621
  if ('refused' in outcome) {
@@ -602,32 +623,38 @@ export class WorkingCopies {
602
623
  const { upload, ...unstaged } = copy.push;
603
624
  return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
604
625
  }
605
- if (outcome.refused.code === 'operation_expired')
606
- return this.recovered(copy, outcome.refused.text);
626
+ if (outcome.refused.code === 'expired')
627
+ return this.recovered(copy);
628
+ const rebased = rebaseFixes(outcome.refused);
607
629
  let settled = await this.save(without(copy));
608
630
  try {
609
- settled = await this.save(await this.caughtUp(settled));
631
+ settled = (await this.caughtUp([settled], true))[0];
610
632
  }
611
- catch (error) {
612
- return { refused: true, text: `${outcome.refused.text}\n${HOUSE_REF} is unchanged: ${causeOf(error)}` };
633
+ catch {
634
+ return {
635
+ refused: true,
636
+ text: rebased ? `${outcome.refused.text}\nHouse's current state did not arrive; push again later` : outcome.refused.text,
637
+ };
613
638
  }
614
639
  return {
615
640
  refused: true,
616
- text: `${outcome.refused.text}\n${HOUSE_REF} holds House's current state; merge or rebase onto it, commit and push again.`,
641
+ text: rebased
642
+ ? `${outcome.refused.text}\n${HOUSE_REF} holds House's current state; rebase onto it and push again`
643
+ : outcome.refused.text,
617
644
  };
618
645
  }
619
646
  return this.finished(await this.save({ ...copy, push: { ...copy.push, state: 'accepted', written: outcome.accepted } }));
620
647
  }
621
- async recovered(copy, reason) {
648
+ async recovered(copy) {
622
649
  const push = copy.push;
623
- const current = await this.save(await this.caughtUp(copy));
650
+ const current = (await this.caughtUp([copy], true))[0];
624
651
  const house = await tree(copy.repository, current.house);
625
- const root = rootOf(current);
652
+ const root = rootOf(current.room_handle);
626
653
  if (!(await this.landed(copy.repository, push.changes, house, await tree(copy.repository, push.commit)))) {
627
654
  await this.save(without(current));
628
655
  return {
629
656
  refused: true,
630
- text: `${reason}; House does not hold ${short(push.commit)}; run house git push again to submit it as a new operation.`,
657
+ text: `House does not hold ${short(push.commit)}; run house push again`,
631
658
  };
632
659
  }
633
660
  const written = [];
@@ -652,10 +679,10 @@ export class WorkingCopies {
652
679
  await this.save(without(copy));
653
680
  return { refused: false, text: integrated.text };
654
681
  }
655
- catch (error) {
682
+ catch {
656
683
  return {
657
684
  refused: false,
658
- text: `House accepted ${short(commit)}; local integration is pending (${causeOf(error)}); run house git push again to finish it.`,
685
+ text: `House accepted ${short(commit)}; run house push again to finish integrating it here`,
659
686
  };
660
687
  }
661
688
  }
@@ -664,7 +691,7 @@ export class WorkingCopies {
664
691
  const push = copy.push;
665
692
  let written = push.written;
666
693
  if (push.changes.some((change) => change.op === 'rename')) {
667
- copy = await this.save(await this.caughtUp(copy));
694
+ copy = (await this.caughtUp([copy], true))[0];
668
695
  const house = await tree(copy.repository, copy.house);
669
696
  const sent = await tree(copy.repository, push.house);
670
697
  if (push.changes.some((change) => change.op === 'rename' && !house.has(relative(change.to)))) {
@@ -678,7 +705,7 @@ export class WorkingCopies {
678
705
  written = [
679
706
  ...written,
680
707
  {
681
- path: `${rootOf(copy)}/${path}`,
708
+ path: `${rootOf(copy.room_handle)}/${path}`,
682
709
  revision: entry === undefined ? null : (revisionAt(copy, copy.house, path) ?? null),
683
710
  content: entry === undefined ? null : await blobText(copy.repository, entry, path),
684
711
  },
@@ -698,7 +725,7 @@ export class WorkingCopies {
698
725
  }
699
726
  const edits = new Map([...files].filter(([path, content]) => (content === null ? selected.has(path) : selected.get(path) !== blobId(content))));
700
727
  const received = [...files.keys()].filter((path) => revisionAt(copy, copy.house, path) === revisionAt(copy, push.house, path));
701
- const accepted = `House ${rootOf(copy)} with ${push.commit} accepted`;
728
+ const accepted = `House ${rootOf(copy.room_handle)} with ${push.commit} accepted`;
702
729
  const house = await importCommits(copy.repository, [
703
730
  ...(edits.size === 0 ? [] : [{ message: accepted, from: push.commit, files: edits }]),
704
731
  {
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
+ });