@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
|
@@ -1,50 +1,38 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Bundled template entry point for the `.claude` customization push-down.
|
|
2
2
|
|
|
3
3
|
Purpose:
|
|
4
|
-
Provide
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
4
|
+
Provide the extension-invoked CLI that publishes the bundled `.claude`
|
|
5
|
+
payload into a destination workspace. This template bootstraps the bundled
|
|
6
|
+
``resources/scripts`` directory onto ``sys.path`` and then delegates all
|
|
7
|
+
pack-selection, C# variant-routing, and memory-mode logic to the bundled
|
|
8
|
+
``push_down_claude_customizations`` engine module so the template and the
|
|
9
|
+
repository source stay behaviorally identical.
|
|
10
|
+
|
|
11
|
+
Settings-local configuration is excluded from push-down because it holds
|
|
12
|
+
host-specific overrides. Agent-memory files under `.claude/agent-memory/`
|
|
13
|
+
are filtered by the engine's content-based scope check; only general-scoped
|
|
14
|
+
memories are distributed.
|
|
15
|
+
|
|
16
|
+
Responsibilities:
|
|
17
|
+
- Ensure the bundled scripts directory is importable when invoked directly
|
|
18
|
+
from ``resources/templates/``.
|
|
19
|
+
- Parse the CLI arguments (``--destination``, ``--packs``,
|
|
20
|
+
``--csharp-variant``, ``--memory-mode``).
|
|
21
|
+
- Resolve the source root and bundle root to the sibling bundled
|
|
22
|
+
``claude-customizations`` directory so the template distributes the
|
|
23
|
+
bundled payload rather than a destination workspace's own `.claude`.
|
|
24
|
+
|
|
25
|
+
Side Effects:
|
|
26
|
+
Mutates ``sys.path`` at import time; reads bundled source files and writes
|
|
27
|
+
destination files through the publisher engine.
|
|
16
28
|
"""
|
|
17
29
|
|
|
18
30
|
from __future__ import annotations
|
|
19
31
|
|
|
20
32
|
import argparse
|
|
21
|
-
import re
|
|
22
33
|
import sys
|
|
23
34
|
from pathlib import Path
|
|
24
35
|
|
|
25
|
-
AGENT_MEMORY_RELATIVE_ROOT = Path(".claude/agent-memory")
|
|
26
|
-
GENERAL_MEMORY_SCOPE = "general"
|
|
27
|
-
REPO_MEMORY_SCOPE = "repo"
|
|
28
|
-
|
|
29
|
-
# Match a leading YAML frontmatter block: the first `---` line, the block body,
|
|
30
|
-
# and the closing `---` line. DOTALL lets the body span multiple lines.
|
|
31
|
-
_FRONTMATTER_PATTERN = re.compile(
|
|
32
|
-
r"\A---[ \t]*\r?\n(.*?)\r?\n---[ \t]*(?:\r?\n|\Z)", re.DOTALL
|
|
33
|
-
)
|
|
34
|
-
# Match a `metadata:` mapping key at column zero, then capture the indented
|
|
35
|
-
# block lines that belong to it (lines that are more-indented or blank) until
|
|
36
|
-
# the next column-zero key or end of the frontmatter body.
|
|
37
|
-
_METADATA_BLOCK_PATTERN = re.compile(
|
|
38
|
-
r"^metadata:[ \t]*\r?\n((?:[ \t]+.*(?:\r?\n|\Z)|\r?\n)*)",
|
|
39
|
-
re.MULTILINE,
|
|
40
|
-
)
|
|
41
|
-
# Match a `scope:` leaf inside the metadata block, capturing its scalar value
|
|
42
|
-
# up to an optional inline comment. Surrounding quotes are stripped later.
|
|
43
|
-
_SCOPE_LEAF_PATTERN = re.compile(
|
|
44
|
-
r"^[ \t]+scope:[ \t]*([^\r\n#]*)",
|
|
45
|
-
re.MULTILINE,
|
|
46
|
-
)
|
|
47
|
-
|
|
48
36
|
|
|
49
37
|
def _ensure_bundled_scripts_import_path() -> None:
|
|
50
38
|
"""Prepend bundled ``resources/scripts`` directory to ``sys.path``.
|
|
@@ -53,8 +41,8 @@ def _ensure_bundled_scripts_import_path() -> None:
|
|
|
53
41
|
Make extension-bundled Python packages importable when this template is
|
|
54
42
|
invoked directly from ``resources/templates/`` rather than from the
|
|
55
43
|
repository root. Without this bootstrap, neither
|
|
56
|
-
``scripts.dev_tools.
|
|
57
|
-
``dev_tools.
|
|
44
|
+
``scripts.dev_tools.push_down_claude_customizations`` nor the fallback
|
|
45
|
+
``dev_tools.push_down_claude_customizations`` import resolves because
|
|
58
46
|
``resources/scripts`` is not on ``sys.path`` by default.
|
|
59
47
|
|
|
60
48
|
Side Effects:
|
|
@@ -71,294 +59,60 @@ def _ensure_bundled_scripts_import_path() -> None:
|
|
|
71
59
|
_ensure_bundled_scripts_import_path()
|
|
72
60
|
|
|
73
61
|
try:
|
|
62
|
+
from scripts.dev_tools.push_down_claude_customizations import (
|
|
63
|
+
CSHARP_VARIANT_CHOICES,
|
|
64
|
+
MEMORY_MODE_CHOICES,
|
|
65
|
+
PushDownSummary,
|
|
66
|
+
push_down_customizations,
|
|
67
|
+
)
|
|
74
68
|
from scripts.dev_tools.push_down_copilot_customizations import (
|
|
75
69
|
PushDownFileSystem,
|
|
76
|
-
PushDownSummary,
|
|
77
70
|
RealPushDownFileSystem,
|
|
78
71
|
resolve_cli_path,
|
|
79
72
|
)
|
|
80
|
-
from scripts.dev_tools.push_down_copilot_customizations import (
|
|
81
|
-
push_down_customizations as push_down_scoped_customizations,
|
|
82
|
-
)
|
|
83
73
|
except ModuleNotFoundError as error:
|
|
84
74
|
if error.name is None or not error.name.startswith("scripts"):
|
|
85
75
|
raise
|
|
76
|
+
from dev_tools.push_down_claude_customizations import (
|
|
77
|
+
CSHARP_VARIANT_CHOICES,
|
|
78
|
+
MEMORY_MODE_CHOICES,
|
|
79
|
+
PushDownSummary,
|
|
80
|
+
push_down_customizations,
|
|
81
|
+
)
|
|
86
82
|
from dev_tools.push_down_copilot_customizations import (
|
|
87
83
|
PushDownFileSystem,
|
|
88
|
-
PushDownSummary,
|
|
89
84
|
RealPushDownFileSystem,
|
|
90
85
|
resolve_cli_path,
|
|
91
86
|
)
|
|
92
|
-
from dev_tools.push_down_copilot_customizations import (
|
|
93
|
-
push_down_customizations as push_down_scoped_customizations,
|
|
94
|
-
)
|
|
95
87
|
|
|
96
|
-
ARTIFACT_DIRECTORY = "artifacts/claude-customizations"
|
|
97
88
|
MODULE_ENTRY_POINT = "scripts.dev_tools.push_down_claude_customizations"
|
|
98
|
-
ROOT_FOLDERS: tuple[Path, ...] = (Path(".claude"),)
|
|
99
|
-
EXCLUDED_RELATIVE_PATHS: tuple[Path, ...] = (Path(".claude/settings.local.json"),)
|
|
100
|
-
|
|
101
|
-
__all__ = [
|
|
102
|
-
"AGENT_MEMORY_RELATIVE_ROOT",
|
|
103
|
-
"ARTIFACT_DIRECTORY",
|
|
104
|
-
"EXCLUDED_RELATIVE_PATHS",
|
|
105
|
-
"GENERAL_MEMORY_SCOPE",
|
|
106
|
-
"PushDownSummary",
|
|
107
|
-
"REPO_MEMORY_SCOPE",
|
|
108
|
-
"ROOT_FOLDERS",
|
|
109
|
-
"main",
|
|
110
|
-
"parse_args",
|
|
111
|
-
"push_down_customizations",
|
|
112
|
-
]
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def _read_memory_scope(content: str) -> str:
|
|
116
|
-
"""Return the declared memory scope from a file's YAML frontmatter.
|
|
117
89
|
|
|
118
|
-
|
|
119
|
-
Extract the `metadata.scope` leaf from the leading YAML frontmatter
|
|
120
|
-
block using a narrow `re`-based parser. This avoids adding a runtime
|
|
121
|
-
YAML dependency (PyYAML) while reading only the single leaf the
|
|
122
|
-
push-down scope filter requires.
|
|
123
|
-
|
|
124
|
-
Args:
|
|
125
|
-
content (str): The full text of a candidate memory file, including any
|
|
126
|
-
leading `---` frontmatter block.
|
|
127
|
-
|
|
128
|
-
Returns:
|
|
129
|
-
str: ``"general"`` only when the frontmatter contains a `metadata:`
|
|
130
|
-
mapping whose `scope:` leaf is exactly ``general`` (quotes and inline
|
|
131
|
-
comments stripped). In every other case — missing frontmatter, no
|
|
132
|
-
closing `---`, no `metadata:` block, no `scope:` leaf, or any value
|
|
133
|
-
other than exactly ``general`` — it returns ``"repo"`` as the fail-safe
|
|
134
|
-
default so nothing leaks by accident.
|
|
90
|
+
__all__ = ["main", "parse_args"]
|
|
135
91
|
|
|
136
|
-
Raises:
|
|
137
|
-
None.
|
|
138
92
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
"""
|
|
142
|
-
|
|
143
|
-
# Isolate the leading frontmatter block; absent or unterminated frontmatter
|
|
144
|
-
# fails safe to the repo scope so unmarked files are never distributed.
|
|
145
|
-
frontmatter_match = _FRONTMATTER_PATTERN.match(content)
|
|
146
|
-
if frontmatter_match is None:
|
|
147
|
-
return REPO_MEMORY_SCOPE
|
|
148
|
-
frontmatter_body = frontmatter_match.group(1)
|
|
149
|
-
|
|
150
|
-
# Locate the metadata mapping; without it there is no scope leaf to read.
|
|
151
|
-
metadata_match = _METADATA_BLOCK_PATTERN.search(frontmatter_body)
|
|
152
|
-
if metadata_match is None:
|
|
153
|
-
return REPO_MEMORY_SCOPE
|
|
154
|
-
metadata_block = metadata_match.group(1)
|
|
155
|
-
|
|
156
|
-
# Read the scope leaf from within the metadata block only; a top-level
|
|
157
|
-
# `scope:` outside `metadata:` is intentionally ignored.
|
|
158
|
-
scope_match = _SCOPE_LEAF_PATTERN.search(metadata_block)
|
|
159
|
-
if scope_match is None:
|
|
160
|
-
return REPO_MEMORY_SCOPE
|
|
161
|
-
|
|
162
|
-
# Strip surrounding whitespace and optional matching quotes before the
|
|
163
|
-
# exact-match comparison; only an exact `general` is treated as general.
|
|
164
|
-
scope_value = scope_match.group(1).strip()
|
|
165
|
-
if (
|
|
166
|
-
len(scope_value) >= 2
|
|
167
|
-
and scope_value[0] == scope_value[-1]
|
|
168
|
-
and scope_value[0] in {'"', "'"}
|
|
169
|
-
):
|
|
170
|
-
scope_value = scope_value[1:-1].strip()
|
|
171
|
-
if scope_value == GENERAL_MEMORY_SCOPE:
|
|
172
|
-
return GENERAL_MEMORY_SCOPE
|
|
173
|
-
return REPO_MEMORY_SCOPE
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def _is_general_memory_file(relative_path: Path, content: str) -> bool:
|
|
177
|
-
"""Return whether a candidate file may be distributed by push-down.
|
|
93
|
+
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
94
|
+
"""Parse CLI arguments for the bundled Claude customization push-down.
|
|
178
95
|
|
|
179
96
|
Purpose:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
97
|
+
Mirror the repository entry point's CLI contract, including the optional
|
|
98
|
+
``--packs``, ``--csharp-variant``, and ``--memory-mode`` flags. With no
|
|
99
|
+
optional flags the parsed namespace yields the backward-compatible
|
|
100
|
+
defaults (``packs=None``, ``csharp_variant="modern"``,
|
|
101
|
+
``memory_mode="overwrite"``).
|
|
184
102
|
|
|
185
103
|
Args:
|
|
186
|
-
|
|
187
|
-
content (str): The full text of the file, used to read the memory
|
|
188
|
-
scope when the path is under the agent-memory subtree.
|
|
104
|
+
argv (list[str] | None): Optional argument list for tests or embedding.
|
|
189
105
|
|
|
190
106
|
Returns:
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
exactly ``general``. ``False`` only for an agent-memory file whose
|
|
194
|
-
scope is not general (the fail-safe exclusion).
|
|
107
|
+
argparse.Namespace: Parsed arguments with attributes ``destination``,
|
|
108
|
+
``packs``, ``csharp_variant``, and ``memory_mode``.
|
|
195
109
|
|
|
196
110
|
Raises:
|
|
197
|
-
|
|
111
|
+
SystemExit: Raised by ``argparse`` when arguments are invalid.
|
|
198
112
|
|
|
199
113
|
Side Effects:
|
|
200
|
-
|
|
114
|
+
Emits usage/help text through ``argparse`` when parsing fails.
|
|
201
115
|
"""
|
|
202
|
-
|
|
203
|
-
# Files outside the agent-memory subtree are always copied; the scope
|
|
204
|
-
# filter never applies to rules, skills, agents, hooks, or settings.
|
|
205
|
-
try:
|
|
206
|
-
relative_path.relative_to(AGENT_MEMORY_RELATIVE_ROOT)
|
|
207
|
-
except ValueError:
|
|
208
|
-
return True
|
|
209
|
-
return _read_memory_scope(content) == GENERAL_MEMORY_SCOPE
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
class _ExcludingFileSystem:
|
|
213
|
-
"""Wrap a PushDownFileSystem and filter specified paths from list_files.
|
|
214
|
-
|
|
215
|
-
Purpose:
|
|
216
|
-
Prevent host-specific files (e.g. `settings.local.json`) from being
|
|
217
|
-
included when the publisher enumerates source files, and exclude
|
|
218
|
-
repository-specific agent memories so only general-scoped memories are
|
|
219
|
-
distributed. Chosen over a post-enumeration filter so the exclusion
|
|
220
|
-
travels with the filesystem contract and is transparent to the shared
|
|
221
|
-
engine.
|
|
222
|
-
|
|
223
|
-
Usage:
|
|
224
|
-
Instantiate with the inner adapter and exclusion paths relative to
|
|
225
|
-
the repo root; pass the result as the `fs` argument to the engine.
|
|
226
|
-
|
|
227
|
-
Invariants / Constraints:
|
|
228
|
-
Excluded paths are resolved once at construction time for O(1) checks.
|
|
229
|
-
The content-based scope filter reads file content only for candidates
|
|
230
|
-
under `.claude/agent-memory/`; all other files skip the read.
|
|
231
|
-
|
|
232
|
-
Side Effects:
|
|
233
|
-
Delegates all I/O to the inner adapter.
|
|
234
|
-
"""
|
|
235
|
-
|
|
236
|
-
def __init__(
|
|
237
|
-
self, inner: PushDownFileSystem, repo_root: Path, excluded: tuple[Path, ...]
|
|
238
|
-
) -> None:
|
|
239
|
-
"""Set up the adapter; resolve exclusion paths relative to repo_root."""
|
|
240
|
-
self._inner = inner
|
|
241
|
-
# Retain the resolved repo root so per-file scope checks can derive the
|
|
242
|
-
# repo-relative path needed by the agent-memory scope filter.
|
|
243
|
-
self._repo_root = repo_root.resolve()
|
|
244
|
-
# Resolve once at init so list_files per-path checks are O(1).
|
|
245
|
-
self._excluded: frozenset[Path] = frozenset(
|
|
246
|
-
(repo_root / p).resolve() for p in excluded
|
|
247
|
-
)
|
|
248
|
-
|
|
249
|
-
def _is_scope_included(self, path: Path) -> bool:
|
|
250
|
-
"""Return whether a candidate file passes the agent-memory scope filter.
|
|
251
|
-
|
|
252
|
-
Purpose:
|
|
253
|
-
Apply the general-vs-repo memory scope decision to one enumerated
|
|
254
|
-
file, reading its content only when the path is under the
|
|
255
|
-
agent-memory subtree.
|
|
256
|
-
|
|
257
|
-
Args:
|
|
258
|
-
path (Path): The absolute candidate path returned by the inner
|
|
259
|
-
adapter's ``list_files``.
|
|
260
|
-
|
|
261
|
-
Returns:
|
|
262
|
-
bool: ``True`` when the file is outside `.claude/agent-memory/` or
|
|
263
|
-
is a general-scoped memory; ``False`` for a non-general memory.
|
|
264
|
-
|
|
265
|
-
Raises:
|
|
266
|
-
None.
|
|
267
|
-
|
|
268
|
-
Side Effects:
|
|
269
|
-
Reads file content via the inner adapter for agent-memory
|
|
270
|
-
candidates only.
|
|
271
|
-
"""
|
|
272
|
-
|
|
273
|
-
# Derive the repo-relative path so the agent-memory check matches the
|
|
274
|
-
# `.claude/agent-memory/` prefix regardless of the absolute location.
|
|
275
|
-
try:
|
|
276
|
-
relative_path = path.resolve().relative_to(self._repo_root)
|
|
277
|
-
except ValueError:
|
|
278
|
-
# A path outside the repo root cannot be an agent memory; include it.
|
|
279
|
-
return True
|
|
280
|
-
|
|
281
|
-
# Skip the content read entirely for files outside the memory subtree.
|
|
282
|
-
try:
|
|
283
|
-
relative_path.relative_to(AGENT_MEMORY_RELATIVE_ROOT)
|
|
284
|
-
except ValueError:
|
|
285
|
-
return True
|
|
286
|
-
|
|
287
|
-
content = self._inner.read_text(path)
|
|
288
|
-
return _is_general_memory_file(relative_path, content)
|
|
289
|
-
|
|
290
|
-
def list_files(self, root: Path) -> list[Path]:
|
|
291
|
-
"""Return inner list_files output with excluded paths removed.
|
|
292
|
-
|
|
293
|
-
Drops paths in ``EXCLUDED_RELATIVE_PATHS`` and any agent-memory file
|
|
294
|
-
that is not general-scoped per the content-based scope filter.
|
|
295
|
-
"""
|
|
296
|
-
return [
|
|
297
|
-
p
|
|
298
|
-
for p in self._inner.list_files(root)
|
|
299
|
-
if p.resolve() not in self._excluded and self._is_scope_included(p)
|
|
300
|
-
]
|
|
301
|
-
|
|
302
|
-
def is_dir(self, path: Path) -> bool:
|
|
303
|
-
"""Delegate to inner adapter."""
|
|
304
|
-
return self._inner.is_dir(path)
|
|
305
|
-
|
|
306
|
-
def is_file(self, path: Path) -> bool:
|
|
307
|
-
"""Delegate to inner adapter."""
|
|
308
|
-
return self._inner.is_file(path)
|
|
309
|
-
|
|
310
|
-
def read_text(self, path: Path) -> str:
|
|
311
|
-
"""Delegate to inner adapter."""
|
|
312
|
-
return self._inner.read_text(path)
|
|
313
|
-
|
|
314
|
-
def write_text(self, path: Path, content: str) -> None:
|
|
315
|
-
"""Delegate to inner adapter."""
|
|
316
|
-
self._inner.write_text(path, content)
|
|
317
|
-
|
|
318
|
-
def ensure_dir(self, path: Path) -> None:
|
|
319
|
-
"""Delegate to inner adapter."""
|
|
320
|
-
self._inner.ensure_dir(path)
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
def _passthrough_rewrite(
|
|
324
|
-
text: str,
|
|
325
|
-
) -> tuple[str, int, int, list[str]]:
|
|
326
|
-
"""Return unmodified text for payloads that do not need command rewrites."""
|
|
327
|
-
|
|
328
|
-
return text, 0, 0, []
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
def push_down_customizations(
|
|
332
|
-
*,
|
|
333
|
-
repo_root: Path,
|
|
334
|
-
destination_root: Path,
|
|
335
|
-
fs: PushDownFileSystem,
|
|
336
|
-
source_root: Path | None = None,
|
|
337
|
-
artifact_root: Path | None = None,
|
|
338
|
-
) -> PushDownSummary:
|
|
339
|
-
"""Copy the `.claude` tree into the destination workspace.
|
|
340
|
-
|
|
341
|
-
Excludes paths in `EXCLUDED_RELATIVE_PATHS` by wrapping `fs` in
|
|
342
|
-
`_ExcludingFileSystem` before delegating to the shared engine.
|
|
343
|
-
"""
|
|
344
|
-
|
|
345
|
-
# Wrap the caller-supplied adapter so enumeration omits excluded paths.
|
|
346
|
-
excluding_fs = _ExcludingFileSystem(fs, repo_root, EXCLUDED_RELATIVE_PATHS)
|
|
347
|
-
return push_down_scoped_customizations(
|
|
348
|
-
repo_root=repo_root,
|
|
349
|
-
destination_root=destination_root,
|
|
350
|
-
fs=excluding_fs,
|
|
351
|
-
source_root=source_root,
|
|
352
|
-
artifact_root=artifact_root,
|
|
353
|
-
root_folders=ROOT_FOLDERS,
|
|
354
|
-
artifact_directory=ARTIFACT_DIRECTORY,
|
|
355
|
-
rewrite_references=_passthrough_rewrite,
|
|
356
|
-
)
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
360
|
-
"""Parse CLI arguments for the Claude customization push-down publisher."""
|
|
361
|
-
|
|
362
116
|
parser = argparse.ArgumentParser(
|
|
363
117
|
description=(
|
|
364
118
|
"Publish bundled Claude customizations with "
|
|
@@ -370,36 +124,106 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
|
370
124
|
required=True,
|
|
371
125
|
help=("Destination workspace root that will receive the copied .claude tree."),
|
|
372
126
|
)
|
|
127
|
+
parser.add_argument(
|
|
128
|
+
"--packs",
|
|
129
|
+
default=None,
|
|
130
|
+
help=(
|
|
131
|
+
"Comma-separated language pack names to publish (for example "
|
|
132
|
+
"'core,typescript'). When omitted, the full tree is published. "
|
|
133
|
+
"'core' is always included."
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
parser.add_argument(
|
|
137
|
+
"--csharp-variant",
|
|
138
|
+
dest="csharp_variant",
|
|
139
|
+
choices=CSHARP_VARIANT_CHOICES,
|
|
140
|
+
default="modern",
|
|
141
|
+
help="C# toolchain variant to source ('modern' default or 'legacy').",
|
|
142
|
+
)
|
|
143
|
+
parser.add_argument(
|
|
144
|
+
"--memory-mode",
|
|
145
|
+
dest="memory_mode",
|
|
146
|
+
choices=MEMORY_MODE_CHOICES,
|
|
147
|
+
default="overwrite",
|
|
148
|
+
help=("Agent-memory handling mode: 'overwrite' (default), 'merge', or 'skip'."),
|
|
149
|
+
)
|
|
373
150
|
return parser.parse_args(argv)
|
|
374
151
|
|
|
375
152
|
|
|
153
|
+
def _parse_packs_argument(packs_value: str | None) -> frozenset[str] | None:
|
|
154
|
+
"""Parse the raw ``--packs`` CLI value into a normalized pack-name set.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
packs_value (str | None): The raw comma-separated value, or ``None`` when
|
|
158
|
+
the flag was omitted.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
frozenset[str] | None: The set of non-empty, stripped pack names, or
|
|
162
|
+
``None`` when the flag was omitted or contained only empty entries (the
|
|
163
|
+
publish-everything default).
|
|
164
|
+
"""
|
|
165
|
+
if packs_value is None:
|
|
166
|
+
return None
|
|
167
|
+
# Strip whitespace and drop empty entries so trailing commas or stray spaces
|
|
168
|
+
# do not produce empty pack names.
|
|
169
|
+
names = {entry.strip() for entry in packs_value.split(",") if entry.strip()}
|
|
170
|
+
if not names:
|
|
171
|
+
return None
|
|
172
|
+
return frozenset(names)
|
|
173
|
+
|
|
174
|
+
|
|
376
175
|
def main(
|
|
377
176
|
argv: list[str] | None = None,
|
|
378
177
|
*,
|
|
379
178
|
repo_root: Path | None = None,
|
|
380
179
|
fs: PushDownFileSystem | None = None,
|
|
381
180
|
) -> int:
|
|
382
|
-
"""Run the Claude customization push-down publisher CLI.
|
|
181
|
+
"""Run the bundled Claude customization push-down publisher CLI.
|
|
182
|
+
|
|
183
|
+
Purpose:
|
|
184
|
+
Parse arguments, resolve the source and bundle roots to the sibling
|
|
185
|
+
bundled ``claude-customizations`` directory, thread the pack selection,
|
|
186
|
+
C# variant, and memory mode into the engine, and print the summary
|
|
187
|
+
artifact path. With no optional flags this is byte-for-byte equivalent
|
|
188
|
+
to the prior bundled publish-everything/overwrite behavior.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
argv (list[str] | None): Optional CLI argument list.
|
|
192
|
+
repo_root (Path | None): Optional explicit source root for tests; when
|
|
193
|
+
absent, the bundled ``claude-customizations`` directory is used.
|
|
194
|
+
fs (PushDownFileSystem | None): Optional filesystem adapter for tests.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
int: Process exit code (``0`` on success).
|
|
383
198
|
|
|
199
|
+
Side Effects:
|
|
200
|
+
Reads bundled source files and writes destination files and the summary
|
|
201
|
+
artifact through the adapter.
|
|
202
|
+
"""
|
|
384
203
|
args = parse_args(argv)
|
|
385
204
|
# Resolve the source root to the bundled customizations directory so the
|
|
386
205
|
# template distributes the bundled `.claude` payload rather than copying a
|
|
387
|
-
# destination workspace's existing `.claude` back onto itself.
|
|
388
|
-
# the
|
|
389
|
-
#
|
|
390
|
-
#
|
|
206
|
+
# destination workspace's existing `.claude` back onto itself. The bundle
|
|
207
|
+
# root that holds the pack manifests and legacy variant subtree is the same
|
|
208
|
+
# customizations directory. The optional `repo_root` argument still allows
|
|
209
|
+
# tests to inject an explicit root.
|
|
391
210
|
customizations_root = (
|
|
392
211
|
Path(__file__).resolve().parent.parent / "claude-customizations"
|
|
393
212
|
)
|
|
394
213
|
resolved_repo_root = resolve_cli_path(repo_root or customizations_root)
|
|
395
214
|
resolved_destination = resolve_cli_path(args.destination)
|
|
396
215
|
resolved_fs = fs or RealPushDownFileSystem()
|
|
397
|
-
|
|
216
|
+
packs = _parse_packs_argument(args.packs)
|
|
217
|
+
summary: PushDownSummary = push_down_customizations(
|
|
398
218
|
repo_root=resolved_repo_root,
|
|
399
219
|
destination_root=resolved_destination,
|
|
400
220
|
fs=resolved_fs,
|
|
401
221
|
source_root=resolved_repo_root,
|
|
402
222
|
artifact_root=resolved_repo_root,
|
|
223
|
+
packs=packs,
|
|
224
|
+
csharp_variant=args.csharp_variant,
|
|
225
|
+
memory_mode=args.memory_mode,
|
|
226
|
+
bundle_root=resolved_repo_root,
|
|
403
227
|
)
|
|
404
228
|
print(f"Wrote push-down summary artifact to: {summary.artifact_path}")
|
|
405
229
|
return 0
|