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) <noreply@anthropic.com>
This commit is contained in:
林 駿甫 (Shunsuke Hayashi) 2026-04-10 10:21:01 +09:00
parent 92c551f1b7
commit 263adf87cd
6 changed files with 103 additions and 16 deletions

View file

@ -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\""
}
]
}

View file

@ -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:start -->
# 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.

View file

@ -821,12 +821,16 @@ impl DeterministicExecutionProtocol {
}
if let Some(vault_path) = obsidian_vault_path() {
let mut attached_notes: std::collections::HashSet<PathBuf> =
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(&note, 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 {
&note.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<String> {
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<PathBuf> {
// 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<PathBuf> {
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<String> {
let normalized: String = title
.chars()

View file

@ -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:

View file

@ -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/}"
# ロック一覧を取得

View file

@ -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