@agentshouse/kit 0.2.1-ce145e9aadbf6 → 0.2.1-ce3d2bb67177c

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 (105) hide show
  1. package/README.md +40 -39
  2. package/dist/acp.js +48 -105
  3. package/dist/agents.js +153 -43
  4. package/dist/api.js +34 -2
  5. package/dist/app-git.js +48 -0
  6. package/dist/bootstrap.json +4 -4
  7. package/dist/bridge.js +36 -57
  8. package/dist/calls.js +63 -0
  9. package/dist/chatgpt.js +32 -0
  10. package/dist/clis.js +127 -70
  11. package/dist/codex-main.js +58 -0
  12. package/dist/connection.js +125 -0
  13. package/dist/conversations.js +620 -323
  14. package/dist/copies.js +188 -128
  15. package/dist/end.js +27 -0
  16. package/dist/enrol-main.js +7 -1
  17. package/dist/events.js +525 -0
  18. package/dist/files.js +123 -17
  19. package/dist/git.js +11 -9
  20. package/dist/home.js +96 -7
  21. package/dist/house-cli.js +327 -0
  22. package/dist/house-main.js +7 -159
  23. package/dist/instructions.js +10 -32
  24. package/dist/kit-cli.js +68 -0
  25. package/dist/kit-main.js +8 -19
  26. package/dist/login.js +117 -104
  27. package/dist/logout.js +14 -13
  28. package/dist/mcp.js +4 -2
  29. package/dist/parts.js +82 -0
  30. package/dist/placement.js +39 -0
  31. package/dist/refusals.js +12 -0
  32. package/dist/relay.js +12 -2
  33. package/dist/resident.js +106 -50
  34. package/dist/scope.js +206 -0
  35. package/dist/secret-input.js +3 -2
  36. package/dist/service-main.js +9 -0
  37. package/dist/service.js +55 -0
  38. package/dist/shell.js +3 -0
  39. package/dist/sign-in.js +37 -31
  40. package/dist/skills.js +84 -15
  41. package/dist/transcription.js +100 -21
  42. package/dist/translate.js +2 -8
  43. package/dist/update.js +71 -50
  44. package/dist/wire.js +22 -0
  45. package/dist/work.js +346 -0
  46. package/how-we-work.md +13 -3
  47. package/package.json +4 -3
  48. package/skills/ask-matt/SKILL.md +17 -11
  49. package/skills/ask-matt/agents/openai.yaml +1 -0
  50. package/skills/code-review/SKILL.md +5 -5
  51. package/skills/codebase-design/DESIGN-IT-TWICE.md +1 -1
  52. package/skills/codebase-design/SKILL.md +0 -2
  53. package/skills/diagnosing-bugs/SKILL.md +2 -4
  54. package/skills/domain-modeling/{CONTEXT-FORMAT.md → GLOSSARY-FORMAT.md} +10 -10
  55. package/skills/domain-modeling/SKILL.md +12 -14
  56. package/skills/grill-me/SKILL.md +1 -2
  57. package/skills/grill-me/agents/openai.yaml +1 -0
  58. package/skills/grill-with-docs/SKILL.md +1 -2
  59. package/skills/grill-with-docs/agents/openai.yaml +1 -0
  60. package/skills/grilling/SKILL.md +2 -2
  61. package/skills/handoff/SKILL.md +2 -3
  62. package/skills/handoff/agents/openai.yaml +1 -0
  63. package/skills/implement/SKILL.md +5 -4
  64. package/skills/implement/agents/openai.yaml +1 -0
  65. package/skills/implement-spec/SKILL.md +40 -0
  66. package/skills/implement-spec/agents/openai.yaml +5 -0
  67. package/skills/improve-codebase-architecture/SKILL.md +6 -7
  68. package/skills/improve-codebase-architecture/agents/openai.yaml +1 -0
  69. package/skills/pr/CREDITS.md +3 -0
  70. package/skills/pr/LICENSE +21 -0
  71. package/skills/pr/SKILL.md +170 -0
  72. package/skills/pr/agents/openai.yaml +3 -0
  73. package/skills/prototype/SKILL.md +0 -2
  74. package/skills/research/SKILL.md +0 -2
  75. package/skills/retro/LICENSE +21 -0
  76. package/skills/retro/SKILL.md +44 -0
  77. package/skills/retro/agents/openai.yaml +5 -0
  78. package/skills/tdd/SKILL.md +2 -4
  79. package/skills/teach/SKILL.md +3 -4
  80. package/skills/teach/agents/openai.yaml +1 -0
  81. package/skills/to-questionnaire/SKILL.md +1 -2
  82. package/skills/to-questionnaire/agents/openai.yaml +1 -0
  83. package/skills/to-spec/SKILL.md +1 -2
  84. package/skills/to-spec/agents/openai.yaml +1 -0
  85. package/skills/to-tickets/SKILL.md +3 -4
  86. package/skills/to-tickets/agents/openai.yaml +1 -0
  87. package/skills/triage/AGENT-BRIEF.md +207 -0
  88. package/skills/triage/LICENSE +21 -0
  89. package/skills/triage/OUT-OF-SCOPE.md +105 -0
  90. package/skills/triage/SKILL.md +112 -0
  91. package/skills/triage/agents/openai.yaml +5 -0
  92. package/skills/wait-what/SKILL.md +2 -3
  93. package/skills/wait-what/agents/openai.yaml +1 -0
  94. package/skills/wayfinder/SKILL.md +5 -6
  95. package/skills/wayfinder/agents/openai.yaml +1 -0
  96. package/skills/wizard/LICENSE +21 -0
  97. package/skills/wizard/SKILL.md +44 -0
  98. package/skills/wizard/agents/openai.yaml +3 -0
  99. package/skills/wizard/template.sh +223 -0
  100. package/skills/writing-for-agents/SKILL.md +0 -2
  101. package/dist/blocks.js +0 -51
  102. package/dist/stream.js +0 -47
  103. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  104. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  105. /package/skills/{resolving-merge-conflicts → implement-spec}/LICENSE +0 -0
