@agentshouse/kit 0.2.1-c9a26041e119a → 0.2.1-ca0e1517cce79

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 (138) hide show
  1. package/README.md +28 -28
  2. package/dist/acp.js +36 -16
  3. package/dist/agents.js +52 -19
  4. package/dist/api.js +5 -5
  5. package/dist/app-git.js +10 -26
  6. package/dist/bootstrap.json +4 -4
  7. package/dist/bridge.js +4 -7
  8. package/dist/calls.js +63 -0
  9. package/dist/chatgpt.js +7 -1
  10. package/dist/clis.js +15 -3
  11. package/dist/{stream.js → connection.js} +33 -30
  12. package/dist/conversations.js +404 -154
  13. package/dist/copies.js +77 -50
  14. package/dist/end.js +9 -4
  15. package/dist/events.js +6 -2
  16. package/dist/files.js +139 -37
  17. package/dist/git.js +3 -4
  18. package/dist/home.js +27 -13
  19. package/dist/house-cli.js +300 -0
  20. package/dist/house-main.js +7 -289
  21. package/dist/instructions.js +28 -7
  22. package/dist/kit-cli.js +68 -0
  23. package/dist/kit-main.js +8 -67
  24. package/dist/login.js +15 -8
  25. package/dist/logout.js +0 -2
  26. package/dist/mcp.js +4 -2
  27. package/dist/placement.js +9 -18
  28. package/dist/resident.js +88 -37
  29. package/dist/scope.js +3 -0
  30. package/dist/service.js +23 -1
  31. package/dist/sign-in.js +19 -37
  32. package/dist/skills.js +12 -93
  33. package/dist/transcription.js +51 -20
  34. package/dist/translate.js +2 -8
  35. package/dist/update.js +42 -45
  36. package/dist/web-wizard.js +100 -0
  37. package/dist/wire.js +19 -3
  38. package/dist/work.js +3 -5
  39. package/package.json +12 -12
  40. package/how-we-work.md +0 -33
  41. package/skills/ask-matt/LICENSE +0 -21
  42. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  43. package/skills/ask-matt/SKILL.md +0 -97
  44. package/skills/ask-matt/agents/openai.yaml +0 -5
  45. package/skills/code-review/LICENSE +0 -21
  46. package/skills/code-review/SKILL.md +0 -89
  47. package/skills/code-review/agents/openai.yaml +0 -3
  48. package/skills/codebase-design/DEEPENING.md +0 -37
  49. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  50. package/skills/codebase-design/LICENSE +0 -21
  51. package/skills/codebase-design/SKILL.md +0 -114
  52. package/skills/codebase-design/agents/openai.yaml +0 -3
  53. package/skills/diagnosing-bugs/LICENSE +0 -21
  54. package/skills/diagnosing-bugs/SKILL.md +0 -138
  55. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  56. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  57. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  58. package/skills/domain-modeling/GLOSSARY-FORMAT.md +0 -60
  59. package/skills/domain-modeling/LICENSE +0 -21
  60. package/skills/domain-modeling/SKILL.md +0 -74
  61. package/skills/domain-modeling/agents/openai.yaml +0 -3
  62. package/skills/grill-me/LICENSE +0 -21
  63. package/skills/grill-me/SKILL.md +0 -7
  64. package/skills/grill-me/agents/openai.yaml +0 -5
  65. package/skills/grill-with-docs/LICENSE +0 -21
  66. package/skills/grill-with-docs/SKILL.md +0 -7
  67. package/skills/grill-with-docs/agents/openai.yaml +0 -5
  68. package/skills/grilling/LICENSE +0 -21
  69. package/skills/grilling/SKILL.md +0 -30
  70. package/skills/grilling/agents/openai.yaml +0 -3
  71. package/skills/handoff/LICENSE +0 -21
  72. package/skills/handoff/SKILL.md +0 -16
  73. package/skills/handoff/agents/openai.yaml +0 -5
  74. package/skills/implement/LICENSE +0 -21
  75. package/skills/implement/SKILL.md +0 -17
  76. package/skills/implement/agents/openai.yaml +0 -5
  77. package/skills/implement-spec/LICENSE +0 -21
  78. package/skills/implement-spec/SKILL.md +0 -40
  79. package/skills/implement-spec/agents/openai.yaml +0 -5
  80. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  81. package/skills/improve-codebase-architecture/LICENSE +0 -21
  82. package/skills/improve-codebase-architecture/SKILL.md +0 -71
  83. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
  84. package/skills/pr/CREDITS.md +0 -3
  85. package/skills/pr/LICENSE +0 -21
  86. package/skills/pr/SKILL.md +0 -170
  87. package/skills/pr/agents/openai.yaml +0 -3
  88. package/skills/prototype/LICENSE +0 -21
  89. package/skills/prototype/LOGIC.md +0 -67
  90. package/skills/prototype/SKILL.md +0 -26
  91. package/skills/prototype/UI.md +0 -112
  92. package/skills/prototype/agents/openai.yaml +0 -3
  93. package/skills/research/LICENSE +0 -21
  94. package/skills/research/SKILL.md +0 -12
  95. package/skills/research/agents/openai.yaml +0 -3
  96. package/skills/retro/LICENSE +0 -21
  97. package/skills/retro/SKILL.md +0 -44
  98. package/skills/retro/agents/openai.yaml +0 -5
  99. package/skills/tdd/LICENSE +0 -21
  100. package/skills/tdd/SKILL.md +0 -38
  101. package/skills/tdd/agents/openai.yaml +0 -3
  102. package/skills/tdd/mocking.md +0 -59
  103. package/skills/tdd/tests.md +0 -77
  104. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  105. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  106. package/skills/teach/LICENSE +0 -21
  107. package/skills/teach/MISSION-FORMAT.md +0 -31
  108. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  109. package/skills/teach/SKILL.md +0 -140
  110. package/skills/teach/agents/openai.yaml +0 -5
  111. package/skills/to-questionnaire/LICENSE +0 -21
  112. package/skills/to-questionnaire/SKILL.md +0 -54
  113. package/skills/to-questionnaire/agents/openai.yaml +0 -5
  114. package/skills/to-spec/LICENSE +0 -21
  115. package/skills/to-spec/SKILL.md +0 -75
  116. package/skills/to-spec/agents/openai.yaml +0 -5
  117. package/skills/to-tickets/LICENSE +0 -21
  118. package/skills/to-tickets/SKILL.md +0 -105
  119. package/skills/to-tickets/agents/openai.yaml +0 -5
  120. package/skills/triage/AGENT-BRIEF.md +0 -207
  121. package/skills/triage/LICENSE +0 -21
  122. package/skills/triage/OUT-OF-SCOPE.md +0 -105
  123. package/skills/triage/SKILL.md +0 -112
  124. package/skills/triage/agents/openai.yaml +0 -5
  125. package/skills/wait-what/LICENSE +0 -21
  126. package/skills/wait-what/SKILL.md +0 -7
  127. package/skills/wait-what/agents/openai.yaml +0 -5
  128. package/skills/wayfinder/LICENSE +0 -21
  129. package/skills/wayfinder/SKILL.md +0 -128
  130. package/skills/wayfinder/agents/openai.yaml +0 -5
  131. package/skills/wizard/LICENSE +0 -21
  132. package/skills/wizard/SKILL.md +0 -44
  133. package/skills/wizard/agents/openai.yaml +0 -3
  134. package/skills/wizard/template.sh +0 -223
  135. package/skills/writing-for-agents/LICENSE +0 -21
  136. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  137. package/skills/writing-for-agents/SKILL.md +0 -81
  138. package/skills/writing-for-agents/agents/openai.yaml +0 -3
