@agentshouse/kit 0.2.1-cbabe8ad337ff → 0.2.1-cbdf76b9a90a3

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 (98) hide show
  1. package/README.md +30 -29
  2. package/dist/acp.js +34 -14
  3. package/dist/agents.js +109 -22
  4. package/dist/api.js +6 -2
  5. package/dist/app-git.js +10 -26
  6. package/dist/bootstrap.json +4 -4
  7. package/dist/bridge.js +10 -24
  8. package/dist/chatgpt.js +32 -0
  9. package/dist/clis.js +51 -9
  10. package/dist/conversations.js +289 -99
  11. package/dist/copies.js +195 -119
  12. package/dist/end.js +22 -3
  13. package/dist/enrol-main.js +7 -1
  14. package/dist/events.js +4 -0
  15. package/dist/files.js +8 -2
  16. package/dist/git.js +10 -8
  17. package/dist/home.js +96 -7
  18. package/dist/house-cli.js +291 -0
  19. package/dist/house-main.js +7 -177
  20. package/dist/inputs.js +63 -0
  21. package/dist/instructions.js +10 -32
  22. package/dist/kit-cli.js +68 -0
  23. package/dist/kit-main.js +8 -22
  24. package/dist/login.js +112 -22
  25. package/dist/logout.js +0 -2
  26. package/dist/parts.js +34 -16
  27. package/dist/placement.js +39 -0
  28. package/dist/relay.js +12 -2
  29. package/dist/resident.js +82 -26
  30. package/dist/scope.js +3 -0
  31. package/dist/service-main.js +9 -0
  32. package/dist/service.js +55 -0
  33. package/dist/sign-in.js +5 -23
  34. package/dist/skills.js +81 -12
  35. package/dist/stream.js +68 -17
  36. package/dist/transcription.js +3 -3
  37. package/dist/translate.js +2 -8
  38. package/dist/update.js +48 -29
  39. package/dist/wire.js +22 -0
  40. package/dist/work.js +3 -5
  41. package/how-we-work.md +13 -3
  42. package/package.json +3 -2
  43. package/skills/ask-matt/SKILL.md +17 -11
  44. package/skills/ask-matt/agents/openai.yaml +1 -0
  45. package/skills/code-review/SKILL.md +5 -5
  46. package/skills/codebase-design/DESIGN-IT-TWICE.md +1 -1
  47. package/skills/codebase-design/SKILL.md +0 -2
  48. package/skills/diagnosing-bugs/SKILL.md +2 -4
  49. package/skills/domain-modeling/{CONTEXT-FORMAT.md → GLOSSARY-FORMAT.md} +10 -10
  50. package/skills/domain-modeling/SKILL.md +12 -14
  51. package/skills/grill-me/SKILL.md +1 -2
  52. package/skills/grill-me/agents/openai.yaml +1 -0
  53. package/skills/grill-with-docs/SKILL.md +1 -2
  54. package/skills/grill-with-docs/agents/openai.yaml +1 -0
  55. package/skills/grilling/SKILL.md +2 -2
  56. package/skills/handoff/SKILL.md +2 -3
  57. package/skills/handoff/agents/openai.yaml +1 -0
  58. package/skills/implement/SKILL.md +5 -4
  59. package/skills/implement/agents/openai.yaml +1 -0
  60. package/skills/implement-spec/SKILL.md +40 -0
  61. package/skills/implement-spec/agents/openai.yaml +5 -0
  62. package/skills/improve-codebase-architecture/SKILL.md +6 -7
  63. package/skills/improve-codebase-architecture/agents/openai.yaml +1 -0
  64. package/skills/pr/CREDITS.md +3 -0
  65. package/skills/pr/LICENSE +21 -0
  66. package/skills/pr/SKILL.md +170 -0
  67. package/skills/pr/agents/openai.yaml +3 -0
  68. package/skills/prototype/SKILL.md +0 -2
  69. package/skills/research/SKILL.md +0 -2
  70. package/skills/retro/LICENSE +21 -0
  71. package/skills/retro/SKILL.md +44 -0
  72. package/skills/retro/agents/openai.yaml +5 -0
  73. package/skills/tdd/SKILL.md +2 -4
  74. package/skills/teach/SKILL.md +3 -4
  75. package/skills/teach/agents/openai.yaml +1 -0
  76. package/skills/to-questionnaire/SKILL.md +1 -2
  77. package/skills/to-questionnaire/agents/openai.yaml +1 -0
  78. package/skills/to-spec/SKILL.md +1 -2
  79. package/skills/to-spec/agents/openai.yaml +1 -0
  80. package/skills/to-tickets/SKILL.md +3 -4
  81. package/skills/to-tickets/agents/openai.yaml +1 -0
  82. package/skills/triage/AGENT-BRIEF.md +207 -0
  83. package/skills/triage/LICENSE +21 -0
  84. package/skills/triage/OUT-OF-SCOPE.md +105 -0
  85. package/skills/triage/SKILL.md +112 -0
  86. package/skills/triage/agents/openai.yaml +5 -0
  87. package/skills/wait-what/SKILL.md +2 -3
  88. package/skills/wait-what/agents/openai.yaml +1 -0
  89. package/skills/wayfinder/SKILL.md +5 -6
  90. package/skills/wayfinder/agents/openai.yaml +1 -0
  91. package/skills/wizard/LICENSE +21 -0
  92. package/skills/wizard/SKILL.md +44 -0
  93. package/skills/wizard/agents/openai.yaml +3 -0
  94. package/skills/wizard/template.sh +223 -0
  95. package/skills/writing-for-agents/SKILL.md +0 -2
  96. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  97. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  98. /package/skills/{resolving-merge-conflicts → implement-spec}/LICENSE +0 -0