package/dist/copies.js CHANGED
@@ -2,8 +2,8 @@ import { parse } from '@agentshouse/mdmodel';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { mkdir, readFile, readdir, realpath, rename, rm, rmdir, stat, writeFile } from 'node:fs/promises';
4
4
  import { dirname, join } from 'node:path';
5
- import { HouseRefusal } from './api.js';
6
- import { blobId, blobText, git, HOUSE_REF, importCommits, localCopies, revision, run, succeeded, tree, } from './git.js';
5
+ import { finalRefusal, HouseRefusal, retrying } from './api.js';
6
+ import { blobId, blobText, git, isText, HOUSE_REF, importCommits, localCopies, revision, run, succeeded, tree, } from './git.js';
7
7
  import { kitHome } from './home.js';
8
8
  import { operationId } from './operation.js';
9
9
  import { refusalOf } from './refusals.js';
@@ -11,11 +11,13 @@ import { changed } from './translate.js';
11
11
  import { relayed } from './relay.js';
12
12
  const BRANCH = 'main';
13
13
  const PROVENANCE = '_provenance';
14
- const REPLICA_RETRY_MS = 30_000;
15
- const IDENTITY = { GIT_COMMITTER_NAME: 'House Kit', GIT_COMMITTER_EMAIL: 'house-kit@localhost' };
14
+ const IDENTITY = { GIT_COMMITTER_NAME: 'Kit', GIT_COMMITTER_EMAIL: 'house-kit@localhost' };
16
15
  function causeOf(error) {
17
16
  return error instanceof Error ? error.message : String(error);
18
17
  }
18
+ function houseCause(error) {
19
+ return error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
20
+ }
19
21
  function logged(error) {
20
22
  process.stderr.write(`kit: ${causeOf(error)}\n`);
21
23
  }
