@agentshouse/kit 0.2.1-cd646adba3f64 → 0.2.1-cde47fd72fe59

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 (100) hide show
  1. package/README.md +30 -28
  2. package/dist/acp.js +46 -103
  3. package/dist/agents.js +142 -41
  4. package/dist/api.js +9 -5
  5. package/dist/app-git.js +10 -26
  6. package/dist/bootstrap.json +4 -4
  7. package/dist/bridge.js +14 -43
  8. package/dist/chatgpt.js +32 -0
  9. package/dist/clis.js +124 -69
  10. package/dist/codex-main.js +58 -0
  11. package/dist/conversations.js +385 -277
  12. package/dist/copies.js +195 -119
  13. package/dist/end.js +19 -4
  14. package/dist/enrol-main.js +7 -1
  15. package/dist/events.js +525 -0
  16. package/dist/files.js +22 -2
  17. package/dist/git.js +10 -8
  18. package/dist/home.js +96 -7
  19. package/dist/house-cli.js +291 -0
  20. package/dist/house-main.js +7 -159
  21. package/dist/inputs.js +37 -0
  22. package/dist/instructions.js +10 -32
  23. package/dist/kit-cli.js +68 -0
  24. package/dist/kit-main.js +8 -22
  25. package/dist/login.js +117 -106
  26. package/dist/logout.js +14 -16
  27. package/dist/parts.js +82 -0
  28. package/dist/placement.js +30 -0
  29. package/dist/relay.js +12 -2
  30. package/dist/resident.js +91 -33
  31. package/dist/scope.js +203 -0
  32. package/dist/service-main.js +9 -0
  33. package/dist/service.js +55 -0
  34. package/dist/sign-in.js +34 -28
  35. package/dist/skills.js +81 -12
  36. package/dist/stream.js +68 -17
  37. package/dist/transcription.js +3 -3
  38. package/dist/translate.js +2 -8
  39. package/dist/update.js +48 -29
  40. package/dist/wire.js +6 -0
  41. package/dist/work.js +346 -0
  42. package/how-we-work.md +13 -3
  43. package/package.json +3 -2
  44. package/skills/ask-matt/SKILL.md +17 -11
  45. package/skills/ask-matt/agents/openai.yaml +1 -0
  46. package/skills/code-review/SKILL.md +5 -5
  47. package/skills/codebase-design/DESIGN-IT-TWICE.md +1 -1
  48. package/skills/codebase-design/SKILL.md +0 -2
  49. package/skills/diagnosing-bugs/SKILL.md +2 -4
  50. package/skills/domain-modeling/{CONTEXT-FORMAT.md → GLOSSARY-FORMAT.md} +10 -10
  51. package/skills/domain-modeling/SKILL.md +12 -14
  52. package/skills/grill-me/SKILL.md +1 -2
  53. package/skills/grill-me/agents/openai.yaml +1 -0
  54. package/skills/grill-with-docs/SKILL.md +1 -2
  55. package/skills/grill-with-docs/agents/openai.yaml +1 -0
  56. package/skills/grilling/SKILL.md +2 -2
  57. package/skills/handoff/SKILL.md +2 -3
  58. package/skills/handoff/agents/openai.yaml +1 -0
  59. package/skills/implement/SKILL.md +5 -4
  60. package/skills/implement/agents/openai.yaml +1 -0
  61. package/skills/implement-spec/SKILL.md +40 -0
  62. package/skills/implement-spec/agents/openai.yaml +5 -0
  63. package/skills/improve-codebase-architecture/SKILL.md +6 -7
  64. package/skills/improve-codebase-architecture/agents/openai.yaml +1 -0
  65. package/skills/pr/CREDITS.md +3 -0
  66. package/skills/pr/LICENSE +21 -0
  67. package/skills/pr/SKILL.md +170 -0
  68. package/skills/pr/agents/openai.yaml +3 -0
  69. package/skills/prototype/SKILL.md +0 -2
  70. package/skills/research/SKILL.md +0 -2
  71. package/skills/retro/LICENSE +21 -0
  72. package/skills/retro/SKILL.md +44 -0
  73. package/skills/retro/agents/openai.yaml +5 -0
  74. package/skills/tdd/SKILL.md +2 -4
  75. package/skills/teach/SKILL.md +3 -4
  76. package/skills/teach/agents/openai.yaml +1 -0
  77. package/skills/to-questionnaire/SKILL.md +1 -2
  78. package/skills/to-questionnaire/agents/openai.yaml +1 -0
  79. package/skills/to-spec/SKILL.md +1 -2
  80. package/skills/to-spec/agents/openai.yaml +1 -0
  81. package/skills/to-tickets/SKILL.md +3 -4
  82. package/skills/to-tickets/agents/openai.yaml +1 -0
  83. package/skills/triage/AGENT-BRIEF.md +207 -0
  84. package/skills/triage/LICENSE +21 -0
  85. package/skills/triage/OUT-OF-SCOPE.md +105 -0
  86. package/skills/triage/SKILL.md +112 -0
  87. package/skills/triage/agents/openai.yaml +5 -0
  88. package/skills/wait-what/SKILL.md +2 -3
  89. package/skills/wait-what/agents/openai.yaml +1 -0
  90. package/skills/wayfinder/SKILL.md +5 -6
  91. package/skills/wayfinder/agents/openai.yaml +1 -0
  92. package/skills/wizard/LICENSE +21 -0
  93. package/skills/wizard/SKILL.md +44 -0
  94. package/skills/wizard/agents/openai.yaml +3 -0
  95. package/skills/wizard/template.sh +223 -0
  96. package/skills/writing-for-agents/SKILL.md +0 -2
  97. package/dist/blocks.js +0 -51
  98. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  99. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  100. /package/skills/{resolving-merge-conflicts → implement-spec}/LICENSE +0 -0
