@agentshouse/kit 0.2.1-alpha.9 → 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.
@@ -2,15 +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
14
  import { createHowWeWork } from './skills.js';
13
- const LAUNCH_BASE = '/agents/house';
15
+ import { transcribe } from './transcription.js';
14
16
  const KILLED = 'the conversation was killed';
15
17
  function causeOf(error) {
16
18
  return error instanceof Error ? error.message : String(error);
@@ -21,21 +23,15 @@ function answerMessage(request, response) {
21
23
  function logged(error) {
22
24
  process.stderr.write(`kit: ${causeOf(error)}\n`);
23
25
  }
24
- function unlessKilled(work, signal) {
25
- return new Promise((resolve, reject) => {
26
- const killed = () => reject(signal.reason);
27
- signal.addEventListener('abort', killed, { once: true });
28
- if (signal.aborted)
29
- killed();
30
- work.then(resolve, reject).finally(() => signal.removeEventListener('abort', killed));
31
- });
32
- }
33
26
  function stop(bridge) {
34
27
  killMarked(`HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`);
35
28
  }
36
29
  class Turn {
37
30
  id = randomUUID();
38
31
  text = '';
32
+ segment = null;
33
+ admitted = false;
34
+ notes = [];
39
35
  sent = [];
40
36
  unsentFrom = null;
41
37
  draftSequence = 0;
@@ -57,6 +53,8 @@ class Conversation {
57
53
  opening = null;
58
54
  kills = 0;
59
55
  killed = new AbortController();
56
+ interrupts = 0;
57
+ interrupted = new AbortController();
60
58
  open = 0;
61
59
  queue = Promise.resolve();
62
60
  reports = Promise.resolve();
@@ -92,6 +90,10 @@ export class Conversations {
92
90
  conversation.killed.abort(new Error(KILLED));
93
91
  this.halt(conversation);
94
92
  }
93
+ if (input.kind === 'interrupt') {
94
+ conversation.interrupts++;
95
+ conversation.interrupted.abort();
96
+ }
95
97
  const work = conversation.queue.then(() => this.carry(conversation, input));
96
98
  conversation.queue = work.catch(() => undefined);
97
99
  this.acks.set(input.input_id, work);
@@ -135,8 +137,11 @@ export class Conversations {
135
137
  return this.open(conversation, input);
136
138
  if (input.kind === 'message')
137
139
  return this.message(conversation, input);
138
- if (input.kind === 'interrupt')
140
+ if (input.kind === 'interrupt') {
141
+ if (--conversation.interrupts === 0)
142
+ conversation.interrupted = new AbortController();
139
143
  await this.interrupt(conversation, String(input.turn_id));
144
+ }
140
145
  if (input.kind === 'kill') {
141
146
  await this.kill(conversation);
142
147
  if (--conversation.kills === 0)
@@ -168,7 +173,9 @@ export class Conversations {
168
173
  const prompt = [];
169
174
  try {
170
175
  const route = await this.route(conversation, input.agent_id);
171
- 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);
172
179
  if (conversation.running === null) {
173
180
  const opened = await this.start(conversation, route, input.provider_session_id);
174
181
  if (input.provider_session_id === null)
@@ -181,8 +188,8 @@ export class Conversations {
181
188
  });
182
189
  }
183
190
  prompt.push({ type: 'text', text: String(input.text) });
184
- if (paths.length > 0)
185
- prompt.push({ type: 'text', text: paths.join('\n') });
191
+ if (lines.length > 0)
192
+ prompt.push({ type: 'text', text: lines.join('\n') });
186
193
  if (conversation.kills > 0)
187
194
  throw new Error(KILLED);
188
195
  }
@@ -194,6 +201,25 @@ export class Conversations {
194
201
  this.prompt(conversation, prompt);
195
202
  return ack;
196
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
+ }
197
223
  async interrupt(conversation, turnId) {
198
224
  if (conversation.running === null || conversation.turn?.id !== turnId)
199
225
  return;
@@ -232,13 +258,13 @@ export class Conversations {
232
258
  }
233
259
  async route(conversation, agentId) {
234
260
  const signal = conversation.killed.signal;
235
- await unlessKilled(this.kit.agents.read(), signal);
261
+ await unlessAborted(this.kit.agents.read(), signal);
236
262
  if (this.kit.agents.route(agentId) === undefined)
237
- await unlessKilled(this.kit.agents.refresh(), signal);
263
+ await unlessAborted(this.kit.agents.refresh(), signal);
238
264
  const route = this.kit.agents.route(agentId);
239
265
  if (route === undefined)
240
266
  throw new Error(`this Environment hosts no Agent ${agentId}`);
241
- if (route.working_directory === join(LAUNCH_BASE, route.agent_id)) {
267
+ if (route.working_directory === join(agentBase(), route.agent_id)) {
242
268
  await mkdir(route.working_directory, { recursive: true });
243
269
  }
244
270
  return route;
@@ -250,7 +276,7 @@ export class Conversations {
250
276
  .onNotification(TURN_ENDED, (params) => params, () => this.ended(conversation))
251
277
  .onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
252
278
  .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal));
253
- await unlessKilled(this.kit.agents.installed(), conversation.killed.signal);
279
+ const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
254
280
  const bridge = await openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
255
281
  if (conversation.kills > 0) {
256
282
  bridge.close();
@@ -258,8 +284,8 @@ export class Conversations {
258
284
  }
259
285
  conversation.opening = bridge;
260
286
  try {
261
- await unlessKilled(createHowWeWork(bridge), conversation.killed.signal);
262
- 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);
263
289
  void adapter.exited.then(() => bridge.close());
264
290
  const agent = adapter.connection.agent;
265
291
  const cwd = route.working_directory;
@@ -272,6 +298,7 @@ export class Conversations {
272
298
  sessionId: opened.sessionId,
273
299
  killed: false,
274
300
  queues: CLIS[route.kind].queues,
301
+ phase: CLIS[route.kind].phase,
275
302
  };
276
303
  const options = await this.launchSettings(running, route, opened.configOptions ?? []);
277
304
  conversation.running = running;
@@ -359,6 +386,9 @@ export class Conversations {
359
386
  return turn;
360
387
  }
361
388
  admitted(conversation, turn) {
389
+ turn.admitted = true;
390
+ for (const note of turn.notes.splice(0))
391
+ this.kit.send(note);
362
392
  if (turn.text !== '') {
363
393
  turn.unsentFrom = 0;
364
394
  this.draft(conversation, turn);
@@ -372,6 +402,17 @@ export class Conversations {
372
402
  this.kit.changed();
373
403
  return turn;
374
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
+ }
375
416
  end(conversation, turn, outcome) {
376
417
  if (turn.ended)
377
418
  return;
@@ -429,7 +470,7 @@ export class Conversations {
429
470
  this.end(conversation, turn, { failed });
430
471
  }
431
472
  else if (turn.prompt === sent) {
432
- this.end(conversation, turn, { text: turn.text });
473
+ this.finish(conversation, turn);
433
474
  }
434
475
  }
435
476
  started(conversation) {
@@ -446,7 +487,7 @@ export class Conversations {
446
487
  if (turn === null)
447
488
  return;
448
489
  if (turn.prompt === null) {
449
- this.end(conversation, turn, { text: turn.text });
490
+ this.finish(conversation, turn);
450
491
  }
451
492
  else {
452
493
  conversation.turn = null;
@@ -500,17 +541,21 @@ export class Conversations {
500
541
  update(conversation, notification) {
501
542
  const update = notification.update;
502
543
  if (update.sessionUpdate === 'agent_message_chunk' && update.content.type === 'text') {
503
- const turn = this.started(conversation);
504
- turn.text += update.content.text;
505
- this.draft(conversation, turn);
544
+ this.chunk(conversation, this.started(conversation), update, update.content.text);
506
545
  }
507
546
  else if (update.sessionUpdate === 'plan') {
508
547
  const turn = this.started(conversation);
548
+ this.close(conversation, turn);
509
549
  turn.entries = update.entries;
510
550
  this.plan(conversation, turn);
511
551
  }
512
- else if (update.sessionUpdate === 'agent_thought_chunk' || update.sessionUpdate === 'tool_call') {
513
- 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);
514
559
  }
515
560
  else if (update.sessionUpdate === 'available_commands_update') {
516
561
  this.commands(conversation, update.availableCommands);
@@ -526,6 +571,38 @@ export class Conversations {
526
571
  conversation.jobs.delete(job.id);
527
572
  this.kit.changed();
528
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
+ }
529
606
  draft(conversation, turn) {
530
607
  const blocks = blocksOf(turn.text);
531
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,11 +1,20 @@
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';
5
7
  const CONFIGURATION = 'kit.json';
8
+ const OWN_AGENT = 'own-agent.json';
6
9
  export function kitHome() {
7
10
  return process.env.HOUSE_KIT_HOME ?? join(homedir(), '.house-kit');
8
11
  }
12
+ export function workspaceRoot() {
13
+ return process.env.HOUSE_KIT_WORKSPACE ?? '/agents/house';
14
+ }
15
+ export function agentBase() {
16
+ return join(workspaceRoot(), 'agents');
17
+ }
9
18
  export async function readHome(name) {
10
19
  try {
11
20
  return JSON.parse(await readFile(join(kitHome(), name), 'utf8'));
@@ -26,8 +35,28 @@ export async function writeHome(name, value) {
26
35
  export function readEnrolment() {
27
36
  return readHome(ENROLMENT);
28
37
  }
29
- export function writeEnrolment(enrolment) {
30
- return writeHome(ENROLMENT, enrolment);
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 });
31
60
  }
32
61
  export async function readConfiguration() {
33
62
  return { skills: (await readHome(CONFIGURATION))?.skills !== false };