@agentshouse/kit 0.1.0-alpha.20 → 0.1.0-alpha.22

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.
package/README.md CHANGED
@@ -10,7 +10,7 @@ On Linux `amd64` or `arm64` with a working Docker Engine, run the published,
10
10
  versioned bootstrap once:
11
11
 
12
12
  ```sh
13
- bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.20/connect-linux.sh)
13
+ bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.22/connect-linux.sh)
14
14
  ```
15
15
 
16
16
  The release asset pins one immutable multi-architecture image digest. It creates
@@ -142,11 +142,18 @@ which the User wrote once this adoption's proof had passed. The adapter
142
142
  packages themselves are Environment prerequisites, not Kit dependencies, and
143
143
  carry no record.
144
144
 
145
- The bootstrap invokes bare `kit login`. The first login names its new
145
+ The bootstrap invokes bare `kit login`, which connects to `https://agents.house`;
146
+ `--house <origin>` names another House. Login records the House in Kit home
147
+ beside the credential. Every House-bound command talks to that recorded House,
148
+ and every command but login refuses `--house` as an unknown argument. The first login names its new
146
149
  Environment after the host computer and stores its Environment id in Kit home;
147
- an explicit later login reconnects that retained id. `--environment <uuid>`
148
- selects a different retained Environment for explicit recovery. A malformed
149
- id or an answer naming a different Environment is refused before installation.
150
+ the panel renames it. An explicit later login reconnects that retained id, and
151
+ `--environment <uuid>` reconnects the Environment it names instead, whether or
152
+ not Kit home records one, and Kit home then records that id. Reconnecting issues the next credential for the same Environment
153
+ and ends the previous one, including one `envs/revoke` ended; House refuses an
154
+ Environment it no longer holds with its cause and never creates a replacement.
155
+ A malformed id or an answer naming a different Environment is refused before
156
+ installation.
150
157
 
151
158
  Use `--manual` when the browser cannot reach the Kit's loopback listener. The
152
159
  command stores the issued Kit User credential in `$HOUSE_KIT_HOME/credential`
@@ -253,7 +260,7 @@ and the batch it produces, and the lease has to outlive both.
253
260
  Apply the declared source state to House:
254
261
 
255
262
  ```sh
256
- kit apply --house https://agents.house
263
+ kit apply
257
264
  ```
258
265
 
259
266
  Only the state House declares - instances, services, `source_ref`, and Room
@@ -442,7 +449,7 @@ its owner asks it to renew what the Environment holds. The Kit acts on the
442
449
  request it acquires, never on its own:
443
450
 
444
451
  ```sh
445
- kit setup --house https://agents.house
452
+ kit setup
446
453
  ```
447
454
 
448
455
  `kit worker run` performs the same step at the start of a pass, before it
@@ -558,7 +565,7 @@ command returns exactly one JSON object such as `{ "authorized": true }`.
558
565
  ## The Worker runtime
559
566
 
560
567
  ```sh
561
- kit worker run --house https://agents.house
568
+ kit worker run
562
569
  ```
563
570
 
564
571
  Worker-runtime readiness validates the tmux this Kit declares support for,
@@ -1112,8 +1119,8 @@ into the evidence each spec writes under `tmp/delivery-evidence/`.
1112
1119
  ## Capturing
1113
1120
 
1114
1121
  ```sh
1115
- kit source capture acme --house https://agents.house # every declared service
1116
- kit source capture acme notes --house https://agents.house # one service
1122
+ kit source capture acme # every declared service
1123
+ kit source capture acme notes # one service
1117
1124
  ```
1118
1125
 
1119
1126
  Each run reads the checkpoint, acquires one bounded page, submits one batch,
@@ -1146,8 +1153,8 @@ Room and Record and exits non-zero while any stays unsaved; acquisition health
1146
1153
  stays about acquisition, so a failed save never changes it.
1147
1154
 
1148
1155
  ```sh
1149
- kit source retry-files acme --house https://agents.house # every declared service
1150
- kit source retry-files acme notes --house https://agents.house # one service
1156
+ kit source retry-files acme # every declared service
1157
+ kit source retry-files acme notes # one service
1151
1158
  ```
1152
1159
 
1153
1160
  `retry-files` saves only the references still outstanding. It runs no connector,
@@ -1203,8 +1210,7 @@ Git credential:
1203
1210
  ```sh
1204
1211
  kit repository capture /srv/acme \
1205
1212
  --room work-room \
1206
- --source-handle acme-repository \
1207
- --house https://agents.house
1213
+ --source-handle acme-repository
1208
1214
  ```
1209
1215
 
1210
1216
  The command runs ordinary `git pull` in the checkout with the local User's Git
@@ -1261,9 +1267,35 @@ committed.
1261
1267
  Local edits, staging and commits change nothing in House until a commit is
1262
1268
  pushed. `refs/house/received` records the House content last received; each
1263
1269
  commit Kit wrote there, and each commit it acknowledged for a push, is a House
1264
- state. The House revision of every file content Kit received, keyed by path and
1265
- Git blob, stays in Kit home under `state/working-copies/`, outside the
1266
- repository. A local commit is a local tree, never a House revision.
1270
+ state. The House revision of every file content Kit received, keyed by path,
1271
+ Git blob and the House state that received it, stays in Kit home under
1272
+ `state/working-copies/`, outside the repository. A local commit is a local tree, never a House revision.
1273
+
1274
+ The resident Kit keeps every active copy current. On each pass, and whenever
1275
+ it reads the saved choice, one `POST /kit/door/discover` tells it where each
1276
+ copied Room stands, and for a Room that moved on it reads the changes after the
1277
+ copy's recorded position through `POST /kit/door/enumerate`. A changed, new or
1278
+ removed authored file becomes a commit on `refs/house/received`, its House
1279
+ revision kept in Kit home; changed, new or removed read-only content, such as
1280
+ Capture with its provenance sidecar or a House projection, is written into or
1281
+ removed from the ignored local tree, and `.gitignore` and `.ignore` follow it.
1282
+ Delivery never touches the branch, the index or a tracked file, and a push
1283
+ waiting for its outcome or its local integration keeps its record. A push takes
1284
+ each file's revision from the newest House state in its own base's history that
1285
+ received that content, so a local commit keeps the base it was made on even when
1286
+ House returns to the same text under a newer revision. A file new to the copy is tracked when a write scope
1287
+ House declares for the Room covers it more closely than a declared protected
1288
+ path, and a local file already sitting where new read-only content arrives
1289
+ stays yours: Kit neither overwrites nor later removes it. When the write scopes
1290
+ or protected paths House declares for the Room change, Kit reads the Room again
1291
+ as a reselection does. A position older than House
1292
+ retains, or from before House's database was restored, makes Kit read the Room
1293
+ again as a reselection does; while House's replica has not reached the position
1294
+ the copy waits for the next pass. A Room House no longer answers under its
1295
+ recorded handle makes Kit read the saved choice again, which follows a handle
1296
+ change or marks the copy inactive, and a Room that lost sync is named on
1297
+ standard error as `operation_denied` and keeps what it holds. `kit worker run`
1298
+ delivers once, after reading the saved choice.
1267
1299
 
1268
1300
  `house git push [commit]`, run anywhere inside a copy or one of its linked
1269
1301
  worktrees, submits one commit, `HEAD` by default, resolved once. Kit compares
@@ -1319,7 +1351,10 @@ commits, and pushes again. After acceptance Kit records House's result for the
1319
1351
  pushed paths, the contents an owner push receives or each accepted revision
1320
1352
  read by name, in a commit that covers the pushed one and in
1321
1353
  `refs/house/received`; a read-only path House regenerates, such as the Wiki
1322
- index, stays out of Git. Kit then
1354
+ index, stays out of Git. The copy's recorded position goes back to where it
1355
+ stood when the push was sent, so the next delivery brings again anything House
1356
+ changed after the push's result, including what delivery received while the
1357
+ push awaited its answer. Kit then
1323
1358
  fast-forwards a clean branch still at the pushed commit, or rebases only its
1324
1359
  later commits. A result not yet readable, uncommitted tracked changes,
1325
1360
  a moved branch or a rebase conflict leave the branch as it was, with no stash or
@@ -1335,8 +1370,8 @@ Room again: if House holds the pushed content the push finishes as accepted,
1335
1370
  otherwise it is dropped and the next push submits it as a new operation.
1336
1371
 
1337
1372
  Deselecting a Room, or losing read access to it, marks its copy inactive:
1338
- launches omit it, a new push is refused as `kit_working_copy_inactive`, and
1339
- nothing local is deleted; a push already sent still settles and integrates. Selecting it again reads the Room
1373
+ launches omit it, delivery stops, a new push is refused as
1374
+ `kit_working_copy_inactive`, and nothing local is deleted; a push already sent still settles and integrates. Selecting it again reads the Room
1340
1375
  again, advances `refs/house/received`, and replaces only the read-only content
1341
1376
  Kit wrote, never the branch, the index or a tracked file. A Room the owner may
1342
1377
  read without sync is refused as `operation_denied` and stays uncopied, while its
@@ -1358,13 +1393,12 @@ beside `library/**`.
1358
1393
 
1359
1394
  ```sh
1360
1395
  kit sync bootstrap ./replica \
1361
- --house https://agents.house \
1362
1396
  --prefix /rooms/work/library \
1363
1397
  --prefix /rooms/work/capture \
1364
1398
  --prefix /private/library
1365
- kit sync pull ./replica --house https://agents.house
1366
- kit sync submit ./replica --house https://agents.house --changes ./changes.json
1367
- kit sync rebootstrap ./replica --house https://agents.house
1399
+ kit sync pull ./replica
1400
+ kit sync submit ./replica --changes ./changes.json
1401
+ kit sync rebootstrap ./replica
1368
1402
  ```
1369
1403
 
1370
1404
  `bootstrap` writes `ROOM.md`, Library text, House-derived projections, and
@@ -1427,16 +1461,13 @@ absolute paths.
1427
1461
 
1428
1462
  ```sh
1429
1463
  kit attachment upload ./notes.bin \
1430
- --house https://agents.house \
1431
1464
  --room room:work \
1432
1465
  --media-type application/pdf
1433
1466
 
1434
1467
  kit attachment upload ./notes.bin \
1435
- --house https://agents.house \
1436
1468
  --private
1437
1469
 
1438
1470
  kit attachment get \
1439
- --house https://agents.house \
1440
1471
  --attachment at_1x.q \
1441
1472
  --room room:work \
1442
1473
  --output ./notes.bin
@@ -1482,7 +1513,7 @@ Any missing prerequisite refuses the create before House ever presents the
1482
1513
  workspace as constrained, naming the exact missing piece.
1483
1514
 
1484
1515
  ```sh
1485
- kit workspace host --house https://agents.house
1516
+ kit workspace host
1486
1517
  ```
1487
1518
 
1488
1519
  The command acquires this host's workspace work under its Environment control
@@ -1537,7 +1568,7 @@ image from the original the Session stored.
1537
1568
  Bind the bot to this Environment:
1538
1569
 
1539
1570
  ```sh
1540
- kit telegram connect --house https://agents.house
1571
+ kit telegram connect
1541
1572
  ```
1542
1573
 
1543
1574
  The command asks for the token on a hidden terminal prompt, or reads one line
@@ -1556,7 +1587,7 @@ proven owner's Telegram user id.
1556
1587
  Run the gateway:
1557
1588
 
1558
1589
  ```sh
1559
- kit telegram serve --house https://agents.house
1590
+ kit telegram serve
1560
1591
  ```
1561
1592
 
1562
1593
  The command holds one Environment control lease for the whole run and performs
@@ -1602,7 +1633,7 @@ command is resident and stops on SIGINT or SIGTERM, printing what it settled.
1602
1633
  Release the bot:
1603
1634
 
1604
1635
  ```sh
1605
- kit telegram disconnect --house https://agents.house
1636
+ kit telegram disconnect
1606
1637
  ```
1607
1638
 
1608
1639
  The command writes a local disconnect marker first, deletes the webhook, removes
@@ -1648,8 +1679,8 @@ Disabling a service or an instance leaves its state in place, so re-enabling
1648
1679
  resumes from the stored cursor instead of importing again.
1649
1680
 
1650
1681
  ```sh
1651
- kit source reset acme --house https://agents.house # every service
1652
- kit source reset acme notes --house https://agents.house # one service
1682
+ kit source reset acme # every service
1683
+ kit source reset acme notes # one service
1653
1684
  ```
1654
1685
 
1655
1686
  Reset is the only deletion of captured state. It removes the named checkpoints
@@ -1686,7 +1717,7 @@ refused rather than read as no limit. A ceiling is a shared ceiling, never a
1686
1717
  reservation, and House infers no supported Agent count from any of these values.
1687
1718
 
1688
1719
  ```sh
1689
- kit host report --house https://agents.house
1720
+ kit host report
1690
1721
  ```
1691
1722
 
1692
1723
  The command reads `/proc/stat` twice a second apart, reads `/proc/meminfo`, and
@@ -1708,7 +1739,7 @@ Description=Report measured host capacity to House
1708
1739
  [Service]
1709
1740
  Type=oneshot
1710
1741
  User=house-kit
1711
- ExecStart=/usr/bin/kit host report --house https://agents.house
1742
+ ExecStart=/usr/bin/kit host report
1712
1743
  ```
1713
1744
 
1714
1745
  ```
@@ -1757,11 +1788,10 @@ manifest, URLs, or attachments.
1757
1788
 
1758
1789
  ```sh
1759
1790
  kit migrate emit my-import --package /home/ada/exports/project
1760
- kit migrate mount /home/ada/exports/project --house https://agents.house \
1761
- --subtree /rooms/work/library/project
1791
+ kit migrate mount /home/ada/exports/project --subtree /rooms/work/library/project
1762
1792
  # Or emit and mount in one maintained mount session:
1763
1793
  kit migrate run my-import --package /home/ada/exports/another-project \
1764
- --house https://agents.house --subtree /private/library/another-project
1794
+ --subtree /private/library/another-project
1765
1795
  ```
1766
1796
 
1767
1797
  Emission refuses an existing output directory. The module reads one JSON line
@@ -1848,8 +1878,8 @@ leaves the pending mounted report plus the local outcomes. Re-migration is
1848
1878
  explicit unmount followed by mount.
1849
1879
 
1850
1880
  ```sh
1851
- kit migrate unmount /rooms/work/library/project --house https://agents.house
1852
- kit migrate unmount /rooms/work/library/project --house https://agents.house \
1881
+ kit migrate unmount /rooms/work/library/project
1882
+ kit migrate unmount /rooms/work/library/project \
1853
1883
  --confirm /rooms/work/library/project