package/dist/api.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { setTimeout as delay } from 'node:timers/promises';
2
- import { end, SIGN_IN_AGAIN } from './end.js';
2
+ import { signInAgain, updateRequired } from './end.js';
3
3
  import { readEnrolment } from './home.js';
4
4
  import { retryable } from './refusals.js';
5
5
  import { relayed } from './relay.js';
6
+ import { KIT_WIRE, KIT_WIRE_HEADER, tooOld } from './wire.js';
6
7
  export class HouseRefusal extends Error {
7
8
  status;
8
9
  text;
@@ -19,7 +20,7 @@ export function retryDelay(attempt) {
19
20
  // Full jitter, doubling from one second to a 30-second cap, spreads the Kits one outage dropped and retries within half a minute of House's return.
20
21
  return Math.random() * Math.min(30_000, 1000 * 2 ** attempt);
21
22
  }
22
- export async function retrying(sent, signal) {
23
+ export async function retrying(sent, signal, pause = retryDelay) {
23
24
  for (let attempt = 0;; attempt++) {
24
25
  try {
25
26
  return await sent();
@@ -28,8 +29,8 @@ export async function retrying(sent, signal) {
28
29
  if (!(error instanceof HouseRefusal) || retryable(error.text) !== true)
29
30
  throw error;
30
31
  }
31
- // A request House refused as retryable is sent again on the jittered curve.
32
- await delay(retryDelay(attempt), undefined, { signal });
32
+ // A request House refused as retryable is sent again after the caller's pause, the jittered curve unless it names its own.
33
+ await delay(pause(attempt), undefined, { signal });
33
34
  }
34
35
  }
35
36
  export async function post(house, path, body, credential, signal) {
@@ -37,12 +38,15 @@ export async function post(house, path, body, credential, signal) {
37
38
  method: 'POST',
38
39
  headers: {
39
40
  'content-type': 'application/json',
41
+ [KIT_WIRE_HEADER]: String(KIT_WIRE),
40
42
  ...(credential === undefined ? {} : { authorization: `Bearer ${credential}` }),
41
43
  },
42
44
  body: JSON.stringify(body),
43
45
  signal,
44
46
  });
45
47
  const text = await answer.text();
48
+ if (!answer.ok && tooOld(text))
49
+ updateRequired();
46
50
  if (!answer.ok)
47
51
  throw new HouseRefusal(path, answer.status, text);
48
52
  return JSON.parse(text);
@@ -54,7 +58,7 @@ async function enrolledPost(path, body, signal) {
54
58
  }
55
59
  catch (error) {
56
60
  if (error instanceof HouseRefusal && error.status === 401)
57
- end(SIGN_IN_AGAIN);
61
+ await signInAgain(credential);
58
62
  throw error;
59
63
  }
60
64
  }
package/dist/app-git.js CHANGED
@@ -1,15 +1,9 @@
1
- import { extname, resolve } from 'node:path';
2
- import { fileURLToPath } from 'node:url';
3
1
  import { run } from './git.js';
4
2
  import { kitHome, readOwnAgent } from './home.js';
5
- import { quoted } from './shell.js';
3
+ const HELPER = '!kit git-credential';
6
4
  function section(house) {
7
5
  return `credential.${new URL('/app/', house).href}`;
8
6
  }
9
- function helper() {
10
- const main = fileURLToPath(new URL(`./kit-main${extname(fileURLToPath(import.meta.url))}`, import.meta.url));
11
- return `!HOUSE_KIT_HOME=${quoted(resolve(kitHome()))} ${quoted(process.execPath)} ${quoted(main)} git-credential`;
12
- }
13
7
  async function configured(args) {
14
8
  try {
15
9
  return await run(kitHome(), ['config', '--global', ...args]);
@@ -20,30 +14,18 @@ async function configured(args) {
20
14
  throw error;
21
15
  }
22
16
  }
23
- async function forget(house) {
24
- await configured(['--remove-section', section(house)]);
25
- }
26
- async function register(house) {
27
- await forget(house);
28
- for (const value of ['', helper()]) {
29
- const ran = await configured(['--add', `${section(house)}.helper`, value]);
17
+ export async function placeAppGit(ownAgent) {
18
+ if (ownAgent === null)
19
+ return;
20
+ await configured(['--remove-section', section(ownAgent.house)]);
21
+ for (const value of ['', HELPER]) {
22
+ const ran = await configured(['--add', `${section(ownAgent.house)}.helper`, value]);
30
23
  if (ran === null)
31
24
  return;
32
25
  if (ran.status !== 0)
33
26
  throw new Error(`git config failed: ${ran.stderr.trim()}`);
34
27
  }
35
28
  }
36
- export async function placeAppGit(before, after) {
37
- if (before !== null && before.house !== after?.house)
38
- await forget(before.house);
39
- if (after !== null)
40
- await register(after.house);
41
- }
42
- export async function forgetAppGit() {
43
- const ownAgent = await readOwnAgent();
44
- if (ownAgent !== null)
45
- await forget(ownAgent.house);
46
- }
47
29
  export async function answerGitCredential(operation) {
48
30
  let asked = '';
49
31
  for await (const chunk of process.stdin)
@@ -55,8 +37,10 @@ export async function answerGitCredential(operation) {
55
37
  .filter((line) => line.includes('='))
56
38
  .map((line) => [line.slice(0, line.indexOf('=')), line.slice(line.indexOf('=') + 1)]));
57
39
  const ownAgent = await readOwnAgent();
58
- if (ownAgent === null)
40
+ if (ownAgent === null) {
41
+ process.stderr.write(`kit: Kit home ${kitHome()} is not signed in; run kit login\n`);
59
42
  return;
43
+ }
60
44
  const house = new URL(ownAgent.house);
61
45
  if (attributes.get('protocol') !== house.protocol.slice(0, -1) || attributes.get('host') !== house.host)
62
46
  return;
@@ -1,23 +1,23 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "version": "0.2.1-cd646adba3f64",
3
+ "version": "0.2.1-cde47fd72fe59",
4
4
  "hosts": {
5
5
  "macos": {
6
6
  "asset": {
7
7
  "name": "connect-linux.sh",
8
- "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-cd646adba3f64/connect-linux.sh"
8
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-cde47fd72fe59/connect-linux.sh"
9
9
  }
10
10
  },
11
11
  "linux": {
12
12
  "asset": {
13
13
  "name": "connect-linux.sh",
14
- "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-cd646adba3f64/connect-linux.sh"
14
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-cde47fd72fe59/connect-linux.sh"
15
15
  }
16
16
  },
17
17
  "windows": {
18
18
  "asset": {
19
19
  "name": "connect-windows.ps1",
20
- "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-cd646adba3f64/connect-windows.ps1"
20
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-cde47fd72fe59/connect-windows.ps1"
21
21
  }
22
22
  }
23
23
  }
package/dist/bridge.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { once } from 'node:events';
3
3
  import { setTimeout as delay } from 'node:timers/promises';
4
- import { mkdir } from 'node:fs/promises';
5
4
  import { createServer, request as httpRequest } from 'node:http';
6
5
  import { request as httpsRequest } from 'node:https';
7
6
  import { delimiter, dirname, join } from 'node:path';
@@ -9,13 +8,12 @@ import { parse } from 'yaml';
9
8
  import { finalRefusal, HouseRefusal, retryDelay, retrying } from './api.js';
10
9
  import { loginPath } from './clis.js';
11
10
  import { changedPaths, } from './copies.js';
12
- import { kitHome, readEnrolment } from './home.js';
11
+ import { kitHome, readEnrolment, socketDirectory } from './home.js';
13
12
  import { callHouse, mcpBody, rpc, UNREACHABLE } from './mcp.js';
14
- import { operationId, uuidOf } from './operation.js';
13
+ import { operationId } from './operation.js';
15
14
  import { refusalOf, retryable } from './refusals.js';
16
15
  import { relayed } from './relay.js';
17
16
  const MCP_REQUEST_BYTES = 4 * 1024 * 1024;
18
- const ATTACHMENTS = '/kit/attachments/upload';
19
17
  export async function received(request) {
20
18
  let text = '';
21
19
  request.setEncoding('utf8');
@@ -68,11 +66,6 @@ function resendAfter(forwarded, attempt) {
68
66
  const seconds = pendingSeconds(forwarded);
69
67
  return seconds === null ? null : seconds * 1000;
70
68
  }
71
- function versioned(conversationId, text) {
72
- const file = JSON.parse(text);
73
- const identity = JSON.stringify([conversationId, file.room_ref ?? null, file.name, file.sha256]);
74
- return JSON.stringify({ ...file, version: uuidOf(createHash('sha256').update(identity).digest(), 8) });
75
- }
76
69
  function editAnswer(forwarded) {
77
70
  const answer = answered(forwarded);
78
71
  if ('refused' in answer)
@@ -88,7 +81,7 @@ function editAnswer(forwarded) {
88
81
  export async function openBridge(house, copies, conversationId, signal) {
89
82
  const path = [join(kitHome(), 'shim'), dirname(process.execPath), await loginPath()].join(delimiter);
90
83
  const origin = (await readEnrolment()).house;
91
- const { credential } = await house.post(`/kit/conversations/${conversationId}/credential`, {}, signal);
84
+ const { credential } = await retrying(() => house.post(`/kit/conversations/${conversationId}/credential`, {}, signal), signal);
92
85
  const closed = new AbortController();
93
86
  const sent = (message, operation) => callHouse(origin, credential, message, operation, closed.signal);
94
87
  const send = async (message, operation) => {
@@ -103,7 +96,7 @@ export async function openBridge(house, copies, conversationId, signal) {
103
96
  const after = resendAfter(forwarded, attempt);
104
97
  if (after === null)
105
98
  return forwarded;
106
- // A write House did not settle is sent again on the jittered curve, or after the wait House names for a pending one.
99
+ // A Tool call House did not settle is sent again on the jittered curve, or after the wait House names for a pending one.
107
100
  await delay(after, undefined, { signal: closed.signal });
108
101
  }
109
102
  };
@@ -141,32 +134,9 @@ export async function openBridge(house, copies, conversationId, signal) {
141
134
  const held = JSON.parse(text).transfer;
142
135
  return { url: held.url, operation: held.operation, bytes: bytes.length, sha256 };
143
136
  };
144
- let mutations = null;
145
- const writes = async (tool) => {
146
- mutations ??= house
147
- .post('/kit/tools/mutations', {}, closed.signal)
148
- .then(({ tools }) => new Set(tools.filter((listed) => listed.writes).map((listed) => listed.name)));
149
- try {
150
- return (await mutations).has(tool);
151
- }
152
- catch (error) {
153
- mutations = null;
154
- throw error;
155
- }
156
- };
157
- const forward = async (path, text) => {
158
- if (path === '/') {
159
- const message = JSON.parse(text);
160
- const tool = message.method === 'tools/call' ? String(message.params.name) : null;
161
- return send(message, tool !== null && (await writes(tool)) ? operationId() : null);
162
- }
163
- const answer = await fetch(relayed(new URL(path, origin)), {
164
- method: 'POST',
165
- headers: { 'content-type': 'application/json', authorization: `Bearer ${credential}` },
166
- body: versioned(conversationId, text),
167
- signal: closed.signal,
168
- });
169
- return { status: answer.status, text: await answer.text() };
137
+ const forward = (text) => {
138
+ const message = JSON.parse(text);
139
+ return send(message, message.method === 'tools/call' ? operationId() : null);
170
140
  };
171
141
  const caller = {
172
142
  key: `conversation:${conversationId}`,
@@ -188,15 +158,15 @@ export async function openBridge(house, copies, conversationId, signal) {
188
158
  const { cwd, commit } = JSON.parse(text);
189
159
  return { status: 200, text: JSON.stringify(await copies.push(cwd, commit, caller)) };
190
160
  }
161
+ if (path === '/git/fetch')
162
+ return { status: 200, text: JSON.stringify(await copies.fetch()) };
191
163
  if (path === '/git/observed') {
192
164
  await copies.observed(JSON.parse(text));
193
165
  return { status: 200, text: '{}' };
194
166
  }
195
- return path === '/' || path === ATTACHMENTS ? forward(path, text) : { status: 404, text: '' };
167
+ return path === '/' ? forward(text) : { status: 404, text: '' };
196
168
  };
197
- const sockets = join(kitHome(), 'bridges');
198
- await mkdir(sockets, { recursive: true, mode: 0o700 });
199
- const socketPath = join(sockets, `${randomUUID()}.sock`);
169
+ const socketPath = join(socketDirectory(), `${randomUUID()}.sock`);
200
170
  const bridge = createServer(async (request, response) => {
201
171
  try {
202
172
  const path = request.url ?? '';
@@ -212,7 +182,8 @@ export async function openBridge(house, copies, conversationId, signal) {
212
182
  response.writeHead(error.status, { 'content-type': 'application/json' }).end(error.text);
213
183
  return;
214
184
  }
215
- response.writeHead(502).end(request.url === '/git/push' && error instanceof Error ? error.message : UNREACHABLE);
185
+ const verb = request.url === '/git/push' || request.url === '/git/fetch';
186
+ response.writeHead(502).end(verb && error instanceof Error ? error.message : UNREACHABLE);
216
187
  }
217
188
  });
218
189
  bridge.listen(socketPath);
@@ -239,7 +210,7 @@ export async function openBridge(house, copies, conversationId, signal) {
239
210
  GIT_CONFIG_VALUE_0: app,
240
211
  },
241
212
  tool: async (name, args) => {
242
- const forwarded = await forward('/', JSON.stringify(call(name, args)));
213
+ const forwarded = await forward(JSON.stringify(call(name, args)));
243
214
  if (forwarded.status !== 200)
244
215
  throw new Error(`House refused ${name} with ${forwarded.status}: ${forwarded.text}`);
245
216
  const answer = JSON.parse(forwarded.text);
@@ -0,0 +1,32 @@
1
+ import { parseArgs } from 'node:util';
2
+ import { placeAppGit } from './app-git.js';
3
+ import { kitHome, readOwnAgent, writeOwnAgent } from './home.js';
4
+ import { accepted, DEFAULT_HOUSE } from './login.js';
5
+ const KEY_SOURCE = 'New key for a script on Connections';
6
+ const USAGE = `usage: kit chatgpt --key <key> [--house <origin>]
7
+ --key <key> a key from ${KEY_SOURCE}
8
+ --house <origin> the House of the key, ${DEFAULT_HOUSE} unless one is stored
9
+ `;
10
+ export async function chatgpt(argv) {
11
+ const { values } = parseArgs({
12
+ args: argv,
13
+ options: { key: { type: 'string' }, house: { type: 'string' }, help: { type: 'boolean', default: false } },
14
+ });
15
+ if (values.help) {
16
+ process.stdout.write(USAGE);
17
+ return;
18
+ }
19
+ if (values.key === undefined)
20
+ throw new Error(`pass --key with a key from ${KEY_SOURCE}`);
21
+ const before = await readOwnAgent();
22
+ const house = values.house ?? before?.house ?? DEFAULT_HOUSE;
23
+ if (!(await accepted(house, values.key)))
24
+ throw new Error(`${house} does not accept this key; pass one from ${KEY_SOURCE}`);
25
+ const relay = process.env.HOUSE_KIT_RELAY || undefined;
26
+ await writeOwnAgent({ house, ...(relay === undefined ? {} : { relay }), credential: values.key });
27
+ await placeAppGit(await readOwnAgent()).catch((error) => process.stderr.write(`kit: ${error.message}\n`));
28
+ if (before !== null && before.credential !== values.key) {
29
+ process.stdout.write('This replaced the key this Kit home held; give each session its own HOUSE_KIT_HOME to keep its own key.\n');
30
+ }
31
+ process.stdout.write(`house now calls ${house} as you, with the key kept in ${kitHome()}.\n`);
32
+ }
package/dist/clis.js CHANGED
@@ -1,57 +1,94 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { readFileSync } from 'node:fs';
3
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { constants, readFileSync } from 'node:fs';
4
+ import { access, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
4
5
  import { homedir, userInfo } from 'node:os';
5
- import { join } from 'node:path';
6
+ import { delimiter, dirname, isAbsolute, join } from 'node:path';
6
7
  import { kitHome } from './home.js';
7
- const RUNNING_JOB = new Set(['running', 'paused']);
8
- async function allowHouseInCodex() {
8
+ const HOUSE_EXECUTABLES = /^host_executable\(name = "house", paths = (\[.*\])\)$/m;
9
+ function houseRule(houses) {
10
+ return `prefix_rule(pattern = ["house"], decision = "allow")\nhost_executable(name = "house", paths = ${JSON.stringify(houses)})\n`;
11
+ }
12
+ const RAW_SUBTYPES = [
13
+ 'background_tasks_changed',
14
+ 'task_started',
15
+ 'task_updated',
16
+ 'task_notification',
17
+ 'session_state_changed',
18
+ 'hook_started',
19
+ 'hook_response',
20
+ ];
21
+ async function executable(path) {
22
+ try {
23
+ await access(path, constants.X_OK);
24
+ return true;
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ async function listedHouses(file) {
31
+ let text;
32
+ try {
33
+ text = await readFile(file, 'utf8');
34
+ }
35
+ catch (error) {
36
+ if (error.code === 'ENOENT')
37
+ return [];
38
+ throw error;
39
+ }
40
+ try {
41
+ const listed = JSON.parse(HOUSE_EXECUTABLES.exec(text)?.[1] ?? '[]');
42
+ return Array.isArray(listed) ? listed.filter((house) => typeof house === 'string') : [];
43
+ }
44
+ catch {
45
+ return [];
46
+ }
47
+ }
48
+ export async function allowHouseInCodex(path) {
49
+ const candidates = [...new Set(path.split(delimiter).filter((directory) => isAbsolute(directory)))].map((directory) => join(directory, 'house'));
9
50
  const rules = join(process.env.CODEX_HOME || join(homedir(), '.codex'), 'rules');
51
+ const file = join(rules, 'house.rules');
52
+ const houses = [];
53
+ for (const house of [...(await listedHouses(file)), ...candidates]) {
54
+ if (!houses.includes(house) && (await executable(house)))
55
+ houses.push(house);
56
+ }
10
57
  await mkdir(rules, { recursive: true });
11
- await writeFile(join(rules, 'house.rules'), 'prefix_rule(pattern = ["house"], decision = "allow")\n');
58
+ const temporary = `${file}.${randomUUID()}`;
59
+ await writeFile(temporary, houseRule(houses));
60
+ await rename(temporary, file);
12
61
  }
13
62
  async function nothing() { }
14
- function threadStatus({ method, params }) {
15
- if (method !== 'session/update' || params?.update?.sessionUpdate !== 'session_info_update')
16
- return undefined;
17
- return params.update._meta?.codex?.threadStatus?.type;
18
- }
19
- function asyncTask({ method, params }) {
20
- const update = params?.update;
21
- if (method !== 'session/update' || update?.asyncTaskId === undefined)
22
- return null;
23
- if (update.sessionUpdate === 'async_task_spawned')
24
- return { id: update.asyncTaskId, running: true };
25
- if (update.sessionUpdate === 'async_task_state_update' && !RUNNING_JOB.has(update.state ?? '')) {
26
- return { id: update.asyncTaskId, running: false };
27
- }
28
- return null;
63
+ function answered(error) {
64
+ return typeof error === 'object' && error !== null ? error : {};
29
65
  }
30
- function airPhase({ _meta }) {
31
- const phase = _meta?.jetbrains?.air?.phase;
32
- if (phase === 'commentary')
33
- return 'note';
34
- if (phase === 'final_answer')
35
- return 'answer';
36
- return null;
66
+ function bridgeInClaudeSandbox(bridge) {
67
+ return process.platform === 'darwin' ? { allowUnixSockets: [bridge] } : { allowAllUnixSockets: true };
37
68
  }
38
69
  export const CLIS = {
39
70
  'codex-acp': {
40
71
  bin: 'codex',
41
72
  minimum: '0.159.1',
42
73
  install: 'curl -fsSL https://chatgpt.com/codex/install.sh | sh',
43
- adapter: { package: '@agentclientprotocol/codex-acp', bin: 'codex-acp', executable: 'CODEX_PATH', env: {} },
74
+ adapter: {
75
+ package: '@agentclientprotocol/codex-acp',
76
+ bin: 'codex-acp',
77
+ executable: 'CODEX_PATH',
78
+ env: { CODEX_CONFIG: JSON.stringify({ features: { shell_zsh_fork: true } }) },
79
+ },
44
80
  args: [],
45
81
  fullAccess: 'agent-full-access',
46
- sessionMeta: {},
82
+ sessionMeta: () => ({}),
83
+ unknownSession: (error) => {
84
+ const { code, data } = answered(error);
85
+ return code === -32603 && typeof data?.details === 'string' && data.details.startsWith('no rollout found');
86
+ },
87
+ renames: true,
47
88
  allowHouse: allowHouseInCodex,
48
89
  signedIn: { command: ['login', 'status'] },
49
90
  login: { args: ['login', '--device-auth'], code: 'show' },
50
- queues: false,
51
- turnStarted: (notice) => threadStatus(notice) === 'active',
52
- turnEnded: (notice) => threadStatus(notice) === 'idle',
53
- job: asyncTask,
54
- phase: airPhase,
91
+ air: ['asyncTasks', 'nativeSubagentSessions'],
55
92
  },
56
93
  'claude-agent-acp': {
57
94
  bin: 'claude',
@@ -65,40 +102,38 @@ export const CLIS = {
65
102
  },
66
103
  args: [],
67
104
  fullAccess: 'bypassPermissions',
68
- sessionMeta: { claudeCode: { options: { allowedTools: ['Bash(house:*)'] } } },
105
+ sessionMeta: (bridge, title) => ({
106
+ claudeCode: {
107
+ options: {
108
+ allowedTools: ['Bash(house:*)'],
109
+ includeHookEvents: true,
110
+ title,
111
+ settings: { sandbox: { network: bridgeInClaudeSandbox(bridge) } },
112
+ },
113
+ emitRawSDKMessages: RAW_SUBTYPES.map((subtype) => ({ type: 'system', subtype })),
114
+ },
115
+ }),
116
+ unknownSession: (error) => answered(error).code === -32002,
117
+ renames: false,
69
118
  allowHouse: nothing,
70
119
  signedIn: { command: ['auth', 'status'] },
71
120
  login: { args: ['auth', 'login', '--claudeai'], code: 'collect', rejected: 'Invalid code' },
72
- queues: false,
73
- turnStarted: () => false,
74
- turnEnded: ({ method, params }) => method === 'session/update' &&
75
- params?.update?.sessionUpdate === 'usage_update' &&
76
- params.update._meta?.['_claude/origin'] !== undefined,
77
- job: asyncTask,
78
- phase: () => null,
121
+ air: ['asyncTasks', 'sessionFailure'],
79
122
  },
80
123
  'grok-build': {
81
124
  bin: 'grok',
82
125
  minimum: '1.0.46',
83
126
  install: 'curl -fsSL https://x.ai/cli/install.sh | bash',
84
127
  adapter: null,
85
- args: ['agent', '--always-approve', '--no-leader', 'stdio'],
128
+ args: ['--sandbox', 'off', 'agent', '--always-approve', '--no-leader', 'stdio'],
86
129
  fullAccess: null,
87
- sessionMeta: {},
130
+ sessionMeta: () => ({}),
131
+ unknownSession: (error) => answered(error).data?.code === 'FS_NOT_FOUND',
132
+ renames: false,
88
133
  allowHouse: nothing,
89
134
  signedIn: { initializeMeta: 'defaultAuthMethodId' },
90
135
  login: { args: ['login', '--device-auth'], code: 'show' },
91
- queues: true,
92
- turnStarted: ({ method, params }) => method === '_x.ai/queue/changed' && params?.runningPromptId !== undefined,
93
- turnEnded: ({ method, params }) => method === '_x.ai/session_notification' && params?.update?.sessionUpdate === 'turn_completed',
94
- job: ({ method, params }) => {
95
- if (method === '_x.ai/task_backgrounded')
96
- return { id: params.update.task_id, running: true };
97
- if (method === '_x.ai/task_completed')
98
- return { id: params.update.task_snapshot.task_id, running: false };
99
- return null;
100
- },
101
- phase: () => null,
136
+ air: ['asyncTasks'],
102
137
  },
103
138
  };
104
139
  const KIT_PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
@@ -135,8 +170,22 @@ export function run(command, args, timeoutMs, detached = false, signal) {
135
170
  timeout: timeoutMs,
136
171
  env: withoutProxy(),
137
172
  detached,
138
- signal,
173
+ signal: detached ? undefined : signal,
139
174
  });
175
+ const stop = () => {
176
+ try {
177
+ process.kill(-child.pid, 'SIGKILL');
178
+ }
179
+ catch { }
180
+ };
181
+ if (detached && signal?.aborted)
182
+ stop();
183
+ if (detached)
184
+ signal?.addEventListener('abort', stop, { once: true });
185
+ const ended = (ran) => {
186
+ signal?.removeEventListener('abort', stop);
187
+ resolve(ran);
188
+ };
140
189
  let stdout = '';
141
190
  let output = '';
142
191
  child.stdout.setEncoding('utf8').on('data', (chunk) => {
@@ -146,25 +195,25 @@ export function run(command, args, timeoutMs, detached = false, signal) {
146
195
  child.stderr.setEncoding('utf8').on('data', (chunk) => {
147
196
  output += chunk;
148
197
  });
149
- child.on('error', (error) => resolve({ status: null, stdout, output: error.message }));
150
- child.on('close', (status) => resolve({ status, stdout, output }));
198
+ child.on('error', (error) => ended({ status: null, stdout, output: error.message }));
199
+ child.on('close', (status) => ended({ status, stdout, output }));
151
200
  });
152
201
  }
153
202
  export function failureOf(ran, program) {
154
203
  return ran.output.trim().slice(-CAUSE_CHARACTERS) || `${program} exited with ${ran.status ?? 'a signal'}`;
155
204
  }
156
- export async function installAdapter(kind) {
205
+ export async function installAdapter(kind, signal) {
157
206
  const adapter = CLIS[kind].adapter;
158
207
  if (adapter === null)
159
208
  return null;
160
209
  const release = KIT_PACKAGE.peerDependencies[adapter.package];
161
210
  if ((await installedAdapter(kind)) === release)
162
211
  return null;
163
- const ran = await run('npm', ['install', '--prefix', prefix(kind), '--no-save', '--no-audit', '--no-fund', '--omit=optional', `${adapter.package}@${release}`], INSTALL_MS);
212
+ const ran = await run('npm', ['install', '--prefix', prefix(kind), '--cache', join(kitHome(), 'npm-cache'), '--no-save', '--no-audit', '--no-fund', '--omit=optional', `${adapter.package}@${release}`], INSTALL_MS, true, signal);
164
213
  return ran.status === 0 ? null : failureOf(ran, 'npm');
165
214
  }
166
- export async function installCli(kind) {
167
- const ran = await run('bash', ['-o', 'pipefail', '-c', CLIS[kind].install], INSTALL_MS, true);
215
+ export async function installCli(kind, signal) {
216
+ const ran = await run('bash', ['-o', 'pipefail', '-c', CLIS[kind].install], INSTALL_MS, true, signal);
168
217
  return ran.status === 0 ? null : failureOf(ran, 'the install');
169
218
  }
170
219
  function loginShell() {
@@ -173,11 +222,11 @@ function loginShell() {
173
222
  const FOUND = 'house-kit-cli ';
174
223
  const LOOKUP = `found=$(command -v "$1") || exit 0; case $found in /*) ;; *) found=$(pwd -P)/$found ;; esac; printf "${FOUND}%s\\n" "$found"`;
175
224
  const USER_BIN = 'case ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) PATH="$PATH:$HOME/.local/bin" ;; esac; export PATH';
176
- function inLoginShell(script, argument = '') {
177
- return run(loginShell(), ['-l', '-i', '-c', `exec /bin/sh -c '${USER_BIN}; ${script}' sh ${argument}`], READ_MS, true);
225
+ function inLoginShell(script, argument = '', signal) {
226
+ return run(loginShell(), ['-l', '-i', '-c', `exec /bin/sh -c '${USER_BIN}; ${script}' sh ${argument}`], READ_MS, true, signal);
178
227
  }
179
- export async function locate(kind) {
180
- const ran = await inLoginShell(LOOKUP, CLIS[kind].bin);
228
+ export async function locate(kind, signal) {
229
+ const ran = await inLoginShell(LOOKUP, CLIS[kind].bin, signal);
181
230
  return ran.stdout.split('\n').findLast((line) => line.startsWith(FOUND))?.slice(FOUND.length) ?? null;
182
231
  }
183
232
  export async function loginPath() {
@@ -185,8 +234,14 @@ export async function loginPath() {
185
234
  const ran = await inLoginShell(`printf "${marker}%s\\n" "$PATH"`);
186
235
  return ran.stdout.split('\n').findLast((line) => line.startsWith(marker)).slice(marker.length);
187
236
  }
188
- export async function versionOf(path) {
189
- return VERSION.exec((await run(path, ['--version'], READ_MS)).stdout)?.[0] ?? null;
237
+ export async function writeCodexRule() {
238
+ const shell = await loginPath().catch(() => '');
239
+ await allowHouseInCodex([dirname(process.execPath), process.env.PATH ?? '', shell].join(delimiter)).catch((error) => {
240
+ throw new Error(`Codex's rule for house could not be written: ${error.message}`);
241
+ });
242
+ }
243
+ export async function versionOf(path, signal) {
244
+ return VERSION.exec((await run(path, ['--version'], READ_MS, false, signal)).stdout)?.[0] ?? null;
190
245
  }
191
246
  function numbers(version) {
192
247
  return version.split(/[.-]/, 3).map(Number);
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
3
+ import { connect } from 'node:net';
4
+ const KIT_ANSWER = '"house-kit-';
5
+ const SYNC = 'house-kit/sync';
6
+ function lines(each) {
7
+ let buffer = '';
8
+ return (chunk) => {
9
+ buffer += chunk;
10
+ for (let index = buffer.indexOf('\n'); index >= 0; index = buffer.indexOf('\n')) {
11
+ each(buffer.slice(0, index + 1));
12
+ buffer = buffer.slice(index + 1);
13
+ }
14
+ };
15
+ }
16
+ function answersKit(line) {
17
+ if (!line.includes(KIT_ANSWER))
18
+ return false;
19
+ try {
20
+ const message = JSON.parse(line);
21
+ return message.method === undefined && typeof message.id === 'string' && message.id.startsWith(KIT_ANSWER.slice(1));
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ const args = process.argv.slice(2);
28
+ const serving = args[0] === 'app-server';
29
+ if (process.env.HOUSE_KIT_SANDBOX === '1' && serving)
30
+ args.push('--disable', 'memories');
31
+ const codex = spawn(process.env.HOUSE_KIT_CODEX, args, { stdio: ['pipe', 'pipe', 'inherit'] });
32
+ const stream = serving ? connect(process.env.HOUSE_KIT_CODEX_STREAM) : null;
33
+ stream?.on('error', () => undefined);
34
+ process.stdin.setEncoding('utf8').on('data', lines((line) => codex.stdin.write(line)));
35
+ process.stdin.on('end', () => codex.stdin.end());
36
+ stream?.setEncoding('utf8').on('data', lines((line) => {
37
+ const message = JSON.parse(line);
38
+ if (message.method === SYNC)
39
+ stream.write(`${JSON.stringify({ jsonrpc: '2.0', id: message.id, result: {} })}\n`);
40
+ else
41
+ codex.stdin.write(line);
42
+ }));
43
+ codex.stdout.setEncoding('utf8').on('data', lines((line) => {
44
+ stream?.write(line);
45
+ if (answersKit(line) || process.stdout.write(line))
46
+ return;
47
+ codex.stdout.pause();
48
+ process.stdout.once('drain', () => codex.stdout.resume());
49
+ }));
50
+ codex.stdin.on('error', () => undefined);
51
+ for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP'])
52
+ process.on(signal, () => codex.kill(signal));
53
+ codex.on('exit', (code, signal) => {
54
+ if (signal === null)
55
+ process.exit(code ?? 1);
56
+ process.removeAllListeners(signal);
57
+ process.kill(process.pid, signal);
58
+ });