agentleFS
allenhutchisonallenhutchison/obsidian-gemini/AGENTS.md

obsidian-gemini

AGENTS.md534 starsChanged 36 days agoMITView on GitHub

Obsidian Gemini Scribe is an Obsidian plugin that integrates Google's Gemini AI models for AI-driven assistance within Obsidian. It provides context-aware chat, document summarization, text rewriting, and IDE-style completions. IMPORTANT: Always run npm install first if you encounter TypeScript errors or missing module errors during build. The build requires all dependencies in node_modules to be present. If you get errors about missing modules like obsidian, @google/genai, handlebars, or tslib, run npm install before attempting npm run build again.

  • Reads credentials
  • Installs packages

We split this AGENTS.md into the rules it gives an agent

43 rules. About 1 is common, 21 are distinctive, and 21 are very specific to this project.

  • ≈1

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈21

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 21

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  • could be automatic: a setting could enforce it
  1. Very specific:

    IMPORTANT: Always run npm install first if you encounter TypeScript errors or missing module errors during build.

    line 35
  2. Very specific:

    Run single test: npm test -- path/to/test.ts

    line 39
  3. Distinctive:

    Obsidian API First: Always use built-in Obsidian API functions when available instead of low-level operations (for detailed Obsidian API …

    line 98
  4. Very specific:

    Use vault.getMarkdownFiles() instead of vault.adapter.list()

    line 99
  5. Very specific:

    Use app.fileManager.processFrontMatter() for frontmatter manipulation

    line 100
  6. Very specific:

    Use vault.getAbstractFileByPath() for file operations

    line 101
  7. Very specific:

    Use app.metadataCache for file metadata access

    line 102
  8. Very specific:

    Use app.fileManager.renameFile() for renaming files (preserves metadata)

    line 103
  9. Very specific:

    Use app.workspace.openLinkText() for clickable file links in views

    line 104
  10. Distinctive:

    File Operations: Always use Obsidian's normalized paths and metadata cache

    line 105
  11. Distinctive:

    System Folder Protection: Always exclude system folders from file operations:

    line 132
  12. Distinctive:

    Use exclusion checks in all vault operation tools

    line 135
  13. Distinctive:

    Tool Execution Order: When AI needs to perform multiple operations:

    line 136
  14. Distinctive:

    Always prioritize read operations before destructive operations

    line 137
  15. Distinctive:

    Use camelCase for variables/functions, PascalCase for classes/types, and kebab-case filenames aligned with their feature area.

    line 181
  16. Distinctive:

    Use proper async/await patterns for all asynchronous operations

    line 183
  17. Very specific:

    logger.log() and logger.debug(): Only output when debug mode is enabled

    line 197
  18. Very specific:

    logger.error() and logger.warn(): Always visible regardless of debug mode

    line 201
  19. Distinctive:

    Use for important errors and warnings that users should always see

    line 202
  20. Very specific:

    Use logger.log() for debug information that helps development and troubleshooting

    line 207
  21. Very specific:

    Use logger.error() for errors that indicate something went wrong

    line 208
  22. Very specific:

    Use logger.warn() for warnings about deprecated features or potential issues

    line 209
  23. Very specific:

    Never use native console.log() or console.debug() directly

    line 210
  24. Very specific:

    Keep unit tests next to implementations and name them after the unit (models.test.ts, main.test.ts)

    line 249
  25. Very specific:

    Run npm test before each PR and run the relevant evals/ eval harness tasks (see npm run eval and the eval-harness skill) after touching …

    line 252
  26. Distinctive:

    When adding features, ensure tests cover:

    line 258
  27. Distinctive:

    Update README.md with new feature description

    line 276
  28. Very specific:

    Create or update relevant user guides in docs/

    line 277
  29. Distinctive:

    Add examples and use cases

    line 278
  30. Distinctive:

    Update table of contents and navigation

    line 279
  31. Distinctive:

    Update code examples to reflect changes

    line 283
  32. Distinctive:

    Remove or rewrite documentation for removed features

    line 287
  33. Distinctive:

    Delete archived docs (users auto-update, no need for old docs)

    line 288
  34. Distinctive:

    Update migration guides if needed

    line 289
  35. Distinctive:

    Update code examples in guides

    line 293
  36. Distinctive:

    Document breaking changes clearly

    line 294
  37. Distinctive:

    When planning new features:

    line 308
  38. Very specific:

    Write concise, imperative commit subjects (Fix agent session cleanup, Improve prompt builder).

    line 347
  39. Very specific:

    Commit generated artifacts (manifest.json, styles.css, versions.json) alongside source changes.

    line 348
  40. Very specific:

    When writing or modifying UI styles, consume these tokens rather than hardcoding color or reaching for raw var(--obsidian-var) / magic …

    line 363
  41. Common:

    Never commit API keys or vault data.

    line 377
  42. Very specific:

    Document new network calls or permissions in docs/ when adding features or capabilities

    line 378
  43. Distinctive:

    Always use native Obsidian API calls when possible.

    line 379

≈ “Common” is an estimate: at least 1 in 5 of the 36,368 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
AGENTS.md

Repository Guidelines

Project Overview

Obsidian Gemini Scribe is an Obsidian plugin that integrates Google's Gemini AI models for AI-driven assistance within Obsidian. It provides context-aware chat, document summarization, text rewriting, and IDE-style completions.

Project Structure & Module Organization

  • src/ contains TypeScript plugin code; src/main.ts is the entry point with domain folders such as agent/, api/, tools/, ui/, and services/, plus shared utilities in utils/.
  • docs/ hosts user and operator guides; prompts/ ships default agent prompts; evals/ holds the agent eval harness (task fixtures, runner, baselines).
  • Unit tests live in the test/ directory mirroring src/ structure as *.test.ts; generated artifacts (main.js, manifest.json, styles.css) stay in the repo root for Obsidian.

Commands

Development

npm install          # Install dependencies
npm run dev          # Development build with watch mode
npm run build        # Production build (generates refs, runs TypeScript check, then bundles)
npm run generate-refs # Regenerate help references from docs/ (runs automatically in build/dev)
npm test             # Run Vitest tests
npm run format       # Format code with Prettier
npm run format-check # Check formatting without changes
npm run lint         # Lint with ESLint (eslint-plugin-obsidianmd recommended preset)
npm run lint:fix     # Auto-fix ESLint violations where possible
npm run lint:cycles  # Fail on circular imports (madge --circular over src/; baseline is 0 and CI's lint workflow enforces it)
npm run check:fields   # Advisory unread-field scan (per-field reachability over src/ interface and type-alias fields; #1469). Always exits 0 — the report lands in the Unread-field workflow artifact. Suppress a genuinely write-only field with 'wiring:keep' on its declaration line. Flip to blocking after the noise rate has been measured for a release.
npm run knip         # Detect unused files, exports, types, and dependencies (configured in knip.json; CI's knip workflow enforces it, so a newly-dead export fails the PR — put intentional public surface in knip.json rather than re-arguing it per PR; every knip.json ignore carries its justification written next to it; nothing may mark an export reachable except a real caller — not an entry-point declaration, not a test-only import, not a JSDoc @public/@beta/@alias tag, which the no-tags-as-reachability ESLint rule fails (#1525))
npm test             # Vitest suite — includes the provider-docs-sync guard (test/api/provider-docs-sync.test.ts): the provider pipeline diagram in AGENTS.md and .claude/guidelines/invariants.md must name exactly the ModelProvider union from src/api/providers/registry.ts, so adding a provider fails the PR until the diagrams are updated (#1567)
npm run install:test-vault # Copy built artifacts into test vault. Target precedence: TEST_VAULT_PLUGIN_DIR (exact plugin folder, validated by id) > TEST_VAULT_DIR (vault root, scanned by id) > default ~/Obsidian/Test Vault
npm run translate    # Regenerate AI translations in src/i18n/ (needs GOOGLE_API_KEY; not part of build/test — runs in its own `Update UI translations` workflow on en.ts changes, or manually)

IMPORTANT: Always run npm install first if you encounter TypeScript errors or missing module errors during build. The build requires all dependencies in node_modules to be present. If you get errors about missing modules like obsidian, @google/genai, handlebars, or tslib, run npm install before attempting npm run build again.

Testing

  • Run single test: npm test -- path/to/test.ts
  • Coverage floor: npm test -- --coverage enforces thresholds in vitest.config.ts (baseline 73.56/73.17/71.00/71.96 for lines/statements/branches/functions; see the thresholds comment there). CI fails when coverage falls below the floor. autoUpdate ratchets the numbers up automatically on full runs; a legitimate drop (e.g. a dead-code sweep) is fixed by editing the number in the same PR, with the reason in the PR body — same convention as .console-baseline.
  • Manual integration: npm run eval drives the eval harness against a real Obsidian instance to validate agent toolchains before shipping (see evals/README.md and the eval-harness skill)
  • Typecheck test files before pushing test changes: the CI lint job runs npm run typecheck:test (tsc --project tsconfig.test.json), which type-checks the test/ tree and catches errors the production build misses — npm run build uses tsc -skipLibCheck and excludes tests. A green local npm run build does not guarantee CI passes; run npm run typecheck:test too. (Classic trap: an expression-bodied arrow like (t) => arr.push(t) returns number, not void, and only the test typecheck flags it.)

Versioning & Releases

For the full release process, use the release skill (from the maintainerd plugin; the repo-specific release gates live in .claude/guidelines/release.md).

Architecture

Core Pattern: Factory + Decorator

src/main.ts → ModelClientFactory.createFromPlugin() → GeminiClient | OllamaClient | OpenAIClient | AnthropicClient → RetryDecorator → ModelApi

The plugin uses a factory pattern (ModelClientFactory in src/api/factory.ts) to create model API clients, wrapped with a retry decorator (RetryDecorator) for resilience. Since the settings redesign, routing is a dense table keyed by feature: settings.features: Record<FeatureId, { provider, model }> (FeatureId = chat, summary, completions, rewrite, webSearch, deepResearch, rag, imageGen), with settings.defaultProvider never serving a request: it only seeds entries missing at settings load (sanitizeFeatureRoutes in src/main.ts), and otherwise feeds display/re-init helpers. The factory resolves each call's provider via featureProvider(settings, featureId) in the leaf module src/api/feature-routing.ts, which returns null when the feature is off (provider === 'none') or unsupported/unconfigured — there is no silent fallback; an unservable feature stays off rather than being re-routed to another provider — then instantiates the ModelApi implementation for the resolved ModelProvider (one client class per provider id; the union lives in src/api/providers/registry.ts). The model comes from resolveFeatureModel(settings, featureId) in src/models.ts, which falls back to the provider's role default only when the stored value is ''. All API implementations follow the ModelApi interface. Retry logic (exponential backoff) is a fixed policy in RetryDecorator, not settings-driven. Each provider lives in its own package under src/api/providers/{gemini,ollama,openai,anthropic}/; the capability matrix lives in src/api/providers/registry.ts and routing helpers live in src/api/feature-routing.ts. Decoding of incoming conversation history — the Gemini Content shape plus the legacy { role, text | message } entry — is shared by the non-Gemini clients in the leaf module src/api/providers/history-walk.ts; OllamaClient/OpenAIClient/AnthropicClient call walkHistoryEntry() (Anthropic passing { acceptsPdf: true }) and then only emit their own message type, so the provider-agnostic half cannot drift between them (the Gemini client speaks Content natively and has no such step). Two shared cross-provider leaves deliberately live at the src/api/providers/ package root, outside the per-provider packages: history-walk.ts (above) and tool-id-ledger.ts (called by AnthropicClient and OpenAIClient).

Key Components

  1. API Layer (src/api/): Factory pattern (ModelClientFactory) for creating provider-appropriate model API clients, decorator pattern (RetryDecorator) for resilience, and interface abstraction (ModelApi) for consistent API interactions. Provider-specific code is encapsulated under src/api/providers/{gemini,ollama,openai,anthropic}/.
  2. Feature Modules: Separate modules for chat, completions (completions.ts), summary (summary.ts), and rewrite (rewrite.ts)
  3. Context System (src/files/index.ts, ScribeFile): Builds context from the active file or explicit file list (no link traversal — the agent follows links dynamically via the read_file tool)
  4. History (src/history/): Markdown-based conversation history with Handlebars templates for agent sessions, stored in [state-folder]/Agent-Sessions/ (legacy note-centric chat history from v3.x remains in [state-folder]/History/)
    • Reasoning persistence: model "thinking" is stored on GeminiConversationEntry.thoughts and serialized as a collapsed > [!reasoning]- callout. Turns with a message (user prompt, final answer) carry a ## header + Message Info table; streamlined reasoning-only turns are just the bare [!reasoning] callout with no header. Parser invariant (SessionHistory.parseHistoryContent): entries are identified by their callout ([!user]/[!assistant]/[!reasoning]), not a ## header, and the parser walks every callout in a ----delimited section (reasoning + tool callouts flow together with no dividers). This keeps the divider-free activity stream, headerless reasoning, and legacy per-entry files all round-tripping — preserve it, and cover format changes with old-format fixtures in test/agent/session-history.test.ts.
  5. Custom Prompts (src/prompts/): User-defined prompt templates stored in [state-folder]/Prompts/
  6. Agent mode (src/agent/, src/tools/): AI agent with tool calling capabilities
    • Session management with persistent history; new session IDs use session_<UUID> from Web Crypto, while existing session_id values in saved history are preserved.
    • Tool registry and execution engine
    • Vault operations tools with permission system
    • Google Search integration (separate from function calling)
    • Web fetch tool using Google's URL Context API
    • Session-level permission system for bypassing confirmations
    • Tool loop detection to prevent infinite execution cycles
    • MCP server integration with stdio and Streamable HTTP transports (src/mcp/)
    • Agent skills system for extensible AI capabilities (see below)
    • AgentLoop (src/agent/agent-loop.ts) — UI-agnostic class that drives the tool-execution loop after the initial model response. Handles iteration, history construction (via pure helpers in agent-loop-helpers.ts), follow-up requests, empty-response retry, cancellation polling, and agentEventBus emissions. UI side effects flow through optional AgentLoopHooks (onToolBatchStart, onToolCallStart, onToolCallComplete, onToolCounted, onFollowUpRequestStart, onModelReasoning, onBudgetUpdate, onFollowUpChunk, onFollowUpStreamReady, onMidLoopCompaction). onModelReasoning fires when an intermediate follow-up response carries model reasoning but continues to another tool batch (the "why I'm calling these tools" thinking); the terminal response's reasoning is returned on AgentLoopResult.thoughts instead. onFollowUpStreamReady fires on the streaming follow-up path with the live StreamingResponse when it starts and with null once it settles, so the UI can register it in the same currentStreamingResponse slot the Stop button cancels — letting Stop halt a mid-stream follow-up immediately instead of waiting for stream.complete (see #1054). The agent view renders both into the tool activity block, interleaved with tool rows. After every tool batch, AgentLoop calls ContextManager.prepareHistory with protectFromIndex pinned to the start of the current turn's tool-loop turns, so long tool chains can be compacted mid-flight without ever folding the in-flight functionCall/thoughtSignature continuity into a summary (only turns from before the loop started are eligible); onMidLoopCompaction fires when that happens so the UI can surface the same "Context Compacted" notice as the pre-turn path (see #662). The tool sort reorders execution only: the replayed turn pair is order-locked to the model's emitted order, and buildToolHistoryTurns enforces that by realigning results onto toolCalls via the sourceIndex stamped pre-sort, so the two arrays are never assumed parallel (callers pass the emitted array the indices came from, never the sorted one), and a cancelled batch gets a synthetic functionResponse per unrun call rather than an unpaired functionCall (see #1499). Callers own: the initial API call, persisting the final response (UI saves to session history; headless callers may write a file), and any UI rendering. AgentLoopOptions.confirmationProvider is required — UI callers pass the AgentView (which implements IConfirmationProvider), headless callers pass an auto-approve/deny provider; the engine never looks it up on the plugin. The agent view (AgentViewTools) is a thin adapter over this loop. Headless callers (e.g. scheduled-task runners) should consume AgentLoop rather than reimplementing the loop. The lazy require('./agent-factory') inside AgentLoop.run is a deliberate pattern to break the import cycle between AgentFactory and the loop — keep top-level imports unless a cycle forces it.
  7. Attachment Pipeline (src/ui/agent-view/agent-view.ts, src/ui/agent-view/agent-view-ui.ts, src/ui/agent-view/agent-view-attachments.ts, src/ui/agent-view/inline-attachment.ts, src/utils/file-classification.ts): Unified drag-and-drop and paste pipeline for file attachments
    • Files dropped or pasted into the agent view are classified by extension using classifyFile() from file-classification.ts
    • Text files (.md, .ts, .json, .base, .canvas, etc.) → context chips (AI reads content)
    • Gemini-supported binary files (images, audio, video, PDF) → base64 inline attachments sent to the model via inlineAttachments on ExtendedModelRequest
    • Unsupported files (.zip, .exe, etc.) → user notification
    • Cumulative 20 MB size limit enforced across vault drops, external drops, and paste
    • Folder drops recursively expand and classify all contained files
    • InlineAttachment (renamed from ImageAttachment) holds base64 data, MIME type, and optional vault path
    • Image attachments show thumbnails; non-image attachments show Lucide icon + filename label
    • Binary file awareness in tools: ReadFileTool uses classifyFile() to detect binary files and reads them via vault.readBinary(), returning inlineData on ToolResult. The tool execution pipeline (agent-view-tools.ts) injects these as inlineData parts alongside functionResponse in conversation history. This allows the agent to autonomously read images, audio, video, and PDFs encountered via tools without manual drag-and-drop.
    • OBSIDIAN_TEXT_EXTENSIONS map in file-classification.ts classifies .base and .canvas as text

Model Configuration

  • Models defined in src/models.ts with automatic version migration
  • Different models for different tasks (chat, summary, completions, rewrite)
  • Settings changes trigger full plugin reload

Important Patterns

  1. Obsidian API First: Always use built-in Obsidian API functions when available instead of low-level operations (for detailed Obsidian API guidance, use the obsidian-plugin-development skill):
    • Use vault.getMarkdownFiles() instead of vault.adapter.list()
    • Use app.fileManager.processFrontMatter() for frontmatter manipulation
    • Use vault.getAbstractFileByPath() for file operations
    • Use app.metadataCache for file metadata access
    • Use app.fileManager.renameFile() for renaming files (preserves metadata)
    • Use app.workspace.openLinkText() for clickable file links in views
  2. File Operations: Always use Obsidian's normalized paths and metadata cache
  3. Error Handling: API calls wrapped with retry logic and exponential backoff
  4. Prompts: Handlebars templates in prompts/ directory, loaded as text files
  5. Debouncing: Completions use a 500ms debounce (the codemirror-companion-extension default; the plugin doesn't override delay) to prevent excessive API calls
  6. State Management: Plugin instance holds all component references with proper cleanup
  7. Folder Structure: Plugin uses structured state folder:
    • [state-folder]/ - Main plugin state folder (default: gemini-scribe)
    • [state-folder]/History/ - Legacy note-centric chat history files (v3.x)
    • [state-folder]/Prompts/ - Custom prompt templates
    • [state-folder]/Agent-Sessions/ - Agent mode session files
    • [state-folder]/Skills/ - Agent skill packages (agentskills.io format)
    • [state-folder]/Scheduled-Tasks/ - Scheduled task definitions and run output
    • [state-folder]/Background-Tasks/ - Output from background deep-research and image-gen tasks
    • [state-folder]/Hooks/ - Lifecycle hook definitions and run output (created when hooksEnabled is true)
    • Automatic migration for existing users from flat structure
    • src/services/state-folder.ts owns this layout (#1382) — STATE_SUBFOLDERS (folder names), STATE_FILES (root-level state files: AGENTS.md, example-prompts.json, rag-index-cache.json, the two debug logs), and stateFolderPath(settings, ...segments), which applies normalizePath once. Compose every state path through the helper; never write normalizePath(`${settings.historyFolder}/Prompts`) by hand. FolderInitializer creates EAGER_SUBFOLDERS, derived from the same constants — Hooks/ is created on demand only when hooksEnabled is on, and read-only legacy History/ is never created by current code. Both omissions are documented in the module. FolderInitializer is the single creator of Scheduled-Tasks/ and Scheduled-Tasks/Runs/ (#1540): LifecycleService.setup() runs the folder pass before refreshing the managers on a re-init, so ScheduledTaskManager.initialize consumes the folders rather than re-creating them. It is a leaf module, so any consumer can import it without a cycle.
  8. System Folder Protection: Always exclude system folders from file operations:
    • The plugin state folder (settings.historyFolder)
    • The .obsidian configuration folder
    • Use exclusion checks in all vault operation tools
  9. Tool Execution Order: When AI needs to perform multiple operations:
    • Always prioritize read operations before destructive operations
    • Sort tool calls to execute reads before writes/deletes
    • Prevents race conditions where files are deleted before being read
  10. Loop Detection: Tool execution includes loop detection to prevent infinite cycles:
  • Tracks identical tool calls within time windows
  • Fixed thresholds (3 identical calls within 30 seconds, set in ToolLoopDetector), always on — not user-configurable
  • Session-specific tracking with automatic cleanup
  • Blocked calls are flagged with loopDetected: true on ToolResult and emit a toolLoopDetected event on the agent bus
  • AgentLoop counts those fires per turn and aborts the turn after AGENT_LOOP_ABORT_THRESHOLD (currently 3) — the result comes back with loopAborted: true and a user-visible notice, which the UI displays but does not persist to session history
  1. Tool Implementation: ToolExecutionContext.plugin is typed ObsidianGemini; no cast is needed in tool implementations — use context.plugin directly.
  2. YAML Frontmatter: Agent instructions include guidance for respecting YAML frontmatter when modifying files
  • The AI is trained to place "top of note" content after frontmatter blocks (defined in prompts/agentRulesPrompt.hbs)
  • YAML frontmatter must start with --- on line 1 and end with ---
  • Content is only placed before frontmatter when explicitly instructed to modify frontmatter
  1. Agent Skills (src/services/skill-manager.ts, src/tools/skill-tools.ts): Extensible skill system following the agentskills.io specification
  • Skills are self-contained packages stored in [state-folder]/Skills/<skill-name>/SKILL.md
  • SkillManager handles discovery, metadata parsing, content loading, resource reading, creation, and name validation
  • Uses progressive disclosure: skill summaries (name + description) are injected into the agent system prompt; full instructions are loaded on-demand via activate_skill
  • Three tools: activate_skill (read-only, loads instructions or resources), create_skill (creates new skill directories with valid SKILL.md), and edit_skill (updates an existing skill's description or instruction body, with diff-review confirmation)
  • Skill names must be lowercase alphanumeric with hyphens, 1-64 chars, no consecutive/leading/trailing hyphens
  • Frontmatter parsing uses Obsidian's native metadataCache API
  • scripts/ directories are treated as read-only reference material (no execution in Obsidian)
  • Bundled help references are auto-generated: scripts/generate-help-references.mjs scans docs/guide/ and docs/reference/ to produce src/services/generated-help-references.ts at build time. Adding or removing a markdown file in those directories automatically updates the help skill — no manual edits to bundled-skills.ts or SKILL.md are needed.
  1. UI String Localization (i18n) (src/i18n/): All static user-facing UI strings (settings, modals, agent view, command names, notices, tooltips) go through t(key, vars?) from src/i18n/index.ts — never hardcode user-visible English. Model-facing strings (prompts, tool descriptions), strings persisted to vault files (history callout markers, frontmatter), logger output, and strings compared with === stay English. src/i18n/en.ts is the source of truth (key → {message, context}); per-language files (ru.ts, pt-br.ts, …) are AI-generated by npm run translate (scripts/translate.mjs, needs GOOGLE_API_KEY) and committed. The script only retranslates keys whose English source hash changed (src/i18n/translation-state.json), so hand-refined translations survive regeneration. Locale comes from Obsidian's getLanguage() with exact → base-language → English fallback. When adding a UI string to a migrated area: add the key + context to en.ts and use t(). You can run npm run translate locally and commit the regenerated files, but you don't have to — the Update UI translations GitHub Action (.github/workflows/i18n-translate.yml) regenerates them automatically when en.ts lands on master (using the GOOGLE_API_KEY repo secret) and opens a chore(i18n) PR for review. It also runs weekly and can be dispatched manually. When adding a language: add a row to LANGUAGES in scripts/translate.mjs, register the export in src/i18n/index.ts, and run npm run translate -- --langs <code> (or trigger the workflow with that langs input).
  2. Plugin type surface — no imports of main.ts (src/types/plugin.ts, src/types/plugin-services.ts): Components never reference the concrete plugin class — import type { ObsidianGemini } from '../types/plugin' (a leaf interface), never import type ObsidianGemini from '../main'. main.ts imports nearly the whole codebase, so a type reference back to it folds the module graph into hundreds of cycles (#1155). The interface declares only core members (settings, apiKey, checkInitialized, …); every service/manager handle (logger, sessionManager, …) is contributed by module augmentation in src/types/plugin-services.ts — an orphan, type-only module nothing imports (it participates via tsconfig include), which keeps types/plugin.ts a true leaf. When adding a service handle to the plugin class in main.ts, mirror it in plugin-services.ts; the class's implements ObsidianGeminiApi clause fails to compile until the two are in sync. More broadly, when a manager and a sibling module (runner, registry, UI section) need the same type or helper, put it in a leaf module both import (services/hook-types.ts, services/skill-types.ts, ui/settings/writer-types.ts, types/settings.ts) instead of importing the manager back. The whole graph is kept acyclic by npm run lint:cycles (madge) in the lint CI workflow.
  3. Derive entity parameter types — don't re-list fields (src/services/hook-types.ts, src/services/scheduled-tasks/types.ts): For a vault-backed entity managed through FileBackedFeatureManager (Hook, ScheduledTask, …), derive the manager's create/update param types from the entity interface — Omit<Entity, 'filePath' | EntityDefaultedField> & Partial<Pick<Entity, EntityDefaultedField>> for create, Partial<Omit<EntityCreateParams, 'slug'>> for update — and have the management modal build its save payload once for both the create and update branch, rather than writing the field list out longhand. A hand-written param type still compiles when the entity gains a field, so the compiler cannot tell you the new field is unsettable or that one save branch dropped it (#1273, #1314, #1322). Full rule in .claude/guidelines/coding.md.

Coding Style & Naming Conventions

For in-depth code quality standards (DRY, SOLID, error handling, performance, security), use the code-review skill.

  • TypeScript-first codebase; group modules by domain and add barrel exports only when they simplify imports.
  • Format with Prettier (npm run format): 2-space indent, 120-column width, semicolons, single quotes, trailing commas.
  • Lint with ESLint (npm run lint, autofix with npm run lint:fix): the eslint-plugin-obsidianmd recommended preset enforces Obsidian-specific best practices (memory-leak prevention, cross-window safety, command-ID conventions, manifest validation, etc.). Pervasive rules that need broader refactoring are temporarily disabled in eslint.config.mjs — see the PERVASIVE_OBSIDIANMD_RULES_TODO block for follow-up items. Never inline-disable an obsidianmd/* rule, no-explicit-any, no-deprecated, @microsoft/sdl/no-inner-html, no-console, no-alert, or the other rules in the eslint-comments/no-restricted-disable list in eslint.config.mjs: the Obsidian community-plugin audit rejects a release that carries one (4.12.0 failed it over ~20 such disables), and lint fails on them locally with the same list. Fix the code instead. A genuine exception becomes a file-scoped override in eslint.config.mjs with its reason. Rules outside that list may still take a line-scoped disable with a -- reason. One repo-specific no-restricted-syntax entry (PATH_CONTAINMENT_RULE) flags hand-rolled path containment through two selectors — the concatenation passed straight to .startsWith() (p.startsWith(folder + '/'), #1402) and the same concatenation hoisted into a local first (const prefix = folder + '/', #1482), which the first selector cannot see — and points at isPathInFolder() in src/utils/file-utils.ts, which owns that predicate and is the rule's only exempt file; a deliberate strict-descendant site takes a line-scoped disable carrying the reason, and both selectors share the one rule so that per-rule disable still suppresses only its own site. A third entry (PROVIDER_LITERAL_RULE, #1317) fails any string literal 'gemini' / 'ollama' / 'openai' / 'anthropic' outside the modules that own provider identity (src/api/providers/**, the src/api routing/credential/status leaves, factory.ts, models.ts, types/) — non-provider modules must ask the registry (getCapabilities/featureProvider/PROVIDER_IDS) instead of branching on provider names; pre-existing sites carry line-scoped disables with reasons that come out as #1308/#703 clear them, and the exemption file list plus per-rule disable caveat live in the comment on the rule in eslint.config.mjs. A second repo-specific entry (no-tags-as-reachability, a local rule in the same file) fails any @public/@beta/@alias JSDoc tag under src/ — the plugin has no public API and the tags would exempt exports from the knip gate (#1525).
  • .editorconfig enforces LF endings and tabbed Markdown/config; avoid hand-editing generated bundles.
  • Use camelCase for variables/functions, PascalCase for classes/types, and kebab-case filenames aligned with their feature area.
  • Handle TypeScript errors properly - ensure all properties are correctly typed
  • Use proper async/await patterns for all asynchronous operations

Console Logging

The plugin uses a dedicated Logger service (src/utils/logger.ts) that respects the debug mode setting. This approach avoids global console patching, preventing conflicts with other plugins and Obsidian's debugging tools.

Accessing the Logger:

  • Plugin components: this.plugin.logger
  • Tool implementations: context.plugin.logger (via ToolExecutionContext)
  • Utility functions: Accept logger as parameter

Logger Methods:

  • logger.log() and logger.debug(): Only output when debug mode is enabled
    • Automatically filtered based on settings.debugMode
    • Prefixed with [Gemini Scribe] for easy identification
  • logger.error() and logger.warn(): Always visible regardless of debug mode
    • Use for important errors and warnings that users should always see
    • Critical failures, API errors, and data integrity issues

Best Practices:

  • Use logger.log() for debug information that helps development and troubleshooting
  • Use logger.error() for errors that indicate something went wrong
  • Use logger.warn() for warnings about deprecated features or potential issues
  • Never use native console.log() or console.debug() directly
  • Pass logger instance to utility functions that need logging

Examples:

// ✅ Good - in plugin components
this.plugin.logger.log('Processing file:', file.path);
this.plugin.logger.debug('Tool execution context:', context);

// ✅ Good - in tool implementations
async execute(params: any, context: ToolExecutionContext) {
    const plugin = context.plugin;
    plugin.logger.log('Executing tool with params:', params);
}

// ✅ Good - in utility functions
export function processData(logger: Logger, data: any) {
    logger.log('Processing data:', data);
}

// ✅ Good - always visible for critical issues
this.plugin.logger.error('Failed to load API key:', error);
this.plugin.logger.warn('Model deprecated, using fallback');

// ❌ Bad - using console directly
console.log('Debug message');

// ❌ Bad - manual debug mode checks (logger handles this)
if (this.plugin.settings.debugMode) {
    this.plugin.logger.log('Debug message');
}

Testing Guidelines

  • Vitest (esbuild-powered) for TypeScript support
  • jsdom environment for DOM testing
  • Test pattern: test/**/?(*.)+(spec|test).[tj]s
  • Keep unit tests next to implementations and name them after the unit (models.test.ts, main.test.ts)
  • Assert observable behavior of prompts, services, and tool orchestration; add regression coverage for bugs
  • Extend shared fixtures under __mocks__/ when mocking new APIs
  • Run npm test before each PR and run the relevant evals/ eval harness tasks (see npm run eval and the eval-harness skill) after touching agent or tool code

For manual testing procedures (desktop symlink setup, mobile testing, smoke test checklists), see docs/contributing/testing.md. For runtime debugging and plugin inspection, use the obsidian-cli skill.

Testing Focus

When adding features, ensure tests cover:

  • Core utility functions
  • API error scenarios with retry behavior
  • File context tree building and circular reference prevention
  • Prompt generation with proper template rendering

Development Practices

Documentation Maintenance

🚨 CRITICAL - DOCUMENTATION IS MANDATORY 🚨

Documentation updates are REQUIRED, not optional. Every code change MUST include corresponding documentation updates in the same PR/commit.

When making ANY change:

  1. Feature Addition:
    • Update README.md with new feature description
    • Create or update relevant user guides in docs/
    • Add examples and use cases
    • Update table of contents and navigation
  2. Feature Updates:
    • Modify ALL affected documentation files
    • Update code examples to reflect changes
    • Revise screenshots or diagrams if needed
  3. Feature Removal:
    • Remove or rewrite documentation for removed features
    • Delete archived docs (users auto-update, no need for old docs)
    • Update migration guides if needed
  4. API/Settings Changes:
    • Update settings-reference.md
    • Update code examples in guides
    • Document breaking changes clearly

Documentation Review Checklist:

  • README.md updated if user-facing change
  • Relevant guides in docs/ updated
  • Code examples tested and current
  • Settings documentation matches actual defaults
  • No references to removed features
  • Internal doc links not broken Remember: Outdated documentation is worse than no documentation. If you change code, you MUST update docs.

Implementation Planning

When planning new features:

  1. Create detailed implementation plans for significant features
  2. Include plans directly in GitHub issues rather than separate files
  3. Structure plans with:
    • Architecture overview
    • Core components with code examples
    • Integration points
    • Testing strategy (unit and integration tests)
    • Migration considerations
    • Timeline estimates

Example: See issue #90 for the custom prompt system implementation plan.

This keeps technical planning centralized and accessible for all contributors.

Autonomous issue pipeline (auto-dev)

The auto-dev skill (from the maintainerd plugin, configured via .claude/maintainerd.json) runs one tick of an unattended issue-to-PR pipeline: it triages open issues for readiness (asking clarifying questions where needed), posts implementation plans for maintainer approval, builds the oldest approved issue into a PR, and addresses CodeRabbit/human review feedback on the open automated PR. When CodeRabbit is rate-limited and a ready, CI-green automated PR has sat past the review window (autoDev.fallbackReviewMinutes, default 60) with no review activity at all, a tick posts its own fresh-eyes fallback self-review — fixing valid findings first, then summarizing in a marker comment with a ## Fallback review heading — so the maintainer still gets a review signal; it is clearly labeled a self-review, capped at one per PR, and never a formal GitHub approval. It never merges — merging is always the maintainer's act — and it keeps at most autoDev.maxPrsInFlight automated PRs (default 6) in flight at once, all awaiting the maintainer's merge.

  • State lives in the auto:* GitHub labels (auto:needs-info, auto:planned, auto:ready, auto:in-progress, auto:parked, auto:skip). Add auto:skip to any issue the pipeline should never touch; add auto:ready to approve a posted plan directly (an approving reply on the plan comment also works); add auto:parked (or reply "park it" to a park proposal) to hold an issue you're still thinking about — the pipeline leaves a parked issue alone until you remove the label or add a new comment. The pipeline builds approved (auto:ready) issues ahead of grooming the backlog, so a greenlit plan never waits behind triage.
  • Comments posted by the pipeline run under the maintainer's own GitHub account and are identified by a hidden <!-- auto-dev --> marker — that marker, not authorship, distinguishes automated comments from human ones.
  • Scheduled runs are driven by a recurring scheduled task (in the cloud) that invokes the skill directly (/auto-dev) each tick — there is no local runner script or permissions allowlist. Each run gets a fresh, disposable full-toolchain sandbox (git, Node, gh authed as the maintainer); the skill owns its own setup (Step 0 puts the sandbox on a clean master and installs deps) and the tick's exit report is the run's summary output. The safety that the old allowlist enforced now lives in the skill's invariants (the self-enforced hard prohibitions in invariant 5: never merge/close, force-push, push to master, release, touch non-auto:* labels, or run destructive shell). Set the schedule's cadence comfortably longer than a typical build tick — there is no lockfile, so the task must not overlap its own runs (the auto:* label state machine, the in-flight cap (autoDev.maxPrsInFlight), and Step 1's orphan age-gate are the backstop).

Driving the maintainer side from a Claude Code session

The maintainer's half of the pipeline (reviewing plans, approving, answering questions) is usually done through an interactive Claude Code session. The review-queue skill (from the maintainerd plugin, invoked as /review-queue or "review the pipeline") packages this whole flow into a guided loop: it surfaces everything blocked on you (open PR, plans, questions, parked, new issues) as a one-line worklist, you reply with an issue/PR number and your decision, and it executes that against the auto:* labels as you (never with the marker), looping until the queue is drained. It never merges on its own and never builds. The steps below are what that skill automates, and what to do when driving the maintainer side by hand. When asked to "review the pipeline", "check what auto-dev is waiting on", "approve the plan on #N", or similar:

  • See the pipeline state: gh issue list --label "auto:planned" (plans awaiting approval), --label "auto:needs-info" (questions/park proposals awaiting answers), --label "auto:ready" (build queue), --label "auto:in-progress" (being built), --label "auto:parked" (issues you've chosen to hold), plus gh pr list --state open filtered to auto/ branches for the PR(s) in flight. Pipeline comments are the ones starting with the hidden <!-- auto-dev --> marker; third-party bot comments (coderabbitai, dependabot, …) are ignored for state decisions; everything else is human input.
  • Approve a plan: reply on the issue with an explicit approval ("Approved.", optionally with amendments — "Approved, but use X instead of Y" counts and the pipeline incorporates the change), or add the auto:ready label directly. Request changes: reply with the changes; the next tick revises the plan. Answer questions on auto:needs-info issues with an ordinary comment; the next tick incorporates it.
  • Never include the <!-- auto-dev --> marker in comments posted on the maintainer's behalf. The pipeline classifies marker comments as its own output — a marked reply would be invisible to it as human input. Don't imitate the pipeline's comment templates either.
  • Taking an issue over manually: before implementing an issue yourself that carries auto:planned/auto:ready (or answering its plan with "I'll do this one"), add auto:skip (or at minimum remove auto:ready) so a scheduled tick doesn't build it concurrently. Remove auto:skip later to hand it back.
  • Reviewing the automated PR works like any PR review: comment on the PR; the next tick addresses the feedback and replies. A PR whose only review is the pipeline's fallback self-review (marker comment with a ## Fallback review heading) has had no independent eyes on it — give it a closer look before merging. Merging is always the human's call — the pipeline never merges, and a merge is what unblocks the next build.
  • Testing / manual ticks: /auto-dev dry-run prints what a tick would do with zero side effects; /auto-dev runs one live tick interactively under normal permission prompts.

Commit & Pull Request Guidelines

For creating pull requests, use the create-pr skill which enforces the PR template and runs all pre-flight checks.

  • Write concise, imperative commit subjects (Fix agent session cleanup, Improve prompt builder); reference issues/PRs with #123
  • Commit generated artifacts (manifest.json, styles.css, versions.json) alongside source changes; main.js is a gitignored build output and is never committed. Use npm run version for releases
  • MANDATORY: Include documentation updates in the same PR/commit as code changes (see Documentation Maintenance section)
  • PRs should explain motivation, highlight user-visible impact, list automated/manual tests, and attach screenshots or vault clips for UI tweaks
  • Flag reviewers who own the affected area and mention required follow-up or rollout notes
  • PR descriptions should explicitly list which documentation files were updated

UI/UX Best Practices

For UI/UX guidelines, use the ui-ux-guidelines skill.

Design system

The plugin has a theme-adaptive design token layer at the top of styles.css (body { --gs-* }) — semantic tokens aliasing Obsidian's theme variables, plus plugin-owned spacing / radius / icon / motion scales and one Gemini brand accent. When writing or modifying UI styles, consume these tokens rather than hardcoding color or reaching for raw var(--obsidian-var) / magic numbers; keep the plugin theme-adaptive. Icons are Lucide via setIcon(), sized from --gs-icon-* (lg = 18px is the default for toolbar/action icons — one size per context). The full reference and token table live in docs/contributing/design-system.md; every surface is now on the tokens (with a few intentional raw-var holdouts). When making a value-preserving CSS/token change, verify no computed-value change in a live vault (obsidian eval + getComputedStyle, or token-resolution equality) rather than eyeballing — that catches token mis-maps and theme-scope bugs a screenshot won't.

Security & Configuration

  • Never commit API keys or vault data; keep secrets in local Obsidian configuration
  • Document new network calls or permissions in docs/ when adding features or capabilities
  • Always use native Obsidian API calls when possible. Documentation here: https://docs.obsidian.md/Home

Download now (ZIP)

  • Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.

    Of the AGENTS.md files we read, the 28,693 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.

  • In this repository8 other files it gives its agents.

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.