@agentshouse/kit 0.2.1-cf110d15f69c8 → 0.2.1-cf3a50c90b895

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 (97) hide show
  1. package/README.md +31 -30
  2. package/dist/acp.js +36 -16
  3. package/dist/agents.js +110 -23
  4. package/dist/api.js +5 -1
  5. package/dist/app-git.js +10 -26
  6. package/dist/bootstrap.json +4 -4
  7. package/dist/bridge.js +3 -6
  8. package/dist/calls.js +63 -0
  9. package/dist/chatgpt.js +32 -0
  10. package/dist/clis.js +20 -4
  11. package/dist/connection.js +125 -0
  12. package/dist/conversations.js +307 -120
  13. package/dist/copies.js +89 -52
  14. package/dist/end.js +11 -2
  15. package/dist/events.js +6 -2
  16. package/dist/files.js +8 -2
  17. package/dist/git.js +11 -9
  18. package/dist/home.js +56 -5
  19. package/dist/house-cli.js +291 -0
  20. package/dist/house-main.js +7 -206
  21. package/dist/instructions.js +4 -28
  22. package/dist/kit-cli.js +68 -0
  23. package/dist/kit-main.js +8 -22
  24. package/dist/login.js +46 -28
  25. package/dist/logout.js +0 -2
  26. package/dist/mcp.js +2 -2
  27. package/dist/placement.js +39 -0
  28. package/dist/relay.js +1 -1
  29. package/dist/resident.js +82 -33
  30. package/dist/scope.js +3 -0
  31. package/dist/service.js +23 -1
  32. package/dist/sign-in.js +19 -37
  33. package/dist/skills.js +84 -15
  34. package/dist/transcription.js +3 -3
  35. package/dist/translate.js +2 -8
  36. package/dist/update.js +69 -50
  37. package/dist/wire.js +22 -0
  38. package/dist/work.js +3 -5
  39. package/how-we-work.md +13 -3
  40. package/package.json +4 -3
  41. package/skills/ask-matt/SKILL.md +17 -11
  42. package/skills/ask-matt/agents/openai.yaml +1 -0
  43. package/skills/code-review/SKILL.md +5 -5
  44. package/skills/codebase-design/DESIGN-IT-TWICE.md +1 -1
  45. package/skills/codebase-design/SKILL.md +0 -2
  46. package/skills/diagnosing-bugs/SKILL.md +2 -4
  47. package/skills/domain-modeling/{CONTEXT-FORMAT.md → GLOSSARY-FORMAT.md} +10 -10
  48. package/skills/domain-modeling/SKILL.md +12 -14
  49. package/skills/grill-me/SKILL.md +1 -2
  50. package/skills/grill-me/agents/openai.yaml +1 -0
  51. package/skills/grill-with-docs/SKILL.md +1 -2
  52. package/skills/grill-with-docs/agents/openai.yaml +1 -0
  53. package/skills/grilling/SKILL.md +2 -2
  54. package/skills/handoff/SKILL.md +2 -3
  55. package/skills/handoff/agents/openai.yaml +1 -0
  56. package/skills/implement/SKILL.md +5 -4
  57. package/skills/implement/agents/openai.yaml +1 -0
  58. package/skills/implement-spec/SKILL.md +40 -0
  59. package/skills/implement-spec/agents/openai.yaml +5 -0
  60. package/skills/improve-codebase-architecture/SKILL.md +6 -7
  61. package/skills/improve-codebase-architecture/agents/openai.yaml +1 -0
  62. package/skills/pr/CREDITS.md +3 -0
  63. package/skills/pr/LICENSE +21 -0
  64. package/skills/pr/SKILL.md +170 -0
  65. package/skills/pr/agents/openai.yaml +3 -0
  66. package/skills/prototype/SKILL.md +0 -2
  67. package/skills/research/SKILL.md +0 -2
  68. package/skills/retro/LICENSE +21 -0
  69. package/skills/retro/SKILL.md +44 -0
  70. package/skills/retro/agents/openai.yaml +5 -0
  71. package/skills/tdd/SKILL.md +2 -4
  72. package/skills/teach/SKILL.md +3 -4
  73. package/skills/teach/agents/openai.yaml +1 -0
  74. package/skills/to-questionnaire/SKILL.md +1 -2
  75. package/skills/to-questionnaire/agents/openai.yaml +1 -0
  76. package/skills/to-spec/SKILL.md +1 -2
  77. package/skills/to-spec/agents/openai.yaml +1 -0
  78. package/skills/to-tickets/SKILL.md +3 -4
  79. package/skills/to-tickets/agents/openai.yaml +1 -0
  80. package/skills/triage/AGENT-BRIEF.md +207 -0
  81. package/skills/triage/LICENSE +21 -0
  82. package/skills/triage/OUT-OF-SCOPE.md +105 -0
  83. package/skills/triage/SKILL.md +112 -0
  84. package/skills/triage/agents/openai.yaml +5 -0
  85. package/skills/wait-what/SKILL.md +2 -3
  86. package/skills/wait-what/agents/openai.yaml +1 -0
  87. package/skills/wayfinder/SKILL.md +5 -6
  88. package/skills/wayfinder/agents/openai.yaml +1 -0
  89. package/skills/wizard/LICENSE +21 -0
  90. package/skills/wizard/SKILL.md +44 -0
  91. package/skills/wizard/agents/openai.yaml +3 -0
  92. package/skills/wizard/template.sh +223 -0
  93. package/skills/writing-for-agents/SKILL.md +0 -2
  94. package/dist/stream.js +0 -74
  95. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  96. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  97. /package/skills/{resolving-merge-conflicts → implement-spec}/LICENSE +0 -0
