@agentshouse/kit 0.2.1-c7f53ff9eaf98 → 0.2.1-c83d468784cc9
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 +28 -28
- package/dist/acp.js +90 -19
- package/dist/afk.js +17 -0
- package/dist/agents.js +87 -44
- package/dist/api.js +5 -5
- package/dist/authority-main.js +3 -3
- package/dist/bridge.js +5 -5
- package/dist/building.js +50 -0
- package/dist/calls.js +71 -0
- package/dist/chunks.js +49 -0
- package/dist/clis.js +45 -40
- package/dist/{stream.js → connection.js} +34 -52
- package/dist/conversations.js +498 -178
- package/dist/copies.js +315 -162
- package/dist/end.js +14 -4
- package/dist/events.js +66 -8
- package/dist/files.js +139 -37
- package/dist/git.js +47 -13
- package/dist/home.js +0 -7
- package/dist/house-cli.js +95 -87
- package/dist/house-main.js +3 -3
- package/dist/instructions.js +29 -9
- package/dist/kit-cli.js +4 -4
- package/dist/kit-main.js +3 -3
- package/dist/login.js +4 -4
- package/dist/logout.js +1 -1
- package/dist/mcp.js +6 -2
- package/dist/parts.js +10 -0
- package/dist/placement.js +21 -20
- package/dist/relay.js +21 -0
- package/dist/resident.js +51 -38
- package/dist/scope.js +15 -4
- package/dist/scripts.js +255 -0
- package/dist/secret-input.js +1 -1
- package/dist/service.js +23 -1
- package/dist/sign-in.js +22 -39
- package/dist/silence.js +28 -0
- package/dist/skills.js +42 -97
- package/dist/transcription.js +83 -43
- package/dist/translate.js +2 -8
- package/dist/update.js +113 -127
- package/dist/wire.js +37 -3
- package/package.json +20 -21
- package/dist/bootstrap.json +0 -24
- package/dist/clis-main.js +0 -23
- package/how-we-work.md +0 -33
- package/skills/ask-matt/LICENSE +0 -21
- package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
- package/skills/ask-matt/SKILL.md +0 -97
- package/skills/ask-matt/agents/openai.yaml +0 -5
- package/skills/code-review/LICENSE +0 -21
- package/skills/code-review/SKILL.md +0 -89
- package/skills/code-review/agents/openai.yaml +0 -3
- package/skills/codebase-design/DEEPENING.md +0 -37
- package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
- package/skills/codebase-design/LICENSE +0 -21
- package/skills/codebase-design/SKILL.md +0 -114
- package/skills/codebase-design/agents/openai.yaml +0 -3
- package/skills/diagnosing-bugs/LICENSE +0 -21
- package/skills/diagnosing-bugs/SKILL.md +0 -138
- package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
- package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
- package/skills/domain-modeling/ADR-FORMAT.md +0 -47
- package/skills/domain-modeling/GLOSSARY-FORMAT.md +0 -60
- package/skills/domain-modeling/LICENSE +0 -21
- package/skills/domain-modeling/SKILL.md +0 -74
- package/skills/domain-modeling/agents/openai.yaml +0 -3
- package/skills/grill-me/LICENSE +0 -21
- package/skills/grill-me/SKILL.md +0 -7
- package/skills/grill-me/agents/openai.yaml +0 -5
- package/skills/grill-with-docs/LICENSE +0 -21
- package/skills/grill-with-docs/SKILL.md +0 -7
- package/skills/grill-with-docs/agents/openai.yaml +0 -5
- package/skills/grilling/LICENSE +0 -21
- package/skills/grilling/SKILL.md +0 -30
- package/skills/grilling/agents/openai.yaml +0 -3
- package/skills/handoff/LICENSE +0 -21
- package/skills/handoff/SKILL.md +0 -16
- package/skills/handoff/agents/openai.yaml +0 -5
- package/skills/implement/LICENSE +0 -21
- package/skills/implement/SKILL.md +0 -17
- package/skills/implement/agents/openai.yaml +0 -5
- package/skills/implement-spec/LICENSE +0 -21
- package/skills/implement-spec/SKILL.md +0 -40
- package/skills/implement-spec/agents/openai.yaml +0 -5
- package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
- package/skills/improve-codebase-architecture/LICENSE +0 -21
- package/skills/improve-codebase-architecture/SKILL.md +0 -71
- package/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
- package/skills/pr/CREDITS.md +0 -3
- package/skills/pr/LICENSE +0 -21
- package/skills/pr/SKILL.md +0 -170
- package/skills/pr/agents/openai.yaml +0 -3
- package/skills/prototype/LICENSE +0 -21
- package/skills/prototype/LOGIC.md +0 -67
- package/skills/prototype/SKILL.md +0 -26
- package/skills/prototype/UI.md +0 -112
- package/skills/prototype/agents/openai.yaml +0 -3
- package/skills/research/LICENSE +0 -21
- package/skills/research/SKILL.md +0 -12
- package/skills/research/agents/openai.yaml +0 -3
- package/skills/retro/LICENSE +0 -21
- package/skills/retro/SKILL.md +0 -44
- package/skills/retro/agents/openai.yaml +0 -5
- package/skills/tdd/LICENSE +0 -21
- package/skills/tdd/SKILL.md +0 -38
- package/skills/tdd/agents/openai.yaml +0 -3
- package/skills/tdd/mocking.md +0 -59
- package/skills/tdd/tests.md +0 -77
- package/skills/teach/GLOSSARY-FORMAT.md +0 -35
- package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
- package/skills/teach/LICENSE +0 -21
- package/skills/teach/MISSION-FORMAT.md +0 -31
- package/skills/teach/RESOURCES-FORMAT.md +0 -32
- package/skills/teach/SKILL.md +0 -140
- package/skills/teach/agents/openai.yaml +0 -5
- package/skills/to-questionnaire/LICENSE +0 -21
- package/skills/to-questionnaire/SKILL.md +0 -54
- package/skills/to-questionnaire/agents/openai.yaml +0 -5
- package/skills/to-spec/LICENSE +0 -21
- package/skills/to-spec/SKILL.md +0 -75
- package/skills/to-spec/agents/openai.yaml +0 -5
- package/skills/to-tickets/LICENSE +0 -21
- package/skills/to-tickets/SKILL.md +0 -105
- package/skills/to-tickets/agents/openai.yaml +0 -5
- package/skills/triage/AGENT-BRIEF.md +0 -207
- package/skills/triage/LICENSE +0 -21
- package/skills/triage/OUT-OF-SCOPE.md +0 -105
- package/skills/triage/SKILL.md +0 -112
- package/skills/triage/agents/openai.yaml +0 -5
- package/skills/wait-what/LICENSE +0 -21
- package/skills/wait-what/SKILL.md +0 -7
- package/skills/wait-what/agents/openai.yaml +0 -5
- package/skills/wayfinder/LICENSE +0 -21
- package/skills/wayfinder/SKILL.md +0 -128
- package/skills/wayfinder/agents/openai.yaml +0 -5
- package/skills/wizard/LICENSE +0 -21
- package/skills/wizard/SKILL.md +0 -44
- package/skills/wizard/agents/openai.yaml +0 -3
- package/skills/wizard/template.sh +0 -223
- package/skills/writing-for-agents/LICENSE +0 -21
- package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
- package/skills/writing-for-agents/SKILL.md +0 -81
- package/skills/writing-for-agents/agents/openai.yaml +0 -3
package/dist/copies.js
CHANGED
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
import { parse } from '@agentshouse/mdmodel';
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { mkdir, readFile, readdir, realpath, rename, rm, rmdir, stat, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { finalRefusal, HouseRefusal, retrying } from './api.js';
|
|
6
|
+
import { chunkTexts, stagedChunks } from './chunks.js';
|
|
6
7
|
import { blobId, blobText, git, isText, HOUSE_REF, importCommits, localCopies, revision, run, succeeded, tree, } from './git.js';
|
|
8
|
+
import { paused } from './files.js';
|
|
7
9
|
import { kitHome } from './home.js';
|
|
8
10
|
import { operationId } from './operation.js';
|
|
9
11
|
import { refusalOf } from './refusals.js';
|
|
10
12
|
import { changed } from './translate.js';
|
|
11
|
-
import { relayed } from './relay.js';
|
|
12
13
|
const BRANCH = 'main';
|
|
13
14
|
const PROVENANCE = '_provenance';
|
|
14
|
-
const IDENTITY = { GIT_COMMITTER_NAME: '
|
|
15
|
+
const IDENTITY = { GIT_COMMITTER_NAME: 'Kit', GIT_COMMITTER_EMAIL: 'house-kit@localhost' };
|
|
16
|
+
const ROOMS = 'rooms/';
|
|
17
|
+
const ENUMERATE = '/kit/door/changes/enumerate';
|
|
18
|
+
const NOT_READY = 'not ready yet; Kit copies it once House has built it';
|
|
19
|
+
const STAGED_CHUNKS = 'house-chunks';
|
|
20
|
+
// A Room answered not ready is asked again on this schedule: its chunk build usually catches up within seconds, so the first asks come soon and the last repeats until it does.
|
|
21
|
+
const ASKED_AGAIN_MS = [1000, 2000, 3000, 5000, 8000, 11_000, 15_000];
|
|
22
|
+
class NotReady extends Error {
|
|
23
|
+
rooms;
|
|
24
|
+
constructor(rooms) {
|
|
25
|
+
super(NOT_READY);
|
|
26
|
+
this.rooms = rooms;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
15
29
|
function causeOf(error) {
|
|
16
30
|
return error instanceof Error ? error.message : String(error);
|
|
17
31
|
}
|
|
@@ -58,6 +72,9 @@ function expiredAuthority(error) {
|
|
|
58
72
|
return undefined;
|
|
59
73
|
}
|
|
60
74
|
}
|
|
75
|
+
function grouped(items, size) {
|
|
76
|
+
return items.flatMap((_, index) => (index % size === 0 ? [items.slice(index, index + size)] : []));
|
|
77
|
+
}
|
|
61
78
|
function covers(path, prefix) {
|
|
62
79
|
return prefix === '' || path === prefix || path.startsWith(`${prefix}/`);
|
|
63
80
|
}
|
|
@@ -69,6 +86,9 @@ function sidecar(path) {
|
|
|
69
86
|
function recordOf(key) {
|
|
70
87
|
return join(kitHome(), 'local-copies', `${key}.json`);
|
|
71
88
|
}
|
|
89
|
+
function stagingPrefix() {
|
|
90
|
+
return `.${createHash('sha256').update(kitHome()).digest('hex').slice(0, 16)}-`;
|
|
91
|
+
}
|
|
72
92
|
function rootOf(handle) {
|
|
73
93
|
return `/rooms/${handle}`;
|
|
74
94
|
}
|
|
@@ -96,32 +116,59 @@ async function removeFile(repository, path) {
|
|
|
96
116
|
return;
|
|
97
117
|
}
|
|
98
118
|
}
|
|
99
|
-
async function
|
|
119
|
+
async function readOnlyFiles(repository, held) {
|
|
100
120
|
const tracked = new Set((await git(repository, ['ls-files', '-z'])).split('\0'));
|
|
101
121
|
const holding = new Set(held);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
122
|
+
return {
|
|
123
|
+
remove: async (path) => {
|
|
124
|
+
if (!holding.has(path))
|
|
125
|
+
return;
|
|
126
|
+
if (!tracked.has(path))
|
|
127
|
+
await removeFile(repository, path);
|
|
128
|
+
holding.delete(path);
|
|
129
|
+
},
|
|
130
|
+
place: async (path, content) => {
|
|
131
|
+
if (tracked.has(path))
|
|
132
|
+
return;
|
|
133
|
+
if (!holding.has(path) && (await exists(join(repository, path))))
|
|
134
|
+
return;
|
|
135
|
+
await writeText(join(repository, path), content);
|
|
136
|
+
holding.add(path);
|
|
137
|
+
},
|
|
138
|
+
held: () => [...holding].sort(),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
async function readOnlyPlaced(repository, held, written, removed) {
|
|
142
|
+
const files = await readOnlyFiles(repository, held);
|
|
143
|
+
for (const path of removed)
|
|
144
|
+
await files.remove(path);
|
|
145
|
+
for (const [path, content] of written)
|
|
146
|
+
await files.place(path, content);
|
|
147
|
+
return files.held();
|
|
148
|
+
}
|
|
149
|
+
async function importedWhole(repository, scope, texts, commit, readOnly) {
|
|
150
|
+
const revisions = {};
|
|
151
|
+
async function* tracked() {
|
|
152
|
+
for await (const { path, content, revision: held, provenance } of texts) {
|
|
153
|
+
if (declaredWritable(path, scope)) {
|
|
154
|
+
revisions[path] = held;
|
|
155
|
+
yield [path, content];
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
await readOnly.place(path, content);
|
|
159
|
+
}
|
|
160
|
+
if (provenance !== undefined)
|
|
161
|
+
await readOnly.place(sidecar(path), provenance);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { house: await importCommits(repository, [{ ...commit, files: tracked() }]), revisions };
|
|
118
165
|
}
|
|
119
166
|
async function ruled(path, rules) {
|
|
120
167
|
if ((await readFile(path, 'utf8').catch(() => null)) !== rules)
|
|
121
168
|
await writeFile(path, rules);
|
|
122
169
|
}
|
|
123
170
|
async function ignoreRules(repository, writable, copy) {
|
|
124
|
-
const readOnly =
|
|
171
|
+
const readOnly = copy.read_only.filter((path) => !path.split('/').includes(PROVENANCE));
|
|
125
172
|
const protectedPath = (path) => copy.protected.some((prefix) => prefix !== '' && covers(path, prefix));
|
|
126
173
|
const whole = copy.protected.filter((prefix) => prefix !== '' && !writable.some((path) => covers(path, prefix)));
|
|
127
174
|
const house = [
|
|
@@ -130,7 +177,7 @@ async function ignoreRules(repository, writable, copy) {
|
|
|
130
177
|
].map(pattern);
|
|
131
178
|
const granted = readOnly.filter((path) => !protectedPath(path)).map(pattern);
|
|
132
179
|
await ruled(join(repository, '.gitignore'), [
|
|
133
|
-
'#
|
|
180
|
+
'# Kit writes this file and .ignore. The paths below are read-only House',
|
|
134
181
|
'# content: Git does not track them, and House refuses a change to them even',
|
|
135
182
|
'# when this file is edited or a path is force-added.',
|
|
136
183
|
'/.gitignore',
|
|
@@ -145,7 +192,7 @@ async function ignoreRules(repository, writable, copy) {
|
|
|
145
192
|
'',
|
|
146
193
|
].join('\n'));
|
|
147
194
|
await ruled(join(repository, '.ignore'), [
|
|
148
|
-
'#
|
|
195
|
+
'# Kit writes this file so native search still reads the read-only House',
|
|
149
196
|
'# content that .gitignore keeps out of Git.',
|
|
150
197
|
`!${PROVENANCE}`,
|
|
151
198
|
...[...house, ...granted].map((rule) => `!${rule}`),
|
|
@@ -181,6 +228,7 @@ export class LocalCopies {
|
|
|
181
228
|
house;
|
|
182
229
|
copies = new Map();
|
|
183
230
|
queues = new Map();
|
|
231
|
+
waiting = new Set();
|
|
184
232
|
answering = Promise.resolve();
|
|
185
233
|
constructor(house) {
|
|
186
234
|
this.house = house;
|
|
@@ -188,7 +236,7 @@ export class LocalCopies {
|
|
|
188
236
|
key: 'owner',
|
|
189
237
|
submit: async (operation, changes) => {
|
|
190
238
|
try {
|
|
191
|
-
const outcome = await retrying(() => house.post('/kit/door/batch', { operation_id: operation, changes }));
|
|
239
|
+
const outcome = await retrying(() => house.post('/kit/door/files/batch', { operation_id: operation, changes }));
|
|
192
240
|
return { accepted: outcome.contents };
|
|
193
241
|
}
|
|
194
242
|
catch (error) {
|
|
@@ -212,83 +260,135 @@ export class LocalCopies {
|
|
|
212
260
|
this.copies.set(copy.key, copy);
|
|
213
261
|
}
|
|
214
262
|
}
|
|
263
|
+
async swept() {
|
|
264
|
+
const rooms = join(localCopies(), 'rooms');
|
|
265
|
+
for (const name of await readdir(rooms).catch(() => [])) {
|
|
266
|
+
if (name.startsWith(stagingPrefix()))
|
|
267
|
+
await rm(join(rooms, name), { recursive: true, force: true });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
215
270
|
async mapping() {
|
|
216
271
|
await this.answering;
|
|
217
272
|
return [...this.copies.values()]
|
|
218
|
-
.filter((copy) => copy.
|
|
273
|
+
.filter((copy) => copy.unfollowed === undefined)
|
|
219
274
|
.map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
|
|
220
275
|
.sort();
|
|
221
276
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
277
|
+
connected() {
|
|
278
|
+
return this.selected();
|
|
279
|
+
}
|
|
280
|
+
async changed(rooms) {
|
|
281
|
+
if (rooms.length > 0)
|
|
282
|
+
await this.selected(rooms);
|
|
226
283
|
}
|
|
227
284
|
fetch() {
|
|
228
285
|
return this.answered(() => this.fetched(true));
|
|
229
286
|
}
|
|
287
|
+
async selected(rooms) {
|
|
288
|
+
const copied = await this.answered(() => this.fetched(false, rooms));
|
|
289
|
+
if (copied.refused)
|
|
290
|
+
logged(copied.text);
|
|
291
|
+
}
|
|
230
292
|
answered(work) {
|
|
231
293
|
const answered = this.answering.then(work);
|
|
232
294
|
this.answering = answered.catch(() => undefined);
|
|
233
295
|
return answered;
|
|
234
296
|
}
|
|
235
|
-
async fetched(refreshing) {
|
|
236
|
-
const { local_copy: answer } = await this.house.post('/kit/local-
|
|
237
|
-
const synced = new Map(answer.rooms.map((room) => [
|
|
238
|
-
|
|
297
|
+
async fetched(refreshing, rooms) {
|
|
298
|
+
const { local_copy: answer } = await this.house.post('/kit/local-copies/selection/read', rooms === undefined ? {} : { rooms });
|
|
299
|
+
const synced = new Map(answer.rooms.map((room) => [`${ROOMS}${room.room_ref}`, room.room_handle]));
|
|
300
|
+
const named = rooms === undefined ? null : new Set(rooms.map((room) => `${ROOMS}${room}`));
|
|
301
|
+
const held = () => [...this.copies.values()].filter((copy) => named === null || named.has(copy.key));
|
|
302
|
+
return this.holding([...new Set([...held().map((copy) => copy.key), ...synced.keys()])].sort(), async () => {
|
|
239
303
|
const said = [];
|
|
240
304
|
let refused = false;
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (
|
|
305
|
+
const mark = refreshing ? 'said' : 'unsaid';
|
|
306
|
+
for (const copy of held()) {
|
|
307
|
+
if (synced.has(copy.key) || copy.unfollowed === 'said' || copy.unfollowed === mark)
|
|
244
308
|
continue;
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
said.push(`${rootOf(copy.room_handle)}:
|
|
248
|
-
}
|
|
249
|
-
catch (error) {
|
|
250
|
-
refused = true;
|
|
251
|
-
said.push(`${rootOf(copy.room_handle)}: ${causeOf(error)}`);
|
|
252
|
-
}
|
|
309
|
+
await this.save({ ...copy, unfollowed: mark });
|
|
310
|
+
if (refreshing)
|
|
311
|
+
said.push(`${rootOf(copy.room_handle)}: no longer followed; kept as it is`);
|
|
253
312
|
}
|
|
254
313
|
const current = [];
|
|
314
|
+
const wanted = new Map();
|
|
255
315
|
for (const [key, handle] of synced) {
|
|
256
316
|
try {
|
|
257
317
|
const held = this.copies.get(key);
|
|
258
|
-
if (held?.erasing)
|
|
259
|
-
continue;
|
|
260
318
|
if (held === undefined || !(await exists(held.repository))) {
|
|
261
|
-
|
|
319
|
+
wanted.set(key, handle);
|
|
262
320
|
continue;
|
|
263
321
|
}
|
|
264
|
-
|
|
322
|
+
const { unfollowed, ...followed } = held;
|
|
323
|
+
current.push(unfollowed === undefined && held.room_handle === handle ? held : await this.save({ ...followed, room_handle: handle }));
|
|
265
324
|
}
|
|
266
325
|
catch (error) {
|
|
267
326
|
refused = true;
|
|
268
327
|
said.push(`${rootOf(handle)}: ${causeOf(error)}`);
|
|
269
328
|
}
|
|
270
329
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
330
|
+
const unready = [];
|
|
331
|
+
const unsettled = [];
|
|
332
|
+
if (wanted.size > 0) {
|
|
333
|
+
const { authorities } = await this.discovered([...wanted.values()]);
|
|
334
|
+
for (const [key, handle] of wanted) {
|
|
335
|
+
const authority = authorities.find((entry) => `${ROOMS}${entry.room_ref}` === key);
|
|
336
|
+
try {
|
|
337
|
+
if (authority === undefined)
|
|
338
|
+
throw new Error('House serves nothing of it to copy');
|
|
339
|
+
said.push(await this.copied({ key, handle, authority }));
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
if (error instanceof NotReady) {
|
|
343
|
+
unready.push(...error.rooms);
|
|
344
|
+
said.push(`${rootOf(handle)}: ${NOT_READY}`);
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
refused = true;
|
|
348
|
+
unsettled.push(key.slice(ROOMS.length));
|
|
349
|
+
said.push(`${rootOf(handle)}: ${causeOf(error)}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
275
352
|
}
|
|
276
|
-
|
|
353
|
+
}
|
|
354
|
+
const reading = refreshing ? current : current.filter((copy) => this.waiting.has(copy.room_ref));
|
|
355
|
+
if (reading.length > 0) {
|
|
356
|
+
const failed = await this.refreshed(reading, unready).then(() => null, houseCause);
|
|
357
|
+
for (const copy of reading) {
|
|
358
|
+
said.push(...(unready.includes(copy.room_ref) ? [`${rootOf(copy.room_handle)}: ${NOT_READY}`] : moved(copy, this.copies.get(copy.key))));
|
|
359
|
+
}
|
|
360
|
+
if (failed !== null) {
|
|
277
361
|
refused = true;
|
|
278
|
-
|
|
362
|
+
unsettled.push(...reading.map((copy) => copy.room_ref));
|
|
363
|
+
said.push(failed);
|
|
279
364
|
}
|
|
280
365
|
}
|
|
366
|
+
for (const room of rooms ?? [...this.waiting]) {
|
|
367
|
+
if (!unready.includes(room) && !unsettled.includes(room))
|
|
368
|
+
this.waiting.delete(room);
|
|
369
|
+
}
|
|
370
|
+
const due = unready.filter((room) => !this.waiting.has(room));
|
|
371
|
+
for (const room of due)
|
|
372
|
+
this.waiting.add(room);
|
|
373
|
+
if (due.length > 0)
|
|
374
|
+
void this.askedAgain(due);
|
|
281
375
|
return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
|
|
282
376
|
});
|
|
283
377
|
}
|
|
378
|
+
async askedAgain(rooms) {
|
|
379
|
+
let asking = rooms;
|
|
380
|
+
for (let step = 0; asking.length > 0; step++) {
|
|
381
|
+
await paused(ASKED_AGAIN_MS[Math.min(step, ASKED_AGAIN_MS.length - 1)]);
|
|
382
|
+
await this.selected().catch(logged);
|
|
383
|
+
asking = asking.filter((room) => this.waiting.has(room));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
284
386
|
async observed(report) {
|
|
285
387
|
const key = await this.keyAt(dirname(report.common));
|
|
286
388
|
if (key === undefined)
|
|
287
389
|
return;
|
|
288
390
|
await this.exclusive(key, async () => {
|
|
289
391
|
const copy = this.copies.get(key);
|
|
290
|
-
if (copy === undefined || copy.erasing)
|
|
291
|
-
return;
|
|
292
392
|
let moves = copy.moves;
|
|
293
393
|
for (const { from, to } of report.moves) {
|
|
294
394
|
const held = moves.find((move) => move.worktree === report.worktree && move.to === from);
|
|
@@ -344,133 +444,170 @@ export class LocalCopies {
|
|
|
344
444
|
this.copies.set(copy.key, copy);
|
|
345
445
|
return copy;
|
|
346
446
|
}
|
|
347
|
-
async
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
write: authority.operations.write,
|
|
369
|
-
protected: authority.protected_paths,
|
|
370
|
-
writable: new Map(),
|
|
371
|
-
revisions: {},
|
|
372
|
-
readOnly: new Map(),
|
|
373
|
-
};
|
|
374
|
-
for (const file of view.manifest.files) {
|
|
375
|
-
const content = contents.get(file.path);
|
|
376
|
-
if (file.writable) {
|
|
377
|
-
read.writable.set(file.path, content);
|
|
378
|
-
read.revisions[file.path] = file.revision;
|
|
379
|
-
}
|
|
380
|
-
else {
|
|
381
|
-
read.readOnly.set(file.path, content);
|
|
447
|
+
async discovered(handles) {
|
|
448
|
+
const discover = (group) => this.house.post('/kit/door/rooms/discover', { prefixes: group.map(rootOf) });
|
|
449
|
+
const [first, ...rest] = handles;
|
|
450
|
+
const { bounds, authorities } = await discover([first]);
|
|
451
|
+
for (const group of grouped(rest, bounds.max_prefixes))
|
|
452
|
+
authorities.push(...(await discover(group)).authorities);
|
|
453
|
+
return { bounds, authorities };
|
|
454
|
+
}
|
|
455
|
+
async chunked(root, room, directory) {
|
|
456
|
+
await mkdir(directory, { recursive: true });
|
|
457
|
+
for (let renewed = false;; renewed = true) {
|
|
458
|
+
const { rooms } = await this.house.post('/kit/door/room-chunks/list', { prefixes: [root] });
|
|
459
|
+
const list = rooms.find((entry) => entry.room_ref === room);
|
|
460
|
+
if (list === undefined)
|
|
461
|
+
throw new Error('House serves nothing of it to copy');
|
|
462
|
+
if (!list.ready)
|
|
463
|
+
throw new NotReady([room]);
|
|
464
|
+
const held = new Set(await readdir(directory));
|
|
465
|
+
const failure = await stagedChunks(directory, list.chunks.filter(({ hash }) => !held.has(hash)));
|
|
466
|
+
if (failure === undefined) {
|
|
467
|
+
return { position: list.position, log_epoch: list.log_epoch, texts: chunkTexts(directory, list.chunks, list.models) };
|
|
382
468
|
}
|
|
383
|
-
if (
|
|
384
|
-
|
|
469
|
+
if (renewed && (await readdir(directory)).length === held.size)
|
|
470
|
+
throw failure;
|
|
385
471
|
}
|
|
386
|
-
return read;
|
|
387
472
|
}
|
|
388
|
-
async
|
|
389
|
-
const
|
|
390
|
-
if (read === null)
|
|
391
|
-
throw new Error('House serves nothing of it to copy');
|
|
392
|
-
const repository = join(localCopies(), 'rooms', read.anchor);
|
|
473
|
+
async materialized({ key, handle, authority }, whole) {
|
|
474
|
+
const repository = join(localCopies(), 'rooms', authority.room_ref);
|
|
393
475
|
if (await exists(repository))
|
|
394
|
-
throw new Error(`${repository} already exists and
|
|
395
|
-
const staging = join(dirname(repository),
|
|
476
|
+
throw new Error(`${repository} already exists and Kit leaves it as it is`);
|
|
477
|
+
const staging = join(dirname(repository), `${stagingPrefix()}${randomUUID()}`);
|
|
478
|
+
const scope = { write: authority.operations.write, protected: authority.protected_paths };
|
|
396
479
|
await mkdir(staging, { recursive: true });
|
|
397
|
-
let
|
|
398
|
-
let readOnly;
|
|
480
|
+
let copy;
|
|
399
481
|
try {
|
|
400
482
|
await git(staging, ['init', '-q', '-b', BRANCH]);
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
483
|
+
const read = await whole(join(staging, '.git', STAGED_CHUNKS));
|
|
484
|
+
const readOnly = await readOnlyFiles(staging, []);
|
|
485
|
+
const { house, revisions } = await importedWhole(staging, scope, read.texts, { message: `House ${rootOf(handle)}`, from: null }, readOnly);
|
|
404
486
|
await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
|
|
405
487
|
await git(staging, ['reset', '-q', '--hard']);
|
|
406
|
-
|
|
407
|
-
|
|
488
|
+
copy = {
|
|
489
|
+
key,
|
|
490
|
+
room_ref: authority.room_ref,
|
|
491
|
+
room_handle: handle,
|
|
492
|
+
repository,
|
|
493
|
+
house,
|
|
494
|
+
position: read.position,
|
|
495
|
+
log_epoch: read.log_epoch,
|
|
496
|
+
...scope,
|
|
497
|
+
read_only: readOnly.held(),
|
|
498
|
+
manifests: { [house]: { parent: null, files: revisions } },
|
|
499
|
+
moves: [],
|
|
500
|
+
};
|
|
501
|
+
await ignoreRules(staging, Object.keys(revisions), copy);
|
|
502
|
+
await rm(join(staging, '.git', STAGED_CHUNKS), { recursive: true, force: true });
|
|
408
503
|
await rename(staging, repository);
|
|
409
504
|
}
|
|
410
505
|
catch (error) {
|
|
411
506
|
await rm(staging, { recursive: true, force: true });
|
|
412
507
|
throw error;
|
|
413
508
|
}
|
|
414
|
-
|
|
415
|
-
key,
|
|
416
|
-
room_ref: read.anchor,
|
|
417
|
-
room_handle: handle,
|
|
418
|
-
repository,
|
|
419
|
-
house,
|
|
420
|
-
position: read.position,
|
|
421
|
-
log_epoch: read.log_epoch,
|
|
422
|
-
write: read.write,
|
|
423
|
-
protected: read.protected,
|
|
424
|
-
read_only: readOnly,
|
|
425
|
-
manifests: { [house]: { parent: null, files: read.revisions } },
|
|
426
|
-
moves: [],
|
|
427
|
-
});
|
|
428
|
-
return `${rootOf(handle)}: copied to ${repository}`;
|
|
509
|
+
return this.save(copy);
|
|
429
510
|
}
|
|
430
|
-
async
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
511
|
+
async copied(wanted) {
|
|
512
|
+
const { handle, authority } = wanted;
|
|
513
|
+
const root = rootOf(handle);
|
|
514
|
+
let answer = null;
|
|
515
|
+
try {
|
|
516
|
+
if (!this.waiting.has(authority.room_ref)) {
|
|
517
|
+
answer = await this.house.post(ENUMERATE, {
|
|
518
|
+
prefixes: [root],
|
|
519
|
+
positions: [
|
|
520
|
+
{ authority: authority.room_ref, position: '0', log_epoch: authority.log_epoch, page: { through: authority.position, read: 0 } },
|
|
521
|
+
],
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
if (expiredAuthority(error) !== authority.room_ref)
|
|
527
|
+
throw error;
|
|
528
|
+
}
|
|
529
|
+
const view = answer?.authorities.find((entry) => entry.authority === authority.room_ref);
|
|
530
|
+
if (answer !== null && view === undefined)
|
|
531
|
+
throw new Error('House serves nothing of it to copy');
|
|
532
|
+
if (view !== undefined && view.next === undefined) {
|
|
533
|
+
const texts = view.changes.filter((change) => !('removed' in change));
|
|
534
|
+
const copy = await this.materialized(wanted, async () => ({ position: view.position, log_epoch: view.log_epoch, texts }));
|
|
535
|
+
return `${root}: copied to ${copy.repository}`;
|
|
536
|
+
}
|
|
537
|
+
const copy = await this.materialized(wanted, (directory) => this.chunked(root, authority.room_ref, directory));
|
|
538
|
+
const [caught] = await this.caughtUp([copy], true);
|
|
539
|
+
await succeeded(copy.repository, ['merge', '--ff-only', '-q', caught.house]);
|
|
540
|
+
return `${root}: copied to ${copy.repository}`;
|
|
451
541
|
}
|
|
452
|
-
async
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
}
|
|
456
|
-
|
|
542
|
+
async received(copy, authority) {
|
|
543
|
+
const root = rootOf(copy.room_handle);
|
|
544
|
+
const directory = join(copy.repository, '.git', STAGED_CHUNKS);
|
|
545
|
+
const scope = { write: authority.operations.write, protected: authority.protected_paths };
|
|
546
|
+
try {
|
|
547
|
+
const read = await this.chunked(root, copy.room_ref, directory);
|
|
548
|
+
const readOnly = await readOnlyFiles(copy.repository, copy.read_only);
|
|
549
|
+
for (const path of copy.read_only)
|
|
550
|
+
await readOnly.remove(path);
|
|
551
|
+
const commit = { message: `House ${root} at position ${read.position}`, from: copy.house, replaces: true };
|
|
552
|
+
const { house, revisions } = await importedWhole(copy.repository, scope, read.texts, commit, readOnly);
|
|
553
|
+
const next = {
|
|
554
|
+
...copy,
|
|
555
|
+
...scope,
|
|
556
|
+
house,
|
|
557
|
+
position: read.position,
|
|
558
|
+
log_epoch: read.log_epoch,
|
|
559
|
+
read_only: readOnly.held(),
|
|
560
|
+
manifests: { ...copy.manifests, [house]: { parent: null, files: revisions } },
|
|
561
|
+
};
|
|
562
|
+
await ignoreRules(copy.repository, Object.keys(revisions), next);
|
|
563
|
+
return next;
|
|
564
|
+
}
|
|
565
|
+
finally {
|
|
566
|
+
await rm(directory, { recursive: true, force: true });
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
async reread(copy) {
|
|
570
|
+
const { authorities } = await this.discovered([copy.room_handle]);
|
|
571
|
+
const authority = authorities.find((entry) => entry.room_ref === copy.room_ref);
|
|
572
|
+
if (authority === undefined)
|
|
573
|
+
throw new NotReady([copy.room_ref]);
|
|
574
|
+
return this.received(copy, authority);
|
|
575
|
+
}
|
|
576
|
+
async refreshed(copies, unready) {
|
|
577
|
+
const { bounds, authorities } = await this.discovered(copies.map((copy) => copy.room_handle));
|
|
578
|
+
const read = [];
|
|
579
|
+
const unchanged = [];
|
|
457
580
|
for (const copy of copies) {
|
|
458
|
-
const authority =
|
|
581
|
+
const authority = authorities.find((entry) => entry.room_ref === copy.room_ref);
|
|
459
582
|
if (authority !== undefined &&
|
|
460
583
|
JSON.stringify([authority.operations.write, authority.protected_paths]) !== JSON.stringify([copy.write, copy.protected])) {
|
|
461
|
-
|
|
584
|
+
try {
|
|
585
|
+
read.push(await this.save(await this.received(copy, authority)));
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
if (!(error instanceof NotReady))
|
|
589
|
+
throw error;
|
|
590
|
+
unready.push(...error.rooms);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
unchanged.push(copy);
|
|
462
595
|
}
|
|
463
596
|
}
|
|
464
|
-
const
|
|
465
|
-
|
|
597
|
+
for (const group of grouped(unchanged, bounds.max_prefixes))
|
|
598
|
+
await this.caughtUp(group, false, unready);
|
|
599
|
+
for (const group of grouped(read, bounds.max_prefixes))
|
|
600
|
+
await this.caughtUp(group, true, unready);
|
|
466
601
|
}
|
|
467
|
-
async caughtUp(copies, whole) {
|
|
602
|
+
async caughtUp(copies, whole, unready) {
|
|
468
603
|
const caught = new Map(copies.map((copy) => [copy.key, copy]));
|
|
604
|
+
const read = new Set();
|
|
605
|
+
const waiting = unready ?? [];
|
|
469
606
|
let asking = [...copies];
|
|
470
607
|
while (asking.length > 0) {
|
|
471
608
|
let answer;
|
|
472
609
|
try {
|
|
473
|
-
answer = await this.house.post(
|
|
610
|
+
answer = await this.house.post(ENUMERATE, {
|
|
474
611
|
prefixes: asking.map((copy) => rootOf(copy.room_handle)),
|
|
475
612
|
positions: asking.map((copy) => ({ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch })),
|
|
476
613
|
});
|
|
@@ -479,8 +616,22 @@ export class LocalCopies {
|
|
|
479
616
|
const expired = asking.find((copy) => copy.room_ref === expiredAuthority(error));
|
|
480
617
|
if (expired === undefined)
|
|
481
618
|
throw error;
|
|
482
|
-
caught.set(expired.key, await this.save(await this.received(expired)));
|
|
483
619
|
asking = asking.filter((copy) => copy !== expired);
|
|
620
|
+
if (read.has(expired.key)) {
|
|
621
|
+
waiting.push(expired.room_ref);
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
read.add(expired.key);
|
|
625
|
+
try {
|
|
626
|
+
const next = await this.save(await this.reread(expired));
|
|
627
|
+
caught.set(expired.key, next);
|
|
628
|
+
asking.push(next);
|
|
629
|
+
}
|
|
630
|
+
catch (refused) {
|
|
631
|
+
if (!(refused instanceof NotReady))
|
|
632
|
+
throw refused;
|
|
633
|
+
waiting.push(expired.room_ref);
|
|
634
|
+
}
|
|
484
635
|
continue;
|
|
485
636
|
}
|
|
486
637
|
const advanced = [];
|
|
@@ -492,8 +643,10 @@ export class LocalCopies {
|
|
|
492
643
|
caught.set(copy.key, next);
|
|
493
644
|
advanced.push(next);
|
|
494
645
|
}
|
|
495
|
-
asking =
|
|
646
|
+
asking = advanced.filter((copy) => whole || read.has(copy.key));
|
|
496
647
|
}
|
|
648
|
+
if (unready === undefined && waiting.length > 0)
|
|
649
|
+
throw new NotReady(waiting);
|
|
497
650
|
return copies.map((copy) => caught.get(copy.key));
|
|
498
651
|
}
|
|
499
652
|
async incorporated(copy, changes, position, logEpoch) {
|
|
@@ -544,8 +697,6 @@ export class LocalCopies {
|
|
|
544
697
|
}
|
|
545
698
|
async pushed(key, worktree, selector, caller) {
|
|
546
699
|
let copy = this.copies.get(key);
|
|
547
|
-
if (copy === undefined || copy.erasing)
|
|
548
|
-
return outside(worktree);
|
|
549
700
|
if (copy.push?.state === 'submitted')
|
|
550
701
|
return this.settle(copy, caller);
|
|
551
702
|
const selected = await run(worktree, ['rev-parse', '--verify', '--quiet', '--end-of-options', `${selector}^{commit}`]);
|
|
@@ -598,7 +749,7 @@ export class LocalCopies {
|
|
|
598
749
|
const arrived = new Set(moved.values());
|
|
599
750
|
const arriving = [...after].filter(([path, entry]) => before.get(path) !== entry);
|
|
600
751
|
if (arriving.length > 0) {
|
|
601
|
-
const { bounds } = await this.house.post('/kit/door/discover', { prefixes: [root] });
|
|
752
|
+
const { bounds } = await this.house.post('/kit/door/rooms/discover', { prefixes: [root] });
|
|
602
753
|
const binary = [];
|
|
603
754
|
for (const [path, entry] of arriving) {
|
|
604
755
|
if (!(await isText(copy.repository, entry, bounds.max_authored_file_bytes)))
|
|
@@ -660,8 +811,10 @@ export class LocalCopies {
|
|
|
660
811
|
}
|
|
661
812
|
if (outcome.refused.code === 'expired')
|
|
662
813
|
return this.recovered(copy);
|
|
663
|
-
const rebased = rebaseFixes(outcome.refused);
|
|
664
814
|
let settled = await this.save(without(copy));
|
|
815
|
+
if (settled.unfollowed !== undefined)
|
|
816
|
+
return { refused: true, text: outcome.refused.text };
|
|
817
|
+
const rebased = rebaseFixes(outcome.refused);
|
|
665
818
|
try {
|
|
666
819
|
settled = (await this.caughtUp([settled], true))[0];
|
|
667
820
|
}
|
|
@@ -813,7 +966,7 @@ export class LocalCopies {
|
|
|
813
966
|
return true;
|
|
814
967
|
}
|
|
815
968
|
async revisionText(path, wanted) {
|
|
816
|
-
const page = await this.house.post('/kit/door/revisions', {
|
|
969
|
+
const page = await this.house.post('/kit/door/files/revisions/list', {
|
|
817
970
|
path,
|
|
818
971
|
revision: wanted,
|
|
819
972
|
});
|