@agentshouse/kit 0.2.1-alpha.9 → 0.2.1-c02dacffca87c

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 (139) hide show
  1. package/README.md +48 -29
  2. package/dist/abort.js +9 -0
  3. package/dist/acp.js +102 -88
  4. package/dist/agents.js +356 -52
  5. package/dist/api.js +40 -4
  6. package/dist/app-git.js +48 -0
  7. package/dist/authority-main.js +2 -2
  8. package/dist/bridge.js +93 -78
  9. package/dist/building.js +50 -0
  10. package/dist/calls.js +71 -0
  11. package/dist/chatgpt.js +32 -0
  12. package/dist/chunks.js +49 -0
  13. package/dist/clis.js +224 -88
  14. package/dist/codex-main.js +58 -0
  15. package/dist/connection.js +105 -0
  16. package/dist/conversations.js +749 -270
  17. package/dist/copies.js +475 -260
  18. package/dist/end.js +33 -0
  19. package/dist/enrol-main.js +7 -1
  20. package/dist/events.js +546 -0
  21. package/dist/files.js +140 -17
  22. package/dist/git-main.js +2 -2
  23. package/dist/git.js +62 -22
  24. package/dist/home.js +120 -9
  25. package/dist/house-cli.js +299 -0
  26. package/dist/house-main.js +5 -102
  27. package/dist/instructions.js +17 -18
  28. package/dist/kit-cli.js +68 -0
  29. package/dist/kit-main.js +8 -16
  30. package/dist/login.js +130 -95
  31. package/dist/logout.js +19 -0
  32. package/dist/mcp.js +43 -0
  33. package/dist/operation.js +7 -4
  34. package/dist/parts.js +82 -0
  35. package/dist/placement.js +31 -0
  36. package/dist/refusals.js +53 -0
  37. package/dist/relay.js +41 -0
  38. package/dist/resident.js +144 -52
  39. package/dist/scope.js +214 -0
  40. package/dist/scripts.js +255 -0
  41. package/dist/secret-input.js +7 -2
  42. package/dist/service-main.js +9 -0
  43. package/dist/service.js +55 -0
  44. package/dist/shell.js +3 -0
  45. package/dist/sign-in.js +52 -32
  46. package/dist/silence.js +28 -0
  47. package/dist/skills.js +48 -39
  48. package/dist/transcription.js +210 -0
  49. package/dist/translate.js +2 -8
  50. package/dist/update.js +127 -0
  51. package/dist/wire.js +40 -0
  52. package/dist/work.js +346 -0
  53. package/package.json +21 -24
  54. package/dist/blocks.js +0 -51
  55. package/dist/bootstrap.json +0 -28
  56. package/dist/stream.js +0 -47
  57. package/how-we-work.md +0 -25
  58. package/skills/ask-matt/LICENSE +0 -21
  59. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  60. package/skills/ask-matt/SKILL.md +0 -91
  61. package/skills/ask-matt/agents/openai.yaml +0 -4
  62. package/skills/code-review/LICENSE +0 -21
  63. package/skills/code-review/SKILL.md +0 -89
  64. package/skills/code-review/agents/openai.yaml +0 -3
  65. package/skills/codebase-design/DEEPENING.md +0 -37
  66. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  67. package/skills/codebase-design/LICENSE +0 -21
  68. package/skills/codebase-design/SKILL.md +0 -116
  69. package/skills/codebase-design/agents/openai.yaml +0 -3
  70. package/skills/diagnosing-bugs/LICENSE +0 -21
  71. package/skills/diagnosing-bugs/SKILL.md +0 -140
  72. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  73. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  74. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  75. package/skills/domain-modeling/CONTEXT-FORMAT.md +0 -60
  76. package/skills/domain-modeling/LICENSE +0 -21
  77. package/skills/domain-modeling/SKILL.md +0 -76
  78. package/skills/domain-modeling/agents/openai.yaml +0 -3
  79. package/skills/grill-me/LICENSE +0 -21
  80. package/skills/grill-me/SKILL.md +0 -8
  81. package/skills/grill-me/agents/openai.yaml +0 -4
  82. package/skills/grill-with-docs/LICENSE +0 -21
  83. package/skills/grill-with-docs/SKILL.md +0 -8
  84. package/skills/grill-with-docs/agents/openai.yaml +0 -4
  85. package/skills/grilling/LICENSE +0 -21
  86. package/skills/grilling/SKILL.md +0 -30
  87. package/skills/grilling/agents/openai.yaml +0 -3
  88. package/skills/handoff/LICENSE +0 -21
  89. package/skills/handoff/SKILL.md +0 -17
  90. package/skills/handoff/agents/openai.yaml +0 -4
  91. package/skills/implement/LICENSE +0 -21
  92. package/skills/implement/SKILL.md +0 -16
  93. package/skills/implement/agents/openai.yaml +0 -4
  94. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  95. package/skills/improve-codebase-architecture/LICENSE +0 -21
  96. package/skills/improve-codebase-architecture/SKILL.md +0 -72
  97. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -4
  98. package/skills/prototype/LICENSE +0 -21
  99. package/skills/prototype/LOGIC.md +0 -67
  100. package/skills/prototype/SKILL.md +0 -28
  101. package/skills/prototype/UI.md +0 -112
  102. package/skills/prototype/agents/openai.yaml +0 -3
  103. package/skills/research/LICENSE +0 -21
  104. package/skills/research/SKILL.md +0 -14
  105. package/skills/research/agents/openai.yaml +0 -3
  106. package/skills/resolving-merge-conflicts/LICENSE +0 -21
  107. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  108. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  109. package/skills/tdd/LICENSE +0 -21
  110. package/skills/tdd/SKILL.md +0 -40
  111. package/skills/tdd/agents/openai.yaml +0 -3
  112. package/skills/tdd/mocking.md +0 -59
  113. package/skills/tdd/tests.md +0 -77
  114. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  115. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  116. package/skills/teach/LICENSE +0 -21
  117. package/skills/teach/MISSION-FORMAT.md +0 -31
  118. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  119. package/skills/teach/SKILL.md +0 -141
  120. package/skills/teach/agents/openai.yaml +0 -4
  121. package/skills/to-questionnaire/LICENSE +0 -21
  122. package/skills/to-questionnaire/SKILL.md +0 -55
  123. package/skills/to-questionnaire/agents/openai.yaml +0 -4
  124. package/skills/to-spec/LICENSE +0 -21
  125. package/skills/to-spec/SKILL.md +0 -76
  126. package/skills/to-spec/agents/openai.yaml +0 -4
  127. package/skills/to-tickets/LICENSE +0 -21
  128. package/skills/to-tickets/SKILL.md +0 -106
  129. package/skills/to-tickets/agents/openai.yaml +0 -4
  130. package/skills/wait-what/LICENSE +0 -21
  131. package/skills/wait-what/SKILL.md +0 -8
  132. package/skills/wait-what/agents/openai.yaml +0 -4
  133. package/skills/wayfinder/LICENSE +0 -21
  134. package/skills/wayfinder/SKILL.md +0 -129
  135. package/skills/wayfinder/agents/openai.yaml +0 -4
  136. package/skills/writing-for-agents/LICENSE +0 -21
  137. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  138. package/skills/writing-for-agents/SKILL.md +0 -83
  139. package/skills/writing-for-agents/agents/openai.yaml +0 -3
