@agentshouse/kit 0.2.1-cb6679d048e60 → 0.2.1-cb66cb2fd0696

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 (136) hide show
  1. package/README.md +47 -46
  2. package/dist/acp.js +102 -108
  3. package/dist/agents.js +214 -62
  4. package/dist/api.js +36 -3
  5. package/dist/app-git.js +48 -0
  6. package/dist/authority-main.js +2 -2
  7. package/dist/bridge.js +40 -59
  8. package/dist/building.js +50 -0
  9. package/dist/calls.js +71 -0
  10. package/dist/chatgpt.js +32 -0
  11. package/dist/clis.js +161 -95
  12. package/dist/codex-main.js +58 -0
  13. package/dist/connection.js +105 -0
  14. package/dist/conversations.js +732 -330
  15. package/dist/copies.js +205 -150
  16. package/dist/end.js +33 -0
  17. package/dist/enrol-main.js +7 -1
  18. package/dist/events.js +546 -0
  19. package/dist/files.js +140 -17
  20. package/dist/git.js +13 -11
  21. package/dist/home.js +89 -7
  22. package/dist/house-cli.js +299 -0
  23. package/dist/house-main.js +7 -159
  24. package/dist/instructions.js +8 -10
  25. package/dist/kit-cli.js +68 -0
  26. package/dist/kit-main.js +8 -19
  27. package/dist/login.js +119 -104
  28. package/dist/logout.js +14 -13
  29. package/dist/mcp.js +8 -3
  30. package/dist/parts.js +82 -0
  31. package/dist/placement.js +31 -0
  32. package/dist/refusals.js +12 -0
  33. package/dist/relay.js +41 -0
  34. package/dist/resident.js +114 -51
  35. package/dist/scope.js +214 -0
  36. package/dist/scripts.js +255 -0
  37. package/dist/secret-input.js +4 -1
  38. package/dist/service-main.js +9 -0
  39. package/dist/service.js +55 -0
  40. package/dist/shell.js +3 -0
  41. package/dist/sign-in.js +40 -32
  42. package/dist/silence.js +28 -0
  43. package/dist/skills.js +49 -35
  44. package/dist/transcription.js +129 -41
  45. package/dist/translate.js +2 -8
  46. package/dist/update.js +91 -104
  47. package/dist/wire.js +22 -0
  48. package/dist/work.js +346 -0
  49. package/package.json +20 -20
  50. package/dist/blocks.js +0 -51
  51. package/dist/bootstrap.json +0 -24
  52. package/dist/clis-main.js +0 -23
  53. package/dist/stream.js +0 -46
  54. package/how-we-work.md +0 -23
  55. package/skills/ask-matt/LICENSE +0 -21
  56. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  57. package/skills/ask-matt/SKILL.md +0 -91
  58. package/skills/ask-matt/agents/openai.yaml +0 -4
  59. package/skills/code-review/LICENSE +0 -21
  60. package/skills/code-review/SKILL.md +0 -89
  61. package/skills/code-review/agents/openai.yaml +0 -3
  62. package/skills/codebase-design/DEEPENING.md +0 -37
  63. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  64. package/skills/codebase-design/LICENSE +0 -21
  65. package/skills/codebase-design/SKILL.md +0 -116
  66. package/skills/codebase-design/agents/openai.yaml +0 -3
  67. package/skills/diagnosing-bugs/LICENSE +0 -21
  68. package/skills/diagnosing-bugs/SKILL.md +0 -140
  69. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  70. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  71. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  72. package/skills/domain-modeling/CONTEXT-FORMAT.md +0 -60
  73. package/skills/domain-modeling/LICENSE +0 -21
  74. package/skills/domain-modeling/SKILL.md +0 -76
  75. package/skills/domain-modeling/agents/openai.yaml +0 -3
  76. package/skills/grill-me/LICENSE +0 -21
  77. package/skills/grill-me/SKILL.md +0 -8
  78. package/skills/grill-me/agents/openai.yaml +0 -4
  79. package/skills/grill-with-docs/LICENSE +0 -21
  80. package/skills/grill-with-docs/SKILL.md +0 -8
  81. package/skills/grill-with-docs/agents/openai.yaml +0 -4
  82. package/skills/grilling/LICENSE +0 -21
  83. package/skills/grilling/SKILL.md +0 -30
  84. package/skills/grilling/agents/openai.yaml +0 -3
  85. package/skills/handoff/LICENSE +0 -21
  86. package/skills/handoff/SKILL.md +0 -17
  87. package/skills/handoff/agents/openai.yaml +0 -4
  88. package/skills/implement/LICENSE +0 -21
  89. package/skills/implement/SKILL.md +0 -16
  90. package/skills/implement/agents/openai.yaml +0 -4
  91. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  92. package/skills/improve-codebase-architecture/LICENSE +0 -21
  93. package/skills/improve-codebase-architecture/SKILL.md +0 -72
  94. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -4
  95. package/skills/prototype/LICENSE +0 -21
  96. package/skills/prototype/LOGIC.md +0 -67
  97. package/skills/prototype/SKILL.md +0 -28
  98. package/skills/prototype/UI.md +0 -112
  99. package/skills/prototype/agents/openai.yaml +0 -3
  100. package/skills/research/LICENSE +0 -21
  101. package/skills/research/SKILL.md +0 -14
  102. package/skills/research/agents/openai.yaml +0 -3
  103. package/skills/resolving-merge-conflicts/LICENSE +0 -21
  104. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  105. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  106. package/skills/tdd/LICENSE +0 -21
  107. package/skills/tdd/SKILL.md +0 -40
  108. package/skills/tdd/agents/openai.yaml +0 -3
  109. package/skills/tdd/mocking.md +0 -59
  110. package/skills/tdd/tests.md +0 -77
  111. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  112. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  113. package/skills/teach/LICENSE +0 -21
  114. package/skills/teach/MISSION-FORMAT.md +0 -31
  115. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  116. package/skills/teach/SKILL.md +0 -141
  117. package/skills/teach/agents/openai.yaml +0 -4
  118. package/skills/to-questionnaire/LICENSE +0 -21
  119. package/skills/to-questionnaire/SKILL.md +0 -55
  120. package/skills/to-questionnaire/agents/openai.yaml +0 -4
  121. package/skills/to-spec/LICENSE +0 -21
  122. package/skills/to-spec/SKILL.md +0 -76
  123. package/skills/to-spec/agents/openai.yaml +0 -4
  124. package/skills/to-tickets/LICENSE +0 -21
  125. package/skills/to-tickets/SKILL.md +0 -106
  126. package/skills/to-tickets/agents/openai.yaml +0 -4
  127. package/skills/wait-what/LICENSE +0 -21
  128. package/skills/wait-what/SKILL.md +0 -8
  129. package/skills/wait-what/agents/openai.yaml +0 -4
  130. package/skills/wayfinder/LICENSE +0 -21
  131. package/skills/wayfinder/SKILL.md +0 -129
  132. package/skills/wayfinder/agents/openai.yaml +0 -4
  133. package/skills/writing-for-agents/LICENSE +0 -21
  134. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  135. package/skills/writing-for-agents/SKILL.md +0 -83
  136. package/skills/writing-for-agents/agents/openai.yaml +0 -3
