@agentshouse/kit 0.2.1-alpha.1 → 0.2.1-alpha.10

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 (109) hide show
  1. package/README.md +20 -2
  2. package/dist/acp.js +3 -3
  3. package/dist/agents.js +6 -0
  4. package/dist/api.js +2 -0
  5. package/dist/authority-main.js +22 -0
  6. package/dist/bootstrap.json +28 -0
  7. package/dist/bridge.js +133 -43
  8. package/dist/clis.js +5 -1
  9. package/dist/configure-main.js +6 -0
  10. package/dist/conversations.js +37 -14
  11. package/dist/copies.js +796 -0
  12. package/dist/enrol-main.js +4 -0
  13. package/dist/git-main.js +153 -0
  14. package/dist/git.js +81 -0
  15. package/dist/home.js +19 -6
  16. package/dist/house-main.js +30 -15
  17. package/dist/instructions.js +8 -3
  18. package/dist/kit-main.js +2 -0
  19. package/dist/login.js +6 -2
  20. package/dist/operation.js +9 -0
  21. package/dist/resident.js +62 -7
  22. package/dist/sign-in.js +6 -2
  23. package/dist/skills.js +83 -0
  24. package/dist/translate.js +51 -0
  25. package/dist/update.js +116 -0
  26. package/how-we-work.md +25 -0
  27. package/package.json +11 -4
  28. package/skills/ask-matt/LICENSE +21 -0
  29. package/skills/ask-matt/PHASE-BOUNDARIES.md +55 -0
  30. package/skills/ask-matt/SKILL.md +91 -0
  31. package/skills/ask-matt/agents/openai.yaml +4 -0
  32. package/skills/code-review/LICENSE +21 -0
  33. package/skills/code-review/SKILL.md +89 -0
  34. package/skills/code-review/agents/openai.yaml +3 -0
  35. package/skills/codebase-design/DEEPENING.md +37 -0
  36. package/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  37. package/skills/codebase-design/LICENSE +21 -0
  38. package/skills/codebase-design/SKILL.md +116 -0
  39. package/skills/codebase-design/agents/openai.yaml +3 -0
  40. package/skills/diagnosing-bugs/LICENSE +21 -0
  41. package/skills/diagnosing-bugs/SKILL.md +140 -0
  42. package/skills/diagnosing-bugs/agents/openai.yaml +3 -0
  43. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
  44. package/skills/domain-modeling/ADR-FORMAT.md +47 -0
  45. package/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  46. package/skills/domain-modeling/LICENSE +21 -0
  47. package/skills/domain-modeling/SKILL.md +76 -0
  48. package/skills/domain-modeling/agents/openai.yaml +3 -0
  49. package/skills/grill-me/LICENSE +21 -0
  50. package/skills/grill-me/SKILL.md +8 -0
  51. package/skills/grill-me/agents/openai.yaml +4 -0
  52. package/skills/grill-with-docs/LICENSE +21 -0
  53. package/skills/grill-with-docs/SKILL.md +8 -0
  54. package/skills/grill-with-docs/agents/openai.yaml +4 -0
  55. package/skills/grilling/LICENSE +21 -0
  56. package/skills/grilling/SKILL.md +30 -0
  57. package/skills/grilling/agents/openai.yaml +3 -0
  58. package/skills/handoff/LICENSE +21 -0
  59. package/skills/handoff/SKILL.md +17 -0
  60. package/skills/handoff/agents/openai.yaml +4 -0
  61. package/skills/implement/LICENSE +21 -0
  62. package/skills/implement/SKILL.md +16 -0
  63. package/skills/implement/agents/openai.yaml +4 -0
  64. package/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
  65. package/skills/improve-codebase-architecture/LICENSE +21 -0
  66. package/skills/improve-codebase-architecture/SKILL.md +72 -0
  67. package/skills/improve-codebase-architecture/agents/openai.yaml +4 -0
  68. package/skills/prototype/LICENSE +21 -0
  69. package/skills/prototype/LOGIC.md +67 -0
  70. package/skills/prototype/SKILL.md +28 -0
  71. package/skills/prototype/UI.md +112 -0
  72. package/skills/prototype/agents/openai.yaml +3 -0
  73. package/skills/research/LICENSE +21 -0
  74. package/skills/research/SKILL.md +14 -0
  75. package/skills/research/agents/openai.yaml +3 -0
  76. package/skills/resolving-merge-conflicts/LICENSE +21 -0
  77. package/skills/resolving-merge-conflicts/SKILL.md +16 -0
  78. package/skills/resolving-merge-conflicts/agents/openai.yaml +3 -0
  79. package/skills/tdd/LICENSE +21 -0
  80. package/skills/tdd/SKILL.md +40 -0
  81. package/skills/tdd/agents/openai.yaml +3 -0
  82. package/skills/tdd/mocking.md +59 -0
  83. package/skills/tdd/tests.md +77 -0
  84. package/skills/teach/GLOSSARY-FORMAT.md +35 -0
  85. package/skills/teach/LEARNING-RECORD-FORMAT.md +46 -0
  86. package/skills/teach/LICENSE +21 -0
  87. package/skills/teach/MISSION-FORMAT.md +31 -0
  88. package/skills/teach/RESOURCES-FORMAT.md +32 -0
  89. package/skills/teach/SKILL.md +141 -0
  90. package/skills/teach/agents/openai.yaml +4 -0
  91. package/skills/to-questionnaire/LICENSE +21 -0
  92. package/skills/to-questionnaire/SKILL.md +55 -0
  93. package/skills/to-questionnaire/agents/openai.yaml +4 -0
  94. package/skills/to-spec/LICENSE +21 -0
  95. package/skills/to-spec/SKILL.md +76 -0
  96. package/skills/to-spec/agents/openai.yaml +4 -0
  97. package/skills/to-tickets/LICENSE +21 -0
  98. package/skills/to-tickets/SKILL.md +106 -0
  99. package/skills/to-tickets/agents/openai.yaml +4 -0
  100. package/skills/wait-what/LICENSE +21 -0
  101. package/skills/wait-what/SKILL.md +8 -0
  102. package/skills/wait-what/agents/openai.yaml +4 -0
  103. package/skills/wayfinder/LICENSE +21 -0
  104. package/skills/wayfinder/SKILL.md +129 -0
  105. package/skills/wayfinder/agents/openai.yaml +4 -0
  106. package/skills/writing-for-agents/LICENSE +21 -0
  107. package/skills/writing-for-agents/SKILL-MECHANICS.md +22 -0
  108. package/skills/writing-for-agents/SKILL.md +83 -0
  109. package/skills/writing-for-agents/agents/openai.yaml +3 -0
