@agentshouse/kit 0.2.1-alpha.9 → 0.2.1-c15804a0fa28d
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 +67 -63
- 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);
|
|
@@ -37,14 +41,11 @@ function sidecar(path) {
|
|
|
37
41
|
segments.splice(-1, 0, PROVENANCE);
|
|
38
42
|
return segments.join('/');
|
|
39
43
|
}
|
|
40
|
-
function rootOf(
|
|
41
|
-
return
|
|
44
|
+
function rootOf(handle) {
|
|
45
|
+
return `/rooms/${handle}`;
|
|
42
46
|
}
|
|
43
47
|
function relative(path) {
|
|
44
|
-
return path.replace(/^\/
|
|
45
|
-
}
|
|
46
|
-
function prefixesOf(selected) {
|
|
47
|
-
return selected.private ? ['/private'] : ROOM_HOMES.map((home) => `${rootOf(selected)}/${home}`);
|
|
48
|
+
return path.replace(/^\/rooms\/[^/]+\//, '');
|
|
48
49
|
}
|
|
49
50
|
function declaredWritable(path, copy) {
|
|
50
51
|
const deepest = (scopes) => Math.max(-1, ...scopes.filter((scope) => covers(path, scope)).map((scope) => scope.length));
|
|
@@ -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,15 +167,17 @@ 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
|
-
for (const path of
|
|
180
|
+
for (const path of names.map((name) => join(home, 'rooms', name))) {
|
|
178
181
|
const text = await readFile(path, 'utf8').catch(() => null);
|
|
179
182
|
if (text === null)
|
|
180
183
|
continue;
|
|
@@ -186,7 +189,7 @@ export class WorkingCopies {
|
|
|
186
189
|
await this.selecting;
|
|
187
190
|
return [...this.copies.values()]
|
|
188
191
|
.filter((copy) => copy.active)
|
|
189
|
-
.map((copy) => `${rootOf(copy)}: ${copy.repository}`)
|
|
192
|
+
.map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
|
|
190
193
|
.sort();
|
|
191
194
|
}
|
|
192
195
|
select() {
|
|
@@ -195,13 +198,8 @@ export class WorkingCopies {
|
|
|
195
198
|
return selected;
|
|
196
199
|
}
|
|
197
200
|
async reselect() {
|
|
198
|
-
const {
|
|
199
|
-
const selected = [
|
|
200
|
-
...(chosen?.rooms ?? [])
|
|
201
|
-
.filter((room) => room.room_handle !== null)
|
|
202
|
-
.map((room) => ({ key: `rooms/${room.room_ref}`, private: false, handle: room.room_handle })),
|
|
203
|
-
...(chosen?.private === true ? [{ key: 'private', private: true, handle: null }] : []),
|
|
204
|
-
];
|
|
201
|
+
const { local_copy: chosen } = await this.house.post('/kit/local-copy/selection', {});
|
|
202
|
+
const selected = (chosen?.rooms ?? []).flatMap((room) => room.room_handle === null ? [] : [{ key: `rooms/${room.room_ref}`, handle: room.room_handle }]);
|
|
205
203
|
const keys = new Set(selected.map((scope) => scope.key));
|
|
206
204
|
const work = [];
|
|
207
205
|
for (const key of this.copies.keys()) {
|
|
@@ -253,11 +251,11 @@ export class WorkingCopies {
|
|
|
253
251
|
async push(cwd, selector, caller) {
|
|
254
252
|
const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
|
|
255
253
|
if (located.status !== 0)
|
|
256
|
-
return { refused: true, text: `${cwd} is not
|
|
254
|
+
return { refused: true, text: `${cwd} is not a House Local copy; run house push inside one` };
|
|
257
255
|
const [common, worktree] = located.stdout.toString('utf8').split('\n');
|
|
258
256
|
const key = await this.keyAt(dirname(common));
|
|
259
257
|
if (key === undefined)
|
|
260
|
-
return { refused: true, text: `${worktree} is not a House
|
|
258
|
+
return { refused: true, text: `${worktree} is not a House Local copy; run house push inside one` };
|
|
261
259
|
return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
|
|
262
260
|
}
|
|
263
261
|
async keyAt(repository) {
|
|
@@ -285,7 +283,7 @@ export class WorkingCopies {
|
|
|
285
283
|
}).catch(logged);
|
|
286
284
|
}
|
|
287
285
|
async save(copy) {
|
|
288
|
-
const path = join(kitHome(), '
|
|
286
|
+
const path = join(kitHome(), 'local-copies', `${copy.key}.json`);
|
|
289
287
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
290
288
|
const temporary = `${path}.${process.pid}`;
|
|
291
289
|
await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
|
|
@@ -293,12 +291,12 @@ export class WorkingCopies {
|
|
|
293
291
|
this.copies.set(copy.key, copy);
|
|
294
292
|
return copy;
|
|
295
293
|
}
|
|
296
|
-
async read(
|
|
297
|
-
const prefixes =
|
|
294
|
+
async read(handle) {
|
|
295
|
+
const prefixes = [rootOf(handle)];
|
|
298
296
|
const discovery = await this.house.post('/kit/door/discover', { prefixes });
|
|
299
297
|
let bundle = await this.house.post('/kit/door/bootstrap', { prefixes });
|
|
300
298
|
if ('transfer' in bundle)
|
|
301
|
-
bundle = (await (await fetch(bundle.transfer.url)).json());
|
|
299
|
+
bundle = (await (await fetch(relayed(bundle.transfer.url))).json());
|
|
302
300
|
const [view] = bundle.scopes;
|
|
303
301
|
if (view === undefined)
|
|
304
302
|
return null;
|
|
@@ -341,13 +339,13 @@ export class WorkingCopies {
|
|
|
341
339
|
return;
|
|
342
340
|
}
|
|
343
341
|
const discovery = await this.house.post('/kit/door/discover', {
|
|
344
|
-
prefixes:
|
|
342
|
+
prefixes: [rootOf(copy.room_handle)],
|
|
345
343
|
});
|
|
346
344
|
const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
|
|
347
345
|
if (authority === undefined)
|
|
348
346
|
return;
|
|
349
347
|
if (authority.managed_replication !== 'admitted') {
|
|
350
|
-
logged(`operation_denied: ${rootOf(copy)} is not synchronized to this Environment`);
|
|
348
|
+
logged(`operation_denied: ${rootOf(copy.room_handle)} is not synchronized to this Environment`);
|
|
351
349
|
return;
|
|
352
350
|
}
|
|
353
351
|
if (JSON.stringify([authority.operations.write, authority.protected_paths]) !==
|
|
@@ -358,12 +356,12 @@ export class WorkingCopies {
|
|
|
358
356
|
await this.save(await this.caughtUp(copy));
|
|
359
357
|
}
|
|
360
358
|
async materialize(scope) {
|
|
361
|
-
const read = await this.read(
|
|
359
|
+
const read = await this.read(scope.handle);
|
|
362
360
|
if (read === null) {
|
|
363
|
-
logged(`House serves nothing of ${rootOf(
|
|
361
|
+
logged(`House serves nothing of ${rootOf(scope.handle)} to copy`);
|
|
364
362
|
return;
|
|
365
363
|
}
|
|
366
|
-
const repository =
|
|
364
|
+
const repository = join(localCopies(), 'rooms', read.anchor);
|
|
367
365
|
if (await exists(repository)) {
|
|
368
366
|
logged(`${repository} already exists and House Kit leaves it as it is`);
|
|
369
367
|
return;
|
|
@@ -375,7 +373,7 @@ export class WorkingCopies {
|
|
|
375
373
|
try {
|
|
376
374
|
await git(staging, ['init', '-q', '-b', BRANCH]);
|
|
377
375
|
house = await importCommits(staging, [
|
|
378
|
-
{ message: `House ${rootOf(
|
|
376
|
+
{ message: `House ${rootOf(scope.handle)}`, from: null, files: read.writable },
|
|
379
377
|
]);
|
|
380
378
|
await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
|
|
381
379
|
await git(staging, ['reset', '-q', '--hard']);
|
|
@@ -389,7 +387,6 @@ export class WorkingCopies {
|
|
|
389
387
|
}
|
|
390
388
|
await this.save({
|
|
391
389
|
key: scope.key,
|
|
392
|
-
private: scope.private,
|
|
393
390
|
room_ref: read.anchor,
|
|
394
391
|
room_handle: scope.handle,
|
|
395
392
|
repository,
|
|
@@ -405,11 +402,11 @@ export class WorkingCopies {
|
|
|
405
402
|
});
|
|
406
403
|
}
|
|
407
404
|
async received(copy) {
|
|
408
|
-
const read = await this.read(copy);
|
|
405
|
+
const read = await this.read(copy.room_handle);
|
|
409
406
|
if (read === null)
|
|
410
407
|
return copy;
|
|
411
408
|
const house = await importCommits(copy.repository, [
|
|
412
|
-
{ message: `House ${rootOf(copy)} at position ${read.position}`, from: copy.house, replaces: true, files: read.writable },
|
|
409
|
+
{ message: `House ${rootOf(copy.room_handle)} at position ${read.position}`, from: copy.house, replaces: true, files: read.writable },
|
|
413
410
|
]);
|
|
414
411
|
const removed = copy.read_only.filter((path) => !read.readOnly.has(path));
|
|
415
412
|
const readOnly = await readOnlyPlaced(copy.repository, copy.read_only, read.readOnly, removed);
|
|
@@ -432,12 +429,12 @@ export class WorkingCopies {
|
|
|
432
429
|
let answer;
|
|
433
430
|
try {
|
|
434
431
|
answer = await this.house.post('/kit/door/enumerate', {
|
|
435
|
-
prefixes:
|
|
432
|
+
prefixes: [rootOf(copy.room_handle)],
|
|
436
433
|
positions: [{ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch }],
|
|
437
434
|
});
|
|
438
435
|
}
|
|
439
436
|
catch (error) {
|
|
440
|
-
const code = error instanceof HouseRefusal ?
|
|
437
|
+
const code = error instanceof HouseRefusal ? (refusalOf(error.text)?.code ?? null) : null;
|
|
441
438
|
if (code === 'position_expired')
|
|
442
439
|
return this.received(copy);
|
|
443
440
|
if (code !== 'replica_behind')
|
|
@@ -482,7 +479,7 @@ export class WorkingCopies {
|
|
|
482
479
|
let { house, manifests } = copy;
|
|
483
480
|
if (files.size > 0) {
|
|
484
481
|
house = await importCommits(copy.repository, [
|
|
485
|
-
{ message: `House ${rootOf(copy)} at position ${position}`, from: copy.house, files },
|
|
482
|
+
{ message: `House ${rootOf(copy.room_handle)} at position ${position}`, from: copy.house, files },
|
|
486
483
|
]);
|
|
487
484
|
manifests = { ...manifests, [house]: { parent: copy.house, files: revisions } };
|
|
488
485
|
}
|
|
@@ -512,8 +509,9 @@ export class WorkingCopies {
|
|
|
512
509
|
}
|
|
513
510
|
copy = await this.save(without(copy));
|
|
514
511
|
}
|
|
515
|
-
if (!copy.active)
|
|
516
|
-
return { refused: true, text: `${rootOf(copy)} is no longer
|
|
512
|
+
if (!copy.active) {
|
|
513
|
+
return { refused: true, text: `${rootOf(copy.room_handle)} is no longer synced to this Environment, so House takes no push from it` };
|
|
514
|
+
}
|
|
517
515
|
const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
|
|
518
516
|
const base = listed.find((ancestor) => ancestor in copy.manifests);
|
|
519
517
|
if (base === undefined) {
|
|
@@ -546,7 +544,7 @@ export class WorkingCopies {
|
|
|
546
544
|
return this.settle(submitted, caller);
|
|
547
545
|
}
|
|
548
546
|
async derived(copy, base, commit) {
|
|
549
|
-
const root = rootOf(copy);
|
|
547
|
+
const root = rootOf(copy.room_handle);
|
|
550
548
|
const before = await tree(copy.repository, base);
|
|
551
549
|
const after = await tree(copy.repository, commit);
|
|
552
550
|
const moved = movedPaths(copy.moves, before, after);
|
|
@@ -582,9 +580,8 @@ export class WorkingCopies {
|
|
|
582
580
|
async settle(start, caller, restaged = false) {
|
|
583
581
|
let copy = start;
|
|
584
582
|
const push = copy.push;
|
|
585
|
-
if (push.caller !== caller.key)
|
|
586
|
-
return this.recovered(copy
|
|
587
|
-
}
|
|
583
|
+
if (push.caller !== caller.key)
|
|
584
|
+
return this.recovered(copy);
|
|
588
585
|
let outcome;
|
|
589
586
|
try {
|
|
590
587
|
outcome = await caller.submit(push.operation_id, push.changes, push.upload, async (upload) => {
|
|
@@ -592,9 +589,10 @@ export class WorkingCopies {
|
|
|
592
589
|
});
|
|
593
590
|
}
|
|
594
591
|
catch (error) {
|
|
592
|
+
const refusal = error instanceof HouseRefusal ? refusalOf(error.text) : null;
|
|
595
593
|
return {
|
|
596
594
|
refused: true,
|
|
597
|
-
text: `House's answer to ${short(push.commit)} did not arrive
|
|
595
|
+
text: refusal?.text ?? `House's answer to ${short(push.commit)} did not arrive; run house push again`,
|
|
598
596
|
};
|
|
599
597
|
}
|
|
600
598
|
if ('refused' in outcome) {
|
|
@@ -603,31 +601,37 @@ export class WorkingCopies {
|
|
|
603
601
|
return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
|
|
604
602
|
}
|
|
605
603
|
if (outcome.refused.code === 'operation_expired')
|
|
606
|
-
return this.recovered(copy
|
|
604
|
+
return this.recovered(copy);
|
|
605
|
+
const rebased = rebaseFixes(outcome.refused);
|
|
607
606
|
let settled = await this.save(without(copy));
|
|
608
607
|
try {
|
|
609
608
|
settled = await this.save(await this.caughtUp(settled));
|
|
610
609
|
}
|
|
611
|
-
catch
|
|
612
|
-
return {
|
|
610
|
+
catch {
|
|
611
|
+
return {
|
|
612
|
+
refused: true,
|
|
613
|
+
text: rebased ? `${outcome.refused.text}\nHouse's current state did not arrive; push again later` : outcome.refused.text,
|
|
614
|
+
};
|
|
613
615
|
}
|
|
614
616
|
return {
|
|
615
617
|
refused: true,
|
|
616
|
-
text:
|
|
618
|
+
text: rebased
|
|
619
|
+
? `${outcome.refused.text}\n${HOUSE_REF} holds House's current state; rebase onto it and push again`
|
|
620
|
+
: outcome.refused.text,
|
|
617
621
|
};
|
|
618
622
|
}
|
|
619
623
|
return this.finished(await this.save({ ...copy, push: { ...copy.push, state: 'accepted', written: outcome.accepted } }));
|
|
620
624
|
}
|
|
621
|
-
async recovered(copy
|
|
625
|
+
async recovered(copy) {
|
|
622
626
|
const push = copy.push;
|
|
623
627
|
const current = await this.save(await this.caughtUp(copy));
|
|
624
628
|
const house = await tree(copy.repository, current.house);
|
|
625
|
-
const root = rootOf(current);
|
|
629
|
+
const root = rootOf(current.room_handle);
|
|
626
630
|
if (!(await this.landed(copy.repository, push.changes, house, await tree(copy.repository, push.commit)))) {
|
|
627
631
|
await this.save(without(current));
|
|
628
632
|
return {
|
|
629
633
|
refused: true,
|
|
630
|
-
text:
|
|
634
|
+
text: `House does not hold ${short(push.commit)}; run house push again`,
|
|
631
635
|
};
|
|
632
636
|
}
|
|
633
637
|
const written = [];
|
|
@@ -652,10 +656,10 @@ export class WorkingCopies {
|
|
|
652
656
|
await this.save(without(copy));
|
|
653
657
|
return { refused: false, text: integrated.text };
|
|
654
658
|
}
|
|
655
|
-
catch
|
|
659
|
+
catch {
|
|
656
660
|
return {
|
|
657
661
|
refused: false,
|
|
658
|
-
text: `House accepted ${short(commit)};
|
|
662
|
+
text: `House accepted ${short(commit)}; run house push again to finish integrating it here`,
|
|
659
663
|
};
|
|
660
664
|
}
|
|
661
665
|
}
|
|
@@ -678,7 +682,7 @@ export class WorkingCopies {
|
|
|
678
682
|
written = [
|
|
679
683
|
...written,
|
|
680
684
|
{
|
|
681
|
-
path: `${rootOf(copy)}/${path}`,
|
|
685
|
+
path: `${rootOf(copy.room_handle)}/${path}`,
|
|
682
686
|
revision: entry === undefined ? null : (revisionAt(copy, copy.house, path) ?? null),
|
|
683
687
|
content: entry === undefined ? null : await blobText(copy.repository, entry, path),
|
|
684
688
|
},
|
|
@@ -698,7 +702,7 @@ export class WorkingCopies {
|
|
|
698
702
|
}
|
|
699
703
|
const edits = new Map([...files].filter(([path, content]) => (content === null ? selected.has(path) : selected.get(path) !== blobId(content))));
|
|
700
704
|
const received = [...files.keys()].filter((path) => revisionAt(copy, copy.house, path) === revisionAt(copy, push.house, path));
|
|
701
|
-
const accepted = `House ${rootOf(copy)} with ${push.commit} accepted`;
|
|
705
|
+
const accepted = `House ${rootOf(copy.room_handle)} with ${push.commit} accepted`;
|
|
702
706
|
const house = await importCommits(copy.repository, [
|
|
703
707
|
...(edits.size === 0 ? [] : [{ message: accepted, from: push.commit, files: edits }]),
|
|
704
708
|
{
|
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'];
|