@agentshouse/kit 0.2.1-c903d1429e34a → 0.2.1-c9a26041e119a

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 (83) hide show
  1. package/README.md +16 -15
  2. package/dist/agents.js +58 -4
  3. package/dist/api.js +5 -1
  4. package/dist/bootstrap.json +4 -4
  5. package/dist/chatgpt.js +26 -0
  6. package/dist/clis.js +42 -9
  7. package/dist/conversations.js +6 -5
  8. package/dist/copies.js +18 -8
  9. package/dist/end.js +4 -0
  10. package/dist/files.js +7 -1
  11. package/dist/git.js +11 -8
  12. package/dist/home.js +41 -4
  13. package/dist/house-main.js +165 -65
  14. package/dist/instructions.js +4 -29
  15. package/dist/kit-main.js +53 -8
  16. package/dist/login.js +39 -13
  17. package/dist/parts.js +34 -16
  18. package/dist/placement.js +48 -0
  19. package/dist/relay.js +1 -1
  20. package/dist/resident.js +8 -4
  21. package/dist/skills.js +81 -12
  22. package/dist/stream.js +62 -13
  23. package/dist/transcription.js +3 -3
  24. package/dist/update.js +28 -6
  25. package/dist/wire.js +6 -0
  26. package/how-we-work.md +13 -3
  27. package/package.json +2 -1
  28. package/skills/ask-matt/SKILL.md +17 -11
  29. package/skills/ask-matt/agents/openai.yaml +1 -0
  30. package/skills/code-review/SKILL.md +5 -5
  31. package/skills/codebase-design/DESIGN-IT-TWICE.md +1 -1
  32. package/skills/codebase-design/SKILL.md +0 -2
  33. package/skills/diagnosing-bugs/SKILL.md +2 -4
  34. package/skills/domain-modeling/{CONTEXT-FORMAT.md → GLOSSARY-FORMAT.md} +10 -10
  35. package/skills/domain-modeling/SKILL.md +12 -14
  36. package/skills/grill-me/SKILL.md +1 -2
  37. package/skills/grill-me/agents/openai.yaml +1 -0
  38. package/skills/grill-with-docs/SKILL.md +1 -2
  39. package/skills/grill-with-docs/agents/openai.yaml +1 -0
  40. package/skills/grilling/SKILL.md +2 -2
  41. package/skills/handoff/SKILL.md +2 -3
  42. package/skills/handoff/agents/openai.yaml +1 -0
  43. package/skills/implement/SKILL.md +5 -4
  44. package/skills/implement/agents/openai.yaml +1 -0
  45. package/skills/implement-spec/SKILL.md +40 -0
  46. package/skills/implement-spec/agents/openai.yaml +5 -0
  47. package/skills/improve-codebase-architecture/SKILL.md +6 -7
  48. package/skills/improve-codebase-architecture/agents/openai.yaml +1 -0
  49. package/skills/pr/CREDITS.md +3 -0
  50. package/skills/pr/LICENSE +21 -0
  51. package/skills/pr/SKILL.md +170 -0
  52. package/skills/pr/agents/openai.yaml +3 -0
  53. package/skills/prototype/SKILL.md +0 -2
  54. package/skills/research/SKILL.md +0 -2
  55. package/skills/retro/LICENSE +21 -0
  56. package/skills/retro/SKILL.md +44 -0
  57. package/skills/retro/agents/openai.yaml +5 -0
  58. package/skills/tdd/SKILL.md +2 -4
  59. package/skills/teach/SKILL.md +3 -4
  60. package/skills/teach/agents/openai.yaml +1 -0
  61. package/skills/to-questionnaire/SKILL.md +1 -2
  62. package/skills/to-questionnaire/agents/openai.yaml +1 -0
  63. package/skills/to-spec/SKILL.md +1 -2
  64. package/skills/to-spec/agents/openai.yaml +1 -0
  65. package/skills/to-tickets/SKILL.md +3 -4
  66. package/skills/to-tickets/agents/openai.yaml +1 -0
  67. package/skills/triage/AGENT-BRIEF.md +207 -0
  68. package/skills/triage/LICENSE +21 -0
  69. package/skills/triage/OUT-OF-SCOPE.md +105 -0
  70. package/skills/triage/SKILL.md +112 -0
  71. package/skills/triage/agents/openai.yaml +5 -0
  72. package/skills/wait-what/SKILL.md +2 -3
  73. package/skills/wait-what/agents/openai.yaml +1 -0
  74. package/skills/wayfinder/SKILL.md +5 -6
  75. package/skills/wayfinder/agents/openai.yaml +1 -0
  76. package/skills/wizard/LICENSE +21 -0
  77. package/skills/wizard/SKILL.md +44 -0
  78. package/skills/wizard/agents/openai.yaml +3 -0
  79. package/skills/wizard/template.sh +223 -0
  80. package/skills/writing-for-agents/SKILL.md +0 -2
  81. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  82. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  83. /package/skills/{resolving-merge-conflicts → implement-spec}/LICENSE +0 -0
