@agentshouse/kit 0.2.1-c3cf4b145dcda → 0.2.1-c46722ad48058

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.
@@ -3,7 +3,7 @@ import { createHash, randomUUID } from 'node:crypto';
3
3
  import { mkdir } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
5
  import { unlessAborted } from './abort.js';
6
- import { killMarked, processes, startAdapter } from './acp.js';
6
+ import { startAdapter } from './acp.js';
7
7
  import { openBridge } from './bridge.js';
8
8
  import { CLIS } from './clis.js';
9
9
  import { readerFor } from './events.js';
@@ -11,12 +11,12 @@ import { placeFiles, uploadBytes } from './files.js';
11
11
  import { agentBase } from './home.js';
12
12
  import { instructions } from './instructions.js';
13
13
  import { Parts } from './parts.js';
14
+ import { killMarked, Scope } from './scope.js';
14
15
  import { holdSecretInput } from './secret-input.js';
15
16
  import { createHowWeWork } from './skills.js';
16
17
  import { transcribe } from './transcription.js';
18
+ import { workFor } from './work.js';
17
19
  const KILLED = 'the conversation was killed';
18
- // A process a conversation left running reports nothing when it exits, so the Kit looks again every five seconds while one runs.
19
- const PROCESS_RECHECK_MS = 5000;
20
20
  function causeOf(error) {
21
21
  return error instanceof Error ? error.message : String(error);
22
22
  }
