@agentshouse/kit 0.2.1-alpha.2 → 0.2.1-alpha.21

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 (114) hide show
  1. package/README.md +39 -3
  2. package/dist/abort.js +9 -0
  3. package/dist/acp.js +27 -7
  4. package/dist/agents.js +204 -52
  5. package/dist/api.js +6 -1
  6. package/dist/authority-main.js +22 -0
  7. package/dist/bootstrap.json +31 -0
  8. package/dist/bridge.js +178 -54
  9. package/dist/clis-main.js +23 -0
  10. package/dist/clis.js +106 -32
  11. package/dist/configure-main.js +6 -0
  12. package/dist/conversations.js +128 -36
  13. package/dist/copies.js +809 -0
  14. package/dist/enrol-main.js +4 -0
  15. package/dist/git-main.js +153 -0
  16. package/dist/git.js +85 -0
  17. package/dist/home.js +36 -7
  18. package/dist/house-main.js +112 -41
  19. package/dist/instructions.js +16 -10
  20. package/dist/kit-main.js +2 -0
  21. package/dist/login.js +29 -5
  22. package/dist/mcp.js +38 -0
  23. package/dist/operation.js +12 -0
  24. package/dist/refusals.js +41 -0
  25. package/dist/resident.js +63 -7
  26. package/dist/sign-in.js +21 -5
  27. package/dist/skills.js +78 -0
  28. package/dist/transcription.js +122 -0
  29. package/dist/translate.js +51 -0
  30. package/dist/update.js +117 -0
  31. package/how-we-work.md +23 -0
  32. package/package.json +13 -9
  33. package/skills/ask-matt/LICENSE +21 -0
  34. package/skills/ask-matt/PHASE-BOUNDARIES.md +55 -0
  35. package/skills/ask-matt/SKILL.md +91 -0
  36. package/skills/ask-matt/agents/openai.yaml +4 -0
  37. package/skills/code-review/LICENSE +21 -0
  38. package/skills/code-review/SKILL.md +89 -0
  39. package/skills/code-review/agents/openai.yaml +3 -0
  40. package/skills/codebase-design/DEEPENING.md +37 -0
  41. package/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  42. package/skills/codebase-design/LICENSE +21 -0
  43. package/skills/codebase-design/SKILL.md +116 -0
  44. package/skills/codebase-design/agents/openai.yaml +3 -0
  45. package/skills/diagnosing-bugs/LICENSE +21 -0
  46. package/skills/diagnosing-bugs/SKILL.md +140 -0
  47. package/skills/diagnosing-bugs/agents/openai.yaml +3 -0
  48. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
  49. package/skills/domain-modeling/ADR-FORMAT.md +47 -0
  50. package/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  51. package/skills/domain-modeling/LICENSE +21 -0
  52. package/skills/domain-modeling/SKILL.md +76 -0
  53. package/skills/domain-modeling/agents/openai.yaml +3 -0
  54. package/skills/grill-me/LICENSE +21 -0
  55. package/skills/grill-me/SKILL.md +8 -0
  56. package/skills/grill-me/agents/openai.yaml +4 -0
  57. package/skills/grill-with-docs/LICENSE +21 -0
  58. package/skills/grill-with-docs/SKILL.md +8 -0
  59. package/skills/grill-with-docs/agents/openai.yaml +4 -0
  60. package/skills/grilling/LICENSE +21 -0
  61. package/skills/grilling/SKILL.md +30 -0
  62. package/skills/grilling/agents/openai.yaml +3 -0
  63. package/skills/handoff/LICENSE +21 -0
  64. package/skills/handoff/SKILL.md +17 -0
  65. package/skills/handoff/agents/openai.yaml +4 -0
  66. package/skills/implement/LICENSE +21 -0
  67. package/skills/implement/SKILL.md +16 -0
  68. package/skills/implement/agents/openai.yaml +4 -0
  69. package/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
  70. package/skills/improve-codebase-architecture/LICENSE +21 -0
  71. package/skills/improve-codebase-architecture/SKILL.md +72 -0
  72. package/skills/improve-codebase-architecture/agents/openai.yaml +4 -0
  73. package/skills/prototype/LICENSE +21 -0
  74. package/skills/prototype/LOGIC.md +67 -0
  75. package/skills/prototype/SKILL.md +28 -0
  76. package/skills/prototype/UI.md +112 -0
  77. package/skills/prototype/agents/openai.yaml +3 -0
  78. package/skills/research/LICENSE +21 -0
  79. package/skills/research/SKILL.md +14 -0
  80. package/skills/research/agents/openai.yaml +3 -0
  81. package/skills/resolving-merge-conflicts/LICENSE +21 -0
  82. package/skills/resolving-merge-conflicts/SKILL.md +16 -0
  83. package/skills/resolving-merge-conflicts/agents/openai.yaml +3 -0
  84. package/skills/tdd/LICENSE +21 -0
  85. package/skills/tdd/SKILL.md +40 -0
  86. package/skills/tdd/agents/openai.yaml +3 -0
  87. package/skills/tdd/mocking.md +59 -0
  88. package/skills/tdd/tests.md +77 -0
  89. package/skills/teach/GLOSSARY-FORMAT.md +35 -0
  90. package/skills/teach/LEARNING-RECORD-FORMAT.md +46 -0
  91. package/skills/teach/LICENSE +21 -0
  92. package/skills/teach/MISSION-FORMAT.md +31 -0
  93. package/skills/teach/RESOURCES-FORMAT.md +32 -0
  94. package/skills/teach/SKILL.md +141 -0
  95. package/skills/teach/agents/openai.yaml +4 -0
  96. package/skills/to-questionnaire/LICENSE +21 -0
  97. package/skills/to-questionnaire/SKILL.md +55 -0
  98. package/skills/to-questionnaire/agents/openai.yaml +4 -0
  99. package/skills/to-spec/LICENSE +21 -0
  100. package/skills/to-spec/SKILL.md +76 -0
  101. package/skills/to-spec/agents/openai.yaml +4 -0
  102. package/skills/to-tickets/LICENSE +21 -0
  103. package/skills/to-tickets/SKILL.md +106 -0
  104. package/skills/to-tickets/agents/openai.yaml +4 -0
  105. package/skills/wait-what/LICENSE +21 -0
  106. package/skills/wait-what/SKILL.md +8 -0
  107. package/skills/wait-what/agents/openai.yaml +4 -0
  108. package/skills/wayfinder/LICENSE +21 -0
  109. package/skills/wayfinder/SKILL.md +129 -0
  110. package/skills/wayfinder/agents/openai.yaml +4 -0
  111. package/skills/writing-for-agents/LICENSE +21 -0
  112. package/skills/writing-for-agents/SKILL-MECHANICS.md +22 -0
  113. package/skills/writing-for-agents/SKILL.md +83 -0
  114. package/skills/writing-for-agents/agents/openai.yaml +3 -0
