@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.
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +203 -31
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +154 -6
- package/dist/commands/update.js.map +1 -1
- package/dist/configurators/workflow.d.ts +6 -2
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +88 -58
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +2 -0
- package/dist/migrations/index.js.map +1 -1
- package/dist/migrations/manifests/0.3.9.json +9 -0
- package/dist/migrations/manifests/0.4.0-beta.1.json +228 -0
- package/dist/templates/claude/agents/dispatch.md +1 -2
- package/dist/templates/claude/agents/implement.md +2 -3
- package/dist/templates/claude/commands/trellis/before-dev.md +29 -0
- package/dist/templates/claude/commands/trellis/check.md +25 -0
- package/dist/templates/claude/commands/trellis/create-command.md +2 -2
- package/dist/templates/claude/commands/trellis/onboard.md +13 -13
- package/dist/templates/claude/commands/trellis/parallel.md +1 -2
- package/dist/templates/claude/commands/trellis/record-session.md +1 -1
- package/dist/templates/claude/commands/trellis/start.md +8 -4
- package/dist/templates/claude/hooks/inject-subagent-context.py +21 -13
- package/dist/templates/claude/hooks/session-start.py +170 -2
- package/dist/templates/codex/skills/before-dev/SKILL.md +34 -0
- package/dist/templates/codex/skills/check/SKILL.md +30 -0
- package/dist/templates/codex/skills/create-command/SKILL.md +2 -2
- package/dist/templates/codex/skills/onboard/SKILL.md +11 -11
- package/dist/templates/codex/skills/record-session/SKILL.md +1 -1
- package/dist/templates/codex/skills/start/SKILL.md +8 -3
- package/dist/templates/cursor/commands/trellis-before-dev.md +29 -0
- package/dist/templates/cursor/commands/trellis-check.md +25 -0
- package/dist/templates/cursor/commands/trellis-create-command.md +2 -2
- package/dist/templates/cursor/commands/trellis-onboard.md +13 -13
- package/dist/templates/cursor/commands/trellis-record-session.md +1 -1
- package/dist/templates/cursor/commands/trellis-start.md +7 -16
- package/dist/templates/gemini/commands/trellis/before-dev.toml +33 -0
- package/dist/templates/gemini/commands/trellis/check.toml +29 -0
- package/dist/templates/gemini/commands/trellis/create-command.toml +2 -2
- package/dist/templates/gemini/commands/trellis/onboard.toml +2 -2
- package/dist/templates/gemini/commands/trellis/record-session.toml +1 -1
- package/dist/templates/gemini/commands/trellis/start.toml +9 -4
- package/dist/templates/iflow/agents/dispatch.md +1 -2
- package/dist/templates/iflow/agents/implement.md +2 -3
- package/dist/templates/iflow/commands/trellis/before-dev.md +29 -0
- package/dist/templates/iflow/commands/trellis/check.md +25 -0
- package/dist/templates/iflow/commands/trellis/create-command.md +2 -2
- package/dist/templates/iflow/commands/trellis/onboard.md +13 -13
- package/dist/templates/iflow/commands/trellis/parallel.md +1 -2
- package/dist/templates/iflow/commands/trellis/record-session.md +1 -1
- package/dist/templates/iflow/commands/trellis/start.md +8 -4
- package/dist/templates/iflow/hooks/inject-subagent-context.py +21 -13
- package/dist/templates/iflow/hooks/session-start.py +156 -1
- package/dist/templates/iflow/settings.json +2 -2
- package/dist/templates/kilo/workflows/before-dev.md +29 -0
- package/dist/templates/kilo/workflows/check.md +25 -0
- package/dist/templates/kilo/workflows/create-command.md +2 -2
- package/dist/templates/kilo/workflows/onboard.md +13 -13
- package/dist/templates/kilo/workflows/parallel.md +1 -2
- package/dist/templates/kilo/workflows/record-session.md +1 -1
- package/dist/templates/kilo/workflows/start.md +8 -3
- package/dist/templates/kiro/skills/before-dev/SKILL.md +34 -0
- package/dist/templates/kiro/skills/check/SKILL.md +30 -0
- package/dist/templates/kiro/skills/create-command/SKILL.md +2 -2
- package/dist/templates/kiro/skills/onboard/SKILL.md +11 -11
- package/dist/templates/kiro/skills/record-session/SKILL.md +1 -1
- package/dist/templates/kiro/skills/start/SKILL.md +8 -3
- package/dist/templates/markdown/spec/backend/script-conventions.md +93 -0
- package/dist/templates/opencode/agents/dispatch.md +1 -2
- package/dist/templates/opencode/agents/implement.md +2 -2
- package/dist/templates/opencode/agents/research.md +1 -2
- package/dist/templates/opencode/commands/trellis/before-dev.md +29 -0
- package/dist/templates/opencode/commands/trellis/check.md +25 -0
- package/dist/templates/opencode/commands/trellis/create-command.md +2 -2
- package/dist/templates/opencode/commands/trellis/onboard.md +13 -13
- package/dist/templates/opencode/commands/trellis/parallel.md +1 -2
- package/dist/templates/opencode/commands/trellis/record-session.md +1 -1
- package/dist/templates/opencode/commands/trellis/start.md +8 -3
- package/dist/templates/opencode/plugin/inject-subagent-context.js +45 -18
- package/dist/templates/opencode/plugin/session-start.js +149 -1
- package/dist/templates/qoder/skills/before-dev/SKILL.md +34 -0
- package/dist/templates/qoder/skills/check/SKILL.md +30 -0
- package/dist/templates/qoder/skills/create-command/SKILL.md +2 -2
- package/dist/templates/qoder/skills/onboard/SKILL.md +13 -13
- package/dist/templates/qoder/skills/record-session/SKILL.md +1 -1
- package/dist/templates/qoder/skills/start/SKILL.md +8 -3
- package/dist/templates/trellis/config.yaml +20 -0
- package/dist/templates/trellis/index.d.ts +11 -0
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +22 -0
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/add_session.py +52 -7
- package/dist/templates/trellis/scripts/common/cli_adapter.py +33 -45
- package/dist/templates/trellis/scripts/common/config.py +152 -0
- package/dist/templates/trellis/scripts/common/git.py +31 -0
- package/dist/templates/trellis/scripts/common/git_context.py +23 -586
- package/dist/templates/trellis/scripts/common/io.py +37 -0
- package/dist/templates/trellis/scripts/common/log.py +45 -0
- package/dist/templates/trellis/scripts/common/packages_context.py +233 -0
- package/dist/templates/trellis/scripts/common/paths.py +46 -0
- package/dist/templates/trellis/scripts/common/phase.py +50 -49
- package/dist/templates/trellis/scripts/common/registry.py +41 -72
- package/dist/templates/trellis/scripts/common/session_context.py +466 -0
- package/dist/templates/trellis/scripts/common/task_context.py +384 -0
- package/dist/templates/trellis/scripts/common/task_queue.py +27 -98
- package/dist/templates/trellis/scripts/common/task_store.py +534 -0
- package/dist/templates/trellis/scripts/common/task_utils.py +96 -6
- package/dist/templates/trellis/scripts/common/tasks.py +109 -0
- package/dist/templates/trellis/scripts/common/types.py +112 -0
- package/dist/templates/trellis/scripts/create_bootstrap.py +31 -26
- package/dist/templates/trellis/scripts/hooks/linear_sync.py +243 -0
- package/dist/templates/trellis/scripts/multi_agent/_bootstrap.py +17 -0
- package/dist/templates/trellis/scripts/multi_agent/cleanup.py +43 -48
- package/dist/templates/trellis/scripts/multi_agent/create_pr.py +336 -45
- package/dist/templates/trellis/scripts/multi_agent/plan.py +2 -26
- package/dist/templates/trellis/scripts/multi_agent/start.py +126 -57
- package/dist/templates/trellis/scripts/multi_agent/status.py +12 -753
- package/dist/templates/trellis/scripts/multi_agent/status_display.py +542 -0
- package/dist/templates/trellis/scripts/multi_agent/status_monitor.py +225 -0
- package/dist/templates/trellis/scripts/task.py +50 -975
- package/dist/templates/trellis/workflow.md +21 -34
- package/dist/types/migration.d.ts +3 -1
- package/dist/types/migration.d.ts.map +1 -1
- package/dist/utils/project-detector.d.ts +23 -0
- package/dist/utils/project-detector.d.ts.map +1 -1
- package/dist/utils/project-detector.js +364 -0
- package/dist/utils/project-detector.js.map +1 -1
- package/dist/utils/template-fetcher.d.ts +2 -2
- package/dist/utils/template-fetcher.d.ts.map +1 -1
- package/dist/utils/template-fetcher.js +5 -5
- package/dist/utils/template-fetcher.js.map +1 -1
- package/package.json +1 -1
- package/dist/templates/claude/commands/trellis/before-backend-dev.md +0 -13
- package/dist/templates/claude/commands/trellis/before-frontend-dev.md +0 -13
- package/dist/templates/claude/commands/trellis/check-backend.md +0 -13
- package/dist/templates/claude/commands/trellis/check-frontend.md +0 -13
- package/dist/templates/codex/skills/before-backend-dev/SKILL.md +0 -18
- package/dist/templates/codex/skills/before-frontend-dev/SKILL.md +0 -18
- package/dist/templates/codex/skills/check-backend/SKILL.md +0 -18
- package/dist/templates/codex/skills/check-frontend/SKILL.md +0 -18
- package/dist/templates/cursor/commands/trellis-before-backend-dev.md +0 -13
- package/dist/templates/cursor/commands/trellis-before-frontend-dev.md +0 -13
- package/dist/templates/cursor/commands/trellis-check-backend.md +0 -13
- package/dist/templates/cursor/commands/trellis-check-frontend.md +0 -13
- package/dist/templates/gemini/commands/trellis/before-backend-dev.toml +0 -17
- package/dist/templates/gemini/commands/trellis/before-frontend-dev.toml +0 -17
- package/dist/templates/gemini/commands/trellis/check-backend.toml +0 -17
- package/dist/templates/gemini/commands/trellis/check-frontend.toml +0 -17
- package/dist/templates/iflow/commands/trellis/before-backend-dev.md +0 -13
- package/dist/templates/iflow/commands/trellis/before-frontend-dev.md +0 -13
- package/dist/templates/iflow/commands/trellis/check-backend.md +0 -13
- package/dist/templates/iflow/commands/trellis/check-frontend.md +0 -13
- package/dist/templates/kilo/workflows/before-backend-dev.md +0 -13
- package/dist/templates/kilo/workflows/before-frontend-dev.md +0 -13
- package/dist/templates/kilo/workflows/check-backend.md +0 -13
- package/dist/templates/kilo/workflows/check-frontend.md +0 -13
- package/dist/templates/kiro/skills/before-backend-dev/SKILL.md +0 -18
- package/dist/templates/kiro/skills/before-frontend-dev/SKILL.md +0 -18
- package/dist/templates/kiro/skills/check-backend/SKILL.md +0 -18
- package/dist/templates/kiro/skills/check-frontend/SKILL.md +0 -18
- package/dist/templates/opencode/commands/trellis/before-backend-dev.md +0 -13
- package/dist/templates/opencode/commands/trellis/before-frontend-dev.md +0 -13
- package/dist/templates/opencode/commands/trellis/check-backend.md +0 -13
- package/dist/templates/opencode/commands/trellis/check-frontend.md +0 -13
- package/dist/templates/qoder/skills/before-backend-dev/SKILL.md +0 -18
- package/dist/templates/qoder/skills/before-frontend-dev/SKILL.md +0 -18
- package/dist/templates/qoder/skills/check-backend/SKILL.md +0 -18
- 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.
|
|
10
|
-
2.
|
|
11
|
-
3.
|
|
12
|
-
4.
|
|
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
|
-
#
|
|
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.
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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 =
|
|
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, _ =
|
|
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
|
-
|
|
433
|
+
run_git(["add", "-A"])
|
|
170
434
|
|
|
171
435
|
# Exclude workspace and temp files
|
|
172
|
-
|
|
173
|
-
|
|
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, _, _ =
|
|
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, _ =
|
|
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
|
-
|
|
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, _ =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
# =============================================================================
|