@dharma-ai-labs/agent-fabric 0.2.48 → 0.2.50

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 (199) hide show
  1. package/dist/index.d.ts +92 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +846 -180
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.test.js +25 -3
  6. package/dist/index.test.js.map +1 -1
  7. package/dist/repositoryCandidateSync.d.ts +55 -0
  8. package/dist/repositoryCandidateSync.d.ts.map +1 -0
  9. package/dist/repositoryCandidateSync.js +177 -0
  10. package/dist/repositoryCandidateSync.js.map +1 -0
  11. package/dist/repositoryCandidateSync.test.d.ts +2 -0
  12. package/dist/repositoryCandidateSync.test.d.ts.map +1 -0
  13. package/dist/repositoryCandidateSync.test.js +141 -0
  14. package/dist/repositoryCandidateSync.test.js.map +1 -0
  15. package/dist/repositoryInstalledKnowledge.d.ts +13 -0
  16. package/dist/repositoryInstalledKnowledge.d.ts.map +1 -0
  17. package/dist/repositoryInstalledKnowledge.js +25 -0
  18. package/dist/repositoryInstalledKnowledge.js.map +1 -0
  19. package/dist/repositoryInstallerFiles.d.ts +6 -0
  20. package/dist/repositoryInstallerFiles.d.ts.map +1 -0
  21. package/dist/repositoryInstallerFiles.js +113 -0
  22. package/dist/repositoryInstallerFiles.js.map +1 -0
  23. package/dist/repositoryKnowledge.d.ts +119 -0
  24. package/dist/repositoryKnowledge.d.ts.map +1 -0
  25. package/dist/repositoryKnowledge.js +462 -0
  26. package/dist/repositoryKnowledge.js.map +1 -0
  27. package/dist/repositoryKnowledge.test.d.ts +2 -0
  28. package/dist/repositoryKnowledge.test.d.ts.map +1 -0
  29. package/dist/repositoryKnowledge.test.js +245 -0
  30. package/dist/repositoryKnowledge.test.js.map +1 -0
  31. package/dist/repositoryKnowledgeRetention.test.d.ts +2 -0
  32. package/dist/repositoryKnowledgeRetention.test.d.ts.map +1 -0
  33. package/dist/repositoryKnowledgeRetention.test.js +386 -0
  34. package/dist/repositoryKnowledgeRetention.test.js.map +1 -0
  35. package/dist/repositoryOnboardingCaller.test.d.ts +2 -0
  36. package/dist/repositoryOnboardingCaller.test.d.ts.map +1 -0
  37. package/dist/repositoryOnboardingCaller.test.js +197 -0
  38. package/dist/repositoryOnboardingCaller.test.js.map +1 -0
  39. package/dist/repositoryOnboardingFlow.test.d.ts +2 -0
  40. package/dist/repositoryOnboardingFlow.test.d.ts.map +1 -0
  41. package/dist/repositoryOnboardingFlow.test.js +47 -0
  42. package/dist/repositoryOnboardingFlow.test.js.map +1 -0
  43. package/dist/repositoryPackage.d.ts +115 -0
  44. package/dist/repositoryPackage.d.ts.map +1 -0
  45. package/dist/repositoryPackage.js +1095 -0
  46. package/dist/repositoryPackage.js.map +1 -0
  47. package/dist/repositoryPackage.test.d.ts +2 -0
  48. package/dist/repositoryPackage.test.d.ts.map +1 -0
  49. package/dist/repositoryPackage.test.js +709 -0
  50. package/dist/repositoryPackage.test.js.map +1 -0
  51. package/dist/repositoryPackageDelivery.d.ts +29 -0
  52. package/dist/repositoryPackageDelivery.d.ts.map +1 -0
  53. package/dist/repositoryPackageDelivery.js +140 -0
  54. package/dist/repositoryPackageDelivery.js.map +1 -0
  55. package/dist/repositoryPackageDelivery.test.d.ts +2 -0
  56. package/dist/repositoryPackageDelivery.test.d.ts.map +1 -0
  57. package/dist/repositoryPackageDelivery.test.js +787 -0
  58. package/dist/repositoryPackageDelivery.test.js.map +1 -0
  59. package/dist/repositoryPackageDeliveryCaller.test.d.ts +2 -0
  60. package/dist/repositoryPackageDeliveryCaller.test.d.ts.map +1 -0
  61. package/dist/repositoryPackageDeliveryCaller.test.js +18 -0
  62. package/dist/repositoryPackageDeliveryCaller.test.js.map +1 -0
  63. package/dist/repositoryPackageDeliveryPreflight.test.d.ts +2 -0
  64. package/dist/repositoryPackageDeliveryPreflight.test.d.ts.map +1 -0
  65. package/dist/repositoryPackageDeliveryPreflight.test.js +29 -0
  66. package/dist/repositoryPackageDeliveryPreflight.test.js.map +1 -0
  67. package/dist/repositoryPackageTransfer.d.ts +85 -0
  68. package/dist/repositoryPackageTransfer.d.ts.map +1 -0
  69. package/dist/repositoryPackageTransfer.js +233 -0
  70. package/dist/repositoryPackageTransfer.js.map +1 -0
  71. package/dist/repositoryPackageTransfer.test.d.ts +2 -0
  72. package/dist/repositoryPackageTransfer.test.d.ts.map +1 -0
  73. package/dist/repositoryPackageTransfer.test.js +173 -0
  74. package/dist/repositoryPackageTransfer.test.js.map +1 -0
  75. package/dist/repositoryPackageTransferV2.test.d.ts +2 -0
  76. package/dist/repositoryPackageTransferV2.test.d.ts.map +1 -0
  77. package/dist/repositoryPackageTransferV2.test.js +549 -0
  78. package/dist/repositoryPackageTransferV2.test.js.map +1 -0
  79. package/dist/repositoryPackageTransferV2Schema.test.d.ts +2 -0
  80. package/dist/repositoryPackageTransferV2Schema.test.d.ts.map +1 -0
  81. package/dist/repositoryPackageTransferV2Schema.test.js +48 -0
  82. package/dist/repositoryPackageTransferV2Schema.test.js.map +1 -0
  83. package/dist/repositoryReleaseMetadata.d.ts +37 -0
  84. package/dist/repositoryReleaseMetadata.d.ts.map +1 -0
  85. package/dist/repositoryReleaseMetadata.js +251 -0
  86. package/dist/repositoryReleaseMetadata.js.map +1 -0
  87. package/dist/repositoryReleaseMetadata.test.d.ts +2 -0
  88. package/dist/repositoryReleaseMetadata.test.d.ts.map +1 -0
  89. package/dist/repositoryReleaseMetadata.test.js +297 -0
  90. package/dist/repositoryReleaseMetadata.test.js.map +1 -0
  91. package/dist/repositoryRoleDerivation.d.ts +11 -0
  92. package/dist/repositoryRoleDerivation.d.ts.map +1 -0
  93. package/dist/repositoryRoleDerivation.js +59 -0
  94. package/dist/repositoryRoleDerivation.js.map +1 -0
  95. package/dist/repositoryRoleDerivation.test.d.ts +2 -0
  96. package/dist/repositoryRoleDerivation.test.d.ts.map +1 -0
  97. package/dist/repositoryRoleDerivation.test.js +33 -0
  98. package/dist/repositoryRoleDerivation.test.js.map +1 -0
  99. package/dist/repositoryRoleMetadata.d.ts +63 -0
  100. package/dist/repositoryRoleMetadata.d.ts.map +1 -0
  101. package/dist/repositoryRoleMetadata.js +203 -0
  102. package/dist/repositoryRoleMetadata.js.map +1 -0
  103. package/dist/repositoryRoleMetadata.test.d.ts +2 -0
  104. package/dist/repositoryRoleMetadata.test.d.ts.map +1 -0
  105. package/dist/repositoryRoleMetadata.test.js +296 -0
  106. package/dist/repositoryRoleMetadata.test.js.map +1 -0
  107. package/dist/repositoryRoleQuestion.d.ts +36 -0
  108. package/dist/repositoryRoleQuestion.d.ts.map +1 -0
  109. package/dist/repositoryRoleQuestion.js +68 -0
  110. package/dist/repositoryRoleQuestion.js.map +1 -0
  111. package/dist/repositoryRoleQuestion.test.d.ts +2 -0
  112. package/dist/repositoryRoleQuestion.test.d.ts.map +1 -0
  113. package/dist/repositoryRoleQuestion.test.js +66 -0
  114. package/dist/repositoryRoleQuestion.test.js.map +1 -0
  115. package/dist/repositorySourceAuthorization.d.ts +43 -0
  116. package/dist/repositorySourceAuthorization.d.ts.map +1 -0
  117. package/dist/repositorySourceAuthorization.js +164 -0
  118. package/dist/repositorySourceAuthorization.js.map +1 -0
  119. package/dist/repositorySourceCapture.test.d.ts +2 -0
  120. package/dist/repositorySourceCapture.test.d.ts.map +1 -0
  121. package/dist/repositorySourceCapture.test.js +303 -0
  122. package/dist/repositorySourceCapture.test.js.map +1 -0
  123. package/dist/repositorySourceSync.d.ts +54 -0
  124. package/dist/repositorySourceSync.d.ts.map +1 -0
  125. package/dist/repositorySourceSync.js +88 -0
  126. package/dist/repositorySourceSync.js.map +1 -0
  127. package/dist/repositorySourceSync.test.d.ts +2 -0
  128. package/dist/repositorySourceSync.test.d.ts.map +1 -0
  129. package/dist/repositorySourceSync.test.js +218 -0
  130. package/dist/repositorySourceSync.test.js.map +1 -0
  131. package/dist/schemas/repository-knowledge-retention-v1.schema.json +26 -0
  132. package/dist/schemas/repository-knowledge-v2.schema.json +74 -0
  133. package/dist/schemas/repository-knowledge.schema.json +63 -0
  134. package/dist/schemas/repository-package-chunk-v2.schema.json +15 -0
  135. package/dist/schemas/repository-package-chunk.schema.json +18 -0
  136. package/dist/schemas/repository-package-delivery-v1.schema.json +51 -0
  137. package/dist/schemas/repository-package-envelope-v1.schema.json +16 -0
  138. package/dist/schemas/repository-package-transfer-v2.schema.json +36 -0
  139. package/dist/schemas/repository-package-transfer.schema.json +34 -0
  140. package/dist/schemas/repository-package-v2.schema.json +491 -0
  141. package/dist/schemas/repository-package-v3.schema.json +36 -0
  142. package/dist/schemas/repository-package.schema.json +94 -0
  143. package/dist/schemas/repository-release-manifest.schema.json +34 -0
  144. package/dist/schemas/repository-role-metadata.schema.json +82 -0
  145. package/dist/schemas/skill-preparation-pointer-v1.schema.json +28 -0
  146. package/dist/schemas/skill-preparation-v1.schema.json +32 -0
  147. package/dist/skillPollResponse.test.d.ts +2 -0
  148. package/dist/skillPollResponse.test.d.ts.map +1 -0
  149. package/dist/skillPollResponse.test.js +37 -0
  150. package/dist/skillPollResponse.test.js.map +1 -0
  151. package/dist/skillPreparation.test.d.ts +2 -0
  152. package/dist/skillPreparation.test.d.ts.map +1 -0
  153. package/dist/skillPreparation.test.js +163 -0
  154. package/dist/skillPreparation.test.js.map +1 -0
  155. package/dist/skillPreparationCache.d.ts +34 -0
  156. package/dist/skillPreparationCache.d.ts.map +1 -0
  157. package/dist/skillPreparationCache.js +371 -0
  158. package/dist/skillPreparationCache.js.map +1 -0
  159. package/dist/skillPreparationCache.test.d.ts +2 -0
  160. package/dist/skillPreparationCache.test.d.ts.map +1 -0
  161. package/dist/skillPreparationCache.test.js +316 -0
  162. package/dist/skillPreparationCache.test.js.map +1 -0
  163. package/dist/skillPreparationCaller.test.d.ts +2 -0
  164. package/dist/skillPreparationCaller.test.d.ts.map +1 -0
  165. package/dist/skillPreparationCaller.test.js +32 -0
  166. package/dist/skillPreparationCaller.test.js.map +1 -0
  167. package/dist/skillPreparationPump.d.ts +11 -0
  168. package/dist/skillPreparationPump.d.ts.map +1 -0
  169. package/dist/skillPreparationPump.js +41 -0
  170. package/dist/skillPreparationPump.js.map +1 -0
  171. package/dist/skillPreparationPump.test.d.ts +2 -0
  172. package/dist/skillPreparationPump.test.d.ts.map +1 -0
  173. package/dist/skillPreparationPump.test.js +87 -0
  174. package/dist/skillPreparationPump.test.js.map +1 -0
  175. package/dist/skillPreparationRecord.d.ts +2 -0
  176. package/dist/skillPreparationRecord.d.ts.map +1 -0
  177. package/dist/skillPreparationRecord.js +36 -0
  178. package/dist/skillPreparationRecord.js.map +1 -0
  179. package/dist/skillPreparationTransaction.d.ts +13 -0
  180. package/dist/skillPreparationTransaction.d.ts.map +1 -0
  181. package/dist/skillPreparationTransaction.js +236 -0
  182. package/dist/skillPreparationTransaction.js.map +1 -0
  183. package/dist/skillPreparationTransaction.test.d.ts +2 -0
  184. package/dist/skillPreparationTransaction.test.d.ts.map +1 -0
  185. package/dist/skillPreparationTransaction.test.js +181 -0
  186. package/dist/skillPreparationTransaction.test.js.map +1 -0
  187. package/dist/skillSyncPolicy.test.d.ts +2 -0
  188. package/dist/skillSyncPolicy.test.d.ts.map +1 -0
  189. package/dist/skillSyncPolicy.test.js +156 -0
  190. package/dist/skillSyncPolicy.test.js.map +1 -0
  191. package/dist/usage.d.ts +1 -1
  192. package/dist/usage.d.ts.map +1 -1
  193. package/dist/usage.js +1 -1
  194. package/dist/usage.js.map +1 -1
  195. package/dist/windowsProcessPortability.test.d.ts +2 -0
  196. package/dist/windowsProcessPortability.test.d.ts.map +1 -0
  197. package/dist/windowsProcessPortability.test.js +14 -0
  198. package/dist/windowsProcessPortability.test.js.map +1 -0
  199. package/package.json +7 -4
