@agentshouse/kit 0.2.1-alpha.10 → 0.2.1-alpha.12

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.
package/dist/agents.js CHANGED
@@ -1,21 +1,64 @@
1
1
  import { client } from '@agentclientprotocol/sdk';
2
2
  import { readFile } from 'node:fs/promises';
3
- import { startAdapter } from './acp.js';
3
+ import { killTree, startAdapter } from './acp.js';
4
4
  import { CLIS, KIT_VERSION, cliCommand, install, run } from './clis.js';
5
5
  import { kitHome } from './home.js';
6
- async function signedIn(kind) {
7
- const probe = CLIS[kind].signedIn;
8
- if ('command' in probe) {
9
- return (await run(cliCommand(kind), probe.command, 30_000)).status === 0;
6
+ const SIGNED_OUT = { signed_in: false, models: [] };
7
+ function select(options, category) {
8
+ const option = options.find((candidate) => candidate.category === category);
9
+ return option?.type === 'select' ? option : undefined;
10
+ }
11
+ function values(options) {
12
+ return options.flatMap((entry) => ('group' in entry ? entry.options : [entry])).map((entry) => entry.value);
13
+ }
14
+ function causeOf(error) {
15
+ return error instanceof Error ? error.message : String(error);
16
+ }
17
+ async function models(kind, adapter, known) {
18
+ const agent = adapter.connection.agent;
19
+ const opened = await agent.request('session/new', { cwd: kitHome(), mcpServers: [] });
20
+ let options = opened.configOptions ?? [];
21
+ const model = select(options, 'model');
22
+ if (model === undefined)
23
+ return [];
24
+ const offered = [];
25
+ for (const value of values(model.options)) {
26
+ if (select(options, 'model')?.currentValue !== value) {
27
+ try {
28
+ options = (await agent.request('session/set_config_option', { sessionId: opened.sessionId, configId: model.id, value })).configOptions;
29
+ }
30
+ catch (error) {
31
+ process.stderr.write(`kit: ${kind} refused its model ${value}: ${causeOf(error)}\n`);
32
+ offered.push({ model: value, efforts: known.find((entry) => entry.model === value)?.efforts ?? [] });
33
+ continue;
34
+ }
35
+ }
36
+ const effort = select(options, 'thought_level');
37
+ offered.push({ model: value, efforts: effort === undefined ? [] : values(effort.options) });
10
38
  }
39
+ return offered;
40
+ }
41
+ async function offer(kind, known) {
42
+ const probe = CLIS[kind].signedIn;
43
+ if ('command' in probe && (await run(cliCommand(kind), probe.command, 30_000)).status !== 0)
44
+ return SIGNED_OUT;
45
+ let adapter;
11
46
  try {
12
- const adapter = await startAdapter(kind, kitHome(), client({ name: '@agentshouse/kit' }));
13
- adapter.child.kill('SIGKILL');
14
- const method = adapter.initialized._meta?.[probe.initializeMeta];
15
- return typeof method === 'string' && method.length > 0;
47
+ adapter = await startAdapter(kind, kitHome(), client({ name: '@agentshouse/kit' }));
48
+ if ('initializeMeta' in probe) {
49
+ const method = adapter.initialized._meta?.[probe.initializeMeta];
50
+ if (typeof method !== 'string' || method.length === 0)
51
+ return SIGNED_OUT;
52
+ }
53
+ return { signed_in: true, models: await models(kind, adapter, known.models) };
54
+ }
55
+ catch (error) {
56
+ process.stderr.write(`kit: ${kind} did not offer its models: ${causeOf(error)}\n`);
57
+ return adapter === undefined && 'initializeMeta' in probe ? known : { signed_in: true, models: known.models };
16
58
  }
17
- catch {
18
- return false;
59
+ finally {
60
+ if (adapter !== undefined)
61
+ killTree(adapter.child);
19
62
  }
20
63
  }
21
64
  async function sshHostKey() {
@@ -31,9 +74,12 @@ export class Agents {
31
74
  house;
32
75
  reading = Promise.resolve();
33
76
  installing = Promise.resolve();
34
- releases = new Map();
77
+ installs = new Map();
78
+ offers = new Map();
35
79
  reporting = null;
36
80
  again = false;
81
+ sending = Promise.resolve();
82
+ sent = null;
37
83
  constructor(house) {
38
84
  this.house = house;
39
85
  }
@@ -62,24 +108,36 @@ export class Agents {
62
108
  this.again = true;
63
109
  return Promise.all([installing, this.reporting]).then(() => undefined);
64
110
  }
65
- async report() {
111
+ report() {
112
+ const sending = this.sending.catch(() => undefined).then(() => this.send());
113
+ this.sending = sending;
114
+ return sending;
115
+ }
116
+ async send() {
66
117
  const agents = [];
67
- for (const [kind, release] of this.releases) {
68
- agents.push({ kind, release, signed_in: release !== null && (await signedIn(kind)) });
118
+ for (const [kind, installed] of this.installs) {
119
+ const offered = installed.release === null ? SIGNED_OUT : await offer(kind, this.offers.get(kind) ?? SIGNED_OUT);
120
+ this.offers.set(kind, offered);
121
+ agents.push({ kind, ...installed, ...offered });
69
122
  }
70
123
  const hostKey = await sshHostKey();
71
- await this.house.deliver('/kit/agents/report', {
124
+ const body = {
72
125
  os: await operatingSystem(),
73
126
  kit_version: KIT_VERSION,
74
127
  agents,
75
128
  ...(hostKey === null ? {} : { ssh_host_key: hostKey }),
76
- });
129
+ };
130
+ const state = JSON.stringify(body);
131
+ if (state === this.sent)
132
+ return;
133
+ await this.house.deliver('/kit/agents/report', body);
134
+ this.sent = state;
77
135
  }
78
136
  async install(kinds) {
79
- const releases = new Map();
137
+ const installs = new Map();
80
138
  for (const kind of kinds)
81
- releases.set(kind, await install(kind));
82
- this.releases = releases;
139
+ installs.set(kind, await install(kind));
140
+ this.installs = installs;
83
141
  }
84
142
  async reports() {
85
143
  try {
package/dist/api.js CHANGED
@@ -8,6 +8,9 @@ export class HouseRefusal extends Error {
8
8
  this.text = text;
9
9
  }
10
10
  }
11
+ export function retryDelay(attempt) {
12
+ return Math.min(30_000, 1000 * 2 ** attempt);
13
+ }
11
14
  export async function post(house, path, body, credential, signal) {
12
15
  const answer = await fetch(new URL(path, house), {
13
16
  method: 'POST',
@@ -38,7 +41,7 @@ export const house = {
38
41
  if (error instanceof HouseRefusal && error.status < 500)
39
42
  throw error;
40
43
  process.stderr.write(`kit: ${path} did not reach House: ${error.message}\n`);
41
- await new Promise((resolve) => setTimeout(resolve, Math.min(30_000, 1000 * 2 ** attempt)));
44
+ await new Promise((resolve) => setTimeout(resolve, retryDelay(attempt)));
42
45
  if (!current())
43
46
  throw error;
44
47
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.2.1-alpha.10",
3
+ "version": "0.2.1-alpha.12",
4
4
  "hosts": {
5
5
  "linux": {
6
- "command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.10/connect-linux.sh)",
6
+ "command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.12/connect-linux.sh)",
7
7
  "asset": {
8
8
  "name": "connect-linux.sh",
9
- "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.10/connect-linux.sh"
9
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.12/connect-linux.sh"
10
10
  },
11
11
  "workspace": {
12
12
  "argument": "--workspace",
@@ -14,10 +14,10 @@
14
14
  }
15
15
  },
16
16
  "windows": {
17
- "command": "& ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.10/connect-windows.ps1)))",
17
+ "command": "& ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.12/connect-windows.ps1)))",
18
18
  "asset": {
19
19
  "name": "connect-windows.ps1",
20
- "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.10/connect-windows.ps1"
20
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.12/connect-windows.ps1"
21
21
  },
22
22
  "workspace": {
23
23
  "argument": "--workspace",
package/dist/bridge.js CHANGED
@@ -1,17 +1,20 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { once } from 'node:events';
3
+ import { setTimeout as delay } from 'node:timers/promises';
3
4
  import { mkdir } from 'node:fs/promises';
4
5
  import { createServer, request as httpRequest } from 'node:http';
5
6
  import { request as httpsRequest } from 'node:https';
6
7
  import { delimiter, join } from 'node:path';
7
8
  import { parse } from 'yaml';
9
+ import { HouseRefusal, retryDelay } from './api.js';
8
10
  import { KIT_VERSION } from './clis.js';
9
- import { changedPaths, refusalCode, } from './copies.js';
11
+ import { changedPaths, } from './copies.js';
10
12
  import { kitHome, readEnrolment } from './home.js';
11
- import { operationId } from './operation.js';
13
+ import { operationId, uuidOf } from './operation.js';
14
+ import { refusalOf } from './refusals.js';
12
15
  const MCP_PROTOCOL_VERSION = '2026-07-28';
13
16
  const MCP_REQUEST_BYTES = 4 * 1024 * 1024;
14
- const FORWARDED = new Set(['/', '/kit/attachments/upload']);
17
+ const ATTACHMENTS = '/kit/attachments/upload';
15
18
  export async function received(request) {
16
19
  let text = '';
17
20
  request.setEncoding('utf8');
@@ -31,16 +34,45 @@ function relay(request, response, target, authorization) {
31
34
  forwarded.on('error', () => response.destroy());
32
35
  request.pipe(forwarded);
33
36
  }
37
+ const UNREACHABLE = 'House is unreachable; call again in a minute';
38
+ function refusalIn(text, status) {
39
+ return refusalOf(text) ?? { code: '', text: text === '' ? `House refused it with ${status}` : text, conflicts: [] };
40
+ }
34
41
  function answered(forwarded) {
35
42
  if (forwarded.status >= 500)
36
- throw new Error(`House answered ${forwarded.status}`);
43
+ throw new HouseRefusal('/', forwarded.status, forwarded.text);
37
44
  if (forwarded.status !== 200)
38
- return { refused: { code: refusalCode(forwarded.text), text: forwarded.text } };
45
+ return { refused: refusalIn(forwarded.text, forwarded.status) };
39
46
  const answer = JSON.parse(forwarded.text);
40
47
  if (answer.error !== undefined)
41
- return { refused: { code: refusalCode(answer.error.message), text: answer.error.message } };
48
+ return { refused: refusalIn(answer.error.message, forwarded.status) };
42
49
  const text = answer.result.content.map((content) => content.text).join('\n');
43
- return answer.result.isError === true ? { refused: { code: refusalCode(text), text } } : { text };
50
+ return answer.result.isError === true ? { refused: refusalIn(text, forwarded.status) } : { text };
51
+ }
52
+ function pendingSeconds(forwarded) {
53
+ if (forwarded.status !== 200)
54
+ return null;
55
+ const { result } = JSON.parse(forwarded.text);
56
+ if (result === undefined || result.isError === true)
57
+ return null;
58
+ const text = result.content.map((content) => content.text).join('\n');
59
+ if (!text.startsWith('{"pending":'))
60
+ return null;
61
+ const seconds = JSON.parse(text).pending.retry_after;
62
+ return typeof seconds === 'number' ? seconds : null;
63
+ }
64
+ function resendAfter(forwarded, attempt) {
65
+ if (forwarded === null)
66
+ return retryDelay(attempt);
67
+ if (forwarded.status >= 500)
68
+ return refusalOf(forwarded.text) === null ? retryDelay(attempt) : null;
69
+ const seconds = pendingSeconds(forwarded);
70
+ return seconds === null ? null : seconds * 1000;
71
+ }
72
+ function versioned(conversationId, text) {
73
+ const file = JSON.parse(text);
74
+ const identity = JSON.stringify([conversationId, file.room_ref ?? null, file.name, file.sha256]);
75
+ return JSON.stringify({ ...file, version: uuidOf(createHash('sha256').update(identity).digest(), 8) });
44
76
  }
45
77
  function editAnswer(forwarded) {
46
78
  const answer = answered(forwarded);
@@ -50,7 +82,7 @@ function editAnswer(forwarded) {
50
82
  return {
51
83
  accepted: results.flatMap((result) => [
52
84
  ...(result.from === undefined ? [] : [{ path: result.from, revision: null, content: null }]),
53
- { path: result.path, revision: result.op === 'remove' ? null : result.revision },
85
+ { path: result.path, revision: result.revision ?? null },
54
86
  ]),
55
87
  };
56
88
  }
@@ -70,7 +102,7 @@ export async function openBridge(house, copies, conversationId, signal) {
70
102
  },
71
103
  },
72
104
  });
73
- const send = async (message, operation) => {
105
+ const sent = async (message, operation) => {
74
106
  const tool = message.method === 'tools/call' ? String(message.params?.name) : null;
75
107
  const answer = await fetch(new URL('/', origin), {
76
108
  method: 'POST',
@@ -87,7 +119,22 @@ export async function openBridge(house, copies, conversationId, signal) {
87
119
  });
88
120
  return { status: answer.status, text: await answer.text() };
89
121
  };
90
- const call = (name, args) => ({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name, arguments: args } });
122
+ const send = async (message, operation) => {
123
+ if (operation === null)
124
+ return sent(message, null);
125
+ for (let attempt = 0;; attempt++) {
126
+ const forwarded = await sent(message, operation).catch((error) => {
127
+ if (closed.signal.aborted)
128
+ throw error;
129
+ return null;
130
+ });
131
+ const after = resendAfter(forwarded, attempt);
132
+ if (after === null)
133
+ return forwarded;
134
+ await delay(after, undefined, { signal: closed.signal });
135
+ }
136
+ };
137
+ const call = (name, args) => ({ jsonrpc: '2.0', id: randomUUID(), method: 'tools/call', params: { name, arguments: args } });
91
138
  const staged = async (operation, changes) => {
92
139
  const bytes = Buffer.from(JSON.stringify({ changes }));
93
140
  const sha256 = createHash('sha256').update(bytes).digest('hex');
@@ -106,24 +153,35 @@ export async function openBridge(house, copies, conversationId, signal) {
106
153
  });
107
154
  const text = await posted.text();
108
155
  if (posted.status >= 500)
109
- throw new Error(`House answered ${posted.status}`);
156
+ throw new HouseRefusal('/', posted.status, text);
110
157
  if (!posted.ok)
111
- return { refused: { code: refusalCode(text), text } };
158
+ return { refused: refusalIn(text, posted.status) };
112
159
  const held = JSON.parse(text).transfer;
113
160
  return { url: held.url, operation: held.operation, bytes: bytes.length, sha256 };
114
161
  };
162
+ let mutations = null;
163
+ const writes = async (tool) => {
164
+ mutations ??= house
165
+ .post('/kit/tools/mutations', {}, closed.signal)
166
+ .then(({ tools }) => new Set(tools.filter((listed) => listed.writes).map((listed) => listed.name)));
167
+ try {
168
+ return (await mutations).has(tool);
169
+ }
170
+ catch (error) {
171
+ mutations = null;
172
+ throw error;
173
+ }
174
+ };
115
175
  const forward = async (path, text) => {
116
176
  if (path === '/') {
117
177
  const message = JSON.parse(text);
118
178
  const tool = message.method === 'tools/call' ? String(message.params?.name) : null;
119
- const writes = tool !== null &&
120
- (await house.post('/kit/tools/mutations', {}, closed.signal)).tools.some((listed) => listed.name === tool && listed.writes);
121
- return send(message, writes ? operationId() : null);
179
+ return send(message, tool !== null && (await writes(tool)) ? operationId() : null);
122
180
  }
123
181
  const answer = await fetch(new URL(path, origin), {
124
182
  method: 'POST',
125
183
  headers: { 'content-type': 'application/json', authorization: `Bearer ${credential}` },
126
- body: text,
184
+ body: versioned(conversationId, text),
127
185
  signal: closed.signal,
128
186
  });
129
187
  return { status: answer.status, text: await answer.text() };
@@ -152,7 +210,7 @@ export async function openBridge(house, copies, conversationId, signal) {
152
210
  await copies.observed(JSON.parse(text));
153
211
  return { status: 200, text: '{}' };
154
212
  }
155
- return FORWARDED.has(path) ? forward(path, text) : { status: 404, text: '' };
213
+ return path === '/' || path === ATTACHMENTS ? forward(path, text) : { status: 404, text: '' };
156
214
  };
157
215
  const sockets = join(kitHome(), 'bridges');
158
216
  await mkdir(sockets, { recursive: true, mode: 0o700 });
@@ -168,7 +226,11 @@ export async function openBridge(house, copies, conversationId, signal) {
168
226
  response.writeHead(answered.status, { 'content-type': 'application/json' }).end(answered.text);
169
227
  }
170
228
  catch (error) {
171
- response.writeHead(502).end(error instanceof Error ? error.message : String(error));
229
+ if (error instanceof HouseRefusal) {
230
+ response.writeHead(error.status, { 'content-type': 'application/json' }).end(error.text);
231
+ return;
232
+ }
233
+ response.writeHead(502).end(request.url === '/git/push' && error instanceof Error ? error.message : UNREACHABLE);
172
234
  }
173
235
  });
174
236
  bridge.listen(socketPath);
@@ -195,7 +257,7 @@ export async function openBridge(house, copies, conversationId, signal) {
195
257
  GIT_CONFIG_VALUE_0: app,
196
258
  },
197
259
  tool: async (name, args) => {
198
- const forwarded = await forward('/', JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name, arguments: args } }));
260
+ const forwarded = await forward('/', JSON.stringify({ jsonrpc: '2.0', id: randomUUID(), method: 'tools/call', params: { name, arguments: args } }));
199
261
  if (forwarded.status !== 200)
200
262
  throw new Error(`House refused ${name} with ${forwarded.status}: ${forwarded.text}`);
201
263
  const answer = JSON.parse(forwarded.text);
package/dist/clis.js CHANGED
@@ -20,6 +20,14 @@ function asyncTask({ method, params }) {
20
20
  }
21
21
  return null;
22
22
  }
23
+ function airPhase({ _meta }) {
24
+ const phase = _meta?.jetbrains?.air?.phase;
25
+ if (phase === 'commentary')
26
+ return 'note';
27
+ if (phase === 'final_answer')
28
+ return 'answer';
29
+ return null;
30
+ }
23
31
  export const CLIS = {
24
32
  'codex-acp': {
25
33
  package: '@agentclientprotocol/codex-acp',
@@ -31,6 +39,7 @@ export const CLIS = {
31
39
  turnStarted: (notice) => threadStatus(notice) === 'active',
32
40
  turnEnded: (notice) => threadStatus(notice) === 'idle',
33
41
  job: asyncTask,
42
+ phase: airPhase,
34
43
  },
35
44
  'claude-agent-acp': {
36
45
  package: '@agentclientprotocol/claude-agent-acp',
@@ -44,6 +53,7 @@ export const CLIS = {
44
53
  params?.update?.sessionUpdate === 'usage_update' &&
45
54
  params.update._meta?.['_claude/origin'] !== undefined,
46
55
  job: asyncTask,
56
+ phase: () => null,
47
57
  },
48
58
  'grok-build': {
49
59
  package: '@xai-official/grok',
@@ -61,6 +71,7 @@ export const CLIS = {
61
71
  return { id: params.update.task_snapshot.task_id, running: false };
62
72
  return null;
63
73
  },
74
+ phase: () => null,
64
75
  },
65
76
  };
66
77
  const KIT_PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
@@ -104,11 +115,12 @@ export function run(command, args, timeoutMs) {
104
115
  export async function install(kind) {
105
116
  const release = pinned(kind);
106
117
  if ((await installedRelease(kind)) === release)
107
- return release;
118
+ return { release, failure: null };
108
119
  const ran = await run('npm', ['install', '--prefix', prefix(kind), '--no-save', '--no-audit', '--no-fund', `${CLIS[kind].package}@${release}`], 15 * 60_000);
109
120
  if (ran.status !== 0) {
110
- process.stderr.write(`kit: ${kind} did not install: ${ran.output.trim()}\n`);
111
- return null;
121
+ const failure = ran.output.trim() || `npm exited with ${ran.status ?? 'a signal'}`;
122
+ process.stderr.write(`kit: ${kind} did not install: ${failure}\n`);
123
+ return { release: null, failure };
112
124
  }
113
- return installedRelease(kind);
125
+ return { release: await installedRelease(kind), failure: null };
114
126
  }
@@ -36,6 +36,9 @@ function stop(bridge) {
36
36
  class Turn {
37
37
  id = randomUUID();
38
38
  text = '';
39
+ segment = null;
40
+ admitted = false;
41
+ notes = [];
39
42
  sent = [];
40
43
  unsentFrom = null;
41
44
  draftSequence = 0;
@@ -272,6 +275,7 @@ export class Conversations {
272
275
  sessionId: opened.sessionId,
273
276
  killed: false,
274
277
  queues: CLIS[route.kind].queues,
278
+ phase: CLIS[route.kind].phase,
275
279
  };
276
280
  const options = await this.launchSettings(running, route, opened.configOptions ?? []);
277
281
  conversation.running = running;
@@ -359,6 +363,9 @@ export class Conversations {
359
363
  return turn;
360
364
  }
361
365
  admitted(conversation, turn) {
366
+ turn.admitted = true;
367
+ for (const note of turn.notes.splice(0))
368
+ this.kit.send(note);
362
369
  if (turn.text !== '') {
363
370
  turn.unsentFrom = 0;
364
371
  this.draft(conversation, turn);
@@ -372,6 +379,17 @@ export class Conversations {
372
379
  this.kit.changed();
373
380
  return turn;
374
381
  }
382
+ finish(conversation, turn) {
383
+ if (turn.ended)
384
+ return;
385
+ const segment = turn.segment;
386
+ turn.segment = null;
387
+ if (segment?.phase === 'note')
388
+ this.note(conversation, turn, segment.text);
389
+ else if (segment !== null)
390
+ this.answer(conversation, turn, segment.text);
391
+ this.end(conversation, turn, { text: turn.text });
392
+ }
375
393
  end(conversation, turn, outcome) {
376
394
  if (turn.ended)
377
395
  return;
@@ -429,7 +447,7 @@ export class Conversations {
429
447
  this.end(conversation, turn, { failed });
430
448
  }
431
449
  else if (turn.prompt === sent) {
432
- this.end(conversation, turn, { text: turn.text });
450
+ this.finish(conversation, turn);
433
451
  }
434
452
  }
435
453
  started(conversation) {
@@ -446,7 +464,7 @@ export class Conversations {
446
464
  if (turn === null)
447
465
  return;
448
466
  if (turn.prompt === null) {
449
- this.end(conversation, turn, { text: turn.text });
467
+ this.finish(conversation, turn);
450
468
  }
451
469
  else {
452
470
  conversation.turn = null;
@@ -500,17 +518,21 @@ export class Conversations {
500
518
  update(conversation, notification) {
501
519
  const update = notification.update;
502
520
  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);
521
+ this.chunk(conversation, this.started(conversation), update, update.content.text);
506
522
  }
507
523
  else if (update.sessionUpdate === 'plan') {
508
524
  const turn = this.started(conversation);
525
+ this.close(conversation, turn);
509
526
  turn.entries = update.entries;
510
527
  this.plan(conversation, turn);
511
528
  }
512
- else if (update.sessionUpdate === 'agent_thought_chunk' || update.sessionUpdate === 'tool_call') {
513
- this.started(conversation);
529
+ else if (update.sessionUpdate === 'tool_call') {
530
+ this.close(conversation, this.started(conversation));
531
+ }
532
+ else if (update.sessionUpdate === 'agent_thought_chunk') {
533
+ const turn = this.started(conversation);
534
+ if (turn.segment?.messageId !== (update.messageId ?? null))
535
+ this.close(conversation, turn);
514
536
  }
515
537
  else if (update.sessionUpdate === 'available_commands_update') {
516
538
  this.commands(conversation, update.availableCommands);
@@ -526,6 +548,38 @@ export class Conversations {
526
548
  conversation.jobs.delete(job.id);
527
549
  this.kit.changed();
528
550
  }
551
+ chunk(conversation, turn, chunk, text) {
552
+ const phase = conversation.running?.phase(chunk) ?? null;
553
+ const messageId = chunk.messageId ?? null;
554
+ if (turn.segment?.messageId !== messageId || turn.segment.phase !== phase)
555
+ this.close(conversation, turn);
556
+ if (phase === 'answer') {
557
+ this.answer(conversation, turn, text);
558
+ return;
559
+ }
560
+ turn.segment ??= { messageId, phase, text: '' };
561
+ turn.segment.text += text;
562
+ }
563
+ close(conversation, turn) {
564
+ const segment = turn.segment;
565
+ if (segment === null)
566
+ return;
567
+ turn.segment = null;
568
+ this.note(conversation, turn, segment.text);
569
+ }
570
+ note(conversation, turn, text) {
571
+ if (text.trim() === '')
572
+ return;
573
+ const note = { type: 'working-note', conversation_id: conversation.id, note_id: randomUUID(), text };
574
+ if (turn.admitted)
575
+ this.kit.send(note);
576
+ else
577
+ turn.notes.push(note);
578
+ }
579
+ answer(conversation, turn, text) {
580
+ turn.text += text;
581
+ this.draft(conversation, turn);
582
+ }
529
583
  draft(conversation, turn) {
530
584
  const blocks = blocksOf(turn.text);
531
585
  const from = Math.min(firstChange(turn.sent, blocks), turn.unsentFrom ?? Infinity);
package/dist/copies.js CHANGED
@@ -6,6 +6,7 @@ import { HouseRefusal } from './api.js';
6
6
  import { blobId, blobText, git, HOUSE_REF, importCommits, revision, run, succeeded, tree, WORKING_COPIES, } 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
12
  const ROOM_HOMES = ['capture', 'library', 'collaboration'];
@@ -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);
@@ -166,7 +170,9 @@ export class WorkingCopies {
166
170
  catch (error) {
167
171
  if (!(error instanceof HouseRefusal) || error.status >= 500)
168
172
  throw error;
169
- return { refused: { code: refusalCode(error.text), text: error.message } };
173
+ return {
174
+ refused: refusalOf(error.text) ?? { code: '', text: `House refused it with ${error.status}`, conflicts: [] },
175
+ };
170
176
  }
171
177
  },
172
178
  };
@@ -253,11 +259,11 @@ export class WorkingCopies {
253
259
  async push(cwd, selector, caller) {
254
260
  const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
255
261
  if (located.status !== 0)
256
- return { refused: true, text: `${cwd} is not in a Git repository` };
262
+ return { refused: true, text: `${cwd} is not a House working copy; run house git push inside one` };
257
263
  const [common, worktree] = located.stdout.toString('utf8').split('\n');
258
264
  const key = await this.keyAt(dirname(common));
259
265
  if (key === undefined)
260
- return { refused: true, text: `${worktree} is not a House working copy` };
266
+ return { refused: true, text: `${worktree} is not a House working copy; run house git push inside one` };
261
267
  return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
262
268
  }
263
269
  async keyAt(repository) {
@@ -437,7 +443,7 @@ export class WorkingCopies {
437
443
  });
438
444
  }
439
445
  catch (error) {
440
- const code = error instanceof HouseRefusal ? refusalCode(error.text) : null;
446
+ const code = error instanceof HouseRefusal ? (refusalOf(error.text)?.code ?? null) : null;
441
447
  if (code === 'position_expired')
442
448
  return this.received(copy);
443
449
  if (code !== 'replica_behind')
@@ -512,8 +518,9 @@ export class WorkingCopies {
512
518
  }
513
519
  copy = await this.save(without(copy));
514
520
  }
515
- if (!copy.active)
516
- return { refused: true, text: `${rootOf(copy)} is no longer kept in this Environment` };
521
+ if (!copy.active) {
522
+ return { refused: true, text: `${rootOf(copy)} is no longer synced to this Environment, so House takes no push from it` };
523
+ }
517
524
  const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
518
525
  const base = listed.find((ancestor) => ancestor in copy.manifests);
519
526
  if (base === undefined) {
@@ -582,9 +589,8 @@ export class WorkingCopies {
582
589
  async settle(start, caller, restaged = false) {
583
590
  let copy = start;
584
591
  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
- }
592
+ if (push.caller !== caller.key)
593
+ return this.recovered(copy);
588
594
  let outcome;
589
595
  try {
590
596
  outcome = await caller.submit(push.operation_id, push.changes, push.upload, async (upload) => {
@@ -592,9 +598,10 @@ export class WorkingCopies {
592
598
  });
593
599
  }
594
600
  catch (error) {
601
+ const refusal = error instanceof HouseRefusal ? refusalOf(error.text) : null;
595
602
  return {
596
603
  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.`,
604
+ text: refusal?.text ?? `House's answer to ${short(push.commit)} did not arrive; run house git push again`,
598
605
  };
599
606
  }
600
607
  if ('refused' in outcome) {
@@ -603,22 +610,28 @@ export class WorkingCopies {
603
610
  return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
604
611
  }
605
612
  if (outcome.refused.code === 'operation_expired')
606
- return this.recovered(copy, outcome.refused.text);
613
+ return this.recovered(copy);
614
+ const rebased = rebaseFixes(outcome.refused);
607
615
  let settled = await this.save(without(copy));
608
616
  try {
609
617
  settled = await this.save(await this.caughtUp(settled));
610
618
  }
611
- catch (error) {
612
- return { refused: true, text: `${outcome.refused.text}\n${HOUSE_REF} is unchanged: ${causeOf(error)}` };
619
+ catch {
620
+ return {
621
+ refused: true,
622
+ text: rebased ? `${outcome.refused.text}\nHouse's current state did not arrive; push again later` : outcome.refused.text,
623
+ };
613
624
  }
614
625
  return {
615
626
  refused: true,
616
- text: `${outcome.refused.text}\n${HOUSE_REF} holds House's current state; merge or rebase onto it, commit and push again.`,
627
+ text: rebased
628
+ ? `${outcome.refused.text}\n${HOUSE_REF} holds House's current state; rebase onto it and push again`
629
+ : outcome.refused.text,
617
630
  };
618
631
  }
619
632
  return this.finished(await this.save({ ...copy, push: { ...copy.push, state: 'accepted', written: outcome.accepted } }));
620
633
  }
621
- async recovered(copy, reason) {
634
+ async recovered(copy) {
622
635
  const push = copy.push;
623
636
  const current = await this.save(await this.caughtUp(copy));
624
637
  const house = await tree(copy.repository, current.house);
@@ -627,7 +640,7 @@ export class WorkingCopies {
627
640
  await this.save(without(current));
628
641
  return {
629
642
  refused: true,
630
- text: `${reason}; House does not hold ${short(push.commit)}; run house git push again to submit it as a new operation.`,
643
+ text: `House does not hold ${short(push.commit)}; run house git push again`,
631
644
  };
632
645
  }
633
646
  const written = [];
@@ -652,10 +665,10 @@ export class WorkingCopies {
652
665
  await this.save(without(copy));
653
666
  return { refused: false, text: integrated.text };
654
667
  }
655
- catch (error) {
668
+ catch {
656
669
  return {
657
670
  refused: false,
658
- text: `House accepted ${short(commit)}; local integration is pending (${causeOf(error)}); run house git push again to finish it.`,
671
+ text: `House accepted ${short(commit)}; run house git push again to finish integrating it here`,
659
672
  };
660
673
  }
661
674
  }
package/dist/git.js CHANGED
@@ -50,7 +50,7 @@ export async function blobText(repository, object, path) {
50
50
  return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
51
51
  }
52
52
  catch {
53
- throw new Error(`${path} is not UTF-8 text`);
53
+ throw new Error(`${path} is binary and House stores only text; remove it from the commit`);
54
54
  }
55
55
  }
56
56
  export function blobId(content) {
@@ -4,10 +4,25 @@ import { readFile } from 'node:fs/promises';
4
4
  import { request } from 'node:http';
5
5
  import { basename, join } from 'node:path';
6
6
  import { kitHome } from './home.js';
7
+ import { refusalOf } from './refusals.js';
7
8
  const USAGE = "usage: house <tool> ['<arguments as JSON>']";
8
- const GIT_HELP = 'Submit a Room commit to House; default HEAD. Use --owner only outside an Agent conversation. Native git push is not a House remote.';
9
- const GIT_USAGE = `usage: house git push [commit] [--owner]\n${GIT_HELP}`;
9
+ const GIT_USAGE = 'usage: house git push [commit] [--owner]';
10
+ const ARGUMENTS = `the arguments are one JSON object in single quotes, like house search '{"query":"invoice"}'`;
11
+ const CLOSED = "this conversation's House connection is closed; nothing to do from here";
12
+ const REVOKED = 'this conversation no longer has House access; nothing to do from here';
10
13
  const socketPath = process.env.HOUSE_BRIDGE;
14
+ function refused(path, status, text) {
15
+ const refusal = refusalOf(text);
16
+ if (refusal !== null)
17
+ return refusal.text;
18
+ if (status === 401)
19
+ return REVOKED;
20
+ if (URL.canParse(path))
21
+ return `the upload answered ${status}; upload the file again`;
22
+ if (status === 502 && /^[A-Za-z]/.test(text))
23
+ return text;
24
+ return `${path} answered ${status}`;
25
+ }
11
26
  function bridged(path, body, headers = { 'content-type': 'application/json' }, to = socketPath) {
12
27
  return new Promise((resolve, reject) => {
13
28
  const sent = request({ socketPath: to, path, method: 'POST', headers }, (answer) => {
@@ -20,15 +35,15 @@ function bridged(path, body, headers = { 'content-type': 'application/json' }, t
20
35
  if (answer.statusCode === 200)
21
36
  resolve(text);
22
37
  else
23
- reject(new Error(`${path} answered ${answer.statusCode}: ${text}`));
38
+ reject(new Error(refused(path, answer.statusCode, text)));
24
39
  });
25
40
  });
26
- sent.on('error', reject);
41
+ sent.on('error', () => reject(new Error(CLOSED)));
27
42
  sent.end(body instanceof Buffer ? body : JSON.stringify(body));
28
43
  });
29
44
  }
30
45
  async function mcp(method, params) {
31
- const answer = JSON.parse(await bridged('/', { jsonrpc: '2.0', id: 1, method, params }));
46
+ const answer = JSON.parse(await bridged('/', { jsonrpc: '2.0', id: randomUUID(), method, params }));
32
47
  if (answer.error !== undefined)
33
48
  throw new Error(answer.error.message);
34
49
  return answer.result;
@@ -36,29 +51,45 @@ async function mcp(method, params) {
36
51
  async function tools() {
37
52
  return (await mcp('tools/list', {})).tools;
38
53
  }
39
- async function upload(path, roomRef) {
40
- const bytes = await readFile(path);
54
+ async function localFile(path) {
55
+ try {
56
+ return await readFile(path);
57
+ }
58
+ catch (error) {
59
+ throw new Error(error.code === 'EISDIR'
60
+ ? `${path} is a folder; upload one file at a time`
61
+ : `${path} is not a readable file`);
62
+ }
63
+ }
64
+ async function upload(path, roomRef, missing) {
65
+ if (typeof path !== 'string' || path === '')
66
+ throw new Error(missing);
67
+ const bytes = await localFile(path);
41
68
  const declared = JSON.parse(await bridged('/kit/attachments/upload', {
42
69
  ...(roomRef === undefined ? {} : { room_ref: roomRef }),
43
- version: randomUUID(),
44
70
  name: basename(path),
45
71
  bytes: bytes.length,
46
72
  sha256: createHash('sha256').update(bytes).digest('hex'),
47
73
  }));
48
- return bridged(declared.upload.url, bytes, {
74
+ if (declared.upload === undefined)
75
+ return declared.attachment;
76
+ const saved = JSON.parse(await bridged(declared.upload.url, bytes, {
49
77
  'content-type': 'application/octet-stream',
50
78
  'x-house-byte-operation': declared.upload.operation,
51
- });
79
+ }));
80
+ if (saved.upload_failure !== undefined)
81
+ throw new Error(`${path} was not saved: ${saved.upload_failure}; call again later`);
82
+ return saved.attachment;
52
83
  }
53
84
  async function gitPush(args) {
54
85
  if (args.includes('--help'))
55
86
  return GIT_USAGE;
56
87
  if (args[0] !== 'push')
57
- throw new Error(GIT_USAGE);
88
+ throw new Error('only house git push [commit] exists; use plain git for the rest');
58
89
  const owner = args.includes('--owner');
59
90
  const commit = args.slice(1).find((arg) => arg !== '--owner') ?? 'HEAD';
60
91
  if (owner && socketPath !== undefined)
61
- throw new Error('house git push --owner runs outside an Agent conversation');
92
+ throw new Error('--owner is not for Agent conversations; run house git push without it');
62
93
  if (!owner && socketPath === undefined)
63
94
  throw new Error('outside an Agent conversation, house git push needs --owner');
64
95
  const pushed = JSON.parse(await bridged('/git/push', { cwd: process.cwd(), commit }, undefined, owner ? join(kitHome(), 'owner.sock') : socketPath));
@@ -66,30 +97,45 @@ async function gitPush(args) {
66
97
  throw new Error(pushed.text);
67
98
  return pushed.text;
68
99
  }
100
+ function argumentsOf(argument) {
101
+ if (argument === undefined)
102
+ return {};
103
+ let parsed;
104
+ try {
105
+ parsed = JSON.parse(argument);
106
+ }
107
+ catch {
108
+ throw new Error(ARGUMENTS);
109
+ }
110
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
111
+ throw new Error(ARGUMENTS);
112
+ return parsed;
113
+ }
69
114
  async function house([verb, argument]) {
70
115
  if (verb === undefined || verb === '--help') {
71
- return [USAGE, `git push [commit]: ${GIT_HELP}`, ...(await tools()).map((tool) => `${tool.name}: ${tool.description}`)].join('\n');
116
+ return [USAGE, 'git push [commit]', ...(await tools()).map((tool) => `${tool.name}: ${tool.description}`)].join('\n');
72
117
  }
73
118
  if (argument === '--help') {
74
119
  const tool = (await tools()).find((listed) => listed.name === verb);
75
120
  if (tool === undefined)
76
- throw new Error(`House has no tool ${verb}`);
77
- return `${tool.description}\n${JSON.stringify(tool.inputSchema, null, 2)}`;
121
+ throw new Error(`${verb} is no Tool; house find_command '{"query":"${verb}"}' finds Commands`);
122
+ return `${tool.description}\n${JSON.stringify(tool.inputSchema)}`;
123
+ }
124
+ const args = argumentsOf(argument);
125
+ if (verb === 'upload_attachment') {
126
+ return JSON.stringify({ attachment: await upload(args.path, args.room_ref, 'upload_attachment needs "path", a local file') });
78
127
  }
79
- const args = (argument === undefined ? {} : JSON.parse(argument));
80
- if (verb === 'upload_attachment')
81
- return upload(String(args.path), args.room_ref);
82
128
  if (verb === 'append_record' && Array.isArray(args.attachments)) {
83
129
  const attachments = [];
84
130
  for (const path of args.attachments) {
85
- attachments.push(JSON.parse(await upload(String(path), args.room_ref)).attachment);
131
+ attachments.push(await upload(path, args.room_ref, 'each attachments entry is the path of a local file'));
86
132
  }
87
133
  args.attachments = attachments;
88
134
  }
89
135
  const result = await mcp('tools/call', { name: verb, arguments: args });
90
136
  const text = result.content.map((content) => content.text).join('\n');
91
137
  if (result.isError === true)
92
- throw new Error(text);
138
+ throw new Error(text.replace(/\n+$/, ''));
93
139
  return text;
94
140
  }
95
141
  const argv = process.argv.slice(2);
@@ -1,33 +1,33 @@
1
- import { readEnrolment } from './home.js';
2
1
  import { HOW_WE_WORK } from './skills.js';
3
2
  const HOUSE_LINE = 'Work with House through the `house` CLI: run `house --help`.';
4
- const FILE_LINE = 'To give the User a file, upload it with `house upload_attachment` and link it in your answer.';
5
- const COPIES_LINE = 'These House paths are Git working copies here; commit in one and run `house git push` to send the commit to House:';
3
+ const FILE_LINE = 'To give the User a file or a page, upload it with `house upload_attachment` and link it; never start a server.';
4
+ const COPIES_LINE = 'House working copies; commit, then `house git push`:';
6
5
  const UNREADABLE = /^(?:\S+: )?(?:path_not_found|room_not_found|operation_denied)\b/;
6
+ const FAILED = /^exit: [1-9]\d*$/;
7
7
  const CUT = /^stderr: shell: output_cut (\d+) of \d+ bytes; continue with: (.+)$/;
8
+ function cutOf(lines) {
9
+ const cut = CUT.exec(lines[0] ?? '');
10
+ return cut !== null && Buffer.byteLength(lines.slice(1).join('\n')) === Number(cut[1]) ? cut : null;
11
+ }
8
12
  async function howWeWork(bridge) {
9
13
  let document = '';
10
14
  for (let command = `cat ${HOW_WE_WORK}`; command !== undefined;) {
11
15
  const read = await bridge.tool('shell', { command });
12
16
  const text = read.content.map((content) => content.text).join('\n');
13
17
  const lines = text.split('\n');
14
- let at = lines[5]?.startsWith('authority: ') ? 6 : 5;
15
- if (read.isError === true || lines[2] !== 'exit: 0') {
16
- const refusal = read.isError === true ? text : lines.slice(at).join('\n').replace(/^stderr: /gm, '');
18
+ if (read.isError === true || (FAILED.test(lines[0] ?? '') && lines[1]?.startsWith('stderr: '))) {
19
+ const refusal = read.isError === true ? text : lines.slice(1).join('\n').replace(/^stderr: /gm, '');
17
20
  if (UNREADABLE.test(refusal))
18
21
  return '';
19
22
  throw new Error(refusal);
20
23
  }
21
- const cut = lines[3] === 'truncation: egress' ? CUT.exec(lines[at++] ?? '') : null;
22
- const shown = Buffer.from(`${lines.slice(at).join('\n')}\n`);
23
- document += (cut === null ? shown : shown.subarray(0, Number(cut[1]))).toString();
24
+ const cut = cutOf(lines);
25
+ document += lines.slice(cut === null ? 0 : 1).join('\n');
24
26
  command = cut?.[2];
25
27
  }
26
28
  return document.replace(/\n$/, '');
27
29
  }
28
30
  export async function instructions(bridge, base, copies) {
29
- const app = new URL('/app/new.git', (await readEnrolment()).house).href;
30
- const appLine = `To show the User a page, upload it or push it to an App instead of starting a server; \`git clone ${app}\` starts a new App.`;
31
- const lines = [HOUSE_LINE, FILE_LINE, appLine, ...(copies.length === 0 ? [] : [COPIES_LINE, ...copies])];
31
+ const lines = [HOUSE_LINE, FILE_LINE, ...(copies.length === 0 ? [] : [COPIES_LINE, ...copies])];
32
32
  return [lines.join('\n'), base, await howWeWork(bridge)].filter((part) => part !== '').join('\n\n');
33
33
  }
package/dist/operation.js CHANGED
@@ -1,9 +1,12 @@
1
1
  import { randomBytes } from 'node:crypto';
2
+ export function uuidOf(bytes, version) {
3
+ bytes[6] = (version << 4) | (bytes[6] & 0x0f);
4
+ bytes[8] = 0x80 | (bytes[8] & 0x3f);
5
+ const hex = bytes.toString('hex', 0, 16);
6
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
7
+ }
2
8
  export function operationId() {
3
9
  const bytes = randomBytes(16);
4
10
  bytes.writeUIntBE(Date.now(), 0, 6);
5
- bytes[6] = 0x70 | (bytes[6] & 0x0f);
6
- bytes[8] = 0x80 | (bytes[8] & 0x3f);
7
- const hex = bytes.toString('hex');
8
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
11
+ return uuidOf(bytes, 7);
9
12
  }
@@ -0,0 +1,41 @@
1
+ import { parse } from 'yaml';
2
+ const HEAD = /^([a-z][a-z0-9_]*): \S/;
3
+ function conflictCodes(detail) {
4
+ let parsed;
5
+ try {
6
+ parsed = parse(detail);
7
+ }
8
+ catch {
9
+ return [];
10
+ }
11
+ const conflicts = parsed?.conflicts;
12
+ if (!Array.isArray(conflicts))
13
+ return [];
14
+ return conflicts.flatMap((conflict) => typeof conflict?.code === 'string' ? [conflict.code] : []);
15
+ }
16
+ function spoken(text) {
17
+ const head = HEAD.exec(text);
18
+ if (head === null)
19
+ return null;
20
+ const said = text.replace(/\n+$/, '');
21
+ const detail = said.indexOf('\n');
22
+ return { code: head[1], text: said, conflicts: detail === -1 ? [] : conflictCodes(said.slice(detail + 1)) };
23
+ }
24
+ export function refusalOf(text) {
25
+ let body;
26
+ try {
27
+ body = JSON.parse(text);
28
+ }
29
+ catch {
30
+ return spoken(text);
31
+ }
32
+ const error = body?.error;
33
+ if (typeof error !== 'object' || error === null)
34
+ return null;
35
+ const { code, message, data } = error;
36
+ const said = typeof message === 'string' ? spoken(message) : null;
37
+ if (said !== null)
38
+ return said;
39
+ const named = typeof code === 'string' ? code : typeof data?.code === 'string' ? data.code : null;
40
+ return named === null ? null : { code: named, text: named, conflicts: [] };
41
+ }
package/dist/resident.js CHANGED
@@ -14,6 +14,7 @@ import { SignIns } from './sign-in.js';
14
14
  import { placeSkillSet } from './skills.js';
15
15
  import { holdStream } from './stream.js';
16
16
  import { Updates } from './update.js';
17
+ const AGENTS_REREAD_MS = 60 * 60_000;
17
18
  function report(error) {
18
19
  process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
19
20
  }
@@ -93,6 +94,7 @@ export async function resident() {
93
94
  await house.deliver('/kit/restarted', {}).catch(report);
94
95
  await updates.resumed().catch(report);
95
96
  logged(agents.refresh());
97
+ setInterval(() => logged(agents.refresh()), AGENTS_REREAD_MS);
96
98
  stream = holdStream({
97
99
  opened: () => {
98
100
  conversations.opened();
package/dist/skills.js CHANGED
@@ -3,10 +3,9 @@ import { cp, lstat, mkdir, readdir, realpath, rm, symlink, writeFile } from 'nod
3
3
  import { homedir } from 'node:os';
4
4
  import { join, relative } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
- import { parse } from 'yaml';
7
6
  import { KIT_VERSION } from './clis.js';
8
- import { refusalCode } from './copies.js';
9
7
  import { kitHome, readConfiguration, readHome, writeHome } from './home.js';
8
+ import { refusalOf } from './refusals.js';
10
9
  export const HOW_WE_WORK = '/private/library/how-we-work.md';
11
10
  const SKILL_SET = fileURLToPath(new URL('../skills/', import.meta.url));
12
11
  const HOW_WE_WORK_TEXT = readFileSync(new URL('../how-we-work.md', import.meta.url), 'utf8');
@@ -63,20 +62,16 @@ export async function placeSkillSet() {
63
62
  else
64
63
  await rm(join(kitHome(), RECORD), { force: true });
65
64
  }
66
- function documentPresent(refusal) {
67
- const { conflicts } = parse(refusal.slice(refusal.indexOf('\n') + 1));
68
- return conflicts.some((conflict) => conflict.code === 'path_exists');
69
- }
70
65
  export async function createHowWeWork(bridge) {
71
66
  if ((await readHome(RECORD)) === null || (await present(join(kitHome(), CREATED))))
72
67
  return;
73
68
  const created = await bridge.tool('edit', { changes: [{ op: 'create', path: HOW_WE_WORK, content: HOW_WE_WORK_TEXT }] });
74
69
  if (created.isError === true) {
75
70
  const text = created.content.map((content) => content.text).join('\n');
76
- const code = refusalCode(text);
77
- if (UNWRITABLE.has(code))
71
+ const refusal = refusalOf(text);
72
+ if (UNWRITABLE.has(refusal?.code ?? ''))
78
73
  return;
79
- if (code !== 'edit_conflict' || !documentPresent(text))
74
+ if (refusal?.code !== 'edit_conflict' || !refusal.conflicts.includes('path_exists'))
80
75
  throw new Error(text);
81
76
  }
82
77
  await writeFile(join(kitHome(), CREATED), '');
package/how-we-work.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A Room is where information people share is kept; it is not the place for
4
4
  private information. Work goes into the Room it belongs to, or the First Room
5
- when none is named. Read and write Rooms with the `house` CLI.
5
+ when none is named.
6
6
 
7
7
  The skills are already set up; never run `setup-matt-pocock-skills`.
8
8
 
@@ -20,6 +20,4 @@ The skills are already set up; never run `setup-matt-pocock-skills`.
20
20
  - Research: `library/research/<slug>.md`
21
21
  - Questionnaires: `library/questionnaires/<slug>.md`
22
22
  - Learning: `library/learning/<topic>/`
23
- - Files a person should open, such as HTML pages, are uploaded to the Room and
24
- linked from the document they belong to; a prototype is one HTML file with
25
- every variant on one page.
23
+ - Prototype: one HTML file with every variant on one page.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentshouse/kit",
3
- "version": "0.2.1-alpha.10",
3
+ "version": "0.2.1-alpha.12",
4
4
  "description": "House Kit connects an Environment to agents.house.",
5
5
  "license": "MIT",
6
6
  "repository": {