@agentshouse/kit 0.2.1-alpha.8 → 0.2.1-c3e4a20bb3183

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 (112) hide show
  1. package/README.md +33 -9
  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 +4 -1
  6. package/dist/bootstrap.json +8 -5
  7. package/dist/bridge.js +85 -51
  8. package/dist/clis-main.js +23 -0
  9. package/dist/clis.js +102 -32
  10. package/dist/configure-main.js +6 -0
  11. package/dist/conversations.js +105 -26
  12. package/dist/copies.js +47 -38
  13. package/dist/git-main.js +2 -2
  14. package/dist/git.js +6 -2
  15. package/dist/home.js +36 -7
  16. package/dist/house-main.js +91 -35
  17. package/dist/instructions.js +14 -13
  18. package/dist/login.js +23 -3
  19. package/dist/mcp.js +38 -0
  20. package/dist/operation.js +7 -4
  21. package/dist/refusals.js +41 -0
  22. package/dist/resident.js +12 -4
  23. package/dist/secret-input.js +4 -2
  24. package/dist/sign-in.js +17 -5
  25. package/dist/skills.js +78 -0
  26. package/dist/stream.js +2 -3
  27. package/dist/transcription.js +122 -0
  28. package/dist/update.js +117 -0
  29. package/how-we-work.md +23 -0
  30. package/package.json +7 -8
  31. package/skills/ask-matt/LICENSE +21 -0
  32. package/skills/ask-matt/PHASE-BOUNDARIES.md +55 -0
  33. package/skills/ask-matt/SKILL.md +91 -0
  34. package/skills/ask-matt/agents/openai.yaml +4 -0
  35. package/skills/code-review/LICENSE +21 -0
  36. package/skills/code-review/SKILL.md +89 -0
  37. package/skills/code-review/agents/openai.yaml +3 -0
  38. package/skills/codebase-design/DEEPENING.md +37 -0
  39. package/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  40. package/skills/codebase-design/LICENSE +21 -0
  41. package/skills/codebase-design/SKILL.md +116 -0
  42. package/skills/codebase-design/agents/openai.yaml +3 -0
  43. package/skills/diagnosing-bugs/LICENSE +21 -0
  44. package/skills/diagnosing-bugs/SKILL.md +140 -0
  45. package/skills/diagnosing-bugs/agents/openai.yaml +3 -0
  46. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
  47. package/skills/domain-modeling/ADR-FORMAT.md +47 -0
  48. package/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  49. package/skills/domain-modeling/LICENSE +21 -0
  50. package/skills/domain-modeling/SKILL.md +76 -0
  51. package/skills/domain-modeling/agents/openai.yaml +3 -0
  52. package/skills/grill-me/LICENSE +21 -0
  53. package/skills/grill-me/SKILL.md +8 -0
  54. package/skills/grill-me/agents/openai.yaml +4 -0
  55. package/skills/grill-with-docs/LICENSE +21 -0
  56. package/skills/grill-with-docs/SKILL.md +8 -0
  57. package/skills/grill-with-docs/agents/openai.yaml +4 -0
  58. package/skills/grilling/LICENSE +21 -0
  59. package/skills/grilling/SKILL.md +30 -0
  60. package/skills/grilling/agents/openai.yaml +3 -0
  61. package/skills/handoff/LICENSE +21 -0
  62. package/skills/handoff/SKILL.md +17 -0
  63. package/skills/handoff/agents/openai.yaml +4 -0
  64. package/skills/implement/LICENSE +21 -0
  65. package/skills/implement/SKILL.md +16 -0
  66. package/skills/implement/agents/openai.yaml +4 -0
  67. package/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
  68. package/skills/improve-codebase-architecture/LICENSE +21 -0
  69. package/skills/improve-codebase-architecture/SKILL.md +72 -0
  70. package/skills/improve-codebase-architecture/agents/openai.yaml +4 -0
  71. package/skills/prototype/LICENSE +21 -0
  72. package/skills/prototype/LOGIC.md +67 -0
  73. package/skills/prototype/SKILL.md +28 -0
  74. package/skills/prototype/UI.md +112 -0
  75. package/skills/prototype/agents/openai.yaml +3 -0
  76. package/skills/research/LICENSE +21 -0
  77. package/skills/research/SKILL.md +14 -0
  78. package/skills/research/agents/openai.yaml +3 -0
  79. package/skills/resolving-merge-conflicts/LICENSE +21 -0
  80. package/skills/resolving-merge-conflicts/SKILL.md +16 -0
  81. package/skills/resolving-merge-conflicts/agents/openai.yaml +3 -0
  82. package/skills/tdd/LICENSE +21 -0
  83. package/skills/tdd/SKILL.md +40 -0
  84. package/skills/tdd/agents/openai.yaml +3 -0
  85. package/skills/tdd/mocking.md +59 -0
  86. package/skills/tdd/tests.md +77 -0
  87. package/skills/teach/GLOSSARY-FORMAT.md +35 -0
  88. package/skills/teach/LEARNING-RECORD-FORMAT.md +46 -0
  89. package/skills/teach/LICENSE +21 -0
  90. package/skills/teach/MISSION-FORMAT.md +31 -0
  91. package/skills/teach/RESOURCES-FORMAT.md +32 -0
  92. package/skills/teach/SKILL.md +141 -0
  93. package/skills/teach/agents/openai.yaml +4 -0
  94. package/skills/to-questionnaire/LICENSE +21 -0
  95. package/skills/to-questionnaire/SKILL.md +55 -0
  96. package/skills/to-questionnaire/agents/openai.yaml +4 -0
  97. package/skills/to-spec/LICENSE +21 -0
  98. package/skills/to-spec/SKILL.md +76 -0
  99. package/skills/to-spec/agents/openai.yaml +4 -0
  100. package/skills/to-tickets/LICENSE +21 -0
  101. package/skills/to-tickets/SKILL.md +106 -0
  102. package/skills/to-tickets/agents/openai.yaml +4 -0
  103. package/skills/wait-what/LICENSE +21 -0
  104. package/skills/wait-what/SKILL.md +8 -0
  105. package/skills/wait-what/agents/openai.yaml +4 -0
  106. package/skills/wayfinder/LICENSE +21 -0
  107. package/skills/wayfinder/SKILL.md +129 -0
  108. package/skills/wayfinder/agents/openai.yaml +4 -0
  109. package/skills/writing-for-agents/LICENSE +21 -0
  110. package/skills/writing-for-agents/SKILL-MECHANICS.md +22 -0
  111. package/skills/writing-for-agents/SKILL.md +83 -0
  112. package/skills/writing-for-agents/agents/openai.yaml +3 -0