package/dist/copies.js CHANGED
@@ -3,7 +3,7 @@ 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
5
  import { finalRefusal, HouseRefusal, retrying } from './api.js';
6
- import { blobId, blobText, git, HOUSE_REF, importCommits, localCopies, revision, run, succeeded, tree, } from './git.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';
@@ -15,6 +15,9 @@ const IDENTITY = { GIT_COMMITTER_NAME: 'House Kit', GIT_COMMITTER_EMAIL: 'house-
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,6 +69,9 @@ function sidecar(path) {
40
69
  segments.splice(-1, 0, PROVENANCE);
41
70
  return segments.join('/');
42
71
  }
72
+ function recordOf(key) {
73
+ return join(kitHome(), 'local-copies', `${key}.json`);
74
+ }
43
75
  function rootOf(handle) {
44
76
  return `/rooms/${handle}`;
45
77
  }
@@ -152,8 +184,7 @@ 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 = {
@@ -185,42 +216,81 @@ 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)
221
+ .filter((copy) => copy.erasing === undefined)
191
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 = (chosen?.rooms ?? []).flatMap((room) => room.room_handle === null ? [] : [{ key: `rooms/${room.room_ref}`, handle: room.room_handle }]);
202
- const keys = new Set(selected.map((scope) => scope.key));
203
- const work = [];
204
- for (const key of this.copies.keys()) {
205
- if (keys.has(key))
206
- continue;
207
- work.push(this.exclusive(key, async () => {
208
- const copy = this.copies.get(key);
209
- if (copy.active)
210
- await this.save({ ...copy, active: false });
211
- }));
212
- }
213
- for (const scope of selected)
214
- work.push(this.exclusive(scope.key, () => this.apply(scope)).catch(logged));
215
- await Promise.all(work);
225
+ connected() {
226
+ return this.selected();
216
227
  }
217
- entries(frame) {
218
- const copy = [...this.copies.values()].find((held) => held.active && held.room_ref === frame.authority);
219
- if (copy === undefined)
220
- return;
221
- if (copy.log_epoch === frame.log_epoch && BigInt(frame.position) <= BigInt(copy.position))
222
- return;
223
- this.deliver(copy.key);
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
+ for (const copy of held()) {
254
+ const returned = synced.has(copy.key);
255
+ if (returned && copy.erasing === undefined)
256
+ continue;
257
+ try {
258
+ await this.erase(copy);
259
+ said.push(`${rootOf(copy.room_handle)}: ${returned ? '' : 'no longer synced to this Environment; '}erased from the Local copy`);
260
+ }
261
+ catch (error) {
262
+ refused = true;
263
+ said.push(`${rootOf(copy.room_handle)}: ${causeOf(error)}`);
264
+ }
265
+ }
266
+ const current = [];
267
+ for (const [key, handle] of synced) {
268
+ try {
269
+ const held = this.copies.get(key);
270
+ if (held?.erasing)
271
+ continue;
272
+ if (held === undefined || !(await exists(held.repository))) {
273
+ said.push(await this.materialize(key, handle));
274
+ continue;
275
+ }
276
+ current.push(held.room_handle === handle ? held : await this.save({ ...held, room_handle: handle }));
277
+ }
278
+ catch (error) {
279
+ refused = true;
280
+ said.push(`${rootOf(handle)}: ${causeOf(error)}`);
281
+ }
282
+ }
283
+ if (refreshing && current.length > 0) {
284
+ const failed = await this.refreshed(current).then(() => null, houseCause);
285
+ for (const copy of current)
286
+ said.push(...moved(copy, this.copies.get(copy.key)));
287
+ if (failed !== null) {
288
+ refused = true;
289
+ said.push(failed);
290
+ }
291
+ }
292
+ return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
293
+ });
224
294
  }
