diff --git a/.codex/skills/git-checkpoint-commit/SKILL.md b/.codex/skills/git-checkpoint-commit/SKILL.md new file mode 100644 index 0000000..715137c --- /dev/null +++ b/.codex/skills/git-checkpoint-commit/SKILL.md @@ -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. diff --git a/.codex/skills/git-checkpoint-commit/agents/openai.yaml b/.codex/skills/git-checkpoint-commit/agents/openai.yaml new file mode 100644 index 0000000..debebf6 --- /dev/null +++ b/.codex/skills/git-checkpoint-commit/agents/openai.yaml @@ -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." diff --git a/.codex/skills/git-checkpoint-commit/scripts/checkpoint_commit.py b/.codex/skills/git-checkpoint-commit/scripts/checkpoint_commit.py new file mode 100644 index 0000000..496be41 --- /dev/null +++ b/.codex/skills/git-checkpoint-commit/scripts/checkpoint_commit.py @@ -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()) diff --git a/.gitignore b/.gitignore index 324da5e..fb14002 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,13 @@ web/.vite/ .omc/ .omx/ .claude/ -.codex/ +.codex/* +!.codex/skills/ +.codex/skills/* +!.codex/skills/agent-change-log/ +!.codex/skills/agent-change-log/** +!.codex/skills/git-checkpoint-commit/ +!.codex/skills/git-checkpoint-commit/** .codex-temp/ .superpowers/ *.log