Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions codex/skills/internal/beautify-commit/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: beautify-commit
description: Git リポジトリで、ベースブランチまたは残したい履歴の終点 commit との差分に含まれる commit や巨大な未整理差分を、意味のある変更単位ごとに整理して commit し直すときに使う。ユーザーが「commit を分割して」「巨大なコミットを分けて」「連続コミットを整理して」「履歴をきれいにして」「この差分を変更単位でコミットして」「ベースブランチとの差分を整理して」などを依頼した場合に使う。基準 ref の指定を確認し、指定がなければデフォルトブランチでよいか yes/no で確認する。git log / diff / status を確認し、必要に応じて grill-me skill で分割方針を詰め、整理後の commit 作成には cmd-commit skill を使う。
description: Git リポジトリで、ベースブランチまたは残したい履歴の終点 commit との差分に含まれる commit や巨大な未整理差分を、意味のある変更単位ごとに整理して commit し直すときに使う。ユーザーが「commit を分割して」「巨大なコミットを分けて」「連続コミットを整理して」「履歴をきれいにして」「この差分を変更単位でコミットして」「ベースブランチとの差分を整理して」などを依頼した場合に使う。基準 ref の指定を確認し、指定がなければデフォルトブランチでよいか yes/no で確認する。git log / diff / status を確認し、必要に応じて grilling skill で分割方針を詰め、整理後の commit 作成には cmd-commit skill を使う。
---

# Split Commit
Expand Down Expand Up @@ -46,7 +46,7 @@ description: Git リポジトリで、ベースブランチまたは残したい

1. 対象範囲を決める。
2. 基準 ref との差分全体を読み、変更単位の候補を作る。
3. 分割案が曖昧、粒度が大きい、責務境界に迷う場合は、必要に応じて `grill-me` skill を使って整理方針を詰める。
3. 分割案が曖昧、粒度が大きい、責務境界に迷う場合は、必要に応じて `grilling` skill を使って整理方針を詰める。
4. 分割案を作り、必要ならユーザーに確認する。
5. バックアップ branch を作る。
6. 基準 ref との差分に含まれる commit をいったん未コミット差分へ戻し、整理して commit し直せる状態にする。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ commit 分割時の代表的な操作パターンをまとめる。
3. 基準 ref がブランチなら、必要に応じて `git merge-base <base-ref> HEAD` で分岐点を確認する。
4. `git log --oneline --reverse <base-ref>..HEAD` で差分に含まれる commit を確認する。
5. `git diff --stat <base-ref>..HEAD` と `git diff <base-ref>..HEAD` で差分全体を読む。
6. 必要に応じて `grill-me` skill を使い、変更単位、依存順、統合すべき commit、分けるべき commit を詰める。
6. 必要に応じて `grilling` skill を使い、変更単位、依存順、統合すべき commit、分けるべき commit を詰める。
7. バックアップ branch を作る。
8. 基準 ref との差分に含まれる commit をいったん未コミット差分へ戻す。
9. 変更単位ごとに stage し、`cmd-commit` skill を使って commit する。
Expand Down
6 changes: 3 additions & 3 deletions docs/skill-dependency-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ internal skills
│ │ └── cmd-rmbranch
│ ├── beautify-commit
│ │ ├── cmd-commit
│ │ └── grill-me (external)
│ │ └── grilling (external)
│ └── ci-fix
│ ├── code-general
│ ├── code-test
Expand Down Expand Up @@ -95,7 +95,7 @@ internal skills
| `cmd-start-branch` | `cmd-dispatch-agent` | 不要ブランチ整理を別 agent に投げるときに使う。 |
| `cmd-start-branch` | `cmd-rmbranch` | 不要ブランチ整理 agent の依頼内容として使う。 |
| `beautify-commit` | `cmd-commit` | 整理後の commit 作成で使う。 |
| `beautify-commit` | `grill-me` | 分割方針が曖昧なときに整理方針を詰めるため使う。 |
| `beautify-commit` | `grilling` | 分割方針が曖昧なときに整理方針を詰めるため使う。 |
| `ci-fix` | `code-general` | CI 失敗の修正に実装変更が必要なときに使う。 |
| `ci-fix` | `code-test` | テスト追加、flake 対策、検証コマンド整理が必要なときに使う。 |
| `ci-fix` | `cmd-create-pr` | CI 修正を PR 作成や PR 更新まで反映するときに使う。 |
Expand Down Expand Up @@ -185,7 +185,7 @@ graph TD
cmd_start_branch --> cmd_rmbranch["cmd-rmbranch"]

