Skip to content

Commit 050cfd0

Browse files
authored
Merge pull request #101 from aaronsb/feat/multi-language
feat: multi-language support, agent tracking, adaptive TUI
2 parents eb372d9 + bd5fc3d commit 050cfd0

35 files changed

Lines changed: 1737 additions & 300 deletions

docs/architecture/system/ADR-107-way-match-corpus-batch-mode-and-locale-support.md

Lines changed: 104 additions & 179 deletions
Large diffs are not rendered by default.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Multilingual Embedding Model Evaluation
2+
3+
**Date:** 2026-04-03T04:07:51Z
4+
5+
**Models:**
6+
- English: `all-MiniLM-L6-v2` (21M)
7+
- Multilingual: `paraphrase-multilingual-MiniLM-L12-v2` Q8_0 (127M)
8+
9+
**Methodology:** Each test embeds a native-language prompt against both an English description (cross-language) and a native-language description (same-language stub scenario). Three scores per test:
10+
11+
- **EN×EN**: English-only model, English description (baseline)
12+
- **Multi×EN**: multilingual model, English description (cross-language)
13+
- **Multi×Native**: multilingual model, native description (same-language stub)
14+
15+
**Threshold:** 0.25 (same-language similarity minimum)
16+
17+
## Results
18+
19+
| Lang | Prompt | EN×EN | Multi×EN | Multi×Native | Pass |
20+
|:-----|:-------|------:|---------:|-------------:|:----:|
21+
| en | check dependencies for vulnerabilities | 0.7574 | 0.6822 | 0.6822 ||
22+
| de | Abhängigkeiten auf Schwachstellen prüfen | 0.0767 | 0.6223 | 0.8243 ||
23+
| es | verificar dependencias por vulnerabilidades | 0.4381 | 0.7893 | 0.8357 ||
24+
| fr | vérifier les dépendances pour vulnérabilités | 0.5223 | 0.7418 | 0.8938 ||
25+
| pt | verificar dependências por vulnerabilidades | 0.4381 | 0.7899 | 0.9605 ||
26+
| ru | проверить зависимости на уязвимости | 0.0295 | 0.7592 | 0.8536 ||
27+
| ja | 依存関係の脆弱性をチェックして | -0.0290 | 0.6861 | 0.9338 ||
28+
| ko | 의존성 취약점 검사 | -0.0163 | 0.7179 | 0.8554 ||
29+
| zh | 检查依赖项的漏洞 | -0.0266 | 0.5866 | 0.8874 ||
30+
| ar | فحص التبعيات بحثاً عن ثغرات | 0.0416 | 0.3995 | 0.9581 ||
31+
| el | έλεγχος εξαρτήσεων για ευπάθειες | -0.0118 | 0.5934 | 0.8065 ||
32+
| en | write a conventional commit message | 0.7930 | 0.7465 | 0.7465 ||
33+
| ja | コミットメッセージを書いて | 0.0929 | 0.5314 | 0.8322 ||
34+
| ko | 커밋 메시지 작성 | 0.1070 | 0.4847 | 0.8074 ||
35+
| zh | 写一个规范的提交信息 | 0.0727 | 0.6833 | 0.8896 ||
36+
| de | eine konventionelle Commit-Nachricht schreiben | 0.3081 | 0.6349 | 0.7801 ||
37+
| ru | написать сообщение коммита | 0.0039 | 0.4563 | 0.5000 ||
38+
| en | add unit tests for the auth module | 0.5089 | 0.7411 | 0.7411 ||
39+
| ja | 認証モジュールのユニットテストを追加して | 0.0009 | 0.7461 | 0.8338 ||
40+
| ko | 인증 모듈에 단위 테스트 추가 | 0.0917 | 0.5602 | 0.7660 ||
41+
| zh | 为认证模块添加单元测试 | 0.0162 | 0.7100 | 0.8278 ||
42+
| de | Unit-Tests für das Auth-Modul hinzufügen | 0.4085 | 0.7445 | 0.8100 ||
43+
| ru | добавить юнит-тесты для модуля аутентификации | 0.0629 | 0.1650 | 0.3210 ||
44+
45+
## Summary
46+
47+
- **Tests:** 23
48+
- **Passed:** 23
49+
- **Failed:** 0
50+
- **Accuracy:** 100.0%
51+
52+
## Timing
53+
54+
| Phase | Duration | Tests | Per-test |
55+
|:------|:---------|------:|---------:|
56+
| EN model batch (23 pairs) | 104ms | 23 | 4ms |
57+
| Multi model cross-language (23 pairs) | 392ms | 23 | 17ms |
58+
| Multi model same-language (23 pairs) | 389ms | 23 | 16ms |
59+
| **Total** | **889ms** | **69** | **12ms** |
60+
61+
## Interpretation
62+
63+
The multilingual model enables three matching strategies:
64+
65+
1. **English ways + English model** — current production. High precision for English prompts.
66+
2. **English ways + multilingual model (cross-language)** — user types in any language, matches against English descriptions. Works but scores 30-50% lower.
67+
3. **Native-language stubs + multilingual model (same-language)** — frontmatter-only `.ja.md` stubs with native descriptions. Consistently scores 0.80+ across tested languages.
68+
69+
**Recommendation:** Ship both models. English ways use the English model (precise, 21MB). Multilingual stubs use the multilingual model (broad, 127MB). Per-way `embed_model` frontmatter field controls routing. This gives per-language threshold tuning without compromising English accuracy.

