@agentshouse/kit 0.2.1-cbdf76b9a90a3 → 0.2.1-cc6246211e00c

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 (134) hide show
  1. package/README.md +22 -22
  2. package/dist/acp.js +4 -3
  3. package/dist/agents.js +2 -2
  4. package/dist/api.js +1 -1
  5. package/dist/authority-main.js +2 -2
  6. package/dist/bridge.js +1 -1
  7. package/dist/building.js +46 -0
  8. package/dist/{inputs.js → calls.js} +20 -20
  9. package/dist/clis-main.js +2 -2
  10. package/dist/clis.js +38 -15
  11. package/dist/{stream.js → connection.js} +26 -23
  12. package/dist/conversations.js +149 -85
  13. package/dist/copies.js +15 -29
  14. package/dist/end.js +3 -3
  15. package/dist/events.js +2 -2
  16. package/dist/files.js +139 -37
  17. package/dist/git.js +3 -3
  18. package/dist/house-cli.js +95 -87
  19. package/dist/house-main.js +3 -3
  20. package/dist/instructions.js +29 -9
  21. package/dist/kit-cli.js +4 -4
  22. package/dist/kit-main.js +3 -3
  23. package/dist/login.js +2 -4
  24. package/dist/mcp.js +4 -2
  25. package/dist/relay.js +21 -0
  26. package/dist/resident.js +40 -34
  27. package/dist/service.js +1 -1
  28. package/dist/sign-in.js +22 -21
  29. package/dist/skills.js +42 -97
  30. package/dist/transcription.js +83 -43
  31. package/dist/update.js +61 -35
  32. package/dist/web-wizard.js +101 -0
  33. package/dist/wire.js +1 -1
  34. package/package.json +20 -21
  35. package/dist/bootstrap.json +0 -24
  36. package/how-we-work.md +0 -33
  37. package/skills/ask-matt/LICENSE +0 -21
  38. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  39. package/skills/ask-matt/SKILL.md +0 -97
  40. package/skills/ask-matt/agents/openai.yaml +0 -5
  41. package/skills/code-review/LICENSE +0 -21
  42. package/skills/code-review/SKILL.md +0 -89
  43. package/skills/code-review/agents/openai.yaml +0 -3
  44. package/skills/codebase-design/DEEPENING.md +0 -37
  45. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  46. package/skills/codebase-design/LICENSE +0 -21
  47. package/skills/codebase-design/SKILL.md +0 -114
  48. package/skills/codebase-design/agents/openai.yaml +0 -3
  49. package/skills/diagnosing-bugs/LICENSE +0 -21
  50. package/skills/diagnosing-bugs/SKILL.md +0 -138
  51. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  52. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  53. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  54. package/skills/domain-modeling/GLOSSARY-FORMAT.md +0 -60
  55. package/skills/domain-modeling/LICENSE +0 -21
  56. package/skills/domain-modeling/SKILL.md +0 -74
  57. package/skills/domain-modeling/agents/openai.yaml +0 -3
  58. package/skills/grill-me/LICENSE +0 -21
  59. package/skills/grill-me/SKILL.md +0 -7
  60. package/skills/grill-me/agents/openai.yaml +0 -5
  61. package/skills/grill-with-docs/LICENSE +0 -21
  62. package/skills/grill-with-docs/SKILL.md +0 -7
  63. package/skills/grill-with-docs/agents/openai.yaml +0 -5
  64. package/skills/grilling/LICENSE +0 -21
  65. package/skills/grilling/SKILL.md +0 -30
  66. package/skills/grilling/agents/openai.yaml +0 -3
  67. package/skills/handoff/LICENSE +0 -21
  68. package/skills/handoff/SKILL.md +0 -16
  69. package/skills/handoff/agents/openai.yaml +0 -5
  70. package/skills/implement/LICENSE +0 -21
  71. package/skills/implement/SKILL.md +0 -17
  72. package/skills/implement/agents/openai.yaml +0 -5
  73. package/skills/implement-spec/LICENSE +0 -21
  74. package/skills/implement-spec/SKILL.md +0 -40
  75. package/skills/implement-spec/agents/openai.yaml +0 -5
  76. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  77. package/skills/improve-codebase-architecture/LICENSE +0 -21
  78. package/skills/improve-codebase-architecture/SKILL.md +0 -71
  79. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
  80. package/skills/pr/CREDITS.md +0 -3
  81. package/skills/pr/LICENSE +0 -21
  82. package/skills/pr/SKILL.md +0 -170
  83. package/skills/pr/agents/openai.yaml +0 -3
  84. package/skills/prototype/LICENSE +0 -21
  85. package/skills/prototype/LOGIC.md +0 -67
  86. package/skills/prototype/SKILL.md +0 -26
  87. package/skills/prototype/UI.md +0 -112
  88. package/skills/prototype/agents/openai.yaml +0 -3
  89. package/skills/research/LICENSE +0 -21
  90. package/skills/research/SKILL.md +0 -12
  91. package/skills/research/agents/openai.yaml +0 -3
  92. package/skills/retro/LICENSE +0 -21
  93. package/skills/retro/SKILL.md +0 -44
  94. package/skills/retro/agents/openai.yaml +0 -5
  95. package/skills/tdd/LICENSE +0 -21
  96. package/skills/tdd/SKILL.md +0 -38
  97. package/skills/tdd/agents/openai.yaml +0 -3
  98. package/skills/tdd/mocking.md +0 -59
  99. package/skills/tdd/tests.md +0 -77
  100. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  101. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  102. package/skills/teach/LICENSE +0 -21
  103. package/skills/teach/MISSION-FORMAT.md +0 -31
  104. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  105. package/skills/teach/SKILL.md +0 -140
  106. package/skills/teach/agents/openai.yaml +0 -5
  107. package/skills/to-questionnaire/LICENSE +0 -21
  108. package/skills/to-questionnaire/SKILL.md +0 -54
  109. package/skills/to-questionnaire/agents/openai.yaml +0 -5
  110. package/skills/to-spec/LICENSE +0 -21
  111. package/skills/to-spec/SKILL.md +0 -75
  112. package/skills/to-spec/agents/openai.yaml +0 -5
  113. package/skills/to-tickets/LICENSE +0 -21
  114. package/skills/to-tickets/SKILL.md +0 -105
  115. package/skills/to-tickets/agents/openai.yaml +0 -5
  116. package/skills/triage/AGENT-BRIEF.md +0 -207
  117. package/skills/triage/LICENSE +0 -21
  118. package/skills/triage/OUT-OF-SCOPE.md +0 -105
  119. package/skills/triage/SKILL.md +0 -112
  120. package/skills/triage/agents/openai.yaml +0 -5
  121. package/skills/wait-what/LICENSE +0 -21
  122. package/skills/wait-what/SKILL.md +0 -7
  123. package/skills/wait-what/agents/openai.yaml +0 -5
  124. package/skills/wayfinder/LICENSE +0 -21
  125. package/skills/wayfinder/SKILL.md +0 -128
  126. package/skills/wayfinder/agents/openai.yaml +0 -5
  127. package/skills/wizard/LICENSE +0 -21
  128. package/skills/wizard/SKILL.md +0 -44
  129. package/skills/wizard/agents/openai.yaml +0 -3
  130. package/skills/wizard/template.sh +0 -223
  131. package/skills/writing-for-agents/LICENSE +0 -21
  132. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  133. package/skills/writing-for-agents/SKILL.md +0 -81
  134. package/skills/writing-for-agents/agents/openai.yaml +0 -3
