@agentshouse/kit 0.2.1-c5262a8cc063d → 0.2.1-c5883630417e1
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 +27 -26
- package/dist/acp.js +16 -93
- package/dist/agents.js +104 -42
- package/dist/api.js +36 -3
- package/dist/app-git.js +64 -0
- package/dist/bootstrap.json +4 -4
- package/dist/bridge.js +36 -52
- package/dist/chatgpt.js +26 -0
- package/dist/clis.js +128 -65
- package/dist/codex-main.js +58 -0
- package/dist/conversations.js +288 -236
- package/dist/copies.js +167 -139
- package/dist/end.js +22 -0
- package/dist/enrol-main.js +7 -1
- package/dist/events.js +521 -0
- package/dist/files.js +23 -2
- package/dist/git.js +11 -8
- package/dist/home.js +81 -6
- package/dist/house-main.js +186 -56
- package/dist/instructions.js +9 -32
- package/dist/kit-main.js +54 -6
- package/dist/login.js +109 -103
- package/dist/logout.js +16 -13
- package/dist/mcp.js +2 -1
- package/dist/parts.js +82 -0
- package/dist/placement.js +48 -0
- package/dist/refusals.js +12 -0
- package/dist/relay.js +20 -0
- package/dist/resident.js +38 -27
- package/dist/scope.js +203 -0
- package/dist/secret-input.js +4 -1
- package/dist/service-main.js +9 -0
- package/dist/service.js +33 -0
- package/dist/shell.js +3 -0
- package/dist/sign-in.js +34 -9
- package/dist/skills.js +81 -12
- package/dist/stream.js +37 -6
- package/dist/transcription.js +57 -9
- package/dist/update.js +30 -6
- package/dist/wire.js +6 -0
- package/dist/work.js +348 -0
- package/how-we-work.md +13 -3
- package/package.json +3 -2
- 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,15 +2,15 @@ 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, localCopies, revision, run, succeeded, tree, } from './git.js';
|
|
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
9
|
import { refusalOf } from './refusals.js';
|
|
10
10
|
import { changed } from './translate.js';
|
|
11
|
+
import { relayed } from './relay.js';
|
|
11
12
|
const BRANCH = 'main';
|
|
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);
|
|
@@ -29,9 +29,29 @@ const REBASED = new Set([
|
|
|
29
29
|
function rebaseFixes(refusal) {
|
|
30
30
|
return refusal.code === 'edit_conflict' && refusal.conflicts.some((code) => REBASED.has(code));
|
|
31
31
|
}
|
|
32
|
+
function textOnly(paths) {
|
|
33
|
+
const [named, them] = paths.length === 1 ? [`${paths[0]} is`, 'it'] : [`${paths.join(', ')} are`, 'them'];
|
|
34
|
+
return `${named} not text, and house push sends text only; remove ${them} from the commit and place ${them} with house upload`;
|
|
35
|
+
}
|
|
32
36
|
function short(commit) {
|
|
33
37
|
return commit.slice(0, 12);
|
|
34
38
|
}
|
|
39
|
+
function moved(before, after) {
|
|
40
|
+
if (before.house !== after.house)
|
|
41
|
+
return [`${rootOf(after.room_handle)}: ${HOUSE_REF} is at ${short(after.house)}`];
|
|
42
|
+
return before.position === after.position ? [] : [`${rootOf(after.room_handle)}: read-only files refreshed`];
|
|
43
|
+
}
|
|
44
|
+
function expiredAuthority(error) {
|
|
45
|
+
if (!(error instanceof HouseRefusal))
|
|
46
|
+
return undefined;
|
|
47
|
+
try {
|
|
48
|
+
const { error: refused } = JSON.parse(error.text);
|
|
49
|
+
return refused?.code === 'expired' && typeof refused.authority === 'string' ? refused.authority : undefined;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
35
55
|
function covers(path, prefix) {
|
|
36
56
|
return prefix === '' || path === prefix || path.startsWith(`${prefix}/`);
|
|
37
57
|
}
|
|
@@ -40,11 +60,11 @@ function sidecar(path) {
|
|
|
40
60
|
segments.splice(-1, 0, PROVENANCE);
|
|
41
61
|
return segments.join('/');
|
|
42
62
|
}
|
|
43
|
-
function rootOf(
|
|
44
|
-
return
|
|
63
|
+
function rootOf(handle) {
|
|
64
|
+
return `/rooms/${handle}`;
|
|
45
65
|
}
|
|
46
66
|
function relative(path) {
|
|
47
|
-
return path.replace(/^\/
|
|
67
|
+
return path.replace(/^\/rooms\/[^/]+\//, '');
|
|
48
68
|
}
|
|
49
69
|
function declaredWritable(path, copy) {
|
|
50
70
|
const deepest = (scopes) => Math.max(-1, ...scopes.filter((scope) => covers(path, scope)).map((scope) => scope.length));
|
|
@@ -152,19 +172,18 @@ export class LocalCopies {
|
|
|
152
172
|
house;
|
|
153
173
|
copies = new Map();
|
|
154
174
|
queues = new Map();
|
|
155
|
-
|
|
156
|
-
selecting = Promise.resolve();
|
|
175
|
+
answering = Promise.resolve();
|
|
157
176
|
constructor(house) {
|
|
158
177
|
this.house = house;
|
|
159
178
|
this.owner = {
|
|
160
179
|
key: 'owner',
|
|
161
180
|
submit: async (operation, changes) => {
|
|
162
181
|
try {
|
|
163
|
-
const outcome = await house.post('/kit/door/batch', { operation_id: operation, changes });
|
|
182
|
+
const outcome = await retrying(() => house.post('/kit/door/batch', { operation_id: operation, changes }));
|
|
164
183
|
return { accepted: outcome.contents };
|
|
165
184
|
}
|
|
166
185
|
catch (error) {
|
|
167
|
-
if (!(error
|
|
186
|
+
if (!finalRefusal(error))
|
|
168
187
|
throw error;
|
|
169
188
|
return {
|
|
170
189
|
refused: refusalOf(error.text) ?? { code: '', text: `House refused it with ${error.status}`, conflicts: [] },
|
|
@@ -176,7 +195,7 @@ export class LocalCopies {
|
|
|
176
195
|
async load() {
|
|
177
196
|
const home = join(kitHome(), 'local-copies');
|
|
178
197
|
const names = (await readdir(join(home, 'rooms')).catch(() => [])).filter((name) => name.endsWith('.json'));
|
|
179
|
-
for (const path of
|
|
198
|
+
for (const path of names.map((name) => join(home, 'rooms', name))) {
|
|
180
199
|
const text = await readFile(path, 'utf8').catch(() => null);
|
|
181
200
|
if (text === null)
|
|
182
201
|
continue;
|
|
@@ -185,47 +204,68 @@ export class LocalCopies {
|
|
|
185
204
|
}
|
|
186
205
|
}
|
|
187
206
|
async mapping() {
|
|
188
|
-
await this.
|
|
207
|
+
await this.answering;
|
|
189
208
|
return [...this.copies.values()]
|
|
190
209
|
.filter((copy) => copy.active)
|
|
191
|
-
.map((copy) => `${rootOf(copy)}: ${copy.repository}`)
|
|
210
|
+
.map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
|
|
192
211
|
.sort();
|
|
193
212
|
}
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
async reselect() {
|
|
200
|
-
const { local_copy: chosen } = await this.house.post('/kit/local-copy/selection', {});
|
|
201
|
-
const selected = [
|
|
202
|
-
...(chosen?.rooms ?? [])
|
|
203
|
-
.filter((room) => room.room_handle !== null)
|
|
204
|
-
.map((room) => ({ key: `rooms/${room.room_ref}`, private: false, handle: room.room_handle })),
|
|
205
|
-
...(chosen?.private === true ? [{ key: 'private', private: true, handle: null }] : []),
|
|
206
|
-
];
|
|
207
|
-
const keys = new Set(selected.map((scope) => scope.key));
|
|
208
|
-
const work = [];
|
|
209
|
-
for (const key of this.copies.keys()) {
|
|
210
|
-
if (keys.has(key))
|
|
211
|
-
continue;
|
|
212
|
-
work.push(this.exclusive(key, async () => {
|
|
213
|
-
const copy = this.copies.get(key);
|
|
214
|
-
if (copy.active)
|
|
215
|
-
await this.save({ ...copy, active: false });
|
|
216
|
-
}));
|
|
217
|
-
}
|
|
218
|
-
for (const scope of selected)
|
|
219
|
-
work.push(this.exclusive(scope.key, () => this.apply(scope)).catch(logged));
|
|
220
|
-
await Promise.all(work);
|
|
213
|
+
async connected() {
|
|
214
|
+
const copied = await this.answered(() => this.fetched(false));
|
|
215
|
+
if (copied.refused)
|
|
216
|
+
logged(copied.text);
|
|
221
217
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
218
|
+
fetch() {
|
|
219
|
+
return this.answered(() => this.fetched(true));
|
|
220
|
+
}
|
|
221
|
+
answered(work) {
|
|
222
|
+
const answered = this.answering.then(work);
|
|
223
|
+
this.answering = answered.catch(() => undefined);
|
|
224
|
+
return answered;
|
|
225
|
+
}
|
|
226
|
+
async fetched(refreshing) {
|
|
227
|
+
const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', {});
|
|
228
|
+
const synced = new Map(answer.rooms.map((room) => [`rooms/${room.room_ref}`, room.room_handle]));
|
|
229
|
+
return this.holding([...new Set([...this.copies.keys(), ...synced.keys()])].sort(), async () => {
|
|
230
|
+
const said = [];
|
|
231
|
+
let refused = false;
|
|
232
|
+
for (const copy of this.copies.values()) {
|
|
233
|
+
if (synced.has(copy.key) || !copy.active)
|
|
234
|
+
continue;
|
|
235
|
+
await this.save({ ...copy, active: false });
|
|
236
|
+
said.push(`${rootOf(copy.room_handle)}: no longer synced to this Environment; its files stay`);
|
|
237
|
+
}
|
|
238
|
+
const current = [];
|
|
239
|
+
for (const [key, handle] of synced) {
|
|
240
|
+
try {
|
|
241
|
+
const held = this.copies.get(key);
|
|
242
|
+
if (held === undefined || !(await exists(held.repository))) {
|
|
243
|
+
said.push(await this.materialize(key, handle));
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const copy = held.room_handle === handle ? held : await this.save({ ...held, room_handle: handle });
|
|
247
|
+
if (copy.active)
|
|
248
|
+
current.push(copy);
|
|
249
|
+
else
|
|
250
|
+
said.push(...moved(copy, await this.save(await this.received({ ...copy, active: true }))));
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
refused = true;
|
|
254
|
+
said.push(`${rootOf(handle)}: ${causeOf(error)}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (refreshing && current.length > 0) {
|
|
258
|
+
try {
|
|
259
|
+
const refreshed = await this.refreshed(current);
|
|
260
|
+
current.forEach((copy, index) => said.push(...moved(copy, refreshed[index])));
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
refused = true;
|
|
264
|
+
said.push(error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
|
|
268
|
+
});
|
|
229
269
|
}
|
|
230
270
|
async observed(report) {
|
|
231
271
|
const key = await this.keyAt(dirname(report.common));
|
|
@@ -275,19 +315,12 @@ export class LocalCopies {
|
|
|
275
315
|
this.queues.set(key, queued.catch(() => undefined));
|
|
276
316
|
return queued;
|
|
277
317
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
this.delivering.add(key);
|
|
282
|
-
void this.exclusive(key, async () => {
|
|
283
|
-
this.delivering.delete(key);
|
|
284
|
-
const copy = this.copies.get(key);
|
|
285
|
-
if (copy.active)
|
|
286
|
-
await this.save(await this.caughtUp(copy));
|
|
287
|
-
}).catch(logged);
|
|
318
|
+
holding(keys, work) {
|
|
319
|
+
const [key, ...rest] = keys;
|
|
320
|
+
return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
|
|
288
321
|
}
|
|
289
322
|
async save(copy) {
|
|
290
|
-
const path = join(kitHome(), 'local-copies',
|
|
323
|
+
const path = join(kitHome(), 'local-copies', `${copy.key}.json`);
|
|
291
324
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
292
325
|
const temporary = `${path}.${process.pid}`;
|
|
293
326
|
await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
|
|
@@ -295,12 +328,12 @@ export class LocalCopies {
|
|
|
295
328
|
this.copies.set(copy.key, copy);
|
|
296
329
|
return copy;
|
|
297
330
|
}
|
|
298
|
-
async read(
|
|
299
|
-
const prefixes = [rootOf(
|
|
331
|
+
async read(handle) {
|
|
332
|
+
const prefixes = [rootOf(handle)];
|
|
300
333
|
const discovery = await this.house.post('/kit/door/discover', { prefixes });
|
|
301
334
|
let bundle = await this.house.post('/kit/door/bootstrap', { prefixes });
|
|
302
335
|
if ('transfer' in bundle)
|
|
303
|
-
bundle = (await (await fetch(bundle.transfer.url)).json());
|
|
336
|
+
bundle = (await (await fetch(relayed(bundle.transfer.url))).json());
|
|
304
337
|
const [view] = bundle.scopes;
|
|
305
338
|
if (view === undefined)
|
|
306
339
|
return null;
|
|
@@ -330,46 +363,13 @@ export class LocalCopies {
|
|
|
330
363
|
}
|
|
331
364
|
return read;
|
|
332
365
|
}
|
|
333
|
-
async
|
|
334
|
-
|
|
335
|
-
if (
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
copy = await this.save({ ...copy, room_handle: scope.handle });
|
|
341
|
-
if (!copy.active) {
|
|
342
|
-
await this.save(await this.received({ ...copy, active: true }));
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
const discovery = await this.house.post('/kit/door/discover', {
|
|
346
|
-
prefixes: [rootOf(copy)],
|
|
347
|
-
});
|
|
348
|
-
const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
|
|
349
|
-
if (authority === undefined)
|
|
350
|
-
return;
|
|
351
|
-
if (authority.managed_replication !== 'admitted') {
|
|
352
|
-
logged(`operation_denied: ${rootOf(copy)} is not synchronized to this Environment`);
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
if (JSON.stringify([authority.operations.write, authority.protected_paths]) !==
|
|
356
|
-
JSON.stringify([copy.write, copy.protected])) {
|
|
357
|
-
await this.save(await this.received(copy));
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
await this.save(await this.caughtUp(copy));
|
|
361
|
-
}
|
|
362
|
-
async materialize(scope) {
|
|
363
|
-
const read = await this.read({ private: scope.private, room_handle: scope.handle });
|
|
364
|
-
if (read === null) {
|
|
365
|
-
logged(`House serves nothing of ${rootOf({ private: scope.private, room_handle: scope.handle })} to copy`);
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
const repository = scope.private ? join(localCopies(), 'private') : join(localCopies(), 'rooms', read.anchor);
|
|
369
|
-
if (await exists(repository)) {
|
|
370
|
-
logged(`${repository} already exists and House Kit leaves it as it is`);
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
366
|
+
async materialize(key, handle) {
|
|
367
|
+
const read = await this.read(handle);
|
|
368
|
+
if (read === null)
|
|
369
|
+
throw new Error('House serves nothing of it to copy');
|
|
370
|
+
const repository = join(localCopies(), 'rooms', read.anchor);
|
|
371
|
+
if (await exists(repository))
|
|
372
|
+
throw new Error(`${repository} already exists and House Kit leaves it as it is`);
|
|
373
373
|
const staging = join(dirname(repository), `.${randomUUID()}`);
|
|
374
374
|
await mkdir(staging, { recursive: true });
|
|
375
375
|
let house;
|
|
@@ -377,7 +377,7 @@ export class LocalCopies {
|
|
|
377
377
|
try {
|
|
378
378
|
await git(staging, ['init', '-q', '-b', BRANCH]);
|
|
379
379
|
house = await importCommits(staging, [
|
|
380
|
-
{ message: `House ${rootOf(
|
|
380
|
+
{ message: `House ${rootOf(handle)}`, from: null, files: read.writable },
|
|
381
381
|
]);
|
|
382
382
|
await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
|
|
383
383
|
await git(staging, ['reset', '-q', '--hard']);
|
|
@@ -390,10 +390,9 @@ export class LocalCopies {
|
|
|
390
390
|
throw error;
|
|
391
391
|
}
|
|
392
392
|
await this.save({
|
|
393
|
-
key
|
|
394
|
-
private: scope.private,
|
|
393
|
+
key,
|
|
395
394
|
room_ref: read.anchor,
|
|
396
|
-
room_handle:
|
|
395
|
+
room_handle: handle,
|
|
397
396
|
repository,
|
|
398
397
|
active: true,
|
|
399
398
|
house,
|
|
@@ -405,13 +404,14 @@ export class LocalCopies {
|
|
|
405
404
|
manifests: { [house]: { parent: null, files: read.revisions } },
|
|
406
405
|
moves: [],
|
|
407
406
|
});
|
|
407
|
+
return `${rootOf(handle)}: copied to ${repository}`;
|
|
408
408
|
}
|
|
409
409
|
async received(copy) {
|
|
410
|
-
const read = await this.read(copy);
|
|
410
|
+
const read = await this.read(copy.room_handle);
|
|
411
411
|
if (read === null)
|
|
412
412
|
return copy;
|
|
413
413
|
const house = await importCommits(copy.repository, [
|
|
414
|
-
{ message: `House ${rootOf(copy)} at position ${read.position}`, from: copy.house, replaces: true, files: read.writable },
|
|
414
|
+
{ message: `House ${rootOf(copy.room_handle)} at position ${read.position}`, from: copy.house, replaces: true, files: read.writable },
|
|
415
415
|
]);
|
|
416
416
|
const removed = copy.read_only.filter((path) => !read.readOnly.has(path));
|
|
417
417
|
const readOnly = await readOnlyPlaced(copy.repository, copy.read_only, read.readOnly, removed);
|
|
@@ -428,30 +428,52 @@ export class LocalCopies {
|
|
|
428
428
|
await ignoreRules(copy.repository, [...read.writable.keys()], next);
|
|
429
429
|
return next;
|
|
430
430
|
}
|
|
431
|
-
async
|
|
432
|
-
|
|
433
|
-
|
|
431
|
+
async refreshed(copies) {
|
|
432
|
+
const discovery = await this.house.post('/kit/door/discover', {
|
|
433
|
+
prefixes: copies.map((copy) => rootOf(copy.room_handle)),
|
|
434
|
+
});
|
|
435
|
+
const regranted = new Map();
|
|
436
|
+
for (const copy of copies) {
|
|
437
|
+
const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
|
|
438
|
+
if (authority !== undefined &&
|
|
439
|
+
JSON.stringify([authority.operations.write, authority.protected_paths]) !== JSON.stringify([copy.write, copy.protected])) {
|
|
440
|
+
regranted.set(copy.key, await this.save(await this.received(copy)));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const caught = await this.caughtUp(copies.filter((copy) => !regranted.has(copy.key)), false);
|
|
444
|
+
return copies.map((copy) => regranted.get(copy.key) ?? caught.find((held) => held.key === copy.key));
|
|
445
|
+
}
|
|
446
|
+
async caughtUp(copies, whole) {
|
|
447
|
+
const caught = new Map(copies.map((copy) => [copy.key, copy]));
|
|
448
|
+
let asking = [...copies];
|
|
449
|
+
while (asking.length > 0) {
|
|
434
450
|
let answer;
|
|
435
451
|
try {
|
|
436
452
|
answer = await this.house.post('/kit/door/enumerate', {
|
|
437
|
-
prefixes:
|
|
438
|
-
positions:
|
|
453
|
+
prefixes: asking.map((copy) => rootOf(copy.room_handle)),
|
|
454
|
+
positions: asking.map((copy) => ({ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch })),
|
|
439
455
|
});
|
|
440
456
|
}
|
|
441
457
|
catch (error) {
|
|
442
|
-
const
|
|
443
|
-
if (
|
|
444
|
-
return this.received(copy);
|
|
445
|
-
if (code !== 'replica_behind')
|
|
458
|
+
const expired = asking.find((copy) => copy.room_ref === expiredAuthority(error));
|
|
459
|
+
if (expired === undefined)
|
|
446
460
|
throw error;
|
|
447
|
-
|
|
448
|
-
|
|
461
|
+
caught.set(expired.key, await this.save(await this.received(expired)));
|
|
462
|
+
asking = asking.filter((copy) => copy !== expired);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
const advanced = [];
|
|
466
|
+
for (const copy of asking) {
|
|
467
|
+
const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
|
|
468
|
+
if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
|
|
469
|
+
continue;
|
|
470
|
+
const next = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
|
|
471
|
+
caught.set(copy.key, next);
|
|
472
|
+
advanced.push(next);
|
|
449
473
|
}
|
|
450
|
-
|
|
451
|
-
if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
|
|
452
|
-
return copy;
|
|
453
|
-
copy = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
|
|
474
|
+
asking = whole ? advanced : [];
|
|
454
475
|
}
|
|
476
|
+
return copies.map((copy) => caught.get(copy.key));
|
|
455
477
|
}
|
|
456
478
|
async incorporated(copy, changes, position, logEpoch) {
|
|
457
479
|
const houseTree = await tree(copy.repository, copy.house);
|
|
@@ -484,7 +506,7 @@ export class LocalCopies {
|
|
|
484
506
|
let { house, manifests } = copy;
|
|
485
507
|
if (files.size > 0) {
|
|
486
508
|
house = await importCommits(copy.repository, [
|
|
487
|
-
{ message: `House ${rootOf(copy)} at position ${position}`, from: copy.house, files },
|
|
509
|
+
{ message: `House ${rootOf(copy.room_handle)} at position ${position}`, from: copy.house, files },
|
|
488
510
|
]);
|
|
489
511
|
manifests = { ...manifests, [house]: { parent: copy.house, files: revisions } };
|
|
490
512
|
}
|
|
@@ -515,7 +537,7 @@ export class LocalCopies {
|
|
|
515
537
|
copy = await this.save(without(copy));
|
|
516
538
|
}
|
|
517
539
|
if (!copy.active) {
|
|
518
|
-
return { refused: true, text: `${rootOf(copy)} is no longer synced to this Environment, so House takes no push from it` };
|
|
540
|
+
return { refused: true, text: `${rootOf(copy.room_handle)} is no longer synced to this Environment, so House takes no push from it` };
|
|
519
541
|
}
|
|
520
542
|
const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
|
|
521
543
|
const base = listed.find((ancestor) => ancestor in copy.manifests);
|
|
@@ -549,12 +571,18 @@ export class LocalCopies {
|
|
|
549
571
|
return this.settle(submitted, caller);
|
|
550
572
|
}
|
|
551
573
|
async derived(copy, base, commit) {
|
|
552
|
-
const root = rootOf(copy);
|
|
574
|
+
const root = rootOf(copy.room_handle);
|
|
553
575
|
const before = await tree(copy.repository, base);
|
|
554
576
|
const after = await tree(copy.repository, commit);
|
|
555
577
|
const moved = movedPaths(copy.moves, before, after);
|
|
556
578
|
const arrived = new Set(moved.values());
|
|
557
|
-
const
|
|
579
|
+
const binary = [];
|
|
580
|
+
for (const [path, entry] of after)
|
|
581
|
+
if (before.get(path) !== entry && !(await isText(copy.repository, entry)))
|
|
582
|
+
binary.push(path);
|
|
583
|
+
if (binary.length > 0)
|
|
584
|
+
throw new Error(textOnly(binary));
|
|
585
|
+
const text = (entry) => blobText(copy.repository, entry);
|
|
558
586
|
const changes = [];
|
|
559
587
|
for (const path of [...new Set([...before.keys(), ...after.keys()])].sort()) {
|
|
560
588
|
if (arrived.has(path))
|
|
@@ -567,7 +595,7 @@ export class LocalCopies {
|
|
|
567
595
|
changes.push({ op: 'rename', path: `${root}/${path}`, to: `${root}/${to}`, base: known });
|
|
568
596
|
const moving = after.get(to);
|
|
569
597
|
if (moving !== prior) {
|
|
570
|
-
changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving
|
|
598
|
+
changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving) });
|
|
571
599
|
}
|
|
572
600
|
continue;
|
|
573
601
|
}
|
|
@@ -576,9 +604,9 @@ export class LocalCopies {
|
|
|
576
604
|
if (next === undefined)
|
|
577
605
|
changes.push({ op: 'remove', path: `${root}/${path}`, base: known });
|
|
578
606
|
else if (prior === undefined)
|
|
579
|
-
changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next
|
|
607
|
+
changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next) });
|
|
580
608
|
else
|
|
581
|
-
changes.push(...changed(`${root}/${path}`, path, known, await text(prior
|
|
609
|
+
changes.push(...changed(`${root}/${path}`, path, known, await text(prior), await text(next)));
|
|
582
610
|
}
|
|
583
611
|
return changes;
|
|
584
612
|
}
|
|
@@ -605,12 +633,12 @@ export class LocalCopies {
|
|
|
605
633
|
const { upload, ...unstaged } = copy.push;
|
|
606
634
|
return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
|
|
607
635
|
}
|
|
608
|
-
if (outcome.refused.code === '
|
|
636
|
+
if (outcome.refused.code === 'expired')
|
|
609
637
|
return this.recovered(copy);
|
|
610
638
|
const rebased = rebaseFixes(outcome.refused);
|
|
611
639
|
let settled = await this.save(without(copy));
|
|
612
640
|
try {
|
|
613
|
-
settled =
|
|
641
|
+
settled = (await this.caughtUp([settled], true))[0];
|
|
614
642
|
}
|
|
615
643
|
catch {
|
|
616
644
|
return {
|
|
@@ -629,9 +657,9 @@ export class LocalCopies {
|
|
|
629
657
|
}
|
|
630
658
|
async recovered(copy) {
|
|
631
659
|
const push = copy.push;
|
|
632
|
-
const current =
|
|
660
|
+
const current = (await this.caughtUp([copy], true))[0];
|
|
633
661
|
const house = await tree(copy.repository, current.house);
|
|
634
|
-
const root = rootOf(current);
|
|
662
|
+
const root = rootOf(current.room_handle);
|
|
635
663
|
if (!(await this.landed(copy.repository, push.changes, house, await tree(copy.repository, push.commit)))) {
|
|
636
664
|
await this.save(without(current));
|
|
637
665
|
return {
|
|
@@ -645,7 +673,7 @@ export class LocalCopies {
|
|
|
645
673
|
written.push({
|
|
646
674
|
path: `${root}/${path}`,
|
|
647
675
|
revision: entry === undefined ? null : (revisionAt(current, current.house, path) ?? null),
|
|
648
|
-
content: entry === undefined ? null : await blobText(copy.repository, entry
|
|
676
|
+
content: entry === undefined ? null : await blobText(copy.repository, entry),
|
|
649
677
|
});
|
|
650
678
|
}
|
|
651
679
|
return this.finished(await this.save({ ...current, push: { ...push, state: 'accepted', written } }));
|
|
@@ -673,7 +701,7 @@ export class LocalCopies {
|
|
|
673
701
|
const push = copy.push;
|
|
674
702
|
let written = push.written;
|
|
675
703
|
if (push.changes.some((change) => change.op === 'rename')) {
|
|
676
|
-
copy =
|
|
704
|
+
copy = (await this.caughtUp([copy], true))[0];
|
|
677
705
|
const house = await tree(copy.repository, copy.house);
|
|
678
706
|
const sent = await tree(copy.repository, push.house);
|
|
679
707
|
if (push.changes.some((change) => change.op === 'rename' && !house.has(relative(change.to)))) {
|
|
@@ -687,9 +715,9 @@ export class LocalCopies {
|
|
|
687
715
|
written = [
|
|
688
716
|
...written,
|
|
689
717
|
{
|
|
690
|
-
path: `${rootOf(copy)}/${path}`,
|
|
718
|
+
path: `${rootOf(copy.room_handle)}/${path}`,
|
|
691
719
|
revision: entry === undefined ? null : (revisionAt(copy, copy.house, path) ?? null),
|
|
692
|
-
content: entry === undefined ? null : await blobText(copy.repository, entry
|
|
720
|
+
content: entry === undefined ? null : await blobText(copy.repository, entry),
|
|
693
721
|
},
|
|
694
722
|
];
|
|
695
723
|
}
|
|
@@ -707,7 +735,7 @@ export class LocalCopies {
|
|
|
707
735
|
}
|
|
708
736
|
const edits = new Map([...files].filter(([path, content]) => (content === null ? selected.has(path) : selected.get(path) !== blobId(content))));
|
|
709
737
|
const received = [...files.keys()].filter((path) => revisionAt(copy, copy.house, path) === revisionAt(copy, push.house, path));
|
|
710
|
-
const accepted = `House ${rootOf(copy)} with ${push.commit} accepted`;
|
|
738
|
+
const accepted = `House ${rootOf(copy.room_handle)} with ${push.commit} accepted`;
|
|
711
739
|
const house = await importCommits(copy.repository, [
|
|
712
740
|
...(edits.size === 0 ? [] : [{ message: accepted, from: push.commit, files: edits }]),
|
|
713
741
|
{
|
|
@@ -734,7 +762,7 @@ export class LocalCopies {
|
|
|
734
762
|
async landed(repository, changes, house, selected) {
|
|
735
763
|
const text = async (path) => {
|
|
736
764
|
const entry = house.get(relative(path));
|
|
737
|
-
return entry === undefined ? undefined : blobText(repository, entry
|
|
765
|
+
return entry === undefined ? undefined : blobText(repository, entry);
|
|
738
766
|
};
|
|
739
767
|
for (const change of changes) {
|
|
740
768
|
const current = await text(change.path);
|
package/dist/end.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { forgetRejected } from './home.js';
|
|
2
|
+
import { installCommand } from './placement.js';
|
|
3
|
+
import { killDescendants } from './scope.js';
|
|
4
|
+
export const REPLACED = 'another Kit started for this Environment';
|
|
5
|
+
function exit(reason, status) {
|
|
6
|
+
process.stderr.write(`kit: ${reason}\n`);
|
|
7
|
+
killDescendants();
|
|
8
|
+
process.exit(status);
|
|
9
|
+
}
|
|
10
|
+
export function end(reason) {
|
|
11
|
+
exit(reason, 0);
|
|
12
|
+
}
|
|
13
|
+
export function restart(reason) {
|
|
14
|
+
exit(reason, 1);
|
|
15
|
+
}
|
|
16
|
+
export function updateRequired(house) {
|
|
17
|
+
end(`This Kit is too old for House. Update it: ${installCommand(house)}`);
|
|
18
|
+
}
|
|
19
|
+
export async function signInAgain(rejected) {
|
|
20
|
+
await forgetRejected(rejected);
|
|
21
|
+
restart('sign in again');
|
|
22
|
+
}
|
package/dist/enrol-main.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { writeEnrolment } from './home.js';
|
|
3
3
|
const [house, environment] = process.argv.slice(2);
|
|
4
|
-
|
|
4
|
+
const relay = process.env.HOUSE_KIT_RELAY || undefined;
|
|
5
|
+
await writeEnrolment({
|
|
6
|
+
house: house,
|
|
7
|
+
...(relay === undefined ? {} : { relay }),
|
|
8
|
+
environment: environment,
|
|
9
|
+
credential: readFileSync(0, 'utf8').trim(),
|
|
10
|
+
});
|