@agentshouse/kit 0.2.1-alpha.9 → 0.2.1-c5262a8cc063d

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,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 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 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 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 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 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,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 };