@agentshouse/kit 0.2.1-cf110d15f69c8 → 0.2.1-cf268dff1869b

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 (128) hide show
  1. package/README.md +38 -37
  2. package/dist/acp.js +90 -19
  3. package/dist/agents.js +155 -45
  4. package/dist/api.js +5 -1
  5. package/dist/app-git.js +10 -26
  6. package/dist/authority-main.js +2 -2
  7. package/dist/bridge.js +4 -7
  8. package/dist/building.js +50 -0
  9. package/dist/calls.js +71 -0
  10. package/dist/chatgpt.js +32 -0
  11. package/dist/clis.js +52 -42
  12. package/dist/connection.js +105 -0
  13. package/dist/conversations.js +526 -175
  14. package/dist/copies.js +89 -52
  15. package/dist/end.js +17 -2
  16. package/dist/events.js +33 -8
  17. package/dist/files.js +139 -31
  18. package/dist/git.js +13 -11
  19. package/dist/home.js +56 -12
  20. package/dist/house-cli.js +299 -0
  21. package/dist/house-main.js +7 -206
  22. package/dist/instructions.js +7 -11
  23. package/dist/kit-cli.js +68 -0
  24. package/dist/kit-main.js +8 -22
  25. package/dist/login.js +49 -31
  26. package/dist/logout.js +0 -2
  27. package/dist/mcp.js +6 -2
  28. package/dist/placement.js +31 -0
  29. package/dist/relay.js +22 -1
  30. package/dist/resident.js +97 -38
  31. package/dist/scope.js +15 -4
  32. package/dist/scripts.js +255 -0
  33. package/dist/service.js +23 -1
  34. package/dist/sign-in.js +22 -39
  35. package/dist/silence.js +28 -0
  36. package/dist/skills.js +49 -35
  37. package/dist/transcription.js +85 -45
  38. package/dist/translate.js +2 -8
  39. package/dist/update.js +113 -105
  40. package/dist/wire.js +40 -0
  41. package/dist/work.js +3 -5
  42. package/package.json +20 -20
  43. package/dist/bootstrap.json +0 -24
  44. package/dist/clis-main.js +0 -23
  45. package/dist/stream.js +0 -74
  46. package/how-we-work.md +0 -23
  47. package/skills/ask-matt/LICENSE +0 -21
  48. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  49. package/skills/ask-matt/SKILL.md +0 -91
  50. package/skills/ask-matt/agents/openai.yaml +0 -4
  51. package/skills/code-review/LICENSE +0 -21
  52. package/skills/code-review/SKILL.md +0 -89
  53. package/skills/code-review/agents/openai.yaml +0 -3
  54. package/skills/codebase-design/DEEPENING.md +0 -37
  55. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  56. package/skills/codebase-design/LICENSE +0 -21
  57. package/skills/codebase-design/SKILL.md +0 -116
  58. package/skills/codebase-design/agents/openai.yaml +0 -3
  59. package/skills/diagnosing-bugs/LICENSE +0 -21
  60. package/skills/diagnosing-bugs/SKILL.md +0 -140
  61. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  62. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  63. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  64. package/skills/domain-modeling/CONTEXT-FORMAT.md +0 -60
  65. package/skills/domain-modeling/LICENSE +0 -21
  66. package/skills/domain-modeling/SKILL.md +0 -76
  67. package/skills/domain-modeling/agents/openai.yaml +0 -3
  68. package/skills/grill-me/LICENSE +0 -21
  69. package/skills/grill-me/SKILL.md +0 -8
  70. package/skills/grill-me/agents/openai.yaml +0 -4
  71. package/skills/grill-with-docs/LICENSE +0 -21
  72. package/skills/grill-with-docs/SKILL.md +0 -8
  73. package/skills/grill-with-docs/agents/openai.yaml +0 -4
  74. package/skills/grilling/LICENSE +0 -21
  75. package/skills/grilling/SKILL.md +0 -30
  76. package/skills/grilling/agents/openai.yaml +0 -3
  77. package/skills/handoff/LICENSE +0 -21
  78. package/skills/handoff/SKILL.md +0 -17
  79. package/skills/handoff/agents/openai.yaml +0 -4
  80. package/skills/implement/LICENSE +0 -21
  81. package/skills/implement/SKILL.md +0 -16
  82. package/skills/implement/agents/openai.yaml +0 -4
  83. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  84. package/skills/improve-codebase-architecture/LICENSE +0 -21
  85. package/skills/improve-codebase-architecture/SKILL.md +0 -72
  86. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -4
  87. package/skills/prototype/LICENSE +0 -21
  88. package/skills/prototype/LOGIC.md +0 -67
  89. package/skills/prototype/SKILL.md +0 -28
  90. package/skills/prototype/UI.md +0 -112
  91. package/skills/prototype/agents/openai.yaml +0 -3
  92. package/skills/research/LICENSE +0 -21
  93. package/skills/research/SKILL.md +0 -14
  94. package/skills/research/agents/openai.yaml +0 -3
  95. package/skills/resolving-merge-conflicts/LICENSE +0 -21
  96. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  97. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  98. package/skills/tdd/LICENSE +0 -21
  99. package/skills/tdd/SKILL.md +0 -40
  100. package/skills/tdd/agents/openai.yaml +0 -3
  101. package/skills/tdd/mocking.md +0 -59
  102. package/skills/tdd/tests.md +0 -77
  103. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  104. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  105. package/skills/teach/LICENSE +0 -21
  106. package/skills/teach/MISSION-FORMAT.md +0 -31
  107. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  108. package/skills/teach/SKILL.md +0 -141
  109. package/skills/teach/agents/openai.yaml +0 -4
  110. package/skills/to-questionnaire/LICENSE +0 -21
  111. package/skills/to-questionnaire/SKILL.md +0 -55
  112. package/skills/to-questionnaire/agents/openai.yaml +0 -4
  113. package/skills/to-spec/LICENSE +0 -21
  114. package/skills/to-spec/SKILL.md +0 -76
  115. package/skills/to-spec/agents/openai.yaml +0 -4
  116. package/skills/to-tickets/LICENSE +0 -21
  117. package/skills/to-tickets/SKILL.md +0 -106
  118. package/skills/to-tickets/agents/openai.yaml +0 -4
  119. package/skills/wait-what/LICENSE +0 -21
  120. package/skills/wait-what/SKILL.md +0 -8
  121. package/skills/wait-what/agents/openai.yaml +0 -4
  122. package/skills/wayfinder/LICENSE +0 -21
  123. package/skills/wayfinder/SKILL.md +0 -129
  124. package/skills/wayfinder/agents/openai.yaml +0 -4
  125. package/skills/writing-for-agents/LICENSE +0 -21
  126. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  127. package/skills/writing-for-agents/SKILL.md +0 -83
  128. package/skills/writing-for-agents/agents/openai.yaml +0 -3
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';
@@ -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
  }
