@agentshouse/kit 0.2.1-alpha.8 → 0.2.1-c063e835a50ea
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 +35 -11
- package/dist/abort.js +9 -0
- package/dist/acp.js +27 -7
- package/dist/agents.js +204 -52
- package/dist/api.js +6 -2
- package/dist/bootstrap.json +10 -14
- package/dist/bridge.js +90 -54
- package/dist/clis-main.js +23 -0
- package/dist/clis.js +102 -32
- package/dist/configure-main.js +6 -0
- package/dist/conversations.js +105 -26
- package/dist/copies.js +49 -39
- package/dist/files.js +2 -1
- package/dist/git-main.js +2 -2
- package/dist/git.js +6 -2
- package/dist/home.js +49 -7
- package/dist/house-main.js +96 -41
- package/dist/instructions.js +14 -13
- package/dist/kit-main.js +4 -1
- package/dist/login.js +24 -4
- package/dist/logout.js +18 -0
- package/dist/mcp.js +39 -0
- package/dist/operation.js +7 -4
- package/dist/refusals.js +41 -0
- package/dist/relay.js +10 -0
- package/dist/resident.js +36 -9
- package/dist/secret-input.js +4 -2
- package/dist/sign-in.js +17 -5
- package/dist/skills.js +78 -0
- package/dist/stream.js +4 -4
- package/dist/transcription.js +122 -0
- package/dist/update.js +117 -0
- package/how-we-work.md +23 -0
- package/package.json +7 -8
- package/skills/ask-matt/LICENSE +21 -0
- package/skills/ask-matt/PHASE-BOUNDARIES.md +55 -0
- package/skills/ask-matt/SKILL.md +91 -0
- package/skills/ask-matt/agents/openai.yaml +4 -0
- package/skills/code-review/LICENSE +21 -0
- package/skills/code-review/SKILL.md +89 -0
- package/skills/code-review/agents/openai.yaml +3 -0
- package/skills/codebase-design/DEEPENING.md +37 -0
- package/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/skills/codebase-design/LICENSE +21 -0
- package/skills/codebase-design/SKILL.md +116 -0
- package/skills/codebase-design/agents/openai.yaml +3 -0
- package/skills/diagnosing-bugs/LICENSE +21 -0
- package/skills/diagnosing-bugs/SKILL.md +140 -0
- package/skills/diagnosing-bugs/agents/openai.yaml +3 -0
- package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
- package/skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/skills/domain-modeling/LICENSE +21 -0
- package/skills/domain-modeling/SKILL.md +76 -0
- package/skills/domain-modeling/agents/openai.yaml +3 -0
- package/skills/grill-me/LICENSE +21 -0
- package/skills/grill-me/SKILL.md +8 -0
- package/skills/grill-me/agents/openai.yaml +4 -0
- package/skills/grill-with-docs/LICENSE +21 -0
- package/skills/grill-with-docs/SKILL.md +8 -0
- package/skills/grill-with-docs/agents/openai.yaml +4 -0
- package/skills/grilling/LICENSE +21 -0
- package/skills/grilling/SKILL.md +30 -0
- package/skills/grilling/agents/openai.yaml +3 -0
- package/skills/handoff/LICENSE +21 -0
- package/skills/handoff/SKILL.md +17 -0
- package/skills/handoff/agents/openai.yaml +4 -0
- package/skills/implement/LICENSE +21 -0
- package/skills/implement/SKILL.md +16 -0
- package/skills/implement/agents/openai.yaml +4 -0
- package/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
- package/skills/improve-codebase-architecture/LICENSE +21 -0
- package/skills/improve-codebase-architecture/SKILL.md +72 -0
- package/skills/improve-codebase-architecture/agents/openai.yaml +4 -0
- package/skills/prototype/LICENSE +21 -0
- package/skills/prototype/LOGIC.md +67 -0
- package/skills/prototype/SKILL.md +28 -0
- package/skills/prototype/UI.md +112 -0
- package/skills/prototype/agents/openai.yaml +3 -0
- package/skills/research/LICENSE +21 -0
- package/skills/research/SKILL.md +14 -0
- package/skills/research/agents/openai.yaml +3 -0
- package/skills/resolving-merge-conflicts/LICENSE +21 -0
- package/skills/resolving-merge-conflicts/SKILL.md +16 -0
- package/skills/resolving-merge-conflicts/agents/openai.yaml +3 -0
- package/skills/tdd/LICENSE +21 -0
- package/skills/tdd/SKILL.md +40 -0
- package/skills/tdd/agents/openai.yaml +3 -0
- package/skills/tdd/mocking.md +59 -0
- package/skills/tdd/tests.md +77 -0
- package/skills/teach/GLOSSARY-FORMAT.md +35 -0
- package/skills/teach/LEARNING-RECORD-FORMAT.md +46 -0
- package/skills/teach/LICENSE +21 -0
- package/skills/teach/MISSION-FORMAT.md +31 -0
- package/skills/teach/RESOURCES-FORMAT.md +32 -0
- package/skills/teach/SKILL.md +141 -0
- package/skills/teach/agents/openai.yaml +4 -0
- package/skills/to-questionnaire/LICENSE +21 -0
- package/skills/to-questionnaire/SKILL.md +55 -0
- package/skills/to-questionnaire/agents/openai.yaml +4 -0
- package/skills/to-spec/LICENSE +21 -0
- package/skills/to-spec/SKILL.md +76 -0
- package/skills/to-spec/agents/openai.yaml +4 -0
- package/skills/to-tickets/LICENSE +21 -0
- package/skills/to-tickets/SKILL.md +106 -0
- package/skills/to-tickets/agents/openai.yaml +4 -0
- package/skills/wait-what/LICENSE +21 -0
- package/skills/wait-what/SKILL.md +8 -0
- package/skills/wait-what/agents/openai.yaml +4 -0
- package/skills/wayfinder/LICENSE +21 -0
- package/skills/wayfinder/SKILL.md +129 -0
- package/skills/wayfinder/agents/openai.yaml +4 -0
- package/skills/writing-for-agents/LICENSE +21 -0
- package/skills/writing-for-agents/SKILL-MECHANICS.md +22 -0
- package/skills/writing-for-agents/SKILL.md +83 -0
- package/skills/writing-for-agents/agents/openai.yaml +3 -0
package/dist/conversations.js
CHANGED
|
@@ -2,14 +2,17 @@ import { client, } from '@agentclientprotocol/sdk';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { mkdir } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { unlessAborted } from './abort.js';
|
|
5
6
|
import { killMarked, startAdapter, TURN_ENDED, TURN_STARTED } from './acp.js';
|
|
6
7
|
import { blocksOf, firstChange } from './blocks.js';
|
|
7
8
|
import { openBridge } from './bridge.js';
|
|
8
9
|
import { CLIS } from './clis.js';
|
|
9
10
|
import { placeFiles } from './files.js';
|
|
11
|
+
import { agentBase } from './home.js';
|
|
10
12
|
import { instructions } from './instructions.js';
|
|
11
13
|
import { holdSecretInput } from './secret-input.js';
|
|
12
|
-
|
|
14
|
+
import { createHowWeWork } from './skills.js';
|
|
15
|
+
import { transcribe } from './transcription.js';
|
|
13
16
|
const KILLED = 'the conversation was killed';
|
|
14
17
|
function causeOf(error) {
|
|
15
18
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -20,21 +23,15 @@ function answerMessage(request, response) {
|
|
|
20
23
|
function logged(error) {
|
|
21
24
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
22
25
|
}
|
|
23
|
-
function unlessKilled(work, signal) {
|
|
24
|
-
return new Promise((resolve, reject) => {
|
|
25
|
-
const killed = () => reject(signal.reason);
|
|
26
|
-
signal.addEventListener('abort', killed, { once: true });
|
|
27
|
-
if (signal.aborted)
|
|
28
|
-
killed();
|
|
29
|
-
work.then(resolve, reject).finally(() => signal.removeEventListener('abort', killed));
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
26
|
function stop(bridge) {
|
|
33
27
|
killMarked(`HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`);
|
|
34
28
|
}
|
|
35
29
|
class Turn {
|
|
36
30
|
id = randomUUID();
|
|
37
31
|
text = '';
|
|
32
|
+
segment = null;
|
|
33
|
+
admitted = false;
|
|
34
|
+
notes = [];
|
|
38
35
|
sent = [];
|
|
39
36
|
unsentFrom = null;
|
|
40
37
|
draftSequence = 0;
|
|
@@ -56,6 +53,8 @@ class Conversation {
|
|
|
56
53
|
opening = null;
|
|
57
54
|
kills = 0;
|
|
58
55
|
killed = new AbortController();
|
|
56
|
+
interrupts = 0;
|
|
57
|
+
interrupted = new AbortController();
|
|
59
58
|
open = 0;
|
|
60
59
|
queue = Promise.resolve();
|
|
61
60
|
reports = Promise.resolve();
|
|
@@ -91,6 +90,10 @@ export class Conversations {
|
|
|
91
90
|
conversation.killed.abort(new Error(KILLED));
|
|
92
91
|
this.halt(conversation);
|
|
93
92
|
}
|
|
93
|
+
if (input.kind === 'interrupt') {
|
|
94
|
+
conversation.interrupts++;
|
|
95
|
+
conversation.interrupted.abort();
|
|
96
|
+
}
|
|
94
97
|
const work = conversation.queue.then(() => this.carry(conversation, input));
|
|
95
98
|
conversation.queue = work.catch(() => undefined);
|
|
96
99
|
this.acks.set(input.input_id, work);
|
|
@@ -134,8 +137,11 @@ export class Conversations {
|
|
|
134
137
|
return this.open(conversation, input);
|
|
135
138
|
if (input.kind === 'message')
|
|
136
139
|
return this.message(conversation, input);
|
|
137
|
-
if (input.kind === 'interrupt')
|
|
140
|
+
if (input.kind === 'interrupt') {
|
|
141
|
+
if (--conversation.interrupts === 0)
|
|
142
|
+
conversation.interrupted = new AbortController();
|
|
138
143
|
await this.interrupt(conversation, String(input.turn_id));
|
|
144
|
+
}
|
|
139
145
|
if (input.kind === 'kill') {
|
|
140
146
|
await this.kill(conversation);
|
|
141
147
|
if (--conversation.kills === 0)
|
|
@@ -167,7 +173,9 @@ export class Conversations {
|
|
|
167
173
|
const prompt = [];
|
|
168
174
|
try {
|
|
169
175
|
const route = await this.route(conversation, input.agent_id);
|
|
170
|
-
const
|
|
176
|
+
const files = input.files;
|
|
177
|
+
const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
|
|
178
|
+
const lines = await this.transcribed(conversation, files, paths);
|
|
171
179
|
if (conversation.running === null) {
|
|
172
180
|
const opened = await this.start(conversation, route, input.provider_session_id);
|
|
173
181
|
if (input.provider_session_id === null)
|
|
@@ -180,8 +188,8 @@ export class Conversations {
|
|
|
180
188
|
});
|
|
181
189
|
}
|
|
182
190
|
prompt.push({ type: 'text', text: String(input.text) });
|
|
183
|
-
if (
|
|
184
|
-
prompt.push({ type: 'text', text:
|
|
191
|
+
if (lines.length > 0)
|
|
192
|
+
prompt.push({ type: 'text', text: lines.join('\n') });
|
|
185
193
|
if (conversation.kills > 0)
|
|
186
194
|
throw new Error(KILLED);
|
|
187
195
|
}
|
|
@@ -193,6 +201,25 @@ export class Conversations {
|
|
|
193
201
|
this.prompt(conversation, prompt);
|
|
194
202
|
return ack;
|
|
195
203
|
}
|
|
204
|
+
async transcribed(conversation, files, paths) {
|
|
205
|
+
const lines = [];
|
|
206
|
+
for (const [index, path] of paths.entries()) {
|
|
207
|
+
lines.push(path);
|
|
208
|
+
if (files[index].media_type?.startsWith('audio/') !== true)
|
|
209
|
+
continue;
|
|
210
|
+
try {
|
|
211
|
+
const text = await transcribe(path, AbortSignal.any([conversation.killed.signal, conversation.interrupted.signal]));
|
|
212
|
+
if (text !== '')
|
|
213
|
+
lines.push(`Transcript: ${text}`);
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
if (conversation.kills > 0)
|
|
217
|
+
throw error;
|
|
218
|
+
logged(new Error(`${files[index].name} was not transcribed: ${causeOf(error)}`));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return lines;
|
|
222
|
+
}
|
|
196
223
|
async interrupt(conversation, turnId) {
|
|
197
224
|
if (conversation.running === null || conversation.turn?.id !== turnId)
|
|
198
225
|
return;
|
|
@@ -231,13 +258,13 @@ export class Conversations {
|
|
|
231
258
|
}
|
|
232
259
|
async route(conversation, agentId) {
|
|
233
260
|
const signal = conversation.killed.signal;
|
|
234
|
-
await
|
|
261
|
+
await unlessAborted(this.kit.agents.read(), signal);
|
|
235
262
|
if (this.kit.agents.route(agentId) === undefined)
|
|
236
|
-
await
|
|
263
|
+
await unlessAborted(this.kit.agents.refresh(), signal);
|
|
237
264
|
const route = this.kit.agents.route(agentId);
|
|
238
265
|
if (route === undefined)
|
|
239
266
|
throw new Error(`this Environment hosts no Agent ${agentId}`);
|
|
240
|
-
if (route.working_directory === join(
|
|
267
|
+
if (route.working_directory === join(agentBase(), route.agent_id)) {
|
|
241
268
|
await mkdir(route.working_directory, { recursive: true });
|
|
242
269
|
}
|
|
243
270
|
return route;
|
|
@@ -249,7 +276,7 @@ export class Conversations {
|
|
|
249
276
|
.onNotification(TURN_ENDED, (params) => params, () => this.ended(conversation))
|
|
250
277
|
.onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
|
|
251
278
|
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal));
|
|
252
|
-
await
|
|
279
|
+
const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
|
|
253
280
|
const bridge = await openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
|
|
254
281
|
if (conversation.kills > 0) {
|
|
255
282
|
bridge.close();
|
|
@@ -257,7 +284,8 @@ export class Conversations {
|
|
|
257
284
|
}
|
|
258
285
|
conversation.opening = bridge;
|
|
259
286
|
try {
|
|
260
|
-
|
|
287
|
+
await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
|
|
288
|
+
const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (job) => this.job(conversation, job), bridge.env);
|
|
261
289
|
void adapter.exited.then(() => bridge.close());
|
|
262
290
|
const agent = adapter.connection.agent;
|
|
263
291
|
const cwd = route.working_directory;
|
|
@@ -270,6 +298,7 @@ export class Conversations {
|
|
|
270
298
|
sessionId: opened.sessionId,
|
|
271
299
|
killed: false,
|
|
272
300
|
queues: CLIS[route.kind].queues,
|
|
301
|
+
phase: CLIS[route.kind].phase,
|
|
273
302
|
};
|
|
274
303
|
const options = await this.launchSettings(running, route, opened.configOptions ?? []);
|
|
275
304
|
conversation.running = running;
|
|
@@ -357,6 +386,9 @@ export class Conversations {
|
|
|
357
386
|
return turn;
|
|
358
387
|
}
|
|
359
388
|
admitted(conversation, turn) {
|
|
389
|
+
turn.admitted = true;
|
|
390
|
+
for (const note of turn.notes.splice(0))
|
|
391
|
+
this.kit.send(note);
|
|
360
392
|
if (turn.text !== '') {
|
|
361
393
|
turn.unsentFrom = 0;
|
|
362
394
|
this.draft(conversation, turn);
|
|
@@ -370,6 +402,17 @@ export class Conversations {
|
|
|
370
402
|
this.kit.changed();
|
|
371
403
|
return turn;
|
|
372
404
|
}
|
|
405
|
+
finish(conversation, turn) {
|
|
406
|
+
if (turn.ended)
|
|
407
|
+
return;
|
|
408
|
+
const segment = turn.segment;
|
|
409
|
+
turn.segment = null;
|
|
410
|
+
if (segment?.phase === 'note')
|
|
411
|
+
this.note(conversation, turn, segment.text);
|
|
412
|
+
else if (segment !== null)
|
|
413
|
+
this.answer(conversation, turn, segment.text);
|
|
414
|
+
this.end(conversation, turn, { text: turn.text });
|
|
415
|
+
}
|
|
373
416
|
end(conversation, turn, outcome) {
|
|
374
417
|
if (turn.ended)
|
|
375
418
|
return;
|
|
@@ -427,7 +470,7 @@ export class Conversations {
|
|
|
427
470
|
this.end(conversation, turn, { failed });
|
|
428
471
|
}
|
|
429
472
|
else if (turn.prompt === sent) {
|
|
430
|
-
this.
|
|
473
|
+
this.finish(conversation, turn);
|
|
431
474
|
}
|
|
432
475
|
}
|
|
433
476
|
started(conversation) {
|
|
@@ -444,7 +487,7 @@ export class Conversations {
|
|
|
444
487
|
if (turn === null)
|
|
445
488
|
return;
|
|
446
489
|
if (turn.prompt === null) {
|
|
447
|
-
this.
|
|
490
|
+
this.finish(conversation, turn);
|
|
448
491
|
}
|
|
449
492
|
else {
|
|
450
493
|
conversation.turn = null;
|
|
@@ -498,17 +541,21 @@ export class Conversations {
|
|
|
498
541
|
update(conversation, notification) {
|
|
499
542
|
const update = notification.update;
|
|
500
543
|
if (update.sessionUpdate === 'agent_message_chunk' && update.content.type === 'text') {
|
|
501
|
-
|
|
502
|
-
turn.text += update.content.text;
|
|
503
|
-
this.draft(conversation, turn);
|
|
544
|
+
this.chunk(conversation, this.started(conversation), update, update.content.text);
|
|
504
545
|
}
|
|
505
546
|
else if (update.sessionUpdate === 'plan') {
|
|
506
547
|
const turn = this.started(conversation);
|
|
548
|
+
this.close(conversation, turn);
|
|
507
549
|
turn.entries = update.entries;
|
|
508
550
|
this.plan(conversation, turn);
|
|
509
551
|
}
|
|
510
|
-
else if (update.sessionUpdate === '
|
|
511
|
-
this.started(conversation);
|
|
552
|
+
else if (update.sessionUpdate === 'tool_call') {
|
|
553
|
+
this.close(conversation, this.started(conversation));
|
|
554
|
+
}
|
|
555
|
+
else if (update.sessionUpdate === 'agent_thought_chunk') {
|
|
556
|
+
const turn = this.started(conversation);
|
|
557
|
+
if (turn.segment?.messageId !== (update.messageId ?? null))
|
|
558
|
+
this.close(conversation, turn);
|
|
512
559
|
}
|
|
513
560
|
else if (update.sessionUpdate === 'available_commands_update') {
|
|
514
561
|
this.commands(conversation, update.availableCommands);
|
|
@@ -524,6 +571,38 @@ export class Conversations {
|
|
|
524
571
|
conversation.jobs.delete(job.id);
|
|
525
572
|
this.kit.changed();
|
|
526
573
|
}
|
|
574
|
+
chunk(conversation, turn, chunk, text) {
|
|
575
|
+
const phase = conversation.running?.phase(chunk) ?? null;
|
|
576
|
+
const messageId = chunk.messageId ?? null;
|
|
577
|
+
if (turn.segment?.messageId !== messageId || turn.segment.phase !== phase)
|
|
578
|
+
this.close(conversation, turn);
|
|
579
|
+
if (phase === 'answer') {
|
|
580
|
+
this.answer(conversation, turn, text);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
turn.segment ??= { messageId, phase, text: '' };
|
|
584
|
+
turn.segment.text += text;
|
|
585
|
+
}
|
|
586
|
+
close(conversation, turn) {
|
|
587
|
+
const segment = turn.segment;
|
|
588
|
+
if (segment === null)
|
|
589
|
+
return;
|
|
590
|
+
turn.segment = null;
|
|
591
|
+
this.note(conversation, turn, segment.text);
|
|
592
|
+
}
|
|
593
|
+
note(conversation, turn, text) {
|
|
594
|
+
if (text.trim() === '')
|
|
595
|
+
return;
|
|
596
|
+
const note = { type: 'working-note', conversation_id: conversation.id, note_id: randomUUID(), text };
|
|
597
|
+
if (turn.admitted)
|
|
598
|
+
this.kit.send(note);
|
|
599
|
+
else
|
|
600
|
+
turn.notes.push(note);
|
|
601
|
+
}
|
|
602
|
+
answer(conversation, turn, text) {
|
|
603
|
+
turn.text += text;
|
|
604
|
+
this.draft(conversation, turn);
|
|
605
|
+
}
|
|
527
606
|
draft(conversation, turn) {
|
|
528
607
|
const blocks = blocksOf(turn.text);
|
|
529
608
|
const from = Math.min(firstChange(turn.sent, blocks), turn.unsentFrom ?? Infinity);
|
package/dist/copies.js
CHANGED
|
@@ -3,12 +3,13 @@ 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 { HouseRefusal } from './api.js';
|
|
6
|
-
import { blobId, blobText, git, HOUSE_REF, importCommits, revision, run, succeeded, tree,
|
|
6
|
+
import { blobId, blobText, git, HOUSE_REF, importCommits, localCopies, revision, run, succeeded, tree, } from './git.js';
|
|
7
7
|
import { kitHome } from './home.js';
|
|
8
8
|
import { operationId } from './operation.js';
|
|
9
|
+
import { refusalOf } from './refusals.js';
|
|
9
10
|
import { changed } from './translate.js';
|
|
11
|
+
import { relayed } from './relay.js';
|
|
10
12
|
const BRANCH = 'main';
|
|
11
|
-
const ROOM_HOMES = ['capture', 'library', 'collaboration'];
|
|
12
13
|
const PROVENANCE = '_provenance';
|
|
13
14
|
const REPLICA_RETRY_MS = 30_000;
|
|
14
15
|
const IDENTITY = { GIT_COMMITTER_NAME: 'House Kit', GIT_COMMITTER_EMAIL: 'house-kit@localhost' };
|
|
@@ -18,13 +19,16 @@ function causeOf(error) {
|
|
|
18
19
|
function logged(error) {
|
|
19
20
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
20
21
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
const REBASED = new Set([
|
|
23
|
+
'field_base_stale',
|
|
24
|
+
'section_base_stale',
|
|
25
|
+
'preamble_base_stale',
|
|
26
|
+
'base_revision_stale',
|
|
27
|
+
'path_exists',
|
|
28
|
+
'path_absent',
|
|
29
|
+
]);
|
|
30
|
+
function rebaseFixes(refusal) {
|
|
31
|
+
return refusal.code === 'edit_conflict' && refusal.conflicts.some((code) => REBASED.has(code));
|
|
28
32
|
}
|
|
29
33
|
function short(commit) {
|
|
30
34
|
return commit.slice(0, 12);
|
|
@@ -43,9 +47,6 @@ function rootOf(copy) {
|
|
|
43
47
|
function relative(path) {
|
|
44
48
|
return path.replace(/^\/private\//, '').replace(/^\/rooms\/[^/]+\//, '');
|
|
45
49
|
}
|
|
46
|
-
function prefixesOf(selected) {
|
|
47
|
-
return selected.private ? ['/private'] : ROOM_HOMES.map((home) => `${rootOf(selected)}/${home}`);
|
|
48
|
-
}
|
|
49
50
|
function declaredWritable(path, copy) {
|
|
50
51
|
const deepest = (scopes) => Math.max(-1, ...scopes.filter((scope) => covers(path, scope)).map((scope) => scope.length));
|
|
51
52
|
return deepest(copy.write) > deepest(copy.protected);
|
|
@@ -147,7 +148,7 @@ function movedPaths(moves, before, after) {
|
|
|
147
148
|
.filter((pair) => pairs.filter((other) => other.from === pair.from || other.to === pair.to).length === 1)
|
|
148
149
|
.map(({ from, to }) => [from, to]));
|
|
149
150
|
}
|
|
150
|
-
export class
|
|
151
|
+
export class LocalCopies {
|
|
151
152
|
owner;
|
|
152
153
|
house;
|
|
153
154
|
copies = new Map();
|
|
@@ -166,13 +167,15 @@ export class WorkingCopies {
|
|
|
166
167
|
catch (error) {
|
|
167
168
|
if (!(error instanceof HouseRefusal) || error.status >= 500)
|
|
168
169
|
throw error;
|
|
169
|
-
return {
|
|
170
|
+
return {
|
|
171
|
+
refused: refusalOf(error.text) ?? { code: '', text: `House refused it with ${error.status}`, conflicts: [] },
|
|
172
|
+
};
|
|
170
173
|
}
|
|
171
174
|
},
|
|
172
175
|
};
|
|
173
176
|
}
|
|
174
177
|
async load() {
|
|
175
|
-
const home = join(kitHome(), '
|
|
178
|
+
const home = join(kitHome(), 'local-copies');
|
|
176
179
|
const names = (await readdir(join(home, 'rooms')).catch(() => [])).filter((name) => name.endsWith('.json'));
|
|
177
180
|
for (const path of [...names.map((name) => join(home, 'rooms', name)), join(home, 'private.json')]) {
|
|
178
181
|
const text = await readFile(path, 'utf8').catch(() => null);
|
|
@@ -195,7 +198,7 @@ export class WorkingCopies {
|
|
|
195
198
|
return selected;
|
|
196
199
|
}
|
|
197
200
|
async reselect() {
|
|
198
|
-
const {
|
|
201
|
+
const { local_copy: chosen } = await this.house.post('/kit/local-copy/selection', {});
|
|
199
202
|
const selected = [
|
|
200
203
|
...(chosen?.rooms ?? [])
|
|
201
204
|
.filter((room) => room.room_handle !== null)
|
|
@@ -253,11 +256,11 @@ export class WorkingCopies {
|
|
|
253
256
|
async push(cwd, selector, caller) {
|
|
254
257
|
const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
|
|
255
258
|
if (located.status !== 0)
|
|
256
|
-
return { refused: true, text: `${cwd} is not
|
|
259
|
+
return { refused: true, text: `${cwd} is not a House Local copy; run house push inside one` };
|
|
257
260
|
const [common, worktree] = located.stdout.toString('utf8').split('\n');
|
|
258
261
|
const key = await this.keyAt(dirname(common));
|
|
259
262
|
if (key === undefined)
|
|
260
|
-
return { refused: true, text: `${worktree} is not a House
|
|
263
|
+
return { refused: true, text: `${worktree} is not a House Local copy; run house push inside one` };
|
|
261
264
|
return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
|
|
262
265
|
}
|
|
263
266
|
async keyAt(repository) {
|
|
@@ -285,7 +288,7 @@ export class WorkingCopies {
|
|
|
285
288
|
}).catch(logged);
|
|
286
289
|
}
|
|
287
290
|
async save(copy) {
|
|
288
|
-
const path = join(kitHome(), '
|
|
291
|
+
const path = join(kitHome(), 'local-copies', copy.private ? 'private.json' : `${copy.key}.json`);
|
|
289
292
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
290
293
|
const temporary = `${path}.${process.pid}`;
|
|
291
294
|
await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
|
|
@@ -294,11 +297,11 @@ export class WorkingCopies {
|
|
|
294
297
|
return copy;
|
|
295
298
|
}
|
|
296
299
|
async read(selected) {
|
|
297
|
-
const prefixes =
|
|
300
|
+
const prefixes = [rootOf(selected)];
|
|
298
301
|
const discovery = await this.house.post('/kit/door/discover', { prefixes });
|
|
299
302
|
let bundle = await this.house.post('/kit/door/bootstrap', { prefixes });
|
|
300
303
|
if ('transfer' in bundle)
|
|
301
|
-
bundle = (await (await fetch(bundle.transfer.url)).json());
|
|
304
|
+
bundle = (await (await fetch(relayed(bundle.transfer.url))).json());
|
|
302
305
|
const [view] = bundle.scopes;
|
|
303
306
|
if (view === undefined)
|
|
304
307
|
return null;
|
|
@@ -341,7 +344,7 @@ export class WorkingCopies {
|
|
|
341
344
|
return;
|
|
342
345
|
}
|
|
343
346
|
const discovery = await this.house.post('/kit/door/discover', {
|
|
344
|
-
prefixes:
|
|
347
|
+
prefixes: [rootOf(copy)],
|
|
345
348
|
});
|
|
346
349
|
const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
|
|
347
350
|
if (authority === undefined)
|
|
@@ -363,7 +366,7 @@ export class WorkingCopies {
|
|
|
363
366
|
logged(`House serves nothing of ${rootOf({ private: scope.private, room_handle: scope.handle })} to copy`);
|
|
364
367
|
return;
|
|
365
368
|
}
|
|
366
|
-
const repository = scope.private ? join(
|
|
369
|
+
const repository = scope.private ? join(localCopies(), 'private') : join(localCopies(), 'rooms', read.anchor);
|
|
367
370
|
if (await exists(repository)) {
|
|
368
371
|
logged(`${repository} already exists and House Kit leaves it as it is`);
|
|
369
372
|
return;
|
|
@@ -432,12 +435,12 @@ export class WorkingCopies {
|
|
|
432
435
|
let answer;
|
|
433
436
|
try {
|
|
434
437
|
answer = await this.house.post('/kit/door/enumerate', {
|
|
435
|
-
prefixes:
|
|
438
|
+
prefixes: [rootOf(copy)],
|
|
436
439
|
positions: [{ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch }],
|
|
437
440
|
});
|
|
438
441
|
}
|
|
439
442
|
catch (error) {
|
|
440
|
-
const code = error instanceof HouseRefusal ?
|
|
443
|
+
const code = error instanceof HouseRefusal ? (refusalOf(error.text)?.code ?? null) : null;
|
|
441
444
|
if (code === 'position_expired')
|
|
442
445
|
return this.received(copy);
|
|
443
446
|
if (code !== 'replica_behind')
|
|
@@ -512,8 +515,9 @@ export class WorkingCopies {
|
|
|
512
515
|
}
|
|
513
516
|
copy = await this.save(without(copy));
|
|
514
517
|
}
|
|
515
|
-
if (!copy.active)
|
|
516
|
-
return { refused: true, text: `${rootOf(copy)} is no longer
|
|
518
|
+
if (!copy.active) {
|
|
519
|
+
return { refused: true, text: `${rootOf(copy)} is no longer synced to this Environment, so House takes no push from it` };
|
|
520
|
+
}
|
|
517
521
|
const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
|
|
518
522
|
const base = listed.find((ancestor) => ancestor in copy.manifests);
|
|
519
523
|
if (base === undefined) {
|
|
@@ -582,9 +586,8 @@ export class WorkingCopies {
|
|
|
582
586
|
async settle(start, caller, restaged = false) {
|
|
583
587
|
let copy = start;
|
|
584
588
|
const push = copy.push;
|
|
585
|
-
if (push.caller !== caller.key)
|
|
586
|
-
return this.recovered(copy
|
|
587
|
-
}
|
|
589
|
+
if (push.caller !== caller.key)
|
|
590
|
+
return this.recovered(copy);
|
|
588
591
|
let outcome;
|
|
589
592
|
try {
|
|
590
593
|
outcome = await caller.submit(push.operation_id, push.changes, push.upload, async (upload) => {
|
|
@@ -592,9 +595,10 @@ export class WorkingCopies {
|
|
|
592
595
|
});
|
|
593
596
|
}
|
|
594
597
|
catch (error) {
|
|
598
|
+
const refusal = error instanceof HouseRefusal ? refusalOf(error.text) : null;
|
|
595
599
|
return {
|
|
596
600
|
refused: true,
|
|
597
|
-
text: `House's answer to ${short(push.commit)} did not arrive
|
|
601
|
+
text: refusal?.text ?? `House's answer to ${short(push.commit)} did not arrive; run house push again`,
|
|
598
602
|
};
|
|
599
603
|
}
|
|
600
604
|
if ('refused' in outcome) {
|
|
@@ -603,22 +607,28 @@ export class WorkingCopies {
|
|
|
603
607
|
return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
|
|
604
608
|
}
|
|
605
609
|
if (outcome.refused.code === 'operation_expired')
|
|
606
|
-
return this.recovered(copy
|
|
610
|
+
return this.recovered(copy);
|
|
611
|
+
const rebased = rebaseFixes(outcome.refused);
|
|
607
612
|
let settled = await this.save(without(copy));
|
|
608
613
|
try {
|
|
609
614
|
settled = await this.save(await this.caughtUp(settled));
|
|
610
615
|
}
|
|
611
|
-
catch
|
|
612
|
-
return {
|
|
616
|
+
catch {
|
|
617
|
+
return {
|
|
618
|
+
refused: true,
|
|
619
|
+
text: rebased ? `${outcome.refused.text}\nHouse's current state did not arrive; push again later` : outcome.refused.text,
|
|
620
|
+
};
|
|
613
621
|
}
|
|
614
622
|
return {
|
|
615
623
|
refused: true,
|
|
616
|
-
text:
|
|
624
|
+
text: rebased
|
|
625
|
+
? `${outcome.refused.text}\n${HOUSE_REF} holds House's current state; rebase onto it and push again`
|
|
626
|
+
: outcome.refused.text,
|
|
617
627
|
};
|
|
618
628
|
}
|
|
619
629
|
return this.finished(await this.save({ ...copy, push: { ...copy.push, state: 'accepted', written: outcome.accepted } }));
|
|
620
630
|
}
|
|
621
|
-
async recovered(copy
|
|
631
|
+
async recovered(copy) {
|
|
622
632
|
const push = copy.push;
|
|
623
633
|
const current = await this.save(await this.caughtUp(copy));
|
|
624
634
|
const house = await tree(copy.repository, current.house);
|
|
@@ -627,7 +637,7 @@ export class WorkingCopies {
|
|
|
627
637
|
await this.save(without(current));
|
|
628
638
|
return {
|
|
629
639
|
refused: true,
|
|
630
|
-
text:
|
|
640
|
+
text: `House does not hold ${short(push.commit)}; run house push again`,
|
|
631
641
|
};
|
|
632
642
|
}
|
|
633
643
|
const written = [];
|
|
@@ -652,10 +662,10 @@ export class WorkingCopies {
|
|
|
652
662
|
await this.save(without(copy));
|
|
653
663
|
return { refused: false, text: integrated.text };
|
|
654
664
|
}
|
|
655
|
-
catch
|
|
665
|
+
catch {
|
|
656
666
|
return {
|
|
657
667
|
refused: false,
|
|
658
|
-
text: `House accepted ${short(commit)};
|
|
668
|
+
text: `House accepted ${short(commit)}; run house push again to finish integrating it here`,
|
|
659
669
|
};
|
|
660
670
|
}
|
|
661
671
|
}
|
package/dist/files.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { relayed } from './relay.js';
|
|
3
4
|
export async function placeFiles(house, directory, files, signal) {
|
|
4
5
|
const paths = [];
|
|
5
6
|
for (const file of files) {
|
|
6
7
|
const original = await house.post('/kit/conversation/originals/get', { version: file.version, download: true }, signal);
|
|
7
|
-
const answer = await fetch(original.download.url, { signal });
|
|
8
|
+
const answer = await fetch(relayed(original.download.url), { signal });
|
|
8
9
|
if (!answer.ok)
|
|
9
10
|
throw new Error(`${file.name} did not download: ${answer.status}`);
|
|
10
11
|
const folder = join(directory, '.house', 'files', original.message, file.version);
|
package/dist/git-main.js
CHANGED
|
@@ -4,7 +4,7 @@ import { accessSync, constants, realpathSync, statSync } from 'node:fs';
|
|
|
4
4
|
import { request } from 'node:http';
|
|
5
5
|
import { constants as system } from 'node:os';
|
|
6
6
|
import { delimiter, dirname, join } from 'node:path';
|
|
7
|
-
import {
|
|
7
|
+
import { localCopies } from './git.js';
|
|
8
8
|
import { kitHome } from './home.js';
|
|
9
9
|
const VALUED = new Set(['-C', '-c', '--git-dir', '--work-tree', '--namespace', '--config-env', '--attr-source', '--super-prefix']);
|
|
10
10
|
const READS = new Set([
|
|
@@ -125,7 +125,7 @@ async function observed(git, args) {
|
|
|
125
125
|
return delegated(git, args);
|
|
126
126
|
const located = captured(git, [...globals, 'rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
|
|
127
127
|
const [common, worktree] = located?.split('\n') ?? [];
|
|
128
|
-
if (common === undefined || worktree === undefined || !dirname(common).startsWith(`${
|
|
128
|
+
if (common === undefined || worktree === undefined || !dirname(common).startsWith(`${localCopies()}/`)) {
|
|
129
129
|
return delegated(git, args);
|
|
130
130
|
}
|
|
131
131
|
const listing = [...globals, '-C', worktree, 'ls-files', '--stage', '-z'];
|
package/dist/git.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
+
import { join } from 'node:path';
|
|
3
4
|
import { withoutProxy } from './clis.js';
|
|
4
|
-
|
|
5
|
+
import { workspaceRoot } from './home.js';
|
|
6
|
+
export function localCopies() {
|
|
7
|
+
return join(workspaceRoot(), 'local-copies');
|
|
8
|
+
}
|
|
5
9
|
export const HOUSE_REF = 'refs/house/received';
|
|
6
10
|
const COMMITTER = 'House Kit <house-kit@localhost>';
|
|
7
11
|
export function run(cwd, args, input, env = {}) {
|
|
@@ -50,7 +54,7 @@ export async function blobText(repository, object, path) {
|
|
|
50
54
|
return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
51
55
|
}
|
|
52
56
|
catch {
|
|
53
|
-
throw new Error(`${path} is
|
|
57
|
+
throw new Error(`${path} is binary and House stores only text; remove it from the commit`);
|
|
54
58
|
}
|
|
55
59
|
}
|
|
56
60
|
export function blobId(content) {
|
package/dist/home.js
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
2
3
|
import { homedir } from 'node:os';
|
|
3
4
|
import { join } from 'node:path';
|
|
4
5
|
const ENROLMENT = 'credential.json';
|
|
6
|
+
const DISCONNECTED = 'disconnected.json';
|
|
7
|
+
const CONFIGURATION = 'kit.json';
|
|
8
|
+
const OWN_AGENT = 'own-agent.json';
|
|
5
9
|
export function kitHome() {
|
|
6
10
|
return process.env.HOUSE_KIT_HOME ?? join(homedir(), '.house-kit');
|
|
7
11
|
}
|
|
8
|
-
export
|
|
12
|
+
export function workspaceRoot() {
|
|
13
|
+
return process.env.HOUSE_KIT_WORKSPACE ?? '/agents/house';
|
|
14
|
+
}
|
|
15
|
+
export function agentBase() {
|
|
16
|
+
return join(workspaceRoot(), 'agents');
|
|
17
|
+
}
|
|
18
|
+
export async function readHome(name) {
|
|
9
19
|
try {
|
|
10
|
-
return JSON.parse(await readFile(join(kitHome(),
|
|
20
|
+
return JSON.parse(await readFile(join(kitHome(), name), 'utf8'));
|
|
11
21
|
}
|
|
12
22
|
catch (error) {
|
|
13
23
|
if (error.code === 'ENOENT')
|
|
@@ -15,10 +25,42 @@ export async function readEnrolment() {
|
|
|
15
25
|
throw error;
|
|
16
26
|
}
|
|
17
27
|
}
|
|
18
|
-
export async function
|
|
28
|
+
export async function writeHome(name, value) {
|
|
19
29
|
const home = kitHome();
|
|
20
30
|
await mkdir(home, { recursive: true, mode: 0o700 });
|
|
21
|
-
const temporary = join(home, `${
|
|
22
|
-
await writeFile(temporary, `${JSON.stringify(
|
|
23
|
-
await rename(temporary, join(home,
|
|
31
|
+
const temporary = join(home, `${name}.${process.pid}`);
|
|
32
|
+
await writeFile(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600 });
|
|
33
|
+
await rename(temporary, join(home, name));
|
|
34
|
+
}
|
|
35
|
+
export function readEnrolment() {
|
|
36
|
+
return readHome(ENROLMENT);
|
|
37
|
+
}
|
|
38
|
+
export async function writeEnrolment(enrolment) {
|
|
39
|
+
await writeHome(ENROLMENT, enrolment);
|
|
40
|
+
await rm(join(kitHome(), DISCONNECTED), { force: true });
|
|
41
|
+
}
|
|
42
|
+
export function readDisconnected() {
|
|
43
|
+
return readHome(DISCONNECTED);
|
|
44
|
+
}
|
|
45
|
+
export async function forgetEnrolment({ house, environment }) {
|
|
46
|
+
await writeHome(DISCONNECTED, { house, environment });
|
|
47
|
+
await rm(join(kitHome(), ENROLMENT), { force: true });
|
|
48
|
+
}
|
|
49
|
+
export function enrolled() {
|
|
50
|
+
return existsSync(join(kitHome(), ENROLMENT));
|
|
51
|
+
}
|
|
52
|
+
export function readOwnAgent() {
|
|
53
|
+
return readHome(OWN_AGENT);
|
|
54
|
+
}
|
|
55
|
+
export function writeOwnAgent(ownAgent) {
|
|
56
|
+
return writeHome(OWN_AGENT, ownAgent);
|
|
57
|
+
}
|
|
58
|
+
export function forgetOwnAgent() {
|
|
59
|
+
return rm(join(kitHome(), OWN_AGENT), { force: true });
|
|
60
|
+
}
|
|
61
|
+
export async function readConfiguration() {
|
|
62
|
+
return { skills: (await readHome(CONFIGURATION))?.skills !== false };
|
|
63
|
+
}
|
|
64
|
+
export function writeConfiguration(configuration) {
|
|
65
|
+
return writeHome(CONFIGURATION, configuration);
|
|
24
66
|
}
|