docs/hooks-and-ways/languages.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# Multi-Language Support
2+
3+
Ways supports multilingual matching and output across 52 languages. The system uses two embedding models: a precise English model and a broad multilingual model, routed per-way via frontmatter.
4+
5+
## Setting output language
6+
7+
The agent writes commit messages, comments, and documentation in the configured language. Resolution order:
8+
9+
1. **`ways.json`** `output_language` field — explicit override
10+
2. **Claude Code** `settings.json` `language` field — agent config (project then user)
11+
3. **System locale** (`$LC_ALL``$LC_MESSAGES``$LANG`)
12+
4. **Default**: `en`
13+
14+
```json
15+
// ways.json — explicit override
16+
{"disabled": [], "output_language": "ja"}
17+
```
18+
19+
```json
20+
// Claude Code settings.json — agent-level
21+
{"language": "japanese"}
22+
```
23+
24+
Setting `output_language: "auto"` skips the override and cascades to Claude Code settings → system locale.
25+
26+
The output language directive is injected via `core.md` at session start. Way content (the guidance text) stays English — the agent reads it fine in any language. Only the file output changes.
27+
28+
## How matching works across languages
29+
30+
Two embedding models handle different matching scenarios:
31+
32+
| Model | File | Size | Languages | Use case |
33+
|-------|------|------|-----------|----------|
34+
| all-MiniLM-L6-v2 | `minilm-l6-v2.gguf` | 21MB | English | Precise EN matching (default) |
35+
| paraphrase-multilingual-MiniLM-L12-v2 | `multilingual-minilm-l12-v2-q8.gguf` | 127MB | 52 | Cross-language and same-language matching |
36+
37+
Both are downloaded by `make setup` and stored in `~/.cache/claude-ways/user/`.
38+
39+
Each way declares which model it uses via the `embed_model` frontmatter field:
40+
41+
```yaml
42+
---
43+
description: security vulnerability scanning
44+
vocabulary: security vulnerability CVE audit
45+
embed_model: en # default — uses English model
46+
embed_threshold: 0.35
47+
---
48+
```
49+
50+
Ways with `embed_model: multilingual` are scored by the multilingual model against a separate corpus.
51+
52+
## Creating language stubs
53+
54+
A language stub is a frontmatter-only `.{lang}.md` file that provides native-language matching vocabulary for an existing way. The way body stays English — only the matching changes.
55+
56+
```
57+
hooks/ways/softwaredev/code/security/
58+
security.md # English way — full body + frontmatter
59+
security.ja.md # Japanese stub — frontmatter only, no body
60+
security.ko.md # Korean stub — frontmatter only, no body
61+
```
62+
63+
Example stub (`security.ja.md`):
64+
65+
```yaml
66+
---
67+
description: セキュリティ脆弱性スキャンと監査
68+
vocabulary: セキュリティ 脆弱性 CVE 監査 認証 暗号化
69+
embed_model: multilingual
70+
embed_threshold: 0.25
71+
---
72+
```
73+
74+
When a Japanese user types a prompt, the scanner:
75+
1. Matches `security.ja.md`'s frontmatter using the multilingual model
76+
2. Injects `security.md`'s English body (the guidance text)
77+
78+
The agent reads the English guidance and responds in the configured output language.
79+
80+
### Why same-language stubs matter
81+
82+
Cross-language matching (Japanese prompt → English description) scores ~0.69. Same-language matching (Japanese prompt → Japanese description) scores ~0.93. The stub's native-language description dramatically improves matching precision.
83+
84+
| Scenario | Cosine similarity |
85+
|----------|----------------:|
86+
| EN prompt → EN description (baseline) | 0.76 |
87+
| JA prompt → EN description (cross-language) | 0.69 |
88+
| JA prompt → JA description (same-language stub) | 0.93 |
89+
90+
See `docs/architecture/system/multilingual-model-evaluation.md` for full test results.
91+
92+
## Supported languages
93+
94+
Languages are defined in `tools/ways-cli/languages.json`. Each entry specifies:
95+
96+
- **`name`** / **`native`** — display names for normalization
97+
- **`bm25_stemmer`** — Snowball stemmer algorithm name, or `"impossible"` if BM25 cannot support this language
98+
99+
### BM25 feasibility
100+
101+
BM25 is the fallback matching engine when the embedding model is unavailable. It works for languages with whitespace word boundaries and suffix-stripping morphology:
102+
103+
**BM25 works**: Danish, Dutch, English, Finnish, French, German, Greek, Hungarian, Italian, Norwegian, Portuguese, Romanian, Russian, Spanish, Swedish, Turkish
104+
105+
**BM25 impossible**: Arabic, Burmese, Chinese, Georgian, Gujarati, Hebrew, Hindi, Japanese, Korean, Marathi, Mongolian, Thai, Urdu, Vietnamese — and others marked `"impossible"` in `languages.json`
106+
107+
For "impossible" languages, the embedding engine is required — not optional. Without it, only keyword/regex patterns fire.
108+
109+
## Checking language status
110+
111+
```bash
112+
# Language coverage report
113+
ways language
114+
115+
# Filter to a specific language
116+
ways language --filter ja
117+
118+
# Machine-readable
119+
ways language --json
120+
121+
# Engine status with corpus breakdown
122+
ways status
123+
```
124+
125+
`ways status` warns if multilingual ways exist in the corpus but the multilingual model is missing.
126+
127+
## Architecture decisions
128+
129+
- **ADR-107**: Full design rationale — language cascade, dual model approach, matching tiers
130+
- **Evaluation report**: `docs/architecture/system/multilingual-model-evaluation.md` — test data across 11 languages × 3 domains