1854
1884
  ```
1855
1885
 
@@ -39,7 +39,7 @@ login_kit() {
39
39
  }
40
40
 
41
41
  start_resident() {
42
- docker run -d --name "$NAME" --restart unless-stopped --user "$(id -u):$(id -g)" --mount "$KIT_HOME_MOUNT" --mount "$WORKSPACE_MOUNT" --label "agentshouse.house=$HOUSE" --env HOME=/kit-home --env HOUSE_KIT_HOME=/kit-home --env HOUSE_KIT_CONTAINER=1 "$IMAGE" resident --house "$HOUSE" >/dev/null
42
+ docker run -d --name "$NAME" --restart unless-stopped --user "$(id -u):$(id -g)" --mount "$KIT_HOME_MOUNT" --mount "$WORKSPACE_MOUNT" --label "agentshouse.house=$HOUSE" --env HOME=/kit-home --env HOUSE_KIT_HOME=/kit-home --env HOUSE_KIT_CONTAINER=1 "$IMAGE" resident >/dev/null
43
43
  }
44
44
 
45
45
  if [[ "${1:-}" == kit || "${1:-}" == house ]]; then
@@ -168,7 +168,7 @@ if docker container inspect "$NAME" >/dev/null 2>&1; then
168
168
  printf 'House Kit updated for Environment %s.\n' "$(cat "$KIT_HOME/environment")"
169
169
  exit 0
170
170
  fi
171
- "${DOCKER_RUN[@]}" credential check --house "$HOUSE"
171
+ "${DOCKER_RUN[@]}" credential check
172
172
  if [[ "$(docker inspect --format '{{.State.Running}}' "$NAME")" == true ]]; then
173
173
  printf 'House Kit is already running for Environment %s.\n' "$(cat "$KIT_HOME/environment")"
174
174
  else
@@ -185,15 +185,14 @@ if [[ ! -f "$KIT_HOME/kit.json" ]]; then
185
185
  fi
186
186
 
187
187
  if [[ -f "$KIT_HOME/credential" ]]; then
188
- "${DOCKER_RUN[@]}" credential check --house "$HOUSE"
188
+ "${DOCKER_RUN[@]}" credential check
189
189
  else
190
- if ((MANUAL)); then
191
- login_kit --house "$HOUSE" --manual
192
- else
193
- login_kit --house "$HOUSE"
194
- fi
190
+ LOGIN=()
191
+ if ((HOUSE_SELECTED)); then LOGIN+=(--house "$HOUSE"); fi
192
+ if ((MANUAL)); then LOGIN+=(--manual); fi
193
+ login_kit "${LOGIN[@]}"
195
194
  fi
196
195
 
197
- "${DOCKER_RUN[@]}" apply --house "$HOUSE"
196
+ "${DOCKER_RUN[@]}" apply
198
197
  start_resident
199
198
  printf 'House Kit connected for Environment %s.\n' "$(cat "$KIT_HOME/environment")"
package/bin/kit.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { join, resolve } from "node:path";
5
- import { connectHouse, storedEnvironment, storedHouse } from "../src/login.js";
5
+ import { connectHouse, loginHouse, storedEnvironment, storedHouse } from "../src/login.js";
6
6
  import { apply } from "../src/apply.js";
7
7
  import { captureSource, resetSource, retrySourceOriginals, sourceStatus } from "../src/source.js";
8
8
  import { setupEnvironment } from "../src/setup.js";
@@ -32,44 +32,44 @@ const USAGE =
32
32
  "usage: kit <login|resident|credential|setup|apply|agents|source|repository|worker|sync|migrate|attachment|host|workspace|telegram> ...";
33
33
  const LOGIN_USAGE =
34
34
  "usage: kit login [--house <url>] [--environment <uuid>] [--manual|--host-browser]";
35
- const HOST_USAGE = "usage: kit host report [--house <url>] [--config <path>]";
35
+ const HOST_USAGE = "usage: kit host report [--config <path>]";
36
36
  const AGENTS_USAGE =
37
- "usage: kit agents update [--house <url>] [--config <path>] | kit agents sign-in <kind> [--house <url>] [--config <path>]";
38
- const WORKSPACE_USAGE = "usage: kit workspace host [--house <url>] [--config <path>] [--passes <count>]";
37
+ "usage: kit agents update [--config <path>] | kit agents sign-in <kind> [--config <path>]";
38
+ const WORKSPACE_USAGE = "usage: kit workspace host [--config <path>] [--passes <count>]";
39
39
  const SOURCE_USAGE =
40
- "usage: kit source <capture|reset|retry-files> <instance> [service] [--house <url>] [--config <path>]\n" +
40
+ "usage: kit source <capture|reset|retry-files> <instance> [service] [--config <path>]\n" +
41
41
  " kit source status <instance> [service] [--config <path>]\n" +
42
42
  " kit source sign-in <instance> [--config <path>]";
43
- const SETUP_USAGE = "usage: kit setup [--house <url>] [--config <path>]";
43
+ const SETUP_USAGE = "usage: kit setup [--config <path>]";
44
44
  const REPOSITORY_USAGE =
45
- "usage: kit repository capture <checkout> --room <room> --source-handle <handle> [--house <url>]";
45
+ "usage: kit repository capture <checkout> --room <room> --source-handle <handle>";
46
46
  const WORKER_USAGE =
47
- "usage: kit worker run [--house <url>] [--config <path>] | kit worker attach [<session>]";
47
+ "usage: kit worker run [--config <path>] | kit worker attach [<session>]";
48
48
  const SYNC_USAGE =
49
49
  "usage: kit sync <bootstrap|pull|rebootstrap|submit> ...";
50
50
  const SYNC_BOOTSTRAP_USAGE =
51
- "usage: kit sync bootstrap <directory> [--house <url>] --prefix <path> [--prefix <path> ...]";
52
- const SYNC_PULL_USAGE = "usage: kit sync pull <directory> [--house <url>]";
53
- const SYNC_REBOOTSTRAP_USAGE = "usage: kit sync rebootstrap <directory> [--house <url>]";
51
+ "usage: kit sync bootstrap <directory> --prefix <path> [--prefix <path> ...]";
52
+ const SYNC_PULL_USAGE = "usage: kit sync pull <directory>";
53
+ const SYNC_REBOOTSTRAP_USAGE = "usage: kit sync rebootstrap <directory>";
54
54
  const SYNC_SUBMIT_USAGE =
55
- "usage: kit sync submit <directory> [--house <url>] [--changes <path>]";
55
+ "usage: kit sync submit <directory> [--changes <path>]";
56
56
  const MIGRATE_USAGE = "usage: kit migrate <emit|mount|unmount|run> ...";
57
57
  const MIGRATE_EMIT_USAGE =
58
58
  "usage: kit migrate emit <migrator> --package <directory> [--config <path>]";
59
59
  const MIGRATE_MOUNT_USAGE =
60
- "usage: kit migrate mount <package> [--house <url>] --subtree <path>";
60
+ "usage: kit migrate mount <package> --subtree <path>";
61
61
  const MIGRATE_UNMOUNT_USAGE =
62
- "usage: kit migrate unmount <subtree> [--house <url>] [--confirm <subtree>]";
62
+ "usage: kit migrate unmount <subtree> [--confirm <subtree>]";
63
63
  const MIGRATE_RUN_USAGE =
64
- "usage: kit migrate run <migrator> [--house <url>] --subtree <path> --package <directory> [--config <path>]";
64
+ "usage: kit migrate run <migrator> --subtree <path> --package <directory> [--config <path>]";
65
65
  const ATTACHMENT_USAGE = "usage: kit attachment <upload|get> ...";
66
66
  const ATTACHMENT_UPLOAD_USAGE =
67
- "usage: kit attachment upload <file> [--house <url>] (--room <ref> | --private) [--name <name>] [--media-type <type>]";
67
+ "usage: kit attachment upload <file> (--room <ref> | --private) [--name <name>] [--media-type <type>]";
68
68
  const ATTACHMENT_GET_USAGE =
69
- "usage: kit attachment get [--house <url>] --attachment <at_ref> (--room <ref> | --private) [--output <path>]";
69
+ "usage: kit attachment get --attachment <at_ref> (--room <ref> | --private) [--output <path>]";
70
70
  const TELEGRAM_USAGE =
71
- "usage: kit telegram <connect|disconnect> [--house <url>]\n" +
72
- " kit telegram serve [--house <url>] [--passes <count>]";
71
+ "usage: kit telegram <connect|disconnect>\n" +
72
+ " kit telegram serve [--passes <count>]";
73
73
 
74
74
  function declared(argv, accepted, positionals, options = {}) {
75
75
  const repeated = options.repeated ?? [];
@@ -115,14 +115,10 @@ function builtGoogleOAuthClient() {
115
115
  return GOOGLE_OAUTH_CLIENT;
116
116
  }
117
117
 
118
- async function selectedHouse(values, directory) {
119
- return storedHouse(directory, false, values["--house"]);
120
- }
121
-
122
118
  async function runLogin(args, directory) {
123
119
  const { values } = declared(args, ["--house", "--manual", "--host-browser", "--environment"], 0, { flags: ["--host-browser"] });
124
120
  if (values["--manual"] && values["--host-browser"]) throw new Error(LOGIN_USAGE);
125
- const house = await storedHouse(directory, true, values["--house"]);
121
+ const house = await loginHouse(directory, values["--house"]);
126
122
  const presentation = values["--manual"] === true ? "manual" : "loopback";
127
123
  await connectHouse({
128
124
  house,
@@ -135,9 +131,9 @@ async function runLogin(args, directory) {
135
131
  }
136
132
 
137
133
  async function runCredentialCheck(args, directory) {
138
- const { values } = declared(args, ["--house"], 0, { exact: true });
134
+ declared(args, [], 0, { exact: true });
139
135
  if (await storedEnvironment(directory) === null) throw new KitRefusal("kit_environment_invalid", "no enrolled Environment id");
140
- const house = await selectedHouse(values, directory);
136
+ const house = await storedHouse(directory);
141
137
  let session;
142
138
  try {
143
139
  session = await openSession({ house, directory, acquire: true });
@@ -153,8 +149,8 @@ async function runCredentialCheck(args, directory) {
153
149
  }
154
150
 
155
151
  async function runResidentCommand(args, directory) {
156
- const { values } = declared(args, ["--house", "--config"], 0, { exact: true });
157
- const house = await selectedHouse(values, directory);
152
+ const { values } = declared(args, ["--config"], 0, { exact: true });
153
+ const house = await storedHouse(directory);
158
154
  const cancellation = new AbortController();
159
155
  const stop = () => cancellation.abort();
160
156
  process.once("SIGINT", stop);
@@ -185,9 +181,8 @@ async function runResidentDrain(args, directory) {
185
181
  }
186
182
 
187
183
  async function runSetup(args, directory) {
188
- const { values } = declared(args, ["--house", "--config"], 0, { exact: true });
189
- const house = await selectedHouse(values, directory);
190
- if (!house) throw new Error(SETUP_USAGE);
184
+ const { values } = declared(args, ["--config"], 0, { exact: true });
185
+ const house = await storedHouse(directory);
191
186
  const cancellation = new AbortController();
192
187
  const cancel = () => cancellation.abort();
193
188
  process.once("SIGINT", cancel);
@@ -240,9 +235,8 @@ async function runSourceSignIn(args, directory) {
240
235
  }
241
236
 
242
237
  async function runApply(args, directory) {
243
- const { values } = declared(args, ["--house", "--config"], 0);
244
- const house = await selectedHouse(values, directory);
245
- if (!house) throw new Error("usage: kit apply --house <url> [--config <path>]");
238
+ const { values } = declared(args, ["--config"], 0);
239
+ const house = await storedHouse(directory);
246
240
  const cancellation = new AbortController();
247
241
  const cancel = () => cancellation.abort();
248
242
  process.once("SIGINT", cancel);
@@ -269,9 +263,9 @@ async function runApply(args, directory) {
269
263
  }
270
264
 
271
265
  async function runSourceCapture(args, directory) {
272
- const { values, rest } = declared(args, ["--house", "--config"], 2);
273
- const house = await selectedHouse(values, directory);
274
- if (!house || rest.length === 0) throw new Error(SOURCE_USAGE);
266
+ const { values, rest } = declared(args, ["--config"], 2);
267
+ if (rest.length === 0) throw new Error(SOURCE_USAGE);
268
+ const house = await storedHouse(directory);
275
269
  const cancellation = new AbortController();
276
270
  process.once("SIGINT", () => cancellation.abort());
277
271
  const result = await captureSource(
@@ -314,9 +308,9 @@ function writeSourceOriginals(instance, service) {
314
308
  }
315
309
 
316
310
  async function runSourceRetryFiles(args, directory) {
317
- const { values, rest } = declared(args, ["--house", "--config"], 2);
318
- const house = await selectedHouse(values, directory);
319
- if (!house || rest.length === 0) throw new Error(SOURCE_USAGE);
311
+ const { values, rest } = declared(args, ["--config"], 2);
312
+ if (rest.length === 0) throw new Error(SOURCE_USAGE);
313
+ const house = await storedHouse(directory);
320
314
  const cancellation = new AbortController();
321
315
  process.once("SIGINT", () => cancellation.abort());
322
316
  const result = await retrySourceOriginals({
@@ -359,9 +353,9 @@ async function runSourceStatus(args, directory) {
359
353
  }
360
354
 
361
355
  async function runSourceReset(args, directory) {
362
- const { values, rest } = declared(args, ["--house", "--config"], 2);
363
- const house = await selectedHouse(values, directory);
364
- if (!house || rest.length === 0) throw new Error(SOURCE_USAGE);
356
+ const { values, rest } = declared(args, ["--config"], 2);
357
+ if (rest.length === 0) throw new Error(SOURCE_USAGE);
358
+ const house = await storedHouse(directory);
365
359
  const result = await resetSource({
366
360
  house,
367
361
  directory,
@@ -377,11 +371,11 @@ async function runSourceReset(args, directory) {
377
371
  }
378
372
 
379
373
  async function runRepositoryCapture(args, directory) {
380
- const { values, rest } = declared(args, ["--house", "--room", "--source-handle"], 1);
381
- const house = await selectedHouse(values, directory);
374
+ const { values, rest } = declared(args, ["--room", "--source-handle"], 1);
382
375
  const room = values["--room"];
383
376
  const sourceHandle = values["--source-handle"];
384
- if (!house || !room || !sourceHandle || rest.length !== 1) throw new Error(REPOSITORY_USAGE);
377
+ if (!room || !sourceHandle || rest.length !== 1) throw new Error(REPOSITORY_USAGE);
378
+ const house = await storedHouse(directory);
385
379
  const cancellation = new AbortController();
386
380
  const cancel = () => cancellation.abort();
387
381
  process.once("SIGINT", cancel);
@@ -429,9 +423,8 @@ function writeConversationFiles(supervised) {
429
423
  }
430
424
 
431
425
  async function runWorkerRun(args, directory) {
432
- const { values } = declared(args, ["--house", "--config"], 0);
433
- const house = await selectedHouse(values, directory);
434
- if (!house) throw new Error(WORKER_USAGE);
426
+ const { values } = declared(args, ["--config"], 0);
427
+ const house = await storedHouse(directory);
435
428
  const supervised = await runWorkers(
436
429
  {
437
430
  house,
@@ -500,12 +493,12 @@ function syncCancellation() {
500
493
  }
501
494
 
502
495
  async function runSyncBootstrap(args, directory) {
503
- const { values, lists, rest } = declared(args, ["--house", "--prefix"], 1, {
496
+ const { lists, rest } = declared(args, ["--prefix"], 1, {
504
497
  repeated: ["--prefix"],
505
498
  exact: true,
506
499
  });
507
- const house = await selectedHouse(values, directory);
508
- if (!house || lists["--prefix"].length === 0) throw new Error(SYNC_BOOTSTRAP_USAGE);
500
+ if (lists["--prefix"].length === 0) throw new Error(SYNC_BOOTSTRAP_USAGE);
501
+ const house = await storedHouse(directory);
509
502
  const run = syncCancellation();
510
503
  try {
511
504
  const result = await bootstrap({
@@ -524,9 +517,8 @@ async function runSyncBootstrap(args, directory) {
524
517
  }
525
518
 
526
519
  async function runSyncPull(args, directory) {
527
- const { values, rest } = declared(args, ["--house"], 1, { exact: true });
528
- const house = await selectedHouse(values, directory);
529
- if (!house) throw new Error(SYNC_PULL_USAGE);
520
+ const { rest } = declared(args, [], 1, { exact: true });
521
+ const house = await storedHouse(directory);
530
522
  const run = syncCancellation();
531
523
  try {
532
524
  const result = await pull({
@@ -544,9 +536,8 @@ async function runSyncPull(args, directory) {
544
536
  }
545
537
 
546
538
  async function runSyncRebootstrap(args, directory) {
547
- const { values, rest } = declared(args, ["--house"], 1, { exact: true });
548
- const house = await selectedHouse(values, directory);
549
- if (!house) throw new Error(SYNC_REBOOTSTRAP_USAGE);
539
+ const { rest } = declared(args, [], 1, { exact: true });
540
+ const house = await storedHouse(directory);
550
541
  const run = syncCancellation();
551
542
  try {
552
543
  const result = await rebootstrap({
@@ -571,9 +562,8 @@ async function readChanges(path) {
571
562
  }
572
563
 
573
564
  async function runSyncSubmit(args, directory) {
574
- const { values, rest } = declared(args, ["--house", "--changes"], 1, { exact: true });
575
- const house = await selectedHouse(values, directory);
576
- if (!house) throw new Error(SYNC_SUBMIT_USAGE);
565
+ const { values, rest } = declared(args, ["--changes"], 1, { exact: true });
566
+ const house = await storedHouse(directory);
577
567
  const run = syncCancellation();
578
568
  try {
579
569
  const result = await submit({
@@ -646,11 +636,11 @@ async function mountEmitted(packagePath, options, run) {
646
636
  }
647
637
 
648
638
  async function runMigrateMount(args, directory) {
649
- const { values, rest } = declared(args, ["--house", "--subtree"], 1, { exact: true });
650
- const house = await selectedHouse(values, directory);
651
- if (!house || rest.length !== 1 || values["--subtree"] === undefined) {
639
+ const { values, rest } = declared(args, ["--subtree"], 1, { exact: true });
640
+ if (rest.length !== 1 || values["--subtree"] === undefined) {
652
641
  throw new Error(MIGRATE_MOUNT_USAGE);
653
642
  }
643
+ const house = await storedHouse(directory);
654
644
  const run = migrateCancellation();
655
645
  try {
656
646
  await mountEmitted(rest[0], { house, directory, subtree: values["--subtree"] }, run);
@@ -660,18 +650,17 @@ async function runMigrateMount(args, directory) {
660
650
  }
661
651
 
662
652
  async function runMigrateRun(args, directory) {
663
- const { values, rest } = declared(args, ["--house", "--subtree", "--package", "--config"], 1, {
653
+ const { values, rest } = declared(args, ["--subtree", "--package", "--config"], 1, {
664
654
  exact: true,
665
655
  });
666
- const house = await selectedHouse(values, directory);
667
656
  if (
668
- !house ||
669
657
  rest.length !== 1 ||
670
658
  values["--subtree"] === undefined ||
671
659
  values["--package"] === undefined
672
660
  ) {
673
661
  throw new Error(MIGRATE_RUN_USAGE);
674
662
  }
663
+ const house = await storedHouse(directory);
675
664
  const run = migrateCancellation();
676
665
  try {
677
666
  const emitted = await emitPackage(
@@ -688,9 +677,9 @@ async function runMigrateRun(args, directory) {
688
677
  }
689
678
 
690
679
  async function runMigrateUnmount(args, directory) {
691
- const { values, rest } = declared(args, ["--house", "--confirm"], 1, { exact: true });
692
- const house = await selectedHouse(values, directory);
693
- if (!house || rest.length !== 1) throw new Error(MIGRATE_UNMOUNT_USAGE);
680
+ const { values, rest } = declared(args, ["--confirm"], 1, { exact: true });
681
+ if (rest.length !== 1) throw new Error(MIGRATE_UNMOUNT_USAGE);
682
+ const house = await storedHouse(directory);
694
683
  const run = migrateCancellation();
695
684
  try {
696
685
  const result = await unmountSubtree({
@@ -746,12 +735,12 @@ function attachmentCancellation() {
746
735
  async function runAttachmentUpload(args, directory) {
747
736
  const { values, rest } = declared(
748
737
  args,
749
- ["--house", "--room", "--private", "--name", "--media-type"],
738
+ ["--room", "--private", "--name", "--media-type"],
750
739
  1,
751
740
  { flags: ["--private"] },
752
741
  );
753
- const house = await selectedHouse(values, directory);
754
- if (!house || rest.length !== 1) throw new Error(ATTACHMENT_UPLOAD_USAGE);
742
+ if (rest.length !== 1) throw new Error(ATTACHMENT_UPLOAD_USAGE);
743
+ const house = await storedHouse(directory);
755
744
  const run = attachmentCancellation();
756
745
  try {
757
746
  const result = await uploadAttachment({
@@ -773,13 +762,13 @@ async function runAttachmentUpload(args, directory) {
773
762
  async function runAttachmentGet(args, directory) {
774
763
  const { values } = declared(
775
764
  args,
776
- ["--house", "--attachment", "--room", "--private", "--output"],
765
+ ["--attachment", "--room", "--private", "--output"],
777
766
  0,
778
767
  { flags: ["--private"] },
779
768
  );
780
- const house = await selectedHouse(values, directory);
781
769
  const attachment = values["--attachment"];
782
- if (!house || !attachment) throw new Error(ATTACHMENT_GET_USAGE);
770
+ if (!attachment) throw new Error(ATTACHMENT_GET_USAGE);
771
+ const house = await storedHouse(directory);
783
772
  const run = attachmentCancellation();
784
773
  try {
785
774
  const result = await getAttachment({
@@ -805,9 +794,8 @@ async function runAttachment(args, directory) {
805
794
  }
806
795
 
807
796
  async function runHostReport(args, directory) {
808
- const { values } = declared(args, ["--house", "--config"], 0, { exact: true });
809
- const house = await selectedHouse(values, directory);
810
- if (!house) throw new Error(HOST_USAGE);
797
+ const { values } = declared(args, ["--config"], 0, { exact: true });
798
+ const house = await storedHouse(directory);
811
799
  const observed = await reportHost({
812
800
  house,
813
801
  directory,
@@ -819,9 +807,8 @@ async function runHostReport(args, directory) {
819
807
  }
820
808
 
821
809
  async function runAgentsUpdate(args, directory) {
822
- const { values } = declared(args, ["--house", "--config"], 0, { exact: true });
823
- const house = await selectedHouse(values, directory);
824
- if (!house) throw new Error(AGENTS_USAGE);
810
+ const { values } = declared(args, ["--config"], 0, { exact: true });
811
+ const house = await storedHouse(directory);
825
812
  const { updated } = await updateAgents({
826
813
  house,
827
814
  directory,
@@ -847,10 +834,10 @@ async function runAgentsUpdate(args, directory) {
847
834
  }
848
835
 
849
836
  async function runAgentsSignIn(args, directory) {
850
- const { values, rest } = declared(args, ["--house", "--config"], 1, { exact: true });
851
- const house = await selectedHouse(values, directory);
837
+ const { values, rest } = declared(args, ["--config"], 1, { exact: true });
852
838
  const [kind] = rest;
853
- if (!house || !kind) throw new Error(AGENTS_USAGE);
839
+ if (!kind) throw new Error(AGENTS_USAGE);
840
+ const house = await storedHouse(directory);
854
841
  const { status, reported } = await signInAgent({
855
842
  house,
856
843
  directory,
@@ -867,13 +854,12 @@ async function runAgentsSignIn(args, directory) {
867
854
  }
868
855
 
869
856
  async function runWorkspaceHost(args, directory) {
870
- const { values } = declared(args, ["--house", "--config", "--passes"], 0, { exact: true });
871
- const house = await selectedHouse(values, directory);
872
- if (!house) throw new Error(WORKSPACE_USAGE);
857
+ const { values } = declared(args, ["--config", "--passes"], 0, { exact: true });
873
858
  const passes = values["--passes"] === undefined ? undefined : Number(values["--passes"]);
874
859
  if (passes !== undefined && (!Number.isSafeInteger(passes) || passes < 1)) {
875
860
  throw new Error(WORKSPACE_USAGE);
876
861
  }
862
+ const house = await storedHouse(directory);
877
863
  const served = await serveWorkspaces({
878
864
  house,
879
865
  directory,
@@ -894,9 +880,8 @@ async function runWorkspaceHost(args, directory) {
894
880
  }
895
881
 
896
882
  async function runTelegramConnect(args, directory) {
897
- const { values } = declared(args, ["--house"], 0, { exact: true });
898
- const house = await selectedHouse(values, directory);
899
- if (!house) throw new Error(TELEGRAM_USAGE);
883
+ declared(args, [], 0, { exact: true });
884
+ const house = await storedHouse(directory);
900
885
  const cancellation = new AbortController();
901
886
  const cancel = () => cancellation.abort();
902
887
  process.once("SIGINT", cancel);
@@ -914,9 +899,8 @@ async function runTelegramConnect(args, directory) {
914
899
  }
915
900
 
916
901
  async function runTelegramDisconnect(args, directory) {
917
- const { values } = declared(args, ["--house"], 0, { exact: true });
918
- const house = await selectedHouse(values, directory);
919
- if (!house) throw new Error(TELEGRAM_USAGE);
902
+ declared(args, [], 0, { exact: true });
903
+ const house = await storedHouse(directory);
920
904
  const cancellation = new AbortController();
921
905
  const cancel = () => cancellation.abort();
922
906
  process.once("SIGINT", cancel);
@@ -936,13 +920,12 @@ async function runTelegramDisconnect(args, directory) {
936
920
  }
937
921
 
938
922
  async function runTelegramServe(args, directory) {
939
- const { values } = declared(args, ["--house", "--passes"], 0, { exact: true });
940
- const house = await selectedHouse(values, directory);
941
- if (!house) throw new Error(TELEGRAM_USAGE);
923
+ const { values } = declared(args, ["--passes"], 0, { exact: true });
942
924
  const passes = values["--passes"] === undefined ? undefined : Number(values["--passes"]);
943
925
  if (passes !== undefined && (!Number.isSafeInteger(passes) || passes < 1)) {
944
926
  throw new Error(TELEGRAM_USAGE);
945
927
  }
928
+ const house = await storedHouse(directory);
946
929
  const cancellation = new AbortController();
947
930
  const cancel = () => cancellation.abort();
948
931
  process.once("SIGINT", cancel);
package/bootstrap.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.1.0-alpha.20",
3
+ "version": "0.1.0-alpha.22",
4
4
  "hosts": {
5
5
  "linux": {
6
- "command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.20/connect-linux.sh)",
6
+ "command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.22/connect-linux.sh)",
7
7
  "asset": {
8
8
  "name": "connect-linux.sh",
9
- "url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.20/connect-linux.sh"
9
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.22/connect-linux.sh"
10
10
  },
11
11
  "workspace": {
12
12
  "argument": "--workspace",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentshouse/kit",
3
- "version": "0.1.0-alpha.20",
3
+ "version": "0.1.0-alpha.22",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
package/src/contract.js CHANGED
@@ -297,7 +297,7 @@ export const EXPLICIT_SYNC_VERSION = "explicit-sync/2";
297
297
  export const KIT_DOOR_UNMOUNT_PATH = "/kit/door/unmount";
298
298
 
299
299
  export const KIT_WORKING_COPY_SELECTION_PATH = "/kit/working-copy/selection";
300
- export const WORKING_COPY_VERSION = "working-copy-state/2";
300
+ export const WORKING_COPY_VERSION = "working-copy-state/3";
301
301
  export const WORKING_COPIES_HOME = "working-copies";
302
302
  export const WORKING_COPY_ROOM_HOMES = ["capture", "library", "collaboration"];
303
303
  export const WORKING_COPY_PRIVATE_HOMES = ["library", "collaboration", "rooms", "agents", "house"];
package/src/login.js CHANGED
@@ -14,6 +14,7 @@ import { writeText } from "./state.js";
14
14
 
15
15
  const ENVIRONMENT =
16
16
  /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
17
+ const DEFAULT_HOUSE = "https://agents.house";
17
18
 
18
19
  function verifier() {
19
20
  return randomBytes(32).toString("base64url");
@@ -112,19 +113,29 @@ function houseOrigin(value) {
112
113
  return value;
113
114
  }
114
115
 
115
- export async function storedHouse(directory, firstLogin = false, requested = undefined) {
116
- let value;
116
+ async function recordedHouse(directory) {
117
117
  try {
118
- value = (await readFile(join(directory, "house"), "utf8")).trim();
118
+ return (await readFile(join(directory, "house"), "utf8")).trim();
119
119
  } catch (error) {
120
- if (error.code === "ENOENT") {
121
- if ((firstLogin || requested !== undefined) &&
122
- await storedEnvironment(directory) === null && !await storedCredentialPresent(directory)) {
123
- return houseOrigin(requested ?? "https://agents.house");
124
- }
125
- }
120
+ if (error.code === "ENOENT") return null;
126
121
  throw new KitRefusal("kit_house_invalid", error.code ?? "unreadable");
127
122
  }
123
+ }
124
+
125
+ export async function storedHouse(directory) {
126
+ const value = await recordedHouse(directory);
127
+ if (value === null) throw new KitRefusal("kit_house_invalid", "ENOENT");
128
+ return houseOrigin(value);
129
+ }
130
+
131
+ export async function loginHouse(directory, requested) {
132
+ const value = await recordedHouse(directory);
133
+ if (value === null) {
134
+ if (await storedEnvironment(directory) !== null || await storedCredentialPresent(directory)) {
135
+ throw new KitRefusal("kit_house_invalid", "ENOENT");
136
+ }
137
+ return houseOrigin(requested ?? DEFAULT_HOUSE);
138
+ }
128
139
  houseOrigin(value);
129
140
  if (requested !== undefined && requested !== value) {
130
141
  throw new KitRefusal("kit_house_mismatch", "this Environment is bound to another House origin");
@@ -133,7 +144,7 @@ export async function storedHouse(directory, firstLogin = false, requested = und
133
144
  }
134
145
 
135
146
  export function enrollmentAct(options, retained, host) {
136
- if (options.label !== undefined || (options.environment !== undefined && !ENVIRONMENT.test(options.environment))) {
147
+ if (options.environment !== undefined && !ENVIRONMENT.test(options.environment)) {
137
148
  throw new KitRefusal("kit_login_rejected", ENROLLMENT_USAGE);
138
149
  }
139
150
  const environment = options.environment ?? retained;
package/src/worker.js CHANGED
@@ -19,7 +19,14 @@ import { localFiles } from "./local-files.js";
19
19
  import { answerText, answerYaml } from "./mcp.js";
20
20
  import { inheritedEnvironment } from "./command.js";
21
21
  import { watchKitWork } from "./kit-work-stream.js";
22
- import { applyWorkingCopies, ownerCaller, pushWorkingCopy, recordIndexChange, workingCopyPaths } from "./working-copy.js";
22
+ import {
23
+ applyWorkingCopies,
24
+ deliverWorkingCopies,
25
+ ownerCaller,
26
+ pushWorkingCopy,
27
+ recordIndexChange,
28
+ workingCopyPaths,
29
+ } from "./working-copy.js";
23
30
  import { readConfiguration } from "./configuration.js";
24
31
  import { launchBase, readyLaunchDirectory } from "./directory.js";
25
32
  import {
@@ -863,7 +870,7 @@ export async function runWorkers(options, dependencies = {}) {
863
870
  const { context, tmux } = await workerContext(options, dependencies);
864
871
  const { session, runtime } = context;
865
872
  const supervised = await maintainControl(session, async (signal) => {
866
- await applySavedWorkingCopies(context, signal);
873
+ await reportedWorkingCopies(applyWorkingCopies, context, signal);
867
874
  return controlledPass(context, dependencies, context.configuration, signal);
868
875
  }, session.signal);
869
876
  return {
@@ -884,9 +891,8 @@ function reported(line) {
884
891
  process.stderr.write(`kit: ${line}\n`);
885
892
  }
886
893
 
887
- async function applySavedWorkingCopies(context, signal) {
888
- const applied = await applyWorkingCopies(context, signal);
889
- for (const refused of applied.refused) {
894
+ async function reportedWorkingCopies(act, context, signal) {
895
+ for (const refused of (await act(context, signal)).refused) {
890
896
  reported(`working copy ${refused.path} refused: ${refused.code}`);
891
897
  }
892
898
  }
@@ -1078,10 +1084,12 @@ export async function runResident(options, dependencies = {}) {
1078
1084
  const needsLogin = await queueAgentRefresh(context, dependencies, signal, undefined, true);
1079
1085
  context.startLogins(needsLogin);
1080
1086
  });
1081
- const copies = coalescedTask(
1082
- signal,
1083
- (error) => reported(`working copies unavailable: ${error?.message ?? error}`),
1084
- () => applySavedWorkingCopies(context, signal),
1087
+ const unavailable = (error) => reported(`working copies unavailable: ${error?.message ?? error}`);
1088
+ const copies = coalescedTask(signal, unavailable, () =>
1089
+ reportedWorkingCopies(applyWorkingCopies, context, signal),
1090
+ );
1091
+ const deliveries = coalescedTask(signal, unavailable, () =>
1092
+ reportedWorkingCopies(deliverWorkingCopies, context, signal),
1085
1093
  );
1086
1094
  const pointer = () => {
1087
1095
  if (!update.requested) agentRefresh.trigger();
@@ -1114,18 +1122,20 @@ export async function runResident(options, dependencies = {}) {
1114
1122
  if (sessions.length > 0) update.answer({ ready: false, sessions });
1115
1123
  else if (
1116
1124
  session.refused ||
1117
- (!served.serving() && logins.size === 0 && agentRefresh.idle() && copies.idle())
1125
+ (!served.serving() && logins.size === 0 && agentRefresh.idle() && copies.idle() && deliveries.idle())
1118
1126
  ) {
1119
1127
  return;
1120
1128
  }
1121
1129
  }
1122
1130
  await wake.wait(interval, signal);
1131
+ if (!update.requested && !session.refused) deliveries.trigger();
1123
1132
  }
1124
1133
  } finally {
1125
1134
  watcher.abort();
1126
1135
  await watching;
1127
1136
  await agentRefresh.settled();
1128
1137
  await copies.settled();
1138
+ await deliveries.settled();
1129
1139
  for (const running of logins.values()) running.controller.abort();
1130
1140
  await Promise.all([...logins.values()].map((running) => running.task));
1131
1141
  await context.agentRefresh;
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { mkdir, readdir, realpath, rename, rm, stat } from "node:fs/promises";
2
+ import { mkdir, readdir, readFile, realpath, rename, rm, stat } from "node:fs/promises";
3
3
  import { dirname, join } from "node:path";
4
4
  import { readBindings } from "./binding.js";
5
5
  import {
@@ -21,7 +21,7 @@ import { operationId } from "./operation.js";
21
21
  import { formatCommandError, houseAnswered, houseFailed, KitRefusal } from "./refusal.js";
22
22
  import { readNote, writeNote, writeText } from "./state.js";
23
23
  import { bootstrapBundle, discoveryOf, door, withSession } from "./sync.js";
24
- import { sidecarPath, structuredOrReplace } from "./sync-library.js";
24
+ import { isGeneratedProvenance, sidecarPath, structuredOrReplace } from "./sync-library.js";
25
25
  import { locate, removeReplicaFile, writeReplicaFile } from "./sync-state.js";
26
26
 
27
27
  const COMMITTER_NAME = "House Kit";
@@ -134,6 +134,7 @@ async function readScope(session, scope, signal) {
134
134
  if (
135
135
  authority === undefined ||
136
136
  !Array.isArray(authority.protected_paths) ||
137
+ !Array.isArray(authority.operations?.write) ||
137
138
  (scope.kind === "room" && view.manifest.anchor !== scope.ref)
138
139
  ) {
139
140
  throw rejected("discover");
@@ -142,11 +143,16 @@ async function readScope(session, scope, signal) {
142
143
  anchor: view.manifest.anchor,
143
144
  position: view.manifest.position,
144
145
  logEpoch: view.manifest.log_epoch,
145
- protectedPaths: authority.protected_paths.filter((path) => typeof path === "string"),
146
+ protectedPaths: authority.protected_paths,
147
+ declared: declaration(authority),
146
148
  files,
147
149
  };
148
150
  }
149
151
 
152
+ function declaration(authority) {
153
+ return { write: authority.operations.write, protected: authority.protected_paths };
154
+ }
155
+
150
156
  function importData(text) {
151
157
  const bytes = Buffer.from(text, "utf8");
152
158
  return [Buffer.from(`data ${bytes.length}\n`), bytes, Buffer.from("\n")];
@@ -210,16 +216,28 @@ async function commitOf(repository, name, signal) {
210
216
  return commit.toString("utf8").trim();
211
217
  }
212
218
 
213
- async function remembered(repository, revisions, commit, files, signal) {
219
+ async function remembered(repository, revisions, commit, files, signal, acknowledged = false) {
214
220
  const tree = await treeOf(repository, commit, signal);
215
221
  const next = { ...revisions };
216
222
  for (const { path, revision } of files) {
217
223
  const entry = tree.get(path);
218
- if (entry !== undefined && revision !== null) next[path] = { ...next[path], [entry.object]: revision };
224
+ if (entry === undefined || revision === null) continue;
225
+ const seen = next[path]?.[entry.object] ?? [];
226
+ if (acknowledged || seen.at(-1)?.revision !== revision) {
227
+ next[path] = { ...next[path], [entry.object]: [...seen, { commit, revision }] };
228
+ }
219
229
  }
220
230
  return next;
221
231
  }
222
232
 
233
+ async function revisionAt(copy, path, entry, base, signal) {
234
+ const seen = entry === undefined ? [] : (copy.revisions[path]?.[entry.object] ?? []);
235
+ for (const known of seen.toReversed()) {
236
+ if (seen.length === 1 || (await contains(copy.repository, known.commit, base, signal))) return known.revision;
237
+ }
238
+ return undefined;
239
+ }
240
+
223
241
  function readOnlyContents(read) {
224
242
  const contents = new Map();
225
243
  for (const file of read.files) {
@@ -244,19 +262,22 @@ async function trackedPaths(repository, signal) {
244
262
  return new Set(listed.toString("utf8").split("\0").filter((path) => path !== ""));
245
263
  }
246
264
 
247
- async function replaceReadOnly(repository, previous, next, signal) {
265
+ async function replaceReadOnly(repository, held, written, removed, signal) {
248
266
  const tracked = await trackedPaths(repository, signal);
249
- const held = new Set(previous);
250
- for (const [path, content] of next) {
267
+ const holding = new Set(held);
268
+ for (const [path, content] of written) {
251
269
  if (tracked.has(path)) continue;
252
270
  const target = locate(repository, "", path);
253
271
  if (!held.has(path) && (await present(target))) continue;
254
272
  await writeReplicaFile(target, content, signal);
273
+ holding.add(path);
255
274
  }
256
- for (const path of held) {
257
- if (next.has(path) || tracked.has(path)) continue;
258
- await removeReplicaFile(locate(repository, "", path));
275
+ for (const path of removed) {
276
+ if (!held.has(path)) continue;
277
+ if (!tracked.has(path)) await removeReplicaFile(locate(repository, "", path));
278
+ holding.delete(path);
259
279
  }
280
+ return [...holding];
260
281
  }
261
282
 
262
283
  function covers(path, prefix) {
@@ -304,17 +325,16 @@ async function writeIgnoreRules(repository, read, signal) {
304
325
  ...[...house, ...granted].map((pattern) => `!${pattern}`),
305
326
  "",
306
327
  ];
307
- await writeText(join(repository, ".gitignore"), gitignore.join("\n"), signal, 0o644);
308
- await writeText(join(repository, ".ignore"), ignore.join("\n"), signal, 0o644);
328
+ await ruled(join(repository, ".gitignore"), gitignore.join("\n"), signal);
329
+ await ruled(join(repository, ".ignore"), ignore.join("\n"), signal);
309
330
  }
310
331
 
311
- function manifest(read, commit) {
312
- return {
313
- commit,
314
- position: read.position,
315
- log_epoch: read.logEpoch,
316
- read_only: [...readOnlyContents(read).keys()],
317
- };
332
+ async function ruled(path, rules, signal) {
333
+ if ((await readFile(path, "utf8").catch(() => null)) !== rules) await writeText(path, rules, signal, 0o644);
334
+ }
335
+
336
+ function manifest(read, commit, readOnly) {
337
+ return { commit, position: read.position, log_epoch: read.logEpoch, read_only: readOnly, declared: read.declared };
318
338
  }
319
339
 
320
340
  function copyPath(directory, copy) {
@@ -376,6 +396,7 @@ async function materialize(context, scope, signal) {
376
396
  const imported = received(scope, read, []);
377
397
  let commit;
378
398
  let revisions;
399
+ let readOnly;
379
400
  try {
380
401
  await runGit(staging, ["init", "-q", "-b", WORKING_COPY_BRANCH], { signal, failure: GIT_FAILED });
381
402
  commit = (await importReceived(staging, imported, signal)).received;
@@ -385,7 +406,7 @@ async function materialize(context, scope, signal) {
385
406
  failure: GIT_FAILED,
386
407
  });
387
408
  await runGit(staging, ["read-tree", "-m", "-u", "HEAD"], { signal, failure: GIT_FAILED });
388
- await replaceReadOnly(staging, [], readOnlyContents(read), signal);
409
+ readOnly = await replaceReadOnly(staging, new Set(), readOnlyContents(read), [], signal);
389
410
  await writeIgnoreRules(staging, read, signal);
390
411
  await rename(staging, repository);
391
412
  } catch (error) {
@@ -399,7 +420,7 @@ async function materialize(context, scope, signal) {
399
420
  room_handle: scope.handle,
400
421
  repository,
401
422
  active: true,
402
- received: manifest(read, commit),
423
+ received: manifest(read, commit, readOnly),
403
424
  revisions,
404
425
  });
405
426
  }
@@ -409,11 +430,14 @@ async function receive(context, copy, scope, signal) {
409
430
  if (read.anchor !== copy.room_ref) throw rejected("bootstrap");
410
431
  const imported = received(scope, read, [copy.received.commit]);
411
432
  const commit = (await importReceived(copy.repository, imported, signal)).received;
412
- await replaceReadOnly(copy.repository, copy.received.read_only, readOnlyContents(read), signal);
433
+ const next = readOnlyContents(read);
434
+ const held = copy.received.read_only;
435
+ const removed = held.filter((path) => !next.has(path));
436
+ const readOnly = await replaceReadOnly(copy.repository, new Set(held), next, removed, signal);
413
437
  await writeIgnoreRules(copy.repository, read, signal);
414
438
  return {
415
439
  ...copy,
416
- received: manifest(read, commit),
440
+ received: manifest(read, commit, readOnly),
417
441
  revisions: await remembered(copy.repository, copy.revisions, commit, imported.files, signal),
418
442
  };
419
443
  }
@@ -432,6 +456,117 @@ function scopeRefused(error) {
432
456
  return error instanceof KitRefusal && (houseAnswered(error) || error.code.startsWith("kit_working_copy_"));
433
457
  }
434
458
 
459
+ function declaredWritable(path, authority) {
460
+ const deepest = (scopes) =>
461
+ Math.max(-1, ...scopes.filter((scope) => scope === "" || covers(path, scope)).map((scope) => scope.length));
462
+ return deepest(authority.operations.write) > deepest(authority.protected_paths);
463
+ }
464
+
465
+ async function incorporated(copy, view, authority, signal) {
466
+ const tree = await treeOf(copy.repository, copy.received.commit, signal);
467
+ const held = new Set(copy.received.read_only);
468
+ const files = [];
469
+ const written = new Map();
470
+ const removed = [];
471
+ for (const change of view.changes) {
472
+ const gone = change.removed === true;
473
+ if (tree.has(change.path) || (!gone && !held.has(change.path) && declaredWritable(change.path, authority))) {
474
+ files.push({ path: change.path, revision: gone ? null : change.revision, content: gone ? null : change.content });
475
+ } else if (gone) {
476
+ removed.push(change.path, sidecarPath(change.path));
477
+ } else {
478
+ written.set(change.path, change.content);
479
+ if (typeof change.provenance === "string") written.set(sidecarPath(change.path), change.provenance);
480
+ }
481
+ }
482
+ const message = `House ${housePath(scopeOf(copy))} at position ${view.position}`;
483
+ const commit =
484
+ files.length === 0
485
+ ? copy.received.commit
486
+ : (await importReceived(copy.repository, { message, parents: [copy.received.commit], files }, signal)).received;
487
+ const readOnly = await replaceReadOnly(copy.repository, held, written, removed, signal);
488
+ if (view.changes.length > 0) {
489
+ const listed = [
490
+ ...[...(await treeOf(copy.repository, commit, signal)).keys()].map((path) => ({ path, writable: true })),
491
+ ...readOnly.filter((path) => !isGeneratedProvenance(path)).map((path) => ({ path, writable: false })),
492
+ ].sort((left, right) => (left.path < right.path ? -1 : 1));
493
+ await writeIgnoreRules(copy.repository, { files: listed, protectedPaths: authority.protected_paths }, signal);
494
+ }
495
+ return {
496
+ ...copy,
497
+ received: { ...copy.received, commit, position: view.position, log_epoch: view.log_epoch, read_only: readOnly },
498
+ revisions: await remembered(copy.repository, copy.revisions, commit, files, signal),
499
+ };
500
+ }
501
+
502
+ async function deliver(context, path, authority, signal) {
503
+ if (
504
+ typeof authority.position !== "string" ||
505
+ typeof authority.log_epoch !== "string" ||
506
+ !Array.isArray(authority.protected_paths) ||
507
+ !Array.isArray(authority.operations?.write)
508
+ ) {
509
+ throw rejected("discover");
510
+ }
511
+ let copy = await readCopy(path);
512
+ if (copy?.active && JSON.stringify(copy.received.declared) !== JSON.stringify(declaration(authority))) {
513
+ await writeCopy(context.directory, await receive(context, copy, scopeOf(copy), signal), signal);
514
+ return;
515
+ }
516
+ while (
517
+ copy?.active &&
518
+ (copy.received.log_epoch !== authority.log_epoch || BigInt(copy.received.position) < BigInt(authority.position))
519
+ ) {
520
+ const since = { position: copy.received.position, log_epoch: copy.received.log_epoch };
521
+ let view;
522
+ try {
523
+ view = await enumerated(context, copy, since, signal);
524
+ } catch (error) {
525
+ if (error.code === "replica_behind") return;
526
+ if (error.code !== "position_expired") throw error;
527
+ await writeCopy(context.directory, await receive(context, copy, scopeOf(copy), signal), signal);
528
+ return;
529
+ }
530
+ if (view.position === since.position) return;
531
+ copy = await incorporated(copy, view, authority, signal);
532
+ await writeCopy(context.directory, copy, signal);
533
+ }
534
+ }
535
+
536
+ async function delivering(context, signal) {
537
+ const copies = (await heldCopies(context.directory)).filter((copy) => copy.active);
538
+ if (copies.length === 0) return { refused: [], lost: false };
539
+ const prefixes = copies.flatMap((copy) => selectedPrefixes(scopeOf(copy)));
540
+ const discovery = discoveryOf(await door(context.session, KIT_DOOR_DISCOVER_PATH, { prefixes }, signal));
541
+ const refused = [];
542
+ let lost = false;
543
+ for (const copy of copies) {
544
+ const at = housePath(scopeOf(copy));
545
+ const authority = discovery.authorities.find((entry) => entry?.room_ref === copy.room_ref);
546
+ if (authority === undefined) {
547
+ lost = true;
548
+ continue;
549
+ }
550
+ if (authority.managed_replication !== "admitted") {
551
+ refused.push({ path: at, code: "operation_denied" });
552
+ continue;
553
+ }
554
+ const path = copyPath(context.directory, copy);
555
+ try {
556
+ await exclusively(path, () => deliver(context, path, authority, signal));
557
+ } catch (error) {
558
+ if (!scopeRefused(error)) throw error;
559
+ refused.push({ path: at, code: error.code });
560
+ }
561
+ }
562
+ return { refused, lost };
563
+ }
564
+
565
+ export async function deliverWorkingCopies(context, signal) {
566
+ const delivered = await delivering(context, signal);
567
+ return delivered.lost ? applyWorkingCopies(context, signal) : { refused: delivered.refused };
568
+ }
569
+
435
570
  export async function applyWorkingCopies(context, signal) {
436
571
  const selected = (await savedSelection(context.session, signal)).filter(
437
572
  (scope) => scope.kind === "private" || scope.handle !== null,
@@ -461,7 +596,7 @@ export async function applyWorkingCopies(context, signal) {
461
596
  refused.push({ path: housePath(scope), code: error.code });
462
597
  }
463
598
  }
464
- return { refused };
599
+ return { refused: [...refused, ...(await delivering(context, signal)).refused] };
465
600
  }
466
601
 
467
602
  export async function workingCopyPaths(directory) {
@@ -694,8 +829,8 @@ function renamed(chains, reached, before, after) {
694
829
  return new Map(pairs.filter(([from, to]) => pairs.filter((pair) => pair[0] === from || pair[1] === to).length === 1));
695
830
  }
696
831
 
697
- async function relocated(copy, root, [from, to], prior, next, signal) {
698
- const base = copy.revisions[from]?.[prior.object];
832
+ async function relocated(copy, root, [from, to], [prior, next], house, signal) {
833
+ const base = await revisionAt(copy, from, prior, house, signal);
699
834
  const rename = { op: "rename", path: `${root}/${from}`, to: `${root}/${to}`, base };
700
835
  if (prior.object === next.object) return [rename];
701
836
  return [rename, { op: "replace", path: `${root}/${to}`, base, content: await blobText(copy.repository, next, to, signal) }];
@@ -713,13 +848,13 @@ async function derived(copy, base, commit, signal) {
713
848
  const prior = before.get(path);
714
849
  if (moved.has(path)) {
715
850
  const to = moved.get(path);
716
- changes.push(...(await relocated(copy, root, [path, to], prior, after.get(to), signal)));
851
+ changes.push(...(await relocated(copy, root, [path, to], [prior, after.get(to)], base, signal)));
717
852
  continue;
718
853
  }
719
854
  const next = after.get(path);
720
855
  if (prior?.object === next?.object) continue;
721
856
  const target = `${root}/${path}`;
722
- const revision = copy.revisions[path]?.[prior?.object];
857
+ const revision = await revisionAt(copy, path, prior, base, signal);
723
858
  if (next === undefined) {
724
859
  changes.push({ op: "remove", path: target, base: revision });
725
860
  continue;
@@ -761,8 +896,15 @@ async function recorded(copy, files, message, acknowledging, signal) {
761
896
  return {
762
897
  copy: {
763
898
  ...copy,
764
- received: { ...copy.received, commit: imported.received },
765
- revisions: await remembered(copy.repository, copy.revisions, imported.received, files, signal),
899
+ received: { ...copy.received, commit: imported.received, ...(acknowledging === null ? {} : copy.push.from) },
900
+ revisions: await remembered(
901
+ copy.repository,
902
+ copy.revisions,
903
+ imported.acknowledged ?? imported.received,
904
+ files,
905
+ signal,
906
+ imported.acknowledged !== null,
907
+ ),
766
908
  },
767
909
  acknowledged: imported.acknowledged,
768
910
  };
@@ -952,11 +1094,12 @@ async function expired(context, copy, text, signal) {
952
1094
  const entry = house.get(path);
953
1095
  written.push({
954
1096
  path: `${root}/${path}`,
955
- revision: entry === undefined ? null : confirmed.revisions[path][entry.object],
1097
+ revision: entry === undefined ? null : await revisionAt(confirmed, path, entry, confirmed.received.commit, signal),
956
1098
  content: await blobText(copy.repository, entry, path, signal),
957
1099
  });
958
1100
  }
959
- const accepted = { ...confirmed, push: { ...push, state: "accepted", written } };
1101
+ const from = { position: confirmed.received.position, log_epoch: confirmed.received.log_epoch };
1102
+ const accepted = { ...confirmed, push: { ...push, state: "accepted", written, from } };
960
1103
  await writeCopy(context.directory, accepted, signal);
961
1104
  return finish(context, accepted, signal);
962
1105
  }
@@ -1030,6 +1173,7 @@ async function pushed(context, path, at, selector, caller, signal) {
1030
1173
  operation_id: operationId(),
1031
1174
  changes,
1032
1175
  commit,
1176
+ from: { position: copy.received.position, log_epoch: copy.received.log_epoch },
1033
1177
  worktree: at.worktree,
1034
1178
  branch,
1035
1179
  ...(since === null ? {} : { since }),