@agentshouse/kit 0.2.1-c845e172df1af → 0.2.1-c849d8d4ab62f

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 (131) hide show
  1. package/README.md +6 -6
  2. package/dist/acp.js +57 -6
  3. package/dist/agents.js +6 -9
  4. package/dist/authority-main.js +2 -2
  5. package/dist/bridge.js +1 -1
  6. package/dist/building.js +46 -0
  7. package/dist/calls.js +11 -3
  8. package/dist/clis-main.js +2 -2
  9. package/dist/clis.js +39 -15
  10. package/dist/connection.js +20 -29
  11. package/dist/conversations.js +235 -81
  12. package/dist/end.js +8 -2
  13. package/dist/events.js +29 -8
  14. package/dist/files.js +53 -40
  15. package/dist/git.js +2 -2
  16. package/dist/house-cli.js +31 -59
  17. package/dist/house-main.js +3 -3
  18. package/dist/instructions.js +29 -9
  19. package/dist/kit-cli.js +2 -2
  20. package/dist/kit-main.js +3 -3
  21. package/dist/login.js +4 -4
  22. package/dist/mcp.js +2 -0
  23. package/dist/relay.js +21 -0
  24. package/dist/resident.js +17 -6
  25. package/dist/scripts.js +255 -0
  26. package/dist/sign-in.js +3 -2
  27. package/dist/silence.js +28 -0
  28. package/dist/skills.js +41 -96
  29. package/dist/transcription.js +83 -43
  30. package/dist/update.js +51 -8
  31. package/package.json +19 -20
  32. package/dist/bootstrap.json +0 -24
  33. package/how-we-work.md +0 -33
  34. package/skills/ask-matt/LICENSE +0 -21
  35. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  36. package/skills/ask-matt/SKILL.md +0 -97
  37. package/skills/ask-matt/agents/openai.yaml +0 -5
  38. package/skills/code-review/LICENSE +0 -21
  39. package/skills/code-review/SKILL.md +0 -89
  40. package/skills/code-review/agents/openai.yaml +0 -3
  41. package/skills/codebase-design/DEEPENING.md +0 -37
  42. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  43. package/skills/codebase-design/LICENSE +0 -21
  44. package/skills/codebase-design/SKILL.md +0 -114
  45. package/skills/codebase-design/agents/openai.yaml +0 -3
  46. package/skills/diagnosing-bugs/LICENSE +0 -21
  47. package/skills/diagnosing-bugs/SKILL.md +0 -138
  48. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  49. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  50. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  51. package/skills/domain-modeling/GLOSSARY-FORMAT.md +0 -60
  52. package/skills/domain-modeling/LICENSE +0 -21
  53. package/skills/domain-modeling/SKILL.md +0 -74
  54. package/skills/domain-modeling/agents/openai.yaml +0 -3
  55. package/skills/grill-me/LICENSE +0 -21
  56. package/skills/grill-me/SKILL.md +0 -7
  57. package/skills/grill-me/agents/openai.yaml +0 -5
  58. package/skills/grill-with-docs/LICENSE +0 -21
  59. package/skills/grill-with-docs/SKILL.md +0 -7
  60. package/skills/grill-with-docs/agents/openai.yaml +0 -5
  61. package/skills/grilling/LICENSE +0 -21
  62. package/skills/grilling/SKILL.md +0 -30
  63. package/skills/grilling/agents/openai.yaml +0 -3
  64. package/skills/handoff/LICENSE +0 -21
  65. package/skills/handoff/SKILL.md +0 -16
  66. package/skills/handoff/agents/openai.yaml +0 -5
  67. package/skills/implement/LICENSE +0 -21
  68. package/skills/implement/SKILL.md +0 -17
  69. package/skills/implement/agents/openai.yaml +0 -5
  70. package/skills/implement-spec/LICENSE +0 -21
  71. package/skills/implement-spec/SKILL.md +0 -40
  72. package/skills/implement-spec/agents/openai.yaml +0 -5
  73. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  74. package/skills/improve-codebase-architecture/LICENSE +0 -21
  75. package/skills/improve-codebase-architecture/SKILL.md +0 -71
  76. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
  77. package/skills/pr/CREDITS.md +0 -3
  78. package/skills/pr/LICENSE +0 -21
  79. package/skills/pr/SKILL.md +0 -170
  80. package/skills/pr/agents/openai.yaml +0 -3
  81. package/skills/prototype/LICENSE +0 -21
  82. package/skills/prototype/LOGIC.md +0 -67
  83. package/skills/prototype/SKILL.md +0 -26
  84. package/skills/prototype/UI.md +0 -112
  85. package/skills/prototype/agents/openai.yaml +0 -3
  86. package/skills/research/LICENSE +0 -21
  87. package/skills/research/SKILL.md +0 -12
  88. package/skills/research/agents/openai.yaml +0 -3
  89. package/skills/retro/LICENSE +0 -21
  90. package/skills/retro/SKILL.md +0 -44
  91. package/skills/retro/agents/openai.yaml +0 -5
  92. package/skills/tdd/LICENSE +0 -21
  93. package/skills/tdd/SKILL.md +0 -38
  94. package/skills/tdd/agents/openai.yaml +0 -3
  95. package/skills/tdd/mocking.md +0 -59
  96. package/skills/tdd/tests.md +0 -77
  97. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  98. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  99. package/skills/teach/LICENSE +0 -21
  100. package/skills/teach/MISSION-FORMAT.md +0 -31
  101. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  102. package/skills/teach/SKILL.md +0 -140
  103. package/skills/teach/agents/openai.yaml +0 -5
  104. package/skills/to-questionnaire/LICENSE +0 -21
  105. package/skills/to-questionnaire/SKILL.md +0 -54
  106. package/skills/to-questionnaire/agents/openai.yaml +0 -5
  107. package/skills/to-spec/LICENSE +0 -21
  108. package/skills/to-spec/SKILL.md +0 -75
  109. package/skills/to-spec/agents/openai.yaml +0 -5
  110. package/skills/to-tickets/LICENSE +0 -21
  111. package/skills/to-tickets/SKILL.md +0 -105
  112. package/skills/to-tickets/agents/openai.yaml +0 -5
  113. package/skills/triage/AGENT-BRIEF.md +0 -207
  114. package/skills/triage/LICENSE +0 -21
  115. package/skills/triage/OUT-OF-SCOPE.md +0 -105
  116. package/skills/triage/SKILL.md +0 -112
  117. package/skills/triage/agents/openai.yaml +0 -5
  118. package/skills/wait-what/LICENSE +0 -21
  119. package/skills/wait-what/SKILL.md +0 -7
  120. package/skills/wait-what/agents/openai.yaml +0 -5
  121. package/skills/wayfinder/LICENSE +0 -21
  122. package/skills/wayfinder/SKILL.md +0 -128
  123. package/skills/wayfinder/agents/openai.yaml +0 -5
  124. package/skills/wizard/LICENSE +0 -21
  125. package/skills/wizard/SKILL.md +0 -44
  126. package/skills/wizard/agents/openai.yaml +0 -3
  127. package/skills/wizard/template.sh +0 -223
  128. package/skills/writing-for-agents/LICENSE +0 -21
  129. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  130. package/skills/writing-for-agents/SKILL.md +0 -81
  131. package/skills/writing-for-agents/agents/openai.yaml +0 -3
