diff --git a/codex/skills/internal/beautify-commit/SKILL.md b/codex/skills/internal/beautify-commit/SKILL.md index c57de35..65d5a88 100644 --- a/codex/skills/internal/beautify-commit/SKILL.md +++ b/codex/skills/internal/beautify-commit/SKILL.md @@ -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 @@ -46,7 +46,7 @@ description: Git リポジトリで、ベースブランチまたは残したい 1. 対象範囲を決める。 2. 基準 ref との差分全体を読み、変更単位の候補を作る。 -3. 分割案が曖昧、粒度が大きい、責務境界に迷う場合は、必要に応じて `grill-me` skill を使って整理方針を詰める。 +3. 分割案が曖昧、粒度が大きい、責務境界に迷う場合は、必要に応じて `grilling` skill を使って整理方針を詰める。 4. 分割案を作り、必要ならユーザーに確認する。 5. バックアップ branch を作る。 6. 基準 ref との差分に含まれる commit をいったん未コミット差分へ戻し、整理して commit し直せる状態にする。 diff --git a/codex/skills/internal/beautify-commit/references/split-workflows.md b/codex/skills/internal/beautify-commit/references/split-workflows.md index 8e28cd3..73be11b 100644 --- a/codex/skills/internal/beautify-commit/references/split-workflows.md +++ b/codex/skills/internal/beautify-commit/references/split-workflows.md @@ -14,7 +14,7 @@ commit 分割時の代表的な操作パターンをまとめる。 3. 基準 ref がブランチなら、必要に応じて `git merge-base HEAD` で分岐点を確認する。 4. `git log --oneline --reverse ..HEAD` で差分に含まれる commit を確認する。 5. `git diff --stat ..HEAD` と `git diff ..HEAD` で差分全体を読む。 -6. 必要に応じて `grill-me` skill を使い、変更単位、依存順、統合すべき commit、分けるべき commit を詰める。 +6. 必要に応じて `grilling` skill を使い、変更単位、依存順、統合すべき commit、分けるべき commit を詰める。 7. バックアップ branch を作る。 8. 基準 ref との差分に含まれる commit をいったん未コミット差分へ戻す。 9. 変更単位ごとに stage し、`cmd-commit` skill を使って commit する。 diff --git a/docs/skill-dependency-map.md b/docs/skill-dependency-map.md index 152a686..3a6310b 100644 --- a/docs/skill-dependency-map.md +++ b/docs/skill-dependency-map.md @@ -21,7 +21,7 @@ internal skills │ │ └── cmd-rmbranch │ ├── beautify-commit │ │ ├── cmd-commit -│ │ └── grill-me (external) +│ │ └── grilling (external) │ └── ci-fix │ ├── code-general │ ├── code-test @@ -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 更新まで反映するときに使う。 | @@ -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"] diff --git a/docs/skill-library.md b/docs/skill-library.md index e51f62c..d3fae4b 100644 --- a/docs/skill-library.md +++ b/docs/skill-library.md @@ -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 やプロンプトを実験的に改善し、評価と反復で性能を詰めるために使う。 | ## 運用メモ diff --git a/external-skills.json b/external-skills.json index f495da3..2f14347 100644 --- a/external-skills.json +++ b/external-skills.json @@ -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" ] }, { diff --git a/scripts/deploy/README.md b/scripts/deploy/README.md index 14466a6..74bab0a 100644 --- a/scripts/deploy/README.md +++ b/scripts/deploy/README.md @@ -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///tree//` 形式だけを受け付けます。`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///tree//` 形式、または `https://github.com///blob///SKILL.md` 形式を受け付けます。`treeHash` は必須で、`url` が指す skill ディレクトリの Git tree hash と照合する 40 文字の hex hash を指定します。dry-run でも取得、tree hash 照合、`SKILL.md` の存在確認を行うため、URL が不正、取得できない、tree hash が一致しない、または取得先に `SKILL.md` がない場合はエラーになります。 対象ディレクトリの tree hash は次のように確認できます。 diff --git a/scripts/deploy/internal/deploy/runner_test.go b/scripts/deploy/internal/deploy/runner_test.go index 1e6bce2..b331052 100644 --- a/scripts/deploy/internal/deploy/runner_test.go +++ b/scripts/deploy/internal/deploy/runner_test.go @@ -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, `{ @@ -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") @@ -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///tree//") { + if !strings.Contains(err.Error(), "GitHub blob URL must end in SKILL.md") { t.Fatalf("expected URL error, got %v", err) } } @@ -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}) }) diff --git a/scripts/deploy/internal/external/external.go b/scripts/deploy/internal/external/external.go index 637ed8d..ef8f23f 100644 --- a/scripts/deploy/internal/external/external.go +++ b/scripts/deploy/internal/external/external.go @@ -8,6 +8,7 @@ import ( "net/url" "os" "os/exec" + pathpkg "path" "path/filepath" "regexp" "strings" @@ -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 @@ -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 @@ -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///tree// 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///tree//") + 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) { diff --git a/scripts/deploy/internal/external/external_test.go b/scripts/deploy/internal/external/external_test.go index 8ac3f19..0333841 100644 --- a/scripts/deploy/internal/external/external_test.go +++ b/scripts/deploy/internal/external/external_test.go @@ -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) { @@ -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) { diff --git a/scripts/deploy/internal/skillscan/skillscan_test.go b/scripts/deploy/internal/skillscan/skillscan_test.go index eeba258..6c40d3f 100644 --- a/scripts/deploy/internal/skillscan/skillscan_test.go +++ b/scripts/deploy/internal/skillscan/skillscan_test.go @@ -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 @@ -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)