@@ -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
  }
@@ -117,7 +133,7 @@ async function ignoreRules(repository, writable, copy) {
117
133
  ].map(pattern);
118
134
  const granted = readOnly.filter((path) => !protectedPath(path)).map(pattern);
119
135
  await ruled(join(repository, '.gitignore'), [
120
- '# 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',
121
137
  '# content: Git does not track them, and House refuses a change to them even',
122
138
  '# when this file is edited or a path is force-added.',
123
139
  '/.gitignore',
@@ -132,7 +148,7 @@ async function ignoreRules(repository, writable, copy) {
132
148
  '',
133
149
  ].join('\n'));
134
150
  await ruled(join(repository, '.ignore'), [
135
- '# 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',
136
152
  '# content that .gitignore keeps out of Git.',
137
153
  `!${PROVENANCE}`,
138
154
  ...[...house, ...granted].map((rule) => `!${rule}`),
@@ -202,34 +218,45 @@ 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.unfollowed === 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
+ const mark = refreshing ? 'said' : 'unsaid';
254
+ for (const copy of held()) {
255
+ if (synced.has(copy.key) || copy.unfollowed === 'said' || copy.unfollowed === mark)
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
+ await this.save({ ...copy, unfollowed: mark });
258
+ if (refreshing)
259
+ said.push(`${rootOf(copy.room_handle)}: no longer followed; kept as it is`);
233
260
  }
234
261
  const current = [];
235
262
  for (const [key, handle] of synced) {
@@ -239,11 +266,8 @@ export class LocalCopies {
239
266
  said.push(await this.materialize(key, handle));
240
267
  continue;
241
268
  }
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 }))));
269
+ const { unfollowed, ...followed } = held;
270
+ current.push(unfollowed === undefined && held.room_handle === handle ? held : await this.save({ ...followed, room_handle: handle }));
247
271
  }