package/dist/agents.js CHANGED
@@ -1,90 +1,394 @@
1
1
  import { client } from '@agentclientprotocol/sdk';
2
2
  import { readFile } from 'node:fs/promises';
3
- import { startAdapter } from './acp.js';
4
- import { CLIS, KIT_VERSION, cliCommand, install, run } from './clis.js';
5
- import { kitHome } from './home.js';
6
- async function signedIn(kind) {
7
- const probe = CLIS[kind].signedIn;
8
- if ('command' in probe) {
9
- return (await run(cliCommand(kind), probe.command, 30_000)).status === 0;
3
+ import { setTimeout as delay } from 'node:timers/promises';
4
+ import { signedOut, startAdapter } from './acp.js';
5
+ import { CLIS, KIT_RELEASE, below, installCli, locate, run, updateCli, versionOf } from './clis.js';
6
+ import { agentBase, kitHome } from './home.js';
7
+ import { holdTranscription, transcriptionReport } from './transcription.js';
8
+ import { KIT_WIRE } from './wire.js';
9
+ // Two quiet seconds let a fresh conversation start before background model discovery competes for its CPU.
10
+ const MODEL_PROBE_QUIET_MS = 2_000;
11
+ // A CLI's maker can publish several releases a day, so running each chosen CLI's update command hourly brings a new one to the next conversation within the hour for one small check.
12
+ export const CLI_UPDATE_MS = 60 * 60_000;
13
+ // A sign-in's model read that has not answered in ten seconds leaves its sign-in to the status check, so a hung CLI never holds the sign-in's result.
14
+ const SIGNED_IN_READ_MS = 10_000;
15
+ const SIGNED_OUT = { signed_in: false, models: [], modes: [] };
16
+ const INSTALLING = { release: null, failure: null, installing: true, ...SIGNED_OUT };
17
+ function select(options, category) {
18
+ const option = options.find((candidate) => candidate.category === category);
19
+ return option?.type === 'select' ? option : undefined;
20
+ }
21
+ function choices(options) {
22
+ return options.flatMap((entry) => ('group' in entry ? entry.options : [entry]));
23
+ }
24
+ function values(options) {
25
+ return choices(options).map((entry) => entry.value);
26
+ }
27
+ function causeOf(error) {
28
+ return error instanceof Error ? error.message : String(error);
29
+ }
30
+ function logged(error) {
31
+ process.stderr.write(`kit: ${causeOf(error)}\n`);
32
+ }
33
+ const LAPTOP_CHASSIS = new Set(['8', '9', '10', '14', '30', '31', '32']);
34
+ const DESKTOP_CHASSIS = new Set(['3', '4', '5', '6', '7', '15', '16']);
35
+ const SERVER_CHASSIS = new Set(['17', '18', '19', '20', '21', '22', '23', '24']);
36
+ export function chassisForm(value) {
37
+ const chassis = value.trim();
38
+ if (LAPTOP_CHASSIS.has(chassis))
39
+ return 'laptop';
40
+ if (DESKTOP_CHASSIS.has(chassis))
41
+ return 'desktop';
42
+ if (SERVER_CHASSIS.has(chassis))
43
+ return 'server';
44
+ return undefined;
45
+ }
46
+ export function hostVm(version) {
47
+ return /docker desktop|microsoft|wsl|linuxkit/i.test(version);
48
+ }
49
+ async function models(kind, adapter, known) {
50
+ const agent = adapter.connection.agent;
51
+ const opened = await agent.request('session/new', { cwd: kitHome(), mcpServers: [] });
52
+ let options = opened.configOptions ?? [];
53
+ const mode = select(options, 'mode');
54
+ const modes = mode === undefined ? [] : choices(mode.options).map(({ value, name }) => ({ mode: value, name }));
55
+ const model = select(options, 'model');
56
+ if (model === undefined)
57
+ return { models: [], modes };
58
+ const offered = [];
59
+ for (const value of values(model.options)) {
60
+ if (select(options, 'model')?.currentValue !== value) {
61
+ try {
62
+ options = (await agent.request('session/set_config_option', { sessionId: opened.sessionId, configId: model.id, value })).configOptions;
63
+ }
64
+ catch (error) {
65
+ process.stderr.write(`kit: ${kind} refused its model ${value}: ${causeOf(error)}\n`);
66
+ offered.push({ model: value, efforts: known.find((entry) => entry.model === value)?.efforts ?? [] });
67
+ continue;
68
+ }
69
+ }
70
+ const effort = select(options, 'thought_level');
71
+ offered.push({ model: value, efforts: effort === undefined ? [] : values(effort.options) });
10
72
  }
73
+ return { models: offered, modes };
74
+ }
75
+ function ended(signal) {
76
+ return new Promise((_, reject) => {
77
+ if (signal.aborted)
78
+ reject(signal.reason);
79
+ signal.addEventListener('abort', () => reject(signal.reason), { once: true });
80
+ });
81
+ }
82
+ async function offer(kind, cli, known, signal) {
83
+ const probe = CLIS[kind].signedIn;
84
+ // A sign-in check that has not answered in half a minute counts as signed out rather than stalling the report.
85
+ if ('command' in probe && (await run(cli, probe.command, 30_000, false, signal)).status !== 0)
86
+ return SIGNED_OUT;
87
+ let adapter;
11
88
  try {
12
- const adapter = await startAdapter(kind, kitHome(), client({ name: '@agentshouse/kit' }));
13
- adapter.child.kill('SIGKILL');
14
- const method = adapter.initialized._meta?.[probe.initializeMeta];
15
- return typeof method === 'string' && method.length > 0;
89
+ adapter = await startAdapter(kind, cli, kitHome(), client({ name: '@agentshouse/kit' }), () => undefined, {}, signal);
90
+ signal?.throwIfAborted();
91
+ if ('initializeMeta' in probe) {
92
+ const method = adapter.initialized._meta?.[probe.initializeMeta];
93
+ if (typeof method !== 'string' || method.length === 0)
94
+ return SIGNED_OUT;
95
+ }
96
+ return { signed_in: true, ...(await models(kind, adapter, known.models)) };
16
97
  }
17
- catch {
18
- return false;
98
+ catch (error) {
99
+ if (!signal?.aborted)
100
+ process.stderr.write(`kit: ${kind} did not offer its models: ${causeOf(error)}\n`);
101
+ if (signedOut(error))
102
+ return SIGNED_OUT;
103
+ return {
104
+ signed_in: adapter === undefined && 'initializeMeta' in probe ? known.signed_in : true,
105
+ models: known.models,
106
+ modes: known.modes,
107
+ };
19
108
  }
109
+ finally {
110
+ adapter?.retire();
111
+ }
112
+ }
113
+ function reported(kind, reading) {
114
+ const { minimum, update } = CLIS[kind];
115
+ return {
116
+ kind,
117
+ release: reading.release,
118
+ ...(reading.release !== null && below(reading.release, minimum) ? { minimum, update } : {}),
119
+ failure: reading.failure,
120
+ ...(reading.installing === undefined ? {} : { installing: reading.installing }),
121
+ signed_in: reading.signed_in,
122
+ models: reading.models,
123
+ modes: reading.modes,
124
+ full_access: CLIS[kind].fullAccess,
125
+ };
126
+ }
127
+ const NOT_READY = "the Agent's CLI is not ready on this computer";
128
+ async function sshHostKey() {
129
+ // Listing a local host key takes a moment, so ten seconds only bounds a stuck ssh-keygen.
130
+ const listed = await run('ssh-keygen', ['-l', '-E', 'sha256', '-f', '/etc/ssh/ssh_host_ed25519_key.pub'], 10_000);
131
+ return listed.status === 0 ? (/^\d+ (SHA256:\S+)/.exec(listed.output)?.[1] ?? null) : null;
20
132
  }
21
133
  async function operatingSystem() {
134
+ // sw_vers answers at once, so ten seconds only bounds a stuck call.
135
+ if (process.platform === 'darwin')
136
+ return `macOS ${(await run('sw_vers', ['-productVersion'], 10_000)).output.trim()}`;
22
137
  const release = await readFile('/etc/os-release', 'utf8');
23
138
  return /^PRETTY_NAME="?([^"\n]*)"?$/m.exec(release)?.[1] ?? release;
24
139
  }
140
+ async function hostForm() {
141
+ if (process.platform === 'darwin') {
142
+ const battery = await run('pmset', ['-g', 'batt'], 10_000);
143
+ return battery.status === 0 && battery.output.includes('InternalBattery') ? 'laptop' : 'desktop';
144
+ }
145
+ if (process.platform !== 'linux')
146
+ return undefined;
147
+ const [container, version, chassis] = await Promise.all([
148
+ readFile('/.dockerenv', 'utf8').then(() => true).catch(() => false),
149
+ readFile('/proc/version', 'utf8').catch(() => ''),
150
+ readFile('/sys/class/dmi/id/chassis_type', 'utf8').catch(() => ''),
151
+ ]);
152
+ if (container && hostVm(version))
153
+ return undefined;
154
+ return chassisForm(chassis);
155
+ }
25
156
  export class Agents {
26
- desired = { agents: [], routes: [] };
157
+ desired = { agents: [], routes: [], transcribe: false };
27
158
  house;
28
159
  reading = Promise.resolve();
160
+ queues = new Map();
29
161
  installing = Promise.resolve();
30
- releases = new Map();
31
- reporting = null;
32
- again = false;
33
- constructor(house) {
162
+ installs = new Map();
163
+ readings = new Map();
164
+ reads = new Map();
165
+ starts = 0;
166
+ sending = Promise.resolve();
167
+ sent = null;
168
+ refreshing = 0;
169
+ loaded = Promise.resolve();
170
+ updating = null;
171
+ changed;
172
+ constructor(house, changed = () => undefined) {
34
173
  this.house = house;
174
+ this.changed = changed;
175
+ }
176
+ idle() {
177
+ return this.refreshing === 0 && this.updating === null;
35
178
  }
36
179
  read() {
37
180
  return this.reading.then(() => undefined, () => undefined);
38
181
  }
39
- installed() {
40
- return this.installing.catch(() => undefined);
41
- }
42
182
  route(agentId) {
43
183
  return this.desired.routes.find((route) => route.agent_id === agentId);
44
184
  }
45
185
  refresh() {
186
+ this.refreshing++;
187
+ this.changed();
46
188
  const reading = this.read().then(async () => {
47
189
  this.desired = await this.house.deliver('/kit/agents/desired', {});
48
190
  return this.desired;
49
191
  });
50
192
  this.reading = reading;
51
- const installing = this.installed()
52
- .then(() => reading)
53
- .then(({ agents }) => this.install(agents));
54
- this.installing = installing;
55
- if (this.reporting === null)
56
- this.reporting = this.reports();
57
- else
58
- this.again = true;
59
- return Promise.all([installing, this.reporting]).then(() => undefined);
60
- }
61
- async report() {
62
- const agents = [];
63
- for (const [kind, release] of this.releases) {
64
- agents.push({ kind, release, signed_in: release !== null && (await signedIn(kind)) });
65
- }
66
- await this.house.deliver('/kit/agents/report', {
67
- os: await operatingSystem(),
68
- kit_version: KIT_VERSION,
69
- agents,
193
+ this.loaded = reading
194
+ .then(({ agents, transcribe }) => Promise.all([
195
+ holdTranscription(transcribe)
196
+ .then((changed) => (changed ? this.report() : undefined))
197
+ .catch(logged),
198
+ Promise.all(agents.map((kind) => this.load(kind)))
199
+ .then(() => this.report())
200
+ .catch(logged),
201
+ ]))
202
+ .catch(() => undefined)
203
+ .finally(() => {
204
+ this.refreshing--;
205
+ this.changed();
206
+ });
207
+ return reading.then(() => undefined);
208
+ }
209
+ keepCurrent() {
210
+ this.updateAll(this.loaded);
211
+ setInterval(() => this.updateAll(Promise.resolve()), CLI_UPDATE_MS);
212
+ }
213
+ cli(kind) {
214
+ this.starts += 1;
215
+ return this.queue(kind, async () => {
216
+ const resolved = await this.resolve(kind);
217
+ if (!('cli' in resolved)) {
218
+ this.update(kind, resolved);
219
+ throw new Error(NOT_READY);
220
+ }
221
+ const { cli, release } = resolved;
222
+ const known = this.readings.get(kind);
223
+ const kept = known?.release === null ? SIGNED_OUT : (known ?? SIGNED_OUT);
224
+ if (known?.release !== release || this.installs.has(kind)) {
225
+ this.update(kind, { release, failure: null, signed_in: kept.signed_in, models: kept.models, modes: kept.modes });
226
+ }
227
+ const read = this.nextRead(kind);
228
+ this.quietOffer(kind, cli, kept)
229
+ .then((offered) => {
230
+ const current = this.readings.get(kind);
231
+ if (this.reads.get(kind) === read && current?.release === release)
232
+ this.update(kind, { ...current, ...offered });
233
+ })
234
+ .catch(logged);
235
+ return cli;
70
236
  });
71
237
  }
72
- async install(kinds) {
73
- const releases = new Map();
74
- for (const kind of kinds)
75
- releases.set(kind, await install(kind));
76
- this.releases = releases;
238
+ located(kind, signal) {
239
+ const owner = {};
240
+ return this.queue(kind, () => this.ensure(kind, signal, owner).finally(() => this.settle(kind, owner)), signal);
241
+ }
242
+ async reread(kind) {
243
+ await this.examine(kind, this.readings.get(kind) ?? SIGNED_OUT, offer);
244
+ await this.report();
77
245
  }
78
- async reports() {
246
+ async recheck(kind, signal) {
247
+ const known = this.readings.get(kind) ?? SIGNED_OUT;
248
+ const owner = {};
79
249
  try {
80
- do {
81
- this.again = false;
82
- await this.installing;
83
- await this.report();
84
- } while (this.again);
250
+ await this.examine(kind, known, (kind, cli) => offer(kind, cli, known, AbortSignal.any([signal, AbortSignal.timeout(SIGNED_IN_READ_MS)])), signal, owner);
85
251
  }
86
252
  finally {
87
- this.reporting = null;
253
+ this.settle(kind, owner);
254
+ }
255
+ if (!signal.aborted)
256
+ await this.report();
257
+ }
258
+ report() {
259
+ const sending = this.sending.catch(() => undefined).then(() => this.send());
260
+ this.sending = sending;
261
+ return sending;
262
+ }
263
+ nextRead(kind) {
264
+ const read = (this.reads.get(kind) ?? 0) + 1;
265
+ this.reads.set(kind, read);
266
+ return read;
267
+ }
268
+ hold(kind, reading) {
269
+ this.readings.set(kind, reading);
270
+ this.installs.delete(kind);
271
+ }
272
+ update(kind, reading) {
273
+ this.hold(kind, reading);
274
+ this.report().catch(logged);
275
+ }
276
+ settle(kind, owner) {
277
+ if (this.installs.get(kind) !== owner)
278
+ return;
279
+ this.installs.delete(kind);
280
+ this.report().catch(logged);
281
+ }
282
+ queue(kind, work, signal) {
283
+ const queued = (this.queues.get(kind) ?? Promise.resolve()).then(() => {
284
+ signal?.throwIfAborted();
285
+ return work();
286
+ });
287
+ this.queues.set(kind, queued.catch(() => undefined));
288
+ return signal === undefined ? queued : Promise.race([queued, ended(signal)]);
289
+ }
290
+ async ensure(kind, signal, owner = {}) {
291
+ const found = await locate(kind, signal);
292
+ if (found !== null)
293
+ return found;
294
+ this.installs.set(kind, owner);
295
+ this.report().catch(logged);
296
+ const installing = this.installing.catch(() => undefined).then(() => installCli(kind, signal));
297
+ this.installing = installing;
298
+ const failure = await installing;
299
+ if (failure !== null)
300
+ throw new Error(failure);
301
+ const installed = await locate(kind, signal);
302
+ if (installed === null)
303
+ throw new Error(`the login shell does not find ${CLIS[kind].bin} after its install`);
304
+ return installed;
305
+ }
306
+ async resolve(kind, signal, owner) {
307
+ let cli;
308
+ try {
309
+ cli = await this.ensure(kind, signal, owner);
310
+ }
311
+ catch (error) {
312
+ const failure = causeOf(error);
313
+ process.stderr.write(`kit: ${kind} did not install: ${failure}\n`);
314
+ return { release: null, failure, ...SIGNED_OUT };
88
315
  }
316
+ const release = await versionOf(cli, signal);
317
+ if (release === null)
318
+ return { release: null, failure: `${cli} --version names no version`, ...SIGNED_OUT };
319
+ return { cli, release };
320
+ }
321
+ async examine(kind, known, read, signal, owner) {
322
+ const current = this.nextRead(kind);
323
+ const resolved = await this.queue(kind, () => this.resolve(kind, signal, owner), signal);
324
+ const reading = 'cli' in resolved ? { release: resolved.release, failure: null, ...(await read(kind, resolved.cli, known)) } : resolved;
325
+ if (this.reads.get(kind) === current && !signal?.aborted)
326
+ this.hold(kind, reading);
327
+ }
328
+ updateAll(after) {
329
+ if (this.updating !== null)
330
+ return;
331
+ this.updating = after
332
+ .then(async () => {
333
+ for (const kind of this.desired.agents) {
334
+ if (this.installs.has(kind) || !this.readings.get(kind)?.release)
335
+ continue;
336
+ const failure = await updateCli(kind);
337
+ if (failure !== null)
338
+ process.stderr.write(`kit: ${kind} did not update: ${failure}\n`);
339
+ await this.readVersion(kind);
340
+ }
341
+ })
342
+ .catch(logged)
343
+ .finally(() => {
344
+ this.updating = null;
345
+ this.changed();
346
+ });
347
+ this.changed();
348
+ }
349
+ async readVersion(kind) {
350
+ const cli = await locate(kind);
351
+ const release = cli === null ? null : await versionOf(cli);
352
+ const current = this.readings.get(kind);
353
+ if (release === null || !current?.release || current.release === release)
354
+ return;
355
+ this.update(kind, { ...current, release });
356
+ }
357
+ async quietOffer(kind, cli, known) {
358
+ for (let seen = -1; seen !== this.starts;) {
359
+ seen = this.starts;
360
+ await delay(MODEL_PROBE_QUIET_MS);
361
+ }
362
+ return offer(kind, cli, known);
363
+ }
364
+ async load(kind) {
365
+ const known = this.readings.get(kind);
366
+ if (known !== undefined && known.failure === null)
367
+ return;
368
+ await this.examine(kind, SIGNED_OUT, (kind, cli, known) => this.quietOffer(kind, cli, known));
369
+ await this.report();
370
+ }
371
+ async send() {
372
+ const agents = this.desired.agents.flatMap((kind) => {
373
+ const reading = this.installs.has(kind) ? INSTALLING : this.readings.get(kind);
374
+ return reading === undefined ? [] : [reported(kind, reading)];
375
+ });
376
+ const hostKey = await sshHostKey();
377
+ const form = await hostForm();
378
+ const body = {
379
+ os: await operatingSystem(),
380
+ ...(form === undefined ? {} : { form }),
381
+ kit_release: KIT_RELEASE,
382
+ wire: KIT_WIRE,
383
+ agent_base: agentBase(),
384
+ agents,
385
+ transcription: transcriptionReport(),
386
+ ...(hostKey === null ? {} : { ssh_host_key: hostKey }),
387
+ };
388
+ const state = JSON.stringify(body);
389
+ if (state === this.sent)
390
+ return;
391
+ await this.house.deliver('/kit/agents/report', body);
392
+ this.sent = state;
89
393
  }
90
394
  }
package/dist/api.js CHANGED
@@ -1,4 +1,9 @@
1
+ import { setTimeout as delay } from 'node:timers/promises';
2
+ import { signInAgain } from './end.js';
1
3
  import { readEnrolment } from './home.js';
4
+ import { retryable } from './refusals.js';
5
+ import { relayed } from './relay.js';
6
+ import { endUnserved, KIT_WIRE, KIT_WIRE_HEADER } from './wire.js';
2
7
  export class HouseRefusal extends Error {
3
8
  status;
4
9
  text;
@@ -8,24 +13,54 @@ export class HouseRefusal extends Error {
8
13
  this.text = text;
9
14
  }
10
15
  }
16
+ export function finalRefusal(error) {
17
+ return error instanceof HouseRefusal && retryable(error.text) === false;
18
+ }
19
+ export function retryDelay(attempt) {
20
+ // Full jitter, doubling from one second to a 30-second cap, spreads the Kit processes one outage dropped and retries within half a minute of House's return.
21
+ return Math.random() * Math.min(30_000, 1000 * 2 ** attempt);
22
+ }
23
+ export async function retrying(sent, signal, pause = retryDelay) {
24
+ for (let attempt = 0;; attempt++) {
25
+ try {
26
+ return await sent();
27
+ }
28
+ catch (error) {
29
+ if (!(error instanceof HouseRefusal) || retryable(error.text) !== true)
30
+ throw error;
31
+ }
32
+ // A request House refused as retryable is sent again after the caller's pause, the jittered curve unless it names its own.
33
+ await delay(pause(attempt), undefined, { signal });
34
+ }
35
+ }
11
36
  export async function post(house, path, body, credential, signal) {
12
- const answer = await fetch(new URL(path, house), {
37
+ const answer = await fetch(relayed(new URL(path, house)), {
13
38
  method: 'POST',
14
39
  headers: {
15
40
  'content-type': 'application/json',
41
+ [KIT_WIRE_HEADER]: String(KIT_WIRE),
16
42
  ...(credential === undefined ? {} : { authorization: `Bearer ${credential}` }),
17
43
  },
18
44
  body: JSON.stringify(body),
19
45
  signal,
20
46
  });
21
47
  const text = await answer.text();
48
+ if (!answer.ok)
49
+ endUnserved(text);
22
50
  if (!answer.ok)
23
51
  throw new HouseRefusal(path, answer.status, text);
24
52
  return JSON.parse(text);
25
53
  }
26
54
  async function enrolledPost(path, body, signal) {
27
55
  const { house, credential } = (await readEnrolment());
28
- return post(house, path, body, credential, signal);
56
+ try {
57
+ return await post(house, path, body, credential, signal);
58
+ }
59
+ catch (error) {
60
+ if (error instanceof HouseRefusal && error.status === 401)
61
+ await signInAgain(credential);
62
+ throw error;
63
+ }
29
64
  }
30
65
  export const house = {
31
66
  post: enrolledPost,
@@ -35,10 +70,11 @@ export const house = {
35
70
  return await enrolledPost(path, body);
36
71
  }
37
72
  catch (error) {
38
- if (error instanceof HouseRefusal && error.status < 500)
73
+ if (finalRefusal(error))
39
74
  throw error;
40
75
  process.stderr.write(`kit: ${path} did not reach House: ${error.message}\n`);
41
- await new Promise((resolve) => setTimeout(resolve, Math.min(30_000, 1000 * 2 ** attempt)));
76
+ // A delivery House did not take is sent again on the jittered curve for as long as it is still current.
77
+ await new Promise((resolve) => setTimeout(resolve, retryDelay(attempt)));
42
78
  if (!current())
43
79
  throw error;
44
80
  }
@@ -0,0 +1,48 @@
1
+ import { run } from './git.js';
2
+ import { kitHome, readOwnAgent } from './home.js';
3
+ const HELPER = '!kit git-credential';
4
+ function section(house) {
5
+ return `credential.${new URL('/app/', house).href}`;
6
+ }
7
+ async function configured(args) {
8
+ try {
9
+ return await run(kitHome(), ['config', '--global', ...args]);
10
+ }
11
+ catch (error) {
12
+ if (error.code === 'ENOENT')
13
+ return null;
14
+ throw error;
15
+ }
16
+ }
17
+ export async function placeAppGit(ownAgent) {
18
+ if (ownAgent === null)
19
+ return;
20
+ await configured(['--remove-section', section(ownAgent.house)]);
21
+ for (const value of ['', HELPER]) {
22
+ const ran = await configured(['--add', `${section(ownAgent.house)}.helper`, value]);
23
+ if (ran === null)
24
+ return;
25
+ if (ran.status !== 0)
26
+ throw new Error(`git config failed: ${ran.stderr.trim()}`);
27
+ }
28
+ }
29
+ export async function answerGitCredential(operation) {
30
+ let asked = '';
31
+ for await (const chunk of process.stdin)
32
+ asked += String(chunk);
33
+ if (operation !== 'get')
34
+ return;
35
+ const attributes = new Map(asked
36
+ .split('\n')
37
+ .filter((line) => line.includes('='))
38
+ .map((line) => [line.slice(0, line.indexOf('=')), line.slice(line.indexOf('=') + 1)]));
39
+ const ownAgent = await readOwnAgent();
40
+ if (ownAgent === null) {
41
+ process.stderr.write(`kit: Kit home ${kitHome()} is not signed in; run kit login\n`);
42
+ return;
43
+ }
44
+ const house = new URL(ownAgent.house);
45
+ if (attributes.get('protocol') !== house.protocol.slice(0, -1) || attributes.get('host') !== house.host)
46
+ return;
47
+ process.stdout.write(`username=house\npassword=${ownAgent.credential}\n`);
48
+ }
@@ -1,10 +1,10 @@
1
- import { setGlobalProxyFromEnv } from 'node:http';
2
1
  import { HouseRefusal, post } from './api.js';
3
2
  import { readEnrolment } from './home.js';
3
+ import { proxyUnlessRelayed } from './relay.js';
4
4
  function unanswered(error) {
5
5
  return error instanceof HouseRefusal ? error.status >= 500 : error instanceof TypeError && error.message === 'fetch failed';
6
6
  }
7
- setGlobalProxyFromEnv();
7
+ proxyUnlessRelayed();
8
8
  try {
9
9
  const { house, credential } = (await readEnrolment());
10
10
  try {