@agentshouse/kit 0.2.1-c5262a8cc063d → 0.2.1-c5883630417e1

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