@mindfoldhq/trellis 0.3.8 → 0.3.10-beta.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 (174) hide show
  1. package/dist/cli/index.js +2 -0
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/commands/init.d.ts +1 -0
  4. package/dist/commands/init.d.ts.map +1 -1
  5. package/dist/commands/init.js +203 -31
  6. package/dist/commands/init.js.map +1 -1
  7. package/dist/commands/update.d.ts.map +1 -1
  8. package/dist/commands/update.js +154 -6
  9. package/dist/commands/update.js.map +1 -1
  10. package/dist/configurators/workflow.d.ts +6 -2
  11. package/dist/configurators/workflow.d.ts.map +1 -1
  12. package/dist/configurators/workflow.js +88 -58
  13. package/dist/configurators/workflow.js.map +1 -1
  14. package/dist/migrations/index.d.ts +1 -0
  15. package/dist/migrations/index.d.ts.map +1 -1
  16. package/dist/migrations/index.js +2 -0
  17. package/dist/migrations/index.js.map +1 -1
  18. package/dist/migrations/manifests/0.3.9.json +9 -0
  19. package/dist/migrations/manifests/0.4.0-beta.1.json +228 -0
  20. package/dist/templates/claude/agents/dispatch.md +1 -2
  21. package/dist/templates/claude/agents/implement.md +2 -3
  22. package/dist/templates/claude/commands/trellis/before-dev.md +29 -0
  23. package/dist/templates/claude/commands/trellis/check.md +25 -0
  24. package/dist/templates/claude/commands/trellis/create-command.md +2 -2
  25. package/dist/templates/claude/commands/trellis/onboard.md +13 -13
  26. package/dist/templates/claude/commands/trellis/parallel.md +1 -2
  27. package/dist/templates/claude/commands/trellis/record-session.md +1 -1
  28. package/dist/templates/claude/commands/trellis/start.md +8 -4
  29. package/dist/templates/claude/hooks/inject-subagent-context.py +21 -13
  30. package/dist/templates/claude/hooks/session-start.py +170 -2
  31. package/dist/templates/codex/skills/before-dev/SKILL.md +34 -0
  32. package/dist/templates/codex/skills/check/SKILL.md +30 -0
  33. package/dist/templates/codex/skills/create-command/SKILL.md +2 -2
  34. package/dist/templates/codex/skills/onboard/SKILL.md +11 -11
  35. package/dist/templates/codex/skills/record-session/SKILL.md +1 -1
  36. package/dist/templates/codex/skills/start/SKILL.md +8 -3
  37. package/dist/templates/cursor/commands/trellis-before-dev.md +29 -0
  38. package/dist/templates/cursor/commands/trellis-check.md +25 -0
  39. package/dist/templates/cursor/commands/trellis-create-command.md +2 -2
  40. package/dist/templates/cursor/commands/trellis-onboard.md +13 -13
  41. package/dist/templates/cursor/commands/trellis-record-session.md +1 -1
  42. package/dist/templates/cursor/commands/trellis-start.md +7 -16
  43. package/dist/templates/gemini/commands/trellis/before-dev.toml +33 -0
  44. package/dist/templates/gemini/commands/trellis/check.toml +29 -0
  45. package/dist/templates/gemini/commands/trellis/create-command.toml +2 -2
  46. package/dist/templates/gemini/commands/trellis/onboard.toml +2 -2
  47. package/dist/templates/gemini/commands/trellis/record-session.toml +1 -1
  48. package/dist/templates/gemini/commands/trellis/start.toml +9 -4
  49. package/dist/templates/iflow/agents/dispatch.md +1 -2
  50. package/dist/templates/iflow/agents/implement.md +2 -3
  51. package/dist/templates/iflow/commands/trellis/before-dev.md +29 -0
  52. package/dist/templates/iflow/commands/trellis/check.md +25 -0
  53. package/dist/templates/iflow/commands/trellis/create-command.md +2 -2
  54. package/dist/templates/iflow/commands/trellis/onboard.md +13 -13
  55. package/dist/templates/iflow/commands/trellis/parallel.md +1 -2
  56. package/dist/templates/iflow/commands/trellis/record-session.md +1 -1
  57. package/dist/templates/iflow/commands/trellis/start.md +8 -4
  58. package/dist/templates/iflow/hooks/inject-subagent-context.py +21 -13
  59. package/dist/templates/iflow/hooks/session-start.py +156 -1
  60. package/dist/templates/iflow/settings.json +2 -2
  61. package/dist/templates/kilo/workflows/before-dev.md +29 -0
  62. package/dist/templates/kilo/workflows/check.md +25 -0
  63. package/dist/templates/kilo/workflows/create-command.md +2 -2
  64. package/dist/templates/kilo/workflows/onboard.md +13 -13
  65. package/dist/templates/kilo/workflows/parallel.md +1 -2
  66. package/dist/templates/kilo/workflows/record-session.md +1 -1
  67. package/dist/templates/kilo/workflows/start.md +8 -3
  68. package/dist/templates/kiro/skills/before-dev/SKILL.md +34 -0
  69. package/dist/templates/kiro/skills/check/SKILL.md +30 -0
  70. package/dist/templates/kiro/skills/create-command/SKILL.md +2 -2
  71. package/dist/templates/kiro/skills/onboard/SKILL.md +11 -11
  72. package/dist/templates/kiro/skills/record-session/SKILL.md +1 -1
  73. package/dist/templates/kiro/skills/start/SKILL.md +8 -3
  74. package/dist/templates/markdown/spec/backend/script-conventions.md +93 -0
  75. package/dist/templates/opencode/agents/dispatch.md +1 -2
  76. package/dist/templates/opencode/agents/implement.md +2 -2
  77. package/dist/templates/opencode/agents/research.md +1 -2
  78. package/dist/templates/opencode/commands/trellis/before-dev.md +29 -0
  79. package/dist/templates/opencode/commands/trellis/check.md +25 -0
  80. package/dist/templates/opencode/commands/trellis/create-command.md +2 -2
  81. package/dist/templates/opencode/commands/trellis/onboard.md +13 -13
  82. package/dist/templates/opencode/commands/trellis/parallel.md +1 -2
  83. package/dist/templates/opencode/commands/trellis/record-session.md +1 -1
  84. package/dist/templates/opencode/commands/trellis/start.md +8 -3
  85. package/dist/templates/opencode/plugin/inject-subagent-context.js +45 -18
  86. package/dist/templates/opencode/plugin/session-start.js +149 -1
  87. package/dist/templates/qoder/skills/before-dev/SKILL.md +34 -0
  88. package/dist/templates/qoder/skills/check/SKILL.md +30 -0
  89. package/dist/templates/qoder/skills/create-command/SKILL.md +2 -2
  90. package/dist/templates/qoder/skills/onboard/SKILL.md +13 -13
  91. package/dist/templates/qoder/skills/record-session/SKILL.md +1 -1
  92. package/dist/templates/qoder/skills/start/SKILL.md +8 -3
  93. package/dist/templates/trellis/config.yaml +20 -0
  94. package/dist/templates/trellis/index.d.ts +11 -0
  95. package/dist/templates/trellis/index.d.ts.map +1 -1
  96. package/dist/templates/trellis/index.js +22 -0
  97. package/dist/templates/trellis/index.js.map +1 -1
  98. package/dist/templates/trellis/scripts/add_session.py +52 -7
  99. package/dist/templates/trellis/scripts/common/cli_adapter.py +33 -45
  100. package/dist/templates/trellis/scripts/common/config.py +152 -0
  101. package/dist/templates/trellis/scripts/common/git.py +31 -0
  102. package/dist/templates/trellis/scripts/common/git_context.py +23 -586
  103. package/dist/templates/trellis/scripts/common/io.py +37 -0
  104. package/dist/templates/trellis/scripts/common/log.py +45 -0
  105. package/dist/templates/trellis/scripts/common/packages_context.py +233 -0
  106. package/dist/templates/trellis/scripts/common/paths.py +46 -0
  107. package/dist/templates/trellis/scripts/common/phase.py +50 -49
  108. package/dist/templates/trellis/scripts/common/registry.py +41 -72
  109. package/dist/templates/trellis/scripts/common/session_context.py +466 -0
  110. package/dist/templates/trellis/scripts/common/task_context.py +384 -0
  111. package/dist/templates/trellis/scripts/common/task_queue.py +27 -98
  112. package/dist/templates/trellis/scripts/common/task_store.py +534 -0
  113. package/dist/templates/trellis/scripts/common/task_utils.py +96 -6
  114. package/dist/templates/trellis/scripts/common/tasks.py +109 -0
  115. package/dist/templates/trellis/scripts/common/types.py +112 -0
  116. package/dist/templates/trellis/scripts/create_bootstrap.py +31 -26
  117. package/dist/templates/trellis/scripts/hooks/linear_sync.py +243 -0
  118. package/dist/templates/trellis/scripts/multi_agent/_bootstrap.py +17 -0
  119. package/dist/templates/trellis/scripts/multi_agent/cleanup.py +43 -48
  120. package/dist/templates/trellis/scripts/multi_agent/create_pr.py +336 -45
  121. package/dist/templates/trellis/scripts/multi_agent/plan.py +2 -26
  122. package/dist/templates/trellis/scripts/multi_agent/start.py +126 -57
  123. package/dist/templates/trellis/scripts/multi_agent/status.py +12 -753
  124. package/dist/templates/trellis/scripts/multi_agent/status_display.py +542 -0
  125. package/dist/templates/trellis/scripts/multi_agent/status_monitor.py +225 -0
  126. package/dist/templates/trellis/scripts/task.py +50 -975
  127. package/dist/templates/trellis/workflow.md +21 -34
  128. package/dist/types/migration.d.ts +3 -1
  129. package/dist/types/migration.d.ts.map +1 -1
  130. package/dist/utils/project-detector.d.ts +23 -0
  131. package/dist/utils/project-detector.d.ts.map +1 -1
  132. package/dist/utils/project-detector.js +364 -0
  133. package/dist/utils/project-detector.js.map +1 -1
  134. package/dist/utils/template-fetcher.d.ts +2 -2
  135. package/dist/utils/template-fetcher.d.ts.map +1 -1
  136. package/dist/utils/template-fetcher.js +5 -5
  137. package/dist/utils/template-fetcher.js.map +1 -1
  138. package/package.json +1 -1
  139. package/dist/templates/claude/commands/trellis/before-backend-dev.md +0 -13
  140. package/dist/templates/claude/commands/trellis/before-frontend-dev.md +0 -13
  141. package/dist/templates/claude/commands/trellis/check-backend.md +0 -13
  142. package/dist/templates/claude/commands/trellis/check-frontend.md +0 -13
  143. package/dist/templates/codex/skills/before-backend-dev/SKILL.md +0 -18
  144. package/dist/templates/codex/skills/before-frontend-dev/SKILL.md +0 -18
  145. package/dist/templates/codex/skills/check-backend/SKILL.md +0 -18
  146. package/dist/templates/codex/skills/check-frontend/SKILL.md +0 -18
  147. package/dist/templates/cursor/commands/trellis-before-backend-dev.md +0 -13
  148. package/dist/templates/cursor/commands/trellis-before-frontend-dev.md +0 -13
  149. package/dist/templates/cursor/commands/trellis-check-backend.md +0 -13
  150. package/dist/templates/cursor/commands/trellis-check-frontend.md +0 -13
  151. package/dist/templates/gemini/commands/trellis/before-backend-dev.toml +0 -17
  152. package/dist/templates/gemini/commands/trellis/before-frontend-dev.toml +0 -17
  153. package/dist/templates/gemini/commands/trellis/check-backend.toml +0 -17
  154. package/dist/templates/gemini/commands/trellis/check-frontend.toml +0 -17
  155. package/dist/templates/iflow/commands/trellis/before-backend-dev.md +0 -13
  156. package/dist/templates/iflow/commands/trellis/before-frontend-dev.md +0 -13
  157. package/dist/templates/iflow/commands/trellis/check-backend.md +0 -13
  158. package/dist/templates/iflow/commands/trellis/check-frontend.md +0 -13
  159. package/dist/templates/kilo/workflows/before-backend-dev.md +0 -13
  160. package/dist/templates/kilo/workflows/before-frontend-dev.md +0 -13
  161. package/dist/templates/kilo/workflows/check-backend.md +0 -13
  162. package/dist/templates/kilo/workflows/check-frontend.md +0 -13
  163. package/dist/templates/kiro/skills/before-backend-dev/SKILL.md +0 -18
  164. package/dist/templates/kiro/skills/before-frontend-dev/SKILL.md +0 -18
  165. package/dist/templates/kiro/skills/check-backend/SKILL.md +0 -18
  166. package/dist/templates/kiro/skills/check-frontend/SKILL.md +0 -18
  167. package/dist/templates/opencode/commands/trellis/before-backend-dev.md +0 -13
  168. package/dist/templates/opencode/commands/trellis/before-frontend-dev.md +0 -13
  169. package/dist/templates/opencode/commands/trellis/check-backend.md +0 -13
  170. package/dist/templates/opencode/commands/trellis/check-frontend.md +0 -13
  171. package/dist/templates/qoder/skills/before-backend-dev/SKILL.md +0 -18
  172. package/dist/templates/qoder/skills/before-frontend-dev/SKILL.md +0 -18
  173. package/dist/templates/qoder/skills/check-backend/SKILL.md +0 -18
  174. package/dist/templates/qoder/skills/check-frontend/SKILL.md +0 -18
