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
11 changes: 11 additions & 0 deletions LESSONS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# LESSONS(実装知見ログ)

## 2026-09-12: settings-advisor のパス判定を root 相対の「パス成分」へ統一(PR #20 レビュー対応)

- 却下した案: Codex の指摘どおり CI 検知(`.circleci` 等の部分文字列一致)と本番設定検知(絶対パスのディレクトリ名走査)を**別々に**直す案。前者に区切り境界チェックを足し、後者だけ root 相対化する。
- 決め手: 両者は同一バグの別断面だった。`isCIPath` は絶対パス `slashPath` を見ていたため、`~/.circleci/repos/app/foo.yml` のように**スキャンルート外の祖先ディレクトリ名**でも `HasCI` が立つ(prod 側と同じ祖先混入)。別々に直すと prod 側だけ root 相対になり、CI 側の祖先混入が残る。実際、追加した `TestScanWorkspaceAncestorOutsideRoot/.circleci` は修正前コードで FAIL することを確認した。
- 覆す条件: スキャンルート外のパス情報(親リポジトリの CI 設定など)を意図的に判定材料に使う要件が出た場合。

## 2026-09-11: settings-advisor のモデル選定を traits 駆動へ移行
- 却下した案: `main.go` 内でモデル名文字列(`Claude Sonnet 4.6 (Thinking)` や `GPT-OSS 120B (Medium)`)をハードコードして分岐する方式
- 決め手: `models.json` 側のモデル名や定義を更新した際に、コード内のハードコード文字列と不整合が生じて推奨モデルが選定されなくなる。また世代交代(Gemini 3.5→3.8等)のたびに Go ソースの修正・再ビルド・全プラットフォームバイナリ再配布が必要になるという密結合の制約を観測。
- 覆す条件: モデルの選定要件が `traits` の一致(`instruction-following`, `quota-independent` 等)では表現できず、Go 側で複雑な外部条件分岐や動的プロファイリングが必要になった場合。

## 2026-07-02: agy 1.0.15 での hooks/rules 実機再検証

### 学んだこと
Expand Down
14 changes: 14 additions & 0 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,17 @@ PR #19 に寄せられた Codex Review 指摘(Go コード 6件、CI ワーク
- [x] ステップ1: `worktree-manager/src/main.go` のバグ・境界条件の修正(main worktree 判定、大文字小文字比較、NUL 区切りパース、ハイフン引数保護、locked 削除、prune stderr 捕捉)
- [x] ステップ2: `main_test.go` に指摘事項の再現・回帰テストを追加し全テスト通過を確認
- [x] ステップ3: 決定論的再ビルド(Go 1.26.5)および CI 設定(govulncheck)の確認・検証

## フェーズ12: settings-advisor の検知精度向上とモデル選定刷新(完了)

### 目的
settings-advisor のスキャン精度(CI/CD、言語、除外ディレクトリ、.env誤検知、本番設定パス)を向上させ、models.json の最新化およびモデル名ハードコードの排除(traits駆動)を行う。

### 完了条件
1. スキャン検知の拡充(GitLab CI等のCI検知、Dart/Swift等の言語追加、.next/target等のスキップ、.env.example等の除外、prodディレクトリ検知)が実装され、テストで検証されていること。
2. models.json が最新モデルに更新され、main.go のモデル名ハードコードが traits 駆動のマッチングに置き換わっていること。
3. ドキュメント(README.md / README_ja.md)が同期され、テスト(go test / go vet)およびビルドが通過すること。

- [x] ステップ1: スキャン検知機能と除外ロジックの改善・テスト追加
- [x] ステップ2: models.json 更新と traits 駆動モデル選定ロジックの実装・テスト追加
- [x] ステップ3: ドキュメント同期(README.md / README_ja.md)とビルド・品質検証
6 changes: 3 additions & 3 deletions settings-advisor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ agy plugin install https://github.com/kwrkb/agy-plugins/settings-advisor

## Recommendation Logic

