@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.
- package/README.md +32 -14
- package/dist/abort.js +9 -0
- package/dist/acp.js +27 -7
- package/dist/agents.js +204 -52
- package/dist/api.js +6 -2
- package/dist/bootstrap.json +10 -14
- package/dist/bridge.js +90 -54
- package/dist/clis-main.js +23 -0
- package/dist/clis.js +102 -32
- package/dist/conversations.js +104 -27
- package/dist/copies.js +49 -39
- package/dist/files.js +2 -1
- package/dist/git-main.js +2 -2
- package/dist/git.js +6 -2
- package/dist/home.js +32 -3
- package/dist/house-main.js +96 -41
- package/dist/instructions.js +13 -12
- package/dist/kit-main.js +4 -1
- package/dist/login.js +24 -4
- package/dist/logout.js +18 -0
- package/dist/mcp.js +39 -0
- package/dist/operation.js +7 -4
- package/dist/refusals.js +41 -0
- package/dist/relay.js +10 -0
- package/dist/resident.js +34 -9
- package/dist/secret-input.js +4 -2
- package/dist/sign-in.js +17 -5
- package/dist/skills.js +5 -10
- package/dist/stream.js +4 -4
- package/dist/transcription.js +122 -0
- package/dist/update.js +117 -0
- package/how-we-work.md +10 -12
- package/package.json +4 -7
package/dist/conversations.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 (
|
|
185
|
-
prompt.push({ type: 'text', text:
|
|
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
|
|
261
|
+
await unlessAborted(this.kit.agents.read(), signal);
|
|
236
262
|
if (this.kit.agents.route(agentId) === undefined)
|
|
237
|
-
await
|
|
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(
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 === '
|
|
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,
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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 {
|
|
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(), '
|
|
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 {
|
|
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
|
|
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
|
|
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(), '
|
|
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 =
|
|
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:
|
|
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(
|
|
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:
|
|
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 ?
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
612
|
-
return {
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
665
|
+
catch {
|
|
656
666
|
return {
|
|
657
667
|
refused: false,
|
|
658
|
-
text: `House accepted ${short(commit)};
|
|
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 {
|
|
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(`${
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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 };
|