@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
@@ -1,50 +1,38 @@
1
- """Publish bundled `.claude` content into a destination workspace.
1
+ """Bundled template entry point for the `.claude` customization push-down.
2
2
 
3
3
  Purpose:
4
- Provide a dedicated public entry point for the Claude customization push-down
5
- workflow while reusing the shared publisher engine behind the existing
6
- `.github` customization flow. Settings-local configuration is excluded from
7
- push-down because it holds host-specific overrides that must not propagate.
8
-
9
- Agent-memory files under `.claude/agent-memory/` are filtered by a
10
- content-based scope check: only memories whose frontmatter declares
11
- `metadata.scope: general` are distributed to a destination workspace. A
12
- memory with an absent, malformed, or unrecognized scope is treated as
13
- `repo` and excluded (fail-safe default), so repository-specific memories do
14
- not leak into consumer workspaces. Files outside `.claude/agent-memory/`
15
- are copied verbatim and are never affected by the scope filter.
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.push_down_copilot_customizations`` nor the fallback
57
- ``dev_tools.push_down_copilot_customizations`` import resolves because
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
- Purpose:
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
- Side Effects:
140
- None.
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
- Decide inclusion for one source file. Files under
181
- `.claude/agent-memory/` are distributed only when general-scoped;
182
- every other file is always distributed and is unaffected by the scope
183
- filter.
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
- relative_path (Path): The file path relative to the repository root.
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
- bool: ``True`` when the path is outside `.claude/agent-memory/`, or when
192
- the path is under that subtree and `_read_memory_scope(content)` is
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
- None.
111
+ SystemExit: Raised by ``argparse`` when arguments are invalid.
198
112
 
199
113
  Side Effects:
200
- None.
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. This mirrors
388
- # the codex template (`push_down_codex_and_agents_customizations.py`), which
389
- # sets its source root to the sibling bundled directory. The optional
390
- # `repo_root` argument still allows tests to inject an explicit root.
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
- summary = push_down_customizations(
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