package/dist/copies.js CHANGED
@@ -11,10 +11,13 @@ import { changed } from './translate.js';
11
11
  import { relayed } from './relay.js';
12
12
  const BRANCH = 'main';
13
13
  const PROVENANCE = '_provenance';
14
- const IDENTITY = { GIT_COMMITTER_NAME: 'House Kit', GIT_COMMITTER_EMAIL: 'house-kit@localhost' };
14
+ const IDENTITY = { GIT_COMMITTER_NAME: 'Kit', GIT_COMMITTER_EMAIL: 'house-kit@localhost' };
15
15
  function causeOf(error) {
16
16
  return error instanceof Error ? error.message : String(error);
17
17
  }
18
+ function houseCause(error) {
19
+ return error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
20
+ }
18
21
  function logged(error) {
19
22
  process.stderr.write(`kit: ${causeOf(error)}\n`);
20
23
  }
@@ -33,6 +36,9 @@ function textOnly(paths) {
33
36
  const [named, them] = paths.length === 1 ? [`${paths[0]} is`, 'it'] : [`${paths.join(', ')} are`, 'them'];
34
37
  return `${named} not text, and house push sends text only; remove ${them} from the commit and place ${them} with house upload`;
35
38
  }
39
+ function outside(path) {
40
+ return { refused: true, text: `${path} is not a House Local copy; run house push inside one` };
41
+ }
36
42
  function short(commit) {
37
43
  return commit.slice(0, 12);
38
44
  }
