@agentshouse/kit 0.2.1-c15804a0fa28d → 0.2.1-c2997cbe74597

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 (101) hide show
  1. package/README.md +27 -26
  2. package/dist/acp.js +16 -93
  3. package/dist/agents.js +138 -42
  4. package/dist/api.js +34 -2
  5. package/dist/app-git.js +64 -0
  6. package/dist/bootstrap.json +4 -4
  7. package/dist/bridge.js +33 -51
  8. package/dist/chatgpt.js +32 -0
  9. package/dist/clis.js +128 -65
  10. package/dist/codex-main.js +58 -0
  11. package/dist/conversations.js +298 -237
  12. package/dist/copies.js +148 -115
  13. package/dist/end.js +22 -0
  14. package/dist/enrol-main.js +7 -1
  15. package/dist/events.js +521 -0
  16. package/dist/files.js +21 -1
  17. package/dist/git.js +11 -8
  18. package/dist/home.js +81 -6
  19. package/dist/house-cli.js +291 -0
  20. package/dist/house-main.js +7 -159
  21. package/dist/instructions.js +9 -32
  22. package/dist/kit-cli.js +68 -0
  23. package/dist/kit-main.js +8 -19
  24. package/dist/login.js +109 -103
  25. package/dist/logout.js +16 -13
  26. package/dist/parts.js +82 -0
  27. package/dist/placement.js +48 -0
  28. package/dist/refusals.js +12 -0
  29. package/dist/relay.js +12 -2
  30. package/dist/resident.js +38 -27
  31. package/dist/scope.js +203 -0
  32. package/dist/secret-input.js +4 -1
  33. package/dist/service-main.js +9 -0
  34. package/dist/service.js +33 -0
  35. package/dist/shell.js +3 -0
  36. package/dist/sign-in.js +34 -9
  37. package/dist/skills.js +81 -12
  38. package/dist/stream.js +87 -11
  39. package/dist/transcription.js +57 -9
  40. package/dist/update.js +30 -6
  41. package/dist/wire.js +6 -0
  42. package/dist/work.js +348 -0
  43. package/how-we-work.md +13 -3
  44. package/package.json +3 -2
  45. package/skills/ask-matt/SKILL.md +17 -11
  46. package/skills/ask-matt/agents/openai.yaml +1 -0
  47. package/skills/code-review/SKILL.md +5 -5
  48. package/skills/codebase-design/DESIGN-IT-TWICE.md +1 -1
  49. package/skills/codebase-design/SKILL.md +0 -2
  50. package/skills/diagnosing-bugs/SKILL.md +2 -4
  51. package/skills/domain-modeling/{CONTEXT-FORMAT.md → GLOSSARY-FORMAT.md} +10 -10
  52. package/skills/domain-modeling/SKILL.md +12 -14
  53. package/skills/grill-me/SKILL.md +1 -2
  54. package/skills/grill-me/agents/openai.yaml +1 -0
  55. package/skills/grill-with-docs/SKILL.md +1 -2
  56. package/skills/grill-with-docs/agents/openai.yaml +1 -0
  57. package/skills/grilling/SKILL.md +2 -2
  58. package/skills/handoff/SKILL.md +2 -3
  59. package/skills/handoff/agents/openai.yaml +1 -0
  60. package/skills/implement/SKILL.md +5 -4
  61. package/skills/implement/agents/openai.yaml +1 -0
  62. package/skills/implement-spec/SKILL.md +40 -0
  63. package/skills/implement-spec/agents/openai.yaml +5 -0
  64. package/skills/improve-codebase-architecture/SKILL.md +6 -7
  65. package/skills/improve-codebase-architecture/agents/openai.yaml +1 -0
  66. package/skills/pr/CREDITS.md +3 -0
  67. package/skills/pr/LICENSE +21 -0
  68. package/skills/pr/SKILL.md +170 -0
  69. package/skills/pr/agents/openai.yaml +3 -0
  70. package/skills/prototype/SKILL.md +0 -2
  71. package/skills/research/SKILL.md +0 -2
  72. package/skills/retro/LICENSE +21 -0
  73. package/skills/retro/SKILL.md +44 -0
  74. package/skills/retro/agents/openai.yaml +5 -0
  75. package/skills/tdd/SKILL.md +2 -4
  76. package/skills/teach/SKILL.md +3 -4
  77. package/skills/teach/agents/openai.yaml +1 -0
  78. package/skills/to-questionnaire/SKILL.md +1 -2
  79. package/skills/to-questionnaire/agents/openai.yaml +1 -0
  80. package/skills/to-spec/SKILL.md +1 -2
  81. package/skills/to-spec/agents/openai.yaml +1 -0
  82. package/skills/to-tickets/SKILL.md +3 -4
  83. package/skills/to-tickets/agents/openai.yaml +1 -0
  84. package/skills/triage/AGENT-BRIEF.md +207 -0
  85. package/skills/triage/LICENSE +21 -0
  86. package/skills/triage/OUT-OF-SCOPE.md +105 -0
  87. package/skills/triage/SKILL.md +112 -0
  88. package/skills/triage/agents/openai.yaml +5 -0
  89. package/skills/wait-what/SKILL.md +2 -3
  90. package/skills/wait-what/agents/openai.yaml +1 -0
  91. package/skills/wayfinder/SKILL.md +5 -6
  92. package/skills/wayfinder/agents/openai.yaml +1 -0
  93. package/skills/wizard/LICENSE +21 -0
  94. package/skills/wizard/SKILL.md +44 -0
  95. package/skills/wizard/agents/openai.yaml +3 -0
  96. package/skills/wizard/template.sh +223 -0
  97. package/skills/writing-for-agents/SKILL.md +0 -2
  98. package/dist/blocks.js +0 -51
  99. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  100. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  101. /package/skills/{resolving-merge-conflicts → implement-spec}/LICENSE +0 -0
