@agentshouse/kit 0.2.1-alpha.8 → 0.2.1-c3e4a20bb3183
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 +33 -9
- package/dist/abort.js +9 -0
- package/dist/acp.js +27 -7
- package/dist/agents.js +204 -52
- package/dist/api.js +4 -1
- package/dist/bootstrap.json +8 -5
- package/dist/bridge.js +85 -51
- 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 +47 -38
- package/dist/git-main.js +2 -2
- package/dist/git.js +6 -2
- package/dist/home.js +36 -7
- package/dist/house-main.js +91 -35
- package/dist/instructions.js +14 -13
- package/dist/login.js +23 -3
- package/dist/mcp.js +38 -0
- package/dist/operation.js +7 -4
- package/dist/refusals.js +41 -0
- package/dist/resident.js +12 -4
- package/dist/secret-input.js +4 -2
- package/dist/sign-in.js +17 -5
- package/dist/skills.js +78 -0
- package/dist/stream.js +2 -3
- 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,12 @@ 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';
|
|
10
11
|
const BRANCH = 'main';
|
|
11
|
-
const ROOM_HOMES = ['capture', 'library', 'collaboration'];
|
|
12
12
|
const PROVENANCE = '_provenance';
|
|
13
13
|
const REPLICA_RETRY_MS = 30_000;
|
|
14
14
|
const IDENTITY = { GIT_COMMITTER_NAME: 'House Kit', GIT_COMMITTER_EMAIL: 'house-kit@localhost' };
|
|
@@ -18,13 +18,16 @@ function causeOf(error) {
|
|
|
18
18
|
function logged(error) {
|
|
19
19
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
const REBASED = new Set([
|
|
22
|
+
'field_base_stale',
|
|
23
|
+
'section_base_stale',
|
|
24
|
+
'preamble_base_stale',
|
|
25
|
+
'base_revision_stale',
|
|
26
|
+
'path_exists',
|
|
27
|
+
'path_absent',
|
|
28
|
+
]);
|
|
29
|
+
function rebaseFixes(refusal) {
|
|
30
|
+
return refusal.code === 'edit_conflict' && refusal.conflicts.some((code) => REBASED.has(code));
|
|
28
31
|
}
|
|
29
32
|
function short(commit) {
|
|
30
33
|
return commit.slice(0, 12);
|
|
@@ -43,9 +46,6 @@ function rootOf(copy) {
|
|
|
43
46
|
function relative(path) {
|
|
44
47
|
return path.replace(/^\/private\//, '').replace(/^\/rooms\/[^/]+\//, '');
|
|
45
48
|
}
|
|
46
|
-
function prefixesOf(selected) {
|
|
47
|
-
return selected.private ? ['/private'] : ROOM_HOMES.map((home) => `${rootOf(selected)}/${home}`);
|
|
48
|
-
}
|
|
49
49
|
function declaredWritable(path, copy) {
|
|
50
50
|
const deepest = (scopes) => Math.max(-1, ...scopes.filter((scope) => covers(path, scope)).map((scope) => scope.length));
|
|
51
51
|
return deepest(copy.write) > deepest(copy.protected);
|
|
@@ -147,7 +147,7 @@ function movedPaths(moves, before, after) {
|
|
|
147
147
|
.filter((pair) => pairs.filter((other) => other.from === pair.from || other.to === pair.to).length === 1)
|
|
148
148
|
.map(({ from, to }) => [from, to]));
|
|
149
149
|
}
|
|
150
|
-
export class
|
|
150
|
+
export class LocalCopies {
|
|
151
151
|
owner;
|
|
152
152
|
house;
|
|
153
153
|
copies = new Map();
|
|
@@ -166,13 +166,15 @@ export class WorkingCopies {
|
|
|
166
166
|
catch (error) {
|
|
167
167
|
if (!(error instanceof HouseRefusal) || error.status >= 500)
|
|
168
168
|
throw error;
|
|
169
|
-
return {
|
|
169
|
+
return {
|
|
170
|
+
refused: refusalOf(error.text) ?? { code: '', text: `House refused it with ${error.status}`, conflicts: [] },
|
|
171
|
+
};
|
|
170
172
|
}
|
|
171
173
|
},
|
|
172
174
|
};
|
|
173
175
|
}
|
|
174
176
|
async load() {
|
|
175
|
-
const home = join(kitHome(), '
|
|
177
|
+
const home = join(kitHome(), 'local-copies');
|
|
176
178
|
const names = (await readdir(join(home, 'rooms')).catch(() => [])).filter((name) => name.endsWith('.json'));
|
|
177
179
|
for (const path of [...names.map((name) => join(home, 'rooms', name)), join(home, 'private.json')]) {
|
|
178
180
|
const text = await readFile(path, 'utf8').catch(() => null);
|
|
@@ -195,7 +197,7 @@ export class WorkingCopies {
|
|
|
195
197
|
return selected;
|
|
196
198
|
}
|
|
197
199
|
async reselect() {
|
|
198
|
-
const {
|
|
200
|
+
const { local_copy: chosen } = await this.house.post('/kit/local-copy/selection', {});
|
|
199
201
|
const selected = [
|
|
200
202
|
...(chosen?.rooms ?? [])
|
|
201
203
|
.filter((room) => room.room_handle !== null)
|
|
@@ -253,11 +255,11 @@ export class WorkingCopies {
|
|
|
253
255
|
async push(cwd, selector, caller) {
|
|
254
256
|
const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
|
|
255
257
|
if (located.status !== 0)
|
|
256
|
-
return { refused: true, text: `${cwd} is not
|
|
258
|
+
return { refused: true, text: `${cwd} is not a House Local copy; run house git push inside one` };
|
|
257
259
|
const [common, worktree] = located.stdout.toString('utf8').split('\n');
|
|
258
260
|
const key = await this.keyAt(dirname(common));
|
|
259
261
|
if (key === undefined)
|
|
260
|
-
return { refused: true, text: `${worktree} is not a House
|
|
262
|
+
return { refused: true, text: `${worktree} is not a House Local copy; run house git push inside one` };
|
|
261
263
|
return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
|
|
262
264
|
}
|
|
263
265
|
async keyAt(repository) {
|
|
@@ -285,7 +287,7 @@ export class WorkingCopies {
|
|
|
285
287
|
}).catch(logged);
|
|
286
288
|
}
|
|
287
289
|
async save(copy) {
|
|
288
|
-
const path = join(kitHome(), '
|
|
290
|
+
const path = join(kitHome(), 'local-copies', copy.private ? 'private.json' : `${copy.key}.json`);
|
|
289
291
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
290
292
|
const temporary = `${path}.${process.pid}`;
|
|
291
293
|
await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
|
|
@@ -294,7 +296,7 @@ export class WorkingCopies {
|
|
|
294
296
|
return copy;
|
|
295
297
|
}
|
|
296
298
|
async read(selected) {
|
|
297
|
-
const prefixes =
|
|
299
|
+
const prefixes = [rootOf(selected)];
|
|
298
300
|
const discovery = await this.house.post('/kit/door/discover', { prefixes });
|
|
299
301
|
let bundle = await this.house.post('/kit/door/bootstrap', { prefixes });
|
|
300
302
|
if ('transfer' in bundle)
|
|
@@ -341,7 +343,7 @@ export class WorkingCopies {
|
|
|
341
343
|
return;
|
|
342
344
|
}
|
|
343
345
|
const discovery = await this.house.post('/kit/door/discover', {
|
|
344
|
-
prefixes:
|
|
346
|
+
prefixes: [rootOf(copy)],
|
|
345
347
|
});
|
|
346
348
|
const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
|
|
347
349
|
if (authority === undefined)
|
|
@@ -363,7 +365,7 @@ export class WorkingCopies {
|
|
|
363
365
|
logged(`House serves nothing of ${rootOf({ private: scope.private, room_handle: scope.handle })} to copy`);
|
|
364
366
|
return;
|
|
365
367
|
}
|
|
366
|
-
const repository = scope.private ? join(
|
|
368
|
+
const repository = scope.private ? join(localCopies(), 'private') : join(localCopies(), 'rooms', read.anchor);
|
|
367
369
|
if (await exists(repository)) {
|
|
368
370
|
logged(`${repository} already exists and House Kit leaves it as it is`);
|
|
369
371
|
return;
|
|
@@ -432,12 +434,12 @@ export class WorkingCopies {
|
|
|
432
434
|
let answer;
|
|
433
435
|
try {
|
|
434
436
|
answer = await this.house.post('/kit/door/enumerate', {
|
|
435
|
-
prefixes:
|
|
437
|
+
prefixes: [rootOf(copy)],
|
|
436
438
|
positions: [{ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch }],
|
|
437
439
|
});
|
|
438
440
|
}
|
|
439
441
|
catch (error) {
|
|
440
|
-
const code = error instanceof HouseRefusal ?
|
|
442
|
+
const code = error instanceof HouseRefusal ? (refusalOf(error.text)?.code ?? null) : null;
|
|
441
443
|
if (code === 'position_expired')
|
|
442
444
|
return this.received(copy);
|
|
443
445
|
if (code !== 'replica_behind')
|
|
@@ -512,8 +514,9 @@ export class WorkingCopies {
|
|
|
512
514
|
}
|
|
513
515
|
copy = await this.save(without(copy));
|
|
514
516
|
}
|
|
515
|
-
if (!copy.active)
|
|
516
|
-
return { refused: true, text: `${rootOf(copy)} is no longer
|
|
517
|
+
if (!copy.active) {
|
|
518
|
+
return { refused: true, text: `${rootOf(copy)} is no longer synced to this Environment, so House takes no push from it` };
|
|
519
|
+
}
|
|
517
520
|
const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
|
|
518
521
|
const base = listed.find((ancestor) => ancestor in copy.manifests);
|
|
519
522
|
if (base === undefined) {
|
|
@@ -582,9 +585,8 @@ export class WorkingCopies {
|
|
|
582
585
|
async settle(start, caller, restaged = false) {
|
|
583
586
|
let copy = start;
|
|
584
587
|
const push = copy.push;
|
|
585
|
-
if (push.caller !== caller.key)
|
|
586
|
-
return this.recovered(copy
|
|
587
|
-
}
|
|
588
|
+
if (push.caller !== caller.key)
|
|
589
|
+
return this.recovered(copy);
|
|
588
590
|
let outcome;
|
|
589
591
|
try {
|
|
590
592
|
outcome = await caller.submit(push.operation_id, push.changes, push.upload, async (upload) => {
|
|
@@ -592,9 +594,10 @@ export class WorkingCopies {
|
|
|
592
594
|
});
|
|
593
595
|
}
|
|
594
596
|
catch (error) {
|
|
597
|
+
const refusal = error instanceof HouseRefusal ? refusalOf(error.text) : null;
|
|
595
598
|
return {
|
|
596
599
|
refused: true,
|
|
597
|
-
text: `House's answer to ${short(push.commit)} did not arrive
|
|
600
|
+
text: refusal?.text ?? `House's answer to ${short(push.commit)} did not arrive; run house git push again`,
|
|
598
601
|
};
|
|
599
602
|
}
|
|
600
603
|
if ('refused' in outcome) {
|
|
@@ -603,22 +606,28 @@ export class WorkingCopies {
|
|
|
603
606
|
return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
|
|
604
607
|
}
|
|
605
608
|
if (outcome.refused.code === 'operation_expired')
|
|
606
|
-
return this.recovered(copy
|
|
609
|
+
return this.recovered(copy);
|
|
610
|
+
const rebased = rebaseFixes(outcome.refused);
|
|
607
611
|
let settled = await this.save(without(copy));
|
|
608
612
|
try {
|
|
609
613
|
settled = await this.save(await this.caughtUp(settled));
|
|
610
614
|
}
|
|
611
|
-
catch
|
|
612
|
-
return {
|
|
615
|
+
catch {
|
|
616
|
+
return {
|
|
617
|
+
refused: true,
|
|
618
|
+
text: rebased ? `${outcome.refused.text}\nHouse's current state did not arrive; push again later` : outcome.refused.text,
|
|
619
|
+
};
|
|
613
620
|
}
|
|
614
621
|
return {
|
|
615
622
|
refused: true,
|
|
616
|
-
text:
|
|
623
|
+
text: rebased
|
|
624
|
+
? `${outcome.refused.text}\n${HOUSE_REF} holds House's current state; rebase onto it and push again`
|
|
625
|
+
: outcome.refused.text,
|
|
617
626
|
};
|
|
618
627
|
}
|
|
619
628
|
return this.finished(await this.save({ ...copy, push: { ...copy.push, state: 'accepted', written: outcome.accepted } }));
|
|
620
629
|
}
|
|
621
|
-
async recovered(copy
|
|
630
|
+
async recovered(copy) {
|
|
622
631
|
const push = copy.push;
|
|
623
632
|
const current = await this.save(await this.caughtUp(copy));
|
|
624
633
|
const house = await tree(copy.repository, current.house);
|
|
@@ -627,7 +636,7 @@ export class WorkingCopies {
|
|
|
627
636
|
await this.save(without(current));
|
|
628
637
|
return {
|
|
629
638
|
refused: true,
|
|
630
|
-
text:
|
|
639
|
+
text: `House does not hold ${short(push.commit)}; run house git push again`,
|
|
631
640
|
};
|
|
632
641
|
}
|
|
633
642
|
const written = [];
|
|
@@ -652,10 +661,10 @@ export class WorkingCopies {
|
|
|
652
661
|
await this.save(without(copy));
|
|
653
662
|
return { refused: false, text: integrated.text };
|
|
654
663
|
}
|
|
655
|
-
catch
|
|
664
|
+
catch {
|
|
656
665
|
return {
|
|
657
666
|
refused: false,
|
|
658
|
-
text: `House accepted ${short(commit)};
|
|
667
|
+
text: `House accepted ${short(commit)}; run house git push again to finish integrating it here`,
|
|
659
668
|
};
|
|
660
669
|
}
|
|
661
670
|
}
|
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,21 @@
|
|
|
1
|
-
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
const ENROLMENT = 'credential.json';
|
|
5
|
+
const CONFIGURATION = 'kit.json';
|
|
6
|
+
const OWN_AGENT = 'own-agent.json';
|
|
5
7
|
export function kitHome() {
|
|
6
8
|
return process.env.HOUSE_KIT_HOME ?? join(homedir(), '.house-kit');
|
|
7
9
|
}
|
|
8
|
-
export
|
|
10
|
+
export function workspaceRoot() {
|
|
11
|
+
return process.env.HOUSE_KIT_WORKSPACE ?? '/agents/house';
|
|
12
|
+
}
|
|
13
|
+
export function agentBase() {
|
|
14
|
+
return join(workspaceRoot(), 'agents');
|
|
15
|
+
}
|
|
16
|
+
export async function readHome(name) {
|
|
9
17
|
try {
|
|
10
|
-
return JSON.parse(await readFile(join(kitHome(),
|
|
18
|
+
return JSON.parse(await readFile(join(kitHome(), name), 'utf8'));
|
|
11
19
|
}
|
|
12
20
|
catch (error) {
|
|
13
21
|
if (error.code === 'ENOENT')
|
|
@@ -15,10 +23,31 @@ export async function readEnrolment() {
|
|
|
15
23
|
throw error;
|
|
16
24
|
}
|
|
17
25
|
}
|
|
18
|
-
export async function
|
|
26
|
+
export async function writeHome(name, value) {
|
|
19
27
|
const home = kitHome();
|
|
20
28
|
await mkdir(home, { recursive: true, mode: 0o700 });
|
|
21
|
-
const temporary = join(home, `${
|
|
22
|
-
await writeFile(temporary, `${JSON.stringify(
|
|
23
|
-
await rename(temporary, join(home,
|
|
29
|
+
const temporary = join(home, `${name}.${process.pid}`);
|
|
30
|
+
await writeFile(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600 });
|
|
31
|
+
await rename(temporary, join(home, name));
|
|
32
|
+
}
|
|
33
|
+
export function readEnrolment() {
|
|
34
|
+
return readHome(ENROLMENT);
|
|
35
|
+
}
|
|
36
|
+
export function writeEnrolment(enrolment) {
|
|
37
|
+
return writeHome(ENROLMENT, enrolment);
|
|
38
|
+
}
|
|
39
|
+
export function readOwnAgent() {
|
|
40
|
+
return readHome(OWN_AGENT);
|
|
41
|
+
}
|
|
42
|
+
export function writeOwnAgent(ownAgent) {
|
|
43
|
+
return writeHome(OWN_AGENT, ownAgent);
|
|
44
|
+
}
|
|
45
|
+
export function forgetOwnAgent() {
|
|
46
|
+
return rm(join(kitHome(), OWN_AGENT), { force: true });
|
|
47
|
+
}
|
|
48
|
+
export async function readConfiguration() {
|
|
49
|
+
return { skills: (await readHome(CONFIGURATION))?.skills !== false };
|
|
50
|
+
}
|
|
51
|
+
export function writeConfiguration(configuration) {
|
|
52
|
+
return writeHome(CONFIGURATION, configuration);
|
|
24
53
|
}
|