@agentshouse/kit 0.2.1-c39de3ed26c1b → 0.2.1-c3db1f18aed8f

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 (70) hide show
  1. package/README.md +2 -2
  2. package/dist/agents.js +1 -0
  3. package/dist/bootstrap.json +4 -4
  4. package/dist/chatgpt.js +2 -1
  5. package/dist/clis.js +3 -2
  6. package/dist/home.js +38 -4
  7. package/dist/house-main.js +50 -19
  8. package/dist/instructions.js +2 -27
  9. package/dist/login.js +8 -9
  10. package/dist/skills.js +80 -12
  11. package/dist/transcription.js +3 -3
  12. package/dist/update.js +1 -0
  13. package/how-we-work.md +11 -3
  14. package/package.json +2 -1
  15. package/skills/ask-matt/SKILL.md +17 -11
  16. package/skills/ask-matt/agents/openai.yaml +1 -0
  17. package/skills/code-review/SKILL.md +5 -5
  18. package/skills/codebase-design/DESIGN-IT-TWICE.md +1 -1
  19. package/skills/codebase-design/SKILL.md +0 -2
  20. package/skills/diagnosing-bugs/SKILL.md +2 -4
  21. package/skills/domain-modeling/{CONTEXT-FORMAT.md → GLOSSARY-FORMAT.md} +10 -10
  22. package/skills/domain-modeling/SKILL.md +12 -14
  23. package/skills/grill-me/SKILL.md +1 -2
  24. package/skills/grill-me/agents/openai.yaml +1 -0
  25. package/skills/grill-with-docs/SKILL.md +1 -2
  26. package/skills/grill-with-docs/agents/openai.yaml +1 -0
  27. package/skills/grilling/SKILL.md +2 -2
  28. package/skills/handoff/SKILL.md +2 -3
  29. package/skills/handoff/agents/openai.yaml +1 -0
  30. package/skills/implement/SKILL.md +5 -4
  31. package/skills/implement/agents/openai.yaml +1 -0
  32. package/skills/implement-spec/SKILL.md +40 -0
  33. package/skills/implement-spec/agents/openai.yaml +5 -0
  34. package/skills/improve-codebase-architecture/SKILL.md +6 -7
  35. package/skills/improve-codebase-architecture/agents/openai.yaml +1 -0
  36. package/skills/pr/CREDITS.md +3 -0
  37. package/skills/pr/LICENSE +21 -0
  38. package/skills/pr/SKILL.md +170 -0
  39. package/skills/pr/agents/openai.yaml +3 -0
  40. package/skills/prototype/SKILL.md +0 -2
  41. package/skills/research/SKILL.md +0 -2
  42. package/skills/retro/LICENSE +21 -0
  43. package/skills/retro/SKILL.md +44 -0
  44. package/skills/retro/agents/openai.yaml +5 -0
  45. package/skills/tdd/SKILL.md +2 -4
  46. package/skills/teach/SKILL.md +3 -4
  47. package/skills/teach/agents/openai.yaml +1 -0
  48. package/skills/to-questionnaire/SKILL.md +1 -2
  49. package/skills/to-questionnaire/agents/openai.yaml +1 -0
  50. package/skills/to-spec/SKILL.md +1 -2
  51. package/skills/to-spec/agents/openai.yaml +1 -0
  52. package/skills/to-tickets/SKILL.md +3 -4
  53. package/skills/to-tickets/agents/openai.yaml +1 -0
  54. package/skills/triage/AGENT-BRIEF.md +207 -0
  55. package/skills/triage/LICENSE +21 -0
  56. package/skills/triage/OUT-OF-SCOPE.md +105 -0
  57. package/skills/triage/SKILL.md +112 -0
  58. package/skills/triage/agents/openai.yaml +5 -0
  59. package/skills/wait-what/SKILL.md +2 -3
  60. package/skills/wait-what/agents/openai.yaml +1 -0
  61. package/skills/wayfinder/SKILL.md +5 -6
  62. package/skills/wayfinder/agents/openai.yaml +1 -0
  63. package/skills/wizard/LICENSE +21 -0
  64. package/skills/wizard/SKILL.md +44 -0
  65. package/skills/wizard/agents/openai.yaml +3 -0
  66. package/skills/wizard/template.sh +223 -0
  67. package/skills/writing-for-agents/SKILL.md +0 -2
  68. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  69. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  70. /package/skills/{resolving-merge-conflicts → implement-spec}/LICENSE +0 -0