@@ -52,6 +58,9 @@ function expiredAuthority(error) {
52
58
  return undefined;
53
59
  }
54
60
  }
61
+ function grouped(items, size) {
62
+ return items.flatMap((_, index) => (index % size === 0 ? [items.slice(index, index + size)] : []));
63
+ }
55
64
  function covers(path, prefix) {
56
65
  return prefix === '' || path === prefix || path.startsWith(`${prefix}/`);
57
66
  }
@@ -60,6 +69,9 @@ function sidecar(path) {
60
69
  segments.splice(-1, 0, PROVENANCE);
61
70
  return segments.join('/');
62
71
  }
72
+ function recordOf(key) {
73
+ return join(kitHome(), 'local-copies', `${key}.json`);
74
+ }
63
75
  function rootOf(handle) {
64
76
  return `/rooms/${handle}`;
65
77
  }
@@ -121,7 +133,7 @@ async function ignoreRules(repository, writable, copy) {
121
133
  ].map(pattern);
122
134
  const granted = readOnly.filter((path) => !protectedPath(path)).map(pattern);
123
135
  await ruled(join(repository, '.gitignore'), [
124
- '# House Kit writes this file and .ignore. The paths below are read-only House',
136
+ '# Kit writes this file and .ignore. The paths below are read-only House',
125
137
  '# content: Git does not track them, and House refuses a change to them even',
126
138
  '# when this file is edited or a path is force-added.',
127
139
  '/.gitignore',
@@ -136,7 +148,7 @@ async function ignoreRules(repository, writable, copy) {
136
148
  '',
137
149
  ].join('\n'));
138
150
  await ruled(join(repository, '.ignore'), [
139
- '# House Kit writes this file so native search still reads the read-only House',
151
+ '# Kit writes this file so native search still reads the read-only House',
140
152
  '# content that .gitignore keeps out of Git.',
141
153
  `!${PROVENANCE}`,
142
154
  ...[...house, ...granted].map((rule) => `!${rule}`),
@@ -206,34 +218,45 @@ export class LocalCopies {
206
218
  async mapping() {
207
219
  await this.answering;
208
220
  return [...this.copies.values()]
209
- .filter((copy) => copy.active)
221
+ .filter((copy) => copy.unfollowed === undefined)
210
222
  .map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
211
223
  .sort();
212
224
  }
213
- async connected() {
214
- const copied = await this.answered(() => this.fetched(false));
215
- if (copied.refused)
216
- logged(copied.text);
225
+ connected() {
226
+ return this.selected();
227
+ }
228
+ async changed(rooms) {
229
+ if (rooms.length > 0)
230
+ await this.selected(rooms);
217
231
  }
218
232
  fetch() {
219
233
  return this.answered(() => this.fetched(true));
220
234
  }
235
+ async selected(rooms) {
236
+ const copied = await this.answered(() => this.fetched(false, rooms));
237
+ if (copied.refused)
238
+ logged(copied.text);
239
+ }
221
240
  answered(work) {
222
241
  const answered = this.answering.then(work);
223
242
  this.answering = answered.catch(() => undefined);
224
243
  return answered;
225
244
  }
226
- async fetched(refreshing) {
227
- 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 });
228
247
  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 () => {
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 () => {
230
251
  const said = [];
231
252
  let refused = false;
232
- for (const copy of this.copies.values()) {
233
- if (synced.has(copy.key) || !copy.active)
253
+ const mark = refreshing ? 'said' : 'unsaid';
254
+ for (const copy of held()) {
255
+ if (synced.has(copy.key) || copy.unfollowed === 'said' || copy.unfollowed === mark)
234
256
  continue;
235
- await this.save({ ...copy, active: false });
236
- said.push(`${rootOf(copy.room_handle)}: no longer synced to this Environment; its files stay`);
257
+ await this.save({ ...copy, unfollowed: mark });
258
+ if (refreshing)
259
+ said.push(`${rootOf(copy.room_handle)}: no longer followed; kept as it is`);
237
260
  }
238
261
  const current = [];
239
262
  for (const [key, handle] of synced) {
@@ -243,11 +266,8 @@ export class LocalCopies {
243
266
  said.push(await this.materialize(key, handle));
244
267
  continue;
245
268
  }
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 }))));
269
+ const { unfollowed, ...followed } = held;
270
+ current.push(unfollowed === undefined && held.room_handle === handle ? held : await this.save({ ...followed, room_handle: handle }));
251
271
  }
252
272
  catch (error) {
253
273
  refused = true;
@@ -255,13 +275,12 @@ export class LocalCopies {
255
275
  }
256
276
  }
257
277
  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) {
278
+ const failed = await this.refreshed(current).then(() => null, houseCause);
279
+ for (const copy of current)
280
+ said.push(...moved(copy, this.copies.get(copy.key)));
281
+ if (failed !== null) {
263
282
  refused = true;
264
- said.push(error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error));
283
+ said.push(failed);
265
284
  }
266
285
  }