@@ -0,0 +1,48 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { run } from './clis.js';
4
+ import { kitHome } from './home.js';
5
+ import { relayOrigin } from './relay.js';
6
+ export function service() {
7
+ if (process.platform !== 'linux' || !process.env.INVOCATION_ID)
8
+ return null;
9
+ return process.env.MANAGERPID ? 'user' : 'system';
10
+ }
11
+ export function throughSudo() {
12
+ return service() === 'system' && process.getuid() !== 0;
13
+ }
14
+ export async function updatesItself() {
15
+ if (service() === null)
16
+ return false;
17
+ if (!throughSudo())
18
+ return true;
19
+ // sudo answers at once, so ten seconds only bounds a stuck sudo.
20
+ return (await run('sudo', ['-n', 'true'], 10_000)).status === 0;
21
+ }
22
+ function installed() {
23
+ try {
24
+ return readFileSync(join(kitHome(), 'placement'), 'utf8').trim();
25
+ }
26
+ catch (error) {
27
+ if (error.code === 'ENOENT')
28
+ return null;
29
+ throw error;
30
+ }
31
+ }
32
+ function entries(house) {
33
+ const host = new URL(house).host;
34
+ const relay = relayOrigin();
35
+ if (relay === undefined)
36
+ return [`curl -fsSL ${host}/kit | sh`, `irm ${host}/kit.ps1 | iex`];
37
+ const entry = `${relay}/${host}/kit`;
38
+ return [`curl -fsSL ${entry} | HOUSE_KIT_RELAY=${relay} sh`, `$env:HOUSE_KIT_RELAY='${relay}'; irm ${entry}.ps1 | iex`];
39
+ }
40
+ export function installCommand(house) {
41
+ const [shell, windows] = entries(house);
42
+ const placement = installed();
43
+ if (placement === 'native')
44
+ return shell;
45
+ if (placement === 'server')
46
+ return `${shell} -s -- --linux`;
47
+ return `${shell} -s -- --container, or ${windows} on Windows`;
48
+ }
package/dist/relay.js CHANGED
@@ -2,7 +2,7 @@ let selected;
2
2
  export function selectRelay(relay) {
3
3
  selected = relay === undefined || relay === '' ? null : relay;
4
4
  }