@@ -4,22 +4,23 @@ import { mkdir } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
5
  import { unlessAborted } from './abort.js';
6
6
  import { startAdapter } from './acp.js';
7
- import { finalRefusal } from './api.js';
7
+ import { finalRefusal, HouseRefusal } from './api.js';
8
8
  import { openBridge } from './bridge.js';
9
9
  import { CLIS } from './clis.js';
10
10
  import { answeredFailure, readerFor } from './events.js';
11
- import { placeFiles, uploadBytes } from './files.js';
11
+ import { inspectedFile, linkedPaths, storeRefusal, uploaded } from './files.js';
12
12
  import { agentBase } from './home.js';
13
13
  import { instructions } from './instructions.js';
14
14
  import { Parts } from './parts.js';
15
15
  import { refusalOf } from './refusals.js';
16
+ import { relayed } from './relay.js';
16
17
  import { killMarked, Scope } from './scope.js';
17
18
  import { holdSecretInput } from './secret-input.js';
18
- import { createHowWeWork } from './skills.js';
19
19
  import { transcribe } from './transcription.js';
20
20
  import { workFor } from './work.js';
21
21
  const SIGNED_OUT = RequestError.authRequired().code;
22
22
  const KILLED = 'the conversation was killed';
23
+ const INTERRUPTED = 'the conversation was interrupted';
23
24
  const REFUSED = 'the CLI refused the prompt';