267
286
  return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
@@ -295,11 +314,11 @@ export class LocalCopies {
295
314
  async push(cwd, selector, caller) {
296
315
  const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
297
316
  if (located.status !== 0)
298
- return { refused: true, text: `${cwd} is not a House Local copy; run house push inside one` };
317
+ return outside(cwd);
299
318
  const [common, worktree] = located.stdout.toString('utf8').split('\n');
300
319
  const key = await this.keyAt(dirname(common));
301
320
  if (key === undefined)
302
- return { refused: true, text: `${worktree} is not a House Local copy; run house push inside one` };
321
+ return outside(worktree);
303
322
  return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
304
323
  }
305
324
  async keyAt(repository) {
@@ -320,7 +339,7 @@ export class LocalCopies {
320
339
  return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
321
340
  }
322
341
  async save(copy) {
323
- const path = join(kitHome(), 'local-copies', `${copy.key}.json`);
342
+ const path = recordOf(copy.key);
324
343
  await mkdir(dirname(path), { recursive: true, mode: 0o700 });
325
344
  const temporary = `${path}.${process.pid}`;
326
345
  await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
@@ -369,7 +388,7 @@ export class LocalCopies {
369
388
  throw new Error('House serves nothing of it to copy');
370
389
  const repository = join(localCopies(), 'rooms', read.anchor);
371
390
  if (await exists(repository))
372
- throw new Error(`${repository} already exists and House Kit leaves it as it is`);
391
+ throw new Error(`${repository} already exists and Kit leaves it as it is`);
373
392
  const staging = join(dirname(repository), `.${randomUUID()}`);
374
393
  await mkdir(staging, { recursive: true });
375
394
  let house;
@@ -394,7 +413,6 @@ export class LocalCopies {
394
413
  room_ref: read.anchor,
395
414
  room_handle: handle,
396
415
  repository,
397
- active: true,
398
416
  house,
399
417
  position: read.position,
400
418
  log_epoch: read.log_epoch,
@@ -429,19 +447,24 @@ export class LocalCopies {
429
447
  return next;
430
448
  }
431
449
  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();
450
+ const discover = (group) => this.house.post('/kit/door/discover', { prefixes: group.map((copy) => rootOf(copy.room_handle)) });
451
+ const [first, ...rest] = copies;
452
+ const { bounds, authorities } = await discover([first]);
453
+ for (const group of grouped(rest, bounds.max_prefixes))
454
+ authorities.push(...(await discover(group)).authorities);
455
+ const unchanged = [];
436
456
  for (const copy of copies) {
437
- const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
457
+ const authority = authorities.find((entry) => entry.room_ref === copy.room_ref);
438
458
  if (authority !== undefined &&
439
459
  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)));
460
+ await this.save(await this.received(copy));
461
+ }
462
+ else {
463
+ unchanged.push(copy);
441
464
  }
442
465
  }
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));
466
+ for (const group of grouped(unchanged, bounds.max_prefixes))
467
+ await this.caughtUp(group, false);
445
468
  }
