@agentshouse/kit 0.2.1-c5d36e97b0fea → 0.2.1-c5d51f1e4ce19

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 (89) hide show
  1. package/README.md +25 -24
  2. package/dist/agents.js +73 -7
  3. package/dist/api.js +6 -2
  4. package/dist/bootstrap.json +4 -4
  5. package/dist/chatgpt.js +32 -0
  6. package/dist/clis.js +42 -9
  7. package/dist/conversations.js +29 -9
  8. package/dist/copies.js +36 -26
  9. package/dist/end.js +17 -3
  10. package/dist/enrol-main.js +7 -1
  11. package/dist/events.js +3 -0
  12. package/dist/files.js +7 -1
  13. package/dist/git.js +11 -8
  14. package/dist/home.js +81 -6
  15. package/dist/house-cli.js +291 -0
  16. package/dist/house-main.js +7 -188
  17. package/dist/instructions.js +7 -31
  18. package/dist/kit-cli.js +68 -0
  19. package/dist/kit-main.js +8 -22
  20. package/dist/login.js +102 -21
  21. package/dist/parts.js +34 -16
  22. package/dist/placement.js +48 -0
  23. package/dist/relay.js +12 -2
  24. package/dist/resident.js +15 -6
  25. package/dist/service-main.js +9 -0
  26. package/dist/service.js +33 -0
  27. package/dist/skills.js +81 -12
  28. package/dist/stream.js +66 -15
  29. package/dist/transcription.js +3 -3
  30. package/dist/update.js +28 -6
  31. package/dist/wire.js +6 -0
  32. package/how-we-work.md +13 -3
  33. package/package.json +3 -2
  34. package/skills/ask-matt/SKILL.md +17 -11
  35. package/skills/ask-matt/agents/openai.yaml +1 -0
  36. package/skills/code-review/SKILL.md +5 -5
  37. package/skills/codebase-design/DESIGN-IT-TWICE.md +1 -1
  38. package/skills/codebase-design/SKILL.md +0 -2
  39. package/skills/diagnosing-bugs/SKILL.md +2 -4
  40. package/skills/domain-modeling/{CONTEXT-FORMAT.md → GLOSSARY-FORMAT.md} +10 -10
  41. package/skills/domain-modeling/SKILL.md +12 -14
  42. package/skills/grill-me/SKILL.md +1 -2
  43. package/skills/grill-me/agents/openai.yaml +1 -0
  44. package/skills/grill-with-docs/SKILL.md +1 -2
  45. package/skills/grill-with-docs/agents/openai.yaml +1 -0
  46. package/skills/grilling/SKILL.md +2 -2
  47. package/skills/handoff/SKILL.md +2 -3
  48. package/skills/handoff/agents/openai.yaml +1 -0
  49. package/skills/implement/SKILL.md +5 -4
  50. package/skills/implement/agents/openai.yaml +1 -0
  51. package/skills/implement-spec/SKILL.md +40 -0
  52. package/skills/implement-spec/agents/openai.yaml +5 -0
  53. package/skills/improve-codebase-architecture/SKILL.md +6 -7
  54. package/skills/improve-codebase-architecture/agents/openai.yaml +1 -0
  55. package/skills/pr/CREDITS.md +3 -0
  56. package/skills/pr/LICENSE +21 -0
  57. package/skills/pr/SKILL.md +170 -0
  58. package/skills/pr/agents/openai.yaml +3 -0
  59. package/skills/prototype/SKILL.md +0 -2
  60. package/skills/research/SKILL.md +0 -2
  61. package/skills/retro/LICENSE +21 -0
  62. package/skills/retro/SKILL.md +44 -0
  63. package/skills/retro/agents/openai.yaml +5 -0
  64. package/skills/tdd/SKILL.md +2 -4
  65. package/skills/teach/SKILL.md +3 -4
  66. package/skills/teach/agents/openai.yaml +1 -0
  67. package/skills/to-questionnaire/SKILL.md +1 -2
  68. package/skills/to-questionnaire/agents/openai.yaml +1 -0
  69. package/skills/to-spec/SKILL.md +1 -2
  70. package/skills/to-spec/agents/openai.yaml +1 -0
  71. package/skills/to-tickets/SKILL.md +3 -4
  72. package/skills/to-tickets/agents/openai.yaml +1 -0
  73. package/skills/triage/AGENT-BRIEF.md +207 -0
  74. package/skills/triage/LICENSE +21 -0
  75. package/skills/triage/OUT-OF-SCOPE.md +105 -0
  76. package/skills/triage/SKILL.md +112 -0
  77. package/skills/triage/agents/openai.yaml +5 -0
  78. package/skills/wait-what/SKILL.md +2 -3
  79. package/skills/wait-what/agents/openai.yaml +1 -0
  80. package/skills/wayfinder/SKILL.md +5 -6
  81. package/skills/wayfinder/agents/openai.yaml +1 -0
  82. package/skills/wizard/LICENSE +21 -0
  83. package/skills/wizard/SKILL.md +44 -0
  84. package/skills/wizard/agents/openai.yaml +3 -0
  85. package/skills/wizard/template.sh +223 -0
  86. package/skills/writing-for-agents/SKILL.md +0 -2
  87. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  88. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  89. /package/skills/{resolving-merge-conflicts → implement-spec}/LICENSE +0 -0
