This guide provides clear, actionable instructions for AI coding agents working in the current codebase. Follow these rules for productivity, accuracy, and maintainability.
- Plugin Structure:
- Core logic in
src/(entry:src/main.ts, class:Plugin).
- Core logic in
- Settings & Localization:
- Settings:
src/settings.ts,src/settings-data.ts - Localization:
assets/locales.ts, per-locale JSON inassets/locales/
- Settings:
- Build System:
- Custom scripts in
scripts/(not webpack/rollup) - Main:
scripts/build.mjs, Install:scripts/obsidian-install.mjs
- Custom scripts in
- External Library:
- Uses
@polyipseity/obsidian-plugin-libraryfor context, i18n, settings, UI
- Uses
Note: Prefer
bunfor development workflows.
-
Setup
bun install— install dependencies and set up Git hooks (preferred).
-
Build & Install
bun run build— production build (runs checks then builds).bun run dev— development/watch build.bun run obsidian:install <vault>— build and install the plugin to a vault.bun run obsidian:install:force <vault>— force install usingbuild:force(skips format).
-
Note:
scripts/obsidian-install.mjsnow fails gracefully whenmanifest.jsonis missing or invalid and prints a concise error message rather than emitting a full stack trace. This makes local tests and CI logs cleaner and eases assertions for failure cases.bun run check— eslint + prettier(check) + markdownlint.bun run format— eslint --fix, prettier --write, markdownlint --fix.
-
Versioning
- Use
changesetsfor PRs; theversionlifecycle script is configured (node scripts/version.mjs).
- Use
-
Localization
- Add locales by copying
assets/locales/en/translation.jsonand updatingassets/locales/*/language.jsonas needed. Seeassets/locales/README.mdfor conventions.
- Add locales by copying
Quick reference for scripts in package.json. Use bun (preferred).
build— runsformatthenbuild:force.build:force— runsnode scripts/build.mjs(internal build implementation).build:dev— runsbuild:forcein dev mode (bun run build:force -- dev).obsidian:install— runsbuildthennode scripts/obsidian-install.mjs(install to vault).obsidian:install:force— runsbuild:forcethennode scripts/obsidian-install.mjs.check— runscheck:eslint,check:prettier,check:md.check:eslint—eslint --cache --max-warnings=0.check:prettier—prettier --check ..check:md—markdownlint-cli2.format— runsformat:eslint,format:prettier,format:md.format:eslint—eslint --cache --fix.format:prettier—prettier --write ..format:md—markdownlint-cli2 --fix.commitlint—commitlint --from=origin/main --to=HEAD.prepare— runsprek installto set up Git hooks.version— version lifecycle script (node scripts/version.mjs).
CI tip: Use
bun install --frozen-lockfilein CI for deterministic installs.
-
Test runner: Vitest (fast, TypeScript support).
-
Test file conventions and meaning:
*.spec.{ts,js,mjs}— Unit tests (BDD-style): prefer a Behavior-Driven mindset; tests describe what the code should do, focus on small, isolated units, and should be fast and hermetic*.test.{ts,js,mjs}— Integration tests (TDD-style): prefer a Test-Driven mindset for integration verification; tests exercise multiple units or real integrations (filesystem, build, etc.).
Note: In JavaScript the extensions
*.specand*.testare tooling-equivalent; this project adopts the semantic distinction above to encourage appropriate test design (BDD forspec, TDD/integration fortest).
Test path guidance: When referencing package scripts from tests, prefer relative paths that resolve to the package-local scripts/ directory (for example, ../../scripts/... from tests/scripts) instead of using repository-root scripts/ paths. This keeps tests package-scoped, hermetic, and easier to run in isolation.
- Config: Minimal config is in
vitest.config.mtsand includes both*.spec.*and*.test.*globs; add inline comments to that file if you change test behavior or providers.
- Prefer
vi.fn()for spies and stubs instead of inline functions so tests can inspect calls and reset behavior easily.- For async behavior, prefer
vi.fn().mockResolvedValue(x)orvi.fn().mockRejectedValue(err)over() => Promise.resolve()/() => Promise.reject()to make intent explicit and improve readability.
- For async behavior, prefer
- Use
vi.doMock/vi.mockwithvi.resetModules()to isolate module-level mocks. When restoring spies/mocks between tests usevi.restoreAllMocks()(commonly in anafterEach). - Use
vi.spyOn()to observe calls to global objects (console, process) rather than reassigning globals directly. - For timer-based tests, prefer
vi.useFakeTimers()andvi.runAllTimers()/vi.advanceTimersByTime()to make assertions deterministic. - Prefer
vi.mocked(...)for typed module mocks where available to access typed members and avoidanycasts.
These conventions improve test clarity, make failures easier to diagnose, and keep suites hermetic and parallelizable.
Helpful local resources:
-
tests/README.md— Examples and recommended patterns forviusage (async stubs, fake timers, spying globals). -
Run locally:
- Full (default):
bun run test— runs both unit and integration tests with coverage. - Unit-only (Vitest CLI):
bun x vitest run "tests/**/*.spec.{js,ts,mjs}" --coverage— fast, good for PR iteration. - Integration-only (Vitest CLI):
bun x vitest run "tests/**/*.test.{js,ts,mjs}" --coverage— use for longer-running integration suites. - Interactive / watch:
bun run test:watch.
Agent note — vitest CLI:
vitestwithout a subcommand defaults to interactive/watch mode. Agents must never run Vitest in watch mode; always usevitest run <options>or add the--runoption so tests execute non-interactively (example:bun x vitest --run "tests/**/*.spec.{js,ts,mjs}"). - Full (default):
-
Git hooks & CI:
- Pre-push: Prek pre-push hook (configured in
prek.toml) runsbun run test— failing tests will block pushes. - CI: CI jobs run the full test suite (both unit and integration). If adding slow or flaky integration tests, mark them clearly (folder or filename) and justify in the PR description; prefer to keep the default suite fast.
- Pre-push: Prek pre-push hook (configured in
-
Guidelines for agents & contributors:
- Unit tests must be deterministic and hermetic; mock external dependencies and avoid network I/O.
- Integration tests may use fixtures or local resources but must be isolated and documented.
- Keep tests small and focused — single assertion / behavior per test where reasonable.
- Test file structure: follow a one test file per source file convention. Place tests so they mirror the source directory structure under
tests/for both unit (spec) tests and integration (test) suites. Name tests after the source file, e.g.,src/utils/foo.js->tests/utils/foo.spec.js(unit and integration). Only split a test across multiple files if a single test file would be unreasonably large; document the reason in the test file header. - When changing test infra (adding coverage providers, changing runtimes, or altering hooks), update
AGENTS.mdwith rationale and practical instructions so other agents can follow the new workflow.
-
PR checklist (for agents):
- Add/modify tests to cover behavior changes and follow the one test file per source file convention.
- Run
bun x vitest run "tests/**/*.spec.{js,ts,mjs}"locally for fast verification andbun run testfor the full suite. - Keep tests parallelizable and idempotent.
- Document any infra changes in
AGENTS.md.
If you need help designing a test or mocking a dependency, ask for a short example to be added to tests/fixtures/.
TypeScript Types:
- Default to
readonly. All TypeScript properties, interfaces, function parameters, and variables must bereadonlyby default. Only use mutable types when mutation is explicitly required and documented. - Do not use the TypeScript
anytype. Preferunknownoverany. When accepting unknown inputs, validate or use type guards to narrowunknownbefore use. Ifanyis truly unavoidable, document the reason and add tests that assert safety. - Never use
ascasting. Avoidvalue as Fooin production code — prefer safe alternatives such as:- runtime type guards (e.g.
function isFoo(v: unknown): v is Foo) and narrowing checks; - explicit generics / factory functions that preserve typing;
- returning
unknownfrom untrusted boundaries and narrowing at the call site. If a singleascast is unavoidable add a comment explaining why, and add a unit test that exercises the runtime assumptions.
- runtime type guards (e.g.
- Make code type-checking friendly. Prefer explicit types for exported APIs (return types and parameter types), keep public interfaces small and well-typed, prefer discriminated unions for runtime branching, and avoid deeply inferred/complex anonymous types at package boundaries. This makes
tscerrors actionable and helps downstream consumers. - Prefer
interfacefor object shapes: Preferinterface Foo { ... }rather thantype Foo = { ... }for object-shaped declarations when possible. Interfaces are typically better for incremental TypeScript performance (caching and declaration merging) and work well with extension and declaration merging patterns. - When you need union, mapped, or conditional types,
typealiases remain appropriate. Document non-trivial type-level logic with a brief comment so readers understand the intent and tradeoffs.
Example:
// preferred for object shapes
interface Settings {
readonly openChangelogOnUpdate: boolean;
readonly noticeTimeout: number;
}
// prefer a type guard over `as` casting
function isSettings(v: unknown): v is Settings {
return (
typeof v === "object" &&
v !== null &&
"openChangelogOnUpdate" in v &&
typeof (v as any).openChangelogOnUpdate === "boolean"
);
}
// acceptable use of `type` for advanced type composition
type JsonValue = string | number | boolean | null | JsonObject | JsonArray;Commit Messages:
-
All commit messages must follow the Conventional Commits standard.
-
Header should be ≤ 72 characters (use 72 as a human-friendly buffer; tooling still accepts up to 100).
-
Body lines must be hard-wrapped at 100 characters (enforced by commitlint/prek). Prefer 72 for messages intended for humans.
-
See
.agents/instructions/commit-message.instructions.mdfor up-to-date rules, examples, and a short agent-oriented summary. -
Run
bun run commitlintlocally to validate message format before pushing; Husky will run checks onprepare/pre-push as configured.Example (compliant):
refactor(eslint): remove @eslint/compat, eslintrc, js; update Prettier rules - Removed @eslint/compat, @eslint/eslintrc, @eslint/js from config and lockfiles - Updated Prettier to v3 and adjusted markdownlint config for new plugin - Cleaned up ESLint overrides and Svelte linting comments Refs: lint config modernization -
Lifecycle: Register/unload all major managers in
Plugin.onload()
- Obsidian API: Peer dependency, entry/manifest must match plugin requirements
- @polyipseity/obsidian-plugin-library: Central for context, i18n, settings, UI, utils
- External Translations: Some from
polyipseity/obsidian-plugin-library
src/main.ts— Plugin entry, lifecycle, contextsrc/settings.ts/src/settings-data.ts— Settings UI/dataassets/locales.ts/assets/locales/— Localization logic/filesscripts/build.mjs/scripts/obsidian-install.mjs— Build/install scriptsREADME.md/assets/locales/README.md— Contributor/translation instructions.agents/instructions/— Task/file-specific instructions.agents/skills/— Agent skills for specialized workflows
Never use
.github/copilot-instructions.md. All agent instructions must be inAGENTS.mdand referenced from here.
Build Script Usage:
# Preferred
bun run obsidian:install D:/path/to/vaultLocalization Reference:
"welcome": "Welcome, {{user}}!"Use as: i18n.t("welcome", { user: "Alice" })
- Always use
AGENTS.mdfor all agent instructions and guidelines. - Do NOT use
.github/copilot-instructions.mdin this project. - All coding standards, workflow rules, and agent skills must be documented and referenced from
AGENTS.mdonly.
- Always use top-level static imports for modules and types where possible. Use
importandimport typeat the top of the file (immediately following any brief file-level documentation header). Placing imports at the top helps TypeScript and tools perform accurate static analysis and keeps dependency graphs consistent. - Placement rule (explicit): imports should be placed before any other executable code in the file. They may appear after a short file-level doc-comment or header but not after code that executes at module load time.
- Dynamic imports: use
await import(...)only when necessary (for example, to isolate a module under test aftervi.resetModules()or to load resources conditionally at runtime). When you use a dynamic import in tests or runtime code, add a short comment explaining why the dynamic import is required. - Testing note: tests may legitimately import modules dynamically to reset module cache, apply mocks, or mock resource imports. Prefer keeping
import type(type-only imports) at the top of test files when types are required by the test. - Avoid reassignment of imported bindings. If you need to replace a function on an imported module for tests, prefer mutating the module object (e.g.,
Object.assign(lib, { fn: myFn })) rather than reassigning the imported binding itself. - Document exceptions: If you must deviate from these rules, add a brief justification in a code comment or the test file header so reviewers can understand the rationale.
Example (imports and types):
/** File header doc comment allowed here */
import type { Settings } from "../src/settings-data.js"; // type-only import at top
import { loadSettings } from "../src/settings.js"; // runtime import at top
// Avoid placing executable logic (e.g., side-effects) above imports.Example (dynamic import justified in a test):
// Necessary for isolation after we set up mocks
const { loadDocumentations } = await import("../../src/documentations.js");- Template merge guidance: This repository is a template and its instruction files under
.agents/instructions/may be periodically merged into repositories created from this template. For downstream repositories, prefer making minimal edits to template instruction files and, whenever practical, add a new repo-specific instruction file (for example,.agents/instructions/<your-repo>.instructions.md) to capture local overrides. Keeping template files minimally changed reduces merge conflicts when pulling upstream template changes; when a template file must be edited, document the rationale and link to a short issue or PR in your repository.
- .agents/instructions/typescript.instructions.md — TypeScript standards
- .agents/instructions/localization.instructions.md — Localization rules
- .agents/instructions/commit-message.instructions.md — Commit message convention
- .agents/skills/plugin-testing/SKILL.md — Plugin testing skill
- .agents/skills/code-review/SKILL.md — Code review skill
- .agents/instructions/agents.instructions.md — AI agent quick rules
This section contains concise, actionable rules and project-specific examples to help AI agents be productive immediately.
- Read this file first. When in doubt, follow concrete examples in
src/,scripts/, andtests/rather than generic advice. - Start by inspecting
src/main.ts,src/settings-data.ts, andassets/locales.tsto learn core patterns: Manager classes (LanguageManager, SettingsManager),.fix()validators, andPluginLocalesusage. - Settings pattern: always prefer
.fix()functions (seeSettings.fix/LocalSettings.fix) to validate/normalize external inputs before persisting or mutating settings. - I18n: use
createI18n(PluginLocales.RESOURCES, ...)andlanguage.value.t(...)for translations. Never hardcode translatable strings—use existing translation keys inassets/locales/. - Build/Dev pattern:
scripts/build.mjsuses esbuildcontext(); passdevasargv[2]to enable watch mode. Tests mockesbuildintests/scripts/build.test.mjs—use those tests as canonical examples for safe refactors. - Script behavior:
scripts/obsidian-install.mjsexits 1 with a short error message whenmanifest.jsonis missing. Make changes in scripts with tests mirroring error conditions (seetests/scripts/obsidian-install.test.mjs). - Test conventions:
*.spec.*= unit (fast, isolated);*.test.*= integration (may use filesystem or child processes). Follow the one-test-file-per-source-file convention and place tests undertests/mirroringsrc/. - Formatting & linting: run
bun run formatandbun run checkbefore committing. CI usesbun install. - Commit rules for agents: use Conventional Commits; run
bun run commitlintlocally when appropriate. Keep headers ≤100 chars and wrap bodies at 100 chars. - Localization rule for agents: when adding text keys, update
assets/locales/en/translation.jsonfirst and add tests or localization notes. Follow.agents/instructions/localization.instructions.md. - PR checklist: run the code review skill (
.agents/skills/code-review/SKILL.md) for structured change assessment.
Note: Keep suggestions and changes small and well-scoped. Prefer to add tests first for behavioral changes and follow the test naming conventions above.
The library @polyipseity/obsidian-plugin-library is vendored as a git submodule at vendor/obsidian-plugin-library/ (see .gitmodules). A sibling checkout also exists at ../obsidian-plugin-library/ in the monorepo — same remote, different history. Do not confuse them. When asked to commit to "the vendor submodule", the target is vendor/obsidian-plugin-library only; verify with git -C vendor/obsidian-plugin-library rev-parse --show-toplevel (must end in vendor/obsidian-plugin-library) before any git write. See .agents/instructions/submodule.instructions.md for the full boundary rule.
For unclear or incomplete sections, provide feedback to improve this guide for future agents.