hooks/ways/check-bash-pre.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ INPUT=$(cat)
1010
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
1111
DESC=$(echo "$INPUT" | jq -r '.tool_input.description // empty' | tr '[:upper:]' '[:lower:]')
1212
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty')
13+
AGENT_ID=$(echo "$INPUT" | jq -r '.agent_id // empty')
14+
[[ -n "$AGENT_ID" ]] && export CLAUDE_AGENT_ID="$AGENT_ID"
1315
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(echo "$INPUT" | jq -r '.cwd // empty')}"
1416

1517
export CLAUDE_PROJECT_DIR="${PROJECT_DIR}"

hooks/ways/check-file-pre.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ source "$(dirname "$0")/require-ways.sh"
99
INPUT=$(cat)
1010
FP=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
1111
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty')
12+
AGENT_ID=$(echo "$INPUT" | jq -r '.agent_id // empty')
13+
[[ -n "$AGENT_ID" ]] && export CLAUDE_AGENT_ID="$AGENT_ID"
1214
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(echo "$INPUT" | jq -r '.cwd // empty')}"
1315

1416
[[ -z "$FP" ]] && exit 0

hooks/ways/check-prompt.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ INPUT=$(cat)
1111
PROMPT=$(echo "$INPUT" | jq -r '.prompt // empty' | tr '[:upper:]' '[:lower:]')
1212
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty')
1313
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(echo "$INPUT" | jq -r '.cwd // empty')}"
14+
AGENT_ID=$(echo "$INPUT" | jq -r '.agent_id // empty')
15+
[[ -n "$AGENT_ID" ]] && export CLAUDE_AGENT_ID="$AGENT_ID"
1416

1517
# Read response topics from Stop hook (if available)
1618
RESPONSE_STATE="/tmp/claude-response-topics-${SESSION_ID}"

hooks/ways/check-response.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
INPUT=$(cat)
1111
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty')
12+
AGENT_ID=$(echo "$INPUT" | jq -r '.agent_id // empty')
13+
[[ -n "$AGENT_ID" ]] && export CLAUDE_AGENT_ID="$AGENT_ID"
1214
TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path // empty')
1315
STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false')
1416

hooks/ways/check-state.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ source "$(dirname "$0")/require-ways.sh"
88

99
INPUT=$(cat)
1010
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty')
11+
AGENT_ID=$(echo "$INPUT" | jq -r '.agent_id // empty')
12+
[[ -n "$AGENT_ID" ]] && export CLAUDE_AGENT_ID="$AGENT_ID"
1113
TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path // empty')
1214
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(echo "$INPUT" | jq -r '.cwd // empty')}"
1315

hooks/ways/check-task-pre.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ source "$(dirname "$0")/require-ways.sh"
1010
INPUT=$(cat)
1111
TASK_PROMPT=$(echo "$INPUT" | jq -r '.tool_input.prompt // empty' | tr '[:upper:]' '[:lower:]')
1212
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty')
13+
AGENT_ID=$(echo "$INPUT" | jq -r '.agent_id // empty')
14+
[[ -n "$AGENT_ID" ]] && export CLAUDE_AGENT_ID="$AGENT_ID"
1315
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(echo "$INPUT" | jq -r '.cwd // empty')}"
1416
TEAM_NAME=$(echo "$INPUT" | jq -r '.tool_input.team_name // empty')
1517

hooks/ways/frontmatter-schema.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ way:
2828
required: optional
2929
default: 0.35
3030
role: Cosine similarity threshold for embedding engine (0.0-1.0, higher = stricter)
31+
embed_model:
32+
type: enum
33+
values: [en, multilingual]
34+
required: optional
35+
default: en
36+
role: Which embedding model to match against (en = all-MiniLM-L6-v2, multilingual = paraphrase-multilingual-MiniLM-L12-v2)
3137
pattern:
3238
type: regex
3339
required: optional

0 commit comments

Comments
 (0)