beautify_commit["beautify-commit"] --> cmd_commit
beautify_commit --> grill_me["grill-me (external)"]
beautify_commit --> grilling["grilling (external)"]

ci_fix["ci-fix"] --> code_general
ci_fix --> code_test["code-test"]
Expand Down
2 changes: 1 addition & 1 deletion docs/skill-library.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ skill 間の明示的な併用・優先関係は [docs/skill-dependency-map.md](
|---|---|---|---|
| `skill-creator` | `anthropics/skills` | 行動定義型 / ワークフロー型 | 新しい skill の作成、既存 skill の更新、メタデータや検証手順の整備に使う。 |
| `frontend-design` | `anthropics/skills` | 行動定義型 / ワークフロー型 | Web UI、ページ、コンポーネント、HTML/CSS/React などを高品質な frontend design として実装するときに使う。 |
| `grill-me` | `mattpocock/skills` | 判断定義型 / 判断基準・評価型 | 計画や設計を厳しく質問し、曖昧さや判断漏れを潰すために使う。 |
| `grilling` | `mattpocock/skills` | 判断定義型 / 判断基準・評価型 | 計画や設計を厳しく質問し、曖昧さや判断漏れを潰すために使う。 |
| `empirical-prompt-tuning` | `mizchi/skills` | 判断定義型 / スコアリング・査定型 | skill やプロンプトを実験的に改善し、評価と反復で性能を詰めるために使う。 |

## 運用メモ
Expand Down
10 changes: 5 additions & 5 deletions external-skills.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
"destination": ["~/.codex/skills/external/skill-creator"]
},
{
"name": "grill-me",
"url": "https://github.com/mattpocock/skills/tree/main/skills/productivity/grill-me",
"name": "grilling",
"url": "https://github.com/mattpocock/skills/blob/main/skills/productivity/grilling/SKILL.md",
"type": "git",
"treeHash": "c817f6f36acd483294144694a935a76d3e3eb101",
"treeHash": "10b0db61f9b3869243db8a1a0ee84f862139b94e",
"destination": [
"~/.codex/skills/external/grill-me",
"~/.claude/skills/grill-me"
"~/.codex/skills/external/grilling",
"~/.claude/skills/grilling"
]
},
{
Expand Down
12 changes: 6 additions & 6 deletions scripts/deploy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,19 +137,19 @@ make build-deploy
```json
[
{
"name": "grill-me",
"url": "https://github.com/mattpocock/skills/tree/main/skills/productivity/grill-me",
"name": "grilling",
"url": "https://github.com/mattpocock/skills/blob/main/skills/productivity/grilling/SKILL.md",
"type": "git",
"treeHash": "2a1ad17028306ebe45f0e49703fa28b9b2e7f499",
"treeHash": "10b0db61f9b3869243db8a1a0ee84f862139b94e",
"destination": [
"~/.codex/skills/external/grill-me",
"~/.claude/skills/grill-me"
"~/.codex/skills/external/grilling",
"~/.claude/skills/grilling"
]
}
]
```

`type` は現在 `git` のみ対応します。`url` は `https://github.com/<owner>/<repo>/tree/<ref>/<path>` 形式だけを受け付けます。`treeHash` は必須で、`url` が指すディレクトリの Git tree hash と照合する 40 文字の hex hash を指定します。dry-run でも取得、tree hash 照合、`SKILL.md` の存在確認を行うため、URL が不正、取得できない、tree hash が一致しない、または取得先に `SKILL.md` がない場合はエラーになります。
`type` は現在 `git` のみ対応します。`url` は `https://github.com/<owner>/<repo>/tree/<ref>/<path>` 形式、または `https://github.com/<owner>/<repo>/blob/<ref>/<path>/SKILL.md` 形式を受け付けます。`treeHash` は必須で、`url` が指す skill ディレクトリの Git tree hash と照合する 40 文字の hex hash を指定します。dry-run でも取得、tree hash 照合、`SKILL.md` の存在確認を行うため、URL が不正、取得できない、tree hash が一致しない、または取得先に `SKILL.md` がない場合はエラーになります。

対象ディレクトリの tree hash は次のように確認できます。

Expand Down
24 changes: 12 additions & 12 deletions scripts/deploy/internal/deploy/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ func TestRunnerDryRunDoesNotWriteFiles(t *testing.T) {
func TestRunnerDeploysExternalSkills(t *testing.T) {
root := t.TempDir()
writeSkill(t, filepath.Join(root, "codex", "skills", "internal", "coding"), "coding", "internal")
externalSource := filepath.Join(root, "external-source", "grill-me")
writeSkill(t, externalSource, "grill-me", "external")
externalSource := filepath.Join(root, "external-source", "grilling")
writeSkill(t, externalSource, "grilling", "external")

config := filepath.Join(root, "deploy.json")
writeConfig(t, config, `{
Expand All @@ -107,29 +107,29 @@ func TestRunnerDeploysExternalSkills(t *testing.T) {
externalConfig := filepath.Join(root, "external-skills.json")
writeConfig(t, externalConfig, `[
{
"name": "grill-me",
"url": "https://github.com/mattpocock/skills/tree/main/skills/productivity/grill-me",
"name": "grilling",
"url": "https://github.com/mattpocock/skills/blob/main/skills/productivity/grilling/SKILL.md",
"type": "git",
"treeHash": "0123456789abcdef0123456789abcdef01234567",
"destination": ["dest/external/grill-me"]
"destination": ["dest/external/grilling"]
}
]`)

var out bytes.Buffer
runner := newRunnerWithFetcher(&out, fakeExternalSkillFetcher{sources: map[string]string{"grill-me": externalSource}})
runner := newRunnerWithFetcher(&out, fakeExternalSkillFetcher{sources: map[string]string{"grilling": externalSource}})
if err := runFromDir(t, root, func() error {
return runner.Run(config, Options{ExternalSkillsPath: externalConfig, NoColor: true})
}); err != nil {
t.Fatal(err)
}

assertFileContent(t, filepath.Join(root, "dest", "external", "grill-me", "SKILL.md"), skillContent("grill-me", "external"))
if !strings.Contains(out.String(), "external-skill") || !strings.Contains(out.String(), "grill-me") {
assertFileContent(t, filepath.Join(root, "dest", "external", "grilling", "SKILL.md"), skillContent("grilling", "external"))
if !strings.Contains(out.String(), "external-skill") || !strings.Contains(out.String(), "grilling") {
t.Fatalf("expected external skill output, got:\n%s", out.String())
}
}

func TestRunnerExternalSkillsRequiresValidGitHubTreeURL(t *testing.T) {
func TestRunnerExternalSkillsRequiresValidGitHubSkillURL(t *testing.T) {
root := t.TempDir()
writeSkill(t, filepath.Join(root, "codex", "skills", "internal", "coding"), "coding", "internal")

Expand All @@ -148,7 +148,7 @@ func TestRunnerExternalSkillsRequiresValidGitHubTreeURL(t *testing.T) {
if err == nil {
t.Fatal("expected invalid URL error")
}
if !strings.Contains(err.Error(), "expected https://github.com/<owner>/<repo>/tree/<ref>/<path>") {
if !strings.Contains(err.Error(), "GitHub blob URL must end in SKILL.md") {
t.Fatalf("expected URL error, got %v", err)
}
}
Expand All @@ -161,11 +161,11 @@ func TestRunnerExternalSkillsRejectsFetchFailure(t *testing.T) {
writeConfig(t, config, `{"items":[{"source":"codex/skills","destination":"dest"}]}`)
externalConfig := filepath.Join(root, "external-skills.json")
writeConfig(t, externalConfig, `[
{"name":"grill-me","url":"https://github.com/mattpocock/skills/tree/main/skills/productivity/grill-me","type":"git","treeHash":"0123456789abcdef0123456789abcdef01234567","destination":["dest/grill-me"]}
{"name":"grilling","url":"https://github.com/mattpocock/skills/blob/main/skills/productivity/grilling/SKILL.md","type":"git","treeHash":"0123456789abcdef0123456789abcdef01234567","destination":["dest/grilling"]}
]`)

var out bytes.Buffer
runner := newRunnerWithFetcher(&out, fakeExternalSkillFetcher{errs: map[string]error{"grill-me": fmt.Errorf("network failed")}})
runner := newRunnerWithFetcher(&out, fakeExternalSkillFetcher{errs: map[string]error{"grilling": fmt.Errorf("network failed")}})
err := runFromDir(t, root, func() error {
return runner.Run(config, Options{ExternalSkillsPath: externalConfig, DryRun: true, NoColor: true})
})
Expand Down
43 changes: 25 additions & 18 deletions scripts/deploy/internal/external/external.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net/url"
"os"
"os/exec"
pathpkg "path"
"path/filepath"
"regexp"
"strings"
Expand All @@ -34,7 +35,7 @@ type GitFetcher struct {
runGit func(args ...string) (string, error)
}

type githubTreeURL struct {
type githubSkillURL struct {
owner string
repo string
ref string
Expand Down Expand Up @@ -88,7 +89,7 @@ func validateConfigSkill(i int, skill Skill) error {
return fmt.Errorf("externalSkills[%d].destination[%d] is required", i, j)
}
}
if _, err := parseGitHubTreeURL(skill.URL); err != nil {
if _, err := parseGitHubSkillURL(skill.URL); err != nil {
return fmt.Errorf("externalSkills[%d].url: %w", i, err)
}
return nil
Expand Down Expand Up @@ -246,55 +247,61 @@ func ReadSkillName(path string) (string, error) {
}

func (f GitFetcher) Fetch(skill Skill, workDir string) (string, error) {
treeURL, err := parseGitHubTreeURL(skill.URL)
skillURL, err := parseGitHubSkillURL(skill.URL)
if err != nil {
return "", err
}

repoDir := filepath.Join(workDir, safePathName(skill.Name))
cloneURL := fmt.Sprintf("https://github.com/%s/%s.git", treeURL.owner, treeURL.repo)
if _, err := f.run("clone", "--depth", "1", "--filter=blob:none", "--sparse", "--branch", treeURL.ref, cloneURL, repoDir); err != nil {
cloneURL := fmt.Sprintf("https://github.com/%s/%s.git", skillURL.owner, skillURL.repo)
if _, err := f.run("clone", "--depth", "1", "--filter=blob:none", "--sparse", "--branch", skillURL.ref, cloneURL, repoDir); err != nil {
return "", err
}
if err := f.verifyTreeHash(skill, treeURL, repoDir); err != nil {
if err := f.verifyTreeHash(skill, skillURL, repoDir); err != nil {
return "", err
}
if _, err := f.run("-C", repoDir, "sparse-checkout", "set", "--", treeURL.path); err != nil {
if _, err := f.run("-C", repoDir, "sparse-checkout", "set", "--", skillURL.path); err != nil {
return "", err
}
return filepath.Join(repoDir, filepath.FromSlash(treeURL.path)), nil
return filepath.Join(repoDir, filepath.FromSlash(skillURL.path)), nil
}

func (f GitFetcher) verifyTreeHash(skill Skill, treeURL githubTreeURL, repoDir string) error {
actual, err := f.run("-C", repoDir, "rev-parse", "HEAD:"+treeURL.path)
func (f GitFetcher) verifyTreeHash(skill Skill, skillURL githubSkillURL, repoDir string) error {
actual, err := f.run("-C", repoDir, "rev-parse", "HEAD:"+skillURL.path)
if err != nil {
return err
}
actual = strings.TrimSpace(actual)
if actual != skill.TreeHash {
return fmt.Errorf("external skill %q tree hash mismatch for %q: expected %s, got %s", skill.Name, treeURL.path, skill.TreeHash, actual)
return fmt.Errorf("external skill %q tree hash mismatch for %q: expected %s, got %s", skill.Name, skillURL.path, skill.TreeHash, actual)
}
return nil
}

func parseGitHubTreeURL(raw string) (githubTreeURL, error) {
func parseGitHubSkillURL(raw string) (githubSkillURL, error) {
u, err := url.Parse(raw)
if err != nil {
return githubTreeURL{}, fmt.Errorf("parse GitHub tree URL: %w", err)
return githubSkillURL{}, fmt.Errorf("parse GitHub skill URL: %w", err)
}
if u.Scheme != "https" || u.Host != "github.com" {
return githubTreeURL{}, fmt.Errorf("only https://github.com/<owner>/<repo>/tree/<ref>/<path> URLs are supported")
return githubSkillURL{}, fmt.Errorf("only GitHub tree URLs or blob URLs ending in SKILL.md are supported")
}
parts := strings.Split(strings.Trim(u.Path, "/"), "/")
if len(parts) < 5 || parts[2] != "tree" {
return githubTreeURL{}, fmt.Errorf("expected https://github.com/<owner>/<repo>/tree/<ref>/<path>")
if len(parts) < 5 || (parts[2] != "tree" && parts[2] != "blob") {
return githubSkillURL{}, fmt.Errorf("expected a GitHub tree URL or a blob URL ending in SKILL.md")
}
owner, repo, ref := parts[0], parts[1], parts[3]
path := strings.Join(parts[4:], "/")
if parts[2] == "blob" {
if pathpkg.Base(path) != "SKILL.md" {
return githubSkillURL{}, fmt.Errorf("GitHub blob URL must end in SKILL.md")
}
path = pathpkg.Dir(path)
}
if owner == "" || repo == "" || ref == "" || path == "" {
return githubTreeURL{}, fmt.Errorf("expected non-empty owner, repo, ref, and path")
return githubSkillURL{}, fmt.Errorf("expected non-empty owner, repo, ref, and skill path")
}
return githubTreeURL{owner: owner, repo: repo, ref: ref, path: path}, nil
return githubSkillURL{owner: owner, repo: repo, ref: ref, path: path}, nil
}

func (f GitFetcher) run(args ...string) (string, error) {
Expand Down
58 changes: 58 additions & 0 deletions scripts/deploy/internal/external/external_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,40 @@ func TestLoadRejectsInvalidTreeHash(t *testing.T) {
}
}

func TestLoadAcceptsGitHubSkillBlobURL(t *testing.T) {
path := filepath.Join(t.TempDir(), "external-skills.json")
if err := os.WriteFile(path, []byte(`[
{"name":"grilling","url":"https://github.com/owner/repo/blob/main/skills/grilling/SKILL.md","type":"git","treeHash":"0123456789abcdef0123456789abcdef01234567","destination":["dest/grilling"]}
]`), 0644); err != nil {
t.Fatal(err)
}

skills, err := Load(path)
if err != nil {
t.Fatal(err)
}
if len(skills) != 1 || skills[0].Name != "grilling" {
t.Fatalf("unexpected skills: %#v", skills)
}
}

func TestLoadRejectsGitHubBlobURLForNonSkillFile(t *testing.T) {
path := filepath.Join(t.TempDir(), "external-skills.json")
if err := os.WriteFile(path, []byte(`[
{"name":"skill","url":"https://github.com/owner/repo/blob/main/skill/README.md","type":"git","treeHash":"0123456789abcdef0123456789abcdef01234567","destination":["dest/skill"]}
]`), 0644); err != nil {
t.Fatal(err)
}

_, err := Load(path)
if err == nil {
t.Fatal("expected invalid blob URL error")
}
if !strings.Contains(err.Error(), "must end in SKILL.md") {
t.Fatalf("unexpected error: %v", err)
}
}

func TestGitFetcherFetchVerifiesTreeHash(t *testing.T) {
var calls []string
fetcher := GitFetcher{runGit: func(args ...string) (string, error) {
Expand All @@ -71,6 +105,30 @@ func TestGitFetcherFetchVerifiesTreeHash(t *testing.T) {
}
}

func TestGitFetcherFetchUsesParentDirectoryForSkillBlobURL(t *testing.T) {
var calls []string
skill := testSkill("grilling")
skill.URL = "https://github.com/owner/repo/blob/main/skills/productivity/grilling/SKILL.md"
fetcher := GitFetcher{runGit: func(args ...string) (string, error) {
calls = append(calls, strings.Join(args, " "))
if isRevParseTree(args, "skills/productivity/grilling") {
return testTreeHash + "\n", nil
}
return "", nil
}}

src, err := fetcher.Fetch(skill, t.TempDir())
if err != nil {
t.Fatal(err)
}
if !strings.HasSuffix(src, filepath.Join("skills", "productivity", "grilling")) {
t.Fatalf("unexpected source path: %s", src)
}
if !strings.HasSuffix(calls[2], "sparse-checkout set -- skills/productivity/grilling") {
t.Fatalf("unexpected sparse-checkout call: %v", calls)
}
}

func TestGitFetcherFetchRejectsTreeHashMismatch(t *testing.T) {
actual := "abcdef0123456789abcdef0123456789abcdef01"
fetcher := GitFetcher{runGit: func(args ...string) (string, error) {
Expand Down
4 changes: 2 additions & 2 deletions scripts/deploy/internal/skillscan/skillscan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func TestWalkSkillDirsFindsRegularSkillFiles(t *testing.T) {
root := t.TempDir()
writeFile(t, filepath.Join(root, "internal", "coding", "SKILL.md"))
writeFile(t, filepath.Join(root, "internal", "coding", "assets", "prompt.md"))
writeFile(t, filepath.Join(root, "external", "grill-me", "SKILL.md"))
writeFile(t, filepath.Join(root, "external", "grilling", "SKILL.md"))
writeFile(t, filepath.Join(root, "README.md"))

var got []Dir
Expand All @@ -30,7 +30,7 @@ func TestWalkSkillDirsFindsRegularSkillFiles(t *testing.T) {
t.Fatal(err)
}

wantNames := []string{"grill-me", "coding"}
wantNames := []string{"grilling", "coding"}
var gotNames []string
for _, dir := range got {
gotNames = append(gotNames, dir.Name)
Expand Down
Loading