@agentshouse/kit 0.2.1-c3cf4b145dcda → 0.2.1-c3db1f18aed8f

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.
Files changed (94) hide show
  1. package/README.md +24 -23
  2. package/dist/acp.js +7 -97
  3. package/dist/agents.js +27 -6
  4. package/dist/api.js +9 -5
  5. package/dist/bootstrap.json +4 -4
  6. package/dist/bridge.js +11 -37
  7. package/dist/chatgpt.js +26 -0
  8. package/dist/clis.js +86 -14
  9. package/dist/codex-main.js +58 -0
  10. package/dist/conversations.js +56 -55
  11. package/dist/copies.js +145 -110
  12. package/dist/end.js +18 -4
  13. package/dist/enrol-main.js +7 -1
  14. package/dist/events.js +10 -76
  15. package/dist/files.js +7 -1
  16. package/dist/git.js +11 -8
  17. package/dist/home.js +78 -6
  18. package/dist/house-main.js +183 -56
  19. package/dist/instructions.js +9 -32
  20. package/dist/kit-main.js +53 -8
  21. package/dist/login.js +93 -107
  22. package/dist/logout.js +14 -14
  23. package/dist/parts.js +34 -16
  24. package/dist/placement.js +48 -0
  25. package/dist/relay.js +12 -2
  26. package/dist/resident.js +35 -24
  27. package/dist/scope.js +203 -0
  28. package/dist/service-main.js +9 -0
  29. package/dist/service.js +33 -0
  30. package/dist/sign-in.js +1 -1
  31. package/dist/skills.js +80 -12
  32. package/dist/stream.js +9 -4
  33. package/dist/transcription.js +3 -3
  34. package/dist/update.js +28 -6
  35. package/dist/wire.js +6 -0
  36. package/dist/work.js +348 -0
  37. package/how-we-work.md +11 -3
  38. package/package.json +3 -2
  39. package/skills/ask-matt/SKILL.md +17 -11
  40. package/skills/ask-matt/agents/openai.yaml +1 -0
  41. package/skills/code-review/SKILL.md +5 -5
  42. package/skills/codebase-design/DESIGN-IT-TWICE.md +1 -1
  43. package/skills/codebase-design/SKILL.md +0 -2
  44. package/skills/diagnosing-bugs/SKILL.md +2 -4
  45. package/skills/domain-modeling/{CONTEXT-FORMAT.md → GLOSSARY-FORMAT.md} +10 -10
  46. package/skills/domain-modeling/SKILL.md +12 -14
  47. package/skills/grill-me/SKILL.md +1 -2
  48. package/skills/grill-me/agents/openai.yaml +1 -0
  49. package/skills/grill-with-docs/SKILL.md +1 -2
  50. package/skills/grill-with-docs/agents/openai.yaml +1 -0
  51. package/skills/grilling/SKILL.md +2 -2
  52. package/skills/handoff/SKILL.md +2 -3
  53. package/skills/handoff/agents/openai.yaml +1 -0
  54. package/skills/implement/SKILL.md +5 -4
  55. package/skills/implement/agents/openai.yaml +1 -0
  56. package/skills/implement-spec/SKILL.md +40 -0
  57. package/skills/implement-spec/agents/openai.yaml +5 -0
  58. package/skills/improve-codebase-architecture/SKILL.md +6 -7
  59. package/skills/improve-codebase-architecture/agents/openai.yaml +1 -0
  60. package/skills/pr/CREDITS.md +3 -0
  61. package/skills/pr/LICENSE +21 -0
  62. package/skills/pr/SKILL.md +170 -0
  63. package/skills/pr/agents/openai.yaml +3 -0
  64. package/skills/prototype/SKILL.md +0 -2
  65. package/skills/research/SKILL.md +0 -2
  66. package/skills/retro/LICENSE +21 -0
  67. package/skills/retro/SKILL.md +44 -0
  68. package/skills/retro/agents/openai.yaml +5 -0
  69. package/skills/tdd/SKILL.md +2 -4
  70. package/skills/teach/SKILL.md +3 -4
  71. package/skills/teach/agents/openai.yaml +1 -0
  72. package/skills/to-questionnaire/SKILL.md +1 -2
  73. package/skills/to-questionnaire/agents/openai.yaml +1 -0
  74. package/skills/to-spec/SKILL.md +1 -2
  75. package/skills/to-spec/agents/openai.yaml +1 -0
  76. package/skills/to-tickets/SKILL.md +3 -4
  77. package/skills/to-tickets/agents/openai.yaml +1 -0
  78. package/skills/triage/AGENT-BRIEF.md +207 -0
  79. package/skills/triage/LICENSE +21 -0
  80. package/skills/triage/OUT-OF-SCOPE.md +105 -0
  81. package/skills/triage/SKILL.md +112 -0
  82. package/skills/triage/agents/openai.yaml +5 -0
  83. package/skills/wait-what/SKILL.md +2 -3
  84. package/skills/wait-what/agents/openai.yaml +1 -0
  85. package/skills/wayfinder/SKILL.md +5 -6
  86. package/skills/wayfinder/agents/openai.yaml +1 -0
  87. package/skills/wizard/LICENSE +21 -0
  88. package/skills/wizard/SKILL.md +44 -0
  89. package/skills/wizard/agents/openai.yaml +3 -0
  90. package/skills/wizard/template.sh +223 -0
  91. package/skills/writing-for-agents/SKILL.md +0 -2
  92. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  93. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  94. /package/skills/{resolving-merge-conflicts → implement-spec}/LICENSE +0 -0
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
3
+ import { connect } from 'node:net';
4
+ const KIT_ANSWER = '"house-kit-';
5
+ const SYNC = 'house-kit/sync';
6
+ function lines(each) {
7
+ let buffer = '';
8
+ return (chunk) => {
9
+ buffer += chunk;
10
+ for (let index = buffer.indexOf('\n'); index >= 0; index = buffer.indexOf('\n')) {
11
+ each(buffer.slice(0, index + 1));
12
+ buffer = buffer.slice(index + 1);
13
+ }
14
+ };
15
+ }
16
+ function answersKit(line) {
17
+ if (!line.includes(KIT_ANSWER))
18
+ return false;
19
+ try {
20
+ const message = JSON.parse(line);
21
+ return message.method === undefined && typeof message.id === 'string' && message.id.startsWith(KIT_ANSWER.slice(1));
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ const args = process.argv.slice(2);
28
+ const serving = args[0] === 'app-server';
29
+ if (process.env.HOUSE_KIT_SANDBOX === '1' && serving)
30
+ args.push('--disable', 'memories');
31
+ const codex = spawn(process.env.HOUSE_KIT_CODEX, args, { stdio: ['pipe', 'pipe', 'inherit'] });
32
+ const stream = serving ? connect(process.env.HOUSE_KIT_CODEX_STREAM) : null;
33
+ stream?.on('error', () => undefined);
34
+ process.stdin.setEncoding('utf8').on('data', lines((line) => codex.stdin.write(line)));
35
+ process.stdin.on('end', () => codex.stdin.end());
36
+ stream?.setEncoding('utf8').on('data', lines((line) => {
37
+ const message = JSON.parse(line);
38
+ if (message.method === SYNC)
39
+ stream.write(`${JSON.stringify({ jsonrpc: '2.0', id: message.id, result: {} })}\n`);
40
+ else
41
+ codex.stdin.write(line);
42
+ }));
43
+ codex.stdout.setEncoding('utf8').on('data', lines((line) => {
44
+ stream?.write(line);
45
+ if (answersKit(line) || process.stdout.write(line))
46
+ return;
47
+ codex.stdout.pause();
48
+ process.stdout.once('drain', () => codex.stdout.resume());
49
+ }));
50
+ codex.stdin.on('error', () => undefined);
51
+ for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP'])
52
+ process.on(signal, () => codex.kill(signal));
53
+ codex.on('exit', (code, signal) => {
54
+ if (signal === null)
55
+ process.exit(code ?? 1);
56
+ process.removeAllListeners(signal);
57
+ process.kill(process.pid, signal);
58
+ });
@@ -3,7 +3,7 @@ import { createHash, randomUUID } from 'node:crypto';
3
3
  import { mkdir } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
5
  import { unlessAborted } from './abort.js';
6
- import { killMarked, processes, startAdapter } from './acp.js';
6
+ import { startAdapter } from './acp.js';
7
7
  import { openBridge } from './bridge.js';
8
8
  import { CLIS } from './clis.js';
9
9
  import { readerFor } from './events.js';
@@ -11,12 +11,13 @@ import { placeFiles, uploadBytes } from './files.js';
11
11
  import { agentBase } from './home.js';
12
12
  import { instructions } from './instructions.js';
13
13
  import { Parts } from './parts.js';
14
+ import { killMarked, Scope } from './scope.js';
14
15
  import { holdSecretInput } from './secret-input.js';
15
16
  import { createHowWeWork } from './skills.js';
16
17
  import { transcribe } from './transcription.js';
18
+ import { workFor } from './work.js';
17
19
  const KILLED = 'the conversation was killed';
18
- // A process a conversation left running reports nothing when it exits, so the Kit looks again every five seconds while one runs.
19
- const PROCESS_RECHECK_MS = 5000;
20
+ export const CONVERSATION_INPUTS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
20
21
  function causeOf(error) {
21
22
  return error instanceof Error ? error.message : String(error);
22
23
  }
@@ -29,9 +30,6 @@ function logged(error) {
29
30
  function marker(bridge) {
30
31
  return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
31
32
  }
32
- function stop(bridge) {
33
- killMarked(marker(bridge));
34
- }
35
33
  class Turn {
36
34
  id = randomUUID();
37
35
  parts;
@@ -53,7 +51,7 @@ class Conversation {
53
51
  turn = null;
54
52
  commands = null;
55
53
  context = null;
56
- busy = new Set();
54
+ leftovers = new Set();
57
55
  waiting = [];
58
56
  sending = null;
59
57
  subagents = new Map();
@@ -86,9 +84,10 @@ export class Conversations {
86
84
  carrying = 0;
87
85
  reporting = 0;
88
86
  watched = false;
89
- recheck = null;
90
87
  constructor(kit) {
91
88
  this.kit = kit;
89
+ // A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
90
+ setInterval(() => this.look(), 2_000);
92
91
  }
93
92
  input(input) {
94
93
  this.carrying++;
@@ -126,22 +125,27 @@ export class Conversations {
126
125
  for (const conversation of this.conversations.values()) {
127
126
  const turn = conversation.turn;
128
127
  const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
129
- if (conversation.busy.size > 0 || conversation.open > (asking ? 1 : 0))
128
+ if (conversation.open > (asking ? 1 : 0))
130
129
  return false;
131
130
  }
132
- for (const conversation of this.conversations.values()) {
133
- const running = conversation.running;
134
- if (running === null || running.baseline === null)
135
- continue;
136
- if ([...processes(marker(running.bridge))].every((found) => running.baseline.has(found)))
137
- continue;
138
- this.recheck ??= setTimeout(() => {
139
- this.recheck = null;
140
- this.kit.changed();
141
- }, PROCESS_RECHECK_MS);
131
+ const conversations = [...this.conversations.values()];
132
+ if (conversations.some((conversation) => conversation.running?.work.busy()))
142
133
  return false;
134
+ return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
135
+ }
136
+ quiet() {
137
+ return this.idle() && [...this.conversations.values()].every((conversation) => conversation.open === 0);
138
+ }
139
+ look() {
140
+ for (const conversation of this.conversations.values()) {
141
+ for (const scope of conversation.leftovers) {
142
+ if (!scope.empty())
143
+ continue;
144
+ scope.close();
145
+ conversation.leftovers.delete(scope);
146
+ }
143
147
  }
144
- return true;
148
+ this.kit.changed();
145
149
  }
146
150
  opened() {
147
151
  this.watched = false;
@@ -295,11 +299,13 @@ export class Conversations {
295
299
  }
296
300
  halt(conversation) {
297
301
  if (conversation.opening !== null)
298
- stop(conversation.opening);
302
+ killMarked(marker(conversation.opening));
303
+ for (const scope of conversation.leftovers)
304
+ scope.kill();
299
305
  if (conversation.running === null)
300
306
  return;
301
307
  conversation.running.killed = true;
302
- stop(conversation.running.bridge);
308
+ conversation.running.scope.kill();
303
309
  }
304
310
  async kill(conversation) {
305
311
  const running = conversation.running;
@@ -350,22 +356,30 @@ export class Conversations {
350
356
  throw new Error(KILLED);
351
357
  }
352
358
  conversation.opening = bridge;
359
+ const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
360
+ const scope = new Scope(marker(bridge), randomUUID(), cli);
353
361
  try {
354
362
  await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
355
- await CLIS[route.kind].allowHouse();
356
- conversation.busy.clear();
357
- const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (message) => this.read(conversation, reader, message), bridge.env);
363
+ await CLIS[route.kind].allowHouse(bridge.env.PATH);
364
+ const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (message) => {
365
+ work.read(message);
366
+ this.read(conversation, reader, message);
367
+ }, { ...bridge.env, ...(await work.open(cli)) }, undefined, scope);
358
368
  void adapter.exited.then(() => bridge.close());
359
369
  const agent = adapter.connection.agent;
360
370
  const cwd = route.working_directory;
361
- const _meta = CLIS[route.kind].sessionMeta;
371
+ const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
362
372
  const opened = sessionId === null
363
373
  ? await agent.request('session/new', { cwd, mcpServers: [], _meta })
364
374
  : { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta })), sessionId };
365
375
  reader.session = opened.sessionId;
366
- const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, baseline: null };
376
+ if (sessionId === null && CLIS[route.kind].renames) {
377
+ const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
378
+ await agent.request('session/prompt', { sessionId: opened.sessionId, prompt });
379
+ }
380
+ const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
367
381
  const options = await this.launchSettings(running, route, opened.configOptions ?? []);
368
- running.baseline = processes(marker(bridge));
382
+ void work.settled().then(() => scope.record());
369
383
  conversation.running = running;
370
384
  conversation.session = running.sessionId;
371
385
  this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
@@ -376,7 +390,9 @@ export class Conversations {
376
390
  return running.sessionId;
377
391
  }
378
392
  catch (error) {
379
- stop(bridge);
393
+ scope.kill();
394
+ work.close();
395
+ conversation.leftovers.add(scope);
380
396
  bridge.close();
381
397
  throw conversation.kills > 0 ? new Error(KILLED) : error;
382
398
  }
@@ -409,17 +425,18 @@ export class Conversations {
409
425
  const running = conversation.running;
410
426
  if (running?.adapter !== adapter)
411
427
  return;
412
- this.detach(conversation);
428
+ this.detach(conversation, running);
413
429
  if (conversation.turn !== null) {
414
430
  this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
415
431
  }
416
432
  this.next(conversation);
417
433
  this.kit.changed();
418
434
  }
419
- detach(conversation) {
435
+ detach(conversation, running) {
436
+ running.work.close();
437
+ conversation.leftovers.add(running.scope);
420
438
  conversation.running = null;
421
439
  conversation.commands = null;
422
- conversation.busy.clear();
423
440
  for (const [id, question] of this.questions) {
424
441
  if (question.conversation === conversation)
425
442
  this.questions.delete(id);
@@ -471,8 +488,6 @@ export class Conversations {
471
488
  turn.parts.measure(conversation.context);
472
489
  for (const part of conversation.held.splice(0))
473
490
  turn.parts.start(part);
474
- for (const event of conversation.running?.reader.turnStarted() ?? [])
475
- this.event(conversation, event);
476
491
  this.kit.changed();
477
492
  return turn;
478
493
  }
@@ -488,7 +503,7 @@ export class Conversations {
488
503
  if (conversation.turn === turn)
489
504
  conversation.turn = null;
490
505
  const body = Promise.allSettled(turn.uploads).then(() => ({
491
- parts: turn.parts.parts,
506
+ parts: turn.parts.saved(),
492
507
  context: turn.parts.context,
493
508
  ...failed,
494
509
  }));
@@ -506,8 +521,8 @@ export class Conversations {
506
521
  }
507
522
  else {
508
523
  if (conversation.open === 1 && conversation.running === running) {
509
- stop(running.bridge);
510
- this.detach(conversation);
524
+ running.scope.kill();
525
+ this.detach(conversation, running);
511
526
  }
512
527
  this.end(conversation, turn, { failed: causeOf(error) });
513
528
  }
@@ -592,20 +607,6 @@ export class Conversations {
592
607
  case 'options':
593
608
  this.options(conversation, event.options);
594
609
  return;
595
- case 'busy':
596
- if (event.running)
597
- conversation.busy.add(event.key);
598
- else
599
- conversation.busy.delete(event.key);
600
- this.kit.changed();
601
- return;
602
- case 'servers': {
603
- const running = conversation.running;
604
- if (running?.baseline)
605
- for (const found of processes(marker(running.bridge)))
606
- running.baseline.add(found);
607
- return;
608
- }
609
610
  case 'context':
610
611
  conversation.context = { used: event.used, window: event.window };
611
612
  conversation.turn?.parts.measure(conversation.context);
@@ -647,7 +648,7 @@ export class Conversations {
647
648
  turn.parts.plan(event.entries);
648
649
  return;
649
650
  case 'image':
650
- turn.uploads.push(this.image(conversation, turn, event.data, event.mediaType));
651
+ turn.uploads.push(this.image(conversation, turn, turn.parts.reserve(), event.data, event.mediaType));
651
652
  this.kit.changed();
652
653
  return;
653
654
  }
@@ -667,7 +668,7 @@ export class Conversations {
667
668
  else
668
669
  conversation.turn.parts.start(part);
669
670
  }
670
- async image(conversation, turn, data, mediaType) {
671
+ async image(conversation, turn, reserved, data, mediaType) {
671
672
  const bytes = Buffer.from(data, 'base64');
672
673
  const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
673
674
  try {
@@ -680,10 +681,10 @@ export class Conversations {
680
681
  const saved = declared.upload === undefined ? declared : await uploadBytes(declared.upload, bytes);
681
682
  if (saved.save.status !== 'saved')
682
683
  throw new Error(saved.save.failure ?? saved.save.status);
683
- turn.parts.start({ type: 'image', file: saved.version, name, media_type: mediaType });
684
+ turn.parts.arrive({ type: 'image', file: saved.version, name, media_type: mediaType }, reserved);
684
685
  }
685
686
  catch (error) {
686
- turn.parts.start({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` });
687
+ turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` }, reserved);
687
688
  }
688
689
  }
689
690
  }