@@ -0,0 +1,64 @@
1
+ import { extname, resolve } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { run } from './git.js';
4
+ import { kitHome, readOwnAgent } from './home.js';
5
+ import { quoted } from './shell.js';
6
+ function section(house) {
7
+ return `credential.${new URL('/app/', house).href}`;
8
+ }
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
+ async function configured(args) {
14
+ try {
15
+ return await run(kitHome(), ['config', '--global', ...args]);
16
+ }
17
+ catch (error) {
18
+ if (error.code === 'ENOENT')
19
+ return null;
20
+ throw error;
21
+ }
22
+ }
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]);
30
+ if (ran === null)
31
+ return;
32
+ if (ran.status !== 0)
33
+ throw new Error(`git config failed: ${ran.stderr.trim()}`);
34
+ }
35
+ }
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
+ export async function answerGitCredential(operation) {
48
+ let asked = '';
49
+ for await (const chunk of process.stdin)
50
+ asked += String(chunk);
51
+ if (operation !== 'get')
52
+ return;
53
+ const attributes = new Map(asked
54
+ .split('\n')
55
+ .filter((line) => line.includes('='))
56
+ .map((line) => [line.slice(0, line.indexOf('=')), line.slice(line.indexOf('=') + 1)]));
57
+ const ownAgent = await readOwnAgent();
58
+ if (ownAgent === null)
59
+ return;
60
+ const house = new URL(ownAgent.house);
61
+ if (attributes.get('protocol') !== house.protocol.slice(0, -1) || attributes.get('host') !== house.host)
62
+ return;
63
+ process.stdout.write(`username=house\npassword=${ownAgent.credential}\n`);
64
+ }
@@ -1,23 +1,23 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "version": "0.2.1-c15804a0fa28d",
3
+ "version": "0.2.1-c2997cbe74597",
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-c15804a0fa28d/connect-linux.sh"
8
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c2997cbe74597/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-c15804a0fa28d/connect-linux.sh"
14
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c2997cbe74597/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-c15804a0fa28d/connect-windows.ps1"
20
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c2997cbe74597/connect-windows.ps1"
21
21
  }
22
22
  }
23
23
  }
package/dist/bridge.js CHANGED
@@ -6,16 +6,15 @@ import { createServer, request as httpRequest } from 'node:http';
6
6
  import { request as httpsRequest } from 'node:https';
