@agentshouse/kit 0.2.1-c845e172df1af → 0.2.1-c849d8d4ab62f

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 (131) hide show
  1. package/README.md +6 -6
  2. package/dist/acp.js +57 -6
  3. package/dist/agents.js +6 -9
  4. package/dist/authority-main.js +2 -2
  5. package/dist/bridge.js +1 -1
  6. package/dist/building.js +46 -0
  7. package/dist/calls.js +11 -3
  8. package/dist/clis-main.js +2 -2
  9. package/dist/clis.js +39 -15
  10. package/dist/connection.js +20 -29
  11. package/dist/conversations.js +235 -81
  12. package/dist/end.js +8 -2
  13. package/dist/events.js +29 -8
  14. package/dist/files.js +53 -40
  15. package/dist/git.js +2 -2
  16. package/dist/house-cli.js +31 -59
  17. package/dist/house-main.js +3 -3
  18. package/dist/instructions.js +29 -9
  19. package/dist/kit-cli.js +2 -2
  20. package/dist/kit-main.js +3 -3
  21. package/dist/login.js +4 -4
  22. package/dist/mcp.js +2 -0
  23. package/dist/relay.js +21 -0
  24. package/dist/resident.js +17 -6
  25. package/dist/scripts.js +255 -0
  26. package/dist/sign-in.js +3 -2
  27. package/dist/silence.js +28 -0
  28. package/dist/skills.js +41 -96
  29. package/dist/transcription.js +83 -43
  30. package/dist/update.js +51 -8
  31. package/package.json +19 -20
  32. package/dist/bootstrap.json +0 -24
  33. package/how-we-work.md +0 -33
  34. package/skills/ask-matt/LICENSE +0 -21
  35. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  36. package/skills/ask-matt/SKILL.md +0 -97
  37. package/skills/ask-matt/agents/openai.yaml +0 -5
  38. package/skills/code-review/LICENSE +0 -21
  39. package/skills/code-review/SKILL.md +0 -89
  40. package/skills/code-review/agents/openai.yaml +0 -3
  41. package/skills/codebase-design/DEEPENING.md +0 -37
  42. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  43. package/skills/codebase-design/LICENSE +0 -21
  44. package/skills/codebase-design/SKILL.md +0 -114
  45. package/skills/codebase-design/agents/openai.yaml +0 -3
  46. package/skills/diagnosing-bugs/LICENSE +0 -21
  47. package/skills/diagnosing-bugs/SKILL.md +0 -138
  48. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  49. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  50. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  51. package/skills/domain-modeling/GLOSSARY-FORMAT.md +0 -60
  52. package/skills/domain-modeling/LICENSE +0 -21
  53. package/skills/domain-modeling/SKILL.md +0 -74
  54. package/skills/domain-modeling/agents/openai.yaml +0 -3
  55. package/skills/grill-me/LICENSE +0 -21
  56. package/skills/grill-me/SKILL.md +0 -7
  57. package/skills/grill-me/agents/openai.yaml +0 -5
  58. package/skills/grill-with-docs/LICENSE +0 -21
  59. package/skills/grill-with-docs/SKILL.md +0 -7
  60. package/skills/grill-with-docs/agents/openai.yaml +0 -5
  61. package/skills/grilling/LICENSE +0 -21
  62. package/skills/grilling/SKILL.md +0 -30
  63. package/skills/grilling/agents/openai.yaml +0 -3
  64. package/skills/handoff/LICENSE +0 -21
  65. package/skills/handoff/SKILL.md +0 -16
  66. package/skills/handoff/agents/openai.yaml +0 -5
  67. package/skills/implement/LICENSE +0 -21
  68. package/skills/implement/SKILL.md +0 -17
  69. package/skills/implement/agents/openai.yaml +0 -5
  70. package/skills/implement-spec/LICENSE +0 -21
  71. package/skills/implement-spec/SKILL.md +0 -40
  72. package/skills/implement-spec/agents/openai.yaml +0 -5
  73. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  74. package/skills/improve-codebase-architecture/LICENSE +0 -21
  75. package/skills/improve-codebase-architecture/SKILL.md +0 -71
  76. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
  77. package/skills/pr/CREDITS.md +0 -3
  78. package/skills/pr/LICENSE +0 -21
  79. package/skills/pr/SKILL.md +0 -170
  80. package/skills/pr/agents/openai.yaml +0 -3
  81. package/skills/prototype/LICENSE +0 -21
  82. package/skills/prototype/LOGIC.md +0 -67
  83. package/skills/prototype/SKILL.md +0 -26
  84. package/skills/prototype/UI.md +0 -112
  85. package/skills/prototype/agents/openai.yaml +0 -3
  86. package/skills/research/LICENSE +0 -21
  87. package/skills/research/SKILL.md +0 -12
  88. package/skills/research/agents/openai.yaml +0 -3
  89. package/skills/retro/LICENSE +0 -21
  90. package/skills/retro/SKILL.md +0 -44
  91. package/skills/retro/agents/openai.yaml +0 -5
  92. package/skills/tdd/LICENSE +0 -21
  93. package/skills/tdd/SKILL.md +0 -38
  94. package/skills/tdd/agents/openai.yaml +0 -3
  95. package/skills/tdd/mocking.md +0 -59
  96. package/skills/tdd/tests.md +0 -77
  97. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  98. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  99. package/skills/teach/LICENSE +0 -21
  100. package/skills/teach/MISSION-FORMAT.md +0 -31
  101. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  102. package/skills/teach/SKILL.md +0 -140
  103. package/skills/teach/agents/openai.yaml +0 -5
  104. package/skills/to-questionnaire/LICENSE +0 -21
  105. package/skills/to-questionnaire/SKILL.md +0 -54
  106. package/skills/to-questionnaire/agents/openai.yaml +0 -5
  107. package/skills/to-spec/LICENSE +0 -21
  108. package/skills/to-spec/SKILL.md +0 -75
  109. package/skills/to-spec/agents/openai.yaml +0 -5
  110. package/skills/to-tickets/LICENSE +0 -21
  111. package/skills/to-tickets/SKILL.md +0 -105
  112. package/skills/to-tickets/agents/openai.yaml +0 -5
  113. package/skills/triage/AGENT-BRIEF.md +0 -207
  114. package/skills/triage/LICENSE +0 -21
  115. package/skills/triage/OUT-OF-SCOPE.md +0 -105
  116. package/skills/triage/SKILL.md +0 -112
  117. package/skills/triage/agents/openai.yaml +0 -5
  118. package/skills/wait-what/LICENSE +0 -21
  119. package/skills/wait-what/SKILL.md +0 -7
  120. package/skills/wait-what/agents/openai.yaml +0 -5
  121. package/skills/wayfinder/LICENSE +0 -21
  122. package/skills/wayfinder/SKILL.md +0 -128
  123. package/skills/wayfinder/agents/openai.yaml +0 -5
  124. package/skills/wizard/LICENSE +0 -21
  125. package/skills/wizard/SKILL.md +0 -44
  126. package/skills/wizard/agents/openai.yaml +0 -3
  127. package/skills/wizard/template.sh +0 -223
  128. package/skills/writing-for-agents/LICENSE +0 -21
  129. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  130. package/skills/writing-for-agents/SKILL.md +0 -81
  131. package/skills/writing-for-agents/agents/openai.yaml +0 -3
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/files.js CHANGED
@@ -4,27 +4,68 @@ import { mkdir, rename, rm } from 'node:fs/promises';
4
4
  import { basename, dirname, join } from 'node:path';
