@agentshouse/kit 0.2.1-alpha.8 → 0.2.1-c031c7457a661
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 +49 -24
- package/dist/abort.js +9 -0
- package/dist/acp.js +49 -86
- package/dist/agents.js +333 -52
- package/dist/api.js +40 -4
- package/dist/app-git.js +48 -0
- package/dist/bootstrap.json +10 -14
- package/dist/bridge.js +93 -78
- package/dist/calls.js +63 -0
- package/dist/chatgpt.js +32 -0
- package/dist/clis-main.js +23 -0
- package/dist/clis.js +217 -72
- package/dist/codex-main.js +58 -0
- package/dist/configure-main.js +6 -0
- package/dist/connection.js +126 -0
- package/dist/conversations.js +642 -261
- package/dist/copies.js +237 -173
- package/dist/end.js +27 -0
- package/dist/enrol-main.js +7 -1
- package/dist/events.js +525 -0
- package/dist/files.js +140 -17
- package/dist/git-main.js +2 -2
- package/dist/git.js +16 -10
- package/dist/home.js +141 -10
- package/dist/house-cli.js +300 -0
- package/dist/house-main.js +5 -102
- package/dist/instructions.js +16 -17
- package/dist/kit-cli.js +68 -0
- package/dist/kit-main.js +8 -16
- package/dist/login.js +128 -95
- package/dist/logout.js +19 -0
- package/dist/mcp.js +41 -0
- package/dist/operation.js +7 -4
- package/dist/parts.js +82 -0
- package/dist/placement.js +39 -0
- package/dist/refusals.js +53 -0
- package/dist/relay.js +20 -0
- package/dist/resident.js +140 -51
- package/dist/scope.js +206 -0
- package/dist/secret-input.js +7 -2
- package/dist/service-main.js +9 -0
- package/dist/service.js +55 -0
- package/dist/shell.js +3 -0
- package/dist/sign-in.js +50 -31
- package/dist/skills.js +66 -0
- package/dist/transcription.js +201 -0
- package/dist/translate.js +2 -8
- package/dist/update.js +138 -0
- package/dist/web-wizard.js +100 -0
- package/dist/wire.js +22 -0
- package/dist/work.js +346 -0
- package/package.json +8 -8
- package/dist/blocks.js +0 -51
- package/dist/stream.js +0 -47
package/dist/copies.js
CHANGED
|
@@ -2,32 +2,64 @@ import { parse } from '@agentshouse/mdmodel';
|
|
|
2
2
|
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
|
-
import { HouseRefusal } from './api.js';
|
|
6
|
-
import { blobId, blobText, git, HOUSE_REF, importCommits, revision, run, succeeded, tree,
|
|
5
|
+
import { finalRefusal, HouseRefusal, retrying } from './api.js';
|
|
6
|
+
import { blobId, blobText, git, isText, 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
|
-
const
|
|
14
|
-
const IDENTITY = { GIT_COMMITTER_NAME: 'House Kit', GIT_COMMITTER_EMAIL: 'house-kit@localhost' };
|
|
14
|
+
const IDENTITY = { GIT_COMMITTER_NAME: 'Kit', GIT_COMMITTER_EMAIL: 'house-kit@localhost' };
|
|
15
15
|
function causeOf(error) {
|
|
16
16
|
return error instanceof Error ? error.message : String(error);
|
|
17
17
|
}
|
|
18
|
+
function houseCause(error) {
|
|
19
|
+
return error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
|
|
20
|
+
}
|
|
18
21
|
function logged(error) {
|
|
19
22
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
20
23
|
}
|
|
21
|
-
|
|
24
|
+
const REBASED = new Set([
|
|
25
|
+
'field_base_stale',
|
|
26
|
+
'section_base_stale',
|
|
27
|
+
'preamble_base_stale',
|
|
28
|
+
'base_revision_stale',
|
|
29
|
+
'path_exists',
|
|
30
|
+
'path_absent',
|
|
31
|
+
]);
|
|
32
|
+
function rebaseFixes(refusal) {
|
|
33
|
+
return refusal.code === 'edit_conflict' && refusal.conflicts.some((code) => REBASED.has(code));
|
|
34
|
+
}
|
|
35
|
+
function textOnly(paths) {
|
|
36
|
+
const [named, them] = paths.length === 1 ? [`${paths[0]} is`, 'it'] : [`${paths.join(', ')} are`, 'them'];
|
|
37
|
+
return `${named} not text, and house push sends text only; remove ${them} from the commit and place ${them} with house upload`;
|
|
38
|
+
}
|
|
39
|
+
function outside(path) {
|
|
40
|
+
return { refused: true, text: `${path} is not a House Local copy; run house push inside one` };
|
|
41
|
+
}
|
|
42
|
+
function short(commit) {
|
|
43
|
+
return commit.slice(0, 12);
|
|
44
|
+
}
|
|
45
|
+
function moved(before, after) {
|
|
46
|
+
if (before.house !== after.house)
|
|
47
|
+
return [`${rootOf(after.room_handle)}: ${HOUSE_REF} is at ${short(after.house)}`];
|
|
48
|
+
return before.position === after.position ? [] : [`${rootOf(after.room_handle)}: read-only files refreshed`];
|
|
49
|
+
}
|
|
50
|
+
function expiredAuthority(error) {
|
|
51
|
+
if (!(error instanceof HouseRefusal))
|
|
52
|
+
return undefined;
|
|
22
53
|
try {
|
|
23
|
-
|
|
54
|
+
const { error: refused } = JSON.parse(error.text);
|
|
55
|
+
return refused?.code === 'expired' && typeof refused.authority === 'string' ? refused.authority : undefined;
|
|
24
56
|
}
|
|
25
57
|
catch {
|
|
26
|
-
return
|
|
58
|
+
return undefined;
|
|
27
59
|
}
|
|
28
60
|
}
|
|
29
|
-
function
|
|
30
|
-
return
|
|
61
|
+
function grouped(items, size) {
|
|
62
|
+
return items.flatMap((_, index) => (index % size === 0 ? [items.slice(index, index + size)] : []));
|
|
31
63
|
}
|
|
32
64
|
function covers(path, prefix) {
|
|
33
65
|
return prefix === '' || path === prefix || path.startsWith(`${prefix}/`);
|
|
@@ -37,14 +69,14 @@ function sidecar(path) {
|
|
|
37
69
|
segments.splice(-1, 0, PROVENANCE);
|
|
38
70
|
return segments.join('/');
|
|
39
71
|
}
|
|
40
|
-
function
|
|
41
|
-
return
|
|
72
|
+
function recordOf(key) {
|
|
73
|
+
return join(kitHome(), 'local-copies', `${key}.json`);
|
|
42
74
|
}
|
|
43
|
-
function
|
|
44
|
-
return
|
|
75
|
+
function rootOf(handle) {
|
|
76
|
+
return `/rooms/${handle}`;
|
|
45
77
|
}
|
|
46
|
-
function
|
|
47
|
-
return
|
|
78
|
+
function relative(path) {
|
|
79
|
+
return path.replace(/^\/rooms\/[^/]+\//, '');
|
|
48
80
|
}
|
|
49
81
|
function declaredWritable(path, copy) {
|
|
50
82
|
const deepest = (scopes) => Math.max(-1, ...scopes.filter((scope) => covers(path, scope)).map((scope) => scope.length));
|
|
@@ -101,7 +133,7 @@ async function ignoreRules(repository, writable, copy) {
|
|
|
101
133
|
].map(pattern);
|
|
102
134
|
const granted = readOnly.filter((path) => !protectedPath(path)).map(pattern);
|
|
103
135
|
await ruled(join(repository, '.gitignore'), [
|
|
104
|
-
'#
|
|
136
|
+
'# Kit writes this file and .ignore. The paths below are read-only House',
|
|
105
137
|
'# content: Git does not track them, and House refuses a change to them even',
|
|
106
138
|
'# when this file is edited or a path is force-added.',
|
|
107
139
|
'/.gitignore',
|
|
@@ -116,7 +148,7 @@ async function ignoreRules(repository, writable, copy) {
|
|
|
116
148
|
'',
|
|
117
149
|
].join('\n'));
|
|
118
150
|
await ruled(join(repository, '.ignore'), [
|
|
119
|
-
'#
|
|
151
|
+
'# Kit writes this file so native search still reads the read-only House',
|
|
120
152
|
'# content that .gitignore keeps out of Git.',
|
|
121
153
|
`!${PROVENANCE}`,
|
|
122
154
|
...[...house, ...granted].map((rule) => `!${rule}`),
|
|
@@ -147,34 +179,35 @@ function movedPaths(moves, before, after) {
|
|
|
147
179
|
.filter((pair) => pairs.filter((other) => other.from === pair.from || other.to === pair.to).length === 1)
|
|
148
180
|
.map(({ from, to }) => [from, to]));
|
|
149
181
|
}
|
|
150
|
-
export class
|
|
182
|
+
export class LocalCopies {
|
|
151
183
|
owner;
|
|
152
184
|
house;
|
|
153
185
|
copies = new Map();
|
|
154
186
|
queues = new Map();
|
|
155
|
-
|
|
156
|
-
selecting = Promise.resolve();
|
|
187
|
+
answering = Promise.resolve();
|
|
157
188
|
constructor(house) {
|
|
158
189
|
this.house = house;
|
|
159
190
|
this.owner = {
|
|
160
191
|
key: 'owner',
|
|
161
192
|
submit: async (operation, changes) => {
|
|
162
193
|
try {
|
|
163
|
-
const outcome = await house.post('/kit/door/batch', { operation_id: operation, changes });
|
|
194
|
+
const outcome = await retrying(() => house.post('/kit/door/batch', { operation_id: operation, changes }));
|
|
164
195
|
return { accepted: outcome.contents };
|
|
165
196
|
}
|
|
166
197
|
catch (error) {
|
|
167
|
-
if (!(error
|
|
198
|
+
if (!finalRefusal(error))
|
|
168
199
|
throw error;
|
|
169
|
-
return {
|
|
200
|
+
return {
|
|
201
|
+
refused: refusalOf(error.text) ?? { code: '', text: `House refused it with ${error.status}`, conflicts: [] },
|
|
202
|
+
};
|
|
170
203
|
}
|
|
171
204
|
},
|
|
172
205
|
};
|
|
173
206
|
}
|
|
174
207
|
async load() {
|
|
175
|
-
const home = join(kitHome(), '
|
|
208
|
+
const home = join(kitHome(), 'local-copies');
|
|
176
209
|
const names = (await readdir(join(home, 'rooms')).catch(() => [])).filter((name) => name.endsWith('.json'));
|
|
177
|
-
for (const path of
|
|
210
|
+
for (const path of names.map((name) => join(home, 'rooms', name))) {
|
|
178
211
|
const text = await readFile(path, 'utf8').catch(() => null);
|
|
179
212
|
if (text === null)
|
|
180
213
|
continue;
|
|
@@ -183,47 +216,75 @@ export class WorkingCopies {
|
|
|
183
216
|
}
|
|
184
217
|
}
|
|
185
218
|
async mapping() {
|
|
186
|
-
await this.
|
|
219
|
+
await this.answering;
|
|
187
220
|
return [...this.copies.values()]
|
|
188
|
-
.filter((copy) => copy.
|
|
189
|
-
.map((copy) => `${rootOf(copy)}: ${copy.repository}`)
|
|
221
|
+
.filter((copy) => copy.unfollowed === undefined)
|
|
222
|
+
.map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
|
|
190
223
|
.sort();
|
|
191
224
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
225
|
+
connected() {
|
|
226
|
+
return this.selected();
|
|
227
|
+
}
|
|
228
|
+
async changed(rooms) {
|
|
229
|
+
if (rooms.length > 0)
|
|
230
|
+
await this.selected(rooms);
|
|
231
|
+
}
|
|
232
|
+
fetch() {
|
|
233
|
+
return this.answered(() => this.fetched(true));
|
|
234
|
+
}
|
|
235
|
+
async selected(rooms) {
|
|
236
|
+
const copied = await this.answered(() => this.fetched(false, rooms));
|
|
237
|
+
if (copied.refused)
|
|
238
|
+
logged(copied.text);
|
|
239
|
+
}
|
|
240
|
+
answered(work) {
|
|
241
|
+
const answered = this.answering.then(work);
|
|
242
|
+
this.answering = answered.catch(() => undefined);
|
|
243
|
+
return answered;
|
|
244
|
+
}
|
|
245
|
+
async fetched(refreshing, rooms) {
|
|
246
|
+
const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', rooms === undefined ? {} : { rooms });
|
|
247
|
+
const synced = new Map(answer.rooms.map((room) => [`rooms/${room.room_ref}`, room.room_handle]));
|
|
248
|
+
const named = rooms === undefined ? null : new Set(rooms.map((room) => `rooms/${room}`));
|
|
249
|
+
const held = () => [...this.copies.values()].filter((copy) => named === null || named.has(copy.key));
|
|
250
|
+
return this.holding([...new Set([...held().map((copy) => copy.key), ...synced.keys()])].sort(), async () => {
|
|
251
|
+
const said = [];
|
|
252
|
+
let refused = false;
|
|
253
|
+
const mark = refreshing ? 'said' : 'unsaid';
|
|
254
|
+
for (const copy of held()) {
|
|
255
|
+
if (synced.has(copy.key) || copy.unfollowed === 'said' || copy.unfollowed === mark)
|
|
256
|
+
continue;
|
|
257
|
+
await this.save({ ...copy, unfollowed: mark });
|
|
258
|
+
if (refreshing)
|
|
259
|
+
said.push(`${rootOf(copy.room_handle)}: no longer followed; kept as it is`);
|
|
260
|
+
}
|
|
261
|
+
const current = [];
|
|
262
|
+
for (const [key, handle] of synced) {
|
|
263
|
+
try {
|
|
264
|
+
const held = this.copies.get(key);
|
|
265
|
+
if (held === undefined || !(await exists(held.repository))) {
|
|
266
|
+
said.push(await this.materialize(key, handle));
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const { unfollowed, ...followed } = held;
|
|
270
|
+
current.push(unfollowed === undefined && held.room_handle === handle ? held : await this.save({ ...followed, room_handle: handle }));
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
refused = true;
|
|
274
|
+
said.push(`${rootOf(handle)}: ${causeOf(error)}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (refreshing && current.length > 0) {
|
|
278
|
+
const failed = await this.refreshed(current).then(() => null, houseCause);
|
|
279
|
+
for (const copy of current)
|
|
280
|
+
said.push(...moved(copy, this.copies.get(copy.key)));
|
|
281
|
+
if (failed !== null) {
|
|
282
|
+
refused = true;
|
|
283
|
+
said.push(failed);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
|
|
287
|
+
});
|
|
227
288
|
}
|
|
228
289
|
async observed(report) {
|
|
229
290
|
const key = await this.keyAt(dirname(report.common));
|
|
@@ -253,11 +314,11 @@ export class WorkingCopies {
|
|
|
253
314
|
async push(cwd, selector, caller) {
|
|
254
315
|
const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
|
|
255
316
|
if (located.status !== 0)
|
|
256
|
-
return
|
|
317
|
+
return outside(cwd);
|
|
257
318
|
const [common, worktree] = located.stdout.toString('utf8').split('\n');
|
|
258
319
|
const key = await this.keyAt(dirname(common));
|
|
259
320
|
if (key === undefined)
|
|
260
|
-
return
|
|
321
|
+
return outside(worktree);
|
|
261
322
|
return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
|
|
262
323
|
}
|
|
263
324
|
async keyAt(repository) {
|
|
@@ -273,19 +334,12 @@ export class WorkingCopies {
|
|
|
273
334
|
this.queues.set(key, queued.catch(() => undefined));
|
|
274
335
|
return queued;
|
|
275
336
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
this.delivering.add(key);
|
|
280
|
-
void this.exclusive(key, async () => {
|
|
281
|
-
this.delivering.delete(key);
|
|
282
|
-
const copy = this.copies.get(key);
|
|
283
|
-
if (copy.active)
|
|
284
|
-
await this.save(await this.caughtUp(copy));
|
|
285
|
-
}).catch(logged);
|
|
337
|
+
holding(keys, work) {
|
|
338
|
+
const [key, ...rest] = keys;
|
|
339
|
+
return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
|
|
286
340
|
}
|
|
287
341
|
async save(copy) {
|
|
288
|
-
const path =
|
|
342
|
+
const path = recordOf(copy.key);
|
|
289
343
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
290
344
|
const temporary = `${path}.${process.pid}`;
|
|
291
345
|
await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
|
|
@@ -293,12 +347,12 @@ export class WorkingCopies {
|
|
|
293
347
|
this.copies.set(copy.key, copy);
|
|
294
348
|
return copy;
|
|
295
349
|
}
|
|
296
|
-
async read(
|
|
297
|
-
const prefixes =
|
|
350
|
+
async read(handle) {
|
|
351
|
+
const prefixes = [rootOf(handle)];
|
|
298
352
|
const discovery = await this.house.post('/kit/door/discover', { prefixes });
|
|
299
353
|
let bundle = await this.house.post('/kit/door/bootstrap', { prefixes });
|
|
300
354
|
if ('transfer' in bundle)
|
|
301
|
-
bundle = (await (await fetch(bundle.transfer.url)).json());
|
|
355
|
+
bundle = (await (await fetch(relayed(bundle.transfer.url))).json());
|
|
302
356
|
const [view] = bundle.scopes;
|
|
303
357
|
if (view === undefined)
|
|
304
358
|
return null;
|
|
@@ -328,46 +382,13 @@ export class WorkingCopies {
|
|
|
328
382
|
}
|
|
329
383
|
return read;
|
|
330
384
|
}
|
|
331
|
-
async
|
|
332
|
-
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
copy = await this.save({ ...copy, room_handle: scope.handle });
|
|
339
|
-
if (!copy.active) {
|
|
340
|
-
await this.save(await this.received({ ...copy, active: true }));
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
const discovery = await this.house.post('/kit/door/discover', {
|
|
344
|
-
prefixes: prefixesOf(copy),
|
|
345
|
-
});
|
|
346
|
-
const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
|
|
347
|
-
if (authority === undefined)
|
|
348
|
-
return;
|
|
349
|
-
if (authority.managed_replication !== 'admitted') {
|
|
350
|
-
logged(`operation_denied: ${rootOf(copy)} is not synchronized to this Environment`);
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
if (JSON.stringify([authority.operations.write, authority.protected_paths]) !==
|
|
354
|
-
JSON.stringify([copy.write, copy.protected])) {
|
|
355
|
-
await this.save(await this.received(copy));
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
await this.save(await this.caughtUp(copy));
|
|
359
|
-
}
|
|
360
|
-
async materialize(scope) {
|
|
361
|
-
const read = await this.read({ private: scope.private, room_handle: scope.handle });
|
|
362
|
-
if (read === null) {
|
|
363
|
-
logged(`House serves nothing of ${rootOf({ private: scope.private, room_handle: scope.handle })} to copy`);
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
const repository = scope.private ? join(WORKING_COPIES, 'private') : join(WORKING_COPIES, 'rooms', read.anchor);
|
|
367
|
-
if (await exists(repository)) {
|
|
368
|
-
logged(`${repository} already exists and House Kit leaves it as it is`);
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
385
|
+
async materialize(key, handle) {
|
|
386
|
+
const read = await this.read(handle);
|
|
387
|
+
if (read === null)
|
|
388
|
+
throw new Error('House serves nothing of it to copy');
|
|
389
|
+
const repository = join(localCopies(), 'rooms', read.anchor);
|
|
390
|
+
if (await exists(repository))
|
|
391
|
+
throw new Error(`${repository} already exists and Kit leaves it as it is`);
|
|
371
392
|
const staging = join(dirname(repository), `.${randomUUID()}`);
|
|
372
393
|
await mkdir(staging, { recursive: true });
|
|
373
394
|
let house;
|
|
@@ -375,7 +396,7 @@ export class WorkingCopies {
|
|
|
375
396
|
try {
|
|
376
397
|
await git(staging, ['init', '-q', '-b', BRANCH]);
|
|
377
398
|
house = await importCommits(staging, [
|
|
378
|
-
{ message: `House ${rootOf(
|
|
399
|
+
{ message: `House ${rootOf(handle)}`, from: null, files: read.writable },
|
|
379
400
|
]);
|
|
380
401
|
await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
|
|
381
402
|
await git(staging, ['reset', '-q', '--hard']);
|
|
@@ -388,12 +409,10 @@ export class WorkingCopies {
|
|
|
388
409
|
throw error;
|
|
389
410
|
}
|
|
390
411
|
await this.save({
|
|
391
|
-
key
|
|
392
|
-
private: scope.private,
|
|
412
|
+
key,
|
|
393
413
|
room_ref: read.anchor,
|
|
394
|
-
room_handle:
|
|
414
|
+
room_handle: handle,
|
|
395
415
|
repository,
|
|
396
|
-
active: true,
|
|
397
416
|
house,
|
|
398
417
|
position: read.position,
|
|
399
418
|
log_epoch: read.log_epoch,
|
|
@@ -403,13 +422,14 @@ export class WorkingCopies {
|
|
|
403
422
|
manifests: { [house]: { parent: null, files: read.revisions } },
|
|
404
423
|
moves: [],
|
|
405
424
|
});
|
|
425
|
+
return `${rootOf(handle)}: copied to ${repository}`;
|
|
406
426
|
}
|
|
407
427
|
async received(copy) {
|
|
408
|
-
const read = await this.read(copy);
|
|
428
|
+
const read = await this.read(copy.room_handle);
|
|
409
429
|
if (read === null)
|
|
410
430
|
return copy;
|
|
411
431
|
const house = await importCommits(copy.repository, [
|
|
412
|
-
{ message: `House ${rootOf(copy)} at position ${read.position}`, from: copy.house, replaces: true, files: read.writable },
|
|
432
|
+
{ message: `House ${rootOf(copy.room_handle)} at position ${read.position}`, from: copy.house, replaces: true, files: read.writable },
|
|
413
433
|
]);
|
|
414
434
|
const removed = copy.read_only.filter((path) => !read.readOnly.has(path));
|
|
415
435
|
const readOnly = await readOnlyPlaced(copy.repository, copy.read_only, read.readOnly, removed);
|
|
@@ -426,30 +446,57 @@ export class WorkingCopies {
|
|
|
426
446
|
await ignoreRules(copy.repository, [...read.writable.keys()], next);
|
|
427
447
|
return next;
|
|
428
448
|
}
|
|
429
|
-
async
|
|
430
|
-
|
|
431
|
-
|
|
449
|
+
async refreshed(copies) {
|
|
450
|
+
const discover = (group) => this.house.post('/kit/door/discover', { prefixes: group.map((copy) => rootOf(copy.room_handle)) });
|
|
451
|
+
const [first, ...rest] = copies;
|
|
452
|
+
const { bounds, authorities } = await discover([first]);
|
|
453
|
+
for (const group of grouped(rest, bounds.max_prefixes))
|
|
454
|
+
authorities.push(...(await discover(group)).authorities);
|
|
455
|
+
const unchanged = [];
|
|
456
|
+
for (const copy of copies) {
|
|
457
|
+
const authority = authorities.find((entry) => entry.room_ref === copy.room_ref);
|
|
458
|
+
if (authority !== undefined &&
|
|
459
|
+
JSON.stringify([authority.operations.write, authority.protected_paths]) !== JSON.stringify([copy.write, copy.protected])) {
|
|
460
|
+
await this.save(await this.received(copy));
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
unchanged.push(copy);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
for (const group of grouped(unchanged, bounds.max_prefixes))
|
|
467
|
+
await this.caughtUp(group, false);
|
|
468
|
+
}
|
|
469
|
+
async caughtUp(copies, whole) {
|
|
470
|
+
const caught = new Map(copies.map((copy) => [copy.key, copy]));
|
|
471
|
+
let asking = [...copies];
|
|
472
|
+
while (asking.length > 0) {
|
|
432
473
|
let answer;
|
|
433
474
|
try {
|
|
434
475
|
answer = await this.house.post('/kit/door/enumerate', {
|
|
435
|
-
prefixes:
|
|
436
|
-
positions:
|
|
476
|
+
prefixes: asking.map((copy) => rootOf(copy.room_handle)),
|
|
477
|
+
positions: asking.map((copy) => ({ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch })),
|
|
437
478
|
});
|
|
438
479
|
}
|
|
439
480
|
catch (error) {
|
|
440
|
-
const
|
|
441
|
-
if (
|
|
442
|
-
return this.received(copy);
|
|
443
|
-
if (code !== 'replica_behind')
|
|
481
|
+
const expired = asking.find((copy) => copy.room_ref === expiredAuthority(error));
|
|
482
|
+
if (expired === undefined)
|
|
444
483
|
throw error;
|
|
445
|
-
|
|
446
|
-
|
|
484
|
+
caught.set(expired.key, await this.save(await this.received(expired)));
|
|
485
|
+
asking = asking.filter((copy) => copy !== expired);
|
|
486
|
+
continue;
|
|
447
487
|
}
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
488
|
+
const advanced = [];
|
|
489
|
+
for (const copy of asking) {
|
|
490
|
+
const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
|
|
491
|
+
if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
|
|
492
|
+
continue;
|
|
493
|
+
const next = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
|
|
494
|
+
caught.set(copy.key, next);
|
|
495
|
+
advanced.push(next);
|
|
496
|
+
}
|
|
497
|
+
asking = whole ? advanced : [];
|
|
452
498
|
}
|
|
499
|
+
return copies.map((copy) => caught.get(copy.key));
|
|
453
500
|
}
|
|
454
501
|
async incorporated(copy, changes, position, logEpoch) {
|
|
455
502
|
const houseTree = await tree(copy.repository, copy.house);
|
|
@@ -482,7 +529,7 @@ export class WorkingCopies {
|
|
|
482
529
|
let { house, manifests } = copy;
|
|
483
530
|
if (files.size > 0) {
|
|
484
531
|
house = await importCommits(copy.repository, [
|
|
485
|
-
{ message: `House ${rootOf(copy)} at position ${position}`, from: copy.house, files },
|
|
532
|
+
{ message: `House ${rootOf(copy.room_handle)} at position ${position}`, from: copy.house, files },
|
|
486
533
|
]);
|
|
487
534
|
manifests = { ...manifests, [house]: { parent: copy.house, files: revisions } };
|
|
488
535
|
}
|
|
@@ -512,8 +559,6 @@ export class WorkingCopies {
|
|
|
512
559
|
}
|
|
513
560
|
copy = await this.save(without(copy));
|
|
514
561
|
}
|
|
515
|
-
if (!copy.active)
|
|
516
|
-
return { refused: true, text: `${rootOf(copy)} is no longer kept in this Environment` };
|
|
517
562
|
const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
|
|
518
563
|
const base = listed.find((ancestor) => ancestor in copy.manifests);
|
|
519
564
|
if (base === undefined) {
|
|
@@ -524,7 +569,7 @@ export class WorkingCopies {
|
|
|
524
569
|
changes = await this.derived(copy, base, commit);
|
|
525
570
|
}
|
|
526
571
|
catch (error) {
|
|
527
|
-
return { refused: true, text:
|
|
572
|
+
return { refused: true, text: houseCause(error) };
|
|
528
573
|
}
|
|
529
574
|
if (changes.length === 0)
|
|
530
575
|
return { refused: false, text: `Nothing to submit: ${short(commit)} matches House.` };
|
|
@@ -546,12 +591,23 @@ export class WorkingCopies {
|
|
|
546
591
|
return this.settle(submitted, caller);
|
|
547
592
|
}
|
|
548
593
|
async derived(copy, base, commit) {
|
|
549
|
-
const root = rootOf(copy);
|
|
594
|
+
const root = rootOf(copy.room_handle);
|
|
550
595
|
const before = await tree(copy.repository, base);
|
|
551
596
|
const after = await tree(copy.repository, commit);
|
|
552
597
|
const moved = movedPaths(copy.moves, before, after);
|
|
553
598
|
const arrived = new Set(moved.values());
|
|
554
|
-
const
|
|
599
|
+
const arriving = [...after].filter(([path, entry]) => before.get(path) !== entry);
|
|
600
|
+
if (arriving.length > 0) {
|
|
601
|
+
const { bounds } = await this.house.post('/kit/door/discover', { prefixes: [root] });
|
|
602
|
+
const binary = [];
|
|
603
|
+
for (const [path, entry] of arriving) {
|
|
604
|
+
if (!(await isText(copy.repository, entry, bounds.max_authored_file_bytes)))
|
|
605
|
+
binary.push(path);
|
|
606
|
+
}
|
|
607
|
+
if (binary.length > 0)
|
|
608
|
+
throw new Error(textOnly(binary));
|
|
609
|
+
}
|
|
610
|
+
const text = (entry) => blobText(copy.repository, entry);
|
|
555
611
|
const changes = [];
|
|
556
612
|
for (const path of [...new Set([...before.keys(), ...after.keys()])].sort()) {
|
|
557
613
|
if (arrived.has(path))
|
|
@@ -564,7 +620,7 @@ export class WorkingCopies {
|
|
|
564
620
|
changes.push({ op: 'rename', path: `${root}/${path}`, to: `${root}/${to}`, base: known });
|
|
565
621
|
const moving = after.get(to);
|
|
566
622
|
if (moving !== prior) {
|
|
567
|
-
changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving
|
|
623
|
+
changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving) });
|
|
568
624
|
}
|
|
569
625
|
continue;
|
|
570
626
|
}
|
|
@@ -573,18 +629,17 @@ export class WorkingCopies {
|
|
|
573
629
|
if (next === undefined)
|
|
574
630
|
changes.push({ op: 'remove', path: `${root}/${path}`, base: known });
|
|
575
631
|
else if (prior === undefined)
|
|
576
|
-
changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next
|
|
632
|
+
changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next) });
|
|
577
633
|
else
|
|
578
|
-
changes.push(...changed(`${root}/${path}`, path, known, await text(prior
|
|
634
|
+
changes.push(...changed(`${root}/${path}`, path, known, await text(prior), await text(next)));
|
|
579
635
|
}
|
|
580
636
|
return changes;
|
|
581
637
|
}
|
|
582
638
|
async settle(start, caller, restaged = false) {
|
|
583
639
|
let copy = start;
|
|
584
640
|
const push = copy.push;
|
|
585
|
-
if (push.caller !== caller.key)
|
|
586
|
-
return this.recovered(copy
|
|
587
|
-
}
|
|
641
|
+
if (push.caller !== caller.key)
|
|
642
|
+
return this.recovered(copy);
|
|
588
643
|
let outcome;
|
|
589
644
|
try {
|
|
590
645
|
outcome = await caller.submit(push.operation_id, push.changes, push.upload, async (upload) => {
|
|
@@ -592,9 +647,10 @@ export class WorkingCopies {
|
|
|
592
647
|
});
|
|
593
648
|
}
|
|
594
649
|
catch (error) {
|
|
650
|
+
const refusal = error instanceof HouseRefusal ? refusalOf(error.text) : null;
|
|
595
651
|
return {
|
|
596
652
|
refused: true,
|
|
597
|
-
text: `House's answer to ${short(push.commit)} did not arrive
|
|
653
|
+
text: refusal?.text ?? `House's answer to ${short(push.commit)} did not arrive; run house push again`,
|
|
598
654
|
};
|
|
599
655
|
}
|
|
600
656
|
if ('refused' in outcome) {
|
|
@@ -602,32 +658,40 @@ export class WorkingCopies {
|
|
|
602
658
|
const { upload, ...unstaged } = copy.push;
|
|
603
659
|
return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
|
|
604
660
|
}
|
|
605
|
-
if (outcome.refused.code === '
|
|
606
|
-
return this.recovered(copy
|
|
661
|
+
if (outcome.refused.code === 'expired')
|
|
662
|
+
return this.recovered(copy);
|
|
607
663
|
let settled = await this.save(without(copy));
|
|
664
|
+
if (settled.unfollowed !== undefined)
|
|
665
|
+
return { refused: true, text: outcome.refused.text };
|
|
666
|
+
const rebased = rebaseFixes(outcome.refused);
|
|
608
667
|
try {
|
|
609
|
-
settled =
|
|
668
|
+
settled = (await this.caughtUp([settled], true))[0];
|
|
610
669
|
}
|
|
611
|
-
catch
|
|
612
|
-
return {
|
|
670
|
+
catch {
|
|
671
|
+
return {
|
|
672
|
+
refused: true,
|
|
673
|
+
text: rebased ? `${outcome.refused.text}\nHouse's current state did not arrive; push again later` : outcome.refused.text,
|
|
674
|
+
};
|
|
613
675
|
}
|
|
614
676
|
return {
|
|
615
677
|
refused: true,
|
|
616
|
-
text:
|
|
678
|
+
text: rebased
|
|
679
|
+
? `${outcome.refused.text}\n${HOUSE_REF} holds House's current state; rebase onto it and push again`
|
|
680
|
+
: outcome.refused.text,
|
|
617
681
|
};
|
|
618
682
|
}
|
|
619
683
|
return this.finished(await this.save({ ...copy, push: { ...copy.push, state: 'accepted', written: outcome.accepted } }));
|
|
620
684
|
}
|
|
621
|
-
async recovered(copy
|
|
685
|
+
async recovered(copy) {
|
|
622
686
|
const push = copy.push;
|
|
623
|
-
const current =
|
|
687
|
+
const current = (await this.caughtUp([copy], true))[0];
|
|
624
688
|
const house = await tree(copy.repository, current.house);
|
|
625
|
-
const root = rootOf(current);
|
|
689
|
+
const root = rootOf(current.room_handle);
|
|
626
690
|
if (!(await this.landed(copy.repository, push.changes, house, await tree(copy.repository, push.commit)))) {
|
|
627
691
|
await this.save(without(current));
|
|
628
692
|
return {
|
|
629
693
|
refused: true,
|
|
630
|
-
text:
|
|
694
|
+
text: `House does not hold ${short(push.commit)}; run house push again`,
|
|
631
695
|
};
|
|
632
696
|
}
|
|
633
697
|
const written = [];
|
|
@@ -636,7 +700,7 @@ export class WorkingCopies {
|
|
|
636
700
|
written.push({
|
|
637
701
|
path: `${root}/${path}`,
|
|
638
702
|
revision: entry === undefined ? null : (revisionAt(current, current.house, path) ?? null),
|
|
639
|
-
content: entry === undefined ? null : await blobText(copy.repository, entry
|
|
703
|
+
content: entry === undefined ? null : await blobText(copy.repository, entry),
|
|
640
704
|
});
|
|
641
705
|
}
|
|
642
706
|
return this.finished(await this.save({ ...current, push: { ...push, state: 'accepted', written } }));
|
|
@@ -652,10 +716,10 @@ export class WorkingCopies {
|
|
|
652
716
|
await this.save(without(copy));
|
|
653
717
|
return { refused: false, text: integrated.text };
|
|
654
718
|
}
|
|
655
|
-
catch
|
|
719
|
+
catch {
|
|
656
720
|
return {
|
|
657
721
|
refused: false,
|
|
658
|
-
text: `House accepted ${short(commit)};
|
|
722
|
+
text: `House accepted ${short(commit)}; run house push again to finish integrating it here`,
|
|
659
723
|
};
|
|
660
724
|
}
|
|
661
725
|
}
|
|
@@ -664,7 +728,7 @@ export class WorkingCopies {
|
|
|
664
728
|
const push = copy.push;
|
|
665
729
|
let written = push.written;
|
|
666
730
|
if (push.changes.some((change) => change.op === 'rename')) {
|
|
667
|
-
copy =
|
|
731
|
+
copy = (await this.caughtUp([copy], true))[0];
|
|
668
732
|
const house = await tree(copy.repository, copy.house);
|
|
669
733
|
const sent = await tree(copy.repository, push.house);
|
|
670
734
|
if (push.changes.some((change) => change.op === 'rename' && !house.has(relative(change.to)))) {
|
|
@@ -678,9 +742,9 @@ export class WorkingCopies {
|
|
|
678
742
|
written = [
|
|
679
743
|
...written,
|
|
680
744
|
{
|
|
681
|
-
path: `${rootOf(copy)}/${path}`,
|
|
745
|
+
path: `${rootOf(copy.room_handle)}/${path}`,
|
|
682
746
|
revision: entry === undefined ? null : (revisionAt(copy, copy.house, path) ?? null),
|
|
683
|
-
content: entry === undefined ? null : await blobText(copy.repository, entry
|
|
747
|
+
content: entry === undefined ? null : await blobText(copy.repository, entry),
|
|
684
748
|
},
|
|
685
749
|
];
|
|
686
750
|
}
|
|
@@ -698,7 +762,7 @@ export class WorkingCopies {
|
|
|
698
762
|
}
|
|
699
763
|
const edits = new Map([...files].filter(([path, content]) => (content === null ? selected.has(path) : selected.get(path) !== blobId(content))));
|
|
700
764
|
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`;
|
|
765
|
+
const accepted = `House ${rootOf(copy.room_handle)} with ${push.commit} accepted`;
|
|
702
766
|
const house = await importCommits(copy.repository, [
|
|
703
767
|
...(edits.size === 0 ? [] : [{ message: accepted, from: push.commit, files: edits }]),
|
|
704
768
|
{
|
|
@@ -725,7 +789,7 @@ export class WorkingCopies {
|
|
|
725
789
|
async landed(repository, changes, house, selected) {
|
|
726
790
|
const text = async (path) => {
|
|
727
791
|
const entry = house.get(relative(path));
|
|
728
|
-
return entry === undefined ? undefined : blobText(repository, entry
|
|
792
|
+
return entry === undefined ? undefined : blobText(repository, entry);
|
|
729
793
|
};
|
|
730
794
|
for (const change of changes) {
|
|
731
795
|
const current = await text(change.path);
|