@@ -29,9 +29,6 @@ function logged(error) {
29
29
  function marker(bridge) {
30
30
  return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
31
31
  }
32
- function stop(bridge) {
33
- killMarked(marker(bridge));
34
- }
35
32
  class Turn {
36
33
  id = randomUUID();
37
34
  parts;
@@ -53,7 +50,7 @@ class Conversation {
53
50
  turn = null;
54
51
  commands = null;
55
52
  context = null;
56
- busy = new Set();
53
+ leftovers = new Set();
57
54
  waiting = [];
58
55
  sending = null;
59
56
  subagents = new Map();
@@ -86,9 +83,10 @@ export class Conversations {
86
83
  carrying = 0;
87
84
  reporting = 0;
88
85
  watched = false;
89
- recheck = null;
90
86
  constructor(kit) {
91
87
  this.kit = kit;
88
+ // A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
89
+ setInterval(() => this.look(), 2_000);
92
90
  }
93
91
  input(input) {
94
92
  this.carrying++;
@@ -126,22 +124,24 @@ export class Conversations {
126
124
  for (const conversation of this.conversations.values()) {
127
125
  const turn = conversation.turn;
128
126
  const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
129
- if (conversation.busy.size > 0 || conversation.open > (asking ? 1 : 0))
127
+ if (conversation.open > (asking ? 1 : 0))
130
128
  return false;
131
129
  }
132
- for (const conversation of this.conversations.values()) {
133
- const running = conversation.running;
134
- if (running === null || running.baseline === null)
135
- continue;
136
- if ([...processes(marker(running.bridge))].every((found) => running.baseline.has(found)))
137
- continue;
138
- this.recheck ??= setTimeout(() => {
139
- this.recheck = null;
140
- this.kit.changed();
141
- }, PROCESS_RECHECK_MS);
130
+ const conversations = [...this.conversations.values()];
131
+ if (conversations.some((conversation) => conversation.running?.work.busy()))
142
132
  return false;
133
+ return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
134
+ }
135
+ look() {
136
+ for (const conversation of this.conversations.values()) {
137
+ for (const scope of conversation.leftovers) {
138
+ if (!scope.empty())
139
+ continue;
140
+ scope.close();
141
+ conversation.leftovers.delete(scope);
142
+ }
143
143
  }
144
- return true;
144
+ this.kit.changed();
145
145
  }
146
146
  opened() {
147
147
  this.watched = false;
@@ -295,11 +295,13 @@ export class Conversations {
295
295
  }
296
296
  halt(conversation) {
297
297
  if (conversation.opening !== null)
298
- stop(conversation.opening);
298
+ killMarked(marker(conversation.opening));
299
+ for (const scope of conversation.leftovers)
300
+ scope.kill();
299
301
  if (conversation.running === null)
300
302
  return;
301
303
  conversation.running.killed = true;
302
- stop(conversation.running.bridge);
304
+ conversation.running.scope.kill();
303
305
  }
304
306
  async kill(conversation) {
305
307
  const running = conversation.running;
@@ -350,22 +352,30 @@ export class Conversations {
350
352
  throw new Error(KILLED);
351
353
  }
352
354
  conversation.opening = bridge;
355
+ const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
356
+ const scope = new Scope(marker(bridge), randomUUID(), cli);
353
357
  try {
354
358
  await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
355
- await CLIS[route.kind].allowHouse();
356
- conversation.busy.clear();
357
- const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (message) => this.read(conversation, reader, message), bridge.env);
359
+ await CLIS[route.kind].allowHouse(bridge.env.PATH);
360
+ const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (message) => {
361
+ work.read(message);
362
+ this.read(conversation, reader, message);
363
+ }, { ...bridge.env, ...(await work.open(cli)) }, undefined, scope);
358
364
  void adapter.exited.then(() => bridge.close());
359
365
  const agent = adapter.connection.agent;
360
366
  const cwd = route.working_directory;
361
- const _meta = CLIS[route.kind].sessionMeta;
367
+ const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
362
368
  const opened = sessionId === null
363
369
  ? await agent.request('session/new', { cwd, mcpServers: [], _meta })
364
370
  : { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta })), sessionId };
365
371
  reader.session = opened.sessionId;
366
- const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, baseline: null };
372
+ if (sessionId === null && CLIS[route.kind].renames) {
373
+ const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
374
+ await agent.request('session/prompt', { sessionId: opened.sessionId, prompt });
375
+ }
376
+ const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
367
377
  const options = await this.launchSettings(running, route, opened.configOptions ?? []);
368
- running.baseline = processes(marker(bridge));
378
+ void work.settled().then(() => scope.record());
369
379
  conversation.running = running;
370
380
  conversation.session = running.sessionId;
371
381
  this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
@@ -376,7 +386,9 @@ export class Conversations {
376
386
  return running.sessionId;
377
387
  }
378
388
  catch (error) {
379
- stop(bridge);
389
+ scope.kill();
390
+ work.close();
391
+ conversation.leftovers.add(scope);
380
392
  bridge.close();
381
393
  throw conversation.kills > 0 ? new Error(KILLED) : error;
382
394
  }
@@ -409,17 +421,18 @@ export class Conversations {
409
421
  const running = conversation.running;
410
422
  if (running?.adapter !== adapter)
411
423
  return;
412
- this.detach(conversation);
424
+ this.detach(conversation, running);
413
425
  if (conversation.turn !== null) {
414
426
  this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
415
427
  }
416
428
  this.next(conversation);
417
429
  this.kit.changed();
418
430
  }
419
- detach(conversation) {
431
+ detach(conversation, running) {
432
+ running.work.close();
433
+ conversation.leftovers.add(running.scope);
420
434
  conversation.running = null;
421
435
  conversation.commands = null;
422
- conversation.busy.clear();
423
436
  for (const [id, question] of this.questions) {
424
437
  if (question.conversation === conversation)
425
438
  this.questions.delete(id);
@@ -471,8 +484,6 @@ export class Conversations {
471
484
  turn.parts.measure(conversation.context);
472
485
  for (const part of conversation.held.splice(0))
473
486
  turn.parts.start(part);
474
- for (const event of conversation.running?.reader.turnStarted() ?? [])
475
- this.event(conversation, event);
476
487
  this.kit.changed();
477
488
  return turn;
478
489
  }
@@ -506,8 +517,8 @@ export class Conversations {
506
517
  }
507
518
  else {
508
519
  if (conversation.open === 1 && conversation.running === running) {
509
- stop(running.bridge);
510
- this.detach(conversation);
520
+ running.scope.kill();
521
+ this.detach(conversation, running);
511
522
  }
512
523
  this.end(conversation, turn, { failed: causeOf(error) });
513
524
  }
@@ -592,20 +603,6 @@ export class Conversations {
592
603
  case 'options':
593
604
  this.options(conversation, event.options);
594
605
  return;
595
- case 'busy':
596
- if (event.running)
597
- conversation.busy.add(event.key);
598
- else
599
- conversation.busy.delete(event.key);
600
- this.kit.changed();
601
- return;
602
- case 'servers': {
603
- const running = conversation.running;
604
- if (running?.baseline)
605
- for (const found of processes(marker(running.bridge)))
606
- running.baseline.add(found);
607
- return;
608
- }
609
606
  case 'context':
610
607
  conversation.context = { used: event.used, window: event.window };
611
608
  conversation.turn?.parts.measure(conversation.context);
package/dist/copies.js CHANGED
@@ -32,6 +32,22 @@ function rebaseFixes(refusal) {
32
32
  function short(commit) {
33
33
  return commit.slice(0, 12);
34
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;
43
+ try {
44
+ const { error: refused } = JSON.parse(error.text);
45
+ return refused?.code === 'expired' && typeof refused.authority === 'string' ? refused.authority : undefined;
46
+ }
47
+ catch {
48
+ return undefined;
49
+ }
50
+ }
35
51
  function covers(path, prefix) {
36
52
  return prefix === '' || path === prefix || path.startsWith(`${prefix}/`);
37
53
  }
@@ -152,8 +168,7 @@ export class LocalCopies {
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 = {
@@ -185,42 +200,68 @@ export class LocalCopies {
185
200
  }
186
201
  }
187
202
  async mapping() {
188
- await this.selecting;
203
+ await this.answering;
189
204
  return [...this.copies.values()]
190
205
  .filter((copy) => copy.active)
191
206
  .map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
192
207
  .sort();
193
208
  }
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);
209
+ async connected() {
210
+ const copied = await this.answered(() => this.fetched(false));
211
+ if (copied.refused)
212
+ logged(copied.text);
216
213
  }
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);
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
+ });
224
265
  }
225
266
  async observed(report) {
226
267
  const key = await this.keyAt(dirname(report.common));
@@ -270,16 +311,9 @@ export class LocalCopies {
270
311
  this.queues.set(key, queued.catch(() => undefined));
271
312
  return queued;
272
313
  }
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);
314
+ holding(keys, work) {
315
+ const [key, ...rest] = keys;
316
+ return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
283
317
  }
284
318
  async save(copy) {
285
319
  const path = join(kitHome(), 'local-copies', `${copy.key}.json`);
@@ -325,46 +359,13 @@ export class LocalCopies {
325
359
  }
326
360
  return read;
327
361
  }
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
- }
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');
363
366
  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
- }
367
+ if (await exists(repository))
368
+ throw new Error(`${repository} already exists and House Kit leaves it as it is`);
368
369
  const staging = join(dirname(repository), `.${randomUUID()}`);
369
370
  await mkdir(staging, { recursive: true });
370
371
  let house;
@@ -372,7 +373,7 @@ export class LocalCopies {
372
373
  try {
373
374
  await git(staging, ['init', '-q', '-b', BRANCH]);
374
375
  house = await importCommits(staging, [
375
- { message: `House ${rootOf(scope.handle)}`, from: null, files: read.writable },
376
+ { message: `House ${rootOf(handle)}`, from: null, files: read.writable },
376
377
  ]);
377
378
  await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
378
379
  await git(staging, ['reset', '-q', '--hard']);
@@ -385,9 +386,9 @@ export class LocalCopies {
385
386
  throw error;
386
387
  }
387
388
  await this.save({
388
- key: scope.key,
389
+ key,
389
390
  room_ref: read.anchor,
390
- room_handle: scope.handle,
391
+ room_handle: handle,
391
392
  repository,
392
393
  active: true,
393
394
  house,
@@ -399,6 +400,7 @@ export class LocalCopies {
399
400
  manifests: { [house]: { parent: null, files: read.revisions } },
400
401
  moves: [],
401
402
  });
403
+ return `${rootOf(handle)}: copied to ${repository}`;
402
404
  }
403
405
  async received(copy) {
404
406
  const read = await this.read(copy.room_handle);
@@ -422,29 +424,52 @@ export class LocalCopies {
422
424
  await ignoreRules(copy.repository, [...read.writable.keys()], next);
423
425
  return next;
424
426
  }
425
- async caughtUp(start) {
426
- let copy = start;
427
- 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) {
428
446
  let answer;
429
447
  try {
430
448
  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 }],
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 })),
433
451
  });
