@oh-my-pi/pi-coding-agent 13.7.5 → 13.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [13.8.0] - 2026-03-04
6
+ ### Added
7
+
8
+ - Added `buildCompactHashlineDiffPreview()` function to generate compact diff previews for model-visible tool responses, collapsing long unchanged runs and consecutive additions/removals to show edit shape without full file content
9
+ - Added project-level discovery for `.agent/` and `.agents/` directories, walking up from cwd to repo root (matching behavior of other providers like `.omp`, `.claude`, `.codex`). Applies to skills, rules, prompts, commands, context files (AGENTS.md), and system prompts (SYSTEM.md)
10
+
11
+ ### Changed
12
+
13
+ - Changed edit tool response to include diff summary with line counts (+added -removed) and a compact diff preview instead of warnings-only output
14
+ - Limited auto context promotion to models with explicit `contextPromotionTarget`; models without a configured target now compact on overflow instead of switching to arbitrary larger models ([#282](https://github.com/can1357/oh-my-pi/issues/282))
15
+
16
+ ### Fixed
17
+
18
+ - Fixed `:thinking` suffix in `modelRoles` config values silently breaking model resolution (e.g., `slow: anthropic/claude-opus-4-6:high`) and being stripped on Ctrl+P role cycling
19
+
20
+ ## [13.7.6] - 2026-03-04
21
+ ### Added
22
+
23
+ - Exported `dedupeParseErrors` utility function to deduplicate parse error messages while preserving order
24
+
25
+ ### Fixed
26
+
27
+ - Reduced duplicate parse error messages when multiple patterns fail on the same file
28
+ - Normalized parse error output in ast-grep to remove pattern-specific prefixes and show only file-level errors
29
+
5
30
  ## [13.7.4] - 2026-03-04
6
31
  ### Added
7
32
  - Added `fetch.useKagiSummarizer` setting to toggle Kagi Universal Summarizer usage in the fetch tool.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "13.7.5",
4
+ "version": "13.8.0",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -41,12 +41,12 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@mozilla/readability": "^0.6",
44
- "@oh-my-pi/omp-stats": "13.7.5",
45
- "@oh-my-pi/pi-agent-core": "13.7.5",
46
- "@oh-my-pi/pi-ai": "13.7.5",
47
- "@oh-my-pi/pi-natives": "13.7.5",
48
- "@oh-my-pi/pi-tui": "13.7.5",
49
- "@oh-my-pi/pi-utils": "13.7.5",
44
+ "@oh-my-pi/omp-stats": "13.8.0",
45
+ "@oh-my-pi/pi-agent-core": "13.8.0",
46
+ "@oh-my-pi/pi-ai": "13.8.0",
47
+ "@oh-my-pi/pi-natives": "13.8.0",
48
+ "@oh-my-pi/pi-tui": "13.8.0",
49
+ "@oh-my-pi/pi-utils": "13.8.0",
50
50
  "@sinclair/typebox": "^0.34",
51
51
  "@xterm/headless": "^6.0",
52
52
  "ajv": "^8.18",
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import { Glob } from "bun";
4
3
  import * as path from "node:path";
4
+ import { Glob } from "bun";
5
5
 
6
6
  const docsDir = path.resolve(import.meta.dir, "../../../docs");
7
7
  const outputPath = path.resolve(import.meta.dir, "../src/internal-urls/docs-index.generated.ts");
@@ -14,10 +14,10 @@ for await (const relativePath of glob.scan(docsDir)) {
14
14
  entries.sort();
15
15
 
16
16
  const docsWithContent = await Promise.all(
17
- entries.map(async (relativePath) => ({
17
+ entries.map(async relativePath => ({
18
18
  relativePath,
19
19
  content: await Bun.file(path.join(docsDir, relativePath)).text(),
20
- }))
20
+ })),
21
21
  );
22
22
 
23
23
  const filenamesLiteral = JSON.stringify(entries);
@@ -27,9 +27,12 @@ export const contextFileCapability = defineCapability<ContextFile>({
27
27
  id: "context-files",
28
28
  displayName: "Context Files",
29
29
  description: "Persistent instruction files (CLAUDE.md, AGENTS.md, etc.) that guide agent behavior",
30
- // Deduplicate by level: one user-level file, one project-level file
31
- // Higher-priority providers shadow lower-priority ones at the same scope
32
- key: file => file.level,
30
+ // Deduplicate by scope: one user-level file, and one project-level file per directory depth.
31
+ // Within each depth level, higher-priority providers shadow lower-priority ones.
32
+ // This supports monorepo hierarchies where AGENTS.md exists at multiple ancestor levels.
33
+ // Clamp depth >= 0: files inside config subdirectories of an ancestor (e.g. .claude/, .github/)
34
+ // are same-scope as the ancestor itself.
35
+ key: file => (file.level === "user" ? "user" : `project:${Math.max(0, file.depth ?? 0)}`),
33
36
  validate: file => {
34
37
  if (!file.path) return "Missing path";
35
38
  if (file.content === undefined) return "Missing content";
@@ -66,6 +66,24 @@ export async function walkUp(
66
66
  }
67
67
  }
68
68
 
69
+ /**
70
+ * Walk up from startDir looking for a `.git` entry (file or directory).
71
+ * Returns the directory containing `.git` (the repo root), or null if not in a git repo.
72
+ * Results are based on the cached readDirEntries, so repeated calls are cheap.
73
+ */
74
+ export async function findRepoRoot(startDir: string): Promise<string | null> {
75
+ let current = resolvePath(startDir);
76
+ while (true) {
77
+ const entries = await readDirEntries(current);
78
+ if (entries.some(e => e.name === ".git")) {
79
+ return current;
80
+ }
81
+ const parent = path.dirname(current);
82
+ if (parent === current) return null;
83
+ current = parent;
84
+ }
85
+ }
86
+
69
87
  export function cacheStats(): { content: number; dir: number } {
70
88
  return {
71
89
  content: contentCache.size,
@@ -11,7 +11,7 @@ import * as path from "node:path";
11
11
  import { getProjectDir, logger } from "@oh-my-pi/pi-utils";
12
12
 
13
13
  import type { Settings } from "../config/settings";
14
- import { clearCache as clearFsCache, cacheStats as fsCacheStats, invalidate as invalidateFs } from "./fs";
14
+ import { clearCache as clearFsCache, findRepoRoot, cacheStats as fsCacheStats, invalidate as invalidateFs } from "./fs";
15
15
  import type {
16
16
  Capability,
17
17
  CapabilityInfo,
@@ -220,7 +220,8 @@ export async function loadCapability<T>(capabilityId: string, options: LoadOptio
220
220
 
221
221
  const cwd = options.cwd ?? getProjectDir();
222
222
  const home = os.homedir();
223
- const ctx: LoadContext = { cwd, home };
223
+ const repoRoot = await findRepoRoot(cwd);
224
+ const ctx: LoadContext = { cwd, home, repoRoot };
224
225
  const providers = filterProviders(capability, options);
225
226
 
226
227
  return await loadImpl(capability, providers, ctx, options);
@@ -14,6 +14,8 @@ export interface LoadContext {
14
14
  cwd: string;
15
15
  /** User home directory */
16
16
  home: string;
17
+ /** Git repository root (directory containing .git), or null if not in a repo */
18
+ repoRoot: string | null;
17
19
  }
18
20
 
19
21
  /**
@@ -23,10 +23,22 @@ export interface ScopedModel {
23
23
  * Parse a model string in "provider/modelId" format.
24
24
  * Returns undefined if the format is invalid.
25
25
  */
26
- export function parseModelString(modelStr: string): { provider: string; id: string } | undefined {
26
+ export function parseModelString(
27
+ modelStr: string,
28
+ ): { provider: string; id: string; thinkingLevel?: ThinkingLevel } | undefined {
27
29
  const slashIdx = modelStr.indexOf("/");
28
30
  if (slashIdx <= 0) return undefined;
29
- return { provider: modelStr.slice(0, slashIdx), id: modelStr.slice(slashIdx + 1) };
31
+ const id = modelStr.slice(slashIdx + 1);
32
+ const provider = modelStr.slice(0, slashIdx);
33
+ // Strip valid thinking level suffix (e.g., "claude-sonnet-4-6:high" -> id "claude-sonnet-4-6", thinkingLevel "high")
34
+ const colonIdx = id.lastIndexOf(":");
35
+ if (colonIdx !== -1) {
36
+ const suffix = id.slice(colonIdx + 1);
37
+ if (isValidThinkingLevel(suffix)) {
38
+ return { provider, id: id.slice(0, colonIdx), thinkingLevel: suffix };
39
+ }
40
+ }
41
+ return { provider, id };
30
42
  }
31
43
 
32
44
  /**
@@ -14,7 +14,6 @@ import { calculateDepth, createSourceMeta } from "./helpers";
14
14
 
15
15
  const PROVIDER_ID = "agents-md";
16
16
  const DISPLAY_NAME = "AGENTS.md";
17
- const MAX_DEPTH = 20; // Prevent walking up excessively far from cwd
18
17
 
19
18
  /**
20
19
  * Load standalone AGENTS.md files.
@@ -25,9 +24,8 @@ async function loadAgentsMd(ctx: LoadContext): Promise<LoadResult<ContextFile>>
25
24
 
26
25
  // Walk up from cwd looking for AGENTS.md files
27
26
  let current = ctx.cwd;
28
- let depth = 0;
29
27
 
30
- while (depth < MAX_DEPTH) {
28
+ while (true) {
31
29
  const candidate = path.join(current, "AGENTS.md");
32
30
  const content = await readFile(candidate);
33
31
 
@@ -49,11 +47,12 @@ async function loadAgentsMd(ctx: LoadContext): Promise<LoadResult<ContextFile>>
49
47
  }
50
48
  }
51
49
 
50
+ if (current === (ctx.repoRoot ?? ctx.home)) break; // scanned repo root or home, stop
51
+
52
52
  // Move to parent directory
53
53
  const parent = path.dirname(current);
54
54
  if (parent === current) break; // Reached filesystem root
55
55
  current = parent;
56
- depth++;
57
56
  }
58
57
 
59
58
  return { items, warnings };
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * Agents (standard) Provider
3
3
  *
4
- * Loads user-level skills, rules, prompts, commands, context files, and system prompts from ~/.agent/.
4
+ * Loads skills, rules, prompts, commands, context files, and system prompts
5
+ * from .agent/ and .agents/ directories at both user (~/) and project levels.
6
+ * Project-level discovery walks up from cwd to repoRoot.
5
7
  */
6
8
  import * as path from "node:path";
7
9
  import { registerProvider } from "../capability";
@@ -13,76 +15,97 @@ import { type Skill, skillCapability } from "../capability/skill";
13
15
  import { type SlashCommand, slashCommandCapability } from "../capability/slash-command";
14
16
  import { type SystemPrompt, systemPromptCapability } from "../capability/system-prompt";
15
17
  import type { LoadContext, LoadResult } from "../capability/types";
16
- import { buildRuleFromMarkdown, createSourceMeta, loadFilesFromDir, scanSkillsFromDir } from "./helpers";
18
+ import {
19
+ buildRuleFromMarkdown,
20
+ calculateDepth,
21
+ createSourceMeta,
22
+ loadFilesFromDir,
23
+ scanSkillsFromDir,
24
+ } from "./helpers";
17
25
 
18
26
  const PROVIDER_ID = "agents";
19
27
  const DISPLAY_NAME = "Agents (standard)";
20
28
  const PRIORITY = 70;
21
- const USER_AGENT_DIR_CANDIDATES = [".agent", ".agents"] as const;
29
+ const AGENT_DIR_CANDIDATES = [".agent", ".agents"] as const;
22
30
 
23
- function getUserAgentPathCandidates(ctx: LoadContext, ...segments: string[]): string[] {
24
- return USER_AGENT_DIR_CANDIDATES.map(baseDir => path.join(ctx.home, baseDir, ...segments));
31
+ /** User-level paths: ~/.agent/<segments> and ~/.agents/<segments>. */
32
+ function getUserPathCandidates(ctx: LoadContext, ...segments: string[]): string[] {
33
+ return AGENT_DIR_CANDIDATES.map(baseDir => path.join(ctx.home, baseDir, ...segments));
25
34
  }
26
35
 
27
- async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
28
- const items: Skill[] = [];
29
- const warnings: string[] = [];
30
- for (const userSkillsDir of getUserAgentPathCandidates(ctx, "skills")) {
31
- const result = await scanSkillsFromDir(ctx, {
32
- dir: userSkillsDir,
33
- providerId: PROVIDER_ID,
34
- level: "user",
35
- });
36
- items.push(...result.items);
37
- warnings.push(...(result.warnings ?? []));
36
+ /** Project-level paths: walk up from cwd to repoRoot, returning .agent/<segments> and .agents/<segments> at each level. */
37
+ function getProjectPathCandidates(ctx: LoadContext, ...segments: string[]): string[] {
38
+ const paths: string[] = [];
39
+ let current = ctx.cwd;
40
+ while (true) {
41
+ for (const baseDir of AGENT_DIR_CANDIDATES) {
42
+ paths.push(path.join(current, baseDir, ...segments));
43
+ }
44
+ if (current === (ctx.repoRoot ?? ctx.home)) break;
45
+ const parent = path.dirname(current);
46
+ if (parent === current) break;
47
+ current = parent;
38
48
  }
49
+ return paths;
50
+ }
51
+
52
+ // Skills
53
+ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
54
+ const projectScans = getProjectPathCandidates(ctx, "skills").map(dir =>
55
+ scanSkillsFromDir(ctx, { dir, providerId: PROVIDER_ID, level: "project" }),
56
+ );
57
+ const userScans = getUserPathCandidates(ctx, "skills").map(dir =>
58
+ scanSkillsFromDir(ctx, { dir, providerId: PROVIDER_ID, level: "user" }),
59
+ );
60
+
61
+ const results = await Promise.all([...projectScans, ...userScans]);
62
+
39
63
  return {
40
- items,
41
- warnings,
64
+ items: results.flatMap(r => r.items),
65
+ warnings: results.flatMap(r => r.warnings ?? []),
42
66
  };
43
67
  }
44
68
 
45
69
  registerProvider<Skill>(skillCapability.id, {
46
70
  id: PROVIDER_ID,
47
71
  displayName: DISPLAY_NAME,
48
- description: "Load skills from ~/.agent/skills (fallback ~/.agents/skills)",
72
+ description: "Load skills from .agent/skills and .agents/skills (project walk-up + user home)",
49
73
  priority: PRIORITY,
50
74
  load: loadSkills,
51
75
  });
52
76
 
53
77
  // Rules
54
78
  async function loadRules(ctx: LoadContext): Promise<LoadResult<Rule>> {
55
- const items: Rule[] = [];
56
- const warnings: string[] = [];
57
- for (const userRulesDir of getUserAgentPathCandidates(ctx, "rules")) {
58
- const result = await loadFilesFromDir<Rule>(ctx, userRulesDir, PROVIDER_ID, "user", {
79
+ const load = (dir: string, level: "user" | "project") =>
80
+ loadFilesFromDir<Rule>(ctx, dir, PROVIDER_ID, level, {
59
81
  extensions: ["md", "mdc"],
60
82
  transform: (name, content, filePath, source) =>
61
83
  buildRuleFromMarkdown(name, content, filePath, source, { stripNamePattern: /\.(md|mdc)$/ }),
62
84
  });
63
- items.push(...result.items);
64
- warnings.push(...(result.warnings ?? []));
65
- }
85
+
86
+ const results = await Promise.all([
87
+ ...getProjectPathCandidates(ctx, "rules").map(dir => load(dir, "project")),
88
+ ...getUserPathCandidates(ctx, "rules").map(dir => load(dir, "user")),
89
+ ]);
90
+
66
91
  return {
67
- items,
68
- warnings,
92
+ items: results.flatMap(r => r.items),
93
+ warnings: results.flatMap(r => r.warnings ?? []),
69
94
  };
70
95
  }
71
96
 
72
97
  registerProvider<Rule>(ruleCapability.id, {
73
98
  id: PROVIDER_ID,
74
99
  displayName: DISPLAY_NAME,
75
- description: "Load rules from ~/.agent/rules (fallback ~/.agents/rules)",
100
+ description: "Load rules from .agent/rules and .agents/rules (project walk-up + user home)",
76
101
  priority: PRIORITY,
77
102
  load: loadRules,
78
103
  });
79
104
 
80
105
  // Prompts
81
106
  async function loadPrompts(ctx: LoadContext): Promise<LoadResult<Prompt>> {
82
- const items: Prompt[] = [];
83
- const warnings: string[] = [];
84
- for (const userPromptsDir of getUserAgentPathCandidates(ctx, "prompts")) {
85
- const result = await loadFilesFromDir<Prompt>(ctx, userPromptsDir, PROVIDER_ID, "user", {
107
+ const load = (dir: string, level: "user" | "project") =>
108
+ loadFilesFromDir<Prompt>(ctx, dir, PROVIDER_ID, level, {
86
109
  extensions: ["md"],
87
110
  transform: (name, content, filePath, source) => ({
88
111
  name: name.replace(/\.md$/, ""),
@@ -91,109 +114,106 @@ async function loadPrompts(ctx: LoadContext): Promise<LoadResult<Prompt>> {
91
114
  _source: source,
92
115
  }),
93
116
  });
94
- items.push(...result.items);
95
- warnings.push(...(result.warnings ?? []));
96
- }
117
+
118
+ const results = await Promise.all([
119
+ ...getProjectPathCandidates(ctx, "prompts").map(dir => load(dir, "project")),
120
+ ...getUserPathCandidates(ctx, "prompts").map(dir => load(dir, "user")),
121
+ ]);
122
+
97
123
  return {
98
- items,
99
- warnings,
124
+ items: results.flatMap(r => r.items),
125
+ warnings: results.flatMap(r => r.warnings ?? []),
100
126
  };
101
127
  }
102
128
 
103
129
  registerProvider<Prompt>(promptCapability.id, {
104
130
  id: PROVIDER_ID,
105
131
  displayName: DISPLAY_NAME,
106
- description: "Load prompts from ~/.agent/prompts (fallback ~/.agents/prompts)",
132
+ description: "Load prompts from .agent/prompts and .agents/prompts (project walk-up + user home)",
107
133
  priority: PRIORITY,
108
134
  load: loadPrompts,
109
135
  });
110
136
 
111
137
  // Slash Commands
112
138
  async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashCommand>> {
113
- const items: SlashCommand[] = [];
114
- const warnings: string[] = [];
115
- for (const userCommandsDir of getUserAgentPathCandidates(ctx, "commands")) {
116
- const result = await loadFilesFromDir<SlashCommand>(ctx, userCommandsDir, PROVIDER_ID, "user", {
139
+ const load = (dir: string, level: "user" | "project") =>
140
+ loadFilesFromDir<SlashCommand>(ctx, dir, PROVIDER_ID, level, {
117
141
  extensions: ["md"],
118
142
  transform: (name, content, filePath, source) => ({
119
143
  name: name.replace(/\.md$/, ""),
120
144
  path: filePath,
121
145
  content,
122
- level: "user",
146
+ level,
123
147
  _source: source,
124
148
  }),
125
149
  });
126
- items.push(...result.items);
127
- warnings.push(...(result.warnings ?? []));
128
- }
150
+
151
+ const results = await Promise.all([
152
+ ...getProjectPathCandidates(ctx, "commands").map(dir => load(dir, "project")),
153
+ ...getUserPathCandidates(ctx, "commands").map(dir => load(dir, "user")),
154
+ ]);
155
+
129
156
  return {
130
- items,
131
- warnings,
157
+ items: results.flatMap(r => r.items),
158
+ warnings: results.flatMap(r => r.warnings ?? []),
132
159
  };
133
160
  }
134
161
 
135
162
  registerProvider<SlashCommand>(slashCommandCapability.id, {
136
163
  id: PROVIDER_ID,
137
164
  displayName: DISPLAY_NAME,
138
- description: "Load commands from ~/.agent/commands (fallback ~/.agents/commands)",
165
+ description: "Load commands from .agent/commands and .agents/commands (project walk-up + user home)",
139
166
  priority: PRIORITY,
140
167
  load: loadSlashCommands,
141
168
  });
142
169
 
143
170
  // Context Files (AGENTS.md)
144
171
  async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFile>> {
145
- const items: ContextFile[] = [];
146
- for (const agentsPath of getUserAgentPathCandidates(ctx, "AGENTS.md")) {
147
- const content = await readFile(agentsPath);
148
- if (!content) {
149
- continue;
150
- }
151
- items.push({
152
- path: agentsPath,
153
- content,
154
- level: "user",
155
- _source: createSourceMeta(PROVIDER_ID, agentsPath, "user"),
156
- });
157
- }
158
- return {
159
- items,
160
- warnings: [],
172
+ const load = async (filePath: string, level: "user" | "project"): Promise<ContextFile | null> => {
173
+ const content = await readFile(filePath);
174
+ if (!content) return null;
175
+ // filePath is <ancestor>/.agent(s)/AGENTS.md — go up past the config dir to the ancestor
176
+ const ancestorDir = path.dirname(path.dirname(filePath));
177
+ const depth = level === "project" ? calculateDepth(ctx.cwd, ancestorDir, path.sep) : undefined;
178
+ return { path: filePath, content, level, depth, _source: createSourceMeta(PROVIDER_ID, filePath, level) };
161
179
  };
180
+
181
+ const results = await Promise.all([
182
+ ...getProjectPathCandidates(ctx, "AGENTS.md").map(p => load(p, "project")),
183
+ ...getUserPathCandidates(ctx, "AGENTS.md").map(p => load(p, "user")),
184
+ ]);
185
+
186
+ return { items: results.filter((r): r is ContextFile => r !== null), warnings: [] };
162
187
  }
163
188
 
164
189
  registerProvider<ContextFile>(contextFileCapability.id, {
165
190
  id: PROVIDER_ID,
166
191
  displayName: DISPLAY_NAME,
167
- description: "Load AGENTS.md from ~/.agent (fallback ~/.agents)",
192
+ description: "Load AGENTS.md from .agent and .agents (project walk-up + user home)",
168
193
  priority: PRIORITY,
169
194
  load: loadContextFiles,
170
195
  });
171
196
 
172
197
  // System Prompt (SYSTEM.md)
173
198
  async function loadSystemPrompt(ctx: LoadContext): Promise<LoadResult<SystemPrompt>> {
174
- const items: SystemPrompt[] = [];
175
- for (const systemPath of getUserAgentPathCandidates(ctx, "SYSTEM.md")) {
176
- const content = await readFile(systemPath);
177
- if (!content) {
178
- continue;
179
- }
180
- items.push({
181
- path: systemPath,
182
- content,
183
- level: "user",
184
- _source: createSourceMeta(PROVIDER_ID, systemPath, "user"),
185
- });
186
- }
187
- return {
188
- items,
189
- warnings: [],
199
+ const load = async (filePath: string, level: "user" | "project"): Promise<SystemPrompt | null> => {
200
+ const content = await readFile(filePath);
201
+ if (!content) return null;
202
+ return { path: filePath, content, level, _source: createSourceMeta(PROVIDER_ID, filePath, level) };
190
203
  };
204
+
205
+ const results = await Promise.all([
206
+ ...getProjectPathCandidates(ctx, "SYSTEM.md").map(p => load(p, "project")),
207
+ ...getUserPathCandidates(ctx, "SYSTEM.md").map(p => load(p, "user")),
208
+ ]);
209
+
210
+ return { items: results.filter((r): r is SystemPrompt => r !== null), warnings: [] };
191
211
  }
192
212
 
193
213
  registerProvider<SystemPrompt>(systemPromptCapability.id, {
194
214
  id: PROVIDER_ID,
195
215
  displayName: DISPLAY_NAME,
196
- description: "Load SYSTEM.md from ~/.agent (fallback ~/.agents)",
216
+ description: "Load SYSTEM.md from .agent and .agents (project walk-up + user home)",
197
217
  priority: PRIORITY,
198
218
  load: loadSystemPrompt,
199
219
  });
@@ -68,12 +68,13 @@ async function getConfigDirs(ctx: LoadContext): Promise<Array<{ dir: string; lev
68
68
  return result;
69
69
  }
70
70
 
71
- function getAncestorDirs(cwd: string): Array<{ dir: string; depth: number }> {
71
+ function getAncestorDirs(cwd: string, stopAt?: string | null): Array<{ dir: string; depth: number }> {
72
72
  const ancestors: Array<{ dir: string; depth: number }> = [];
73
73
  let current = cwd;
74
74
  let depth = 0;
75
75
  while (true) {
76
76
  ancestors.push({ dir: current, depth });
77
+ if (stopAt && current === stopAt) break;
77
78
  const parent = path.dirname(current);
78
79
  if (parent === current) break;
79
80
  current = parent;
@@ -82,8 +83,11 @@ function getAncestorDirs(cwd: string): Array<{ dir: string; depth: number }> {
82
83
  return ancestors;
83
84
  }
84
85
 
85
- async function findNearestProjectConfigDir(cwd: string): Promise<{ dir: string; depth: number } | null> {
86
- for (const ancestor of getAncestorDirs(cwd)) {
86
+ async function findNearestProjectConfigDir(
87
+ cwd: string,
88
+ repoRoot?: string | null,
89
+ ): Promise<{ dir: string; depth: number } | null> {
90
+ for (const ancestor of getAncestorDirs(cwd, repoRoot)) {
87
91
  const configDir = await ifNonEmptyDir(ancestor.dir, PATHS.projectDir);
88
92
  if (configDir) return { dir: configDir, depth: ancestor.depth };
89
93
  }
@@ -215,7 +219,7 @@ async function loadSystemPrompt(ctx: LoadContext): Promise<LoadResult<SystemProm
215
219
  });
216
220
  }
217
221
 
218
- const nearestProjectConfigDir = await findNearestProjectConfigDir(ctx.cwd);
222
+ const nearestProjectConfigDir = await findNearestProjectConfigDir(ctx.cwd, ctx.repoRoot);
219
223
  if (nearestProjectConfigDir) {
220
224
  const projectPath = path.join(nearestProjectConfigDir.dir, "SYSTEM.md");
221
225
  const projectContent = await readFile(projectPath);
@@ -242,18 +246,27 @@ registerProvider<SystemPrompt>(systemPromptCapability.id, {
242
246
 
243
247
  // Skills
244
248
  async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
245
- const configDirs = await getConfigDirs(ctx);
246
- const results = await Promise.all(
247
- configDirs.map(({ dir, level }) =>
248
- scanSkillsFromDir(ctx, {
249
- dir: path.join(dir, "skills"),
250
- providerId: PROVIDER_ID,
251
- level,
252
- requireDescription: true,
253
- }),
254
- ),
249
+ // Walk up from cwd finding .omp/skills/ in ancestors (closest first)
250
+ const ancestors = getAncestorDirs(ctx.cwd, ctx.repoRoot ?? ctx.home);
251
+ const projectScans = ancestors.map(({ dir }) =>
252
+ scanSkillsFromDir(ctx, {
253
+ dir: path.join(dir, PATHS.projectDir, "skills"),
254
+ providerId: PROVIDER_ID,
255
+ level: "project",
256
+ requireDescription: true,
257
+ }),
255
258
  );
256
259
 
260
+ // User-level scan from ~/.omp/agent/skills/
261
+ const userScan = scanSkillsFromDir(ctx, {
262
+ dir: path.join(ctx.home, PATHS.userAgent, "skills"),
263
+ providerId: PROVIDER_ID,
264
+ level: "user",
265
+ requireDescription: true,
266
+ });
267
+
268
+ const results = await Promise.all([...projectScans, userScan]);
269
+
257
270
  return {
258
271
  items: results.flatMap(r => r.items),
259
272
  warnings: results.flatMap(r => r.warnings ?? []),
@@ -795,7 +808,7 @@ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFil
795
808
  });
796
809
  }
797
810
 
798
- const nearestProjectConfigDir = await findNearestProjectConfigDir(ctx.cwd);
811
+ const nearestProjectConfigDir = await findNearestProjectConfigDir(ctx.cwd, ctx.repoRoot);
799
812
  if (nearestProjectConfigDir) {
800
813
  const projectPath = path.join(nearestProjectConfigDir.dir, "AGENTS.md");
801
814
  const projectContent = await readFile(projectPath);
@@ -145,7 +145,7 @@ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFil
145
145
  const projectClaudeMd = path.join(projectBase, "CLAUDE.md");
146
146
  const projectContent = await readFile(projectClaudeMd);
147
147
  if (projectContent !== null) {
148
- const depth = calculateDepth(ctx.cwd, projectBase, path.sep);
148
+ const depth = calculateDepth(ctx.cwd, path.dirname(projectBase), path.sep);
149
149
  items.push({
150
150
  path: projectClaudeMd,
151
151
  content: projectContent,
@@ -164,11 +164,27 @@ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFil
164
164
 
165
165
  async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
166
166
  const userSkillsDir = path.join(getUserClaude(ctx), "skills");
167
- const projectSkillsDir = path.join(getProjectClaude(ctx), "skills");
168
167
 
169
- const [userResult, projectResult] = await Promise.allSettled([
168
+ // Walk up from cwd finding .claude/skills/ in ancestors
169
+ const projectScans: Promise<LoadResult<Skill>>[] = [];
170
+ let current = ctx.cwd;
171
+ while (true) {
172
+ projectScans.push(
173
+ scanSkillsFromDir(ctx, {
174
+ dir: path.join(current, CONFIG_DIR, "skills"),
175
+ providerId: PROVIDER_ID,
176
+ level: "project",
177
+ }),
178
+ );
179
+ if (current === (ctx.repoRoot ?? ctx.home)) break;
180
+ const parent = path.dirname(current);
181
+ if (parent === current) break; // filesystem root
182
+ current = parent;
183
+ }
184
+
185
+ const [userResult, ...projectResults] = await Promise.allSettled([
170
186
  scanSkillsFromDir(ctx, { dir: userSkillsDir, providerId: PROVIDER_ID, level: "user" }),
171
- scanSkillsFromDir(ctx, { dir: projectSkillsDir, providerId: PROVIDER_ID, level: "project" }),
187
+ ...projectScans,
172
188
  ]);
173
189
 
174
190
  const items: Skill[] = [];
@@ -181,11 +197,13 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
181
197
  warnings.push(`Failed to scan Claude user skills in ${userSkillsDir}: ${String(userResult.reason)}`);
182
198
  }
183
199
 
184
- if (projectResult.status === "fulfilled") {
185
- items.push(...projectResult.value.items);
186
- warnings.push(...(projectResult.value.warnings ?? []));
187
- } else if (!isMissingDirectoryError(projectResult.reason)) {
188
- warnings.push(`Failed to scan Claude project skills in ${projectSkillsDir}: ${String(projectResult.reason)}`);
200
+ for (const projectResult of projectResults) {
201
+ if (projectResult.status === "fulfilled") {
202
+ items.push(...projectResult.value.items);
203
+ warnings.push(...(projectResult.value.warnings ?? []));
204
+ } else if (!isMissingDirectoryError(projectResult.reason)) {
205
+ warnings.push(`Failed to scan Claude project skills: ${String(projectResult.reason)}`);
206
+ }
189
207
  }
190
208
 
191
209
  return { items, warnings };