225
295
  async observed(report) {
226
296
  const key = await this.keyAt(dirname(report.common));
@@ -228,6 +298,8 @@ export class LocalCopies {
228
298
  return;
229
299
  await this.exclusive(key, async () => {
230
300
  const copy = this.copies.get(key);
301
+ if (copy === undefined || copy.erasing)
302
+ return;
231
303
  let moves = copy.moves;
232
304
  for (const { from, to } of report.moves) {
233
305
  const held = moves.find((move) => move.worktree === report.worktree && move.to === from);
@@ -250,11 +322,11 @@ export class LocalCopies {
250
322
  async push(cwd, selector, caller) {
251
323
  const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
252
324
  if (located.status !== 0)
253
- return { refused: true, text: `${cwd} is not a House Local copy; run house push inside one` };
325
+ return outside(cwd);
254
326
  const [common, worktree] = located.stdout.toString('utf8').split('\n');
255
327
  const key = await this.keyAt(dirname(common));
256
328
  if (key === undefined)
257
- return { refused: true, text: `${worktree} is not a House Local copy; run house push inside one` };
329
+ return outside(worktree);
258
330
  return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
259
331
  }
260
332
  async keyAt(repository) {
@@ -270,19 +342,12 @@ export class LocalCopies {
270
342
  this.queues.set(key, queued.catch(() => undefined));
271
343
  return queued;
272
344
  }
273
- deliver(key) {
274
- if (this.delivering.has(key))
275
- return;
276
- this.delivering.add(key);
277
- void this.exclusive(key, async () => {
278
- this.delivering.delete(key);
279
- const copy = this.copies.get(key);
280
- if (copy.active)
281
- await this.save(await this.caughtUp(copy));
282
- }).catch(logged);
345
+ holding(keys, work) {
346
+ const [key, ...rest] = keys;
347
+ return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
283
348
  }
284
349
  async save(copy) {
285
- const path = join(kitHome(), 'local-copies', `${copy.key}.json`);
350
+ const path = recordOf(copy.key);
286
351
  await mkdir(dirname(path), { recursive: true, mode: 0o700 });
287
352
  const temporary = `${path}.${process.pid}`;
288
353
  await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
@@ -290,6 +355,12 @@ export class LocalCopies {
290
355
  this.copies.set(copy.key, copy);
291
356
  return copy;
292
357
  }
358
+ async erase(copy) {
359
+ await this.save({ ...copy, erasing: true });
360
+ await rm(copy.repository, { recursive: true, force: true });
361
+ await rm(recordOf(copy.key), { force: true });
362
+ this.copies.delete(copy.key);
363
+ }
293
364
  async read(handle) {
294
365
  const prefixes = [rootOf(handle)];
295
366
  const discovery = await this.house.post('/kit/door/discover', { prefixes });
@@ -325,46 +396,13 @@ export class LocalCopies {
325
396
  }
326
397
  return read;
327
398
  }
328
- async apply(scope) {
329
- let copy = this.copies.get(scope.key);
330
- if (copy === undefined || !(await exists(copy.repository))) {
331
- await this.materialize(scope);
332
- return;
333
- }
334
- if (copy.room_handle !== scope.handle)
335
- copy = await this.save({ ...copy, room_handle: scope.handle });
336
- if (!copy.active) {
337
- await this.save(await this.received({ ...copy, active: true }));
338
- return;
339
- }
340
- const discovery = await this.house.post('/kit/door/discover', {
341
- prefixes: [rootOf(copy.room_handle)],
342
- });
343
- const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
344
- if (authority === undefined)
345
- return;
346
- if (authority.managed_replication !== 'admitted') {
347
- logged(`operation_denied: ${rootOf(copy.room_handle)} is not synchronized to this Environment`);
348
- return;
349
- }
350
- if (JSON.stringify([authority.operations.write, authority.protected_paths]) !==
351
- JSON.stringify([copy.write, copy.protected])) {
352
- await this.save(await this.received(copy));
353
- return;
354
- }
355
- await this.save(await this.caughtUp(copy));
356
- }
357
- async materialize(scope) {
358
- const read = await this.read(scope.handle);
359
- if (read === null) {
360
- logged(`House serves nothing of ${rootOf(scope.handle)} to copy`);
361
- return;
362
- }
399
+ async materialize(key, handle) {
400
+ const read = await this.read(handle);
401
+ if (read === null)
402
+ throw new Error('House serves nothing of it to copy');
363
403
  const repository = join(localCopies(), 'rooms', read.anchor);
364
- if (await exists(repository)) {
365
- logged(`${repository} already exists and House Kit leaves it as it is`);
366
- return;
367
- }
404
+ if (await exists(repository))
405
+ throw new Error(`${repository} already exists and House Kit leaves it as it is`);
368
406
  const staging = join(dirname(repository), `.${randomUUID()}`);
369
407
  await mkdir(staging, { recursive: true });
370
408
  let house;
@@ -372,7 +410,7 @@ export class LocalCopies {
372
410
  try {
373
411
  await git(staging, ['init', '-q', '-b', BRANCH]);
374
412
  house = await importCommits(staging, [
375
- { message: `House ${rootOf(scope.handle)}`, from: null, files: read.writable },
413
+ { message: `House ${rootOf(handle)}`, from: null, files: read.writable },
376
414
  ]);
377
415
  await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
378
416
  await git(staging, ['reset', '-q', '--hard']);
@@ -385,11 +423,10 @@ export class LocalCopies {
385
423
  throw error;
386
424
  }
387
425
  await this.save({
388
- key: scope.key,
426
+ key,
389
427
  room_ref: read.anchor,
390
- room_handle: scope.handle,
428
+ room_handle: handle,
391
429
  repository,
392
- active: true,
393
430
  house,
394
431
  position: read.position,
395
432
  log_epoch: read.log_epoch,
@@ -399,6 +436,7 @@ export class LocalCopies {
399
436
  manifests: { [house]: { parent: null, files: read.revisions } },
400
437
  moves: [],
401
438
  });
439
+ return `${rootOf(handle)}: copied to ${repository}`;
402
440
  }
403
441
  async received(copy) {
404
442
  const read = await this.read(copy.room_handle);
@@ -422,29 +460,57 @@ export class LocalCopies {
422
460
  await ignoreRules(copy.repository, [...read.writable.keys()], next);
423
461
  return next;
424
462
  }
425
- async caughtUp(start) {
426
- let copy = start;
427
- for (;;) {
463
+ async refreshed(copies) {
464
+ const discover = (group) => this.house.post('/kit/door/discover', { prefixes: group.map((copy) => rootOf(copy.room_handle)) });
465
+ const [first, ...rest] = copies;
466
+ const { bounds, authorities } = await discover([first]);
467
+ for (const group of grouped(rest, bounds.max_prefixes))
468
+ authorities.push(...(await discover(group)).authorities);
469
+ const unchanged = [];
470
+ for (const copy of copies) {
471
+ const authority = authorities.find((entry) => entry.room_ref === copy.room_ref);
472
+ if (authority !== undefined &&
473
+ JSON.stringify([authority.operations.write, authority.protected_paths]) !== JSON.stringify([copy.write, copy.protected])) {
474
+ await this.save(await this.received(copy));
475
+ }
476
+ else {
477
+ unchanged.push(copy);
478
+ }
479
+ }
480
+ for (const group of grouped(unchanged, bounds.max_prefixes))
481
+ await this.caughtUp(group, false);
482
+ }
483
+ async caughtUp(copies, whole) {
484
+ const caught = new Map(copies.map((copy) => [copy.key, copy]));
485
+ let asking = [...copies];
486
+ while (asking.length > 0) {
428
487
  let answer;
429
488
  try {
430
489
  answer = await this.house.post('/kit/door/enumerate', {
431
- prefixes: [rootOf(copy.room_handle)],
432
- positions: [{ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch }],
490
+ prefixes: asking.map((copy) => rootOf(copy.room_handle)),
491
+ positions: asking.map((copy) => ({ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch })),
433
492
  });
434
493
  }
435
494
  catch (error) {
436
- const code = error instanceof HouseRefusal ? (refusalOf(error.text)?.code ?? null) : null;
437
- if (code === 'position_expired')
438
- return this.received(copy);
439
- if (code !== 'replica_behind')
495
+ const expired = asking.find((copy) => copy.room_ref === expiredAuthority(error));
496
+ if (expired === undefined)
440
497
  throw error;
441
- return copy;
498
+ caught.set(expired.key, await this.save(await this.received(expired)));
499
+ asking = asking.filter((copy) => copy !== expired);
500
+ continue;
501
+ }
502
+ const advanced = [];
503
+ for (const copy of asking) {
504
+ const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
505
+ if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
506
+ continue;
507
+ const next = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
508
+ caught.set(copy.key, next);
509
+ advanced.push(next);
442
510
  }
443
- const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
444
- if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
445
- return copy;
446
- copy = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
511
+ asking = whole ? advanced : [];
447
512
  }
513
+ return copies.map((copy) => caught.get(copy.key));
448
514
  }
449
515
  async incorporated(copy, changes, position, logEpoch) {
450
516
  const houseTree = await tree(copy.repository, copy.house);
@@ -494,6 +560,8 @@ export class LocalCopies {
494
560
  }
495
561
  async pushed(key, worktree, selector, caller) {
496
562
  let copy = this.copies.get(key);
563
+ if (copy === undefined || copy.erasing)
564
+ return outside(worktree);
497
565
  if (copy.push?.state === 'submitted')
498
566
  return this.settle(copy, caller);
499
567
  const selected = await run(worktree, ['rev-parse', '--verify', '--quiet', '--end-of-options', `${selector}^{commit}`]);
@@ -507,9 +575,6 @@ export class LocalCopies {
507
575
  }
508
576
  copy = await this.save(without(copy));
509
577
  }
510
- if (!copy.active) {
511
- return { refused: true, text: `${rootOf(copy.room_handle)} is no longer synced to this Environment, so House takes no push from it` };
512
- }
513
578
  const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
514
579
  const base = listed.find((ancestor) => ancestor in copy.manifests);
515
580
  if (base === undefined) {
@@ -520,7 +585,7 @@ export class LocalCopies {
520
585
  changes = await this.derived(copy, base, commit);
521
586
  }
522
587
  catch (error) {
523
- return { refused: true, text: causeOf(error) };
588
+ return { refused: true, text: houseCause(error) };
524
589
  }
525
590
  if (changes.length === 0)
526
591
  return { refused: false, text: `Nothing to submit: ${short(commit)} matches House.` };
@@ -547,7 +612,18 @@ export class LocalCopies {
547
612
  const after = await tree(copy.repository, commit);
548
613
  const moved = movedPaths(copy.moves, before, after);
549
614
  const arrived = new Set(moved.values());
550
- const text = (entry, path) => blobText(copy.repository, entry, path);
615
+ const arriving = [...after].filter(([path, entry]) => before.get(path) !== entry);
616
+ if (arriving.length > 0) {
617
+ const { bounds } = await this.house.post('/kit/door/discover', { prefixes: [root] });
618
+ const binary = [];
619
+ for (const [path, entry] of arriving) {
620
+ if (!(await isText(copy.repository, entry, bounds.max_authored_file_bytes)))
621
+ binary.push(path);
622
+ }
623
+ if (binary.length > 0)
624
+ throw new Error(textOnly(binary));
625
+ }
626
+ const text = (entry) => blobText(copy.repository, entry);
551
627
  const changes = [];
552
628
  for (const path of [...new Set([...before.keys(), ...after.keys()])].sort()) {
553
629
  if (arrived.has(path))
@@ -560,7 +636,7 @@ export class LocalCopies {
560
636
  changes.push({ op: 'rename', path: `${root}/${path}`, to: `${root}/${to}`, base: known });
561
637
  const moving = after.get(to);
562
638
  if (moving !== prior) {
563
- changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving, to) });
639
+ changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving) });
564
640
  }
565
641
  continue;
566
642
  }
@@ -569,9 +645,9 @@ export class LocalCopies {
569
645
  if (next === undefined)
570
646
  changes.push({ op: 'remove', path: `${root}/${path}`, base: known });
571
647
  else if (prior === undefined)
572
- changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next, path) });
648
+ changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next) });
573
649
  else
574
- changes.push(...changed(`${root}/${path}`, path, known, await text(prior, path), await text(next, path)));
650
+ changes.push(...changed(`${root}/${path}`, path, known, await text(prior), await text(next)));
575
651
  }
576
652
  return changes;
577
653
  }
@@ -598,12 +674,12 @@ export class LocalCopies {
598
674
  const { upload, ...unstaged } = copy.push;
599
675
  return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
600
676
  }
601
- if (outcome.refused.code === 'operation_expired')
677
+ if (outcome.refused.code === 'expired')
602
678
  return this.recovered(copy);
603
679
  const rebased = rebaseFixes(outcome.refused);
604
680
  let settled = await this.save(without(copy));
605
681
  try {
606
- settled = await this.save(await this.caughtUp(settled));
682
+ settled = (await this.caughtUp([settled], true))[0];
607
683
  }
608
684
  catch {
609
685
  return {
@@ -622,7 +698,7 @@ export class LocalCopies {
622
698
  }
623
699
  async recovered(copy) {
624
700
  const push = copy.push;
625
- const current = await this.save(await this.caughtUp(copy));
701
+ const current = (await this.caughtUp([copy], true))[0];
626
702
  const house = await tree(copy.repository, current.house);
627
703
  const root = rootOf(current.room_handle);
628
704
  if (!(await this.landed(copy.repository, push.changes, house, await tree(copy.repository, push.commit)))) {
@@ -638,7 +714,7 @@ export class LocalCopies {
638
714
  written.push({
639
715
  path: `${root}/${path}`,
640
716
  revision: entry === undefined ? null : (revisionAt(current, current.house, path) ?? null),
641
- content: entry === undefined ? null : await blobText(copy.repository, entry, path),
717
+ content: entry === undefined ? null : await blobText(copy.repository, entry),
642
718
  });
643
719
  }
644
720
  return this.finished(await this.save({ ...current, push: { ...push, state: 'accepted', written } }));
@@ -666,7 +742,7 @@ export class LocalCopies {
666
742
  const push = copy.push;
667
743
  let written = push.written;
668
744
  if (push.changes.some((change) => change.op === 'rename')) {
669
- copy = await this.save(await this.caughtUp(copy));
745
+ copy = (await this.caughtUp([copy], true))[0];
670
746
  const house = await tree(copy.repository, copy.house);
671
747
  const sent = await tree(copy.repository, push.house);
672
748
  if (push.changes.some((change) => change.op === 'rename' && !house.has(relative(change.to)))) {
@@ -682,7 +758,7 @@ export class LocalCopies {
682
758
  {
683
759
  path: `${rootOf(copy.room_handle)}/${path}`,
684
760
  revision: entry === undefined ? null : (revisionAt(copy, copy.house, path) ?? null),
685
- content: entry === undefined ? null : await blobText(copy.repository, entry, path),
761
+ content: entry === undefined ? null : await blobText(copy.repository, entry),
686
762
  },
687
763
  ];
688
764
  }
@@ -727,7 +803,7 @@ export class LocalCopies {
727
803
  async landed(repository, changes, house, selected) {
728
804
  const text = async (path) => {
729
805
  const entry = house.get(relative(path));
730
- return entry === undefined ? undefined : blobText(repository, entry, path);
806
+ return entry === undefined ? undefined : blobText(repository, entry);
731
807
  };
732
808
  for (const change of changes) {
733
809
  const current = await text(change.path);
package/dist/end.js CHANGED
@@ -1,8 +1,27 @@
1
+ import { KIT_VERSION } from './clis.js';
2
+ import { forgetRejected } from './home.js';
3
+ import { installCommand } from './placement.js';
1
4
  import { killDescendants } from './scope.js';
2
5
  export const REPLACED = 'another Kit started for this Environment';
3
- export const SIGN_IN_AGAIN = 'sign in again';
4
- export function end(reason) {
6
+ function exit(reason, status) {
5
7
  process.stderr.write(`kit: ${reason}\n`);
6
8
  killDescendants();
7
- process.exit(0);
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_VERSION} 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');
8
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
+ });
package/dist/events.js CHANGED
@@ -512,6 +512,10 @@ class GrokReader extends Reader {
512
512
  return [];
513
513
  }
514
514
  }
515
+ export function answeredFailure(answer) {
516
+ const failure = answer._meta?.jetbrains?.air?.sessionFailure;
517
+ return typeof failure?.title === 'string' ? { title: failure.title, limit: failure.category === 'limit' } : null;
518
+ }
515
519
  export function readerFor(kind) {
516
520
  if (kind === 'claude-agent-acp')
517
521
  return new ClaudeReader();
package/dist/files.js CHANGED
@@ -1,11 +1,17 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { mkdir, writeFile } from 'node:fs/promises';
2
3
  import { join } from 'node:path';
3
4
  import { HouseRefusal, retrying } from './api.js';
4
5
  import { relayed } from './relay.js';
6
+ export function verified(bytes, sha256, name) {
7
+ if (createHash('sha256').update(bytes).digest('hex') !== sha256)
8
+ throw new Error(`${name} did not arrive whole; download it again`);
9
+ return bytes;
10
+ }
5
11
  export async function placeFiles(house, directory, files, signal) {
6
12
  const paths = [];
7
13
  for (const file of files) {
8
- const original = await house.post('/kit/conversation/originals/get', { version: file.version, download: true }, signal);
14
+ const original = await retrying(() => house.post('/kit/conversation/originals/get', { version: file.version, download: true }, signal), signal);
9
15
  const answer = await fetch(relayed(original.download.url), { signal });
10
16
  if (!answer.ok)
11
17
  throw new Error(`${file.name} did not download: ${answer.status}`);
@@ -16,7 +22,7 @@ export async function placeFiles(house, directory, files, signal) {
16
22
  });
17
23
  await mkdir(folder, { recursive: true });
18
24
  const path = join(folder, file.name);
19
- await writeFile(path, Buffer.from(await answer.arrayBuffer()));
25
+ await writeFile(path, verified(Buffer.from(await answer.arrayBuffer()), file.sha256, file.name));
20
26
  paths.push(path);
21
27
  }
22
28
  return paths;
package/dist/git.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { isUtf8 } from 'node:buffer';
2
3
  import { createHash } from 'node:crypto';
3
4
  import { join } from 'node:path';
4
5
  import { withoutProxy } from './clis.js';
@@ -48,14 +49,15 @@ export async function tree(repository, commit) {
48
49
  }
49
50
  return entries;
50
51
  }
51
- export async function blobText(repository, object, path) {
52
- const bytes = (await run(repository, ['cat-file', 'blob', object])).stdout;
53
- try {
54
- return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
55
- }
56
- catch {
57
- throw new Error(`${path} is binary and House stores only text; remove it from the commit`);
58
- }
52
+ async function blob(repository, object) {
53
+ return (await run(repository, ['cat-file', 'blob', object])).stdout;
54
+ }
55
+ export async function blobText(repository, object) {
56
+ return (await blob(repository, object)).toString('utf8');
57
+ }
58
+ export async function isText(repository, object, maxBytes) {
59
+ const bytes = await blob(repository, object);
60
+ return bytes.length <= maxBytes && isUtf8(bytes) && !bytes.includes(0);
59
61
  }
60
62
  export function blobId(content) {
61
63
  const bytes = Buffer.from(content, 'utf8');