@agentshouse/kit 0.2.1-c3cf4b145dcda → 0.2.1-c3db1f18aed8f

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 +24 -23
  2. package/dist/acp.js +7 -97
  3. package/dist/agents.js +27 -6
  4. package/dist/api.js +9 -5
  5. package/dist/bootstrap.json +4 -4
  6. package/dist/bridge.js +11 -37
  7. package/dist/chatgpt.js +26 -0
  8. package/dist/clis.js +86 -14
  9. package/dist/codex-main.js +58 -0
  10. package/dist/conversations.js +56 -55
  11. package/dist/copies.js +145 -110
  12. package/dist/end.js +18 -4
  13. package/dist/enrol-main.js +7 -1
  14. package/dist/events.js +10 -76
  15. package/dist/files.js +7 -1
  16. package/dist/git.js +11 -8
  17. package/dist/home.js +78 -6
  18. package/dist/house-main.js +183 -56
  19. package/dist/instructions.js +9 -32
  20. package/dist/kit-main.js +53 -8
  21. package/dist/login.js +93 -107
  22. package/dist/logout.js +14 -14
  23. package/dist/parts.js +34 -16
  24. package/dist/placement.js +48 -0
  25. package/dist/relay.js +12 -2
  26. package/dist/resident.js +35 -24
  27. package/dist/scope.js +203 -0
  28. package/dist/service-main.js +9 -0
  29. package/dist/service.js +33 -0
  30. package/dist/sign-in.js +1 -1
  31. package/dist/skills.js +80 -12
  32. package/dist/stream.js +9 -4
  33. package/dist/transcription.js +3 -3
  34. package/dist/update.js +28 -6
  35. package/dist/wire.js +6 -0
  36. package/dist/work.js +348 -0
  37. package/how-we-work.md +11 -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';