package/dist/resident.js CHANGED
@@ -10,10 +10,11 @@ import { house } from './api.js';
10
10
  import { received } from './bridge.js';
11
11
  import { CONVERSATION_METHODS, Conversations } from './conversations.js';
12
12
  import { LocalCopies } from './copies.js';
13
- import { restart } from './end.js';
14
- import { agentBase, enrolled, forgetRestart, kitHome, ownerLock, ownerSocket, restartAsked } from './home.js';
13
+ import { restart, unknownMethod } from './end.js';
14
+ import { agentBase, enrolled, forgetRestart, kitHome, ownerLock, ownerSocket, readEnrolment, restartAsked } from './home.js';
15
15
  import { Calls } from './calls.js';
16
16
  import { killDescendants } from './scope.js';
17
+ import { removeInputs, Scripts } from './scripts.js';
17
18
  import { quoted } from './shell.js';
18
19
  import { SignIns } from './sign-in.js';
19
20
  import { placeSkillSet } from './skills.js';
@@ -116,7 +117,7 @@ async function started() {
116
117
  killDescendants();
117
118
  process.exit(1);
118
119
  });
119
- await placeSkillSet();
120
+ await placeSkillSet((await readEnrolment()).house);
120
121
  await mkdir(agentBase(), { recursive: true });