5
5
  import { Readable, Transform } from 'node:stream';
6
6
  import { pipeline } from 'node:stream/promises';
7
- import { retrying, retryDelay } from './api.js';
7
+ import { parseDocument } from 'yaml';
8
+ import { retryDelay } from './api.js';
8
9
  import { Unsettled } from './mcp.js';
9
10
  import { relayed } from './relay.js';
10
11
  const STALLED_ROUNDS = 8;
12
+ const PART_CHECKSUM = 'x-amz-checksum-sha256';
13
+ const LINK = /\]\((\/rooms\/[^/)\s]+\/[^)\s]+)\)/gu;
14
+ export function linkedPaths(text) {
15
+ return [...new Set(Array.from(text.matchAll(LINK), (link) => link[1]))];
16
+ }
17
+ export function inspectedFile(answer) {
18
+ const document = parseDocument(answer);
19
+ if (document.errors.length > 0)
20
+ return null;
21
+ const inspected = document.toJS();
22
+ if (typeof inspected?.download !== 'string')
23
+ return null;
24
+ return {
25
+ download: inspected.download,
26
+ sha256: String(inspected.sha256),
27
+ media_type: typeof inspected.media_type === 'string' ? inspected.media_type : null,
28
+ };
29
+ }
11
30
  function paused(milliseconds) {
12
31
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
13
32
  }