@@ -29,9 +29,29 @@ const REBASED = new Set([
29
29
  function rebaseFixes(refusal) {
30
30
  return refusal.code === 'edit_conflict' && refusal.conflicts.some((code) => REBASED.has(code));
31
31
  }
32
+ function textOnly(paths) {
33
+ const [named, them] = paths.length === 1 ? [`${paths[0]} is`, 'it'] : [`${paths.join(', ')} are`, 'them'];
34
+ return `${named} not text, and house push sends text only; remove ${them} from the commit and place ${them} with house upload`;
35
+ }
32
36
  function short(commit) {
33
37
  return commit.slice(0, 12);
34
38
  }
39
+ function moved(before, after) {
40
+ if (before.house !== after.house)
41
+ return [`${rootOf(after.room_handle)}: ${HOUSE_REF} is at ${short(after.house)}`];
42
+ return before.position === after.position ? [] : [`${rootOf(after.room_handle)}: read-only files refreshed`];
43
+ }
44
+ function expiredAuthority(error) {
45
+ if (!(error instanceof HouseRefusal))
46
+ return undefined;
47
+ try {
48
+ const { error: refused } = JSON.parse(error.text);
49
+ return refused?.code === 'expired' && typeof refused.authority === 'string' ? refused.authority : undefined;
50
+ }
51
+ catch {
52
+ return undefined;
53
+ }
54
+ }
35
55
  function covers(path, prefix) {
36
56
  return prefix === '' || path === prefix || path.startsWith(`${prefix}/`);
37
57
  }
@@ -152,8 +172,7 @@ export class LocalCopies {
152
172
  house;
153
173
  copies = new Map();
154
174
  queues = new Map();
155
- delivering = new Set();
156
- selecting = Promise.resolve();
175
+ answering = Promise.resolve();
157
176
  constructor(house) {
158
177
  this.house = house;
159
178
  this.owner = {
@@ -185,42 +204,68 @@ export class LocalCopies {
185
204
  }
186
205
  }
187
206
  async mapping() {
188
- await this.selecting;
207
+ await this.answering;
189
208
  return [...this.copies.values()]
190
209
  .filter((copy) => copy.active)
191
210
  .map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
192
211
  .sort();
193
212
  }
194
- select() {
195
- const selected = this.selecting.then(() => this.reselect());
196
- this.selecting = selected.catch(() => undefined);
197
- return selected;
198
- }
199
- async reselect() {
200
- const { local_copy: chosen } = await this.house.post('/kit/local-copy/selection', {});
201
- const selected = (chosen?.rooms ?? []).flatMap((room) => room.room_handle === null ? [] : [{ key: `rooms/${room.room_ref}`, handle: room.room_handle }]);
202
- const keys = new Set(selected.map((scope) => scope.key));
203
- const work = [];
204
- for (const key of this.copies.keys()) {
205
- if (keys.has(key))
206
- continue;
207
- work.push(this.exclusive(key, async () => {
208
- const copy = this.copies.get(key);
209
- if (copy.active)
210
- await this.save({ ...copy, active: false });
211
- }));
212
- }
213
- for (const scope of selected)
214
- work.push(this.exclusive(scope.key, () => this.apply(scope)).catch(logged));
215
- await Promise.all(work);
213
+ async connected() {
214
+ const copied = await this.answered(() => this.fetched(false));
215
+ if (copied.refused)
216
+ logged(copied.text);
216
217
  }
217
- entries(frame) {
218
- const copy = [...this.copies.values()].find((held) => held.active && held.room_ref === frame.authority);
219
- if (copy === undefined)
220
- return;
221
- if (copy.log_epoch === frame.log_epoch && BigInt(frame.position) <= BigInt(copy.position))
222
- return;
223
- this.deliver(copy.key);
218
+ fetch() {
219
+ return this.answered(() => this.fetched(true));
220
+ }
221
+ answered(work) {
222
+ const answered = this.answering.then(work);
223
+ this.answering = answered.catch(() => undefined);
224
+ return answered;
225
+ }
226
+ async fetched(refreshing) {
227
+ const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', {});
228
+ const synced = new Map(answer.rooms.map((room) => [`rooms/${room.room_ref}`, room.room_handle]));
229
+ return this.holding([...new Set([...this.copies.keys(), ...synced.keys()])].sort(), async () => {
230
+ const said = [];
231
+ let refused = false;
232
+ for (const copy of this.copies.values()) {
233
+ if (synced.has(copy.key) || !copy.active)
234
+ continue;
235
+ await this.save({ ...copy, active: false });
236
+ said.push(`${rootOf(copy.room_handle)}: no longer synced to this Environment; its files stay`);
237
+ }
238
+ const current = [];
239
+ for (const [key, handle] of synced) {
240
+ try {
241
+ const held = this.copies.get(key);
242
+ if (held === undefined || !(await exists(held.repository))) {
243
+ said.push(await this.materialize(key, handle));
244
+ continue;
245
+ }
246
+ const copy = held.room_handle === handle ? held : await this.save({ ...held, room_handle: handle });
247
+ if (copy.active)
248
+ current.push(copy);
249
+ else
250
+ said.push(...moved(copy, await this.save(await this.received({ ...copy, active: true }))));
251
+ }
252
+ catch (error) {
253
+ refused = true;
254
+ said.push(`${rootOf(handle)}: ${causeOf(error)}`);
255
+ }
256
+ }
257
+ if (refreshing && current.length > 0) {
258
+ try {
259
+ const refreshed = await this.refreshed(current);
260
+ current.forEach((copy, index) => said.push(...moved(copy, refreshed[index])));
261
+ }
262
+ catch (error) {
263
+ refused = true;
264
+ said.push(error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error));
265
+ }
266
+ }
267
+ return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
268
+ });
224
269
  }
225
270
  async observed(report) {
226
271
  const key = await this.keyAt(dirname(report.common));
@@ -270,16 +315,9 @@ export class LocalCopies {
270
315
  this.queues.set(key, queued.catch(() => undefined));
271
316
  return queued;
272
317
  }
273
- deliver(key) {
274
- if (this.delivering.has(key))
275
- return;
276
- this.delivering.add(key);
277
- void this.exclusive(key, async () => {
278
- this.delivering.delete(key);
279
- const copy = this.copies.get(key);
280
- if (copy.active)
281
- await this.save(await this.caughtUp(copy));
282
- }).catch(logged);
318
+ holding(keys, work) {
319
+ const [key, ...rest] = keys;
320
+ return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
283
321
  }
284
322
  async save(copy) {
285
323
  const path = join(kitHome(), 'local-copies', `${copy.key}.json`);
@@ -325,46 +363,13 @@ export class LocalCopies {
325
363
  }
326
364
  return read;
327
365
  }
328
- async apply(scope) {
329
- let copy = this.copies.get(scope.key);
330
- if (copy === undefined || !(await exists(copy.repository))) {
331
- await this.materialize(scope);
332
- return;
333
- }
334
- if (copy.room_handle !== scope.handle)
335
- copy = await this.save({ ...copy, room_handle: scope.handle });
336
- if (!copy.active) {
337
- await this.save(await this.received({ ...copy, active: true }));
338
- return;
339
- }
340
- const discovery = await this.house.post('/kit/door/discover', {
341
- prefixes: [rootOf(copy.room_handle)],
342
- });
343
- const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
344
- if (authority === undefined)
345
- return;
346
- if (authority.managed_replication !== 'admitted') {
347
- logged(`operation_denied: ${rootOf(copy.room_handle)} is not synchronized to this Environment`);
348
- return;
349
- }
350
- if (JSON.stringify([authority.operations.write, authority.protected_paths]) !==
351
- JSON.stringify([copy.write, copy.protected])) {
352
- await this.save(await this.received(copy));
353
- return;
354
- }
355
- await this.save(await this.caughtUp(copy));
356
- }
357
- async materialize(scope) {
358
- const read = await this.read(scope.handle);
359
- if (read === null) {
360
- logged(`House serves nothing of ${rootOf(scope.handle)} to copy`);
361
- return;
362
- }
366
+ async materialize(key, handle) {
367
+ const read = await this.read(handle);
368
+ if (read === null)
369
+ throw new Error('House serves nothing of it to copy');
363
370
  const repository = join(localCopies(), 'rooms', read.anchor);
364
- if (await exists(repository)) {
365
- logged(`${repository} already exists and House Kit leaves it as it is`);
366
- return;
367
- }
371
+ if (await exists(repository))
372
+ throw new Error(`${repository} already exists and House Kit leaves it as it is`);
368
373
  const staging = join(dirname(repository), `.${randomUUID()}`);
369
374
  await mkdir(staging, { recursive: true });
370
375
  let house;
@@ -372,7 +377,7 @@ export class LocalCopies {
372
377
  try {
373
378
  await git(staging, ['init', '-q', '-b', BRANCH]);
374
379
  house = await importCommits(staging, [
375
- { message: `House ${rootOf(scope.handle)}`, from: null, files: read.writable },
380
+ { message: `House ${rootOf(handle)}`, from: null, files: read.writable },
376
381
  ]);
377
382
  await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
378
383
  await git(staging, ['reset', '-q', '--hard']);
@@ -385,9 +390,9 @@ export class LocalCopies {
385
390
  throw error;
386
391
  }
387
392
  await this.save({
388
- key: scope.key,
393
+ key,
389
394
  room_ref: read.anchor,
390
- room_handle: scope.handle,
395
+ room_handle: handle,
391
396
  repository,
392
397
  active: true,
393
398
  house,
@@ -399,6 +404,7 @@ export class LocalCopies {
399
404
  manifests: { [house]: { parent: null, files: read.revisions } },
400
405
  moves: [],
401
406
  });
407
+ return `${rootOf(handle)}: copied to ${repository}`;
402
408
  }
403
409
  async received(copy) {
404
410
  const read = await this.read(copy.room_handle);
@@ -422,29 +428,52 @@ export class LocalCopies {
422
428
  await ignoreRules(copy.repository, [...read.writable.keys()], next);
423
429
  return next;
424
430
  }
425
- async caughtUp(start) {
426
- let copy = start;
427
- for (;;) {
431
+ async refreshed(copies) {
432
+ const discovery = await this.house.post('/kit/door/discover', {
433
+ prefixes: copies.map((copy) => rootOf(copy.room_handle)),
434
+ });
435
+ const regranted = new Map();
436
+ for (const copy of copies) {
437
+ const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
438
+ if (authority !== undefined &&
439
+ JSON.stringify([authority.operations.write, authority.protected_paths]) !== JSON.stringify([copy.write, copy.protected])) {
440
+ regranted.set(copy.key, await this.save(await this.received(copy)));
441
+ }
442
+ }
443
+ const caught = await this.caughtUp(copies.filter((copy) => !regranted.has(copy.key)), false);
444
+ return copies.map((copy) => regranted.get(copy.key) ?? caught.find((held) => held.key === copy.key));
445
+ }
446
+ async caughtUp(copies, whole) {
447
+ const caught = new Map(copies.map((copy) => [copy.key, copy]));
448
+ let asking = [...copies];
449
+ while (asking.length > 0) {
428
450
  let answer;
429
451
  try {
430
452
  answer = await this.house.post('/kit/door/enumerate', {
431
- prefixes: [rootOf(copy.room_handle)],
432
- positions: [{ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch }],
453
+ prefixes: asking.map((copy) => rootOf(copy.room_handle)),
454
+ positions: asking.map((copy) => ({ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch })),
433
455
  });
434
456
  }
435
457
  catch (error) {
436
- const code = error instanceof HouseRefusal ? (refusalOf(error.text)?.code ?? null) : null;
437
- if (code === 'position_expired')
438
- return this.received(copy);
439
- if (code !== 'replica_behind')
458
+ const expired = asking.find((copy) => copy.room_ref === expiredAuthority(error));
459
+ if (expired === undefined)
440
460
  throw error;
441
- return copy;
461
+ caught.set(expired.key, await this.save(await this.received(expired)));
462
+ asking = asking.filter((copy) => copy !== expired);
463
+ continue;
464
+ }
465
+ const advanced = [];
466
+ for (const copy of asking) {
467
+ const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
468
+ if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
469
+ continue;
470
+ const next = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
471
+ caught.set(copy.key, next);
472
+ advanced.push(next);
442
473
  }
443
- const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
444
- if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
445
- return copy;
446
- copy = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
474
+ asking = whole ? advanced : [];
447
475
  }
476
+ return copies.map((copy) => caught.get(copy.key));
448
477
  }
449
478
  async incorporated(copy, changes, position, logEpoch) {
450
479
  const houseTree = await tree(copy.repository, copy.house);
@@ -547,7 +576,13 @@ export class LocalCopies {
547
576
  const after = await tree(copy.repository, commit);
548
577
  const moved = movedPaths(copy.moves, before, after);
549
578
  const arrived = new Set(moved.values());
550
- const text = (entry, path) => blobText(copy.repository, entry, path);
579
+ const binary = [];
580
+ for (const [path, entry] of after)
581
+ if (before.get(path) !== entry && !(await isText(copy.repository, entry)))
582
+ binary.push(path);
583
+ if (binary.length > 0)
584
+ throw new Error(textOnly(binary));
585
+ const text = (entry) => blobText(copy.repository, entry);
551
586
  const changes = [];
552
587
  for (const path of [...new Set([...before.keys(), ...after.keys()])].sort()) {
553
588
  if (arrived.has(path))
@@ -560,7 +595,7 @@ export class LocalCopies {
560
595
  changes.push({ op: 'rename', path: `${root}/${path}`, to: `${root}/${to}`, base: known });
561
596
  const moving = after.get(to);
562
597
  if (moving !== prior) {
563
- changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving, to) });
598
+ changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving) });
564
599
  }
565
600
  continue;
566
601
  }
@@ -569,9 +604,9 @@ export class LocalCopies {
569
604
  if (next === undefined)
570
605
  changes.push({ op: 'remove', path: `${root}/${path}`, base: known });
571
606
  else if (prior === undefined)
572
- changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next, path) });
607
+ changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next) });
573
608
  else
574
- changes.push(...changed(`${root}/${path}`, path, known, await text(prior, path), await text(next, path)));
609
+ changes.push(...changed(`${root}/${path}`, path, known, await text(prior), await text(next)));
575
610
  }
576
611
  return changes;
577
612
  }
@@ -598,12 +633,12 @@ export class LocalCopies {
598
633
  const { upload, ...unstaged } = copy.push;
599
634
  return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
600
635
  }
601
- if (outcome.refused.code === 'operation_expired')
636
+ if (outcome.refused.code === 'expired')
602
637
  return this.recovered(copy);
603
638
  const rebased = rebaseFixes(outcome.refused);
604
639
  let settled = await this.save(without(copy));
605
640
  try {
606
- settled = await this.save(await this.caughtUp(settled));
641
+ settled = (await this.caughtUp([settled], true))[0];
607
642
  }
608
643
  catch {
609
644
  return {
@@ -622,7 +657,7 @@ export class LocalCopies {
622
657
  }
623
658
  async recovered(copy) {
624
659
  const push = copy.push;
625
- const current = await this.save(await this.caughtUp(copy));
660
+ const current = (await this.caughtUp([copy], true))[0];
626
661
  const house = await tree(copy.repository, current.house);
627
662
  const root = rootOf(current.room_handle);
628
663
  if (!(await this.landed(copy.repository, push.changes, house, await tree(copy.repository, push.commit)))) {
@@ -638,7 +673,7 @@ export class LocalCopies {
638
673
  written.push({
639
674
  path: `${root}/${path}`,
640
675
  revision: entry === undefined ? null : (revisionAt(current, current.house, path) ?? null),
641
- content: entry === undefined ? null : await blobText(copy.repository, entry, path),
676
+ content: entry === undefined ? null : await blobText(copy.repository, entry),
642
677
  });
643
678
  }
644
679
  return this.finished(await this.save({ ...current, push: { ...push, state: 'accepted', written } }));
@@ -666,7 +701,7 @@ export class LocalCopies {
666
701
  const push = copy.push;
667
702
  let written = push.written;
668
703
  if (push.changes.some((change) => change.op === 'rename')) {
669
- copy = await this.save(await this.caughtUp(copy));
704
+ copy = (await this.caughtUp([copy], true))[0];
670
705
  const house = await tree(copy.repository, copy.house);
671
706
  const sent = await tree(copy.repository, push.house);
672
707
  if (push.changes.some((change) => change.op === 'rename' && !house.has(relative(change.to)))) {
@@ -682,7 +717,7 @@ export class LocalCopies {
682
717
  {
683
718
  path: `${rootOf(copy.room_handle)}/${path}`,
684
719
  revision: entry === undefined ? null : (revisionAt(copy, copy.house, path) ?? null),
685
- content: entry === undefined ? null : await blobText(copy.repository, entry, path),
720
+ content: entry === undefined ? null : await blobText(copy.repository, entry),
686
721
  },
687
722
  ];
688
723
  }
@@ -727,7 +762,7 @@ export class LocalCopies {
727
762
  async landed(repository, changes, house, selected) {
728
763
  const text = async (path) => {
729
764
  const entry = house.get(relative(path));
730
- return entry === undefined ? undefined : blobText(repository, entry, path);
765
+ return entry === undefined ? undefined : blobText(repository, entry);
731
766
  };
732
767
  for (const change of changes) {
733
768
  const current = await text(change.path);
package/dist/end.js CHANGED
@@ -1,8 +1,22 @@
1
- import { killDescendants } from './acp.js';
1
+ import { forgetRejected } from './home.js';
2
+ import { installCommand } from './placement.js';
3
+ import { killDescendants } from './scope.js';
2
4
  export const REPLACED = 'another Kit started for this Environment';
3
- export const SIGN_IN_AGAIN = 'sign in again';
4
- export function end(reason) {
5
+ function exit(reason, status) {
5
6
  process.stderr.write(`kit: ${reason}\n`);
6
7
  killDescendants();
7
- process.exit(0);
8
+ process.exit(status);
9
+ }
10
+ export function end(reason) {
11
+ exit(reason, 0);
12
+ }
13
+ export function restart(reason) {
14
+ exit(reason, 1);
15
+ }
16
+ export function updateRequired(house) {
17
+ end(`This Kit is too old for House. Update it: ${installCommand(house)}`);
18
+ }
19
+ export async function signInAgain(rejected) {
20
+ await forgetRejected(rejected);
21
+ restart('sign in again');
8
22
  }
@@ -1,4 +1,10 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { writeEnrolment } from './home.js';
3
3
  const [house, environment] = process.argv.slice(2);
4
- await writeEnrolment({ house: house, environment: environment, credential: readFileSync(0, 'utf8').trim() });
4
+ const relay = process.env.HOUSE_KIT_RELAY || undefined;
5
+ await writeEnrolment({
6
+ house: house,
7
+ ...(relay === undefined ? {} : { relay }),
8
+ environment: environment,
9
+ credential: readFileSync(0, 'utf8').trim(),
10
+ });
package/dist/events.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const COMMAND = { shown: 'command' };
2
2
  const HIDDEN = { shown: 'hidden' };
3
3
  const FINISHED = new Set(['completed', 'failed']);
4
- const RUNNING_JOB = new Set(['running', 'paused']);
4
+ export const RUNNING_JOB = new Set(['running', 'paused']);
5
5
  const ROUTINE_TARGETS = { send_routine_request: 'access', cancel_routine_request: 'request' };
6
6
  function words(line) {
7
7
  const found = [];
@@ -113,9 +113,6 @@ class Reader {
113
113
  refused(option) {
114
114
  this.requestedOptions.delete(option);
115
115
  }
116
- turnStarted() {
117
- return [];
118
- }
119
116
  closeTurn() {
120
117
  const events = [];
121
118
  for (const [id, call] of this.calls) {
@@ -187,10 +184,7 @@ class Reader {
187
184
  const label = this.jobs.get(id) ?? '';
188
185
  if (state !== 'running')
189
186
  this.jobs.delete(id);
190
- return [
191
- { kind: 'busy', key: `job:${id}`, running: state === 'running' },
192
- { kind: 'marker', marker: 'job', text: `${label}: ${state}` },
193
- ];
187
+ return [{ kind: 'marker', marker: 'job', text: `${label}: ${state}` }];
194
188
  }
195
189
  optionsChanged(options) {
196
190
  const events = [];
@@ -275,7 +269,6 @@ class Reader {
275
269
  class ClaudeReader extends Reader {
276
270
  watchedOptions = { model: 'model' };
277
271
  tasks = new Map();
278
- scheduled = new Map();
279
272
  own(message) {
280
273
  if (message.params?.update?._meta?.claudeCode?.parentToolUseId !== undefined)
281
274
  return [];
@@ -295,10 +288,6 @@ class ClaudeReader extends Reader {
295
288
  return [];
296
289
  const sdk = message.params.message;
297
290
  switch (sdk.subtype) {
298
- case 'background_tasks_changed':
299
- return [{ kind: 'busy', key: 'tasks', running: sdk.tasks.length > 0 }];
300
- case 'session_state_changed':
301
- return [{ kind: 'busy', key: 'state', running: sdk.state === 'running' }];
302
291
  case 'task_started':
303
292
  if (sdk.tool_use_id !== undefined)
304
293
  this.tasks.set(sdk.task_id, sdk.tool_use_id);
@@ -309,16 +298,6 @@ class ClaudeReader extends Reader {
309
298
  return [];
310
299
  }
311
300
  }
312
- turnStarted() {
313
- const events = [];
314
- for (const [id, entry] of this.scheduled) {
315
- if (entry.recurring)
316
- continue;
317
- this.scheduled.delete(id);
318
- events.push({ kind: 'busy', key: `scheduled:${id}`, running: false });
319
- }
320
- return events;
321
- }
322
301
  decide(call, final) {
323
302
  const tool = call.meta.claudeCode?.toolName;
324
303
  if (tool === 'Agent' || tool === 'Task' || call.meta.jetbrains?.air?.subagent === true) {
@@ -350,22 +329,7 @@ class ClaudeReader extends Reader {
350
329
  if (call.status !== 'completed')
351
330
  return [];
352
331
  if (tool === 'CronCreate' || (tool === 'ScheduleWakeup' && call.rawInput?.stop !== true)) {
353
- this.scheduled.set(call.id, {
354
- recurring: tool === 'CronCreate' && call.rawInput?.recurring !== false,
355
- text: contentText(call.content) ?? '',
356
- });
357
- return [
358
- { kind: 'busy', key: `scheduled:${call.id}`, running: true },
359
- { kind: 'marker', marker: 'job', text: `${call.rawInput?.prompt ?? call.title ?? ''}: scheduled` },
360
- ];
361
- }
362
- if (tool === 'CronDelete') {
363
- for (const [id, entry] of this.scheduled) {
364
- if (!entry.text.includes(String(call.rawInput?.id)))
365
- continue;
366
- this.scheduled.delete(id);
367
- return [{ kind: 'busy', key: `scheduled:${id}`, running: false }];
368
- }
332
+ return [{ kind: 'marker', marker: 'job', text: `${call.rawInput?.prompt ?? call.title ?? ''}: scheduled` }];
369
333
  }
370
334
  return [];
371
335
  }
@@ -402,15 +366,9 @@ class CodexReader extends Reader {
402
366
  case 'subagent_spawned':
403
367
  this.children.set(update.subagentSessionId, { id: null, text: '' });
404
368
  this.subagents.add(update.subagentSessionId);
405
- return [
406
- { kind: 'busy', key: `subagent:${update.subagentSessionId}`, running: true },
407
- { kind: 'subagent', id: update.subagentSessionId, description: update.name },
408
- ];
369
+ return [{ kind: 'subagent', id: update.subagentSessionId, description: update.name }];
409
370
  case 'subagent_state_update':
410
- return [
411
- { kind: 'busy', key: `subagent:${update.subagentSessionId}`, running: false },
412
- ...this.finish(update.subagentSessionId, this.children.get(update.subagentSessionId)?.text || null),
413
- ];
371
+ return this.finish(update.subagentSessionId, this.children.get(update.subagentSessionId)?.text || null);
414
372
  default:
415
373
  return super.update(update);
416
374
  }
@@ -427,10 +385,6 @@ class CodexReader extends Reader {
427
385
  child.id = id;
428
386
  child.text += update.content.text;
429
387
  }
430
- const status = update._meta?.codex?.threadStatus?.type;
431
- if (update.sessionUpdate === 'session_info_update' && status !== undefined) {
432
- return [{ kind: 'busy', key: `thread:${session}`, running: status === 'active' }];
433
- }
434
388
  return [];
435
389
  }
436
390
  decide(call) {
@@ -460,7 +414,6 @@ class CodexReader extends Reader {
460
414
  class GrokReader extends Reader {
461
415
  watchedOptions = {};
462
416
  children = new Map();
463
- queues = new Map();
464
417
  models = [];
465
418
  model = null;
466
419
  answered(result) {
@@ -473,15 +426,10 @@ class GrokReader extends Reader {
473
426
  if (this.session !== null && session !== undefined && session !== this.session && !this.children.has(session))
474
427
  return [];
475
428
  if (message.method === '_x.ai/queue/changed') {
476
- const running = message.params.runningPromptId ?? null;
477
- this.queues.set(session, { running, entries: message.params.entries.length });
478
- return [this.queued(session), ...(running !== null && session === this.session ? [{ kind: 'started' }] : [])];
429
+ return (message.params.runningPromptId ?? null) !== null && session === this.session ? [{ kind: 'started' }] : [];
479
430
  }
480
431
  if (message.params?.update?.sessionUpdate === 'turn_completed') {
481
- const queue = this.queues.get(session);
482
- if (queue !== undefined && queue.running === message.params.update.prompt_id)
483
- queue.running = null;
484
- return [this.queued(session), ...(session === this.session ? [{ kind: 'ended' }] : [])];
432
+ return session === this.session ? [{ kind: 'ended' }] : [];
485
433
  }
486
434
  if (message.method === '_x.ai/models/update') {
487
435
  this.offered(message.params);
@@ -489,10 +437,6 @@ class GrokReader extends Reader {
489
437
  }
490
438
  return super.read(message);
491
439
  }
492
- queued(session) {
493
- const queue = this.queues.get(session);
494
- return { kind: 'busy', key: `queue:${session}`, running: queue !== undefined && (queue.running !== null || queue.entries > 0) };
495
- }
496
440
  offered(models) {
497
441
  this.models = models.availableModels ?? this.models;
498
442
  this.model = models.currentModelId ?? this.model;
@@ -508,16 +452,9 @@ class GrokReader extends Reader {
508
452
  case '_x.ai/task_completed':
509
453
  return this.job(update.task_snapshot.task_id, null, update.task_snapshot.exit_code === 0 ? 'completed' : 'failed');
510
454
  case '_x.ai/scheduled_task_created':
511
- return [
512
- { kind: 'busy', key: `scheduled:${update.task_id}`, running: true },
513
- { kind: 'marker', marker: 'job', text: `${update.prompt}: next run ${update.next_fire_at}` },
514
- ];
515
- case '_x.ai/scheduled_task_deleted':
516
- return [{ kind: 'busy', key: `scheduled:${update.task_id}`, running: false }];
455
+ return [{ kind: 'marker', marker: 'job', text: `${update.prompt}: next run ${update.next_fire_at}` }];
517
456
  case '_x.ai/session_notification':
518
457
  return this.notification(update);
519
- case '_x.ai/mcp_initialized':
520
- return [{ kind: 'servers' }];
521
458
  default:
522
459
  return [];
523
460
  }
@@ -538,14 +475,11 @@ class GrokReader extends Reader {
538
475
  candidate.decided.description === update.description) ??
539
476
  [...this.calls.values()].find((candidate) => candidate.decided?.shown === 'subagent' && ![...this.children.values()].includes(candidate.id));
540
477
  this.children.set(update.child_session_id, call?.id ?? null);
541
- return [{ kind: 'busy', key: `subagent:${update.child_session_id}`, running: true }];
478
+ return [];
542
479
  }
543
480
  case 'subagent_finished': {
544
481
  const call = this.children.get(update.child_session_id);
545
- return [
546
- { kind: 'busy', key: `subagent:${update.child_session_id}`, running: false },
547
- ...(typeof call === 'string' ? this.finish(call, update.output ?? null) : []),
548
- ];
482
+ return typeof call === 'string' ? this.finish(call, update.output ?? null) : [];
549
483
  }
550
484
  case 'auto_compact_completed':
551
485
  return [{ kind: 'marker', marker: 'compaction', text: '' }];
package/dist/files.js CHANGED
@@ -1,7 +1,13 @@
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) {
@@ -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;