5
- function relayOrigin() {
5
+ export function relayOrigin() {
6
6
  if (selected !== undefined)
7
7
  return selected ?? undefined;
8
8
  const ambient = process.env.HOUSE_KIT_RELAY;
package/dist/resident.js CHANGED
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url';
7
7
  import { Agents } from './agents.js';
8
8
  import { house } from './api.js';
9
9
  import { received } from './bridge.js';
10
- import { Conversations } from './conversations.js';
10
+ import { CONVERSATION_INPUTS, Conversations } from './conversations.js';
11
11
  import { LocalCopies } from './copies.js';
12
12
  import { restart } from './end.js';
13
13
  import { agentBase, enrolled, forgetRestart, kitHome, restartAsked } from './home.js';
@@ -84,7 +84,7 @@ export async function resident() {
84
84
  await placeSkillSet();
85
85
  await mkdir(agentBase(), { recursive: true });
86
86
  process.env.HOUSE_KIT_RESIDENT = String(process.pid);
87
- const agents = new Agents(house);
87
+ const agents = new Agents(house, () => changed());
88
88
  const copies = new LocalCopies(house);
89
89
  await copies.load();
90
90
  await installShim(join(kitHome(), 'shim', 'git'), 'git-main');
@@ -92,7 +92,7 @@ export async function resident() {
92
92
  await serveOwner(copies);
93
93
  let stream = null;
94
94
  let reported = false;
95
- const idle = () => conversations.idle() && signIns.idle();
95
+ const idle = () => conversations.idle() && signIns.idle() && agents.idle();
96
96
  const changed = () => {
97
97
  if (!idle()) {
98
98
  reported = false;
@@ -140,8 +140,12 @@ export async function resident() {
140
140
  signIns.start(arrived);
141
141
  else if (arrived.kind === 'update')
142
142
  updates.start(arrived);
143
- else
143
+ else if (CONVERSATION_INPUTS.has(arrived.kind))
144
144
  conversations.input(arrived);
145
+ else {
146
+ const refused = { refused: `This Kit does not know ${arrived.kind}; update the Kit` };
147
+ logged(house.deliver(`/kit/inputs/${arrived.input_id}/ack`, refused).finally(changed));
148
+ }
145
149
  }
146
150
  },
147
151
  });
package/dist/skills.js CHANGED
@@ -1,17 +1,31 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { readFileSync } from 'node:fs';
2
- import { cp, lstat, mkdir, readdir, realpath, rm, symlink, writeFile } from 'node:fs/promises';
3
+ import { cp, lstat, mkdir, readdir, realpath, rm, symlink } from 'node:fs/promises';
3
4
  import { homedir } from 'node:os';
4
5
  import { join, relative } from 'node:path';
5
6
  import { fileURLToPath } from 'node:url';
7
+ import { parse } from 'yaml';
6
8
  import { KIT_VERSION } from './clis.js';
7
9
  import { kitHome, readConfiguration, readHome, writeHome } from './home.js';
8
10
  import { refusalOf } from './refusals.js';
9
- export const HOW_WE_WORK = '/rooms/private/how-we-work.md';
11
+ const HOW_WE_WORK = '/rooms/private/how-we-work.md';
10
12
  const SKILL_SET = fileURLToPath(new URL('../skills/', import.meta.url));
11
13
  const HOW_WE_WORK_TEXT = readFileSync(new URL('../how-we-work.md', import.meta.url), 'utf8');
14
+ const SHIPPED_HOW_WE_WORK = [
15
+ 'ae4dda60da7b174f22dc5fa9bf07fc27c25864932a74230de4f9f509ddd91f41',
16
+ '0bdcd43e1603ba6673a3cae56d70bbf90657cbeefd124e91a47be2ca1cbd6c93',
17
+ '4106a87ec66da6f7b2a10e74d18e6559d68d5f4fe7415dae68f5c7bdb9081968',
18
+ 'ac814a2197038f037b657bb719340395f89f319ac9fcdd386629c3fe2cec68f1',
19
+ '29951ee394aad9979eecfed3e5dc32c2c86e2226b6a380db5dbd4f305f4d1156',
20
+ ];
12
21
  const RECORD = 'skill-set.json';
13
- const CREATED = 'how-we-work-created';
22
+ const WRITTEN = 'how-we-work.json';
14
23
  const UNWRITABLE = new Set(['room_not_found', 'operation_denied']);
24
+ const PRESENT = new Set(['path_exists']);
25
+ const CHANGED = new Set(['base_revision_stale', 'path_absent']);
26
+ const UNREADABLE = /^(?:\S+: )?(?:path_not_found|room_not_found|operation_denied)\b/;
27
+ const FAILED = /^exit: [1-9]\d*$/;
28
+ const CUT = /^stderr: shell: output_cut (\d+) of \d+ bytes; continue with: (.+)$/;
15
29
  function skillsDirectory() {
16
30
  return join(homedir(), '.agents', 'skills');
17
31
  }
@@ -49,6 +63,14 @@ async function install() {
49
63
  await writeHome(RECORD, { version: KIT_VERSION, names });
50
64
  }
51
65
  export async function placeSkillSet() {
66
+ try {
67
+ await replaceSkillSet();
68
+ }
69
+ catch (error) {
70
+ process.stderr.write(`kit: the skills could not be placed: ${error.message}\n`);
71
+ }
72
+ }
73
+ async function replaceSkillSet() {
52
74
  const accepted = (await readConfiguration()).skills;
53
75
  const installed = await readHome(RECORD);
54
76
  if (accepted && installed?.version === KIT_VERSION)
@@ -62,17 +84,64 @@ export async function placeSkillSet() {
62
84
  else
63
85
  await rm(join(kitHome(), RECORD), { force: true });
64
86
  }
87
+ function sha256(text) {
88
+ return createHash('sha256').update(text).digest('hex');
89
+ }
90
+ function cutOf(lines) {
91
+ const cut = CUT.exec(lines[0] ?? '');
92
+ return cut !== null && Buffer.byteLength(lines.slice(1).join('\n')) === Number(cut[1]) ? cut : null;
93
+ }
94
+ export async function readHowWeWork(bridge) {
95
+ let document = '';
96
+ for (let command = `cat ${HOW_WE_WORK}`; command !== undefined;) {
97
+ const read = await bridge.tool('shell', { command });
98
+ const text = read.content.map((content) => content.text).join('\n');
99
+ const lines = text.split('\n');
100
+ if (read.isError === true || (FAILED.test(lines[0] ?? '') && lines[1]?.startsWith('stderr: '))) {
101
+ const refusal = read.isError === true ? text : lines.slice(1).join('\n').replace(/^stderr: /gm, '');
102
+ if (UNREADABLE.test(refusal))
103
+ return '';
104
+ throw new Error(refusal);
105
+ }
106
+ const cut = cutOf(lines);
107
+ document += lines.slice(cut === null ? 0 : 1).join('\n');
108
+ command = cut?.[2];
109
+ }
110
+ return document;
111
+ }
112
+ async function currentRevision(bridge) {
113
+ const inspected = await bridge.tool('inspect', { path: HOW_WE_WORK });
114
+ const text = inspected.content.map((content) => content.text).join('\n');
115
+ if (inspected.isError === true)
116
+ throw new Error(text);
117
+ return parse(text).revisions[0].revision;
118
+ }
119
+ async function edit(bridge, change, kept) {
120
+ const edited = await bridge.tool('edit', { changes: [{ ...change, path: HOW_WE_WORK, content: HOW_WE_WORK_TEXT }] });
121
+ if (edited.isError !== true)
122
+ return 'written';
123
+ const text = edited.content.map((content) => content.text).join('\n');
124
+ const refusal = refusalOf(text);
125
+ if (UNWRITABLE.has(refusal?.code ?? ''))
126
+ return 'unwritable';
127
+ if (refusal?.code === 'edit_conflict' && refusal.conflicts.some((conflict) => kept.has(conflict)))
128
+ return 'kept';
129
+ throw new Error(text);
130
+ }
65
131
  export async function createHowWeWork(bridge) {
66
- if ((await readHome(RECORD)) === null || (await present(join(kitHome(), CREATED))))
132
+ if (!(await readConfiguration()).skills || (await readHome(RECORD))?.version === undefined)
133
+ return;
134
+ const current = sha256(HOW_WE_WORK_TEXT);
135
+ if ((await readHome(WRITTEN)) === current)
136
+ return;
137
+ const created = await edit(bridge, { op: 'create' }, PRESENT);
138
+ if (created === 'unwritable')
67
139
  return;
68
- const created = await bridge.tool('edit', { changes: [{ op: 'create', path: HOW_WE_WORK, content: HOW_WE_WORK_TEXT }] });
69
- if (created.isError === true) {
70
- const text = created.content.map((content) => content.text).join('\n');
71
- const refusal = refusalOf(text);
72
- if (UNWRITABLE.has(refusal?.code ?? ''))
140
+ if (created === 'kept') {
141
+ const base = await currentRevision(bridge);
142
+ const held = sha256(await readHowWeWork(bridge));
143
+ if (held !== current && SHIPPED_HOW_WE_WORK.includes(held) && (await edit(bridge, { op: 'replace', base }, CHANGED)) === 'unwritable')
73
144
  return;
74
- if (refusal?.code !== 'edit_conflict' || !refusal.conflicts.includes('path_exists'))
75
- throw new Error(text);
76
145
  }
77
- await writeFile(join(kitHome(), CREATED), '');
146
+ await writeHome(WRITTEN, current);
78
147
  }
package/dist/stream.js CHANGED
@@ -1,11 +1,42 @@
1
1
  import { subscribe } from 'node:diagnostics_channel';
2
2
  import { finalRefusal, HouseRefusal, retryDelay } from './api.js';
3
- import { end, REPLACED, signInAgain } from './end.js';
3
+ import { end, REPLACED, signInAgain, updateRequired } from './end.js';
4
4
  import { readEnrolment } from './home.js';
5
5
  import { relayed } from './relay.js';
6
+ import { KIT_WIRE, tooOld } from './wire.js';
6
7
  const PATH = '/kit/stream';
7
- const SUBPROTOCOL = 'house.kit.stream.1';
8
+ const SUBPROTOCOL = `house.kit.stream.${KIT_WIRE}`;
8
9
  const REFUSAL_HEADER = 'x-house-refusal';
10
+ // House beats each stream every thirty seconds, so two silent beats mean this Kit cannot hear House.
11
+ export const STREAM_SILENCE_MS = 60_000;
12
+ const SYSTEM_STREAM_CLOCK = {
13
+ later: (delay, callback) => setTimeout(callback, delay),
14
+ clear: (timer) => clearTimeout(timer),
15
+ };
16
+ export class StreamSilence {
17
+ timer = null;
18
+ silent;
19
+ clock;
20
+ interval;
21
+ constructor(silent, clock = SYSTEM_STREAM_CLOCK, interval = STREAM_SILENCE_MS) {
22
+ this.silent = silent;
23
+ this.clock = clock;
24
+ this.interval = interval;
25
+ }
26
+ heard() {
27
+ this.stop();
28
+ this.timer = this.clock.later(this.interval, () => {
29
+ this.timer = null;
30
+ this.silent();
31
+ });
32
+ }
33
+ stop() {
34
+ if (this.timer === null)
35
+ return;
36
+ this.clock.clear(this.timer);
37
+ this.timer = null;
38
+ }
39
+ }
9
40
  function streamUrl(house) {
10
41
  const url = new URL(PATH, house);
11
42
  url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
@@ -36,17 +67,10 @@ export function holdStream(handlers) {
36
67
  headers: { authorization: `Bearer ${credential}` },
37
68
  });
38
69
  let opened = false;
39
- opening.onopen = () => {
40
- opened = true;
41
- failures = 0;
42
- socket = opening;
43
- handlers.opened();
44
- };
45
- opening.onmessage = (event) => {
46
- handlers.frame(JSON.parse(String(event.data)));
47
- };
48
- opening.onerror = () => undefined;
49
- opening.onclose = (event) => {
70
+ let silence = null;
71
+ const closed = (event) => {
72
+ silence?.stop();
73
+ silence = null;
50
74
  if (socket === opening)
51
75
  socket = null;
52
76
  process.stderr.write(`kit: control stream closed ${event.code} ${event.reason}\n`);
@@ -56,11 +80,36 @@ export function holdStream(handlers) {
56
80
  void signInAgain(credential);
57
81
  return;
58
82
  }
83
+ if (refused !== null && tooOld(refused.text))
84
+ updateRequired(house);
59
85
  if (finalRefusal(refused))
60
86
  end(refused.message);
61
87
  // A closed socket reopens in one to three seconds, spread so Kits one restart closed return apart; an opening House refused as retryable or left unanswered backs off on the jittered curve.
62
88
  setTimeout(() => void connect(), opened ? 1000 + Math.random() * 2000 : retryDelay(failures++));
63
89
  };
90
+ opening.onopen = () => {
91
+ opened = true;
92
+ failures = 0;
93
+ socket = opening;
94
+ silence = new StreamSilence(() => {
95
+ if (socket !== opening)
96
+ return;
97
+ socket = null;
98
+ opening.onclose = null;
99
+ opening.close();
100
+ closed(new CloseEvent('close', { code: 1006, reason: 'heartbeat_missed' }));
101
+ });
102
+ silence.heard();
103
+ handlers.opened();
104
+ };
105
+ opening.onmessage = (event) => {
106
+ silence?.heard();
107
+ const frame = JSON.parse(String(event.data));
108
+ if (frame.type !== 'beat')
109
+ handlers.frame(frame);
110
+ };
111
+ opening.onerror = () => undefined;
112
+ opening.onclose = closed;
64
113
  };
65
114
  void connect();
66
115
  return {
@@ -49,10 +49,10 @@ async function executed(command, args, signal) {
49
49
  function present(path) {
50
50
  return stat(path).then(() => true, () => false);
51
51
  }
52
- async function cpus() {
52
+ async function threads() {
53
53
  const [quota, period] = (await readFile('/sys/fs/cgroup/cpu.max', 'utf8').catch(() => 'max')).split(' ');
54
54
  const allowed = quota === 'max' ? Infinity : Math.ceil(Number(quota) / Number(period));
55
- return Math.min(availableParallelism(), allowed);
55
+ return Math.max(1, Math.floor(Math.min(availableParallelism(), allowed) / 2));
56
56
  }
57
57
  async function sha256(path) {
58
58
  const hash = createHash('sha256');
@@ -140,7 +140,7 @@ async function transcribed(path, signal) {
140
140
  await executed(installed(pinned.decoder), ['-nostdin', '-hide_banner', '-loglevel', 'error', '-i', path, '-ar', '16000', '-ac', '1', '-c:a', 'pcm_s16le',
141
141
  '-f', 'segment', '-segment_time', String(CHUNK_SECONDS), join(work, '%05d.wav')], signal);
142
142
  const parts = (await readdir(work)).sort().flatMap((part) => ['-f', join(work, part)]);
143
- const text = await executed(installed(pinned.engine), ['-t', String(await cpus()), '-np', '-m', installed(MODEL), ...parts], signal);
143
+ const text = await executed(installed(pinned.engine), ['-t', String(await threads()), '-np', '-m', installed(MODEL), ...parts], signal);
144
144
  return text
145
145
  .split('\n')
146
146
  .map((line) => line.trim())
package/dist/update.js CHANGED
@@ -4,8 +4,8 @@ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
4
4
  import { homedir } from 'node:os';
5
5
  import { join } from 'node:path';
6
6
  import { run } from './clis.js';
7
- import { kitHome } from './home.js';
8
- const RECORD = 'outcome=$1; shift; bash "$0" --linux "$@"; printf %s "$?" > "$outcome"; systemctl start house-kit.service';
7
+ import { kitHome, workspaceRoot } from './home.js';
8
+ import { service, throughSudo } from './placement.js';
9
9
  function logged(error) {
10
10
  process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
11
11
  }
@@ -21,6 +21,21 @@ function outcome() {
21
21
  function unit(inputId) {
22
22
  return `house-kit-update-${inputId}`;
23
23
  }
24
+ function manager() {
25
+ return service() === 'user' ? ['--user'] : [];
26
+ }
27
+ function elevated() {
28
+ return throughSudo() ? ['sudo', '-n'] : [];
29
+ }
30
+ function account() {
31
+ return throughSudo() ? [`--uid=${process.getuid()}`, `--gid=${process.getgid()}`] : [];
32
+ }
33
+ function placement() {
34
+ return service() === 'user' ? ['--workspace', workspaceRoot()] : ['--linux'];
35
+ }
36
+ function record() {
37
+ return `outcome=$1; shift; bash "$0" "$@"; printf %s "$?" > "$outcome"; ${[...elevated(), 'systemctl', ...manager(), 'start', 'house-kit.service'].join(' ')}`;
38
+ }
24
39
  async function pendingInput() {
25
40
  try {
26
41
  return JSON.parse(await readFile(pending(), 'utf8')).input_id;
@@ -41,7 +56,7 @@ async function finished(inputId) {
41
56
  if (ack !== null)
42
57
  return ack;
43
58
  // systemctl answers at once, so ten seconds only bounds a stuck service manager.
44
- if ((await run('systemctl', ['is-active', '--quiet', unit(inputId)], 10_000)).status !== 0) {
59
+ if ((await run('systemctl', [...manager(), 'is-active', '--quiet', unit(inputId)], 10_000)).status !== 0) {
45
60
  return (await recorded()) ?? { refused: 'the Kit bootstrap did not finish' };
46
61
  }
47
62
  // The bootstrap runs for minutes, so checking each second sees its outcome within a second at little cost.
@@ -50,19 +65,26 @@ async function finished(inputId) {
50
65
  }
51
66
  function rerun(script, inputId, flags) {
52
67
  return new Promise((resolve) => {
53
- const child = spawn('systemd-run', [
68
+ const [command, ...args] = [
69
+ ...elevated(),
70
+ 'systemd-run',
71
+ ...manager(),
72
+ ...account(),
54
73
  '--wait',
55
74
  '--collect',
56
75
  '--quiet',
57
76
  `--unit=${unit(inputId)}`,
58
77
  `--setenv=HOME=${homedir()}`,
78
+ `--setenv=HOUSE_KIT_HOME=${kitHome()}`,
59
79
  '/bin/sh',
60
80
  '-c',
61
- RECORD,
81
+ record(),
62
82
  script,
63
83
  outcome(),
84
+ ...placement(),
64
85
  ...flags,
65
- ], { stdio: ['ignore', 'ignore', 'inherit'] });
86
+ ];
87
+ const child = spawn(command, args, { stdio: ['ignore', 'ignore', 'inherit'] });
66
88
  child.on('error', () => resolve({ refused: 'no service manager runs this Kit' }));
67
89
  child.on('close', () => resolve(null));
68
90
  });
package/dist/wire.js ADDED
@@ -0,0 +1,6 @@
1
+ import { refusalOf } from './refusals.js';
2
+ export const KIT_WIRE = 1;
3
+ export const KIT_WIRE_HEADER = 'x-house-kit-wire';
4
+ export function tooOld(text) {
5
+ return refusalOf(text)?.code === 'kit_update_required';
6
+ }
package/how-we-work.md CHANGED
@@ -4,20 +4,30 @@ A Room is where information people share is kept; it is not the place for
4
4
  private information. Work goes into the Room it belongs to, or the First Room
5
5
  when none is named.
6
6
 
7
+ Every House answer carries the `url` of what it names; send the User that link.
8
+
7
9
  The skills are already set up; never run `setup-matt-pocock-skills`.
8
10
 
9
11
  ## Where skill output goes
10
12
 
11
- - Glossary (`CONTEXT.md`): `glossary.md`
13
+ - Glossary (`GLOSSARY.md`): `glossary.md`
12
14
  - Decisions (ADRs): `decisions/NNNN-<slug>.md`
13
15
  - Plan (spec): `projects/<name>/plan.md`
14
16
  - Tasks (tickets): `projects/<name>/tasks/NN-<slug>.md`, with a
15
17
  `Status:` line (`needs-triage`, `needs-info`, `ready-for-agent`,
16
- `ready-for-human`, `wontfix`), a `Blocked by:` line and comments under
17
- `## Comments`
18
+ `ready-for-human`, `wontfix`, or `done` once implemented), a
19
+ `Category:` line (`bug`, `enhancement`), a `Blocked by:` line and comments
20
+ under `## Comments`; triage takes no pull requests
21
+ - Rejected requests (triage): `out-of-scope/<concept>.md`
22
+ - Implementation notes (implement-spec): `projects/<name>/notes/`
23
+ - PR bodies (pr): `projects/<name>/prs/<branch>.md`
18
24
  - Map (Wayfinding): `projects/<name>/map.md`, its tasks under
19
25
  `tasks/` with `Type:`, `Status: claimed|resolved` and `Blocked by:` lines
20
26
  - Research: `research/<slug>.md`
27
+ - Architecture reports (improve-codebase-architecture): `architecture/<slug>.html`
28
+ - Handoffs: `handoffs/<slug>.md`
21
29
  - Questionnaires: `questionnaires/<slug>.md`
30
+ - Retrospectives (retro): `retros/<date>-<slug>.md`
31
+ - Wizards: `wizards/<slug>.sh`; the values they capture stay out of the Room
22
32
  - Learning: `learning/<topic>/`
23
33
  - Prototype: one HTML file with every variant on one page.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentshouse/kit",
3
- "version": "0.2.1-c903d1429e34a",
3
+ "version": "0.2.1-c9a26041e119a",
4
4
  "description": "House Kit connects an Environment to agents.house.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -23,6 +23,7 @@
23
23
  "scripts": {
24
24
  "build": "tsc -p tsconfig.build.json && node scripts/bootstrap-manifest.mjs",
25
25
  "engine": "mkdir -p tmp/engine && docker run --rm -v \"$PWD:/kit\" -w /kit alpine:3.24@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6 sh scripts/engine.sh tmp/engine",
26
+ "sync-skills": "node scripts/sync-skills.mjs",
26
27
  "test": "tsc -p tsconfig.json && vitest run"
27
28
  },
28
29
  "dependencies": {
@@ -1,34 +1,41 @@
1
1
  ---
2
2
  name: ask-matt
3
3
  description: Ask which skill or flow fits your situation. A router over the skills in this repo.
4
+ disable-model-invocation: true
4
5
  ---
5
6
 
6
- <!-- Vendored from mattpocock/skills at commit c55ee46073ed923f86ce59a5eb3b6d895095d1b7 (skills/engineering/ask-matt). MIT licensed — see LICENSE in this directory. -->
7
-
8
7
  # Ask Matt
9
8
 
10
9
  You don't remember every skill, so ask.
11
10
 
11
+ Before stating what a skill does or recommending a step be skipped, read that skill's SKILL.md: the summaries here are for orientation only.
12
+
12
13
  A **flow** is a path through the skills. Most paths run along one **main flow**, and two **on-ramps** merge onto it. Everything else is standalone, or a vocabulary layer that runs underneath.
13
14
 
14
15
  ## The main flow: idea → ship
15
16
 
16
17
  The route most work travels. You have an idea and want it built.
17
18
 
18
- 1. **`/grill-with-docs`** sharpens the idea by interview. Start here whenever you are **working in a working directory**: it's stateful, retaining what it learns in `CONTEXT.md` and ADRs. (No working directory? Use `/grill-me` instead, covered under Standalone. Both run the same `/grilling` primitive; `grill-with-docs` is the one that leaves a paper trail, which makes it the better of the two whenever a repo is there to leave it in.)
19
+ 1. **`/grill-with-docs`** sharpens the idea by interview. Start here whenever you are **working in a working directory**: it's stateful, retaining what it learns in `GLOSSARY.md` and ADRs. (No working directory? Use `/grill-me` instead, covered under Standalone. Both run the same `/grilling` primitive; `grill-with-docs` is the one that leaves a paper trail, which makes it the better of the two whenever a repo is there to leave it in.)
19
20
  2. **Branch: can you settle every question in conversation?** If a question needs a runnable answer (state, business logic, a UI you have to see), detour through a prototype, bridged by **`/handoff`** in both directions (a prototype lives in its own directory, which is exactly what `/handoff` is for; see Phase boundaries):
20
21
  - **`/handoff`** out, then open a fresh session against that file,
21
22
  - **`/prototype`** to answer the question with throwaway code,
22
23
  - **`/handoff`** back what you learned, and reference it from the original idea thread.
23
24
  3. **Branch: is this a multi-session build?**
24
- - **Yes** → **`/to-spec`** (turn the thread into a spec), then **`/to-tickets`** to split it into tracer-bullet tickets, each declaring its **blocking edges**. On a local tracker that's one file per ticket under `.scratch/<feature>/issues/`, worked blockers-first by hand; on a real tracker the edges become native blocking links, so any ticket whose blockers are done can be grabbed: kick off **`/implement`** per ticket, **`/clear`ing context between each one**. Each ticket is self-contained, so the last one's context is disposable.
25
+ - **Yes** → **`/to-spec`** (turn the thread into a spec), then **`/to-tickets`** to split it into tracer-bullet tickets, each declaring its **blocking edges**. Then work the tickets one of two ways:
26
+ - **`/implement`** per ticket, **`/clear`ing context between each one**. On a local tracker that's one file per ticket under `.scratch/<feature>/issues/`, worked blockers-first by hand; on a real tracker the edges become native blocking links, so any ticket whose blockers are done can be grabbed. Each ticket is self-contained, so the last one's context is disposable.
27
+ - **`/implement-spec`** for the whole spec in one run. It reads the tickets as a **task graph**, runs implementer subagents across the ready **frontier** in parallel, and lands everything on one **integration branch**. Reach for it when you'd rather orchestrate the build than drive each ticket yourself.
25
28
  - **No** → **`/implement`** right here, in the same context window.
26
29
 
27
- Either way, **`/implement`** builds each issue by driving **`/tdd`** internally (one red-green slice at a time), then closes out by running **`/code-review`**, a two-axis review (Standards + Spec) of the diff, before committing. Reach for **`/tdd`** on its own when you just want to build a concrete behaviour test-first without a full spec, and **`/code-review`** on its own whenever you want to review a branch or PR against a fixed point.
30
+ Either way, the code gets built by driving **`/tdd`** (one red-green slice at a time) and closes out with **`/code-review`**, a two-axis review (Standards + Spec) of the diff. `/implement` runs both per ticket; `/implement-spec`'s implementers each drive `/tdd`, and it runs one `/code-review` over the integration branch. Reach for **`/tdd`** on its own when you just want to build a concrete behaviour test-first without a full spec, and **`/code-review`** on its own whenever you want to review a branch or PR against a fixed point.
31
+
32
+ When the work goes up as a pull request, **`/pr`** shapes the body: the smallest visual that shows the change, before/after evidence that it works, and a one-way or two-way door call. It's model-invoked, so the agent reaches for it whenever it writes a PR.
33
+
34
+ 4. **`/retro`** closes the loop. After a build, and especially one that went sideways, it looks back over the session and suggests changes to the agent's **environment**, not the code: navigation pointers, automated checks, the coding standards `/code-review` enforces, steering files, tooling. Mechanical mistakes become deterministic checks; judgement calls become coding standards. The next build then starts from a better environment.
28
35
 
29
36
  ### Context hygiene
30
37
 
31
- Keep steps 1–3 in **one unbroken context window** (don't compact or clear until after `/to-tickets`) so the grilling, spec, and tickets all build on the same thinking. Each `/implement` then starts fresh, working from the ticket.
38
+ Keep steps 1–3 in **one unbroken context window** (don't compact or clear until after `/to-tickets`) so the grilling, spec, and tickets all build on the same thinking. Each `/implement` then starts fresh, working from the ticket. Run `/retro` in the session it's looking back on, before you clear; after clearing, point it at that session's log instead.
32
39
 
33
40
  The limit on this is the **[smart zone](https://www.aihero.dev/ai-coding-dictionary/smart-zone)**: the window (~150k tokens on state-of-the-art models) within which the model still reasons sharply. If a session approaches it before `/to-tickets`, don't push on degraded; `/compact` at the nearest phase boundary and carry on (see Phase boundaries).
34
41
 
@@ -40,7 +47,7 @@ A starting situation that generates work, then merges onto the main flow.
40
47
 
41
48
  Triage is only for issues **you didn't create**: bug reports, incoming feature requests, anything that arrives raw. Tickets that `/to-tickets` produced are already agent-ready, so **don't triage them**.
42
49
 
43
- - **Something's broken** → **`/diagnosing-bugs`**. For the hard ones: the bug that resists a first glance, the intermittent flake, the regression that crept in between two known-good states. It refuses to theorise until it has a **tight feedback loop** (one command that already goes red on *this* bug), then fixes with a regression test. Its post-mortem hands off to **`/improve-codebase-architecture`** when the real finding is that there's no good seam to lock the bug down.
50
+ - **Something's broken** → **`/diagnosing-bugs`**. For the hard ones: the bug that resists a first glance, the intermittent flake, the regression that crept in between two known-good states. It refuses to theorise until it has a **tight feedback loop** (one command that already goes red on *this* bug), then fixes with a regression test. Once the fix is in, run **`/retro`** in the same session to ask what would have prevented the bug; where the real finding is that there's no good seam to lock it down, that's a job for **`/improve-codebase-architecture`**.
44
51
 
45
52
  - **A huge, foggy effort: a greenfield project or a huge feature build, too big for one session** → **`/wayfinder`**, the most cognitively demanding flow here. When the way from here to the destination isn't visible yet, it charts a **shared map** of **decision tickets** on the issue tracker and resolves them one at a time, producing **decisions, not deliverables**, until the fog is pushed back and the way is clear. Where **`/grill-with-docs`** sharpens an idea you can hold in one session, wayfinder is for the idea you can't, and it's slower and denser, so save it for exactly that, never a well-scoped feature.
46
53
 
@@ -56,7 +63,7 @@ Not feature work, just upkeep.
56
63
 
57
64
  Two model-invoked references that run *beneath* the other skills, each the single source of truth for its vocabulary. Reach for them directly when the **words**, not the process, are the problem; or let the skills above pull them in.
58
65
 
59
- - **`/domain-modeling`**: sharpen the project's *domain* language: challenge a fuzzy term, resolve an overloaded word ("account" doing three jobs), record a hard-to-reverse decision as an ADR. It's the active discipline `/grill-with-docs` drives to keep `CONTEXT.md` a clean glossary.
66
+ - **`/domain-modeling`**: sharpen the project's *domain* language: challenge a fuzzy term, resolve an overloaded word ("account" doing three jobs), record a hard-to-reverse decision as an ADR. It's the active discipline `/grill-with-docs` drives to keep `GLOSSARY.md` a clean glossary.
60
67
  - **`/codebase-design`** is the deep-module vocabulary (module, interface, depth, seam, adapter, leverage, locality) for designing a module's *shape*: a lot of behaviour behind a small interface at a clean seam. `/tdd` and `/improve-codebase-architecture` both speak it.
61
68
 
62
69
  ## Phase boundaries
@@ -75,14 +82,13 @@ Read [PHASE-BOUNDARIES.md](PHASE-BOUNDARIES.md) for the ordered tree: the five q
75
82
 
76
83
  Off the main flow entirely.
77
84
 
78
- - **`/grill-me`**: the same relentless interview as `/grill-with-docs`, but **stateless**: it saves nothing locally and builds no `CONTEXT.md`. Reach for it when you are **not working in a working directory** (sharpening a plan, a design, a piece of writing, anything with no repo under it). If you are in a working directory, use `/grill-with-docs` instead: it runs the same interview and leaves a paper trail, so it is strictly the better one.
85
+ - **`/grill-me`**: the same relentless interview as `/grill-with-docs`, but **stateless**: it saves nothing locally and builds no `GLOSSARY.md`. Reach for it when you are **not working in a working directory** (sharpening a plan, a design, a piece of writing, anything with no repo under it). If you are in a working directory, use `/grill-with-docs` instead: it runs the same interview and leaves a paper trail, so it is strictly the better one.
79
86
  - **`/grilling`** is the interview primitive itself: rounds, the frontier, facts are the agent's job and decisions are yours. `/grill-me` and `/grill-with-docs` are the two named ways in, and `/triage`, `/wayfinder` and `/improve-codebase-architecture` all run it internally. Reach for it directly only when you want the interview with no wrapper around it.
80
- - **`/resolving-merge-conflicts`** works an in-progress merge or rebase conflict hunk by hunk, resolving by **intent** traced to each side's primary source rather than by picking lines, then finishes the operation. It never runs `--abort`. Standalone and off every flow: reach for it when you are already mid-conflict.
81
87
  - **`/prototype`** is a small, throwaway program that answers one design question: does this state model feel right, or what should this UI look like. Throwaway is a constraint on how the code is written, not a promise to destroy it: the answer folds into the real code, and the prototype itself is kept as a **primary source** on a `prototype/<name>` branch out of main, pointed at from the implementation issue. It's the detour in step 2 of the main flow, but reach for it any time a design question is hard to settle on paper.
82
88
  - **`/research`**: delegate reading legwork to a **background agent**: it investigates a question against **primary sources**, then leaves a cited Markdown file in the repo. Keep working while it reads. The file it produces is something to take *into* the main flow at `/grill-with-docs`, since research feeds the thinking rather than replacing it.
83
89
  - **`/to-questionnaire`** comes in when the thing blocking you isn't in your head or the codebase but in **someone else's**, and it writes them a questionnaire to fill in. It's the inverse of `/grill-me`: instead of interviewing you about the subject, it interviews you about the **send** (who it's going to, what you need back) and aims the questions at the gap. What comes back is material for `/grill-with-docs` or `/to-spec`.
84
90
  - **`/wizard`** is for the steps only a **human** can take: provisioning infrastructure, setting up credentials or CI secrets, clicking through an unfamiliar third-party dashboard, running a one-off migration or cutover. It generates an interactive bash script that opens each URL, captures each value, and writes it into `.env` and GitHub secrets, so the procedure stops being something you re-explain to an agent every time. Model-invoked, so the agent reaches for it the moment it hits a wall only you can pass. If the agent could just do it itself, it should; this is for where a human is genuinely in the loop.
85
- - **`/wait-what`** is the corrective for a message that didn't land. Use it mid-conversation, inside any other skill, and the agent re-pitches what it just said with the context you were missing, in plain English, using the `CONTEXT.md` vocabulary. It works after the fact; `/grill-with-docs` is the upfront cure, because a shared language agreed early is what stops the jargon arriving at all.
91
+ - **`/wait-what`** is the corrective for a message that didn't land. Use it mid-conversation, inside any other skill, and the agent re-pitches what it just said with the context you were missing, in plain English, using the `GLOSSARY.md` vocabulary. It works after the fact; `/grill-with-docs` is the upfront cure, because a shared language agreed early is what stops the jargon arriving at all.
86
92
  - **`/teach`**: learn a concept over multiple sessions, using the current directory as a stateful workspace.
87
93
  - **`/writing-for-agents`** is the reference for writing documents agents consume: skills, AGENTS.md, pointed-at docs.
88
94
 
@@ -2,3 +2,4 @@ interface:
2
2
  display_name: "Ask Matt"
3
3
  short_description: "Find the right skill or workflow"
4
4
  policy:
5
+ allow_implicit_invocation: false
@@ -3,8 +3,6 @@ name: code-review
3
3
  description: "Review the changes since a fixed point (commit, branch, tag, or merge-base) along two axes: Standards (does the code follow this repo's documented coding standards?) and Spec (does the code match what the originating issue/spec asked for?). Runs both reviews in parallel sub-agents and reports them side by side. Use when the user wants to review a branch, a PR, work-in-progress changes, or asks to \"review since X\"."
4
4
  ---
5
5
 
6
- <!-- Vendored from mattpocock/skills at commit c55ee46073ed923f86ce59a5eb3b6d895095d1b7 (skills/engineering/code-review). MIT licensed — see LICENSE in this directory. -->
7
-
8
6
  Two-axis review of the diff between `HEAD` and a fixed point the user supplies:
9
7
 
10
8
  - **Standards**: does the code conform to this repo's documented coding standards?
@@ -12,7 +10,7 @@ Two-axis review of the diff between `HEAD` and a fixed point the user supplies:
12
10
 
13
11
  Both axes run as **parallel sub-agents** so they don't pollute each other's context, then this skill aggregates their findings.
14
12
 
15
- The issue tracker should have been provided to you. If `docs/agents/issue-tracker.md` is missing, tell the user to run `/setup-matt-pocock-skills`.
13
+ The issue tracker should have been provided to you. If not, tell the user to run `/setup-matt-pocock-skills`.
16
14
 
17
15
  ## Process
18
16
 
@@ -28,14 +26,14 @@ Before going further, confirm the fixed point resolves (`git rev-parse <fixed-po
28
26
 
29
27
  Look for the originating spec, in this order:
30
28
 
31
- 1. Issue references in the commit messages (`#123`, `Closes #45`, GitLab `!67`, etc.), fetched via the workflow in `docs/agents/issue-tracker.md`.
29
+ 1. Issue references in the commit messages (`#123`, `Closes #45`, GitLab `!67`, etc.), fetched via the workflow in the tracker doc.
32
30
  2. A path the user passed as an argument.
33
31
  3. A spec file under `docs/`, `specs/`, or `.scratch/` matching the branch name or feature.
34
32
  4. If nothing is found, ask the user where the spec is. If they say there isn't one, the **Spec** sub-agent will skip and report "no spec available".
35
33
 
36
34
  ### 3. Identify the standards sources
37
35
 
38
- Anything in the repo that documents how code should be written, such as `CODING_STANDARDS.md` or `CONTRIBUTING.md`.
36
+ Search the repo for every file that documents how code should be written. When `CODING_STANDARDS.md` or `CONTRIBUTING.md` exists, it must be on the list.
39
37
 
40
38
  On top of whatever the repo documents, the Standards axis always carries the **smell baseline** below: a fixed set of Fowler code smells (_Refactoring_, ch.3) that applies even when a repo documents nothing. Two rules bind it:
41
39
 
@@ -59,6 +57,8 @@ Each smell reads *what it is* → *how to fix*; match it against the diff:
59
57
 
60
58
  ### 4. Spawn both sub-agents in parallel
61
59
 
60
+ Issue both sub-agent calls together, in the foreground, and aggregate the reports they return.
61
+
62
62
  **Standards sub-agent prompt** should include:
63
63
 
64
64
  - The full diff command and commit list.
@@ -27,7 +27,7 @@ Prompt each sub-agent with a separate technical brief (file paths, coupling deta
27
27
  - Agent 3: "Optimise for the most common caller: make the default case trivial."
28
28
  - Agent 4 (if applicable): "Design around ports & adapters for cross-seam dependencies."
29
29
 
30
- Include both [SKILL.md](SKILL.md) vocabulary and CONTEXT.md vocabulary in the brief so each sub-agent names things consistently with the architecture language and the project's domain language.
30
+ Include both [SKILL.md](SKILL.md) vocabulary and GLOSSARY.md vocabulary in the brief so each sub-agent names things consistently with the architecture language and the project's domain language.
31
31
 
32
32
  Each sub-agent outputs:
33
33