14
33
  export function storeRefusal(act, status) {
15
34
  return new Error(`the ${act} answered ${status}; ${act} the file again`);
16
35
  }
36
+ async function partDigests(uploading, partBytes) {
37
+ const whole = createHash('sha256');
38
+ const digests = [];
39
+ for (let start = 0; digests.length === 0 || start < uploading.bytes; start += partBytes) {
40
+ const end = Math.min(start + partBytes, uploading.bytes);
41
+ const digest = createHash('sha256');
42
+ for await (const chunk of uploading.read(start, end)) {
43
+ digest.update(chunk);
44
+ whole.update(chunk);
45
+ }
46
+ digests.push(digest.digest());
47
+ }
48
+ if (whole.digest('hex') !== uploading.sha256)
49
+ throw new Error(`${uploading.name} changed while it was uploaded; upload it again`);
50
+ return digests;
51
+ }
17
52
  export async function uploaded(declared, uploading) {
18
- let upload = declared;
53
+ const partBytes = declared.part_bytes;
54
+ const digests = await partDigests(uploading, partBytes);
55
+ const partSha256 = digests.map((digest) => digest.toString('hex'));
56
+ let answer = await finishing(uploading, declared.id, partSha256);
19
57
  let idle = 0;
20
58
  let stalled = 0;
21
- for (;;) {
59
+ while ('upload' in answer) {
60
+ const { id, parts } = answer.upload;
22
61
  let sent = 0;
23
62
  let failure = null;
24
- for (const { part, url } of upload.parts) {
25
- const start = (part - 1) * upload.part_bytes;
63
+ for (const { part, url } of parts ?? []) {
64
+ const start = (part - 1) * partBytes;
26
65
  try {
27
- await uploading.send(url, start, Math.min(start + upload.part_bytes, uploading.bytes));
66
+ await uploading.send(url, start, Math.min(start + partBytes, uploading.bytes), {
67
+ [PART_CHECKSUM]: digests[part - 1].toString('base64'),
68
+ });
28
69
  sent++;
29
70
  }
30
71
  catch (error) {
@@ -39,24 +80,17 @@ export async function uploaded(declared, uploading) {
39
80
  else {
40
81
  if (failure !== null && ++stalled >= STALLED_ROUNDS)
41
82
  throw failure;
42
- // A round that sent no part, its store unreachable or not yet started, is followed by the next finishing call on the jittered curve.
83
+ // A round that sent no part, its store unreachable, is followed by the next finishing call on the jittered curve.
43
84
  await paused(retryDelay(idle++));
44
85
  }
45
- let answer = await finishing(uploading, upload.id);
46
- while ('checking' in answer) {
47
- // A finishing call that finds House still checking the stored bytes is sent again after five seconds, the cadence at which House takes up a check it handed over.
48
- await paused(5_000);
49
- answer = await finishing(uploading, upload.id);
50
- }
51
- if (!('upload' in answer))
52
- return answer;
53
- upload = answer.upload;
86
+ answer = await finishing(uploading, id, partSha256);
54
87
  }
88
+ return answer;
55
89
  }
56
- async function finishing(uploading, upload) {
90
+ async function finishing(uploading, upload, partSha256) {
57
91
  for (let unsettled = 1;; unsettled++) {
58
92
  try {
59
- return await uploading.finish(upload);
93
+ return await uploading.finish(upload, partSha256);
60
94
  }
61
95
  catch (error) {
62
96
  if (!(error instanceof Unsettled) || unsettled === STALLED_ROUNDS)
@@ -104,28 +138,7 @@ export async function downloaded(address, fetching, destination, name) {
104
138
  return;
105
139
  }
106
140
  }
107
- async function fetched(url, signal) {
141
+ export async function fetched(url, signal) {
108
142
  const answer = await fetch(relayed(url), { signal });
109
143
  return { status: answer.status, body: answer.body ?? Readable.from([]) };
110
144
  }
111
- export async function placeFiles(house, directory, files, signal) {
112
- const paths = [];
113
- for (const file of files) {
114
- const original = () => retrying(() => house.post('/kit/conversation/originals/get', { version: file.version, download: true }, signal), signal);
115
- let first = await original();
116
- const folder = join(directory, '.house', 'files', first.message, file.version);
117
- await mkdir(join(directory, '.house')).catch((error) => {
118
- if (error.code !== 'EEXIST')
119
- throw error;
120
- });
121
- await mkdir(folder, { recursive: true });
122
- const path = join(folder, file.name);
123
- await downloaded(async () => {
124
- const answered = first ?? (await original());
125
- first = null;
126
- return { download: answered.download.url, sha256: file.sha256 };
127
- }, (url) => fetched(url, signal), path, file.name);
128
- paths.push(path);
129
- }
130
- return paths;
131
- }
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,17 +1,16 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { createReadStream } from 'node:fs';
3
- import { readFile } from 'node:fs/promises';
4
- import { request, setGlobalProxyFromEnv } from 'node:http';
3
+ import { request } from 'node:http';
5
4
  import { request as httpsRequest } from 'node:https';
6
- import { basename, extname, join, resolve } from 'node:path';
5
+ import { extname, join, resolve } from 'node:path';
7
6
  import { Readable } from 'node:stream';
8
7
  import { pipeline } from 'node:stream/promises';
9
- import { downloaded, storeRefusal, uploaded } from './files.js';
8
+ import { downloaded, inspectedFile, storeRefusal, uploaded } from './files.js';
10
9
  import { HOUSE_REF } from './git.js';
11
10
  import { ownerSocket, readOwnAgent } from './home.js';
12
11
  import { callHouse, rpc, UNREACHABLE, Unsettled } from './mcp.js';
13
12
  import { refusalOf, retryable } from './refusals.js';
14
- import { relayed, selectRelay } from './relay.js';
13
+ import { proxyUnlessRelayed, relayed, selectRelay } from './relay.js';
15
14
  import { parse } from 'yaml';
16
15
  const USAGE = "usage: house <tool> ['<arguments as JSON>']";
17
16
  const PUSH_USAGE = 'usage: house push [commit] [--owner]';
@@ -90,7 +89,6 @@ async function direct(message) {
90
89
  if (ownAgent === null)
91
90
  throw new Error(UNCONNECTED);
92
91
  selectRelay(process.env.HOUSE_KIT_RELAY || ownAgent.relay);
93
- setGlobalProxyFromEnv();
94
92
  const answer = await callHouse(ownAgent.house, ownAgent.credential, message).catch(() => {
95
93
  throw new Unsettled(UNREACHABLE);
96
94
  });
@@ -116,43 +114,35 @@ function unreadable(path, error) {
116
114
  ? `${path} is a folder; upload one file at a time`
117
115
  : `${path} is not a readable file`);
118
116
  }
119
- async function localFile(path) {
120
- try {
121
- return await readFile(path);
122
- }
123
- catch (error) {
124
- throw unreadable(path, error);
125
- }
126
- }
127
117
  function opened(url, method, headers, body) {
128
118
  return new Promise((answered, failed) => {
129
119
  const local = socketPath !== undefined;
130
120
  const target = local ? null : relayed(url);
131
121
  if (!local)
132
- setGlobalProxyFromEnv();
122
+ proxyUnlessRelayed();
133
123
  const sent = target === null
134
124
  ? request({ socketPath, path: url, method, headers }, answered)
135
125
  : (target.protocol === 'https:' ? httpsRequest : request)(target, { method, headers }, answered);
136
126
  sent.on('error', (error) => failed(new Error(!local ? UNREACHABLE : error.code === 'EPERM' || error.code === 'EACCES' ? SANDBOXED : CLOSED)));
137
- if (body instanceof Readable)
138
- pipeline(body, sent).catch(() => undefined);
127
+ if (body === undefined)
128
+ sent.end();
139
129
  else
140
- sent.end(body);
130
+ pipeline(body, sent).catch(() => undefined);
141
131
  });
142
132
  }
143
- async function textOf(answer) {
144
- return Buffer.concat(await answer.toArray()).toString('utf8');
133
+ function ranged(file, start, end) {
134
+ return end === start ? undefined : createReadStream(file, { start, end: end - 1 });
145
135
  }
146
- async function sent(url, bytes, operation) {
147
- const headers = { 'content-type': 'application/octet-stream', 'x-house-byte-operation': operation };
148
- const answer = await opened(url, 'POST', headers, bytes);
149
- const text = await textOf(answer);
150
- if (answer.statusCode === 200)
151
- return text;
152
- throw new Error(refusalOf(text)?.text ?? storeRefusal('upload', answer.statusCode).message);
136
+ async function* read(file, start, end) {
137
+ try {
138
+ yield* ranged(file, start, end) ?? [];
139
+ }
140
+ catch (error) {
141
+ throw unreadable(file, error);
142
+ }
153
143
  }
154
- async function sentPart(url, file, start, end) {
155
- const answer = await opened(url, 'PUT', { 'content-length': String(end - start) }, end === start ? undefined : createReadStream(file, { start, end: end - 1 }));
144
+ async function sentPart(url, file, start, end, headers) {
145
+ const answer = await opened(url, 'PUT', { ...headers, 'content-length': String(end - start) }, ranged(file, start, end));
156
146
  await answer.toArray();
157
147
  if (answer.statusCode !== 200)
158
148
  throw storeRefusal('upload', answer.statusCode);
@@ -166,10 +156,10 @@ async function download(args) {
166
156
  throw new Error('download needs "file", the local file to write');
167
157
  const written = resolve(file);
168
158
  await downloaded(async () => {
169
- const inspected = parse(await called('inspect', selected));
170
- if (typeof inspected?.download !== 'string')
159
+ const file = inspectedFile(await called('inspect', selected));
160
+ if (file === null)
171
161
  throw new Error(`${String(selected.path)} holds no binary file; shell reads its text`);
172
- return { download: inspected.download, sha256: String(inspected.sha256) };
162
+ return file;
173
163
  }, async (url) => {
174
164
  const answer = await opened(url, 'GET', {});
175
165
  return { status: answer.statusCode, body: answer };
@@ -191,10 +181,10 @@ async function described(file) {
191
181
  const mediaType = MEDIA_TYPES.get(extname(file).slice(1).toLowerCase());
192
182
  return { bytes, sha256: digest.digest('hex'), ...(mediaType === undefined ? {} : { media_type: mediaType }) };
193
183
  }
194
- async function finished(upload, room) {
195
- const text = await called('upload', { upload, room_ref: room });
184
+ async function finished(upload, room, partSha256) {
185
+ const text = await called('upload', { upload, room_ref: room, part_sha256: partSha256 });
196
186
  const answer = parse(text);
197
- return answer !== null && ('upload' in answer || 'checking' in answer) ? answer : { text };
187
+ return answer !== null && 'upload' in answer ? answer : { text };
198
188
  }
199
189
  async function upload({ file, ...destination }) {
200
190
  if (typeof file !== 'string' || file === '')
@@ -205,33 +195,15 @@ async function upload({ file, ...destination }) {
205
195
  if (declared?.upload === undefined)
206
196
  return answer;
207
197
  const accepted = await uploaded(declared.upload, {
198
+ name: file,
208
199
  bytes: description.bytes,
209
- send: (url, start, end) => sentPart(url, file, start, end),
210
- finish: (id) => finished(id, destination.room_ref),
200
+ sha256: description.sha256,
201
+ read: (start, end) => read(file, start, end),
202
+ send: (url, start, end, headers) => sentPart(url, file, start, end, headers),
203
+ finish: (id, partSha256) => finished(id, destination.room_ref, partSha256),
211
204
  });
212
205
  return accepted.text;
213
206
  }
214
- async function appendRecord(args) {
215
- const held = new Map();
216
- const attachments = Array.isArray(args.attachments)
217
- ? await Promise.all(args.attachments.map(async (attachment) => {
218
- if (typeof attachment !== 'string')
219
- return attachment;
220
- const description = await described(attachment);
221
- held.set(description.sha256, attachment);
222
- return { name: basename(attachment), ...description };
223
- }))
224
- : args.attachments;
225
- const record = { ...args, attachments };
226
- for (;;) {
227
- const answer = await called('append_record', record);
228
- const { upload } = parse(answer);
229
- if (upload === undefined)
230
- return answer;
231
- for (const grant of upload)
232
- await sent(grant.url, await localFile(held.get(grant.sha256)), grant.operation);
233
- }
234
- }
235
207
  function unexpected(argument, usage) {
236
208
  return new Error(`unexpected argument ${argument}; ${usage}`);
237
209
  }
@@ -299,7 +271,7 @@ async function house(args) {
299
271
  return [...(tool.description === undefined ? [] : [tool.description]), JSON.stringify(tool.inputSchema)].join('\n');
300
272
  }
301
273
  const parsed = argumentsOf(argument);
302
- return verb === 'upload' ? upload(parsed) : verb === 'append_record' ? appendRecord(parsed) : called(verb, parsed);
274
+ return verb === 'upload' ? upload(parsed) : called(verb, parsed);
303
275
  }
304
276
  async function called(name, args) {
305
277
  const result = await mcp('tools/call', { name, arguments: args });
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- const [major = 0, minor = 0] = process.versions.node.split('.').map(Number);
3
- if (major < 24 || (major === 24 && minor < 14)) {
4
- process.stderr.write(`house: the Kit needs Node 24.14.0 or newer; this is ${process.versions.node}\n`);
2
+ const [major = 0, minor = 0, patch = 0] = process.versions.node.split('.').map(Number);
3
+ if (major < 24 || (major === 24 && (minor < 22 || (minor === 22 && patch < 1)))) {
4
+ process.stderr.write(`house: the Kit needs Node 24.22.1 or newer; this is ${process.versions.node}\n`);
5
5
  process.exitCode = 1;
6
6
  }
7
7
  else {
@@ -1,12 +1,32 @@
1
+ import { agentInstructions } from '@agentshouse/skills';
1
2
  import { HOUSE_REF } from './git.js';
2
- import { readHowWeWork } from './skills.js';
3
- const SOURCE_LINE = "From House:";
4
- const HOUSE_LINE = "Work with House through the `house` CLI: run `house --help`, and `house find_command` for House's guides and formats.";
5
- const FILE_LINE = 'To give the User a file, upload it with `house upload` and link it; for an App, a Site or a website, follow `house find_command \'{"query":"app"}\'`. Never start a server.';
6
- const READ_LINE = "Optional per-Room instructions: `/rooms/private/rooms/<handle>/AGENTS.md`.";
7
- const REQUEST_LINE = "For another Agent's help, `send_routine_request`; its mail comes back into this conversation. If an Agent of your User has no Routine to call, write one in the Private Room and `create_routine_access` with your User as its only caller.";
8
- const COPIES_LINE = `House Local copies; before work run \`house fetch\` and rebase onto \`${HOUSE_REF}\`; after it commit, then \`house push\`:`;
3
+ const OWNER_RULES = '/rooms/private/owner-rules.md';
4
+ const UNREADABLE = /^(?:\S+: )?(?:path_not_found|room_not_found|operation_denied)\b/;
5
+ const FAILED = /^exit: [1-9]\d*$/;
6
+ const CUT = /^stderr: shell: output_cut (\d+) of \d+ bytes; continue with: (.+)$/;
7
+ function cutOf(lines) {
8
+ const cut = CUT.exec(lines[0] ?? '');
9
+ return cut !== null && Buffer.byteLength(lines.slice(1).join('\n')) === Number(cut[1]) ? cut : null;
10
+ }
11
+ async function readOwnerRules(bridge) {
12
+ let document = '';
13
+ for (let command = `cat ${OWNER_RULES}`; command !== undefined;) {
14
+ const read = await bridge.tool('shell', { command });
15
+ const text = read.content.map((content) => content.text).join('\n');
16
+ const lines = text.split('\n');
17
+ if (read.isError === true || (FAILED.test(lines[0] ?? '') && lines[1]?.startsWith('stderr: '))) {
18
+ const refusal = read.isError === true ? text : lines.slice(1).join('\n').replace(/^stderr: /gm, '');
19
+ if (UNREADABLE.test(refusal))
20
+ return '';
21
+ throw new Error(refusal);
22
+ }
23
+ const cut = cutOf(lines);
24
+ document += lines.slice(cut === null ? 0 : 1).join('\n');
25
+ command = cut?.[2];
26
+ }
27
+ return document;
28
+ }
9
29
  export async function instructions(bridge, base, copies) {
10
- const lines = [SOURCE_LINE, HOUSE_LINE, FILE_LINE, READ_LINE, REQUEST_LINE, ...(copies.length === 0 ? [] : [COPIES_LINE, ...copies])];
11
- return [lines.join('\n'), base, (await readHowWeWork(bridge)).replace(/\n$/, '')].filter((part) => part !== '').join('\n\n');
30
+ const lines = agentInstructions(HOUSE_REF, copies);
31
+ return [lines.join('\n'), base, (await readOwnerRules(bridge)).replace(/\n$/, '')].filter((part) => part !== '').join('\n\n');
12
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/kit-main.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- const [major = 0, minor = 0] = process.versions.node.split('.').map(Number);
3
- if (major < 24 || (major === 24 && minor < 14)) {
4
- process.stderr.write(`kit: the Kit needs Node 24.14.0 or newer; this is ${process.versions.node}\n`);
2
+ const [major = 0, minor = 0, patch = 0] = process.versions.node.split('.').map(Number);
3
+ if (major < 24 || (major === 24 && (minor < 22 || (minor === 22 && patch < 1)))) {
4
+ process.stderr.write(`kit: the Kit needs Node 24.22.1 or newer; this is ${process.versions.node}\n`);
5
5
  process.exitCode = 1;
6
6
  }
7
7
  else {
package/dist/login.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { createHash, randomBytes } from 'node:crypto';
3
- import { hostname } from 'node:os';
4
3
  import { parseArgs } from 'node:util';
5
4
  import { HouseRefusal, post, retrying } from './api.js';
6
5
  import { placeAppGit } from './app-git.js';
@@ -11,6 +10,7 @@ import { refusalOf } from './refusals.js';
11
10
  import { selectRelay } from './relay.js';
12
11
  import { startService } from './service.js';
13
12
  import { placeSkillSet } from './skills.js';
13
+ import { endUnserved } from './wire.js';
14
14
  export const DEFAULT_HOUSE = 'https://agents.house';
15
15
  const OPENER = process.platform === 'darwin' ? 'open' : 'xdg-open';
16
16
  // Polling every two seconds brings the User's confirmation to the waiting Kit moments after they give it.
@@ -26,6 +26,7 @@ export async function accepted(house, credential) {
26
26
  return true;
27
27
  if (checked.status === 401)
28
28
  return false;
29
+ endUnserved(checked.text);
29
30
  throw new Error(`House answered ${checked.status} when Kit checked this computer's own agent's connection: ${checked.text}`);
30
31
  }
31
32
  const USAGE = `usage: kit login [--house <origin>] [--environment <id>] [--rooms <ref>[,<ref>...] | --no-private]
@@ -91,13 +92,12 @@ export async function login(argv) {
91
92
  let named = environment;
92
93
  let exchanged;
93
94
  if (values.code !== undefined) {
94
- exchanged = await post(house, '/kit/token', { code: values.code, label: hostname() });
95
+ exchanged = await post(house, '/kit/token', { code: values.code });
95
96
  }
96
97
  else {
97
98
  held = ownAgent !== null && ownAgent.house === house && (await accepted(house, ownAgent.credential));
98
99
  const verifier = randomBytes(32).toString('base64url');
99
100
  const start = () => retrying(() => post(house, '/kit', {
100
- label: hostname(),
101
101
  ...(named === undefined
102
102
  ? { act: 'new_environment', ...(replaces === undefined ? {} : { replaces }) }
103
103
  : { act: 'existing_environment', environment_id: named }),
@@ -121,7 +121,7 @@ export async function login(argv) {
121
121
  open(started.start_url);
122
122
  exchanged = await retrying(() => post(house, '/kit/token', { login: started.login, code_verifier: verifier }), undefined, () => POLL_INTERVAL_MS);
123
123
  }
124
- await placeSkillSet();
124
+ await placeSkillSet(house);
125
125
  await writeCodexRule().catch((error) => process.stderr.write(`kit: ${error.message}\n`));
126
126
  const relayRecord = relay === undefined ? {} : { relay };
127
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,
package/dist/relay.js CHANGED
@@ -1,6 +1,11 @@
1
+ import { setGlobalProxyFromEnv } from 'node:http';
2
+ const PROXY = /^https?_proxy$/i;
3
+ const LOOPBACK = ['127.0.0.1', 'localhost', '::1'];
1
4
  let selected;
5
+ let restoreProxy;
2
6
  export function selectRelay(relay) {
3
7
  selected = relay === undefined || relay === '' ? null : relay;
8
+ proxyUnlessRelayed();
4
9
  }
5
10
  export function relayOrigin() {
6
11
  if (selected !== undefined)
@@ -8,6 +13,22 @@ export function relayOrigin() {
8
13
  const ambient = process.env.HOUSE_KIT_RELAY;
9
14
  return ambient === undefined || ambient === '' ? undefined : ambient;
10
15
  }
16
+ function unlessRelayed() {
17
+ if (relayOrigin() === undefined)
18
+ return { ...process.env };
19
+ return Object.fromEntries(Object.entries(process.env).filter(([name]) => !PROXY.test(name)));
20
+ }
21
+ export function proxyUnlessRelayed() {
22
+ restoreProxy?.();
23
+ restoreProxy = setGlobalProxyFromEnv(unlessRelayed());
24
+ }
25
+ export function childEnvironment() {
26
+ const environment = unlessRelayed();
27
+ if (!Object.keys(environment).some((name) => PROXY.test(name)))
28
+ return environment;
29
+ const bypassed = [environment.NO_PROXY ?? environment.no_proxy, ...LOOPBACK].filter((entry) => entry !== undefined && entry !== '').join(',');
30
+ return { ...environment, NO_PROXY: bypassed, no_proxy: bypassed };
31
+ }
11
32
  export function relayed(target) {
12
33
  const url = new URL(target);
13
34
  const relay = relayOrigin();