package/dist/copies.js CHANGED
@@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto';
3
3
  import { mkdir, readFile, readdir, realpath, rename, rm, rmdir, stat, writeFile } from 'node:fs/promises';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { finalRefusal, HouseRefusal, retrying } from './api.js';
6
- import { blobId, blobText, git, HOUSE_REF, importCommits, localCopies, revision, run, succeeded, tree, } from './git.js';
6
+ import { blobId, blobText, git, isText, HOUSE_REF, importCommits, localCopies, revision, run, succeeded, tree, } from './git.js';
7
7
  import { kitHome } from './home.js';
8
8
  import { operationId } from './operation.js';
9
9
  import { refusalOf } from './refusals.js';
@@ -29,6 +29,10 @@ const REBASED = new Set([
29
29
  function rebaseFixes(refusal) {
30
30
  return refusal.code === 'edit_conflict' && refusal.conflicts.some((code) => REBASED.has(code));
31
31
  }
32
+ function textOnly(paths) {
33
+ const [named, them] = paths.length === 1 ? [`${paths[0]} is`, 'it'] : [`${paths.join(', ')} are`, 'them'];
34
+ return `${named} not text, and house push sends text only; remove ${them} from the commit and place ${them} with house upload`;
35
+ }
32
36
  function short(commit) {
33
37
  return commit.slice(0, 12);
34
38
  }
@@ -42,7 +46,7 @@ function expiredAuthority(error) {
42
46
  return undefined;
43
47
  try {
44
48
  const { error: refused } = JSON.parse(error.text);
45
- return refused?.code === 'position_expired' && typeof refused.authority === 'string' ? refused.authority : undefined;
49
+ return refused?.code === 'expired' && typeof refused.authority === 'string' ? refused.authority : undefined;
46
50
  }
47
51
  catch {
48
52
  return undefined;
@@ -168,7 +172,7 @@ export class LocalCopies {
168
172
  house;
169
173
  copies = new Map();
170
174
  queues = new Map();
171
- selecting = Promise.resolve();
175
+ answering = Promise.resolve();
172
176
  constructor(house) {
173
177
  this.house = house;
174
178
  this.owner = {
@@ -200,39 +204,39 @@ export class LocalCopies {
200
204
  }
201
205
  }
202
206
  async mapping() {
203
- await this.selecting;
207
+ await this.answering;
204
208
  return [...this.copies.values()]
205
209
  .filter((copy) => copy.active)
206
210
  .map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
207
211
  .sort();
208
212
  }
209
- async select() {
210
- const selected = await this.selected(() => this.fetched(false));
211
- if (selected.refused)
212
- logged(selected.text);
213
+ async connected() {
214
+ const copied = await this.answered(() => this.fetched(false));
215
+ if (copied.refused)
216
+ logged(copied.text);
213
217
  }
214
218
  fetch() {
215
- return this.selected(() => this.fetched(true));
219
+ return this.answered(() => this.fetched(true));
216
220
  }
217
- selected(work) {
218
- const selected = this.selecting.then(work);
219
- this.selecting = selected.catch(() => undefined);
220
- return selected;
221
+ answered(work) {
222
+ const answered = this.answering.then(work);
223
+ this.answering = answered.catch(() => undefined);
224
+ return answered;
221
225
  }
222
226
  async fetched(refreshing) {
223
- const { local_copy: chosen } = await this.house.post('/kit/local-copy/selection', {});
224
- const selected = new Map(chosen.rooms.map((room) => [`rooms/${room.room_ref}`, room.room_handle]));
225
- return this.holding([...new Set([...this.copies.keys(), ...selected.keys()])].sort(), async () => {
227
+ const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', {});
228
+ const synced = new Map(answer.rooms.map((room) => [`rooms/${room.room_ref}`, room.room_handle]));
229
+ return this.holding([...new Set([...this.copies.keys(), ...synced.keys()])].sort(), async () => {
226
230
  const said = [];
227
231
  let refused = false;
228
232
  for (const copy of this.copies.values()) {
229
- if (selected.has(copy.key) || !copy.active)
233
+ if (synced.has(copy.key) || !copy.active)
230
234
  continue;
231
235
  await this.save({ ...copy, active: false });
232
236
  said.push(`${rootOf(copy.room_handle)}: no longer synced to this Environment; its files stay`);
233
237
  }
234
238
  const current = [];
235
- for (const [key, handle] of selected) {
239
+ for (const [key, handle] of synced) {
236
240
  try {
237
241
  const held = this.copies.get(key);
238
242
  if (held === undefined || !(await exists(held.repository))) {
@@ -572,7 +576,13 @@ export class LocalCopies {
572
576
  const after = await tree(copy.repository, commit);
573
577
  const moved = movedPaths(copy.moves, before, after);
574
578
  const arrived = new Set(moved.values());
575
- const text = (entry, path) => blobText(copy.repository, entry, path);
579
+ const binary = [];
580
+ for (const [path, entry] of after)
581
+ if (before.get(path) !== entry && !(await isText(copy.repository, entry)))
582
+ binary.push(path);
583
+ if (binary.length > 0)
584
+ throw new Error(textOnly(binary));
585
+ const text = (entry) => blobText(copy.repository, entry);
576
586
  const changes = [];
577
587
  for (const path of [...new Set([...before.keys(), ...after.keys()])].sort()) {
578
588
  if (arrived.has(path))
@@ -585,7 +595,7 @@ export class LocalCopies {
585
595
  changes.push({ op: 'rename', path: `${root}/${path}`, to: `${root}/${to}`, base: known });
586
596
  const moving = after.get(to);
587
597
  if (moving !== prior) {
588
- changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving, to) });
598
+ changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving) });
589
599
  }
590
600
  continue;
591
601
  }
@@ -594,9 +604,9 @@ export class LocalCopies {
594
604
  if (next === undefined)
595
605
  changes.push({ op: 'remove', path: `${root}/${path}`, base: known });
596
606
  else if (prior === undefined)
597
- changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next, path) });
607
+ changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next) });
598
608
  else
599
- changes.push(...changed(`${root}/${path}`, path, known, await text(prior, path), await text(next, path)));
609
+ changes.push(...changed(`${root}/${path}`, path, known, await text(prior), await text(next)));
600
610
  }
601
611
  return changes;
602
612
  }
@@ -623,7 +633,7 @@ export class LocalCopies {
623
633
  const { upload, ...unstaged } = copy.push;
624
634
  return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
625
635
  }
626
- if (outcome.refused.code === 'operation_expired')
636
+ if (outcome.refused.code === 'expired')
627
637
  return this.recovered(copy);
628
638
  const rebased = rebaseFixes(outcome.refused);
629
639
  let settled = await this.save(without(copy));
@@ -663,7 +673,7 @@ export class LocalCopies {
663
673
  written.push({
664
674
  path: `${root}/${path}`,
665
675
  revision: entry === undefined ? null : (revisionAt(current, current.house, path) ?? null),
666
- content: entry === undefined ? null : await blobText(copy.repository, entry, path),
676
+ content: entry === undefined ? null : await blobText(copy.repository, entry),
667
677
  });
668
678
  }
669
679
  return this.finished(await this.save({ ...current, push: { ...push, state: 'accepted', written } }));
@@ -707,7 +717,7 @@ export class LocalCopies {
707
717
  {
708
718
  path: `${rootOf(copy.room_handle)}/${path}`,
709
719
  revision: entry === undefined ? null : (revisionAt(copy, copy.house, path) ?? null),
710
- content: entry === undefined ? null : await blobText(copy.repository, entry, path),
720
+ content: entry === undefined ? null : await blobText(copy.repository, entry),
711
721
  },
712
722
  ];
713
723
  }
@@ -752,7 +762,7 @@ export class LocalCopies {
752
762
  async landed(repository, changes, house, selected) {
753
763
  const text = async (path) => {
754
764
  const entry = house.get(relative(path));
755
- return entry === undefined ? undefined : blobText(repository, entry, path);
765
+ return entry === undefined ? undefined : blobText(repository, entry);
756
766
  };
757
767
  for (const change of changes) {
758
768
  const current = await text(change.path);
package/dist/end.js CHANGED
@@ -1,8 +1,22 @@
1
+ import { forgetRejected } from './home.js';
2
+ import { installCommand } from './placement.js';
1
3
  import { killDescendants } from './scope.js';
2
4
  export const REPLACED = 'another Kit started for this Environment';
3
- export const SIGN_IN_AGAIN = 'sign in again';
4
- export function end(reason) {
5
+ function exit(reason, status) {
5
6
  process.stderr.write(`kit: ${reason}\n`);
6
7
  killDescendants();
7
- process.exit(0);
8
+ process.exit(status);
9
+ }
10
+ export function end(reason) {
11
+ exit(reason, 0);
12
+ }
13
+ export function restart(reason) {
14
+ exit(reason, 1);
15
+ }
16
+ export function updateRequired(house) {
17
+ end(`This Kit is too old for House. Update it: ${installCommand(house)}`);
18
+ }
19
+ export async function signInAgain(rejected) {
20
+ await forgetRejected(rejected);
21
+ restart('sign in again');
8
22
  }
@@ -1,4 +1,10 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { writeEnrolment } from './home.js';
3
3
  const [house, environment] = process.argv.slice(2);
4
- await writeEnrolment({ house: house, environment: environment, credential: readFileSync(0, 'utf8').trim() });
4
+ const relay = process.env.HOUSE_KIT_RELAY || undefined;
5
+ await writeEnrolment({
6
+ house: house,
7
+ ...(relay === undefined ? {} : { relay }),
8
+ environment: environment,
9
+ credential: readFileSync(0, 'utf8').trim(),
10
+ });
package/dist/events.js CHANGED
@@ -512,6 +512,9 @@ class GrokReader extends Reader {
512
512
  return [];
513
513
  }
514
514
  }
515
+ export function answeredFailure(answer) {
516
+ return answer._meta?.jetbrains?.air?.sessionFailure?.title ?? null;
517
+ }
515
518
  export function readerFor(kind) {
516
519
  if (kind === 'claude-agent-acp')
517
520
  return new ClaudeReader();
package/dist/files.js CHANGED
@@ -1,7 +1,13 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { mkdir, writeFile } from 'node:fs/promises';
2
3
  import { join } from 'node:path';
3
4
  import { HouseRefusal, retrying } from './api.js';
4
5
  import { relayed } from './relay.js';
6
+ export function verified(bytes, sha256, name) {
7
+ if (createHash('sha256').update(bytes).digest('hex') !== sha256)
8
+ throw new Error(`${name} did not arrive whole; download it again`);
9
+ return bytes;
10
+ }
5
11
  export async function placeFiles(house, directory, files, signal) {
6
12
  const paths = [];
7
13
  for (const file of files) {
@@ -16,7 +22,7 @@ export async function placeFiles(house, directory, files, signal) {
16
22
  });
17
23
  await mkdir(folder, { recursive: true });
18
24
  const path = join(folder, file.name);
19
- await writeFile(path, Buffer.from(await answer.arrayBuffer()));
25
+ await writeFile(path, verified(Buffer.from(await answer.arrayBuffer()), file.sha256, file.name));
20
26
  paths.push(path);
21
27
  }
22
28
  return paths;
package/dist/git.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { isUtf8 } from 'node:buffer';
2
3
  import { createHash } from 'node:crypto';
3
4
  import { join } from 'node:path';
4
5
  import { withoutProxy } from './clis.js';
@@ -48,14 +49,16 @@ export async function tree(repository, commit) {
48
49
  }
49
50
  return entries;
50
51
  }
51
- export async function blobText(repository, object, path) {
52
- const bytes = (await run(repository, ['cat-file', 'blob', object])).stdout;
53
- try {
54
- return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
55
- }
56
- catch {
57
- throw new Error(`${path} is binary and House stores only text; remove it from the commit`);
58
- }
52
+ const TEXT_MAX_BYTES = 16 * 1024 * 1024;
53
+ async function blob(repository, object) {
54
+ return (await run(repository, ['cat-file', 'blob', object])).stdout;
55
+ }
56
+ export async function blobText(repository, object) {
57
+ return (await blob(repository, object)).toString('utf8');
58
+ }
59
+ export async function isText(repository, object) {
60
+ const bytes = await blob(repository, object);
61
+ return bytes.length <= TEXT_MAX_BYTES && isUtf8(bytes) && !bytes.includes(0);
59
62
  }
60
63
  export function blobId(content) {
61
64
  const bytes = Buffer.from(content, 'utf8');
package/dist/home.js CHANGED
@@ -1,13 +1,49 @@
1
- import { existsSync } from 'node:fs';
1
+ import { randomUUID } from 'node:crypto';
2
+ import { accessSync, chmodSync, constants, existsSync, linkSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync } from 'node:fs';
2
3
  import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
3
- import { homedir } from 'node:os';
4
+ import { homedir, tmpdir } from 'node:os';
4
5
  import { join } from 'node:path';
6
+ import { selectRelay } from './relay.js';
5
7
  const ENROLMENT = 'credential.json';
6
8
  const DISCONNECTED = 'disconnected.json';
7
9
  const CONFIGURATION = 'kit.json';
8
10
  const OWN_AGENT = 'own-agent.json';
11
+ const RESTART = 'restart';
12
+ let defaulted;
13
+ function writable(directory) {
14
+ try {
15
+ accessSync(directory, constants.W_OK | constants.X_OK);
16
+ return true;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ function ownTemporaryHome() {
23
+ const home = join(tmpdir(), `house-kit-${process.getuid()}`);
24
+ try {
25
+ mkdirSync(home, { mode: 0o700 });
26
+ }
27
+ catch (error) {
28
+ if (error.code !== 'EEXIST')
29
+ throw error;
30
+ }
31
+ const found = lstatSync(home);
32
+ if (found.isSymbolicLink())
33
+ throw new Error(`${home} is a symbolic link`);
34
+ if (!found.isDirectory())
35
+ throw new Error(`${home} is not a directory`);
36
+ if (found.uid !== process.getuid())
37
+ throw new Error(`${home} is not owned by this User`);
38
+ chmodSync(home, 0o700);
39
+ return home;
40
+ }
41
+ function defaultHome() {
42
+ const home = join(homedir(), '.house-kit');
43
+ return existsSync(home) || writable(homedir()) ? home : ownTemporaryHome();
44
+ }
9
45
  export function kitHome() {
10
- return process.env.HOUSE_KIT_HOME ?? join(homedir(), '.house-kit');
46
+ return process.env.HOUSE_KIT_HOME || (defaulted ??= defaultHome());
11
47
  }
12
48
  export function workspaceRoot() {
13
49
  return process.env.HOUSE_KIT_WORKSPACE ?? '/agents/house';
@@ -28,12 +64,15 @@ export async function readHome(name) {
28
64
  export async function writeHome(name, value) {
29
65
  const home = kitHome();
30
66
  await mkdir(home, { recursive: true, mode: 0o700 });
31
- const temporary = join(home, `${name}.${process.pid}`);
67
+ const temporary = join(home, `${name}.${randomUUID()}`);
32
68
  await writeFile(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600 });
33
69
  await rename(temporary, join(home, name));
34
70
  }
35
- export function readEnrolment() {
36
- return readHome(ENROLMENT);
71
+ export async function readEnrolment() {
72
+ const enrolment = await readHome(ENROLMENT);
73
+ if (enrolment !== null)
74
+ selectRelay(process.env.HOUSE_KIT_RELAY || enrolment.relay);
75
+ return enrolment;
37
76
  }
38
77
  export async function writeEnrolment(enrolment) {
39
78
  await writeHome(ENROLMENT, enrolment);
@@ -42,13 +81,49 @@ export async function writeEnrolment(enrolment) {
42
81
  export function readDisconnected() {
43
82
  return readHome(DISCONNECTED);
44
83
  }
84
+ export function forgetDisconnected() {
85
+ return rm(join(kitHome(), DISCONNECTED), { force: true });
86
+ }
45
87
  export async function forgetEnrolment({ house, environment }) {
46
88
  await writeHome(DISCONNECTED, { house, environment });
47
89
  await rm(join(kitHome(), ENROLMENT), { force: true });
48
90
  }
91
+ export async function forgetRejected(rejected) {
92
+ const held = await readHome(ENROLMENT);
93
+ if (held?.credential !== rejected)
94
+ return;
95
+ await writeHome(DISCONNECTED, { house: held.house, environment: held.environment });
96
+ const enrolment = join(kitHome(), ENROLMENT);
97
+ const taken = `${enrolment}.${process.pid}`;
98
+ try {
99
+ renameSync(enrolment, taken);
100
+ }
101
+ catch (error) {
102
+ if (error.code === 'ENOENT')
103
+ return;
104
+ throw error;
105
+ }
106
+ if (JSON.parse(readFileSync(taken, 'utf8')).credential !== rejected) {
107
+ try {
108
+ linkSync(taken, enrolment);
109
+ }
110
+ catch (error) {
111
+ if (error.code !== 'EEXIST')
112
+ throw error;
113
+ }
114
+ rmSync(join(kitHome(), DISCONNECTED), { force: true });
115
+ }
116
+ rmSync(taken);
117
+ }
49
118
  export function enrolled() {
50
119
  return existsSync(join(kitHome(), ENROLMENT));
51
120
  }
121
+ export function restartAsked() {
122
+ return existsSync(join(kitHome(), RESTART));
123
+ }
124
+ export function forgetRestart() {
125
+ return rm(join(kitHome(), RESTART), { force: true });
126
+ }
52
127
  export function readOwnAgent() {
53
128
  return readHome(OWN_AGENT);
54
129
  }