434
452
  }
435
453
  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')
454
+ const expired = asking.find((copy) => copy.room_ref === expiredAuthority(error));
455
+ if (expired === undefined)
440
456
  throw error;
441
- return copy;
457
+ caught.set(expired.key, await this.save(await this.received(expired)));
458
+ asking = asking.filter((copy) => copy !== expired);
459
+ continue;
460
+ }
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);
442
469
  }
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));
470
+ asking = whole ? advanced : [];
447
471
  }
472
+ return copies.map((copy) => caught.get(copy.key));
448
473
  }
449
474
  async incorporated(copy, changes, position, logEpoch) {
450
475
  const houseTree = await tree(copy.repository, copy.house);
@@ -598,12 +623,12 @@ export class LocalCopies {
598
623
  const { upload, ...unstaged } = copy.push;
599
624
  return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
600
625
  }
601
- if (outcome.refused.code === 'operation_expired')
626
+ if (outcome.refused.code === 'expired')
602
627
  return this.recovered(copy);
603
628
  const rebased = rebaseFixes(outcome.refused);
604
629
  let settled = await this.save(without(copy));
605
630
  try {
606
- settled = await this.save(await this.caughtUp(settled));
631
+ settled = (await this.caughtUp([settled], true))[0];
607
632
  }
608
633
  catch {
609
634
  return {
@@ -622,7 +647,7 @@ export class LocalCopies {
622
647
  }
623
648
  async recovered(copy) {
624
649
  const push = copy.push;
625
- const current = await this.save(await this.caughtUp(copy));
650
+ const current = (await this.caughtUp([copy], true))[0];
626
651
  const house = await tree(copy.repository, current.house);
627
652
  const root = rootOf(current.room_handle);
628
653
  if (!(await this.landed(copy.repository, push.changes, house, await tree(copy.repository, push.commit)))) {
@@ -666,7 +691,7 @@ export class LocalCopies {
666
691
  const push = copy.push;
667
692
  let written = push.written;
668
693
  if (push.changes.some((change) => change.op === 'rename')) {
669
- copy = await this.save(await this.caughtUp(copy));
694
+ copy = (await this.caughtUp([copy], true))[0];
670
695
  const house = await tree(copy.repository, copy.house);
671
696
  const sent = await tree(copy.repository, push.house);
672
697
  if (push.changes.some((change) => change.op === 'rename' && !house.has(relative(change.to)))) {
package/dist/end.js CHANGED
@@ -1,4 +1,4 @@
1
- import { killDescendants } from './acp.js';
1
+ import { killDescendants } from './scope.js';
2
2
  export const REPLACED = 'another Kit started for this Environment';
3
3
  export const SIGN_IN_AGAIN = 'sign in again';
4
4
  export function end(reason) {
@@ -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
+ });