amalgm 0.1.257 → 0.1.259

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 (26) hide show
  1. package/lib/cli.js +207 -3
  2. package/lib/layout.js +30 -9
  3. package/lib/shared-realtime-tunnel.js +12 -14
  4. package/package.json +2 -2
  5. package/runtime/scripts/amalgm-mcp/lib/layout.js +32 -9
  6. package/runtime/scripts/amalgm-mcp/observer/index.js +65 -4
  7. package/runtime/scripts/amalgm-mcp/observer/scan.js +60 -1
  8. package/runtime/scripts/amalgm-mcp/observer/store.js +43 -8
  9. package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +186 -32
  10. package/runtime/scripts/amalgm-mcp/registration/entity-content.js +33 -6
  11. package/runtime/scripts/amalgm-mcp/registration/index.js +21 -0
  12. package/runtime/scripts/amalgm-mcp/registration/service.js +117 -11
  13. package/runtime/scripts/amalgm-mcp/registry/index.js +1 -1
  14. package/runtime/scripts/amalgm-mcp/repocard/capture.js +16 -8
  15. package/runtime/scripts/amalgm-mcp/server/routes/entities.js +6 -0
  16. package/runtime/scripts/amalgm-mcp/tests/entity-cloud.test.js +67 -8
  17. package/runtime/scripts/amalgm-mcp/tests/entity-materialization.test.js +3 -3
  18. package/runtime/scripts/amalgm-mcp/tests/observer.test.js +85 -0
  19. package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +95 -3
  20. package/runtime/scripts/amalgm-mcp/tests/repocard.test.js +21 -0
  21. package/runtime/scripts/amalgm-mcp/tests/system-catalog.test.js +3 -6
  22. package/runtime/scripts/amalgm-mcp/tests/toolbox-artifacts.test.js +88 -0
  23. package/runtime/scripts/amalgm-mcp/tests/toolbox-cloud-registration.test.js +91 -0
  24. package/runtime/scripts/amalgm-mcp/toolbox/artifacts.js +225 -0
  25. package/runtime/scripts/amalgm-mcp/toolbox/store.js +59 -10
  26. package/runtime/scripts/amalgm-mcp/toolbox/system-catalog.js +14 -5
@@ -47,6 +47,10 @@ const Database = require('better-sqlite3');
47
47
  const { STORAGE_DIR } = require('../config');
48
48
  const { ensureDir } = require('../lib/storage');
49
49
  const { ensureUserHome } = require('../lib/layout');
50
+ const {
51
+ materializeSystemToolArtifacts,
52
+ migrateLegacyToolboxCatalog,
53
+ } = require('../toolbox/artifacts');
50
54
  const { createObserver } = require('../observer');
51
55
  const { shouldEnroll } = require('../workspace/tree/inclusion');
52
56
  const { classifyFile } = require('./classify');
@@ -58,8 +62,8 @@ const { transportBytes, localState, unpack } = require('./repo-states');
58
62
  const { machineId, userId } = require('../workspace/identity');
59
63
 
60
64
  const ENTITIES_DIR = path.join(STORAGE_DIR, 'entities');
61
- const USER_HOME_CLOUD_CONTRACT = 'amalgm-user-home@1';
62
- const USER_HOME_CLOUD_SCHEMA_VERSION = 1;
65
+ const USER_HOME_CLOUD_CONTRACT = 'amalgm-user-home@2';
66
+ const USER_HOME_CLOUD_SCHEMA_VERSION = 2;
63
67
  const ATOMIC_REGISTRATION_THRESHOLD = 500;
64
68
 
65
69
  let live = null;