package/dist/calls.js ADDED
@@ -0,0 +1,63 @@
1
+ import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { kitHome } from './home.js';
4
+ function logged(error) {
5
+ process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
6
+ }
7
+ export class Calls {
8
+ house;
9
+ changed;
10
+ carried = new Map();
11
+ path = join(kitHome(), 'call-results.json');
12
+ results;
13
+ returned = new Set();
14
+ open = 0;
15
+ constructor(house, changed) {
16
+ this.house = house;
17
+ this.changed = changed;
18
+ this.results = new Map(existsSync(this.path) ? Object.entries(JSON.parse(readFileSync(this.path, 'utf8'))) : []);
19
+ }
20
+ keepResult(callId, result) {
21
+ if (this.returned.has(callId))
22
+ return;
23
+ this.results.set(callId, result);
24
+ this.save();
25
+ }
26
+ idle() {
27
+ return this.open === 0;
28
+ }
29
+ carry(callId, work) {
30
+ const known = this.carried.get(callId);
31
+ const result = this.results.get(callId);
32
+ const carried = known ?? (result === undefined ? work() : Promise.resolve({ result }));
33
+ if (known === undefined) {
34
+ this.carried.set(callId, carried);
35
+ carried.catch(() => this.carried.delete(callId));
36
+ }
37
+ this.open++;
38
+ this.changed();
39
+ return carried
40
+ .then(async ({ result, returned }) => {
41
+ try {
42
+ await this.house.deliver(`/kit/inputs/${callId}/ack`, result);
43
+ }
44
+ finally {
45
+ this.returned.add(callId);
46
+ if (this.results.delete(callId))
47
+ this.save();
48
+ }
49
+ if (known === undefined)
50
+ await returned?.();
51
+ })
52
+ .catch(logged)
53
+ .finally(() => {
54
+ this.open--;
55
+ this.changed();
56
+ });
57
+ }
58
+ save() {
59
+ const temporary = `${this.path}.${process.pid}`;
60
+ writeFileSync(temporary, JSON.stringify(Object.fromEntries(this.results)), { mode: 0o600 });
61
+ renameSync(temporary, this.path);
62
+ }
63
+ }
@@ -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,8 +1,9 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
2
3
  import { constants, readFileSync } from 'node:fs';
