Skip to content
This repository was archived by the owner on Jun 18, 2026. It is now read-only.

Commit 38a0fb3

Browse files
committed
feat: externalise bash blocks — Phase 2, 25 per-skill scripts across 11 skills
workspace-init (3 scripts), work-end (2), git-squash (3), work-pause (1), work-resume (1), issue-workflow (1), retro-issues (1), publish-blog (1), git-commit (1), handover (1), work-start (1). 324 tests. Closes #123
1 parent 0afcd3c commit 38a0fb3

42 files changed

Lines changed: 7965 additions & 530 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

git-commit/SKILL.md

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -216,10 +216,10 @@ git log @{u}..HEAD --oneline 2>/dev/null || git log origin/HEAD..HEAD --oneline
216216
217217
If user replies **SQUASH**:
218218
```bash
219-
git reset --soft HEAD~1
220-
# Re-stage all (previous + new changes are now combined in working tree)
221-
# Commit with a single message covering the combined change
219+
python3 ~/.claude/skills/git-commit/commit_exec.py squash <project>
222220
```
221+
Read `SQUASHED` from output. If `ERROR=no_parent` → warn and continue without squashing.
222+
Re-stage all (previous + new changes are now combined in working tree) and commit with a single message covering the combined change.
223223

224224
If no unpushed commits, or no squash benefit, continue silently.
225225

@@ -302,25 +302,22 @@ Then ask exactly:
302302
**If documentation updates were proposed**, run in this exact order:
303303
1. Let update-claude-md apply its changes (if proposed)
304304
2. Apply README.md changes per readme-sync.md workflow (if proposed)
305-
3. Stage updated files: `git add CLAUDE.md README.md` (only files that were changed)
306-
4. Commit with the confirmed message:
305+
3. Stage updated doc files:
307306
```bash
308-
git commit -m "<subject>" -m "<body if any>"
307+
python3 ~/.claude/skills/git-commit/commit_exec.py stage-docs <project> files=CLAUDE.md,README.md
309308
```
310-
5. Confirm success:
309+
Read `STAGED=<count>` from output (only files that were actually changed get staged).
310+
4. Commit all staged files:
311311
```bash
312-
git log --oneline -1
312+
python3 ~/.claude/skills/git-commit/commit_exec.py commit <project> message=<confirmed-message> files=<all-staged-files-csv>
313313
```
314+
Read `COMMITTED=yes, SHA=<sha>` from output. If `ERROR=nothing_to_commit` → warn user.
314315

315-
**If no documentation updates**, run in this exact order:
316-
1. Commit with the confirmed message:
316+
**If no documentation updates**, commit directly:
317317
```bash
318-
git commit -m "<subject>" -m "<body if any>"
319-
```
320-
2. Confirm success:
321-
```bash
322-
git log --oneline -1
318+
python3 ~/.claude/skills/git-commit/commit_exec.py commit <project> message=<confirmed-message> files=<staged-files-csv>
323319
```
320+
Read `COMMITTED=yes, SHA=<sha>` from output. If `ERROR=nothing_to_commit` → warn user.
324321

325322
### Step 8 — Handle edge cases
326323

