@agentshouse/kit 0.2.1-cbabe8ad337ff → 0.2.1-cbdf76b9a90a3
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 +30 -29
- package/dist/acp.js +34 -14
- package/dist/agents.js +109 -22
- package/dist/api.js +6 -2
- package/dist/app-git.js +10 -26
- package/dist/bootstrap.json +4 -4
- package/dist/bridge.js +10 -24
- package/dist/chatgpt.js +32 -0
- package/dist/clis.js +51 -9
- package/dist/conversations.js +289 -99
- package/dist/copies.js +195 -119
- package/dist/end.js +22 -3
- package/dist/enrol-main.js +7 -1
- package/dist/events.js +4 -0
- package/dist/files.js +8 -2
- package/dist/git.js +10 -8
- package/dist/home.js +96 -7
- package/dist/house-cli.js +291 -0
- package/dist/house-main.js +7 -177
- package/dist/inputs.js +63 -0
- package/dist/instructions.js +10 -32
- package/dist/kit-cli.js +68 -0
- package/dist/kit-main.js +8 -22
- package/dist/login.js +112 -22
- package/dist/logout.js +0 -2
- package/dist/parts.js +34 -16
- package/dist/placement.js +39 -0
- package/dist/relay.js +12 -2
- package/dist/resident.js +82 -26
- package/dist/scope.js +3 -0
- package/dist/service-main.js +9 -0
- package/dist/service.js +55 -0
- package/dist/sign-in.js +5 -23
- package/dist/skills.js +81 -12
- package/dist/stream.js +68 -17
- package/dist/transcription.js +3 -3
- package/dist/translate.js +2 -8
- package/dist/update.js +48 -29
- package/dist/wire.js +22 -0
- package/dist/work.js +3 -5
- 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/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
|
@@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
3
3
|
import { mkdir, readFile, readdir, realpath, rename, rm, rmdir, stat, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { finalRefusal, HouseRefusal, retrying } from './api.js';
|
|
6
|
-
import { blobId, blobText, git, HOUSE_REF, importCommits, localCopies, revision, run, succeeded, tree, } from './git.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';
|
|
@@ -15,6 +15,9 @@ const IDENTITY = { GIT_COMMITTER_NAME: 'House Kit', GIT_COMMITTER_EMAIL: 'house-
|
|
|
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
|
}
|
|
@@ -29,9 +32,35 @@ const REBASED = new Set([
|
|
|
29
32
|
function rebaseFixes(refusal) {
|
|
30
33
|
return refusal.code === 'edit_conflict' && refusal.conflicts.some((code) => REBASED.has(code));
|
|
31
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
|
+
}
|
|
32
42
|
function short(commit) {
|
|
33
43
|
return commit.slice(0, 12);
|
|
34
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;
|
|
53
|
+
try {
|
|
54
|
+
const { error: refused } = JSON.parse(error.text);
|
|
55
|
+
return refused?.code === 'expired' && typeof refused.authority === 'string' ? refused.authority : undefined;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function grouped(items, size) {
|
|
62
|
+
return items.flatMap((_, index) => (index % size === 0 ? [items.slice(index, index + size)] : []));
|
|
63
|
+
}
|
|
35
64
|
function covers(path, prefix) {
|
|
36
65
|
return prefix === '' || path === prefix || path.startsWith(`${prefix}/`);
|
|
37
66
|
}
|
|
@@ -40,6 +69,9 @@ function sidecar(path) {
|
|
|
40
69
|
segments.splice(-1, 0, PROVENANCE);
|
|
41
70
|
return segments.join('/');
|
|
42
71
|
}
|
|
72
|
+
function recordOf(key) {
|
|
73
|
+
return join(kitHome(), 'local-copies', `${key}.json`);
|
|
74
|
+
}
|
|
43
75
|
function rootOf(handle) {
|
|
44
76
|
return `/rooms/${handle}`;
|
|
45
77
|
}
|
|
@@ -152,8 +184,7 @@ export class LocalCopies {
|
|
|
152
184
|
house;
|
|
153
185
|
copies = new Map();
|
|
154
186
|
queues = new Map();
|
|
155
|
-
|
|
156
|
-
selecting = Promise.resolve();
|
|
187
|
+
answering = Promise.resolve();
|
|
157
188
|
constructor(house) {
|
|
158
189
|
this.house = house;
|
|
159
190
|
this.owner = {
|
|
@@ -185,42 +216,81 @@ export class LocalCopies {
|
|
|
185
216
|
}
|
|
186
217
|
}
|
|
187
218
|
async mapping() {
|
|
188
|
-
await this.
|
|
219
|
+
await this.answering;
|
|
189
220
|
return [...this.copies.values()]
|
|
190
|
-
.filter((copy) => copy.
|
|
221
|
+
.filter((copy) => copy.erasing === undefined)
|
|
191
222
|
.map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
|
|
192
223
|
.sort();
|
|
193
224
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
this.selecting = selected.catch(() => undefined);
|
|
197
|
-
return selected;
|
|
198
|
-
}
|
|
199
|
-
async reselect() {
|
|
200
|
-
const { local_copy: chosen } = await this.house.post('/kit/local-copy/selection', {});
|
|
201
|
-
const selected = (chosen?.rooms ?? []).flatMap((room) => room.room_handle === null ? [] : [{ key: `rooms/${room.room_ref}`, handle: room.room_handle }]);
|
|
202
|
-
const keys = new Set(selected.map((scope) => scope.key));
|
|
203
|
-
const work = [];
|
|
204
|
-
for (const key of this.copies.keys()) {
|
|
205
|
-
if (keys.has(key))
|
|
206
|
-
continue;
|
|
207
|
-
work.push(this.exclusive(key, async () => {
|
|
208
|
-
const copy = this.copies.get(key);
|
|
209
|
-
if (copy.active)
|
|
210
|
-
await this.save({ ...copy, active: false });
|
|
211
|
-
}));
|
|
212
|
-
}
|
|
213
|
-
for (const scope of selected)
|
|
214
|
-
work.push(this.exclusive(scope.key, () => this.apply(scope)).catch(logged));
|
|
215
|
-
await Promise.all(work);
|
|
225
|
+
connected() {
|
|
226
|
+
return this.selected();
|
|
216
227
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
228
|
+
async changed(rooms) {
|
|
229
|
+
if (rooms.length > 0)
|
|
230
|
+
await this.selected(rooms);
|
|
231
|
+
}
|
|
232
|
+
fetch() {
|
|
233
|
+
return this.answered(() => this.fetched(true));
|
|
234
|
+
}
|
|
235
|
+
async selected(rooms) {
|
|
236
|
+
const copied = await this.answered(() => this.fetched(false, rooms));
|
|
237
|
+
if (copied.refused)
|
|
238
|
+
logged(copied.text);
|
|
239
|
+
}
|
|
240
|
+
answered(work) {
|
|
241
|
+
const answered = this.answering.then(work);
|
|
242
|
+
this.answering = answered.catch(() => undefined);
|
|
243
|
+
return answered;
|
|
244
|
+
}
|
|
245
|
+
async fetched(refreshing, rooms) {
|
|
246
|
+
const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', rooms === undefined ? {} : { rooms });
|
|
247
|
+
const synced = new Map(answer.rooms.map((room) => [`rooms/${room.room_ref}`, room.room_handle]));
|
|
248
|
+
const named = rooms === undefined ? null : new Set(rooms.map((room) => `rooms/${room}`));
|
|
249
|
+
const held = () => [...this.copies.values()].filter((copy) => named === null || named.has(copy.key));
|
|
250
|
+
return this.holding([...new Set([...held().map((copy) => copy.key), ...synced.keys()])].sort(), async () => {
|
|
251
|
+
const said = [];
|
|
252
|
+
let refused = false;
|
|
253
|
+
for (const copy of held()) {
|
|
254
|
+
const returned = synced.has(copy.key);
|
|
255
|
+
if (returned && copy.erasing === undefined)
|
|
256
|
+
continue;
|
|
257
|
+
try {
|
|
258
|
+
await this.erase(copy);
|
|
259
|
+
said.push(`${rootOf(copy.room_handle)}: ${returned ? '' : 'no longer synced to this Environment; '}erased from the Local copy`);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
refused = true;
|
|
263
|
+
said.push(`${rootOf(copy.room_handle)}: ${causeOf(error)}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const current = [];
|
|
267
|
+
for (const [key, handle] of synced) {
|
|
268
|
+
try {
|
|
269
|
+
const held = this.copies.get(key);
|
|
270
|
+
if (held?.erasing)
|
|
271
|
+
continue;
|
|
272
|
+
if (held === undefined || !(await exists(held.repository))) {
|
|
273
|
+
said.push(await this.materialize(key, handle));
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
current.push(held.room_handle === handle ? held : await this.save({ ...held, room_handle: handle }));
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
refused = true;
|
|
280
|
+
said.push(`${rootOf(handle)}: ${causeOf(error)}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (refreshing && current.length > 0) {
|
|
284
|
+
const failed = await this.refreshed(current).then(() => null, houseCause);
|
|
285
|
+
for (const copy of current)
|
|
286
|
+
said.push(...moved(copy, this.copies.get(copy.key)));
|
|
287
|
+
if (failed !== null) {
|
|
288
|
+
refused = true;
|
|
289
|
+
said.push(failed);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
|
|
293
|
+
});
|
|
224
294
|
}
|
|
225
295
|
async observed(report) {
|
|
226
296
|
const key = await this.keyAt(dirname(report.common));
|
|
@@ -228,6 +298,8 @@ export class LocalCopies {
|
|
|
228
298
|
return;
|
|
229
299
|
await this.exclusive(key, async () => {
|
|
230
300
|
const copy = this.copies.get(key);
|
|
301
|
+
if (copy === undefined || copy.erasing)
|
|
302
|
+
return;
|
|
231
303
|
let moves = copy.moves;
|
|
232
304
|
for (const { from, to } of report.moves) {
|
|
233
305
|
const held = moves.find((move) => move.worktree === report.worktree && move.to === from);
|
|
@@ -250,11 +322,11 @@ export class LocalCopies {
|
|
|
250
322
|
async push(cwd, selector, caller) {
|
|
251
323
|
const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
|
|
252
324
|
if (located.status !== 0)
|
|
253
|
-
return
|
|
325
|
+
return outside(cwd);
|
|
254
326
|
const [common, worktree] = located.stdout.toString('utf8').split('\n');
|
|
255
327
|
const key = await this.keyAt(dirname(common));
|
|
256
328
|
if (key === undefined)
|
|
257
|
-
return
|
|
329
|
+
return outside(worktree);
|
|
258
330
|
return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
|
|
259
331
|
}
|
|
260
332
|
async keyAt(repository) {
|
|
@@ -270,19 +342,12 @@ export class LocalCopies {
|
|
|
270
342
|
this.queues.set(key, queued.catch(() => undefined));
|
|
271
343
|
return queued;
|
|
272
344
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
this.delivering.add(key);
|
|
277
|
-
void this.exclusive(key, async () => {
|
|
278
|
-
this.delivering.delete(key);
|
|
279
|
-
const copy = this.copies.get(key);
|
|
280
|
-
if (copy.active)
|
|
281
|
-
await this.save(await this.caughtUp(copy));
|
|
282
|
-
}).catch(logged);
|
|
345
|
+
holding(keys, work) {
|
|
346
|
+
const [key, ...rest] = keys;
|
|
347
|
+
return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
|
|
283
348
|
}
|
|
284
349
|
async save(copy) {
|
|
285
|
-
const path =
|
|
350
|
+
const path = recordOf(copy.key);
|
|
286
351
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
287
352
|
const temporary = `${path}.${process.pid}`;
|
|
288
353
|
await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
|
|
@@ -290,6 +355,12 @@ export class LocalCopies {
|
|
|
290
355
|
this.copies.set(copy.key, copy);
|
|
291
356
|
return copy;
|
|
292
357
|
}
|
|
358
|
+
async erase(copy) {
|
|
359
|
+
await this.save({ ...copy, erasing: true });
|
|
360
|
+
await rm(copy.repository, { recursive: true, force: true });
|
|
361
|
+
await rm(recordOf(copy.key), { force: true });
|
|
362
|
+
this.copies.delete(copy.key);
|
|
363
|
+
}
|
|
293
364
|
async read(handle) {
|
|
294
365
|
const prefixes = [rootOf(handle)];
|
|
295
366
|
const discovery = await this.house.post('/kit/door/discover', { prefixes });
|
|
@@ -325,46 +396,13 @@ export class LocalCopies {
|
|
|
325
396
|
}
|
|
326
397
|
return read;
|
|
327
398
|
}
|
|
328
|
-
async
|
|
329
|
-
|
|
330
|
-
if (
|
|
331
|
-
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
if (copy.room_handle !== scope.handle)
|
|
335
|
-
copy = await this.save({ ...copy, room_handle: scope.handle });
|
|
336
|
-
if (!copy.active) {
|
|
337
|
-
await this.save(await this.received({ ...copy, active: true }));
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
const discovery = await this.house.post('/kit/door/discover', {
|
|
341
|
-
prefixes: [rootOf(copy.room_handle)],
|
|
342
|
-
});
|
|
343
|
-
const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
|
|
344
|
-
if (authority === undefined)
|
|
345
|
-
return;
|
|
346
|
-
if (authority.managed_replication !== 'admitted') {
|
|
347
|
-
logged(`operation_denied: ${rootOf(copy.room_handle)} is not synchronized to this Environment`);
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
if (JSON.stringify([authority.operations.write, authority.protected_paths]) !==
|
|
351
|
-
JSON.stringify([copy.write, copy.protected])) {
|
|
352
|
-
await this.save(await this.received(copy));
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
await this.save(await this.caughtUp(copy));
|
|
356
|
-
}
|
|
357
|
-
async materialize(scope) {
|
|
358
|
-
const read = await this.read(scope.handle);
|
|
359
|
-
if (read === null) {
|
|
360
|
-
logged(`House serves nothing of ${rootOf(scope.handle)} to copy`);
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
399
|
+
async materialize(key, handle) {
|
|
400
|
+
const read = await this.read(handle);
|
|
401
|
+
if (read === null)
|
|
402
|
+
throw new Error('House serves nothing of it to copy');
|
|
363
403
|
const repository = join(localCopies(), 'rooms', read.anchor);
|
|
364
|
-
if (await exists(repository))
|
|
365
|
-
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
404
|
+
if (await exists(repository))
|
|
405
|
+
throw new Error(`${repository} already exists and House Kit leaves it as it is`);
|
|
368
406
|
const staging = join(dirname(repository), `.${randomUUID()}`);
|
|
369
407
|
await mkdir(staging, { recursive: true });
|
|
370
408
|
let house;
|
|
@@ -372,7 +410,7 @@ export class LocalCopies {
|
|
|
372
410
|
try {
|
|
373
411
|
await git(staging, ['init', '-q', '-b', BRANCH]);
|
|
374
412
|
house = await importCommits(staging, [
|
|
375
|
-
{ message: `House ${rootOf(
|
|
413
|
+
{ message: `House ${rootOf(handle)}`, from: null, files: read.writable },
|
|
376
414
|
]);
|
|
377
415
|
await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
|
|
378
416
|
await git(staging, ['reset', '-q', '--hard']);
|
|
@@ -385,11 +423,10 @@ export class LocalCopies {
|
|
|
385
423
|
throw error;
|
|
386
424
|
}
|
|
387
425
|
await this.save({
|
|
388
|
-
key
|
|
426
|
+
key,
|
|
389
427
|
room_ref: read.anchor,
|
|
390
|
-
room_handle:
|
|
428
|
+
room_handle: handle,
|
|
391
429
|
repository,
|
|
392
|
-
active: true,
|
|
393
430
|
house,
|
|
394
431
|
position: read.position,
|
|
395
432
|
log_epoch: read.log_epoch,
|
|
@@ -399,6 +436,7 @@ export class LocalCopies {
|
|
|
399
436
|
manifests: { [house]: { parent: null, files: read.revisions } },
|
|
400
437
|
moves: [],
|
|
401
438
|
});
|
|
439
|
+
return `${rootOf(handle)}: copied to ${repository}`;
|
|
402
440
|
}
|
|
403
441
|
async received(copy) {
|
|
404
442
|
const read = await this.read(copy.room_handle);
|
|
@@ -422,29 +460,57 @@ export class LocalCopies {
|
|
|
422
460
|
await ignoreRules(copy.repository, [...read.writable.keys()], next);
|
|
423
461
|
return next;
|
|
424
462
|
}
|
|
425
|
-
async
|
|
426
|
-
|
|
427
|
-
|
|
463
|
+
async refreshed(copies) {
|
|
464
|
+
const discover = (group) => this.house.post('/kit/door/discover', { prefixes: group.map((copy) => rootOf(copy.room_handle)) });
|
|
465
|
+
const [first, ...rest] = copies;
|
|
466
|
+
const { bounds, authorities } = await discover([first]);
|
|
467
|
+
for (const group of grouped(rest, bounds.max_prefixes))
|
|
468
|
+
authorities.push(...(await discover(group)).authorities);
|
|
469
|
+
const unchanged = [];
|
|
470
|
+
for (const copy of copies) {
|
|
471
|
+
const authority = authorities.find((entry) => entry.room_ref === copy.room_ref);
|
|
472
|
+
if (authority !== undefined &&
|
|
473
|
+
JSON.stringify([authority.operations.write, authority.protected_paths]) !== JSON.stringify([copy.write, copy.protected])) {
|
|
474
|
+
await this.save(await this.received(copy));
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
unchanged.push(copy);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
for (const group of grouped(unchanged, bounds.max_prefixes))
|
|
481
|
+
await this.caughtUp(group, false);
|
|
482
|
+
}
|
|
483
|
+
async caughtUp(copies, whole) {
|
|
484
|
+
const caught = new Map(copies.map((copy) => [copy.key, copy]));
|
|
485
|
+
let asking = [...copies];
|
|
486
|
+
while (asking.length > 0) {
|
|
428
487
|
let answer;
|
|
429
488
|
try {
|
|
430
489
|
answer = await this.house.post('/kit/door/enumerate', {
|
|
431
|
-
prefixes:
|
|
432
|
-
positions:
|
|
490
|
+
prefixes: asking.map((copy) => rootOf(copy.room_handle)),
|
|
491
|
+
positions: asking.map((copy) => ({ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch })),
|
|
433
492
|
});
|
|
434
493
|
}
|
|
435
494
|
catch (error) {
|
|
436
|
-
const
|
|
437
|
-
if (
|
|
438
|
-
return this.received(copy);
|
|
439
|
-
if (code !== 'replica_behind')
|
|
495
|
+
const expired = asking.find((copy) => copy.room_ref === expiredAuthority(error));
|
|
496
|
+
if (expired === undefined)
|
|
440
497
|
throw error;
|
|
441
|
-
|
|
498
|
+
caught.set(expired.key, await this.save(await this.received(expired)));
|
|
499
|
+
asking = asking.filter((copy) => copy !== expired);
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
const advanced = [];
|
|
503
|
+
for (const copy of asking) {
|
|
504
|
+
const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
|
|
505
|
+
if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
|
|
506
|
+
continue;
|
|
507
|
+
const next = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
|
|
508
|
+
caught.set(copy.key, next);
|
|
509
|
+
advanced.push(next);
|
|
442
510
|
}
|
|
443
|
-
|
|
444
|
-
if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
|
|
445
|
-
return copy;
|
|
446
|
-
copy = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
|
|
511
|
+
asking = whole ? advanced : [];
|
|
447
512
|
}
|
|
513
|
+
return copies.map((copy) => caught.get(copy.key));
|
|
448
514
|
}
|
|
449
515
|
async incorporated(copy, changes, position, logEpoch) {
|
|
450
516
|
const houseTree = await tree(copy.repository, copy.house);
|
|
@@ -494,6 +560,8 @@ export class LocalCopies {
|
|
|
494
560
|
}
|
|
495
561
|
async pushed(key, worktree, selector, caller) {
|
|
496
562
|
let copy = this.copies.get(key);
|
|
563
|
+
if (copy === undefined || copy.erasing)
|
|
564
|
+
return outside(worktree);
|
|
497
565
|
if (copy.push?.state === 'submitted')
|
|
498
566
|
return this.settle(copy, caller);
|
|
499
567
|
const selected = await run(worktree, ['rev-parse', '--verify', '--quiet', '--end-of-options', `${selector}^{commit}`]);
|
|
@@ -507,9 +575,6 @@ export class LocalCopies {
|
|
|
507
575
|
}
|
|
508
576
|
copy = await this.save(without(copy));
|
|
509
577
|
}
|
|
510
|
-
if (!copy.active) {
|
|
511
|
-
return { refused: true, text: `${rootOf(copy.room_handle)} is no longer synced to this Environment, so House takes no push from it` };
|
|
512
|
-
}
|
|
513
578
|
const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
|
|
514
579
|
const base = listed.find((ancestor) => ancestor in copy.manifests);
|
|
515
580
|
if (base === undefined) {
|
|
@@ -520,7 +585,7 @@ export class LocalCopies {
|
|
|
520
585
|
changes = await this.derived(copy, base, commit);
|
|
521
586
|
}
|
|
522
587
|
catch (error) {
|
|
523
|
-
return { refused: true, text:
|
|
588
|
+
return { refused: true, text: houseCause(error) };
|
|
524
589
|
}
|
|
525
590
|
if (changes.length === 0)
|
|
526
591
|
return { refused: false, text: `Nothing to submit: ${short(commit)} matches House.` };
|
|
@@ -547,7 +612,18 @@ export class LocalCopies {
|
|
|
547
612
|
const after = await tree(copy.repository, commit);
|
|
548
613
|
const moved = movedPaths(copy.moves, before, after);
|
|
549
614
|
const arrived = new Set(moved.values());
|
|
550
|
-
const
|
|
615
|
+
const arriving = [...after].filter(([path, entry]) => before.get(path) !== entry);
|
|
616
|
+
if (arriving.length > 0) {
|
|
617
|
+
const { bounds } = await this.house.post('/kit/door/discover', { prefixes: [root] });
|
|
618
|
+
const binary = [];
|
|
619
|
+
for (const [path, entry] of arriving) {
|
|
620
|
+
if (!(await isText(copy.repository, entry, bounds.max_authored_file_bytes)))
|
|
621
|
+
binary.push(path);
|
|
622
|
+
}
|
|
623
|
+
if (binary.length > 0)
|
|
624
|
+
throw new Error(textOnly(binary));
|
|
625
|
+
}
|
|
626
|
+
const text = (entry) => blobText(copy.repository, entry);
|
|
551
627
|
const changes = [];
|
|
552
628
|
for (const path of [...new Set([...before.keys(), ...after.keys()])].sort()) {
|
|
553
629
|
if (arrived.has(path))
|
|
@@ -560,7 +636,7 @@ export class LocalCopies {
|
|
|
560
636
|
changes.push({ op: 'rename', path: `${root}/${path}`, to: `${root}/${to}`, base: known });
|
|
561
637
|
const moving = after.get(to);
|
|
562
638
|
if (moving !== prior) {
|
|
563
|
-
changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving
|
|
639
|
+
changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving) });
|
|
564
640
|
}
|
|
565
641
|
continue;
|
|
566
642
|
}
|
|
@@ -569,9 +645,9 @@ export class LocalCopies {
|
|
|
569
645
|
if (next === undefined)
|
|
570
646
|
changes.push({ op: 'remove', path: `${root}/${path}`, base: known });
|
|
571
647
|
else if (prior === undefined)
|
|
572
|
-
changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next
|
|
648
|
+
changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next) });
|
|
573
649
|
else
|
|
574
|
-
changes.push(...changed(`${root}/${path}`, path, known, await text(prior
|
|
650
|
+
changes.push(...changed(`${root}/${path}`, path, known, await text(prior), await text(next)));
|
|
575
651
|
}
|
|
576
652
|
return changes;
|
|
577
653
|
}
|
|
@@ -598,12 +674,12 @@ export class LocalCopies {
|
|
|
598
674
|
const { upload, ...unstaged } = copy.push;
|
|
599
675
|
return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
|
|
600
676
|
}
|
|
601
|
-
if (outcome.refused.code === '
|
|
677
|
+
if (outcome.refused.code === 'expired')
|
|
602
678
|
return this.recovered(copy);
|
|
603
679
|
const rebased = rebaseFixes(outcome.refused);
|
|
604
680
|
let settled = await this.save(without(copy));
|
|
605
681
|
try {
|
|
606
|
-
settled =
|
|
682
|
+
settled = (await this.caughtUp([settled], true))[0];
|
|
607
683
|
}
|
|
608
684
|
catch {
|
|
609
685
|
return {
|
|
@@ -622,7 +698,7 @@ export class LocalCopies {
|
|
|
622
698
|
}
|
|
623
699
|
async recovered(copy) {
|
|
624
700
|
const push = copy.push;
|
|
625
|
-
const current =
|
|
701
|
+
const current = (await this.caughtUp([copy], true))[0];
|
|
626
702
|
const house = await tree(copy.repository, current.house);
|
|
627
703
|
const root = rootOf(current.room_handle);
|
|
628
704
|
if (!(await this.landed(copy.repository, push.changes, house, await tree(copy.repository, push.commit)))) {
|
|
@@ -638,7 +714,7 @@ export class LocalCopies {
|
|
|
638
714
|
written.push({
|
|
639
715
|
path: `${root}/${path}`,
|
|
640
716
|
revision: entry === undefined ? null : (revisionAt(current, current.house, path) ?? null),
|
|
641
|
-
content: entry === undefined ? null : await blobText(copy.repository, entry
|
|
717
|
+
content: entry === undefined ? null : await blobText(copy.repository, entry),
|
|
642
718
|
});
|
|
643
719
|
}
|
|
644
720
|
return this.finished(await this.save({ ...current, push: { ...push, state: 'accepted', written } }));
|
|
@@ -666,7 +742,7 @@ export class LocalCopies {
|
|
|
666
742
|
const push = copy.push;
|
|
667
743
|
let written = push.written;
|
|
668
744
|
if (push.changes.some((change) => change.op === 'rename')) {
|
|
669
|
-
copy =
|
|
745
|
+
copy = (await this.caughtUp([copy], true))[0];
|
|
670
746
|
const house = await tree(copy.repository, copy.house);
|
|
671
747
|
const sent = await tree(copy.repository, push.house);
|
|
672
748
|
if (push.changes.some((change) => change.op === 'rename' && !house.has(relative(change.to)))) {
|
|
@@ -682,7 +758,7 @@ export class LocalCopies {
|
|
|
682
758
|
{
|
|
683
759
|
path: `${rootOf(copy.room_handle)}/${path}`,
|
|
684
760
|
revision: entry === undefined ? null : (revisionAt(copy, copy.house, path) ?? null),
|
|
685
|
-
content: entry === undefined ? null : await blobText(copy.repository, entry
|
|
761
|
+
content: entry === undefined ? null : await blobText(copy.repository, entry),
|
|
686
762
|
},
|
|
687
763
|
];
|
|
688
764
|
}
|
|
@@ -727,7 +803,7 @@ export class LocalCopies {
|
|
|
727
803
|
async landed(repository, changes, house, selected) {
|
|
728
804
|
const text = async (path) => {
|
|
729
805
|
const entry = house.get(relative(path));
|
|
730
|
-
return entry === undefined ? undefined : blobText(repository, entry
|
|
806
|
+
return entry === undefined ? undefined : blobText(repository, entry);
|
|
731
807
|
};
|
|
732
808
|
for (const change of changes) {
|
|
733
809
|
const current = await text(change.path);
|
package/dist/end.js
CHANGED
|
@@ -1,8 +1,27 @@
|
|
|
1
|
+
import { KIT_VERSION } from './clis.js';
|
|
2
|
+
import { forgetRejected } from './home.js';
|
|
3
|
+
import { installCommand } from './placement.js';
|
|
1
4
|
import { killDescendants } from './scope.js';
|
|
2
5
|
export const REPLACED = 'another Kit started for this Environment';
|
|
3
|
-
|
|
4
|
-
export function end(reason) {
|
|
6
|
+
function exit(reason, status) {
|
|
5
7
|
process.stderr.write(`kit: ${reason}\n`);
|
|
6
8
|
killDescendants();
|
|
7
|
-
process.exit(
|
|
9
|
+
process.exit(status);
|
|
10
|
+
}
|
|
11
|
+
export function end(reason) {
|
|
12
|
+
exit(reason, process.env.HOUSE_KIT_RESIDENT === String(process.pid) ? 0 : 1);
|
|
13
|
+
}
|
|
14
|
+
export function restart(reason) {
|
|
15
|
+
exit(reason, 1);
|
|
16
|
+
}
|
|
17
|
+
export function updateRequired() {
|
|
18
|
+
const command = installCommand();
|
|
19
|
+
end(command === null ? 'This Kit is too old for House; install it again.' : `This Kit is too old for House. Update it: ${command}`);
|
|
20
|
+
}
|
|
21
|
+
export function newerThanHouse(release, command) {
|
|
22
|
+
end(`Kit ${KIT_VERSION} is newer than this Building supports (Kit ${release}). Install Kit ${release}: ${command}`);
|
|
23
|
+
}
|
|
24
|
+
export async function signInAgain(rejected) {
|
|
25
|
+
await forgetRejected(rejected);
|
|
26
|
+
restart('sign in again');
|
|
8
27
|
}
|
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
|
+
});
|
package/dist/events.js
CHANGED
|
@@ -512,6 +512,10 @@ class GrokReader extends Reader {
|
|
|
512
512
|
return [];
|
|
513
513
|
}
|
|
514
514
|
}
|
|
515
|
+
export function answeredFailure(answer) {
|
|
516
|
+
const failure = answer._meta?.jetbrains?.air?.sessionFailure;
|
|
517
|
+
return typeof failure?.title === 'string' ? { title: failure.title, limit: failure.category === 'limit' } : null;
|
|
518
|
+
}
|
|
515
519
|
export function readerFor(kind) {
|
|
516
520
|
if (kind === 'claude-agent-acp')
|
|
517
521
|
return new ClaudeReader();
|
package/dist/files.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
4
|
import { HouseRefusal, retrying } from './api.js';
|
|
4
5
|
import { relayed } from './relay.js';
|
|
6
|
+
export function verified(bytes, sha256, name) {
|
|
7
|
+
if (createHash('sha256').update(bytes).digest('hex') !== sha256)
|
|
8
|
+
throw new Error(`${name} did not arrive whole; download it again`);
|
|
9
|
+
return bytes;
|
|
10
|
+
}
|
|
5
11
|
export async function placeFiles(house, directory, files, signal) {
|
|
6
12
|
const paths = [];
|
|
7
13
|
for (const file of files) {
|
|
8
|
-
const original = await house.post('/kit/conversation/originals/get', { version: file.version, download: true }, signal);
|
|
14
|
+
const original = await retrying(() => house.post('/kit/conversation/originals/get', { version: file.version, download: true }, signal), signal);
|
|
9
15
|
const answer = await fetch(relayed(original.download.url), { signal });
|
|
10
16
|
if (!answer.ok)
|
|
11
17
|
throw new Error(`${file.name} did not download: ${answer.status}`);
|
|
@@ -16,7 +22,7 @@ export async function placeFiles(house, directory, files, signal) {
|
|
|
16
22
|
});
|
|
17
23
|
await mkdir(folder, { recursive: true });
|
|
18
24
|
const path = join(folder, file.name);
|
|
19
|
-
await writeFile(path, Buffer.from(await answer.arrayBuffer()));
|
|
25
|
+
await writeFile(path, verified(Buffer.from(await answer.arrayBuffer()), file.sha256, file.name));
|
|
20
26
|
paths.push(path);
|
|
21
27
|
}
|
|
22
28
|
return paths;
|
package/dist/git.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { isUtf8 } from 'node:buffer';
|
|
2
3
|
import { createHash } from 'node:crypto';
|
|
3
4
|
import { join } from 'node:path';
|
|
4
5
|
import { withoutProxy } from './clis.js';
|
|
@@ -48,14 +49,15 @@ export async function tree(repository, commit) {
|
|
|
48
49
|
}
|
|
49
50
|
return entries;
|
|
50
51
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
async function blob(repository, object) {
|
|
53
|
+
return (await run(repository, ['cat-file', 'blob', object])).stdout;
|
|
54
|
+
}
|
|
55
|
+
export async function blobText(repository, object) {
|
|
56
|
+
return (await blob(repository, object)).toString('utf8');
|
|
57
|
+
}
|
|
58
|
+
export async function isText(repository, object, maxBytes) {
|
|
59
|
+
const bytes = await blob(repository, object);
|
|
60
|
+
return bytes.length <= maxBytes && isUtf8(bytes) && !bytes.includes(0);
|
|
59
61
|
}
|
|
60
62
|
export function blobId(content) {
|
|
61
63
|
const bytes = Buffer.from(content, 'utf8');
|