3
4
  import { access, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
4
5
  import { homedir, userInfo } from 'node:os';
5
- import { delimiter, isAbsolute, join } from 'node:path';
6
+ import { delimiter, dirname, isAbsolute, join } from 'node:path';
6
7
  import { kitHome } from './home.js';
7
8
  const HOUSE_EXECUTABLES = /^host_executable\(name = "house", paths = (\[.*\])\)$/m;
8
9
  function houseRule(houses) {
@@ -54,11 +55,14 @@ export async function allowHouseInCodex(path) {
54
55
  houses.push(house);
55
56
  }
56
57
  await mkdir(rules, { recursive: true });
57
- const temporary = `${file}.${process.pid}`;
58
+ const temporary = `${file}.${randomUUID()}`;
58
59
  await writeFile(temporary, houseRule(houses));
59
60
  await rename(temporary, file);
60
61
  }
61
62
  async function nothing() { }
63
+ function answered(error) {
64
+ return typeof error === 'object' && error !== null ? error : {};
65
+ }
62
66
  function bridgeInClaudeSandbox(bridge) {
63
67
  return process.platform === 'darwin' ? { allowUnixSockets: [bridge] } : { allowAllUnixSockets: true };
64
68
  }
@@ -76,6 +80,10 @@ export const CLIS = {
76
80
  args: [],
77
81
  fullAccess: 'agent-full-access',
78
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
+ },
79
87
  renames: true,
80
88
  allowHouse: allowHouseInCodex,
81
89
  signedIn: { command: ['login', 'status'] },
@@ -105,6 +113,7 @@ export const CLIS = {
105
113
  emitRawSDKMessages: RAW_SUBTYPES.map((subtype) => ({ type: 'system', subtype })),
106
114
  },
107
115
  }),
116
+ unknownSession: (error) => answered(error).code === -32002,
108
117
  renames: false,
109
118
  allowHouse: nothing,
110
119
  signedIn: { command: ['auth', 'status'] },
@@ -119,6 +128,7 @@ export const CLIS = {
119
128
  args: ['--sandbox', 'off', 'agent', '--always-approve', '--no-leader', 'stdio'],
120
129
  fullAccess: null,
121
130
  sessionMeta: () => ({}),
131
+ unknownSession: (error) => answered(error).data?.code === 'FS_NOT_FOUND',
122
132
  renames: false,
123
133
  allowHouse: nothing,
124
134
  signedIn: { initializeMeta: 'defaultAuthMethodId' },
@@ -127,7 +137,7 @@ export const CLIS = {
127
137
  },
128
138
  };
129
139
  const KIT_PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
130
- export const KIT_VERSION = KIT_PACKAGE.version;
140
+ export const KIT_RELEASE = KIT_PACKAGE.version;
131
141
  const PROXY = /^https?_proxy$/i;
132
142
  const VERSION = /\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?/;
133
143
  // An install still running after fifteen minutes is ended and reported as failed, so a hung download cannot hold its CLI's report.
@@ -199,7 +209,7 @@ export async function installAdapter(kind, signal) {
199
209
  const release = KIT_PACKAGE.peerDependencies[adapter.package];
200
210
  if ((await installedAdapter(kind)) === release)
201
211
  return null;
202
- const ran = await run('npm', ['install', '--prefix', prefix(kind), '--no-save', '--no-audit', '--no-fund', '--omit=optional', `${adapter.package}@${release}`], INSTALL_MS, true, signal);
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);
203
213
  return ran.status === 0 ? null : failureOf(ran, 'npm');
204
214
  }
205
215
  export async function installCli(kind, signal) {
@@ -224,6 +234,12 @@ export async function loginPath() {
224
234
  const ran = await inLoginShell(`printf "${marker}%s\\n" "$PATH"`);
225
235
  return ran.stdout.split('\n').findLast((line) => line.startsWith(marker)).slice(marker.length);
226
236
  }
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
+ }
227
243
  export async function versionOf(path, signal) {
228
244
  return VERSION.exec((await run(path, ['--version'], READ_MS, false, signal)).stdout)?.[0] ?? null;
229
245
  }