248
272
  catch (error) {
249
273
  refused = true;
@@ -251,13 +275,12 @@ export class LocalCopies {
251
275
  }
252
276
  }
253
277
  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) {
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) {
259
282
  refused = true;
260
- said.push(error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error));
283
+ said.push(failed);
261
284
  }
262
285
  }
263
286
  return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
@@ -291,11 +314,11 @@ export class LocalCopies {
291
314
  async push(cwd, selector, caller) {
292
315
  const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
293
316
  if (located.status !== 0)
294
- return { refused: true, text: `${cwd} is not a House Local copy; run house push inside one` };
317
+ return outside(cwd);
295
318
  const [common, worktree] = located.stdout.toString('utf8').split('\n');
296
319
  const key = await this.keyAt(dirname(common));
297
320
  if (key === undefined)
298
- return { refused: true, text: `${worktree} is not a House Local copy; run house push inside one` };
321
+ return outside(worktree);
299
322
  return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
300
323
  }
301
324
  async keyAt(repository) {
@@ -316,7 +339,7 @@ export class LocalCopies {
316
339
  return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
317
340
  }
318
341
  async save(copy) {
319
- const path = join(kitHome(), 'local-copies', `${copy.key}.json`);
342
+ const path = recordOf(copy.key);
320
343
  await mkdir(dirname(path), { recursive: true, mode: 0o700 });
321
344
  const temporary = `${path}.${process.pid}`;
322
345
  await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
@@ -365,7 +388,7 @@ export class LocalCopies {
365
388
  throw new Error('House serves nothing of it to copy');
366
389
  const repository = join(localCopies(), 'rooms', read.anchor);
367
390
  if (await exists(repository))
368
- 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`);
369
392
  const staging = join(dirname(repository), `.${randomUUID()}`);
370
393
  await mkdir(staging, { recursive: true });
371
394
  let house;
@@ -390,7 +413,6 @@ export class LocalCopies {
390
413
  room_ref: read.anchor,
391
414
  room_handle: handle,
392
415
  repository,
393
- active: true,
394
416
  house,
395
417
  position: read.position,
396
418
  log_epoch: read.log_epoch,
@@ -425,19 +447,24 @@ export class LocalCopies {
425
447
  return next;
426
448
  }
427
449
  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();
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 = [];
432
456
  for (const copy of copies) {
433
- const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
457
+ const authority = authorities.find((entry) => entry.room_ref === copy.room_ref);
434
458
  if (authority !== undefined &&
435
459
  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)));
460
+ await this.save(await this.received(copy));
461
+ }
462
+ else {
463
+ unchanged.push(copy);
437
464
  }
438
465
  }
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));
466
+ for (const group of grouped(unchanged, bounds.max_prefixes))
467
+ await this.caughtUp(group, false);
441
468
  }
442
469
  async caughtUp(copies, whole) {
443
470
  const caught = new Map(copies.map((copy) => [copy.key, copy]));
@@ -532,9 +559,6 @@ export class LocalCopies {
532
559
  }
533
560
  copy = await this.save(without(copy));
534
561
  }
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
562
  const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
539
563
  const base = listed.find((ancestor) => ancestor in copy.manifests);
540
564
  if (base === undefined) {
@@ -545,7 +569,7 @@ export class LocalCopies {
545
569
  changes = await this.derived(copy, base, commit);
546
570
  }
547
571
  catch (error) {
548
- return { refused: true, text: causeOf(error) };
572
+ return { refused: true, text: houseCause(error) };
549
573
  }
550
574
  if (changes.length === 0)
551
575
  return { refused: false, text: `Nothing to submit: ${short(commit)} matches House.` };
@@ -572,7 +596,18 @@ export class LocalCopies {
572
596
  const after = await tree(copy.repository, commit);
573
597
  const moved = movedPaths(copy.moves, before, after);
574
598
  const arrived = new Set(moved.values());
575
- const text = (entry, path) => blobText(copy.repository, entry, path);
599
+ const arriving = [...after].filter(([path, entry]) => before.get(path) !== entry);
600
+ if (arriving.length > 0) {
601
+ const { bounds } = await this.house.post('/kit/door/discover', { prefixes: [root] });
602
+ const binary = [];
603
+ for (const [path, entry] of arriving) {
604
+ if (!(await isText(copy.repository, entry, bounds.max_authored_file_bytes)))
605
+ binary.push(path);
606
+ }
607
+ if (binary.length > 0)
608
+ throw new Error(textOnly(binary));
609
+ }
610
+ const text = (entry) => blobText(copy.repository, entry);
576
611
  const changes = [];
577
612
  for (const path of [...new Set([...before.keys(), ...after.keys()])].sort()) {
578
613
  if (arrived.has(path))
@@ -585,7 +620,7 @@ export class LocalCopies {
585
620
  changes.push({ op: 'rename', path: `${root}/${path}`, to: `${root}/${to}`, base: known });
586
621
  const moving = after.get(to);
587
622
  if (moving !== prior) {
588
- changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving, to) });
623
+ changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving) });
589
624
  }
590
625
  continue;
591
626
  }
@@ -594,9 +629,9 @@ export class LocalCopies {
594
629
  if (next === undefined)
595
630
  changes.push({ op: 'remove', path: `${root}/${path}`, base: known });
596
631
  else if (prior === undefined)
597
- changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next, path) });
632
+ changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next) });
598
633
  else
599
- changes.push(...changed(`${root}/${path}`, path, known, await text(prior, path), await text(next, path)));
634
+ changes.push(...changed(`${root}/${path}`, path, known, await text(prior), await text(next)));
600
635
  }
601
636
  return changes;
602
637
  }
@@ -625,8 +660,10 @@ export class LocalCopies {
625
660
  }
626
661
  if (outcome.refused.code === 'expired')
627
662
  return this.recovered(copy);
628
- const rebased = rebaseFixes(outcome.refused);
629
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);
630
667
  try {
631
668
  settled = (await this.caughtUp([settled], true))[0];
632
669
  }
@@ -663,7 +700,7 @@ export class LocalCopies {
663
700
  written.push({
664
701
  path: `${root}/${path}`,
665
702
  revision: entry === undefined ? null : (revisionAt(current, current.house, path) ?? null),
666
- content: entry === undefined ? null : await blobText(copy.repository, entry, path),
703
+ content: entry === undefined ? null : await blobText(copy.repository, entry),
667
704
  });
668
705
  }
669
706
  return this.finished(await this.save({ ...current, push: { ...push, state: 'accepted', written } }));
@@ -707,7 +744,7 @@ export class LocalCopies {
707
744
  {
708
745
  path: `${rootOf(copy.room_handle)}/${path}`,
709
746
  revision: entry === undefined ? null : (revisionAt(copy, copy.house, path) ?? null),
710
- content: entry === undefined ? null : await blobText(copy.repository, entry, path),
747
+ content: entry === undefined ? null : await blobText(copy.repository, entry),
711
748
  },
712
749
  ];
713
750
  }
@@ -752,7 +789,7 @@ export class LocalCopies {
752
789
  async landed(repository, changes, house, selected) {
753
790
  const text = async (path) => {
754
791
  const entry = house.get(relative(path));
755
- return entry === undefined ? undefined : blobText(repository, entry, path);
792
+ return entry === undefined ? undefined : blobText(repository, entry);
756
793
  };
757
794
  for (const change of changes) {
758
795
  const current = await text(change.path);
package/dist/end.js CHANGED
@@ -1,17 +1,32 @@
1
+ import { KIT_RELEASE } from './clis.js';
1
2
  import { forgetRejected } from './home.js';
3
+ import { installCommand } from './placement.js';
2
4
  import { killDescendants } from './scope.js';
3
- export const REPLACED = 'another Kit started for this Environment';
5
+ export const REPLACED = 'another Kit process started for this Environment';
4
6
  function exit(reason, status) {
5
7
  process.stderr.write(`kit: ${reason}\n`);
6
8
  killDescendants();
7
9
  process.exit(status);
8
10
  }
9
11
  export function end(reason) {
10
- exit(reason, 0);
12
+ exit(reason, process.env.HOUSE_KIT_RESIDENT === String(process.pid) ? 0 : 1);
11
13
  }
12
14
  export function restart(reason) {
13
15
  exit(reason, 1);
14
16
  }
17
+ export function updateLine(lead) {
18
+ const command = installCommand();
19
+ return command === null ? `${lead}; install it again.` : `${lead}. Update it: ${command}`;
20
+ }
21
+ export function updateRequired() {
22
+ end(updateLine('This Kit is too old for House'));
23
+ }
24
+ export function unknownMethod(kind) {
25
+ return updateLine(`This Kit does not know ${kind}`);
26
+ }
27
+ export function newerThanHouse(release, command) {
28
+ end(`Kit ${KIT_RELEASE} is newer than this Building supports (Kit ${release}). Install Kit ${release}: ${command}`);
29
+ }
15
30
  export async function signInAgain(rejected) {
16
31
  await forgetRejected(rejected);
17
32
  restart('sign in again');
package/dist/events.js CHANGED
@@ -1,8 +1,13 @@
1
+ const AUTONOMOUS_ORIGINS = new Set(['task-notification', 'peer', 'coordinator', 'observer', 'observer-activity']);
1
2
  const COMMAND = { shown: 'command' };
2
3
  const HIDDEN = { shown: 'hidden' };
3
4
  const FINISHED = new Set(['completed', 'failed']);
4
5
  export const RUNNING_JOB = new Set(['running', 'paused']);
5
- const ROUTINE_TARGETS = { send_routine_request: 'access', cancel_routine_request: 'request' };
6
+ const MESSAGE_TARGETS = new Map([
7
+ ['send_routine_request', ['routine', 'agent']],
8
+ ['cancel_routine_request', ['request']],
9
+ ['send_mail', ['conversation', 'request']],
10
+ ]);
6
11
  function words(line) {
7
12
  const found = [];
8
13
  let word = null;
@@ -48,11 +53,13 @@ export function houseMessage(line) {
48
53
  catch {
49
54
  return null;
50
55
  }
51
- const argument = ROUTINE_TARGETS[call.command];
52
- if (argument === undefined)
56
+ if (call === null)
53
57
  return null;
54
- const target = call.arguments?.[argument];
55
- return { shown: 'message', command: call.command, target: typeof target === 'string' ? target : null };
58
+ const targets = MESSAGE_TARGETS.get(call.command);
59
+ if (targets === undefined)
60
+ return null;
61
+ const target = targets.map((argument) => call.arguments?.[argument]).find((value) => typeof value === 'string');
62
+ return { shown: 'message', command: call.command, target: target ?? null };
56
63
  }
57
64
  function decidedByLine(command, final) {
58
65
  const line = typeof command === 'string' ? command : Array.isArray(command) ? command.at(-1) : undefined;
@@ -269,6 +276,7 @@ class Reader {
269
276
  class ClaudeReader extends Reader {
270
277
  watchedOptions = { model: 'model' };
271
278
  tasks = new Map();
279
+ stop = null;
272
280
  own(message) {
273
281
  if (message.params?.update?._meta?.claudeCode?.parentToolUseId !== undefined)
274
282
  return [];
@@ -276,7 +284,9 @@ class ClaudeReader extends Reader {
276
284
  }
277
285
  update(update) {
278
286
  if (update.sessionUpdate === 'usage_update' && update._meta?.['_claude/origin'] !== undefined) {
279
- return [...super.update(update), { kind: 'ended' }];
287
+ const stop = this.stop;
288
+ this.stop = null;
289
+ return [...super.update(update), { kind: 'ended', stop }];
280
290
  }
281
291
  if (update.sessionUpdate === 'session_info_update' && update._meta?.jetbrains?.air?.sessionFailure !== undefined) {
282
292
  return [{ kind: 'marker', marker: 'retry', text: update._meta.jetbrains.air.sessionFailure.title }];
@@ -287,6 +297,13 @@ class ClaudeReader extends Reader {
287
297
  if (message.method !== '_claude/sdkMessage')
288
298
  return [];
289
299
  const sdk = message.params.message;
300
+ if (sdk.type === 'result') {
301
+ if (AUTONOMOUS_ORIGINS.has(sdk.origin?.kind))
302
+ this.stop = null;
303
+ else
304
+ this.stop = sdk.stop_reason === 'refusal' ? 'refusal' : sdk.is_error === true ? null : 'end_turn';
305
+ return [];
306
+ }
290
307
  switch (sdk.subtype) {
291
308
  case 'task_started':
292
309
  if (sdk.tool_use_id !== undefined)
@@ -294,6 +311,10 @@ class ClaudeReader extends Reader {
294
311
  return [];
295
312
  case 'task_notification':
296
313
  return sdk.tool_use_id === undefined ? [] : this.finish(sdk.tool_use_id, sdk.summary ?? null);
314
+ case 'task_progress':
315
+ if (!this.subagents.has(sdk.tool_use_id) || this.done.has(sdk.tool_use_id) || typeof sdk.description !== 'string')
316
+ return [];
317
+ return [{ kind: 'subagent-activity', id: sdk.tool_use_id, activity: sdk.description }];
297
318
  default:
298
319
  return [];
299
320
  }
@@ -356,7 +377,7 @@ class CodexReader extends Reader {
356
377
  if (codex?.threadStatus?.type === 'active')
357
378
  return [{ kind: 'started' }];
358
379
  if (codex?.threadStatus?.type === 'idle')
359
- return [{ kind: 'ended' }];
380
+ return [{ kind: 'ended', stop: null }];
360
381
  if (codex?.error !== undefined)
361
382
  return [{ kind: 'marker', marker: 'retry', text: codex.error.message ?? '' }];
362
383
  return [];
@@ -429,7 +450,7 @@ class GrokReader extends Reader {
429
450
  return (message.params.runningPromptId ?? null) !== null && session === this.session ? [{ kind: 'started' }] : [];
430
451
  }
431
452
  if (message.params?.update?.sessionUpdate === 'turn_completed') {
432
- return session === this.session ? [{ kind: 'ended' }] : [];
453
+ return session === this.session ? [{ kind: 'ended', stop: null }] : [];
433
454
  }
434
455
  if (message.method === '_x.ai/models/update') {
435
456
  this.offered(message.params);
@@ -512,6 +533,10 @@ class GrokReader extends Reader {
512
533
  return [];
513
534
  }
514
535
  }
536
+ export function answeredFailure(answer) {
537
+ const failure = answer._meta?.jetbrains?.air?.sessionFailure;
538
+ return typeof failure?.title === 'string' ? { title: failure.title, limit: failure.category === 'limit' } : null;
539
+ }
515
540
  export function readerFor(kind) {
516
541
  if (kind === 'claude-agent-acp')
517
542
  return new ClaudeReader();
package/dist/files.js CHANGED
@@ -1,36 +1,144 @@
1
- import { mkdir, writeFile } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
- 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';
4
10
  import { relayed } from './relay.js';
5
- export async function placeFiles(house, directory, files, signal) {
6
- const paths = [];
7
- for (const file of files) {
8
- const original = await house.post('/kit/conversation/originals/get', { version: file.version, download: true }, signal);
9
- const answer = await fetch(relayed(original.download.url), { signal });
10
- if (!answer.ok)
11
- throw new Error(`${file.name} did not download: ${answer.status}`);
12
- const folder = join(directory, '.house', 'files', original.message, file.version);
13
- await mkdir(join(directory, '.house')).catch((error) => {
14
- 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)
15
97
  throw error;
16
- });
17
- await mkdir(folder, { recursive: true });
18
- const path = join(folder, file.name);
19
- await writeFile(path, Buffer.from(await answer.arrayBuffer()));
20
- 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));
21
101
  }
22
- return paths;
23
- }
24
- export function uploadBytes(upload, bytes) {
25
- return retrying(async () => {
26
- const answer = await fetch(relayed(upload.url), {
27
- method: 'POST',
28
- headers: { 'content-type': 'application/octet-stream', 'x-house-byte-operation': upload.operation },
29
- 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
+ },
30
127
  });
31
- const text = await answer.text();
32
- if (!answer.ok)
33
- throw new HouseRefusal(upload.url, answer.status, text);
34
- return JSON.parse(text);
35
- });
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([]) };
36
144
  }