121
122
  await installShim(join(kitHome(), 'shim', 'git'), 'git-main');
122
123
  await installShim(codexLauncher(), 'codex-main');
@@ -142,6 +143,9 @@ export async function resident() {
142
143
  else if (restartAsked() && conversations.quiet()) {
143
144
  restart('restarting for an updated CLI');
144
145
  }
146
+ else if (update.held() && conversations.quiet()) {
147
+ update.apply(connection.pause());
148
+ }
145
149
  else if (!reported) {
146
150
  reported = true;
147
151
  logged(house.deliver('/kit/idle', {}, idle));
@@ -157,10 +161,13 @@ export async function resident() {
157
161
  changed,
158
162
  });
159
163
  const signIns = new SignIns(house, agents, calls);
160
- const update = new KitUpdate(calls);
164
+ const update = new KitUpdate(calls, changed);
165
+ removeInputs();
166
+ const scripts = new Scripts(calls, (notification) => connection?.send(notification) ?? false);
161
167
  for (const signal of ['SIGTERM', 'SIGINT']) {
162
168
  process.once(signal, () => {
163
169
  killDescendants();
170
+ removeInputs();
164
171
  process.kill(process.pid, signal);
165
172
  });
166
173
  }
@@ -173,15 +180,18 @@ export async function resident() {
173
180
  changed();
174
181
  logged(copies.connected());
175
182
  },
183
+ closed: () => scripts.disconnected(),
176
184
  call: (call) => {
177
185
  if (call.kind === 'sign_in')
178
186
  signIns.start(call);
179
187
  else if (call.kind === 'update')
180
- update.start(call);
188
+ update.hold(call);
189
+ else if (call.kind === 'run_wizard' || call.kind === 'run_function')
190
+ scripts.start(call);
181
191
  else if (CONVERSATION_METHODS.has(call.kind))
182
192
  conversations.call(call);
183
193
  else {
184
- const refused = { refused: `This Kit does not know ${call.kind}; update the Kit` };
194
+ const refused = { refused: unknownMethod(call.kind) };
185
195
  void calls.carry(call.input_id, async () => ({ result: refused }));
186
196
  }
187
197
  },
@@ -192,6 +202,7 @@ export async function resident() {
192
202
  logged(copies.changed(notification.rooms));
193
203
  if (notification.type === 'work_available' && notification.subject === 'agents')
194
204
  logged(agents.refresh());
205
+ scripts.notified(notification);
195
206
  },
196
207
  });
197
208
  }
