@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.
- package/out/mcp-server.js +93 -20
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/orchestrator.md +6 -2
- package/resources/claude-customizations/.claude/agents/pr-author.md +78 -0
- package/resources/claude-customizations/.claude/agents/task-researcher.md +10 -6
- package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +68 -10
- package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +28 -1
- package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +40 -14
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +210 -0
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +153 -10
- package/resources/claude-customizations/.claude/hooks/validate-pr-author-output.ps1 +136 -0
- package/resources/claude-customizations/.claude/hooks/validate-task-researcher-output.ps1 +22 -6
- package/resources/claude-customizations/.claude/settings.json +22 -0
- package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +0 -1
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +76 -2
- package/resources/claude-customizations/.claude/skills/research-issue/SKILL.md +5 -3
- package/resources/claude-customizations/.claude-variants/csharp-legacy/agents/csharp-typed-engineer.md +69 -0
- package/resources/claude-customizations/.claude-variants/csharp-legacy/rules/csharp.md +96 -0
- package/resources/claude-customizations/.claude-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +77 -0
- package/resources/claude-customizations/.claude-variants/csharp-legacy/skills/invoke-csharp-engineer/SKILL.md +64 -0
- package/resources/claude-customizations/pack-manifests/core.json +65 -0
- package/resources/claude-customizations/pack-manifests/csharp-legacy.json +11 -0
- package/resources/claude-customizations/pack-manifests/csharp-modern.json +12 -0
- package/resources/claude-customizations/pack-manifests/powershell.json +14 -0
- package/resources/claude-customizations/pack-manifests/python.json +14 -0
- package/resources/claude-customizations/pack-manifests/typescript.json +9 -0
- package/resources/codex-and-agents-customizations/.agents/skills/feature-promotion-lifecycle/SKILL.md +30 -13
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +113 -10
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +33 -8
- package/resources/codex-and-agents-customizations/.agents/skills/repo-automation-adapter/SKILL.md +12 -7
- package/resources/codex-and-agents-customizations/.codex/config.toml +65 -6
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-checkpoint-monotonic.ps1 +303 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-consistency.ps1 +300 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +148 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-pr-author-skill.ps1 +336 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-promotion-mcp-only.ps1 +83 -17
- package/resources/config/orchestration-routing.json +2 -10
- package/resources/customizations/.github/agents/pr-author.agent.md +25 -0
- package/resources/customizations/.github/agents/task-researcher.agent.md +4 -4
- package/resources/customizations/.github/prompts/fillout-prd-feature.prompt.md +1 -1
- package/resources/customizations/.github/prompts/research-issue.prompt.md +4 -3
- package/resources/scripts/dev_tools/push_down_claude_customizations.py +253 -224
- package/resources/scripts/dev_tools/push_down_claude_filesystem.py +472 -0
- package/resources/scripts/dev_tools/push_down_claude_pack_selection.py +401 -0
- package/resources/scripts/dev_tools/validate_orchestrator_state.py +106 -29
- package/resources/scripts/dev_tools/validate_policy_audit_artifact.py +24 -0
- 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)
|