From 441f1192eee7356f1b52f67e6ef73c2a71a2b3f1 Mon Sep 17 00:00:00 2001
From: th-kim0823
Date: Sun, 10 May 2026 21:07:36 +0900
Subject: [PATCH] docs(fb-42): wire schema + README + SMOKE + design + SKILL +
INDEX
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Add bulk_search_item.v1 + bulk_search_response.v1 wire schemas
- Register both in WIRE_SCHEMAS const
- README: --bulk flag mention + MCP tool list 7→8 (bulk_search)
- SMOKE: bulk multi-query walkthrough (CLI + MCP equivalent)
- Design §2.2: Bulk multi-query (fb-42) subsection (additive minor)
- SKILL: mcp__kebab__bulk_search section + tool table row
- Task spec status open→completed, banner replaced
- INDEX: fb-42 row 머지 (rerank hint deferred)
- Fix: missed Capabilities {bulk_search} in cli wire.rs test (Task 7 leftover)
- Fix: missed tools.len() 7→8 in cli_mcp_smoke (Task 5 leftover)
Co-Authored-By: Claude Opus 4.7 (1M context)
---
README.md | 6 ++---
crates/kebab-app/src/schema.rs | 2 ++
crates/kebab-cli/src/wire.rs | 2 +-
crates/kebab-cli/tests/cli_mcp_smoke.rs | 4 ++--
docs/SMOKE.md | 17 +++++++++++++
.../2026-04-27-kebab-final-form-design.md | 6 +++++
.../v1/bulk_search_item.schema.json | 20 ++++++++++++++++
.../v1/bulk_search_response.schema.json | 24 +++++++++++++++++++
integrations/claude-code/kebab/SKILL.md | 14 ++++++++++-
tasks/INDEX.md | 2 +-
tasks/p9/p9-fb-42-bulk-multi-query-rerank.md | 7 ++++--
11 files changed, 94 insertions(+), 10 deletions(-)
create mode 100644 docs/wire-schema/v1/bulk_search_item.schema.json
create mode 100644 docs/wire-schema/v1/bulk_search_response.schema.json
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)
## 증상 / 동기