@@ -0,0 +1,255 @@
1
+ import { spawn as spawnPiped } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { once } from 'node:events';
4
+ import { rmSync } from 'node:fs';
5
+ import { mkdir, rm, writeFile } from 'node:fs/promises';
6
+ import { constants } from 'node:os';
7
+ import { join } from 'node:path';
8
+ import { loginPath, onLoginPath } from './clis.js';
9
+ import { kitHome } from './home.js';
10
+ import { childEnvironment } from './relay.js';
11
+ import { killTree } from './scope.js';
12
+ const TERMINAL = 'xterm-256color';
13
+ const FIRST_SIZE = { cols: 80, rows: 24 };
14
+ const SIGNALLED = 128;
15
+ const NEWLINE = 0x0a;
16
+ const NO_BASH = "bash is not on the login shell's PATH";
17
+ const LOST = { refused: 'the Kit lost its connection to House' };
18
+ function causeOf(error) {
19
+ return error instanceof Error ? error.message : String(error);
20
+ }
21
+ function failed(plan, cause) {
22
+ process.stderr.write(`kit: ${plan.named} could not start: ${cause}\n`);
23
+ return { refused: `the script could not start: ${cause}` };
24
+ }
25
+ function planned(call) {
26
+ if (call.kind === 'run_wizard')
27
+ return { named: 'a Web Wizard', script: call.script, terminal: 'page', function: null };
28
+ return {
29
+ named: 'an Environment function',
30
+ script: call.script,
31
+ terminal: call.terminal ? 'at once' : null,
32
+ function: { input: call.input, timeCap: call.time_cap, outputCap: call.output_cap },
33
+ };
34
+ }
35
+ function inputs() {
36
+ return join(kitHome(), 'inputs');
37
+ }
38
+ async function inputFile(value) {
39
+ await mkdir(inputs(), { recursive: true, mode: 0o700 });
40
+ const path = join(inputs(), `${randomUUID()}.json`);
41
+ await writeFile(path, JSON.stringify(value), { mode: 0o600, flag: 'wx' });
42
+ return path;
43
+ }
44
+ export function removeInputs() {
45
+ rmSync(inputs(), { recursive: true, force: true });
46
+ }
47
+ function struck(line, typed) {
48
+ return typed.some((typedLine) => line.includes(typedLine));
49
+ }
50
+ class KeptOutput {
51
+ cap;
52
+ typedText = '';
53
+ text = '';
54
+ bytes = 0;
55
+ line = '';
56
+ lineBytes = 0;
57
+ dropping = false;
58
+ cut = false;
59
+ constructor(cap) {
60
+ this.cap = cap;
61
+ }
62
+ typed(data) {
63
+ this.typedText += data;
64
+ }
65
+ add(data) {
66
+ const pieces = data.split('\n');
67
+ const rest = pieces.pop();
68
+ for (const piece of pieces)
69
+ this.ended(piece);
70
+ this.extend(rest);
71
+ }
72
+ kept() {
73
+ const typed = this.typedLines(true);
74
+ this.text = [...this.text.split(/(?<=\n)/), this.line].filter((line) => line !== '' && !struck(line, typed)).join('');
75
+ this.bytes = Buffer.byteLength(this.text);
76
+ this.trim();
77
+ return { output: this.text, cut: this.cut };
78
+ }
79
+ typedLines(all) {
80
+ const lines = this.typedText.split(/\r\n|\r|\n/);
81
+ if (!all)
82
+ lines.pop();
83
+ return lines.filter((line) => line !== '');
84
+ }
85
+ ended(piece) {
86
+ const line = `${this.line}${piece}\n`;
87
+ const dropped = this.dropping;
88
+ this.line = '';
89
+ this.lineBytes = 0;
90
+ this.dropping = false;
91
+ if (dropped || struck(line, this.typedLines(false)))
92
+ return;
93
+ this.text += line;
94
+ this.bytes += Buffer.byteLength(line);
95
+ if (this.bytes > 2 * this.cap)
96
+ this.trim();
97
+ }
98
+ extend(piece) {
99
+ if (this.dropping)
100
+ return;
101
+ this.line += piece;
102
+ this.lineBytes += Buffer.byteLength(piece);
103
+ if (this.lineBytes <= this.cap)
104
+ return;
105
+ this.text = '';
106
+ this.bytes = 0;
107
+ this.line = '';
108
+ this.lineBytes = 0;
109
+ this.dropping = true;
110
+ this.cut = true;
111
+ }
112
+ trim() {
113
+ if (this.bytes <= this.cap)
114
+ return;
115
+ const bytes = Buffer.from(this.text);
116
+ const start = bytes.indexOf(NEWLINE, bytes.length - this.cap - 1) + 1;
117
+ this.text = bytes.subarray(start).toString('utf8');
118
+ this.bytes = bytes.length - start;
119
+ this.cut = true;
120
+ }
121
+ }
122
+ export class Scripts {
123
+ calls;
124
+ send;
125
+ runs = new Map();
126
+ constructor(calls, send) {
127
+ this.calls = calls;
128
+ this.send = send;
129
+ }
130
+ start(call) {
131
+ void this.calls.carry(call.input_id, () => this.run(call.input_id, planned(call)));
132
+ }
133
+ notified(notification) {
134
+ const run = this.runs.get(String(notification.wizard));
135
+ if (run === undefined)
136
+ return;
137
+ if (notification.type === 'wizard_input')
138
+ this.typed(run, String(notification.data));
139
+ if (notification.type === 'wizard_size')
140
+ this.resized(run, { cols: Number(notification.cols), rows: Number(notification.rows) });
141
+ if (notification.type === 'wizard_end' && run.plan.terminal === 'page')
142
+ this.end(run, {});
143
+ }
144
+ disconnected() {
145
+ for (const run of this.runs.values())
146
+ this.end(run, LOST);
147
+ }
148
+ async run(id, plan) {
149
+ const size = plan.terminal === 'page' ? null : FIRST_SIZE;
150
+ const output = plan.function === null ? null : new KeptOutput(plan.function.outputCap);
151
+ const run = { plan, input: null, output, process: null, pipes: [], terminal: null, size, typed: [], ended: null, sized: Promise.withResolvers() };
152
+ if (size !== null)
153
+ run.sized.resolve();
154
+ this.runs.set(id, run);
155
+ try {
156
+ return { result: await this.ran(id, run), release: plan.function !== null };
157
+ }
158
+ finally {
159
+ this.runs.delete(id);
160
+ if (run.input !== null)
161
+ await rm(run.input, { force: true });
162
+ }
163
+ }
164
+ async ran(id, run) {
165
+ const { plan, output } = run;
166
+ let exited;
167
+ try {
168
+ const { spawn } = await import('@lydell/node-pty');
169
+ const shell = await onLoginPath('bash');
170
+ if (shell === null)
171
+ return failed(plan, NO_BASH);
172
+ const path = await loginPath();
173
+ await run.sized.promise;
174
+ if (plan.function !== null)
175
+ run.input = await inputFile(plan.function.input);
176
+ if (run.ended !== null)
177
+ return run.ended;
178
+ const environment = { ...childEnvironment(), PATH: path, ...(run.input === null ? {} : { HOUSE_INPUT: run.input }) };
179
+ if (plan.terminal === null) {
180
+ ({ exited } = await this.piped(plan.script, shell, environment, run));
181
+ }
182
+ else {
183
+ const terminal = spawn(shell, ['-c', plan.script], { name: TERMINAL, ...run.size, cwd: process.cwd(), env: environment });
184
+ run.process = terminal;
185
+ run.terminal = terminal;
186
+ terminal.onData((data) => {
187
+ this.send({ type: 'wizard_output', wizard: id, data });
188
+ output?.add(data);
189
+ });
190
+ for (const data of run.typed.splice(0))
191
+ terminal.write(data);
192
+ exited = new Promise((resolve) => terminal.onExit(({ exitCode, signal }) => resolve(signal ? SIGNALLED + signal : exitCode)));
193
+ }
194
+ }
195
+ catch (error) {
196
+ return failed(plan, causeOf(error));
197
+ }
198
+ let timedOut = false;
199
+ // House sends a function run's time cap in seconds, and a run still going at its cap ends with its process group and the output pipes it holds.
200
+ const capped = plan.function === null ? undefined : setTimeout(() => {
201
+ timedOut = true;
202
+ this.stop(run);
203
+ }, plan.function.timeCap * 1000);
204
+ const code = await exited;
205
+ clearTimeout(capped);
206
+ if (run.ended !== null)
207
+ return run.ended;
208
+ if (output === null)
209
+ return { exit_code: code };
210
+ const kept = output.kept();
211
+ return timedOut ? { timed_out: true, ...kept } : { exit_code: code, ...kept };
212
+ }
213
+ async piped(script, shell, environment, run) {
214
+ const child = spawnPiped(shell, ['-c', script], { cwd: process.cwd(), env: environment, detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
215
+ const exited = new Promise((resolve) => child.once('close', (code, signal) => resolve(signal === null ? code : SIGNALLED + constants.signals[signal])));
216
+ await once(child, 'spawn');
217
+ run.process = { pid: child.pid };
218
+ run.pipes = [child.stdout, child.stderr];
219
+ for (const pipe of run.pipes) {
220
+ pipe.setEncoding('utf8');
221
+ pipe.on('data', (data) => run.output.add(data));
222
+ }
223
+ if (run.ended !== null)
224
+ this.stop(run);
225
+ return { exited };
226
+ }
227
+ typed(run, data) {
228
+ run.output?.typed(data);
229
+ if (run.terminal === null)
230
+ run.typed.push(data);
231
+ else
232
+ run.terminal.write(data);
233
+ }
234
+ resized(run, size) {
235
+ run.size = size;
236
+ run.sized.resolve();
237
+ try {
238
+ run.terminal?.resize(size.cols, size.rows);
239
+ }
240
+ catch { }
241
+ }
242
+ end(run, result) {
243
+ if (run.ended !== null)
244
+ return;
245
+ run.ended = result;
246
+ run.sized.resolve();
247
+ this.stop(run);
248
+ }
249
+ stop(run) {
250
+ if (run.process !== null)
251
+ killTree(run.process);
252
+ for (const pipe of run.pipes)
253
+ pipe.destroy();
254
+ }
255
+ }
package/dist/sign-in.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { createInterface } from 'node:readline';
3
- import { CLIS, withoutProxy } from './clis.js';
3
+ import { CLIS } from './clis.js';
4
+ import { childEnvironment } from './relay.js';
4
5
  import { killTree } from './scope.js';
5
6
  import { holdSecretInput } from './secret-input.js';
6
7
  const ESCAPE = /\u001b\[[\d;]*m/g;
@@ -56,7 +57,7 @@ export class SignIns {
56
57
  const child = spawn(cli, login.args, {
57
58
  stdio: ['pipe', 'pipe', 'pipe'],
58
59
  detached: true,
59
- env: withoutProxy(),
60
+ env: childEnvironment(),
60
61
  });
61
62
  const running = () => child.exitCode === null && child.signalCode === null;
62
63
  const stop = () => killTree(child);
@@ -0,0 +1,28 @@
1
+ const SYSTEM_CLOCK = {
2
+ later: (delay, callback) => setTimeout(callback, delay),
3
+ clear: (timer) => clearTimeout(timer),
4
+ };
5
+ export class Silence {
6
+ timer = null;
7
+ interval;
8
+ silent;
9
+ clock;
10
+ constructor(interval, silent, clock = SYSTEM_CLOCK) {
11
+ this.interval = interval;
12
+ this.silent = silent;
13
+ this.clock = clock;
14
+ }
15
+ heard() {
16
+ this.stop();
17
+ this.timer = this.clock.later(this.interval, () => {
18
+ this.timer = null;
19
+ this.silent();
20
+ });
21
+ }
22
+ stop() {
23
+ if (this.timer === null)
24
+ return;
25
+ this.clock.clear(this.timer);
26
+ this.timer = null;
27
+ }
28
+ }
package/dist/skills.js CHANGED
@@ -1,31 +1,14 @@
1
- import { createHash } from 'node:crypto';
2
- import { readFileSync } from 'node:fs';
3
- import { cp, lstat, mkdir, readdir, realpath, rm, symlink } from 'node:fs/promises';
1
+ import { cp, lstat, mkdir, realpath, rm, symlink, writeFile } from 'node:fs/promises';
4
2
  import { homedir } from 'node:os';
5
3
  import { join, relative } from 'node:path';
6
- import { fileURLToPath } from 'node:url';
4
+ import { GUIDE, listSkills } from '@agentshouse/skills';
7
5
  import { parse } from 'yaml';
8
6
  import { KIT_RELEASE } from './clis.js';
9
7
  import { kitHome, readConfiguration, readHome, writeHome } from './home.js';
10
- import { refusalOf } from './refusals.js';
11
- const HOW_WE_WORK = '/rooms/private/how-we-work.md';
12
- const SKILL_SET = fileURLToPath(new URL('../skills/', import.meta.url));
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
- '29951ee394aad9979eecfed3e5dc32c2c86e2226b6a380db5dbd4f305f4d1156',
20
- ];
21
8
  const RECORD = 'skill-set.json';
22
- const WRITTEN = 'how-we-work.json';
23
- const UNWRITABLE = new Set(['room_not_found', 'operation_denied']);
24
- const PRESENT = new Set(['path_exists']);
25
- const CHANGED = new Set(['base_revision_stale', 'path_absent']);
26
- const UNREADABLE = /^(?:\S+: )?(?:path_not_found|room_not_found|operation_denied)\b/;
27
- const FAILED = /^exit: [1-9]\d*$/;
28
- const CUT = /^stderr: shell: output_cut (\d+) of \d+ bytes; continue with: (.+)$/;
9
+ const ENTRY = 'SKILL.md';
10
+ const FRONTMATTER = /^---\n([\s\S]*?)\n---\n/u;
11
+ const ORIGIN = '<origin>';
29
12
  function skillsDirectory() {
30
13
  return join(homedir(), '.agents', 'skills');
31
14
  }
@@ -43,105 +26,67 @@ async function present(path) {
43
26
  throw error;
44
27
  }
45
28
  }
46
- async function install() {
47
- const names = [];
48
- for (const name of (await readdir(SKILL_SET)).sort()) {
49
- if (!(await present(join(skillsDirectory(), name))) && !(await present(join(claudeDirectory(), name)))) {
50
- names.push(name);
29
+ function skillName(text) {
30
+ const front = FRONTMATTER.exec(text);
31
+ const fields = (front === null ? null : parse(front[1]));
32
+ return typeof fields?.name === 'string' && typeof fields.description === 'string' ? fields.name : undefined;
33
+ }
34
+ function skillSet(origin) {
35
+ const upstream = listSkills().map(({ name, directory }) => ({
36
+ name,
37
+ place: (destination) => cp(directory, destination, { recursive: true }),
38
+ }));
39
+ const pages = GUIDE.flatMap(({ text }) => {
40
+ const name = skillName(text);
41
+ if (name === undefined)
42
+ return [];
43
+ const place = async (destination) => {
44
+ await mkdir(destination);
45
+ await writeFile(join(destination, ENTRY), text.replaceAll(ORIGIN, origin));
46
+ };
47
+ return [{ name, place }];
48
+ });
49
+ return [...upstream, ...pages].sort((left, right) => (left.name < right.name ? -1 : 1));
50
+ }
51
+ async function install(origin) {
52
+ const placed = [];
53
+ for (const skill of skillSet(origin)) {
54
+ if (!(await present(join(skillsDirectory(), skill.name))) && !(await present(join(claudeDirectory(), skill.name)))) {
55
+ placed.push(skill);
51
56
  }
52
57
  }
58
+ const names = placed.map((skill) => skill.name);
53
59
  await writeHome(RECORD, { names });
54
60
  await mkdir(skillsDirectory(), { recursive: true });
55
61
  await mkdir(claudeDirectory(), { recursive: true });
56
62
  const skills = await realpath(skillsDirectory());
57
63
  const claude = await realpath(claudeDirectory());
58
- for (const name of names) {
59
- await cp(join(SKILL_SET, name), join(skillsDirectory(), name), { recursive: true });
64
+ for (const { name, place } of placed) {
65
+ await place(join(skillsDirectory(), name));
60
66
  if (claude !== skills)
61
67
  await symlink(relative(claude, join(skills, name)), join(claudeDirectory(), name));
62
68
  }
63
- await writeHome(RECORD, { version: KIT_RELEASE, names });
69
+ await writeHome(RECORD, { version: KIT_RELEASE, origin, names });
64
70
  }
65
- export async function placeSkillSet() {
71
+ export async function placeSkillSet(origin) {
66
72
  try {
67
- await replaceSkillSet();
73
+ await replaceSkillSet(origin);
68
74
  }
69
75
  catch (error) {
70
76
  process.stderr.write(`kit: the skills could not be placed: ${error.message}\n`);
71
77
  }
72
78
  }
73
- async function replaceSkillSet() {
79
+ async function replaceSkillSet(origin) {
74
80
  const accepted = (await readConfiguration()).skills;
75
81
  const installed = await readHome(RECORD);
76
- if (accepted && installed?.version === KIT_RELEASE)
82
+ if (accepted && installed?.version === KIT_RELEASE && installed.origin === origin)
77
83
  return;
78
84
  for (const name of installed?.names ?? []) {
79
85
  await rm(join(claudeDirectory(), name), { recursive: true, force: true });
80
86
  await rm(join(skillsDirectory(), name), { recursive: true, force: true });
81
87
  }
82
88
  if (accepted)
83
- await install();
89
+ await install(origin);
84
90
  else
85
91
  await rm(join(kitHome(), RECORD), { force: true });
86
92
  }
87
- function sha256(text) {
88
- return createHash('sha256').update(text).digest('hex');
89
- }
90
- function cutOf(lines) {
91
- const cut = CUT.exec(lines[0] ?? '');
92
- return cut !== null && Buffer.byteLength(lines.slice(1).join('\n')) === Number(cut[1]) ? cut : null;
93
- }
94
- export async function readHowWeWork(bridge) {
95
- let document = '';
96
- for (let command = `cat ${HOW_WE_WORK}`; command !== undefined;) {
97
- const read = await bridge.tool('shell', { command });
98
- const text = read.content.map((content) => content.text).join('\n');
99
- const lines = text.split('\n');
100
- if (read.isError === true || (FAILED.test(lines[0] ?? '') && lines[1]?.startsWith('stderr: '))) {
101
- const refusal = read.isError === true ? text : lines.slice(1).join('\n').replace(/^stderr: /gm, '');
102
- if (UNREADABLE.test(refusal))
103
- return '';
104
- throw new Error(refusal);
105
- }
106
- const cut = cutOf(lines);
107
- document += lines.slice(cut === null ? 0 : 1).join('\n');
108
- command = cut?.[2];
109
- }
110
- return document;
111
- }
112
- async function currentRevision(bridge) {
113
- const inspected = await bridge.tool('inspect', { path: HOW_WE_WORK });
114
- const text = inspected.content.map((content) => content.text).join('\n');
115
- if (inspected.isError === true)
116
- throw new Error(text);
117
- return parse(text).revisions[0].revision;
118
- }
119
- async function edit(bridge, change, kept) {
120
- const edited = await bridge.tool('edit', { changes: [{ ...change, path: HOW_WE_WORK, content: HOW_WE_WORK_TEXT }] });
121
- if (edited.isError !== true)
122
- return 'written';
123
- const text = edited.content.map((content) => content.text).join('\n');
124
- const refusal = refusalOf(text);
125
- if (UNWRITABLE.has(refusal?.code ?? ''))
126
- return 'unwritable';
127
- if (refusal?.code === 'edit_conflict' && refusal.conflicts.some((conflict) => kept.has(conflict)))
128
- return 'kept';
129
- throw new Error(text);
130
- }
131
- export async function createHowWeWork(bridge) {
132
- if (!(await readConfiguration()).skills || (await readHome(RECORD))?.version === undefined)
133
- return;
134
- const current = sha256(HOW_WE_WORK_TEXT);
135
- if ((await readHome(WRITTEN)) === current)
136
- return;
137
- const created = await edit(bridge, { op: 'create' }, PRESENT);
138
- if (created === 'unwritable')
139
- return;
140
- if (created === 'kept') {
141
- const base = await currentRevision(bridge);
142
- const held = sha256(await readHowWeWork(bridge));
143
- if (held !== current && SHIPPED_HOW_WE_WORK.includes(held) && (await edit(bridge, { op: 'replace', base }, CHANGED)) === 'unwritable')
144
- return;
145
- }
146
- await writeHome(WRITTEN, current);
147
- }