- Added 14 bundled TTSR rules (TypeScript and Rust conventions) embedded into the binary via the lowest-priority `builtin-defaults` provider.
- Extracted rule bucketing into `bucketRules` with support for `disabledRules` and `builtinRules` settings.
- Added `ttsr.builtinRules` and `ttsr.disabledRules` settings to control which rules are active per session.
Marketplace and `omp plugin link` installs write to
`<plugins>/node_modules/` rather than to `extensions:` in settings,
so the original PR still missed their sibling skills/, hooks/,
tools/, commands/, rules/, prompts/, .mcp.json sub-trees. Wire
listOmpExtensionRoots to enumerate getEnabledPlugins(cwd, { home })
in addition to CLI-injected and settings-driven roots.
Adds an optional { home } parameter to getEnabledPlugins so the
discovery loader can pass through LoadContext.home for tempdir-rooted
tests. The getPluginsNodeModules/getPluginsPackageJson/
getPluginsLockfile helpers gain the same optional home overload so
they mirror getPluginsDir.
Per-PR review feedback: https://github.com/can1357/oh-my-pi/pull/1498
Bug 1: capability loaders in src/discovery/builtin.ts only walked
.omp/ and ~/.omp/agent/, so extension packages registered via
extensions: in settings or --extension on the CLI shipped their
skills/, hooks/pre|post/, tools/, commands/, rules/, prompts/, and
.mcp.json silently — the docs at omp.sh/docs/extension-authoring
advertise the opposite. Add a new omp-plugins discovery provider that
scans every configured extension package directory for those
sub-trees, plus a small omp-extension-roots helper that resolves the
union of settings-driven and CLI-injected roots. main.ts injects CLI
extension paths via injectOmpExtensionCliRoots before any capability
load.
Bug 2: install was never registered as a top-level subcommand, so
`omp install ./my-extension` was rewritten to `launch install
./my-extension` and forwarded to the LLM as an initial prompt. Add a
top-level install command that routes local paths to plugin link and
remote specs to plugin install. Extract the command table into
src/cli-commands.ts so tests can introspect registered subcommands
without triggering cli.ts's top-level await.
Fixes#1496
Adds optional autoloadSkills field to agent frontmatter that automatically loads listed skills when a sub-agent is spawned. Uses the same buildSkillPromptMessage + sendCustomMessage mechanism as interactive skill loading, queued via sendCustomMessage({ triggerTurn: false }) before the first session.prompt(task). No extra agent turns, no new injection path. Skills stay in listing for sub-resource access. Compaction behavior matches manual loading. Unknown skill names silently skipped.
Lore-id: f85fdbdc
Constraint: autoload must use buildSkillPromptMessage + sendCustomMessage, never modify systemPrompt or use contextFiles
Constraint: triggerTurn must be false to avoid extra agent turns
Rejected: append to systemPrompt | agent cannot distinguish skill content from own instructions
Rejected: contextFiles injection | agent sees opaque file blob, cannot discover sub-resources
Rejected: promptCustomMessage per skill | N extra agent turns with model inference
Directive: autoload skill names are resolved against parent session skill list at spawn time in task/index.ts
Tested: TypeScript compiles clean with tsc --noEmit
Tested: parseAgentFields parses array and CSV string frontmatter
Tested: parseAgentFields returns undefined for absent and empty fields
Not-tested: bun test cannot run locally due to missing pi_natives native addon (requires Rust toolchain)
Confidence: high
Scope-risk: moderate
Reversibility: clean
The native rule discovery provider only scanned the rules/ subdirectory under .omp/ and ~/.omp/agent/. The documented top-level RULES.md file (per https://omp.sh/docs/context-files) was silently ignored — neither ~/.omp/agent/RULES.md nor <repo>/.omp/RULES.md was ever read.
Load both as synthetic rules with alwaysApply forced to true (the whole point of RULES.md is to be reattached every turn). Project scope walks up from cwd to repoRoot using the same nearest-.omp/ search AGENTS.md uses.
Fixes#1266
`getProjectPathCandidates` walks up from cwd to repoRoot (or home as a
fallback). When cwd is anywhere under $HOME and no closer .git boundary
exists, that walk-up reaches the home directory and enumerates
`~/.agent/<segments>` and `~/.agents/<segments>` as project paths.
`getUserPathCandidates` then enumerates the very same directories as
user paths, so every skill/rule/prompt/command/AGENTS.md found there
loaded twice. The capability deduper marks the second copy as shadowed,
but the Extension Dashboard renders shadowed entries — so users see one
active + one greyed-out duplicate of every home-level skill.
Skip the home directory inside the walk-up while still terminating the
loop on it; ancestors above home are still visited if the cwd happens
to live above (e.g. monorepo `home: tempDir` test fixtures).
Tests:
- Drop the duplicated copy of `getProjectPathCandidates` from the
monorepo-skills test; import the real one so behavior stays in sync.
- Replace the old "walk-up stops at home when no repo root" assertion
(which encoded the buggy behavior) with one that pins the new
contract: home-level `.agent[s]/skills` are NOT enumerated as
project paths.
- Add an explicit regression assertion that project ∩ user candidate
sets are empty when cwd is under home.
Fixes#1116
The ClaudePluginManifest interface was missing the `commands` field
(the standard Claude plugin format), and resolvePluginDir only checked
the legacy `slash-commands` key. Plugins declaring their command path
via `"commands": "..."` silently fell back to the hardcoded
`<plugin-root>/commands/` directory, which doesn't exist for
Claude-format plugins, so no slash commands were ever loaded.
Changes:
- Added `commands?: string` to ClaudePluginManifest.
- Changed resolvePluginDir to accept ReadonlyArray<keyof
ClaudePluginManifest> and iterate in priority order; the first
non-empty match wins.
- loadSlashCommands now passes ["commands", "slash-commands"] so
the canonical Claude plugin key takes precedence over the legacy one.
- Added two regression tests: one covering the `commands` key in
isolation, one verifying its precedence over `slash-commands` when
both fields are present.
Fixes#1076
listClaudePluginRoots accepts a home parameter so callers (and tests)
can override the base directory, but the OMP registry path was constructed
via getPluginsDir() which always reads the global dirs resolver anchored
to os.homedir(). This caused every test that passed a temp dir as home to
receive the real user registry alongside the test fixture, producing
length mismatches (+1 root in every assertion).
Fix: add an optional home override to getPluginsDir(). The override only
short-circuits the resolver when home differs from RESOLVER_HOME (the
os.homedir() value captured at module load, i.e. what dirs is anchored
to). Production callers that pass os.homedir() match RESOLVER_HOME and
still go through the XDG-aware resolver, preserving read/write coherence
with the marketplace writer and the cache invalidator. Tests passing a
temp HOME mismatch and short-circuit to <home>/<configDir>/plugins for
deterministic isolation.
- Replaced duplicated plugin-registry cache invalidation blocks with clearPluginRootsAndCaches in command and selector setup paths.
- Updated OMP plugin registry path resolution to use getPluginsDir for reads and cache invalidation, matching marketplace write locations.
- Removed a redundant project-scope marketplace test after centralizing cache-root invalidation logic.
Claude marketplace plugins (e.g. context7@claude-plugins-official) ship
.mcp.json with the server map at the top level rather than under the
mcpServers key. The loader only accepted the nested shape, so install
appeared to succeed but no MCP tools were registered. Detect both shapes
and validate that each entry declares command (stdio) or url (HTTP/SSE)
before registering.
Fixes#851
- Renamed subagent completion flow from `submit_result` to `yield` across SDK tools, prompts, and docs.
- Updated executor/task handling to require and parse `yield` calls, replacing legacy submit-result extraction and state flags.
- Added `subagent-yield-reminder` and updated system prompts to require `yield` with `result.data` or `result.error`.
- Renamed hidden-tool and registration plumbing to `yield`, including discovery helpers and renderer/test surface.
Mirror the existing `commands.enableClaudeUser`/`commands.enableClaudeProject`
schema entries so the OpenCode discovery provider exposes the same
user/project toggle surface as Claude. Default remains true to preserve
current behavior.
Fixes#661
- Standardized missing-file read errors and now return `File not found: <path>` for absent edit targets.
- Centralized AI provider, usage, and OAuth helpers into shared modules to remove duplicated logic.
- Migrated OAuth/API-key login flows to shared factory helpers and removed inline prompt/token-exchange code.
- Reused shared tools and formatter utilities for discovery, stream tails, LSP batching, and source formatting.
- Consolidated repeated test helpers and fixtures into shared modules, replacing inline helper duplicates.
- Extracted prompt rendering and formatting utilities from coding-agent to centralized pi-utils package with new API surface (prompt.render, prompt.format, prompt.registerHelper).
- Migrated parseFrontmatter utility from coding-agent to pi-utils package; updated 8 files to import from @oh-my-pi/pi-utils.
- Removed 170-line prompt-format.ts module and consolidated 192 lines of Handlebars helper registrations into pi-utils prompt module.
- Updated 60+ files across coding-agent and typescript-edit-benchmark to use new prompt.render() and prompt.format() API from pi-utils.
- Simplified prompt-templates.ts by delegating core functionality to pi-utils while retaining custom helper registrations (jtdToTypeScript, jsonStringify, etc.).
When a plugin is installed in both user and project scope, the selector
now shows separate scoped entries (e.g. 'plugin-name@1.0 [project]' and
'plugin-name@1.0 [user]') and forwards the selected scope to
uninstallPlugin(), preventing the disambiguation error introduced when
dual-scope support was added.
Plugins can now be installed at user scope (global) or project scope
(per-project, higher capability priority). Scope is encoded in registry
file location, not a metadata field:
user: ~/.omp/plugins/installed_plugins.json
project: <nearest-project>/.omp/plugins/installed_plugins.json
cache: ~/.omp/plugins/cache/plugins/ (shared, path-referenced)
Project root discovery: resolveActiveProjectRegistryPath(cwd) walks up
from cwd looking for the nearest .omp/ directory, falling back to the
nearest .git root. This is the single resolver used by install, uninstall,
list, upgrade, discovery, and doctor.
Discovery: listClaudePluginRoots(home, cwd?) reads both registries when
cwd is provided. Project entries shadow user entries for the same plugin
ID. Cache key is canonical ("${home}:${resolvedProjectPath}") so nested
cwds within the same project share a cache entry.
Manager changes:
- installPlugin({ scope? }): routes registry reads/writes by scope;
checks collectReferencedPaths() across both registries before deleting
any cached plugin dir to prevent cross-scope data loss
- uninstallPlugin(id, scope?), setPluginEnabled(id, enabled, scope?),
upgradePlugin(id, scope?): throw a disambiguation error when the plugin
exists in both scopes and no scope is specified
- upgradePluginAcrossScopes(id): new; upgrades all scopes the plugin is
installed in; returns InstalledPluginEntry[]
- upgradeAllPlugins(): uses upgradePluginAcrossScopes; result includes scope
- listInstalledPlugins(): returns InstalledPluginSummary[] merged from both
registries; user entries marked shadowedBy: "project" when overridden
CLI: omp plugin install|uninstall|upgrade|enable|disable --scope user|project
Slash: /marketplace install [--scope user|project] name@marketplace
MarketplaceManager constructed with projectInstalledRegistryPath in all
CLI handlers and builtin-registry.ts via resolveActiveProjectRegistryPath.
.gitignore: .omp/plugins/ added (local runtime state, not committed).
preloadPluginRoots/clearClaudePluginRootsCache carry cwd through for LSP.
main.ts passes getProjectDir() at startup.
Tests: 226 pass across 13 files. New: project-scope.test.ts (resolver
walk-up, .git fallback, null return, canonical path, shadow precedence);
manager scope tests (registry isolation, disambiguation errors, cross-scope
cache-ref protection, upgradePluginAcrossScopes, shadowedBy marking).
fixes#581
- Carry over enabled:false from existing entries during force reinstall
and upgrade; disabled plugins stay disabled without user intent change
- After clearing plugin roots cache, fire-and-forget preloadPluginRoots()
so sync LSP config reads stay valid for the rest of the session
- Store injected --plugin-dir roots in module-level variable; survive
cache clears by re-merging on next listClaudePluginRoots() call
- Defer old plugin deletion in force-reinstall until new cache succeeds;
transient failures no longer leave the plugin uninstalled
- Classify HTTP URLs by pathname instead of full string; query params
no longer cause .json URLs to be misclassified as git
- Honor enabled:false flag in listClaudePluginRoots (skip disabled plugins)
- Normalize local source URIs to absolute paths in addMarketplace
- Resolver returns explicit tempCloneRoot instead of path-based inference
* feat(utils): full XDG Base Directory support for all path helpers
Implement XDG-first resolution across all omp path helpers, extend the
migration command to cover every data/state/cache location, and fix
five data-safety issues found in review.
dirs.ts:
- Add getXdgCachePath() helper ($XDG_CACHE_HOME/omp/<subpath>)
- Add isDefaultAgentDir() helper: XDG lookup is only valid when the
resolved agentDir equals the process default (~/.omp/agent); custom
profiles set via PI_CODING_AGENT_DIR or setAgentDir() are never
silently redirected to the global XDG database
- Update 15 functions to XDG-first resolution:
data: getPluginsDir, getRemoteDir, getRemoteHostDir, getPythonEnvDir,
getWorktreeBaseDir
state: getReportsDir, getSshControlDir, getCrashLogPath, getDebugLogPath
cache: getPuppeteerDir, getGpuCachePath, getNativesDir
- Guard XDG lookup with isDefaultAgentDir(agentDir ?? getAgentDir()) in
all 9 agent-subdir helpers so that callers passing the global default
agentDir still resolve to the migrated XDG location, while callers
passing a non-default agentDir or running under a custom profile via
setAgentDir() bypass XDG entirely
- Plugin-derived helpers delegate to getPluginsDir() and follow XDG
resolution automatically
migrate-xdg.ts:
- Add getXdgCacheHome() helper
- Extend MigrationItem.category to include 'cache'
- Add 12 new migration entries: reports, plugins, remote, ssh-control,
remote-host, python-env, puppeteer, wt, gpu_cache.json, natives,
omp-crash.log, omp-debug.log
- Refuse to run when PI_CODING_AGENT_DIR points to a non-default
profile: migration only makes sense for the default ~/.omp/agent tree
- copyDirectory returns skipped source paths (target existed, non-force)
- verifyIntegrity: remove size-mismatch early-return that masked stale
targets as successful copies
- executeMigration: delete only entries that were actually copied;
use rmdir on source dir so it is removed only when empty, preserving
any skipped files for a subsequent --force run
- executeMigration: rename partial target to <target>.bak on integrity
failure instead of deleting; preserves pre-existing user data while
preventing getXdgDataPath from treating the partial tree as
authoritative; source remains intact for re-copy on next run
test isolation:
- Set XDG_DATA_HOME/XDG_STATE_HOME to non-existent paths in
memories-runtime.test.ts beforeEach/afterEach to prevent
getXdgDataPath/getXdgStatePath from resolving to real user data
* fix(utils,coding-agent): fix XDG support issues
dirs.ts:
- Refactor path resolution into DirResolver class. XDG base dirs are
resolved once at construction from env vars (Linux only, no
existsSync). setAgentDir creates a fresh instance, naturally
invalidating all cached paths and recomputing isDefaultProfile.
- getRootSubdir/agentSubdir accept optional XdgCategory parameter;
when set, the XDG base replaces the config root. Every accessor
is a one-liner delegate.
- Non-Linux platforms: XDG fields are null, zero overhead. No
filesystem probing, no string comparisons on the hot path.
- Config-only subdirs (themes, tools, commands, prompts, modules)
have no XDG category — they stay under the config root.
- Remove `import { env } from 'bun'`, use process.env consistently.
- Restore JSDoc comments to document actual defaults (~/.omp/...).
migrate-xdg.ts:
- Gate migrateToXdg on Linux — exits with clear error on other
platforms.
- Fix data loss bug: verifyIntegrity now accepts a Set of skipped
paths and skips verification for files that were intentionally not
copied (pre-existing at target in non-force mode).
- Fix nested directory source deletion: recursive removeSourceEntries
walks the tree and only deletes files not in the skipped set.
- Remove dead _sourcePath variable and unused force parameter from
verifyIntegrity.
- Remove `import { env } from 'bun'`, use process.env consistently.
logger.ts:
- Revert JSDoc to document ~/.omp/logs/ as default.
oauth.ts:
- Replace direct getAgentSubdir call with getTestAuthPath().
CHANGELOG.md:
- Merge duplicate section headers under [Unreleased].
- Add missing blank line before [13.11.1].
---------
Co-authored-by: can1357 <me@can.ac>
- Added per-rule `interruptMode` override capability to TTSR interrupt logic via optional frontmatter field.
- Changed interrupt behavior to respect per-rule `interruptMode` settings with fallback to global `ttsr.interruptMode` configuration.
- Extended `Rule` and `RuleConfig` interfaces with optional `interruptMode` property for granular control.
- Updated rule discovery to extract and validate `interruptMode` from frontmatter with proper enum type checking.
* fix(path): PI_CONFIG_DIR discovery
* refactor: centralize agent dir name into getConfigAgentDirName()
- Add getConfigAgentDirName() to utils/dirs.ts as single source of truth
- Replace duplicated "/agent" suffix construction in helpers.ts and config.ts
- Let biome format mcp/manager.ts canonically
---------
Co-authored-by: can1357 <me@can.ac>
- Introduced Effort enum and ThinkingConfig metadata for per-model reasoning capabilities with min/max effort levels.
- Migrated thinking level API from string-based ThinkingLevel to structured Effort enum across agent and AI packages.
- Added model-thinking module with effort mapping, policy application, and semantic versioning utilities for provider-specific thinking modes.
- Removed supportsXhigh() function and replaced effort clamping with model-aware validation using ThinkingConfig metadata.
- Expanded models.json with thinking configuration objects for 50+ models including Claude, Gemini, and OpenAI variants.
- Added Python analysis scripts for edit tool usage patterns and tool invocation stream processing.
- Added fallback to parse the legacy 'thinking' field when 'thinkingLevel' is not provided. The 'thinkingLevel' field takes precedence when both are present. Added tests to verify backward compatibility and field precedence.
- Extracted thinking module with ThinkingEffort, ThinkingLevel, and ThinkingMode types to centralize reasoning configuration across packages.
- Migrated ThinkingLevel type from pi-agent-core to pi-ai package with new validation functions parseThinkingLevel() and getAvailableThinkingLevel().
- Consolidated thinking level constants and descriptions into reusable exports (ALL_THINKING_LEVELS, THINKING_MODE_DESCRIPTIONS) for consistent UI display.
- Removed local thinking-effort-label utility and replaced formatThinkingEffortLabel() with centralized formatThinking() function from pi-ai.
- Refactored thinking mode handling to distinguish ThinkingSelector (user-facing with 'off' option) from ThinkingEffort (provider-level).
- Removed ancestor directory walking logic from codex and opencode skill loading. Simplified project skills discovery to use single project directory instead of scanning up the filesystem tree. Fixed variable shadowing issues in gemini and github context file loading.
NanoGPT exposes per-thinking-level model IDs (e.g.
anthropic/claude-opus-4.6:thinking:low) as separate entries. These
pollute the model list and collide with parseModelString's thinking
level suffix stripping.
Filter out all :thinking and :thinking:level suffixed nanogpt models
during discovery. When a :thinking variant existed, mark the
corresponding base model as reasoning-capable. NanoGPT already supports
reasoning_effort via the OpenAI-compat path so no provider-side model
ID suffixing is needed.
* feat: monorepo-friendly discovery for AGENTS.md and skills
Support hierarchical config discovery in monorepos by walking up from
cwd through ancestor directories, combining files from all levels
instead of only checking the immediate working directory.
AGENTS.md context files:
- Change dedup key from file.level to file.level + depth, so context
files at different directory levels coexist instead of shadowing
- Root AGENTS.md and sub-project AGENTS.md are both included in the
system prompt, ordered from least specific to most specific
Skills:
- Modify all 4 providers with project-level skill directories (native,
claude, codex, opencode) to walk up from cwd through ancestors,
scanning for skills at each level
- Skills at closer directories win on name conflicts via existing
name-based dedup
Repo root boundary:
- Add findRepoRoot() utility that detects .git to identify repo root
- Add repoRoot field to LoadContext, computed once in loadCapability()
- All walk-up traversals (AGENTS.md, skills, nearest config dir) stop
at the repo root, preventing discovery from leaking outside the repo
- When not in a git repo, falls back to walking to filesystem root
Files changed:
- capability/fs.ts: add findRepoRoot()
- capability/types.ts: add repoRoot to LoadContext
- capability/index.ts: compute repoRoot in loadCapability()
- capability/context-file.ts: depth-aware dedup key
- discovery/agents-md.ts: bound walk-up at repo root
- discovery/builtin.ts: getAncestorDirs stopAt param, skill walk-up
- discovery/claude.ts: skill walk-up with repo root bound
- discovery/codex.ts: skill walk-up with repo root bound
- discovery/opencode.ts: skill walk-up with repo root bound
- extensibility/skills.ts: add repoRoot to inline LoadContext
* feat(agents-provider): add project-level discovery with ancestor walk-up for all capability types
The agents provider (.agent/.agents directories) previously only loaded
capabilities from the user home directory (~/.agent/, ~/.agents/). This
meant project-level .agents/ directories in monorepo roots were not
discovered when sessions started from subdirectories.
Add getProjectPathCandidates() helper that walks from cwd up to repoRoot,
scanning both .agent/ and .agents/ at each ancestor level. Apply this to
all six capability types: skills, rules, prompts, commands, context files
(AGENTS.md), and system prompts (SYSTEM.md). This matches the ancestor
walk-up behavior already present in the builtin (.omp), claude, codex,
and opencode providers.
All loaders now parallelize project-level and user-level scans via
Promise.all. Project-level results appear closest-first so dedup at the
capability layer picks the nearest override.
* fix(discovery): remove hard cap of 20 on ancestor directory walking
All walk-up loops already terminate naturally at repoRoot or filesystem
root. The depth < 20 / .slice(0, 20) / MAX_DEPTH caps were redundant
safety guards that would silently stop discovery in deeply nested
projects.
Removed from: agents.ts, agents-md.ts, builtin.ts, claude.ts, codex.ts,
opencode.ts, and corresponding test files.
* fix(agents-provider): set depth on project-level ContextFile entries for correct dedup
loadContextFiles was creating project-level ContextFile entries without
a depth field. Since contextFileCapability.key uses
`project:${file.depth ?? 0}`, all ancestor-level AGENTS.md files
collapsed to the same key and only the first survived dedup.
Compute depth via calculateDepth(cwd, ancestorDir) where ancestorDir is
two levels up from the file path (past the .agent/.agents config dir).
This gives each ancestor level a distinct dedup key.
* fix(discovery): stop ancestor walk-up at $HOME when not in a git repo
When repoRoot is null (no .git found), walk-up loops traversed all the
way to filesystem root. This caused $HOME to be scanned as a project-
level directory and then again as user-level, producing duplicates.
All providers now use `ctx.repoRoot ?? ctx.home` as the stop boundary:
stop at repoRoot if in a repo, otherwise stop at home. Applied to
agents.ts, agents-md.ts, builtin.ts, claude.ts, codex.ts, and
opencode.ts.
* fix(context-files): clamp depth >= 0 in dedup key and fix provider depth computations
The dedup key `project:${file.depth}` used raw depth, which could be
negative when providers computed it from config subdirectories (e.g.
.claude/, .github/, .gemini/) rather than the ancestor directory. This
caused same-scope cwd-level files to get distinct keys like project:-1
and project:0, bypassing dedup and injecting conflicting instructions.
Two-layer fix:
1. Key function: clamp to Math.max(0, depth) so any file at or below
cwd is treated as cwd-scope (depth 0). Defensive against future
providers.
2. Providers: fix root cause in claude.ts, gemini.ts, github.ts to
compute depth from the ancestor directory (parent of the config
subdir), not the config subdir itself.
---------
Co-authored-by: Can Bölük <can1357@users.noreply.github.com>
- Replaced external dependencies with native Bun APIs: glob, TOML parsing, and HTML parsing.
- Removed 7 unused dependencies (file-type, glob, ignore, marked, zod, ms, @types/ms) and simplified package.json.
- Migrated mermaid cache key type from string to bigint using Bun.hash.xxHash64() for efficient hashing.
- Deleted ignore-files utility module and removed rootDir/ignoreMatcher parameters from loadFilesFromDir().
- Implemented inline MIME type detection for images, reducing sniff buffer from 4100 to 12 bytes.
Add an optional `oauth` config block on MCP server entries in mcp.json,
allowing explicit `clientId` and `callbackPort` values for servers that
don't expose these through auto-discovery (e.g. Slack).
The `oauth.clientId` is used as a fallback — if the server's error
response or well-known metadata includes a client_id, that takes
precedence. The `callbackPort` defaults to 3000 when not specified.
- Fixed skill discovery to continue loading project skills when user skills directory is missing.
- Added error handling in scanSkillsFromDir to gracefully handle ENOENT and log other read failures.
- Added test case verifying project skills load successfully despite missing user skills directory.
- Consolidated @oh-my-pi/pi-utils subpath imports into single package root import across 100+ files.
- Moved tryParseJson utility from local web scrapers module to @oh-my-pi/pi-utils package for centralized JSON parsing.
- Renamed loadSkillsFromDir to scanSkillsFromDir and refactored skill discovery to use fs.promises.readdir instead of glob-based approach.
- Replaced custom parseJSON with tryParseJson across discovery modules for consistent error handling.
- Removed emitCustomToolSessionEvent method and cleanupSshResources function, consolidating shutdown logic into dispose method.
- Updated glob pattern construction to use GlobBuilder with literal_separator(true) for improved path handling.
- Added fallback empty string for undefined tool descriptions across all AI providers to prevent runtime errors.
- Made `description` field required in CustomTool interface and normalized tool descriptions in agent-loop.
- Refactored tool normalization in agent-loop by renaming `injectIntentIntoTools()` to `normalizeTools()` with conditional intent injection.
- Added comprehensive description metadata to todo-write tool schema fields for improved clarity.