@agentshouse/kit 0.2.1-alpha.9 → 0.2.1-c05cd91c26b1d
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 +45 -26
- package/dist/abort.js +9 -0
- package/dist/acp.js +48 -85
- package/dist/agents.js +312 -53
- package/dist/api.js +40 -4
- package/dist/app-git.js +48 -0
- package/dist/bootstrap.json +10 -14
- package/dist/bridge.js +92 -77
- package/dist/chatgpt.js +32 -0
- package/dist/clis-main.js +23 -0
- package/dist/clis.js +213 -71
- package/dist/codex-main.js +58 -0
- package/dist/conversations.js +428 -233
- package/dist/copies.js +233 -171
- package/dist/end.js +23 -0
- package/dist/enrol-main.js +7 -1
- package/dist/events.js +525 -0
- package/dist/files.js +24 -3
- package/dist/git-main.js +2 -2
- package/dist/git.js +15 -9
- package/dist/home.js +127 -9
- package/dist/house-cli.js +291 -0
- package/dist/house-main.js +5 -102
- package/dist/inputs.js +37 -0
- package/dist/instructions.js +10 -31
- package/dist/kit-cli.js +68 -0
- package/dist/kit-main.js +8 -16
- package/dist/login.js +129 -94
- package/dist/logout.js +19 -0
- package/dist/mcp.js +39 -0
- package/dist/operation.js +7 -4
- package/dist/parts.js +82 -0
- package/dist/placement.js +30 -0
- package/dist/refusals.js +53 -0
- package/dist/relay.js +20 -0
- package/dist/resident.js +122 -41
- package/dist/scope.js +203 -0
- package/dist/secret-input.js +7 -2
- package/dist/service-main.js +9 -0
- package/dist/service.js +33 -0
- package/dist/shell.js +3 -0
- package/dist/sign-in.js +47 -28
- package/dist/skills.js +80 -16
- package/dist/stream.js +89 -13
- package/dist/transcription.js +170 -0
- package/dist/update.js +138 -0
- package/dist/wire.js +6 -0
- package/dist/work.js +346 -0
- package/how-we-work.md +22 -14
- package/package.json +5 -7
- package/skills/ask-matt/SKILL.md +17 -11
- package/skills/ask-matt/agents/openai.yaml +1 -0
- package/skills/code-review/SKILL.md +5 -5
- package/skills/codebase-design/DESIGN-IT-TWICE.md +1 -1
- package/skills/codebase-design/SKILL.md +0 -2
- package/skills/diagnosing-bugs/SKILL.md +2 -4
- package/skills/domain-modeling/{CONTEXT-FORMAT.md → GLOSSARY-FORMAT.md} +10 -10
- package/skills/domain-modeling/SKILL.md +12 -14
- package/skills/grill-me/SKILL.md +1 -2
- package/skills/grill-me/agents/openai.yaml +1 -0
- package/skills/grill-with-docs/SKILL.md +1 -2
- package/skills/grill-with-docs/agents/openai.yaml +1 -0
- package/skills/grilling/SKILL.md +2 -2
- package/skills/handoff/SKILL.md +2 -3
- package/skills/handoff/agents/openai.yaml +1 -0
- package/skills/implement/SKILL.md +5 -4
- package/skills/implement/agents/openai.yaml +1 -0
- package/skills/implement-spec/SKILL.md +40 -0
- package/skills/implement-spec/agents/openai.yaml +5 -0
- package/skills/improve-codebase-architecture/SKILL.md +6 -7
- package/skills/improve-codebase-architecture/agents/openai.yaml +1 -0
- package/skills/pr/CREDITS.md +3 -0
- package/skills/pr/LICENSE +21 -0
- package/skills/pr/SKILL.md +170 -0
- package/skills/pr/agents/openai.yaml +3 -0
- package/skills/prototype/SKILL.md +0 -2
- package/skills/research/SKILL.md +0 -2
- package/skills/retro/LICENSE +21 -0
- package/skills/retro/SKILL.md +44 -0
- package/skills/retro/agents/openai.yaml +5 -0
- package/skills/tdd/SKILL.md +2 -4
- package/skills/teach/SKILL.md +3 -4
- package/skills/teach/agents/openai.yaml +1 -0
- package/skills/to-questionnaire/SKILL.md +1 -2
- package/skills/to-questionnaire/agents/openai.yaml +1 -0
- package/skills/to-spec/SKILL.md +1 -2
- package/skills/to-spec/agents/openai.yaml +1 -0
- package/skills/to-tickets/SKILL.md +3 -4
- package/skills/to-tickets/agents/openai.yaml +1 -0
- package/skills/triage/AGENT-BRIEF.md +207 -0
- package/skills/triage/LICENSE +21 -0
- package/skills/triage/OUT-OF-SCOPE.md +105 -0
- package/skills/triage/SKILL.md +112 -0
- package/skills/triage/agents/openai.yaml +5 -0
- package/skills/wait-what/SKILL.md +2 -3
- package/skills/wait-what/agents/openai.yaml +1 -0
- package/skills/wayfinder/SKILL.md +5 -6
- package/skills/wayfinder/agents/openai.yaml +1 -0
- package/skills/wizard/LICENSE +21 -0
- package/skills/wizard/SKILL.md +44 -0
- package/skills/wizard/agents/openai.yaml +3 -0
- package/skills/wizard/template.sh +223 -0
- package/skills/writing-for-agents/SKILL.md +0 -2
- package/dist/blocks.js +0 -51
- package/skills/resolving-merge-conflicts/SKILL.md +0 -16
- package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
- /package/skills/{resolving-merge-conflicts → implement-spec}/LICENSE +0 -0
package/dist/copies.js
CHANGED
|
@@ -2,33 +2,62 @@ 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 REPLICA_RETRY_MS = 30_000;
|
|
14
14
|
const IDENTITY = { GIT_COMMITTER_NAME: 'House 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 short(commit) {
|
|
30
|
-
return commit.slice(0, 12);
|
|
31
|
-
}
|
|
32
61
|
function covers(path, prefix) {
|
|
33
62
|
return prefix === '' || path === prefix || path.startsWith(`${prefix}/`);
|
|
34
63
|
}
|
|
@@ -37,14 +66,14 @@ function sidecar(path) {
|
|
|
37
66
|
segments.splice(-1, 0, PROVENANCE);
|
|
38
67
|
return segments.join('/');
|
|
39
68
|
}
|
|
40
|
-
function
|
|
41
|
-
return
|
|
69
|
+
function recordOf(key) {
|
|
70
|
+
return join(kitHome(), 'local-copies', `${key}.json`);
|
|
42
71
|
}
|
|
43
|
-
function
|
|
44
|
-
return
|
|
72
|
+
function rootOf(handle) {
|
|
73
|
+
return `/rooms/${handle}`;
|
|
45
74
|
}
|
|
46
|
-
function
|
|
47
|
-
return
|
|
75
|
+
function relative(path) {
|
|
76
|
+
return path.replace(/^\/rooms\/[^/]+\//, '');
|
|
48
77
|
}
|
|
49
78
|
function declaredWritable(path, copy) {
|
|
50
79
|
const deepest = (scopes) => Math.max(-1, ...scopes.filter((scope) => covers(path, scope)).map((scope) => scope.length));
|
|
@@ -147,34 +176,35 @@ function movedPaths(moves, before, after) {
|
|
|
147
176
|
.filter((pair) => pairs.filter((other) => other.from === pair.from || other.to === pair.to).length === 1)
|
|
148
177
|
.map(({ from, to }) => [from, to]));
|
|
149
178
|
}
|
|
150
|
-
export class
|
|
179
|
+
export class LocalCopies {
|
|
151
180
|
owner;
|
|
152
181
|
house;
|
|
153
182
|
copies = new Map();
|
|
154
183
|
queues = new Map();
|
|
155
|
-
|
|
156
|
-
selecting = Promise.resolve();
|
|
184
|
+
answering = Promise.resolve();
|
|
157
185
|
constructor(house) {
|
|
158
186
|
this.house = house;
|
|
159
187
|
this.owner = {
|
|
160
188
|
key: 'owner',
|
|
161
189
|
submit: async (operation, changes) => {
|
|
162
190
|
try {
|
|
163
|
-
const outcome = await house.post('/kit/door/batch', { operation_id: operation, changes });
|
|
191
|
+
const outcome = await retrying(() => house.post('/kit/door/batch', { operation_id: operation, changes }));
|
|
164
192
|
return { accepted: outcome.contents };
|
|
165
193
|
}
|
|
166
194
|
catch (error) {
|
|
167
|
-
if (!(error
|
|
195
|
+
if (!finalRefusal(error))
|
|
168
196
|
throw error;
|
|
169
|
-
return {
|
|
197
|
+
return {
|
|
198
|
+
refused: refusalOf(error.text) ?? { code: '', text: `House refused it with ${error.status}`, conflicts: [] },
|
|
199
|
+
};
|
|
170
200
|
}
|
|
171
201
|
},
|
|
172
202
|
};
|
|
173
203
|
}
|
|
174
204
|
async load() {
|
|
175
|
-
const home = join(kitHome(), '
|
|
205
|
+
const home = join(kitHome(), 'local-copies');
|
|
176
206
|
const names = (await readdir(join(home, 'rooms')).catch(() => [])).filter((name) => name.endsWith('.json'));
|
|
177
|
-
for (const path of
|
|
207
|
+
for (const path of names.map((name) => join(home, 'rooms', name))) {
|
|
178
208
|
const text = await readFile(path, 'utf8').catch(() => null);
|
|
179
209
|
if (text === null)
|
|
180
210
|
continue;
|
|
@@ -183,47 +213,73 @@ export class WorkingCopies {
|
|
|
183
213
|
}
|
|
184
214
|
}
|
|
185
215
|
async mapping() {
|
|
186
|
-
await this.
|
|
216
|
+
await this.answering;
|
|
187
217
|
return [...this.copies.values()]
|
|
188
|
-
.filter((copy) => copy.
|
|
189
|
-
.map((copy) => `${rootOf(copy)}: ${copy.repository}`)
|
|
218
|
+
.filter((copy) => copy.erasing === undefined)
|
|
219
|
+
.map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
|
|
190
220
|
.sort();
|
|
191
221
|
}
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
222
|
+
async connected() {
|
|
223
|
+
const copied = await this.answered(() => this.fetched(false));
|
|
224
|
+
if (copied.refused)
|
|
225
|
+
logged(copied.text);
|
|
226
|
+
}
|
|
227
|
+
fetch() {
|
|
228
|
+
return this.answered(() => this.fetched(true));
|
|
229
|
+
}
|
|
230
|
+
answered(work) {
|
|
231
|
+
const answered = this.answering.then(work);
|
|
232
|
+
this.answering = answered.catch(() => undefined);
|
|
233
|
+
return answered;
|
|
234
|
+
}
|
|
235
|
+
async fetched(refreshing) {
|
|
236
|
+
const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', {});
|
|
237
|
+
const synced = new Map(answer.rooms.map((room) => [`rooms/${room.room_ref}`, room.room_handle]));
|
|
238
|
+
return this.holding([...new Set([...this.copies.keys(), ...synced.keys()])].sort(), async () => {
|
|
239
|
+
const said = [];
|
|
240
|
+
let refused = false;
|
|
241
|
+
for (const copy of [...this.copies.values()]) {
|
|
242
|
+
const returned = synced.has(copy.key);
|
|
243
|
+
if (returned && copy.erasing === undefined)
|
|
244
|
+
continue;
|
|
245
|
+
try {
|
|
246
|
+
await this.erase(copy);
|
|
247
|
+
said.push(`${rootOf(copy.room_handle)}: ${returned ? '' : 'no longer synced to this Environment; '}erased from the Local copy`);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
refused = true;
|
|
251
|
+
said.push(`${rootOf(copy.room_handle)}: ${causeOf(error)}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const current = [];
|
|
255
|
+
for (const [key, handle] of synced) {
|
|
256
|
+
try {
|
|
257
|
+
const held = this.copies.get(key);
|
|
258
|
+
if (held?.erasing)
|
|
259
|
+
continue;
|
|
260
|
+
if (held === undefined || !(await exists(held.repository))) {
|
|
261
|
+
said.push(await this.materialize(key, handle));
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
current.push(held.room_handle === handle ? held : await this.save({ ...held, room_handle: handle }));
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
refused = true;
|
|
268
|
+
said.push(`${rootOf(handle)}: ${causeOf(error)}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (refreshing && current.length > 0) {
|
|
272
|
+
try {
|
|
273
|
+
const refreshed = await this.refreshed(current);
|
|
274
|
+
current.forEach((copy, index) => said.push(...moved(copy, refreshed[index])));
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
refused = true;
|
|
278
|
+
said.push(houseCause(error));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
|
|
282
|
+
});
|
|
227
283
|
}
|
|
228
284
|
async observed(report) {
|
|
229
285
|
const key = await this.keyAt(dirname(report.common));
|
|
@@ -231,6 +287,8 @@ export class WorkingCopies {
|
|
|
231
287
|
return;
|
|
232
288
|
await this.exclusive(key, async () => {
|
|
233
289
|
const copy = this.copies.get(key);
|
|
290
|
+
if (copy === undefined || copy.erasing)
|
|
291
|
+
return;
|
|
234
292
|
let moves = copy.moves;
|
|
235
293
|
for (const { from, to } of report.moves) {
|
|
236
294
|
const held = moves.find((move) => move.worktree === report.worktree && move.to === from);
|
|
@@ -253,11 +311,11 @@ export class WorkingCopies {
|
|
|
253
311
|
async push(cwd, selector, caller) {
|
|
254
312
|
const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
|
|
255
313
|
if (located.status !== 0)
|
|
256
|
-
return
|
|
314
|
+
return outside(cwd);
|
|
257
315
|
const [common, worktree] = located.stdout.toString('utf8').split('\n');
|
|
258
316
|
const key = await this.keyAt(dirname(common));
|
|
259
317
|
if (key === undefined)
|
|
260
|
-
return
|
|
318
|
+
return outside(worktree);
|
|
261
319
|
return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
|
|
262
320
|
}
|
|
263
321
|
async keyAt(repository) {
|
|
@@ -273,19 +331,12 @@ export class WorkingCopies {
|
|
|
273
331
|
this.queues.set(key, queued.catch(() => undefined));
|
|
274
332
|
return queued;
|
|
275
333
|
}
|
|
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);
|
|
334
|
+
holding(keys, work) {
|
|
335
|
+
const [key, ...rest] = keys;
|
|
336
|
+
return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
|
|
286
337
|
}
|
|
287
338
|
async save(copy) {
|
|
288
|
-
const path =
|
|
339
|
+
const path = recordOf(copy.key);
|
|
289
340
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
290
341
|
const temporary = `${path}.${process.pid}`;
|
|
291
342
|
await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
|
|
@@ -293,12 +344,18 @@ export class WorkingCopies {
|
|
|
293
344
|
this.copies.set(copy.key, copy);
|
|
294
345
|
return copy;
|
|
295
346
|
}
|
|
296
|
-
async
|
|
297
|
-
|
|
347
|
+
async erase(copy) {
|
|
348
|
+
await this.save({ ...copy, erasing: true });
|
|
349
|
+
await rm(copy.repository, { recursive: true, force: true });
|
|
350
|
+
await rm(recordOf(copy.key), { force: true });
|
|
351
|
+
this.copies.delete(copy.key);
|
|
352
|
+
}
|
|
353
|
+
async read(handle) {
|
|
354
|
+
const prefixes = [rootOf(handle)];
|
|
298
355
|
const discovery = await this.house.post('/kit/door/discover', { prefixes });
|
|
299
356
|
let bundle = await this.house.post('/kit/door/bootstrap', { prefixes });
|
|
300
357
|
if ('transfer' in bundle)
|
|
301
|
-
bundle = (await (await fetch(bundle.transfer.url)).json());
|
|
358
|
+
bundle = (await (await fetch(relayed(bundle.transfer.url))).json());
|
|
302
359
|
const [view] = bundle.scopes;
|
|
303
360
|
if (view === undefined)
|
|
304
361
|
return null;
|
|
@@ -328,46 +385,13 @@ export class WorkingCopies {
|
|
|
328
385
|
}
|
|
329
386
|
return read;
|
|
330
387
|
}
|
|
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
|
-
}
|
|
388
|
+
async materialize(key, handle) {
|
|
389
|
+
const read = await this.read(handle);
|
|
390
|
+
if (read === null)
|
|
391
|
+
throw new Error('House serves nothing of it to copy');
|
|
392
|
+
const repository = join(localCopies(), 'rooms', read.anchor);
|
|
393
|
+
if (await exists(repository))
|
|
394
|
+
throw new Error(`${repository} already exists and House Kit leaves it as it is`);
|
|
371
395
|
const staging = join(dirname(repository), `.${randomUUID()}`);
|
|
372
396
|
await mkdir(staging, { recursive: true });
|
|
373
397
|
let house;
|
|
@@ -375,7 +399,7 @@ export class WorkingCopies {
|
|
|
375
399
|
try {
|
|
376
400
|
await git(staging, ['init', '-q', '-b', BRANCH]);
|
|
377
401
|
house = await importCommits(staging, [
|
|
378
|
-
{ message: `House ${rootOf(
|
|
402
|
+
{ message: `House ${rootOf(handle)}`, from: null, files: read.writable },
|
|
379
403
|
]);
|
|
380
404
|
await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
|
|
381
405
|
await git(staging, ['reset', '-q', '--hard']);
|
|
@@ -388,12 +412,10 @@ export class WorkingCopies {
|
|
|
388
412
|
throw error;
|
|
389
413
|
}
|
|
390
414
|
await this.save({
|
|
391
|
-
key
|
|
392
|
-
private: scope.private,
|
|
415
|
+
key,
|
|
393
416
|
room_ref: read.anchor,
|
|
394
|
-
room_handle:
|
|
417
|
+
room_handle: handle,
|
|
395
418
|
repository,
|
|
396
|
-
active: true,
|
|
397
419
|
house,
|
|
398
420
|
position: read.position,
|
|
399
421
|
log_epoch: read.log_epoch,
|
|
@@ -403,13 +425,14 @@ export class WorkingCopies {
|
|
|
403
425
|
manifests: { [house]: { parent: null, files: read.revisions } },
|
|
404
426
|
moves: [],
|
|
405
427
|
});
|
|
428
|
+
return `${rootOf(handle)}: copied to ${repository}`;
|
|
406
429
|
}
|
|
407
430
|
async received(copy) {
|
|
408
|
-
const read = await this.read(copy);
|
|
431
|
+
const read = await this.read(copy.room_handle);
|
|
409
432
|
if (read === null)
|
|
410
433
|
return copy;
|
|
411
434
|
const house = await importCommits(copy.repository, [
|
|
412
|
-
{ message: `House ${rootOf(copy)} at position ${read.position}`, from: copy.house, replaces: true, files: read.writable },
|
|
435
|
+
{ message: `House ${rootOf(copy.room_handle)} at position ${read.position}`, from: copy.house, replaces: true, files: read.writable },
|
|
413
436
|
]);
|
|
414
437
|
const removed = copy.read_only.filter((path) => !read.readOnly.has(path));
|
|
415
438
|
const readOnly = await readOnlyPlaced(copy.repository, copy.read_only, read.readOnly, removed);
|
|
@@ -426,30 +449,52 @@ export class WorkingCopies {
|
|
|
426
449
|
await ignoreRules(copy.repository, [...read.writable.keys()], next);
|
|
427
450
|
return next;
|
|
428
451
|
}
|
|
429
|
-
async
|
|
430
|
-
|
|
431
|
-
|
|
452
|
+
async refreshed(copies) {
|
|
453
|
+
const discovery = await this.house.post('/kit/door/discover', {
|
|
454
|
+
prefixes: copies.map((copy) => rootOf(copy.room_handle)),
|
|
455
|
+
});
|
|
456
|
+
const regranted = new Map();
|
|
457
|
+
for (const copy of copies) {
|
|
458
|
+
const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
|
|
459
|
+
if (authority !== undefined &&
|
|
460
|
+
JSON.stringify([authority.operations.write, authority.protected_paths]) !== JSON.stringify([copy.write, copy.protected])) {
|
|
461
|
+
regranted.set(copy.key, await this.save(await this.received(copy)));
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const caught = await this.caughtUp(copies.filter((copy) => !regranted.has(copy.key)), false);
|
|
465
|
+
return copies.map((copy) => regranted.get(copy.key) ?? caught.find((held) => held.key === copy.key));
|
|
466
|
+
}
|
|
467
|
+
async caughtUp(copies, whole) {
|
|
468
|
+
const caught = new Map(copies.map((copy) => [copy.key, copy]));
|
|
469
|
+
let asking = [...copies];
|
|
470
|
+
while (asking.length > 0) {
|
|
432
471
|
let answer;
|
|
433
472
|
try {
|
|
434
473
|
answer = await this.house.post('/kit/door/enumerate', {
|
|
435
|
-
prefixes:
|
|
436
|
-
positions:
|
|
474
|
+
prefixes: asking.map((copy) => rootOf(copy.room_handle)),
|
|
475
|
+
positions: asking.map((copy) => ({ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch })),
|
|
437
476
|
});
|
|
438
477
|
}
|
|
439
478
|
catch (error) {
|
|
440
|
-
const
|
|
441
|
-
if (
|
|
442
|
-
return this.received(copy);
|
|
443
|
-
if (code !== 'replica_behind')
|
|
479
|
+
const expired = asking.find((copy) => copy.room_ref === expiredAuthority(error));
|
|
480
|
+
if (expired === undefined)
|
|
444
481
|
throw error;
|
|
445
|
-
|
|
446
|
-
|
|
482
|
+
caught.set(expired.key, await this.save(await this.received(expired)));
|
|
483
|
+
asking = asking.filter((copy) => copy !== expired);
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const advanced = [];
|
|
487
|
+
for (const copy of asking) {
|
|
488
|
+
const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
|
|
489
|
+
if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
|
|
490
|
+
continue;
|
|
491
|
+
const next = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
|
|
492
|
+
caught.set(copy.key, next);
|
|
493
|
+
advanced.push(next);
|
|
447
494
|
}
|
|
448
|
-
|
|
449
|
-
if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
|
|
450
|
-
return copy;
|
|
451
|
-
copy = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
|
|
495
|
+
asking = whole ? advanced : [];
|
|
452
496
|
}
|
|
497
|
+
return copies.map((copy) => caught.get(copy.key));
|
|
453
498
|
}
|
|
454
499
|
async incorporated(copy, changes, position, logEpoch) {
|
|
455
500
|
const houseTree = await tree(copy.repository, copy.house);
|
|
@@ -482,7 +527,7 @@ export class WorkingCopies {
|
|
|
482
527
|
let { house, manifests } = copy;
|
|
483
528
|
if (files.size > 0) {
|
|
484
529
|
house = await importCommits(copy.repository, [
|
|
485
|
-
{ message: `House ${rootOf(copy)} at position ${position}`, from: copy.house, files },
|
|
530
|
+
{ message: `House ${rootOf(copy.room_handle)} at position ${position}`, from: copy.house, files },
|
|
486
531
|
]);
|
|
487
532
|
manifests = { ...manifests, [house]: { parent: copy.house, files: revisions } };
|
|
488
533
|
}
|
|
@@ -499,6 +544,8 @@ export class WorkingCopies {
|
|
|
499
544
|
}
|
|
500
545
|
async pushed(key, worktree, selector, caller) {
|
|
501
546
|
let copy = this.copies.get(key);
|
|
547
|
+
if (copy === undefined || copy.erasing)
|
|
548
|
+
return outside(worktree);
|
|
502
549
|
if (copy.push?.state === 'submitted')
|
|
503
550
|
return this.settle(copy, caller);
|
|
504
551
|
const selected = await run(worktree, ['rev-parse', '--verify', '--quiet', '--end-of-options', `${selector}^{commit}`]);
|
|
@@ -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,38 @@ 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);
|
|
663
|
+
const rebased = rebaseFixes(outcome.refused);
|
|
607
664
|
let settled = await this.save(without(copy));
|
|
608
665
|
try {
|
|
609
|
-
settled =
|
|
666
|
+
settled = (await this.caughtUp([settled], true))[0];
|
|
610
667
|
}
|
|
611
|
-
catch
|
|
612
|
-
return {
|
|
668
|
+
catch {
|
|
669
|
+
return {
|
|
670
|
+
refused: true,
|
|
671
|
+
text: rebased ? `${outcome.refused.text}\nHouse's current state did not arrive; push again later` : outcome.refused.text,
|
|
672
|
+
};
|
|
613
673
|
}
|
|
614
674
|
return {
|
|
615
675
|
refused: true,
|
|
616
|
-
text:
|
|
676
|
+
text: rebased
|
|
677
|
+
? `${outcome.refused.text}\n${HOUSE_REF} holds House's current state; rebase onto it and push again`
|
|
678
|
+
: outcome.refused.text,
|
|
617
679
|
};
|
|
618
680
|
}
|
|
619
681
|
return this.finished(await this.save({ ...copy, push: { ...copy.push, state: 'accepted', written: outcome.accepted } }));
|
|
620
682
|
}
|
|
621
|
-
async recovered(copy
|
|
683
|
+
async recovered(copy) {
|
|
622
684
|
const push = copy.push;
|
|
623
|
-
const current =
|
|
685
|
+
const current = (await this.caughtUp([copy], true))[0];
|
|
624
686
|
const house = await tree(copy.repository, current.house);
|
|
625
|
-
const root = rootOf(current);
|
|
687
|
+
const root = rootOf(current.room_handle);
|
|
626
688
|
if (!(await this.landed(copy.repository, push.changes, house, await tree(copy.repository, push.commit)))) {
|
|
627
689
|
await this.save(without(current));
|
|
628
690
|
return {
|
|
629
691
|
refused: true,
|
|
630
|
-
text:
|
|
692
|
+
text: `House does not hold ${short(push.commit)}; run house push again`,
|
|
631
693
|
};
|
|
632
694
|
}
|
|
633
695
|
const written = [];
|
|
@@ -636,7 +698,7 @@ export class WorkingCopies {
|
|
|
636
698
|
written.push({
|
|
637
699
|
path: `${root}/${path}`,
|
|
638
700
|
revision: entry === undefined ? null : (revisionAt(current, current.house, path) ?? null),
|
|
639
|
-
content: entry === undefined ? null : await blobText(copy.repository, entry
|
|
701
|
+
content: entry === undefined ? null : await blobText(copy.repository, entry),
|
|
640
702
|
});
|
|
641
703
|
}
|
|
642
704
|
return this.finished(await this.save({ ...current, push: { ...push, state: 'accepted', written } }));
|
|
@@ -652,10 +714,10 @@ export class WorkingCopies {
|
|
|
652
714
|
await this.save(without(copy));
|
|
653
715
|
return { refused: false, text: integrated.text };
|
|
654
716
|
}
|
|
655
|
-
catch
|
|
717
|
+
catch {
|
|
656
718
|
return {
|
|
657
719
|
refused: false,
|
|
658
|
-
text: `House accepted ${short(commit)};
|
|
720
|
+
text: `House accepted ${short(commit)}; run house push again to finish integrating it here`,
|
|
659
721
|
};
|
|
660
722
|
}
|
|
661
723
|
}
|
|
@@ -664,7 +726,7 @@ export class WorkingCopies {
|
|
|
664
726
|
const push = copy.push;
|
|
665
727
|
let written = push.written;
|
|
666
728
|
if (push.changes.some((change) => change.op === 'rename')) {
|
|
667
|
-
copy =
|
|
729
|
+
copy = (await this.caughtUp([copy], true))[0];
|
|
668
730
|
const house = await tree(copy.repository, copy.house);
|
|
669
731
|
const sent = await tree(copy.repository, push.house);
|
|
670
732
|
if (push.changes.some((change) => change.op === 'rename' && !house.has(relative(change.to)))) {
|
|
@@ -678,9 +740,9 @@ export class WorkingCopies {
|
|
|
678
740
|
written = [
|
|
679
741
|
...written,
|
|
680
742
|
{
|
|
681
|
-
path: `${rootOf(copy)}/${path}`,
|
|
743
|
+
path: `${rootOf(copy.room_handle)}/${path}`,
|
|
682
744
|
revision: entry === undefined ? null : (revisionAt(copy, copy.house, path) ?? null),
|
|
683
|
-
content: entry === undefined ? null : await blobText(copy.repository, entry
|
|
745
|
+
content: entry === undefined ? null : await blobText(copy.repository, entry),
|
|
684
746
|
},
|
|
685
747
|
];
|
|
686
748
|
}
|
|
@@ -698,7 +760,7 @@ export class WorkingCopies {
|
|
|
698
760
|
}
|
|
699
761
|
const edits = new Map([...files].filter(([path, content]) => (content === null ? selected.has(path) : selected.get(path) !== blobId(content))));
|
|
700
762
|
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`;
|
|
763
|
+
const accepted = `House ${rootOf(copy.room_handle)} with ${push.commit} accepted`;
|
|
702
764
|
const house = await importCommits(copy.repository, [
|
|
703
765
|
...(edits.size === 0 ? [] : [{ message: accepted, from: push.commit, files: edits }]),
|
|
704
766
|
{
|
|
@@ -725,7 +787,7 @@ export class WorkingCopies {
|
|
|
725
787
|
async landed(repository, changes, house, selected) {
|
|
726
788
|
const text = async (path) => {
|
|
727
789
|
const entry = house.get(relative(path));
|
|
728
|
-
return entry === undefined ? undefined : blobText(repository, entry
|
|
790
|
+
return entry === undefined ? undefined : blobText(repository, entry);
|
|
729
791
|
};
|
|
730
792
|
for (const change of changes) {
|
|
731
793
|
const current = await text(change.path);
|