@danmoisan/drm-copilot-mcp 0.0.5 → 0.0.6

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 (47) hide show
  1. package/out/mcp-server.js +93 -20
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agents/orchestrator.md +6 -2
  4. package/resources/claude-customizations/.claude/agents/pr-author.md +78 -0
  5. package/resources/claude-customizations/.claude/agents/task-researcher.md +10 -6
  6. package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +68 -10
  7. package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +28 -1
  8. package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +40 -14
  9. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +210 -0
  10. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +153 -10
  11. package/resources/claude-customizations/.claude/hooks/validate-pr-author-output.ps1 +136 -0
  12. package/resources/claude-customizations/.claude/hooks/validate-task-researcher-output.ps1 +22 -6
  13. package/resources/claude-customizations/.claude/settings.json +22 -0
  14. package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +0 -1
  15. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +76 -2
  16. package/resources/claude-customizations/.claude/skills/research-issue/SKILL.md +5 -3
  17. package/resources/claude-customizations/.claude-variants/csharp-legacy/agents/csharp-typed-engineer.md +69 -0
  18. package/resources/claude-customizations/.claude-variants/csharp-legacy/rules/csharp.md +96 -0
  19. package/resources/claude-customizations/.claude-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +77 -0
  20. package/resources/claude-customizations/.claude-variants/csharp-legacy/skills/invoke-csharp-engineer/SKILL.md +64 -0
  21. package/resources/claude-customizations/pack-manifests/core.json +65 -0
  22. package/resources/claude-customizations/pack-manifests/csharp-legacy.json +11 -0
  23. package/resources/claude-customizations/pack-manifests/csharp-modern.json +12 -0
  24. package/resources/claude-customizations/pack-manifests/powershell.json +14 -0
  25. package/resources/claude-customizations/pack-manifests/python.json +14 -0
  26. package/resources/claude-customizations/pack-manifests/typescript.json +9 -0
  27. package/resources/codex-and-agents-customizations/.agents/skills/feature-promotion-lifecycle/SKILL.md +30 -13
  28. package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +113 -10
  29. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +33 -8
  30. package/resources/codex-and-agents-customizations/.agents/skills/repo-automation-adapter/SKILL.md +12 -7
  31. package/resources/codex-and-agents-customizations/.codex/config.toml +65 -6
  32. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-checkpoint-monotonic.ps1 +303 -0
  33. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-consistency.ps1 +300 -0
  34. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +148 -0
  35. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-pr-author-skill.ps1 +336 -0
  36. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-promotion-mcp-only.ps1 +83 -17
  37. package/resources/config/orchestration-routing.json +2 -10
  38. package/resources/customizations/.github/agents/pr-author.agent.md +25 -0
  39. package/resources/customizations/.github/agents/task-researcher.agent.md +4 -4
  40. package/resources/customizations/.github/prompts/fillout-prd-feature.prompt.md +1 -1
  41. package/resources/customizations/.github/prompts/research-issue.prompt.md +4 -3
  42. package/resources/scripts/dev_tools/push_down_claude_customizations.py +253 -224
  43. package/resources/scripts/dev_tools/push_down_claude_filesystem.py +472 -0
  44. package/resources/scripts/dev_tools/push_down_claude_pack_selection.py +401 -0
  45. package/resources/scripts/dev_tools/validate_orchestrator_state.py +106 -29
  46. package/resources/scripts/dev_tools/validate_policy_audit_artifact.py +24 -0
  47. package/resources/templates/push_down_claude_customizations.py +128 -304
