diff --git a/README.md b/README.md index 6bad478..f12a31c 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ kebab doctor |------|------| | `kebab init` | XDG 경로에 데이터 디렉토리 + config.toml 생성 | | `kebab ingest []` | Markdown / 이미지 / PDF 색인 (idempotent). TTY 에서는 stderr 진행 바, non-TTY (CI / pipe) 는 stderr 한 줄씩, `--json` 은 stdout 에 `ingest_progress.v1` 라인 streaming 후 마지막에 `ingest_report.v1`. Ctrl-C 한 번이면 현재 asset 마무리 후 abort (부분 commit 보존, idempotent re-run), 두 번째 Ctrl-C 는 hard exit. Markdown title 이 frontmatter 에 없어도 첫 H1 → H2 → 첫 paragraph 80 자 → 파일명 순으로 자동 채움 (parser_version `md-frontmatter-v2`) — 기존 색인된 doc 도 다음 ingest 에서 새 title 로 갱신. **Incremental** (p9-fb-23): 두 번째 이후의 ingest 는 변하지 않은 doc (blake3 + parser/chunker/embedder version 모두 동일) 의 parse/chunk/embed/vector upsert 를 자동 스킵. final summary 에 `N unchanged` 카운트 표시. `--force-reingest` 로 skip 무시 강제 재처리. **지원 형식** (extractor 자동 결정 — config 에 명시 불가): Markdown (`.md`), 이미지 (`.png` / `.jpg` / `.jpeg`, OCR + caption), PDF (`.pdf`). 다른 확장자는 자동 skip — `IngestItem.warnings` 에 사유 (`"unsupported media type: .docx"` 등), `IngestReport.skipped_by_extension` 에 카운트 분류, CLI / TUI summary 에 breakdown 표시. | -| `kebab search --mode {lexical,vector,hybrid} "" [--no-cache] [--max-tokens N] [--snippet-chars N] [--cursor ] [--tag T] [--lang L] [--path-glob G] [--trust-min LEVEL] [--media TYPE] [--ingested-after RFC3339] [--doc-id ID] [--trace]` | 검색. hybrid는 RRF fusion, citation 포함. 같은 process 안에서 동일 query (NFKC + trim + lowercase 정규화) 반복 시 in-process LRU 캐시 hit (capacity = `[search] cache_capacity`, default 256). `--no-cache` 로 강제 bypass — 디버깅용. ingest commit 발생 시 `kv['corpus_revision']` bump 으로 모든 entry 자동 stale. **`--max-tokens` / `--snippet-chars` / `--cursor` (p9-fb-34)** — agent budget controls. `--json` 출력은 `search_response.v1` wrapper (`{hits, next_cursor, truncated}`) — pre-fb-34 의 bare array 와 호환 안 됨. mismatched cursor → `error.v1.code = stale_cursor`. **filter flags (p9-fb-36):** `--tag` 는 반복 가능 flag (`--tag rust --tag async`) 로 OR 매칭, `--media` 는 `,` 구분 다중 값 OR 매칭, 나머지 flags 간은 AND 조합. `--trust-min` 은 `primary\|secondary\|generated` 중 하나 (해당 level 이상 포함). `--ingested-after` 는 RFC3339 UTC — 파싱 실패 시 `error.v1.code = config_invalid` (exit 2). `--media md` 는 `markdown` alias 로 정규화. 알 수 없는 `--media` 값은 무조건 empty hits (오류 아님). **`--trace` (p9-fb-37)** — `search_response.v1.trace` 에 lexical / vector pre-fusion 후보 + RRF union + per-stage timing (`lexical_ms` / `vector_ms` / `fusion_ms` / `total_ms`) 노출. trace 요청은 캐시 우회 (`--no-cache` 없이도 항상 cold). | +| `kebab search --mode {lexical,vector,hybrid} "" [--no-cache] [--max-tokens N] [--snippet-chars N] [--cursor ] [--tag T] [--lang L] [--path-glob G] [--trust-min LEVEL] [--media TYPE] [--ingested-after RFC3339] [--doc-id ID] [--trace] [--bulk]` | 검색. hybrid는 RRF fusion, citation 포함. 같은 process 안에서 동일 query (NFKC + trim + lowercase 정규화) 반복 시 in-process LRU 캐시 hit (capacity = `[search] cache_capacity`, default 256). `--no-cache` 로 강제 bypass — 디버깅용. ingest commit 발생 시 `kv['corpus_revision']` bump 으로 모든 entry 자동 stale. **`--max-tokens` / `--snippet-chars` / `--cursor` (p9-fb-34)** — agent budget controls. `--json` 출력은 `search_response.v1` wrapper (`{hits, next_cursor, truncated}`) — pre-fb-34 의 bare array 와 호환 안 됨. mismatched cursor → `error.v1.code = stale_cursor`. **filter flags (p9-fb-36):** `--tag` 는 반복 가능 flag (`--tag rust --tag async`) 로 OR 매칭, `--media` 는 `,` 구분 다중 값 OR 매칭, 나머지 flags 간은 AND 조합. `--trust-min` 은 `primary\|secondary\|generated` 중 하나 (해당 level 이상 포함). `--ingested-after` 는 RFC3339 UTC — 파싱 실패 시 `error.v1.code = config_invalid` (exit 2). `--media md` 는 `markdown` alias 로 정규화. 알 수 없는 `--media` 값은 무조건 empty hits (오류 아님). **`--trace` (p9-fb-37)** — `search_response.v1.trace` 에 lexical / vector pre-fusion 후보 + RRF union + per-stage timing (`lexical_ms` / `vector_ms` / `fusion_ms` / `total_ms`) 노출. trace 요청은 캐시 우회 (`--no-cache` 없이도 항상 cold). **`--bulk` (p9-fb-42)** — stdin ndjson 으로 N query 한 번에 실행. `--json` 면 stdout per-query ndjson (`bulk_search_item.v1`) + stderr summary (`bulk_summary: total=N succeeded=S failed=F`). Cap 100. agent 가 query decomposition 후 sub-query 일괄 실행 시 single round-trip — App instance 재사용으로 캐시 / embedder cold-start 비용 한 번만. Per-query failure 는 item 의 `error` (error.v1) 에 격리, 다른 query 계속 진행. | | `kebab list docs` | 색인된 문서 목록 | | `kebab inspect doc ` / `kebab inspect chunk ` | raw record 보기 | | `kebab fetch chunk [--context N]` / `kebab fetch doc [--max-tokens N]` / `kebab fetch span [--max-tokens N]` | (p9-fb-35) verbatim text fetch from indexed corpus. wire = `fetch_result.v1` (kind discriminator). chunk: target + ±N ordinal-context chunks. doc: full normalized markdown. span: 1-based line range (PDF/audio rejected as `error.v1.code = span_not_supported`). chars/4 budget on doc/span. | @@ -83,7 +83,7 @@ kebab doctor | `kebab schema [--json]` | introspection — wire schemas / capabilities / models / stats 한 번에. `--json` 은 `schema.v1` wire; 사람 모드는 서식 출력. **stats 에 (p9-fb-37) `media_breakdown` (5 keys: markdown / pdf / image / audio / other) + `lang_breakdown` (BCP-47 코드, NULL 은 literal `"null"`) + `index_bytes` (sqlite + lancedb on-disk 합계) + `stale_doc_count` (`config.search.stale_threshold_days` 초과 doc 수) 추가.** | | `kebab ingest-file ` | 단일 파일 ingest (workspace 외부 가능). 바이트는 `/_external/.` 로 copy. `.kebabignore` 매치 시 stderr warn 후 진행 (explicit ingest 가 bypass intent). | | `kebab ingest-stdin --title [--source-uri ]` | stdin 의 markdown 본문 ingest. frontmatter (title + source_uri) 자동 prepend. v1 markdown only. | -| `kebab mcp` | MCP (Model Context Protocol) stdio server. agent host (Claude Code / Cursor / OpenAI Agents) 가 spawn 하여 tool 호출 (`search` / `ask` / `schema` / `doctor` / `ingest_file` / `ingest_stdin`). `--config` honor. | +| `kebab mcp` | MCP (Model Context Protocol) stdio server. agent host (Claude Code / Cursor / OpenAI Agents) 가 spawn 하여 tool 호출 (`search` / `bulk_search` / `ask` / `schema` / `doctor` / `ingest_file` / `ingest_stdin`). `--config` honor. | 모든 명령에 `--json` 플래그. 출력은 frozen wire schema v1 (`schema_version` 항상 포함, 예: `ingest_report.v1`, `ingest_progress.v1`, `search_hit.v1`, `answer.v1`, `doctor.v1`, `reset_report.v1`, `schema.v1`). `--json` 모드에서 fatal error 는 stderr 에 `error.v1` ndjson 으로 emit (exit code 0/1/2/3 unchanged). @@ -198,7 +198,7 @@ config 예시는 [docs/SMOKE.md](docs/SMOKE.md) 의 `/tmp/kebab-smoke/config.tom ## MCP 사용 -`kebab mcp` 가 stdio MCP server. 6 tool: `search` / `ask` / `schema` / `doctor` / `ingest_file` / `ingest_stdin`. +`kebab mcp` 가 stdio MCP server. 7 tool: `search` / `bulk_search` (p9-fb-42 — N query 한 번에) / `ask` / `schema` / `doctor` / `ingest_file` / `ingest_stdin`. Claude Code 빠른 등록 (`~/.claude/mcp.json` 또는 host 동등 위치): diff --git a/crates/kebab-app/src/schema.rs b/crates/kebab-app/src/schema.rs index d21e487..793f4d5 100644 --- a/crates/kebab-app/src/schema.rs +++ b/crates/kebab-app/src/schema.rs @@ -86,6 +86,8 @@ const WIRE_SCHEMAS: &[&str] = &[ "citation.v1", "schema.v1", "error.v1", + "bulk_search_item.v1", + "bulk_search_response.v1", ]; /// Build a [`SchemaV1`] introspection report for the given config. diff --git a/crates/kebab-cli/src/wire.rs b/crates/kebab-cli/src/wire.rs index 10d0047..a71397a 100644 --- a/crates/kebab-cli/src/wire.rs +++ b/crates/kebab-cli/src/wire.rs @@ -311,7 +311,7 @@ mod tests { json_mode: true, ingest_progress: true, ingest_cancellation: true, rag_multi_turn: true, search_cache: true, incremental_ingest: true, streaming_ask: false, http_daemon: false, mcp_server: false, - single_file_ingest: false, + single_file_ingest: false, bulk_search: true, }, models: Models { parser_version: "x".to_string(), diff --git a/crates/kebab-cli/tests/cli_mcp_smoke.rs b/crates/kebab-cli/tests/cli_mcp_smoke.rs index cc22302..a1929c2 100644 --- a/crates/kebab-cli/tests/cli_mcp_smoke.rs +++ b/crates/kebab-cli/tests/cli_mcp_smoke.rs @@ -66,8 +66,8 @@ fn cli_mcp_initialize_then_tools_list() { .expect("tools/list result.tools must be an array"); assert_eq!( tools.len(), - 7, - "expected 7 tools (schema, doctor, search, ask, fetch, ingest_file, ingest_stdin), got {}: {list}", + 8, + "expected 8 tools (schema, doctor, search, bulk_search, ask, fetch, ingest_file, ingest_stdin), got {}: {list}", tools.len() ); diff --git a/docs/SMOKE.md b/docs/SMOKE.md index 3121076..7022d45 100644 --- a/docs/SMOKE.md +++ b/docs/SMOKE.md @@ -222,6 +222,23 @@ kebab --config /tmp/kebab-smoke/config.toml schema --json | jq .stats Look for: `media_breakdown` (5 keys), `lang_breakdown`, `index_bytes`, `stale_doc_count`. +### Bulk multi-query (fb-42) + +Stdin ndjson으로 N query 한 번에: + +```bash +printf '{"query":"rust","mode":"lexical"}\n{"query":"async","mode":"lexical"}\n' \ + | kebab --config /tmp/kebab-smoke/config.toml search --bulk --json +``` + +stdout: per-query ndjson (`bulk_search_item.v1`). stderr: `bulk_summary: total=2 succeeded=2 failed=0`. + +MCP tool 동등: + +```json +{"name":"kebab__bulk_search","arguments":{"queries":[{"query":"rust"},{"query":"async"}]}} +``` + ## P6-4 이미지 ingestion 옵션 `config.toml` 에 다음 절을 추가하면 `kebab ingest` 가 `**/*.png` / `**/*.jpg` 등 이미지 자산도 함께 색인합니다 (텍스트만 색인하려면 생략): diff --git a/docs/superpowers/specs/2026-04-27-kebab-final-form-design.md b/docs/superpowers/specs/2026-04-27-kebab-final-form-design.md index de15699..a4efb74 100644 --- a/docs/superpowers/specs/2026-04-27-kebab-final-form-design.md +++ b/docs/superpowers/specs/2026-04-27-kebab-final-form-design.md @@ -245,6 +245,12 @@ normalize: rrf_score = raw_rrf / (2 / (k_rrf + 1)) `score_kind` 는 wire schema v1 에 **optional** 필드로 추가 (additive, backwards-compat). 누락 시 historical default `rrf` 로 해석. +#### Bulk multi-query (fb-42) + +`kebab search --bulk` (stdin ndjson) + `mcp__kebab__bulk_search` tool 신규. agent 가 N sub-query 한 번에 실행 — query decomposition 시 단일 round-trip. Cap 100 per call. Sequential for-loop, App instance 재사용 → 캐시 / embedder cold-start 비용 한 번만. + +Per-query failure 는 `bulk_search_item.v1.error` (error.v1) 에 격리, 다른 query 계속 진행. wire shape additive minor (`bulk_search_item.v1` + `bulk_search_response.v1` 신규). + ### 2.3 Answer ```json diff --git a/docs/wire-schema/v1/bulk_search_item.schema.json b/docs/wire-schema/v1/bulk_search_item.schema.json new file mode 100644 index 0000000..eb82731 --- /dev/null +++ b/docs/wire-schema/v1/bulk_search_item.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://kb.local/wire/v1/bulk_search_item.schema.json", + "title": "BulkSearchItem v1", + "description": "p9-fb-42: per-query result inside a bulk_search response. `response` XOR `error` — exactly one is non-null. `query` is the input echo so consumers can correlate without index tracking.", + "type": "object", + "required": ["schema_version", "query", "response", "error"], + "properties": { + "schema_version": { "const": "bulk_search_item.v1" }, + "query": { "type": "object", "description": "Input echo (verbatim JSON object)." }, + "response":{ + "type": ["object", "null"], + "description": "search_response.v1 payload on success; null when error is non-null." + }, + "error": { + "type": ["object", "null"], + "description": "error.v1 payload when this query failed; null on success." + } + } +} diff --git a/docs/wire-schema/v1/bulk_search_response.schema.json b/docs/wire-schema/v1/bulk_search_response.schema.json new file mode 100644 index 0000000..e4e9c9a --- /dev/null +++ b/docs/wire-schema/v1/bulk_search_response.schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://kb.local/wire/v1/bulk_search_response.schema.json", + "title": "BulkSearchResponse v1", + "description": "p9-fb-42: MCP envelope for bulk_search. CLI emits raw `bulk_search_item.v1` ndjson without this envelope (summary on stderr).", + "type": "object", + "required": ["schema_version", "results", "summary"], + "properties": { + "schema_version": { "const": "bulk_search_response.v1" }, + "results": { + "type": "array", + "items": { "type": "object", "description": "bulk_search_item.v1" } + }, + "summary": { + "type": "object", + "required": ["total", "succeeded", "failed"], + "properties": { + "total": { "type": "integer", "minimum": 0 }, + "succeeded": { "type": "integer", "minimum": 0 }, + "failed": { "type": "integer", "minimum": 0 } + } + } + } +} diff --git a/integrations/claude-code/kebab/SKILL.md b/integrations/claude-code/kebab/SKILL.md index 2be037e..ebd6089 100644 --- a/integrations/claude-code/kebab/SKILL.md +++ b/integrations/claude-code/kebab/SKILL.md @@ -28,11 +28,12 @@ User-specific trigger keywords (team names, system names, internal acronyms) bel ## MCP tools (preferred) -When `kebab` is registered as an MCP server (see `~/.claude/mcp.json` example below), seven tools are exposed as `mcp__kebab__`: +When `kebab` is registered as an MCP server (see `~/.claude/mcp.json` example below), eight tools are exposed as `mcp__kebab__`: | tool | purpose | mutation | |------|---------|----------| | `mcp__kebab__search` | corpus search → `search_response.v1` (`{hits, next_cursor, truncated}`) | no | +| `mcp__kebab__bulk_search` | N queries in one call → `bulk_search_response.v1` (`{results, summary}`) | no | | `mcp__kebab__ask` | RAG answer → `answer.v1` | no | | `mcp__kebab__fetch` | verbatim text → `fetch_result.v1` (chunk / doc / span) | no | | `mcp__kebab__schema` | capability discovery → `schema.v1` | no | @@ -60,6 +61,17 @@ Input: - When `truncated: true`, the budget loop modified the page (snippet shortening or k reduction). `next_cursor` is **independent** — non-null whenever more hits may be reachable. Caller may widen `max_tokens` (re-issue same query for fuller snippets / more hits per page) or follow `next_cursor` (advance through more hits) or both. Mismatched cursor (corpus_revision changed) returns `error.v1.code = stale_cursor` — re-issue the search to obtain a fresh one. - **`trace: true` (p9-fb-37)** — debug aid. Response carries an extra `trace` block: `lexical[]` + `vector[]` (pre-fusion candidates), `rrf_inputs[]` (RRF union before final cut), and `timing` (`lexical_ms`, `vector_ms`, `fusion_ms`, `total_ms`). Trace bypasses the search cache (always cold). Use sparingly — it bloats the wire response and is for diagnosing "why did this hit / not hit", not normal retrieval. +### `mcp__kebab__bulk_search` + +N개 query 한 번에 — agent loop 효율 개선. 각 query 는 `mcp__kebab__search` 와 동일 input shape (query 필수, 나머지 optional). Cap 100. + +Input: +```json +{"queries": [{"query": "..."}, {"query": "...", "mode": "lexical"}, ...]} +``` + +Output: `bulk_search_response.v1` envelope — `results: [bulk_search_item.v1]` (각 item = `{query, response | null, error | null}`) + `summary: {total, succeeded, failed}`. Per-query 실패는 item 의 error 에 격리, 다른 query 계속 진행. + ### `mcp__kebab__ask` — when you need the answer Use when the user wants a synthesized answer, not a list of links. diff --git a/tasks/INDEX.md b/tasks/INDEX.md index 9da5b55..aa42923 100644 --- a/tasks/INDEX.md +++ b/tasks/INDEX.md @@ -134,7 +134,7 @@ P0~P5 는 직렬. P6~P9 는 P5 이후 병렬 가능. ### 🎯 0.6.0 또는 P+ — reasoning - [p9-fb-41 multi-hop reasoning](p9/p9-fb-41-multi-hop-reasoning.md) — ⏳ 미구현, brainstorm 필요 (XL, eval 인프라 선행) - - [p9-fb-42 bulk multi-query + re-rank hint](p9/p9-fb-42-bulk-multi-query-rerank.md) — ⏳ 미구현, brainstorm 필요 (Nice) + - [p9-fb-42 bulk multi-query + re-rank hint](p9/p9-fb-42-bulk-multi-query-rerank.md) — ✅ 머지 (2026-05-10) — bulk only, rerank hint deferred ## Post-merge 핫픽스 diff --git a/tasks/p9/p9-fb-42-bulk-multi-query-rerank.md b/tasks/p9/p9-fb-42-bulk-multi-query-rerank.md index efa82c0..bdddbb0 100644 --- a/tasks/p9/p9-fb-42-bulk-multi-query-rerank.md +++ b/tasks/p9/p9-fb-42-bulk-multi-query-rerank.md @@ -3,7 +3,7 @@ phase: P9 component: kebab-cli + kebab-search task_id: p9-fb-42 title: "Bulk multi-query + re-rank hint — agent loop 효율" -status: open +status: completed target_version: 0.6.0+ depends_on: [] unblocks: [] @@ -14,7 +14,10 @@ source_feedback: 사용자 도그푸딩 2026-05-06 — agent 가 N 개 query 동 # p9-fb-42 — Bulk multi-query + re-rank hint -> ⏳ **백로그 only — 미구현 (Nice-to-have).** 본 spec 은 도그푸딩 피드백 skeleton. 구현 착수 전 [superpowers:brainstorming](../../docs/superpowers/) 으로 설계 단계 선행 필요. multi-query input 형식 / 결과 합성 정책 / re-rank hint 의 LLM 호출 비용 brainstorm 후 확정. +> ✅ **Bulk multi-query 부분 구현 완료.** 본 spec 의 rerank hint lever 는 별도 task 로 분리 (fb-39 cross-encoder 설계 후). +> +> - Design: [`docs/superpowers/specs/2026-05-10-p9-fb-42-bulk-multi-query-design.md`](../../docs/superpowers/specs/2026-05-10-p9-fb-42-bulk-multi-query-design.md) +> - Plan: [`docs/superpowers/plans/2026-05-10-p9-fb-42-bulk-multi-query.md`](../../docs/superpowers/plans/2026-05-10-p9-fb-42-bulk-multi-query.md) ## 증상 / 동기