@@ -260,6 +264,19 @@ function createRegistrationService({
260
264
  */
261
265
  async function bootstrapUserHome({ identitySnapshot = null } = {}) {
262
266
  const declaration = ensureUserHome(homeDir);
267
+ // Materialize every built-in tool before the shared registration boundary
268
+ // first sees `toolbox/`. Each file is an exact managed child; user tools
269
+ // remain ordinary sibling files. A legacy aggregate is moved outside the
270
+ // watched tree only after per-tool replacements exist.
271
+ const toolbox = declaration.roots.find((entry) => entry.key === 'toolbox');
272
+ if (!toolbox) throw new Error('declared user-home is missing its toolbox root');
273
+ const migrated = migrateLegacyToolboxCatalog({ toolboxDir: toolbox.path });
274
+ const systemTools = materializeSystemToolArtifacts({ toolboxDir: toolbox.path });
275
+ declaration.materializedExactPaths = [
276
+ ...(declaration.materializedExactPaths || []),
277
+ ...(migrated.artifactPaths || []).map((target) => path.relative(homeDir, target).split(path.sep).join('/')),
278
+ ...(systemTools.changedRelativePaths || []),
279
+ ];
263
280
  const seeds = identitySeeds(identitySnapshot, declaration);
264
281
  const addedRoots = boundary.current.addMany(declaration.roots.map((entry) => ({
265
282
  targetPath: entry.path,
@@ -274,25 +291,50 @@ function createRegistrationService({
274
291
  };
275
292
  });
276
293
 
277
- const system = roots.find((entry) => entry.key === 'system');
278
- if (!system) throw new Error('declared user home has no system root');
279
- const tree = boundary.current.tree(system.path);
280
- if (!tree?.root()) throw new Error(`declared system root was not registered: ${system.path}`);
281
-
282
- const recordAt = (relativePath) => {
294
+ const recordAt = (rootKey, relativePath, { required = true } = {}) => {
295
+ const root = roots.find((entry) => entry.key === rootKey);
296
+ if (!root) throw new Error(`declared user-home root was not registered: ${rootKey}`);
297
+ const tree = boundary.current.tree(root.path);
298
+ if (!tree?.root()) throw new Error(`declared user-home root was not registered: ${root.path}`);
283
299
  let record = tree.root();
284
300
  for (const name of relativePath.split('/')) {
285
301
  record = tree.registry.activeAt(record.uuid, name);
286
- if (!record) throw new Error(`declared user-home target was not registered: ${relativePath}`);
302
+ if (!record) {
303
+ if (!required) return null;
304
+ throw new Error(`declared user-home target was not registered: ${rootKey}/${relativePath}`);
305
+ }
287
306
  }
288
307
  return record;
289
308
  };
290
309
 
310
+ // An upgraded installation can already watch an older declared root.
311
+ // Re-registering that root must remain only an availability check, so the
312
+ // narrow exception is a root holding an exact node that the layout just
313
+ // materialized or that the registry proves missing. Detect and Record
314
+ // still create/update its identity through the shared boundary.
315
+ const rootsToReconcile = new Set((declaration.materializedExactPaths || [])
316
+ .map((relativePath) => relativePath.split('/')[0]));
317
+ for (const entry of declaration.exactManaged) {
318
+ if (entry.relativePath === entry.rootKey) continue;
319
+ const relativePath = entry.relativePath.slice(entry.rootKey.length + 1);
320
+ if (!recordAt(entry.rootKey, relativePath, { required: false })) {
321
+ rootsToReconcile.add(entry.rootKey);
322
+ }
323
+ }
324
+ for (const rootKey of rootsToReconcile) {
325
+ const root = roots.find((candidate) => candidate.key === rootKey);
326
+ if (!root) throw new Error(`declared user-home root was not registered: ${rootKey}`);
327
+ boundary.current.reconcileDeclared(root.path);
328
+ roots = roots.map((candidate) => candidate.key === rootKey
329
+ ? { ...candidate, record: boundary.current.tree(candidate.path).root() }
330
+ : candidate);
331
+ }
332
+
291
333
  let exactManaged = declaration.exactManaged.map((entry) => {
292
334
  const root = roots.find((candidate) => candidate.key === entry.rootKey);
293
335
  const isRoot = entry.relativePath === entry.rootKey;
294
336
  const relativePath = isRoot ? '' : entry.relativePath.slice(entry.rootKey.length + 1);
295
- const record = isRoot ? root?.record : recordAt(relativePath);
337
+ const record = isRoot ? root?.record : recordAt(entry.rootKey, relativePath);
296
338
  if (!record) throw new Error(`declared user-home managed target was not registered: ${entry.relativePath}`);
297
339
  return {
298
340
  key: entry.key,
@@ -767,6 +809,66 @@ function createRegistrationService({
767
809
  }
768
810
  }
769
811
 
812
+ /** The status surface composes each layer's own facts without creating a
813
+ * second registry or treating local paths as cloud metadata. A cloud-only
814
+ * catalog record is intentionally visible with `localPath: null`; only
815
+ * `amalgm add` creates a watcher binding for it. */
816
+ function status() {
817
+ const cloud = entityCloud.status();
818
+ const localRecords = boundary.current.registry.syncRecords();
819
+ const localByUuid = new Map(localRecords.map((record) => [record.uuid, record]));
820
+ const cloudByUuid = new Map(cloud.catalog.records.map((record) => [record.uuid, record]));
821
+ function cloudPathOf(uuid) {
822
+ const names = [];
823
+ const seen = new Set();
824
+ let record = cloudByUuid.get(uuid) || null;
825
+ while (record) {
826
+ if (seen.has(record.uuid)) return null;
827
+ seen.add(record.uuid);
828
+ names.unshift(record.name);
829
+ if (record.parentUUID === null) break;
830
+ record = cloudByUuid.get(record.parentUUID) || null;
831
+ if (!record) return null;
832
+ }
833
+ return names.length > 0 ? names.join('/') : null;
834
+ }
835
+ const roots = observer.status().map((root) => {
836
+ const tree = boundary.current.tree(root.path);
837
+ const record = tree ? tree.root() : null;
838
+ return {
839
+ uuid: record?.uuid || null,
840
+ path: root.path,
841
+ kind: root.kind,
842
+ watching: root.watching,
843
+ degraded: root.degraded,
844
+ addressWatching: root.addressWatching,
845
+ addressDegraded: root.addressDegraded,
846
+ };
847
+ });
848
+ const rootByUuid = new Map(roots.filter((root) => root.uuid !== null).map((root) => [root.uuid, root]));
849
+ const uuids = new Set([...localByUuid.keys(), ...cloudByUuid.keys()]);
850
+ const entities = [...uuids].sort().map((uuid) => {
851
+ const local = localByUuid.get(uuid) || null;
852
+ const record = cloudByUuid.get(uuid) || local;
853
+ let localPath = null;
854
+ if (local) {
855
+ try { localPath = boundary.current.addressOf(uuid); } catch {}
856
+ }
857
+ return {
858
+ ...record,
859
+ // A local journal projects its intent into the catalog before the
860
+ // gateway acknowledges it. Call that fact `catalogued`, not
861
+ // `cloudBacked`: the enclosing cloud summary says whether the
862
+ // catalog is actually pinned to an authoritative current head.
863
+ catalogued: cloudByUuid.has(uuid),
864
+ cloudPath: cloudByUuid.has(uuid) ? cloudPathOf(uuid) : null,
865
+ localPath,
866
+ root: rootByUuid.get(uuid) || null,
867
+ };
868
+ });
869
+ return { cloud, roots, entities };
870
+ }
871
+
770
872
  return {
771
873
  dir,
772
874
  homeDir,
@@ -820,9 +922,13 @@ function createRegistrationService({
820
922
  const received = entityCloud.receiveDownload({
821
923
  resourceId, contentHash, kind, partIndex, sha256, dataBase64,
822
924
  });
823
- advanceMaterializations();
925
+ // A mid-file part cannot complete any waiting tree; only a finished
926
+ // content head can. Advancing on every part re-walks every waiting
927
+ // job once per part, which is quadratic over a large download.
928
+ if (received?.complete) advanceMaterializations();
824
929
  return received;
825
930
  },
931
+ status,
826
932
  advanceMaterializations,
827
933
  bootstrapUserHome,
828
934
  exactUserHomeCloudRecords,
@@ -686,7 +686,7 @@ function createRegistry(options) {
686
686
 
687
687
  /**
688
688
  * First cloud bootstrap may meet a pre-cloud local safe home. Its six
689
- * roots and three exact managed nodes are platform truth, so they may
689
+ * roots and ten exact managed nodes are platform truth, so they may
690
690
  * adopt the authenticated cloud UUIDs without changing any user-owned
691
691
  * descendant identity. This is one atomic graph rekey, never a delete or
692
692
  * local-wins merge, and callers must prove that no ordinary entity-cloud
@@ -70,15 +70,23 @@ function untrackedFiles(repoPath) {
70
70
  // its own card; the parent's checkpoint must not contain it.
71
71
  if (rel.endsWith('/')) return [];
72
72
  const abs = path.join(repoPath, rel);
73
- const stat = fs.lstatSync(abs);
74
- if (stat.isSymbolicLink()) {
75
- const content = Buffer.from(fs.readlinkSync(abs));
76
- return [{ path: rel, mode: '120000', content, sha256: sha256(content) }];
73
+ try {
74
+ const stat = fs.lstatSync(abs);
75
+ if (stat.isSymbolicLink()) {
76
+ const content = Buffer.from(fs.readlinkSync(abs));
77
+ return [{ path: rel, mode: '120000', content, sha256: sha256(content) }];
78
+ }
79
+ if (stat.isDirectory()) return []; // same boundary, unmarked: never read a directory as a file
80
+ const content = fs.readFileSync(abs);
81
+ const mode = stat.mode & 0o111 ? '100755' : '100644';
82
+ return [{ path: rel, mode, content, sha256: sha256(content) }];
83
+ } catch (error) {
84
+ // Git's file listing is necessarily a point-in-time observation. A file
85
+ // may be removed or renamed before it can be read; the next watch event
86
+ // captures its replacement, while this capture truthfully omits it.
87
+ if (error && error.code === 'ENOENT') return [];
88
+ throw error;
77
89
  }
78
- if (stat.isDirectory()) return []; // same boundary, unmarked: never read a directory as a file
79
- const content = fs.readFileSync(abs);
80
- const mode = stat.mode & 0o111 ? '100755' : '100644';
81
- return [{ path: rel, mode, content, sha256: sha256(content) }];
82
90
  });
83
91
  }
84
92
 
@@ -316,6 +316,12 @@ async function handleEntityRoutes(ctx) {
316
316
  return true;
317
317
  }
318
318
 
319
+ if (ctx.pathname === '/entities/status' && ctx.method === 'GET') {
320
+ ctx.res.setHeader('Cache-Control', 'no-store');
321
+ ctx.sendJson(200, registrationService().status());
322
+ return true;
323
+ }
324
+
319
325
  const materializationMatch = /^\/entities\/add\/(mat_[A-Za-z0-9_-]+)$/.exec(ctx.pathname);
320
326
  if (materializationMatch && ctx.method === 'GET') {
321
327
  const service = registrationService();
@@ -76,6 +76,37 @@ test('a signed-in registration is refused until the cloud entity resource is act
76
76
  assert.doesNotThrow(() => cloud.assertRegistrationReady());
77
77
  });
78
78
 
79
+ test('entity-cloud status reports an unsettled catalog as catching up without repairing it', () => {
80
+ const db = new Database(':memory:');
81
+ const cloud = createEntityCloud({ db, userId: 'user-1', deviceId: 'device-1' });
82
+ const registry = createRegistry({ db, ephemeral: true, emit: () => {}, journal: cloud.journal });
83
+ const root = registry.enrollRoot({ type: 'workspace', name: 'ground' });
84
+ const bootstrap = cloud.bootstrap(registry.syncRecords());
85
+ cloud.install({
86
+ registry,
87
+ resourceId: bootstrap.resourceId,
88
+ authorityEpoch: 1,
89
+ headVersion: 0,
90
+ snapshotBase64: bootstrap.snapshotBase64,
91
+ snapshotChecksum: bootstrap.snapshotChecksum,
92
+ });
93
+
94
+ const current = cloud.status();
95
+ assert.equal(current.catalog.current, true);
96
+ assert.equal(current.catalog.records[0].uuid, root.uuid);
97
+ assert.equal(current.outbox.pending, 0);
98
+
99
+ const child = registry.create({ type: 'folder', parentUUID: root.uuid, name: 'child' });
100
+ const catchingUp = cloud.status();
101
+ assert.equal(catchingUp.catalog.current, false, 'an unacknowledged local mutation never masquerades as an active cloud head');
102
+ assert.equal(catchingUp.outbox.pending, 1);
103
+ assert.equal(catchingUp.catalog.records.some((record) => record.uuid === child.uuid), true,
104
+ 'status may show the local catalog projection while naming it catching up');
105
+
106
+ const after = cloud.status();
107
+ assert.deepEqual(after, catchingUp, 'status is read-only: it does not claim, retry, or repair delivery');
108
+ });
109
+
79
110
  test('a large registration replaces the complete graph in one cloud operation and a recipient waits for its compacted snapshot', () => {
80
111
  const db = new Database(':memory:');
81
112
  const cloud = createEntityCloud({ db, userId: 'user-1', deviceId: 'device-1' });
@@ -244,7 +275,7 @@ test('a compacted remote snapshot enters the cloud catalog, not locally watched
244
275
  fs.rmSync(targetDir, { recursive: true, force: true });
245
276
  });
246
277
 
247
- test('add accepts only a catalog installed by the active compacted snapshot, never a snapshot plus a mutation tail', () => {
278
+ test('add follows in-order mirrored mutations and refuses after a version gap until the compacted snapshot returns', () => {
248
279
  const sourceDb = new Database(':memory:');
249
280
  const sourceCloud = createEntityCloud({ db: sourceDb, userId: 'user-1', deviceId: 'source-device' });
250
281
  const sourceRegistry = createRegistry({ db: sourceDb, ephemeral: true, emit: () => {}, journal: sourceCloud.journal });
@@ -277,7 +308,7 @@ test('add accepts only a catalog installed by the active compacted snapshot, nev
277
308
  envelope: {
278
309
  resourceId,
279
310
  mutationId: 'ent_remote_change',
280
- contract: 'amalgm-entity-registry@2',
311
+ contract: 'amalgm-entity-registry@3',
281
312
  schemaVersion: 1,
282
313
  operationKind: 'entity.upsert',
283
314
  operation: { kind: 'entity.upsert', record: changed },
@@ -286,25 +317,53 @@ test('add accepts only a catalog installed by the active compacted snapshot, nev
286
317
  committedAt: '2026-08-04T00:00:00.000Z',
287
318
  });
288
319
 
320
+ // Version 13 arrived in official order on a snapshot-consistent catalog:
321
+ // the catalog IS the compacted head, so add needs no re-download.
322
+ const inOrder = path.join(targetDir, 'in-order');
323
+ fs.mkdirSync(inOrder);
324
+ assert.equal(
325
+ targetCloud.startMaterialization({ uuid: root.uuid, targetPath: inOrder }).headVersion,
326
+ 13,
327
+ 'an in-order mirrored mutation keeps the catalog materializable at the new head',
328
+ );
329
+
330
+ // Version 15 skips 14: the catalog can no longer prove it equals the
331
+ // head, so add refuses until a fresh compacted snapshot is installed.
332
+ const gapped = { ...changed, name: 'gapped.md', version: 'd'.repeat(64) };
333
+ targetCloud.ingest({
334
+ registry: targetRegistry,
335
+ envelope: {
336
+ resourceId,
337
+ mutationId: 'ent_remote_gap',
338
+ contract: 'amalgm-entity-registry@3',
339
+ schemaVersion: 1,
340
+ operationKind: 'entity.upsert',
341
+ operation: { kind: 'entity.upsert', record: gapped },
342
+ },
343
+ version: 15,
344
+ committedAt: '2026-08-04T00:00:01.000Z',
345
+ });
346
+ const afterGap = path.join(targetDir, 'after-gap');
347
+ fs.mkdirSync(afterGap);
289
348
  assert.throws(
290
- () => targetCloud.startMaterialization({ uuid: root.uuid, targetPath: targetDir }),
349
+ () => targetCloud.startMaterialization({ uuid: root.uuid, targetPath: afterGap }),
291
350
  /catalog is not pinned to the active cloud head/,
292
- 'the mutation updated live catalog facts, but not the snapshot barrier',
351
+ 'a version gap drops the snapshot barrier until compaction returns',
293
352
  );
294
353
 
295
354
  const cloudRoot = initialSnapshot.records.find((record) => record.uuid === root.uuid);
296
- const compactedBytes = Buffer.from(JSON.stringify(snapshotFromRecords([cloudRoot, changed])), 'utf8');
355
+ const compactedBytes = Buffer.from(JSON.stringify(snapshotFromRecords([cloudRoot, gapped])), 'utf8');
297
356
  targetCloud.install({
298
357
  registry: targetRegistry,
299
358
  resourceId,
300
359
  authorityEpoch: 3,
301
- headVersion: 13,
360
+ headVersion: 15,
302
361
  snapshotBase64: compactedBytes.toString('base64'),
303
362
  snapshotChecksum: require('crypto').createHash('sha256').update(compactedBytes).digest('hex'),
304
363
  });
305
364
  assert.equal(
306
- targetCloud.startMaterialization({ uuid: root.uuid, targetPath: targetDir }).headVersion,
307
- 13,
365
+ targetCloud.startMaterialization({ uuid: root.uuid, targetPath: afterGap }).headVersion,
366
+ 15,
308
367
  'the fresh compacted snapshot—not a history replay—makes add available again',
309
368
  );
310
369
  } finally {
@@ -189,7 +189,7 @@ test('an added ordinary workspace stays live: a later cloud file head replaces l
189
189
  envelope: {
190
190
  resourceId: sourceCloud.identity().resourceId,
191
191
  mutationId: 'ent_remote_file_change',
192
- contract: 'amalgm-entity-registry@2',
192
+ contract: 'amalgm-entity-registry@3',
193
193
  schemaVersion: 1,
194
194
  operationKind: 'entity.upsert',
195
195
  operation: { kind: 'entity.upsert', record: remoteRecord },
@@ -296,7 +296,7 @@ test('add preserves a symlink UUID and later retargets the same literal link wit
296
296
  const ingested = target.ingestCloudMutation({
297
297
  envelope: {
298
298
  resourceId: sourceCloud.identity().resourceId, mutationId: 'ent_remote_link_change',
299
- contract: 'amalgm-entity-registry@2', schemaVersion: 1,
299
+ contract: 'amalgm-entity-registry@3', schemaVersion: 1,
300
300
  operationKind: 'entity.upsert', operation: { kind: 'entity.upsert', record: sourceRegistry.syncRecord(changed.uuid) },
301
301
  },
302
302
  version: 42, committedAt: '2026-08-05T00:00:00.000Z',
@@ -420,7 +420,7 @@ test('add recreates an actual Git repository from verified Repocard cargo, then
420
420
  const ingested = target.ingestCloudMutation({
421
421
  envelope: {
422
422
  resourceId: bootstrap.resourceId, mutationId: 'ent_remote_repo_change',
423
- contract: 'amalgm-entity-registry@2', schemaVersion: 1,
423
+ contract: 'amalgm-entity-registry@3', schemaVersion: 1,
424
424
  operationKind: 'entity.upsert', operation: { kind: 'entity.upsert', record: remoteRecord },
425
425
  },
426
426
  version: 2, committedAt: '2026-08-05T00:00:00.000Z',
@@ -655,3 +655,88 @@ test('watcher death rebuilds the doorbell and reconciles the gap once', () => {
655
655
  fs.watch = realWatch;
656
656
  }
657
657
  });
658
+
659
+ // --- the scoped scan ------------------------------------------------------
660
+ // A NAMED doorbell earns a look at its own directory, never a walk of the
661
+ // world; anything structural escalates to the full walk unchanged. The
662
+ // injected enrollment policy is the witness: the full walk asks it about
663
+ // every directory's entries, so a scoped look must never ask about a
664
+ // sibling directory's ground.
665
+
666
+ const { fakeWatchKit } = require('./observer.rig');
667
+
668
+ function scopedRig() {
669
+ const dir = makeDir('scoped');
670
+ fs.mkdirSync(path.join(dir, 'a'));
671
+ fs.mkdirSync(path.join(dir, 'b'));
672
+ fs.writeFileSync(path.join(dir, 'a', 'one.md'), 'v1');
673
+ fs.writeFileSync(path.join(dir, 'b', 'two.md'), 'v1');
674
+ const asked = [];
675
+ const bells = fakeWatchKit();
676
+ const { observer, events } = makeObserver({
677
+ settleMs: 15,
678
+ watch: bells.watch,
679
+ shouldEnroll: (relPath) => {
680
+ asked.push(relPath);
681
+ return true;
682
+ },
683
+ });
684
+ observer.enrollRoot(dir);
685
+ observer.start();
686
+ events.length = 0;
687
+ asked.length = 0;
688
+ return { dir, asked, bells, observer, events };
689
+ }
690
+
691
+ async function settled(events, arrived, deadlineMs = 5_000) {
692
+ const deadline = Date.now() + deadlineMs;
693
+ while (!arrived() && Date.now() < deadline) {
694
+ await new Promise((resolve) => setTimeout(resolve, 10));
695
+ }
696
+ return arrived();
697
+ }
698
+
699
+ test('a named doorbell scans only its own directory', async () => {
700
+ const { dir, asked, bells, observer, events } = scopedRig();
701
+ fs.writeFileSync(path.join(dir, 'a', 'one.md'), 'v2');
702
+ bells.ring(dir, 'a/one.md');
703
+ assert.ok(await settled(events, () => eventsOf(events, 'file.changed').length > 0));
704
+ observer.stop();
705
+
706
+ const changed = eventsOf(events, 'file.changed');
707
+ assert.equal(changed.length, 1);
708
+ assert.equal(changed[0].path, 'a/one.md');
709
+ assert.ok(asked.includes('a/one.md'), 'the rung directory was read');
710
+ assert.ok(!asked.includes('b/two.md'), `the sibling directory was never walked: ${asked.join(', ')}`);
711
+ });
712
+
713
+ test('a create under a named doorbell escalates to the full walk', async () => {
714
+ const { dir, asked, bells, observer, events } = scopedRig();
715
+ fs.writeFileSync(path.join(dir, 'a', 'new.md'), 'fresh');
716
+ bells.ring(dir, 'a/new.md');
717
+ assert.ok(await settled(events, () => eventsOf(events, 'file.created').length > 0));
718
+ observer.stop();
719
+
720
+ const created = eventsOf(events, 'file.created');
721
+ assert.equal(created.length, 1);
722
+ assert.equal(created[0].path, 'a/new.md');
723
+ assert.ok(asked.includes('b/two.md'), 'structural news pays for the whole walk');
724
+ });
725
+
726
+ test('a cross-directory move naming only the new side still concludes a rename', async () => {
727
+ const { dir, bells, observer, events } = scopedRig();
728
+ fs.renameSync(path.join(dir, 'a', 'one.md'), path.join(dir, 'b', 'moved.md'));
729
+ // A rename may lawfully deliver only the NEW name: the scoped look sees
730
+ // an orphan entry, refuses to conclude, and the full walk's inode rescue
731
+ // names the move — never a create beside a ghost.
732
+ bells.ring(dir, 'b/moved.md');
733
+ assert.ok(await settled(events, () => eventsOf(events, 'file.renamed').length > 0));
734
+ observer.stop();
735
+
736
+ const renamed = eventsOf(events, 'file.renamed');
737
+ assert.equal(renamed.length, 1);
738
+ assert.equal(renamed[0].from, 'a/one.md');
739
+ assert.equal(renamed[0].to, 'b/moved.md');
740
+ assert.equal(eventsOf(events, 'file.created').length, 0);
741
+ assert.equal(eventsOf(events, 'file.deleted').length, 0);
742
+ });
@@ -54,6 +54,7 @@ const { ENTITIES_DIR, createRegistrationService, registrationService } = require
54
54
  const { adapterFor } = require('../adapters');
55
55
  const { createRuntimeHttpServer } = require('../server/http-service');
56
56
  const { createLocalServiceRouter } = require('../server/local-service-router');
57
+ const { SYSTEM_TOOL_ARTIFACTS } = require('../lib/layout');
57
58
 
58
59
  const services = [];
59
60
  let server;
@@ -203,6 +204,7 @@ test('user-home bootstrap creates only declared roots and registers them through
203
204
  ['agents-root', 'exact'], ['apps-root', 'exact'], ['automations-root', 'exact'],
204
205
  ['toolbox-root', 'exact'], ['workspaces-root', 'exact'], ['system-root', 'exact'],
205
206
  ['home-manifest', 'exact'], ['defaults', 'exact'], ['policy', 'exact'],
207
+ ...SYSTEM_TOOL_ARTIFACTS.map((entry) => [entry.key, 'exact']),
206
208
  ],
207
209
  );
208
210
  for (const entry of bootstrapped.exactManaged) {
@@ -218,7 +220,7 @@ test('user-home bootstrap creates only declared roots and registers them through
218
220
  }
219
221
 
220
222
  const cloudRecords = await service.exactUserHomeCloudRecords();
221
- assert.equal(cloudRecords.length, 9,
223
+ assert.equal(cloudRecords.length, 16,
222
224
  'the declared home is the entire graph eligible to repair a legacy empty cloud registry');
223
225
  assert.deepEqual(
224
226
  new Set(cloudRecords.map((record) => record.uuid)),
@@ -233,6 +235,46 @@ test('user-home bootstrap creates only declared roots and registers them through
233
235
  );
234
236
  });
235
237
 
238
+ test('an upgraded declared home registers every built-in tool through Detect and Record', async () => {
239
+ const home = path.join(tempRoot, 'user-home-upgrade');
240
+ const service = createRegistrationService({
241
+ dir: path.join(tempRoot, 'state-user-home-upgrade'),
242
+ homeDir: home,
243
+ });
244
+ services.push(service);
245
+
246
+ for (const root of ['agents', 'apps', 'automations', 'toolbox', 'workspaces', 'system']) {
247
+ fs.mkdirSync(path.join(home, root), { recursive: true });
248
+ }
249
+ fs.mkdirSync(path.join(home, 'system', 'defaults'));
250
+ fs.mkdirSync(path.join(home, 'system', 'policy'));
251
+ fs.writeFileSync(path.join(home, 'system', 'home.json'), JSON.stringify({
252
+ schemaVersion: 1,
253
+ kind: 'amalgm-user-home',
254
+ roots: [],
255
+ exactManaged: [],
256
+ }));
257
+ service.registration.addMany(['agents', 'apps', 'automations', 'toolbox', 'workspaces', 'system']
258
+ .map((root) => path.join(home, root)));
259
+
260
+ const bootstrapped = await service.bootstrapUserHome();
261
+ const toolbox = bootstrapped.roots.find((entry) => entry.key === 'toolbox');
262
+ const browser = bootstrapped.exactManaged.find((entry) => entry.key === 'amalgm-browser');
263
+ const manifest = bootstrapped.exactManaged.find((entry) => entry.key === 'home-manifest');
264
+ assert.equal(browser.record.parentUUID, toolbox.record.uuid,
265
+ 'the declared browser tool is a real child of the existing toolbox root');
266
+ assert.equal(browser.record.type, 'file.text');
267
+ assert.equal(service.registration.registry.syncRecord(manifest.record.uuid).payloadVersion, require('crypto')
268
+ .createHash('sha256')
269
+ .update(fs.readFileSync(path.join(home, 'system', 'home.json')))
270
+ .digest('hex'), 'the rewritten exact manifest is observed before its cloud identity is returned');
271
+ for (const artifact of SYSTEM_TOOL_ARTIFACTS) {
272
+ assert.ok(fs.existsSync(path.join(home, artifact.relativePath)), `${artifact.key} is materialized`);
273
+ assert.ok(bootstrapped.exactManaged.some((entry) => entry.key === artifact.key),
274
+ `${artifact.key} is an exact managed entity`);
275
+ }
276
+ });
277
+
236
278
  test('entity-cloud bootstrap publishes only the verified user home when an old local repo exists', async () => {
237
279
  const service = createRegistrationService({
238
280
  dir: path.join(tempRoot, 'state-user-home-cloud-boundary'),
@@ -248,13 +290,13 @@ test('entity-cloud bootstrap publishes only the verified user home when an old l
248
290
  });
249
291
 
250
292
  const records = await service.exactUserHomeCloudRecords();
251
- assert.equal(records.length, 9);
293
+ assert.equal(records.length, 16);
252
294
  assert.equal(records.some((record) => record.uuid === oldRepo.uuid), false,
253
295
  'login never turns existing user-selected ground into an implicit cloud registration');
254
296
 
255
297
  const bootstrap = service.entityCloud.bootstrap(records);
256
298
  const snapshot = JSON.parse(Buffer.from(bootstrap.snapshotBase64, 'base64').toString('utf8'));
257
- assert.equal(snapshot.records.length, 9);
299
+ assert.equal(snapshot.records.length, 16);
258
300
  service.entityCloud.install({
259
301
  registry: service.registration.registry,
260
302
  resourceId: bootstrap.resourceId,
@@ -393,6 +435,56 @@ test('a signed-in register call writes every entity delivery intent in identity.
393
435
  }
394
436
  });
395
437
 
438
+ test('service status exposes only a verified cloud path as a navigable workspace', async () => {
439
+ const service = createRegistrationService({
440
+ dir: path.join(tempRoot, 'state-cloud-status'),
441
+ homeDir: path.join(tempRoot, 'cloud-status-home'),
442
+ cloudUserId: 'user-cloud-status',
443
+ cloudDeviceId: 'computer-cloud-status',
444
+ });
445
+ const bootstrap = service.entityCloud.bootstrap(service.registration.registry.syncRecords());
446
+ service.entityCloud.install({
447
+ registry: service.registration.registry,
448
+ resourceId: bootstrap.resourceId,
449
+ authorityEpoch: 1,
450
+ headVersion: 0,
451
+ snapshotBase64: bootstrap.snapshotBase64,
452
+ snapshotChecksum: bootstrap.snapshotChecksum,
453
+ });
454
+ const ground = makeGround('cloud-status');
455
+ fs.mkdirSync(path.join(ground, 'src'));
456
+
457
+ try {
458
+ const registered = await service.register(ground);
459
+ const beforeAcknowledgement = service.status();
460
+ assert.equal(beforeAcknowledgement.cloud.catalog.current, false,
461
+ 'a pending local registration is visible to status but not a navigable verified cloud workspace');
462
+
463
+ let version = 0;
464
+ for (;;) {
465
+ const [claim] = service.entityCloud.claim();
466
+ if (!claim) break;
467
+ version += 1;
468
+ service.entityCloud.acknowledge({
469
+ resourceId: claim.resourceId,
470
+ mutationId: claim.mutationId,
471
+ version,
472
+ committedAt: '2026-08-05T00:00:00.000Z',
473
+ });
474
+ }
475
+
476
+ const status = service.status();
477
+ assert.equal(status.cloud.catalog.current, true);
478
+ const root = status.entities.find((entity) => entity.uuid === registered.record.uuid);
479
+ assert.equal(root.cloudPath, path.basename(ground));
480
+ const child = status.entities.find((entity) => entity.cloudPath === `${path.basename(ground)}/src`);
481
+ assert.ok(child, 'the cloud path is derived from canonical parentage, never a local absolute path');
482
+ assert.equal(child.localPath, path.join(ground, 'src'));
483
+ } finally {
484
+ service.close();
485
+ }
486
+ });
487
+
396
488
  test('a large signed-in register call publishes one complete cloud graph before Watch begins', async () => {
397
489
  const service = createRegistrationService({
398
490
  dir: path.join(tempRoot, 'state-cloud-register-large'),
@@ -175,6 +175,27 @@ test('an unborn repo with staged and untracked work round-trips', () => {
175
175
  assert.equal(state.card.bundle, null);
176
176
  });
177
177
 
178
+ test('a file that disappears after Git lists it is omitted instead of aborting capture', () => {
179
+ const source = makeRepo('vanishing-untracked');
180
+ const gone = path.join(source, 'gone-during-capture.md');
181
+ fs.writeFileSync(gone, 'transient');
182
+ const originalLstat = fs.lstatSync;
183
+ fs.lstatSync = (candidate, ...args) => {
184
+ if (candidate === gone) {
185
+ const error = new Error('gone');
186
+ error.code = 'ENOENT';
187
+ throw error;
188
+ }
189
+ return originalLstat(candidate, ...args);
190
+ };
191
+ try {
192
+ const state = capture(source, { withBundle: false });
193
+ assert.equal(state.checkpoint.untracked.some((file) => file.path === 'gone-during-capture.md'), false);
194
+ } finally {
195
+ fs.lstatSync = originalLstat;
196
+ }
197
+ });
198
+
178
199
  // --- identity ------------------------------------------------------------------
179
200
 
180
201
  test('capture is deterministic: same state, same ids, twice', () => {