package/dist/mcp.js ADDED
@@ -0,0 +1,38 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { KIT_VERSION } from './clis.js';
3
+ const MCP_PROTOCOL_VERSION = '2026-07-28';
4
+ export const UNREACHABLE = 'House is unreachable; call again in a minute';
5
+ export function rpc(method, params) {
6
+ return { jsonrpc: '2.0', id: randomUUID(), method, params };
7
+ }
8
+ export function mcpBody(message, operation) {
9
+ return JSON.stringify({
10
+ ...message,
11
+ params: {
12
+ ...message.params,
13
+ _meta: {
14
+ 'io.modelcontextprotocol/protocolVersion': MCP_PROTOCOL_VERSION,
15
+ 'io.modelcontextprotocol/clientInfo': { name: '@agentshouse/kit', version: KIT_VERSION },
16
+ 'io.modelcontextprotocol/clientCapabilities': {},
17
+ ...(operation === null ? {} : { 'agents.house/agent-operation': operation }),
18
+ },
19
+ },
20
+ });
21
+ }
22
+ export async function callHouse(origin, credential, message, operation = null, signal) {
23
+ const tool = message.method === 'tools/call' ? String(message.params.name) : null;
24
+ const answer = await fetch(new URL('/', origin), {
25
+ method: 'POST',
26
+ headers: {
27
+ 'content-type': 'application/json',
28
+ authorization: `Bearer ${credential}`,
29
+ accept: 'application/json, text/event-stream',
30
+ 'mcp-protocol-version': MCP_PROTOCOL_VERSION,
31
+ 'mcp-method': message.method,
32
+ ...(tool === null ? {} : { 'mcp-name': tool }),
33
+ },
34
+ body: mcpBody(message, operation),
35
+ signal,
36
+ });
37
+ return { status: answer.status, text: await answer.text() };
38
+ }
@@ -0,0 +1,12 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ export function uuidOf(bytes, version) {
3
+ bytes[6] = (version << 4) | (bytes[6] & 0x0f);
4
+ bytes[8] = 0x80 | (bytes[8] & 0x3f);
5
+ const hex = bytes.toString('hex', 0, 16);
6
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
7
+ }
8
+ export function operationId() {
9
+ const bytes = randomBytes(16);
10
+ bytes.writeUIntBE(Date.now(), 0, 6);
11
+ return uuidOf(bytes, 7);
12
+ }
@@ -0,0 +1,41 @@
1
+ import { parse } from 'yaml';
2
+ const HEAD = /^([a-z][a-z0-9_]*): \S/;
3
+ function conflictCodes(detail) {
4
+ let parsed;
5
+ try {
6
+ parsed = parse(detail);
7
+ }
8
+ catch {
9
+ return [];
10
+ }
11
+ const conflicts = parsed?.conflicts;
12
+ if (!Array.isArray(conflicts))
13
+ return [];
14
+ return conflicts.flatMap((conflict) => typeof conflict?.code === 'string' ? [conflict.code] : []);
15
+ }
16
+ function spoken(text) {
17
+ const head = HEAD.exec(text);
18
+ if (head === null)
19
+ return null;
20
+ const said = text.replace(/\n+$/, '');
21
+ const detail = said.indexOf('\n');
22
+ return { code: head[1], text: said, conflicts: detail === -1 ? [] : conflictCodes(said.slice(detail + 1)) };
23
+ }
24
+ export function refusalOf(text) {
25
+ let body;
26
+ try {
27
+ body = JSON.parse(text);
28
+ }
29
+ catch {
30
+ return spoken(text);
31
+ }
32
+ const error = body?.error;
33
+ if (typeof error !== 'object' || error === null)
34
+ return null;
35
+ const { code, message, data } = error;
36
+ const said = typeof message === 'string' ? spoken(message) : null;
37
+ if (said !== null)
38
+ return said;
39
+ const named = typeof code === 'string' ? code : typeof data?.code === 'string' ? data.code : null;
40
+ return named === null ? null : { code: named, text: named, conflicts: [] };
41
+ }
package/dist/resident.js CHANGED
@@ -1,24 +1,69 @@
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 { agentBase, 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();
60
+ await mkdir(agentBase(), { recursive: true });
20
61
  process.env.HOUSE_KIT_RESIDENT = String(process.pid);
21
62
  const agents = new Agents(house);
63
+ const copies = new WorkingCopies(house);
64
+ await copies.load();
65
+ await installShim();
66
+ await serveOwner(copies);
22
67
  let stream = null;
23
68
  let reported = false;
24
69
  const idle = () => conversations.idle() && signIns.idle();
@@ -34,10 +79,12 @@ export async function resident() {
34
79
  const conversations = new Conversations({
35
80
  house,
36
81
  agents,
82
+ copies,
37
83
  send: (frame) => stream?.send(frame) ?? false,
38
84
  changed,
39
85
  });
40
86
  const signIns = new SignIns(house, agents, changed);
87
+ const updates = new Updates(house);
41
88
  for (const signal of ['SIGTERM', 'SIGINT']) {
42
89
  process.once(signal, () => {
43
90
  killDescendants();
@@ -45,21 +92,30 @@ export async function resident() {
45
92
  });
46
93
  }
47
94
  await house.deliver('/kit/restarted', {}).catch(report);
95
+ await updates.resumed().catch(report);
48
96
  logged(agents.refresh());
49
97
  stream = holdStream({
50
98
  opened: () => {
51
99
  conversations.opened();
52
100
  changed();
101
+ logged(copies.select());
53
102
  },
54
- frame: (received) => {
55
- if (received.type === 'work_available' && received.subject === 'agents')
103
+ frame: (arrived) => {
104
+ if (arrived.type === 'work_available' && arrived.subject === 'agents')
56
105
  logged(agents.refresh());
57
- if (received.type === 'input') {
106
+ if (arrived.type === 'work_available' && arrived.subject === 'working_copy')
107
+ logged(copies.select());
108
+ if (arrived.type === 'entries') {
109
+ copies.entries(arrived);
110
+ }
111
+ if (arrived.type === 'input') {
58
112
  reported = false;
59
- if (received.kind === 'sign_in')
60
- signIns.start(received);
113
+ if (arrived.kind === 'sign_in')
114
+ signIns.start(arrived);
115
+ else if (arrived.kind === 'update')
116
+ updates.start(arrived);
61
117
  else
62
- conversations.input(received);
118
+ conversations.input(arrived);
63
119
  }
64
120
  },
65
121
  });
package/dist/sign-in.js CHANGED
@@ -1,13 +1,16 @@
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, 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+/;
8
8
  const CODE = /^[A-Z0-9]+-[A-Z0-9]+$/;
9
+ function causeOf(error) {
10
+ return error instanceof Error ? error.message : String(error);
11
+ }
9
12
  function logged(error) {
10
- process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
13
+ process.stderr.write(`kit: ${causeOf(error)}\n`);
11
14
  }
12
15
  export class SignIns {
13
16
  house;
@@ -28,7 +31,7 @@ export class SignIns {
28
31
  this.running.add(input.input_id);
29
32
  this.changed();
30
33
  this.login(input)
31
- .then(() => this.agents.report())
34
+ .then(() => this.agents.reread(input.cli))
32
35
  .then(() => this.house.deliver(`/kit/inputs/${input.input_id}/ack`, {}))
33
36
  .catch(logged)
34
37
  .finally(() => {
@@ -37,9 +40,20 @@ export class SignIns {
37
40
  });
38
41
  }
39
42
  async login(input) {
40
- await this.agents.installed();
43
+ let cli;
44
+ try {
45
+ cli = await this.agents.located(input.cli);
46
+ }
47
+ catch (error) {
48
+ process.stderr.write(`kit: the ${input.cli} sign-in failed: ${causeOf(error)}\n`);
49
+ return;
50
+ }
41
51
  const login = CLIS[input.cli].login;
42
- const child = spawn(cliCommand(input.cli), login.args, { stdio: ['pipe', 'pipe', 'pipe'], detached: true });
52
+ const child = spawn(cli, login.args, {
53
+ stdio: ['pipe', 'pipe', 'pipe'],
54
+ detached: true,
55
+ env: withoutProxy(),
56
+ });
43
57
  const running = () => child.exitCode === null && child.signalCode === null;
44
58
  let link;
45
59
  let code;
@@ -50,6 +64,8 @@ export class SignIns {
50
64
  if (plain === '')
51
65
  return;
52
66
  last = plain;
67
+ if (login.code === 'collect' && plain.includes(login.rejected))
68
+ killTree(child);
53
69
  link ??= LINK.exec(plain)?.[0];
54
70
  if (CODE.test(plain))
55
71
  code ??= plain;
package/dist/skills.js ADDED
@@ -0,0 +1,78 @@
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 { KIT_VERSION } from './clis.js';
7
+ import { kitHome, readConfiguration, readHome, writeHome } from './home.js';
8
+ import { refusalOf } from './refusals.js';
9
+ export const HOW_WE_WORK = '/private/library/how-we-work.md';
10
+ const SKILL_SET = fileURLToPath(new URL('../skills/', import.meta.url));
11
+ const HOW_WE_WORK_TEXT = readFileSync(new URL('../how-we-work.md', import.meta.url), 'utf8');
12
+ const RECORD = 'skill-set.json';
13
+ const CREATED = 'how-we-work-created';
14
+ const UNWRITABLE = new Set(['room_not_found', 'operation_denied']);
15
+ function skillsDirectory() {
16
+ return join(homedir(), '.agents', 'skills');
17
+ }
18
+ function claudeDirectory() {
19
+ return join(homedir(), '.claude', 'skills');
20
+ }
21
+ async function present(path) {
22
+ try {
23
+ await lstat(path);
24
+ return true;
25
+ }
26
+ catch (error) {
27
+ if (error.code === 'ENOENT')
28
+ return false;
29
+ throw error;
30
+ }
31
+ }
32
+ async function install() {
33
+ const names = [];
34
+ for (const name of (await readdir(SKILL_SET)).sort()) {
35
+ if (!(await present(join(skillsDirectory(), name))) && !(await present(join(claudeDirectory(), name)))) {
36
+ names.push(name);
37
+ }
38
+ }
39
+ await writeHome(RECORD, { names });
40
+ await mkdir(skillsDirectory(), { recursive: true });
41
+ await mkdir(claudeDirectory(), { recursive: true });
42
+ const skills = await realpath(skillsDirectory());
43
+ const claude = await realpath(claudeDirectory());
44
+ for (const name of names) {
45
+ await cp(join(SKILL_SET, name), join(skillsDirectory(), name), { recursive: true });
46
+ if (claude !== skills)
47
+ await symlink(relative(claude, join(skills, name)), join(claudeDirectory(), name));
48
+ }
49
+ await writeHome(RECORD, { version: KIT_VERSION, names });
50
+ }
51
+ export async function placeSkillSet() {
52
+ const accepted = (await readConfiguration()).skills;
53
+ const installed = await readHome(RECORD);
54
+ if (accepted && installed?.version === KIT_VERSION)
55
+ return;
56
+ for (const name of installed?.names ?? []) {
57
+ await rm(join(claudeDirectory(), name), { recursive: true, force: true });
58
+ await rm(join(skillsDirectory(), name), { recursive: true, force: true });
59
+ }
60
+ if (accepted)
61
+ await install();
62
+ else
63
+ await rm(join(kitHome(), RECORD), { force: true });
64
+ }
65
+ export async function createHowWeWork(bridge) {
66
+ if ((await readHome(RECORD)) === null || (await present(join(kitHome(), CREATED))))
67
+ 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 ?? ''))
73
+ return;
74
+ if (refusal?.code !== 'edit_conflict' || !refusal.conflicts.includes('path_exists'))
75
+ throw new Error(text);
76
+ }
77
+ await writeFile(join(kitHome(), CREATED), '');
78
+ }
@@ -0,0 +1,122 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { createReadStream } from 'node:fs';
3
+ import { chmod, mkdir, mkdtemp, readdir, readFile, rename, rm, stat } from 'node:fs/promises';
4
+ import { availableParallelism, tmpdir } from 'node:os';
5
+ import { basename, join } from 'node:path';
6
+ import { pipeline } from 'node:stream/promises';
7
+ import { unlessAborted } from './abort.js';
8
+ import { failureOf, run } from './clis.js';
9
+ import { kitHome } from './home.js';
10
+ const RELEASE = 'https://github.com/agentshouse/kit/releases/download/engine-2';
11
+ export const MODEL = {
12
+ url: 'https://huggingface.co/ggml-org/parakeet-GGUF/resolve/35156454d1a39de06863303dd209fd2bed6ee079/ggml-parakeet-tdt-0.6b-v3-q4_k.bin',
13
+ sha256: '8b205b8b39c6535e153de6fb11c51db46125d45c4f16ba496fe41a0fe71b885e',
14
+ executable: false,
15
+ };
16
+ export const ENGINES = {
17
+ 'linux-x64': {
18
+ decoder: { url: `${RELEASE}/ffmpeg-linux-x86_64`, sha256: '1f8235d206428305afb89c6e8ab99d100425a0ba61e22bb4c7aff017db8c8de5', executable: true },
19
+ engine: { url: `${RELEASE}/parakeet-cli-linux-x86_64`, sha256: 'bb2585695992fe1927a33960002cdf51b8a90a96ad65593c3d0a9f1fbc0865c2', executable: true },
20
+ },
21
+ 'linux-arm64': {
22
+ decoder: { url: `${RELEASE}/ffmpeg-linux-aarch64`, sha256: 'a7e08a0fae0179d1078312f6bb13d2a9aa4d4c71a1d82a022953f25dee452f5e', executable: true },
23
+ engine: { url: `${RELEASE}/parakeet-cli-linux-aarch64`, sha256: 'cbc0bedc8daae97f1ef9dcee4200c06226218e300e0fd62ead1306d22742b7ed', executable: true },
24
+ },
25
+ 'darwin-arm64': {
26
+ decoder: { url: `${RELEASE}/ffmpeg-darwin-arm64`, sha256: '7abe154aaed3d6b49f5167ca7221bb66bed51388925d99a7e4261a41fe52fba6', executable: true },
27
+ engine: { url: `${RELEASE}/parakeet-cli-darwin-arm64`, sha256: '9a2aafc0922ad7c0b6db6eff1641e96572ba32ac40bf995a1df242ed2febf297', executable: true },
28
+ },
29
+ };
30
+ const CHUNK_SECONDS = 20;
31
+ let held = Promise.resolve();
32
+ function directory() {
33
+ return join(kitHome(), 'transcription');
34
+ }
35
+ function installed(artifact) {
36
+ return join(directory(), artifact.sha256);
37
+ }
38
+ async function executed(command, args, signal) {
39
+ const ran = await run(command, args, 0, false, signal);
40
+ if (ran.status !== 0)
41
+ throw new Error(failureOf(ran, basename(command)));
42
+ return ran.stdout;
43
+ }
44
+ function present(path) {
45
+ return stat(path).then(() => true, () => false);
46
+ }
47
+ async function cpus() {
48
+ const [quota, period] = (await readFile('/sys/fs/cgroup/cpu.max', 'utf8').catch(() => 'max')).split(' ');
49
+ const allowed = quota === 'max' ? Infinity : Math.ceil(Number(quota) / Number(period));
50
+ return Math.min(availableParallelism(), allowed);
51
+ }
52
+ async function sha256(path) {
53
+ const hash = createHash('sha256');
54
+ await pipeline(createReadStream(path), hash);
55
+ return hash.digest('hex');
56
+ }
57
+ async function install(artifacts, signal) {
58
+ await mkdir(directory(), { recursive: true });
59
+ const pinned = new Set(artifacts.map((artifact) => artifact.sha256));
60
+ for (const name of await readdir(directory())) {
61
+ if (!pinned.has(name))
62
+ await rm(join(directory(), name), { recursive: true, force: true });
63
+ }
64
+ const missing = [];
65
+ for (const artifact of artifacts)
66
+ if (!(await present(installed(artifact))))
67
+ missing.push(artifact);
68
+ try {
69
+ for (const artifact of missing) {
70
+ const part = `${installed(artifact)}.part`;
71
+ await executed('curl', ['-fsSL', '--retry', '3', '-o', part, artifact.url], signal);
72
+ if ((await sha256(part)) !== artifact.sha256)
73
+ throw new Error(`${artifact.url} does not match its sha256`);
74
+ }
75
+ for (const artifact of missing) {
76
+ if (artifact.executable)
77
+ await chmod(`${installed(artifact)}.part`, 0o755);
78
+ await rename(`${installed(artifact)}.part`, installed(artifact));
79
+ }
80
+ }
81
+ finally {
82
+ for (const artifact of missing)
83
+ await rm(`${installed(artifact)}.part`, { force: true });
84
+ }
85
+ }
86
+ async function transcribed(path, signal) {
87
+ const platform = `${process.platform}-${process.arch}`;
88
+ const pinned = ENGINES[platform];
89
+ if (pinned === undefined)
90
+ throw new Error(`no transcription engine is pinned for ${platform}`);
91
+ await install([pinned.decoder, pinned.engine, MODEL], signal);
92
+ const work = await mkdtemp(join(tmpdir(), 'kit-transcription-'));
93
+ try {
94
+ await executed(installed(pinned.decoder), ['-nostdin', '-hide_banner', '-loglevel', 'error', '-i', path, '-ar', '16000', '-ac', '1', '-c:a', 'pcm_s16le',
95
+ '-f', 'segment', '-segment_time', String(CHUNK_SECONDS), join(work, '%05d.wav')], signal);
96
+ const parts = (await readdir(work)).sort().flatMap((part) => ['-f', join(work, part)]);
97
+ const text = await executed(installed(pinned.engine), ['-t', String(await cpus()), '-np', '-m', installed(MODEL), ...parts], signal);
98
+ return text
99
+ .split('\n')
100
+ .map((line) => line.trim())
101
+ .filter((line) => line !== '')
102
+ .join(' ');
103
+ }
104
+ finally {
105
+ await rm(work, { recursive: true, force: true });
106
+ }
107
+ }
108
+ export async function transcribe(path, signal) {
109
+ const before = held;
110
+ let release;
111
+ const mine = new Promise((resolve) => {
112
+ release = resolve;
113
+ });
114
+ held = Promise.all([before, mine]);
115
+ try {
116
+ await unlessAborted(before, signal);
117
+ return await transcribed(path, signal);
118
+ }
119
+ finally {
120
+ release();
121
+ }
122
+ }
@@ -0,0 +1,51 @@
1
+ import { apply, parse, serialize } from '@agentshouse/mdmodel';
2
+ function same(left, right) {
3
+ return JSON.stringify(left) === JSON.stringify(right);
4
+ }
5
+ function applied(document, changes) {
6
+ let current = document;
7
+ for (const change of changes) {
8
+ const result = apply(current, change);
9
+ if (result.outcome !== 'applied')
10
+ return null;
11
+ current = result.document;
12
+ }
13
+ return serialize(current);
14
+ }
15
+ function components(path, before, after, text) {
16
+ const edits = [];
17
+ const changes = [];
18
+ if (before.preamble !== after.preamble) {
19
+ edits.push({ op: 'replace_preamble', path, base: before.preamble, content: after.preamble });
20
+ changes.push({ kind: 'preamble', value: after.preamble, prior: before.preamble });
21
+ }
22
+ for (const name of new Set([...Object.keys(before.fields), ...Object.keys(after.fields)])) {
23
+ const prior = before.fields[name];
24
+ const value = after.fields[name];
25
+ if (same(prior, value))
26
+ continue;
27
+ if (value === undefined)
28
+ return null;
29
+ edits.push({ op: 'set_field', path, field: name, ...(prior === undefined ? {} : { base: prior }), value });
30
+ changes.push({ kind: 'field', name, value, prior });
31
+ }
32
+ for (const name of new Set([...Object.keys(before.sections), ...Object.keys(after.sections)])) {
33
+ const prior = before.sections[name];
34
+ const value = after.sections[name];
35
+ if (prior === value)
36
+ continue;
37
+ if (prior === undefined || value === undefined)
38
+ return null;
39
+ edits.push({ op: 'replace_section', path, section: name, base: prior, content: value });
40
+ changes.push({ kind: 'section', name, value, prior });
41
+ }
42
+ return edits.length > 0 && applied(before, changes) === text ? edits : null;
43
+ }
44
+ export function changed(path, file, base, before, after) {
45
+ if (!file.endsWith('.md'))
46
+ return [{ op: 'replace', path, base, content: after }];
47
+ const parsedBefore = parse(before, file);
48
+ const parsedAfter = parse(after, file);
49
+ const structured = parsedBefore.ok && parsedAfter.ok ? components(path, parsedBefore.document, parsedAfter.document, after) : null;
50
+ return structured ?? [{ op: 'replace', path, base, content: after }];
51
+ }
package/dist/update.js ADDED
@@ -0,0 +1,117 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
4
+ import { homedir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { run } from './clis.js';
7
+ import { kitHome } from './home.js';
8
+ const RECORD = 'outcome=$1; shift; bash "$0" --linux "$@"; printf %s "$?" > "$outcome"; systemctl start house-kit.service';
9
+ function logged(error) {
10
+ process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
11
+ }
12
+ function directory() {
13
+ return join(kitHome(), 'update');
14
+ }
15
+ function pending() {
16
+ return join(directory(), 'pending.json');
17
+ }
18
+ function outcome() {
19
+ return join(directory(), 'outcome');
20
+ }
21
+ function unit(inputId) {
22
+ return `house-kit-update-${inputId}`;
23
+ }
24
+ async function pendingInput() {
25
+ try {
26
+ return JSON.parse(await readFile(pending(), 'utf8')).input_id;
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ async function recorded() {
33
+ const status = await readFile(outcome(), 'utf8').catch(() => null);
34
+ if (status === null)
35
+ return null;
36
+ return status.trim() === '0' ? {} : { refused: `the Kit bootstrap exited with ${status.trim()}` };
37
+ }
38
+ async function finished(inputId) {
39
+ for (;;) {
40
+ const ack = await recorded();
41
+ if (ack !== null)
42
+ return ack;
43
+ if ((await run('systemctl', ['is-active', '--quiet', unit(inputId)], 10_000)).status !== 0) {
44
+ return (await recorded()) ?? { refused: 'the Kit bootstrap did not finish' };
45
+ }
46
+ await new Promise((resolve) => setTimeout(resolve, 1000));
47
+ }
48
+ }
49
+ function rerun(script, inputId, flags) {
50
+ return new Promise((resolve) => {
51
+ const child = spawn('systemd-run', [
52
+ '--wait',
53
+ '--collect',
54
+ '--quiet',
55
+ `--unit=${unit(inputId)}`,
56
+ `--setenv=HOME=${homedir()}`,
57
+ '/bin/sh',
58
+ '-c',
59
+ RECORD,
60
+ script,
61
+ outcome(),
62
+ ...flags,
63
+ ], { stdio: ['ignore', 'ignore', 'inherit'] });
64
+ child.on('error', () => resolve({ refused: 'no service manager runs this Kit' }));
65
+ child.on('close', () => resolve(null));
66
+ });
67
+ }
68
+ export class Updates {
69
+ house;
70
+ held = new Set();
71
+ queue = Promise.resolve();
72
+ constructor(house) {
73
+ this.house = house;
74
+ }
75
+ async resumed() {
76
+ const inputId = await pendingInput();
77
+ if (inputId === null)
78
+ return;
79
+ await this.acknowledge(inputId, await finished(inputId));
80
+ }
81
+ start(input) {
82
+ if (this.held.has(input.input_id))
83
+ return;
84
+ this.held.add(input.input_id);
85
+ this.queue = this.queue
86
+ .then(() => this.carry(input))
87
+ .then((ack) => this.acknowledge(input.input_id, ack))
88
+ .catch(logged)
89
+ .finally(() => this.held.delete(input.input_id));
90
+ }
91
+ async carry(input) {
92
+ if ((await pendingInput()) === input.input_id)
93
+ return finished(input.input_id);
94
+ const answer = await fetch(input.bootstrap);
95
+ if (!answer.ok)
96
+ return { refused: `the Kit bootstrap could not be downloaded: ${answer.status}` };
97
+ const script = Buffer.from(await answer.arrayBuffer());
98
+ if (createHash('sha256').update(script).digest('hex') !== input.sha256) {
99
+ return { refused: 'the Kit bootstrap does not match its sha256' };
100
+ }
101
+ await mkdir(directory(), { recursive: true, mode: 0o700 });
102
+ const path = join(directory(), 'connect-linux.sh');
103
+ await writeFile(path, script, { mode: 0o700 });
104
+ await rm(outcome(), { force: true });
105
+ await writeFile(pending(), `${JSON.stringify({ input_id: input.input_id })}\n`, { mode: 0o600 });
106
+ return (await rerun(path, input.input_id, input.update_clis ? ['--update-clis'] : [])) ?? (await recorded()) ?? {
107
+ refused: 'the Kit bootstrap did not finish',
108
+ };
109
+ }
110
+ async acknowledge(inputId, ack) {
111
+ await this.house.deliver(`/kit/inputs/${inputId}/ack`, ack);
112
+ if ((await pendingInput()) !== inputId)
113
+ return;
114
+ await rm(pending(), { force: true });
115
+ await rm(outcome(), { force: true });
116
+ }
117
+ }
package/how-we-work.md ADDED
@@ -0,0 +1,23 @@
1
+ # How we work
2
+
3
+ A Room is where information people share is kept; it is not the place for
4
+ private information. Work goes into the Room it belongs to, or the First Room
5
+ when none is named.
6
+
7
+ The skills are already set up; never run `setup-matt-pocock-skills`.
8
+
9
+ ## Where skill output goes
10
+
11
+ - Glossary (`CONTEXT.md`): `library/glossary.md`
12
+ - Decisions (ADRs): `library/decisions/NNNN-<slug>.md`
13
+ - Plan (spec): `library/projects/<name>/plan.md`
14
+ - Tasks (tickets): `library/projects/<name>/tasks/NN-<slug>.md`, with a
15
+ `Status:` line (`needs-triage`, `needs-info`, `ready-for-agent`,
16
+ `ready-for-human`, `wontfix`), a `Blocked by:` line and comments under
17
+ `## Comments`
18
+ - Map (Wayfinding): `library/projects/<name>/map.md`, its tasks under
19
+ `tasks/` with `Type:`, `Status: claimed|resolved` and `Blocked by:` lines
20
+ - Research: `library/research/<slug>.md`
21
+ - Questionnaires: `library/questionnaires/<slug>.md`
22
+ - Learning: `library/learning/<topic>/`
23
+ - Prototype: one HTML file with every variant on one page.