446
469
  async caughtUp(copies, whole) {
447
470
  const caught = new Map(copies.map((copy) => [copy.key, copy]));
@@ -536,9 +559,6 @@ export class LocalCopies {
536
559
  }
537
560
  copy = await this.save(without(copy));
538
561
  }
539
- if (!copy.active) {
540
- return { refused: true, text: `${rootOf(copy.room_handle)} is no longer synced to this Environment, so House takes no push from it` };
541
- }
542
562
  const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
543
563
  const base = listed.find((ancestor) => ancestor in copy.manifests);
544
564
  if (base === undefined) {
@@ -549,7 +569,7 @@ export class LocalCopies {
549
569
  changes = await this.derived(copy, base, commit);
550
570
  }
551
571
  catch (error) {
552
- return { refused: true, text: causeOf(error) };
572
+ return { refused: true, text: houseCause(error) };
553
573
  }
554
574
  if (changes.length === 0)
555
575
  return { refused: false, text: `Nothing to submit: ${short(commit)} matches House.` };
@@ -576,12 +596,17 @@ export class LocalCopies {
576
596
  const after = await tree(copy.repository, commit);
577
597
  const moved = movedPaths(copy.moves, before, after);
578
598
  const arrived = new Set(moved.values());
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));
599
+ const arriving = [...after].filter(([path, entry]) => before.get(path) !== entry);
600
+ if (arriving.length > 0) {
601
+ const { bounds } = await this.house.post('/kit/door/discover', { prefixes: [root] });
602
+ const binary = [];
603
+ for (const [path, entry] of arriving) {
604
+ if (!(await isText(copy.repository, entry, bounds.max_authored_file_bytes)))
605
+ binary.push(path);
606
+ }
607
+ if (binary.length > 0)
608
+ throw new Error(textOnly(binary));
609
+ }
585
610
  const text = (entry) => blobText(copy.repository, entry);
586
611
  const changes = [];