@@ -6,10 +6,11 @@ Usage:
6
6
  python3 create_pr.py [task-dir] [--dry-run]
7
7
 
8
8
  This script:
9
- 1. Stages and commits all changes (excluding workspace/)
10
- 2. Pushes to origin
11
- 3. Creates a Draft PR using `gh pr create`
12
- 4. Updates task.json with status="completed", pr_url, and current_phase
9
+ 1. Handles submodule changes (commit, push, PR) if any submodules are configured
10
+ 2. Stages and commits all main-repo changes (excluding workspace/)
11
+ 3. Pushes to origin
12
+ 4. Creates a Draft PR using `gh pr create`
13
+ 5. Updates task.json with status="completed", pr_url, submodule_prs, and current_phase
13
14
 
14
15
  Note: This is the only action that performs git commit, as it's the final
15
16
  step after all implementation and checks are complete.
@@ -18,15 +19,16 @@ step after all implementation and checks are complete.
18
19
  from __future__ import annotations
19
20
 
20
21
  import argparse
21
- import json
22
22
  import subprocess
23
23
  import sys
24
24
  from pathlib import Path
25
25
 
26
- # Add parent directory to path for imports
27
- sys.path.insert(0, str(Path(__file__).parent.parent))
26
+ import _bootstrap # noqa: F401 — adds parent scripts/ dir to sys.path
28
27
 
