@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.
- 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 +4 -1
- package/dist/bootstrap.json +10 -14
- package/dist/bridge.js +85 -51
- package/dist/clis-main.js +23 -0
- package/dist/clis.js +102 -32
- package/dist/conversations.js +104 -27
- package/dist/copies.js +47 -38
- 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 +38 -0
- package/dist/operation.js +7 -4
- package/dist/refusals.js +41 -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 +2 -3
- 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,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,
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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 {
|
|
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(), '
|
|
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 {
|
|
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
|
|
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
|
|
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(), '
|
|
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 =
|
|
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:
|
|
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(
|
|
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:
|
|
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 ?
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
612
|
-
return {
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
664
|
+
catch {
|
|
656
665
|
return {
|
|
657
666
|
refused: false,
|
|
658
|
-
text: `House accepted ${short(commit)};
|
|
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 {
|
|
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 };
|