diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index a4edf4de..8f187ed9 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -255,6 +255,18 @@ "authentication": "ON_INSTALL" }, "category": "productivity" + }, + { + "name": "voice-prompt", + "source": { + "source": "local", + "path": "./plugins/voice-prompt" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "content" } ] } diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index f1eafd78..24d4b25d 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -4,8 +4,8 @@ "name": "YoungjaeDev" }, "metadata": { - "description": "Personal Claude Code plugin collection with 23 specialized plugins. Codex 0.135 + Hermes Agent native — generated `.agents/plugins/marketplace.json` + per-plugin `.codex-plugin/plugin.json` (`scripts/sync-codex-manifests.mjs`) and Hermes `plugin.yaml` + `__init__.py` adapters (`scripts/sync-hermes-manifests.mjs`).", - "version": "2.9.0" + "description": "Personal Claude Code plugin collection with 24 specialized plugins. Codex 0.135 + Hermes Agent native — generated `.agents/plugins/marketplace.json` + per-plugin `.codex-plugin/plugin.json` (`scripts/sync-codex-manifests.mjs`) and Hermes `plugin.yaml` + `__init__.py` adapters (`scripts/sync-hermes-manifests.mjs`).", + "version": "2.10.0" }, "plugins": [ { @@ -168,6 +168,13 @@ "description": "Review and correct PLAUD voice-recorder notes (Whisper transcript + separate LLM summary). Fixes STT misrecognition (Korean+English code-switching, proper nouns, numbers) against a project term dictionary, grill-me interviews to resolve ambiguities, and writes a transcript-grounded *.corrected.md back to .llmwiki/raw/transcripts without touching the originals.", "version": "0.1.0", "category": "productivity" + }, + { + "name": "voice-prompt", + "source": "./plugins/voice-prompt", + "description": "Normalize Korean voice-mode STT input before acting on it. Strips speech fillers, fixes orthography and Korean-to-English code-switching, resolves garbled identifiers (file, function, branch, skill names) against the actual repo instead of guessing, and never rewrites numbers or dates. Echoes one line of what it understood, then proceeds; gates only irreversible actions. Sibling of plaud-note-taking, which corrects archival transcripts instead of live commands.", + "version": "0.1.0", + "category": "content" } ] } diff --git a/.claude/settings.json b/.claude/settings.json index 69571e2a..dd7c05bb 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -23,7 +23,8 @@ "./plugins/brightdata-guide", "./plugins/gws-sync", "./plugins/mem0-ops", - "./plugins/plaud-note-taking" + "./plugins/plaud-note-taking", + "./plugins/voice-prompt" ] } } diff --git a/.claude/spec/2026-07-30-voice-prompt.md b/.claude/spec/2026-07-30-voice-prompt.md new file mode 100644 index 00000000..14979de7 --- /dev/null +++ b/.claude/spec/2026-07-30-voice-prompt.md @@ -0,0 +1,265 @@ +# voice-prompt — 보이스 모드 STT 입력 정규화 플러그인 + +## 한눈에 보기 + +Claude Code 보이스 모드를 한국어로 쓰면 음성 인식 결과가 세 가지 방식으로 깨진다. 한글 +맞춤법이 틀리고, 한국어 발음으로 말한 영어 단어가 엉뚱한 한글로 바뀌고("로더 파일"), 말버릇 +("어쨌든", "그냥", "뭐")이 명령에 섞여 들어온다. 이 플러그인은 그 입력을 실행 전에 정리하되, +**확신이 있는 것만 조용히 고치고, 파일명 같은 식별자는 저장소에 실제로 찾아보고, 그래도 +갈리는 것만 한 번에 되묻는다.** + +핵심은 이것이 텍스트 청소기가 아니라는 점이다. 말버릇 제거는 모델이 이미 잘 하는 일이라 +값이 낮다. 실제 값은 **모델이 추측으로 넘어가던 자리에 확인 절차를 끼워넣는 것**에 있다 — +파일명을 지어내지 말고 찾아볼 것, 숫자는 절대 고치지 말 것, 이해한 내용을 한 줄로 보고할 +것, 되돌리기 어려운 작업은 물어볼 것. + +## 왜 필요한가 + +기존 `plaud-note-taking` 플러그인이 같은 도메인(음성 인식 결과 정정)을 다루지만 입력의 +지위가 정반대다. 회의 녹음 전사록은 손대면 안 되는 증거이고, 보이스 모드 입력은 실행되고 +버려지는 일회용 명령이다. 그래서 말버릇을 보존하는 쪽과 제거하는 쪽으로 규율이 갈린다. +한 플러그인에 두 규율을 담으면 본문이 서로를 오염시키므로 별도 플러그인으로 분리한다. + +재사용하는 것은 음성 인식 오류의 분포와 "정정에는 근거가 필요하다" 는 원칙이고, 버리는 것은 +증거 보존 규율 전체다. + +## 각 절이 답하는 질문 + +| 절 | 답하는 질문 | +|---|---| +| 동작 기제 | 스킬이 입력을 실제로 바꾸는가, 아니면 모델의 해석을 바꾸는가 | +| 3단 분류 | 무엇을 조용히 고치고, 무엇을 묻고, 무엇을 절대 손대지 않는가 | +| 말버릇 처리 | 단어 목록 없이 어떻게 말버릇만 골라 지우는가 | +| 확정된 결정 | 인터뷰로 정한 열 가지 | +| 파일 구성 | 무엇을 새로 만들고 어디를 함께 고치는가 | +| 만들지 않는 것 | 의도적으로 뺀 것과 그 이유 | +| 검증 | 무엇을 실행해 확인하는가 | + +## 관계도 + +```text +[타이핑] /voice-prompt:voice-prompt + └─ 스킬 본문이 컨텍스트에 적재됨 (입력을 가로채지 않음) + +[보이스 발화] ── 원문 그대로 도착 ──┐ + ↓ + 모델이 원문 + 스킬 지침을 함께 읽음 + ↓ + ┌──────────── 3단 분류 ────────────┐ + ↓ ↓ ↓ + [자동 수정] [질문] [손대지 않음] + 말버릇·맞춤법· 후보 0건/다건· 숫자·날짜·버전· + 저장소 단일후보· 행동이 갈림· PR번호·경로 리터럴 + 자기수정 파괴적 대상 + ↓ ↓ ↓ + └──── 1줄 에코 (고친 항목 노출) ────┘ + ↓ + 실행 (파괴적이면 확인 먼저) +``` + +--- + +## Part 1 — 동작 기제 + +### 스킬은 전처리 필터가 아니다 + +`/voice-prompt:voice-prompt` 를 호출하면 스킬 본문이 도구 결과로 컨텍스트에 적재된다. 이후 발화는 **원문 +그대로** 사용자 메시지로 도착하며, 음성 인식과 모델 사이에 개입하는 층이 없다. 모델이 원문과 +지침을 함께 읽고 자기 추론 과정에서 정규화한 뒤 행동한다. + +프롬프트 제출에 개입하는 유일한 hook 인 `UserPromptSubmit` 도 치환 수단이 아니다. 출력이 +`additionalContext` 로 들어가 컨텍스트에 **덧붙는다** — 사용자 발화를 대체하지 않는다 +(`plugins/core-config/hooks/prompt_inject.sh` 의 4-7행 계약과 111-113행 출력부로 확인). +즉 이 저장소에는 발화 텍스트를 바꿔치기하는 표면이 아예 없다. + +### 그래서 값이 어디서 나오는가 + +- **말버릇 제거와 맞춤법 교정은 한계 효용이 낮다.** 모델은 스킬 없이도 "어쨌든 뭐 그냥" 을 + 무시한다. 이 항목들은 이미 되는 일을 명시적이고 일관되게 만드는 정도다. +- **영어 단어 전환은 이해력으로 해결되지 않는다.** "로더 파일" 을 들으면 모델은 파일명을 + 추측한다. `loader.py` 가 실재하는지 찾아보지 않으면 알 수 없다. 독해 실패가 아니라 **행동 + 누락**이고, 이것만은 지침으로 고칠 수 있다. + +따라서 이 스킬이 실제로 추가하는 네 가지는 전부 변환이 아니라 결정이다. + +| 추가되는 것 | 성격 | +|---|---| +| 식별자는 추측하지 말고 저장소에 대조한다 | 행동 추가 | +| 숫자·PR번호·버전은 절대 고치지 않는다 | 행동 금지 | +| 이해한 내용을 한 줄로 보고한다 | 관측 가능성 | +| 되돌리기 어려운 작업은 확인한다 | 게이트 | + +### 보장 수준 (정직하게) + +다른 모든 스킬과 같은 지침 준수 수준이며, 긴 세션에서는 흐려질 수 있다. hook 재주입으로 +지속을 강제하는 대안은 마커 상태 파일과 Codex 대응 디스크립터까지 끌고 오므로 채택하지 +않았다. 대신 **에코 라인이 스킬이 걸렸는지 판별하는 관측 지점**이며, 에코가 사라지면 +재호출하라는 지침을 본문과 플러그인 `CLAUDE.md` 에 명시한다. 순서 규율(대조 → 에코 → 실행)이 +"잘못 행동한 뒤 사후 통보" 를 막는 완화책이다. + +--- + +## Part 2 — 정정 규율 + +### 3단 분류 + +`plaud-note-taking` 의 네 가지 상태(`[확인됨]`·`[정정]`·`[해석]`·`[확인 필요]`)는 문서에 +표시를 붙일 표면이 있어서 성립하는 구조다. 실행형 스킬에는 그 표면이 없으므로 세 가지로 +줄어든다. + +| 분류 | 조건 | 행동 | +|---|---|---| +| 자동 수정 | 근거가 있다 — 말버릇 부류 일치, 맞춤법, 저장소 단일 후보, 설치된 스킬 목록 단일 후보, 개인 프로필 항목, 발화 안에서의 자기수정 | 조용히 고치고 에코에 한 항목으로 노출 | +| 질문 | 후보가 없거나 여러 개, 두 해석이 서로 다른 행동을 유발, 되돌리기 어려운 작업의 대상 | 한 라운드로 묶어 되묻기 | +| 손대지 않음 | 숫자·날짜·버전·PR 및 이슈 번호·금액·경로 리터럴·인용된 문자열 | 그대로 전달. 근거 없는 추측 금지 | + +세 번째가 안전장치다. `PR 189` 가 `PR 180` 으로 들리면 잘못된 PR 을 건드리므로, 숫자는 +문맥이 아무리 그럴듯해도 자동 수정 대상이 아니다. + +### 말버릇 처리 — 목록이 아니라 부류와 기능 검사 + +정지 단어 목록으로 구현하면 반드시 깨진다. "그냥 지워" 의 "그냥" 은 "다른 건 하지 말고" 라는 +의미이고, "일단 커밋해" 의 "일단" 은 순서 지시다. 그래서 번들 참조 문서는 **기능 부류**로 +정의하고, 삭제 전에 기능 잔존 검사를 통과시킨다. + +| 부류 | 예 | 처리 | +|---|---|---| +| 담화 표지 | 어쨌든, 어찌됐든, 하여튼, 아무튼 | 삭제 | +| 간투사 | 어, 음, 그, 저, 뭐, 이제 | 삭제 | +| 완화 표현 | ~것 같은데, ~려나, 좀 | 확신도 정보를 담으므로 보존 | +| 즉시 반복·재시작 | 같은 어구 반복, 문장 중단 후 다시 시작 | 마지막 형태만 남김 | +| 자기수정 표지 | 아 아니, 그거 말고, 다시 | **삭제 대상이 아니라 정정 근거** | + +마지막 부류가 중요하다. 자기수정 뒤의 발화가 이긴다 — "foo.py 고쳐, 아 아니 bar.py" 는 +"bar.py 고쳐" 가 된다. 회의록에서 쓰던 규칙인데 보이스 모드에서 훨씬 자주 발동한다. + +**기능 잔존 검사**: 그 단어를 지웠을 때 행동이 달라지면 말버릇이 아니다. 애매하면 보존한다. + +--- + +## Part 3 — 확정된 결정 (인터뷰 4라운드) + +| 항목 | 결정 | 근거 | +|---|---|---| +| 배치 | 신규 독립 플러그인 `voice-prompt` | `plaud-note-taking` 과 규율이 반대 | +| 호출 | 명시적 타이핑 1회, 해제할 때까지 매 입력 | 자동 감지 없음. 호출을 타이핑한 뒤 보이스로 진입하는 실제 흐름 | +| 진행 | 한 줄 보고 후 즉시 실행 | 왕복 없이 음성의 속도를 지킴 | +| 보고 내용 | 정규화 결과 + 고친 항목만 짧게 | 조용한 수정은 잡을 수 없다 | +| 재작성 강도 | 말버릇 삭제 + 최소 정규화. 구조·어순·의도 표현 보존 | 정정은 의미를 복원할 뿐 개선하지 않는다 | +| 식별자 | 저장소 대조 후 해소. 단일 후보는 확정, 없거나 여러 개면 질문 | 저장소가 식별자 사전. 질문 횟수를 가장 크게 줄임 | +| 스킬명 오인식 | 설치된 스킬 목록에 대조 | 플러그인 이름이 길고 영어라 실사용 최빈 파손 지점 | +| 되돌리기 어려운 작업 | 즉시 실행의 유일한 예외로 명시 확인 | 오인식 위험과 복구 불가가 겹치는 자리 | +| 개인 특징 | 번들은 일반 한국어 말버릇 부류, 설정 파일은 개인 사례 | 범용성과 개인화 분리. 과적합 방지선 | +| 사전 증식 | 사용자 확인 후에만 파일에 기록 | 틀린 대응이 조용히 굳는 것을 막음 | + +--- + +## Part 4 — 파일 구성 + +### 신규 (6개) + +```text +plugins/voice-prompt/ +├── .claude-plugin/plugin.json # 이름·버전 0.1.0·설명 +├── CLAUDE.md # 존재 이유 + plaud 와의 경계 +└── skills/voice-prompt/ + ├── SKILL.md # 본문 + ├── references/stt-error-classes.md # 오류 등급표, 실행형으로 재작성 + ├── references/korean-filler.md # 일반 말버릇 부류 + 기능 잔존 검사 + └── templates/speech-profile.md # 빈 템플릿 +``` + +`templates/speech-profile.md` 는 **사용자가 승인한 항목을 처음 기록할 때** 그 쓰기의 첫 +단계로 `.claude/voice-prompt/speech-profile.md` 로 복사된다. 시딩은 별도 권한이 아니라 이미 +승인된 쓰기의 일부이므로, 스킬이 켜졌다는 이유만으로 파일이 생기지는 않는다. 번들 경로는 +가정하지 않고 크로스 런타임 `PLUGIN_ROOT` resolver 로 해소한다 — Codex 0.135 는 +`CLAUDE_PLUGIN_ROOT` 를 export 하지 않아 소스 트리 밖에서 첫 단계부터 실패한다. + +플러그인 캐시는 갱신 때 지워지므로 살아있는 사전이 그 안에 있을 수 없다. 개인 말버릇 사례와 +도메인 용어 대응을 한 파일 두 표에 담는다 — 둘 다 이 사용자와 이 프로젝트에만 참이고 수명이 +같아서 나눌 이유가 없다. + +`plaud-note-taking` 의 사전과 공유하지 않는다. 그쪽은 회의 참석자와 회사명을 담고 이쪽은 +개인 발음 습관을 담아 내용이 겹치지 않는다. + +### 본문 언어 + +스킬 본문과 참조 문서는 영어로 쓴다 (세 런타임과 Codex 클라우드 리뷰어가 한 언어를 읽는다). +**예외**는 도메인 내용이다 — 말버릇 목록, 예문, 설명 frontmatter 의 한국어 트리거 문구는 +한국어로 유지한다. 번역하면 스킬 매칭과 말버릇 탐지가 깨진다. + +### 함께 고치는 곳 (6개) + +| 파일 | 변경 | +|---|---| +| `.claude-plugin/marketplace.json` | `voice-prompt` 항목 추가 (분류 `content`), `metadata.version` 2.9.0 → 2.10.0 | +| `AGENTS.md` | 플러그인 수 23 → 24, Content 표에 행 추가, `eligible 21개` → 22, `# 21 entries` → 22 | +| `README.md` | 배지·모음 문구 23 → 24, 구조 트리 항목, `21 / 23 플러그인` → `22 / 24 플러그인`, 상세 절, 670행 산문 총계 | +| `.claude/settings.json` | `plugins.local` 에 경로 추가 | +| `scripts/check-skill-tool-portability.mjs` | `PILOTS` 에 새 경로 (baseline 아님 — 신규는 표준 대응으로 시작) | +| 생성물 | `node scripts/sync-codex-manifests.mjs` 실행 | + +Codex 대상에 포함한다. 제외에는 이유가 필요한데 여기엔 없다 — 순환도 없고 `Agent` 도구 +의존도 없으며, Codex 에 보이스 모드가 없어도 음성 인식 결과를 붙여넣는 경로는 유효하다. +Hermes 는 허용 목록 밖이라 어댑터를 만들지 않는다. + +--- + +## Part 5 — 만들지 않는 것 + +- **테스트 스위트.** 산출물이 순수 산문이고 실행되는 셸 계약이 저장소 대조 한 줄뿐이며, + 그것은 이식성 가드가 이미 검사한다. `council` 이 테스트를 가진 이유는 본문에 아무도 실행하지 + 않는 외부 명령 호출 계약이 박혀 있어서인데 여기엔 그런 것이 없다. 가드 6종이 실행 가능한 + 검사 역할을 한다. +- **세션 상태 파일.** 명시 호출이라 켜져 있는지를 디스크에 물어볼 일이 없다. +- **`UserPromptSubmit` hook.** 지속을 기제로 강제하려면 마커 파일과 hook 스크립트와 Codex + 대응 디스크립터가 붙는다. 에코 라인이 매 턴 상태를 보여주므로 흐려지면 재호출하는 것으로 + 대체한다. +- **`plaud-note-taking` 과의 공유 참조 문서.** 오류 등급표 하나를 복제한다. 표가 얇고 두 + 스킬의 대응 방침 열이 서로 반대라 공유하면 조건 분기가 붙는다. +- **`tcrei-prompt` 와의 통합.** 그쪽은 재사용할 프롬프트를 파일로 구조화하고 이쪽은 라이브 + 발화를 즉시 실행한다. 산출물과 수명이 다르다. + +--- + +## Part 6 — 검증 + +```bash +node scripts/sync-codex-manifests.mjs # 매니페스트 생성 +node scripts/sync-codex-manifests.mjs --check # 어긋남 + 설명 1024자 +node scripts/sync-hermes-manifests.mjs --check # 고아 어댑터 없음 +node scripts/check-doc-consistency.mjs # 트리·표·수 (24 / 22 / 5) +node scripts/check-shell-portability.mjs # GNU 전용 구문 없음 +node scripts/check-skill-tool-portability.mjs --check # PILOTS 표준 대응 적용 +``` + +가드가 잡지 못하는 두 가지는 손으로 확인한다. + +1. **README 670행 산문 총계** — `check-doc-consistency` 의 정규식이 `(\d+) / (\d+) 플러그인` + 형태만 찾는데 그 문장은 숫자 뒤에 "플러그인" 이 없다. `AGENTS.md` 가 명시하는 알려진 + 구멍이다. +2. **끝에서 끝까지 걸어보기** — 발화 하나를 절차대로 통과시켜 보고 줄이 형식대로 나오는지 + 본다. 산출물이 산문이므로 이것이 정직한 통합 검증이다. + +```text +입력: "어쨌든 뭐 그냥 저 로더 파일에서 초기화 부분 좀 고쳐야 될 것 같은데" +기대: → src/skipjack/loader.py 초기화 부분 고쳐야 할 것 같은데 + (로더 파일 → loader.py: 저장소 단일 후보 · 말버릇 3어 삭제) +근거: "그냥" 은 기능 잔존 검사 통과 → 삭제 (여기서는 "다른 건 하지 말고" 가 아님) + "~것 같은데" 는 확신도 정보를 담으므로 보존 +``` + +--- + +## 용어 + +| 용어 | 뜻 | +|---|---| +| 음성 인식 (STT) | 말소리를 글자로 바꾸는 처리. 보이스 모드가 발화를 텍스트로 만드는 단계 | +| 코드 전환 | 한 문장 안에서 두 언어를 섞어 쓰는 것. 한국어 문장 속 영어 기술 용어가 대표 사례이며 오인식이 가장 잦다 | +| 담화 표지 | 문장의 내용이 아니라 말의 흐름을 표시하는 말. "어쨌든", "아무튼" | +| 간투사 | 다음 말을 고르는 동안 채워 넣는 소리. "어", "음" | +| 완화 표현 | 단정을 피해 부드럽게 만드는 표현. "~것 같은데" | +| 기능 잔존 검사 | 그 단어를 지웠을 때 행동이 달라지는지 보는 판정. 달라지면 말버릇이 아니다 | +| 에코 | 모델이 이해한 내용을 실행 전에 한 줄로 되읽어 보여주는 것 | +| 워크트리 | 한 저장소를 여러 디렉터리에 동시에 체크아웃하는 git 기능 | diff --git a/AGENTS.md b/AGENTS.md index d68b8a2f..2195006a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,7 +15,7 @@ 플러그인 트리 하나를 Claude Code, Codex 0.135(`scripts/sync-codex-manifests.mjs`), Hermes Agent(`scripts/sync-hermes-manifests.mjs`)가 함께 읽습니다 — one source, three runtimes. -## Plugins (23) +## Plugins (24) ### Core | Plugin | Description | @@ -58,6 +58,7 @@ | `translator` | Web article translation to Korean | | `tcrei-prompt` | Rewrite prompts using Google's TCREI structure for next-session reuse | | `tally-form` | Checklist markdown to Tally questionnaire/survey form — deterministic urllib builder, theme presets, section dividers, per-question choices (required/checkbox) + short-answer inputs (text/number/email/phone/link), native scheduling (matrix/date/time), form images (logo/cover/IMAGE) + redirect, idempotent publish, humanize routing. Dev-survey + lecture-consultation presets | +| `voice-prompt` | 한국어 보이스 모드 STT 입력을 실행 전에 정규화. 3단 분류 — 자동 수정(말버릇 부류·맞춤법·코드스위칭·발화 내 자기수정) / 질문(후보 0건·다건, 두 해석이 다른 행동을 유발) / 손대지 않음(숫자·날짜·버전·PR 번호). 식별자는 추측하지 않고 `git ls-files`·브랜치 목록·설치된 스킬 목록에 대조해 단일 후보만 확정. 이해한 내용을 1줄 에코한 뒤 즉시 실행하고, 되돌리기 어려운 작업만 명시 확인. 명시 호출 후 해제까지 유지. 개인 사례는 `.claude/voice-prompt/speech-profile.md`, 일반 한국어 말버릇은 번들 레퍼런스 | ### Planning | Plugin | Description | @@ -218,7 +219,7 @@ node scripts/sync-codex-manifests.mjs --check # CI drift guard ``` - Claude 와 Codex 0.135 가 **동일한** `plugins//` 트리를 직접 읽습니다. 별도 mirror / body transform 없음 (구 `codex-bridge` 플러그인은 1.40.0 에서 제거). Skill 본문은 in-place 로 읽히므로 transform 이 없고, frontmatter 유효성만 남습니다. -- 생성물은 `.agents/plugins/marketplace.json` + 플러그인별 `.codex-plugin/plugin.json`, eligible 21개 대상. `.agents/` 와 `plugins//.codex-plugin/` 하위 파일은 손으로 편집하지 마세요 — `scripts/sync-codex-manifests.mjs` 가 진실의 원천입니다. +- 생성물은 `.agents/plugins/marketplace.json` + 플러그인별 `.codex-plugin/plugin.json`, eligible 22개 대상. `.agents/` 와 `plugins//.codex-plugin/` 하위 파일은 손으로 편집하지 마세요 — `scripts/sync-codex-manifests.mjs` 가 진실의 원천입니다. - 새 플러그인 추가 / 기존 플러그인의 `version` / `description` / `category` 변경 시 반드시 `node scripts/sync-codex-manifests.mjs` 를 실행해 매니페스트를 재생성하세요. `--check` 는 플러그인 제거 후 남은 orphan 매니페스트도 감지합니다. - Skill `description` frontmatter 는 1024자 미만으로 유지하세요. Codex 0.135 는 1024자 초과 description 을 가진 skill 을 **silent 하게 skip** 합니다 (Claude Code 는 제한이 없어 위반이 안 보임). `--check` 가 drift 외에 description 길이도 검증하고, 공유 `.githooks/pre-commit` 이 매 커밋마다 실행합니다 — clone 당 한 번 `git config core.hooksPath .githooks` 로 활성화하세요. 전체 trigger 목록 / per-tool rationale 는 description 이 아니라 skill 본문에 두세요. - Skill `description` frontmatter 에 콜론+공백(`: `) 이 들어가면 반드시 따옴표로 감싸세요 (또는 `>-` block scalar). 안 하면 YAML frontmatter 가 nested mapping 으로 파싱돼 `mapping values are not allowed here` 로 실패하고 skill 이 양쪽 런타임에서 silent 하게 로드 안 됩니다. `plugin.json` / `marketplace.json` 은 JSON 이라 무관; lenient 매니페스트 생성기와 `--check` 는 못 잡습니다. @@ -264,7 +265,7 @@ macOS CI 레그(`validate-codex.yml` 의 `macos` job)는 BSD 폴백이 실제로 ```bash codex plugin marketplace add ~/.claude/plugins/marketplaces/my-claude-plugins -codex plugin list --marketplace my-claude-plugins # 21 entries +codex plugin list --marketplace my-claude-plugins # 22 entries codex plugin marketplace remove my-claude-plugins # 검증 후 정리 ``` diff --git a/README.md b/README.md index 3a307128..523358e2 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ # my-claude-plugins -Claude Code를 위한 23개 플러그인 모음 - GitHub 워크플로우부터 AI 이미지 생성까지. Codex 0.135 와 Hermes Agent 도 동일한 소스 트리를 네이티브로 로드합니다 (shared source). +Claude Code를 위한 24개 플러그인 모음 - GitHub 워크플로우부터 AI 이미지 생성까지. Codex 0.135 와 Hermes Agent 도 동일한 소스 트리를 네이티브로 로드합니다 (shared source). -[![Plugins](https://img.shields.io/badge/plugins-23-blue.svg)](https://github.com/YoungjaeDev/my-claude-plugins) +[![Plugins](https://img.shields.io/badge/plugins-24-blue.svg)](https://github.com/YoungjaeDev/my-claude-plugins) [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) [![Claude Code](https://img.shields.io/badge/Claude%20Code-compatible-purple.svg)](https://docs.anthropic.com/claude-code) @@ -97,6 +97,7 @@ rm -rf ~/.claude/plugins/cache/my-claude-plugins/ | **Content** | `translator` | 웹 아티클 한국어 번역 | | | `tcrei-prompt` | Google TCREI 구조로 프롬프트 재작성 | | | `tally-form` | 체크리스트 md → Tally 설문/상담 폼 빌드·게시 (테마 프리셋, 구분선, 문항별 보기·필수·복수선택·단답, matrix/date/time 일정 조율, 이미지·redirect, idempotent) | +| | `voice-prompt` | 한국어 보이스 모드 STT 입력 정규화 — 말버릇·맞춤법·코드스위칭 자동 수정, 식별자는 `git ls-files`·스킬 목록 대조로 해소, 숫자·PR 번호는 손대지 않음. 1줄 에코 후 즉시 실행, 되돌리기 어려운 작업만 확인 | | **Planning** | `interview` | 구조화된 요구사항 수집 | | | `project-init` | Day-1 프로젝트 부트스트랩 (.claude/ + CLAUDE.md + AGENTS.md w/ Codex review guidelines + gh repo create) | | **Docs** | `docs-forge` | README/CHANGELOG 생성 (CRO 최적화) + 배포 문서 템플릿 + MOC 인덱스 | @@ -624,6 +625,28 @@ PLAUD 음성 녹음기가 만든 노트를 검토·정정합니다. PLAUD는 녹 +
+voice-prompt - 보이스 모드 STT 입력 정규화 + +한국어 보이스 모드가 넘겨준 음성 인식 결과를 실행 **전에** 명령으로 되돌립니다. `/voice-prompt:voice-prompt` 를 한 번 타이핑하면 해제할 때까지 모든 입력에 적용됩니다 (자동 감지 없음). 활성화 시 `.claude/voice-prompt/speech-profile.md` 를 읽어 이전 세션에서 확정한 항목을 되살립니다. + +**이건 텍스트 청소기가 아닙니다.** 말버릇 제거는 모델이 이미 잘 하는 일이라 값이 낮습니다. 실제 값은 **모델이 추측으로 넘어가던 자리에 확인 절차를 끼워넣는 것**입니다 — "로더 파일"을 들으면 모델은 파일명을 지어내는데, `loader.py` 가 실재하는지 찾아보지 않으면 알 수 없습니다. 독해 실패가 아니라 행동 누락입니다. + +**3단 분류:** +- **자동 수정** — 말버릇 부류, 맞춤법, 저장소 단일 후보, 스킬 목록 단일 후보, 발화 내 자기수정 +- **질문** — 후보 0건 또는 다건, 두 해석이 서로 다른 행동을 유발, 되돌리기 어려운 작업 대상 (한 라운드로 묶어서) +- **손대지 않음** — 숫자·날짜·버전·PR/이슈 번호·금액·경로 리터럴. `PR 189` 가 `PR 180` 으로 들리면 조용히 잘못된 PR 을 건드리므로, 문맥이 아무리 그럴듯해도 추측 금지 + +**말버릇은 목록이 아니라 기능 검사로 지웁니다.** "그냥 지워"의 "그냥"은 "다른 건 하지 말고"이고 "일단 커밋해"의 "일단"은 순서 지시입니다. 그 단어를 지웠을 때 행동이 달라지면 말버릇이 아니고, 애매하면 보존합니다. 자기수정 표지("아 아니")는 삭제 대상이 아니라 정정 근거로, 그 뒤의 발화가 이깁니다. + +**에코 → 실행:** `→ src/loader.py 초기화 부분 고쳐 (로더 파일→loader.py, 필러 3어 삭제)` 처럼 한 줄 보고 후 즉시 진행. 푸시·머지·삭제 등 되돌리기 어려운 작업만 대상을 명시해 확인받습니다. + +**개인화:** 일반 한국어 말버릇은 번들 레퍼런스에, 개인 발음 습관과 도메인 용어는 `.claude/voice-prompt/speech-profile.md` 에 분리합니다. 사용자 확인 없이 프로필에 쓰지 않습니다. + +**한계:** 스킬은 입력을 가로채지 않습니다 — 지침 준수 기반이라 긴 세션에서 흐려질 수 있고, 에코가 사라지면 재호출하라는 신호입니다. + +
+ ## Configuration ### settings.json @@ -667,7 +690,7 @@ codex plugin marketplace add ~/.claude/plugins/marketplaces/my-claude-plugins codex plugin add llm-wiki@my-claude-plugins ``` -Codex 에서 제외되는 플러그인은 `codex-image` 와 `council` 둘입니다 (`codex-image` 는 Claude->Codex 브리지라 Codex 로 sync 하면 순환, `council` 은 codex 를 의석으로 앉히므로 Codex 에서 돌리면 자기 자신을 소환하는 순환이고 Claude 의석이 Agent 도구를 필요로 함). `core-config` 는 skill 이 없지만 번들 Codex hooks (`hooks/codex-hooks.json`) 를 실어 hooks-only 매니페스트로 Codex 에 sync 됩니다 (native `UserPromptSubmit` 훅). 즉 21 / 23 플러그인이 Codex 로 sync 되며 (core-config 는 hooks-only, 나머지는 skill 단위), `deepwiki` 와 `project-init` 은 1.41.0 부터 Claude 에서는 command + skill 양쪽으로, Codex 에서는 skill 로만 동작합니다 (Codex 는 command surface 를 로드하지 않음). +Codex 에서 제외되는 플러그인은 `codex-image` 와 `council` 둘입니다 (`codex-image` 는 Claude->Codex 브리지라 Codex 로 sync 하면 순환, `council` 은 codex 를 의석으로 앉히므로 Codex 에서 돌리면 자기 자신을 소환하는 순환이고 Claude 의석이 Agent 도구를 필요로 함). `core-config` 는 skill 이 없지만 번들 Codex hooks (`hooks/codex-hooks.json`) 를 실어 hooks-only 매니페스트로 Codex 에 sync 됩니다 (native `UserPromptSubmit` 훅). 즉 22 / 24 플러그인이 Codex 로 sync 되며 (core-config 는 hooks-only, 나머지는 skill 단위), `deepwiki` 와 `project-init` 은 1.41.0 부터 Claude 에서는 command + skill 양쪽으로, Codex 에서는 skill 로만 동작합니다 (Codex 는 command surface 를 로드하지 않음). Codex 0.135 manifest top-level은 `skills` / `hooks` / `mcpServers` / `apps` 만 지원하므로, command-bearing 플러그인(`docs-forge`, `deepwiki` 등)도 Codex 측에는 skill만 노출됩니다 — Claude 측 commands 는 그대로 동작합니다. `github-dev` 는 모든 워크플로가 skill 로 전환돼 command surface 가 없으므로 Claude·Codex 양쪽에서 동일하게 동작합니다. @@ -720,7 +743,7 @@ shared-source 배선은 6개 가드가 매 PR 과 매 커밋(`.githooks/pre-comm - `sync-codex-manifests.mjs --check` — Codex 매니페스트 drift + skill `description` 1024자 초과(Codex silent skip) + 번들 hook 디스크립터 shape·참조 스크립트 존재·orphan. - `sync-hermes-manifests.mjs --check` — Hermes 어댑터 drift + orphan. -- `check-doc-consistency.mjs` — 플러그인 트리·표·카운트(총 23 / Codex-eligible 21 / Hermes 5)가 `manifest-eligibility.mjs` SoT 와 일치. +- `check-doc-consistency.mjs` — 플러그인 트리·표·카운트(총 24 / Codex-eligible 22 / Hermes 5)가 `manifest-eligibility.mjs` SoT 와 일치. - `check-skill-tool-portability.mjs --check` — 공유 스킬 본문의 `AskUserQuestion` 사용이 파일럿 표준 매핑 또는 baseline 에 등록됐는지(미등록 크로스런타임 상호작용 경로 차단). - `check-shell-portability.mjs` — GNU 전용 셸 구문(`md5sum`·`sed -i`·`grep -P`·`date -d`·`stat -c`·`timeout`·`${VAR,,}`·`mapfile`·`declare -A` 등)이 **폴백도 capability probe 도 없이** 쓰인 경우 차단. 정상 폴백 쌍(`stat -c … || stat -f …`)과 probe 분기는 통과하고, 증거는 코드만 인정합니다(대체재를 언급하는 주석은 폴백이 아님). 예외는 `# portability-ok: <사유>`. - `check-skill-prose.mjs` — 500줄 초과·깊은 참조 경로에 대한 정보성 경고(비차단, 항상 exit 0). @@ -790,6 +813,7 @@ node scripts/install-skills.mjs # 또는 hermes plugins install │ ├── project-init/ # Day-1 프로젝트 부트스트랩 (인터뷰 + .claude/ + AGENTS.md + gh repo) │ ├── gws-sync/ # 로컬 → Google Drive 단방향 제안형 동기화 (gws CLI 기반) │ ├── plaud-note-taking/ # PLAUD 노트(Whisper 전사록+LLM 요약) STT·용어 정정 +│ ├── voice-prompt/ # 보이스 모드 STT 입력 정규화 (3단 분류 + 저장소 대조) │ └── mem0-ops/ # 플릿 레벨 mem0 진단·정리 (fleet-scan/doctor/cleanup) ├── AGENTS.md # 세 런타임 공통 최상위 지침 (정본) ├── CLAUDE.md # @AGENTS.md import diff --git a/plugins/voice-prompt/.claude-plugin/plugin.json b/plugins/voice-prompt/.claude-plugin/plugin.json new file mode 100644 index 00000000..02456452 --- /dev/null +++ b/plugins/voice-prompt/.claude-plugin/plugin.json @@ -0,0 +1,5 @@ +{ + "name": "voice-prompt", + "version": "0.1.0", + "description": "Normalize Korean voice-mode STT input before acting on it. Strips speech fillers, fixes orthography and Korean-to-English code-switching, resolves garbled identifiers (file, function, branch, skill names) against the actual repo instead of guessing, and never rewrites numbers or dates. Echoes one line of what it understood, then proceeds; gates only irreversible actions. Sibling of plaud-note-taking, which corrects archival transcripts instead of live commands." +} diff --git a/plugins/voice-prompt/.codex-plugin/plugin.json b/plugins/voice-prompt/.codex-plugin/plugin.json new file mode 100644 index 00000000..bdc2bc5e --- /dev/null +++ b/plugins/voice-prompt/.codex-plugin/plugin.json @@ -0,0 +1,10 @@ +{ + "name": "voice-prompt", + "version": "0.1.0", + "description": "Normalize Korean voice-mode STT input before acting on it. Strips speech fillers, fixes orthography and Korean-to-English code-switching, resolves garbled identifiers (file, function, branch, skill names) against the actual repo instead of guessing, and never rewrites numbers or dates. Echoes one line of what it understood, then proceeds; gates only irreversible actions. Sibling of plaud-note-taking, which corrects archival transcripts instead of live commands.", + "author": { + "name": "YoungjaeDev" + }, + "license": "MIT", + "skills": "./skills/" +} diff --git a/plugins/voice-prompt/CLAUDE.md b/plugins/voice-prompt/CLAUDE.md new file mode 100644 index 00000000..0c1fd647 --- /dev/null +++ b/plugins/voice-prompt/CLAUDE.md @@ -0,0 +1,68 @@ +# voice-prompt + +One skill, `voice-prompt`, that normalizes Korean voice-mode STT input before the session acts +on it. Typed once as `/voice-prompt:voice-prompt`, it stays active until released. + +## Why this exists + +Korean voice mode breaks in three ways: orthography misrecognition, failed Korean-to-English +code-switching (an English identifier spoken with Korean pronunciation comes back as Korean +syllables — "로더 파일"), and speech habits landing inside the command ("어쨌든", "그냥", "뭐"). + +Two of the three barely need a plugin. A model already ignores fillers and reads through +misspellings, so making that explicit buys consistency, not new capability. The third one is +different in kind: hearing "로더 파일", a model **guesses** a filename. It cannot know whether +`loader.py` exists without looking. That is not a comprehension failure, it is a missing action — +and a missing action is exactly what instructions can add. + +So this plugin is not a text cleaner. It is a threshold changer. Everything it really +contributes is a decision rather than a transform: + +| What it adds | Kind | +|---|---| +| Resolve identifiers against the repo instead of guessing | action added | +| Never rewrite numbers, dates, versions, PR/issue numbers | action forbidden | +| Echo one line of what was understood before acting | observability | +| Confirm before an irreversible action | gate | + +## Shape + +```text +plugins/voice-prompt/ +├── CLAUDE.md +├── .claude-plugin/plugin.json +└── skills/voice-prompt/ + ├── SKILL.md # the body + ├── references/stt-error-classes.md # error taxonomy, live-command stance + ├── references/korean-filler.md # filler classes + function-residue test + └── templates/speech-profile.md # seeded to .claude/voice-prompt/speech-profile.md +``` + +## Boundary with plaud-note-taking + +Both correct STT output, and the error distribution is the same, so the taxonomy is shared in +spirit. The discipline is not. `plaud-note-taking` treats its transcript as immutable evidence +and preserves every filler because the filler is part of the record; this skill treats its input +as a disposable command and deletes the filler because the filler is noise. Their term +dictionaries do not overlap either — one holds meeting attendees and company names, the other +holds one speaker's pronunciation habits. + +Two plugins, not one skill with a mode flag: a body carrying both stances would need a +conditional on every rule. + +## Known limit + +This is instruction-following, not interception. Nothing sits between STT and the model — a +skill cannot rewrite the incoming message, and `UserPromptSubmit` hook output arrives as +appended `additionalContext` rather than a replacement. So the normalization happens in the +model's own reasoning and carries the same reliability as any other skill: it can fade in a long +session. + +The echo line is the tell. If it stops appearing, the skill has drifted out — re-invoke +`/voice-prompt:voice-prompt`. That self-signal is why the echo is mandatory rather than optional, +and why this plugin ships no marker file or re-injection hook to enforce stickiness. + +The persisted profile has the same property: `.claude/voice-prompt/speech-profile.md` is an +ordinary project file, so nothing loads it for you. The activation step reads it explicitly — +without that read it would be a write-only dictionary, and every session would re-ask the same +misrecognition. diff --git a/plugins/voice-prompt/skills/voice-prompt/SKILL.md b/plugins/voice-prompt/skills/voice-prompt/SKILL.md new file mode 100644 index 00000000..3e287327 --- /dev/null +++ b/plugins/voice-prompt/skills/voice-prompt/SKILL.md @@ -0,0 +1,314 @@ +--- +name: voice-prompt +description: "Normalize Korean voice-mode STT input before acting on it: strip speech fillers, fix orthography and Korean-to-English code-switching, resolve garbled identifiers (file, function, branch, skill names) against the actual repo instead of guessing, and never rewrite numbers or dates. Echoes one line of what it understood and proceeds, asking once in a single batched round only where two readings imply different actions. Stays active until released. Use ONLY when the user explicitly invokes /voice-prompt:voice-prompt or asks to turn on voice-input normalization — do NOT auto-fire from an incidental mention of voice mode or dictation. Triggers — 보이스 모드 정리, 음성 입력 정규화, STT 정규화, 말버릇 제거, 받아쓰기 교정, voice input cleanup, normalize dictation." +version: 0.1.0 +--- + +# Voice-prompt — normalize spoken input before acting + +Korean voice mode hands the session a transcript, not a command. This skill turns it back into a +command: delete what carries no instruction, fix what is certainly wrong, **look up** what only +the repo can settle, and ask about the rest — once. + +## Cross-runtime interactive input + +Every question below runs through a **capability-aware** interactive-input gate rather than one +hardcoded tool: + +- **Claude Code** — use `AskUserQuestion`. +- **Codex** — use `request_user_input` when that tool is exposed. When it is not, ask ONE + concise blocking question only where a wrong assumption would be costly; otherwise proceed on + a documented safe default and state the assumption. +- **Hermes** — use `clarify`. + +Full policy: `AGENTS.md` → "Cross-runtime interactive input policy". + +## Hermes Agent compatibility + +| Claude/Codex term | Hermes tool | +|---|---| +| Bash | terminal | +| Read | read_file | +| Write | write_file | +| Grep/Glob | search_files | +| AskUserQuestion | clarify | + +Plugin skills are explicit opt-in loads in Hermes — the description never surfaces this body on +its own. **Load it by its bare name: `skill_view("voice-prompt")`.** The qualified +`:` form comes from a generated plugin adapter, and `voice-prompt` is outside +`HERMES_ELIGIBLE`, so no adapter exists for it; the skill-unit install +(`node scripts/install-skills.mjs`, which wraps `npx skills`) registers the frontmatter `name` +verbatim. Only if the plugin is added to the allowlist does the qualified form become the +right one. + +## What this skill actually changes (read before applying it) + +Nothing intercepts the input. The body you are reading sits in context; the user's utterance +arrives verbatim as a normal message. Normalization therefore happens in your own reasoning, and +`UserPromptSubmit` hook output cannot substitute for it (that output is appended +`additionalContext`, not a replacement of the message). + +That constrains where the value is: + +- **Filler removal and spelling are low-yield.** You already read through "어쨌든 뭐 그냥". Doing + it explicitly buys consistency, not capability. Do not spend the user's attention narrating it. +- **Code-switched identifiers are the real failure.** Hearing "로더 파일" you would otherwise + *guess* a filename. You cannot know `loader.py` exists without looking. **Look.** This is the + one step that changes outcomes, and it is a step, not an insight. + +So the four things this skill adds are decisions: look identifiers up, never rewrite numbers, +echo what you understood, and gate irreversible actions. + +## Activation contract + +Invoked as **`/voice-prompt:voice-prompt`** under Claude Code — a plugin skill is registered under +its plugin namespace, so that qualified form is the identifier to document and to type. (Under +Hermes the skill-unit install registers the bare `voice-prompt` instead; see the compatibility note +above. Different runtimes, not a contradiction.) + +Active from explicit invocation until released. Applies to every subsequent input in the session, +typed or spoken — no auto-detection, no sniffing for "does this look like STT". + +- **Read the live speech profile once, at activation.** `.claude/voice-prompt/speech-profile.md` + is an ordinary project file; nothing places it in context automatically, so an entry the user + confirmed in an earlier session is invisible until you open it. Read it (with the file-reading + tool, not a shell `cat`) if it exists — an absent file is the normal first-run state, not an + error. **Skip this and the profile is write-only:** the "an entry in the speech profile" basis in + the table below can never fire, and the same misrecognition gets re-asked every session. +- **Release** on any of: "보이스 모드 끝", "보이스 오프", "정규화 그만", "stop voice-prompt". + Confirm the release in one line and stop applying the rules. +- **The echo line is the liveness signal.** It is mandatory on every turn precisely so the user + can see the skill is still in force. If you notice you have skipped it, resume — a missing echo + reads to the user as "the skill drifted out" and prompts a needless re-invocation. + +## Three states + +Sort every questionable span into exactly one. There is no inline tagging here — an executed +command has no surface to annotate — so the four states used for archival transcripts collapse to +three: fix it, ask about it, or leave it alone. + +| State | Condition | Action | +|---|---|---| +| **Auto-fix** | A basis exists: a filler class from `references/korean-filler.md`, plain orthography, a single repo candidate, a single installed-skill candidate, an entry in the speech profile, or the speaker's own self-correction inside the utterance | Fix silently; surface it as one item in the echo | +| **Ask** | Zero candidates or several; two readings imply different actions; the span names the target of an irreversible action | One batched question round | +| **Leave alone** | Numbers, dates, versions, PR/issue numbers, amounts, literal paths, quoted strings | Pass through unchanged | + +**"Leave alone" is the safety property, not a gap.** `PR 189` misheard as `PR 180` sends work at +the wrong PR, and no amount of surrounding context licenses a guess about a number. Context +*flags* a candidate; it never authorizes rewriting one. + +A correction restores what was said. It does not improve it — do not tighten the user's phrasing, +add qualifiers they did not speak, or widen a request because a bigger version seems more useful. + +## Process + +### Step 1 — strip what carries no instruction + +Apply `references/korean-filler.md`. Two rules dominate: + +- **Function-residue test before deleting anything.** If removing the word changes what you would + do, it is not a filler. "그냥 지워" means "delete it and nothing else"; "일단 커밋해" orders a + sequence. When unsure, keep it. +- **A self-correction marker is a basis, not a filler.** "아 아니", "그거 말고", "다시" mean the + utterance after them wins: `foo.py 고쳐, 아 아니 bar.py` normalizes to `bar.py 고쳐`. + +Fix orthography and obvious code-switching in the same pass (`references/stt-error-classes.md`). +Preserve sentence structure, word order, and the user's own way of expressing intent — including +hedges, which carry confidence information ("~것 같은데" is not noise). + +### Step 2 — resolve identifiers against the repo, do not guess + +For every span that looks like a file, directory, function, branch, or skill name, get a +candidate list before using it. + +**Transliterate first, then search.** The spoken form is Korean and the repo is English, so +searching for the transcript verbatim finds nothing: "로더" appears in no path, and grepping it +returns zero candidates even when `loader.py` is sitting right there. Write down the English stems +the Korean pronunciation could be, *then* look each one up. This conversion is the step that does +the actual work — skip it and the whole feature silently no-ops. + +| Spoken | English stem candidates | +|---|---| +| 로더 | `loader` | +| 씨알픽스 | `cr-fix`, `crfix` | +| 이식성 (a translated word, not a transliteration) | `portability` | +| 커밋 | `commit` | + +**Hand the stems to `grep` as data, never as shell source.** Writing them into a quoted heredoc +and reading them with `grep -f` is the only form with a mechanical guarantee: nothing inside +`<<'STEMS'` is parsed by the shell, so a quote, backtick, `$(…)`, or newline that survived into a +stem is inert. Substituting a stem into the command text — or into a `set --` line — does **not** +have that property, because both are parsed as source before `grep` ever runs, and `grep -F` only +sees what the shell already handed it. + +```bash +stems=$(mktemp) || { echo "voice-prompt: mktemp failed" >&2; exit 1; } +cat > "$stems" <<'STEMS' +loader +STEMS + +# Fetch the list, then filter — separate statuses, so a git failure cannot hide +# behind grep's "no match". `grep -f` unions every stem in one pass, so there is +# also no loop piping into `sort` whose exit status would replace the real one. +files=$(git ls-files -co --exclude-standard) || { + rm -f "$stems" + echo "voice-prompt: git ls-files failed — a tool error, not zero candidates" >&2; exit 1; } +candidates=$(printf '%s\n' "$files" | grep -iF -f "$stems"); rc=$? +[ "$rc" -le 1 ] || { rm -f "$stems"; echo "voice-prompt: grep failed (status $rc)" >&2; exit 1; } + +# Branches — same stem file. +brs=$(git branch -a --format='%(refname:short)') || { + rm -f "$stems"; echo "voice-prompt: git branch failed" >&2; exit 1; } +brmatch=$(printf '%s\n' "$brs" | grep -iF -f "$stems"); rc=$? +[ "$rc" -le 1 ] || { rm -f "$stems"; echo "voice-prompt: grep failed (status $rc)" >&2; exit 1; } +rm -f "$stems" +``` + +`-F` matches each stem literally, so a syllable that happens to be a regex metacharacter cannot +alter the *search* either. Two exit-status rules carry the rest: + +- **Only `grep` status 1 means zero candidates**, and that is a question, not an error. Status 2 or + higher is a grep failure — surface it instead of asking the user about a search that never ran. +- **Never let a pipeline decide whether the lookup failed.** A `git`-into-`grep` pipeline reports a + `git` failure as grep's exit 1, which reads as "no such file"; a candidate loop piping into + `sort` has its failure exit replaced by sort's success. `grep -f` avoids both by needing neither. + (`set -o pipefail` is not the fix: it flips the second case the other way, reporting a plain + no-match as failure.) + +- **Exactly one candidate** → auto-fix, and name it in the echo. +- **Zero** → first suspect your own transliteration, not the user. Try the other spellings the + pronunciation allows (hyphenated / concatenated, transliteration vs. translation) before asking. + Only when every stem comes back empty is it a question. +- **Several** → ask. Offer the near-misses you found as options; a list of real paths is easier to + answer than an open question. +- **Function and symbol names** → prefer the session's symbol tooling (LSP or Serena) over a text + search, then fall back to `grep`. +- **Skill and command names** → resolve against the installed-skill listing already in your + context, not the filesystem. Long English plugin names spoken in Korean are the single most + frequent break: "슬래시 씨알픽스" is `/github-dev:cr-fix`. Same rule — one candidate resolves, + several ask. + +### Step 3 — ask once, batched + +Collect everything still unresolved and ask in **one** round through the interactive-input gate. +Latency is the whole reason voice mode exists; a chain of single questions defeats it. + +Each option should be a concrete candidate ("`src/loader.py`" / "`tests/loader_test.py`"), not a +restatement of the ambiguity. If a question is not worth a round trip, it belongs in "leave +alone" — pass the span through untouched and say so in the echo. + +### Step 4 — echo one line, then act + +```text +→ (, ) +``` + +Keep the parenthetical to what actually changed, shortest form: `로더 파일→loader.py`, +`필러 3어 삭제`. Nothing changed means no parenthetical. Never pad it with what you considered +and rejected. + +Then proceed immediately. No approval round trip — **with one exception**: + +**Irreversible or outward-facing actions get an explicit confirmation, not an echo.** Push, merge, +force-push, reset, branch or file deletion, publishing, sending, anything that leaves the machine. +Name the action and its resolved target together, then wait: + +```text +push → origin/feat/192-voice-prompt-plugin, 맞습니까? +``` + +This is the only place the immediate-execution rule yields. Misrecognition risk and +irreversibility overlap here, which is exactly the intersection worth one round trip. + +### Step 5 — propose profile additions, never write them unasked + +When the same misrecognition recurs, reuse the resolution for the rest of the session from memory. +Persist it only after the user confirms. + +**Order matters: confirmation comes first, then any write.** Seeding is not a separate permission — +it happens only as the first step of a persist the user has already approved, so nothing is ever +created just because the skill ran. Never seed eagerly at activation. + +The bundled template path must be resolved, not assumed: Codex 0.135 does not export +`CLAUDE_PLUGIN_ROOT`, so a literal `` or a bare `${CLAUDE_PLUGIN_ROOT}` fails at step +one outside the source tree. + +**Test each candidate for the template, rather than picking a root and then testing.** Choosing +one root first means a half-extracted cache directory aborts the whole step instead of falling +through to a good older version. + +```bash +cache_root="${CODEX_PLUGIN_CACHE:-$HOME/.codex/plugins/cache}" +hermes_root="${HERMES_HOME:-$HOME/.hermes}" +cl=$(mktemp) || { echo "voice-prompt: mktemp failed" >&2; exit 1; } + +# Candidate roots, most specific first. A wrong guess costs nothing — each entry +# still has to hold the template to win. +{ + if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then printf '%s\n' "$CLAUDE_PLUGIN_ROOT"; fi + printf '%s\n' plugins/voice-prompt + # Codex cache, newest version FIRST. Sort on the version basename, never the + # full path: lexicographic path order ranks zeta/…/0.1.0 above alpha/…/0.2.0, + # and 0.9.0 above 0.10.0. sort -V orders X.Y.Z; BSD/macOS sort has no -V, so + # degrade to a reverse numeric dotted-field sort. + if sort -V /dev/null 2>&1; then + ls -1d "$cache_root"/*/voice-prompt/* 2>/dev/null \ + | awk -F/ '{print $NF "\t" $0}' | sort -Vr | cut -f2- + else + ls -1d "$cache_root"/*/voice-prompt/* 2>/dev/null \ + | awk -F/ '{print $NF "\t" $0}' | sort -t. -k1,1nr -k2,2nr -k3,3nr | cut -f2- + fi + # Hermes: voice-prompt is outside HERMES_ELIGIBLE, so no plugin adapter is + # generated and the skill-unit install is the only Hermes route. + printf '%s\n' "$hermes_root/plugins/voice-prompt" "$hermes_root/skills/voice-prompt" + # Project-scope skill-unit install. `npx skills` owns this layout, so these are + # candidate paths, not a verified contract — harmless, since the -f test decides. + printf '%s\n' .agents/skills/voice-prompt .claude/skills/voice-prompt +} > "$cl" + +TEMPLATE="" +while IFS= read -r root; do + [ -n "$root" ] || continue + # Two layouts: bundled plugin tree, and the flat skill-unit install. + for t in "$root/skills/voice-prompt/templates/speech-profile.md" \ + "$root/templates/speech-profile.md"; do + if [ -f "$t" ]; then TEMPLATE="$t"; break; fi + done + if [ -n "$TEMPLATE" ]; then break; fi +done < "$cl" +rm -f "$cl" +[ -n "$TEMPLATE" ] || { echo "voice-prompt: bundled template not found" >&2; exit 1; } + +# Runs only after the user confirmed the entry being persisted. +PROFILE=.claude/voice-prompt/speech-profile.md +[ -f "$PROFILE" ] || { mkdir -p .claude/voice-prompt && cp "$TEMPLATE" "$PROFILE"; } +``` + +The profile lives under `.claude/` — a stable per-project config path — because a plugin-cache +copy is wiped on cache refresh and cannot hold a project's terms. Never add a person's name, a +client or company name, or an amount without separate verification; those default to a question. + +## Prohibitions + +- Never rewrite a number, date, version, PR/issue number, or amount from context. +- Never invent an identifier. If the repo has no candidate, ask. +- Never widen, tighten, or "improve" the request. Restore the utterance; do not edit the intent. +- Never skip the echo. It is the user's only view into what you changed. +- Never execute an irreversible action off a spoken instruction without the Step 4 confirmation. +- Never write to the speech profile without explicit confirmation. +- Never reconstruct a dropped or garbled clause by guessing what would fit. Ask. + +## Verification before acting + +- [ ] Every deleted word passed the function-residue test? +- [ ] Every identifier either resolved to a single real candidate or asked about? +- [ ] Numbers, dates, and PR/issue numbers passed through untouched? +- [ ] Open questions batched into one round, with concrete candidates as options? +- [ ] Echo line present, listing only what actually changed? +- [ ] Irreversible action confirmed with its resolved target named? + +## Reference files + +- `references/stt-error-classes.md` — which error classes to expect and the correction stance for each. +- `references/korean-filler.md` — filler classes, the function-residue test, and what never to delete. +- `templates/speech-profile.md` — empty template for the per-project profile. diff --git a/plugins/voice-prompt/skills/voice-prompt/references/korean-filler.md b/plugins/voice-prompt/skills/voice-prompt/references/korean-filler.md new file mode 100644 index 00000000..e7a5ba0e --- /dev/null +++ b/plugins/voice-prompt/skills/voice-prompt/references/korean-filler.md @@ -0,0 +1,63 @@ +# Korean speech fillers — classes, and the test that decides + +A stopword list cannot do this job. Half of these words carry meaning in some positions and none +in others, so the decision has to be made per occurrence. This file gives the **classes** to +recognize and the **test** that resolves each occurrence. + +## The function-residue test + +> Remove the word. If what you would do changes, it was not a filler. + +That is the whole rule. Run it before every deletion, and **keep the word when the answer is +unclear** — a preserved filler costs a slightly longer sentence, while a deleted operator changes +the command. + +Worked cases: + +| Utterance | Verdict | +|---|---| +| "어쨌든 뭐 그냥 이 파일 고쳐" | `어쨌든`·`뭐` are fillers. `그냥` runs the test and **stays** — here it can also mean "only this file", and an unclear scope is a keep. | +| "**그냥** 지워" | Keep. Means "delete it and nothing else" — removing it drops a scope constraint. | +| "**일단** 커밋해" | Keep. Orders a sequence: commit first, then the rest. | +| "이거 **좀** 고쳐" | Filler (softener). | +| "**좀만** 고쳐" | Keep. Bounds the size of the change. | +| "고쳐야 **될 것 같은데**" | Keep. Carries confidence, and confidence can decide whether to act or confirm. | + +## The classes + +| Class | Examples | Default | +|---|---|---| +| **담화 표지** (discourse marker) | 어쨌든, 어찌됐든, 하여튼, 아무튼, 그래서 뭐, 그건 그렇고 | Delete | +| **간투사** (hesitation) | 어, 음, 그, 저, 뭐, 이제, 그니까 | Delete | +| **완화 표현** (hedge) | ~것 같은데, ~려나, ~인데, 좀 | **Keep** — carries confidence | +| **즉시 반복 / 재시작** | Same phrase twice; a sentence abandoned and restarted | Keep the final form only | +| **자기수정 표지** (self-correction) | 아 아니, 아니 그거 말고, 다시, 아니아니 | **Never delete** — it is a correction basis | +| **호칭 / 확인 요청** | 클로드, 야, 알겠지, 됐지 | Delete unless it selects a target | + +Two rows are not deletions at all: + +- **Hedges stay.** "고쳐야 될 것 같은데" is a weaker instruction than "고쳐". Flattening it into an + imperative manufactures certainty the speaker did not express — the same failure as widening a + request. +- **Self-correction markers are load-bearing.** They mean the following form wins: + `foo.py 고쳐, 아 아니 bar.py` → `bar.py 고쳐`. Deleting the marker and keeping both filenames + produces two targets where the speaker named one. + +## Why these are bundled rather than per-user + +Everything above is general Korean speech, not one speaker's idiosyncrasy. "어쨌든", "하여튼", +"그냥", "뭐" are in every Korean speaker's mouth, so putting them here — instead of in a personal +profile — is what makes the skill work on first run for anyone. + +The per-project `.claude/voice-prompt/speech-profile.md` is for what genuinely does **not** +generalize: one speaker's recurring mispronunciation of a specific term, a personal shorthand, a +project's domain vocabulary that no repo lookup can resolve. If a candidate entry would be true of +Korean speakers in general, it belongs in this file instead, as a change to the plugin. + +## What deletion must never do + +- Never delete across a clause boundary to make a sentence shorter. +- Never delete a word that names or scopes a target. +- Never delete the second half of a self-correction. +- Never treat a dropped or garbled clause as a filler run and drop it silently — that is a + question (see `stt-error-classes.md`). diff --git a/plugins/voice-prompt/skills/voice-prompt/references/stt-error-classes.md b/plugins/voice-prompt/skills/voice-prompt/references/stt-error-classes.md new file mode 100644 index 00000000..43a685eb --- /dev/null +++ b/plugins/voice-prompt/skills/voice-prompt/references/stt-error-classes.md @@ -0,0 +1,50 @@ +# STT error classes and the correction stance for each + +What Korean speech-to-text reliably gets wrong, and what to do about each class when the +transcript is a **live command** rather than a record. + +The class list is a property of Korean STT, not of any one product, so it matches the taxonomy the +sibling `plaud-note-taking` skill uses on meeting transcripts. The *stance* column does not match, +and that is the whole reason these are two files. There, the transcript is evidence and an +unfixable span is annotated in place and left for a human. Here, the transcript is about to be +executed, so every span must end in one of three terminal states — fixed, asked about, or passed +through untouched. + +## The classes + +| Error class | What to expect | Stance here | +|---|---|---| +| **Code-switched identifiers** (KO pronunciation of an EN name) | The dominant failure. A single-language model approximates the embedded English into Korean syllables: `loader` → "로더", `cr-fix` → "씨알픽스", `commit` → "커밋" (already loaned) or "코멋" (not). | **Resolve against the repo or the installed-skill list.** One candidate fixes it; zero or several is a question. Never settle it from plausibility. | +| **English loanwords** | Mangled or half-transliterated, and the correct target is often a real English word rather than the loaned Korean one. | Fix only when it changes **spelling, not which thing is meant** — that is the orthography case below. If the correct target is a *different word*, context alone is not a basis: look it up or ask. Identifiers always belong to the row above. | +| **Orthography (맞춤법)** | Spacing and 받침 errors, worse on fast or noisy speech. Rarely changes meaning. | Fix silently. Low value, low risk. | +| **Homophone substitution** | Korean is dense with homophones; the model picks the frequent one, which is often wrong in a technical sentence. | Fix only when every reading but one is **impossible** — elimination, not a likelihood judgement. If two readings survive, ask; if they imply different actions, ask even when one feels far more likely. | +| **Numbers, dates, versions, amounts** | Reliably mangled, and the mangled form is indistinguishable from a correct one. | **Never rewrite.** Pass through. This is the safety property, not a limitation. | +| **Dropped or merged clauses** | Fragments vanish; two sentences fuse when the speaker does not pause. | Do not reconstruct. A plausible completion is an invented instruction. Ask. | +| **Self-correction mid-utterance** | "아 아니", "그거 말고", "다시" followed by the intended form. Frequent in speech, since the speaker cannot backspace. | Not an error to fix — a **basis**. The form after the marker wins. | +| **Sentence boundaries and punctuation** | Auto-segmentation is imperfect; a command can absorb the next thought. | Re-segment only when every candidate split implies the **same action, target, and ordering**. When they differ behaviorally, ask before executing — the echo reports a reading after the fact and cannot undo a wrong one. | + +## Why "never rewrite a number" is absolute + +Every other class has a recoverable failure mode: a wrong filler deletion loses a nuance, a wrong +orthography fix is cosmetic, a wrong identifier guess fails loudly when the path does not exist. + +A misheard number fails **silently and plausibly**. `PR 189` heard as `PR 180` names a real PR, so +nothing errors — the work simply lands in the wrong place. There is no lookup that distinguishes +"the number I heard" from "the number that was said", which is why context is not admissible here +even when it feels overwhelming. + +The same reasoning covers versions (`0.1.0` vs `0.10`), amounts, and dates. + +## Evidence hierarchy for a correction + +In descending order. A correction needs one of these; "it probably means X" is not one of them. + +1. **The repo itself** — a single candidate from `git ls-files`, the branch list, or symbol tooling. +2. **The installed-skill listing** — for skill and command names, resolved from session context. +3. **The speech profile** — `.claude/voice-prompt/speech-profile.md`, entries the user confirmed. +4. **In-utterance self-correction** — the speaker's own restatement. +5. **Unambiguous orthography** — a misspelling with exactly one valid target, where the fix changes + spelling and not which thing is meant. + +Anything else is a question. Context is a signal that flags a candidate for lookup; it is never +standalone authorization to rewrite. diff --git a/plugins/voice-prompt/skills/voice-prompt/templates/speech-profile.md b/plugins/voice-prompt/skills/voice-prompt/templates/speech-profile.md new file mode 100644 index 00000000..1f627f0e --- /dev/null +++ b/plugins/voice-prompt/skills/voice-prompt/templates/speech-profile.md @@ -0,0 +1,45 @@ +# Speech profile (template) + +This bundled file is an **empty template**, not the live profile. The live, per-project profile is +`.claude/voice-prompt/speech-profile.md` — a stable config path, because a plugin-cache copy is +wiped on cache refresh and cannot hold a project's terms. + +Scope: only what does **not** generalize. General Korean fillers live in +`references/korean-filler.md`, and anything the repo can answer (file, branch, symbol, skill +names) is resolved by lookup, not recorded here. What is left is one speaker's recurring +mispronunciations and the domain vocabulary no lookup can settle. + +The skill **proposes** entries and never writes this file on its own. Add a row only after the +user explicitly confirms it — a trusted project document may *support* the proposal, but it does +not authorize persistence, and a term "coming up a lot" authorizes nothing. An entry here changes +how future commands are read, so the user is the only gate. + +## Domain terms + +Words a repo lookup cannot resolve: company names, product names, internal jargon, external +service names. + +| STT output (candidates) | Correct spelling | Basis | +|---|---|---| + +## Recurring mispronunciations + +Speaker-specific patterns that repeat across sessions. A row here is a shortcut for a lookup that +already succeeded several times — not a substitute for one that has not run. + +| Heard as | Means | Basis | +|---|---|---| + +## Personal filler additions + +Only habits that are **not** general Korean speech. If a candidate would be true of Korean +speakers broadly, it belongs in `references/korean-filler.md` as a plugin change instead. + +| Expression | Delete or keep | Note | +|---|---|---| + +## What must not go in this file + +Do not add a person's name, a client or company contact, an amount, or a contract term without +separate verification. Those are expensive to get wrong and default to a question rather than an +auto-correction. Never store credentials, phone numbers, or email addresses here. diff --git a/scripts/check-skill-tool-portability.mjs b/scripts/check-skill-tool-portability.mjs index 33b51ad8..1b576a27 100644 --- a/scripts/check-skill-tool-portability.mjs +++ b/scripts/check-skill-tool-portability.mjs @@ -37,6 +37,7 @@ const PILOTS = [ 'plugins/interview/skills/interview-methodology/SKILL.md', 'plugins/github-dev/skills/decompose-issue/SKILL.md', 'plugins/council/skills/convene/SKILL.md', + 'plugins/voice-prompt/skills/voice-prompt/SKILL.md', ]; // Reviewed baseline debt — skills that still hardcode AskUserQuestion, migration deferred.