@ranimontagna/agent-toolkit 0.2.3 → 0.3.0

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 (102) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +21 -7
  3. package/dist/bin/agent-toolkit.js +9 -4
  4. package/dist/bin/agent-toolkit.js.map +1 -1
  5. package/dist/src/ariadne/args.d.ts +2 -0
  6. package/dist/src/ariadne/args.js +146 -0
  7. package/dist/src/ariadne/args.js.map +1 -0
  8. package/dist/src/ariadne/checks.d.ts +24 -0
  9. package/dist/src/ariadne/checks.js +74 -0
  10. package/dist/src/ariadne/checks.js.map +1 -0
  11. package/dist/src/ariadne/cli.d.ts +38 -0
  12. package/dist/src/ariadne/cli.js +206 -0
  13. package/dist/src/ariadne/cli.js.map +1 -0
  14. package/dist/src/ariadne/doctor.d.ts +16 -0
  15. package/dist/src/ariadne/doctor.js +207 -0
  16. package/dist/src/ariadne/doctor.js.map +1 -0
  17. package/dist/src/ariadne/git.d.ts +58 -0
  18. package/dist/src/ariadne/git.js +577 -0
  19. package/dist/src/ariadne/git.js.map +1 -0
  20. package/dist/src/ariadne/import.d.ts +2 -0
  21. package/dist/src/ariadne/import.js +93 -0
  22. package/dist/src/ariadne/import.js.map +1 -0
  23. package/dist/src/ariadne/init.d.ts +47 -0
  24. package/dist/src/ariadne/init.js +345 -0
  25. package/dist/src/ariadne/init.js.map +1 -0
  26. package/dist/src/ariadne/lock.d.ts +22 -0
  27. package/dist/src/ariadne/lock.js +478 -0
  28. package/dist/src/ariadne/lock.js.map +1 -0
  29. package/dist/src/ariadne/loop.d.ts +26 -0
  30. package/dist/src/ariadne/loop.js +1316 -0
  31. package/dist/src/ariadne/loop.js.map +1 -0
  32. package/dist/src/ariadne/ownership.d.ts +32 -0
  33. package/dist/src/ariadne/ownership.js +161 -0
  34. package/dist/src/ariadne/ownership.js.map +1 -0
  35. package/dist/src/ariadne/process.d.ts +27 -0
  36. package/dist/src/ariadne/process.js +302 -0
  37. package/dist/src/ariadne/process.js.map +1 -0
  38. package/dist/src/ariadne/prompt.d.ts +11 -0
  39. package/dist/src/ariadne/prompt.js +29 -0
  40. package/dist/src/ariadne/prompt.js.map +1 -0
  41. package/dist/src/ariadne/render.d.ts +7 -0
  42. package/dist/src/ariadne/render.js +72 -0
  43. package/dist/src/ariadne/render.js.map +1 -0
  44. package/dist/src/ariadne/result.d.ts +54 -0
  45. package/dist/src/ariadne/result.js +129 -0
  46. package/dist/src/ariadne/result.js.map +1 -0
  47. package/dist/src/ariadne/runtimes/antigravity.d.ts +2 -0
  48. package/dist/src/ariadne/runtimes/antigravity.js +17 -0
  49. package/dist/src/ariadne/runtimes/antigravity.js.map +1 -0
  50. package/dist/src/ariadne/runtimes/claude.d.ts +2 -0
  51. package/dist/src/ariadne/runtimes/claude.js +17 -0
  52. package/dist/src/ariadne/runtimes/claude.js.map +1 -0
  53. package/dist/src/ariadne/runtimes/codex.d.ts +2 -0
  54. package/dist/src/ariadne/runtimes/codex.js +24 -0
  55. package/dist/src/ariadne/runtimes/codex.js.map +1 -0
  56. package/dist/src/ariadne/runtimes/gemini.d.ts +2 -0
  57. package/dist/src/ariadne/runtimes/gemini.js +18 -0
  58. package/dist/src/ariadne/runtimes/gemini.js.map +1 -0
  59. package/dist/src/ariadne/runtimes/index.d.ts +4 -0
  60. package/dist/src/ariadne/runtimes/index.js +82 -0
  61. package/dist/src/ariadne/runtimes/index.js.map +1 -0
  62. package/dist/src/ariadne/runtimes/opencode.d.ts +2 -0
  63. package/dist/src/ariadne/runtimes/opencode.js +19 -0
  64. package/dist/src/ariadne/runtimes/opencode.js.map +1 -0
  65. package/dist/src/ariadne/runtimes/shared.d.ts +24 -0
  66. package/dist/src/ariadne/runtimes/shared.js +141 -0
  67. package/dist/src/ariadne/runtimes/shared.js.map +1 -0
  68. package/dist/src/ariadne/runtimes/types.d.ts +56 -0
  69. package/dist/src/ariadne/runtimes/types.js +3 -0
  70. package/dist/src/ariadne/runtimes/types.js.map +1 -0
  71. package/dist/src/ariadne/schema.d.ts +11 -0
  72. package/dist/src/ariadne/schema.js +214 -0
  73. package/dist/src/ariadne/schema.js.map +1 -0
  74. package/dist/src/ariadne/status.d.ts +72 -0
  75. package/dist/src/ariadne/status.js +280 -0
  76. package/dist/src/ariadne/status.js.map +1 -0
  77. package/dist/src/ariadne/store.d.ts +99 -0
  78. package/dist/src/ariadne/store.js +738 -0
  79. package/dist/src/ariadne/store.js.map +1 -0
  80. package/dist/src/ariadne/types.d.ts +128 -0
  81. package/dist/src/ariadne/types.js +5 -0
  82. package/dist/src/ariadne/types.js.map +1 -0
  83. package/dist/src/ariadne/usage.d.ts +1 -0
  84. package/dist/src/ariadne/usage.js +8 -0
  85. package/dist/src/ariadne/usage.js.map +1 -0
  86. package/dist/src/cli.d.ts +5 -0
  87. package/dist/src/cli.js +14 -0
  88. package/dist/src/cli.js.map +1 -0
  89. package/dist/src/skills-audit.d.ts +6 -0
  90. package/dist/src/skills-audit.js +36 -2
  91. package/dist/src/skills-audit.js.map +1 -1
  92. package/dist/src/tool-lock.d.ts +20 -0
  93. package/dist/src/tool-lock.js +26 -0
  94. package/dist/src/tool-lock.js.map +1 -1
  95. package/dist/src/usage.js +1 -0
  96. package/dist/src/usage.js.map +1 -1
  97. package/package.json +7 -3
  98. package/skills/workflow/ariadne/NOTICE.md +11 -0
  99. package/skills/workflow/ariadne/SKILL.md +96 -0
  100. package/skills/workflow/ariadne-prd/NOTICE.md +11 -0
  101. package/skills/workflow/ariadne-prd/SKILL.md +69 -0
  102. package/tools.lock.json +40 -21
