@agentshouse/kit 0.2.1-cf110d15f69c8 → 0.2.1-cf268dff1869b
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 +38 -37
- package/dist/acp.js +90 -19
- package/dist/agents.js +155 -45
- package/dist/api.js +5 -1
- package/dist/app-git.js +10 -26
- package/dist/authority-main.js +2 -2
- package/dist/bridge.js +4 -7
- package/dist/building.js +50 -0
- package/dist/calls.js +71 -0
- package/dist/chatgpt.js +32 -0
- package/dist/clis.js +52 -42
- package/dist/connection.js +105 -0
- package/dist/conversations.js +526 -175
- package/dist/copies.js +89 -52
- package/dist/end.js +17 -2
- package/dist/events.js +33 -8
- package/dist/files.js +139 -31
- package/dist/git.js +13 -11
- package/dist/home.js +56 -12
- package/dist/house-cli.js +299 -0
- package/dist/house-main.js +7 -206
- package/dist/instructions.js +7 -11
- package/dist/kit-cli.js +68 -0
- package/dist/kit-main.js +8 -22
- package/dist/login.js +49 -31
- package/dist/logout.js +0 -2
- package/dist/mcp.js +6 -2
- package/dist/placement.js +31 -0
- package/dist/relay.js +22 -1
- package/dist/resident.js +97 -38
- package/dist/scope.js +15 -4
- package/dist/scripts.js +255 -0
- package/dist/service.js +23 -1
- package/dist/sign-in.js +22 -39
- package/dist/silence.js +28 -0
- package/dist/skills.js +49 -35
- package/dist/transcription.js +85 -45
- package/dist/translate.js +2 -8
- package/dist/update.js +113 -105
- package/dist/wire.js +40 -0
- package/dist/work.js +3 -5
- package/package.json +20 -20
- package/dist/bootstrap.json +0 -24
- package/dist/clis-main.js +0 -23
- package/dist/stream.js +0 -74
- package/how-we-work.md +0 -23
- package/skills/ask-matt/LICENSE +0 -21
- package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
- package/skills/ask-matt/SKILL.md +0 -91
- package/skills/ask-matt/agents/openai.yaml +0 -4
- 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 -116
- package/skills/codebase-design/agents/openai.yaml +0 -3
- package/skills/diagnosing-bugs/LICENSE +0 -21
- package/skills/diagnosing-bugs/SKILL.md +0 -140
- 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/CONTEXT-FORMAT.md +0 -60
- package/skills/domain-modeling/LICENSE +0 -21
- package/skills/domain-modeling/SKILL.md +0 -76
- package/skills/domain-modeling/agents/openai.yaml +0 -3
- package/skills/grill-me/LICENSE +0 -21
- package/skills/grill-me/SKILL.md +0 -8
- package/skills/grill-me/agents/openai.yaml +0 -4
- package/skills/grill-with-docs/LICENSE +0 -21
- package/skills/grill-with-docs/SKILL.md +0 -8
- package/skills/grill-with-docs/agents/openai.yaml +0 -4
- 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 -17
- package/skills/handoff/agents/openai.yaml +0 -4
- package/skills/implement/LICENSE +0 -21
- package/skills/implement/SKILL.md +0 -16
- package/skills/implement/agents/openai.yaml +0 -4
- 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 -72
- package/skills/improve-codebase-architecture/agents/openai.yaml +0 -4
- package/skills/prototype/LICENSE +0 -21
- package/skills/prototype/LOGIC.md +0 -67
- package/skills/prototype/SKILL.md +0 -28
- 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 -14
- package/skills/research/agents/openai.yaml +0 -3
- package/skills/resolving-merge-conflicts/LICENSE +0 -21
- package/skills/resolving-merge-conflicts/SKILL.md +0 -16
- package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
- package/skills/tdd/LICENSE +0 -21
- package/skills/tdd/SKILL.md +0 -40
- 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 -141
- package/skills/teach/agents/openai.yaml +0 -4
- package/skills/to-questionnaire/LICENSE +0 -21
- package/skills/to-questionnaire/SKILL.md +0 -55
- package/skills/to-questionnaire/agents/openai.yaml +0 -4
- package/skills/to-spec/LICENSE +0 -21
- package/skills/to-spec/SKILL.md +0 -76
- package/skills/to-spec/agents/openai.yaml +0 -4
- package/skills/to-tickets/LICENSE +0 -21
- package/skills/to-tickets/SKILL.md +0 -106
- package/skills/to-tickets/agents/openai.yaml +0 -4
- package/skills/wait-what/LICENSE +0 -21
- package/skills/wait-what/SKILL.md +0 -8
- package/skills/wait-what/agents/openai.yaml +0 -4
- package/skills/wayfinder/LICENSE +0 -21
- package/skills/wayfinder/SKILL.md +0 -129
- package/skills/wayfinder/agents/openai.yaml +0 -4
- 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 -83
- package/skills/writing-for-agents/agents/openai.yaml +0 -3
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';
|
|
@@ -11,10 +11,13 @@ import { changed } from './translate.js';
|
|
|
11
11
|
import { relayed } from './relay.js';
|
|
12
12
|
const BRANCH = 'main';
|
|
13
13
|
const PROVENANCE = '_provenance';
|
|
14
|
-
const IDENTITY = { GIT_COMMITTER_NAME: '
|
|
14
|
+
const IDENTITY = { GIT_COMMITTER_NAME: 'Kit', GIT_COMMITTER_EMAIL: 'house-kit@localhost' };
|
|
15
15
|
function causeOf(error) {
|
|
16
16
|
return error instanceof Error ? error.message : String(error);
|
|
17
17
|
}
|
|
18
|
+
function houseCause(error) {
|
|
19
|
+
return error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
|
|
20
|
+
}
|
|
18
21
|
function logged(error) {
|
|
19
22
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
20
23
|
}
|
|
@@ -29,6 +32,13 @@ 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
|
}
|
|
@@ -48,6 +58,9 @@ function expiredAuthority(error) {
|
|
|
48
58
|
return undefined;
|
|
49
59
|
}
|
|
50
60
|
}
|
|
61
|
+
function grouped(items, size) {
|
|
62
|
+
return items.flatMap((_, index) => (index % size === 0 ? [items.slice(index, index + size)] : []));
|
|
63
|
+
}
|
|
51
64
|
function covers(path, prefix) {
|
|
52
65
|
return prefix === '' || path === prefix || path.startsWith(`${prefix}/`);
|
|
53
66
|
}
|
|
@@ -56,6 +69,9 @@ function sidecar(path) {
|
|
|
56
69
|
segments.splice(-1, 0, PROVENANCE);
|
|
57
70
|
return segments.join('/');
|
|
58
71
|
}
|
|
72
|
+
function recordOf(key) {
|
|
73
|
+
return join(kitHome(), 'local-copies', `${key}.json`);
|
|
74
|
+
}
|
|
59
75
|
function rootOf(handle) {
|
|
60
76
|
return `/rooms/${handle}`;
|
|
61
77
|
}
|
|
@@ -117,7 +133,7 @@ async function ignoreRules(repository, writable, copy) {
|
|
|
117
133
|
].map(pattern);
|
|
118
134
|
const granted = readOnly.filter((path) => !protectedPath(path)).map(pattern);
|
|
119
135
|
await ruled(join(repository, '.gitignore'), [
|
|
120
|
-
'#
|
|
136
|
+
'# Kit writes this file and .ignore. The paths below are read-only House',
|
|
121
137
|
'# content: Git does not track them, and House refuses a change to them even',
|
|
122
138
|
'# when this file is edited or a path is force-added.',
|
|
123
139
|
'/.gitignore',
|
|
@@ -132,7 +148,7 @@ async function ignoreRules(repository, writable, copy) {
|
|
|
132
148
|
'',
|
|
133
149
|
].join('\n'));
|
|
134
150
|
await ruled(join(repository, '.ignore'), [
|
|
135
|
-
'#
|
|
151
|
+
'# Kit writes this file so native search still reads the read-only House',
|
|
136
152
|
'# content that .gitignore keeps out of Git.',
|
|
137
153
|
`!${PROVENANCE}`,
|
|
138
154
|
...[...house, ...granted].map((rule) => `!${rule}`),
|
|
@@ -202,34 +218,45 @@ export class LocalCopies {
|
|
|
202
218
|
async mapping() {
|
|
203
219
|
await this.answering;
|
|
204
220
|
return [...this.copies.values()]
|
|
205
|
-
.filter((copy) => copy.
|
|
221
|
+
.filter((copy) => copy.unfollowed === undefined)
|
|
206
222
|
.map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
|
|
207
223
|
.sort();
|
|
208
224
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
225
|
+
connected() {
|
|
226
|
+
return this.selected();
|
|
227
|
+
}
|
|
228
|
+
async changed(rooms) {
|
|
229
|
+
if (rooms.length > 0)
|
|
230
|
+
await this.selected(rooms);
|
|
213
231
|
}
|
|
214
232
|
fetch() {
|
|
215
233
|
return this.answered(() => this.fetched(true));
|
|
216
234
|
}
|
|
235
|
+
async selected(rooms) {
|
|
236
|
+
const copied = await this.answered(() => this.fetched(false, rooms));
|
|
237
|
+
if (copied.refused)
|
|
238
|
+
logged(copied.text);
|
|
239
|
+
}
|
|
217
240
|
answered(work) {
|
|
218
241
|
const answered = this.answering.then(work);
|
|
219
242
|
this.answering = answered.catch(() => undefined);
|
|
220
243
|
return answered;
|
|
221
244
|
}
|
|
222
|
-
async fetched(refreshing) {
|
|
223
|
-
const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', {});
|
|
245
|
+
async fetched(refreshing, rooms) {
|
|
246
|
+
const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', rooms === undefined ? {} : { rooms });
|
|
224
247
|
const synced = new Map(answer.rooms.map((room) => [`rooms/${room.room_ref}`, room.room_handle]));
|
|
225
|
-
|
|
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 () => {
|
|
226
251
|
const said = [];
|
|
227
252
|
let refused = false;
|
|
228
|
-
|
|
229
|
-
|
|
253
|
+
const mark = refreshing ? 'said' : 'unsaid';
|
|
254
|
+
for (const copy of held()) {
|
|
255
|
+
if (synced.has(copy.key) || copy.unfollowed === 'said' || copy.unfollowed === mark)
|
|
230
256
|
continue;
|
|
231
|
-
await this.save({ ...copy,
|
|
232
|
-
|
|
257
|
+
await this.save({ ...copy, unfollowed: mark });
|
|
258
|
+
if (refreshing)
|
|
259
|
+
said.push(`${rootOf(copy.room_handle)}: no longer followed; kept as it is`);
|
|
233
260
|
}
|
|
234
261
|
const current = [];
|
|
235
262
|
for (const [key, handle] of synced) {
|
|
@@ -239,11 +266,8 @@ export class LocalCopies {
|
|
|
239
266
|
said.push(await this.materialize(key, handle));
|
|
240
267
|
continue;
|
|
241
268
|
}
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
current.push(copy);
|
|
245
|
-
else
|
|
246
|
-
said.push(...moved(copy, await this.save(await this.received({ ...copy, active: true }))));
|
|
269
|
+
const { unfollowed, ...followed } = held;
|
|
270
|
+
current.push(unfollowed === undefined && held.room_handle === handle ? held : await this.save({ ...followed, room_handle: handle }));
|
|
247
271
|
}
|
|
248
272
|
catch (error) {
|
|
249
273
|
refused = true;
|
|
@@ -251,13 +275,12 @@ export class LocalCopies {
|
|
|
251
275
|
}
|
|
252
276
|
}
|
|
253
277
|
if (refreshing && current.length > 0) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
catch (error) {
|
|
278
|
+
const failed = await this.refreshed(current).then(() => null, houseCause);
|
|
279
|
+
for (const copy of current)
|
|
280
|
+
said.push(...moved(copy, this.copies.get(copy.key)));
|
|
281
|
+
if (failed !== null) {
|
|
259
282
|
refused = true;
|
|
260
|
-
said.push(
|
|
283
|
+
said.push(failed);
|
|
261
284
|
}
|
|
262
285
|
}
|
|
263
286
|
return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
|
|
@@ -291,11 +314,11 @@ export class LocalCopies {
|
|
|
291
314
|
async push(cwd, selector, caller) {
|
|
292
315
|
const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
|
|
293
316
|
if (located.status !== 0)
|
|
294
|
-
return
|
|
317
|
+
return outside(cwd);
|
|
295
318
|
const [common, worktree] = located.stdout.toString('utf8').split('\n');
|
|
296
319
|
const key = await this.keyAt(dirname(common));
|
|
297
320
|
if (key === undefined)
|
|
298
|
-
return
|
|
321
|
+
return outside(worktree);
|
|
299
322
|
return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
|
|
300
323
|
}
|
|
301
324
|
async keyAt(repository) {
|
|
@@ -316,7 +339,7 @@ export class LocalCopies {
|
|
|
316
339
|
return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
|
|
317
340
|
}
|
|
318
341
|
async save(copy) {
|
|
319
|
-
const path =
|
|
342
|
+
const path = recordOf(copy.key);
|
|
320
343
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
321
344
|
const temporary = `${path}.${process.pid}`;
|
|
322
345
|
await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
|
|
@@ -365,7 +388,7 @@ export class LocalCopies {
|
|
|
365
388
|
throw new Error('House serves nothing of it to copy');
|
|
366
389
|
const repository = join(localCopies(), 'rooms', read.anchor);
|
|
367
390
|
if (await exists(repository))
|
|
368
|
-
throw new Error(`${repository} already exists and
|
|
391
|
+
throw new Error(`${repository} already exists and Kit leaves it as it is`);
|
|
369
392
|
const staging = join(dirname(repository), `.${randomUUID()}`);
|
|
370
393
|
await mkdir(staging, { recursive: true });
|
|
371
394
|
let house;
|
|
@@ -390,7 +413,6 @@ export class LocalCopies {
|
|
|
390
413
|
room_ref: read.anchor,
|
|
391
414
|
room_handle: handle,
|
|
392
415
|
repository,
|
|
393
|
-
active: true,
|
|
394
416
|
house,
|
|
395
417
|
position: read.position,
|
|
396
418
|
log_epoch: read.log_epoch,
|
|
@@ -425,19 +447,24 @@ export class LocalCopies {
|
|
|
425
447
|
return next;
|
|
426
448
|
}
|
|
427
449
|
async refreshed(copies) {
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
});
|
|
431
|
-
const
|
|
450
|
+
const discover = (group) => this.house.post('/kit/door/discover', { prefixes: group.map((copy) => rootOf(copy.room_handle)) });
|
|
451
|
+
const [first, ...rest] = copies;
|
|
452
|
+
const { bounds, authorities } = await discover([first]);
|
|
453
|
+
for (const group of grouped(rest, bounds.max_prefixes))
|
|
454
|
+
authorities.push(...(await discover(group)).authorities);
|
|
455
|
+
const unchanged = [];
|
|
432
456
|
for (const copy of copies) {
|
|
433
|
-
const authority =
|
|
457
|
+
const authority = authorities.find((entry) => entry.room_ref === copy.room_ref);
|
|
434
458
|
if (authority !== undefined &&
|
|
435
459
|
JSON.stringify([authority.operations.write, authority.protected_paths]) !== JSON.stringify([copy.write, copy.protected])) {
|
|
436
|
-
|
|
460
|
+
await this.save(await this.received(copy));
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
unchanged.push(copy);
|
|
437
464
|
}
|
|
438
465
|
}
|
|
439
|
-
const
|
|
440
|
-
|
|
466
|
+
for (const group of grouped(unchanged, bounds.max_prefixes))
|
|
467
|
+
await this.caughtUp(group, false);
|
|
441
468
|
}
|
|
442
469
|
async caughtUp(copies, whole) {
|
|
443
470
|
const caught = new Map(copies.map((copy) => [copy.key, copy]));
|
|
@@ -532,9 +559,6 @@ export class LocalCopies {
|
|
|
532
559
|
}
|
|
533
560
|
copy = await this.save(without(copy));
|
|
534
561
|
}
|
|
535
|
-
if (!copy.active) {
|
|
536
|
-
return { refused: true, text: `${rootOf(copy.room_handle)} is no longer synced to this Environment, so House takes no push from it` };
|
|
537
|
-
}
|
|
538
562
|
const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
|
|
539
563
|
const base = listed.find((ancestor) => ancestor in copy.manifests);
|
|
540
564
|
if (base === undefined) {
|
|
@@ -545,7 +569,7 @@ export class LocalCopies {
|
|
|
545
569
|
changes = await this.derived(copy, base, commit);
|
|
546
570
|
}
|
|
547
571
|
catch (error) {
|
|
548
|
-
return { refused: true, text:
|
|
572
|
+
return { refused: true, text: houseCause(error) };
|
|
549
573
|
}
|
|
550
574
|
if (changes.length === 0)
|
|
551
575
|
return { refused: false, text: `Nothing to submit: ${short(commit)} matches House.` };
|
|
@@ -572,7 +596,18 @@ export class LocalCopies {
|
|
|
572
596
|
const after = await tree(copy.repository, commit);
|
|
573
597
|
const moved = movedPaths(copy.moves, before, after);
|
|
574
598
|
const arrived = new Set(moved.values());
|
|
575
|
-
const
|
|
599
|
+
const arriving = [...after].filter(([path, entry]) => before.get(path) !== entry);
|
|
600
|
+
if (arriving.length > 0) {
|
|
601
|
+
const { bounds } = await this.house.post('/kit/door/discover', { prefixes: [root] });
|
|
602
|
+
const binary = [];
|
|
603
|
+
for (const [path, entry] of arriving) {
|
|
604
|
+
if (!(await isText(copy.repository, entry, bounds.max_authored_file_bytes)))
|
|
605
|
+
binary.push(path);
|
|
606
|
+
}
|
|
607
|
+
if (binary.length > 0)
|
|
608
|
+
throw new Error(textOnly(binary));
|
|
609
|
+
}
|
|
610
|
+
const text = (entry) => blobText(copy.repository, entry);
|
|
576
611
|
const changes = [];
|
|
577
612
|
for (const path of [...new Set([...before.keys(), ...after.keys()])].sort()) {
|
|
578
613
|
if (arrived.has(path))
|
|
@@ -585,7 +620,7 @@ export class LocalCopies {
|
|
|
585
620
|
changes.push({ op: 'rename', path: `${root}/${path}`, to: `${root}/${to}`, base: known });
|
|
586
621
|
const moving = after.get(to);
|
|
587
622
|
if (moving !== prior) {
|
|
588
|
-
changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving
|
|
623
|
+
changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving) });
|
|
589
624
|
}
|
|
590
625
|
continue;
|
|
591
626
|
}
|
|
@@ -594,9 +629,9 @@ export class LocalCopies {
|
|
|
594
629
|
if (next === undefined)
|
|
595
630
|
changes.push({ op: 'remove', path: `${root}/${path}`, base: known });
|
|
596
631
|
else if (prior === undefined)
|
|
597
|
-
changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next
|
|
632
|
+
changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next) });
|
|
598
633
|
else
|
|
599
|
-
changes.push(...changed(`${root}/${path}`, path, known, await text(prior
|
|
634
|
+
changes.push(...changed(`${root}/${path}`, path, known, await text(prior), await text(next)));
|
|
600
635
|
}
|
|
601
636
|
return changes;
|
|
602
637
|
}
|
|
@@ -625,8 +660,10 @@ export class LocalCopies {
|
|
|
625
660
|
}
|
|
626
661
|
if (outcome.refused.code === 'expired')
|
|
627
662
|
return this.recovered(copy);
|
|
628
|
-
const rebased = rebaseFixes(outcome.refused);
|
|
629
663
|
let settled = await this.save(without(copy));
|
|
664
|
+
if (settled.unfollowed !== undefined)
|
|
665
|
+
return { refused: true, text: outcome.refused.text };
|
|
666
|
+
const rebased = rebaseFixes(outcome.refused);
|
|
630
667
|
try {
|
|
631
668
|
settled = (await this.caughtUp([settled], true))[0];
|
|
632
669
|
}
|
|
@@ -663,7 +700,7 @@ export class LocalCopies {
|
|
|
663
700
|
written.push({
|
|
664
701
|
path: `${root}/${path}`,
|
|
665
702
|
revision: entry === undefined ? null : (revisionAt(current, current.house, path) ?? null),
|
|
666
|
-
content: entry === undefined ? null : await blobText(copy.repository, entry
|
|
703
|
+
content: entry === undefined ? null : await blobText(copy.repository, entry),
|
|
667
704
|
});
|
|
668
705
|
}
|
|
669
706
|
return this.finished(await this.save({ ...current, push: { ...push, state: 'accepted', written } }));
|
|
@@ -707,7 +744,7 @@ export class LocalCopies {
|
|
|
707
744
|
{
|
|
708
745
|
path: `${rootOf(copy.room_handle)}/${path}`,
|
|
709
746
|
revision: entry === undefined ? null : (revisionAt(copy, copy.house, path) ?? null),
|
|
710
|
-
content: entry === undefined ? null : await blobText(copy.repository, entry
|
|
747
|
+
content: entry === undefined ? null : await blobText(copy.repository, entry),
|
|
711
748
|
},
|
|
712
749
|
];
|
|
713
750
|
}
|
|
@@ -752,7 +789,7 @@ export class LocalCopies {
|
|
|
752
789
|
async landed(repository, changes, house, selected) {
|
|
753
790
|
const text = async (path) => {
|
|
754
791
|
const entry = house.get(relative(path));
|
|
755
|
-
return entry === undefined ? undefined : blobText(repository, entry
|
|
792
|
+
return entry === undefined ? undefined : blobText(repository, entry);
|
|
756
793
|
};
|
|
757
794
|
for (const change of changes) {
|
|
758
795
|
const current = await text(change.path);
|
package/dist/end.js
CHANGED
|
@@ -1,17 +1,32 @@
|
|
|
1
|
+
import { KIT_RELEASE } from './clis.js';
|
|
1
2
|
import { forgetRejected } from './home.js';
|
|
3
|
+
import { installCommand } from './placement.js';
|
|
2
4
|
import { killDescendants } from './scope.js';
|
|
3
|
-
export const REPLACED = 'another Kit started for this Environment';
|
|
5
|
+
export const REPLACED = 'another Kit process started for this Environment';
|
|
4
6
|
function exit(reason, status) {
|
|
5
7
|
process.stderr.write(`kit: ${reason}\n`);
|
|
6
8
|
killDescendants();
|
|
7
9
|
process.exit(status);
|
|
8
10
|
}
|
|
9
11
|
export function end(reason) {
|
|
10
|
-
exit(reason, 0);
|
|
12
|
+
exit(reason, process.env.HOUSE_KIT_RESIDENT === String(process.pid) ? 0 : 1);
|
|
11
13
|
}
|
|
12
14
|
export function restart(reason) {
|
|
13
15
|
exit(reason, 1);
|
|
14
16
|
}
|
|
17
|
+
export function updateLine(lead) {
|
|
18
|
+
const command = installCommand();
|
|
19
|
+
return command === null ? `${lead}; install it again.` : `${lead}. Update it: ${command}`;
|
|
20
|
+
}
|
|
21
|
+
export function updateRequired() {
|
|
22
|
+
end(updateLine('This Kit is too old for House'));
|
|
23
|
+
}
|
|
24
|
+
export function unknownMethod(kind) {
|
|
25
|
+
return updateLine(`This Kit does not know ${kind}`);
|
|
26
|
+
}
|
|
27
|
+
export function newerThanHouse(release, command) {
|
|
28
|
+
end(`Kit ${KIT_RELEASE} is newer than this Building supports (Kit ${release}). Install Kit ${release}: ${command}`);
|
|
29
|
+
}
|
|
15
30
|
export async function signInAgain(rejected) {
|
|
16
31
|
await forgetRejected(rejected);
|
|
17
32
|
restart('sign in again');
|
package/dist/events.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
const AUTONOMOUS_ORIGINS = new Set(['task-notification', 'peer', 'coordinator', 'observer', 'observer-activity']);
|
|
1
2
|
const COMMAND = { shown: 'command' };
|
|
2
3
|
const HIDDEN = { shown: 'hidden' };
|
|
3
4
|
const FINISHED = new Set(['completed', 'failed']);
|
|
4
5
|
export const RUNNING_JOB = new Set(['running', 'paused']);
|
|
5
|
-
const
|
|
6
|
+
const MESSAGE_TARGETS = new Map([
|
|
7
|
+
['send_routine_request', ['routine', 'agent']],
|
|
8
|
+
['cancel_routine_request', ['request']],
|
|
9
|
+
['send_mail', ['conversation', 'request']],
|
|
10
|
+
]);
|
|
6
11
|
function words(line) {
|
|
7
12
|
const found = [];
|
|
8
13
|
let word = null;
|
|
@@ -48,11 +53,13 @@ export function houseMessage(line) {
|
|
|
48
53
|
catch {
|
|
49
54
|
return null;
|
|
50
55
|
}
|
|
51
|
-
|
|
52
|
-
if (argument === undefined)
|
|
56
|
+
if (call === null)
|
|
53
57
|
return null;
|
|
54
|
-
const
|
|
55
|
-
|
|
58
|
+
const targets = MESSAGE_TARGETS.get(call.command);
|
|
59
|
+
if (targets === undefined)
|
|
60
|
+
return null;
|
|
61
|
+
const target = targets.map((argument) => call.arguments?.[argument]).find((value) => typeof value === 'string');
|
|
62
|
+
return { shown: 'message', command: call.command, target: target ?? null };
|
|
56
63
|
}
|
|
57
64
|
function decidedByLine(command, final) {
|
|
58
65
|
const line = typeof command === 'string' ? command : Array.isArray(command) ? command.at(-1) : undefined;
|
|
@@ -269,6 +276,7 @@ class Reader {
|
|
|
269
276
|
class ClaudeReader extends Reader {
|
|
270
277
|
watchedOptions = { model: 'model' };
|
|
271
278
|
tasks = new Map();
|
|
279
|
+
stop = null;
|
|
272
280
|
own(message) {
|
|
273
281
|
if (message.params?.update?._meta?.claudeCode?.parentToolUseId !== undefined)
|
|
274
282
|
return [];
|
|
@@ -276,7 +284,9 @@ class ClaudeReader extends Reader {
|
|
|
276
284
|
}
|
|
277
285
|
update(update) {
|
|
278
286
|
if (update.sessionUpdate === 'usage_update' && update._meta?.['_claude/origin'] !== undefined) {
|
|
279
|
-
|
|
287
|
+
const stop = this.stop;
|
|
288
|
+
this.stop = null;
|
|
289
|
+
return [...super.update(update), { kind: 'ended', stop }];
|
|
280
290
|
}
|
|
281
291
|
if (update.sessionUpdate === 'session_info_update' && update._meta?.jetbrains?.air?.sessionFailure !== undefined) {
|
|
282
292
|
return [{ kind: 'marker', marker: 'retry', text: update._meta.jetbrains.air.sessionFailure.title }];
|
|
@@ -287,6 +297,13 @@ class ClaudeReader extends Reader {
|
|
|
287
297
|
if (message.method !== '_claude/sdkMessage')
|
|
288
298
|
return [];
|
|
289
299
|
const sdk = message.params.message;
|
|
300
|
+
if (sdk.type === 'result') {
|
|
301
|
+
if (AUTONOMOUS_ORIGINS.has(sdk.origin?.kind))
|
|
302
|
+
this.stop = null;
|
|
303
|
+
else
|
|
304
|
+
this.stop = sdk.stop_reason === 'refusal' ? 'refusal' : sdk.is_error === true ? null : 'end_turn';
|
|
305
|
+
return [];
|
|
306
|
+
}
|
|
290
307
|
switch (sdk.subtype) {
|
|
291
308
|
case 'task_started':
|
|
292
309
|
if (sdk.tool_use_id !== undefined)
|
|
@@ -294,6 +311,10 @@ class ClaudeReader extends Reader {
|
|
|
294
311
|
return [];
|
|
295
312
|
case 'task_notification':
|
|
296
313
|
return sdk.tool_use_id === undefined ? [] : this.finish(sdk.tool_use_id, sdk.summary ?? null);
|
|
314
|
+
case 'task_progress':
|
|
315
|
+
if (!this.subagents.has(sdk.tool_use_id) || this.done.has(sdk.tool_use_id) || typeof sdk.description !== 'string')
|
|
316
|
+
return [];
|
|
317
|
+
return [{ kind: 'subagent-activity', id: sdk.tool_use_id, activity: sdk.description }];
|
|
297
318
|
default:
|
|
298
319
|
return [];
|
|
299
320
|
}
|
|
@@ -356,7 +377,7 @@ class CodexReader extends Reader {
|
|
|
356
377
|
if (codex?.threadStatus?.type === 'active')
|
|
357
378
|
return [{ kind: 'started' }];
|
|
358
379
|
if (codex?.threadStatus?.type === 'idle')
|
|
359
|
-
return [{ kind: 'ended' }];
|
|
380
|
+
return [{ kind: 'ended', stop: null }];
|
|
360
381
|
if (codex?.error !== undefined)
|
|
361
382
|
return [{ kind: 'marker', marker: 'retry', text: codex.error.message ?? '' }];
|
|
362
383
|
return [];
|
|
@@ -429,7 +450,7 @@ class GrokReader extends Reader {
|
|
|
429
450
|
return (message.params.runningPromptId ?? null) !== null && session === this.session ? [{ kind: 'started' }] : [];
|
|
430
451
|
}
|
|
431
452
|
if (message.params?.update?.sessionUpdate === 'turn_completed') {
|
|
432
|
-
return session === this.session ? [{ kind: 'ended' }] : [];
|
|
453
|
+
return session === this.session ? [{ kind: 'ended', stop: null }] : [];
|
|
433
454
|
}
|
|
434
455
|
if (message.method === '_x.ai/models/update') {
|
|
435
456
|
this.offered(message.params);
|
|
@@ -512,6 +533,10 @@ class GrokReader extends Reader {
|
|
|
512
533
|
return [];
|
|
513
534
|
}
|
|
514
535
|
}
|
|
536
|
+
export function answeredFailure(answer) {
|
|
537
|
+
const failure = answer._meta?.jetbrains?.air?.sessionFailure;
|
|
538
|
+
return typeof failure?.title === 'string' ? { title: failure.title, limit: failure.category === 'limit' } : null;
|
|
539
|
+
}
|
|
515
540
|
export function readerFor(kind) {
|
|
516
541
|
if (kind === 'claude-agent-acp')
|
|
517
542
|
return new ClaudeReader();
|
package/dist/files.js
CHANGED
|
@@ -1,36 +1,144 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { createWriteStream } from 'node:fs';
|
|
3
|
+
import { mkdir, rename, rm } from 'node:fs/promises';
|
|
4
|
+
import { basename, dirname, join } from 'node:path';
|
|
5
|
+
import { Readable, Transform } from 'node:stream';
|
|
6
|
+
import { pipeline } from 'node:stream/promises';
|
|
7
|
+
import { parseDocument } from 'yaml';
|
|
8
|
+
import { retryDelay } from './api.js';
|
|
9
|
+
import { Unsettled } from './mcp.js';
|
|
4
10
|
import { relayed } from './relay.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
const STALLED_ROUNDS = 8;
|
|
12
|
+
const PART_CHECKSUM = 'x-amz-checksum-sha256';
|
|
13
|
+
const LINK = /\]\((\/rooms\/[^/)\s]+\/[^)\s]+)\)/gu;
|
|
14
|
+
export function linkedPaths(text) {
|
|
15
|
+
return [...new Set(Array.from(text.matchAll(LINK), (link) => link[1]))];
|
|
16
|
+
}
|
|
17
|
+
export function inspectedFile(answer) {
|
|
18
|
+
const document = parseDocument(answer);
|
|
19
|
+
if (document.errors.length > 0)
|
|
20
|
+
return null;
|
|
21
|
+
const inspected = document.toJS();
|
|
22
|
+
if (typeof inspected?.download !== 'string')
|
|
23
|
+
return null;
|
|
24
|
+
return {
|
|
25
|
+
download: inspected.download,
|
|
26
|
+
sha256: String(inspected.sha256),
|
|
27
|
+
media_type: typeof inspected.media_type === 'string' ? inspected.media_type : null,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function paused(milliseconds) {
|
|
31
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
32
|
+
}
|
|
33
|
+
export function storeRefusal(act, status) {
|
|
34
|
+
return new Error(`the ${act} answered ${status}; ${act} the file again`);
|
|
35
|
+
}
|
|
36
|
+
async function partDigests(uploading, partBytes) {
|
|
37
|
+
const whole = createHash('sha256');
|
|
38
|
+
const digests = [];
|
|
39
|
+
for (let start = 0; digests.length === 0 || start < uploading.bytes; start += partBytes) {
|
|
40
|
+
const end = Math.min(start + partBytes, uploading.bytes);
|
|
41
|
+
const digest = createHash('sha256');
|
|
42
|
+
for await (const chunk of uploading.read(start, end)) {
|
|
43
|
+
digest.update(chunk);
|
|
44
|
+
whole.update(chunk);
|
|
45
|
+
}
|
|
46
|
+
digests.push(digest.digest());
|
|
47
|
+
}
|
|
48
|
+
if (whole.digest('hex') !== uploading.sha256)
|
|
49
|
+
throw new Error(`${uploading.name} changed while it was uploaded; upload it again`);
|
|
50
|
+
return digests;
|
|
51
|
+
}
|
|
52
|
+
export async function uploaded(declared, uploading) {
|
|
53
|
+
const partBytes = declared.part_bytes;
|
|
54
|
+
const digests = await partDigests(uploading, partBytes);
|
|
55
|
+
const partSha256 = digests.map((digest) => digest.toString('hex'));
|
|
56
|
+
let answer = await finishing(uploading, declared.id, partSha256);
|
|
57
|
+
let idle = 0;
|
|
58
|
+
let stalled = 0;
|
|
59
|
+
while ('upload' in answer) {
|
|
60
|
+
const { id, parts } = answer.upload;
|
|
61
|
+
let sent = 0;
|
|
62
|
+
let failure = null;
|
|
63
|
+
for (const { part, url } of parts ?? []) {
|
|
64
|
+
const start = (part - 1) * partBytes;
|
|
65
|
+
try {
|
|
66
|
+
await uploading.send(url, start, Math.min(start + partBytes, uploading.bytes), {
|
|
67
|
+
[PART_CHECKSUM]: digests[part - 1].toString('base64'),
|
|
68
|
+
});
|
|
69
|
+
sent++;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
failure = error;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (sent > 0) {
|
|
77
|
+
idle = 0;
|
|
78
|
+
stalled = 0;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
if (failure !== null && ++stalled >= STALLED_ROUNDS)
|
|
82
|
+
throw failure;
|
|
83
|
+
// A round that sent no part, its store unreachable, is followed by the next finishing call on the jittered curve.
|
|
84
|
+
await paused(retryDelay(idle++));
|
|
85
|
+
}
|
|
86
|
+
answer = await finishing(uploading, id, partSha256);
|
|
87
|
+
}
|
|
88
|
+
return answer;
|
|
89
|
+
}
|
|
90
|
+
async function finishing(uploading, upload, partSha256) {
|
|
91
|
+
for (let unsettled = 1;; unsettled++) {
|
|
92
|
+
try {
|
|
93
|
+
return await uploading.finish(upload, partSha256);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (!(error instanceof Unsettled) || unsettled === STALLED_ROUNDS)
|
|
15
97
|
throw error;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
await writeFile(path, Buffer.from(await answer.arrayBuffer()));
|
|
20
|
-
paths.push(path);
|
|
98
|
+
}
|
|
99
|
+
// A finishing call House did not settle, unreached or refused short of final, is sent again on the jittered curve as a conversation resends it, so a dropped connection or a House restart resumes the upload instead of ending it.
|
|
100
|
+
await paused(retryDelay(unsettled - 1));
|
|
21
101
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
102
|
+
}
|
|
103
|
+
async function drained(body) {
|
|
104
|
+
for await (const _chunk of body)
|
|
105
|
+
;
|
|
106
|
+
}
|
|
107
|
+
export async function downloaded(address, fetching, destination, name) {
|
|
108
|
+
for (let asked = 1;; asked++) {
|
|
109
|
+
const { download, sha256 } = await address();
|
|
110
|
+
const answer = await fetching(download);
|
|
111
|
+
if (answer.status === 403 && asked === 1) {
|
|
112
|
+
await drained(answer.body);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (answer.status !== 200) {
|
|
116
|
+
await drained(answer.body);
|
|
117
|
+
throw storeRefusal('download', answer.status);
|
|
118
|
+
}
|
|
119
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
120
|
+
const partial = join(dirname(destination), `.${basename(destination)}.${randomUUID()}`);
|
|
121
|
+
const digest = createHash('sha256');
|
|
122
|
+
const hashed = new Transform({
|
|
123
|
+
transform(chunk, _encoding, done) {
|
|
124
|
+
digest.update(chunk);
|
|
125
|
+
done(null, chunk);
|
|
126
|
+
},
|
|
30
127
|
});
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
128
|
+
try {
|
|
129
|
+
await pipeline(answer.body, hashed, createWriteStream(partial, { flags: 'wx' }));
|
|
130
|
+
if (digest.digest('hex') !== sha256)
|
|
131
|
+
throw new Error(`${name} did not arrive whole; download it again`);
|
|
132
|
+
await rename(partial, destination);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
await rm(partial, { force: true });
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export async function fetched(url, signal) {
|
|
142
|
+
const answer = await fetch(relayed(url), { signal });
|
|
143
|
+
return { status: answer.status, body: answer.body ?? Readable.from([]) };
|
|
36
144
|
}
|