package/README.md CHANGED
@@ -33,7 +33,7 @@ them out; running it again without the flag brings them back.
33
33
 
34
34
  `kit login` opens or prints a link you confirm in any browser, on this computer
35
35
  or another, continues on its own and keeps the Environment's credential in
36
- `~/.house-kit`, or in `$HOUSE_KIT_HOME` when it is set. `--environment <id>`
36
+ `$HOUSE_KIT_HOME` when it is set, else in `~/.house-kit`, or in `house-kit-<uid>` under the temporary directory when your home is read-only. `--environment <id>`
37
37
  reconnects an Environment you already have; `--rooms <ref>[,<ref>...]` gives it
38
38
  only those Rooms and `--no-private` every Room but your Private Room; `--code <code>` connects immediately without a link.
39
39
  A login also connects your own agent: outside an Agent conversation, `house <tool>` calls
@@ -52,7 +52,7 @@ both, without `--owner`. `house upload` and `house download` move a Room's other
52
52
 
53
53
  House Kit turns each audio file of your message to an Agent into text on this
54
54
  computer before the Agent reads it. While transcription is on, the speech
55
- engine and model use about 425 MB in `~/.house-kit`; turning it off deletes them.
55
+ engine and model use about 425 MB beside the Environment's credential; turning it off deletes them.
56
56
 
57
57
  ## License
58
58
 
package/dist/agents.js CHANGED
@@ -277,6 +277,7 @@ export class Agents {
277
277
  if (known !== undefined && known.failure === null && known.minimum === undefined)
278
278
  return;
279
279
  await this.examine(kind, SIGNED_OUT, offer);
280
+ await this.report();
280
281
  }
281
282
  async send() {
282
283
  const agents = this.desired.agents.flatMap((kind) => {
@@ -1,23 +1,23 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "version": "0.2.1-c39de3ed26c1b",
3
+ "version": "0.2.1-c3db1f18aed8f",
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-c39de3ed26c1b/connect-linux.sh"
8
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c3db1f18aed8f/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-c39de3ed26c1b/connect-linux.sh"
14
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c3db1f18aed8f/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-c39de3ed26c1b/connect-windows.ps1"
20
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c3db1f18aed8f/connect-windows.ps1"
21
21
  }
22
22
  }
23
23
  }
package/dist/chatgpt.js CHANGED
@@ -20,6 +20,7 @@ export async function chatgpt(argv) {
20
20
  const house = values.house ?? (await readOwnAgent())?.house ?? DEFAULT_HOUSE;
21
21
  if (!(await accepted(house, values.key)))
22
22
  throw new Error(`${house} does not accept this key; pass one from ${KEY_SOURCE}`);
23
- await writeOwnAgent({ house, credential: values.key });
23
+ const relay = process.env.HOUSE_KIT_RELAY || undefined;
24
+ await writeOwnAgent({ house, ...(relay === undefined ? {} : { relay }), credential: values.key });
24
25
  process.stdout.write(`house now calls ${house} as you, with the key kept in ${kitHome()}.\n`);
25
26
  }
package/dist/clis.js CHANGED
@@ -1,4 +1,5 @@
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';
@@ -54,7 +55,7 @@ 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
  }
@@ -199,7 +200,7 @@ export async function installAdapter(kind, signal) {
199
200
  const release = KIT_PACKAGE.peerDependencies[adapter.package];
200
201
  if ((await installedAdapter(kind)) === release)
201
202
  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);
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);
203
204
  return ran.status === 0 ? null : failureOf(ran, 'npm');
204
205
  }
