@agentshouse/kit 0.2.1-ca0e1517cce79 → 0.2.1-ca1f38641026d

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.
@@ -1,9 +1,9 @@
1
- import { client, RequestError, } from '@agentclientprotocol/sdk';
1
+ import { client, } from '@agentclientprotocol/sdk';
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
3
  import { mkdir } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
5
  import { unlessAborted } from './abort.js';
6
- import { startAdapter } from './acp.js';
6
+ import { signedOut, startAdapter } from './acp.js';
7
7
  import { finalRefusal, HouseRefusal } from './api.js';
8
8
  import { openBridge } from './bridge.js';
9
9
  import { CLIS } from './clis.js';
@@ -18,7 +18,6 @@ import { killMarked, Scope } from './scope.js';
18
18
  import { holdSecretInput } from './secret-input.js';
19
19
  import { transcribe } from './transcription.js';
20
20
  import { workFor } from './work.js';
21
- const SIGNED_OUT = RequestError.authRequired().code;
22
21
  const KILLED = 'the conversation was killed';
23
22
  const INTERRUPTED = 'the conversation was interrupted';
24
23
  const REFUSED = 'the CLI refused the prompt';
@@ -34,6 +33,7 @@ const NO_SESSION = "the Agent's CLI did not open a session";
34
33
  const NO_RESUME = "the Agent's CLI could not resume this conversation";
35
34
  const NOT_SIGNED_IN = "the Agent's CLI is not signed in; sign it in on the Agent's machine";
36
35
  const NO_SETTINGS = "the Agent's CLI refused the Agent's model, mode or effort";
36
+ const UNSTEERED = "the Agent's CLI did not take the message into its running turn";
37
37
  class NotStarted extends Error {
38
38
  }
39
39
  class ResumeRefused extends NotStarted {
@@ -46,9 +46,6 @@ async function step(reason, work) {
46
46
  throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
47
47
  }
48
48
  }
49
- function signedOut(error) {
50
- return typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
51
- }
52
49
  function unopened(reason) {
53
50
  return (error) => (signedOut(error) ? NOT_SIGNED_IN : reason);
54
51
  }
@@ -65,7 +62,7 @@ const EXIT_PLAN_MODE = {
65
62
  return value;
66
63
  },
67
64
  };
68
- export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
65
+ export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'done', 'option', 'answer']);
69
66
  function causeOf(error) {
70
67
  return error instanceof Error ? error.message : String(error);
71
68
  }
@@ -73,7 +70,9 @@ function answeredWith(error) {
73
70
  return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
74
71
  }
75
72
  function exitedWith(running, cause) {
76
- return running.killed ? { failure: 'killed', cause: KILLED } : { failure: 'exited', cause };
73
+ if (running.killed)
74
+ return { failure: 'killed', cause: KILLED };
75
+ return { failure: running.adapter.silent() ? 'silent' : 'exited', cause };
77
76
  }
