From 263adf87cd825118aefc11185904b12e53e4b690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=20=E9=A7=BF=E7=94=AB=20=28Shunsuke=20Hayashi=29?= Date: Fri, 10 Apr 2026 10:21:01 +0900 Subject: [PATCH] feat(protocol): expand Obsidian wikilinks in attach_context When attaching Obsidian notes, extract [[wikilinks]] from the note content and resolve them to actual vault files. Linked notes are attached as "obsidian_wikilink" type, bounded by remaining_tokens. Supports both [[Note]] and [[Note|Display]] syntax. Closes #102 Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/settings.json | 4 +- AGENTS.md | 15 +++--- crates/miyabi-core/src/protocol.rs | 83 ++++++++++++++++++++++++++++++ docs/dtp/reviews/round2-code.md | 3 ++ scripts/hook-check-lock.sh | 8 +-- scripts/hook-post-bash.sh | 6 ++- 6 files changed, 103 insertions(+), 16 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index 8d458aa..a54d556 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -3,13 +3,13 @@ "PreToolUse": [ { "matcher": "Edit|Write|NotebookEdit", - "hook": "bash /Users/shunsukehayashi/dev/platform/miyabi-cli-standalone/scripts/hook-check-lock.sh \"$TOOL_INPUT\"" + "hook": "bash scripts/hook-check-lock.sh \"$TOOL_INPUT\"" } ], "PostToolUse": [ { "matcher": "Bash", - "hook": "bash /Users/shunsukehayashi/dev/platform/miyabi-cli-standalone/scripts/hook-post-bash.sh \"$TOOL_INPUT\"" + "hook": "bash scripts/hook-post-bash.sh \"$TOOL_INPUT\"" } ] } diff --git a/AGENTS.md b/AGENTS.md index 562172f..e2c99f2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,26 +1,23 @@ # Repository Guidelines ## Project Structure & Module Organization -- Rust workspace lives under `crates/`: `miyabi-cli` (CLI entry), `miyabi-tui` (UI), and `miyabi-core` (shared logic, config, sessions). Shared workspace config in `Cargo.toml`. -- TypeScript side sits in `src/` with `index.ts` as the entry point; Vitest specs live in `tests/`. Build artifacts output to `dist/`. -- Supporting assets: `.claude/` agent configs, `docs/` for design notes, `.miyabi/` for local runtime data, and `.github/` for CI workflows. +- Rust workspace lives under `crates/`: `miyabi-cli` (CLI entry), `miyabi-tui` (UI), and `miyabi-core` (shared logic, config, sessions, Polaris/DTP: `gate`, `store`, `lock`, `protocol`). Shared workspace config in `Cargo.toml`. +- ルートに `package.json` はない(過去テンプレート由来の記述は削除済み)。フロント系ツールが別ディレクトリにあれば、その README に従う。 +- Supporting assets: `.claude/` agent configs, `docs/` for design notes, `project_memory/` for Polaris タスク台帳(`tasks.json`)、`.github/` for CI workflows。 ## Build, Test, and Development Commands - Rust: `cargo build --workspace --release` (release build), `cargo test --all` (unit/integration), `cargo clippy --all-targets -- -D warnings` (lint), `cargo fmt --all` (format). -- TypeScript: `npm run dev` (tsx watch), `npm run build` (tsc emit to `dist/`), `npm run typecheck` (tsc no emit), `npm run lint` (eslint per `.eslintrc.json`), `npm test` (vitest). -- Install toolchains: `npm install` for JS deps; Rust uses stable 1.75+ per workspace metadata. +- Toolchain: `rust-version` は `Cargo.toml` の workspace メタデータに準拠(現状 1.75+)。 ## Coding Style & Naming Conventions - Rust: run `cargo fmt` before commits; clippy must be clean with `-D warnings`. Modules/files use `snake_case`, types/traits `PascalCase`, constants `SCREAMING_SNAKE_CASE`. Prefer `anyhow::Result` and `thiserror` for errors; avoid `unwrap` in non-test code. -- TypeScript: strict mode on; avoid `any` (warned), silence unused via `_` prefix. Follow ESLint rules and keep imports sorted logically. Prefer async/await over callbacks. ## Testing Guidelines - Rust tests colocated in each crate (`mod tests` blocks or `tests/` directories); write integration tests when touching cross-crate behavior. Run `cargo test --all` before PRs. -- TypeScript tests follow `*.test.ts` in `tests/`; structure with `describe/it` and keep fast. Aim to cover error paths and CLI flags. ## Commit & Pull Request Guidelines - Use Conventional Commits (`feat:`, `fix:`, `refactor:`, `docs:`, `test:`, `chore:`). Keep subject under ~72 chars; include scope when helpful (`feat(tui): add diff renderer`). -- PRs should describe the change, linked issue, and test evidence (`cargo test --all`, `npm test`, etc.). Add screenshots or terminal recordings for TUI changes when relevant. Keep diffs focused; separate refactors from feature work. +- PRs should describe the change, linked issue, and test evidence (`cargo test --all`). Add screenshots or terminal recordings for TUI changes when relevant. Keep diffs focused; separate refactors from feature work. ## Environment & Configuration - Environment defaults from `.env.example`; typical variables: `GITHUB_TOKEN`, `ANTHROPIC_API_KEY`, `REPOSITORY`, optional `RUST_LOG`/`RUST_BACKTRACE` for debugging. @@ -29,7 +26,7 @@ # GitNexus — Code Intelligence -This project is indexed by GitNexus as **miyabi-cli-standalone** (5691 symbols, 12441 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **miyabi-cli-standalone** (5866 symbols, 12893 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/crates/miyabi-core/src/protocol.rs b/crates/miyabi-core/src/protocol.rs index c31038c..096ab1f 100644 --- a/crates/miyabi-core/src/protocol.rs +++ b/crates/miyabi-core/src/protocol.rs @@ -821,12 +821,16 @@ impl DeterministicExecutionProtocol { } if let Some(vault_path) = obsidian_vault_path() { + let mut attached_notes: std::collections::HashSet = + std::collections::HashSet::new(); for note in find_obsidian_notes(&vault_path, &task.title, 3)? { if remaining_tokens == 0 { break; } let content = read_file_snippet(¬e, FILE_SNIPPET_LINE_LIMIT) .map_err(ProtocolError::from)?; + // Expand wikilinks from this note + let linked = extract_wikilinks(&content); push_attachment( &mut attachments, &mut remaining_tokens, @@ -834,6 +838,29 @@ impl DeterministicExecutionProtocol { ¬e.display().to_string(), &content, ); + attached_notes.insert(note); + + for link_name in linked { + if remaining_tokens == 0 { + break; + } + if let Some(linked_path) = resolve_wikilink(&vault_path, &link_name) { + if attached_notes.contains(&linked_path) { + continue; + } + let linked_content = + read_file_snippet(&linked_path, FILE_SNIPPET_LINE_LIMIT) + .map_err(ProtocolError::from)?; + push_attachment( + &mut attachments, + &mut remaining_tokens, + "obsidian_wikilink", + &linked_path.display().to_string(), + &linked_content, + ); + attached_notes.insert(linked_path); + } + } } } @@ -1469,6 +1496,62 @@ fn collect_obsidian_matches( Ok(()) } +fn extract_wikilinks(content: &str) -> Vec { + let mut links = Vec::new(); + let mut chars = content.chars().peekable(); + while let Some(ch) = chars.next() { + if ch == '[' && chars.peek() == Some(&'[') { + chars.next(); // consume second '[' + let mut name = String::new(); + for inner in chars.by_ref() { + if inner == ']' { + break; + } + if inner == '|' { + // [[Note|Display]] — take the note part before | + break; + } + name.push(inner); + } + let trimmed = name.trim().to_string(); + if !trimmed.is_empty() { + links.push(trimmed); + } + } + } + links +} + +fn resolve_wikilink(vault_path: &Path, link_name: &str) -> Option { + // Try exact match first + let exact = vault_path.join(format!("{link_name}.md")); + if exact.exists() { + return Some(exact); + } + // Search recursively for the note + find_note_by_name(vault_path, link_name) +} + +fn find_note_by_name(directory: &Path, name: &str) -> Option { + let target = format!("{}.md", name.to_ascii_lowercase()); + for entry in fs::read_dir(directory).ok()? { + let entry = entry.ok()?; + let path = entry.path(); + if path.is_dir() { + if let Some(found) = find_note_by_name(&path, name) { + return Some(found); + } + } else if path + .file_name() + .and_then(|n| n.to_str()) + .is_some_and(|n| n.to_ascii_lowercase() == target) + { + return Some(path); + } + } + None +} + fn title_keywords(title: &str) -> Vec { let normalized: String = title .chars() diff --git a/docs/dtp/reviews/round2-code.md b/docs/dtp/reviews/round2-code.md index 6b0ff92..3c0c9e3 100644 --- a/docs/dtp/reviews/round2-code.md +++ b/docs/dtp/reviews/round2-code.md @@ -1,5 +1,8 @@ # Round 2 Code Review: Deterministic Task Protocol Rust Codebase +> **アーカイブ・スコープ注意(2026-04-10)** +> 本文は **当時の作業ディレクトリ**(`openclaw-workspace`)向けの調査記録であり、**本リポジトリ `miyabi-cli-standalone` の `crates/miyabi-core` / `crates/miyabi-cli` に対するコードレビュー結果ではありません**。ここに書かれた「Rust が無い」等の記述は **そのワークスペース限定**です。現行実装・レビューは `docs/dtp/PLAYBOOK-v2.md` および `crates/**/*.rs` を正としてください。 + 対象依頼: - Read all Rust source files in `src/`: `lib.rs`, `types.rs`, `state.rs`, `dag.rs`, `lock.rs`, `store.rs`, `protocol.rs`, `gate.rs`, `main.rs` - Check: diff --git a/scripts/hook-check-lock.sh b/scripts/hook-check-lock.sh index 856d160..d3a9580 100755 --- a/scripts/hook-check-lock.sh +++ b/scripts/hook-check-lock.sh @@ -5,8 +5,11 @@ # ロックされていないファイルへの書き込みをブロックする。 TOOL_INPUT="$1" -MIYABI_BIN="/Users/shunsukehayashi/dev/platform/miyabi-cli-standalone/target/release/miyabi" -STORE="/Users/shunsukehayashi/dev/platform/miyabi-cli-standalone/project_memory/tasks.json" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +# 上書き可: 別ビルドパスやインストール済み miyabi を指す +MIYABI_BIN="${MIYABI_BIN:-$REPO_ROOT/target/release/miyabi}" +STORE="${POLARIS_TASK_STORE:-$REPO_ROOT/project_memory/tasks.json}" # miyabi バイナリがなければスキップ if [ ! -x "$MIYABI_BIN" ]; then @@ -33,7 +36,6 @@ if [ -z "$TARGET_FILE" ]; then fi # リポルートからの相対パスに変換 -REPO_ROOT="/Users/shunsukehayashi/dev/platform/miyabi-cli-standalone" REL_PATH="${TARGET_FILE#$REPO_ROOT/}" # ロック一覧を取得 diff --git a/scripts/hook-post-bash.sh b/scripts/hook-post-bash.sh index 141c654..8be572c 100755 --- a/scripts/hook-post-bash.sh +++ b/scripts/hook-post-bash.sh @@ -5,8 +5,10 @@ # ここでは軽い状態表示のみ。 TOOL_INPUT="$1" -MIYABI_BIN="/Users/shunsukehayashi/dev/platform/miyabi-cli-standalone/target/release/miyabi" -STORE="/Users/shunsukehayashi/dev/platform/miyabi-cli-standalone/project_memory/tasks.json" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +MIYABI_BIN="${MIYABI_BIN:-$REPO_ROOT/target/release/miyabi}" +STORE="${POLARIS_TASK_STORE:-$REPO_ROOT/project_memory/tasks.json}" # miyabi バイナリがなければスキップ if [ ! -x "$MIYABI_BIN" ]; then