@agentshouse/kit 0.2.1-cf110d15f69c8 → 0.2.1-cf6326dfa5c3e

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 (94) hide show
  1. package/README.md +20 -19
  2. package/dist/acp.js +34 -14
  3. package/dist/agents.js +108 -21
  4. package/dist/api.js +5 -1
  5. package/dist/app-git.js +10 -26
  6. package/dist/bootstrap.json +4 -4
  7. package/dist/bridge.js +3 -6
  8. package/dist/chatgpt.js +32 -0
  9. package/dist/clis.js +19 -3
  10. package/dist/conversations.js +209 -77
  11. package/dist/copies.js +98 -47
  12. package/dist/end.js +5 -0
  13. package/dist/events.js +4 -0
  14. package/dist/files.js +8 -2
  15. package/dist/git.js +10 -8
  16. package/dist/home.js +56 -5
  17. package/dist/house-cli.js +291 -0
  18. package/dist/house-main.js +7 -206
  19. package/dist/inputs.js +63 -0
  20. package/dist/instructions.js +4 -28
  21. package/dist/kit-cli.js +68 -0
  22. package/dist/kit-main.js +8 -22
  23. package/dist/login.js +46 -28
  24. package/dist/logout.js +0 -2
  25. package/dist/placement.js +30 -0
  26. package/dist/relay.js +1 -1
  27. package/dist/resident.js +65 -16
  28. package/dist/service.js +22 -0
  29. package/dist/sign-in.js +5 -23
  30. package/dist/skills.js +81 -12
  31. package/dist/stream.js +66 -17
  32. package/dist/transcription.js +3 -3
  33. package/dist/translate.js +2 -8
  34. package/dist/update.js +48 -29
  35. package/dist/wire.js +6 -0
  36. package/dist/work.js +3 -5
  37. package/how-we-work.md +13 -3
  38. package/package.json +3 -2
  39. package/skills/ask-matt/SKILL.md +17 -11
  40. package/skills/ask-matt/agents/openai.yaml +1 -0
  41. package/skills/code-review/SKILL.md +5 -5
  42. package/skills/codebase-design/DESIGN-IT-TWICE.md +1 -1
  43. package/skills/codebase-design/SKILL.md +0 -2
  44. package/skills/diagnosing-bugs/SKILL.md +2 -4
  45. package/skills/domain-modeling/{CONTEXT-FORMAT.md → GLOSSARY-FORMAT.md} +10 -10
  46. package/skills/domain-modeling/SKILL.md +12 -14
  47. package/skills/grill-me/SKILL.md +1 -2
  48. package/skills/grill-me/agents/openai.yaml +1 -0
  49. package/skills/grill-with-docs/SKILL.md +1 -2
  50. package/skills/grill-with-docs/agents/openai.yaml +1 -0
  51. package/skills/grilling/SKILL.md +2 -2
  52. package/skills/handoff/SKILL.md +2 -3
  53. package/skills/handoff/agents/openai.yaml +1 -0
  54. package/skills/implement/SKILL.md +5 -4
  55. package/skills/implement/agents/openai.yaml +1 -0
  56. package/skills/implement-spec/SKILL.md +40 -0
  57. package/skills/implement-spec/agents/openai.yaml +5 -0
  58. package/skills/improve-codebase-architecture/SKILL.md +6 -7
  59. package/skills/improve-codebase-architecture/agents/openai.yaml +1 -0
  60. package/skills/pr/CREDITS.md +3 -0
  61. package/skills/pr/LICENSE +21 -0
  62. package/skills/pr/SKILL.md +170 -0
  63. package/skills/pr/agents/openai.yaml +3 -0
  64. package/skills/prototype/SKILL.md +0 -2
  65. package/skills/research/SKILL.md +0 -2
  66. package/skills/retro/LICENSE +21 -0
  67. package/skills/retro/SKILL.md +44 -0
  68. package/skills/retro/agents/openai.yaml +5 -0
  69. package/skills/tdd/SKILL.md +2 -4
  70. package/skills/teach/SKILL.md +3 -4
  71. package/skills/teach/agents/openai.yaml +1 -0
  72. package/skills/to-questionnaire/SKILL.md +1 -2
  73. package/skills/to-questionnaire/agents/openai.yaml +1 -0
  74. package/skills/to-spec/SKILL.md +1 -2
  75. package/skills/to-spec/agents/openai.yaml +1 -0
  76. package/skills/to-tickets/SKILL.md +3 -4
  77. package/skills/to-tickets/agents/openai.yaml +1 -0
  78. package/skills/triage/AGENT-BRIEF.md +207 -0
  79. package/skills/triage/LICENSE +21 -0
  80. package/skills/triage/OUT-OF-SCOPE.md +105 -0
  81. package/skills/triage/SKILL.md +112 -0
  82. package/skills/triage/agents/openai.yaml +5 -0
  83. package/skills/wait-what/SKILL.md +2 -3
  84. package/skills/wait-what/agents/openai.yaml +1 -0
  85. package/skills/wayfinder/SKILL.md +5 -6
  86. package/skills/wayfinder/agents/openai.yaml +1 -0
  87. package/skills/wizard/LICENSE +21 -0
  88. package/skills/wizard/SKILL.md +44 -0
  89. package/skills/wizard/agents/openai.yaml +3 -0
  90. package/skills/wizard/template.sh +223 -0
  91. package/skills/writing-for-agents/SKILL.md +0 -2
  92. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  93. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  94. /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,6 +32,13 @@ 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
  }
