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

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 (116) hide show
  1. package/README.md +35 -11
  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 -2
  6. package/dist/bootstrap.json +10 -14
  7. package/dist/bridge.js +90 -54
  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 +49 -39
  13. package/dist/files.js +2 -1
  14. package/dist/git-main.js +2 -2
  15. package/dist/git.js +6 -2
  16. package/dist/home.js +49 -7
  17. package/dist/house-main.js +96 -41
  18. package/dist/instructions.js +14 -13
  19. package/dist/kit-main.js +4 -1
  20. package/dist/login.js +24 -4
  21. package/dist/logout.js +18 -0
  22. package/dist/mcp.js +39 -0
  23. package/dist/operation.js +7 -4
  24. package/dist/refusals.js +41 -0
  25. package/dist/relay.js +10 -0
  26. package/dist/resident.js +36 -9
  27. package/dist/secret-input.js +4 -2
  28. package/dist/sign-in.js +17 -5
  29. package/dist/skills.js +78 -0
  30. package/dist/stream.js +4 -4
  31. package/dist/transcription.js +122 -0
  32. package/dist/update.js +117 -0
  33. package/how-we-work.md +23 -0
  34. package/package.json +7 -8
  35. package/skills/ask-matt/LICENSE +21 -0
  36. package/skills/ask-matt/PHASE-BOUNDARIES.md +55 -0
  37. package/skills/ask-matt/SKILL.md +91 -0
  38. package/skills/ask-matt/agents/openai.yaml +4 -0
  39. package/skills/code-review/LICENSE +21 -0
  40. package/skills/code-review/SKILL.md +89 -0
  41. package/skills/code-review/agents/openai.yaml +3 -0
  42. package/skills/codebase-design/DEEPENING.md +37 -0
  43. package/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  44. package/skills/codebase-design/LICENSE +21 -0
  45. package/skills/codebase-design/SKILL.md +116 -0
  46. package/skills/codebase-design/agents/openai.yaml +3 -0
  47. package/skills/diagnosing-bugs/LICENSE +21 -0
  48. package/skills/diagnosing-bugs/SKILL.md +140 -0
  49. package/skills/diagnosing-bugs/agents/openai.yaml +3 -0
  50. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
  51. package/skills/domain-modeling/ADR-FORMAT.md +47 -0
  52. package/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  53. package/skills/domain-modeling/LICENSE +21 -0
  54. package/skills/domain-modeling/SKILL.md +76 -0
  55. package/skills/domain-modeling/agents/openai.yaml +3 -0
  56. package/skills/grill-me/LICENSE +21 -0
  57. package/skills/grill-me/SKILL.md +8 -0
  58. package/skills/grill-me/agents/openai.yaml +4 -0
  59. package/skills/grill-with-docs/LICENSE +21 -0
  60. package/skills/grill-with-docs/SKILL.md +8 -0
  61. package/skills/grill-with-docs/agents/openai.yaml +4 -0
  62. package/skills/grilling/LICENSE +21 -0
  63. package/skills/grilling/SKILL.md +30 -0
  64. package/skills/grilling/agents/openai.yaml +3 -0
  65. package/skills/handoff/LICENSE +21 -0
  66. package/skills/handoff/SKILL.md +17 -0
  67. package/skills/handoff/agents/openai.yaml +4 -0
  68. package/skills/implement/LICENSE +21 -0
  69. package/skills/implement/SKILL.md +16 -0
  70. package/skills/implement/agents/openai.yaml +4 -0
  71. package/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
  72. package/skills/improve-codebase-architecture/LICENSE +21 -0
  73. package/skills/improve-codebase-architecture/SKILL.md +72 -0
  74. package/skills/improve-codebase-architecture/agents/openai.yaml +4 -0
  75. package/skills/prototype/LICENSE +21 -0
  76. package/skills/prototype/LOGIC.md +67 -0
  77. package/skills/prototype/SKILL.md +28 -0
  78. package/skills/prototype/UI.md +112 -0
  79. package/skills/prototype/agents/openai.yaml +3 -0
  80. package/skills/research/LICENSE +21 -0
  81. package/skills/research/SKILL.md +14 -0
  82. package/skills/research/agents/openai.yaml +3 -0
  83. package/skills/resolving-merge-conflicts/LICENSE +21 -0
  84. package/skills/resolving-merge-conflicts/SKILL.md +16 -0
  85. package/skills/resolving-merge-conflicts/agents/openai.yaml +3 -0
  86. package/skills/tdd/LICENSE +21 -0
  87. package/skills/tdd/SKILL.md +40 -0
  88. package/skills/tdd/agents/openai.yaml +3 -0
  89. package/skills/tdd/mocking.md +59 -0
  90. package/skills/tdd/tests.md +77 -0
  91. package/skills/teach/GLOSSARY-FORMAT.md +35 -0
  92. package/skills/teach/LEARNING-RECORD-FORMAT.md +46 -0
  93. package/skills/teach/LICENSE +21 -0
  94. package/skills/teach/MISSION-FORMAT.md +31 -0
  95. package/skills/teach/RESOURCES-FORMAT.md +32 -0
  96. package/skills/teach/SKILL.md +141 -0
  97. package/skills/teach/agents/openai.yaml +4 -0
  98. package/skills/to-questionnaire/LICENSE +21 -0
  99. package/skills/to-questionnaire/SKILL.md +55 -0
  100. package/skills/to-questionnaire/agents/openai.yaml +4 -0
  101. package/skills/to-spec/LICENSE +21 -0
  102. package/skills/to-spec/SKILL.md +76 -0
  103. package/skills/to-spec/agents/openai.yaml +4 -0
  104. package/skills/to-tickets/LICENSE +21 -0
  105. package/skills/to-tickets/SKILL.md +106 -0
  106. package/skills/to-tickets/agents/openai.yaml +4 -0
  107. package/skills/wait-what/LICENSE +21 -0
  108. package/skills/wait-what/SKILL.md +8 -0
  109. package/skills/wait-what/agents/openai.yaml +4 -0
  110. package/skills/wayfinder/LICENSE +21 -0
  111. package/skills/wayfinder/SKILL.md +129 -0
  112. package/skills/wayfinder/agents/openai.yaml +4 -0
  113. package/skills/writing-for-agents/LICENSE +21 -0
  114. package/skills/writing-for-agents/SKILL-MECHANICS.md +22 -0
  115. package/skills/writing-for-agents/SKILL.md +83 -0
  116. 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,13 @@ 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';