- **Model Tier**: Classifies into `light` / `mid` / `heavy` based on LOC and language diversity. Elevates to `heavy` when `task_hint` contains keywords like refactoring or architecture design. Prioritizes specialized models (e.g. Claude Sonnet for strict instruction following, GPT-OSS for fallback/quota diversity).
- **Sandbox**: Recommends `enableTerminalSandbox: true` upon detecting `.env` or secret configurations.
- **Tool Permissions**: Recommends `proceed-in-sandbox` when CI/CD workflows are present, and `strict` when production configuration files are detected.
- **Model Tier**: Classifies into `light` / `mid` / `heavy` based on LOC and language diversity across supported languages (Go, TS, JS, Python, Rust, Dart, Swift, Vue, Svelte, PHP, Scala, C/C++, C#, Java, Kotlin, Ruby, Zig, Lua, SQL, Shell, Terraform). Excludes build artifacts and package directories (`.next`, `.nuxt`, `out`, `target`, `.dart_tool`, `Pods`, `node_modules`, `dist`, `build`, etc.). Elevates to `heavy` when `task_hint` contains keywords like refactoring or architecture design. Dynamically matches `traits` in `models.json` (e.g. `instruction-following` for Claude Sonnet, `quota-independent` for GPT-OSS).
- **Sandbox**: Recommends `enableTerminalSandbox: true` upon detecting actual `.env` configurations (templates such as `.env.example` or `.env.sample` are safely ignored).
- **Tool Permissions**: Recommends `proceed-in-sandbox` when CI/CD configurations are present (GitHub Actions, GitLab CI, CircleCI, Bitbucket Pipelines, Azure Pipelines), and `strict` when production configuration files or directory paths (`prod` / `production` tokens) are detected.

## Bundled Skill

Expand Down
8 changes: 4 additions & 4 deletions settings-advisor/README_ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ agy plugin install https://github.com/kwrkb/agy-plugins/settings-advisor

## 推奨ロジック

- **モデルティア**: 総行数と言語数から `light` / `mid` / `heavy` を判定。`task_hint` に「リファクタ/設計/アーキ/migrate」等が含まれる場合は `heavy` に格上げ。「手順/フォーマット/strict/document」等は instruction-following 重視の **Claude Sonnet 4.6 (Thinking)**、「quota/枯渇/フォールバック/別視点」等は **GPT-OSS 120B** を優先候補にする。
- **モデル名**: `models.json` に定義し、`agy models` の表示名と一致させている(推奨はそのまま `/model` で選択可能)。
- **サンドボックス**: `.env`(系)を検出したら `enableTerminalSandbox: true` を提案(機密漏洩防止)。
- **ツール許可モード(`toolPermission`)**: `.github/workflows` 検出時は `proceed-in-sandbox`、本番設定ファイル(`*.prod.*` / `production.*` 等)検出時は最も厳格な `strict` を提案。値はいずれも agy 本体の有効列挙値(`always-proceed` / `request-review` / `strict` / `proceed-in-sandbox`)。
- **モデルティア**: 総行数と言語数から `light` / `mid` / `heavy` を判定。Go, TS, JS, Python, Rust, Dart, Swift, Vue, Svelte, PHP, Scala, C/C++, C#, Java, Kotlin, Ruby, Zig, Lua, SQL, Shell, Terraform などの主要開発言語を幅広く走査。ビルド生成物やパッケージキャッシュ(`.next`, `.nuxt`, `out`, `target`, `.dart_tool`, `Pods`, `node_modules`, `dist`, `build` 等)はスキップ。`task_hint` に「リファクタ/設計/アーキ/migrate」等が含まれる場合は `heavy` に格上げ。
- **モデル名**: `models.json` の定義および `traits`(`instruction-following` による Claude Sonnet、`quota-independent` による GPT-OSS など)に基づき動的に推奨モデルをマッチング。
- **サンドボックス**: 実機密設定の `.env`(系)を検出したら `enableTerminalSandbox: true` を提案(`.env.example` や `.env.sample` などのテンプレートは誤検知防止のため除外)。
- **ツール許可モード(`toolPermission`)**: CI/CD 設定(GitHub Actions, GitLab CI, CircleCI, Bitbucket, Azure Pipelines)検出時は `proceed-in-sandbox`、本番設定ファイルまたはパス(ファイル名や `config/prod/` などのディレクトリ名に `prod`/`production` トークンを含む場合)検出時は最も厳格な `strict` を提案。値はいずれも agy 本体の有効列挙値(`always-proceed` / `request-review` / `strict` / `proceed-in-sandbox`)。

## 同梱スキル

Expand Down
Binary file modified settings-advisor/bin/settings-advisor-darwin-arm64
Binary file not shown.
Binary file modified settings-advisor/bin/settings-advisor-linux-amd64
Binary file not shown.
Binary file modified settings-advisor/bin/settings-advisor.exe
Binary file not shown.
6 changes: 3 additions & 3 deletions settings-advisor/models.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"models": [
{"id": "gemini-3.5-flash-low", "name": "Gemini 3.5 Flash (Low)", "tier": "light", "traits": ["fast", "cheap"]},
{"id": "gemini-3.5-flash-medium", "name": "Gemini 3.5 Flash (Medium)", "tier": "light", "traits": ["fast", "balanced"]},
{"id": "gemini-3.5-flash-high", "name": "Gemini 3.5 Flash (High)", "tier": "mid", "traits": ["accurate", "cost-effective"]},
{"id": "gemini-3.8-flash-low", "name": "Gemini 3.8 Flash (Low)", "tier": "light", "traits": ["fast", "cheap"]},
{"id": "gemini-3.8-flash-medium", "name": "Gemini 3.8 Flash (Medium)", "tier": "light", "traits": ["fast", "balanced"]},
{"id": "gemini-3.8-flash-high", "name": "Gemini 3.8 Flash (High)", "tier": "mid", "traits": ["accurate", "cost-effective"]},
{"id": "gemini-3.1-pro-low", "name": "Gemini 3.1 Pro (Low)", "tier": "mid", "traits": ["accurate", "context-long"]},
{"id": "gemini-3.1-pro-high", "name": "Gemini 3.1 Pro (High)", "tier": "heavy", "traits": ["most-accurate", "context-long"]},
{"id": "claude-sonnet-4.6", "name": "Claude Sonnet 4.6 (Thinking)", "tier": "mid", "traits": ["instruction-following", "format-strict"]},
Expand Down
198 changes: 165 additions & 33 deletions settings-advisor/src/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,45 @@ type WorkspaceMetrics struct {
}

var targetExts = map[string]bool{
".go": true,
".ts": true,
".js": true,
".py": true,
".rs": true,
".rb": true,
".java": true,
".kt": true,
".cs": true,
".cpp": true,
".c": true,
".html": true,
".css": true,
".sh": true,
".go": true,
".ts": true,
".js": true,
".py": true,
".rs": true,
".rb": true,
".java": true,
".kt": true,
".cs": true,
".cpp": true,
".c": true,
".html": true,
".css": true,
".sh": true,
".dart": true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Rebuild the bundled binaries for the changed scanner

Installed users execute the committed native binary through gemini-extension.json, but this commit changes main.go without updating any of the three native binaries. I confirmed that the bundled Linux binary still reports a Dart-only workspace as 0 lines, treats .env.example as a real environment file, and misses GitLab CI, so the documented scanning enhancements are not delivered by a normal plugin installation. Rebuild and commit every distributed platform binary.

AGENTS.md reference: AGENTS.md:L46-L48

Useful? React with 👍 / 👎.

".swift": true,
".vue": true,
".svelte": true,
".php": true,
".scala": true,
".zig": true,
".lua": true,
".sql": true,
".tf": true,
}

var skipDirs = map[string]bool{

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip Terraform and Swift dependency caches

When scanning an initialized Terraform or Swift workspace, .terraform/modules and SwiftPM's .build/checkouts contain dependency source with the newly counted .tf and .swift extensions, but neither directory is in this skip map. Those vendored files are therefore included in TotalLines and can push a small workspace into the mid or heavy tier despite the advertised package/build exclusion; add these standard cache directories and path regression tests.

AGENTS.md reference: AGENTS.md:L40-L42

Useful? React with 👍 / 👎.

".git": true,
"node_modules": true,
"vendor": true,
".venv": true,
"dist": true,
"build": true,
"target": true,
".next": true,
".nuxt": true,
"out": true,
".dart_tool": true,
"Pods": true,
Comment on lines +75 to +80

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not skip the requested scan root

When the explicitly supplied repository path itself is named one of the newly added entries such as target, out, or Pods, filepath.Walk invokes this branch for the root and immediately returns filepath.SkipDir, producing an empty scan. The skip check should apply only to descendant directories (for example, when path != root) so arbitrary valid repository paths remain scannable, with a path-boundary regression test.

AGENTS.md reference: AGENTS.md:L42-L42

Useful? React with 👍 / 👎.

}

func main() {
Expand Down Expand Up @@ -148,37 +173,46 @@ func scanWorkspace(root string) (WorkspaceMetrics, error) {
}

if info.IsDir() {
name := info.Name()
// 巨大なディレクトリはスキップ
if name == ".git" || name == "node_modules" || name == "vendor" || name == ".venv" || name == "dist" || name == "build" {
// ルート自身は利用者が明示指定したパスなので、名前が skipDirs に
// 一致しても走査する(例: "target" というリポジトリを直接指定)。
if path != root && skipDirs[info.Name()] {
return filepath.SkipDir
}
return nil
}

fileName := strings.ToLower(info.Name())

// .env 検知
if fileName == ".env" || strings.HasPrefix(fileName, ".env.") {
// .env 検知(テンプレート・サンプルは除外)
if isEnvFile(fileName) {
metrics.HasEnv = true
}

// CI/CD 検知。Windows では filepath.Walk が "\" 区切りを返すため、
// スラッシュ正規化してから判定する。
if strings.Contains(filepath.ToSlash(path), ".github/workflows") {
// パス由来の判定は root からの相対パスのみを見る。絶対パスを使うと
// ワークスペース外の祖先ディレクトリ名(例: /mnt/production/repos/app,
// ~/.circleci/repos/app)が誤検知の原因になる。
dirParts, relOK := relDirComponents(root, path)

// CI/CD 検知(GitHub Actions, GitLab CI, CircleCI, Bitbucket, Azure)
if isCIPath(dirParts, relOK, fileName) {
metrics.HasCI = true
}

ext := filepath.Ext(fileName)

// 本番設定ファイル検知。"product.json" / "reproduce.yaml" 等の誤検知を避けるため、
// 拡張子を除いた名前を区切り(. - _)でトークン化し "prod"/"production" 単独一致のみ採る。
// 拡張子を除いたファイル名およびパス中のディレクトリ名を区切り(. - _)でトークン化し
// "prod"/"production" 単独一致のみ採る。
if ext == ".json" || ext == ".yaml" || ext == ".yml" || ext == ".toml" {
base := strings.TrimSuffix(fileName, ext)
for _, tok := range strings.FieldsFunc(base, isNameSeparator) {
if tok == "prod" || tok == "production" {
metrics.HasProdConfig = true
break
if hasProdToken(base) {
metrics.HasProdConfig = true
} else if relOK {
for _, part := range dirParts {
if hasProdToken(strings.ToLower(part)) {
metrics.HasProdConfig = true
break
}
}
}
}
Expand All @@ -204,7 +238,90 @@ func scanWorkspace(root string) (WorkspaceMetrics, error) {
return metrics, err
}

// isNameSeparator はファイル名トークン分割に使う区切り文字を判定する。
// envTemplateMarkers は「中身がダミーのテンプレート」を示す末尾トークン。
var envTemplateMarkers = map[string]bool{
"example": true,
"sample": true,
"template": true,
"dist": true,
"test": true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat .env.test as an actual environment file

When a repository stores test-environment variables in .env.test, this marker makes isEnvFile return false, so HasEnv remains false and the sandbox recommendation is suppressed. .env.test commonly denotes an environment-specific configuration rather than a template; only unambiguous template suffixes such as example, sample, and template should be excluded.

Useful? React with 👍 / 👎.

"defaults": true,
"schema": true,
}

// isEnvFile は実値が入った .env ファイルであるかを判定する(.env.example 等のテンプレートは除外)。
// 判定は接尾辞の完全一致ではなく**末尾トークン**で行う。`.env.production.example` や
// `.env.local.sample` のような修飾付きテンプレートを取りこぼさず、かつ `.env.production`
// や `.env.test.local`(実値を持つ)は env ファイルとして残すため。
func isEnvFile(fileName string) bool {
if fileName == ".env" {
return true
}
if strings.HasPrefix(fileName, ".env.") {
suffix := strings.TrimPrefix(fileName, ".env.")
parts := strings.Split(suffix, ".")
return !envTemplateMarkers[parts[len(parts)-1]]
}
return false
}

// relDirComponents は root から path までの相対パスの「ディレクトリ部分」を
// パス区切りで分解して返す。path が root 配下でない、または相対化できない場合は
// ok=false を返し、呼び出し側はパス由来の判定を行わない。
func relDirComponents(root, path string) ([]string, bool) {
rel, err := filepath.Rel(root, path)
if err != nil {
return nil, false
}
slashRel := filepath.ToSlash(rel)
if slashRel == ".." || strings.HasPrefix(slashRel, "../") {
return nil, false
}
dir := filepath.ToSlash(filepath.Dir(slashRel))
if dir == "." || dir == "" {
return nil, true // root 直下のファイル(ディレクトリ成分なし)
}
return strings.Split(dir, "/"), true
}

// isCIPath は CI/CD 関連のパスまたは設定ファイルであるかを判定する。
// dirParts は root からの相対ディレクトリ成分。部分文字列ではなくパス成分単位で
// 比較し、".circleci-disabled/" や ".gitlab/circle/" のような近似名を弾く。
func isCIPath(dirParts []string, relOK bool, fileName string) bool {
if fileName == ".gitlab-ci.yml" ||
fileName == "bitbucket-pipelines.yml" ||
fileName == "azure-pipelines.yml" {
return true
}
if !relOK {
return false
}
for i, part := range dirParts {
if part == ".circleci" {
return true
}
if i+1 < len(dirParts) {
next := dirParts[i+1]
if (part == ".github" && next == "workflows") ||
(part == ".gitlab" && next == "ci") {
return true
}
}
}
return false
}

// hasProdToken はトークン分割した名前に prod / production が含まれるかを判定する。
func hasProdToken(name string) bool {
for _, tok := range strings.FieldsFunc(name, isNameSeparator) {
if tok == "prod" || tok == "production" {
return true
}
}
return false
}

// isNameSeparator はファイル名・ディレクトリ名トークン分割に使う区切り文字を判定する。
func isNameSeparator(r rune) bool {
return r == '.' || r == '-' || r == '_'
}
Expand Down Expand Up @@ -347,18 +464,33 @@ func generateRecommendations(metrics WorkspaceMetrics, modelCfg ModelConfig, tas
}
}

// 特化モデルの優先
preferModel := ""
// 特化モデルの優先(traits 駆動のマッチング)
var targetTrait string
for _, kw := range midSonnetKeywords {
if strings.Contains(taskHintLower, kw) {
preferModel = "Claude Sonnet 4.6 (Thinking)"
targetTrait = "instruction-following"
break
}
}
if preferModel == "" {
if targetTrait == "" {
for _, kw := range midOSSKeywords {
if strings.Contains(taskHintLower, kw) {
preferModel = "GPT-OSS 120B (Medium)"
targetTrait = "quota-independent"
break
}
}
}

preferModel := ""
if targetTrait != "" {
for _, m := range modelCfg.Models {
for _, tr := range m.Traits {
if tr == targetTrait {
preferModel = m.Name
break
}
}
if preferModel != "" {
break
}
}
Expand Down
Loading
Loading