205
206
  export async function installCli(kind, signal) {
package/dist/home.js CHANGED
@@ -1,6 +1,7 @@
1
- import { existsSync, linkSync, readFileSync, renameSync, rmSync } from 'node:fs';
1
+ import { randomUUID } from 'node:crypto';
2
+ import { accessSync, chmodSync, constants, existsSync, linkSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync } from 'node:fs';
2
3
  import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
3
- import { homedir } from 'node:os';
4
+ import { homedir, tmpdir } from 'node:os';
4
5
  import { join } from 'node:path';
5
6
  import { selectRelay } from './relay.js';
6
7
  const ENROLMENT = 'credential.json';
@@ -8,8 +9,41 @@ const DISCONNECTED = 'disconnected.json';
8
9
  const CONFIGURATION = 'kit.json';
9
10
  const OWN_AGENT = 'own-agent.json';
10
11
  const RESTART = 'restart';
12
+ let defaulted;
13
+ function writable(directory) {
14
+ try {
15
+ accessSync(directory, constants.W_OK | constants.X_OK);
16
+ return true;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ function ownTemporaryHome() {
23
+ const home = join(tmpdir(), `house-kit-${process.getuid()}`);
24
+ try {
25
+ mkdirSync(home, { mode: 0o700 });
26
+ }
27
+ catch (error) {
28
+ if (error.code !== 'EEXIST')
29
+ throw error;
30
+ }
31
+ const found = lstatSync(home);
32
+ if (found.isSymbolicLink())
33
+ throw new Error(`${home} is a symbolic link`);
34
+ if (!found.isDirectory())
35
+ throw new Error(`${home} is not a directory`);
36
+ if (found.uid !== process.getuid())
37
+ throw new Error(`${home} is not owned by this User`);
38
+ chmodSync(home, 0o700);
39
+ return home;
40
+ }
41
+ function defaultHome() {
42
+ const home = join(homedir(), '.house-kit');
43
+ return existsSync(home) || writable(homedir()) ? home : ownTemporaryHome();
44
+ }
11
45
  export function kitHome() {
12
- return process.env.HOUSE_KIT_HOME ?? join(homedir(), '.house-kit');
46
+ return process.env.HOUSE_KIT_HOME || (defaulted ??= defaultHome());
13
47
  }
14
48
  export function workspaceRoot() {
15
49
  return process.env.HOUSE_KIT_WORKSPACE ?? '/agents/house';
@@ -30,7 +64,7 @@ export async function readHome(name) {
30
64
  export async function writeHome(name, value) {
31
65
  const home = kitHome();
32
66
  await mkdir(home, { recursive: true, mode: 0o700 });
33
- const temporary = join(home, `${name}.${process.pid}`);
67
+ const temporary = join(home, `${name}.${randomUUID()}`);
34
68
  await writeFile(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600 });
35
69
  await rename(temporary, join(home, name));
36
70
  }
@@ -2,13 +2,13 @@
2
2
  import { createHash } from 'node:crypto';
3
3
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
4
  import { request, setGlobalProxyFromEnv } from 'node:http';
5
- import { dirname, extname, join, resolve } from 'node:path';
5
+ import { basename, dirname, extname, join, resolve } from 'node:path';
6
6
  import { verified } from './files.js';
7
7
  import { HOUSE_REF } from './git.js';
8
- import { kitHome, readEnrolment, readOwnAgent } from './home.js';
8
+ import { kitHome, readOwnAgent } from './home.js';
9
9
  import { callHouse, rpc, UNREACHABLE } from './mcp.js';
10
10
  import { refusalOf } from './refusals.js';
11
- import { relayed } from './relay.js';
11
+ import { relayed, selectRelay } from './relay.js';
12
12
  import { parse, stringify } from 'yaml';
13
13
  const USAGE = "usage: house <tool> ['<arguments as JSON>']";
14
14
  const PUSH_USAGE = 'usage: house push [commit] [--owner]';
@@ -23,6 +23,7 @@ const SANDBOXED = "your CLI's sandbox kept house from House; run house directly,
23
23
  const REVOKED = 'this conversation no longer has House access; nothing to do from here';
24
24
  const UNCONNECTED = "this computer's own agent has no House connection; run kit login";
25
25
  const DISCONNECTED = "House no longer accepts this computer's own agent; run kit login";
26
+ const UNCONFIRMED = 'with no result; a write may or may not have taken effect, so read before you repeat it';
26
27
  const socketPath = process.env.HOUSE_BRIDGE;
27
28
  const MEDIA_TYPES = new Map([
28
29
  ['avif', 'image/avif'],
@@ -51,7 +52,7 @@ const MEDIA_TYPES = new Map([
51
52
  ['xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
52
53
  ['zip', 'application/zip'],
53
54
  ]);
54
- function refused(path, method, status, text) {
55
+ function refused(path, method, status, text, local) {
55
56
  const refusal = refusalOf(text);
56
57
  if (refusal !== null)
57
58
  return refusal.text;
@@ -60,9 +61,12 @@ function refused(path, method, status, text) {
60
61
  const act = method === 'GET' ? 'download' : 'upload';
61
62
  if (URL.canParse(path))
62
63
  return `the ${act} answered ${status}; ${act} the file again`;
63
- if (status === 502 && /^[A-Za-z]/.test(text))
64
+ if (status === 502 && local)
64
65
  return text;
65
- return `${path} answered ${status}`;
66
+ return path === '/' ? unconfirmed(status) : `${path} answered ${status}`;
67
+ }
68
+ function unconfirmed(status) {
69
+ return `House answered ${status} ${UNCONFIRMED}`;
66
70
  }
67
71
  function exchanged(path, method, body, headers = { 'content-type': 'application/json' }, to = socketPath) {
68
72
  return new Promise((resolve, reject) => {
@@ -74,7 +78,7 @@ function exchanged(path, method, body, headers = { 'content-type': 'application/
74
78
  if (answer.statusCode === 200)
75
79
  resolve(bytes);
76
80
  else
77
- reject(new Error(refused(path, method, answer.statusCode, bytes.toString('utf8'))));
81
+ reject(new Error(refused(path, method, answer.statusCode, bytes.toString('utf8'), answer.headers['content-type'] === undefined)));
78
82
  });
79
83
  });
80
84
  sent.on('error', (error) => reject(new Error(error.code === 'EPERM' || error.code === 'EACCES' ? SANDBOXED : CLOSED)));
@@ -88,14 +92,14 @@ async function direct(message) {
88
92
  const ownAgent = await readOwnAgent();
89
93
  if (ownAgent === null)
90
94
  throw new Error(UNCONNECTED);
91
- await readEnrolment();
95
+ selectRelay(process.env.HOUSE_KIT_RELAY || ownAgent.relay);
92
96
  setGlobalProxyFromEnv();
93
97
  const answer = await callHouse(ownAgent.house, ownAgent.credential, message).catch(() => {
94
98
  throw new Error(UNREACHABLE);
95
99
  });
96
100
  if (answer.status === 200)
97
101
  return answer.text;
98
- throw new Error(answer.status === 401 ? DISCONNECTED : (refusalOf(answer.text)?.text ?? `House answered ${answer.status}`));
102
+ throw new Error(answer.status === 401 ? DISCONNECTED : (refusalOf(answer.text)?.text ?? unconfirmed(answer.status)));
99
103
  }
100
104
  async function mcp(method, params) {
101
105
  const message = rpc(method, params);
@@ -127,7 +131,7 @@ async function transferred(url, method, bytes, headers) {
127
131
  const received = Buffer.from(await answer.arrayBuffer());
128
132
  if (answer.status === 200)
129
133
  return received;
130
- throw new Error(refused(url, method, answer.status, received.toString('utf8')));
134
+ throw new Error(refused(url, method, answer.status, received.toString('utf8'), false));
131
135
  }
132
136
  async function sent(url, bytes, operation) {
133
137
  const headers = { 'content-type': 'application/octet-stream', 'x-house-byte-operation': operation };
@@ -149,23 +153,50 @@ async function download(args) {
149
153
  await writeFile(written, bytes);
150
154
  return written;
151
155
  }
156
+ async function described(file) {
157
+ const bytes = await localFile(file);
158
+ const mediaType = MEDIA_TYPES.get(extname(file).slice(1).toLowerCase());
159
+ return {
160
+ bytes,
161
+ description: {
162
+ bytes: bytes.length,
163
+ sha256: createHash('sha256').update(bytes).digest('hex'),
164
+ ...(mediaType === undefined ? {} : { media_type: mediaType }),
165
+ },
166
+ };
167
+ }
152
168
  async function upload({ file, ...destination }) {
153
169
  if (typeof file !== 'string' || file === '')
154
170
  throw new Error('upload needs "file", a local file');
155
- const bytes = await localFile(file);
156
- const mediaType = MEDIA_TYPES.get(extname(file).slice(1).toLowerCase());
157
- const answer = await called('upload', {
158
- ...destination,
159
- bytes: bytes.length,
160
- sha256: createHash('sha256').update(bytes).digest('hex'),
161
- ...(mediaType === undefined ? {} : { media_type: mediaType }),
162
- });
171
+ const { bytes, description } = await described(file);
172
+ const answer = await called('upload', { ...destination, ...description });
163
173
  const declared = parse(answer);
164
174
  if (declared.upload === undefined)
165
175
  return answer;
166
176
  const written = JSON.parse(await sent(declared.upload.url, bytes, declared.upload.operation));
167
177
  return stringify(written, { lineWidth: 0 }).trimEnd();
168
178
  }
179
+ async function appendRecord(args) {
180
+ const held = new Map();
181
+ const attachments = Array.isArray(args.attachments)
182
+ ? await Promise.all(args.attachments.map(async (attachment) => {
183
+ if (typeof attachment !== 'string')
184
+ return attachment;
185
+ const { bytes, description } = await described(attachment);
186
+ held.set(description.sha256, bytes);
187
+ return { name: basename(attachment), ...description };
188
+ }))
189
+ : args.attachments;
190
+ const record = { ...args, attachments };
191
+ for (;;) {
192
+ const answer = await called('append_record', record);
193
+ const { upload } = parse(answer);
194
+ if (upload === undefined)
195
+ return answer;
196
+ for (const grant of upload)
197
+ await sent(grant.url, held.get(grant.sha256), grant.operation);
198
+ }
199
+ }
169
200
  function unexpected(argument, usage) {
170
201
  return new Error(`unexpected argument ${argument}; ${usage}`);
171
202
  }
@@ -233,7 +264,7 @@ async function house(args) {
233
264
  return [...(tool.description === undefined ? [] : [tool.description]), JSON.stringify(tool.inputSchema)].join('\n');
234
265
  }
235
266
  const parsed = argumentsOf(argument);
236
- return verb === 'upload' ? upload(parsed) : called(verb, parsed);
267
+ return verb === 'upload' ? upload(parsed) : verb === 'append_record' ? appendRecord(parsed) : called(verb, parsed);
237
268
  }
238
269
  async function called(name, args) {
239
270
  const result = await mcp('tools/call', { name, arguments: args });
@@ -1,36 +1,11 @@
1
1
  import { HOUSE_REF } from './git.js';
2
- import { HOW_WE_WORK } from './skills.js';
2
+ import { readHowWeWork } from './skills.js';
3
3
  const SOURCE_LINE = "From House:";
4
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
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
6
  const READ_LINE = "Optional per-Room instructions: `/rooms/private/rooms/<handle>/AGENTS.md`.";
7
7
  const COPIES_LINE = `House Local copies; before work run \`house fetch\` and rebase onto \`${HOUSE_REF}\`; after it commit, then \`house push\`:`;
8
- const UNREADABLE = /^(?:\S+: )?(?:path_not_found|room_not_found|operation_denied)\b/;
9
- const FAILED = /^exit: [1-9]\d*$/;
10
- const CUT = /^stderr: shell: output_cut (\d+) of \d+ bytes; continue with: (.+)$/;
11
- function cutOf(lines) {
12
- const cut = CUT.exec(lines[0] ?? '');
13
- return cut !== null && Buffer.byteLength(lines.slice(1).join('\n')) === Number(cut[1]) ? cut : null;
14
- }
15
- async function howWeWork(bridge) {
16
- let document = '';
17
- for (let command = `cat ${HOW_WE_WORK}`; command !== undefined;) {
18
- const read = await bridge.tool('shell', { command });
19
- const text = read.content.map((content) => content.text).join('\n');
20
- const lines = text.split('\n');
21
- if (read.isError === true || (FAILED.test(lines[0] ?? '') && lines[1]?.startsWith('stderr: '))) {
22
- const refusal = read.isError === true ? text : lines.slice(1).join('\n').replace(/^stderr: /gm, '');
23
- if (UNREADABLE.test(refusal))
24
- return '';
25
- throw new Error(refusal);
26
- }
27
- const cut = cutOf(lines);
28
- document += lines.slice(cut === null ? 0 : 1).join('\n');
29
- command = cut?.[2];
30
- }
31
- return document.replace(/\n$/, '');
32
- }
33
8
  export async function instructions(bridge, base, copies) {
34
9
  const lines = [SOURCE_LINE, HOUSE_LINE, FILE_LINE, READ_LINE, ...(copies.length === 0 ? [] : [COPIES_LINE, ...copies])];
35
- return [lines.join('\n'), base, await howWeWork(bridge)].filter((part) => part !== '').join('\n\n');
10
+ return [lines.join('\n'), base, (await readHowWeWork(bridge)).replace(/\n$/, '')].filter((part) => part !== '').join('\n\n');
36
11
  }
package/dist/login.js CHANGED
@@ -96,28 +96,27 @@ export async function login(argv) {
96
96
  open(started.start_url);
97
97
  exchanged = await retrying(() => post(house, '/kit/token', { login: started.login, code_verifier: verifier }), undefined, () => POLL_INTERVAL_MS);
98
98
  }
99
- try {
100
- await placeSkillSet();
101
- }
102
- catch (error) {
103
- process.stderr.write(`kit: the skills could not be placed: ${error.message}\n`);
104
- }
99
+ await placeSkillSet();
105
100
  await writeCodexRule().catch((error) => process.stderr.write(`kit: ${error.message}\n`));
101
+ const relayRecord = relay === undefined ? {} : { relay };
106
102
  await writeEnrolment({
107
103
  house,
108
- ...(relay === undefined ? {} : { relay }),
104
+ ...relayRecord,
109
105
  environment: exchanged.environment,
110
106
  credential: exchanged.credential,
111
107
  });
112
108
  process.stdout.write(`Environment ${exchanged.environment} is connected to ${house}.\n`);
113
109
  if (exchanged.headless_credential !== undefined) {
114
- await writeOwnAgent({ house, user: exchanged.user, credential: exchanged.headless_credential });
110
+ await writeOwnAgent({ house, ...relayRecord, user: exchanged.user, credential: exchanged.headless_credential });
115
111
  }
116
112
  else if (held && ownAgent !== null && ownAgent.user !== exchanged.user) {
117
113
  await forgetOwnAgent();
118
114
  process.stdout.write("This computer's own agent was another User's; run kit login again to connect it to you.\n");
119
115
  }
120
- await placeAppGit(ownAgent, await readOwnAgent());
116
+ else if (held && ownAgent !== null) {
117
+ await writeOwnAgent({ house, ...relayRecord, user: ownAgent.user, credential: ownAgent.credential });
118
+ }
119
+ await placeAppGit(ownAgent, await readOwnAgent()).catch((error) => process.stderr.write(`kit: ${error.message}\n`));
121
120
  try {
122
121
  startService();
123
122
  }
package/dist/skills.js CHANGED
@@ -1,17 +1,30 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { readFileSync } from 'node:fs';
2
- import { cp, lstat, mkdir, readdir, realpath, rm, symlink, writeFile } from 'node:fs/promises';
3
+ import { cp, lstat, mkdir, readdir, realpath, rm, symlink } from 'node:fs/promises';
3
4
  import { homedir } from 'node:os';
4
5
  import { join, relative } from 'node:path';
5
6
  import { fileURLToPath } from 'node:url';
7
+ import { parse } from 'yaml';
6
8
  import { KIT_VERSION } from './clis.js';
7
9
  import { kitHome, readConfiguration, readHome, writeHome } from './home.js';
8
10
  import { refusalOf } from './refusals.js';
9
- export const HOW_WE_WORK = '/rooms/private/how-we-work.md';
11
+ const HOW_WE_WORK = '/rooms/private/how-we-work.md';
10
12
  const SKILL_SET = fileURLToPath(new URL('../skills/', import.meta.url));
11
13
  const HOW_WE_WORK_TEXT = readFileSync(new URL('../how-we-work.md', import.meta.url), 'utf8');
14
+ const SHIPPED_HOW_WE_WORK = [
15
+ 'ae4dda60da7b174f22dc5fa9bf07fc27c25864932a74230de4f9f509ddd91f41',
16
+ '0bdcd43e1603ba6673a3cae56d70bbf90657cbeefd124e91a47be2ca1cbd6c93',
17
+ '4106a87ec66da6f7b2a10e74d18e6559d68d5f4fe7415dae68f5c7bdb9081968',
18
+ 'ac814a2197038f037b657bb719340395f89f319ac9fcdd386629c3fe2cec68f1',
19
+ ];
12
20
  const RECORD = 'skill-set.json';
13
- const CREATED = 'how-we-work-created';
21
+ const WRITTEN = 'how-we-work.json';
14
22
  const UNWRITABLE = new Set(['room_not_found', 'operation_denied']);
23
+ const PRESENT = new Set(['path_exists']);
24
+ const CHANGED = new Set(['base_revision_stale', 'path_absent']);
25
+ const UNREADABLE = /^(?:\S+: )?(?:path_not_found|room_not_found|operation_denied)\b/;
26
+ const FAILED = /^exit: [1-9]\d*$/;
27
+ const CUT = /^stderr: shell: output_cut (\d+) of \d+ bytes; continue with: (.+)$/;
15
28
  function skillsDirectory() {
16
29
  return join(homedir(), '.agents', 'skills');
17
30
  }
@@ -49,6 +62,14 @@ async function install() {
49
62
  await writeHome(RECORD, { version: KIT_VERSION, names });
50
63
  }
51
64
  export async function placeSkillSet() {
65
+ try {
66
+ await replaceSkillSet();
67
+ }
68
+ catch (error) {
69
+ process.stderr.write(`kit: the skills could not be placed: ${error.message}\n`);
70
+ }
71
+ }
72
+ async function replaceSkillSet() {
52
73
  const accepted = (await readConfiguration()).skills;
53
74
  const installed = await readHome(RECORD);
54
75
  if (accepted && installed?.version === KIT_VERSION)
@@ -62,17 +83,64 @@ export async function placeSkillSet() {
62
83
  else
63
84
  await rm(join(kitHome(), RECORD), { force: true });
64
85
  }
86
+ function sha256(text) {
87
+ return createHash('sha256').update(text).digest('hex');
88
+ }
89
+ function cutOf(lines) {
90
+ const cut = CUT.exec(lines[0] ?? '');
91
+ return cut !== null && Buffer.byteLength(lines.slice(1).join('\n')) === Number(cut[1]) ? cut : null;
92
+ }
93
+ export async function readHowWeWork(bridge) {
94
+ let document = '';
95
+ for (let command = `cat ${HOW_WE_WORK}`; command !== undefined;) {
96
+ const read = await bridge.tool('shell', { command });
97
+ const text = read.content.map((content) => content.text).join('\n');
98
+ const lines = text.split('\n');
99
+ if (read.isError === true || (FAILED.test(lines[0] ?? '') && lines[1]?.startsWith('stderr: '))) {
100
+ const refusal = read.isError === true ? text : lines.slice(1).join('\n').replace(/^stderr: /gm, '');
101
+ if (UNREADABLE.test(refusal))
102
+ return '';
103
+ throw new Error(refusal);
104
+ }
105
+ const cut = cutOf(lines);
106
+ document += lines.slice(cut === null ? 0 : 1).join('\n');
107
+ command = cut?.[2];
108
+ }
109
+ return document;
110
+ }
111
+ async function currentRevision(bridge) {
112
+ const inspected = await bridge.tool('inspect', { path: HOW_WE_WORK });
113
+ const text = inspected.content.map((content) => content.text).join('\n');
114
+ if (inspected.isError === true)
115
+ throw new Error(text);
116
+ return parse(text).revisions[0].revision;
117
+ }
118
+ async function edit(bridge, change, kept) {
119
+ const edited = await bridge.tool('edit', { changes: [{ ...change, path: HOW_WE_WORK, content: HOW_WE_WORK_TEXT }] });
120
+ if (edited.isError !== true)
121
+ return 'written';
122
+ const text = edited.content.map((content) => content.text).join('\n');
123
+ const refusal = refusalOf(text);
124
+ if (UNWRITABLE.has(refusal?.code ?? ''))
125
+ return 'unwritable';
126
+ if (refusal?.code === 'edit_conflict' && refusal.conflicts.some((conflict) => kept.has(conflict)))
127
+ return 'kept';
128
+ throw new Error(text);
129
+ }
65
130
  export async function createHowWeWork(bridge) {
66
- if ((await readHome(RECORD)) === null || (await present(join(kitHome(), CREATED))))
131
+ if (!(await readConfiguration()).skills || (await readHome(RECORD))?.version === undefined)
132
+ return;
133
+ const current = sha256(HOW_WE_WORK_TEXT);
134
+ if ((await readHome(WRITTEN)) === current)
135
+ return;
136
+ const created = await edit(bridge, { op: 'create' }, PRESENT);
137
+ if (created === 'unwritable')
67
138
  return;
68
- const created = await bridge.tool('edit', { changes: [{ op: 'create', path: HOW_WE_WORK, content: HOW_WE_WORK_TEXT }] });
69
- if (created.isError === true) {
70
- const text = created.content.map((content) => content.text).join('\n');
71
- const refusal = refusalOf(text);
72
- if (UNWRITABLE.has(refusal?.code ?? ''))
139
+ if (created === 'kept') {
140
+ const base = await currentRevision(bridge);
141
+ const held = sha256(await readHowWeWork(bridge));
142
+ if (held !== current && SHIPPED_HOW_WE_WORK.includes(held) && (await edit(bridge, { op: 'replace', base }, CHANGED)) === 'unwritable')
73
143
  return;
74
- if (refusal?.code !== 'edit_conflict' || !refusal.conflicts.includes('path_exists'))
75
- throw new Error(text);
76
144
  }
77
- await writeFile(join(kitHome(), CREATED), '');
145
+ await writeHome(WRITTEN, current);
78
146
  }
@@ -49,10 +49,10 @@ async function executed(command, args, signal) {
49
49
  function present(path) {
50
50
  return stat(path).then(() => true, () => false);
51
51
  }
52
- async function cpus() {
52
+ async function threads() {
53
53
  const [quota, period] = (await readFile('/sys/fs/cgroup/cpu.max', 'utf8').catch(() => 'max')).split(' ');
54
54
  const allowed = quota === 'max' ? Infinity : Math.ceil(Number(quota) / Number(period));
55
- return Math.min(availableParallelism(), allowed);
55
+ return Math.max(1, Math.floor(Math.min(availableParallelism(), allowed) / 2));
56
56
  }
57
57
  async function sha256(path) {
58
58
  const hash = createHash('sha256');
@@ -140,7 +140,7 @@ async function transcribed(path, signal) {
140
140
  await executed(installed(pinned.decoder), ['-nostdin', '-hide_banner', '-loglevel', 'error', '-i', path, '-ar', '16000', '-ac', '1', '-c:a', 'pcm_s16le',
141
141
  '-f', 'segment', '-segment_time', String(CHUNK_SECONDS), join(work, '%05d.wav')], signal);
142
142
  const parts = (await readdir(work)).sort().flatMap((part) => ['-f', join(work, part)]);
143
- const text = await executed(installed(pinned.engine), ['-t', String(await cpus()), '-np', '-m', installed(MODEL), ...parts], signal);
143
+ const text = await executed(installed(pinned.engine), ['-t', String(await threads()), '-np', '-m', installed(MODEL), ...parts], signal);
144
144
  return text
145
145
  .split('\n')
146
146
  .map((line) => line.trim())
package/dist/update.js CHANGED
@@ -75,6 +75,7 @@ function rerun(script, inputId, flags) {
75
75
  '--quiet',
76
76
  `--unit=${unit(inputId)}`,
77
77
  `--setenv=HOME=${homedir()}`,
78
+ `--setenv=HOUSE_KIT_HOME=${kitHome()}`,
78
79
  '/bin/sh',
79
80
  '-c',
80
81
  record(),
package/how-we-work.md CHANGED
@@ -8,16 +8,24 @@ The skills are already set up; never run `setup-matt-pocock-skills`.
8
8
 
9
9
  ## Where skill output goes
10
10
 
11
- - Glossary (`CONTEXT.md`): `glossary.md`
11
+ - Glossary (`GLOSSARY.md`): `glossary.md`
12
12
  - Decisions (ADRs): `decisions/NNNN-<slug>.md`
13
13
  - Plan (spec): `projects/<name>/plan.md`
14
14
  - Tasks (tickets): `projects/<name>/tasks/NN-<slug>.md`, with a
15
15
  `Status:` line (`needs-triage`, `needs-info`, `ready-for-agent`,
16
- `ready-for-human`, `wontfix`), a `Blocked by:` line and comments under
17
- `## Comments`
16
+ `ready-for-human`, `wontfix`, or `done` once implemented), a
17
+ `Category:` line (`bug`, `enhancement`), a `Blocked by:` line and comments
18
+ under `## Comments`; triage takes no pull requests
19
+ - Rejected requests (triage): `out-of-scope/<concept>.md`
20
+ - Implementation notes (implement-spec): `projects/<name>/notes/`
21
+ - PR bodies (pr): `projects/<name>/prs/<branch>.md`
18
22
  - Map (Wayfinding): `projects/<name>/map.md`, its tasks under
19
23
  `tasks/` with `Type:`, `Status: claimed|resolved` and `Blocked by:` lines
20
24
  - Research: `research/<slug>.md`
25
+ - Architecture reports (improve-codebase-architecture): `architecture/<slug>.html`
26
+ - Handoffs: `handoffs/<slug>.md`
21
27
  - Questionnaires: `questionnaires/<slug>.md`
28
+ - Retrospectives (retro): `retros/<date>-<slug>.md`
29
+ - Wizards: `wizards/<slug>.sh`; the values they capture stay out of the Room
22
30
  - Learning: `learning/<topic>/`
23
31
  - Prototype: one HTML file with every variant on one page.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentshouse/kit",
3
- "version": "0.2.1-c39de3ed26c1b",
3
+ "version": "0.2.1-c3db1f18aed8f",
4
4
  "description": "House Kit connects an Environment to agents.house.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -23,6 +23,7 @@
23
23
  "scripts": {
24
24
  "build": "tsc -p tsconfig.build.json && node scripts/bootstrap-manifest.mjs",
25
25
  "engine": "mkdir -p tmp/engine && docker run --rm -v \"$PWD:/kit\" -w /kit alpine:3.24@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6 sh scripts/engine.sh tmp/engine",
26
+ "sync-skills": "node scripts/sync-skills.mjs",
26
27
  "test": "tsc -p tsconfig.json && vitest run"
27
28
  },
28
29
  "dependencies": {