@@ -30,9 +32,35 @@ const REBASED = new Set([
30
32
  function rebaseFixes(refusal) {
31
33
  return refusal.code === 'edit_conflict' && refusal.conflicts.some((code) => REBASED.has(code));
32
34
  }
35
+ function textOnly(paths) {
36
+ const [named, them] = paths.length === 1 ? [`${paths[0]} is`, 'it'] : [`${paths.join(', ')} are`, 'them'];
37
+ return `${named} not text, and house push sends text only; remove ${them} from the commit and place ${them} with house upload`;
38
+ }
39
+ function outside(path) {
40
+ return { refused: true, text: `${path} is not a House Local copy; run house push inside one` };
41
+ }
33
42
  function short(commit) {
34
43
  return commit.slice(0, 12);
35
44
  }
45
+ function moved(before, after) {
46
+ if (before.house !== after.house)
47
+ return [`${rootOf(after.room_handle)}: ${HOUSE_REF} is at ${short(after.house)}`];
48
+ return before.position === after.position ? [] : [`${rootOf(after.room_handle)}: read-only files refreshed`];
49
+ }
50
+ function expiredAuthority(error) {
51
+ if (!(error instanceof HouseRefusal))
52
+ return undefined;
53
+ try {
54
+ const { error: refused } = JSON.parse(error.text);
55
+ return refused?.code === 'expired' && typeof refused.authority === 'string' ? refused.authority : undefined;
56
+ }
57
+ catch {
58
+ return undefined;
59
+ }
60
+ }
61
+ function grouped(items, size) {
62
+ return items.flatMap((_, index) => (index % size === 0 ? [items.slice(index, index + size)] : []));
63
+ }
36
64
  function covers(path, prefix) {
37
65
  return prefix === '' || path === prefix || path.startsWith(`${prefix}/`);
38
66
  }
@@ -41,6 +69,9 @@ function sidecar(path) {
41
69
  segments.splice(-1, 0, PROVENANCE);
42
70
  return segments.join('/');
43
71
  }
72
+ function recordOf(key) {
73
+ return join(kitHome(), 'local-copies', `${key}.json`);
74
+ }
44
75
  function rootOf(handle) {
45
76
  return `/rooms/${handle}`;
46
77
  }
@@ -102,7 +133,7 @@ async function ignoreRules(repository, writable, copy) {
102
133
  ].map(pattern);
103
134
  const granted = readOnly.filter((path) => !protectedPath(path)).map(pattern);
104
135
  await ruled(join(repository, '.gitignore'), [
105
- '# House Kit writes this file and .ignore. The paths below are read-only House',
136
+ '# Kit writes this file and .ignore. The paths below are read-only House',
106
137
  '# content: Git does not track them, and House refuses a change to them even',
107
138
  '# when this file is edited or a path is force-added.',
108
139
  '/.gitignore',
@@ -117,7 +148,7 @@ async function ignoreRules(repository, writable, copy) {
117
148
  '',
118
149
  ].join('\n'));
119
150
  await ruled(join(repository, '.ignore'), [
120
- '# House Kit writes this file so native search still reads the read-only House',
151
+ '# Kit writes this file so native search still reads the read-only House',
121
152
  '# content that .gitignore keeps out of Git.',
122
153
  `!${PROVENANCE}`,
123
154
  ...[...house, ...granted].map((rule) => `!${rule}`),
@@ -153,19 +184,18 @@ export class LocalCopies {
153
184
  house;
154
185
  copies = new Map();
155
186
  queues = new Map();
156
- delivering = new Set();
157
- selecting = Promise.resolve();
187
+ answering = Promise.resolve();
158
188
  constructor(house) {
159
189
  this.house = house;
160
190
  this.owner = {
161
191
  key: 'owner',
162
192
  submit: async (operation, changes) => {
163
193
  try {
164
- const outcome = await house.post('/kit/door/batch', { operation_id: operation, changes });
194
+ const outcome = await retrying(() => house.post('/kit/door/batch', { operation_id: operation, changes }));
165
195
  return { accepted: outcome.contents };
166
196
  }
167
197
  catch (error) {
168
- if (!(error instanceof HouseRefusal) || error.status >= 500)
198
+ if (!finalRefusal(error))
169
199
  throw error;
170
200
  return {
171
201
  refused: refusalOf(error.text) ?? { code: '', text: `House refused it with ${error.status}`, conflicts: [] },
@@ -186,42 +216,75 @@ export class LocalCopies {
186
216
  }
187
217
  }
188
218
  async mapping() {
189
- await this.selecting;
219
+ await this.answering;
190
220
  return [...this.copies.values()]
191
- .filter((copy) => copy.active)
221
+ .filter((copy) => copy.unfollowed === undefined)
192
222
  .map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
193
223
  .sort();
194
224
  }
195
- select() {
196
- const selected = this.selecting.then(() => this.reselect());
197
- this.selecting = selected.catch(() => undefined);
198
- return selected;
199
- }
200
- async reselect() {
201
- const { local_copy: chosen } = await this.house.post('/kit/local-copy/selection', {});
202
- const selected = (chosen?.rooms ?? []).flatMap((room) => room.room_handle === null ? [] : [{ key: `rooms/${room.room_ref}`, handle: room.room_handle }]);
203
- const keys = new Set(selected.map((scope) => scope.key));
204
- const work = [];
205
- for (const key of this.copies.keys()) {
206
- if (keys.has(key))
207
- continue;
208
- work.push(this.exclusive(key, async () => {
209
- const copy = this.copies.get(key);
210
- if (copy.active)
211
- await this.save({ ...copy, active: false });
212
- }));
213
- }
214
- for (const scope of selected)
215
- work.push(this.exclusive(scope.key, () => this.apply(scope)).catch(logged));
216
- await Promise.all(work);
225
+ connected() {
226
+ return this.selected();
217
227
  }
218
- entries(frame) {
219
- const copy = [...this.copies.values()].find((held) => held.active && held.room_ref === frame.authority);
220
- if (copy === undefined)
221
- return;
222
- if (copy.log_epoch === frame.log_epoch && BigInt(frame.position) <= BigInt(copy.position))
223
- return;
224
- this.deliver(copy.key);
228
+ async changed(rooms) {
229
+ if (rooms.length > 0)
230
+ await this.selected(rooms);
231
+ }
232
+ fetch() {
233
+ return this.answered(() => this.fetched(true));
234
+ }
235
+ async selected(rooms) {
236
+ const copied = await this.answered(() => this.fetched(false, rooms));
237
+ if (copied.refused)
238
+ logged(copied.text);
239
+ }
240
+ answered(work) {
241
+ const answered = this.answering.then(work);
242
+ this.answering = answered.catch(() => undefined);
243
+ return answered;
244
+ }
245
+ async fetched(refreshing, rooms) {
246
+ const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', rooms === undefined ? {} : { rooms });
247
+ const synced = new Map(answer.rooms.map((room) => [`rooms/${room.room_ref}`, room.room_handle]));
248
+ const named = rooms === undefined ? null : new Set(rooms.map((room) => `rooms/${room}`));
249
+ const held = () => [...this.copies.values()].filter((copy) => named === null || named.has(copy.key));
250
+ return this.holding([...new Set([...held().map((copy) => copy.key), ...synced.keys()])].sort(), async () => {
251
+ const said = [];
252
+ let refused = false;
253
+ const mark = refreshing ? 'said' : 'unsaid';
254
+ for (const copy of held()) {
255
+ if (synced.has(copy.key) || copy.unfollowed === 'said' || copy.unfollowed === mark)
256
+ continue;
257
+ await this.save({ ...copy, unfollowed: mark });
258
+ if (refreshing)
259
+ said.push(`${rootOf(copy.room_handle)}: no longer followed; kept as it is`);
260
+ }
261
+ const current = [];
262
+ for (const [key, handle] of synced) {
263
+ try {
264
+ const held = this.copies.get(key);
265
+ if (held === undefined || !(await exists(held.repository))) {
266
+ said.push(await this.materialize(key, handle));
267
+ continue;
268
+ }
269
+ const { unfollowed, ...followed } = held;
270
+ current.push(unfollowed === undefined && held.room_handle === handle ? held : await this.save({ ...followed, room_handle: handle }));
271
+ }
272
+ catch (error) {
273
+ refused = true;
274
+ said.push(`${rootOf(handle)}: ${causeOf(error)}`);
275
+ }
276
+ }
277
+ if (refreshing && current.length > 0) {
278
+ const failed = await this.refreshed(current).then(() => null, houseCause);
279
+ for (const copy of current)
280
+ said.push(...moved(copy, this.copies.get(copy.key)));
281
+ if (failed !== null) {
282
+ refused = true;
283
+ said.push(failed);
284
+ }
285
+ }
286
+ return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
287
+ });
225
288
  }
226
289
  async observed(report) {
227
290
  const key = await this.keyAt(dirname(report.common));
@@ -251,11 +314,11 @@ export class LocalCopies {
251
314
  async push(cwd, selector, caller) {
252
315
  const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
253
316
  if (located.status !== 0)
254
- return { refused: true, text: `${cwd} is not a House Local copy; run house push inside one` };
317
+ return outside(cwd);
255
318
  const [common, worktree] = located.stdout.toString('utf8').split('\n');
256
319
  const key = await this.keyAt(dirname(common));
257
320
  if (key === undefined)
258
- return { refused: true, text: `${worktree} is not a House Local copy; run house push inside one` };
321
+ return outside(worktree);
259
322
  return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
260
323
  }
261
324
  async keyAt(repository) {
@@ -271,19 +334,12 @@ export class LocalCopies {
271
334
  this.queues.set(key, queued.catch(() => undefined));
272
335
  return queued;
273
336
  }
274
- deliver(key) {
275
- if (this.delivering.has(key))
276
- return;
277
- this.delivering.add(key);
278
- void this.exclusive(key, async () => {
279
- this.delivering.delete(key);
280
- const copy = this.copies.get(key);
281
- if (copy.active)
282
- await this.save(await this.caughtUp(copy));
283
- }).catch(logged);
337
+ holding(keys, work) {
338
+ const [key, ...rest] = keys;
339
+ return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
284
340
  }
285
341
  async save(copy) {
286
- const path = join(kitHome(), 'local-copies', `${copy.key}.json`);
342
+ const path = recordOf(copy.key);
287
343
  await mkdir(dirname(path), { recursive: true, mode: 0o700 });
288
344
  const temporary = `${path}.${process.pid}`;
289
345
  await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
@@ -326,46 +382,13 @@ export class LocalCopies {
326
382
  }
327
383
  return read;
328
384
  }
329
- async apply(scope) {
330
- let copy = this.copies.get(scope.key);
331
- if (copy === undefined || !(await exists(copy.repository))) {
332
- await this.materialize(scope);
333
- return;
334
- }
335
- if (copy.room_handle !== scope.handle)
336
- copy = await this.save({ ...copy, room_handle: scope.handle });
337
- if (!copy.active) {
338
- await this.save(await this.received({ ...copy, active: true }));
339
- return;
340
- }
341
- const discovery = await this.house.post('/kit/door/discover', {
342
- prefixes: [rootOf(copy.room_handle)],
343
- });
344
- const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
345
- if (authority === undefined)
346
- return;
347
- if (authority.managed_replication !== 'admitted') {
348
- logged(`operation_denied: ${rootOf(copy.room_handle)} is not synchronized to this Environment`);
349
- return;
350
- }
351
- if (JSON.stringify([authority.operations.write, authority.protected_paths]) !==
352
- JSON.stringify([copy.write, copy.protected])) {
353
- await this.save(await this.received(copy));
354
- return;
355
- }
356
- await this.save(await this.caughtUp(copy));
357
- }
358
- async materialize(scope) {
359
- const read = await this.read(scope.handle);
360
- if (read === null) {
361
- logged(`House serves nothing of ${rootOf(scope.handle)} to copy`);
362
- return;
363
- }
385
+ async materialize(key, handle) {
386
+ const read = await this.read(handle);
387
+ if (read === null)
388
+ throw new Error('House serves nothing of it to copy');
364
389
  const repository = join(localCopies(), 'rooms', read.anchor);
365
- if (await exists(repository)) {
366
- logged(`${repository} already exists and House Kit leaves it as it is`);
367
- return;
368
- }
390
+ if (await exists(repository))
391
+ throw new Error(`${repository} already exists and Kit leaves it as it is`);
369
392
  const staging = join(dirname(repository), `.${randomUUID()}`);
370
393
  await mkdir(staging, { recursive: true });
371
394
  let house;
@@ -373,7 +396,7 @@ export class LocalCopies {
373
396
  try {
374
397
  await git(staging, ['init', '-q', '-b', BRANCH]);
375
398
  house = await importCommits(staging, [
376
- { message: `House ${rootOf(scope.handle)}`, from: null, files: read.writable },
399
+ { message: `House ${rootOf(handle)}`, from: null, files: read.writable },
377
400
  ]);
378
401
  await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
379
402
  await git(staging, ['reset', '-q', '--hard']);
@@ -386,11 +409,10 @@ export class LocalCopies {
386
409
  throw error;
387
410
  }
388
411
  await this.save({
389
- key: scope.key,
412
+ key,
390
413
  room_ref: read.anchor,
391
- room_handle: scope.handle,
414
+ room_handle: handle,
392
415
  repository,
393
- active: true,
394
416
  house,
395
417
  position: read.position,
396
418
  log_epoch: read.log_epoch,
@@ -400,6 +422,7 @@ export class LocalCopies {
400
422
  manifests: { [house]: { parent: null, files: read.revisions } },
401
423
  moves: [],
402
424
  });
425
+ return `${rootOf(handle)}: copied to ${repository}`;
403
426
  }
404
427
  async received(copy) {
405
428
  const read = await this.read(copy.room_handle);
@@ -423,30 +446,57 @@ export class LocalCopies {
423
446
  await ignoreRules(copy.repository, [...read.writable.keys()], next);
424
447
  return next;
425
448
  }
426
- async caughtUp(start) {
427
- let copy = start;
428
- for (;;) {
449
+ async refreshed(copies) {
450
+ const discover = (group) => this.house.post('/kit/door/discover', { prefixes: group.map((copy) => rootOf(copy.room_handle)) });
451
+ const [first, ...rest] = copies;
452
+ const { bounds, authorities } = await discover([first]);
453
+ for (const group of grouped(rest, bounds.max_prefixes))
454
+ authorities.push(...(await discover(group)).authorities);
455
+ const unchanged = [];
456
+ for (const copy of copies) {
457
+ const authority = authorities.find((entry) => entry.room_ref === copy.room_ref);
458
+ if (authority !== undefined &&
459
+ JSON.stringify([authority.operations.write, authority.protected_paths]) !== JSON.stringify([copy.write, copy.protected])) {
460
+ await this.save(await this.received(copy));
461
+ }
462
+ else {
463
+ unchanged.push(copy);
464
+ }
465
+ }
466
+ for (const group of grouped(unchanged, bounds.max_prefixes))
467
+ await this.caughtUp(group, false);
468
+ }
469
+ async caughtUp(copies, whole) {
470
+ const caught = new Map(copies.map((copy) => [copy.key, copy]));
471
+ let asking = [...copies];
472
+ while (asking.length > 0) {
429
473
  let answer;
430
474
  try {
431
475
  answer = await this.house.post('/kit/door/enumerate', {
432
- prefixes: [rootOf(copy.room_handle)],
433
- positions: [{ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch }],
476
+ prefixes: asking.map((copy) => rootOf(copy.room_handle)),
477
+ positions: asking.map((copy) => ({ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch })),
434
478
  });
435
479
  }
436
480
  catch (error) {
437
- const code = error instanceof HouseRefusal ? (refusalOf(error.text)?.code ?? null) : null;
438
- if (code === 'position_expired')
439
- return this.received(copy);
440
- if (code !== 'replica_behind')
481
+ const expired = asking.find((copy) => copy.room_ref === expiredAuthority(error));
482
+ if (expired === undefined)
441
483
  throw error;
442
- setTimeout(() => this.deliver(copy.key), REPLICA_RETRY_MS).unref();
443
- return copy;
484
+ caught.set(expired.key, await this.save(await this.received(expired)));
485
+ asking = asking.filter((copy) => copy !== expired);
486
+ continue;
444
487
  }
445
- const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
446
- if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
447
- return copy;
448
- copy = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
488
+ const advanced = [];
489
+ for (const copy of asking) {
490
+ const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
491
+ if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
492
+ continue;
493
+ const next = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
494
+ caught.set(copy.key, next);
495
+ advanced.push(next);
496
+ }
497
+ asking = whole ? advanced : [];
449
498
  }
499
+ return copies.map((copy) => caught.get(copy.key));
450
500
  }
451
501
  async incorporated(copy, changes, position, logEpoch) {
452
502
  const houseTree = await tree(copy.repository, copy.house);
@@ -509,9 +559,6 @@ export class LocalCopies {
509
559
  }
510
560
  copy = await this.save(without(copy));
511
561
  }
512
- if (!copy.active) {
513
- return { refused: true, text: `${rootOf(copy.room_handle)} is no longer synced to this Environment, so House takes no push from it` };
514
- }
515
562
  const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
516
563
  const base = listed.find((ancestor) => ancestor in copy.manifests);
517
564
  if (base === undefined) {
@@ -522,7 +569,7 @@ export class LocalCopies {
522
569
  changes = await this.derived(copy, base, commit);
523
570
  }
524
571
  catch (error) {
525
- return { refused: true, text: causeOf(error) };
572
+ return { refused: true, text: houseCause(error) };
526
573
  }
527
574
  if (changes.length === 0)
528
575
  return { refused: false, text: `Nothing to submit: ${short(commit)} matches House.` };
@@ -549,7 +596,18 @@ export class LocalCopies {
549
596
  const after = await tree(copy.repository, commit);
550
597
  const moved = movedPaths(copy.moves, before, after);
551
598
  const arrived = new Set(moved.values());
552
- const text = (entry, path) => blobText(copy.repository, entry, path);
599
+ const arriving = [...after].filter(([path, entry]) => before.get(path) !== entry);
600
+ if (arriving.length > 0) {
601
+ const { bounds } = await this.house.post('/kit/door/discover', { prefixes: [root] });
602
+ const binary = [];
603
+ for (const [path, entry] of arriving) {
604
+ if (!(await isText(copy.repository, entry, bounds.max_authored_file_bytes)))
605
+ binary.push(path);
606
+ }
607
+ if (binary.length > 0)
608
+ throw new Error(textOnly(binary));
609
+ }
610
+ const text = (entry) => blobText(copy.repository, entry);
553
611
  const changes = [];
554
612
  for (const path of [...new Set([...before.keys(), ...after.keys()])].sort()) {
555
613
  if (arrived.has(path))
@@ -562,7 +620,7 @@ export class LocalCopies {
562
620
  changes.push({ op: 'rename', path: `${root}/${path}`, to: `${root}/${to}`, base: known });
563
621
  const moving = after.get(to);
564
622
  if (moving !== prior) {
565
- changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving, to) });
623
+ changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving) });
566
624
  }
567
625
  continue;
568
626
  }
@@ -571,9 +629,9 @@ export class LocalCopies {
571
629
  if (next === undefined)
572
630
  changes.push({ op: 'remove', path: `${root}/${path}`, base: known });
573
631
  else if (prior === undefined)
574
- changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next, path) });
632
+ changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next) });
575
633
  else
576
- changes.push(...changed(`${root}/${path}`, path, known, await text(prior, path), await text(next, path)));
634
+ changes.push(...changed(`${root}/${path}`, path, known, await text(prior), await text(next)));
577
635
  }
578
636
  return changes;
579
637
  }
@@ -600,12 +658,14 @@ export class LocalCopies {
600
658
  const { upload, ...unstaged } = copy.push;
601
659
  return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
602
660
  }
603
- if (outcome.refused.code === 'operation_expired')
661
+ if (outcome.refused.code === 'expired')
604
662
  return this.recovered(copy);
605
- const rebased = rebaseFixes(outcome.refused);
606
663
  let settled = await this.save(without(copy));
664
+ if (settled.unfollowed !== undefined)
665
+ return { refused: true, text: outcome.refused.text };
666
+ const rebased = rebaseFixes(outcome.refused);
607
667
  try {
608
- settled = await this.save(await this.caughtUp(settled));
668
+ settled = (await this.caughtUp([settled], true))[0];
609
669
  }
610
670
  catch {
611
671
  return {
@@ -624,7 +684,7 @@ export class LocalCopies {
624
684
  }
625
685
  async recovered(copy) {
626
686
  const push = copy.push;
627
- const current = await this.save(await this.caughtUp(copy));
687
+ const current = (await this.caughtUp([copy], true))[0];
628
688
  const house = await tree(copy.repository, current.house);
629
689
  const root = rootOf(current.room_handle);
630
690
  if (!(await this.landed(copy.repository, push.changes, house, await tree(copy.repository, push.commit)))) {
@@ -640,7 +700,7 @@ export class LocalCopies {
640
700
  written.push({
641
701
  path: `${root}/${path}`,
642
702
  revision: entry === undefined ? null : (revisionAt(current, current.house, path) ?? null),
643
- content: entry === undefined ? null : await blobText(copy.repository, entry, path),
703
+ content: entry === undefined ? null : await blobText(copy.repository, entry),
644
704
  });
645
705
  }
646
706
  return this.finished(await this.save({ ...current, push: { ...push, state: 'accepted', written } }));
@@ -668,7 +728,7 @@ export class LocalCopies {
668
728
  const push = copy.push;
669
729
  let written = push.written;
670
730
  if (push.changes.some((change) => change.op === 'rename')) {
671
- copy = await this.save(await this.caughtUp(copy));
731
+ copy = (await this.caughtUp([copy], true))[0];
672
732
  const house = await tree(copy.repository, copy.house);
673
733
  const sent = await tree(copy.repository, push.house);
674
734
  if (push.changes.some((change) => change.op === 'rename' && !house.has(relative(change.to)))) {
@@ -684,7 +744,7 @@ export class LocalCopies {
684
744
  {
685
745
  path: `${rootOf(copy.room_handle)}/${path}`,
686
746
  revision: entry === undefined ? null : (revisionAt(copy, copy.house, path) ?? null),
687
- content: entry === undefined ? null : await blobText(copy.repository, entry, path),
747
+ content: entry === undefined ? null : await blobText(copy.repository, entry),
688
748
  },
689
749
  ];
690
750
  }
@@ -729,7 +789,7 @@ export class LocalCopies {
729
789
  async landed(repository, changes, house, selected) {
730
790
  const text = async (path) => {
731
791
  const entry = house.get(relative(path));
732
- return entry === undefined ? undefined : blobText(repository, entry, path);
792
+ return entry === undefined ? undefined : blobText(repository, entry);
733
793
  };
734
794
  for (const change of changes) {
735
795
  const current = await text(change.path);
package/dist/end.js ADDED
@@ -0,0 +1,27 @@
1
+ import { KIT_RELEASE } from './clis.js';
2
+ import { forgetRejected } from './home.js';
3
+ import { installCommand } from './placement.js';
4
+ import { killDescendants } from './scope.js';
5
+ export const REPLACED = 'another Kit process started for this Environment';
6
+ function exit(reason, status) {
7
+ process.stderr.write(`kit: ${reason}\n`);
8
+ killDescendants();
9
+ process.exit(status);
10
+ }
11
+ export function end(reason) {
12
+ exit(reason, process.env.HOUSE_KIT_RESIDENT === String(process.pid) ? 0 : 1);
13
+ }
14
+ export function restart(reason) {
15
+ exit(reason, 1);
16
+ }
17
+ export function updateRequired() {
18
+ const command = installCommand();
19
+ end(command === null ? 'This Kit is too old for House; install it again.' : `This Kit is too old for House. Update it: ${command}`);
20
+ }
21
+ export function newerThanHouse(release, command) {
22
+ end(`Kit ${KIT_RELEASE} is newer than this Building supports (Kit ${release}). Install Kit ${release}: ${command}`);
23
+ }
24
+ export async function signInAgain(rejected) {
25
+ await forgetRejected(rejected);
26
+ restart('sign in again');
27
+ }
@@ -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
+ });