chore(skills): add git checkpoint commit loop
This commit is contained in:
85
.codex/skills/git-checkpoint-commit/SKILL.md
Normal file
85
.codex/skills/git-checkpoint-commit/SKILL.md
Normal file
@@ -0,0 +1,85 @@
|
||||
---
|
||||
name: git-checkpoint-commit
|
||||
description: Use when a coding task finishes a verified bug fix, key feature, risky refactor checkpoint, local backup commit, checkpoint commit, 提交备份, 本地提交, or when an active session has accumulated about five meaningful edit rounds and should create a scoped local git commit without pushing.
|
||||
---
|
||||
|
||||
# Git Checkpoint Commit
|
||||
|
||||
## Overview
|
||||
|
||||
Use this skill to keep a local git backup loop during active development. The goal is to commit verified, task-scoped progress at meaningful checkpoints without mixing unrelated user changes or pushing remotely.
|
||||
|
||||
## Trigger Rules
|
||||
|
||||
Create a local checkpoint commit when any condition is true:
|
||||
|
||||
- A bug fix is implemented and the targeted regression check passes.
|
||||
- A key feature slice is implemented and the smallest relevant verification passes.
|
||||
- A risky refactor reaches a behavior-preserving checkpoint.
|
||||
- The current session has reached about five meaningful edit rounds since the last commit.
|
||||
- The user asks for `提交`, `本地提交`, `备份`, `checkpoint`, `commit`, or `形成提交循环`.
|
||||
|
||||
Do not use this skill when the user explicitly says not to commit, when the change is exploratory and unverified, or when the only available commit would include unrelated dirty files.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Inspect the working tree with `git status --short`.
|
||||
2. Identify files or hunks owned by the current task.
|
||||
3. Run the smallest relevant verification first.
|
||||
- In X-Financial, run backend tests inside `x-financial-main` when backend code is involved.
|
||||
- Use targeted frontend tests/builds for web-only changes.
|
||||
4. Commit only the task-owned files.
|
||||
5. Report the commit hash and verification evidence.
|
||||
6. Reset the session edit counter to zero after a successful checkpoint.
|
||||
|
||||
## Safety Rules
|
||||
|
||||
- Never commit unrelated user changes just to make the tree clean.
|
||||
- Never push as part of this skill.
|
||||
- Never rewrite history, amend old commits, or run destructive git commands.
|
||||
- If the same file contains unrelated hunks, split the staging carefully with `git add -p` or a narrower manual patch.
|
||||
- If the index already contains staged changes, inspect them first; do not mix them into a checkpoint unless they belong to the same current task.
|
||||
- If verification cannot run, say why in the commit body or final report and prefer a `chore(checkpoint)` message rather than a confident `fix` or `feat`.
|
||||
|
||||
## Commit Style
|
||||
|
||||
Use normal semantic messages for completed, verified work:
|
||||
|
||||
- `fix(workbench): keep application preview after draft save failure`
|
||||
- `feat(reimbursements): add attachment association job polling`
|
||||
- `refactor(claims): split draft flow serialization`
|
||||
|
||||
Use checkpoint messages for interim backup points:
|
||||
|
||||
- `chore(checkpoint): backup attachment association flow`
|
||||
- `chore(checkpoint): backup after five edit rounds`
|
||||
|
||||
Keep the subject concise. Add body lines only when the verification state or scope needs clarity.
|
||||
|
||||
## Helper Script
|
||||
|
||||
Use `scripts/checkpoint_commit.py` when a path-scoped local commit is enough:
|
||||
|
||||
```bash
|
||||
python3 .codex/skills/git-checkpoint-commit/scripts/checkpoint_commit.py \
|
||||
--message "chore(checkpoint): backup workbench AI flow" \
|
||||
web/src/composables/workbenchAiMode/useWorkbenchAiExpenseFlow.js \
|
||||
web/tests/workbench-ai-mode-switch.test.mjs
|
||||
```
|
||||
|
||||
Preview before committing:
|
||||
|
||||
```bash
|
||||
python3 .codex/skills/git-checkpoint-commit/scripts/checkpoint_commit.py \
|
||||
--dry-run \
|
||||
web/src/utils/expenseApplicationPreview.js
|
||||
```
|
||||
|
||||
The script refuses to commit the whole tree unless `--allow-all` is passed. Use `--allow-all` only when the current task truly owns every dirty file.
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
- Mistaking backup for verification. Verify first, then commit.
|
||||
- Staging `.` in a dirty worktree. Stage explicit paths or hunks.
|
||||
- Combining documentation cleanup, feature work, and unrelated local edits in one checkpoint.
|
||||
- Continuing indefinitely after multiple verified slices. Commit once the counter reaches five meaningful edit rounds.
|
||||
4
.codex/skills/git-checkpoint-commit/agents/openai.yaml
Normal file
4
.codex/skills/git-checkpoint-commit/agents/openai.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Git Checkpoint Commit"
|
||||
short_description: "Create local backup commits at safe milestones"
|
||||
default_prompt: "Use $git-checkpoint-commit to checkpoint verified task changes into a local git commit."
|
||||
127
.codex/skills/git-checkpoint-commit/scripts/checkpoint_commit.py
Normal file
127
.codex/skills/git-checkpoint-commit/scripts/checkpoint_commit.py
Normal file
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create a path-scoped local git checkpoint commit."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def run_git(args: list[str], cwd: Path, *, check: bool = True) -> subprocess.CompletedProcess[str]:
|
||||
result = subprocess.run(
|
||||
["git", *args],
|
||||
cwd=cwd,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=60,
|
||||
)
|
||||
if check and result.returncode != 0:
|
||||
message = result.stderr.strip() or result.stdout.strip()
|
||||
raise SystemExit(f"git {' '.join(args)} failed: {message}")
|
||||
return result
|
||||
|
||||
|
||||
def repo_root() -> Path:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=60,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise SystemExit("Not inside a git repository.")
|
||||
return Path(result.stdout.strip())
|
||||
|
||||
|
||||
def load_paths(args: argparse.Namespace) -> list[str]:
|
||||
paths = list(args.paths)
|
||||
if args.paths_from_file:
|
||||
raw_lines = Path(args.paths_from_file).read_text(encoding="utf-8").splitlines()
|
||||
paths.extend(line.strip() for line in raw_lines if line.strip() and not line.startswith("#"))
|
||||
return paths
|
||||
|
||||
|
||||
def ensure_clean_index(root: Path) -> None:
|
||||
staged = run_git(["diff", "--cached", "--name-only"], root).stdout.strip()
|
||||
if staged:
|
||||
raise SystemExit(
|
||||
"Refusing to continue because the index already has staged changes:\n"
|
||||
f"{staged}\n"
|
||||
"Commit or unstage them before running this checkpoint helper."
|
||||
)
|
||||
|
||||
|
||||
def status_for(root: Path, paths: list[str], allow_all: bool) -> str:
|
||||
command = ["status", "--short"]
|
||||
if not allow_all:
|
||||
command.extend(["--", *paths])
|
||||
return run_git(command, root).stdout.strip()
|
||||
|
||||
|
||||
def infer_message(paths: list[str], allow_all: bool) -> str:
|
||||
if allow_all or not paths:
|
||||
return "chore(checkpoint): backup current task changes"
|
||||
first_parts = [Path(item).parts[0] for item in paths if Path(item).parts]
|
||||
scope = first_parts[0] if first_parts else "task"
|
||||
return f"chore(checkpoint): backup {scope} changes"
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("paths", nargs="*", help="Task-owned paths to stage and commit.")
|
||||
parser.add_argument("-m", "--message", help="Commit message subject/body.")
|
||||
parser.add_argument("--paths-from-file", help="Read additional pathspecs from a UTF-8 text file.")
|
||||
parser.add_argument("--dry-run", action="store_true", help="Show matching changes without staging.")
|
||||
parser.add_argument("--allow-all", action="store_true", help="Allow committing all dirty files.")
|
||||
parser.add_argument("--no-verify", action="store_true", help="Pass --no-verify to git commit.")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
root = repo_root()
|
||||
paths = load_paths(args)
|
||||
|
||||
if not paths and not args.allow_all:
|
||||
raise SystemExit("Pass explicit paths, or use --allow-all when the current task owns all changes.")
|
||||
|
||||
current_status = status_for(root, paths, args.allow_all)
|
||||
if not current_status:
|
||||
print("No matching changes to commit.")
|
||||
return 0
|
||||
|
||||
print(current_status)
|
||||
if args.dry_run:
|
||||
return 0
|
||||
|
||||
ensure_clean_index(root)
|
||||
|
||||
# 使用 -A 保留删除/重命名等变更,但只作用于明确传入的 pathspec。
|
||||
if args.allow_all:
|
||||
run_git(["add", "-A"], root)
|
||||
else:
|
||||
run_git(["add", "-A", "--", *paths], root)
|
||||
|
||||
staged_summary = run_git(["diff", "--cached", "--name-status"], root).stdout.strip()
|
||||
if not staged_summary:
|
||||
raise SystemExit("No staged changes after git add.")
|
||||
|
||||
message = args.message or infer_message(paths, args.allow_all)
|
||||
commit_command = ["commit"]
|
||||
if args.no_verify:
|
||||
commit_command.append("--no-verify")
|
||||
commit_command.extend(["-m", message])
|
||||
commit_result = run_git(commit_command, root)
|
||||
sys.stdout.write(commit_result.stdout)
|
||||
|
||||
commit_hash = run_git(["rev-parse", "--short", "HEAD"], root).stdout.strip()
|
||||
print(f"checkpoint_commit={commit_hash}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user