24
25
  const NO_CONNECTION = "the Agent's computer could not open this conversation; try again";
25
26
  const GONE = 'House no longer holds this conversation; start a new one';
@@ -35,6 +36,8 @@ const NOT_SIGNED_IN = "the Agent's CLI is not signed in; sign it in on the Agent
35
36
  const NO_SETTINGS = "the Agent's CLI refused the Agent's model, mode or effort";
36
37
  class NotStarted extends Error {
37
38
  }
39
+ class ResumeRefused extends NotStarted {
40
+ }
38
41
  async function step(reason, work) {
39
42
  try {
40
43
  return await work;
@@ -62,7 +65,7 @@ const EXIT_PLAN_MODE = {
62
65
  return value;
63
66
  },
64
67
  };
65
- export const CONVERSATION_INPUTS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
68
+ export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
66
69
  function causeOf(error) {
67
70
  return error instanceof Error ? error.message : String(error);
68
71
  }
@@ -75,16 +78,28 @@ function exitedWith(running, cause) {
75
78
  function answerMessage(request, response) {
76
79
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
77
80
  }
81
+ async function sentPart(url, bytes, headers) {
82
+ const answer = await fetch(relayed(url), { method: 'PUT', headers, body: bytes });
83
+ await answer.arrayBuffer();
84
+ if (!answer.ok)
85
+ throw storeRefusal('upload', answer.status);
86
+ }
78
87
  function logged(error) {
79
88
  process.stderr.write(`kit: ${causeOf(error)}\n`);
80
89
  }
81
90
  function refusal(conversation, error) {
82
91
  if (conversation.kills > 0)
83
92
  return KILLED;
84
- const cause = error instanceof NotStarted ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
93
+ const cause = error instanceof Error && error.cause !== undefined ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
85
94
  logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
86
95
  return causeOf(error);
87
96
  }
97
+ function audioOf(result) {
98
+ if (result.isError === true)
99
+ return null;
100
+ const file = inspectedFile(result.content.map((content) => content.text).join('\n'));
101
+ return file?.media_type?.startsWith('audio/') === true ? file : null;
102
+ }
88
103
  function marker(bridge) {
89
104
  return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
90
105
  }
@@ -117,6 +132,7 @@ class Conversation {
117
132
  subagents = new Map();
118
133
  held = [];
119
134
  opening = null;
135
+ bridge = null;
120
136
  launch = null;
121
137
  prompted = null;
122
138
  kills = 0;
@@ -154,23 +170,23 @@ export class Conversations {
154
170
  // A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
155
171
  setInterval(() => this.look(), 2_000);
156
172
  }
157
- input(input) {
158
- void this.kit.inputs.carry(input.input_id, () => {
159
- const conversation = this.conversation(input.conversation_id);
160
- if (input.kind === 'kill') {
173
+ call(call) {
174
+ void this.kit.calls.carry(call.input_id, () => {
175
+ const conversation = this.conversation(call.conversation_id);
176
+ if (call.kind === 'kill') {
161
177
  conversation.kills++;
162
178
  conversation.killed.abort(new Error(KILLED));
163
179
  for (const waiting of conversation.waiting.splice(0))
164
180
  waiting.settle({ refused: KILLED });
165
181
  this.halt(conversation);
166
182
  }
167
- if (input.kind === 'interrupt') {
183
+ if (call.kind === 'interrupt') {
168
184
  conversation.interrupts++;
169
185
  conversation.interrupted.abort();
170
186
  }
171
- const handled = conversation.queue.then(() => this.carry(conversation, input));
187
+ const handled = conversation.queue.then(() => this.carry(conversation, call));
172
188
  conversation.queue = handled.catch(() => undefined);
173
- return handled.then(async ({ ack }) => ({ ack: await ack }));
189
+ return handled.then(async ({ result }) => ({ result: await result }));
174
190
  });
175
191
  }
176
192
  idle() {
@@ -230,39 +246,39 @@ export class Conversations {
230
246
  }
231
247
  return conversation;
232
248
  }
233
- async carry(conversation, input) {
234
- if (input.kind === 'open')
235
- return { ack: this.open(conversation, input) };
236
- if (input.kind === 'message')
237
- return this.message(conversation, input);
249
+ async carry(conversation, call) {
250
+ if (call.kind === 'open')
251
+ return { result: this.open(conversation, call) };
252
+ if (call.kind === 'message')
253
+ return this.message(conversation, call);
238
254
  await this.launched(conversation);
239
- if (input.kind === 'interrupt') {
255
+ if (call.kind === 'interrupt') {
240
256
  if (--conversation.interrupts === 0)
241
257
  conversation.interrupted = new AbortController();
242
- await this.interrupt(conversation, String(input.turn_id));
258
+ await this.interrupt(conversation, String(call.turn_id));
243
259
  }
244
- if (input.kind === 'kill') {
260
+ if (call.kind === 'kill') {
245
261
  await this.kill(conversation);
246
262
  await conversation.sending;
247
263
  if (--conversation.kills === 0)
248
264
  conversation.killed = new AbortController();
249
265
  }
250
- if (input.kind === 'option')
251
- await this.option(conversation, String(input.option), input.value);
252
- if (input.kind === 'answer') {
253
- const question = this.questions.get(String(input.interaction_id));
266
+ if (call.kind === 'option')
267
+ await this.option(conversation, String(call.option), call.value);
268
+ if (call.kind === 'answer') {
269
+ const question = this.questions.get(String(call.interaction_id));
254
270
  if (question === undefined) {
255
- const text = answerMessage(input.request, input.response);
256
- return this.message(conversation, { ...input, text, files: [], first: false });
271
+ const text = answerMessage(call.request, call.response);
272
+ return this.message(conversation, { ...call, text, first: false, message_id: null });
257
273
  }
258
- question.answer(input.response);
274
+ question.answer(call.response);
259
275
  }
260
- return { ack: Promise.resolve({}) };
276
+ return { result: Promise.resolve({}) };
261
277
  }
262
- open(conversation, input) {
278
+ open(conversation, call) {
263
279
  if (conversation.running !== null)
264
280
  return Promise.resolve({});
265
- const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, input.agent_id), null, true);
281
+ const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, call.agent_id), null, true);
266
282
  return launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) }));
267
283
  }
268
284
  launch(conversation, route, sessionId, empty) {
@@ -283,6 +299,7 @@ export class Conversations {
283
299
  if (conversation.launch !== launch)
284
300
  return;
285
301
  conversation.launch = null;
302
+ this.closeIdleBridge(conversation);
286
303
  this.next(conversation);
287
304
  });
288
305
  this.next(conversation);
@@ -300,32 +317,28 @@ export class Conversations {
300
317
  read.catch(() => undefined);
301
318
  return read;
302
319
  }
303
- async message(conversation, input) {
304
- const content = [{ type: 'text', text: String(input.text) }];
320
+ async message(conversation, call) {
321
+ const content = [{ type: 'text', text: String(call.text) }];
305
322
  let route;
306
323
  let instructing = null;
307
324
  try {
308
- route = await this.route(conversation, input.agent_id);
309
- if (input.first === true)
325
+ route = await this.route(conversation, call.agent_id);
326
+ if (call.first === true)
310
327
  instructing = this.instructed(conversation, route);
311
- const files = input.files;
312
- if (files.length > 0)
313
- this.prepares(conversation, 'files');
314
- const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
315
- const lines = await this.transcribed(conversation, files, paths);
316
- if (lines.length > 0)
317
- content.push({ type: 'text', text: lines.join('\n') });
328
+ if (call.message_id !== null)
329
+ content.push(...(await this.transcribed(conversation, String(call.message_id), String(call.text))));
318
330
  if (conversation.kills > 0)
319
331
  throw new Error(KILLED);
320
332
  }
321
333
  catch (error) {
334
+ this.closeIdleBridge(conversation);
322
335
  this.prepares(conversation, null);
323
- return { ack: Promise.resolve({ refused: refusal(conversation, error) }) };
336
+ return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
324
337
  }
325
338
  conversation.preparing = null;
326
339
  return {
327
- ack: new Promise((settle) => {
328
- conversation.waiting.push({ input, route, content, instructing, settle });
340
+ result: new Promise((settle) => {
341
+ conversation.waiting.push({ call, route, content, instructing, settle });
329
342
  this.next(conversation);
330
343
  }),
331
344
  };
@@ -346,8 +359,8 @@ export class Conversations {
346
359
  });
347
360
  }
348
361
  async send(conversation, waiting) {
349
- const { input } = waiting;
350
- let ack = {};
362
+ const { call } = waiting;
363
+ let result = {};
351
364
  let started = false;
352
365
  let instructing = waiting.instructing;
353
366
  const prompt = [];
@@ -357,18 +370,18 @@ export class Conversations {
357
370
  await this.launched(conversation);
358
371
  }
359
372
  if (conversation.running === null) {
360
- const stored = conversation.session ?? input.provider_session_id;
361
- const empty = input.first === true && conversation.prompted !== stored;
373
+ const stored = conversation.session ?? call.provider_session_id;
374
+ const empty = call.first === true && conversation.prompted !== stored;
362
375
  const launch = this.launch(conversation, Promise.resolve(waiting.route), stored, empty);
363
- if (input.first === true)
376
+ if (call.first === true)
364
377
  instructing = this.instructed(conversation, waiting.route);
365
378
  started = (await launch.session) !== stored;
366
379
  }
367
380
  const session = conversation.running.sessionId;
368
- if (started || (input.first === true && session !== input.provider_session_id)) {
369
- ack = { provider_session_id: session };
381
+ if (started || (call.first === true && session !== call.provider_session_id)) {
382
+ result = { provider_session_id: session };
370
383
  }
371
- if (input.first === true) {
384
+ if (call.first === true) {
372
385
  prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
373
386
  }
374
387
  if (conversation.kills > 0)
@@ -380,12 +393,14 @@ export class Conversations {
380
393
  conversation.session = null;
381
394
  await this.kill(conversation);
382
395
  }
383
- waiting.settle({ refused: refusal(conversation, error) });
396
+ const refused = refusal(conversation, error);
397
+ const resumeRefused = call.kind === 'message' && conversation.kills === 0 && error instanceof ResumeRefused;
398
+ waiting.settle(resumeRefused ? { refused, resume_refused: true } : { refused });
384
399
  return;
385
400
  }
386
401
  this.starts(conversation, null);
387
- this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.inputs.hands(String(input.input_id), ack));
388
- waiting.settle(ack);
402
+ this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.calls.keepResult(String(call.input_id), result));
403
+ waiting.settle(result);
389
404
  }
390
405
  starts(conversation, step) {
391
406
  conversation.starting = step;
@@ -415,25 +430,65 @@ export class Conversations {
415
430
  retell(conversation) {
416
431
  conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
417
432
  }
418
- async transcribed(conversation, files, paths) {
419
- const lines = [];
420
- for (const [index, path] of paths.entries()) {
421
- lines.push(path);
422
- if (files[index].media_type?.startsWith('audio/') !== true)
423
- continue;
424
- this.prepares(conversation, 'transcription');
425
- try {
426
- const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
427
- if (text !== null && text !== '')
428
- lines.push(`Transcript: ${text}`);
429
- }
430
- catch (error) {
431
- if (conversation.kills > 0)
432
- throw error;
433
- logged(new Error(`${files[index].name} was not transcribed: ${causeOf(error)}`));
433
+ async transcribed(conversation, messageId, text) {
434
+ const paths = linkedPaths(text);
435
+ if (paths.length === 0)
436
+ return [];
437
+ const killed = conversation.killed.signal;
438
+ const interrupted = conversation.interrupted.signal;
439
+ const transcribing = AbortSignal.any([killed, interrupted]);
440
+ let signal = killed;
441
+ const blocks = [];
442
+ try {
443
+ const bridge = await step(unbridged, unlessAborted(this.bridged(conversation), killed));
444
+ const read = async (path) => audioOf(await step(unbridged, unlessAborted(bridge.tool('inspect', { path }), signal)));
445
+ for (const path of paths) {
446
+ let answered = await read(path);
447
+ if (answered === null)
448
+ continue;
449
+ signal = transcribing;
450
+ this.prepares(conversation, 'transcription');
451
+ const said = await transcribe(async () => {
452
+ const address = answered ?? (await read(path));
453
+ answered = null;
454
+ if (address === null)
455
+ throw new Error(`${path} is no longer an audio file`);
456
+ return address;
457
+ }, signal);
458
+ const transcript = `/kit/conversations/${conversation.id}/messages/${messageId}/transcripts`;
459
+ await step(unbridged, unlessAborted(this.kit.house.deliver(transcript, { path, text: said }, () => !signal.aborted), signal));
460
+ blocks.push({ type: 'text', text: `${path}\nTranscript: ${said}` });
434
461
  }
435
462
  }
436
- return lines;
463
+ catch (error) {
464
+ if (signal === transcribing && interrupted.aborted && conversation.kills === 0)
465
+ throw new NotStarted(INTERRUPTED, { cause: error });
466
+ throw error;
467
+ }
468
+ return blocks;
469
+ }
470
+ bridged(conversation) {
471
+ if (conversation.bridge === null) {
472
+ const bridging = openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
473
+ conversation.bridge = bridging;
474
+ bridging.catch(() => {
475
+ if (conversation.bridge === bridging)
476
+ conversation.bridge = null;
477
+ });
478
+ }
479
+ return conversation.bridge;
480
+ }
481
+ closeBridge(conversation, bridging, bridge) {
482
+ bridge.close();
483
+ if (conversation.bridge === bridging)
484
+ conversation.bridge = null;
485
+ }
486
+ closeIdleBridge(conversation) {
487
+ const bridging = conversation.bridge;
488
+ if (bridging === null || conversation.running !== null || conversation.launch !== null)
489
+ return;
490
+ conversation.bridge = null;
491
+ void bridging.then((bridge) => bridge.close(), () => undefined);
437
492
  }
438
493
  async interrupt(conversation, turnId) {
439
494
  if (conversation.running === null || conversation.turn?.id !== turnId)
@@ -497,9 +552,10 @@ export class Conversations {
497
552
  .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
498
553
  .onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
499
554
  const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
500
- const bridge = await step(unbridged, openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal));
555
+ const bridging = this.bridged(conversation);
556
+ const bridge = await step(unbridged, bridging);
501
557
  if (conversation.kills > 0) {
502
- bridge.close();
558
+ this.closeBridge(conversation, bridging, bridge);
503
559
  throw new Error(KILLED);
504
560
  }
505
561
  conversation.opening = bridge;
@@ -507,7 +563,6 @@ export class Conversations {
507
563
  const scope = new Scope(marker(bridge), randomUUID(), cli);
508
564
  let adapter = null;
509
565
  try {
510
- await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
511
566
  bridged(bridge);
512
567
  await CLIS[route.kind].allowHouse(bridge.env.PATH);
513
568
  const streamed = await step(NO_CONNECTION, work.open(cli));
@@ -515,7 +570,7 @@ export class Conversations {
515
570
  work.read(message);
516
571
  this.read(conversation, reader, message);
517
572
  }, { ...bridge.env, ...streamed }, undefined, scope));
518
- void adapter.exited.then(() => bridge.close());
573
+ void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
519
574
  const agent = adapter.connection.agent;
520
575
  const cwd = route.working_directory;
521
576
  const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
@@ -526,7 +581,7 @@ export class Conversations {
526
581
  const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
527
582
  await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
528
583
  }
529
- const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
584
+ const running = { adapter, bridging, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
530
585
  const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
531
586
  void work.settled().then(() => scope.record());
532
587
  conversation.running = running;
@@ -540,7 +595,7 @@ export class Conversations {
540
595
  adapter?.retire();
541
596
  work.close();
542
597
  conversation.leftovers.add(scope);
543
- bridge.close();
598
+ this.closeBridge(conversation, bridging, bridge);
544
599
  throw conversation.kills > 0 ? new Error(KILLED) : error;
545
600
  }
546
601
  finally {
@@ -553,7 +608,7 @@ export class Conversations {
553
608
  }
554
609
  catch (error) {
555
610
  if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
556
- throw new NotStarted(unopened(NO_RESUME)(error), { cause: error });
611
+ throw signedOut(error) ? new NotStarted(NOT_SIGNED_IN, { cause: error }) : new ResumeRefused(NO_RESUME, { cause: error });
557
612
  }
558
613
  logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
559
614
  return null;
@@ -589,6 +644,8 @@ export class Conversations {
589
644
  this.kit.changed();
590
645
  }
591
646
  detach(conversation, running) {
647
+ if (conversation.bridge === running.bridging)
648
+ conversation.bridge = null;
592
649
  running.work.close();
593
650
  conversation.leftovers.add(running.scope);
594
651
  conversation.running = null;
@@ -857,20 +914,27 @@ export class Conversations {
857
914
  async image(conversation, turn, reserved, data, mediaType) {
858
915
  const bytes = Buffer.from(data, 'base64');
859
916
  const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
917
+ const sha256 = createHash('sha256').update(bytes).digest('hex');
860
918
  try {
861
919
  const declared = (await this.deliver(conversation, turn, 'images', {
862
920
  name,
863
921
  media_type: mediaType,
864
922
  bytes: bytes.length,
865
- sha256: createHash('sha256').update(bytes).digest('hex'),
923
+ sha256,
866
924
  }));
867
- const saved = declared.upload === undefined ? declared : await uploadBytes(declared.upload, bytes);
868
- if (saved.save.status !== 'saved')
869
- throw new Error(saved.save.failure ?? saved.save.status);
870
- turn.parts.arrive({ type: 'image', file: saved.version, name, media_type: mediaType }, reserved);
925
+ const saved = await uploaded(declared.upload, {
926
+ name,
927
+ bytes: bytes.length,
928
+ sha256,
929
+ read: (start, end) => [bytes.subarray(start, end)],
930
+ send: (url, start, end, headers) => sentPart(url, bytes.subarray(start, end), headers),
931
+ finish: (upload, partSha256) => this.kit.house.deliver('/kit/uploads/finish', { upload, part_sha256: partSha256 }),
932
+ });
933
+ turn.parts.arrive({ type: 'image', file: saved.path, name, media_type: mediaType }, reserved);
871
934
  }
872
935
  catch (error) {
873
- turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` }, reserved);
936
+ const cause = error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
937
+ turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${cause}` }, reserved);
874
938
  }
875
939
  }
876
940
  }
package/dist/copies.js CHANGED
@@ -11,7 +11,7 @@ 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
  }
@@ -133,7 +133,7 @@ async function ignoreRules(repository, writable, copy) {
133
133
  ].map(pattern);
134
134
  const granted = readOnly.filter((path) => !protectedPath(path)).map(pattern);
135
135
  await ruled(join(repository, '.gitignore'), [
136
- '# 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',
137
137
  '# content: Git does not track them, and House refuses a change to them even',
138
138
  '# when this file is edited or a path is force-added.',
139
139
  '/.gitignore',
@@ -148,7 +148,7 @@ async function ignoreRules(repository, writable, copy) {
148
148
  '',
149
149
  ].join('\n'));
150
150
  await ruled(join(repository, '.ignore'), [
151
- '# 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',
152
152
  '# content that .gitignore keeps out of Git.',
153
153
  `!${PROVENANCE}`,
154
154
  ...[...house, ...granted].map((rule) => `!${rule}`),
@@ -218,7 +218,7 @@ export class LocalCopies {
218
218
  async mapping() {
219
219
  await this.answering;
220
220
  return [...this.copies.values()]
221
- .filter((copy) => copy.erasing === undefined)
221
+ .filter((copy) => copy.unfollowed === undefined)
222
222
  .map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
223
223
  .sort();
224
224
  }
@@ -250,30 +250,24 @@ export class LocalCopies {
250
250
  return this.holding([...new Set([...held().map((copy) => copy.key), ...synced.keys()])].sort(), async () => {
251
251
  const said = [];
252
252
  let refused = false;
253
+ const mark = refreshing ? 'said' : 'unsaid';
253
254
  for (const copy of held()) {
254
- const returned = synced.has(copy.key);
255
- if (returned && copy.erasing === undefined)
255
+ if (synced.has(copy.key) || copy.unfollowed === 'said' || copy.unfollowed === mark)
256
256
  continue;
257
- try {
258
- await this.erase(copy);
259
- said.push(`${rootOf(copy.room_handle)}: ${returned ? '' : 'no longer synced to this Environment; '}erased from the Local copy`);
260
- }
261
- catch (error) {
262
- refused = true;
263
- said.push(`${rootOf(copy.room_handle)}: ${causeOf(error)}`);
264
- }
257
+ await this.save({ ...copy, unfollowed: mark });
258
+ if (refreshing)
259
+ said.push(`${rootOf(copy.room_handle)}: no longer followed; kept as it is`);
265
260
  }
266
261
  const current = [];
267
262
  for (const [key, handle] of synced) {
268
263
  try {
269
264
  const held = this.copies.get(key);
270
- if (held?.erasing)
271
- continue;
272
265
  if (held === undefined || !(await exists(held.repository))) {
273
266
  said.push(await this.materialize(key, handle));
274
267
  continue;
275
268
  }
276
- current.push(held.room_handle === handle ? held : await this.save({ ...held, room_handle: handle }));
269
+ const { unfollowed, ...followed } = held;
270
+ current.push(unfollowed === undefined && held.room_handle === handle ? held : await this.save({ ...followed, room_handle: handle }));
277
271
  }
278
272
  catch (error) {
279
273
  refused = true;
@@ -298,8 +292,6 @@ export class LocalCopies {
298
292
  return;
299
293
  await this.exclusive(key, async () => {
300
294
  const copy = this.copies.get(key);
301
- if (copy === undefined || copy.erasing)
302
- return;
303
295
  let moves = copy.moves;
304
296
  for (const { from, to } of report.moves) {
305
297
  const held = moves.find((move) => move.worktree === report.worktree && move.to === from);
@@ -355,12 +347,6 @@ export class LocalCopies {
355
347
  this.copies.set(copy.key, copy);
356
348
  return copy;
357
349
  }
358
- async erase(copy) {
359
- await this.save({ ...copy, erasing: true });
360
- await rm(copy.repository, { recursive: true, force: true });
361
- await rm(recordOf(copy.key), { force: true });
362
- this.copies.delete(copy.key);
363
- }
364
350
  async read(handle) {
365
351
  const prefixes = [rootOf(handle)];
366
352
  const discovery = await this.house.post('/kit/door/discover', { prefixes });
@@ -402,7 +388,7 @@ export class LocalCopies {
402
388
  throw new Error('House serves nothing of it to copy');
403
389
  const repository = join(localCopies(), 'rooms', read.anchor);
404
390
  if (await exists(repository))
405
- 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`);
406
392
  const staging = join(dirname(repository), `.${randomUUID()}`);
407
393
  await mkdir(staging, { recursive: true });
408
394
  let house;
@@ -560,8 +546,6 @@ export class LocalCopies {
560
546
  }
561
547
  async pushed(key, worktree, selector, caller) {
562
548
  let copy = this.copies.get(key);
563
- if (copy === undefined || copy.erasing)
564
- return outside(worktree);
565
549
  if (copy.push?.state === 'submitted')
566
550
  return this.settle(copy, caller);
567
551
  const selected = await run(worktree, ['rev-parse', '--verify', '--quiet', '--end-of-options', `${selector}^{commit}`]);
@@ -676,8 +660,10 @@ export class LocalCopies {
676
660
  }
677
661
  if (outcome.refused.code === 'expired')
678
662
  return this.recovered(copy);
679
- const rebased = rebaseFixes(outcome.refused);
680
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);
681
667
  try {
682
668
  settled = (await this.caughtUp([settled], true))[0];
683
669
  }
package/dist/end.js CHANGED
@@ -1,8 +1,8 @@
1
- import { KIT_VERSION } from './clis.js';
1
+ import { KIT_RELEASE } from './clis.js';
2
2
  import { forgetRejected } from './home.js';
3
3
  import { installCommand } from './placement.js';
4
4
  import { killDescendants } from './scope.js';
5
- export const REPLACED = 'another Kit started for this Environment';
5
+ export const REPLACED = 'another Kit process started for this Environment';
6
6
  function exit(reason, status) {
7
7
  process.stderr.write(`kit: ${reason}\n`);
8
8
  killDescendants();
@@ -19,7 +19,7 @@ export function updateRequired() {
19
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
20
  }
21
21
  export function newerThanHouse(release, command) {
22
- end(`Kit ${KIT_VERSION} is newer than this Building supports (Kit ${release}). Install Kit ${release}: ${command}`);
22
+ end(`Kit ${KIT_RELEASE} is newer than this Building supports (Kit ${release}). Install Kit ${release}: ${command}`);
23
23
  }
24
24
  export async function signInAgain(rejected) {
25
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];