@@ -0,0 +1,125 @@
1
+ import { subscribe } from 'node:diagnostics_channel';
2
+ import { HouseRefusal, retryDelay } from './api.js';
3
+ import { end, REPLACED, signInAgain } from './end.js';
4
+ import { readEnrolment } from './home.js';
5
+ import { relayed } from './relay.js';
6
+ import { endUnserved, KIT_WIRE } from './wire.js';
7
+ const PATH = '/kit/stream';
8
+ const SUBPROTOCOL = `house.kit.stream.${KIT_WIRE}`;
9
+ const REFUSAL_HEADER = 'x-house-refusal';
10
+ // House beats each Kit connection every thirty seconds, so two silent beats mean this Kit process cannot hear House.
11
+ export const CONNECTION_SILENCE_MS = 60_000;
12
+ const SYSTEM_CONNECTION_CLOCK = {
13
+ later: (delay, callback) => setTimeout(callback, delay),
14
+ clear: (timer) => clearTimeout(timer),
15
+ };
16
+ export class ConnectionSilence {
17
+ timer = null;
18
+ silent;
19
+ clock;
20
+ interval;
21
+ constructor(silent, clock = SYSTEM_CONNECTION_CLOCK, interval = CONNECTION_SILENCE_MS) {
22
+ this.silent = silent;
23
+ this.clock = clock;
24
+ this.interval = interval;
25
+ }
26
+ heard() {
27
+ this.stop();
28
+ this.timer = this.clock.later(this.interval, () => {
29
+ this.timer = null;
30
+ this.silent();
31
+ });
32
+ }
33
+ stop() {
34
+ if (this.timer === null)
35
+ return;
36
+ this.clock.clear(this.timer);
37
+ this.timer = null;
38
+ }
39
+ }
40
+ function connectionUrl(house) {
41
+ const url = new URL(PATH, house);
42
+ url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
43
+ return url;
44
+ }
45
+ function header(headers, name) {
46
+ for (let index = 0; index < headers.length; index += 2) {
47
+ if (headers[index].toString('latin1').toLowerCase() === name)
48
+ return headers[index + 1].toString('latin1');
49
+ }
50
+ return '';
51
+ }
52
+ let refused = null;
53
+ subscribe('undici:request:headers', (message) => {
54
+ const { request, response } = message;
55
+ if (!request.path.endsWith(PATH))
56
+ return;
57
+ refused = new HouseRefusal(PATH, response.statusCode, header(response.headers, REFUSAL_HEADER) || response.statusText);
58
+ });
59
+ export function holdConnection(handlers) {
60
+ let connection = null;
61
+ let failures = 0;
62
+ const connect = async () => {
63
+ const { house, credential } = (await readEnrolment());
64
+ refused = null;
65
+ const opening = new WebSocket(relayed(connectionUrl(house)), {
66
+ protocols: [SUBPROTOCOL],
67
+ headers: { authorization: `Bearer ${credential}` },
68
+ });
69
+ let opened = false;
70
+ let silence = null;
71
+ const closed = (event) => {
72
+ silence?.stop();
73
+ silence = null;
74
+ if (connection === opening)
75
+ connection = null;
76
+ process.stderr.write(`kit: connection closed ${event.code} ${event.reason}\n`);
77
+ if (event.reason === 'replaced')
78
+ end(REPLACED);
79
+ if (event.reason === 'credential_rejected' || refused?.status === 401) {
80
+ void signInAgain(credential);
81
+ return;
82
+ }
83
+ if (refused !== null)
84
+ endUnserved(refused.text);
85
+ if (!opened && refused !== null)
86
+ process.stderr.write(`kit: ${refused.message}\n`);
87
+ // A closed connection reopens in one to three seconds, spread so Kit processes one restart closed return apart; an opening House refused or left unanswered backs off on the jittered curve, retryable or not, since a resident outlives a House deploy.
88
+ setTimeout(() => void connect(), opened ? 1000 + Math.random() * 2000 : retryDelay(failures++));
89
+ };
90
+ opening.onopen = () => {
91
+ opened = true;
92
+ failures = 0;
93
+ connection = opening;
94
+ silence = new ConnectionSilence(() => {
95
+ if (connection !== opening)
96
+ return;
97
+ connection = null;
98
+ opening.onclose = null;
99
+ opening.close();
100
+ closed(new CloseEvent('close', { code: 1006, reason: 'heartbeat_missed' }));
101
+ });
102
+ silence.heard();
103
+ handlers.opened();
104
+ };
105
+ opening.onmessage = (event) => {
106
+ silence?.heard();
107
+ const arrived = JSON.parse(String(event.data));
108
+ if (arrived.type === 'input')
109
+ handlers.call(arrived);
110
+ else if (arrived.type !== 'beat')
111
+ handlers.notification(arrived);
112
+ };
113
+ opening.onerror = () => undefined;
114
+ opening.onclose = closed;
115
+ };
116
+ void connect();
117
+ return {
118
+ send(notification) {
119
+ if (connection === null || connection.readyState !== WebSocket.OPEN)
120
+ return false;
121
+ connection.send(JSON.stringify(notification));
122
+ return true;
123
+ },
124
+ };
125
+ }