29
- from common.git_context import _run_git_command
28
+ from common.config import get_submodule_packages
29
+ from common.git import run_git
30
+ from common.io import read_json, write_json
31
+ from common.log import Colors
30
32
  from common.paths import (
31
33
  DIR_WORKFLOW,
32
34
  FILE_TASK_JSON,
@@ -35,41 +37,277 @@ from common.paths import (
35
37
  )
36
38
  from common.phase import get_phase_for_action
37
39
 
40
+ # Colors, read_json, write_json
41
+ # are now imported from common.log and common.io above.
42
+
43
+
38
44
  # =============================================================================
39
- # Colors
45
+ # Submodule PR Helpers
40
46
  # =============================================================================
41
47
 
48
+ # Warning message prepended to main PR body when submodule PRs exist
49
+ _SUBMODULE_SQUASH_WARNING_MARKER = (
50
+ "Merge submodule PR(s) first. If squash-merged, update submodule ref after merge."
51
+ )
42
52
 
43
- class Colors:
44
- RED = "\033[0;31m"
45
- GREEN = "\033[0;32m"
46
- YELLOW = "\033[1;33m"
47
- BLUE = "\033[0;34m"
48
- NC = "\033[0m"
49
53
 
54
+ def _get_submodule_default_branch(submodule_abs: Path) -> str:
55
+ """Get the default branch of a submodule repository.
50
56
 
51
- # =============================================================================
52
- # Helper Functions
53
- # =============================================================================
57
+ Uses `git symbolic-ref refs/remotes/origin/HEAD` for portability
58
+ (no grep, no English-dependent output).
59
+
60
+ Returns:
61
+ Default branch name (e.g. "main"), falls back to "main" on failure.
62
+ """
63
+ ret, out, _ = run_git(
64
+ ["symbolic-ref", "refs/remotes/origin/HEAD"], cwd=submodule_abs
65
+ )
66
+ if ret == 0 and out.strip():
67
+ # Output: "refs/remotes/origin/main" -> "main"
68
+ ref = out.strip()
69
+ prefix = "refs/remotes/origin/"
70
+ if ref.startswith(prefix):
71
+ return ref[len(prefix):]
72
+ return "main"
73
+
74
+
75
+ def _process_submodule_changes(
76
+ repo_root: Path,
77
+ current_branch: str,
78
+ commit_prefix: str,
79
+ scope: str,
80
+ task_name: str,
81
+ task_data: dict,
82
+ task_json: Path,
83
+ dry_run: bool,
84
+ ) -> tuple[dict[str, str], list[str], bool]:
85
+ """Process submodule changes: commit, push, create PRs.
86
+
87
+ Returns:
88
+ Tuple of (submodule_prs dict, changed_submodule_paths list, success bool).
89
+ On failure, submodule_prs contains URLs persisted so far.
90
+ """
91
+ submodule_packages = get_submodule_packages(repo_root)
92
+ if not submodule_packages:
93
+ return {}, [], True
94
+
95
+ # Load existing submodule_prs for incremental merge
96
+ raw_prs = task_data.get("submodule_prs")
97
+ submodule_prs: dict[str, str] = dict(raw_prs) if isinstance(raw_prs, dict) else {}
98
+
99
+ # Detect which submodules have changes
100
+ changed: list[tuple[str, str]] = [] # (name, path)
101
+ for pkg_name, pkg_path in submodule_packages.items():
102
+ sub_abs = repo_root / pkg_path
103
+ if not sub_abs.is_dir():
104
+ continue
105
+
106
+ ret, status_out, _ = run_git(
107
+ ["status", "--porcelain"], cwd=sub_abs
108
+ )
109
+ if ret != 0:
110
+ continue
111
+ if status_out.strip():
112
+ changed.append((pkg_name, pkg_path))
113
+
114
+ if not changed:
115
+ return submodule_prs, [], True
116
+
117
+ # Determine submodule branch name: <repo-dir-name>/<main-branch>
118
+ repo_dir_name = repo_root.name
119
+ sub_branch = f"{repo_dir_name}/{current_branch}"
120
+
121
+ print(f"\n{Colors.BLUE}=== Submodule Changes Detected ==={Colors.NC}")
122
+ for pkg_name, pkg_path in changed:
123
+ print(f" - {pkg_name} ({pkg_path})")
124
+ print()
54
125
 
126
+ changed_paths: list[str] = []
127
+
128
+ for pkg_name, pkg_path in changed:
129
+ sub_abs = repo_root / pkg_path
130
+ sub_base = _get_submodule_default_branch(sub_abs)
131
+ sub_commit_msg = f"{commit_prefix}({scope}): {task_name}"
132
+
133
+ print(f"{Colors.YELLOW}Processing submodule: {pkg_name} ({pkg_path}){Colors.NC}")
134
+ print(f" Submodule base branch: {sub_base}")
135
+ print(f" Submodule branch: {sub_branch}")
136
+
137
+ if dry_run:
138
+ print(f" [DRY-RUN] Would checkout branch: {sub_branch}")
139
+ print(f" [DRY-RUN] Would commit: {sub_commit_msg}")
140
+ print(f" [DRY-RUN] Would push to: origin/{sub_branch}")
141
+ print(f" [DRY-RUN] Would create PR: {sub_branch} -> {sub_base}")
142
+ submodule_prs[pkg_name] = "https://github.com/example/repo/pull/DRY-RUN"
143
+ changed_paths.append(pkg_path)
144
+ continue
145
+
146
+ # --- Checkout or create branch in submodule ---
147
+ ret, _, _ = run_git(
148
+ ["show-ref", "--verify", "--quiet", f"refs/heads/{sub_branch}"],
149
+ cwd=sub_abs,
150
+ )
151
+ if ret == 0:
152
+ # Branch exists, checkout
153
+ ret, _, err = run_git(
154
+ ["checkout", sub_branch], cwd=sub_abs
155
+ )
156
+ if ret != 0:
157
+ print(f"{Colors.RED}Failed to checkout branch in {pkg_name}: {err}{Colors.NC}")
158
+ return submodule_prs, changed_paths, False
159
+
160
+ # Check for divergence (reuse risk)
161
+ ret_anc, _, _ = run_git(
162
+ ["merge-base", "--is-ancestor", sub_base, sub_branch],
163
+ cwd=sub_abs,
164
+ )
165
+ if ret_anc != 0:
166
+ print(
167
+ f" {Colors.YELLOW}[WARN] submodule branch has diverged history, "
168
+ f"consider recreating{Colors.NC}"
169
+ )
170
+ else:
171
+ # Create new branch
172
+ ret, _, err = run_git(
173
+ ["checkout", "-b", sub_branch], cwd=sub_abs
174
+ )
175
+ if ret != 0:
176
+ print(f"{Colors.RED}Failed to create branch in {pkg_name}: {err}{Colors.NC}")
177
+ return submodule_prs, changed_paths, False
55
178
 
56
- def _read_json_file(path: Path) -> dict | None:
57
- """Read and parse a JSON file."""
58
- try:
59
- return json.loads(path.read_text(encoding="utf-8"))
60
- except (FileNotFoundError, json.JSONDecodeError, OSError):
61
- return None
179
+ # --- Stage and commit ---
180
+ run_git(["add", "-A"], cwd=sub_abs)
62
181
 
182
+ ret, _, _ = run_git(["diff", "--cached", "--quiet"], cwd=sub_abs)
183
+ if ret != 0:
184
+ # Has staged changes
185
+ ret, _, err = run_git(
186
+ ["commit", "-m", sub_commit_msg], cwd=sub_abs
187
+ )
188
+ if ret != 0:
189
+ print(f"{Colors.RED}Failed to commit in {pkg_name}: {err}{Colors.NC}")
190
+ return submodule_prs, changed_paths, False
191
+ print(f" {Colors.GREEN}Committed in {pkg_name}{Colors.NC}")
192
+ else:
193
+ print(f" No staged changes in {pkg_name}, skipping commit")
63
194
 
64
- def _write_json_file(path: Path, data: dict) -> bool:
65
- """Write dict to JSON file."""
66
- try:
67
- path.write_text(
68
- json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8"
195
+ # --- Push ---
196
+ ret, _, err = run_git(
197
+ ["push", "-u", "origin", sub_branch], cwd=sub_abs
69
198
  )
70
- return True
71
- except (OSError, IOError):
72
- return False
199
+ if ret != 0:
200
+ print(f"{Colors.RED}Failed to push {pkg_name}: {err}{Colors.NC}")
201
+ return submodule_prs, changed_paths, False
202
+ print(f" {Colors.GREEN}Pushed {pkg_name} to origin/{sub_branch}{Colors.NC}")
203
+
204
+ # --- Create or reuse PR ---
205
+ result = subprocess.run(
206
+ [
207
+ "gh", "pr", "list",
208
+ "--head", sub_branch,
209
+ "--base", sub_base,
210
+ "--json", "url",
211
+ "--jq", ".[0].url",
212
+ ],
213
+ capture_output=True,
214
+ text=True,
215
+ encoding="utf-8",
216
+ errors="replace",
217
+ cwd=str(sub_abs),
218
+ )
219
+ existing_sub_pr = result.stdout.strip()
220
+
221
+ if existing_sub_pr:
222
+ print(f" {Colors.YELLOW}PR already exists: {existing_sub_pr}{Colors.NC}")
223
+ sub_pr_url = existing_sub_pr
224
+ else:
225
+ result = subprocess.run(
226
+ [
227
+ "gh", "pr", "create",
228
+ "--draft",
229
+ "--base", sub_base,
230
+ "--title", f"{commit_prefix}({scope}): {task_name} [{pkg_name}]",
231
+ "--body", f"Submodule changes for {task_name}",
232
+ ],
233
+ capture_output=True,
234
+ text=True,
235
+ encoding="utf-8",
236
+ errors="replace",
237
+ cwd=str(sub_abs),
238
+ )
239
+ if result.returncode != 0:
240
+ print(
241
+ f"{Colors.RED}Failed to create PR for {pkg_name}: "
242
+ f"{result.stderr}{Colors.NC}"
243
+ )
244
+ return submodule_prs, changed_paths, False
245
+
246
+ sub_pr_url = result.stdout.strip()
247
+ print(f" {Colors.GREEN}PR created for {pkg_name}: {sub_pr_url}{Colors.NC}")
248
+
249
+ # Persist immediately (incremental, supports re-entry)
250
+ submodule_prs[pkg_name] = sub_pr_url
251
+ task_data["submodule_prs"] = submodule_prs
252
+ write_json(task_json, task_data)
253
+
254
+ changed_paths.append(pkg_path)
255
+
256
+ return submodule_prs, changed_paths, True
257
+
258
+
259
+ def _build_submodule_warning(submodule_prs: dict[str, str]) -> str:
260
+ """Build the squash-merge warning block for the main PR body."""
261
+ pr_lines = "\n".join(f"> - {name}: {url}" for name, url in submodule_prs.items())
262
+ return (
263
+ f"> {_SUBMODULE_SQUASH_WARNING_MARKER}\n"
264
+ f">\n"
265
+ f"> Submodule PRs:\n"
266
+ f"{pr_lines}\n"
267
+ f"\n---\n\n"
268
+ )
269
+
270
+
271
+ def _ensure_submodule_warning_on_existing_pr(
272
+ submodule_prs: dict[str, str],
273
+ dry_run: bool,
274
+ ) -> None:
275
+ """Read-modify-write: add squash warning to existing PR if missing."""
276
+ if dry_run:
277
+ print("[DRY-RUN] Would check/add submodule warning to existing PR")
278
+ return
279
+
280
+ # Read current PR body
281
+ result = subprocess.run(
282
+ [
283
+ "gh", "pr", "view",
284
+ "--json", "body",
285
+ "--jq", ".body",
286
+ ],
287
+ capture_output=True,
288
+ text=True,
289
+ encoding="utf-8",
290
+ errors="replace",
291
+ )
292
+ if result.returncode != 0:
293
+ return
294
+
295
+ current_body = result.stdout.strip()
296
+ if _SUBMODULE_SQUASH_WARNING_MARKER in current_body:
297
+ return # Warning already present
298
+
299
+ # Prepend warning to existing body
300
+ warning = _build_submodule_warning(submodule_prs)
301
+ new_body = warning + current_body
302
+
303
+ subprocess.run(
304
+ ["gh", "pr", "edit", "--body", new_body],
305
+ capture_output=True,
306
+ text=True,
307
+ encoding="utf-8",
308
+ errors="replace",
309
+ )
310
+ print(f" {Colors.GREEN}Added submodule merge warning to existing PR{Colors.NC}")
73
311
 
74
312
 
75
313
  # =============================================================================
@@ -127,7 +365,7 @@ def main() -> int:
127
365
  print()
128
366
 
129
367
  # Read task config
130
- task_data = _read_json_file(task_json)
368
+ task_data = read_json(task_json)
131
369
  if not task_data:
132
370
  print(f"{Colors.RED}Error: Failed to read task.json{Colors.NC}")
133
371
  return 1
@@ -158,36 +396,66 @@ def main() -> int:
158
396
  print()
159
397
 
160
398
  # Get current branch
161
- _, branch_out, _ = _run_git_command(["branch", "--show-current"])
399
+ _, branch_out, _ = run_git(["branch", "--show-current"])
162
400
  current_branch = branch_out.strip()
163
401
  print(f"Current branch: {current_branch}")
164
402
 
403
+ # =============================================================================
404
+ # Submodule PR Flow (runs BEFORE main repo staging)
405
+ # =============================================================================
406
+ submodule_prs, changed_submodule_paths, sub_success = _process_submodule_changes(
407
+ repo_root=repo_root,
408
+ current_branch=current_branch,
409
+ commit_prefix=commit_prefix,
410
+ scope=scope,
411
+ task_name=task_name,
412
+ task_data=task_data,
413
+ task_json=task_json,
414
+ dry_run=args.dry_run,
415
+ )
416
+
417
+ if not sub_success:
418
+ print(
419
+ f"\n{Colors.RED}Submodule PR flow failed. "
420
+ f"Skipping main repo commit/PR.{Colors.NC}"
421
+ )
422
+ print("Already-created submodule PRs have been saved to task.json.")
423
+ return 1
424
+
425
+ # =============================================================================
426
+ # Main Repo: Stage, Commit, Push, PR
427
+ # =============================================================================
428
+
165
429
  # Check for changes
166
430
  print(f"{Colors.YELLOW}Checking for changes...{Colors.NC}")
167
431
 
168
432
  # Stage changes
169
- _run_git_command(["add", "-A"])
433
+ run_git(["add", "-A"])
170
434
 
171
435
  # Exclude workspace and temp files
172
- _run_git_command(["reset", f"{DIR_WORKFLOW}/workspace/"])
173
- _run_git_command(["reset", ".agent-log", ".session-id"])
436
+ run_git(["reset", f"{DIR_WORKFLOW}/workspace/"])
437
+ run_git(["reset", ".agent-log", ".session-id"])
438
+
439
+ # If submodules changed, ensure their ref updates are staged
440
+ for sub_path in changed_submodule_paths:
441
+ run_git(["add", sub_path])
174
442
 
175
443
  # Check if there are staged changes
176
- ret, _, _ = _run_git_command(["diff", "--cached", "--quiet"])
444
+ ret, _, _ = run_git(["diff", "--cached", "--quiet"])
177
445
  has_staged_changes = ret != 0
178
446
 
179
447
  if not has_staged_changes:
180
448
  print(f"{Colors.YELLOW}No staged changes to commit{Colors.NC}")
181
449
 
182
450
  # Check for unpushed commits
183
- ret, log_out, _ = _run_git_command(
451
+ ret, log_out, _ = run_git(
184
452
  ["log", f"origin/{current_branch}..HEAD", "--oneline"]
185
453
  )
186
454
  unpushed = len([line for line in log_out.splitlines() if line.strip()])
187
455
 
188
456
  if unpushed == 0:
189
457
  if args.dry_run:
190
- _run_git_command(["reset", "HEAD"])
458
+ run_git(["reset", "HEAD"])
191
459
  print(f"{Colors.RED}No changes to create PR{Colors.NC}")
192
460
  return 1
193
461
 
@@ -200,11 +468,11 @@ def main() -> int:
200
468
  if args.dry_run:
201
469
  print(f"[DRY-RUN] Would commit with message: {commit_msg}")
202
470
  print("[DRY-RUN] Staged files:")
203
- _, staged_out, _ = _run_git_command(["diff", "--cached", "--name-only"])
471
+ _, staged_out, _ = run_git(["diff", "--cached", "--name-only"])
204
472
  for line in staged_out.splitlines():
205
473
  print(f" - {line}")
206
474
  else:
207
- _run_git_command(["commit", "-m", commit_msg])
475
+ run_git(["commit", "-m", commit_msg])
208
476
  print(f"{Colors.GREEN}Committed: {commit_msg}{Colors.NC}")
209
477
 
210
478
  # Push to remote
@@ -212,7 +480,7 @@ def main() -> int:
212
480
  if args.dry_run:
213
481
  print(f"[DRY-RUN] Would push to: origin/{current_branch}")
214
482
  else:
215
- ret, _, err = _run_git_command(["push", "-u", "origin", current_branch])
483
+ ret, _, err = run_git(["push", "-u", "origin", current_branch])
216
484
  if ret != 0:
217
485
  print(f"{Colors.RED}Failed to push: {err}{Colors.NC}")
218
486
  return 1
@@ -223,6 +491,9 @@ def main() -> int:
223
491
  pr_title = f"{commit_prefix}({scope}): {task_name}"
224
492
  pr_url = ""
225
493
 
494
+ # Build PR body with optional submodule warning
495
+ has_submodule_prs = bool(submodule_prs)
496
+
226
497
  if args.dry_run:
227
498
  print("[DRY-RUN] Would create PR:")
228
499
  print(f" Title: {pr_title}")
@@ -231,6 +502,8 @@ def main() -> int:
231
502
  prd_file = target_dir_path / "prd.md"
232
503
  if prd_file.is_file():
233
504
  print(" Body: (from prd.md)")
505
+ if has_submodule_prs:
506
+ print(" Body includes submodule merge warning")
234
507
  pr_url = "https://github.com/example/repo/pull/DRY-RUN"
235
508
  else:
236
509
  # Check if PR already exists
@@ -258,6 +531,12 @@ def main() -> int:
258
531
  if existing_pr:
259
532
  print(f"{Colors.YELLOW}PR already exists: {existing_pr}{Colors.NC}")
260
533
  pr_url = existing_pr
534
+
535
+ # Read-modify-write: add submodule warning if missing
536
+ if has_submodule_prs:
537
+ _ensure_submodule_warning_on_existing_pr(
538
+ submodule_prs, args.dry_run
539
+ )
261
540
  else:
262
541
  # Read PRD as PR body
263
542
  pr_body = ""
@@ -265,6 +544,10 @@ def main() -> int:
265
544
  if prd_file.is_file():
266
545
  pr_body = prd_file.read_text(encoding="utf-8")
267
546
 
547
+ # Prepend submodule warning if applicable
548
+ if has_submodule_prs:
549
+ pr_body = _build_submodule_warning(submodule_prs) + pr_body
550
+
268
551
  # Create PR
269
552
  result = subprocess.run(
270
553
  [
@@ -298,6 +581,8 @@ def main() -> int:
298
581
  print("[DRY-RUN] Would update task.json:")
299
582
  print(" status: completed")
300
583
  print(f" pr_url: {pr_url}")
584
+ if has_submodule_prs:
585
+ print(f" submodule_prs: {submodule_prs}")
301
586
  print(" current_phase: (set to create-pr phase)")
302
587
  else:
303
588
  # Get the phase number for create-pr action
@@ -308,19 +593,25 @@ def main() -> int:
308
593
  task_data["status"] = "completed"
309
594
  task_data["pr_url"] = pr_url
310
595
  task_data["current_phase"] = create_pr_phase
596
+ if has_submodule_prs:
597
+ task_data["submodule_prs"] = submodule_prs
311
598
 
312
- _write_json_file(task_json, task_data)
599
+ write_json(task_json, task_data)
313
600
  print(
314
601
  f"{Colors.GREEN}Task status updated to 'completed', phase {create_pr_phase}{Colors.NC}"
315
602
  )
316
603
 
317
604
  # In dry-run, reset the staging area
318
605
  if args.dry_run:
319
- _run_git_command(["reset", "HEAD"])
606
+ run_git(["reset", "HEAD"])
320
607
 
321
608
  print()
322
609
  print(f"{Colors.GREEN}=== PR Created Successfully ==={Colors.NC}")
323
610
  print(f"PR URL: {pr_url}")
611
+ if has_submodule_prs:
612
+ print("Submodule PRs:")
613
+ for name, url in submodule_prs.items():
614
+ print(f" - {name}: {url}")
324
615
 
325
616
  return 0
326
617
 
@@ -24,38 +24,14 @@ import subprocess
24
24
  import sys
25
25
  from pathlib import Path
26
26
 
27
- # Add parent directory to path for imports
28
- sys.path.insert(0, str(Path(__file__).parent.parent))
27
+ import _bootstrap # noqa: F401 — adds parent scripts/ dir to sys.path
29
28
 
30
29
  from common.cli_adapter import get_cli_adapter
30
+ from common.log import Colors, log_info, log_success, log_error
31
31
  from common.paths import get_repo_root
32
32
  from common.developer import ensure_developer
33
33
 
34
34
 
35
- # =============================================================================
36
- # Colors
37
- # =============================================================================
38
-
39
- class Colors:
40
- RED = "\033[0;31m"
41
- GREEN = "\033[0;32m"
42
- YELLOW = "\033[1;33m"
43
- BLUE = "\033[0;34m"
44
- NC = "\033[0m"
45
-
46
-
47
- def log_info(msg: str) -> None:
48
- print(f"{Colors.BLUE}[INFO]{Colors.NC} {msg}")
49
-
50
-
51
- def log_success(msg: str) -> None:
52
- print(f"{Colors.GREEN}[SUCCESS]{Colors.NC} {msg}")
53
-
54
-
55
- def log_error(msg: str) -> None:
56
- print(f"{Colors.RED}[ERROR]{Colors.NC} {msg}")
57
-
58
-
59
35
  # =============================================================================
60
36
  # Constants
61
37
  # =============================================================================