@agentshouse/kit 0.2.1-alpha.8 → 0.2.1-c05cd91c26b1d

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 (150) hide show
  1. package/README.md +46 -21
  2. package/dist/abort.js +9 -0
  3. package/dist/acp.js +48 -85
  4. package/dist/agents.js +312 -53
  5. package/dist/api.js +40 -4
  6. package/dist/app-git.js +48 -0
  7. package/dist/bootstrap.json +10 -14
  8. package/dist/bridge.js +92 -77
  9. package/dist/chatgpt.js +32 -0
  10. package/dist/clis-main.js +23 -0
  11. package/dist/clis.js +213 -71
  12. package/dist/codex-main.js +58 -0
  13. package/dist/configure-main.js +6 -0
  14. package/dist/conversations.js +429 -232
  15. package/dist/copies.js +233 -171
  16. package/dist/end.js +23 -0
  17. package/dist/enrol-main.js +7 -1
  18. package/dist/events.js +525 -0
  19. package/dist/files.js +24 -3
  20. package/dist/git-main.js +2 -2
  21. package/dist/git.js +15 -9
  22. package/dist/home.js +141 -10
  23. package/dist/house-cli.js +291 -0
  24. package/dist/house-main.js +5 -102
  25. package/dist/inputs.js +37 -0
  26. package/dist/instructions.js +10 -31
  27. package/dist/kit-cli.js +68 -0
  28. package/dist/kit-main.js +8 -16
  29. package/dist/login.js +129 -94
  30. package/dist/logout.js +19 -0
  31. package/dist/mcp.js +39 -0
  32. package/dist/operation.js +7 -4
  33. package/dist/parts.js +82 -0
  34. package/dist/placement.js +30 -0
  35. package/dist/refusals.js +53 -0
  36. package/dist/relay.js +20 -0
  37. package/dist/resident.js +123 -40
  38. package/dist/scope.js +203 -0
  39. package/dist/secret-input.js +7 -2
  40. package/dist/service-main.js +9 -0
  41. package/dist/service.js +33 -0
  42. package/dist/shell.js +3 -0
  43. package/dist/sign-in.js +47 -28
  44. package/dist/skills.js +147 -0
  45. package/dist/stream.js +89 -13
  46. package/dist/transcription.js +170 -0
  47. package/dist/update.js +138 -0
  48. package/dist/wire.js +6 -0
  49. package/dist/work.js +346 -0
  50. package/how-we-work.md +33 -0
  51. package/package.json +8 -8
  52. package/skills/ask-matt/LICENSE +21 -0
  53. package/skills/ask-matt/PHASE-BOUNDARIES.md +55 -0
  54. package/skills/ask-matt/SKILL.md +97 -0
  55. package/skills/ask-matt/agents/openai.yaml +5 -0
  56. package/skills/code-review/LICENSE +21 -0
  57. package/skills/code-review/SKILL.md +89 -0
  58. package/skills/code-review/agents/openai.yaml +3 -0
  59. package/skills/codebase-design/DEEPENING.md +37 -0
  60. package/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  61. package/skills/codebase-design/LICENSE +21 -0
  62. package/skills/codebase-design/SKILL.md +114 -0
  63. package/skills/codebase-design/agents/openai.yaml +3 -0
  64. package/skills/diagnosing-bugs/LICENSE +21 -0
  65. package/skills/diagnosing-bugs/SKILL.md +138 -0
  66. package/skills/diagnosing-bugs/agents/openai.yaml +3 -0
  67. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
  68. package/skills/domain-modeling/ADR-FORMAT.md +47 -0
  69. package/skills/domain-modeling/GLOSSARY-FORMAT.md +60 -0
  70. package/skills/domain-modeling/LICENSE +21 -0
  71. package/skills/domain-modeling/SKILL.md +74 -0
  72. package/skills/domain-modeling/agents/openai.yaml +3 -0
  73. package/skills/grill-me/LICENSE +21 -0
  74. package/skills/grill-me/SKILL.md +7 -0
  75. package/skills/grill-me/agents/openai.yaml +5 -0
  76. package/skills/grill-with-docs/LICENSE +21 -0
  77. package/skills/grill-with-docs/SKILL.md +7 -0
  78. package/skills/grill-with-docs/agents/openai.yaml +5 -0
  79. package/skills/grilling/LICENSE +21 -0
  80. package/skills/grilling/SKILL.md +30 -0
  81. package/skills/grilling/agents/openai.yaml +3 -0
  82. package/skills/handoff/LICENSE +21 -0
  83. package/skills/handoff/SKILL.md +16 -0
  84. package/skills/handoff/agents/openai.yaml +5 -0
  85. package/skills/implement/LICENSE +21 -0
  86. package/skills/implement/SKILL.md +17 -0
  87. package/skills/implement/agents/openai.yaml +5 -0
  88. package/skills/implement-spec/LICENSE +21 -0
  89. package/skills/implement-spec/SKILL.md +40 -0
  90. package/skills/implement-spec/agents/openai.yaml +5 -0
  91. package/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
  92. package/skills/improve-codebase-architecture/LICENSE +21 -0
  93. package/skills/improve-codebase-architecture/SKILL.md +71 -0
  94. package/skills/improve-codebase-architecture/agents/openai.yaml +5 -0
  95. package/skills/pr/CREDITS.md +3 -0
  96. package/skills/pr/LICENSE +21 -0
  97. package/skills/pr/SKILL.md +170 -0
  98. package/skills/pr/agents/openai.yaml +3 -0
  99. package/skills/prototype/LICENSE +21 -0
  100. package/skills/prototype/LOGIC.md +67 -0
  101. package/skills/prototype/SKILL.md +26 -0
  102. package/skills/prototype/UI.md +112 -0
  103. package/skills/prototype/agents/openai.yaml +3 -0
  104. package/skills/research/LICENSE +21 -0
  105. package/skills/research/SKILL.md +12 -0
  106. package/skills/research/agents/openai.yaml +3 -0
  107. package/skills/retro/LICENSE +21 -0
  108. package/skills/retro/SKILL.md +44 -0
  109. package/skills/retro/agents/openai.yaml +5 -0
  110. package/skills/tdd/LICENSE +21 -0
  111. package/skills/tdd/SKILL.md +38 -0
  112. package/skills/tdd/agents/openai.yaml +3 -0
  113. package/skills/tdd/mocking.md +59 -0
  114. package/skills/tdd/tests.md +77 -0
  115. package/skills/teach/GLOSSARY-FORMAT.md +35 -0
  116. package/skills/teach/LEARNING-RECORD-FORMAT.md +46 -0
  117. package/skills/teach/LICENSE +21 -0
  118. package/skills/teach/MISSION-FORMAT.md +31 -0
  119. package/skills/teach/RESOURCES-FORMAT.md +32 -0
  120. package/skills/teach/SKILL.md +140 -0
  121. package/skills/teach/agents/openai.yaml +5 -0
  122. package/skills/to-questionnaire/LICENSE +21 -0
  123. package/skills/to-questionnaire/SKILL.md +54 -0
  124. package/skills/to-questionnaire/agents/openai.yaml +5 -0
  125. package/skills/to-spec/LICENSE +21 -0
  126. package/skills/to-spec/SKILL.md +75 -0
  127. package/skills/to-spec/agents/openai.yaml +5 -0
  128. package/skills/to-tickets/LICENSE +21 -0
  129. package/skills/to-tickets/SKILL.md +105 -0
  130. package/skills/to-tickets/agents/openai.yaml +5 -0
  131. package/skills/triage/AGENT-BRIEF.md +207 -0
  132. package/skills/triage/LICENSE +21 -0
  133. package/skills/triage/OUT-OF-SCOPE.md +105 -0
  134. package/skills/triage/SKILL.md +112 -0
  135. package/skills/triage/agents/openai.yaml +5 -0
  136. package/skills/wait-what/LICENSE +21 -0
  137. package/skills/wait-what/SKILL.md +7 -0
  138. package/skills/wait-what/agents/openai.yaml +5 -0
  139. package/skills/wayfinder/LICENSE +21 -0
  140. package/skills/wayfinder/SKILL.md +128 -0
  141. package/skills/wayfinder/agents/openai.yaml +5 -0
  142. package/skills/wizard/LICENSE +21 -0
  143. package/skills/wizard/SKILL.md +44 -0
  144. package/skills/wizard/agents/openai.yaml +3 -0
  145. package/skills/wizard/template.sh +223 -0
  146. package/skills/writing-for-agents/LICENSE +21 -0
  147. package/skills/writing-for-agents/SKILL-MECHANICS.md +22 -0
  148. package/skills/writing-for-agents/SKILL.md +81 -0
  149. package/skills/writing-for-agents/agents/openai.yaml +3 -0
  150. package/dist/blocks.js +0 -51
