claude-dev-env 4.0.0 → 4.2.0

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 (32) hide show
  1. package/.agents/skills/everything-search/SKILL.md +18 -3
  2. package/.agents/skills/everything-search/scripts/everything_search.py +204 -0
  3. package/.agents/skills/everything-search/scripts/everything_search_command_constants/__init__.py +1 -0
  4. package/.agents/skills/everything-search/scripts/everything_search_command_constants/config/__init__.py +1 -0
  5. package/.agents/skills/everything-search/scripts/everything_search_command_constants/config/constants.py +11 -0
  6. package/.agents/skills/everything-search/scripts/test_everything_search.py +172 -0
  7. package/.agents/skills/syncing-submodules/SKILL.md +101 -0
  8. package/.agents/skills/syncing-submodules/reference/run-tasks.md +9 -0
  9. package/.agents/skills/syncing-submodules/scripts/submodule_sync.py +383 -0
  10. package/.agents/skills/syncing-submodules/scripts/submodule_sync_constants/__init__.py +1 -0
  11. package/.agents/skills/syncing-submodules/scripts/submodule_sync_constants/config/__init__.py +1 -0
  12. package/.agents/skills/syncing-submodules/scripts/submodule_sync_constants/config/constants.py +66 -0
  13. package/.agents/skills/syncing-submodules/scripts/sync_parent_pointer.py +88 -0
  14. package/.agents/skills/syncing-submodules/scripts/test_submodule_sync.py +303 -0
  15. package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +83 -0
  16. package/bin/ever-shipped-skills.mjs +1 -0
  17. package/bin/ever-shipped-skills.test.mjs +30 -0
  18. package/bin/git_hooks_installer.mjs +0 -1
  19. package/bin/git_hooks_installer.test.mjs +16 -1
  20. package/bin/install.mjs +5 -2
  21. package/bin/install.prune.test.mjs +95 -24
  22. package/bin/install.test.mjs +7 -0
  23. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +5 -8
  24. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +7 -13
  25. package/package.json +1 -1
  26. package/rules/filesystem-search.md +5 -5
  27. package/hooks/blocking/es_exe_path_rewriter.py +0 -159
  28. package/hooks/blocking/test_es_exe_path_rewriter.py +0 -447
  29. package/hooks/git-hooks/post_commit.py +0 -241
  30. package/hooks/git-hooks/test_post_commit.py +0 -298
  31. package/hooks/hooks_constants/path_rewriter_constants.py +0 -13
  32. package/hooks/hooks_constants/test_path_rewriter_constants.py +0 -57