11
+ import { relayed } from './relay.js';
10
12
  const BRANCH = 'main';
11
- const ROOM_HOMES = ['capture', 'library', 'collaboration'];
12
13
  const PROVENANCE = '_provenance';
13
14
  const REPLICA_RETRY_MS = 30_000;
14
15
  const IDENTITY = { GIT_COMMITTER_NAME: 'House Kit', GIT_COMMITTER_EMAIL: 'house-kit@localhost' };
@@ -18,13 +19,16 @@ function causeOf(error) {
18
19
  function logged(error) {
19
20
  process.stderr.write(`kit: ${causeOf(error)}\n`);
20
21
  }
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
- }
22
+ const REBASED = new Set([
23
+ 'field_base_stale',
24
+ 'section_base_stale',
25
+ 'preamble_base_stale',
26
+ 'base_revision_stale',
27
+ 'path_exists',
28
+ 'path_absent',
29
+ ]);
30
+ function rebaseFixes(refusal) {
31
+ return refusal.code === 'edit_conflict' && refusal.conflicts.some((code) => REBASED.has(code));
28
32
  }
29
33
  function short(commit) {
30
34
  return commit.slice(0, 12);
@@ -43,9 +47,6 @@ function rootOf(copy) {
43
47
  function relative(path) {
44
48
  return path.replace(/^\/private\//, '').replace(/^\/rooms\/[^/]+\//, '');
45
49
  }
46
- function prefixesOf(selected) {
47
- return selected.private ? ['/private'] : ROOM_HOMES.map((home) => `${rootOf(selected)}/${home}`);
48
- }
49
50
  function declaredWritable(path, copy) {
50
51
  const deepest = (scopes) => Math.max(-1, ...scopes.filter((scope) => covers(path, scope)).map((scope) => scope.length));
51
52
  return deepest(copy.write) > deepest(copy.protected);
@@ -147,7 +148,7 @@ function movedPaths(moves, before, after) {
147
148
  .filter((pair) => pairs.filter((other) => other.from === pair.from || other.to === pair.to).length === 1)
148
149
  .map(({ from, to }) => [from, to]));
149
150
  }
150
- export class WorkingCopies {
151
+ export class LocalCopies {
151
152
  owner;
152
153
  house;
153
154
  copies = new Map();
@@ -166,13 +167,15 @@ export class WorkingCopies {
166
167
  catch (error) {
167
168
  if (!(error instanceof HouseRefusal) || error.status >= 500)
168
169
  throw error;
169
- return { refused: { code: refusalCode(error.text), text: error.message } };
170
+ return {
171
+ refused: refusalOf(error.text) ?? { code: '', text: `House refused it with ${error.status}`, conflicts: [] },
172
+ };
170
173
  }
171
174
  },
172
175
  };
173
176
  }
174
177
  async load() {
175
- const home = join(kitHome(), 'working-copies');
178
+ const home = join(kitHome(), 'local-copies');
176
179
  const names = (await readdir(join(home, 'rooms')).catch(() => [])).filter((name) => name.endsWith('.json'));
177
180
  for (const path of [...names.map((name) => join(home, 'rooms', name)), join(home, 'private.json')]) {
178
181
  const text = await readFile(path, 'utf8').catch(() => null);
@@ -195,7 +198,7 @@ export class WorkingCopies {
195
198
  return selected;
196
199
  }
197
200
  async reselect() {
198
- const { working_copy: chosen } = await this.house.post('/kit/working-copy/selection', {});
201
+ const { local_copy: chosen } = await this.house.post('/kit/local-copy/selection', {});
199
202
  const selected = [
200
203
  ...(chosen?.rooms ?? [])
201
204
  .filter((room) => room.room_handle !== null)
@@ -253,11 +256,11 @@ export class WorkingCopies {
253
256
  async push(cwd, selector, caller) {
254
257
  const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
255
258
  if (located.status !== 0)
256
- return { refused: true, text: `${cwd} is not in a Git repository` };
259
+ return { refused: true, text: `${cwd} is not a House Local copy; run house push inside one` };
257
260
  const [common, worktree] = located.stdout.toString('utf8').split('\n');
258
261
  const key = await this.keyAt(dirname(common));
259
262
  if (key === undefined)
260
- return { refused: true, text: `${worktree} is not a House working copy` };
263
+ return { refused: true, text: `${worktree} is not a House Local copy; run house push inside one` };
261
264
  return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
262
265
  }
263
266
  async keyAt(repository) {
@@ -285,7 +288,7 @@ export class WorkingCopies {
285
288
  }).catch(logged);
286
289
  }
287
290
  async save(copy) {
288
- const path = join(kitHome(), 'working-copies', copy.private ? 'private.json' : `${copy.key}.json`);
291
+ const path = join(kitHome(), 'local-copies', copy.private ? 'private.json' : `${copy.key}.json`);
289
292
  await mkdir(dirname(path), { recursive: true, mode: 0o700 });
290
293
  const temporary = `${path}.${process.pid}`;
291
294
  await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
@@ -294,11 +297,11 @@ export class WorkingCopies {
294
297
  return copy;
295
298
  }
296
299
  async read(selected) {
297
- const prefixes = prefixesOf(selected);
300
+ const prefixes = [rootOf(selected)];
298
301
  const discovery = await this.house.post('/kit/door/discover', { prefixes });
299
302
  let bundle = await this.house.post('/kit/door/bootstrap', { prefixes });
300
303
  if ('transfer' in bundle)
301
- bundle = (await (await fetch(bundle.transfer.url)).json());
304
+ bundle = (await (await fetch(relayed(bundle.transfer.url))).json());
302
305
  const [view] = bundle.scopes;
303
306
  if (view === undefined)
304
307
  return null;
@@ -341,7 +344,7 @@ export class WorkingCopies {
341
344
  return;
342
345
  }
343
346
  const discovery = await this.house.post('/kit/door/discover', {
344
- prefixes: prefixesOf(copy),
347
+ prefixes: [rootOf(copy)],
345
348
  });
346
349
  const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
347
350
  if (authority === undefined)
@@ -363,7 +366,7 @@ export class WorkingCopies {
363
366
  logged(`House serves nothing of ${rootOf({ private: scope.private, room_handle: scope.handle })} to copy`);
364
367
  return;
365
368
  }
366
- const repository = scope.private ? join(WORKING_COPIES, 'private') : join(WORKING_COPIES, 'rooms', read.anchor);
369
+ const repository = scope.private ? join(localCopies(), 'private') : join(localCopies(), 'rooms', read.anchor);
367
370
  if (await exists(repository)) {
368
371
  logged(`${repository} already exists and House Kit leaves it as it is`);
369
372
  return;
@@ -432,12 +435,12 @@ export class WorkingCopies {
432
435
  let answer;
433
436
  try {
434
437
  answer = await this.house.post('/kit/door/enumerate', {
435
- prefixes: prefixesOf(copy),
438
+ prefixes: [rootOf(copy)],
436
439
  positions: [{ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch }],
437
440
  });
438
441
  }
439
442
  catch (error) {
440
- const code = error instanceof HouseRefusal ? refusalCode(error.text) : null;
443
+ const code = error instanceof HouseRefusal ? (refusalOf(error.text)?.code ?? null) : null;
441
444
  if (code === 'position_expired')
442
445
  return this.received(copy);
443
446
  if (code !== 'replica_behind')
@@ -512,8 +515,9 @@ export class WorkingCopies {
512
515
  }
513
516
  copy = await this.save(without(copy));
514
517
  }
515
- if (!copy.active)
516
- return { refused: true, text: `${rootOf(copy)} is no longer kept in this Environment` };
518
+ if (!copy.active) {
519
+ return { refused: true, text: `${rootOf(copy)} is no longer synced to this Environment, so House takes no push from it` };
520
+ }
517
521
  const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
518
522
  const base = listed.find((ancestor) => ancestor in copy.manifests);
519
523
  if (base === undefined) {
@@ -582,9 +586,8 @@ export class WorkingCopies {
582
586
  async settle(start, caller, restaged = false) {
583
587
  let copy = start;
584
588
  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
- }
589
+ if (push.caller !== caller.key)
590
+ return this.recovered(copy);
588
591
  let outcome;
589
592
  try {
590
593
  outcome = await caller.submit(push.operation_id, push.changes, push.upload, async (upload) => {
@@ -592,9 +595,10 @@ export class WorkingCopies {
592
595
  });
593
596
  }
594
597
  catch (error) {
598
+ const refusal = error instanceof HouseRefusal ? refusalOf(error.text) : null;
595
599
  return {
596
600
  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.`,
601
+ text: refusal?.text ?? `House's answer to ${short(push.commit)} did not arrive; run house push again`,
598
602
  };
599
603
  }
600
604
  if ('refused' in outcome) {
@@ -603,22 +607,28 @@ export class WorkingCopies {
603
607
  return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
604
608
  }
605
609
  if (outcome.refused.code === 'operation_expired')
606
- return this.recovered(copy, outcome.refused.text);
610
+ return this.recovered(copy);
611
+ const rebased = rebaseFixes(outcome.refused);
607
612
  let settled = await this.save(without(copy));
608
613
  try {
609
614
  settled = await this.save(await this.caughtUp(settled));
610
615
  }
611
- catch (error) {
612
- return { refused: true, text: `${outcome.refused.text}\n${HOUSE_REF} is unchanged: ${causeOf(error)}` };
616
+ catch {
617
+ return {
618
+ refused: true,
619
+ text: rebased ? `${outcome.refused.text}\nHouse's current state did not arrive; push again later` : outcome.refused.text,
620
+ };
613
621
  }
614
622
  return {
615
623
  refused: true,
616
- text: `${outcome.refused.text}\n${HOUSE_REF} holds House's current state; merge or rebase onto it, commit and push again.`,
624
+ text: rebased
625
+ ? `${outcome.refused.text}\n${HOUSE_REF} holds House's current state; rebase onto it and push again`
626
+ : outcome.refused.text,
617
627
  };
618
628
  }
619
629
  return this.finished(await this.save({ ...copy, push: { ...copy.push, state: 'accepted', written: outcome.accepted } }));
620
630
  }
621
- async recovered(copy, reason) {
631
+ async recovered(copy) {
622
632
  const push = copy.push;
623
633
  const current = await this.save(await this.caughtUp(copy));
624
634
  const house = await tree(copy.repository, current.house);
@@ -627,7 +637,7 @@ export class WorkingCopies {
627
637
  await this.save(without(current));
628
638
  return {
629
639
  refused: true,
630
- text: `${reason}; House does not hold ${short(push.commit)}; run house git push again to submit it as a new operation.`,
640
+ text: `House does not hold ${short(push.commit)}; run house push again`,
631
641
  };
632
642
  }
633
643
  const written = [];
@@ -652,10 +662,10 @@ export class WorkingCopies {
652
662
  await this.save(without(copy));
653
663
  return { refused: false, text: integrated.text };
654
664
  }
655
- catch (error) {
665
+ catch {
656
666
  return {
657
667
  refused: false,
658
- text: `House accepted ${short(commit)}; local integration is pending (${causeOf(error)}); run house git push again to finish it.`,
668
+ text: `House accepted ${short(commit)}; run house push again to finish integrating it here`,
659
669
  };
660
670
  }
661
671
  }
package/dist/files.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { mkdir, writeFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
+ import { relayed } from './relay.js';
3
4
  export async function placeFiles(house, directory, files, signal) {
4
5
  const paths = [];
5
6
  for (const file of files) {
6
7
  const original = await house.post('/kit/conversation/originals/get', { version: file.version, download: true }, signal);
7
- const answer = await fetch(original.download.url, { signal });
8
+ const answer = await fetch(relayed(original.download.url), { signal });
8
9
  if (!answer.ok)
9
10
  throw new Error(`${file.name} did not download: ${answer.status}`);
10
11
  const folder = join(directory, '.house', 'files', original.message, file.version);
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,23 @@
1
- import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
2
3
  import { homedir } from 'node:os';
3
4
  import { join } from 'node:path';
4
5
  const ENROLMENT = 'credential.json';
6
+ const DISCONNECTED = 'disconnected.json';
7
+ const CONFIGURATION = 'kit.json';
8
+ const OWN_AGENT = 'own-agent.json';
5
9
  export function kitHome() {
6
10
  return process.env.HOUSE_KIT_HOME ?? join(homedir(), '.house-kit');
7
11
  }
8
- export async function readEnrolment() {
12
+ export function workspaceRoot() {
13
+ return process.env.HOUSE_KIT_WORKSPACE ?? '/agents/house';
14
+ }
15
+ export function agentBase() {
16
+ return join(workspaceRoot(), 'agents');
17
+ }
18
+ export async function readHome(name) {
9
19
  try {
10
- return JSON.parse(await readFile(join(kitHome(), ENROLMENT), 'utf8'));
20
+ return JSON.parse(await readFile(join(kitHome(), name), 'utf8'));
11
21
  }
12
22
  catch (error) {
13
23
  if (error.code === 'ENOENT')
@@ -15,10 +25,42 @@ export async function readEnrolment() {
15
25
  throw error;
16
26
  }
17
27
  }
18
- export async function writeEnrolment(enrolment) {
28
+ export async function writeHome(name, value) {
19
29
  const home = kitHome();
20
30
  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));
31
+ const temporary = join(home, `${name}.${process.pid}`);
32
+ await writeFile(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600 });
33
+ await rename(temporary, join(home, name));
34
+ }
35
+ export function readEnrolment() {
36
+ return readHome(ENROLMENT);
37
+ }
38
+ export async function writeEnrolment(enrolment) {
39
+ await writeHome(ENROLMENT, enrolment);
40
+ await rm(join(kitHome(), DISCONNECTED), { force: true });
41
+ }
42
+ export function readDisconnected() {
43
+ return readHome(DISCONNECTED);
44
+ }
45
+ export async function forgetEnrolment({ house, environment }) {
46
+ await writeHome(DISCONNECTED, { house, environment });
47
+ await rm(join(kitHome(), ENROLMENT), { force: true });
48
+ }
49
+ export function enrolled() {
50
+ return existsSync(join(kitHome(), ENROLMENT));
51
+ }
52
+ export function readOwnAgent() {
53
+ return readHome(OWN_AGENT);
54
+ }
55
+ export function writeOwnAgent(ownAgent) {
56
+ return writeHome(OWN_AGENT, ownAgent);
57
+ }
58
+ export function forgetOwnAgent() {
59
+ return rm(join(kitHome(), OWN_AGENT), { force: true });
60
+ }
61
+ export async function readConfiguration() {
62
+ return { skills: (await readHome(CONFIGURATION))?.skills !== false };
63
+ }
64
+ export function writeConfiguration(configuration) {
65
+ return writeHome(CONFIGURATION, configuration);
24
66
  }