@@ -48,6 +58,9 @@ function expiredAuthority(error) {
48
58
  return undefined;
49
59
  }
50
60
  }
61
+ function grouped(items, size) {
62
+ return items.flatMap((_, index) => (index % size === 0 ? [items.slice(index, index + size)] : []));
63
+ }
51
64
  function covers(path, prefix) {
52
65
  return prefix === '' || path === prefix || path.startsWith(`${prefix}/`);
53
66
  }
@@ -56,6 +69,9 @@ function sidecar(path) {
56
69
  segments.splice(-1, 0, PROVENANCE);
57
70
  return segments.join('/');
58
71
  }
72
+ function recordOf(key) {
73
+ return join(kitHome(), 'local-copies', `${key}.json`);
74
+ }
59
75
  function rootOf(handle) {
60
76
  return `/rooms/${handle}`;
61
77
  }
@@ -202,48 +218,62 @@ export class LocalCopies {
202
218
  async mapping() {
203
219
  await this.answering;
204
220
  return [...this.copies.values()]
205
- .filter((copy) => copy.active)
221
+ .filter((copy) => copy.erasing === undefined)
206
222
  .map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
207
223
  .sort();
208
224
  }
209
- async connected() {
210
- const copied = await this.answered(() => this.fetched(false));
211
- if (copied.refused)
212
- logged(copied.text);
225
+ connected() {
226
+ return this.selected();
227
+ }
228
+ async changed(rooms) {
229
+ if (rooms.length > 0)
230
+ await this.selected(rooms);
213
231
  }
214
232
  fetch() {
215
233
  return this.answered(() => this.fetched(true));
216
234
  }
235
+ async selected(rooms) {
236
+ const copied = await this.answered(() => this.fetched(false, rooms));
237
+ if (copied.refused)
238
+ logged(copied.text);
239
+ }
217
240
  answered(work) {
218
241
  const answered = this.answering.then(work);
219
242
  this.answering = answered.catch(() => undefined);
220
243
  return answered;
221
244
  }
222
- async fetched(refreshing) {
223
- const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', {});
245
+ async fetched(refreshing, rooms) {
246
+ const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', rooms === undefined ? {} : { rooms });
224
247
  const synced = new Map(answer.rooms.map((room) => [`rooms/${room.room_ref}`, room.room_handle]));
225
- return this.holding([...new Set([...this.copies.keys(), ...synced.keys()])].sort(), async () => {
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 () => {
226
251
  const said = [];
227
252
  let refused = false;
228
- for (const copy of this.copies.values()) {
229
- if (synced.has(copy.key) || !copy.active)
253
+ for (const copy of held()) {
254
+ const returned = synced.has(copy.key);
255
+ if (returned && copy.erasing === undefined)
230
256
  continue;
231
- await this.save({ ...copy, active: false });
232
- said.push(`${rootOf(copy.room_handle)}: no longer synced to this Environment; its files stay`);
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
+ }
233
265
  }
234
266
  const current = [];
235
267
  for (const [key, handle] of synced) {
236
268
  try {
237
269
  const held = this.copies.get(key);
270
+ if (held?.erasing)
271
+ continue;
238
272
  if (held === undefined || !(await exists(held.repository))) {
239
273
  said.push(await this.materialize(key, handle));
240
274
  continue;
241
275
  }
242
- const copy = held.room_handle === handle ? held : await this.save({ ...held, room_handle: handle });
243
- if (copy.active)
244
- current.push(copy);
245
- else
246
- said.push(...moved(copy, await this.save(await this.received({ ...copy, active: true }))));
276
+ current.push(held.room_handle === handle ? held : await this.save({ ...held, room_handle: handle }));
247
277
  }
248
278
  catch (error) {
249
279
  refused = true;
@@ -251,13 +281,12 @@ export class LocalCopies {
251
281
  }
252
282
  }
253
283
  if (refreshing && current.length > 0) {
254
- try {
255
- const refreshed = await this.refreshed(current);
256
- current.forEach((copy, index) => said.push(...moved(copy, refreshed[index])));
257
- }
258
- catch (error) {
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) {
259
288
  refused = true;
260
- said.push(error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error));
289
+ said.push(failed);
261
290
  }
262
291
  }
263
292
  return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
@@ -269,6 +298,8 @@ export class LocalCopies {
269
298
  return;
270
299
  await this.exclusive(key, async () => {
271
300
  const copy = this.copies.get(key);
301
+ if (copy === undefined || copy.erasing)
302
+ return;
272
303
  let moves = copy.moves;
273
304
  for (const { from, to } of report.moves) {
274
305
  const held = moves.find((move) => move.worktree === report.worktree && move.to === from);
@@ -291,11 +322,11 @@ export class LocalCopies {
291
322
  async push(cwd, selector, caller) {
292
323
  const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
293
324
  if (located.status !== 0)
294
- return { refused: true, text: `${cwd} is not a House Local copy; run house push inside one` };
325
+ return outside(cwd);
295
326
  const [common, worktree] = located.stdout.toString('utf8').split('\n');
296
327
  const key = await this.keyAt(dirname(common));
297
328
  if (key === undefined)
298
- return { refused: true, text: `${worktree} is not a House Local copy; run house push inside one` };
329
+ return outside(worktree);
299
330
  return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
300
331
  }
301
332
  async keyAt(repository) {
@@ -316,7 +347,7 @@ export class LocalCopies {
316
347
  return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
317
348
  }
318
349
  async save(copy) {
319
- const path = join(kitHome(), 'local-copies', `${copy.key}.json`);
350
+ const path = recordOf(copy.key);
320
351
  await mkdir(dirname(path), { recursive: true, mode: 0o700 });
321
352
  const temporary = `${path}.${process.pid}`;
322
353
  await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
@@ -324,6 +355,12 @@ export class LocalCopies {
324
355
  this.copies.set(copy.key, copy);
325
356
  return copy;
326
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
+ }
327
364
  async read(handle) {
328
365
  const prefixes = [rootOf(handle)];
329
366
  const discovery = await this.house.post('/kit/door/discover', { prefixes });
@@ -390,7 +427,6 @@ export class LocalCopies {
390
427
  room_ref: read.anchor,
391
428
  room_handle: handle,
392
429
  repository,
393
- active: true,
394
430
  house,
395
431
  position: read.position,
396
432
  log_epoch: read.log_epoch,
@@ -425,19 +461,24 @@ export class LocalCopies {
425
461
  return next;
426
462
  }
427
463
  async refreshed(copies) {
428
- const discovery = await this.house.post('/kit/door/discover', {
429
- prefixes: copies.map((copy) => rootOf(copy.room_handle)),
430
- });
431
- const regranted = new Map();
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 = [];
432
470
  for (const copy of copies) {
433
- const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
471
+ const authority = authorities.find((entry) => entry.room_ref === copy.room_ref);
434
472
  if (authority !== undefined &&
435
473
  JSON.stringify([authority.operations.write, authority.protected_paths]) !== JSON.stringify([copy.write, copy.protected])) {
436
- regranted.set(copy.key, await this.save(await this.received(copy)));
474
+ await this.save(await this.received(copy));
475
+ }
476
+ else {
477
+ unchanged.push(copy);
437
478
  }
438
479
  }
439
- const caught = await this.caughtUp(copies.filter((copy) => !regranted.has(copy.key)), false);
440
- return copies.map((copy) => regranted.get(copy.key) ?? caught.find((held) => held.key === copy.key));
480
+ for (const group of grouped(unchanged, bounds.max_prefixes))
481
+ await this.caughtUp(group, false);
441
482
  }
442
483
  async caughtUp(copies, whole) {
443
484
  const caught = new Map(copies.map((copy) => [copy.key, copy]));
@@ -519,6 +560,8 @@ export class LocalCopies {
519
560
  }
520
561
  async pushed(key, worktree, selector, caller) {
521
562
  let copy = this.copies.get(key);
563
+ if (copy === undefined || copy.erasing)
564
+ return outside(worktree);
522
565
  if (copy.push?.state === 'submitted')
523
566
  return this.settle(copy, caller);
524
567
  const selected = await run(worktree, ['rev-parse', '--verify', '--quiet', '--end-of-options', `${selector}^{commit}`]);
@@ -532,9 +575,6 @@ export class LocalCopies {
532
575
  }
533
576
  copy = await this.save(without(copy));
534
577
  }
535
- if (!copy.active) {
536
- return { refused: true, text: `${rootOf(copy.room_handle)} is no longer synced to this Environment, so House takes no push from it` };
537
- }
538
578
  const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
539
579
  const base = listed.find((ancestor) => ancestor in copy.manifests);
540
580
  if (base === undefined) {
@@ -545,7 +585,7 @@ export class LocalCopies {
545
585
  changes = await this.derived(copy, base, commit);
546
586
  }
547
587
  catch (error) {
548
- return { refused: true, text: causeOf(error) };
588
+ return { refused: true, text: houseCause(error) };
549
589
  }
550
590
  if (changes.length === 0)
551
591
  return { refused: false, text: `Nothing to submit: ${short(commit)} matches House.` };
@@ -572,7 +612,18 @@ export class LocalCopies {
572
612
  const after = await tree(copy.repository, commit);
573
613
  const moved = movedPaths(copy.moves, before, after);
574
614
  const arrived = new Set(moved.values());
575
- 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);
576
627
  const changes = [];
577
628
  for (const path of [...new Set([...before.keys(), ...after.keys()])].sort()) {
578
629
  if (arrived.has(path))
@@ -585,7 +636,7 @@ export class LocalCopies {
585
636
  changes.push({ op: 'rename', path: `${root}/${path}`, to: `${root}/${to}`, base: known });
586
637
  const moving = after.get(to);
587
638
  if (moving !== prior) {
588
- 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) });
589
640
  }
590
641
  continue;
591
642
  }
@@ -594,9 +645,9 @@ export class LocalCopies {
594
645
  if (next === undefined)
595
646
  changes.push({ op: 'remove', path: `${root}/${path}`, base: known });
596
647
  else if (prior === undefined)
597
- changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next, path) });
648
+ changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next) });
598
649
  else
599
- 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)));
600
651
  }
601
652
  return changes;
602
653
  }
@@ -663,7 +714,7 @@ export class LocalCopies {
663
714
  written.push({
664
715
  path: `${root}/${path}`,
665
716
  revision: entry === undefined ? null : (revisionAt(current, current.house, path) ?? null),
666
- content: entry === undefined ? null : await blobText(copy.repository, entry, path),
717
+ content: entry === undefined ? null : await blobText(copy.repository, entry),
667
718
  });
668
719
  }
669
720
  return this.finished(await this.save({ ...current, push: { ...push, state: 'accepted', written } }));
@@ -707,7 +758,7 @@ export class LocalCopies {
707
758
  {
708
759
  path: `${rootOf(copy.room_handle)}/${path}`,
709
760
  revision: entry === undefined ? null : (revisionAt(copy, copy.house, path) ?? null),
710
- content: entry === undefined ? null : await blobText(copy.repository, entry, path),
761
+ content: entry === undefined ? null : await blobText(copy.repository, entry),
711
762
  },
712
763
  ];
713
764
  }
@@ -752,7 +803,7 @@ export class LocalCopies {
752
803
  async landed(repository, changes, house, selected) {
753
804
  const text = async (path) => {
754
805
  const entry = house.get(relative(path));
755
- return entry === undefined ? undefined : blobText(repository, entry, path);
806
+ return entry === undefined ? undefined : blobText(repository, entry);
756
807
  };
757
808
  for (const change of changes) {
758
809
  const current = await text(change.path);
package/dist/end.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { forgetRejected } from './home.js';
2
+ import { installCommand } from './placement.js';
2
3
  import { killDescendants } from './scope.js';
3
4
  export const REPLACED = 'another Kit started for this Environment';
4
5
  function exit(reason, status) {
@@ -12,6 +13,10 @@ export function end(reason) {
12
13
  export function restart(reason) {
13
14
  exit(reason, 1);
14
15
  }
16
+ export function updateRequired() {
17
+ const command = installCommand();
18
+ end(command === null ? 'This Kit is too old for House; install it again.' : `This Kit is too old for House. Update it: ${command}`);
19
+ }
15
20
  export async function signInAgain(rejected) {
16
21
  await forgetRejected(rejected);
17
22
  restart('sign in again');
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');
package/dist/home.js CHANGED
@@ -1,15 +1,63 @@
1
- import { existsSync, linkSync, readFileSync, renameSync, rmSync } from 'node:fs';
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { accessSync, chmodSync, constants, existsSync, linkSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync } from 'node:fs';
2
3
  import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
3
- import { homedir } from 'node:os';
4
- import { join } from 'node:path';
4
+ import { homedir, tmpdir } from 'node:os';
5
+ import { join, resolve } from 'node:path';
5
6
  import { selectRelay } from './relay.js';
6
7
  const ENROLMENT = 'credential.json';
7
8
  const DISCONNECTED = 'disconnected.json';
8
9
  const CONFIGURATION = 'kit.json';
9
10
  const OWN_AGENT = 'own-agent.json';
10
11
  const RESTART = 'restart';
12
+ let defaulted;
13
+ function writable(directory) {
14
+ try {
15
+ accessSync(directory, constants.W_OK | constants.X_OK);
16
+ return true;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ function privateDirectory(path) {
23
+ try {
24
+ mkdirSync(path, { mode: 0o700 });
25
+ }
26
+ catch (error) {
27
+ if (error.code !== 'EEXIST')
28
+ throw error;
29
+ }
30
+ const found = lstatSync(path);
31
+ if (found.isSymbolicLink())
32
+ throw new Error(`${path} is a symbolic link`);
33
+ if (!found.isDirectory())
34
+ throw new Error(`${path} is not a directory`);
35
+ if (found.uid !== process.geteuid())
36
+ throw new Error(`${path} is not owned by this User`);
37
+ chmodSync(path, 0o700);
38
+ return path;
39
+ }
40
+ function ownTemporaryHome() {
41
+ return privateDirectory(join(tmpdir(), `house-kit-${process.getuid()}`));
42
+ }
43
+ function defaultHome() {
44
+ const home = join(homedir(), '.house-kit');
45
+ return existsSync(home) || writable(homedir()) ? home : ownTemporaryHome();
46
+ }
11
47
  export function kitHome() {
12
- return process.env.HOUSE_KIT_HOME ?? join(homedir(), '.house-kit');
48
+ return process.env.HOUSE_KIT_HOME || (defaulted ??= defaultHome());
49
+ }
50
+ export function socketDirectory() {
51
+ const sockets = privateDirectory(join(realpathSync('/tmp'), `hk-${process.geteuid()}`));
52
+ const home = kitHome();
53
+ const physical = existsSync(home) ? realpathSync(home) : resolve(home);
54
+ return privateDirectory(join(sockets, createHash('sha256').update(physical).digest('hex').slice(0, 12)));
55
+ }
56
+ export function ownerSocket() {
57
+ return join(socketDirectory(), 'owner.sock');
58
+ }
59
+ export function ownerLock() {
60
+ return join(socketDirectory(), 'owner.lock');
13
61
  }
14
62
  export function workspaceRoot() {
15
63
  return process.env.HOUSE_KIT_WORKSPACE ?? '/agents/house';
@@ -30,7 +78,7 @@ export async function readHome(name) {
30
78
  export async function writeHome(name, value) {
31
79
  const home = kitHome();
32
80
  await mkdir(home, { recursive: true, mode: 0o700 });
33
- const temporary = join(home, `${name}.${process.pid}`);
81
+ const temporary = join(home, `${name}.${randomUUID()}`);
34
82
  await writeFile(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600 });
35
83
  await rename(temporary, join(home, name));
36
84
  }
@@ -47,6 +95,9 @@ export async function writeEnrolment(enrolment) {
47
95
  export function readDisconnected() {
48
96
  return readHome(DISCONNECTED);
49
97
  }
98
+ export function forgetDisconnected() {
99
+ return rm(join(kitHome(), DISCONNECTED), { force: true });
100
+ }
50
101
  export async function forgetEnrolment({ house, environment }) {
51
102
  await writeHome(DISCONNECTED, { house, environment });
52
103
  await rm(join(kitHome(), ENROLMENT), { force: true });