587
612
  for (const path of [...new Set([...before.keys(), ...after.keys()])].sort()) {
@@ -635,8 +660,10 @@ export class LocalCopies {
635
660
  }
636
661
  if (outcome.refused.code === 'expired')
637
662
  return this.recovered(copy);
638
- const rebased = rebaseFixes(outcome.refused);
639
663
  let settled = await this.save(without(copy));
664
+ if (settled.unfollowed !== undefined)
665
+ return { refused: true, text: outcome.refused.text };
666
+ const rebased = rebaseFixes(outcome.refused);
640
667
  try {
641
668
  settled = (await this.caughtUp([settled], true))[0];
642
669
  }
package/dist/end.js CHANGED
@@ -1,20 +1,25 @@
1
+ import { KIT_RELEASE } from './clis.js';
1
2
  import { forgetRejected } from './home.js';
2
3
  import { installCommand } from './placement.js';
3
4
  import { killDescendants } from './scope.js';
4
- export const REPLACED = 'another Kit started for this Environment';
5
+ export const REPLACED = 'another Kit process started for this Environment';
5
6
  function exit(reason, status) {
6
7
  process.stderr.write(`kit: ${reason}\n`);
7
8
  killDescendants();
8
9
  process.exit(status);
9
10
  }
10
11
  export function end(reason) {
11
- exit(reason, 0);
12
+ exit(reason, process.env.HOUSE_KIT_RESIDENT === String(process.pid) ? 0 : 1);
12
13
  }
13
14
  export function restart(reason) {
14
15
  exit(reason, 1);
15
16
  }
16
- export function updateRequired(house) {
17
- end(`This Kit is too old for House. Update it: ${installCommand(house)}`);
17
+ export function updateRequired() {
18
+ const command = installCommand();
19
+ end(command === null ? 'This Kit is too old for House; install it again.' : `This Kit is too old for House. Update it: ${command}`);
20
+ }
21
+ export function newerThanHouse(release, command) {
22
+ end(`Kit ${KIT_RELEASE} is newer than this Building supports (Kit ${release}). Install Kit ${release}: ${command}`);
18
23
  }
19
24
  export async function signInAgain(rejected) {
20
25
  await forgetRejected(rejected);
package/dist/events.js CHANGED
@@ -2,7 +2,7 @@ const COMMAND = { shown: 'command' };
2
2
  const HIDDEN = { shown: 'hidden' };
3
3
  const FINISHED = new Set(['completed', 'failed']);
4
4
  export const RUNNING_JOB = new Set(['running', 'paused']);
5
- const ROUTINE_TARGETS = { send_routine_request: 'access', cancel_routine_request: 'request' };
5
+ const MESSAGE_TARGETS = { send_routine_request: 'access', cancel_routine_request: 'request', send_mail: 'request' };
6
6
  function words(line) {
7
7
  const found = [];
8
8
  let word = null;
@@ -48,7 +48,7 @@ export function houseMessage(line) {
48
48
  catch {
49
49
  return null;
50
50
  }
51
- const argument = ROUTINE_TARGETS[call.command];
51
+ const argument = MESSAGE_TARGETS[call.command];
52
52
  if (argument === undefined)
53
53
  return null;
54
54
  const target = call.arguments?.[argument];
@@ -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,42 +1,144 @@
1
- import { createHash } from 'node:crypto';
2
- import { mkdir, writeFile } from 'node:fs/promises';
3
- import { join } from 'node:path';
4
- import { HouseRefusal, retrying } from './api.js';
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { createWriteStream } from 'node:fs';
3
+ import { mkdir, rename, rm } from 'node:fs/promises';
4
+ import { basename, dirname, join } from 'node:path';
5
+ import { Readable, Transform } from 'node:stream';
6
+ import { pipeline } from 'node:stream/promises';
7
+ import { parseDocument } from 'yaml';
8
+ import { retryDelay } from './api.js';
9
+ import { Unsettled } from './mcp.js';
5
10
  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
- }
11
- export async function placeFiles(house, directory, files, signal) {
12
- const paths = [];
13
- for (const file of files) {
14
- const original = await house.post('/kit/conversation/originals/get', { version: file.version, download: true }, signal);
15
- const answer = await fetch(relayed(original.download.url), { signal });
16
- if (!answer.ok)
17
- throw new Error(`${file.name} did not download: ${answer.status}`);
18
- const folder = join(directory, '.house', 'files', original.message, file.version);
19
- await mkdir(join(directory, '.house')).catch((error) => {
20
- if (error.code !== 'EEXIST')
11
+ const STALLED_ROUNDS = 8;
12
+ const PART_CHECKSUM = 'x-amz-checksum-sha256';
13
+ const LINK = /\]\((\/rooms\/[^/)\s]+\/[^)\s]+)\)/gu;
14
+ export function linkedPaths(text) {
15
+ return [...new Set(Array.from(text.matchAll(LINK), (link) => link[1]))];
16
+ }
17
+ export function inspectedFile(answer) {
18
+ const document = parseDocument(answer);
19
+ if (document.errors.length > 0)
20
+ return null;
21
+ const inspected = document.toJS();
22
+ if (typeof inspected?.download !== 'string')
23
+ return null;
24
+ return {
25
+ download: inspected.download,
26
+ sha256: String(inspected.sha256),
27
+ media_type: typeof inspected.media_type === 'string' ? inspected.media_type : null,
28
+ };
29
+ }
30
+ function paused(milliseconds) {
31
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
32
+ }
33
+ export function storeRefusal(act, status) {
34
+ return new Error(`the ${act} answered ${status}; ${act} the file again`);
35
+ }
36
+ async function partDigests(uploading, partBytes) {
37
+ const whole = createHash('sha256');
38
+ const digests = [];
39
+ for (let start = 0; digests.length === 0 || start < uploading.bytes; start += partBytes) {
40
+ const end = Math.min(start + partBytes, uploading.bytes);
41
+ const digest = createHash('sha256');
42
+ for await (const chunk of uploading.read(start, end)) {
43
+ digest.update(chunk);
44
+ whole.update(chunk);
45
+ }
46
+ digests.push(digest.digest());
47
+ }
48
+ if (whole.digest('hex') !== uploading.sha256)
49
+ throw new Error(`${uploading.name} changed while it was uploaded; upload it again`);
50
+ return digests;
51
+ }
52
+ export async function uploaded(declared, uploading) {
53
+ const partBytes = declared.part_bytes;
54
+ const digests = await partDigests(uploading, partBytes);
55
+ const partSha256 = digests.map((digest) => digest.toString('hex'));
56
+ let answer = await finishing(uploading, declared.id, partSha256);
57
+ let idle = 0;
58
+ let stalled = 0;
59
+ while ('upload' in answer) {
60
+ const { id, parts } = answer.upload;
61
+ let sent = 0;
62
+ let failure = null;
63
+ for (const { part, url } of parts ?? []) {
64
+ const start = (part - 1) * partBytes;
65
+ try {
66
+ await uploading.send(url, start, Math.min(start + partBytes, uploading.bytes), {
67
+ [PART_CHECKSUM]: digests[part - 1].toString('base64'),
68
+ });
69
+ sent++;
70
+ }
71
+ catch (error) {
72
+ failure = error;
73
+ break;
74
+ }
75
+ }
76
+ if (sent > 0) {
77
+ idle = 0;
78
+ stalled = 0;
79
+ }
80
+ else {
81
+ if (failure !== null && ++stalled >= STALLED_ROUNDS)
82
+ throw failure;
83
+ // A round that sent no part, its store unreachable, is followed by the next finishing call on the jittered curve.
84
+ await paused(retryDelay(idle++));
85
+ }
86
+ answer = await finishing(uploading, id, partSha256);
87
+ }
88
+ return answer;
89
+ }
90
+ async function finishing(uploading, upload, partSha256) {
91
+ for (let unsettled = 1;; unsettled++) {
92
+ try {
93
+ return await uploading.finish(upload, partSha256);
94
+ }
95
+ catch (error) {
96
+ if (!(error instanceof Unsettled) || unsettled === STALLED_ROUNDS)
21
97
  throw error;
22
- });
23
- await mkdir(folder, { recursive: true });
24
- const path = join(folder, file.name);
25
- await writeFile(path, verified(Buffer.from(await answer.arrayBuffer()), file.sha256, file.name));
26
- paths.push(path);
98
+ }
99
+ // A finishing call House did not settle, unreached or refused short of final, is sent again on the jittered curve as a conversation resends it, so a dropped connection or a House restart resumes the upload instead of ending it.
100
+ await paused(retryDelay(unsettled - 1));
27
101
  }
28
- return paths;
29
- }
30
- export function uploadBytes(upload, bytes) {
31
- return retrying(async () => {
32
- const answer = await fetch(relayed(upload.url), {
33
- method: 'POST',
34
- headers: { 'content-type': 'application/octet-stream', 'x-house-byte-operation': upload.operation },
35
- body: bytes,
102
+ }
103
+ async function drained(body) {
104
+ for await (const _chunk of body)
105
+ ;
106
+ }
107
+ export async function downloaded(address, fetching, destination, name) {
108
+ for (let asked = 1;; asked++) {
109
+ const { download, sha256 } = await address();
110
+ const answer = await fetching(download);
111
+ if (answer.status === 403 && asked === 1) {
112
+ await drained(answer.body);
113
+ continue;
114
+ }
115
+ if (answer.status !== 200) {
116
+ await drained(answer.body);
117
+ throw storeRefusal('download', answer.status);
118
+ }
119
+ await mkdir(dirname(destination), { recursive: true });
120
+ const partial = join(dirname(destination), `.${basename(destination)}.${randomUUID()}`);
121
+ const digest = createHash('sha256');
122
+ const hashed = new Transform({
123
+ transform(chunk, _encoding, done) {
124
+ digest.update(chunk);
125
+ done(null, chunk);
126
+ },
36
127
  });
37
- const text = await answer.text();
38
- if (!answer.ok)
39
- throw new HouseRefusal(upload.url, answer.status, text);
40
- return JSON.parse(text);
41
- });
128
+ try {
129
+ await pipeline(answer.body, hashed, createWriteStream(partial, { flags: 'wx' }));
130
+ if (digest.digest('hex') !== sha256)
131
+ throw new Error(`${name} did not arrive whole; download it again`);
132
+ await rename(partial, destination);
133
+ }
134
+ catch (error) {
135
+ await rm(partial, { force: true });
136
+ throw error;
137
+ }
138
+ return;
139
+ }
140
+ }
141
+ export async function fetched(url, signal) {
142
+ const answer = await fetch(relayed(url), { signal });
143
+ return { status: answer.status, body: answer.body ?? Readable.from([]) };
42
144
  }
package/dist/git.js CHANGED
@@ -8,7 +8,7 @@ export function localCopies() {
8
8
  return join(workspaceRoot(), 'local-copies');
9
9
  }
10
10
  export const HOUSE_REF = 'refs/house/received';
11
- const COMMITTER = 'House Kit <house-kit@localhost>';
11
+ const COMMITTER = 'Kit <house-kit@localhost>';
12
12
  export function run(cwd, args, input, env = {}) {
13
13
  return new Promise((resolve, reject) => {
14
14
  const child = spawn('git', args, { cwd, env: { ...withoutProxy(), ...env } });
@@ -49,16 +49,15 @@ export async function tree(repository, commit) {
49
49
  }
50
50
  return entries;
51
51
  }
52
- const TEXT_MAX_BYTES = 16 * 1024 * 1024;
53
52
  async function blob(repository, object) {
54
53
  return (await run(repository, ['cat-file', 'blob', object])).stdout;
55
54
  }
56
55
  export async function blobText(repository, object) {
57
56
  return (await blob(repository, object)).toString('utf8');
58
57
  }
59
- export async function isText(repository, object) {
58
+ export async function isText(repository, object, maxBytes) {
60
59
  const bytes = await blob(repository, object);
61
- return bytes.length <= TEXT_MAX_BYTES && isUtf8(bytes) && !bytes.includes(0);
60
+ return bytes.length <= maxBytes && isUtf8(bytes) && !bytes.includes(0);
62
61
  }
63
62
  export function blobId(content) {
64
63
  const bytes = Buffer.from(content, 'utf8');
package/dist/home.js CHANGED
@@ -1,8 +1,8 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { accessSync, chmodSync, constants, existsSync, linkSync, lstatSync, mkdirSync, 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';
3
3
  import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
4
4
  import { homedir, tmpdir } from 'node:os';
5
- import { join } from 'node:path';
5
+ import { join, resolve } from 'node:path';
6
6
  import { selectRelay } from './relay.js';
7
7
  const ENROLMENT = 'credential.json';
8
8
  const DISCONNECTED = 'disconnected.json';
@@ -19,24 +19,26 @@ function writable(directory) {
19
19
  return false;
20
20
  }
21
21
  }
22
- function ownTemporaryHome() {
23
- const home = join(tmpdir(), `house-kit-${process.getuid()}`);
22
+ function privateDirectory(path) {
24
23
  try {
25
- mkdirSync(home, { mode: 0o700 });
24
+ mkdirSync(path, { mode: 0o700 });
26
25
  }
27
26
  catch (error) {
28
27
  if (error.code !== 'EEXIST')
29
28
  throw error;
30
29
  }
31
- const found = lstatSync(home);
30
+ const found = lstatSync(path);
32
31
  if (found.isSymbolicLink())
33
- throw new Error(`${home} is a symbolic link`);
32
+ throw new Error(`${path} is a symbolic link`);
34
33
  if (!found.isDirectory())
35
- throw new Error(`${home} is not a directory`);
36
- if (found.uid !== process.getuid())
37
- throw new Error(`${home} is not owned by this User`);
38
- chmodSync(home, 0o700);
39
- return home;
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()}`));
40
42
  }
41
43
  function defaultHome() {
42
44
  const home = join(homedir(), '.house-kit');
@@ -45,6 +47,18 @@ function defaultHome() {
45
47
  export function kitHome() {
46
48
  return process.env.HOUSE_KIT_HOME || (defaulted ??= defaultHome());
47
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');
61
+ }
48
62
  export function workspaceRoot() {
49
63
  return process.env.HOUSE_KIT_WORKSPACE ?? '/agents/house';
50
64
  }