@@ -0,0 +1,472 @@
1
+ """Filtering filesystem wrapper for the `.claude` customization push-down.
2
+
3
+ Purpose:
4
+ Provide the filesystem adapter that presents a filtered, variant-aware view
5
+ of the source `.claude` tree to the shared publisher engine. The entry point
6
+ (`scripts.dev_tools.push_down_claude_customizations`) composes this wrapper
7
+ with the pack-selection helpers and delegates the copy to the shared engine.
8
+
9
+ Responsibilities:
10
+ - Read the agent-memory scope leaf (`metadata.scope`) from YAML frontmatter
11
+ without a runtime YAML dependency.
12
+ - Decide per-file inclusion for the general-vs-repo memory scope filter.
13
+ - Wrap an inner ``PushDownFileSystem`` to apply host-file exclusions, pack
14
+ selection, C# legacy variant source redirection, and the memory mode.
15
+
16
+ Usage:
17
+ Instantiated by the push-down entry point with the inner adapter, the source
18
+ root, and the optional selection inputs. The result is passed as the ``fs``
19
+ argument to the shared engine.
20
+
21
+ Invariants / Constraints:
22
+ - Destination derivation in the engine relies on
23
+ ``source.relative_to(source_root)``; this wrapper preserves the canonical
24
+ destination path while optionally redirecting the read source.
25
+ - The memory scope filter reads file content only for agent-memory
26
+ candidates; all other files skip the read.
27
+
28
+ Side Effects:
29
+ Delegates all I/O to the inner adapter.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import re
35
+ from pathlib import Path
36
+ from typing import TYPE_CHECKING
37
+
38
+ try:
39
+ from scripts.dev_tools.push_down_claude_pack_selection import (
40
+ CSHARP_CANONICAL_PATHS,
41
+ CSharpVariant,
42
+ MemoryMode,
43
+ resolve_variant_source_path,
44
+ )
45
+ except ModuleNotFoundError as error: # pragma: no cover - bundled import fallback
46
+ if error.name is None or not error.name.startswith("scripts"):
47
+ raise
48
+ from dev_tools.push_down_claude_pack_selection import (
49
+ CSHARP_CANONICAL_PATHS,
50
+ CSharpVariant,
51
+ MemoryMode,
52
+ resolve_variant_source_path,
53
+ )
54
+
55
+ if TYPE_CHECKING:
56
+ from scripts.dev_tools.push_down_copilot_customizations_filesystem import (
57
+ PushDownFileSystem,
58
+ )
59
+
60
+ AGENT_MEMORY_RELATIVE_ROOT = Path(".claude/agent-memory")
61
+ GENERAL_MEMORY_SCOPE = "general"
62
+ REPO_MEMORY_SCOPE = "repo"
63
+
64
+ # Match a leading YAML frontmatter block: the first `---` line, the block body,
65
+ # and the closing `---` line. DOTALL lets the body span multiple lines.
66
+ _FRONTMATTER_PATTERN = re.compile(
67
+ r"\A---[ \t]*\r?\n(.*?)\r?\n---[ \t]*(?:\r?\n|\Z)", re.DOTALL
68
+ )
69
+ # Match a `metadata:` mapping key at column zero, then capture the indented
70
+ # block lines that belong to it (lines that are more-indented or blank) until
71
+ # the next column-zero key or end of the frontmatter body.
72
+ _METADATA_BLOCK_PATTERN = re.compile(
73
+ r"^metadata:[ \t]*\r?\n((?:[ \t]+.*(?:\r?\n|\Z)|\r?\n)*)",
74
+ re.MULTILINE,
75
+ )
76
+ # Match a `scope:` leaf inside the metadata block, capturing its scalar value
77
+ # up to an optional inline comment. Surrounding quotes are stripped later.
78
+ _SCOPE_LEAF_PATTERN = re.compile(
79
+ r"^[ \t]+scope:[ \t]*([^\r\n#]*)",
80
+ re.MULTILINE,
81
+ )
82
+
83
+
84
+ def read_memory_scope(content: str) -> str:
85
+ """Return the declared memory scope from a file's YAML frontmatter.
86
+
87
+ Purpose:
88
+ Extract the `metadata.scope` leaf from the leading YAML frontmatter
89
+ block using a narrow `re`-based parser. This avoids adding a runtime
90
+ YAML dependency (PyYAML) while reading only the single leaf the
91
+ push-down scope filter requires.
92
+
93
+ Args:
94
+ content (str): The full text of a candidate memory file, including any
95
+ leading `---` frontmatter block.
96
+
97
+ Returns:
98
+ str: ``"general"`` only when the frontmatter contains a `metadata:`
99
+ mapping whose `scope:` leaf is exactly ``general`` (quotes and inline
100
+ comments stripped). In every other case — missing frontmatter, no
101
+ closing `---`, no `metadata:` block, no `scope:` leaf, or any value
102
+ other than exactly ``general`` — it returns ``"repo"`` as the fail-safe
103
+ default so nothing leaks by accident.
104
+
105
+ Raises:
106
+ None.
107
+
108
+ Side Effects:
109
+ None.
110
+ """
111
+
112
+ # Isolate the leading frontmatter block; absent or unterminated frontmatter
113
+ # fails safe to the repo scope so unmarked files are never distributed.
114
+ frontmatter_match = _FRONTMATTER_PATTERN.match(content)
115
+ if frontmatter_match is None:
116
+ return REPO_MEMORY_SCOPE
117
+ frontmatter_body = frontmatter_match.group(1)
118
+
119
+ # Locate the metadata mapping; without it there is no scope leaf to read.
120
+ metadata_match = _METADATA_BLOCK_PATTERN.search(frontmatter_body)
121
+ if metadata_match is None:
122
+ return REPO_MEMORY_SCOPE
123
+ metadata_block = metadata_match.group(1)
124
+
125
+ # Read the scope leaf from within the metadata block only; a top-level
126
+ # `scope:` outside `metadata:` is intentionally ignored.
127
+ scope_match = _SCOPE_LEAF_PATTERN.search(metadata_block)
128
+ if scope_match is None:
129
+ return REPO_MEMORY_SCOPE
130
+
131
+ # Strip surrounding whitespace and optional matching quotes before the
132
+ # exact-match comparison; only an exact `general` is treated as general.
133
+ scope_value = scope_match.group(1).strip()
134
+ if (
135
+ len(scope_value) >= 2
136
+ and scope_value[0] == scope_value[-1]
137
+ and scope_value[0] in {'"', "'"}
138
+ ):
139
+ scope_value = scope_value[1:-1].strip()
140
+ if scope_value == GENERAL_MEMORY_SCOPE:
141
+ return GENERAL_MEMORY_SCOPE
142
+ return REPO_MEMORY_SCOPE
143
+
144
+
145
+ def is_general_memory_file(relative_path: Path, content: str) -> bool:
146
+ """Return whether a candidate file may be distributed by push-down.
147
+
148
+ Purpose:
149
+ Decide inclusion for one source file. Files under
150
+ `.claude/agent-memory/` are distributed only when general-scoped;
151
+ every other file is always distributed and is unaffected by the scope
152
+ filter.
153
+
154
+ Args:
155
+ relative_path (Path): The file path relative to the repository root.
156
+ content (str): The full text of the file, used to read the memory
157
+ scope when the path is under the agent-memory subtree.
158
+
159
+ Returns:
160
+ bool: ``True`` when the path is outside `.claude/agent-memory/`, or when
161
+ the path is under that subtree and `read_memory_scope(content)` is
162
+ exactly ``general``. ``False`` only for an agent-memory file whose
163
+ scope is not general (the fail-safe exclusion).
164
+
165
+ Raises:
166
+ None.
167
+
168
+ Side Effects:
169
+ None.
170
+ """
171
+
172
+ # Files outside the agent-memory subtree are always copied; the scope
173
+ # filter never applies to rules, skills, agents, hooks, or settings.
174
+ try:
175
+ relative_path.relative_to(AGENT_MEMORY_RELATIVE_ROOT)
176
+ except ValueError:
177
+ return True
178
+ return read_memory_scope(content) == GENERAL_MEMORY_SCOPE
179
+
180
+
181
+ class ExcludingFileSystem:
182
+ """Wrap a PushDownFileSystem with scope, pack, variant, and memory filters.
183
+
184
+ Purpose:
185
+ Present a filtered, variant-aware view of the source tree to the shared
186
+ publisher engine so the engine's destination derivation
187
+ (``source.relative_to(source_root)``) stays correct while the actual
188
+ published set, the per-file source content, and the agent-memory
189
+ handling all honor the selected packs, C# variant, and memory mode.
190
+
191
+ Responsibilities:
192
+ - Exclude host-specific files (for example `settings.local.json`).
193
+ - Apply the general-vs-repo agent-memory scope filter (unchanged).
194
+ - When a pack selection is active, restrict enumeration to the
195
+ `.claude`-relative destination paths in ``published_paths``.
196
+ - When the legacy C# variant is active, redirect reads of the four
197
+ canonical C# destination paths to the bundle-only legacy source so the
198
+ destination receives legacy content at the canonical path.
199
+ - Apply the memory mode: ``overwrite`` keeps prior behavior, ``skip``
200
+ excludes the entire `.claude/agent-memory/**` subtree, and ``merge``
201
+ excludes only general-scoped memories whose destination file already
202
+ exists.
203
+
204
+ Usage:
205
+ Instantiate with the inner adapter, the source root, and the optional
206
+ selection inputs; pass the result as the `fs` argument to the engine.
207
+
208
+ Invariants / Constraints:
209
+ Excluded paths are resolved once at construction time for O(1) checks.
210
+ Content reads occur only for agent-memory candidates (scope filter) and
211
+ for legacy variant reads (source redirection). When ``published_paths``
212
+ is ``None`` the pack filter is inert and behavior matches the prior
213
+ publish-everything contract.
214
+
215
+ Side Effects:
216
+ Delegates all I/O to the inner adapter.
217
+ """
218
+
219
+ def __init__(
220
+ self,
221
+ inner: PushDownFileSystem,
222
+ repo_root: Path,
223
+ excluded: tuple[Path, ...],
224
+ *,
225
+ source_root: Path | None = None,
226
+ destination_root: Path | None = None,
227
+ published_paths: frozenset[str] | None = None,
228
+ csharp_variant: CSharpVariant = "modern",
229
+ memory_mode: MemoryMode = "overwrite",
230
+ variant_root: Path | None = None,
231
+ ) -> None:
232
+ """Set up the adapter and resolve filter inputs against the source root.
233
+
234
+ Args:
235
+ inner (PushDownFileSystem): The wrapped adapter performing real I/O.
236
+ repo_root (Path): Repository root used to resolve excluded paths and
237
+ the agent-memory scope prefix.
238
+ excluded (tuple[Path, ...]): Repo-relative paths to always exclude.
239
+ source_root (Path | None): Root the engine enumerates from; defaults
240
+ to ``repo_root``. Used to map between absolute paths and
241
+ `.claude`-relative paths for pack filtering and variant reads.
242
+ destination_root (Path | None): Destination workspace root, required
243
+ for the ``merge`` memory mode's destination-existence check.
244
+ published_paths (frozenset[str] | None): The `.claude`-relative paths
245
+ to publish, or ``None`` to publish everything (no pack filter).
246
+ csharp_variant (CSharpVariant): Selected C# variant; ``"legacy"``
247
+ redirects canonical C# reads to the legacy source.
248
+ memory_mode (MemoryMode): One of ``overwrite``/``merge``/``skip``.
249
+ variant_root (Path | None): Bundle root that holds the legacy variant
250
+ subtree (``.claude-variants/csharp-legacy/``). Defaults to the
251
+ source root when not supplied. The repository CLI passes the
252
+ nested bundle directory; the template passes its bundle root.
253
+ """
254
+ self._inner = inner
255
+ # Retain the resolved repo root so per-file scope checks can derive the
256
+ # repo-relative path needed by the agent-memory scope filter.
257
+ self._repo_root = repo_root.resolve()
258
+ # Keep both the unresolved and resolved source roots. The resolved root
259
+ # is used for relative-path comparisons (so absolute candidate paths
260
+ # match), while the unresolved root is used to build redirected read
261
+ # paths so they live in the same key space the engine enumerated from.
262
+ self._source_root_raw = source_root or repo_root
263
+ self._source_root = self._source_root_raw.resolve()
264
+ # The variant root is the bundle directory that contains the legacy
265
+ # variant subtree; legacy C# reads are redirected beneath it.
266
+ self._variant_root_raw = variant_root or self._source_root_raw
267
+ self._destination_root = destination_root
268
+ self._published_paths = published_paths
269
+ self._csharp_variant: CSharpVariant = csharp_variant
270
+ self._memory_mode: MemoryMode = memory_mode
271
+ # Resolve once at init so list_files per-path checks are O(1).
272
+ self._excluded: frozenset[Path] = frozenset(
273
+ (repo_root / p).resolve() for p in excluded
274
+ )
275
+
276
+ def _source_relative_posix(self, path: Path) -> str | None:
277
+ """Return the source-relative POSIX path, or None when outside the root.
278
+
279
+ Args:
280
+ path (Path): An absolute candidate path from the inner adapter.
281
+
282
+ Returns:
283
+ str | None: The path relative to the source root as a POSIX string,
284
+ or ``None`` when the path is not under the source root.
285
+ """
286
+
287
+ try:
288
+ return path.resolve().relative_to(self._source_root).as_posix()
289
+ except ValueError:
290
+ return None
291
+
292
+ def _is_pack_included(self, path: Path) -> bool:
293
+ """Return whether a candidate path is in the active published set.
294
+
295
+ Args:
296
+ path (Path): An absolute candidate path from the inner adapter.
297
+
298
+ Returns:
299
+ bool: ``True`` when no pack filter is active (publish everything) or
300
+ when the path's `.claude`-relative form is in ``published_paths``.
301
+ """
302
+
303
+ # A None published set means no --packs was supplied, so the pack filter
304
+ # is inert and every enumerated file is included.
305
+ if self._published_paths is None:
306
+ return True
307
+ relative = self._source_relative_posix(path)
308
+ if relative is None:
309
+ return True
310
+ return relative in self._published_paths
311
+
312
+ def _is_scope_included(self, path: Path) -> bool:
313
+ """Return whether a candidate file passes the agent-memory scope filter.
314
+
315
+ Purpose:
316
+ Apply the general-vs-repo memory scope decision to one enumerated
317
+ file, reading its content only when the path is under the
318
+ agent-memory subtree.
319
+
320
+ Args:
321
+ path (Path): The absolute candidate path returned by the inner
322
+ adapter's ``list_files``.
323
+
324
+ Returns:
325
+ bool: ``True`` when the file is outside `.claude/agent-memory/` or
326
+ is a general-scoped memory; ``False`` for a non-general memory.
327
+
328
+ Raises:
329
+ None.
330
+
331
+ Side Effects:
332
+ Reads file content via the inner adapter for agent-memory
333
+ candidates only.
334
+ """
335
+
336
+ # Derive the repo-relative path so the agent-memory check matches the
337
+ # `.claude/agent-memory/` prefix regardless of the absolute location.
338
+ try:
339
+ relative_path = path.resolve().relative_to(self._repo_root)
340
+ except ValueError:
341
+ # A path outside the repo root cannot be an agent memory; include it.
342
+ return True
343
+
344
+ # Skip the content read entirely for files outside the memory subtree.
345
+ try:
346
+ relative_path.relative_to(AGENT_MEMORY_RELATIVE_ROOT)
347
+ except ValueError:
348
+ return True
349
+
350
+ content = self.read_text(path)
351
+ return is_general_memory_file(relative_path, content)
352
+
353
+ def _is_memory_mode_included(self, path: Path) -> bool:
354
+ """Return whether a candidate file passes the memory-mode filter.
355
+
356
+ Purpose:
357
+ Apply the selected memory mode to agent-memory files. ``overwrite``
358
+ includes every memory (prior behavior); ``skip`` excludes the entire
359
+ agent-memory subtree; ``merge`` excludes only memories whose
360
+ destination file already exists so existing destination memories are
361
+ preserved and new ones are still written.
362
+
363
+ Args:
364
+ path (Path): An absolute candidate path from the inner adapter.
365
+
366
+ Returns:
367
+ bool: ``True`` when the file should be published under the active
368
+ memory mode; ``False`` when the memory mode excludes it.
369
+
370
+ Side Effects:
371
+ For ``merge``, reads destination existence via the inner adapter.
372
+ """
373
+
374
+ # Derive the source-relative path so the agent-memory prefix check is
375
+ # independent of the absolute source location.
376
+ relative = self._source_relative_posix(path)
377
+ if relative is None:
378
+ return True
379
+
380
+ # Only agent-memory files are affected by the memory mode; all other
381
+ # files are always included regardless of the mode.
382
+ try:
383
+ Path(relative).relative_to(AGENT_MEMORY_RELATIVE_ROOT)
384
+ except ValueError:
385
+ return True
386
+
387
+ # Route by memory mode: skip drops all memories; merge keeps only those
388
+ # absent at the destination; overwrite (default) keeps everything.
389
+ if self._memory_mode == "skip":
390
+ return False
391
+ if self._memory_mode == "merge":
392
+ if self._destination_root is None:
393
+ return True
394
+ destination_path = self._destination_root / relative
395
+ # Exclude memories that already exist at the destination so merge
396
+ # never overwrites an existing destination memory file.
397
+ return not self._inner.is_file(destination_path)
398
+ return True
399
+
400
+ def _resolve_read_source(self, path: Path) -> Path:
401
+ """Return the actual source path to read for a requested path.
402
+
403
+ Purpose:
404
+ Redirect reads of the four canonical C# destination paths to the
405
+ bundle-only legacy source when the legacy variant is selected, so
406
+ the destination receives legacy content at the canonical path.
407
+
408
+ Args:
409
+ path (Path): The absolute path the engine asked to read.
410
+
411
+ Returns:
412
+ Path: The redirected legacy source path for canonical C# files under
413
+ the legacy variant; otherwise the original ``path`` unchanged.
414
+ """
415
+
416
+ # Variant redirection only applies to the legacy C# variant; modern and
417
+ # all non-C# reads pass through unchanged.
418
+ if self._csharp_variant != "legacy":
419
+ return path
420
+ relative = self._source_relative_posix(path)
421
+ if relative is None or relative not in CSHARP_CANONICAL_PATHS:
422
+ return path
423
+ redirected_relative = resolve_variant_source_path(relative, "legacy")
424
+ # The resolver returns a `.claude-variants/csharp-legacy/...` path. Join
425
+ # it under the variant (bundle) root, dropping the leading
426
+ # `.claude-variants` segment which the bundle root already implies via
427
+ # the resolver-produced tail. Build from the unresolved root so the path
428
+ # lives in the same key space the engine enumerated from.
429
+ return self._variant_root_raw / redirected_relative
430
+
431
+ def list_files(self, root: Path) -> list[Path]:
432
+ """Return inner list_files output with all active filters applied.
433
+
434
+ Drops paths in ``EXCLUDED_RELATIVE_PATHS``, any agent-memory file that
435
+ is not general-scoped, any file outside the active published-pack set,
436
+ and any agent-memory file excluded by the selected memory mode.
437
+ """
438
+
439
+ # Apply the four enumeration filters in sequence: hard exclusions, pack
440
+ # selection, agent-memory scope, then memory mode.
441
+ return [
442
+ p
443
+ for p in self._inner.list_files(root)
444
+ if p.resolve() not in self._excluded
445
+ and self._is_pack_included(p)
446
+ and self._is_scope_included(p)
447
+ and self._is_memory_mode_included(p)
448
+ ]
449
+
450
+ def is_dir(self, path: Path) -> bool:
451
+ """Delegate to inner adapter."""
452
+ return self._inner.is_dir(path)
453
+
454
+ def is_file(self, path: Path) -> bool:
455
+ """Delegate to inner adapter."""
456
+ return self._inner.is_file(path)
457
+
458
+ def read_text(self, path: Path) -> str:
459
+ """Read text, redirecting canonical C# reads to the legacy source.
460
+
461
+ For the legacy C# variant, a read of a canonical C# destination path is
462
+ served from the bundle-only legacy source; all other reads pass through.
463
+ """
464
+ return self._inner.read_text(self._resolve_read_source(path))
465
+
466
+ def write_text(self, path: Path, content: str) -> None:
467
+ """Delegate to inner adapter."""
468
+ self._inner.write_text(path, content)
469
+
470
+ def ensure_dir(self, path: Path) -> None:
471
+ """Delegate to inner adapter."""
472
+ self._inner.ensure_dir(path)