@@ -2,14 +2,17 @@ import { client, } from '@agentclientprotocol/sdk';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { mkdir } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
+ import { unlessAborted } from './abort.js';
5
6
  import { killMarked, startAdapter, TURN_ENDED, TURN_STARTED } from './acp.js';
6
7
  import { blocksOf, firstChange } from './blocks.js';
7
8
  import { openBridge } from './bridge.js';
8
9
  import { CLIS } from './clis.js';
9
10
  import { placeFiles } from './files.js';
11
+ import { agentBase } from './home.js';
10
12
  import { instructions } from './instructions.js';
11
13
  import { holdSecretInput } from './secret-input.js';
12
- const LAUNCH_BASE = '/agents/house';
14
+ import { createHowWeWork } from './skills.js';
15
+ import { transcribe } from './transcription.js';
13
16
  const KILLED = 'the conversation was killed';
14
17
  function causeOf(error) {
15
18
  return error instanceof Error ? error.message : String(error);
@@ -20,21 +23,15 @@ function answerMessage(request, response) {
20
23
  function logged(error) {
21
24
  process.stderr.write(`kit: ${causeOf(error)}\n`);
22
25
  }
23
- function unlessKilled(work, signal) {
24
- return new Promise((resolve, reject) => {
25
- const killed = () => reject(signal.reason);
26
- signal.addEventListener('abort', killed, { once: true });
27
- if (signal.aborted)
28
- killed();
29
- work.then(resolve, reject).finally(() => signal.removeEventListener('abort', killed));
30
- });
31
- }
32
26
  function stop(bridge) {
33
27
  killMarked(`HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`);
34
28
  }
35
29
  class Turn {
36
30
  id = randomUUID();
37
31
  text = '';
32
+ segment = null;
33
+ admitted = false;
34
+ notes = [];
38
35
  sent = [];
39
36
  unsentFrom = null;
40
37
  draftSequence = 0;
@@ -56,6 +53,8 @@ class Conversation {
56
53
  opening = null;
57
54
  kills = 0;
58
55
  killed = new AbortController();
56
+ interrupts = 0;
57
+ interrupted = new AbortController();
59
58
  open = 0;
60
59
  queue = Promise.resolve();
61
60
  reports = Promise.resolve();
@@ -91,6 +90,10 @@ export class Conversations {
91
90
  conversation.killed.abort(new Error(KILLED));
92
91
  this.halt(conversation);
93
92
  }
93
+ if (input.kind === 'interrupt') {
94
+ conversation.interrupts++;
95
+ conversation.interrupted.abort();
96
+ }
94
97
  const work = conversation.queue.then(() => this.carry(conversation, input));
95
98
  conversation.queue = work.catch(() => undefined);
96
99
  this.acks.set(input.input_id, work);
@@ -134,8 +137,11 @@ export class Conversations {
134
137
  return this.open(conversation, input);
135
138
  if (input.kind === 'message')
136
139
  return this.message(conversation, input);
137
- if (input.kind === 'interrupt')
140
+ if (input.kind === 'interrupt') {
141
+ if (--conversation.interrupts === 0)
142
+ conversation.interrupted = new AbortController();
138
143
  await this.interrupt(conversation, String(input.turn_id));
144
+ }
139
145
  if (input.kind === 'kill') {
140
146
  await this.kill(conversation);
141
147
  if (--conversation.kills === 0)
@@ -167,7 +173,9 @@ export class Conversations {
167
173
  const prompt = [];
168
174
  try {
169
175
  const route = await this.route(conversation, input.agent_id);
170
- const paths = await placeFiles(this.kit.house, route.working_directory, input.files, conversation.killed.signal);
176
+ const files = input.files;
177
+ const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
178
+ const lines = await this.transcribed(conversation, files, paths);
171
179
  if (conversation.running === null) {
172
180
  const opened = await this.start(conversation, route, input.provider_session_id);
173
181
  if (input.provider_session_id === null)
@@ -180,8 +188,8 @@ export class Conversations {
180
188
  });
181
189
  }
182
190
  prompt.push({ type: 'text', text: String(input.text) });
183
- if (paths.length > 0)
184
- prompt.push({ type: 'text', text: paths.join('\n') });
191
+ if (lines.length > 0)
192
+ prompt.push({ type: 'text', text: lines.join('\n') });
185
193
  if (conversation.kills > 0)
186
194
  throw new Error(KILLED);
187
195
  }
@@ -193,6 +201,25 @@ export class Conversations {
193
201
  this.prompt(conversation, prompt);
194
202
  return ack;
195
203
  }
204
+ async transcribed(conversation, files, paths) {
205
+ const lines = [];
206
+ for (const [index, path] of paths.entries()) {
207
+ lines.push(path);
208
+ if (files[index].media_type?.startsWith('audio/') !== true)
209
+ continue;
210
+ try {
211
+ const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
212
+ if (text !== '')
213
+ lines.push(`Transcript: ${text}`);
214
+ }
215
+ catch (error) {
216
+ if (conversation.kills > 0)
217
+ throw error;
218
+ logged(new Error(`${files[index].name} was not transcribed: ${causeOf(error)}`));
219
+ }
220
+ }
221
+ return lines;
222
+ }
196
223
  async interrupt(conversation, turnId) {
197
224
  if (conversation.running === null || conversation.turn?.id !== turnId)
198
225
  return;
@@ -231,13 +258,13 @@ export class Conversations {
231
258
  }
232
259
  async route(conversation, agentId) {
233
260
  const signal = conversation.killed.signal;
234
- await unlessKilled(this.kit.agents.read(), signal);
261
+ await unlessAborted(this.kit.agents.read(), signal);
235
262
  if (this.kit.agents.route(agentId) === undefined)
236
- await unlessKilled(this.kit.agents.refresh(), signal);
263
+ await unlessAborted(this.kit.agents.refresh(), signal);
237
264
  const route = this.kit.agents.route(agentId);
238
265
  if (route === undefined)
239
266
  throw new Error(`this Environment hosts no Agent ${agentId}`);
240
- if (route.working_directory === join(LAUNCH_BASE, route.agent_id)) {
267
+ if (route.working_directory === join(agentBase(), route.agent_id)) {
241
268
  await mkdir(route.working_directory, { recursive: true });
242
269
  }
243
270
  return route;
@@ -249,7 +276,7 @@ export class Conversations {
249
276
  .onNotification(TURN_ENDED, (params) => params, () => this.ended(conversation))
250
277
  .onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
251
278
  .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal));
252
- await unlessKilled(this.kit.agents.installed(), conversation.killed.signal);
279
+ const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
253
280
  const bridge = await openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
254
281
  if (conversation.kills > 0) {
255
282
  bridge.close();
@@ -257,7 +284,8 @@ export class Conversations {
257
284
  }
258
285
  conversation.opening = bridge;
259
286
  try {
260
- const adapter = await startAdapter(route.kind, route.working_directory, app, (job) => this.job(conversation, job), bridge.env);
287
+ await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
288
+ const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (job) => this.job(conversation, job), bridge.env);
261
289
  void adapter.exited.then(() => bridge.close());
262
290
  const agent = adapter.connection.agent;
263
291
  const cwd = route.working_directory;
@@ -270,6 +298,7 @@ export class Conversations {
270
298
  sessionId: opened.sessionId,
271
299
  killed: false,
272
300
  queues: CLIS[route.kind].queues,
301
+ phase: CLIS[route.kind].phase,
273
302
  };
274
303
  const options = await this.launchSettings(running, route, opened.configOptions ?? []);
275
304
  conversation.running = running;
@@ -357,6 +386,9 @@ export class Conversations {
357
386
  return turn;
358
387
  }
359
388
  admitted(conversation, turn) {
389
+ turn.admitted = true;
390
+ for (const note of turn.notes.splice(0))
391
+ this.kit.send(note);
360
392
  if (turn.text !== '') {
361
393
  turn.unsentFrom = 0;
362
394
  this.draft(conversation, turn);
@@ -370,6 +402,17 @@ export class Conversations {
370
402
  this.kit.changed();
371
403
  return turn;
372
404
  }
405
+ finish(conversation, turn) {
406
+ if (turn.ended)
407
+ return;
408
+ const segment = turn.segment;
409
+ turn.segment = null;
410
+ if (segment?.phase === 'note')
411
+ this.note(conversation, turn, segment.text);
412
+ else if (segment !== null)
413
+ this.answer(conversation, turn, segment.text);
414
+ this.end(conversation, turn, { text: turn.text });
415
+ }
373
416
  end(conversation, turn, outcome) {
374
417
  if (turn.ended)
375
418
  return;
@@ -427,7 +470,7 @@ export class Conversations {
427
470
  this.end(conversation, turn, { failed });
428
471
  }
429
472
  else if (turn.prompt === sent) {
430
- this.end(conversation, turn, { text: turn.text });
473
+ this.finish(conversation, turn);
431
474
  }
432
475
  }
433
476
  started(conversation) {
@@ -444,7 +487,7 @@ export class Conversations {
444
487
  if (turn === null)
445
488
  return;
446
489
  if (turn.prompt === null) {
447
- this.end(conversation, turn, { text: turn.text });
490
+ this.finish(conversation, turn);
448
491
  }
449
492
  else {
450
493
  conversation.turn = null;
@@ -498,17 +541,21 @@ export class Conversations {
498
541
  update(conversation, notification) {
499
542
  const update = notification.update;
500
543
  if (update.sessionUpdate === 'agent_message_chunk' && update.content.type === 'text') {
501
- const turn = this.started(conversation);
502
- turn.text += update.content.text;
503
- this.draft(conversation, turn);
544
+ this.chunk(conversation, this.started(conversation), update, update.content.text);
504
545
  }
505
546
  else if (update.sessionUpdate === 'plan') {
506
547
  const turn = this.started(conversation);
548
+ this.close(conversation, turn);
507
549
  turn.entries = update.entries;
508
550
  this.plan(conversation, turn);
509
551
  }
510
- else if (update.sessionUpdate === 'agent_thought_chunk' || update.sessionUpdate === 'tool_call') {
511
- this.started(conversation);
552
+ else if (update.sessionUpdate === 'tool_call') {
553
+ this.close(conversation, this.started(conversation));
554
+ }
555
+ else if (update.sessionUpdate === 'agent_thought_chunk') {
556
+ const turn = this.started(conversation);
557
+ if (turn.segment?.messageId !== (update.messageId ?? null))
558
+ this.close(conversation, turn);
512
559
  }
513
560
  else if (update.sessionUpdate === 'available_commands_update') {
514
561
  this.commands(conversation, update.availableCommands);
@@ -524,6 +571,38 @@ export class Conversations {
524
571
  conversation.jobs.delete(job.id);
525
572
  this.kit.changed();
526
573
  }
574
+ chunk(conversation, turn, chunk, text) {
575
+ const phase = conversation.running?.phase(chunk) ?? null;
576
+ const messageId = chunk.messageId ?? null;
577
+ if (turn.segment?.messageId !== messageId || turn.segment.phase !== phase)
578
+ this.close(conversation, turn);
579
+ if (phase === 'answer') {
580
+ this.answer(conversation, turn, text);
581
+ return;
582
+ }
583
+ turn.segment ??= { messageId, phase, text: '' };
584
+ turn.segment.text += text;
585
+ }
586
+ close(conversation, turn) {
587
+ const segment = turn.segment;
588
+ if (segment === null)
589
+ return;
590
+ turn.segment = null;
591
+ this.note(conversation, turn, segment.text);
592
+ }
593
+ note(conversation, turn, text) {
594
+ if (text.trim() === '')
595
+ return;
596
+ const note = { type: 'working-note', conversation_id: conversation.id, note_id: randomUUID(), text };
597
+ if (turn.admitted)
598
+ this.kit.send(note);
599
+ else
600
+ turn.notes.push(note);
601
+ }
602
+ answer(conversation, turn, text) {
603
+ turn.text += text;
604
+ this.draft(conversation, turn);
605
+ }
527
606
  draft(conversation, turn) {
528
607
  const blocks = blocksOf(turn.text);
529
608
  const from = Math.min(firstChange(turn.sent, blocks), turn.unsentFrom ?? Infinity);
package/dist/copies.js CHANGED
@@ -3,12 +3,12 @@ import { randomUUID } from 'node:crypto';
3
3
  import { mkdir, readFile, readdir, realpath, rename, rm, rmdir, stat, writeFile } from 'node:fs/promises';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { HouseRefusal } from './api.js';
6
- import { blobId, blobText, git, HOUSE_REF, importCommits, revision, run, succeeded, tree, WORKING_COPIES, } from './git.js';
6
+ import { blobId, blobText, git, HOUSE_REF, importCommits, localCopies, revision, run, succeeded, tree, } from './git.js';
7
7
  import { kitHome } from './home.js';
8
8
  import { operationId } from './operation.js';
9
+ import { refusalOf } from './refusals.js';
9
10
  import { changed } from './translate.js';
10
11
  const BRANCH = 'main';
11
- const ROOM_HOMES = ['capture', 'library', 'collaboration'];
12
12
  const PROVENANCE = '_provenance';
13
13
  const REPLICA_RETRY_MS = 30_000;
14
14
  const IDENTITY = { GIT_COMMITTER_NAME: 'House Kit', GIT_COMMITTER_EMAIL: 'house-kit@localhost' };
@@ -18,13 +18,16 @@ function causeOf(error) {
18
18
  function logged(error) {
19
19
  process.stderr.write(`kit: ${causeOf(error)}\n`);
20
20
  }
21
- export function refusalCode(text) {
22
- try {
23
- return String(JSON.parse(text).error.code);
24
- }
25
- catch {
26
- return text.split(':')[0].trim();
27
- }
21
+ const REBASED = new Set([
22
+ 'field_base_stale',
23
+ 'section_base_stale',
24
+ 'preamble_base_stale',
25
+ 'base_revision_stale',
26
+ 'path_exists',
27
+ 'path_absent',
28
+ ]);
29
+ function rebaseFixes(refusal) {
30
+ return refusal.code === 'edit_conflict' && refusal.conflicts.some((code) => REBASED.has(code));
28
31
  }
29
32
  function short(commit) {
30
33
  return commit.slice(0, 12);
@@ -43,9 +46,6 @@ function rootOf(copy) {
43
46
  function relative(path) {
44
47
  return path.replace(/^\/private\//, '').replace(/^\/rooms\/[^/]+\//, '');
45
48
  }
46
- function prefixesOf(selected) {
47
- return selected.private ? ['/private'] : ROOM_HOMES.map((home) => `${rootOf(selected)}/${home}`);
48
- }
49
49
  function declaredWritable(path, copy) {
50
50
  const deepest = (scopes) => Math.max(-1, ...scopes.filter((scope) => covers(path, scope)).map((scope) => scope.length));
51
51
  return deepest(copy.write) > deepest(copy.protected);
@@ -147,7 +147,7 @@ function movedPaths(moves, before, after) {
147
147
  .filter((pair) => pairs.filter((other) => other.from === pair.from || other.to === pair.to).length === 1)
148
148
  .map(({ from, to }) => [from, to]));
149
149
  }
150
- export class WorkingCopies {
150
+ export class LocalCopies {
151
151
  owner;
152
152
  house;
153
153
  copies = new Map();
@@ -166,13 +166,15 @@ export class WorkingCopies {
166
166
  catch (error) {
167
167
  if (!(error instanceof HouseRefusal) || error.status >= 500)
168
168
  throw error;
169
- return { refused: { code: refusalCode(error.text), text: error.message } };
169
+ return {
170
+ refused: refusalOf(error.text) ?? { code: '', text: `House refused it with ${error.status}`, conflicts: [] },
171
+ };
170
172
  }
171
173
  },
172
174
  };
173
175
  }
174
176
  async load() {
175
- const home = join(kitHome(), 'working-copies');
177
+ const home = join(kitHome(), 'local-copies');
176
178
  const names = (await readdir(join(home, 'rooms')).catch(() => [])).filter((name) => name.endsWith('.json'));
177
179
  for (const path of [...names.map((name) => join(home, 'rooms', name)), join(home, 'private.json')]) {
178
180
  const text = await readFile(path, 'utf8').catch(() => null);
@@ -195,7 +197,7 @@ export class WorkingCopies {
195
197
  return selected;
196
198
  }
197
199
  async reselect() {
198
- const { working_copy: chosen } = await this.house.post('/kit/working-copy/selection', {});
200
+ const { local_copy: chosen } = await this.house.post('/kit/local-copy/selection', {});
199
201
  const selected = [
200
202
  ...(chosen?.rooms ?? [])
201
203
  .filter((room) => room.room_handle !== null)
@@ -253,11 +255,11 @@ export class WorkingCopies {
253
255
  async push(cwd, selector, caller) {
254
256
  const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
255
257
  if (located.status !== 0)
256
- return { refused: true, text: `${cwd} is not in a Git repository` };
258
+ return { refused: true, text: `${cwd} is not a House Local copy; run house git push inside one` };
257
259
  const [common, worktree] = located.stdout.toString('utf8').split('\n');
258
260
  const key = await this.keyAt(dirname(common));
259
261
  if (key === undefined)
260
- return { refused: true, text: `${worktree} is not a House working copy` };
262
+ return { refused: true, text: `${worktree} is not a House Local copy; run house git push inside one` };
261
263
  return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
262
264
  }
263
265
  async keyAt(repository) {
@@ -285,7 +287,7 @@ export class WorkingCopies {
285
287
  }).catch(logged);
286
288
  }
287
289
  async save(copy) {
288
- const path = join(kitHome(), 'working-copies', copy.private ? 'private.json' : `${copy.key}.json`);
290
+ const path = join(kitHome(), 'local-copies', copy.private ? 'private.json' : `${copy.key}.json`);
289
291
  await mkdir(dirname(path), { recursive: true, mode: 0o700 });
290
292
  const temporary = `${path}.${process.pid}`;
291
293
  await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
@@ -294,7 +296,7 @@ export class WorkingCopies {
294
296
  return copy;
295
297
  }
296
298
  async read(selected) {
297
- const prefixes = prefixesOf(selected);
299
+ const prefixes = [rootOf(selected)];
298
300
  const discovery = await this.house.post('/kit/door/discover', { prefixes });
299
301
  let bundle = await this.house.post('/kit/door/bootstrap', { prefixes });
300
302
  if ('transfer' in bundle)
@@ -341,7 +343,7 @@ export class WorkingCopies {
341
343
  return;
342
344
  }
343
345
  const discovery = await this.house.post('/kit/door/discover', {
344
- prefixes: prefixesOf(copy),
346
+ prefixes: [rootOf(copy)],
345
347
  });
346
348
  const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
347
349
  if (authority === undefined)
@@ -363,7 +365,7 @@ export class WorkingCopies {
363
365
  logged(`House serves nothing of ${rootOf({ private: scope.private, room_handle: scope.handle })} to copy`);
364
366
  return;
365
367
  }
366
- const repository = scope.private ? join(WORKING_COPIES, 'private') : join(WORKING_COPIES, 'rooms', read.anchor);
368
+ const repository = scope.private ? join(localCopies(), 'private') : join(localCopies(), 'rooms', read.anchor);
367
369
  if (await exists(repository)) {
368
370
  logged(`${repository} already exists and House Kit leaves it as it is`);
369
371
  return;
@@ -432,12 +434,12 @@ export class WorkingCopies {
432
434
  let answer;
433
435
  try {
434
436
  answer = await this.house.post('/kit/door/enumerate', {
435
- prefixes: prefixesOf(copy),
437
+ prefixes: [rootOf(copy)],
436
438
  positions: [{ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch }],
437
439
  });
438
440
  }
439
441
  catch (error) {
440
- const code = error instanceof HouseRefusal ? refusalCode(error.text) : null;
442
+ const code = error instanceof HouseRefusal ? (refusalOf(error.text)?.code ?? null) : null;
441
443
  if (code === 'position_expired')
442
444
  return this.received(copy);
443
445
  if (code !== 'replica_behind')
@@ -512,8 +514,9 @@ export class WorkingCopies {
512
514
  }
513
515
  copy = await this.save(without(copy));
514
516
  }
515
- if (!copy.active)
516
- return { refused: true, text: `${rootOf(copy)} is no longer kept in this Environment` };
517
+ if (!copy.active) {
518
+ return { refused: true, text: `${rootOf(copy)} is no longer synced to this Environment, so House takes no push from it` };
519
+ }
517
520
  const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
518
521
  const base = listed.find((ancestor) => ancestor in copy.manifests);
519
522
  if (base === undefined) {
@@ -582,9 +585,8 @@ export class WorkingCopies {
582
585
  async settle(start, caller, restaged = false) {
583
586
  let copy = start;
584
587
  const push = copy.push;
585
- if (push.caller !== caller.key) {
586
- return this.recovered(copy, `${short(push.commit)} was pushed by another caller and awaited its outcome`);
587
- }
588
+ if (push.caller !== caller.key)
589
+ return this.recovered(copy);
588
590
  let outcome;
589
591
  try {
590
592
  outcome = await caller.submit(push.operation_id, push.changes, push.upload, async (upload) => {
@@ -592,9 +594,10 @@ export class WorkingCopies {
592
594
  });
593
595
  }
594
596
  catch (error) {
597
+ const refusal = error instanceof HouseRefusal ? refusalOf(error.text) : null;
595
598
  return {
596
599
  refused: true,
597
- text: `House's answer to ${short(push.commit)} did not arrive (${causeOf(error)}); run house git push again to recover its outcome.`,
600
+ text: refusal?.text ?? `House's answer to ${short(push.commit)} did not arrive; run house git push again`,
598
601
  };
599
602
  }
600
603
  if ('refused' in outcome) {
@@ -603,22 +606,28 @@ export class WorkingCopies {
603
606
  return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
604
607
  }
605
608
  if (outcome.refused.code === 'operation_expired')
606
- return this.recovered(copy, outcome.refused.text);
609
+ return this.recovered(copy);
610
+ const rebased = rebaseFixes(outcome.refused);
607
611
  let settled = await this.save(without(copy));
608
612
  try {
609
613
  settled = await this.save(await this.caughtUp(settled));
610
614
  }
611
- catch (error) {
612
- return { refused: true, text: `${outcome.refused.text}\n${HOUSE_REF} is unchanged: ${causeOf(error)}` };
615
+ catch {
616
+ return {
617
+ refused: true,
618
+ text: rebased ? `${outcome.refused.text}\nHouse's current state did not arrive; push again later` : outcome.refused.text,
619
+ };
613
620
  }
614
621
  return {
615
622
  refused: true,
616
- text: `${outcome.refused.text}\n${HOUSE_REF} holds House's current state; merge or rebase onto it, commit and push again.`,
623
+ text: rebased
624
+ ? `${outcome.refused.text}\n${HOUSE_REF} holds House's current state; rebase onto it and push again`
625
+ : outcome.refused.text,
617
626
  };
618
627
  }
619
628
  return this.finished(await this.save({ ...copy, push: { ...copy.push, state: 'accepted', written: outcome.accepted } }));
620
629
  }
621
- async recovered(copy, reason) {
630
+ async recovered(copy) {
622
631
  const push = copy.push;
623
632
  const current = await this.save(await this.caughtUp(copy));
624
633
  const house = await tree(copy.repository, current.house);
@@ -627,7 +636,7 @@ export class WorkingCopies {
627
636
  await this.save(without(current));
628
637
  return {
629
638
  refused: true,
630
- text: `${reason}; House does not hold ${short(push.commit)}; run house git push again to submit it as a new operation.`,
639
+ text: `House does not hold ${short(push.commit)}; run house git push again`,
631
640
  };
632
641
  }
633
642
  const written = [];
@@ -652,10 +661,10 @@ export class WorkingCopies {
652
661
  await this.save(without(copy));
653
662
  return { refused: false, text: integrated.text };
654
663
  }
655
- catch (error) {
664
+ catch {
656
665
  return {
657
666
  refused: false,
658
- text: `House accepted ${short(commit)}; local integration is pending (${causeOf(error)}); run house git push again to finish it.`,
667
+ text: `House accepted ${short(commit)}; run house git push again to finish integrating it here`,
659
668
  };
660
669
  }
661
670
  }
package/dist/git-main.js CHANGED
@@ -4,7 +4,7 @@ import { accessSync, constants, realpathSync, statSync } from 'node:fs';
4
4
  import { request } from 'node:http';
5
5
  import { constants as system } from 'node:os';
6
6
  import { delimiter, dirname, join } from 'node:path';
7
- import { WORKING_COPIES } from './git.js';
7
+ import { localCopies } from './git.js';
8
8
  import { kitHome } from './home.js';
9
9
  const VALUED = new Set(['-C', '-c', '--git-dir', '--work-tree', '--namespace', '--config-env', '--attr-source', '--super-prefix']);
10
10
  const READS = new Set([
@@ -125,7 +125,7 @@ async function observed(git, args) {
125
125
  return delegated(git, args);
126
126
  const located = captured(git, [...globals, 'rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
127
127
  const [common, worktree] = located?.split('\n') ?? [];
128
- if (common === undefined || worktree === undefined || !dirname(common).startsWith(`${WORKING_COPIES}/`)) {
128
+ if (common === undefined || worktree === undefined || !dirname(common).startsWith(`${localCopies()}/`)) {
129
129
  return delegated(git, args);
130
130
  }
131
131
  const listing = [...globals, '-C', worktree, 'ls-files', '--stage', '-z'];
package/dist/git.js CHANGED
@@ -1,7 +1,11 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { createHash } from 'node:crypto';
3
+ import { join } from 'node:path';
3
4
  import { withoutProxy } from './clis.js';
4
- export const WORKING_COPIES = '/agents/house/working-copies';
5
+ import { workspaceRoot } from './home.js';
6
+ export function localCopies() {
7
+ return join(workspaceRoot(), 'local-copies');
8
+ }
5
9
  export const HOUSE_REF = 'refs/house/received';
6
10
  const COMMITTER = 'House Kit <house-kit@localhost>';
7
11
  export function run(cwd, args, input, env = {}) {
@@ -50,7 +54,7 @@ export async function blobText(repository, object, path) {
50
54
  return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
51
55
  }
52
56
  catch {
53
- throw new Error(`${path} is not UTF-8 text`);
57
+ throw new Error(`${path} is binary and House stores only text; remove it from the commit`);
54
58
  }
55
59
  }
56
60
  export function blobId(content) {
package/dist/home.js CHANGED
@@ -1,13 +1,21 @@
1
- import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
1
+ import { mkdir, readFile, rename, rm, 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';
6
+ const OWN_AGENT = 'own-agent.json';
5
7
  export function kitHome() {
6
8
  return process.env.HOUSE_KIT_HOME ?? join(homedir(), '.house-kit');
7
9
  }
8
- export async function readEnrolment() {
10
+ export function workspaceRoot() {
11
+ return process.env.HOUSE_KIT_WORKSPACE ?? '/agents/house';
12
+ }
13
+ export function agentBase() {
14
+ return join(workspaceRoot(), 'agents');
15
+ }
16
+ export async function readHome(name) {
9
17
  try {
10
- return JSON.parse(await readFile(join(kitHome(), ENROLMENT), 'utf8'));
18
+ return JSON.parse(await readFile(join(kitHome(), name), 'utf8'));
11
19
  }
12
20
  catch (error) {
13
21
  if (error.code === 'ENOENT')
@@ -15,10 +23,31 @@ export async function readEnrolment() {
15
23
  throw error;
16
24
  }
17
25
  }
18
- export async function writeEnrolment(enrolment) {
26
+ export async function writeHome(name, value) {
19
27
  const home = kitHome();
20
28
  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));
29
+ const temporary = join(home, `${name}.${process.pid}`);
30
+ await writeFile(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600 });
31
+ await rename(temporary, join(home, name));
32
+ }
33
+ export function readEnrolment() {
34
+ return readHome(ENROLMENT);
35
+ }
36
+ export function writeEnrolment(enrolment) {
37
+ return writeHome(ENROLMENT, enrolment);
38
+ }
39
+ export function readOwnAgent() {
40
+ return readHome(OWN_AGENT);
41
+ }
42
+ export function writeOwnAgent(ownAgent) {
43
+ return writeHome(OWN_AGENT, ownAgent);
44
+ }
45
+ export function forgetOwnAgent() {
46
+ return rm(join(kitHome(), OWN_AGENT), { force: true });
47
+ }
48
+ export async function readConfiguration() {
49
+ return { skills: (await readHome(CONFIGURATION))?.skills !== false };
50
+ }
51
+ export function writeConfiguration(configuration) {
52
+ return writeHome(CONFIGURATION, configuration);
24
53
  }