@@ -0,0 +1,738 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { AriadneStateError, validateConfig, validatePrd } from "./schema.js";
5
+ export const ARIADNE_OWNERSHIP_CHECKPOINT_MARKER = "<!-- ariadne-ownership-checkpoint:v1 -->";
6
+ const OWNERSHIP_CHANGES = new Set([
7
+ "head",
8
+ "prd",
9
+ "progress",
10
+ "operational",
11
+ ]);
12
+ function ownershipMarkerError(message) {
13
+ throw new AriadneStateError(".ariadne-quarantine.json", `invalid ownership violation marker: ${message}`);
14
+ }
15
+ function validateOwnershipViolation(input) {
16
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
17
+ ownershipMarkerError("expected an object");
18
+ }
19
+ const marker = input;
20
+ const allowed = new Set([
21
+ "schemaVersion",
22
+ "runId",
23
+ "storyId",
24
+ "detectedAt",
25
+ "certifiedHead",
26
+ "observedHead",
27
+ "certifiedRef",
28
+ "observedRef",
29
+ "changed",
30
+ ]);
31
+ if (Object.keys(marker).some((key) => !allowed.has(key))) {
32
+ ownershipMarkerError("contains an unsupported field");
33
+ }
34
+ if (marker.schemaVersion !== 1) {
35
+ ownershipMarkerError("expected schema version 1");
36
+ }
37
+ for (const key of [
38
+ "runId",
39
+ "storyId",
40
+ "detectedAt",
41
+ "certifiedHead",
42
+ "observedHead",
43
+ ]) {
44
+ if (typeof marker[key] !== "string" || marker[key].trim() === "") {
45
+ ownershipMarkerError(`${key} must be a non-empty string`);
46
+ }
47
+ }
48
+ const hasCertifiedRef = marker.certifiedRef !== undefined;
49
+ const hasObservedRef = marker.observedRef !== undefined;
50
+ if (hasCertifiedRef !== hasObservedRef) {
51
+ ownershipMarkerError("certifiedRef and observedRef must either both be present or both be absent");
52
+ }
53
+ if (hasCertifiedRef &&
54
+ (typeof marker.certifiedRef !== "string" ||
55
+ marker.certifiedRef.trim() === "" ||
56
+ typeof marker.observedRef !== "string" ||
57
+ marker.observedRef.trim() === "")) {
58
+ ownershipMarkerError("certifiedRef and observedRef must be non-empty strings");
59
+ }
60
+ if (Number.isNaN(Date.parse(marker.detectedAt))) {
61
+ ownershipMarkerError("detectedAt must be an ISO timestamp");
62
+ }
63
+ if (!Array.isArray(marker.changed) ||
64
+ marker.changed.length === 0 ||
65
+ marker.changed.some((change) => typeof change !== "string" ||
66
+ !OWNERSHIP_CHANGES.has(change)) ||
67
+ new Set(marker.changed).size !== marker.changed.length) {
68
+ ownershipMarkerError("changed must contain unique ownership surfaces");
69
+ }
70
+ return {
71
+ schemaVersion: 1,
72
+ runId: marker.runId,
73
+ storyId: marker.storyId,
74
+ detectedAt: marker.detectedAt,
75
+ certifiedHead: marker.certifiedHead,
76
+ observedHead: marker.observedHead,
77
+ ...(hasCertifiedRef
78
+ ? {
79
+ certifiedRef: marker.certifiedRef,
80
+ observedRef: marker.observedRef,
81
+ }
82
+ : {}),
83
+ changed: [...marker.changed],
84
+ };
85
+ }
86
+ function checkpointError(message) {
87
+ throw new AriadneStateError(".ariadne-quarantine.checkpoint.json", `invalid ownership checkpoint: ${message}`);
88
+ }
89
+ function validatePersistedCertificate(value, label) {
90
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
91
+ checkpointError(`${label} must be an object`);
92
+ }
93
+ const record = value;
94
+ if (Object.keys(record).some((key) => !["device", "inode", "links", "sha256"].includes(key)) ||
95
+ !Number.isSafeInteger(record.device) ||
96
+ !Number.isSafeInteger(record.inode) ||
97
+ record.links !== 1 ||
98
+ typeof record.sha256 !== "string" ||
99
+ !/^[0-9a-f]{64}$/i.test(record.sha256)) {
100
+ checkpointError(`${label} contains invalid certificate fields`);
101
+ }
102
+ return {
103
+ device: record.device,
104
+ inode: record.inode,
105
+ links: 1,
106
+ sha256: record.sha256.toLowerCase(),
107
+ };
108
+ }
109
+ function validateOwnershipCheckpoint(input) {
110
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
111
+ checkpointError("expected an object");
112
+ }
113
+ const record = input;
114
+ const allowed = new Set([
115
+ "schemaVersion",
116
+ "runId",
117
+ "storyId",
118
+ "certifiedAt",
119
+ "certifiedHead",
120
+ "certifiedRef",
121
+ "prd",
122
+ "progress",
123
+ ]);
124
+ if (Object.keys(record).some((key) => !allowed.has(key))) {
125
+ checkpointError("contains an unsupported field");
126
+ }
127
+ if (record.schemaVersion !== 1)
128
+ checkpointError("expected schema version 1");
129
+ for (const key of [
130
+ "runId",
131
+ "storyId",
132
+ "certifiedAt",
133
+ "certifiedHead",
134
+ "certifiedRef",
135
+ ]) {
136
+ if (typeof record[key] !== "string" || record[key].trim() === "") {
137
+ checkpointError(`${key} must be a non-empty string`);
138
+ }
139
+ }
140
+ if (Number.isNaN(Date.parse(record.certifiedAt))) {
141
+ checkpointError("certifiedAt must be an ISO timestamp");
142
+ }
143
+ return {
144
+ schemaVersion: 1,
145
+ runId: record.runId,
146
+ storyId: record.storyId,
147
+ certifiedAt: record.certifiedAt,
148
+ certifiedHead: record.certifiedHead,
149
+ certifiedRef: record.certifiedRef,
150
+ prd: validatePersistedCertificate(record.prd, "prd"),
151
+ progress: validatePersistedCertificate(record.progress, "progress"),
152
+ };
153
+ }
154
+ function safePathPart(value, label) {
155
+ if (!value ||
156
+ path.basename(value) !== value ||
157
+ value === "." ||
158
+ value === "..") {
159
+ throw new Error(`Invalid ${label}: ${value}`);
160
+ }
161
+ return value;
162
+ }
163
+ export class AriadneStore {
164
+ projectRoot;
165
+ paths;
166
+ constructor(projectRoot) {
167
+ this.projectRoot = projectRoot;
168
+ const root = path.join(projectRoot, ".ariadne");
169
+ this.paths = {
170
+ root,
171
+ config: path.join(root, "config.json"),
172
+ prd: path.join(root, "prd.json"),
173
+ progress: path.join(root, "progress.md"),
174
+ lock: path.join(root, "lock"),
175
+ runs: path.join(root, "runs"),
176
+ ownershipViolation: path.join(projectRoot, ".ariadne-quarantine.json"),
177
+ ownershipCheckpoint: path.join(projectRoot, ".ariadne-quarantine.checkpoint.json"),
178
+ archive: path.join(root, "archive"),
179
+ };
180
+ }
181
+ ensureLayout() {
182
+ this.ensureRealDirectory(this.paths.root, "Ariadne state directory");
183
+ this.ensureRealDirectory(this.paths.archive, "Ariadne archive directory");
184
+ }
185
+ loadConfig() {
186
+ return validateConfig(this.readJson(this.paths.config));
187
+ }
188
+ loadPrd() {
189
+ return validatePrd(this.readJson(this.paths.prd));
190
+ }
191
+ hasCanonicalState() {
192
+ return fs.existsSync(this.paths.prd) && fs.existsSync(this.paths.progress);
193
+ }
194
+ saveConfig(config) {
195
+ this.writeJsonAtomic(this.paths.config, validateConfig(config));
196
+ }
197
+ savePrd(prd) {
198
+ return this.writeJsonAtomic(this.paths.prd, validatePrd(prd));
199
+ }
200
+ loadOwnershipViolation() {
201
+ const contents = this.readProjectRootFile(this.paths.ownershipViolation, "ownership violation marker");
202
+ if (contents === undefined)
203
+ return undefined;
204
+ let value;
205
+ try {
206
+ value = JSON.parse(contents);
207
+ }
208
+ catch {
209
+ ownershipMarkerError("contains malformed JSON");
210
+ }
211
+ return validateOwnershipViolation(value);
212
+ }
213
+ saveOwnershipViolation(marker) {
214
+ this.writeQuarantineAtomic(validateOwnershipViolation(marker));
215
+ }
216
+ loadOwnershipCheckpoint() {
217
+ const contents = this.readProjectRootFile(this.paths.ownershipCheckpoint, "ownership checkpoint");
218
+ if (contents === undefined)
219
+ return undefined;
220
+ try {
221
+ return validateOwnershipCheckpoint(JSON.parse(contents));
222
+ }
223
+ catch (error) {
224
+ if (error instanceof AriadneStateError)
225
+ throw error;
226
+ checkpointError("contains malformed JSON");
227
+ }
228
+ }
229
+ saveOwnershipCheckpoint(input) {
230
+ if (input.prd.source !== this.paths.prd) {
231
+ checkpointError("prd certificate has an unexpected source");
232
+ }
233
+ if (input.progress.source !== this.paths.progress) {
234
+ checkpointError("progress certificate has an unexpected source");
235
+ }
236
+ this.assertCanonicalCertificate(input.prd);
237
+ this.assertCanonicalCertificate(input.progress);
238
+ const certificate = (value) => ({
239
+ device: value.device,
240
+ inode: value.inode,
241
+ links: value.links,
242
+ sha256: createHash("sha256").update(value.contents).digest("hex"),
243
+ });
244
+ const checkpoint = validateOwnershipCheckpoint({
245
+ schemaVersion: 1,
246
+ runId: input.runId,
247
+ storyId: input.storyId,
248
+ certifiedAt: input.certifiedAt,
249
+ certifiedHead: input.certifiedHead,
250
+ certifiedRef: input.certifiedRef,
251
+ prd: certificate(input.prd),
252
+ progress: certificate(input.progress),
253
+ });
254
+ this.writeProjectRootAtomic(this.paths.ownershipCheckpoint, checkpoint, ".ariadne-quarantine.checkpoint.tmp-", "Ariadne ownership checkpoint");
255
+ return checkpoint;
256
+ }
257
+ ownershipCheckpointChanges(checkpoint) {
258
+ const changes = [];
259
+ const changed = (source, expected) => {
260
+ try {
261
+ const actual = this.captureCanonicalCertificate(source);
262
+ return (actual.device !== expected.device ||
263
+ actual.inode !== expected.inode ||
264
+ actual.links !== expected.links ||
265
+ createHash("sha256").update(actual.contents).digest("hex") !==
266
+ expected.sha256);
267
+ }
268
+ catch {
269
+ return true;
270
+ }
271
+ };
272
+ if (changed(this.paths.prd, checkpoint.prd))
273
+ changes.push("prd");
274
+ if (changed(this.paths.progress, checkpoint.progress)) {
275
+ changes.push("progress");
276
+ }
277
+ return changes;
278
+ }
279
+ ownershipCheckpointRequired() {
280
+ if (!fs.existsSync(this.paths.progress) && !fs.existsSync(this.paths.prd)) {
281
+ return false;
282
+ }
283
+ return this.captureCanonicalCertificate(this.paths.progress).contents.includes(ARIADNE_OWNERSHIP_CHECKPOINT_MARKER);
284
+ }
285
+ assertNoOwnershipViolation() {
286
+ const violation = this.loadOwnershipViolation();
287
+ if (!violation)
288
+ return;
289
+ throw new AriadneStateError(".ariadne-quarantine.json", `Unresolved runtime ownership violation from run ${violation.runId} (${violation.changed.join(", ")}); inspect and restore Git/canonical state, then deliberately remove ${this.paths.ownershipViolation}`);
290
+ }
291
+ appendProgress(entry, expected) {
292
+ this.ensureRealDirectory(this.paths.root, "Ariadne state directory");
293
+ const flags = fs.constants.O_RDWR | fs.constants.O_APPEND | fs.constants.O_NOFOLLOW;
294
+ let descriptor;
295
+ try {
296
+ descriptor = fs.openSync(this.paths.progress, flags);
297
+ }
298
+ catch {
299
+ throw new AriadneStateError(this.paths.progress, "Ariadne progress file is unavailable or unsafe");
300
+ }
301
+ try {
302
+ const before = this.certificateFromDescriptor(this.paths.progress, descriptor, "Ariadne progress");
303
+ if (expected && !this.sameCertificate(before, expected)) {
304
+ throw new AriadneStateError(this.paths.progress, "Ariadne progress changed before the coordinator append");
305
+ }
306
+ const addition = `${entry}\n`;
307
+ fs.writeFileSync(descriptor, addition, "utf8");
308
+ fs.fsyncSync(descriptor);
309
+ const afterStat = fs.fstatSync(descriptor);
310
+ const after = {
311
+ source: this.paths.progress,
312
+ kind: "file",
313
+ device: afterStat.dev,
314
+ inode: afterStat.ino,
315
+ links: afterStat.nlink,
316
+ contents: `${before.contents}${addition}`,
317
+ };
318
+ this.assertCanonicalCertificate(after);
319
+ return after;
320
+ }
321
+ finally {
322
+ fs.closeSync(descriptor);
323
+ }
324
+ }
325
+ captureCanonicalCertificate(source) {
326
+ const flags = fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW;
327
+ let descriptor;
328
+ try {
329
+ descriptor = fs.openSync(source, flags);
330
+ }
331
+ catch {
332
+ throw new AriadneStateError(source, "Ariadne canonical state is unavailable or unsafe");
333
+ }
334
+ try {
335
+ return this.certificateFromDescriptor(source, descriptor, "Ariadne canonical state");
336
+ }
337
+ finally {
338
+ fs.closeSync(descriptor);
339
+ }
340
+ }
341
+ assertCanonicalCertificate(certificate) {
342
+ const current = this.captureCanonicalCertificate(certificate.source);
343
+ if (!this.sameCertificate(current, certificate)) {
344
+ throw new AriadneStateError(certificate.source, "Coordinator-owned canonical Ariadne state changed after coordinator certification");
345
+ }
346
+ }
347
+ createRunDir(runId) {
348
+ const safeRunId = safePathPart(runId, "run id");
349
+ this.ensureRunsDirectory();
350
+ const directory = path.join(this.paths.runs, safeRunId);
351
+ this.ensureRealDirectory(directory, "Ariadne run directory");
352
+ return this.assertRunDirectory(safeRunId);
353
+ }
354
+ assertRunDirectory(runId) {
355
+ const safeRunId = safePathPart(runId, "run id");
356
+ this.assertRealDirectory(this.paths.root, "Ariadne state directory");
357
+ this.assertRealDirectory(this.paths.runs, "Ariadne runs directory");
358
+ const directory = path.join(this.paths.runs, safeRunId);
359
+ this.assertRealDirectory(directory, "Ariadne run directory");
360
+ const canonicalRuns = fs.realpathSync(this.paths.runs);
361
+ const canonicalDirectory = fs.realpathSync(directory);
362
+ if (path.dirname(canonicalDirectory) !== canonicalRuns) {
363
+ throw new AriadneStateError(directory, "Ariadne run directory escaped the machine-local runs root");
364
+ }
365
+ return directory;
366
+ }
367
+ assertRunArtifacts(runId, names) {
368
+ const directory = this.assertRunDirectory(runId);
369
+ for (const name of names) {
370
+ const safeName = safePathPart(name, "run file name");
371
+ const source = path.join(directory, safeName);
372
+ let stat;
373
+ try {
374
+ stat = fs.lstatSync(source);
375
+ }
376
+ catch (error) {
377
+ if (error.code === "ENOENT")
378
+ continue;
379
+ throw error;
380
+ }
381
+ if (stat.isSymbolicLink()) {
382
+ throw new AriadneStateError(source, "Ariadne run artifact must not be a symbolic link");
383
+ }
384
+ if (!stat.isFile()) {
385
+ throw new AriadneStateError(source, "Ariadne run artifact must be a regular file");
386
+ }
387
+ if (stat.nlink !== 1) {
388
+ throw new AriadneStateError(source, "Ariadne run artifact must have a single link and must not be hard-linked");
389
+ }
390
+ }
391
+ }
392
+ assertRunDirectoryContents(runId) {
393
+ const directory = this.assertRunDirectory(runId);
394
+ this.assertRunArtifacts(runId, fs.readdirSync(directory));
395
+ }
396
+ certifyRunBoundary(runId, requiredArtifacts = [], expectedArtifacts = []) {
397
+ const safeRunId = safePathPart(runId, "run id");
398
+ const directory = this.assertRunDirectory(safeRunId);
399
+ const artifacts = requiredArtifacts.map((name) => {
400
+ const safeName = safePathPart(name, "run file name");
401
+ return this.capturePathIdentity(path.join(directory, safeName), "file", "Ariadne required run artifact");
402
+ });
403
+ const boundary = {
404
+ runId: safeRunId,
405
+ root: this.capturePathIdentity(this.paths.root, "directory", "Ariadne state directory"),
406
+ runs: this.capturePathIdentity(this.paths.runs, "directory", "Ariadne runs directory"),
407
+ directory: this.capturePathIdentity(directory, "directory", "Ariadne run directory"),
408
+ artifacts,
409
+ };
410
+ this.assertExpectedArtifacts(boundary, expectedArtifacts);
411
+ this.assertRunBoundary(boundary);
412
+ return boundary;
413
+ }
414
+ extendRunBoundary(boundary, expectedArtifacts) {
415
+ this.assertRunBoundary(boundary);
416
+ const expectedDirectory = boundary.directory.source;
417
+ const additions = expectedArtifacts.map((expected) => {
418
+ if (path.dirname(expected.source) !== expectedDirectory) {
419
+ throw new AriadneStateError(expected.source, "Ariadne output identity escaped the certified run directory");
420
+ }
421
+ const captured = this.capturePathIdentity(expected.source, "file", "Ariadne certified output artifact");
422
+ if (!this.sameIdentity(captured, expected)) {
423
+ throw new AriadneStateError(expected.source, "Ariadne output identity changed before run-boundary certification");
424
+ }
425
+ return captured;
426
+ });
427
+ const sources = new Set(boundary.artifacts.map((artifact) => artifact.source));
428
+ if (additions.some((artifact) => sources.has(artifact.source))) {
429
+ throw new AriadneStateError(expectedDirectory, "Ariadne run boundary cannot certify an artifact twice");
430
+ }
431
+ const extended = {
432
+ ...boundary,
433
+ artifacts: [...boundary.artifacts, ...additions],
434
+ };
435
+ this.assertRunBoundary(extended);
436
+ return extended;
437
+ }
438
+ assertRunBoundary(boundary) {
439
+ const safeRunId = safePathPart(boundary.runId, "run id");
440
+ const expectedDirectory = path.join(this.paths.runs, safeRunId);
441
+ if (boundary.root.source !== this.paths.root ||
442
+ boundary.runs.source !== this.paths.runs ||
443
+ boundary.directory.source !== expectedDirectory ||
444
+ boundary.artifacts.some((artifact) => path.dirname(artifact.source) !== expectedDirectory)) {
445
+ throw new AriadneStateError(expectedDirectory, "Ariadne run boundary does not match the managed state layout");
446
+ }
447
+ this.assertPathIdentity(boundary.root);
448
+ this.assertPathIdentity(boundary.runs);
449
+ this.assertPathIdentity(boundary.directory);
450
+ for (const artifact of boundary.artifacts) {
451
+ this.assertPathIdentity(artifact);
452
+ }
453
+ this.assertRunDirectoryContents(safeRunId);
454
+ }
455
+ writeRunJson(runId, name, value) {
456
+ const destination = path.join(this.createRunDir(runId), safePathPart(name, "run file name"));
457
+ this.writeJsonAtomic(destination, value);
458
+ return destination;
459
+ }
460
+ writeRunTextExclusive(runId, name, contents) {
461
+ const destination = path.join(this.createRunDir(runId), safePathPart(name, "run file name"));
462
+ let descriptor;
463
+ try {
464
+ descriptor = fs.openSync(destination, "wx", 0o600);
465
+ }
466
+ catch {
467
+ throw new AriadneStateError(destination, "Ariadne run artifact already exists or cannot be created safely");
468
+ }
469
+ try {
470
+ fs.writeFileSync(descriptor, contents, "utf8");
471
+ fs.fsyncSync(descriptor);
472
+ const stat = fs.fstatSync(descriptor);
473
+ const identity = {
474
+ source: destination,
475
+ kind: "file",
476
+ device: stat.dev,
477
+ inode: stat.ino,
478
+ links: stat.nlink,
479
+ };
480
+ this.assertPathIdentity(identity);
481
+ return identity;
482
+ }
483
+ finally {
484
+ fs.closeSync(descriptor);
485
+ }
486
+ }
487
+ archiveImportedPrd(sourceText, timestamp) {
488
+ const normalized = new Date(timestamp)
489
+ .toISOString()
490
+ .replace(/:/g, "-")
491
+ .replace(".", "-");
492
+ const destination = path.join(this.paths.archive, `import-${normalized}`, "prd.json");
493
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
494
+ fs.writeFileSync(destination, sourceText, "utf8");
495
+ return destination;
496
+ }
497
+ readJson(source) {
498
+ let contents;
499
+ try {
500
+ contents = fs.readFileSync(source, "utf8");
501
+ }
502
+ catch {
503
+ throw new AriadneStateError(source, "unable to read Ariadne state");
504
+ }
505
+ try {
506
+ return JSON.parse(contents);
507
+ }
508
+ catch {
509
+ throw new AriadneStateError(source, "contains malformed JSON");
510
+ }
511
+ }
512
+ assertRealDirectory(source, label) {
513
+ let stat;
514
+ try {
515
+ stat = fs.lstatSync(source);
516
+ }
517
+ catch (error) {
518
+ if (error.code === "ENOENT")
519
+ throw error;
520
+ throw new AriadneStateError(source, `${label} is unavailable`);
521
+ }
522
+ if (stat.isSymbolicLink()) {
523
+ throw new AriadneStateError(source, `${label} must not be a symbolic link`);
524
+ }
525
+ if (!stat.isDirectory()) {
526
+ throw new AriadneStateError(source, `${label} must be a directory`);
527
+ }
528
+ }
529
+ capturePathIdentity(source, kind, label) {
530
+ let stat;
531
+ try {
532
+ stat = fs.lstatSync(source);
533
+ }
534
+ catch {
535
+ throw new AriadneStateError(source, `${label} is unavailable`);
536
+ }
537
+ if (stat.isSymbolicLink() ||
538
+ (kind === "directory" ? !stat.isDirectory() : !stat.isFile())) {
539
+ throw new AriadneStateError(source, `${label} must be a real ${kind}`);
540
+ }
541
+ if (kind === "file" && stat.nlink !== 1) {
542
+ throw new AriadneStateError(source, `${label} must have a single link and must not be hard-linked`);
543
+ }
544
+ return {
545
+ source,
546
+ kind,
547
+ device: stat.dev,
548
+ inode: stat.ino,
549
+ links: stat.nlink,
550
+ };
551
+ }
552
+ assertPathIdentity(identity) {
553
+ const current = this.capturePathIdentity(identity.source, identity.kind, `Ariadne ${identity.kind} boundary`);
554
+ if (current.device !== identity.device ||
555
+ current.inode !== identity.inode ||
556
+ (identity.kind === "file" && current.links !== identity.links)) {
557
+ throw new AriadneStateError(identity.source, `Ariadne ${identity.kind} boundary changed during the run`);
558
+ }
559
+ }
560
+ ensureRealDirectory(source, label) {
561
+ try {
562
+ this.assertRealDirectory(source, label);
563
+ return;
564
+ }
565
+ catch (error) {
566
+ if (error.code !== "ENOENT")
567
+ throw error;
568
+ }
569
+ fs.mkdirSync(source, { mode: 0o700 });
570
+ this.assertRealDirectory(source, label);
571
+ }
572
+ ensureRunsDirectory() {
573
+ this.ensureRealDirectory(this.paths.root, "Ariadne state directory");
574
+ this.ensureRealDirectory(this.paths.runs, "Ariadne runs directory");
575
+ }
576
+ assertProjectRoot() {
577
+ this.assertRealDirectory(this.projectRoot, "Ariadne project root");
578
+ }
579
+ readProjectRootFile(source, label) {
580
+ this.assertProjectRoot();
581
+ let descriptor;
582
+ try {
583
+ descriptor = fs.openSync(source, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
584
+ }
585
+ catch (error) {
586
+ if (error.code === "ENOENT")
587
+ return undefined;
588
+ throw new AriadneStateError(source, `${label} must be a regular non-symbolic file`);
589
+ }
590
+ try {
591
+ const stat = fs.fstatSync(descriptor);
592
+ if (!stat.isFile() || stat.nlink !== 1) {
593
+ throw new AriadneStateError(source, `${label} must be a regular non-symbolic single-link file`);
594
+ }
595
+ const pathIdentity = this.capturePathIdentity(source, "file", label);
596
+ if (pathIdentity.device !== stat.dev ||
597
+ pathIdentity.inode !== stat.ino ||
598
+ pathIdentity.links !== stat.nlink) {
599
+ throw new AriadneStateError(source, `${label} changed while open`);
600
+ }
601
+ const contents = fs.readFileSync(descriptor, "utf8");
602
+ this.assertPathIdentity(pathIdentity);
603
+ return contents;
604
+ }
605
+ finally {
606
+ fs.closeSync(descriptor);
607
+ }
608
+ }
609
+ sameIdentity(left, right) {
610
+ return (left.source === right.source &&
611
+ left.device === right.device &&
612
+ left.inode === right.inode &&
613
+ left.links === right.links);
614
+ }
615
+ sameCertificate(left, right) {
616
+ return this.sameIdentity(left, right) && left.contents === right.contents;
617
+ }
618
+ assertExpectedArtifacts(boundary, expectedArtifacts) {
619
+ for (const expected of expectedArtifacts) {
620
+ const actual = boundary.artifacts.find((artifact) => artifact.source === expected.source);
621
+ if (!actual || !this.sameIdentity(actual, expected)) {
622
+ throw new AriadneStateError(expected.source, "Ariadne run artifact does not match its creator-certified identity");
623
+ }
624
+ }
625
+ }
626
+ certificateFromDescriptor(source, descriptor, label) {
627
+ const stat = fs.fstatSync(descriptor);
628
+ if (!stat.isFile() || stat.nlink !== 1) {
629
+ throw new AriadneStateError(source, `${label} must be a regular single-link file`);
630
+ }
631
+ const pathIdentity = this.capturePathIdentity(source, "file", label);
632
+ const identity = {
633
+ source,
634
+ kind: "file",
635
+ device: stat.dev,
636
+ inode: stat.ino,
637
+ links: stat.nlink,
638
+ };
639
+ if (!this.sameIdentity(pathIdentity, identity)) {
640
+ throw new AriadneStateError(source, `${label} path changed while open`);
641
+ }
642
+ const contents = Buffer.alloc(stat.size);
643
+ let offset = 0;
644
+ while (offset < contents.length) {
645
+ const read = fs.readSync(descriptor, contents, offset, contents.length - offset, offset);
646
+ if (read === 0)
647
+ break;
648
+ offset += read;
649
+ }
650
+ if (offset !== contents.length) {
651
+ throw new AriadneStateError(source, `${label} changed while being read`);
652
+ }
653
+ this.assertPathIdentity(identity);
654
+ return { ...identity, contents: contents.toString("utf8") };
655
+ }
656
+ writeQuarantineAtomic(marker) {
657
+ this.writeProjectRootAtomic(this.paths.ownershipViolation, marker, ".ariadne-quarantine.tmp-", "Ariadne quarantine marker");
658
+ }
659
+ writeProjectRootAtomic(destination, value, temporaryPrefix, label) {
660
+ this.assertProjectRoot();
661
+ const temporary = path.join(this.projectRoot, `${temporaryPrefix}${randomUUID()}`);
662
+ try {
663
+ const descriptor = fs.openSync(temporary, "wx", 0o600);
664
+ try {
665
+ fs.writeFileSync(descriptor, `${JSON.stringify(value, null, 2)}\n`, "utf8");
666
+ fs.fsyncSync(descriptor);
667
+ const stat = fs.fstatSync(descriptor);
668
+ if (!stat.isFile() || stat.nlink !== 1) {
669
+ throw new AriadneStateError(temporary, `${label} temporary must be a regular single-link file`);
670
+ }
671
+ }
672
+ finally {
673
+ fs.closeSync(descriptor);
674
+ }
675
+ this.assertProjectRoot();
676
+ fs.renameSync(temporary, destination);
677
+ this.capturePathIdentity(destination, "file", label);
678
+ const parentDescriptor = fs.openSync(this.projectRoot, "r");
679
+ try {
680
+ fs.fsyncSync(parentDescriptor);
681
+ }
682
+ finally {
683
+ fs.closeSync(parentDescriptor);
684
+ }
685
+ }
686
+ catch (error) {
687
+ fs.rmSync(temporary, { force: true });
688
+ throw error;
689
+ }
690
+ }
691
+ writeJsonAtomic(destination, value) {
692
+ const parent = path.dirname(destination);
693
+ if (parent === this.paths.root) {
694
+ this.ensureRealDirectory(parent, "Ariadne state directory");
695
+ }
696
+ else if (parent === this.paths.runs) {
697
+ this.ensureRunsDirectory();
698
+ }
699
+ else if (path.dirname(parent) === this.paths.runs) {
700
+ this.assertRunDirectory(path.basename(parent));
701
+ }
702
+ else {
703
+ throw new AriadneStateError(destination, "Ariadne atomic write escaped the managed state layout");
704
+ }
705
+ const temporary = `${destination}.tmp-${randomUUID()}`;
706
+ const contents = `${JSON.stringify(value, null, 2)}\n`;
707
+ try {
708
+ const descriptor = fs.openSync(temporary, "wx", 0o600);
709
+ let identity;
710
+ try {
711
+ fs.writeFileSync(descriptor, contents, "utf8");
712
+ fs.fsyncSync(descriptor);
713
+ const stat = fs.fstatSync(descriptor);
714
+ identity = {
715
+ source: destination,
716
+ kind: "file",
717
+ device: stat.dev,
718
+ inode: stat.ino,
719
+ links: stat.nlink,
720
+ };
721
+ if (!stat.isFile() || stat.nlink !== 1) {
722
+ throw new AriadneStateError(destination, "Ariadne atomic write must remain a regular single-link file");
723
+ }
724
+ }
725
+ finally {
726
+ fs.closeSync(descriptor);
727
+ }
728
+ fs.renameSync(temporary, destination);
729
+ this.assertPathIdentity(identity);
730
+ return { ...identity, contents };
731
+ }
732
+ catch (error) {
733
+ fs.rmSync(temporary, { force: true });
734
+ throw error;
735
+ }
736
+ }
737
+ }
738
+ //# sourceMappingURL=store.js.map