@@ -0,0 +1,709 @@
1
+ import assert from 'node:assert/strict';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { createHash } from 'node:crypto';
4
+ import { link, mkdir, mkdtemp, readFile, readdir, rename, symlink, unlink, writeFile } from 'node:fs/promises';
5
+ import { tmpdir } from 'node:os';
6
+ import { dirname, resolve } from 'node:path';
7
+ import test from 'node:test';
8
+ import { Ajv2020 } from 'ajv/dist/2020.js';
9
+ import { inventoryRepositoryPackage, serializeRepositoryPackageSnapshot, writeRepositoryPackageSnapshot, } from './repositoryPackage.js';
10
+ import { installRepositoryAgentFabricSkill, run } from './index.js';
11
+ import { canonicalize } from '@dharma-ai-labs/agent-fabric-contracts';
12
+ async function fixture() {
13
+ const workspace = await mkdtemp(resolve(tmpdir(), 'repository-package-'));
14
+ const put = async (path, text) => {
15
+ await mkdir(dirname(resolve(workspace, path)), { recursive: true });
16
+ await writeFile(resolve(workspace, path), text);
17
+ };
18
+ return { workspace, put, organizationId: 'org_fixture', workspaceId: 'workspace_fixture' };
19
+ }
20
+ async function treeBytes(root, path = '') {
21
+ const files = [];
22
+ for (const entry of (await readdir(resolve(root, path), { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
23
+ const child = path ? `${path}/${entry.name}` : entry.name;
24
+ if (entry.isDirectory())
25
+ files.push(...await treeBytes(root, child));
26
+ else
27
+ files.push({ path: child, content: (await readFile(resolve(root, child))).toString('base64') });
28
+ }
29
+ return files;
30
+ }
31
+ test('collecting a candidate preserves every byte and path of an installed shared release', async () => {
32
+ const f = await fixture();
33
+ await f.put('.codex/skills/review/SKILL.md', '# Changed source skill');
34
+ const snapshot = await inventoryRepositoryPackage(f);
35
+ const root = '.agents/skills/dharma-agent-fabric';
36
+ await f.put(`${root}/SKILL.md`, '# Accepted signed skill');
37
+ await f.put(`${root}/.dharma-agent-fabric.json`, JSON.stringify({ managedBy: 'dharma-agent-fabric' }));
38
+ await f.put(`${root}/MANIFEST.json`, JSON.stringify({ schema: 'dharma.repository-release-manifest/v1', sourceSnapshotHash: `sha256:${'a'.repeat(64)}` }));
39
+ await f.put(`${root}/knowledge/CATALOG.json`, '{"schema":"dharma.repository-knowledge/v2","concepts":[{"definition":"Retained knowledge"}]}');
40
+ await f.put(`${root}/skills/source/.codex/skills/review/SKILL.md`, '# Accepted source copy');
41
+ await f.put(`${root}/COPY-JOURNAL.json`, '{"interrupted":"must not replay against signed release"}');
42
+ const before = await treeBytes(resolve(f.workspace, root));
43
+ const collected = await writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot });
44
+ assert.equal(collected.authority, 'local_inventory_not_signed');
45
+ assert.equal(collected.managedCopiesPath, null);
46
+ assert.ok(collected.manifestPath.startsWith('.dharma/repository-source/snapshots/'));
47
+ assert.ok(collected.snapshotPath.startsWith('.dharma/repository-source/snapshots/'));
48
+ assert.deepEqual(await treeBytes(resolve(f.workspace, root)), before);
49
+ assert.equal(await readFile(resolve(f.workspace, collected.snapshotPath), 'utf8'), serializeRepositoryPackageSnapshot(snapshot));
50
+ assert.deepEqual(JSON.parse(await readFile(resolve(f.workspace, collected.manifestPath), 'utf8')), snapshot.manifest);
51
+ assert.deepEqual(await writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot }), collected);
52
+ assert.deepEqual(await treeBytes(resolve(f.workspace, root)), before);
53
+ });
54
+ test('candidate-only collection never installs or replaces the bootstrap skill inventory', async () => {
55
+ const f = await fixture();
56
+ await f.put('skills/review/SKILL.md', '# Original');
57
+ const initial = await inventoryRepositoryPackage(f);
58
+ await writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot: initial });
59
+ const root = '.agents/skills/dharma-agent-fabric';
60
+ const before = await treeBytes(resolve(f.workspace, root));
61
+ await f.put('skills/review/SKILL.md', '# Changed');
62
+ const snapshot = await inventoryRepositoryPackage(f);
63
+ const collected = await writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot, candidateOnly: true });
64
+ assert.equal(collected.managedCopiesPath, null);
65
+ assert.ok(collected.snapshotPath.startsWith('.dharma/repository-source/snapshots/'));
66
+ assert.deepEqual(await treeBytes(resolve(f.workspace, root)), before);
67
+ });
68
+ test('candidate-only collection creates no active skill tree and rejects corrupt immutable sidecars', async () => {
69
+ const f = await fixture();
70
+ await f.put('skills/review/SKILL.md', '# Review');
71
+ const snapshot = await inventoryRepositoryPackage(f);
72
+ const input = { workspace: f.workspace, snapshot, candidateOnly: true };
73
+ const collected = await writeRepositoryPackageSnapshot(input);
74
+ assert.equal(collected.disposition, 'candidate_only');
75
+ await assert.rejects(readdir(resolve(f.workspace, '.agents')), /ENOENT/);
76
+ await writeFile(resolve(f.workspace, collected.manifestPath), '{}');
77
+ await assert.rejects(writeRepositoryPackageSnapshot(input), /CAS conflict/);
78
+ assert.equal(await readFile(resolve(f.workspace, collected.manifestPath), 'utf8'), '{}');
79
+ await assert.rejects(readdir(resolve(f.workspace, '.agents')), /ENOENT/);
80
+ });
81
+ test('candidate storage rejects symlinked source-state ancestors without touching their targets', async () => {
82
+ const f = await fixture();
83
+ const other = await fixture();
84
+ await f.put('skills/review/SKILL.md', '# Review');
85
+ const snapshot = await inventoryRepositoryPackage(f);
86
+ await symlink(other.workspace, resolve(f.workspace, '.dharma'), 'dir');
87
+ await assert.rejects(writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot, candidateOnly: true }), /symlink/);
88
+ assert.deepEqual(await readdir(other.workspace), []);
89
+ });
90
+ test('candidate-only collection does not replay an interrupted local copy transaction', async () => {
91
+ const f = await fixture();
92
+ await f.put('skills/review/SKILL.md', '# Original');
93
+ await writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot: await inventoryRepositoryPackage(f) });
94
+ await f.put('skills/review/SKILL.md', '# Intermediate');
95
+ const interrupted = await inventoryRepositoryPackage(f);
96
+ await assert.rejects(writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot: interrupted,
97
+ onCopyCheckpoint(point) { if (point === 'file_backed_up')
98
+ throw new Error('interrupted'); } }), /interrupted/);
99
+ const root = '.agents/skills/dharma-agent-fabric';
100
+ const before = await treeBytes(resolve(f.workspace, root));
101
+ await f.put('skills/review/SKILL.md', '# Latest');
102
+ const latest = await inventoryRepositoryPackage(f);
103
+ const collected = await writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot: latest, candidateOnly: true });
104
+ assert.equal(collected.disposition, 'candidate_only');
105
+ assert.deepEqual(await treeBytes(resolve(f.workspace, root)), before);
106
+ await writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot: latest });
107
+ assert.equal(await readFile(resolve(f.workspace, root, 'skills/source/skills/review/SKILL.md'), 'utf8'), '# Latest');
108
+ });
109
+ test('legacy in-skill snapshot journals remain recoverable without copying new CAS objects into the skill', async () => {
110
+ const f = await fixture();
111
+ await f.put('skills/review/SKILL.md', '# Original');
112
+ const initial = await inventoryRepositoryPackage(f);
113
+ const collected = await writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot: initial });
114
+ const root = '.agents/skills/dharma-agent-fabric';
115
+ await f.put(`${root}/snapshots/${initial.manifest.snapshotHash.slice(7)}.json`, serializeRepositoryPackageSnapshot(initial));
116
+ await unlink(resolve(f.workspace, collected.snapshotPath));
117
+ await f.put('skills/review/SKILL.md', '# Changed');
118
+ const next = await inventoryRepositoryPackage(f);
119
+ await writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot: next });
120
+ assert.deepEqual(await readdir(resolve(f.workspace, root, 'snapshots')), [`${initial.manifest.snapshotHash.slice(7)}.json`]);
121
+ assert.equal(await readFile(resolve(f.workspace, root, 'skills/source/skills/review/SKILL.md'), 'utf8'), '# Changed');
122
+ });
123
+ test('inventories all provider roots and companions without claiming runtime use', async () => {
124
+ const f = await fixture();
125
+ for (const root of ['.agents/skills', '.claude/skills', '.codex/skills', 'skills']) {
126
+ await f.put(`${root}/review/SKILL.md`, '# Review\n[Procedure](references/procedure.md)');
127
+ await f.put(`${root}/review/references/procedure.md`, 'Use visible evidence.');
128
+ }
129
+ const result = await inventoryRepositoryPackage(f);
130
+ assert.equal(result.manifest.skills.length, 4);
131
+ assert.equal(result.manifest.files.length, 8);
132
+ assert.ok(result.manifest.skills.every(skill => skill.availability === 'available' && skill.observation.state === 'not_observed'));
133
+ assert.ok(!serializeRepositoryPackageSnapshot(result).includes(f.workspace));
134
+ });
135
+ test('stable CAS serialization deduplicates blobs and excludes capture time and generated roots', async () => {
136
+ const f = await fixture();
137
+ await f.put('skills/review/SKILL.md', '# Review');
138
+ await f.put('skills/review/references/a.md', 'Same content');
139
+ await f.put('skills/review/references/b.md', 'Same content');
140
+ const first = await inventoryRepositoryPackage({ ...f, now: new Date('2026-09-17T00:00:00Z') });
141
+ await writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot: first });
142
+ const second = await inventoryRepositoryPackage({ ...f, now: new Date('2026-09-18T00:00:00Z') });
143
+ assert.equal(first.manifest.snapshotHash, second.manifest.snapshotHash);
144
+ assert.equal(serializeRepositoryPackageSnapshot(first), serializeRepositoryPackageSnapshot(second));
145
+ assert.equal(first.blobs.length, 2);
146
+ await writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot: second });
147
+ await f.put('skills/review/SKILL.md', '# Changed');
148
+ assert.notEqual((await inventoryRepositoryPackage(f)).manifest.snapshotHash, first.manifest.snapshotHash);
149
+ });
150
+ test('follows relative dependencies once, including cycles, and snapshots only approved outputs', async () => {
151
+ const f = await fixture();
152
+ await f.put('skills/review/SKILL.md', '[Guide](../../docs/a.md)\n[Missing](references/missing.md)');
153
+ await f.put('docs/a.md', '[Loop](../skills/review/SKILL.md)');
154
+ await f.put('output/approved.json', '{"status":"passed"}');
155
+ await f.put('output/unapproved.md', 'Must not collect');
156
+ const snapshot = await inventoryRepositoryPackage({ ...f, approvedOutputs: ['output/approved.json'] });
157
+ assert.deepEqual(snapshot.manifest.files.map(file => file.path), ['docs/a.md', 'output/approved.json', 'skills/review/SKILL.md']);
158
+ assert.equal(snapshot.manifest.skills[0]?.availability, 'partial');
159
+ assert.ok(snapshot.manifest.exclusions.some(item => item.reason === 'missing_dependency'));
160
+ });
161
+ test('never reads symlinked roots, companions, or dependency escapes', async () => {
162
+ const f = await fixture();
163
+ const outside = await fixture();
164
+ await outside.put('secret.md', 'Outside private data');
165
+ await f.put('skills/review/SKILL.md', '[Outside](../../../secret.md)\n[Link](references/link.md)');
166
+ await mkdir(resolve(f.workspace, 'skills/review/references'));
167
+ await symlink(resolve(outside.workspace, 'secret.md'), resolve(f.workspace, 'skills/review/references/link.md'));
168
+ await symlink(outside.workspace, resolve(f.workspace, '.claude'));
169
+ const snapshot = await inventoryRepositoryPackage(f);
170
+ assert.ok(snapshot.manifest.exclusions.some(item => item.reason === 'symlink'));
171
+ assert.ok(!serializeRepositoryPackageSnapshot(snapshot).includes('Outside private data'));
172
+ await assert.rejects(inventoryRepositoryPackage({ ...f, approvedOutputs: ['../secret.md'] }));
173
+ await assert.rejects(inventoryRepositoryPackage({ ...f, approvedOutputs: ['C:\\private\\secret.md'] }));
174
+ });
175
+ test('excludes environment, credentials, binary content and secrets rather than shipping redacted source', async () => {
176
+ const f = await fixture();
177
+ await f.put('skills/review/SKILL.md', '# Safe');
178
+ await f.put('skills/review/.env.production', 'PRIVATE_VALUE=must-not-appear');
179
+ await f.put('skills/review/credential.json', '{"password":"must-not-appear"}');
180
+ await f.put('skills/review/.envrc', 'PRIVATE_VALUE=must-not-appear');
181
+ await f.put('skills/review/.npmrc', '//registry.npmjs.org/:_authToken=must-not-appear');
182
+ await f.put('skills/review/references/short.yaml', "'password': 'p'\n");
183
+ await f.put('skills/review/references/private.md', '-----BEGIN PRIVATE KEY-----\nprivate-value');
184
+ await f.put('skills/review/references/token.json', '{"access_token":"abcdefgh123456789"}');
185
+ await f.put('skills/review/references/binary.txt', '\u0000binary');
186
+ await f.put('skills/review/references/path.md', 'Private file /home/customer/private/project.md');
187
+ await f.put(`skills/review/references/ghp_${'a'.repeat(30)}.md`, 'Filename must not leak');
188
+ const snapshot = await inventoryRepositoryPackage(f);
189
+ const serialized = serializeRepositoryPackageSnapshot(snapshot);
190
+ assert.equal(snapshot.manifest.files.length, 1);
191
+ assert.ok(!serialized.includes('must-not-appear'));
192
+ assert.ok(!snapshot.blobs.some(blob => Buffer.from(blob.contentBase64, 'base64').toString().includes('private-value')));
193
+ assert.equal(snapshot.manifest.skills[0]?.availability, 'partial');
194
+ assert.ok(!serialized.includes(`ghp_${'a'.repeat(30)}`));
195
+ });
196
+ test('observations require the exact skill and an approved immutable source snapshot', async () => {
197
+ const f = await fixture();
198
+ await f.put('skills/review/SKILL.md', '# Safe');
199
+ await f.put('output/receipt.json', '{"used":true}');
200
+ const approvedOutputs = ['output/receipt.json'];
201
+ const base = await inventoryRepositoryPackage({ ...f, approvedOutputs });
202
+ const observation = { skillPath: 'skills/review', skillHash: base.manifest.skills[0].contentHash,
203
+ sourcePath: 'output/receipt.json', sourceHash: base.manifest.files.find(file => file.role === 'approved_output').sha256 };
204
+ const observed = await inventoryRepositoryPackage({ ...f, approvedOutputs, observations: [observation] });
205
+ assert.equal(observed.manifest.skills[0]?.observation.state, 'reported_observed');
206
+ assert.equal(observed.manifest.skills[0]?.observation.authority, 'caller_supplied_not_runtime_verified');
207
+ await assert.rejects(inventoryRepositoryPackage({ ...f, observations: [observation] }));
208
+ await assert.rejects(inventoryRepositoryPackage({ ...f, approvedOutputs, observations: [{ ...observation, skillHash: `sha256:${'0'.repeat(64)}` }] }));
209
+ });
210
+ test('bounds entry, byte and dependency traversal without silently truncating inventory', async () => {
211
+ const f = await fixture();
212
+ await f.put('skills/review/SKILL.md', '[A](../../docs/a.md)');
213
+ await f.put('docs/a.md', '[B](b.md)');
214
+ await f.put('docs/b.md', '# B');
215
+ await assert.rejects(inventoryRepositoryPackage({ ...f, limits: { maximumEntries: 1 } }), /limit/);
216
+ await assert.rejects(inventoryRepositoryPackage({ ...f, limits: { maximumFileBytes: 4 } }), /limit/);
217
+ await assert.rejects(inventoryRepositoryPackage({ ...f, limits: { maximumDependencies: 1 } }), /limit/);
218
+ });
219
+ test('schema validates the manifest and rejects unbounded fields and false observation states', async () => {
220
+ const f = await fixture();
221
+ await f.put('skills/review/SKILL.md', '# Safe');
222
+ const { manifest } = await inventoryRepositoryPackage(f);
223
+ const schema = JSON.parse(await readFile(new URL('../../../schemas/repository-package.schema.json', import.meta.url), 'utf8'));
224
+ const ajv = new Ajv2020({ strict: true });
225
+ const validate = ajv.compile(schema);
226
+ assert.equal(validate(manifest), true, JSON.stringify(validate.errors));
227
+ assert.equal(validate({ ...manifest, secret: 'no' }), false);
228
+ assert.equal(validate({ ...manifest, skills: [{ ...manifest.skills[0], observation: { state: 'observed' } }] }), false);
229
+ });
230
+ test('installer integrates MANIFEST and local CAS without collecting its generated outputs', async () => {
231
+ const f = await fixture();
232
+ await f.put('.codex/skills/review/SKILL.md', '# Safe');
233
+ const installed = await installRepositoryAgentFabricSkill({ ...f, hqUrl: 'https://example.invalid', policyRevision: '1' });
234
+ const manifest = JSON.parse(await readFile(resolve(f.workspace, installed.repositoryPackage.manifestPath), 'utf8'));
235
+ assert.equal(manifest.skills.length, 1);
236
+ assert.equal(manifest.skills[0].path, '.codex/skills/review');
237
+ const refreshed = await inventoryRepositoryPackage(f);
238
+ assert.equal(refreshed.manifest.snapshotHash, manifest.snapshotHash);
239
+ });
240
+ for (const kind of ['symlink', 'hardlink']) {
241
+ test(`installer refuses linked generated leaves (${kind}) before changing original source bytes`, async () => {
242
+ const f = await fixture();
243
+ const original = '.codex/skills/customer/SKILL.md';
244
+ const bytes = '# Original customer skill\nPreserve this exact content.\n';
245
+ await f.put(original, bytes);
246
+ await f.put('.agents/skills/dharma-agent-fabric/.dharma-agent-fabric.json', JSON.stringify({ managedBy: 'dharma-agent-fabric', workspaceId: f.workspaceId }));
247
+ const target = resolve(f.workspace, '.agents/skills/dharma-agent-fabric/SKILL.md');
248
+ if (kind === 'symlink')
249
+ await symlink(resolve(f.workspace, original), target);
250
+ else
251
+ await link(resolve(f.workspace, original), target);
252
+ let rejected = false;
253
+ try {
254
+ await installRepositoryAgentFabricSkill({ ...f, hqUrl: 'https://example.invalid', policyRevision: '1' });
255
+ }
256
+ catch {
257
+ rejected = true;
258
+ }
259
+ assert.equal(await readFile(resolve(f.workspace, original), 'utf8'), bytes, kind);
260
+ assert.equal(rejected, true, kind);
261
+ });
262
+ }
263
+ test('installer rejects a linked managed root before writing outside the work repository', async () => {
264
+ const f = await fixture(), outside = await fixture();
265
+ const bytes = '# Outside source\nNever overwrite.\n';
266
+ await outside.put('.dharma-agent-fabric.json', JSON.stringify({ managedBy: 'dharma-agent-fabric', workspaceId: f.workspaceId }));
267
+ await outside.put('SKILL.md', bytes);
268
+ await mkdir(resolve(f.workspace, '.agents/skills'), { recursive: true });
269
+ await symlink(outside.workspace, resolve(f.workspace, '.agents/skills/dharma-agent-fabric'), 'dir');
270
+ let rejected = false;
271
+ try {
272
+ await installRepositoryAgentFabricSkill({ ...f, hqUrl: 'https://example.invalid', policyRevision: '1' });
273
+ }
274
+ catch {
275
+ rejected = true;
276
+ }
277
+ assert.equal(await readFile(resolve(outside.workspace, 'SKILL.md'), 'utf8'), bytes);
278
+ await assert.rejects(readFile(resolve(outside.workspace, 'references/organization.md')));
279
+ assert.equal(rejected, true);
280
+ });
281
+ test('installer rejects invalid ownership markers before touching generated files', async () => {
282
+ const f = await fixture();
283
+ await f.put('.agents/skills/dharma-agent-fabric/.dharma-agent-fabric.json', '{}');
284
+ await f.put('.agents/skills/dharma-agent-fabric/SKILL.md', '# Preserve unmanaged bytes');
285
+ let rejected = false;
286
+ try {
287
+ await installRepositoryAgentFabricSkill({ ...f, hqUrl: 'https://example.invalid', policyRevision: '1' });
288
+ }
289
+ catch {
290
+ rejected = true;
291
+ }
292
+ assert.equal(await readFile(resolve(f.workspace, '.agents/skills/dharma-agent-fabric/SKILL.md'), 'utf8'), '# Preserve unmanaged bytes');
293
+ assert.equal(rejected, true);
294
+ });
295
+ test('installer preflights connection leaves before modifying the managed skill', async () => {
296
+ const f = await fixture(), outside = await fixture();
297
+ await f.put('.agents/skills/dharma-agent-fabric/.dharma-agent-fabric.json', JSON.stringify({ managedBy: 'dharma-agent-fabric', workspaceId: f.workspaceId }));
298
+ await f.put('.agents/skills/dharma-agent-fabric/SKILL.md', '# Existing managed skill');
299
+ await outside.put('connection.json', '{"preserve":true}');
300
+ await mkdir(resolve(f.workspace, '.dharma'), { recursive: true });
301
+ await symlink(resolve(outside.workspace, 'connection.json'), resolve(f.workspace, '.dharma/agent-fabric.json'));
302
+ await assert.rejects(installRepositoryAgentFabricSkill({ ...f, hqUrl: 'https://example.invalid', policyRevision: '1' }), /symlink/);
303
+ assert.equal(await readFile(resolve(f.workspace, '.agents/skills/dharma-agent-fabric/SKILL.md'), 'utf8'), '# Existing managed skill');
304
+ assert.equal(await readFile(resolve(outside.workspace, 'connection.json'), 'utf8'), '{"preserve":true}');
305
+ });
306
+ test('snapshot command defaults to dry-run without enrollment, writes or network', async () => {
307
+ const f = await fixture();
308
+ await f.put('skills/review/SKILL.md', '# Safe');
309
+ const result = await run(['repositories', 'snapshot', '--workspace', f.workspace, '--organization-id', f.organizationId,
310
+ '--workspace-id', f.workspaceId, '--dry-run']);
311
+ assert.equal(result.dryRun, true);
312
+ assert.equal(result.localMutation, false);
313
+ assert.equal(result.serverMutation, false);
314
+ await assert.rejects(readFile(resolve(f.workspace, '.agents/skills/dharma-agent-fabric/MANIFEST.json')));
315
+ });
316
+ test('repository installer creates one unsigned catalog and includes exact catalog bytes in MANIFEST', async () => {
317
+ const f = await fixture();
318
+ const input = { ...f, hqUrl: 'https://example.invalid', policyRevision: '1', repositoryAgentId: 'repo_agent_fixture' };
319
+ const installed = await installRepositoryAgentFabricSkill(input);
320
+ assert.equal(installed.knowledge?.disposition, 'initialized');
321
+ const snapshot = await inventoryRepositoryPackage(input);
322
+ assert.equal(snapshot.manifest.knowledge?.authority, 'locally_initialized_unsigned');
323
+ assert.equal(snapshot.manifest.knowledge?.atlasAssociation, 'local_scope_only');
324
+ const file = snapshot.manifest.files.find(file => file.role === 'knowledge');
325
+ assert.ok(file);
326
+ const bytes = await readFile(resolve(f.workspace, file.path));
327
+ assert.equal(file.sha256, `sha256:${createHash('sha256').update(bytes).digest('hex')}`);
328
+ assert.equal(snapshot.blobs.find(blob => blob.sha256 === file.sha256)?.contentBase64, bytes.toString('base64'));
329
+ assert.doesNotThrow(() => serializeRepositoryPackageSnapshot(snapshot));
330
+ const repeated = await installRepositoryAgentFabricSkill(input);
331
+ assert.equal(repeated.knowledge?.disposition, 'reused');
332
+ assert.equal(repeated.knowledge?.catalog.knowledgeBaseId, installed.knowledge?.catalog.knowledgeBaseId);
333
+ assert.equal(repeated.repositoryPackage.snapshotHash, installed.repositoryPackage.snapshotHash);
334
+ await assert.rejects(inventoryRepositoryPackage({ ...input, repositoryAgentId: 'foreign_repo_agent' }), /scope mismatch/);
335
+ const connectionPath = resolve(f.workspace, '.dharma/agent-fabric.json');
336
+ const before = await readFile(connectionPath);
337
+ await assert.rejects(installRepositoryAgentFabricSkill({ ...input, organizationId: 'org_foreign' }), /scope mismatch/);
338
+ assert.deepEqual(await readFile(connectionPath), before);
339
+ });
340
+ test('generated knowledge path cannot be smuggled into ordinary output inventory', async () => {
341
+ const f = await fixture();
342
+ await installRepositoryAgentFabricSkill({ ...f, hqUrl: 'https://example.invalid', policyRevision: '1', repositoryAgentId: 'repo_agent_fixture' });
343
+ await assert.rejects(inventoryRepositoryPackage({ ...f,
344
+ approvedOutputs: ['.agents/skills/dharma-agent-fabric/knowledge/CATALOG.json'] }), /scope or limit/);
345
+ });
346
+ test('CAS serialization rejects a self-consistent snapshot with forged knowledge mapping', async () => {
347
+ const f = await fixture();
348
+ const input = { ...f, hqUrl: 'https://example.invalid', policyRevision: '1', repositoryAgentId: 'repo_agent_fixture' };
349
+ await installRepositoryAgentFabricSkill(input);
350
+ const snapshot = await inventoryRepositoryPackage(input);
351
+ snapshot.manifest.knowledge.knowledgeBaseId = `repository-kb:sha256:${'a'.repeat(64)}`;
352
+ const { snapshotHash: _hash, snapshotId: _id, ...base } = snapshot.manifest;
353
+ snapshot.manifest.snapshotHash = `sha256:${createHash('sha256').update(canonicalize(base)).digest('hex')}`;
354
+ snapshot.manifest.snapshotId = `repository-package-${snapshot.manifest.snapshotHash.slice(7)}`;
355
+ assert.throws(() => serializeRepositoryPackageSnapshot(snapshot), /knowledge mapping integrity/);
356
+ });
357
+ test('CAS persistence rejects tampered snapshots, corrupt existing objects and symlinked destinations', async () => {
358
+ const f = await fixture();
359
+ await f.put('skills/review/SKILL.md', '# Safe');
360
+ const snapshot = await inventoryRepositoryPackage(f);
361
+ await assert.rejects(writeRepositoryPackageSnapshot({ workspace: f.workspace,
362
+ snapshot: { ...snapshot, blobs: [{ ...snapshot.blobs[0], contentBase64: Buffer.from('tamper').toString('base64') }] } }));
363
+ const persisted = await writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot });
364
+ await writeFile(resolve(f.workspace, persisted.snapshotPath), 'corrupt');
365
+ await assert.rejects(writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot }), /conflict/);
366
+ const other = await fixture();
367
+ await other.put('skills/review/SKILL.md', '# Safe');
368
+ await mkdir(resolve(other.workspace, '.agents/skills'), { recursive: true });
369
+ await symlink(f.workspace, resolve(other.workspace, '.agents/skills/dharma-agent-fabric'));
370
+ await assert.rejects(writeRepositoryPackageSnapshot({ workspace: other.workspace, snapshot: await inventoryRepositoryPackage(other) }), /symlink/);
371
+ });
372
+ test('concurrent persistence publishes complete CAS objects and preserves unmanaged skills', async () => {
373
+ const f = await fixture();
374
+ await f.put('skills/review/SKILL.md', '# Safe');
375
+ const snapshot = await inventoryRepositoryPackage(f);
376
+ const results = await Promise.all([writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot }),
377
+ writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot })]);
378
+ assert.deepEqual(results[0], results[1]);
379
+ assert.equal(await readFile(resolve(f.workspace, results[0].snapshotPath), 'utf8'), serializeRepositoryPackageSnapshot(snapshot));
380
+ const unmanaged = await fixture();
381
+ await unmanaged.put('.agents/skills/dharma-agent-fabric/SKILL.md', '# User owned');
382
+ await assert.rejects(writeRepositoryPackageSnapshot({ workspace: unmanaged.workspace,
383
+ snapshot: await inventoryRepositoryPackage(unmanaged) }), /unmanaged/);
384
+ assert.equal(await readFile(resolve(unmanaged.workspace, '.agents/skills/dharma-agent-fabric/SKILL.md'), 'utf8'), '# User owned');
385
+ });
386
+ test('filesystem case semantics are preserved and Windows traversal and protected paths are rejected', async () => {
387
+ const f = await fixture();
388
+ await f.put('skills/Review/SKILL.md', '# Uppercase');
389
+ await f.put('skills/review/SKILL.md', '# Lowercase');
390
+ await f.put('skills/review/.ENV.local', 'password=must-not-appear');
391
+ const snapshot = await inventoryRepositoryPackage(f);
392
+ const directories = (await readdir(resolve(f.workspace, 'skills'))).sort();
393
+ assert.deepEqual(snapshot.manifest.skills.map(skill => skill.path), directories.map(path => `skills/${path}`));
394
+ assert.equal(directories.length, await readFile(resolve(f.workspace, 'skills/Review/SKILL.md'), 'utf8') === '# Uppercase' ? 2 : 1);
395
+ assert.ok(!snapshot.manifest.files.some(file => file.path.includes('.ENV')));
396
+ for (const path of ['skills\\review\\SKILL.md', '//outside/file.md', 'skills/../outside.md', '.env', 'skills/review/.ENV.local']) {
397
+ await assert.rejects(inventoryRepositoryPackage({ ...f, approvedOutputs: [path] }));
398
+ }
399
+ });
400
+ test('installer permits an operating-system ancestor alias but rejects an aliased workspace', async () => {
401
+ const f = await fixture();
402
+ const parent = await mkdtemp(resolve(tmpdir(), 'repository-alias-'));
403
+ const alias = resolve(parent, 'system-alias');
404
+ await symlink(dirname(f.workspace), alias, process.platform === 'win32' ? 'junction' : 'dir');
405
+ const workspace = resolve(alias, f.workspace.slice(dirname(f.workspace).length + 1));
406
+ await f.put('skills/review/SKILL.md', '# Safe');
407
+ const result = await installRepositoryAgentFabricSkill({ ...f, workspace, repositoryAgentId: 'repo_agent_fixture',
408
+ hqUrl: 'https://example.invalid', policyRevision: '1' });
409
+ assert.equal(JSON.parse(await readFile(resolve(workspace, result.repositoryPackage.manifestPath), 'utf8')).skills.length, 1);
410
+ assert.equal(result.knowledge?.disposition, 'initialized');
411
+ const linkedWorkspace = resolve(parent, 'workspace-alias');
412
+ await symlink(f.workspace, linkedWorkspace, process.platform === 'win32' ? 'junction' : 'dir');
413
+ await assert.rejects(installRepositoryAgentFabricSkill({ ...f, workspace: linkedWorkspace,
414
+ hqUrl: 'https://example.invalid', policyRevision: '1' }), /symlink/);
415
+ });
416
+ test('explicit snapshot apply refreshes only an installed managed root and rejects conflicting modes', async () => {
417
+ const f = await fixture();
418
+ await f.put('skills/review/SKILL.md', '# Safe');
419
+ const args = ['repositories', 'snapshot', '--workspace', f.workspace, '--organization-id', f.organizationId, '--workspace-id', f.workspaceId];
420
+ await assert.rejects(run([...args, '--apply']), /installed managed/);
421
+ await assert.rejects(run([...args, '--apply', '--dry-run']), /Choose/);
422
+ await installRepositoryAgentFabricSkill({ ...f, hqUrl: 'https://example.invalid', policyRevision: '1' });
423
+ await f.put('output/report.json', '{"checks":3}');
424
+ const result = await run([...args, '--approved-output', 'output/report.json', '--apply']);
425
+ assert.equal(result.localMutation, true);
426
+ assert.equal(result.serverMutation, false);
427
+ assert.ok(result.snapshot.manifest.files.some(file => file.path === 'output/report.json'));
428
+ });
429
+ test('opaque secret filenames still mark their owning skill partial without leaking names', async () => {
430
+ const f = await fixture();
431
+ await f.put('skills/review/SKILL.md', '# Safe');
432
+ const name = `ghp_${'b'.repeat(30)}`;
433
+ await f.put(`skills/review/${name}.md`, '# Companion');
434
+ const snapshot = await inventoryRepositoryPackage(f);
435
+ assert.equal(snapshot.manifest.skills[0]?.availability, 'partial');
436
+ assert.ok(!serializeRepositoryPackageSnapshot(snapshot).includes(name));
437
+ });
438
+ test('approving an existing skill source does not suppress its dependency graph', async () => {
439
+ const f = await fixture();
440
+ await f.put('skills/review/SKILL.md', '[Guide](../../docs/guide.md)');
441
+ await f.put('docs/guide.md', '# Guide');
442
+ const snapshot = await inventoryRepositoryPackage({ ...f, approvedOutputs: ['skills/review/SKILL.md'] });
443
+ assert.ok(snapshot.manifest.files.some(file => file.path === 'docs/guide.md'));
444
+ });
445
+ const managedRoot = '.agents/skills/dharma-agent-fabric';
446
+ test('managed copies preserve original skills, relative companions, and visible partial status', async () => {
447
+ const f = await fixture();
448
+ await f.put('.codex/skills/review/SKILL.md', '[Guide](references/guide.md)\n[Shared](../../docs/shared.md)\n[Missing](references/missing.md)');
449
+ await f.put('.codex/skills/review/references/guide.md', '# Guide');
450
+ await f.put('.codex/docs/shared.md', '# Shared');
451
+ await f.put('output/report.md', '# Approved report');
452
+ const snapshot = await inventoryRepositoryPackage({ ...f, approvedOutputs: ['output/report.md'] });
453
+ await writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot });
454
+ const skill = snapshot.manifest.skills[0];
455
+ assert.equal(skill.managedPath, 'skills/source/.codex/skills/review');
456
+ assert.equal(skill.availability, 'partial');
457
+ const file = snapshot.manifest.files.find(file => file.path === '.codex/skills/review/references/guide.md');
458
+ assert.equal(file.managedPath, 'skills/source/.codex/skills/review/references/guide.md');
459
+ assert.equal(await readFile(resolve(f.workspace, managedRoot, file.managedPath), 'utf8'), '# Guide');
460
+ assert.equal(await readFile(resolve(f.workspace, '.codex/skills/review/references/guide.md'), 'utf8'), '# Guide');
461
+ assert.equal(await readFile(resolve(f.workspace, managedRoot, 'skills/source/.codex/docs/shared.md'), 'utf8'), '# Shared');
462
+ await assert.rejects(readFile(resolve(f.workspace, managedRoot, 'skills/source/output/report.md')));
463
+ assert.equal((await inventoryRepositoryPackage({ ...f, approvedOutputs: ['output/report.md'] })).manifest.snapshotHash, snapshot.manifest.snapshotHash);
464
+ });
465
+ test('managed copies deterministically replace, rename and remove only owned copies', async () => {
466
+ const f = await fixture();
467
+ await f.put('skills/old/SKILL.md', '# Old');
468
+ await f.put('skills/old/references/keep.md', '# Keep');
469
+ await writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot: await inventoryRepositoryPackage(f) });
470
+ await f.put(`${managedRoot}/skills/source/skills/old/user.txt`, 'Unmanaged addition');
471
+ await rename(resolve(f.workspace, 'skills/old'), resolve(f.workspace, 'skills/new'));
472
+ await f.put('skills/new/SKILL.md', '# Updated');
473
+ const snapshot = await inventoryRepositoryPackage(f);
474
+ const result = await writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot });
475
+ assert.equal(await readFile(resolve(f.workspace, managedRoot, 'skills/source/skills/new/SKILL.md'), 'utf8'), '# Updated');
476
+ await assert.rejects(readFile(resolve(f.workspace, managedRoot, 'skills/source/skills/old/SKILL.md')));
477
+ assert.equal(await readFile(resolve(f.workspace, managedRoot, 'skills/source/skills/old/user.txt'), 'utf8'), 'Unmanaged addition');
478
+ await unlink(resolve(f.workspace, 'skills/new/references/keep.md'));
479
+ await writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot: await inventoryRepositoryPackage(f) });
480
+ await assert.rejects(readFile(resolve(f.workspace, managedRoot, 'skills/source/skills/new/references/keep.md')));
481
+ assert.equal(result.managedCopiesPath, `${managedRoot}/skills/source`);
482
+ });
483
+ test('unmanaged destination collisions and changed owned copies are preserved and fail closed', async () => {
484
+ const f = await fixture();
485
+ await f.put('skills/review/SKILL.md', '# Safe');
486
+ await f.put(`${managedRoot}/skills/source/skills/review/SKILL.md`, '# User copy');
487
+ await assert.rejects(writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot: await inventoryRepositoryPackage(f) }), /unmanaged|conflict/);
488
+ assert.equal(await readFile(resolve(f.workspace, managedRoot, 'skills/source/skills/review/SKILL.md'), 'utf8'), '# User copy');
489
+ const other = await fixture();
490
+ await other.put('skills/review/SKILL.md', '# Safe');
491
+ await writeRepositoryPackageSnapshot({ workspace: other.workspace, snapshot: await inventoryRepositoryPackage(other) });
492
+ await other.put(`${managedRoot}/skills/source/skills/review/SKILL.md`, '# User changed it');
493
+ await other.put('skills/review/SKILL.md', '# Updated');
494
+ await assert.rejects(writeRepositoryPackageSnapshot({ workspace: other.workspace, snapshot: await inventoryRepositoryPackage(other) }), /conflict/);
495
+ assert.equal(await readFile(resolve(other.workspace, managedRoot, 'skills/source/skills/review/SKILL.md'), 'utf8'), '# User changed it');
496
+ });
497
+ test('copy journal recovers interruption at every mutation and metadata boundary', async () => {
498
+ for (const checkpoint of ['journal_prepared', 'file_backed_up', 'file_installed', 'copies_index_written', 'manifest_written', 'cleanup_file_removed']) {
499
+ const f = await fixture();
500
+ await f.put('skills/review/SKILL.md', '# Before');
501
+ await writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot: await inventoryRepositoryPackage(f) });
502
+ await f.put('skills/review/SKILL.md', '# After');
503
+ const snapshot = await inventoryRepositoryPackage(f);
504
+ await assert.rejects(writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot,
505
+ onCopyCheckpoint: async (point) => { if (point === checkpoint)
506
+ throw new Error('simulated crash'); } }), /simulated crash/);
507
+ await writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot });
508
+ assert.equal(await readFile(resolve(f.workspace, managedRoot, 'skills/source/skills/review/SKILL.md'), 'utf8'), '# After');
509
+ assert.equal(JSON.parse(await readFile(resolve(f.workspace, managedRoot, 'MANIFEST.json'), 'utf8')).snapshotHash, snapshot.manifest.snapshotHash);
510
+ await assert.rejects(readFile(resolve(f.workspace, managedRoot, 'COPY-JOURNAL.json')));
511
+ }
512
+ });
513
+ test('journal recovery refuses intervening unmanaged files instead of overwriting them', async () => {
514
+ const f = await fixture();
515
+ await f.put('skills/review/SKILL.md', '# Before');
516
+ await writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot: await inventoryRepositoryPackage(f) });
517
+ await f.put('skills/review/SKILL.md', '# After');
518
+ const snapshot = await inventoryRepositoryPackage(f);
519
+ await assert.rejects(writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot,
520
+ onCopyCheckpoint: async (point) => { if (point === 'file_backed_up')
521
+ throw new Error('simulated crash'); } }));
522
+ await f.put(`${managedRoot}/skills/source/skills/review/SKILL.md`, '# Intervening user file');
523
+ await assert.rejects(writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot }), /conflict/);
524
+ assert.equal(await readFile(resolve(f.workspace, managedRoot, 'skills/source/skills/review/SKILL.md'), 'utf8'), '# Intervening user file');
525
+ assert.ok(await readFile(resolve(f.workspace, managedRoot, 'COPY-JOURNAL.json')));
526
+ });
527
+ test('managed mapping rejects escaping or symlinked destinations without writing outside', async () => {
528
+ const f = await fixture();
529
+ await f.put('skills/review/SKILL.md', '# Safe');
530
+ const snapshot = await inventoryRepositoryPackage(f);
531
+ const outside = await fixture();
532
+ await mkdir(resolve(f.workspace, managedRoot, 'skills'), { recursive: true });
533
+ await symlink(outside.workspace, resolve(f.workspace, managedRoot, 'skills/source'));
534
+ await assert.rejects(writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot }), /symlink/);
535
+ await assert.rejects(readFile(resolve(outside.workspace, 'skills/review/SKILL.md')));
536
+ const tampered = structuredClone(snapshot);
537
+ tampered.manifest.files[0].managedPath = '../outside/SKILL.md';
538
+ assert.throws(() => serializeRepositoryPackageSnapshot(tampered), /integrity|mapping/);
539
+ });
540
+ test('native managed-copy fixture preserves originals and removes only journal-owned stale paths', async () => {
541
+ const f = await fixture();
542
+ await f.put('.claude/skills/review/SKILL.md', '# Review\n[Guide](references/guide.md)');
543
+ await f.put('.claude/skills/review/references/guide.md', '# Guide');
544
+ const before = await inventoryRepositoryPackage(f);
545
+ await writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot: before });
546
+ await f.put(`${managedRoot}/skills/source/.claude/skills/review/user.txt`, '# User');
547
+ await f.put('.claude/skills/review/SKILL.md', '# Revision');
548
+ await unlink(resolve(f.workspace, '.claude/skills/review/references/guide.md'));
549
+ const after = await inventoryRepositoryPackage(f);
550
+ await writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot: after });
551
+ assert.equal(await readFile(resolve(f.workspace, managedRoot, 'skills/source/.claude/skills/review/SKILL.md'), 'utf8'), '# Revision');
552
+ assert.equal(await readFile(resolve(f.workspace, '.claude/skills/review/SKILL.md'), 'utf8'), '# Revision');
553
+ assert.equal(await readFile(resolve(f.workspace, managedRoot, 'skills/source/.claude/skills/review/user.txt'), 'utf8'), '# User');
554
+ await assert.rejects(readFile(resolve(f.workspace, managedRoot, 'skills/source/.claude/skills/review/references/guide.md')));
555
+ assert.equal((await inventoryRepositoryPackage(f)).manifest.snapshotHash, after.manifest.snapshotHash);
556
+ });
557
+ test('actual process exit leaves a recoverable journal and dead writer lock', async () => {
558
+ const f = await fixture();
559
+ await f.put('skills/review/SKILL.md', '# Before');
560
+ await writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot: await inventoryRepositoryPackage(f) });
561
+ await f.put('skills/review/SKILL.md', '# After');
562
+ const script = `import { inventoryRepositoryPackage, writeRepositoryPackageSnapshot } from ${JSON.stringify(new URL('./repositoryPackage.js', import.meta.url).href)};
563
+ const input = ${JSON.stringify({ workspace: f.workspace, organizationId: f.organizationId, workspaceId: f.workspaceId })};
564
+ await writeRepositoryPackageSnapshot({ workspace: input.workspace, snapshot: await inventoryRepositoryPackage(input),
565
+ onCopyCheckpoint(point) { if (point === 'file_backed_up') process.exit(86); } });`;
566
+ const exited = spawnSync(process.execPath, ['--input-type=module', '-e', script], { timeout: 10_000, encoding: 'utf8' });
567
+ assert.equal(exited.status, 86, exited.stderr);
568
+ assert.ok(await readFile(resolve(f.workspace, managedRoot, 'COPY-JOURNAL.json')));
569
+ await writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot: await inventoryRepositoryPackage(f) });
570
+ assert.equal(await readFile(resolve(f.workspace, managedRoot, 'skills/source/skills/review/SKILL.md'), 'utf8'), '# After');
571
+ await assert.rejects(readFile(resolve(f.workspace, managedRoot, 'COPY-LOCK.json')));
572
+ await assert.rejects(readFile(resolve(f.workspace, managedRoot, 'COPY-JOURNAL.json')));
573
+ });
574
+ test('recovery preserves same-content intervening files without treating them as staged copies', async () => {
575
+ const f = await fixture();
576
+ await f.put('skills/review/SKILL.md', '# Before');
577
+ await writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot: await inventoryRepositoryPackage(f) });
578
+ await f.put('skills/review/SKILL.md', '# After');
579
+ const snapshot = await inventoryRepositoryPackage(f);
580
+ await assert.rejects(writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot,
581
+ onCopyCheckpoint(point) { if (point === 'file_backed_up')
582
+ throw new Error('interrupted'); } }));
583
+ await f.put(`${managedRoot}/skills/source/skills/review/SKILL.md`, '# After');
584
+ await assert.rejects(writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot }), /conflict/);
585
+ assert.equal(await readFile(resolve(f.workspace, managedRoot, 'skills/source/skills/review/SKILL.md'), 'utf8'), '# After');
586
+ });
587
+ test('malformed ownership and journal metadata cannot authorize stale deletions or escaping writes', async () => {
588
+ const f = await fixture();
589
+ await f.put('skills/review/SKILL.md', '# Before');
590
+ await writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot: await inventoryRepositoryPackage(f) });
591
+ const indexPath = resolve(f.workspace, managedRoot, 'COPY-INDEX.json');
592
+ const original = await readFile(indexPath, 'utf8');
593
+ const index = JSON.parse(original);
594
+ index.files[0].path = '../outside.md';
595
+ await writeFile(indexPath, JSON.stringify(index));
596
+ await assert.rejects(writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot: await inventoryRepositoryPackage(f) }), /integrity/);
597
+ await writeFile(indexPath, original);
598
+ await f.put('skills/review/SKILL.md', '# After');
599
+ const snapshot = await inventoryRepositoryPackage(f);
600
+ await assert.rejects(writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot,
601
+ onCopyCheckpoint(point) { if (point === 'journal_prepared')
602
+ throw new Error('interrupted'); } }));
603
+ const journalPath = resolve(f.workspace, managedRoot, 'COPY-JOURNAL.json');
604
+ const journal = JSON.parse(await readFile(journalPath, 'utf8'));
605
+ journal.transactionId = '../../outside';
606
+ await writeFile(journalPath, JSON.stringify(journal));
607
+ await assert.rejects(writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot }), /integrity/);
608
+ assert.equal(await readFile(resolve(f.workspace, managedRoot, 'skills/source/skills/review/SKILL.md'), 'utf8'), '# Before');
609
+ });
610
+ test('unmanaged metadata and stale symlink replacements are never overwritten or removed', async () => {
611
+ const f = await fixture();
612
+ await f.put('skills/review/SKILL.md', '# Before');
613
+ await f.put(`${managedRoot}/MANIFEST.json`, '{"note":"unmanaged"}');
614
+ await assert.rejects(writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot: await inventoryRepositoryPackage(f) }), /integrity|conflict/);
615
+ assert.equal(await readFile(resolve(f.workspace, managedRoot, 'MANIFEST.json'), 'utf8'), '{"note":"unmanaged"}');
616
+ const other = await fixture();
617
+ await other.put('skills/review/SKILL.md', '# Before');
618
+ await other.put('skills/review/guide.md', '# Guide');
619
+ await writeRepositoryPackageSnapshot({ workspace: other.workspace, snapshot: await inventoryRepositoryPackage(other) });
620
+ const copied = resolve(other.workspace, managedRoot, 'skills/source/skills/review/guide.md');
621
+ await unlink(copied);
622
+ await symlink(resolve(other.workspace, 'skills/review/guide.md'), copied);
623
+ await unlink(resolve(other.workspace, 'skills/review/SKILL.md'));
624
+ await assert.rejects(writeRepositoryPackageSnapshot({ workspace: other.workspace, snapshot: await inventoryRepositoryPackage(other) }), /symlink/);
625
+ assert.equal(await readFile(copied, 'utf8'), '# Guide');
626
+ });
627
+ test('rehashing a malicious mapping does not bypass the exact source-to-managed mapping check', async () => {
628
+ const f = await fixture();
629
+ await f.put('skills/review/SKILL.md', '# Before');
630
+ const snapshot = await inventoryRepositoryPackage(f);
631
+ snapshot.manifest.files[0].managedPath = 'skills/source/elsewhere/SKILL.md';
632
+ const { snapshotHash: _hash, snapshotId: _id, ...base } = snapshot.manifest;
633
+ const { canonicalize } = await import('@dharma-ai-labs/agent-fabric-contracts');
634
+ snapshot.manifest.snapshotHash = `sha256:${createHash('sha256').update(canonicalize(base)).digest('hex')}`;
635
+ snapshot.manifest.snapshotId = `repository-package-${snapshot.manifest.snapshotHash.slice(7)}`;
636
+ assert.throws(() => serializeRepositoryPackageSnapshot(snapshot), /mapping integrity/);
637
+ });
638
+ test('recovery detects edits to unchanged companions before committing new ownership metadata', async () => {
639
+ const f = await fixture();
640
+ await f.put('skills/review/SKILL.md', '# Before');
641
+ await f.put('skills/review/guide.md', '# Guide');
642
+ const previous = await inventoryRepositoryPackage(f);
643
+ await writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot: previous });
644
+ await f.put('skills/review/SKILL.md', '# After');
645
+ const snapshot = await inventoryRepositoryPackage(f);
646
+ await assert.rejects(writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot,
647
+ onCopyCheckpoint(point) { if (point === 'journal_prepared')
648
+ throw new Error('interrupted'); } }));
649
+ await f.put(`${managedRoot}/skills/source/skills/review/guide.md`, '# User edit');
650
+ await assert.rejects(writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot }), /conflict/);
651
+ assert.equal(await readFile(resolve(f.workspace, managedRoot, 'skills/source/skills/review/guide.md'), 'utf8'), '# User edit');
652
+ assert.equal(JSON.parse(await readFile(resolve(f.workspace, managedRoot, 'MANIFEST.json'), 'utf8')).snapshotHash, previous.manifest.snapshotHash);
653
+ });
654
+ test('interrupted removals and initial additions replay deterministically and recover to a newer snapshot', async () => {
655
+ for (const initial of [false, true]) {
656
+ const f = await fixture();
657
+ await f.put('skills/review/SKILL.md', '# Review');
658
+ await f.put('skills/review/guide.md', '# Guide');
659
+ if (!initial) {
660
+ await writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot: await inventoryRepositoryPackage(f) });
661
+ await unlink(resolve(f.workspace, 'skills/review/guide.md'));
662
+ }
663
+ const snapshot = await inventoryRepositoryPackage(f);
664
+ await assert.rejects(writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot,
665
+ onCopyCheckpoint(point) { if (point === (initial ? 'file_installed' : 'file_backed_up'))
666
+ throw new Error('interrupted'); } }));
667
+ await f.put('skills/review/SKILL.md', '# Latest');
668
+ const latest = await inventoryRepositoryPackage(f);
669
+ await writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot: latest });
670
+ assert.equal(await readFile(resolve(f.workspace, managedRoot, 'skills/source/skills/review/SKILL.md'), 'utf8'), '# Latest');
671
+ if (!initial)
672
+ await assert.rejects(readFile(resolve(f.workspace, managedRoot, 'skills/source/skills/review/guide.md')));
673
+ assert.equal(JSON.parse(await readFile(resolve(f.workspace, managedRoot, 'MANIFEST.json'), 'utf8')).snapshotHash, latest.manifest.snapshotHash);
674
+ }
675
+ });
676
+ test('later manifests remain backward-compatible with inventory-only v1 snapshots', async () => {
677
+ const f = await fixture();
678
+ await f.put('skills/review/SKILL.md', '# Review');
679
+ const snapshot = await inventoryRepositoryPackage(f);
680
+ for (const file of snapshot.manifest.files)
681
+ delete file.managedPath;
682
+ for (const skill of snapshot.manifest.skills)
683
+ delete skill.managedPath;
684
+ const { snapshotHash: _hash, snapshotId: _id, ...base } = snapshot.manifest;
685
+ const { canonicalize } = await import('@dharma-ai-labs/agent-fabric-contracts');
686
+ snapshot.manifest.snapshotHash = `sha256:${createHash('sha256').update(canonicalize(base)).digest('hex')}`;
687
+ snapshot.manifest.snapshotId = `repository-package-${snapshot.manifest.snapshotHash.slice(7)}`;
688
+ await f.put(`${managedRoot}/snapshots/${snapshot.manifest.snapshotHash.slice(7)}.json`, serializeRepositoryPackageSnapshot(snapshot));
689
+ await f.put(`${managedRoot}/MANIFEST.json`, `${canonicalize(snapshot.manifest)}\n`);
690
+ await writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot });
691
+ assert.equal(await readFile(resolve(f.workspace, managedRoot, 'skills/source/skills/review/SKILL.md'), 'utf8'), '# Review');
692
+ });
693
+ test('all providers share a bounded source mirror without recursive generated skills or secret copies', async () => {
694
+ const f = await fixture();
695
+ for (const provider of ['.agents/skills', '.claude/skills', '.codex/skills', 'skills']) {
696
+ await f.put(`${provider}/review/SKILL.md`, '# Review\n[Loop](references/guide.md)');
697
+ await f.put(`${provider}/review/references/guide.md`, '[Entry](../SKILL.md)');
698
+ await f.put(`${provider}/review/.env.local`, 'PRIVATE_VALUE=never-copy');
699
+ }
700
+ const snapshot = await inventoryRepositoryPackage(f);
701
+ await writeRepositoryPackageSnapshot({ workspace: f.workspace, snapshot });
702
+ for (const skill of snapshot.manifest.skills) {
703
+ assert.equal(skill.availability, 'partial');
704
+ assert.equal(await readFile(resolve(f.workspace, managedRoot, skill.managedPath, 'SKILL.md'), 'utf8'), '# Review\n[Loop](references/guide.md)');
705
+ await assert.rejects(readFile(resolve(f.workspace, managedRoot, skill.managedPath, '.env.local')));
706
+ }
707
+ assert.equal((await inventoryRepositoryPackage(f)).manifest.snapshotHash, snapshot.manifest.snapshotHash);
708
+ });
709
+ //# sourceMappingURL=repositoryPackage.test.js.map