package/dist/copies.js CHANGED
@@ -2,33 +2,62 @@ import { parse } from '@agentshouse/mdmodel';
2
2
  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
- import { HouseRefusal } from './api.js';
6
- import { blobId, blobText, git, HOUSE_REF, importCommits, revision, run, succeeded, tree, WORKING_COPIES, } from './git.js';
5
+ import { finalRefusal, HouseRefusal, retrying } from './api.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
+ import { refusalOf } from './refusals.js';
9
10
  import { changed } from './translate.js';
11
+ import { relayed } from './relay.js';
10
12
  const BRANCH = 'main';
11
- const ROOM_HOMES = ['capture', 'library', 'collaboration'];
12
13
  const PROVENANCE = '_provenance';
13
- const REPLICA_RETRY_MS = 30_000;
14
14
  const IDENTITY = { GIT_COMMITTER_NAME: 'House Kit', GIT_COMMITTER_EMAIL: 'house-kit@localhost' };
15
15
  function causeOf(error) {
16
16
  return error instanceof Error ? error.message : String(error);
17
17
  }
18
+ function houseCause(error) {
19
+ return error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
20
+ }
18
21
  function logged(error) {
19
22
  process.stderr.write(`kit: ${causeOf(error)}\n`);
20
23
  }
21
- export function refusalCode(text) {
24
+ const REBASED = new Set([
25
+ 'field_base_stale',
26
+ 'section_base_stale',
27
+ 'preamble_base_stale',
28
+ 'base_revision_stale',
29
+ 'path_exists',
30
+ 'path_absent',
31
+ ]);
32
+ function rebaseFixes(refusal) {
33
+ return refusal.code === 'edit_conflict' && refusal.conflicts.some((code) => REBASED.has(code));
34
+ }
35
+ function textOnly(paths) {
36
+ const [named, them] = paths.length === 1 ? [`${paths[0]} is`, 'it'] : [`${paths.join(', ')} are`, 'them'];
37
+ return `${named} not text, and house push sends text only; remove ${them} from the commit and place ${them} with house upload`;
38
+ }
39
+ function outside(path) {
40
+ return { refused: true, text: `${path} is not a House Local copy; run house push inside one` };
41
+ }
42
+ function short(commit) {
43
+ return commit.slice(0, 12);
44
+ }
45
+ function moved(before, after) {
46
+ if (before.house !== after.house)
47
+ return [`${rootOf(after.room_handle)}: ${HOUSE_REF} is at ${short(after.house)}`];
48
+ return before.position === after.position ? [] : [`${rootOf(after.room_handle)}: read-only files refreshed`];
49
+ }
50
+ function expiredAuthority(error) {
51
+ if (!(error instanceof HouseRefusal))
52
+ return undefined;
22
53
  try {
23
- return String(JSON.parse(text).error.code);
54
+ const { error: refused } = JSON.parse(error.text);
55
+ return refused?.code === 'expired' && typeof refused.authority === 'string' ? refused.authority : undefined;
24
56
  }
25
57
  catch {
26
- return text.split(':')[0].trim();
58
+ return undefined;
27
59
  }
28
60
  }
29
- function short(commit) {
30
- return commit.slice(0, 12);
31
- }
32
61
  function covers(path, prefix) {
33
62
  return prefix === '' || path === prefix || path.startsWith(`${prefix}/`);
34
63
  }
@@ -37,14 +66,14 @@ function sidecar(path) {
37
66
  segments.splice(-1, 0, PROVENANCE);
38
67
  return segments.join('/');
39
68
  }
40
- function rootOf(copy) {
41
- return copy.private ? '/private' : `/rooms/${copy.room_handle}`;
69
+ function recordOf(key) {
70
+ return join(kitHome(), 'local-copies', `${key}.json`);
42
71
  }
43
- function relative(path) {
44
- return path.replace(/^\/private\//, '').replace(/^\/rooms\/[^/]+\//, '');
72
+ function rootOf(handle) {
73
+ return `/rooms/${handle}`;
45
74
  }
46
- function prefixesOf(selected) {
47
- return selected.private ? ['/private'] : ROOM_HOMES.map((home) => `${rootOf(selected)}/${home}`);
75
+ function relative(path) {
76
+ return path.replace(/^\/rooms\/[^/]+\//, '');
48
77
  }
49
78
  function declaredWritable(path, copy) {
50
79
  const deepest = (scopes) => Math.max(-1, ...scopes.filter((scope) => covers(path, scope)).map((scope) => scope.length));
@@ -147,34 +176,35 @@ function movedPaths(moves, before, after) {
147
176
  .filter((pair) => pairs.filter((other) => other.from === pair.from || other.to === pair.to).length === 1)
148
177
  .map(({ from, to }) => [from, to]));
149
178
  }
150
- export class WorkingCopies {
179
+ export class LocalCopies {
151
180
  owner;
152
181
  house;
153
182
  copies = new Map();
154
183
  queues = new Map();
155
- delivering = new Set();
156
- selecting = Promise.resolve();
184
+ answering = Promise.resolve();
157
185
  constructor(house) {
158
186
  this.house = house;
159
187
  this.owner = {
160
188
  key: 'owner',
161
189
  submit: async (operation, changes) => {
162
190
  try {
163
- const outcome = await house.post('/kit/door/batch', { operation_id: operation, changes });
191
+ const outcome = await retrying(() => house.post('/kit/door/batch', { operation_id: operation, changes }));
164
192
  return { accepted: outcome.contents };
165
193
  }
166
194
  catch (error) {
167
- if (!(error instanceof HouseRefusal) || error.status >= 500)
195
+ if (!finalRefusal(error))
168
196
  throw error;
169
- return { refused: { code: refusalCode(error.text), text: error.message } };
197
+ return {
198
+ refused: refusalOf(error.text) ?? { code: '', text: `House refused it with ${error.status}`, conflicts: [] },
199
+ };
170
200
  }
171
201
  },
172
202
  };
173
203
  }
174
204
  async load() {
175
- const home = join(kitHome(), 'working-copies');
205
+ const home = join(kitHome(), 'local-copies');
176
206
  const names = (await readdir(join(home, 'rooms')).catch(() => [])).filter((name) => name.endsWith('.json'));
177
- for (const path of [...names.map((name) => join(home, 'rooms', name)), join(home, 'private.json')]) {
207
+ for (const path of names.map((name) => join(home, 'rooms', name))) {
178
208
  const text = await readFile(path, 'utf8').catch(() => null);
179
209
  if (text === null)
180
210
  continue;
@@ -183,47 +213,73 @@ export class WorkingCopies {
183
213
  }
184
214
  }
185
215
  async mapping() {
186
- await this.selecting;
216
+ await this.answering;
187
217
  return [...this.copies.values()]
188
- .filter((copy) => copy.active)
189
- .map((copy) => `${rootOf(copy)}: ${copy.repository}`)
218
+ .filter((copy) => copy.erasing === undefined)
219
+ .map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
190
220
  .sort();
191
221
  }
192
- select() {
193
- const selected = this.selecting.then(() => this.reselect());
194
- this.selecting = selected.catch(() => undefined);
195
- return selected;
196
- }
197
- async reselect() {
198
- const { working_copy: chosen } = await this.house.post('/kit/working-copy/selection', {});
199
- const selected = [
200
- ...(chosen?.rooms ?? [])
201
- .filter((room) => room.room_handle !== null)
202
- .map((room) => ({ key: `rooms/${room.room_ref}`, private: false, handle: room.room_handle })),
203
- ...(chosen?.private === true ? [{ key: 'private', private: true, handle: null }] : []),
204
- ];
205
- const keys = new Set(selected.map((scope) => scope.key));
206
- const work = [];
207
- for (const key of this.copies.keys()) {
208
- if (keys.has(key))
209
- continue;
210
- work.push(this.exclusive(key, async () => {
211
- const copy = this.copies.get(key);
212
- if (copy.active)
213
- await this.save({ ...copy, active: false });
214
- }));
215
- }
216
- for (const scope of selected)
217
- work.push(this.exclusive(scope.key, () => this.apply(scope)).catch(logged));
218
- await Promise.all(work);
219
- }
220
- entries(frame) {
221
- const copy = [...this.copies.values()].find((held) => held.active && held.room_ref === frame.authority);
222
- if (copy === undefined)
223
- return;
224
- if (copy.log_epoch === frame.log_epoch && BigInt(frame.position) <= BigInt(copy.position))
225
- return;
226
- this.deliver(copy.key);
222
+ async connected() {
223
+ const copied = await this.answered(() => this.fetched(false));
224
+ if (copied.refused)
225
+ logged(copied.text);
226
+ }
227
+ fetch() {
228
+ return this.answered(() => this.fetched(true));
229
+ }
230
+ answered(work) {
231
+ const answered = this.answering.then(work);
232
+ this.answering = answered.catch(() => undefined);
233
+ return answered;
234
+ }
235
+ async fetched(refreshing) {
236
+ const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', {});
237
+ const synced = new Map(answer.rooms.map((room) => [`rooms/${room.room_ref}`, room.room_handle]));
238
+ return this.holding([...new Set([...this.copies.keys(), ...synced.keys()])].sort(), async () => {
239
+ const said = [];
240
+ let refused = false;
241
+ for (const copy of [...this.copies.values()]) {
242
+ const returned = synced.has(copy.key);
243
+ if (returned && copy.erasing === undefined)
244
+ continue;
245
+ try {
246
+ await this.erase(copy);
247
+ said.push(`${rootOf(copy.room_handle)}: ${returned ? '' : 'no longer synced to this Environment; '}erased from the Local copy`);
248
+ }
249
+ catch (error) {
250
+ refused = true;
251
+ said.push(`${rootOf(copy.room_handle)}: ${causeOf(error)}`);
252
+ }
253
+ }
254
+ const current = [];
255
+ for (const [key, handle] of synced) {
256
+ try {
257
+ const held = this.copies.get(key);
258
+ if (held?.erasing)
259
+ continue;
260
+ if (held === undefined || !(await exists(held.repository))) {
261
+ said.push(await this.materialize(key, handle));
262
+ continue;
263
+ }
264
+ current.push(held.room_handle === handle ? held : await this.save({ ...held, room_handle: handle }));
265
+ }
266
+ catch (error) {
267
+ refused = true;
268
+ said.push(`${rootOf(handle)}: ${causeOf(error)}`);
269
+ }
270
+ }
271
+ if (refreshing && current.length > 0) {
272
+ try {
273
+ const refreshed = await this.refreshed(current);
274
+ current.forEach((copy, index) => said.push(...moved(copy, refreshed[index])));
275
+ }
276
+ catch (error) {
277
+ refused = true;
278
+ said.push(houseCause(error));
279
+ }
280
+ }
281
+ return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
282
+ });
227
283
  }
228
284
  async observed(report) {
229
285
  const key = await this.keyAt(dirname(report.common));
@@ -231,6 +287,8 @@ export class WorkingCopies {
231
287
  return;
232
288
  await this.exclusive(key, async () => {
233
289
  const copy = this.copies.get(key);
290
+ if (copy === undefined || copy.erasing)
291
+ return;
234
292
  let moves = copy.moves;
235
293
  for (const { from, to } of report.moves) {
236
294
  const held = moves.find((move) => move.worktree === report.worktree && move.to === from);
@@ -253,11 +311,11 @@ export class WorkingCopies {
253
311
  async push(cwd, selector, caller) {
254
312
  const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
255
313
  if (located.status !== 0)
256
- return { refused: true, text: `${cwd} is not in a Git repository` };
314
+ return outside(cwd);
257
315
  const [common, worktree] = located.stdout.toString('utf8').split('\n');
258
316
  const key = await this.keyAt(dirname(common));
259
317
  if (key === undefined)
260
- return { refused: true, text: `${worktree} is not a House working copy` };
318
+ return outside(worktree);
261
319
  return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
262
320
  }
263
321
  async keyAt(repository) {
@@ -273,19 +331,12 @@ export class WorkingCopies {
273
331
  this.queues.set(key, queued.catch(() => undefined));
274
332
  return queued;
275
333
  }
276
- deliver(key) {
277
- if (this.delivering.has(key))
278
- return;
279
- this.delivering.add(key);
280
- void this.exclusive(key, async () => {
281
- this.delivering.delete(key);
282
- const copy = this.copies.get(key);
283
- if (copy.active)
284
- await this.save(await this.caughtUp(copy));
285
- }).catch(logged);
334
+ holding(keys, work) {
335
+ const [key, ...rest] = keys;
336
+ return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
286
337
  }
287
338
  async save(copy) {
288
- const path = join(kitHome(), 'working-copies', copy.private ? 'private.json' : `${copy.key}.json`);
339
+ const path = recordOf(copy.key);
289
340
  await mkdir(dirname(path), { recursive: true, mode: 0o700 });
290
341
  const temporary = `${path}.${process.pid}`;
291
342
  await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
@@ -293,12 +344,18 @@ export class WorkingCopies {
293
344
  this.copies.set(copy.key, copy);
294
345
  return copy;
295
346
  }
296
- async read(selected) {
297
- const prefixes = prefixesOf(selected);
347
+ async erase(copy) {
348
+ await this.save({ ...copy, erasing: true });
349
+ await rm(copy.repository, { recursive: true, force: true });
350
+ await rm(recordOf(copy.key), { force: true });
351
+ this.copies.delete(copy.key);
352
+ }
353
+ async read(handle) {
354
+ const prefixes = [rootOf(handle)];
298
355
  const discovery = await this.house.post('/kit/door/discover', { prefixes });
299
356
  let bundle = await this.house.post('/kit/door/bootstrap', { prefixes });
300
357
  if ('transfer' in bundle)
301
- bundle = (await (await fetch(bundle.transfer.url)).json());
358
+ bundle = (await (await fetch(relayed(bundle.transfer.url))).json());
302
359
  const [view] = bundle.scopes;
303
360
  if (view === undefined)
304
361
  return null;
@@ -328,46 +385,13 @@ export class WorkingCopies {
328
385
  }
329
386
  return read;
330
387
  }
331
- async apply(scope) {
332
- let copy = this.copies.get(scope.key);
333
- if (copy === undefined || !(await exists(copy.repository))) {
334
- await this.materialize(scope);
335
- return;
336
- }
337
- if (copy.room_handle !== scope.handle)
338
- copy = await this.save({ ...copy, room_handle: scope.handle });
339
- if (!copy.active) {
340
- await this.save(await this.received({ ...copy, active: true }));
341
- return;
342
- }
343
- const discovery = await this.house.post('/kit/door/discover', {
344
- prefixes: prefixesOf(copy),
345
- });
346
- const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
347
- if (authority === undefined)
348
- return;
349
- if (authority.managed_replication !== 'admitted') {
350
- logged(`operation_denied: ${rootOf(copy)} is not synchronized to this Environment`);
351
- return;
352
- }
353
- if (JSON.stringify([authority.operations.write, authority.protected_paths]) !==
354
- JSON.stringify([copy.write, copy.protected])) {
355
- await this.save(await this.received(copy));
356
- return;
357
- }
358
- await this.save(await this.caughtUp(copy));
359
- }
360
- async materialize(scope) {
361
- const read = await this.read({ private: scope.private, room_handle: scope.handle });
362
- if (read === null) {
363
- logged(`House serves nothing of ${rootOf({ private: scope.private, room_handle: scope.handle })} to copy`);
364
- return;
365
- }
366
- const repository = scope.private ? join(WORKING_COPIES, 'private') : join(WORKING_COPIES, 'rooms', read.anchor);
367
- if (await exists(repository)) {
368
- logged(`${repository} already exists and House Kit leaves it as it is`);
369
- return;
370
- }
388
+ async materialize(key, handle) {
389
+ const read = await this.read(handle);
390
+ if (read === null)
391
+ throw new Error('House serves nothing of it to copy');
392
+ const repository = join(localCopies(), 'rooms', read.anchor);
393
+ if (await exists(repository))
394
+ throw new Error(`${repository} already exists and House Kit leaves it as it is`);
371
395
  const staging = join(dirname(repository), `.${randomUUID()}`);
372
396
  await mkdir(staging, { recursive: true });
373
397
  let house;
@@ -375,7 +399,7 @@ export class WorkingCopies {
375
399
  try {
376
400
  await git(staging, ['init', '-q', '-b', BRANCH]);
377
401
  house = await importCommits(staging, [
378
- { message: `House ${rootOf({ private: scope.private, room_handle: scope.handle })}`, from: null, files: read.writable },
402
+ { message: `House ${rootOf(handle)}`, from: null, files: read.writable },
379
403
  ]);
380
404
  await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
381
405
  await git(staging, ['reset', '-q', '--hard']);
@@ -388,12 +412,10 @@ export class WorkingCopies {
388
412
  throw error;
389
413
  }
390
414
  await this.save({
391
- key: scope.key,
392
- private: scope.private,
415
+ key,
393
416
  room_ref: read.anchor,
394
- room_handle: scope.handle,
417
+ room_handle: handle,
395
418
  repository,
396
- active: true,
397
419
  house,
398
420
  position: read.position,
399
421
  log_epoch: read.log_epoch,
@@ -403,13 +425,14 @@ export class WorkingCopies {
403
425
  manifests: { [house]: { parent: null, files: read.revisions } },
404
426
  moves: [],
405
427
  });
428
+ return `${rootOf(handle)}: copied to ${repository}`;
406
429
  }
407
430
  async received(copy) {
408
- const read = await this.read(copy);
431
+ const read = await this.read(copy.room_handle);
409
432
  if (read === null)
410
433
  return copy;
411
434
  const house = await importCommits(copy.repository, [
412
- { message: `House ${rootOf(copy)} at position ${read.position}`, from: copy.house, replaces: true, files: read.writable },
435
+ { message: `House ${rootOf(copy.room_handle)} at position ${read.position}`, from: copy.house, replaces: true, files: read.writable },
413
436
  ]);
414
437
  const removed = copy.read_only.filter((path) => !read.readOnly.has(path));
415
438
  const readOnly = await readOnlyPlaced(copy.repository, copy.read_only, read.readOnly, removed);
@@ -426,30 +449,52 @@ export class WorkingCopies {
426
449
  await ignoreRules(copy.repository, [...read.writable.keys()], next);
427
450
  return next;
428
451
  }
429
- async caughtUp(start) {
430
- let copy = start;
431
- for (;;) {
452
+ async refreshed(copies) {
453
+ const discovery = await this.house.post('/kit/door/discover', {
454
+ prefixes: copies.map((copy) => rootOf(copy.room_handle)),
455
+ });
456
+ const regranted = new Map();
457
+ for (const copy of copies) {
458
+ const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
459
+ if (authority !== undefined &&
460
+ JSON.stringify([authority.operations.write, authority.protected_paths]) !== JSON.stringify([copy.write, copy.protected])) {
461
+ regranted.set(copy.key, await this.save(await this.received(copy)));
462
+ }
463
+ }
464
+ const caught = await this.caughtUp(copies.filter((copy) => !regranted.has(copy.key)), false);
465
+ return copies.map((copy) => regranted.get(copy.key) ?? caught.find((held) => held.key === copy.key));
466
+ }
467
+ async caughtUp(copies, whole) {
468
+ const caught = new Map(copies.map((copy) => [copy.key, copy]));
469
+ let asking = [...copies];
470
+ while (asking.length > 0) {
432
471
  let answer;
433
472
  try {
434
473
  answer = await this.house.post('/kit/door/enumerate', {
435
- prefixes: prefixesOf(copy),
436
- positions: [{ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch }],
474
+ prefixes: asking.map((copy) => rootOf(copy.room_handle)),
475
+ positions: asking.map((copy) => ({ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch })),
437
476
  });
438
477
  }
439
478
  catch (error) {
440
- const code = error instanceof HouseRefusal ? refusalCode(error.text) : null;
441
- if (code === 'position_expired')
442
- return this.received(copy);
443
- if (code !== 'replica_behind')
479
+ const expired = asking.find((copy) => copy.room_ref === expiredAuthority(error));
480
+ if (expired === undefined)
444
481
  throw error;
445
- setTimeout(() => this.deliver(copy.key), REPLICA_RETRY_MS).unref();
446
- return copy;
482
+ caught.set(expired.key, await this.save(await this.received(expired)));
483
+ asking = asking.filter((copy) => copy !== expired);
484
+ continue;
485
+ }
486
+ const advanced = [];
487
+ for (const copy of asking) {
488
+ const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
489
+ if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
490
+ continue;
491
+ const next = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
492
+ caught.set(copy.key, next);
493
+ advanced.push(next);
447
494
  }
448
- const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
449
- if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
450
- return copy;
451
- copy = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
495
+ asking = whole ? advanced : [];
452
496
  }
497
+ return copies.map((copy) => caught.get(copy.key));
453
498
  }
454
499
  async incorporated(copy, changes, position, logEpoch) {
455
500
  const houseTree = await tree(copy.repository, copy.house);
@@ -482,7 +527,7 @@ export class WorkingCopies {
482
527
  let { house, manifests } = copy;
483
528
  if (files.size > 0) {
484
529
  house = await importCommits(copy.repository, [
485
- { message: `House ${rootOf(copy)} at position ${position}`, from: copy.house, files },
530
+ { message: `House ${rootOf(copy.room_handle)} at position ${position}`, from: copy.house, files },
486
531
  ]);
487
532
  manifests = { ...manifests, [house]: { parent: copy.house, files: revisions } };
488
533
  }
@@ -499,6 +544,8 @@ export class WorkingCopies {
499
544
  }
500
545
  async pushed(key, worktree, selector, caller) {
501
546
  let copy = this.copies.get(key);
547
+ if (copy === undefined || copy.erasing)
548
+ return outside(worktree);
502
549
  if (copy.push?.state === 'submitted')
503
550
  return this.settle(copy, caller);
504
551
  const selected = await run(worktree, ['rev-parse', '--verify', '--quiet', '--end-of-options', `${selector}^{commit}`]);
@@ -512,8 +559,6 @@ export class WorkingCopies {
512
559
  }
513
560
  copy = await this.save(without(copy));
514
561
  }
515
- if (!copy.active)
516
- return { refused: true, text: `${rootOf(copy)} is no longer kept in this Environment` };
517
562
  const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
518
563
  const base = listed.find((ancestor) => ancestor in copy.manifests);
519
564
  if (base === undefined) {
@@ -524,7 +569,7 @@ export class WorkingCopies {
524
569
  changes = await this.derived(copy, base, commit);
525
570
  }
526
571
  catch (error) {
527
- return { refused: true, text: causeOf(error) };
572
+ return { refused: true, text: houseCause(error) };
528
573
  }
529
574
  if (changes.length === 0)
530
575
  return { refused: false, text: `Nothing to submit: ${short(commit)} matches House.` };
@@ -546,12 +591,23 @@ export class WorkingCopies {
546
591
  return this.settle(submitted, caller);
547
592
  }
548
593
  async derived(copy, base, commit) {
549
- const root = rootOf(copy);
594
+ const root = rootOf(copy.room_handle);
550
595
  const before = await tree(copy.repository, base);
551
596
  const after = await tree(copy.repository, commit);
552
597
  const moved = movedPaths(copy.moves, before, after);
553
598
  const arrived = new Set(moved.values());
554
- const text = (entry, path) => blobText(copy.repository, entry, path);
599
+ const arriving = [...after].filter(([path, entry]) => before.get(path) !== entry);
600
+ if (arriving.length > 0) {
601
+ const { bounds } = await this.house.post('/kit/door/discover', { prefixes: [root] });
602
+ const binary = [];
603
+ for (const [path, entry] of arriving) {
604
+ if (!(await isText(copy.repository, entry, bounds.max_authored_file_bytes)))
605
+ binary.push(path);
606
+ }
607
+ if (binary.length > 0)
608
+ throw new Error(textOnly(binary));
609
+ }
610
+ const text = (entry) => blobText(copy.repository, entry);
555
611
  const changes = [];
556
612
  for (const path of [...new Set([...before.keys(), ...after.keys()])].sort()) {
557
613
  if (arrived.has(path))
@@ -564,7 +620,7 @@ export class WorkingCopies {
564
620
  changes.push({ op: 'rename', path: `${root}/${path}`, to: `${root}/${to}`, base: known });
565
621
  const moving = after.get(to);
566
622
  if (moving !== prior) {
567
- changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving, to) });
623
+ changes.push({ op: 'replace', path: `${root}/${to}`, base: known, content: await text(moving) });
568
624
  }
569
625
  continue;
570
626
  }
@@ -573,18 +629,17 @@ export class WorkingCopies {
573
629
  if (next === undefined)
574
630
  changes.push({ op: 'remove', path: `${root}/${path}`, base: known });
575
631
  else if (prior === undefined)
576
- changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next, path) });
632
+ changes.push({ op: 'create', path: `${root}/${path}`, content: await text(next) });
577
633
  else
578
- changes.push(...changed(`${root}/${path}`, path, known, await text(prior, path), await text(next, path)));
634
+ changes.push(...changed(`${root}/${path}`, path, known, await text(prior), await text(next)));
579
635
  }
580
636
  return changes;
581
637
  }
582
638
  async settle(start, caller, restaged = false) {
583
639
  let copy = start;
584
640
  const push = copy.push;
585
- if (push.caller !== caller.key) {
586
- return this.recovered(copy, `${short(push.commit)} was pushed by another caller and awaited its outcome`);
587
- }
641
+ if (push.caller !== caller.key)
642
+ return this.recovered(copy);
588
643
  let outcome;
589
644
  try {
590
645
  outcome = await caller.submit(push.operation_id, push.changes, push.upload, async (upload) => {
@@ -592,9 +647,10 @@ export class WorkingCopies {
592
647
  });
593
648
  }
594
649
  catch (error) {
650
+ const refusal = error instanceof HouseRefusal ? refusalOf(error.text) : null;
595
651
  return {
596
652
  refused: true,
597
- text: `House's answer to ${short(push.commit)} did not arrive (${causeOf(error)}); run house git push again to recover its outcome.`,
653
+ text: refusal?.text ?? `House's answer to ${short(push.commit)} did not arrive; run house push again`,
598
654
  };
599
655
  }
600
656
  if ('refused' in outcome) {
@@ -602,32 +658,38 @@ export class WorkingCopies {
602
658
  const { upload, ...unstaged } = copy.push;
603
659
  return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
604
660
  }
605
- if (outcome.refused.code === 'operation_expired')
606
- return this.recovered(copy, outcome.refused.text);
661
+ if (outcome.refused.code === 'expired')
662
+ return this.recovered(copy);
663
+ const rebased = rebaseFixes(outcome.refused);
607
664
  let settled = await this.save(without(copy));
608
665
  try {
609
- settled = await this.save(await this.caughtUp(settled));
666
+ settled = (await this.caughtUp([settled], true))[0];
610
667
  }
611
- catch (error) {
612
- return { refused: true, text: `${outcome.refused.text}\n${HOUSE_REF} is unchanged: ${causeOf(error)}` };
668
+ catch {
669
+ return {
670
+ refused: true,
671
+ text: rebased ? `${outcome.refused.text}\nHouse's current state did not arrive; push again later` : outcome.refused.text,
672
+ };
613
673
  }
614
674
  return {
615
675
  refused: true,
616
- text: `${outcome.refused.text}\n${HOUSE_REF} holds House's current state; merge or rebase onto it, commit and push again.`,
676
+ text: rebased
677
+ ? `${outcome.refused.text}\n${HOUSE_REF} holds House's current state; rebase onto it and push again`
678
+ : outcome.refused.text,
617
679
  };
618
680
  }
619
681
  return this.finished(await this.save({ ...copy, push: { ...copy.push, state: 'accepted', written: outcome.accepted } }));
620
682
  }
621
- async recovered(copy, reason) {
683
+ async recovered(copy) {
622
684
  const push = copy.push;
623
- const current = await this.save(await this.caughtUp(copy));
685
+ const current = (await this.caughtUp([copy], true))[0];
624
686
  const house = await tree(copy.repository, current.house);
625
- const root = rootOf(current);
687
+ const root = rootOf(current.room_handle);
626
688
  if (!(await this.landed(copy.repository, push.changes, house, await tree(copy.repository, push.commit)))) {
627
689
  await this.save(without(current));
628
690
  return {
629
691
  refused: true,
630
- text: `${reason}; House does not hold ${short(push.commit)}; run house git push again to submit it as a new operation.`,
692
+ text: `House does not hold ${short(push.commit)}; run house push again`,
631
693
  };
632
694
  }
633
695
  const written = [];
@@ -636,7 +698,7 @@ export class WorkingCopies {
636
698
  written.push({
637
699
  path: `${root}/${path}`,
638
700
  revision: entry === undefined ? null : (revisionAt(current, current.house, path) ?? null),
639
- content: entry === undefined ? null : await blobText(copy.repository, entry, path),
701
+ content: entry === undefined ? null : await blobText(copy.repository, entry),
640
702
  });
641
703
  }
642
704
  return this.finished(await this.save({ ...current, push: { ...push, state: 'accepted', written } }));
@@ -652,10 +714,10 @@ export class WorkingCopies {
652
714
  await this.save(without(copy));
653
715
  return { refused: false, text: integrated.text };
654
716
  }
655
- catch (error) {
717
+ catch {
656
718
  return {
657
719
  refused: false,
658
- text: `House accepted ${short(commit)}; local integration is pending (${causeOf(error)}); run house git push again to finish it.`,
720
+ text: `House accepted ${short(commit)}; run house push again to finish integrating it here`,
659
721
  };
660
722
  }
661
723
  }
@@ -664,7 +726,7 @@ export class WorkingCopies {
664
726
  const push = copy.push;
665
727
  let written = push.written;
666
728
  if (push.changes.some((change) => change.op === 'rename')) {
667
- copy = await this.save(await this.caughtUp(copy));
729
+ copy = (await this.caughtUp([copy], true))[0];
668
730
  const house = await tree(copy.repository, copy.house);
669
731
  const sent = await tree(copy.repository, push.house);
670
732
  if (push.changes.some((change) => change.op === 'rename' && !house.has(relative(change.to)))) {
@@ -678,9 +740,9 @@ export class WorkingCopies {
678
740
  written = [
679
741
  ...written,
680
742
  {
681
- path: `${rootOf(copy)}/${path}`,
743
+ path: `${rootOf(copy.room_handle)}/${path}`,
682
744
  revision: entry === undefined ? null : (revisionAt(copy, copy.house, path) ?? null),
683
- content: entry === undefined ? null : await blobText(copy.repository, entry, path),
745
+ content: entry === undefined ? null : await blobText(copy.repository, entry),
684
746
  },
685
747
  ];
686
748
  }
@@ -698,7 +760,7 @@ export class WorkingCopies {
698
760
  }
699
761
  const edits = new Map([...files].filter(([path, content]) => (content === null ? selected.has(path) : selected.get(path) !== blobId(content))));
700
762
  const received = [...files.keys()].filter((path) => revisionAt(copy, copy.house, path) === revisionAt(copy, push.house, path));
701
- const accepted = `House ${rootOf(copy)} with ${push.commit} accepted`;
763
+ const accepted = `House ${rootOf(copy.room_handle)} with ${push.commit} accepted`;
702
764
  const house = await importCommits(copy.repository, [
703
765
  ...(edits.size === 0 ? [] : [{ message: accepted, from: push.commit, files: edits }]),
704
766
  {
@@ -725,7 +787,7 @@ export class WorkingCopies {
725
787
  async landed(repository, changes, house, selected) {
726
788
  const text = async (path) => {
727
789
  const entry = house.get(relative(path));
728
- return entry === undefined ? undefined : blobText(repository, entry, path);
790
+ return entry === undefined ? undefined : blobText(repository, entry);
729
791
  };
730
792
  for (const change of changes) {
731
793
  const current = await text(change.path);