78
77
  function answerMessage(request, response) {
79
78
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
@@ -103,12 +102,16 @@ function audioOf(result) {
103
102
  function marker(bridge) {
104
103
  return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
105
104
  }
105
+ function steers(adapter) {
106
+ return adapter.initialized._meta?.steering?.supported === true;
107
+ }
106
108
  class Turn {
107
109
  id = randomUUID();
108
110
  parts;
109
111
  questions = 0;
110
112
  secrets = 0;
111
113
  prompted = false;
114
+ steered = false;
112
115
  taken = null;
113
116
  ended = false;
114
117
  images = 0;
@@ -128,6 +131,7 @@ class Conversation {
128
131
  context = null;
129
132
  leftovers = new Set();
130
133
  waiting = [];
134
+ done = false;
131
135
  sending = null;
132
136
  subagents = new Map();
133
137
  held = [];
@@ -139,7 +143,7 @@ class Conversation {
139
143
  killed = new AbortController();
140
144
  interrupts = 0;
141
145
  interrupted = new AbortController();
142
- open = 0;
146
+ turns = new Set();
143
147
  queue = Promise.resolve();
144
148
  reports = Promise.resolve();
145
149
  starting = null;
@@ -195,7 +199,7 @@ export class Conversations {
195
199
  for (const conversation of this.conversations.values()) {
196
200
  const turn = conversation.turn;
197
201
  const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
198
- if (conversation.open > (asking ? 1 : 0))
202
+ if (conversation.turns.size > (asking ? 1 : 0))
199
203
  return false;
200
204
  }
201
205
  const conversations = [...this.conversations.values()];
@@ -204,7 +208,7 @@ export class Conversations {
204
208
  return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
205
209
  }
206
210
  quiet() {
207
- return this.idle() && [...this.conversations.values()].every((conversation) => conversation.open === 0);
211
+ return this.idle() && [...this.conversations.values()].every((conversation) => conversation.turns.size === 0);
208
212
  }
209
213
  look() {
210
214
  for (const conversation of this.conversations.values()) {
@@ -220,8 +224,10 @@ export class Conversations {
220
224
  opened() {
221
225
  this.watched = false;
222
226
  for (const conversation of this.conversations.values()) {
223
- if (conversation.running !== null)
227
+ if (conversation.running !== null) {
224
228
  this.facts(conversation);
229
+ this.subagentsChanged(conversation);
230
+ }
225
231
  else if (conversation.exitUntold)
226
232
  this.process(conversation, false);
227
233
  if (conversation.untold || conversation.told !== null)
@@ -248,9 +254,14 @@ export class Conversations {
248
254
  }
249
255
  async carry(conversation, call) {
250
256
  if (call.kind === 'open')
251
- return { result: this.open(conversation, call) };
257
+ return this.open(conversation, call);
252
258
  if (call.kind === 'message')
253
259
  return this.message(conversation, call);
260
+ if (call.kind === 'done') {
261
+ conversation.done = true;
262
+ this.next(conversation);
263
+ return { result: Promise.resolve({}) };
264
+ }
254
265
  await this.launched(conversation);
255
266
  if (call.kind === 'interrupt') {
256
267
  if (--conversation.interrupts === 0)
@@ -275,11 +286,17 @@ export class Conversations {
275
286
  }
276
287
  return { result: Promise.resolve({}) };
277
288
  }
278
- open(conversation, call) {
279
- if (conversation.running !== null)
280
- return Promise.resolve({});
289
+ async open(conversation, call) {
290
+ await this.stop(conversation);
281
291
  const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, call.agent_id), null, true);
282
- return launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) }));
292
+ return {
293
+ result: launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) })),
294
+ };
295
+ }
296
+ async stop(conversation) {
297
+ const running = conversation.running;
298
+ running?.scope.kill();
299
+ await running?.adapter.exited;
283
300
  }
284
301
  launch(conversation, route, sessionId, empty) {
285
302
  let opened;
@@ -318,7 +335,10 @@ export class Conversations {
318
335
  return read;
319
336
  }
320
337
  async message(conversation, call) {
321
- const content = [{ type: 'text', text: String(call.text) }];
338
+ const content = [
339
+ ...(typeof call.recent === 'string' ? [{ type: 'text', text: call.recent }] : []),
340
+ { type: 'text', text: String(call.text) },
341
+ ];
322
342
  let route;
323
343
  let instructing = null;
324
344
  try {
@@ -336,21 +356,71 @@ export class Conversations {
336
356
  return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
337
357
  }
338
358
  conversation.preparing = null;
339
- return {
340
- result: new Promise((settle) => {
341
- conversation.waiting.push({ call, route, content, instructing, settle });
342
- this.next(conversation);
343
- }),
344
- };
359
+ const { promise, resolve: settle } = Promise.withResolvers();
360
+ const waiting = { call, route, content, instructing, settle };
361
+ if (call.at_once === true && conversation.turns.size > 0)
362
+ await this.atOnce(conversation, waiting);
363
+ else
364
+ conversation.waiting.push(waiting);
365
+ this.next(conversation);
366
+ return { result: promise };
367
+ }
368
+ async atOnce(conversation, waiting) {
369
+ while (conversation.sending !== null)
370
+ await conversation.sending;
371
+ const running = conversation.running;
372
+ if (running !== null && conversation.turns.size > 0 && steers(running.adapter)) {
373
+ conversation.sending = this.steer(conversation, running, waiting);
374
+ await conversation.sending;
375
+ conversation.sending = null;
376
+ return;
377
+ }
378
+ this.ahead(conversation, waiting);
379
+ if (conversation.turns.size > 0)
380
+ await this.cancel(conversation);
381
+ }
382
+ ahead(conversation, waiting) {
383
+ const behind = conversation.waiting.findIndex((held) => held.call.at_once !== true);
384
+ conversation.waiting.splice(behind === -1 ? conversation.waiting.length : behind, 0, waiting);
385
+ }
386
+ async steer(conversation, running, waiting) {
387
+ let outcome;
388
+ try {
389
+ ({ outcome } = await running.adapter.connection.agent.request('_session/steering', {
390
+ sessionId: running.sessionId,
391
+ prompt: waiting.content,
392
+ _meta: { steering: { idleBehavior: 'promptRequired' } },
393
+ }));
394
+ }
395
+ catch (error) {
396
+ outcome = error;
397
+ }
398
+ if (outcome === 'promptRequired') {
399
+ this.ahead(conversation, waiting);
400
+ }
401
+ else if (outcome === 'injected' || outcome === 'startedNewTurn') {
402
+ if (outcome === 'injected') {
403
+ for (const turn of conversation.turns)
404
+ turn.steered ||= turn.prompted;
405
+ }
406
+ this.kit.calls.keepResult(String(waiting.call.input_id), {});
407
+ waiting.settle({});
408
+ }
409
+ else {
410
+ const cause = outcome instanceof Error ? outcome : new Error(`the CLI answered ${JSON.stringify(outcome)}`);
411
+ waiting.settle({ refused: refusal(conversation, new NotStarted(UNSTEERED, { cause })) });
412
+ }
345
413
  }
346
414
  next(conversation) {
347
- if (conversation.sending !== null || conversation.open > 0)
415
+ if (conversation.sending !== null || conversation.turns.size > 0)
348
416
  return;
349
417
  const waiting = conversation.waiting.shift();
350
418
  if (waiting === undefined) {
351
419
  const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
352
420
  if (conversation.starting !== step)
353
421
  this.starts(conversation, step);
422
+ if (conversation.done && conversation.launch === null)
423
+ void this.stop(conversation).catch(logged);
354
424
  return;
355
425
  }
356
426
  conversation.sending = this.send(conversation, waiting).finally(() => {
@@ -491,7 +561,11 @@ export class Conversations {
491
561
  void bridging.then((bridge) => bridge.close(), () => undefined);
492
562
  }
493
563
  async interrupt(conversation, turnId) {
494
- if (conversation.running === null || conversation.turn?.id !== turnId)
564
+ if ([...conversation.turns].some((turn) => turn.id === turnId))
565
+ await this.cancel(conversation);
566
+ }
567
+ async cancel(conversation) {
568
+ if (conversation.running === null)
495
569
  return;
496
570
  await conversation.running.adapter.connection.agent.notify('session/cancel', {
497
571
  sessionId: conversation.running.sessionId,
@@ -585,6 +659,7 @@ export class Conversations {
585
659
  const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
586
660
  void work.settled().then(() => scope.record());
587
661
  conversation.running = running;
662
+ adapter.turn(conversation.turns.size > 0);
588
663
  conversation.session = running.sessionId;
589
664
  conversation.options = options;
590
665
  this.facts(conversation);
@@ -651,6 +726,10 @@ export class Conversations {
651
726
  conversation.running = null;
652
727
  conversation.commands = null;
653
728
  conversation.options = null;
729
+ if (conversation.subagents.size > 0) {
730
+ conversation.subagents.clear();
731
+ this.subagentsChanged(conversation);
732
+ }
654
733
  for (const [id, question] of this.questions) {
655
734
  if (question.conversation === conversation)
656
735
  this.questions.delete(id);
@@ -667,6 +746,10 @@ export class Conversations {
667
746
  if (conversation.options !== null)
668
747
  this.options(conversation, conversation.options);
669
748
  }
749
+ subagentsChanged(conversation) {
750
+ const subagents = [...conversation.subagents.values()].map(({ description, activity }) => ({ description, activity }));
751
+ this.kit.send({ type: 'subagents', conversation_id: conversation.id, subagents });
752
+ }
670
753
  commands(conversation, commands) {
671
754
  conversation.commands = commands;
672
755
  if (conversation.running === null)
@@ -701,7 +784,8 @@ export class Conversations {
701
784
  }
702
785
  begin(conversation) {
703
786
  const turn = new Turn((operation) => this.write(conversation, turn, operation));
704
- conversation.open++;
787
+ conversation.turns.add(turn);
788
+ conversation.running?.adapter.turn(true);
705
789
  void this.deliver(conversation, turn, 'started', {}).catch(logged);
706
790
  conversation.turn = turn;
707
791
  if (conversation.context !== null)
@@ -723,7 +807,8 @@ export class Conversations {
723
807
  if (notice !== null)
724
808
  turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
725
809
  turn.ended = true;
726
- conversation.open--;
810
+ conversation.turns.delete(turn);
811
+ conversation.running?.adapter.turn(conversation.turns.size > 0);
727
812
  if (conversation.turn === turn)
728
813
  conversation.turn = null;
729
814
  const body = Promise.allSettled(turn.uploads).then(() => ({
@@ -756,7 +841,7 @@ export class Conversations {
756
841
  }
757
842
  else {
758
843
  this.took(turn);
759
- if (conversation.open === 1 && conversation.running === running) {
844
+ if (conversation.turns.size === 1 && conversation.turns.has(turn) && conversation.running === running) {
760
845
  running.adapter.retire();
761
846
  this.detach(conversation, running);
762
847
  }
@@ -769,19 +854,22 @@ export class Conversations {
769
854
  this.took(turn);
770
855
  return turn;
771
856
  }
772
- ended(conversation) {
857
+ ended(conversation, stop) {
773
858
  const turn = conversation.turn;
774
859
  if (turn === null)
775
860
  return;
776
- if (!turn.prompted) {
777
- this.end(conversation, turn, null);
778
- }
779
- else {
780
- for (const event of conversation.running?.reader.closeTurn() ?? [])
781
- this.apply(conversation, turn, event);
782
- conversation.turn = null;
783
- this.kit.changed();
861
+ const held = stop !== null && !turn.steered && [...conversation.subagents.values()].some((subagent) => subagent.turn === turn);
862
+ if (!turn.prompted || held) {
863
+ this.took(turn);
864
+ this.end(conversation, turn, stop === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
865
+ return;
784
866
  }
867
+ if (stop !== null)
868
+ return;
869
+ for (const event of conversation.running?.reader.closeTurn() ?? [])
870
+ this.apply(conversation, turn, event);
871
+ conversation.turn = null;
872
+ this.kit.changed();
785
873
  }
786
874
  async ask(conversation, method, params, secret, signal) {
787
875
  const turn = this.started(conversation);
@@ -837,7 +925,7 @@ export class Conversations {
837
925
  this.started(conversation);
838
926
  return;
839
927
  case 'ended':
840
- this.ended(conversation);
928
+ this.ended(conversation, event.stop);
841
929
  return;
842
930
  case 'commands':
843
931
  this.commands(conversation, event.commands);
@@ -852,6 +940,9 @@ export class Conversations {
852
940
  case 'subagent-done':
853
941
  this.subagentDone(conversation, event.id, event.result);
854
942
  return;
943
+ case 'subagent-activity':
944
+ this.subagentActivity(conversation, event.id, event.activity);
945
+ return;
855
946
  case 'marker':
856
947
  case 'image':
857
948
  if (conversation.turn !== null)
@@ -878,7 +969,8 @@ export class Conversations {
878
969
  return;
879
970
  case 'subagent': {
880
971
  const index = turn.parts.start({ type: 'subagent', description: event.description, status: 'running', result: null });
881
- conversation.subagents.set(event.id, { description: event.description, turn, index });
972
+ conversation.subagents.set(event.id, { description: event.description, turn, index, activity: null });
973
+ this.subagentsChanged(conversation);
882
974
  return;
883
975
  }
884
976
  case 'message':
@@ -901,6 +993,7 @@ export class Conversations {
901
993
  if (subagent === undefined)
902
994
  return;
903
995
  conversation.subagents.delete(id);
996
+ this.subagentsChanged(conversation);
904
997
  if (subagent.turn === conversation.turn) {
905
998
  subagent.turn.parts.done(subagent.index, result);
906
999
  return;
@@ -911,6 +1004,13 @@ export class Conversations {
911
1004
  else
912
1005
  conversation.turn.parts.start(part);
913
1006
  }
1007
+ subagentActivity(conversation, id, activity) {
1008
+ const subagent = conversation.subagents.get(id);
1009
+ if (subagent === undefined || subagent.activity === activity)
1010
+ return;
1011
+ subagent.activity = activity;
1012
+ this.subagentsChanged(conversation);
1013
+ }
914
1014
  async image(conversation, turn, reserved, data, mediaType) {
915
1015
  const bytes = Buffer.from(data, 'base64');
916
1016
  const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
package/dist/end.js CHANGED
@@ -14,9 +14,15 @@ export function end(reason) {
14
14
  export function restart(reason) {
15
15
  exit(reason, 1);
16
16
  }
17
- export function updateRequired() {
17
+ function updateLine(lead) {
18
18
  const command = installCommand();
19
- end(command === null ? 'This Kit is too old for House; install it again.' : `This Kit is too old for House. Update it: ${command}`);
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}`);
20
26
  }
21
27
  export function newerThanHouse(release, command) {
22
28
  end(`Kit ${KIT_RELEASE} is newer than this Building supports (Kit ${release}). Install Kit ${release}: ${command}`);
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 MESSAGE_TARGETS = { send_routine_request: 'access', cancel_routine_request: 'request', send_mail: '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 = MESSAGE_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);
package/dist/git.js CHANGED
@@ -2,8 +2,8 @@ import { spawn } from 'node:child_process';
2
2
  import { isUtf8 } from 'node:buffer';
3
3
  import { createHash } from 'node:crypto';
4
4
  import { join } from 'node:path';
5
- import { withoutProxy } from './clis.js';
6
5
  import { workspaceRoot } from './home.js';
6
+ import { childEnvironment } from './relay.js';
7
7
  export function localCopies() {
8
8
  return join(workspaceRoot(), 'local-copies');
9
9
  }
@@ -11,7 +11,7 @@ export const HOUSE_REF = 'refs/house/received';
11
11
  const COMMITTER = 'Kit <house-kit@localhost>';
12
12
  export function run(cwd, args, input, env = {}) {
13
13
  return new Promise((resolve, reject) => {
14
- const child = spawn('git', args, { cwd, env: { ...withoutProxy(), ...env } });
14
+ const child = spawn('git', args, { cwd, env: { ...childEnvironment(), ...env } });
15
15
  const stdout = [];
16
16
  let stderr = '';
17
17
  child.stdout.on('data', (chunk) => stdout.push(chunk));
package/dist/house-cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { createReadStream } from 'node:fs';
3
- import { request, setGlobalProxyFromEnv } from 'node:http';
3
+ import { request } from 'node:http';
4
4
  import { request as httpsRequest } from 'node:https';
5
5
  import { extname, join, resolve } from 'node:path';
6
6
  import { Readable } from 'node:stream';
@@ -10,7 +10,7 @@ import { HOUSE_REF } from './git.js';
10
10
  import { ownerSocket, readOwnAgent } from './home.js';
11
11
  import { callHouse, rpc, UNREACHABLE, Unsettled } from './mcp.js';
12
12
  import { refusalOf, retryable } from './refusals.js';
13
- import { relayed, selectRelay } from './relay.js';
13
+ import { proxyUnlessRelayed, relayed, selectRelay } from './relay.js';
14
14
  import { parse } from 'yaml';
15
15
  const USAGE = "usage: house <tool> ['<arguments as JSON>']";
16
16
  const PUSH_USAGE = 'usage: house push [commit] [--owner]';
@@ -89,7 +89,6 @@ async function direct(message) {
89
89
  if (ownAgent === null)
90
90
  throw new Error(UNCONNECTED);
91
91
  selectRelay(process.env.HOUSE_KIT_RELAY || ownAgent.relay);
92
- setGlobalProxyFromEnv();
93
92
  const answer = await callHouse(ownAgent.house, ownAgent.credential, message).catch(() => {
94
93
  throw new Unsettled(UNREACHABLE);
95
94
  });
@@ -120,7 +119,7 @@ function opened(url, method, headers, body) {
120
119
  const local = socketPath !== undefined;
121
120
  const target = local ? null : relayed(url);
122
121
  if (!local)
123
- setGlobalProxyFromEnv();
122
+ proxyUnlessRelayed();
124
123
  const sent = target === null
125
124
  ? request({ socketPath, path: url, method, headers }, answered)
126
125
  : (target.protocol === 'https:' ? httpsRequest : request)(target, { method, headers }, answered);
@@ -1,6 +1,6 @@
1
1
  import { agentInstructions } from '@agentshouse/skills';
2
2
  import { HOUSE_REF } from './git.js';
3
- const HOW_WE_WORK = '/rooms/private/how-we-work.md';
3
+ const OWNER_RULES = '/rooms/private/owner-rules.md';
4
4
  const UNREADABLE = /^(?:\S+: )?(?:path_not_found|room_not_found|operation_denied)\b/;
5
5
  const FAILED = /^exit: [1-9]\d*$/;
6
6
  const CUT = /^stderr: shell: output_cut (\d+) of \d+ bytes; continue with: (.+)$/;
@@ -8,9 +8,9 @@ function cutOf(lines) {
8
8
  const cut = CUT.exec(lines[0] ?? '');
9
9
  return cut !== null && Buffer.byteLength(lines.slice(1).join('\n')) === Number(cut[1]) ? cut : null;
10
10
  }
11
- async function readHowWeWork(bridge) {
11
+ async function readOwnerRules(bridge) {
12
12
  let document = '';
13
- for (let command = `cat ${HOW_WE_WORK}`; command !== undefined;) {
13
+ for (let command = `cat ${OWNER_RULES}`; command !== undefined;) {
14
14
  const read = await bridge.tool('shell', { command });
15
15
  const text = read.content.map((content) => content.text).join('\n');
16
16
  const lines = text.split('\n');
@@ -28,5 +28,5 @@ async function readHowWeWork(bridge) {
28
28
  }
29
29
  export async function instructions(bridge, base, copies) {
30
30
  const lines = agentInstructions(HOUSE_REF, copies);
31
- return [lines.join('\n'), base, (await readHowWeWork(bridge)).replace(/\n$/, '')].filter((part) => part !== '').join('\n\n');
31
+ return [lines.join('\n'), base, (await readOwnerRules(bridge)).replace(/\n$/, '')].filter((part) => part !== '').join('\n\n');
32
32
  }
package/dist/kit-cli.js CHANGED
@@ -1,9 +1,9 @@
1
- import { setGlobalProxyFromEnv } from 'node:http';
2
1
  import { parseArgs } from 'node:util';
3
2
  import { answerGitCredential } from './app-git.js';
4
3
  import { chatgpt } from './chatgpt.js';
5
4
  import { login } from './login.js';
6
5
  import { logout } from './logout.js';
6
+ import { proxyUnlessRelayed } from './relay.js';
7
7
  import { resident } from './resident.js';
8
8
  const USAGE = 'usage: kit <login|logout|resident|chatgpt>\n';
9
9
  const COMMANDS = new Map([
@@ -36,7 +36,7 @@ const COMMANDS = new Map([
36
36
  ],
37
37
  ]);
38
38
  const [command, ...argv] = process.argv.slice(2);
39
- setGlobalProxyFromEnv();
39
+ proxyUnlessRelayed();
40
40
  async function run() {
41
41
  if (command === 'login')
42
42
  return login(argv);
package/dist/login.js CHANGED
@@ -10,6 +10,7 @@ import { refusalOf } from './refusals.js';
10
10
  import { selectRelay } from './relay.js';
11
11
  import { startService } from './service.js';
12
12
  import { placeSkillSet } from './skills.js';
13
+ import { endUnserved } from './wire.js';
13
14
  export const DEFAULT_HOUSE = 'https://agents.house';
14
15
  const OPENER = process.platform === 'darwin' ? 'open' : 'xdg-open';
15
16
  // Polling every two seconds brings the User's confirmation to the waiting Kit moments after they give it.
@@ -25,6 +26,7 @@ export async function accepted(house, credential) {
25
26
  return true;
26
27
  if (checked.status === 401)
27
28
  return false;
29
+ endUnserved(checked.text);
28
30
  throw new Error(`House answered ${checked.status} when Kit checked this computer's own agent's connection: ${checked.text}`);
29
31
  }
30
32
  const USAGE = `usage: kit login [--house <origin>] [--environment <id>] [--rooms <ref>[,<ref>...] | --no-private]
@@ -119,7 +121,7 @@ export async function login(argv) {
119
121
  open(started.start_url);
120
122
  exchanged = await retrying(() => post(house, '/kit/token', { login: started.login, code_verifier: verifier }), undefined, () => POLL_INTERVAL_MS);
121
123
  }
122
- await placeSkillSet();
124
+ await placeSkillSet(house);
123
125
  await writeCodexRule().catch((error) => process.stderr.write(`kit: ${error.message}\n`));
124
126
  const relayRecord = relay === undefined ? {} : { relay };
125
127
  await writeEnrolment({
package/dist/mcp.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { KIT_RELEASE } from './clis.js';
3
3
  import { relayed } from './relay.js';
4
+ import { KIT_WIRE, KIT_WIRE_HEADER } from './wire.js';
4
5
  const MCP_PROTOCOL_VERSION = '2026-07-28';
5
6
  export const UNREACHABLE = 'House is unreachable; call again in a minute';
6
7
  export class Unsettled extends Error {
@@ -33,6 +34,7 @@ export async function callHouse(origin, credential, message, operation = null, s
33
34
  'mcp-protocol-version': MCP_PROTOCOL_VERSION,
34
35
  'mcp-method': message.method,
35
36
  ...(tool === null ? {} : { 'mcp-name': tool }),
37
+ [KIT_WIRE_HEADER]: String(KIT_WIRE),
36
38
  },
37
39
  body: mcpBody(message, operation),
38
40
  signal,