git-commit/commit_exec.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
#!/usr/bin/env python3
2+
"""
3+
commit_exec.py — Externalized git operations for git-commit
4+
5+
Subcommands:
6+
7+
commit <project> message=<msg> files=<csv>
8+
Stage listed files and commit with the given message.
9+
Output: COMMITTED=yes, SHA=<sha>
10+
Error: ERROR=nothing_to_commit
11+
12+
squash <project>
13+
Soft-reset HEAD~1 to combine with previous commit.
14+
Output: SQUASHED=yes
15+
Error: ERROR=no_parent
16+
17+
stage-docs <project> files=<csv>
18+
Stage documentation files (CLAUDE.md, README.md, etc.).
19+
Output: STAGED=<count>
20+
21+
Exit codes:
22+
0 success
23+
1 error
24+
"""
25+
26+
import subprocess
27+
import sys
28+
29+
30+
def run_git(repo: str, *args: str) -> tuple[bool, str]:
31+
"""Run git command. Returns (success, stdout)."""
32+
try:
33+
result = subprocess.run(
34+
["git", "-C", repo] + list(args),
35+
capture_output=True,
36+
text=True,
37+
check=False,
38+
)
39+
return result.returncode == 0, result.stdout.strip()
40+
except Exception as e:
41+
return False, str(e)
42+
43+
44+
def commit(project: str, message: str, files: list[str]) -> int:
45+
"""Stage files and commit."""
46+
if not files:
47+
print("ERROR=no_files")
48+
return 1
49+
50+
for f in files:
51+
ok, _ = run_git(project, "add", f)
52+
if not ok:
53+
print(f"ERROR=add_failed:{f}")
54+
return 1
55+
56+
# Check something is actually staged
57+
ok, diff_out = run_git(project, "diff", "--staged", "--stat")
58+
if ok and not diff_out.strip():
59+
print("ERROR=nothing_to_commit")
60+
return 1
61+
62+
ok, _ = run_git(project, "commit", "-m", message)
63+
if not ok:
64+
print("ERROR=nothing_to_commit")
65+
return 1
66+
67+
ok, sha = run_git(project, "rev-parse", "HEAD")
68+
if not ok:
69+
print("ERROR=sha_failed")
70+
return 1
71+
72+
print("COMMITTED=yes")
73+
print(f"SHA={sha}")
74+
return 0
75+
76+
77+
def squash(project: str) -> int:
78+
"""Soft-reset HEAD~1 to combine with previous commit."""
79+
# Check HEAD has a parent
80+
ok, _ = run_git(project, "rev-parse", "HEAD~1")
81+
if not ok:
82+
print("ERROR=no_parent")
83+
return 1
84+
85+
ok, _ = run_git(project, "reset", "--soft", "HEAD~1")
86+
if not ok:
87+
print("ERROR=reset_failed")
88+
return 1
89+
90+
print("SQUASHED=yes")
91+
return 0
92+
93+
94+
def stage_docs(project: str, files: list[str]) -> int:
95+
"""Stage documentation files."""
96+
if not files:
97+
print("STAGED=0")
98+
return 0
99+
100+
count = 0
101+
for f in files:
102+
ok, _ = run_git(project, "add", f)
103+
if ok:
104+
count += 1
105+
106+
print(f"STAGED={count}")
107+
return 0
108+
109+
110+
def parse_kv_args(args: list[str]) -> dict[str, str]:
111+
"""Parse key=value arguments into dict."""
112+
result: dict[str, str] = {}
113+
for arg in args:
114+
if "=" in arg:
115+
key, _, val = arg.partition("=")
116+
result[key] = val
117+
return result
118+
119+
120+
def main() -> int:
121+
if len(sys.argv) < 2:
122+
print("ERROR=missing_subcommand")
123+
return 1
124+
125+
cmd = sys.argv[1]
126+
127+
if cmd == "commit":
128+
if len(sys.argv) < 3:
129+
print("ERROR=missing_args")
130+
return 1
131+
project = sys.argv[2]
132+
kv = parse_kv_args(sys.argv[3:])
133+
message = kv.get("message")
134+
files_csv = kv.get("files", "")
135+
if not message:
136+
print("ERROR=missing_message")
137+
return 1
138+
files = [f.strip() for f in files_csv.split(",") if f.strip()]
139+
return commit(project, message, files)
140+
141+
elif cmd == "squash":
142+
if len(sys.argv) < 3:
143+
print("ERROR=missing_args")
144+
return 1
145+
project = sys.argv[2]
146+
return squash(project)
147+
148+
elif cmd == "stage-docs":
149+
if len(sys.argv) < 3:
150+
print("ERROR=missing_args")
151+
return 1
152+
project = sys.argv[2]
153+
kv = parse_kv_args(sys.argv[3:])
154+
files_csv = kv.get("files", "")
155+
files = [f.strip() for f in files_csv.split(",") if f.strip()]
156+
return stage_docs(project, files)
157+
158+
else:
159+
print("ERROR=unknown_subcommand")
160+
return 1
161+
162+
163+
if __name__ == "__main__":
164+
sys.exit(main())

0 commit comments

Comments
 (0)