@@ -0,0 +1,4 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { writeEnrolment } from './home.js';
3
+ const [house, environment] = process.argv.slice(2);
4
+ await writeEnrolment({ house: house, environment: environment, credential: readFileSync(0, 'utf8').trim() });
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env node
2
+ import { spawn, spawnSync } from 'node:child_process';
3
+ import { accessSync, constants, realpathSync, statSync } from 'node:fs';
4
+ import { request } from 'node:http';
5
+ import { constants as system } from 'node:os';
6
+ import { delimiter, dirname, join } from 'node:path';
7
+ import { WORKING_COPIES } from './git.js';
8
+ import { kitHome } from './home.js';
9
+ const VALUED = new Set(['-C', '-c', '--git-dir', '--work-tree', '--namespace', '--config-env', '--attr-source', '--super-prefix']);
10
+ const READS = new Set([
11
+ 'blame',
12
+ 'branch',
13
+ 'cat-file',
14
+ 'check-ignore',
15
+ 'config',
16
+ 'describe',
17
+ 'diff',
18
+ 'fetch',
19
+ 'for-each-ref',
20
+ 'grep',
21
+ 'help',
22
+ 'log',
23
+ 'ls-files',
24
+ 'ls-tree',
25
+ 'reflog',
26
+ 'remote',
27
+ 'rev-list',
28
+ 'rev-parse',
29
+ 'shortlog',
30
+ 'show',
31
+ 'show-ref',
32
+ 'status',
33
+ 'tag',
34
+ 'version',
35
+ ]);
36
+ const RELAYED = ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT'];
37
+ function nativeGit() {
38
+ const shim = join(kitHome(), 'shim');
39
+ for (const directory of (process.env.PATH ?? '').split(delimiter)) {
40
+ if (directory === '')
41
+ continue;
42
+ try {
43
+ if (realpathSync(directory) === realpathSync(shim))
44
+ continue;
45
+ const candidate = join(directory, 'git');
46
+ if (!statSync(candidate).isFile())
47
+ continue;
48
+ accessSync(candidate, constants.X_OK);
49
+ return candidate;
50
+ }
51
+ catch { }
52
+ }
53
+ return null;
54
+ }
55
+ function captured(git, args) {
56
+ const ran = spawnSync(git, args, {
57
+ stdio: ['ignore', 'pipe', 'ignore'],
58
+ maxBuffer: Infinity,
59
+ env: { ...process.env, LC_ALL: 'C' },
60
+ });
61
+ return ran.status === 0 ? ran.stdout.toString('utf8') : null;
62
+ }
63
+ function delegated(git, args) {
64
+ return new Promise((resolve) => {
65
+ const child = spawn(git, args, { stdio: 'inherit' });
66
+ const relay = (signal) => child.kill(signal);
67
+ for (const signal of RELAYED)
68
+ process.on(signal, relay);
69
+ const settle = (outcome) => {
70
+ for (const signal of RELAYED)
71
+ process.off(signal, relay);
72
+ resolve(outcome);
73
+ };
74
+ child.once('error', (error) => {
75
+ process.stderr.write(`git: ${error.message}\n`);
76
+ settle({ status: 127, signal: null });
77
+ });
78
+ child.once('close', (status, signal) => settle({ status, signal }));
79
+ });
80
+ }
81
+ function staged(listing) {
82
+ const entries = new Map();
83
+ for (const record of (listing ?? '').split('\0')) {
84
+ const tab = record.indexOf('\t');
85
+ if (tab >= 0)
86
+ entries.set(record.slice(tab + 1), record.slice(0, tab).split(' ')[1]);
87
+ }
88
+ return entries;
89
+ }
90
+ function moved(planned, before, after) {
91
+ const moves = [];
92
+ for (const line of planned.split('\n')) {
93
+ if (!line.startsWith('Renaming '))
94
+ continue;
95
+ const renaming = line.slice('Renaming '.length);
96
+ for (let at = renaming.indexOf(' to '); at >= 0; at = renaming.indexOf(' to ', at + 1)) {
97
+ const from = renaming.slice(0, at);
98
+ const to = renaming.slice(at + 4);
99
+ if (before.has(from) && !after.has(from) && after.get(to) === before.get(from)) {
100
+ moves.push({ from, to });
101
+ break;
102
+ }
103
+ }
104
+ }
105
+ return moves;
106
+ }
107
+ function reported(socketPath, body) {
108
+ return new Promise((resolve) => {
109
+ const sent = request({ socketPath, path: '/git/observed', method: 'POST' }, (answer) => {
110
+ answer.resume();
111
+ answer.on('end', resolve);
112
+ });
113
+ sent.on('error', () => resolve());
114
+ sent.end(JSON.stringify(body));
115
+ });
116
+ }
117
+ async function observed(git, args) {
118
+ let index = 0;
119
+ while (index < args.length && args[index].startsWith('-'))
120
+ index += VALUED.has(args[index]) ? 2 : 1;
121
+ const globals = args.slice(0, index);
122
+ const name = args[index];
123
+ const bridge = process.env.HOUSE_BRIDGE;
124
+ if (bridge === undefined || name === undefined || READS.has(name))
125
+ return delegated(git, args);
126
+ const located = captured(git, [...globals, 'rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
127
+ const [common, worktree] = located?.split('\n') ?? [];
128
+ if (common === undefined || worktree === undefined || !dirname(common).startsWith(`${WORKING_COPIES}/`)) {
129
+ return delegated(git, args);
130
+ }
131
+ const listing = [...globals, '-C', worktree, 'ls-files', '--stage', '-z'];
132
+ const before = name === 'mv' ? staged(captured(git, listing)) : null;
133
+ const planned = name === 'mv' ? (captured(git, [...globals, 'mv', '--dry-run', ...args.slice(index + 1)]) ?? '') : '';
134
+ const outcome = await delegated(git, args);
135
+ const moves = before !== null && outcome.status === 0 ? moved(planned, before, staged(captured(git, listing))) : [];
136
+ await reported(bridge, { common, worktree, moves });
137
+ return outcome;
138
+ }
139
+ const git = nativeGit();
140
+ if (git === null) {
141
+ process.stderr.write('git: no installed Git found on PATH\n');
142
+ process.exitCode = 127;
143
+ }
144
+ else {
145
+ observed(git, process.argv.slice(2)).then(({ status, signal }) => {
146
+ if (signal === null) {
147
+ process.exitCode = status ?? 1;
148
+ return;
149
+ }
150
+ process.exitCode = 128 + system.signals[signal];
151
+ process.kill(process.pid, signal);
152
+ });
153
+ }
package/dist/git.js ADDED
@@ -0,0 +1,81 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { withoutProxy } from './clis.js';
4
+ export const WORKING_COPIES = '/agents/house/working-copies';
5
+ export const HOUSE_REF = 'refs/house/received';
6
+ const COMMITTER = 'House Kit <house-kit@localhost>';
7
+ export function run(cwd, args, input, env = {}) {
8
+ return new Promise((resolve, reject) => {
9
+ const child = spawn('git', args, { cwd, env: { ...withoutProxy(), ...env } });
10
+ const stdout = [];
11
+ let stderr = '';
12
+ child.stdout.on('data', (chunk) => stdout.push(chunk));
13
+ child.stderr.on('data', (chunk) => {
14
+ stderr += chunk.toString('utf8');
15
+ });
16
+ child.on('error', reject);
17
+ child.on('close', (status) => resolve({ status, stdout: Buffer.concat(stdout), stderr }));
18
+ child.stdin.on('error', () => undefined);
19
+ child.stdin.end(input);
20
+ });
21
+ }
22
+ export async function git(cwd, args, input, env) {
23
+ const ran = await run(cwd, args, input, env);
24
+ if (ran.status !== 0)
25
+ throw new Error(`git ${args[0]} failed: ${ran.stderr.trim()}`);
26
+ return ran.stdout.toString('utf8');
27
+ }
28
+ export async function succeeded(cwd, args, env) {
29
+ return (await run(cwd, args, undefined, env)).status === 0;
30
+ }
31
+ export async function revision(cwd, name) {
32
+ return (await git(cwd, ['rev-parse', '--verify', `${name}^{commit}`])).trim();
33
+ }
34
+ export async function tree(repository, commit) {
35
+ const listed = await git(repository, ['ls-tree', '-r', '-z', commit]);
36
+ const entries = new Map();
37
+ for (const entry of listed.split('\0')) {
38
+ const tab = entry.indexOf('\t');
39
+ if (tab < 0)
40
+ continue;
41
+ const [, type, object] = entry.slice(0, tab).split(' ');
42
+ if (type === 'blob')
43
+ entries.set(entry.slice(tab + 1), object);
44
+ }
45
+ return entries;
46
+ }
47
+ export async function blobText(repository, object, path) {
48
+ const bytes = (await run(repository, ['cat-file', 'blob', object])).stdout;
49
+ try {
50
+ return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
51
+ }
52
+ catch {
53
+ throw new Error(`${path} is not UTF-8 text`);
54
+ }
55
+ }
56
+ export function blobId(content) {
57
+ const bytes = Buffer.from(content, 'utf8');
58
+ return createHash('sha1').update(`blob ${bytes.length}\0`).update(bytes).digest('hex');
59
+ }
60
+ function quoted(path) {
61
+ return `"${path.replace(/[\\"]/g, '\\$&').replace(/\n/g, '\\n')}"`;
62
+ }
63
+ function data(text) {
64
+ const bytes = Buffer.from(text, 'utf8');
65
+ return [Buffer.from(`data ${bytes.length}\n`), bytes, Buffer.from('\n')];
66
+ }
67
+ export async function importCommits(repository, commits) {
68
+ const stream = [];
69
+ for (const [index, commit] of commits.entries()) {
70
+ stream.push(Buffer.from(`commit ${HOUSE_REF}\nmark :${index + 1}\ncommitter ${COMMITTER} ${Math.floor(Date.now() / 1000)} +0000\n`), ...data(commit.message), ...(commit.from === null ? [] : [Buffer.from(`from ${commit.from}\n`)]), ...(commit.merge === undefined ? [] : [Buffer.from(`merge ${commit.merge}\n`)]), ...(commit.replaces === true ? [Buffer.from('deleteall\n')] : []));
71
+ for (const [path, content] of commit.files) {
72
+ if (content === null)
73
+ stream.push(Buffer.from(`D ${quoted(path)}\n`));
74
+ else
75
+ stream.push(Buffer.from(`M 100644 inline ${quoted(path)}\n`), ...data(content));
76
+ }
77
+ stream.push(Buffer.from('\n'));
78
+ }
79
+ await git(repository, ['fast-import', '--quiet', '--force'], Buffer.concat(stream));
80
+ return revision(repository, HOUSE_REF);
81
+ }
package/dist/home.js CHANGED
@@ -2,12 +2,13 @@ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
2
2
  import { homedir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  const ENROLMENT = 'credential.json';
5
+ const CONFIGURATION = 'kit.json';
5
6
  export function kitHome() {
6
7
  return process.env.HOUSE_KIT_HOME ?? join(homedir(), '.house-kit');
7
8
  }
8
- export async function readEnrolment() {
9
+ export async function readHome(name) {
9
10
  try {
10
- return JSON.parse(await readFile(join(kitHome(), ENROLMENT), 'utf8'));
11
+ return JSON.parse(await readFile(join(kitHome(), name), 'utf8'));
11
12
  }
12
13
  catch (error) {
13
14
  if (error.code === 'ENOENT')
@@ -15,10 +16,22 @@ export async function readEnrolment() {
15
16
  throw error;
16
17
  }
17
18
  }
18
- export async function writeEnrolment(enrolment) {
19
+ export async function writeHome(name, value) {
19
20
  const home = kitHome();
20
21
  await mkdir(home, { recursive: true, mode: 0o700 });
21
- const temporary = join(home, `${ENROLMENT}.${process.pid}`);
22
- await writeFile(temporary, `${JSON.stringify(enrolment)}\n`, { mode: 0o600 });
23
- await rename(temporary, join(home, ENROLMENT));
22
+ const temporary = join(home, `${name}.${process.pid}`);
23
+ await writeFile(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600 });
24
+ await rename(temporary, join(home, name));
25
+ }
26
+ export function readEnrolment() {
27
+ return readHome(ENROLMENT);
28
+ }
29
+ export function writeEnrolment(enrolment) {
30
+ return writeHome(ENROLMENT, enrolment);
31
+ }
32
+ export async function readConfiguration() {
33
+ return { skills: (await readHome(CONFIGURATION))?.skills !== false };
34
+ }
35
+ export function writeConfiguration(configuration) {
36
+ return writeHome(CONFIGURATION, configuration);
24
37
  }
@@ -2,12 +2,15 @@
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
3
  import { readFile } from 'node:fs/promises';
4
4
  import { request } from 'node:http';
5
- import { basename } from 'node:path';
5
+ import { basename, join } from 'node:path';
6
+ import { kitHome } from './home.js';
6
7
  const USAGE = "usage: house <tool> ['<arguments as JSON>']";
8
+ const GIT_HELP = 'Submit a Room commit to House; default HEAD. Use --owner only outside an Agent conversation. Native git push is not a House remote.';
9
+ const GIT_USAGE = `usage: house git push [commit] [--owner]\n${GIT_HELP}`;
7
10
  const socketPath = process.env.HOUSE_BRIDGE;
8
- function bridged(path, body) {
11
+ function bridged(path, body, headers = { 'content-type': 'application/json' }, to = socketPath) {
9
12
  return new Promise((resolve, reject) => {
10
- const sent = request({ socketPath, path, method: 'POST', headers: { 'content-type': 'application/json' } }, (answer) => {
13
+ const sent = request({ socketPath: to, path, method: 'POST', headers }, (answer) => {
11
14
  let text = '';
12
15
  answer.setEncoding('utf8');
13
16
  answer.on('data', (chunk) => {
@@ -21,7 +24,7 @@ function bridged(path, body) {
21
24
  });
22
25
  });
23
26
  sent.on('error', reject);
24
- sent.end(JSON.stringify(body));
27
+ sent.end(body instanceof Buffer ? body : JSON.stringify(body));
25
28
  });
26
29
  }
27
30
  async function mcp(method, params) {
@@ -42,19 +45,30 @@ async function upload(path, roomRef) {
42
45
  bytes: bytes.length,
43
46
  sha256: createHash('sha256').update(bytes).digest('hex'),
44
47
  }));
45
- const answer = await fetch(declared.upload.url, {
46
- method: 'POST',
47
- headers: { 'content-type': 'application/octet-stream', 'x-house-byte-operation': declared.upload.operation },
48
- body: bytes,
48
+ return bridged(declared.upload.url, bytes, {
49
+ 'content-type': 'application/octet-stream',
50
+ 'x-house-byte-operation': declared.upload.operation,
49
51
  });
50
- const text = await answer.text();
51
- if (!answer.ok)
52
- throw new Error(`House refused the bytes of ${path} with ${answer.status}: ${text}`);
53
- return text;
52
+ }
53
+ async function gitPush(args) {
54
+ if (args.includes('--help'))
55
+ return GIT_USAGE;
56
+ if (args[0] !== 'push')
57
+ throw new Error(GIT_USAGE);
58
+ const owner = args.includes('--owner');
59
+ const commit = args.slice(1).find((arg) => arg !== '--owner') ?? 'HEAD';
60
+ if (owner && socketPath !== undefined)
61
+ throw new Error('house git push --owner runs outside an Agent conversation');
62
+ if (!owner && socketPath === undefined)
63
+ throw new Error('outside an Agent conversation, house git push needs --owner');
64
+ const pushed = JSON.parse(await bridged('/git/push', { cwd: process.cwd(), commit }, undefined, owner ? join(kitHome(), 'owner.sock') : socketPath));
65
+ if (pushed.refused)
66
+ throw new Error(pushed.text);
67
+ return pushed.text;
54
68
  }
55
69
  async function house([verb, argument]) {
56
70
  if (verb === undefined || verb === '--help') {
57
- return [USAGE, ...(await tools()).map((tool) => `${tool.name}: ${tool.description}`)].join('\n');
71
+ return [USAGE, `git push [commit]: ${GIT_HELP}`, ...(await tools()).map((tool) => `${tool.name}: ${tool.description}`)].join('\n');
58
72
  }
59
73
  if (argument === '--help') {
60
74
  const tool = (await tools()).find((listed) => listed.name === verb);
@@ -78,12 +92,13 @@ async function house([verb, argument]) {
78
92
  throw new Error(text);
79
93
  return text;
80
94
  }
81
- if (socketPath === undefined) {
95
+ const argv = process.argv.slice(2);
96
+ if (argv[0] !== 'git' && socketPath === undefined) {
82
97
  process.stderr.write('house: house runs inside an Agent conversation\n');
83
98
  process.exitCode = 1;
84
99
  }
85
100
  else {
86
- house(process.argv.slice(2)).then((text) => {
101
+ (argv[0] === 'git' ? gitPush(argv.slice(1)) : house(argv)).then((text) => {
87
102
  process.stdout.write(`${text}\n`);
88
103
  }, (error) => {
89
104
  process.stderr.write(`house: ${error instanceof Error ? error.message : String(error)}\n`);
@@ -1,6 +1,8 @@
1
+ import { readEnrolment } from './home.js';
2
+ import { HOW_WE_WORK } from './skills.js';
1
3
  const HOUSE_LINE = 'Work with House through the `house` CLI: run `house --help`.';
2
4
  const FILE_LINE = 'To give the User a file, upload it with `house upload_attachment` and link it in your answer.';
3
- const HOW_WE_WORK = '/private/library/how-we-work.md';
5
+ const COPIES_LINE = 'These House paths are Git working copies here; commit in one and run `house git push` to send the commit to House:';
4
6
  const UNREADABLE = /^(?:\S+: )?(?:path_not_found|room_not_found|operation_denied)\b/;
5
7
  const CUT = /^stderr: shell: output_cut (\d+) of \d+ bytes; continue with: (.+)$/;
6
8
  async function howWeWork(bridge) {
@@ -23,6 +25,9 @@ async function howWeWork(bridge) {
23
25
  }
24
26
  return document.replace(/\n$/, '');
25
27
  }
26
- export async function instructions(bridge, base) {
27
- return [`${HOUSE_LINE}\n${FILE_LINE}`, base, await howWeWork(bridge)].filter((part) => part !== '').join('\n\n');
28
+ export async function instructions(bridge, base, copies) {
29
+ const app = new URL('/app/new.git', (await readEnrolment()).house).href;
30
+ const appLine = `To show the User a page, upload it or push it to an App instead of starting a server; \`git clone ${app}\` starts a new App.`;
31
+ const lines = [HOUSE_LINE, FILE_LINE, appLine, ...(copies.length === 0 ? [] : [COPIES_LINE, ...copies])];
32
+ return [lines.join('\n'), base, await howWeWork(bridge)].filter((part) => part !== '').join('\n\n');
28
33
  }
package/dist/kit-main.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
+ import { setGlobalProxyFromEnv } from 'node:http';
2
3
  import { login } from './login.js';
3
4
  import { resident } from './resident.js';
4
5
  const [command, ...argv] = process.argv.slice(2);
6
+ setGlobalProxyFromEnv();
5
7
  async function run() {
6
8
  if (command === 'login')
7
9
  return login(argv);
package/dist/login.js CHANGED
@@ -26,8 +26,9 @@ function listen() {
26
26
  server.close();
27
27
  server.closeAllConnections();
28
28
  };
29
+ const port = Number(process.env.HOUSE_KIT_LOGIN_PORT ?? 0);
29
30
  server.once('error', reject);
30
- server.listen(0, '127.0.0.1', () => {
31
+ server.listen(port, port === 0 ? '127.0.0.1' : '0.0.0.0', () => {
31
32
  const address = server.address();
32
33
  if (address === null || typeof address === 'string') {
33
34
  reject(new Error('the login callback has no port'));
@@ -49,8 +50,11 @@ function listen() {
49
50
  }
50
51
  async function typed(question) {
51
52
  const reader = createInterface({ input: process.stdin, output: process.stdout });
53
+ const closed = new Promise((_, reject) => {
54
+ reader.once('close', () => reject(new Error('no code was typed')));
55
+ });
52
56
  try {
53
- return (await reader.question(question)).trim();
57
+ return (await Promise.race([reader.question(question), closed])).trim();
54
58
  }
55
59
  finally {
56
60
  reader.close();
@@ -0,0 +1,9 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ export function operationId() {
3
+ const bytes = randomBytes(16);
4
+ bytes.writeUIntBE(Date.now(), 0, 6);
5
+ bytes[6] = 0x70 | (bytes[6] & 0x0f);
6
+ bytes[8] = 0x80 | (bytes[8] & 0x3f);
7
+ const hex = bytes.toString('hex');
8
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
9
+ }
package/dist/resident.js CHANGED
@@ -1,24 +1,68 @@
1
+ import { once } from 'node:events';
2
+ import { mkdir, rm, writeFile } from 'node:fs/promises';
3
+ import { createServer } from 'node:http';
4
+ import { extname, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
1
6
  import { killDescendants } from './acp.js';
2
7
  import { Agents } from './agents.js';
3
8
  import { house } from './api.js';
9
+ import { received } from './bridge.js';
4
10
  import { Conversations } from './conversations.js';
5
- import { readEnrolment } from './home.js';
11
+ import { WorkingCopies } from './copies.js';
12
+ import { kitHome, readEnrolment } from './home.js';
6
13
  import { SignIns } from './sign-in.js';
14
+ import { placeSkillSet } from './skills.js';
7
15
  import { holdStream } from './stream.js';
16
+ import { Updates } from './update.js';
8
17
  function report(error) {
9
18
  process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
10
19
  }
11
20
  function logged(work) {
12
21
  work.catch(report);
13
22
  }
23
+ function quoted(value) {
24
+ return `'${value.replaceAll("'", `'\\''`)}'`;
25
+ }
26
+ async function installShim() {
27
+ const main = fileURLToPath(new URL(`./git-main${extname(fileURLToPath(import.meta.url))}`, import.meta.url));
28
+ await mkdir(join(kitHome(), 'shim'), { recursive: true });
29
+ await writeFile(join(kitHome(), 'shim', 'git'), `#!/bin/sh\nexec ${quoted(process.execPath)} ${quoted(main)} "$@"\n`, {
30
+ mode: 0o755,
31
+ });
32
+ }
33
+ async function serveOwner(copies) {
34
+ const socketPath = join(kitHome(), 'owner.sock');
35
+ await rm(socketPath, { force: true });
36
+ const server = createServer(async (request, response) => {
37
+ try {
38
+ if (request.url !== '/git/push') {
39
+ response.writeHead(404).end();
40
+ return;
41
+ }
42
+ const { cwd, commit } = JSON.parse(await received(request));
43
+ const pushed = await copies.push(cwd, commit, copies.owner);
44
+ response.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(pushed));
45
+ }
46
+ catch (error) {
47
+ response.writeHead(502).end(error instanceof Error ? error.message : String(error));
48
+ }
49
+ });
50
+ server.listen(socketPath);
51
+ await once(server, 'listening');
52
+ }
14
53
  export async function resident() {
15
54
  if ((await readEnrolment()) === null) {
16
55
  process.stderr.write('kit: this Environment is not enrolled; run kit login\n');
17
56
  process.exitCode = 1;
18
57
  return;
19
58
  }
59
+ await placeSkillSet();
20
60
  process.env.HOUSE_KIT_RESIDENT = String(process.pid);
21
61
  const agents = new Agents(house);
62
+ const copies = new WorkingCopies(house);
63
+ await copies.load();
64
+ await installShim();
65
+ await serveOwner(copies);
22
66
  let stream = null;
23
67
  let reported = false;
24
68
  const idle = () => conversations.idle() && signIns.idle();
@@ -34,10 +78,12 @@ export async function resident() {
34
78
  const conversations = new Conversations({
35
79
  house,
36
80
  agents,
81
+ copies,
37
82
  send: (frame) => stream?.send(frame) ?? false,
38
83
  changed,
39
84
  });
40
85
  const signIns = new SignIns(house, agents, changed);
86
+ const updates = new Updates(house);
41
87
  for (const signal of ['SIGTERM', 'SIGINT']) {
42
88
  process.once(signal, () => {
43
89
  killDescendants();
@@ -45,21 +91,30 @@ export async function resident() {
45
91
  });
46
92
  }
47
93
  await house.deliver('/kit/restarted', {}).catch(report);
94
+ await updates.resumed().catch(report);
48
95
  logged(agents.refresh());
49
96
  stream = holdStream({
50
97
  opened: () => {
51
98
  conversations.opened();
52
99
  changed();
100
+ logged(copies.select());
53
101
  },
54
- frame: (received) => {
55
- if (received.type === 'work_available' && received.subject === 'agents')
102
+ frame: (arrived) => {
103
+ if (arrived.type === 'work_available' && arrived.subject === 'agents')
56
104
  logged(agents.refresh());
57
- if (received.type === 'input') {
105
+ if (arrived.type === 'work_available' && arrived.subject === 'working_copy')
106
+ logged(copies.select());
107
+ if (arrived.type === 'entries') {
108
+ copies.entries(arrived);
109
+ }
110
+ if (arrived.type === 'input') {
58
111
  reported = false;
59
- if (received.kind === 'sign_in')
60
- signIns.start(received);
112
+ if (arrived.kind === 'sign_in')
113
+ signIns.start(arrived);
114
+ else if (arrived.kind === 'update')
115
+ updates.start(arrived);
61
116
  else
62
- conversations.input(received);
117
+ conversations.input(arrived);
63
118
  }
64
119
  },
65
120
  });
package/dist/sign-in.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { createInterface } from 'node:readline';
3
3
  import { killTree } from './acp.js';
4
- import { CLIS, cliCommand } from './clis.js';
4
+ import { CLIS, cliCommand, withoutProxy } from './clis.js';
5
5
  import { holdSecretInput } from './secret-input.js';
6
6
  const ESCAPE = /\u001b\[[\d;]*m/g;
7
7
  const LINK = /https:\/\/\S+/;
@@ -39,7 +39,11 @@ export class SignIns {
39
39
  async login(input) {
40
40
  await this.agents.installed();
41
41
  const login = CLIS[input.cli].login;
42
- const child = spawn(cliCommand(input.cli), login.args, { stdio: ['pipe', 'pipe', 'pipe'], detached: true });
42
+ const child = spawn(cliCommand(input.cli), login.args, {
43
+ stdio: ['pipe', 'pipe', 'pipe'],
44
+ detached: true,
45
+ env: withoutProxy(),
46
+ });
43
47
  const running = () => child.exitCode === null && child.signalCode === null;
44
48
  let link;
45
49
  let code;
package/dist/skills.js ADDED
@@ -0,0 +1,83 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { cp, lstat, mkdir, readdir, realpath, rm, symlink, writeFile } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { join, relative } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { parse } from 'yaml';
7
+ import { KIT_VERSION } from './clis.js';
8
+ import { refusalCode } from './copies.js';
9
+ import { kitHome, readConfiguration, readHome, writeHome } from './home.js';
10
+ export const HOW_WE_WORK = '/private/library/how-we-work.md';
11
+ const SKILL_SET = fileURLToPath(new URL('../skills/', import.meta.url));
12
+ const HOW_WE_WORK_TEXT = readFileSync(new URL('../how-we-work.md', import.meta.url), 'utf8');
13
+ const RECORD = 'skill-set.json';
14
+ const CREATED = 'how-we-work-created';
15
+ const UNWRITABLE = new Set(['room_not_found', 'operation_denied']);
16
+ function skillsDirectory() {
17
+ return join(homedir(), '.agents', 'skills');
18
+ }
19
+ function claudeDirectory() {
20
+ return join(homedir(), '.claude', 'skills');
21
+ }
22
+ async function present(path) {
23
+ try {
24
+ await lstat(path);
25
+ return true;
26
+ }
27
+ catch (error) {
28
+ if (error.code === 'ENOENT')
29
+ return false;
30
+ throw error;
31
+ }
32
+ }
33
+ async function install() {
34
+ const names = [];
35
+ for (const name of (await readdir(SKILL_SET)).sort()) {
36
+ if (!(await present(join(skillsDirectory(), name))) && !(await present(join(claudeDirectory(), name)))) {
37
+ names.push(name);
38
+ }
39
+ }
40
+ await writeHome(RECORD, { names });
41
+ await mkdir(skillsDirectory(), { recursive: true });
42
+ await mkdir(claudeDirectory(), { recursive: true });
43
+ const skills = await realpath(skillsDirectory());
44
+ const claude = await realpath(claudeDirectory());
45
+ for (const name of names) {
46
+ await cp(join(SKILL_SET, name), join(skillsDirectory(), name), { recursive: true });
47
+ if (claude !== skills)
48
+ await symlink(relative(claude, join(skills, name)), join(claudeDirectory(), name));
49
+ }
50
+ await writeHome(RECORD, { version: KIT_VERSION, names });
51
+ }
52
+ export async function placeSkillSet() {
53
+ const accepted = (await readConfiguration()).skills;
54
+ const installed = await readHome(RECORD);
55
+ if (accepted && installed?.version === KIT_VERSION)
56
+ return;
57
+ for (const name of installed?.names ?? []) {
58
+ await rm(join(claudeDirectory(), name), { recursive: true, force: true });
59
+ await rm(join(skillsDirectory(), name), { recursive: true, force: true });
60
+ }
61
+ if (accepted)
62
+ await install();
63
+ else
64
+ await rm(join(kitHome(), RECORD), { force: true });
65
+ }
66
+ function documentPresent(refusal) {
67
+ const { conflicts } = parse(refusal.slice(refusal.indexOf('\n') + 1));
68
+ return conflicts.some((conflict) => conflict.code === 'path_exists');
69
+ }
70
+ export async function createHowWeWork(bridge) {
71
+ if ((await readHome(RECORD)) === null || (await present(join(kitHome(), CREATED))))
72
+ return;
73
+ const created = await bridge.tool('edit', { changes: [{ op: 'create', path: HOW_WE_WORK, content: HOW_WE_WORK_TEXT }] });
74
+ if (created.isError === true) {
75
+ const text = created.content.map((content) => content.text).join('\n');
76
+ const code = refusalCode(text);
77
+ if (UNWRITABLE.has(code))
78
+ return;
79
+ if (code !== 'edit_conflict' || !documentPresent(text))
80
+ throw new Error(text);
81
+ }
82
+ await writeFile(join(kitHome(), CREATED), '');
83
+ }