@@ -13,13 +13,23 @@ Search files instantly on Windows using the Everything command-line interface (e
13
13
 
14
14
  ## Hard limits
15
15
 
16
- Every search carries a scope: a project path or registry token, an `ext:` filter, a `dm:` date filter, a `size:` filter, or a name pattern. A bare whole-drive scan or a network-share sweep is out of bounds — narrow the search to what you need.
16
+ Every search carries a scope: a project path or registry token, an `ext:` filter, a `dm:` date filter, a `size:` filter, or a name pattern. A bare whole-drive scan or a network-share sweep is out of bounds. Narrow the search to what you need.
17
17
 
18
18
  When `es.exe` fails or returns nothing, self-heal first: fall back to the `Glob` tool (name and path patterns) or `Grep` (file contents), and report the outage so the reader knows the index was unavailable. When self-healing also fails, ask the user through `AskUserQuestion` with a short analysis and next-step options.
19
19
 
20
20
  ## Registry tokens
21
21
 
22
- The `es_exe_path_rewriter` hook resolves scope tokens before the command runs. A `{project-name}` placeholder or a bare registry key from `~/.claude/project-paths.json` becomes its quoted absolute path in the command. The hook allows and rewrites — it never blocks — so a search scoped to a registered project names the project token and lets the hook fill in the path.
22
+ Run project-name searches through this command:
23
+
24
+ ```text
25
+ python "${CLAUDE_SKILL_DIR}/scripts/everything_search.py" <project-name> <search arguments>
26
+ ```
27
+
28
+ The command reads `~/.claude/project-paths.json`. An exact project name or `{project-name}` argument becomes one absolute path. Unknown names, absolute paths, flags, and spaces stay as they are. The command then starts `es.exe` without a shell.
29
+
30
+ Put the search scope first. The first argument names a project, path, file, extension, date, or size. An options-only request exits before `es.exe` starts. Use `-version` alone to read the installed Everything version.
31
+
32
+ Path and operator searches run when the registry file is missing. Malformed registry content, an empty search, a missing `es.exe`, or a process start failure returns a nonzero exit. After a successful start, the command returns the `es.exe` exit status.
23
33
 
24
34
  ## Instructions
25
35
 
@@ -131,7 +141,7 @@ Run separate searches for each extension type:
131
141
 
132
142
  - Everything must be running (system tray) for es.exe to work
133
143
  - Run one extension per search for cleaner results
134
- - Use `dm:` for recent file searches instead of manual date filtering
144
+ - Use `dm:` to filter recent files by the index timestamp
135
145
  - Combine with path to narrow scope
136
146
  - Results return instantly regardless of drive size
137
147
 
@@ -152,3 +162,8 @@ Everything indexes REAL paths only, not junctions or mapped drives.
152
162
  **Common drive mappings:**
153
163
  - `Z:\Projects\` -> `Y:\Work\Projects\`
154
164
  - When in Z:\, translate to Y:\ equivalent for Everything searches
165
+
166
+ ## Files
167
+
168
+ - `scripts/everything_search.py` runs one Everything search without a shell.
169
+ - `scripts/test_everything_search.py` verifies registry expansion and command failures.
@@ -0,0 +1,204 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import shutil
5
+ import subprocess
6
+ import sys
7
+ from collections.abc import Mapping, Sequence
8
+ from pathlib import Path, PurePosixPath, PureWindowsPath
9
+ from typing import TextIO
10
+
11
+ from everything_search_command_constants.config.constants import (
12
+ CLAUDE_DIRECTORY_NAME,
13
+ EXECUTABLE_NAME,
14
+ EXECUTION_ERROR_EXIT_CODE,
15
+ INFORMATIONAL_ARGUMENT,
16
+ INVALID_INPUT_EXIT_CODE,
17
+ PROJECT_PATHS_FILE_NAME,
18
+ REGISTRY_META_KEY,
19
+ SEARCH_SCOPE_REQUIRED_MESSAGE,
20
+ UTF8_ENCODING,
21
+ )
22
+
23
+
24
+ class RegistryRunFatal(ValueError):
25
+ """Stop the run when the project-path registry is unreadable or invalid."""
26
+
27
+
28
+ def load_registry(registry_path: Path) -> dict[str, str]:
29
+ """Read project names and absolute paths from a registry file.
30
+
31
+ Args:
32
+ registry_path: Registry file to read.
33
+
34
+ Returns:
35
+ The registered paths keyed by project name.
36
+
37
+ Raises:
38
+ RegistryRunFatal: The file is unreadable or its content is invalid.
39
+ """
40
+ if not registry_path.is_file():
41
+ return {}
42
+ try:
43
+ all_registry_entries = json.loads(
44
+ registry_path.read_text(encoding=UTF8_ENCODING)
45
+ )
46
+ except (OSError, UnicodeError, json.JSONDecodeError) as error:
47
+ raise RegistryRunFatal(f"Malformed registry: {error}") from error
48
+ return _validated_registry_entries(all_registry_entries)
49
+
50
+
51
+ def _validated_registry_entries(all_registry_entries: object) -> dict[str, str]:
52
+ if not isinstance(all_registry_entries, dict):
53
+ raise RegistryRunFatal("Invalid registry: expected an object")
54
+ all_registered_paths_by_name: dict[str, str] = {}
55
+ for each_registry_name, each_registered_path in all_registry_entries.items():
56
+ if each_registry_name == REGISTRY_META_KEY:
57
+ continue
58
+ if not isinstance(each_registry_name, str) or not isinstance(
59
+ each_registered_path, str
60
+ ):
61
+ raise RegistryRunFatal("Invalid registry: names and paths must be strings")
62
+ if not _is_absolute_path(each_registered_path):
63
+ raise RegistryRunFatal("Invalid registry: paths must be absolute")
64
+ all_registered_paths_by_name[each_registry_name] = each_registered_path
65
+ return all_registered_paths_by_name
66
+
67
+
68
+ def _is_absolute_path(search_argument: str) -> bool:
69
+ try:
70
+ return (
71
+ PureWindowsPath(search_argument).is_absolute()
72
+ or PurePosixPath(search_argument).is_absolute()
73
+ )
74
+ except ValueError:
75
+ return False
76
+
77
+
78
+ def _registry_name(search_argument: str) -> str:
79
+ if search_argument.startswith("{") and search_argument.endswith("}"):
80
+ return search_argument[1:-1]
81
+ return search_argument
82
+
83
+
84
+ def expand_search_arguments(
85
+ all_search_arguments: Sequence[str],
86
+ all_registered_paths_by_name: Mapping[str, str],
87
+ ) -> list[str]:
88
+ """Return the registered path for an exact project name or `{project-name}` token.
89
+
90
+ Unknown names, absolute paths, flags, and spaces stay as their own arguments.
91
+
92
+ Args:
93
+ all_search_arguments: Everything search arguments.
94
+ all_registered_paths_by_name: Absolute paths keyed by project name.
95
+
96
+ Returns:
97
+ Arguments with exact project names as their registered paths.
98
+ """
99
+ all_expanded_arguments: list[str] = []
100
+ for each_search_argument in all_search_arguments:
101
+ registry_name = _registry_name(each_search_argument)
102
+ if (
103
+ not _is_absolute_path(each_search_argument)
104
+ and registry_name in all_registered_paths_by_name
105
+ ):
106
+ all_expanded_arguments.append(all_registered_paths_by_name[registry_name])
107
+ else:
108
+ all_expanded_arguments.append(each_search_argument)
109
+ return all_expanded_arguments
110
+
111
+
112
+ def _run_search(
113
+ executable_path: str,
114
+ all_search_arguments: Sequence[str],
115
+ search_stdout: TextIO,
116
+ search_stderr: TextIO,
117
+ ) -> int:
118
+ try:
119
+ search_run = subprocess.run(
120
+ [executable_path, *all_search_arguments],
121
+ capture_output=True,
122
+ check=False,
123
+ encoding=UTF8_ENCODING,
124
+ shell=False,
125
+ text=True,
126
+ )
127
+ except OSError as error:
128
+ search_stderr.write(f"{error}\n")
129
+ return EXECUTION_ERROR_EXIT_CODE
130
+ search_stdout.write(search_run.stdout)
131
+ search_stderr.write(search_run.stderr)
132
+ return search_run.returncode
133
+
134
+
135
+ def _run_registered_search(
136
+ all_search_arguments: Sequence[str],
137
+ registry_path: Path,
138
+ executable_path: str | None,
139
+ search_stdout: TextIO,
140
+ search_stderr: TextIO,
141
+ ) -> int:
142
+ try:
143
+ all_registered_paths_by_name = load_registry(registry_path)
144
+ except RegistryRunFatal as error:
145
+ search_stderr.write(f"{error}\n")
146
+ return INVALID_INPUT_EXIT_CODE
147
+ selected_executable_path = executable_path or shutil.which(EXECUTABLE_NAME)
148
+ if selected_executable_path is None:
149
+ search_stderr.write(f"{EXECUTABLE_NAME} was not found.\n")
150
+ return EXECUTION_ERROR_EXIT_CODE
151
+ all_expanded_arguments = expand_search_arguments(
152
+ all_search_arguments, all_registered_paths_by_name
153
+ )
154
+ return _run_search(
155
+ selected_executable_path,
156
+ all_expanded_arguments,
157
+ search_stdout,
158
+ search_stderr,
159
+ )
160
+
161
+
162
+ def _has_search_scope(all_search_arguments: Sequence[str]) -> bool:
163
+ first_argument = all_search_arguments[0]
164
+ return bool(first_argument.strip()) and not first_argument.startswith("-")
165
+
166
+
167
+ def _is_informational_operation(all_search_arguments: Sequence[str]) -> bool:
168
+ return (
169
+ len(all_search_arguments) == 1
170
+ and all_search_arguments[0].casefold() == INFORMATIONAL_ARGUMENT
171
+ )
172
+
173
+
174
+ def main(all_search_arguments: Sequence[str]) -> int:
175
+ """Expand project names and run one Everything search.
176
+
177
+ Args:
178
+ all_search_arguments: Arguments passed to Everything.
179
+
180
+ Returns:
181
+ The Everything process exit status or a command error status.
182
+ """
183
+ if not all_search_arguments:
184
+ sys.stderr.write("At least one search argument is required.\n")
185
+ return INVALID_INPUT_EXIT_CODE
186
+ if not _has_search_scope(all_search_arguments) and not _is_informational_operation(
187
+ all_search_arguments
188
+ ):
189
+ sys.stderr.write(SEARCH_SCOPE_REQUIRED_MESSAGE)
190
+ return INVALID_INPUT_EXIT_CODE
191
+ selected_registry_path = (
192
+ Path.home() / CLAUDE_DIRECTORY_NAME / PROJECT_PATHS_FILE_NAME
193
+ )
194
+ return _run_registered_search(
195
+ all_search_arguments,
196
+ selected_registry_path,
197
+ None,
198
+ sys.stdout,
199
+ sys.stderr,
200
+ )
201
+
202
+
203
+ if __name__ == "__main__":
204
+ raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1 @@
1
+ """Constants for the Everything search command."""
@@ -0,0 +1 @@
1
+ """Configuration for the Everything search command."""
@@ -0,0 +1,11 @@
1
+ """Configuration constants for the Everything search command."""
2
+
3
+ CLAUDE_DIRECTORY_NAME = ".claude"
4
+ EXECUTABLE_NAME = "es.exe"
5
+ EXECUTION_ERROR_EXIT_CODE = 1
6
+ INFORMATIONAL_ARGUMENT = "-version"
7
+ INVALID_INPUT_EXIT_CODE = 2
8
+ PROJECT_PATHS_FILE_NAME = "project-paths.json"
9
+ REGISTRY_META_KEY = "_meta"
10
+ SEARCH_SCOPE_REQUIRED_MESSAGE = "The first search argument must define a path, project, name, extension, date, or size scope.\n"
11
+ UTF8_ENCODING = "utf-8"
@@ -0,0 +1,172 @@
1
+ import importlib
2
+ import io
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+ from unittest.mock import patch
7
+
8
+ import pytest
9
+
10
+ SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
11
+ if str(SCRIPTS_DIRECTORY) not in sys.path:
12
+ sys.path.insert(0, str(SCRIPTS_DIRECTORY))
13
+
14
+ search = importlib.import_module("everything_search")
15
+
16
+ KNOWN_PATH = "Y:\\Projects\\my-repo"
17
+ REGISTRY = {"my-repo": KNOWN_PATH}
18
+
19
+
20
+ def test_expand_search_arguments_rewrites_bare_registry_token() -> None:
21
+ assert search.expand_search_arguments(["my-repo", "config.py"], REGISTRY) == [
22
+ KNOWN_PATH,
23
+ "config.py",
24
+ ]
25
+
26
+
27
+ def test_expand_search_arguments_rewrites_exact_placeholder_token() -> None:
28
+ assert search.expand_search_arguments(["{my-repo}", "config.py"], REGISTRY) == [
29
+ KNOWN_PATH,
30
+ "config.py",
31
+ ]
32
+
33
+
34
+ def test_expand_search_arguments_preserves_unknown_tokens_and_spaces() -> None:
35
+ all_arguments = ["unknown-name", "foo bar", "config.py"]
36
+ assert search.expand_search_arguments(all_arguments, REGISTRY) == all_arguments
37
+
38
+
39
+ @pytest.mark.parametrize(
40
+ "absolute_path",
41
+ [
42
+ "C:/Users/example/file.py",
43
+ "\\\\server\\share\\folder",
44
+ "/mnt/c/Projects/repo",
45
+ ],
46
+ )
47
+ def test_expand_search_arguments_preserves_absolute_paths(absolute_path: str) -> None:
48
+ assert search.expand_search_arguments([absolute_path], REGISTRY) == [absolute_path]
49
+
50
+
51
+ def test_expand_search_arguments_preserves_embedded_flag_tokens() -> None:
52
+ all_arguments = ["--flag={my-repo}", "--regex=^{my-repo}$"]
53
+ assert search.expand_search_arguments(all_arguments, REGISTRY) == all_arguments
54
+
55
+
56
+ def test_load_registry_returns_empty_mapping_for_missing_file(tmp_path: Path) -> None:
57
+ assert search.load_registry(tmp_path / "missing.json") == {}
58
+
59
+
60
+ @pytest.mark.parametrize(
61
+ "registry_bytes",
62
+ [
63
+ b"\xff",
64
+ b"{not valid",
65
+ json.dumps({"my-repo": 42}).encode(),
66
+ json.dumps({"my-repo": ""}).encode(),
67
+ json.dumps({"my-repo": "relative/path"}).encode(),
68
+ ],
69
+ )
70
+ def test_load_registry_reports_invalid_input(
71
+ tmp_path: Path, registry_bytes: bytes
72
+ ) -> None:
73
+ registry_path = tmp_path / "project-paths.json"
74
+ registry_path.write_bytes(registry_bytes)
75
+ with pytest.raises(search.RegistryRunFatal, match="(Malformed|Invalid)"):
76
+ search.load_registry(registry_path)
77
+
78
+
79
+ def test_main_runs_direct_search_without_registry_or_shell() -> None:
80
+ completed_process = search.subprocess.CompletedProcess(
81
+ args=["es.exe", "foo bar"],
82
+ returncode=0,
83
+ stdout="found path\n",
84
+ stderr="",
85
+ )
86
+ captured_stdout = io.StringIO()
87
+ with (
88
+ patch.object(search, "load_registry", return_value={}),
89
+ patch.object(search.shutil, "which", return_value="es.exe"),
90
+ patch.object(
91
+ search.subprocess, "run", return_value=completed_process
92
+ ) as run_process,
93
+ patch.object(search.sys, "stdout", captured_stdout),
94
+ ):
95
+ exit_code = search.main(["foo bar"])
96
+ assert exit_code == 0
97
+ assert captured_stdout.getvalue() == "found path\n"
98
+ run_process.assert_called_once_with(
99
+ ["es.exe", "foo bar"],
100
+ capture_output=True,
101
+ check=False,
102
+ encoding="utf-8",
103
+ shell=False,
104
+ text=True,
105
+ )
106
+
107
+
108
+ def test_main_passes_through_child_exit_status() -> None:
109
+ completed_process = search.subprocess.CompletedProcess(
110
+ args=["es.exe", "config.py"], returncode=7, stdout="", stderr="failed\n"
111
+ )
112
+ captured_stderr = io.StringIO()
113
+ with (
114
+ patch.object(search, "load_registry", return_value={}),
115
+ patch.object(search.shutil, "which", return_value="es.exe"),
116
+ patch.object(search.subprocess, "run", return_value=completed_process),
117
+ patch.object(search.sys, "stderr", captured_stderr),
118
+ ):
119
+ exit_code = search.main(["config.py"])
120
+ assert exit_code == 7
121
+ assert captured_stderr.getvalue() == "failed\n"
122
+
123
+
124
+ def test_main_reports_missing_executable() -> None:
125
+ captured_stderr = io.StringIO()
126
+ with (
127
+ patch.object(search, "load_registry", return_value={}),
128
+ patch.object(search.shutil, "which", return_value=None),
129
+ patch.object(search.sys, "stderr", captured_stderr),
130
+ ):
131
+ exit_code = search.main(["config.py"])
132
+ assert exit_code != 0
133
+ assert "es.exe" in captured_stderr.getvalue()
134
+
135
+
136
+ def test_main_reports_empty_search_arguments() -> None:
137
+ captured_stderr = io.StringIO()
138
+ with patch.object(search.sys, "stderr", captured_stderr):
139
+ exit_code = search.main([])
140
+ assert exit_code == search.INVALID_INPUT_EXIT_CODE
141
+ assert "search argument" in captured_stderr.getvalue()
142
+
143
+
144
+ @pytest.mark.parametrize("all_arguments", [["-n", "50"], [""], [" "]])
145
+ def test_main_rejects_unscoped_search_without_launching_child(
146
+ all_arguments: list[str],
147
+ ) -> None:
148
+ captured_stderr = io.StringIO()
149
+ with (
150
+ patch.object(search.subprocess, "run") as run_process,
151
+ patch.object(search.sys, "stderr", captured_stderr),
152
+ ):
153
+ exit_code = search.main(all_arguments)
154
+ assert exit_code == search.INVALID_INPUT_EXIT_CODE
155
+ assert "first search argument" in captured_stderr.getvalue().lower()
156
+ run_process.assert_not_called()
157
+
158
+
159
+ def test_main_allows_version_information_operation() -> None:
160
+ completed_process = search.subprocess.CompletedProcess(
161
+ args=["es.exe", "-version"], returncode=0, stdout="1.1.0.30\n", stderr=""
162
+ )
163
+ with (
164
+ patch.object(search, "load_registry", return_value={}),
165
+ patch.object(search.shutil, "which", return_value="es.exe"),
166
+ patch.object(
167
+ search.subprocess, "run", return_value=completed_process
168
+ ) as run_process,
169
+ ):
170
+ exit_code = search.main(["-version"])
171
+ assert exit_code == 0
172
+ run_process.assert_called_once()
@@ -0,0 +1,101 @@
1
+ ---
2
+ name: syncing-submodules
3
+ description: >-
4
+ Records a submodule's current commit in its parent repository.
5
+ Triggers: submodule sync, /submodule-sync, sync parent submodule,
6
+ update parent pointer, record submodule commit, commit submodule pointer,
7
+ sync superproject, update superproject pointer, parent repo submodule update,
8
+ finish submodule commit. Does not handle clone, checkout, update main,
9
+ or ordinary commits.
10
+ ---
11
+
12
+ # Syncing submodules
13
+
14
+ ## Principle
15
+
16
+ A submodule commit changes the child repository first. Change the parent pointer only when this skill runs. Limit the parent commit to the submodule path so other staged work stays staged.
17
+
18
+ ## Gotchas
19
+
20
+ - Inherited `GIT_*` variables can redirect child Git commands. The script removes them from its Git environment.
21
+ - Submodule paths can contain brackets or wildcard characters. The script sends Git a literal pathspec.
22
+ - `git diff --cached --quiet` uses exit 1 for a changed pointer. Any higher exit is a failure.
23
+ - The parent repository may already contain staged work. The path-only commit must leave it staged.
24
+ - A repository without a superproject is a successful `not_submodule` result.
25
+ - The old hook returned success after a failed parent commit. This command returns a nonzero exit.
26
+
27
+ ## When this applies
28
+
29
+ Use this skill after committing inside a checked-out submodule when its parent repository should record the new commit.
30
+
31
+ Refusals use the first matching line:
32
+
33
+ - Clone, initialize, or check out a submodule: `Use Git's submodule commands. This skill records an existing submodule commit in its parent.`
34
+ - Fast-forward `main`: `Use /update. This skill changes one parent submodule pointer.`
35
+ - Make an ordinary commit or push: `Use /source-command-commit. This skill commits only a parent submodule pointer.`
36
+
37
+ ## Process
38
+
39
+ ### Seed the run tasks
40
+
41
+ Register every item in `reference/run-tasks.md` as a session task with the host task tool. Complete each task with an exit code, JSON field, or repository readback. If the host has no task tool, stop with `A task tool is required to track this repository-changing operation.`
42
+
43
+ ### Run the command
44
+
45
+ Execute the bundled script. Do not reconstruct the Git sequence by hand.
46
+
47
+ ```text
48
+ python "${CLAUDE_SKILL_DIR}/scripts/sync_parent_pointer.py" --repository "<submodule-path>"
49
+ ```
50
+
51
+ Python 3 and Git are required. GitHub CLI is optional and only supplies an open pull request URL.
52
+
53
+ The script writes one JSON object to standard output. Exit 0 permits `updated`, `unchanged`, and `not_submodule`. Exit 1 reports a Git or repository failure. Exit 2 reports invalid arguments. Treat every nonzero exit as incomplete work. Fix the reported condition, rerun the command, and keep the task open until the command exits 0.
54
+
55
+ ### Report the result
56
+
57
+ - `updated`: report `parent_repository`, `submodule_path`, `commit`, and `parent_commit`.
58
+ - `unchanged`: report that the parent already records `commit`.
59
+ - `not_submodule`: report that no parent repository changed.
60
+ - `pull_request_url`: invoke `/pr-title-description` with that URL after reporting the sync result.
61
+
62
+ If `/pr-title-description` is not installed, report `pull_request_url` and finish the successful sync.
63
+
64
+ ## Constraints
65
+
66
+ - Run the script from this skill package.
67
+ - Commit only the literal submodule path in the parent repository.
68
+ - Preserve every unrelated staged parent path.
69
+ - Never push, switch branches, rewrite commits, or change Git configuration.
70
+ - Never treat a failed Git command as success.
71
+
72
+ ## Examples
73
+
74
+ - Updated pointer: `status` is `updated`, `submodule_path` is `modules/child`, and `parent_commit` names the new parent commit.
75
+ - No superproject: `status` is `not_submodule`; every parent and commit field is null.
76
+ - Failed stage: exit 1, `status` is `error`, and standard error starts with `syncing-submodules:`.
77
+
78
+ ## Sub-skills
79
+
80
+ | Skill | When | Produces | Missing behavior |
81
+ |---|---|---|---|
82
+ | `/pr-title-description` | The result includes `pull_request_url` | A title and description based on the full pull request diff | Report the URL and keep the completed sync |
83
+
84
+ ## File index
85
+
86
+ | File | Purpose |
87
+ |---|---|
88
+ | `SKILL.md` | Selection, operation boundary, result handling, and peer routing |
89
+ | `reference/run-tasks.md` | Task seeds for each repository-changing run |
90
+ | `scripts/sync_parent_pointer.py` | Execute the parent pointer sync and return one JSON result |
91
+ | `scripts/submodule_sync.py` | Own the Git operation and typed sync report |
92
+ | `scripts/test_sync_parent_pointer.py` | Prove command parsing, JSON output, and hook deletion |
93
+ | `scripts/test_submodule_sync.py` | Prove exact commits, preserved staging, no-op states, and failures |
94
+ | `scripts/submodule_sync_constants/__init__.py` | Mark the constants package |
95
+ | `scripts/submodule_sync_constants/config/__init__.py` | Mark the configuration package |
96
+ | `scripts/submodule_sync_constants/config/constants.py` | Named command, message, field, and exit-code constants |
97
+
98
+ ## Folder map
99
+
100
+ - `reference/` contains the run task seeds.
101
+ - `scripts/` contains the executable command, Git operation, tests, and constants package.
@@ -0,0 +1,9 @@
1
+ # Run task seeds
2
+
3
+ - Resolve the requested repository path.
4
+ - Execute `scripts/sync_parent_pointer.py` for that path.
5
+ - Record the exit code and parse the single JSON result.
6
+ - Fix any reported failure and rerun until the command exits 0.
7
+ - Read back the parent pointer and unrelated staged paths when the status is `updated`.
8
+ - Report the result fields.
9
+ - Route an open pull request URL to `/pr-title-description` or report the URL when that skill is missing.