7
7
  import { delimiter, dirname, join } from 'node:path';
8
8
  import { parse } from 'yaml';
9
- import { HouseRefusal, retryDelay } from './api.js';
9
+ import { finalRefusal, HouseRefusal, retryDelay, retrying } from './api.js';
10
10
  import { loginPath } from './clis.js';
11
11
  import { changedPaths, } from './copies.js';
12
12
  import { kitHome, readEnrolment } from './home.js';
13
13
  import { callHouse, mcpBody, rpc, UNREACHABLE } from './mcp.js';
14
- import { operationId, uuidOf } from './operation.js';
15
- import { refusalOf } from './refusals.js';
14
+ import { operationId } from './operation.js';
15
+ import { refusalOf, retryable } from './refusals.js';
16
16
  import { relayed } from './relay.js';
17
17
  const MCP_REQUEST_BYTES = 4 * 1024 * 1024;
18
- const ATTACHMENTS = '/kit/attachments/upload';
19
18
  export async function received(request) {
20
19
  let text = '';
21
20
  request.setEncoding('utf8');
@@ -40,8 +39,6 @@ function refusalIn(text, status) {
40
39
  return refusalOf(text) ?? { code: '', text: text === '' ? `House refused it with ${status}` : text, conflicts: [] };
41
40
  }
42
41
  function answered(forwarded) {
43
- if (forwarded.status >= 500)
44
- throw new HouseRefusal('/', forwarded.status, forwarded.text);
45
42
  if (forwarded.status !== 200)
46
43
  return { refused: refusalIn(forwarded.text, forwarded.status) };
47
44
  const answer = JSON.parse(forwarded.text);
@@ -65,16 +62,11 @@ function pendingSeconds(forwarded) {
65
62
  function resendAfter(forwarded, attempt) {
66
63
  if (forwarded === null)
67
64
  return retryDelay(attempt);
68
- if (forwarded.status >= 500)
69
- return refusalOf(forwarded.text) === null ? retryDelay(attempt) : null;
65
+ if (forwarded.status !== 200)
66
+ return retryable(forwarded.text) === false ? null : retryDelay(attempt);
70
67
  const seconds = pendingSeconds(forwarded);
71
68
  return seconds === null ? null : seconds * 1000;
72
69
  }
73
- function versioned(conversationId, text) {
74
- const file = JSON.parse(text);
75
- const identity = JSON.stringify([conversationId, file.room_ref ?? null, file.name, file.sha256]);
76
- return JSON.stringify({ ...file, version: uuidOf(createHash('sha256').update(identity).digest(), 8) });
77
- }
78
70
  function editAnswer(forwarded) {
79
71
  const answer = answered(forwarded);
80
72
  if ('refused' in answer)
@@ -105,6 +97,7 @@ export async function openBridge(house, copies, conversationId, signal) {
105
97
  const after = resendAfter(forwarded, attempt);
106
98
  if (after === null)
107
99
  return forwarded;
100
+ // A Tool call House did not settle is sent again on the jittered curve, or after the wait House names for a pending one.
108
101
  await delay(after, undefined, { signal: closed.signal });
109
102
  }
110
103
  };
@@ -119,46 +112,32 @@ export async function openBridge(house, copies, conversationId, signal) {
119
112
  if ('refused' in prepared)
120
113
  return prepared;
121
114
  const { transfer } = parse(prepared.text);
122
- const posted = await fetch(relayed(transfer.url), {
123
- method: 'POST',
124
- headers: { 'content-type': 'text/plain; charset=utf-8' },
125
- body: Buffer.concat([Buffer.from(`${transfer.operation}\n`), bytes]),
126
- signal: closed.signal,
127
- });
128
- const text = await posted.text();
129
- if (posted.status >= 500)
130
- throw new HouseRefusal('/', posted.status, text);
131
- if (!posted.ok)
132
- return { refused: refusalIn(text, posted.status) };
133
- const held = JSON.parse(text).transfer;
134
- return { url: held.url, operation: held.operation, bytes: bytes.length, sha256 };
135
- };
136
- let mutations = null;
137
- const writes = async (tool) => {
138
- mutations ??= house
139
- .post('/kit/tools/mutations', {}, closed.signal)
140
- .then(({ tools }) => new Set(tools.filter((listed) => listed.writes).map((listed) => listed.name)));
115
+ let text;
141
116
  try {
142
- return (await mutations).has(tool);
117
+ text = await retrying(async () => {
118
+ const posted = await fetch(relayed(transfer.url), {
119
+ method: 'POST',
120
+ headers: { 'content-type': 'text/plain; charset=utf-8' },
121
+ body: Buffer.concat([Buffer.from(`${transfer.operation}\n`), bytes]),
122
+ signal: closed.signal,
123
+ });
124
+ const answer = await posted.text();
125
+ if (!posted.ok)
126
+ throw new HouseRefusal('/', posted.status, answer);
127
+ return answer;
128
+ }, closed.signal);
143
129
  }
144
130
  catch (error) {
145
- mutations = null;
146
- throw error;
131
+ if (!finalRefusal(error))
132
+ throw error;
133
+ return { refused: refusalIn(error.text, error.status) };
147
134
  }
135
+ const held = JSON.parse(text).transfer;
136
+ return { url: held.url, operation: held.operation, bytes: bytes.length, sha256 };
148
137
  };
149
- const forward = async (path, text) => {
150
- if (path === '/') {
151
- const message = JSON.parse(text);
152
- const tool = message.method === 'tools/call' ? String(message.params.name) : null;
153
- return send(message, tool !== null && (await writes(tool)) ? operationId() : null);
154
- }
155
- const answer = await fetch(relayed(new URL(path, origin)), {
156
- method: 'POST',
157
- headers: { 'content-type': 'application/json', authorization: `Bearer ${credential}` },
158
- body: versioned(conversationId, text),
159
- signal: closed.signal,
160
- });
161
- return { status: answer.status, text: await answer.text() };
138
+ const forward = (text) => {
139
+ const message = JSON.parse(text);
140
+ return send(message, message.method === 'tools/call' ? operationId() : null);
162
141
  };
163
142
  const caller = {
164
143
  key: `conversation:${conversationId}`,
@@ -180,11 +159,13 @@ export async function openBridge(house, copies, conversationId, signal) {
180
159
  const { cwd, commit } = JSON.parse(text);
181
160
  return { status: 200, text: JSON.stringify(await copies.push(cwd, commit, caller)) };
182
161
  }
162
+ if (path === '/git/fetch')
163
+ return { status: 200, text: JSON.stringify(await copies.fetch()) };
183
164
  if (path === '/git/observed') {
184
165
  await copies.observed(JSON.parse(text));
185
166
  return { status: 200, text: '{}' };
186
167
  }
187
- return path === '/' || path === ATTACHMENTS ? forward(path, text) : { status: 404, text: '' };
168
+ return path === '/' ? forward(text) : { status: 404, text: '' };
188
169
  };
189
170
  const sockets = join(kitHome(), 'bridges');
190
171
  await mkdir(sockets, { recursive: true, mode: 0o700 });
@@ -204,7 +185,8 @@ export async function openBridge(house, copies, conversationId, signal) {
204
185
  response.writeHead(error.status, { 'content-type': 'application/json' }).end(error.text);
205
186
  return;
206
187
  }
207
- response.writeHead(502).end(request.url === '/git/push' && error instanceof Error ? error.message : UNREACHABLE);
188
+ const verb = request.url === '/git/push' || request.url === '/git/fetch';
189
+ response.writeHead(502).end(verb && error instanceof Error ? error.message : UNREACHABLE);
208
190
  }
209
191
  });
210
192
  bridge.listen(socketPath);
@@ -231,7 +213,7 @@ export async function openBridge(house, copies, conversationId, signal) {
231
213
  GIT_CONFIG_VALUE_0: app,
232
214
  },
233
215
  tool: async (name, args) => {
234
- const forwarded = await forward('/', JSON.stringify(call(name, args)));
216
+ const forwarded = await forward(JSON.stringify(call(name, args)));
235
217
  if (forwarded.status !== 200)
236
218
  throw new Error(`House refused ${name} with ${forwarded.status}: ${forwarded.text}`);
237
219
  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(before, 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,48 +1,87 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { readFileSync } from 'node:fs';
3
- import { readFile } from 'node:fs/promises';
4
- import { userInfo } from 'node:os';
5
- import { join } from 'node:path';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { constants, readFileSync } from 'node:fs';
4
+ import { access, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
5
+ import { homedir, userInfo } from 'node:os';
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
- function threadStatus({ method, params }) {
9
- if (method !== 'session/update' || params?.update?.sessionUpdate !== 'session_info_update')
10
- return undefined;
11
- return params.update._meta?.codex?.threadStatus?.type;
12
- }
13
- function asyncTask({ method, params }) {
14
- const update = params?.update;
15
- if (method !== 'session/update' || update?.asyncTaskId === undefined)
16
- return null;
17
- if (update.sessionUpdate === 'async_task_spawned')
18
- return { id: update.asyncTaskId, running: true };
19
- if (update.sessionUpdate === 'async_task_state_update' && !RUNNING_JOB.has(update.state ?? '')) {
20
- return { id: update.asyncTaskId, running: false };
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;
21
28
  }
22
- return null;
23
29
  }
24
- function airPhase({ _meta }) {
25
- const phase = _meta?.jetbrains?.air?.phase;
26
- if (phase === 'commentary')
27
- return 'note';
28
- if (phase === 'final_answer')
29
- return 'answer';
30
- return null;
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'));
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
+ }
57
+ await mkdir(rules, { recursive: true });
58
+ const temporary = `${file}.${randomUUID()}`;
59
+ await writeFile(temporary, houseRule(houses));
60
+ await rename(temporary, file);
61
+ }
62
+ async function nothing() { }
63
+ function bridgeInClaudeSandbox(bridge) {
64
+ return process.platform === 'darwin' ? { allowUnixSockets: [bridge] } : { allowAllUnixSockets: true };
31
65
  }
32
66
  export const CLIS = {
33
67
  'codex-acp': {
34
68
  bin: 'codex',
35
69
  minimum: '0.159.1',
36
70
  install: 'curl -fsSL https://chatgpt.com/codex/install.sh | sh',
37
- adapter: { package: '@agentclientprotocol/codex-acp', bin: 'codex-acp', executable: 'CODEX_PATH', env: {} },
71
+ adapter: {
72
+ package: '@agentclientprotocol/codex-acp',
73
+ bin: 'codex-acp',
74
+ executable: 'CODEX_PATH',
75
+ env: { CODEX_CONFIG: JSON.stringify({ features: { shell_zsh_fork: true } }) },
76
+ },
38
77
  args: [],
78
+ fullAccess: 'agent-full-access',
79
+ sessionMeta: () => ({}),
80
+ renames: true,
81
+ allowHouse: allowHouseInCodex,
39
82
  signedIn: { command: ['login', 'status'] },
40
83
  login: { args: ['login', '--device-auth'], code: 'show' },
41
- queues: false,
42
- turnStarted: (notice) => threadStatus(notice) === 'active',
43
- turnEnded: (notice) => threadStatus(notice) === 'idle',
44
- job: asyncTask,
45
- phase: airPhase,
84
+ air: ['asyncTasks', 'nativeSubagentSessions'],
46
85
  },
47
86
  'claude-agent-acp': {
48
87
  bin: 'claude',
@@ -52,45 +91,49 @@ export const CLIS = {
52
91
  package: '@agentclientprotocol/claude-agent-acp',
53
92
  bin: 'claude-agent-acp',
54
93
  executable: 'CLAUDE_CODE_EXECUTABLE',
55
- env: { CLAUDE_CODE_ENTRYPOINT: 'claude-agent-acp' },
94
+ env: { CLAUDE_CODE_ENTRYPOINT: 'claude-agent-acp', IS_SANDBOX: '1' },
56
95
  },
57
96
  args: [],
97
+ fullAccess: 'bypassPermissions',
98
+ sessionMeta: (bridge, title) => ({
99
+ claudeCode: {
100
+ options: {
101
+ allowedTools: ['Bash(house:*)'],
102
+ includeHookEvents: true,
103
+ title,
104
+ settings: { sandbox: { network: bridgeInClaudeSandbox(bridge) } },
105
+ },
106
+ emitRawSDKMessages: RAW_SUBTYPES.map((subtype) => ({ type: 'system', subtype })),
107
+ },
108
+ }),
109
+ renames: false,
110
+ allowHouse: nothing,
58
111
  signedIn: { command: ['auth', 'status'] },
59
112
  login: { args: ['auth', 'login', '--claudeai'], code: 'collect', rejected: 'Invalid code' },
60
- queues: false,
61
- turnStarted: () => false,
62
- turnEnded: ({ method, params }) => method === 'session/update' &&
63
- params?.update?.sessionUpdate === 'usage_update' &&
64
- params.update._meta?.['_claude/origin'] !== undefined,
65
- job: asyncTask,
66
- phase: () => null,
113
+ air: ['asyncTasks', 'sessionFailure'],
67
114
  },
68
115
  'grok-build': {
69
116
  bin: 'grok',
70
117
  minimum: '1.0.46',
71
118
  install: 'curl -fsSL https://x.ai/cli/install.sh | bash',
72
119
  adapter: null,
73
- args: ['agent', '--no-leader', 'stdio'],
120
+ args: ['--sandbox', 'off', 'agent', '--always-approve', '--no-leader', 'stdio'],
121
+ fullAccess: null,
122
+ sessionMeta: () => ({}),
123
+ renames: false,
124
+ allowHouse: nothing,
74
125
  signedIn: { initializeMeta: 'defaultAuthMethodId' },
75
126
  login: { args: ['login', '--device-auth'], code: 'show' },
76
- queues: true,
77
- turnStarted: ({ method, params }) => method === '_x.ai/queue/changed' && params?.runningPromptId !== undefined,
78
- turnEnded: ({ method, params }) => method === '_x.ai/session_notification' && params?.update?.sessionUpdate === 'turn_completed',
79
- job: ({ method, params }) => {
80
- if (method === '_x.ai/task_backgrounded')
81
- return { id: params.update.task_id, running: true };
82
- if (method === '_x.ai/task_completed')
83
- return { id: params.update.task_snapshot.task_id, running: false };
84
- return null;
85
- },
86
- phase: () => null,
127
+ air: ['asyncTasks'],
87
128
  },
88
129
  };
89
130
  const KIT_PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
90
131
  export const KIT_VERSION = KIT_PACKAGE.version;
91
132
  const PROXY = /^https?_proxy$/i;
92
133
  const VERSION = /\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?/;
134
+ // An install still running after fifteen minutes is ended and reported as failed, so a hung download cannot hold its CLI's report.
93
135
  const INSTALL_MS = 15 * 60_000;
136
+ // A login shell or a version read is ended after half a minute, so a profile that waits for input cannot stall the report.
94
137
  const READ_MS = 30_000;
95
138
  const CAUSE_CHARACTERS = 4000;
96
139
  function prefix(kind) {
@@ -118,8 +161,22 @@ export function run(command, args, timeoutMs, detached = false, signal) {
118
161
  timeout: timeoutMs,
119
162
  env: withoutProxy(),
120
163
  detached,
121
- signal,
164
+ signal: detached ? undefined : signal,
122
165
  });
166
+ const stop = () => {
167
+ try {
168
+ process.kill(-child.pid, 'SIGKILL');
169
+ }
170
+ catch { }
171
+ };
172
+ if (detached && signal?.aborted)
173
+ stop();
174
+ if (detached)
175
+ signal?.addEventListener('abort', stop, { once: true });
176
+ const ended = (ran) => {
177
+ signal?.removeEventListener('abort', stop);
178
+ resolve(ran);
179
+ };
123
180
  let stdout = '';
124
181
  let output = '';
125
182
  child.stdout.setEncoding('utf8').on('data', (chunk) => {
@@ -129,25 +186,25 @@ export function run(command, args, timeoutMs, detached = false, signal) {
129
186
  child.stderr.setEncoding('utf8').on('data', (chunk) => {
130
187
  output += chunk;
131
188
  });
132
- child.on('error', (error) => resolve({ status: null, stdout, output: error.message }));
133
- child.on('close', (status) => resolve({ status, stdout, output }));
189
+ child.on('error', (error) => ended({ status: null, stdout, output: error.message }));
190
+ child.on('close', (status) => ended({ status, stdout, output }));
134
191
  });
135
192
  }
136
193
  export function failureOf(ran, program) {
137
194
  return ran.output.trim().slice(-CAUSE_CHARACTERS) || `${program} exited with ${ran.status ?? 'a signal'}`;
138
195
  }
139
- export async function installAdapter(kind) {
196
+ export async function installAdapter(kind, signal) {
140
197
  const adapter = CLIS[kind].adapter;
141
198
  if (adapter === null)
142
199
  return null;
143
200
  const release = KIT_PACKAGE.peerDependencies[adapter.package];
144
201
  if ((await installedAdapter(kind)) === release)
145
202
  return null;
146
- const ran = await run('npm', ['install', '--prefix', prefix(kind), '--no-save', '--no-audit', '--no-fund', '--omit=optional', `${adapter.package}@${release}`], INSTALL_MS);
203
+ 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);
147
204
  return ran.status === 0 ? null : failureOf(ran, 'npm');
148
205
  }
149
- export async function installCli(kind) {
150
- const ran = await run('bash', ['-o', 'pipefail', '-c', CLIS[kind].install], INSTALL_MS, true);
206
+ export async function installCli(kind, signal) {
207
+ const ran = await run('bash', ['-o', 'pipefail', '-c', CLIS[kind].install], INSTALL_MS, true, signal);
151
208
  return ran.status === 0 ? null : failureOf(ran, 'the install');
152
209
  }
153
210
  function loginShell() {
@@ -156,11 +213,11 @@ function loginShell() {
156
213
  const FOUND = 'house-kit-cli ';
157
214
  const LOOKUP = `found=$(command -v "$1") || exit 0; case $found in /*) ;; *) found=$(pwd -P)/$found ;; esac; printf "${FOUND}%s\\n" "$found"`;
158
215
  const USER_BIN = 'case ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) PATH="$PATH:$HOME/.local/bin" ;; esac; export PATH';
159
- function inLoginShell(script, argument = '') {
160
- return run(loginShell(), ['-l', '-i', '-c', `exec /bin/sh -c '${USER_BIN}; ${script}' sh ${argument}`], READ_MS, true);
216
+ function inLoginShell(script, argument = '', signal) {
217
+ return run(loginShell(), ['-l', '-i', '-c', `exec /bin/sh -c '${USER_BIN}; ${script}' sh ${argument}`], READ_MS, true, signal);
161
218
  }
162
- export async function locate(kind) {
163
- const ran = await inLoginShell(LOOKUP, CLIS[kind].bin);
219
+ export async function locate(kind, signal) {
220
+ const ran = await inLoginShell(LOOKUP, CLIS[kind].bin, signal);
164
221
  return ran.stdout.split('\n').findLast((line) => line.startsWith(FOUND))?.slice(FOUND.length) ?? null;
165
222
  }
166
223
  export async function loginPath() {
@@ -168,8 +225,14 @@ export async function loginPath() {
168
225
  const ran = await inLoginShell(`printf "${marker}%s\\n" "$PATH"`);
169
226
  return ran.stdout.split('\n').findLast((line) => line.startsWith(marker)).slice(marker.length);
170
227
  }
171
- export async function versionOf(path) {
172
- return VERSION.exec((await run(path, ['--version'], READ_MS)).stdout)?.[0] ?? null;
228
+ export async function writeCodexRule() {
229
+ const shell = await loginPath().catch(() => '');
230
+ await allowHouseInCodex([dirname(process.execPath), process.env.PATH ?? '', shell].join(delimiter)).catch((error) => {
231
+ throw new Error(`Codex's rule for house could not be written: ${error.message}`);
232
+ });
233
+ }
234
+ export async function versionOf(path, signal) {
235
+ return VERSION.exec((await run(path, ['--version'], READ_MS, false, signal)).stdout)?.[0] ?? null;
173
236
  }
174
237
  function numbers(version) {
175
238
  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
+ });