package/dist/copies.js CHANGED
@@ -2,19 +2,22 @@ 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';
10
10
  import { changed } from './translate.js';
11
+ import { relayed } from './relay.js';
11
12
  const BRANCH = 'main';
12
13
  const PROVENANCE = '_provenance';
13
- const REPLICA_RETRY_MS = 30_000;
14
- 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' };
15
15
  function causeOf(error) {
16
16
  return error instanceof Error ? error.message : String(error);
17
17
  }
18
+ function houseCause(error) {
19
+ return error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
20
+ }
18
21
  function logged(error) {
19
22
  process.stderr.write(`kit: ${causeOf(error)}\n`);
20
23
  }
@@ -29,9 +32,35 @@ const REBASED = new Set([
29
32
  function rebaseFixes(refusal) {
30
33
  return refusal.code === 'edit_conflict' && refusal.conflicts.some((code) => REBASED.has(code));
31
34
  }
35
+ function textOnly(paths) {
36
+ const [named, them] = paths.length === 1 ? [`${paths[0]} is`, 'it'] : [`${paths.join(', ')} are`, 'them'];
37
+ return `${named} not text, and house push sends text only; remove ${them} from the commit and place ${them} with house upload`;
38
+ }
39
+ function outside(path) {
40
+ return { refused: true, text: `${path} is not a House Local copy; run house push inside one` };
41
+ }
32
42
  function short(commit) {
33
43
  return commit.slice(0, 12);
34
44
  }
45
+ function moved(before, after) {
46
+ if (before.house !== after.house)
47
+ return [`${rootOf(after.room_handle)}: ${HOUSE_REF} is at ${short(after.house)}`];
48
+ return before.position === after.position ? [] : [`${rootOf(after.room_handle)}: read-only files refreshed`];
49
+ }
50
+ function expiredAuthority(error) {
51
+ if (!(error instanceof HouseRefusal))
52
+ return undefined;
53
+ try {
54
+ const { error: refused } = JSON.parse(error.text);
55
+ return refused?.code === 'expired' && typeof refused.authority === 'string' ? refused.authority : undefined;
56
+ }
57
+ catch {
58
+ return undefined;
59
+ }
60
+ }
61
+ function grouped(items, size) {
62
+ return items.flatMap((_, index) => (index % size === 0 ? [items.slice(index, index + size)] : []));
63
+ }
35
64
  function covers(path, prefix) {
36
65
  return prefix === '' || path === prefix || path.startsWith(`${prefix}/`);
37
66
  }
@@ -40,11 +69,14 @@ function sidecar(path) {
40
69
  segments.splice(-1, 0, PROVENANCE);
41
70
  return segments.join('/');
42
71
  }
43
- function rootOf(copy) {
44
- return copy.private ? '/private' : `/rooms/${copy.room_handle}`;
72
+ function recordOf(key) {
73
+ return join(kitHome(), 'local-copies', `${key}.json`);
74
+ }
75
+ function rootOf(handle) {
76
+ return `/rooms/${handle}`;
45
77
  }
46
78
  function relative(path) {
47
- return path.replace(/^\/private\//, '').replace(/^\/rooms\/[^/]+\//, '');
79
+ return path.replace(/^\/rooms\/[^/]+\//, '');
48
80
  }
49
81
  function declaredWritable(path, copy) {
50
82
  const deepest = (scopes) => Math.max(-1, ...scopes.filter((scope) => covers(path, scope)).map((scope) => scope.length));
@@ -101,7 +133,7 @@ async function ignoreRules(repository, writable, copy) {
101
133
  ].map(pattern);
102
134
  const granted = readOnly.filter((path) => !protectedPath(path)).map(pattern);
103
135
  await ruled(join(repository, '.gitignore'), [
104
- '# 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',
105
137
  '# content: Git does not track them, and House refuses a change to them even',
106
138
  '# when this file is edited or a path is force-added.',
107
139
  '/.gitignore',
@@ -116,7 +148,7 @@ async function ignoreRules(repository, writable, copy) {
116
148
  '',
117
149
  ].join('\n'));
118
150
  await ruled(join(repository, '.ignore'), [
119
- '# 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',
120
152
  '# content that .gitignore keeps out of Git.',
121
153
  `!${PROVENANCE}`,
122
154
  ...[...house, ...granted].map((rule) => `!${rule}`),
@@ -152,19 +184,18 @@ export class LocalCopies {
152
184
  house;
153
185
  copies = new Map();
154
186
  queues = new Map();
155
- delivering = new Set();
156
- selecting = Promise.resolve();
187
+ answering = Promise.resolve();
157
188
  constructor(house) {
158
189
  this.house = house;
159
190
  this.owner = {
160
191
  key: 'owner',
161
192
  submit: async (operation, changes) => {
162
193
  try {
163
- 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 }));
164
195
  return { accepted: outcome.contents };
165
196
  }
166
197
  catch (error) {
167
- if (!(error instanceof HouseRefusal) || error.status >= 500)
198
+ if (!finalRefusal(error))
168
199
  throw error;
169
200
  return {
170
201
  refused: refusalOf(error.text) ?? { code: '', text: `House refused it with ${error.status}`, conflicts: [] },
@@ -176,7 +207,7 @@ export class LocalCopies {
176
207
  async load() {
177
208
  const home = join(kitHome(), 'local-copies');
178
209
  const names = (await readdir(join(home, 'rooms')).catch(() => [])).filter((name) => name.endsWith('.json'));
179
- for (const path of [...names.map((name) => join(home, 'rooms', name)), join(home, 'private.json')]) {
210
+ for (const path of names.map((name) => join(home, 'rooms', name))) {
180
211
  const text = await readFile(path, 'utf8').catch(() => null);
181
212
  if (text === null)
182
213
  continue;
@@ -185,47 +216,75 @@ export class LocalCopies {
185
216
  }
186
217
  }
187
218
  async mapping() {
188
- await this.selecting;
219
+ await this.answering;
189
220
  return [...this.copies.values()]
190
- .filter((copy) => copy.active)
191
- .map((copy) => `${rootOf(copy)}: ${copy.repository}`)
221
+ .filter((copy) => copy.unfollowed === undefined)
222
+ .map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
192
223
  .sort();
193
224
  }
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 = [
202
- ...(chosen?.rooms ?? [])
203
- .filter((room) => room.room_handle !== null)
204
- .map((room) => ({ key: `rooms/${room.room_ref}`, private: false, handle: room.room_handle })),
205
- ...(chosen?.private === true ? [{ key: 'private', private: true, handle: null }] : []),
206
- ];
207
- const keys = new Set(selected.map((scope) => scope.key));
208
- const work = [];
209
- for (const key of this.copies.keys()) {
210
- if (keys.has(key))
211
- continue;
212
- work.push(this.exclusive(key, async () => {
213
- const copy = this.copies.get(key);
214
- if (copy.active)
215
- await this.save({ ...copy, active: false });
216
- }));
217
- }
218
- for (const scope of selected)
219
- work.push(this.exclusive(scope.key, () => this.apply(scope)).catch(logged));
220
- await Promise.all(work);
225
+ connected() {
226
+ return this.selected();
221
227
  }
222
- entries(frame) {
223
- const copy = [...this.copies.values()].find((held) => held.active && held.room_ref === frame.authority);
224
- if (copy === undefined)
225
- return;
226
- if (copy.log_epoch === frame.log_epoch && BigInt(frame.position) <= BigInt(copy.position))
227
- return;
228
- 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
+ });
229
288
  }
230
289
  async observed(report) {
231
290
  const key = await this.keyAt(dirname(report.common));
@@ -255,11 +314,11 @@ export class LocalCopies {
255
314
  async push(cwd, selector, caller) {
256
315
  const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
257
316
  if (located.status !== 0)
258
- return { refused: true, text: `${cwd} is not a House Local copy; run house push inside one` };
317
+ return outside(cwd);
259
318
  const [common, worktree] = located.stdout.toString('utf8').split('\n');
260
319
  const key = await this.keyAt(dirname(common));
261
320
  if (key === undefined)
262
- return { refused: true, text: `${worktree} is not a House Local copy; run house push inside one` };
321
+ return outside(worktree);
263
322
  return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
264
323
  }
265
324
  async keyAt(repository) {
@@ -275,19 +334,12 @@ export class LocalCopies {
275
334
  this.queues.set(key, queued.catch(() => undefined));
276
335
  return queued;
277
336
  }
278
- deliver(key) {
279
- if (this.delivering.has(key))
280
- return;
281
- this.delivering.add(key);
282
- void this.exclusive(key, async () => {
283
- this.delivering.delete(key);
284
- const copy = this.copies.get(key);
285
- if (copy.active)
286
- await this.save(await this.caughtUp(copy));
287
- }).catch(logged);
337
+ holding(keys, work) {
338
+ const [key, ...rest] = keys;
339
+ return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
288
340
  }
289
341
  async save(copy) {
290
- const path = join(kitHome(), 'local-copies', copy.private ? 'private.json' : `${copy.key}.json`);
342
+ const path = recordOf(copy.key);
291
343
  await mkdir(dirname(path), { recursive: true, mode: 0o700 });
292
344
  const temporary = `${path}.${process.pid}`;
293
345
  await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
@@ -295,12 +347,12 @@ export class LocalCopies {
295
347
  this.copies.set(copy.key, copy);
296
348
  return copy;
297
349
  }
298
- async read(selected) {
299
- const prefixes = [rootOf(selected)];
350
+ async read(handle) {
351
+ const prefixes = [rootOf(handle)];
300
352
  const discovery = await this.house.post('/kit/door/discover', { prefixes });
301
353
  let bundle = await this.house.post('/kit/door/bootstrap', { prefixes });
302
354
  if ('transfer' in bundle)
303
- bundle = (await (await fetch(bundle.transfer.url)).json());
355
+ bundle = (await (await fetch(relayed(bundle.transfer.url))).json());
304
356
  const [view] = bundle.scopes;
305
357
  if (view === undefined)
306
358
  return null;
@@ -330,46 +382,13 @@ export class LocalCopies {
330
382
  }
331
383
  return read;
332
384
  }
333
- async apply(scope) {
334
- let copy = this.copies.get(scope.key);
335
- if (copy === undefined || !(await exists(copy.repository))) {
336
- await this.materialize(scope);
337
- return;
338
- }
339
- if (copy.room_handle !== scope.handle)
340
- copy = await this.save({ ...copy, room_handle: scope.handle });
341
- if (!copy.active) {
342
- await this.save(await this.received({ ...copy, active: true }));
343
- return;
344
- }
345
- const discovery = await this.house.post('/kit/door/discover', {
346
- prefixes: [rootOf(copy)],
347
- });
348
- const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
349
- if (authority === undefined)
350
- return;
351
- if (authority.managed_replication !== 'admitted') {
352
- logged(`operation_denied: ${rootOf(copy)} is not synchronized to this Environment`);
353
- return;
354
- }
355
- if (JSON.stringify([authority.operations.write, authority.protected_paths]) !==
356
- JSON.stringify([copy.write, copy.protected])) {
357
- await this.save(await this.received(copy));
358
- return;
359
- }
360
- await this.save(await this.caughtUp(copy));
361
- }
362
- async materialize(scope) {
363
- const read = await this.read({ private: scope.private, room_handle: scope.handle });
364
- if (read === null) {
365
- logged(`House serves nothing of ${rootOf({ private: scope.private, room_handle: scope.handle })} to copy`);
366
- return;
367
- }
368
- const repository = scope.private ? join(localCopies(), 'private') : join(localCopies(), 'rooms', read.anchor);
369
- if (await exists(repository)) {
370
- logged(`${repository} already exists and House Kit leaves it as it is`);
371
- return;
372
- }
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');
389
+ const repository = join(localCopies(), 'rooms', read.anchor);
390
+ if (await exists(repository))
391
+ throw new Error(`${repository} already exists and Kit leaves it as it is`);
373
392
  const staging = join(dirname(repository), `.${randomUUID()}`);
374
393
  await mkdir(staging, { recursive: true });
375
394
  let house;
@@ -377,7 +396,7 @@ export class LocalCopies {
377
396
  try {
378
397
  await git(staging, ['init', '-q', '-b', BRANCH]);
379
398
  house = await importCommits(staging, [
380
- { message: `House ${rootOf({ private: scope.private, room_handle: scope.handle })}`, from: null, files: read.writable },
399
+ { message: `House ${rootOf(handle)}`, from: null, files: read.writable },
381
400
  ]);
382
401
  await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
383
402
  await git(staging, ['reset', '-q', '--hard']);
@@ -390,12 +409,10 @@ export class LocalCopies {
390
409
  throw error;
391
410
  }
392
411
  await this.save({
393
- key: scope.key,
394
- private: scope.private,
412
+ key,
395
413
  room_ref: read.anchor,
396
- room_handle: scope.handle,
414
+ room_handle: handle,
397
415
  repository,
398
- active: true,
399
416
  house,
400
417
  position: read.position,
401
418
  log_epoch: read.log_epoch,
@@ -405,13 +422,14 @@ export class LocalCopies {
405
422
  manifests: { [house]: { parent: null, files: read.revisions } },
406
423
  moves: [],
407
424
  });
425
+ return `${rootOf(handle)}: copied to ${repository}`;
408
426
  }
409
427
  async received(copy) {
410
- const read = await this.read(copy);
428
+ const read = await this.read(copy.room_handle);
411
429
  if (read === null)
412
430
  return copy;
413
431
  const house = await importCommits(copy.repository, [
414
- { message: `House ${rootOf(copy)} at position ${read.position}`, from: copy.house, replaces: true, files: read.writable },
432
+ { message: `House ${rootOf(copy.room_handle)} at position ${read.position}`, from: copy.house, replaces: true, files: read.writable },
415
433
  ]);
416
434
  const removed = copy.read_only.filter((path) => !read.readOnly.has(path));
417
435
  const readOnly = await readOnlyPlaced(copy.repository, copy.read_only, read.readOnly, removed);
@@ -428,30 +446,57 @@ export class LocalCopies {
428
446
  await ignoreRules(copy.repository, [...read.writable.keys()], next);
429
447
  return next;
430
448
  }
431
- async caughtUp(start) {
432
- let copy = start;
433
- 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) {
434
473
  let answer;
435
474
  try {
436
475
  answer = await this.house.post('/kit/door/enumerate', {
437
- prefixes: [rootOf(copy)],
438
- 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 })),
439
478
  });
440
479
  }
441
480
  catch (error) {
442
- const code = error instanceof HouseRefusal ? (refusalOf(error.text)?.code ?? null) : null;
443
- if (code === 'position_expired')
444
- return this.received(copy);
445
- if (code !== 'replica_behind')
481
+ const expired = asking.find((copy) => copy.room_ref === expiredAuthority(error));
482
+ if (expired === undefined)
446
483
  throw error;
447
- setTimeout(() => this.deliver(copy.key), REPLICA_RETRY_MS).unref();
448
- return copy;
484
+ caught.set(expired.key, await this.save(await this.received(expired)));
485
+ asking = asking.filter((copy) => copy !== expired);
486
+ continue;
449
487
  }
450
- const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
451
- if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
452
- return copy;
453
- 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 : [];
454
498
  }
499
+ return copies.map((copy) => caught.get(copy.key));
455
500
  }
456
501
  async incorporated(copy, changes, position, logEpoch) {
457
502
  const houseTree = await tree(copy.repository, copy.house);
@@ -484,7 +529,7 @@ export class LocalCopies {
484
529
  let { house, manifests } = copy;
485
530
  if (files.size > 0) {
486
531
  house = await importCommits(copy.repository, [
487
- { message: `House ${rootOf(copy)} at position ${position}`, from: copy.house, files },
532
+ { message: `House ${rootOf(copy.room_handle)} at position ${position}`, from: copy.house, files },
488
533
  ]);
489
534
  manifests = { ...manifests, [house]: { parent: copy.house, files: revisions } };
490
535
  }
@@ -514,9 +559,6 @@ export class LocalCopies {
514
559
  }
515
560
  copy = await this.save(without(copy));
516
561
  }
517
- if (!copy.active) {
518
- return { refused: true, text: `${rootOf(copy)} is no longer synced to this Environment, so House takes no push from it` };
519
- }
520
562
  const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
521
563
  const base = listed.find((ancestor) => ancestor in copy.manifests);
522
564
  if (base === undefined) {
@@ -527,7 +569,7 @@ export class LocalCopies {
527
569
  changes = await this.derived(copy, base, commit);
528
570
  }
529
571
  catch (error) {
530
- return { refused: true, text: causeOf(error) };
572
+ return { refused: true, text: houseCause(error) };
531
573
  }
532
574
  if (changes.length === 0)
533
575
  return { refused: false, text: `Nothing to submit: ${short(commit)} matches House.` };
@@ -549,12 +591,23 @@ export class LocalCopies {
549
591
  return this.settle(submitted, caller);
550
592
  }
551
593
  async derived(copy, base, commit) {
552
- const root = rootOf(copy);
594
+ const root = rootOf(copy.room_handle);
553
595
  const before = await tree(copy.repository, base);
554
596
  const after = await tree(copy.repository, commit);
555
597
  const moved = movedPaths(copy.moves, before, after);
556
598
  const arrived = new Set(moved.values());
557
- 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);
558
611
  const changes = [];
559
612
  for (const path of [...new Set([...before.keys(), ...after.keys()])].sort()) {
560
613
  if (arrived.has(path))
@@ -567,7 +620,7 @@ export class LocalCopies {
567
620
  changes.push({ op: 'rename', path: `${root}/${path}`, to: `${root}/${to}`, base: known });
568
621
  const moving = after.get(to);
569
622
  if (moving !== prior) {
570
- 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) });
571
624
  }
572
625
  continue;
573
626
  }
@@ -576,9 +629,9 @@ export class LocalCopies {
576
629
  if (next === undefined)
577
630
  changes.push({ op: 'remove', path: `${root}/${path}`, base: known });
578
631
  else if (prior === undefined)
579
- changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next, path) });
632
+ changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next) });
580
633
  else
581
- 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)));
582
635
  }
583
636
  return changes;
584
637
  }
@@ -605,12 +658,14 @@ export class LocalCopies {
605
658
  const { upload, ...unstaged } = copy.push;
606
659
  return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
607
660
  }
608
- if (outcome.refused.code === 'operation_expired')
661
+ if (outcome.refused.code === 'expired')
609
662
  return this.recovered(copy);
610
- const rebased = rebaseFixes(outcome.refused);
611
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);
612
667
  try {
613
- settled = await this.save(await this.caughtUp(settled));
668
+ settled = (await this.caughtUp([settled], true))[0];
614
669
  }
615
670
  catch {
616
671
  return {
@@ -629,9 +684,9 @@ export class LocalCopies {
629
684
  }
630
685
  async recovered(copy) {
631
686
  const push = copy.push;
632
- const current = await this.save(await this.caughtUp(copy));
687
+ const current = (await this.caughtUp([copy], true))[0];
633
688
  const house = await tree(copy.repository, current.house);
634
- const root = rootOf(current);
689
+ const root = rootOf(current.room_handle);
635
690
  if (!(await this.landed(copy.repository, push.changes, house, await tree(copy.repository, push.commit)))) {
636
691
  await this.save(without(current));
637
692
  return {
@@ -645,7 +700,7 @@ export class LocalCopies {
645
700
  written.push({
646
701
  path: `${root}/${path}`,
647
702
  revision: entry === undefined ? null : (revisionAt(current, current.house, path) ?? null),
648
- content: entry === undefined ? null : await blobText(copy.repository, entry, path),
703
+ content: entry === undefined ? null : await blobText(copy.repository, entry),
649
704
  });
650
705
  }
651
706
  return this.finished(await this.save({ ...current, push: { ...push, state: 'accepted', written } }));
@@ -673,7 +728,7 @@ export class LocalCopies {
673
728
  const push = copy.push;
674
729
  let written = push.written;
675
730
  if (push.changes.some((change) => change.op === 'rename')) {
676
- copy = await this.save(await this.caughtUp(copy));
731
+ copy = (await this.caughtUp([copy], true))[0];
677
732
  const house = await tree(copy.repository, copy.house);
678
733
  const sent = await tree(copy.repository, push.house);
679
734
  if (push.changes.some((change) => change.op === 'rename' && !house.has(relative(change.to)))) {
@@ -687,9 +742,9 @@ export class LocalCopies {
687
742
  written = [
688
743
  ...written,
689
744
  {
690
- path: `${rootOf(copy)}/${path}`,
745
+ path: `${rootOf(copy.room_handle)}/${path}`,
691
746
  revision: entry === undefined ? null : (revisionAt(copy, copy.house, path) ?? null),
692
- content: entry === undefined ? null : await blobText(copy.repository, entry, path),
747
+ content: entry === undefined ? null : await blobText(copy.repository, entry),
693
748
  },
694
749
  ];
695
750
  }
@@ -707,7 +762,7 @@ export class LocalCopies {
707
762
  }
708
763
  const edits = new Map([...files].filter(([path, content]) => (content === null ? selected.has(path) : selected.get(path) !== blobId(content))));
709
764
  const received = [...files.keys()].filter((path) => revisionAt(copy, copy.house, path) === revisionAt(copy, push.house, path));
710
- const accepted = `House ${rootOf(copy)} with ${push.commit} accepted`;
765
+ const accepted = `House ${rootOf(copy.room_handle)} with ${push.commit} accepted`;
711
766
  const house = await importCommits(copy.repository, [
712
767
  ...(edits.size === 0 ? [] : [{ message: accepted, from: push.commit, files: edits }]),
713
768
  {
@@ -734,7 +789,7 @@ export class LocalCopies {
734
789
  async landed(repository, changes, house, selected) {
735
790
  const text = async (path) => {
736
791
  const entry = house.get(relative(path));
737
- return entry === undefined ? undefined : blobText(repository, entry, path);
792
+ return entry === undefined ? undefined : blobText(repository, entry);
738
793
  };
739
794
  for (const change of changes) {
740
795
  const current = await text(change.path);
package/dist/end.js ADDED
@@ -0,0 +1,33 @@
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
+ function updateLine(lead) {
18
+ const command = installCommand();
19
+ return command === null ? `${lead}; install it again.` : `${lead}. Update it: ${command}`;
20
+ }
21
+ export function updateRequired() {
22
+ end(updateLine('This Kit is too old for House'));
23
+ }
24
+ export function unknownMethod(kind) {
25
+ return updateLine(`This Kit does not know ${kind}`);
26
+ }
27
+ export function newerThanHouse(release, command) {
28
+ end(`Kit ${KIT_RELEASE} is newer than this Building supports (Kit ${release}). Install Kit ${release}: ${command}`);
29
+ }
30
+ export async function signInAgain(rejected) {
31
+ await forgetRejected(rejected);
32
+ restart('sign in again');
33
+ }
@@ -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
+ });