solwyn-cli 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,727 @@
1
+ """Single-source rendering and safe repository writes for agent guidance."""
2
+
3
+ import os
4
+ import secrets
5
+ import stat
6
+ from enum import StrEnum
7
+ from pathlib import Path
8
+
9
+ from pydantic import BaseModel, ConfigDict
10
+
11
+ from solwyn_cli.artifacts._atomic import (
12
+ atomic_exchange as _atomic_exchange,
13
+ atomic_noreplace as _atomic_noreplace,
14
+ )
15
+
16
+
17
+ CLAUDE_SKILL_PATH = Path(".claude/skills/solwyn/SKILL.md")
18
+ CURSOR_RULE_PATH = Path(".cursor/rules/solwyn.mdc")
19
+ AGENTS_PATH = Path("AGENTS.md")
20
+
21
+ _CLAUDE_FRONTMATTER = (
22
+ "---\nname: solwyn\ndescription: Use Solwyn budget guardrails safely.\n---\n\n"
23
+ )
24
+ _CURSOR_FRONTMATTER = (
25
+ "---\n"
26
+ "description: Apply Solwyn budget guardrails safely.\n"
27
+ "alwaysApply: false\n"
28
+ "---\n\n"
29
+ )
30
+ _AGENTS_BEGIN = "<!-- BEGIN SOLWYN AGENT CONTEXT -->"
31
+ _AGENTS_END = "<!-- END SOLWYN AGENT CONTEXT -->"
32
+ _TRANSACTION_PREFIX = ".solwyn-agent-"
33
+ _DIRECTORY_FLAGS = (
34
+ os.O_RDONLY
35
+ | getattr(os, "O_DIRECTORY", 0)
36
+ | getattr(os, "O_NOFOLLOW", 0)
37
+ | getattr(os, "O_CLOEXEC", 0)
38
+ )
39
+ _FILE_IDENTITY = tuple[int, int, int, int, int, int]
40
+
41
+
42
+ class _ArtifactState(BaseModel):
43
+ model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
44
+
45
+ relative_path: Path
46
+ parent_path: Path
47
+ parent_fd: int
48
+ parent_identity: tuple[int, int]
49
+ target_name: str
50
+ original_identity: _FILE_IDENTITY | None
51
+ content: str
52
+ temporary_name: str | None = None
53
+ backup_name: str | None = None
54
+ committed: bool = False
55
+ installed_identity: _FILE_IDENTITY | None = None
56
+ retain_backup_for_recovery: bool = False
57
+ retain_temporary_for_recovery: bool = False
58
+ rollback_recovery_name: str | None = None
59
+ conflict_detected: bool = False
60
+
61
+
62
+ class ArtifactRecoveryError(OSError):
63
+ """Rollback preserved concurrent edits or retained original recovery bytes."""
64
+
65
+ def __init__(
66
+ self,
67
+ recovery_paths: tuple[Path, ...],
68
+ conflict_paths: tuple[Path, ...] = (),
69
+ ) -> None:
70
+ self.recovery_paths = recovery_paths
71
+ self.conflict_paths = conflict_paths
72
+ details: list[str] = []
73
+ if recovery_paths:
74
+ details.append(
75
+ "retained backup(s): " + ", ".join(str(path) for path in recovery_paths)
76
+ )
77
+ if conflict_paths:
78
+ details.append(
79
+ "preserved concurrent target(s): "
80
+ + ", ".join(str(path) for path in conflict_paths)
81
+ )
82
+ super().__init__(
83
+ "Agent artifact rollback failed; manual recovery required; "
84
+ + "; ".join(details)
85
+ )
86
+
87
+
88
+ class ArtifactSurface(StrEnum):
89
+ """The four public surfaces backed by the same semantic guidance."""
90
+
91
+ CLAUDE_SKILL = "claude_skill"
92
+ CURSOR_RULE = "cursor_rule"
93
+ AGENTS_SECTION = "agents_section"
94
+ AGENT_CONTEXT = "agent_context"
95
+
96
+
97
+ def agent_guidance() -> str:
98
+ """Return the canonical semantic source shared by every agent surface."""
99
+ return """# Solwyn budget guardrails
100
+
101
+ Use Solwyn to observe and guard AI-provider spend without exposing credentials or duplicating reservations.
102
+
103
+ ## Polling and reservations
104
+
105
+ - Use `solwyn budget status --json` for polling. It is a pure read and does not hold budget.
106
+ - Use `solwyn budget check` followed by `solwyn budget confirm` only around a specific un-instrumented provider spend that you make yourself.
107
+ - Never wrap SDK-instrumented code with `budget check` or `budget confirm`; the Solwyn SDK already reserves and confirms that spend.
108
+ - Fail-open is explicit: when a check returns `allowed=true` and `reservation_id=null`, skip confirm.
109
+ - Do not create repeated unconfirmed checks. They temporarily hold budget and can cause self-denial.
110
+
111
+ ## Machine contract and free-tier backoff
112
+
113
+ - Use `--json` (or `--format json` / `SOLWYN_FORMAT=json`) for schema-bearing machine output. Success schema names are additive-only; JSON errors use `solwyn.error.v1` on stderr.
114
+ - Frozen exit codes are: 0 success, 1 generic/API, 2 usage, 3 auth, 4 budget denial, and 5 not found.
115
+ - On rate limits, honor `Retry-After` once for at most 10 seconds. On the free tier, cache and back off rather than creating a retry storm.
116
+
117
+ ## Trust boundary
118
+
119
+ - Keep Solwyn credentials only in the OS keychain. Never echo them or place them in config files or argv.
120
+ - Solwyn must never collect, store, or proxy provider keys, prompts, or responses.
121
+ - Do not add telemetry or update checks.
122
+ """
123
+
124
+
125
+ def _normalized_guidance() -> str:
126
+ return agent_guidance().rstrip("\n")
127
+
128
+
129
+ def render_surface(surface: ArtifactSurface) -> str:
130
+ """Render one surface using only framing around the canonical guidance."""
131
+ guidance = _normalized_guidance()
132
+ if surface is ArtifactSurface.CLAUDE_SKILL:
133
+ return f"{_CLAUDE_FRONTMATTER}{guidance}\n"
134
+ if surface is ArtifactSurface.CURSOR_RULE:
135
+ return f"{_CURSOR_FRONTMATTER}{guidance}\n"
136
+ if surface is ArtifactSurface.AGENTS_SECTION:
137
+ return f"{_AGENTS_BEGIN}\n{guidance}\n{_AGENTS_END}\n"
138
+ return f"{guidance}\n"
139
+
140
+
141
+ def extract_guidance(surface: ArtifactSurface, content: str) -> str:
142
+ """Extract semantic content for parity checks and downstream validation."""
143
+ if surface is ArtifactSurface.CLAUDE_SKILL:
144
+ if not content.startswith(_CLAUDE_FRONTMATTER):
145
+ raise ValueError("Invalid Claude skill framing.")
146
+ return content.removeprefix(_CLAUDE_FRONTMATTER).rstrip("\n")
147
+ if surface is ArtifactSurface.CURSOR_RULE:
148
+ if not content.startswith(_CURSOR_FRONTMATTER):
149
+ raise ValueError("Invalid Cursor rule framing.")
150
+ return content.removeprefix(_CURSOR_FRONTMATTER).rstrip("\n")
151
+ if surface is ArtifactSurface.AGENTS_SECTION:
152
+ prefix = f"{_AGENTS_BEGIN}\n"
153
+ suffix = f"\n{_AGENTS_END}\n"
154
+ if not content.startswith(prefix) or not content.endswith(suffix):
155
+ raise ValueError("Invalid AGENTS section framing.")
156
+ return content.removeprefix(prefix).removesuffix(suffix)
157
+ return content.rstrip("\n")
158
+
159
+
160
+ def _fence(line: str) -> tuple[str, int] | None:
161
+ stripped = line.lstrip(" ")
162
+ if len(line) - len(stripped) > 3 or not stripped:
163
+ return None
164
+ character = stripped[0]
165
+ if character not in {"`", "~"}:
166
+ return None
167
+ length = len(stripped) - len(stripped.lstrip(character))
168
+ return (character, length) if length >= 3 else None
169
+
170
+
171
+ def _owned_marker_offsets(existing: str) -> tuple[list[int], list[tuple[int, int]]]:
172
+ begins: list[int] = []
173
+ ends: list[tuple[int, int]] = []
174
+ open_fence: tuple[str, int] | None = None
175
+ offset = 0
176
+ for line in existing.splitlines(keepends=True):
177
+ body = line.rstrip("\r\n")
178
+ fence = _fence(body)
179
+ if open_fence is not None:
180
+ if fence is not None and fence[0] == open_fence[0]:
181
+ stripped = body.lstrip(" ")
182
+ run_length = len(stripped) - len(stripped.lstrip(fence[0]))
183
+ if run_length >= open_fence[1] and not stripped[run_length:].strip():
184
+ open_fence = None
185
+ elif fence is not None:
186
+ open_fence = fence
187
+ elif body == _AGENTS_BEGIN:
188
+ begins.append(offset)
189
+ elif body == _AGENTS_END:
190
+ ends.append((offset, offset + len(line)))
191
+ offset += len(line)
192
+ return begins, ends
193
+
194
+
195
+ def _merge_agents(existing: str, section: str) -> str:
196
+ begins, ends = _owned_marker_offsets(existing)
197
+ if not begins and not ends:
198
+ if not existing:
199
+ return section
200
+ separator = "\n" if existing.endswith("\n") else "\n\n"
201
+ return f"{existing}{separator}{section}"
202
+ if len(begins) != 1 or len(ends) != 1 or begins[0] >= ends[0][0]:
203
+ raise ValueError("AGENTS.md contains malformed Solwyn section markers.")
204
+ return f"{existing[: begins[0]]}{section}{existing[ends[0][1] :]}"
205
+
206
+
207
+ def _fsync_staged_file(file_fd: int) -> None:
208
+ os.fsync(file_fd)
209
+
210
+
211
+ def _fsync_directory(directory_fd: int) -> None:
212
+ os.fsync(directory_fd)
213
+
214
+
215
+ def _open_artifact_parent(
216
+ repository: Path, relative_path: Path
217
+ ) -> tuple[int, Path, tuple[int, int]]:
218
+ current_path = repository
219
+ current_fd = os.open(repository, _DIRECTORY_FLAGS)
220
+ try:
221
+ for component in relative_path.parent.parts:
222
+ created = False
223
+ try:
224
+ os.mkdir(component, mode=0o755, dir_fd=current_fd)
225
+ created = True
226
+ except FileExistsError:
227
+ pass
228
+ next_fd = os.open(component, _DIRECTORY_FLAGS, dir_fd=current_fd)
229
+ if created:
230
+ _fsync_directory(current_fd)
231
+ os.close(current_fd)
232
+ current_fd = next_fd
233
+ current_path = current_path / component
234
+ metadata = os.fstat(current_fd)
235
+ if not stat.S_ISDIR(metadata.st_mode):
236
+ raise OSError(f"Agent artifact parent is not a directory: {current_path}")
237
+ return current_fd, current_path, (metadata.st_dev, metadata.st_ino)
238
+ except BaseException:
239
+ os.close(current_fd)
240
+ raise
241
+
242
+
243
+ def _file_identity(metadata: os.stat_result) -> _FILE_IDENTITY:
244
+ return (
245
+ metadata.st_dev,
246
+ metadata.st_ino,
247
+ metadata.st_mode,
248
+ metadata.st_size,
249
+ metadata.st_mtime_ns,
250
+ metadata.st_ctime_ns,
251
+ )
252
+
253
+
254
+ def _target_identity(parent_fd: int, target_name: str) -> _FILE_IDENTITY | None:
255
+ try:
256
+ metadata = os.stat(target_name, dir_fd=parent_fd, follow_symlinks=False)
257
+ except FileNotFoundError:
258
+ return None
259
+ return _file_identity(metadata)
260
+
261
+
262
+ def _validate_initial_target(state: _ArtifactState) -> None:
263
+ identity = state.original_identity
264
+ if identity is not None and not stat.S_ISREG(identity[2]):
265
+ raise OSError(
266
+ f"Refusing non-regular agent artifact target: {state.relative_path}"
267
+ )
268
+
269
+
270
+ def _read_agents(state: _ArtifactState) -> str:
271
+ if state.original_identity is None:
272
+ return ""
273
+ flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_CLOEXEC", 0)
274
+ file_fd = os.open(state.target_name, flags, dir_fd=state.parent_fd)
275
+ try:
276
+ if _file_identity(os.fstat(file_fd)) != state.original_identity:
277
+ raise OSError(
278
+ f"Agent artifact changed during agent artifact write: "
279
+ f"{state.relative_path}"
280
+ )
281
+ with os.fdopen(file_fd, encoding="utf-8", newline="") as artifact_file:
282
+ file_fd = -1
283
+ return artifact_file.read()
284
+ finally:
285
+ if file_fd >= 0:
286
+ os.close(file_fd)
287
+
288
+
289
+ def _unique_transaction_name(kind: str, target_name: str) -> str:
290
+ return f"{_TRANSACTION_PREFIX}{kind}-{target_name}-{secrets.token_hex(8)}"
291
+
292
+
293
+ def _unlink_at(parent_fd: int, name: str | None) -> None:
294
+ if name is None:
295
+ return
296
+ try:
297
+ os.unlink(name, dir_fd=parent_fd)
298
+ except FileNotFoundError:
299
+ pass
300
+
301
+
302
+ def _stage_artifact(state: _ArtifactState) -> str:
303
+ temporary_name = _unique_transaction_name("stage", state.target_name)
304
+ mode = 0o644
305
+ if state.original_identity is not None:
306
+ mode = stat.S_IMODE(state.original_identity[2]) & 0o777
307
+ flags = (
308
+ os.O_WRONLY
309
+ | os.O_CREAT
310
+ | os.O_EXCL
311
+ | getattr(os, "O_NOFOLLOW", 0)
312
+ | getattr(os, "O_CLOEXEC", 0)
313
+ )
314
+ file_fd = -1
315
+ try:
316
+ file_fd = os.open(
317
+ temporary_name,
318
+ flags,
319
+ mode,
320
+ dir_fd=state.parent_fd,
321
+ )
322
+ os.fchmod(file_fd, mode)
323
+ with os.fdopen(
324
+ file_fd, mode="w", encoding="utf-8", newline=""
325
+ ) as temporary_file:
326
+ file_fd = -1
327
+ temporary_file.write(state.content)
328
+ temporary_file.flush()
329
+ _fsync_staged_file(temporary_file.fileno())
330
+ return temporary_name
331
+ except BaseException:
332
+ if file_fd >= 0:
333
+ os.close(file_fd)
334
+ _unlink_at(state.parent_fd, temporary_name)
335
+ raise
336
+
337
+
338
+ def _assert_parent_stable(state: _ArtifactState) -> None:
339
+ descriptor_metadata = os.fstat(state.parent_fd)
340
+ try:
341
+ path_metadata = os.stat(state.parent_path, follow_symlinks=False)
342
+ except FileNotFoundError:
343
+ path_metadata = None
344
+ descriptor_identity = (descriptor_metadata.st_dev, descriptor_metadata.st_ino)
345
+ path_identity = (
346
+ (path_metadata.st_dev, path_metadata.st_ino)
347
+ if path_metadata is not None and stat.S_ISDIR(path_metadata.st_mode)
348
+ else None
349
+ )
350
+ if (
351
+ descriptor_identity != state.parent_identity
352
+ or path_identity != state.parent_identity
353
+ ):
354
+ raise OSError(
355
+ f"Agent artifact parent changed during agent artifact write: "
356
+ f"{state.relative_path.parent}"
357
+ )
358
+
359
+
360
+ def _assert_target_stable(state: _ArtifactState) -> None:
361
+ if _target_identity(state.parent_fd, state.target_name) != state.original_identity:
362
+ raise OSError(
363
+ f"Agent artifact changed during agent artifact write: {state.relative_path}"
364
+ )
365
+
366
+
367
+ def _same_file_version(
368
+ actual: _FILE_IDENTITY | None,
369
+ expected: _FILE_IDENTITY | None,
370
+ ) -> bool:
371
+ if actual is None or expected is None:
372
+ return actual is expected
373
+ return actual[:-1] == expected[:-1]
374
+
375
+
376
+ def _during_backup_copy(state: _ArtifactState) -> None:
377
+ """Test seam after opening the stable source and before copying its bytes."""
378
+
379
+
380
+ def _write_all(file_fd: int, content: bytes) -> None:
381
+ remaining = memoryview(content)
382
+ while remaining:
383
+ written = os.write(file_fd, remaining)
384
+ if written == 0:
385
+ raise OSError("Could not copy agent artifact backup.")
386
+ remaining = remaining[written:]
387
+
388
+
389
+ def _backup_artifact(state: _ArtifactState) -> str | None:
390
+ if state.original_identity is None:
391
+ return None
392
+ backup_name = _unique_transaction_name("backup", state.target_name)
393
+ source_fd = -1
394
+ backup_fd = -1
395
+ try:
396
+ source_flags = (
397
+ os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_CLOEXEC", 0)
398
+ )
399
+ source_fd = os.open(
400
+ state.target_name,
401
+ source_flags,
402
+ dir_fd=state.parent_fd,
403
+ )
404
+ if (
405
+ _file_identity(os.fstat(source_fd)) != state.original_identity
406
+ or _target_identity(state.parent_fd, state.target_name)
407
+ != state.original_identity
408
+ ):
409
+ raise OSError(
410
+ f"Agent artifact changed during agent artifact write: "
411
+ f"{state.relative_path}"
412
+ )
413
+
414
+ backup_flags = (
415
+ os.O_WRONLY
416
+ | os.O_CREAT
417
+ | os.O_EXCL
418
+ | getattr(os, "O_NOFOLLOW", 0)
419
+ | getattr(os, "O_CLOEXEC", 0)
420
+ )
421
+ mode = stat.S_IMODE(state.original_identity[2]) & 0o777
422
+ backup_fd = os.open(
423
+ backup_name,
424
+ backup_flags,
425
+ mode,
426
+ dir_fd=state.parent_fd,
427
+ )
428
+ os.fchmod(backup_fd, mode)
429
+ _during_backup_copy(state)
430
+ while chunk := os.read(source_fd, 128 * 1024):
431
+ _write_all(backup_fd, chunk)
432
+ _fsync_staged_file(backup_fd)
433
+
434
+ source_identity = _file_identity(os.fstat(source_fd))
435
+ backup_identity = _target_identity(state.parent_fd, backup_name)
436
+ backup_descriptor_identity = _file_identity(os.fstat(backup_fd))
437
+ if (
438
+ source_identity != state.original_identity
439
+ or _target_identity(state.parent_fd, state.target_name)
440
+ != state.original_identity
441
+ or backup_identity is None
442
+ or backup_identity != backup_descriptor_identity
443
+ or not stat.S_ISREG(backup_identity[2])
444
+ or backup_identity[:2] == source_identity[:2]
445
+ or backup_identity[3] != state.original_identity[3]
446
+ ):
447
+ raise OSError(
448
+ f"Agent artifact changed during agent artifact write: "
449
+ f"{state.relative_path}"
450
+ )
451
+ return backup_name
452
+ except BaseException:
453
+ _unlink_at(state.parent_fd, backup_name)
454
+ raise
455
+ finally:
456
+ if source_fd >= 0:
457
+ os.close(source_fd)
458
+ if backup_fd >= 0:
459
+ os.close(backup_fd)
460
+
461
+
462
+ def _before_artifact_commit() -> None:
463
+ """Test seam immediately before stable-parent and target revalidation."""
464
+
465
+
466
+ def _cleanup_transaction(states: list[_ArtifactState]) -> None:
467
+ for state in states:
468
+ if not state.retain_temporary_for_recovery:
469
+ _unlink_at(state.parent_fd, state.temporary_name)
470
+ if not state.retain_backup_for_recovery:
471
+ _unlink_at(state.parent_fd, state.backup_name)
472
+
473
+
474
+ def _state_recovery_paths(state: _ArtifactState) -> list[Path]:
475
+ names = []
476
+ if state.retain_backup_for_recovery and state.backup_name is not None:
477
+ names.append(state.backup_name)
478
+ if state.retain_temporary_for_recovery and state.temporary_name is not None:
479
+ names.append(state.temporary_name)
480
+ if state.rollback_recovery_name is not None:
481
+ names.append(state.rollback_recovery_name)
482
+ return [state.relative_path.parent / name for name in names]
483
+
484
+
485
+ def _rollback_transaction(states: list[_ArtifactState]) -> None:
486
+ rollback_error: OSError | None = None
487
+ conflict_paths: list[Path] = [
488
+ state.relative_path for state in states if state.conflict_detected
489
+ ]
490
+ for state in reversed(states):
491
+ if not state.committed:
492
+ continue
493
+ if state.original_identity is not None and state.backup_name is not None:
494
+ try:
495
+ _atomic_exchange(
496
+ state.backup_name,
497
+ state.target_name,
498
+ state.parent_fd,
499
+ )
500
+ except OSError as error:
501
+ state.retain_backup_for_recovery = True
502
+ rollback_error = rollback_error or error
503
+ continue
504
+ displaced_identity = _target_identity(
505
+ state.parent_fd,
506
+ state.backup_name,
507
+ )
508
+ if _same_file_version(displaced_identity, state.installed_identity):
509
+ _unlink_at(state.parent_fd, state.backup_name)
510
+ state.backup_name = None
511
+ _fsync_directory(state.parent_fd)
512
+ continue
513
+ try:
514
+ _atomic_exchange(
515
+ state.backup_name,
516
+ state.target_name,
517
+ state.parent_fd,
518
+ )
519
+ except OSError as error:
520
+ state.rollback_recovery_name = state.backup_name
521
+ state.backup_name = None
522
+ rollback_error = rollback_error or error
523
+ else:
524
+ state.retain_backup_for_recovery = True
525
+ state.conflict_detected = True
526
+ if state.relative_path not in conflict_paths:
527
+ conflict_paths.append(state.relative_path)
528
+ continue
529
+
530
+ tombstone = _unique_transaction_name("rollback", state.target_name)
531
+ try:
532
+ _atomic_noreplace(state.target_name, tombstone, state.parent_fd)
533
+ except FileNotFoundError:
534
+ state.conflict_detected = True
535
+ if state.relative_path not in conflict_paths:
536
+ conflict_paths.append(state.relative_path)
537
+ continue
538
+ except OSError as error:
539
+ rollback_error = rollback_error or error
540
+ continue
541
+ displaced_identity = _target_identity(state.parent_fd, tombstone)
542
+ if _same_file_version(displaced_identity, state.installed_identity):
543
+ _unlink_at(state.parent_fd, tombstone)
544
+ _fsync_directory(state.parent_fd)
545
+ continue
546
+ try:
547
+ _atomic_noreplace(tombstone, state.target_name, state.parent_fd)
548
+ except OSError as error:
549
+ state.rollback_recovery_name = tombstone
550
+ rollback_error = rollback_error or error
551
+ state.conflict_detected = True
552
+ if state.relative_path not in conflict_paths:
553
+ conflict_paths.append(state.relative_path)
554
+ _cleanup_transaction(states)
555
+ for state in states:
556
+ try:
557
+ _fsync_directory(state.parent_fd)
558
+ except OSError as error:
559
+ rollback_error = rollback_error or error
560
+ recovery_paths = tuple(
561
+ path for state in states for path in _state_recovery_paths(state)
562
+ )
563
+ ordered_conflicts = tuple(dict.fromkeys(conflict_paths))
564
+ if recovery_paths or ordered_conflicts:
565
+ raise ArtifactRecoveryError(
566
+ recovery_paths,
567
+ ordered_conflicts,
568
+ ) from rollback_error
569
+ if rollback_error is not None:
570
+ raise OSError(
571
+ "Could not roll back agent artifact transaction."
572
+ ) from rollback_error
573
+
574
+
575
+ def _close_parents(states: list[_ArtifactState]) -> None:
576
+ for state in states:
577
+ os.close(state.parent_fd)
578
+
579
+
580
+ def write_agent_artifacts(root: Path) -> None:
581
+ """Transactionally write artifacts using POSIX no-follow directory handles."""
582
+ if os.name != "posix":
583
+ raise OSError("Agent artifact writes require a POSIX filesystem.")
584
+ repository = root.resolve(strict=True)
585
+ rendered = {
586
+ CLAUDE_SKILL_PATH: render_surface(ArtifactSurface.CLAUDE_SKILL),
587
+ CURSOR_RULE_PATH: render_surface(ArtifactSurface.CURSOR_RULE),
588
+ }
589
+ states: list[_ArtifactState] = []
590
+ commit_started = False
591
+ try:
592
+ parent_fd, parent_path, parent_identity = _open_artifact_parent(
593
+ repository, AGENTS_PATH
594
+ )
595
+ agents_state = _ArtifactState(
596
+ relative_path=AGENTS_PATH,
597
+ parent_path=parent_path,
598
+ parent_fd=parent_fd,
599
+ parent_identity=parent_identity,
600
+ target_name=AGENTS_PATH.name,
601
+ original_identity=_target_identity(parent_fd, AGENTS_PATH.name),
602
+ content="",
603
+ )
604
+ states.append(agents_state)
605
+ _validate_initial_target(agents_state)
606
+ agents_state.content = _merge_agents(
607
+ _read_agents(agents_state),
608
+ render_surface(ArtifactSurface.AGENTS_SECTION),
609
+ )
610
+
611
+ for relative_path in (CLAUDE_SKILL_PATH, CURSOR_RULE_PATH):
612
+ parent_fd, parent_path, parent_identity = _open_artifact_parent(
613
+ repository, relative_path
614
+ )
615
+ state = _ArtifactState(
616
+ relative_path=relative_path,
617
+ parent_path=parent_path,
618
+ parent_fd=parent_fd,
619
+ parent_identity=parent_identity,
620
+ target_name=relative_path.name,
621
+ original_identity=_target_identity(parent_fd, relative_path.name),
622
+ content=rendered.get(relative_path, ""),
623
+ )
624
+ states.append(state)
625
+ _validate_initial_target(state)
626
+ states = [states[1], states[2], states[0]]
627
+
628
+ for state in states:
629
+ state.temporary_name = _stage_artifact(state)
630
+ for state in states:
631
+ _fsync_directory(state.parent_fd)
632
+
633
+ _before_artifact_commit()
634
+ for state in states:
635
+ _assert_parent_stable(state)
636
+ _assert_target_stable(state)
637
+ state.backup_name = _backup_artifact(state)
638
+ for state in states:
639
+ _fsync_directory(state.parent_fd)
640
+ for state in states:
641
+ _assert_parent_stable(state)
642
+ _assert_target_stable(state)
643
+ for state in states:
644
+ commit_started = True
645
+ if state.temporary_name is None:
646
+ raise OSError("Agent artifact stage is missing.")
647
+ staged_identity = _target_identity(
648
+ state.parent_fd,
649
+ state.temporary_name,
650
+ )
651
+ if staged_identity is None:
652
+ raise OSError("Agent artifact stage is missing.")
653
+ state.installed_identity = staged_identity
654
+ if state.original_identity is None:
655
+ try:
656
+ _atomic_noreplace(
657
+ state.temporary_name,
658
+ state.target_name,
659
+ state.parent_fd,
660
+ )
661
+ except FileExistsError:
662
+ state.conflict_detected = True
663
+ raise OSError(
664
+ f"Agent artifact changed during agent artifact write: "
665
+ f"{state.relative_path}"
666
+ ) from None
667
+ state.temporary_name = None
668
+ state.committed = True
669
+ else:
670
+ _atomic_exchange(
671
+ state.temporary_name,
672
+ state.target_name,
673
+ state.parent_fd,
674
+ )
675
+ state.committed = True
676
+ displaced_identity = _target_identity(
677
+ state.parent_fd,
678
+ state.temporary_name,
679
+ )
680
+ if not _same_file_version(
681
+ displaced_identity,
682
+ state.original_identity,
683
+ ):
684
+ state.conflict_detected = True
685
+ state.retain_backup_for_recovery = True
686
+ try:
687
+ _atomic_exchange(
688
+ state.temporary_name,
689
+ state.target_name,
690
+ state.parent_fd,
691
+ )
692
+ except OSError:
693
+ state.retain_temporary_for_recovery = True
694
+ else:
695
+ state.committed = False
696
+ raise OSError(
697
+ f"Agent artifact changed during agent artifact write: "
698
+ f"{state.relative_path}"
699
+ )
700
+ installed_identity = _target_identity(
701
+ state.parent_fd,
702
+ state.target_name,
703
+ )
704
+ if installed_identity is None or not _same_file_version(
705
+ installed_identity, staged_identity
706
+ ):
707
+ raise OSError(
708
+ f"Agent artifact changed during agent artifact write: "
709
+ f"{state.relative_path}"
710
+ )
711
+ state.installed_identity = installed_identity
712
+ if state.temporary_name is not None:
713
+ _unlink_at(state.parent_fd, state.temporary_name)
714
+ state.temporary_name = None
715
+ _fsync_directory(state.parent_fd)
716
+ _assert_parent_stable(state)
717
+ _cleanup_transaction(states)
718
+ for state in states:
719
+ _fsync_directory(state.parent_fd)
720
+ except BaseException:
721
+ if commit_started:
722
+ _rollback_transaction(states)
723
+ else:
724
+ _cleanup_transaction(states)
725
+ raise
726
+ finally:
727
+ _close_parents(states)