Skip to content

Commit d13846f

Browse files
author
Tehan
committed
fix(skill-memory): accept plain filesystem path in skill provenance (opencode #33580)
opencode's skill tool changed the 'Base directory for this skill:' line from a file:// URL to a plain filesystem path (upstream #33580). Our parser hard-required file:/// so parseSkillProvenance returned null on current opencode -> skill-load registry never populated -> every agent ctx_skill_note failed with a provenance parse error. Agent-written notes silently stopped 2026-07-02 (only historian-path notes, which bypass this parser, continued). Widen BASE_DIR_REGEX to capture the rest of the line and branch on the value: file:// -> fileURLToPath (legacy/back-compat); otherwise treat as a plain path. Keeps the line-anchor + last-match decoy-rejection invariant. +4 regression tests (plain global, plain project, plain decoy last-match, plain mid-line-ignore); all existing file:// tests unchanged.
1 parent 3a1733d commit d13846f

2 files changed

Lines changed: 55 additions & 19 deletions

File tree

packages/plugin/src/features/magic-context/skill-memory/provenance.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,36 @@ describe("parseSkillProvenance", () => {
9191
const result = parseSkillProvenance(output, "right");
9292
expect(result!.resolvedPath).toBe(`${HOME}/.config/opencode/skills/right/SKILL.md`);
9393
});
94+
95+
test("parses a PLAIN filesystem path (opencode #33580 — no file:// prefix)", () => {
96+
const output = `# Skill: council\nsome content\nBase directory for this skill: ${HOME}/.config/opencode/skills/council`;
97+
const result = parseSkillProvenance(output, "council");
98+
expect(result).not.toBeNull();
99+
expect(result!.resolvedPath).toBe(`${HOME}/.config/opencode/skills/council/SKILL.md`);
100+
expect(result!.tier).toBe("global");
101+
expect(result!.skillSource).toBe("opencode-global");
102+
});
103+
104+
test("parses a PLAIN filesystem path for a PROJECT skill", () => {
105+
const output = `Base directory for this skill: ${HOME}/projects/foo/.opencode/skills/my-skill`;
106+
const result = parseSkillProvenance(output, "my-skill");
107+
expect(result!.tier).toBe("project");
108+
expect(result!.skillSource).toBe("opencode-project");
109+
});
110+
111+
test("takes the LAST match with a plain-path decoy followed by the real plain line", () => {
112+
const output =
113+
`# Skill: skill-memory internals\n` +
114+
`Example: Base directory for this skill: /decoy/evil\n` +
115+
`more prose\n` +
116+
`Base directory for this skill: ${HOME}/.config/opencode/skills/real`;
117+
const result = parseSkillProvenance(output, "real");
118+
expect(result!.resolvedPath).toBe(`${HOME}/.config/opencode/skills/real/SKILL.md`);
119+
});
120+
121+
test("ignores a mid-line (non-line-anchored) plain-path mention", () => {
122+
const output = `Note: see Base directory for this skill: /wrong/x for details.\nBase directory for this skill: ${HOME}/.config/opencode/skills/right`;
123+
const result = parseSkillProvenance(output, "right");
124+
expect(result!.resolvedPath).toBe(`${HOME}/.config/opencode/skills/right/SKILL.md`);
125+
});
94126
});

packages/plugin/src/features/magic-context/skill-memory/provenance.ts

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,35 @@ export interface SkillProvenance {
88
loadedAt: number;
99
}
1010

11-
// Matches: "Base directory for this skill: file:///abs/path/to/skill/dir"
12-
// Uses fileURLToPath (not naive regex capture) for cross-platform correctness.
13-
// Anchored to line-start (`^…/gm`) AND we take the LAST match: opencode appends
14-
// this provenance line at the END of the tool output, so if the skill's own
15-
// CONTENT contains the same phrase (e.g. a skill documenting skill-memory
16-
// provenance), a first-match/unanchored parse would capture the wrong URL and
17-
// misdirect recall to a bogus skill identity. Line-anchoring rejects mid-prose
18-
// mentions; last-match ensures the real trailing provenance line wins even if an
19-
// example block reproduces it at column 0.
20-
const BASE_DIR_REGEX = /^Base directory for this skill: (file:\/\/\/[^\n\r]+)/gm;
11+
// Matches: "Base directory for this skill: <path-or-file-url>"
12+
// opencode #33580 changed the emit from a file:// URL to a PLAIN filesystem
13+
// path, so we accept BOTH forms. Anchored to line-start (^…/gm) + last-match:
14+
// opencode appends this provenance line at the END of the tool output, so if
15+
// the skill's own CONTENT contains the phrase at column 0, last-match ensures
16+
// the real trailing line wins; line-anchoring rejects mid-prose mentions.
17+
const BASE_DIR_REGEX = /^Base directory for this skill: (.+)$/gm;
2118

2219
export function parseSkillProvenance(output: string, skillId: string): SkillProvenance | null {
2320
const matches = [...output.matchAll(BASE_DIR_REGEX)];
2421
if (matches.length === 0) return null;
2522

26-
const fileUrl = matches[matches.length - 1][1].trim();
23+
const raw = matches[matches.length - 1][1].trim();
24+
if (!raw) return null;
25+
2726
let absDir: string;
28-
try {
29-
// Normalize OS-native separators to forward slashes: on Windows
30-
// fileURLToPath yields backslash paths, which would fail the
31-
// forward-slash startsWith/includes tier/source checks below and
32-
// misclassify global skills as project-local.
33-
absDir = fileURLToPath(new URL(fileUrl)).replace(/\\/g, "/");
34-
} catch {
35-
return null;
27+
if (raw.startsWith("file://")) {
28+
// Legacy opencode (pre-#33580) emitted a file:// URL. Use fileURLToPath
29+
// (not naive slice) for cross-platform + percent-decoding correctness.
30+
try {
31+
absDir = fileURLToPath(new URL(raw)).replace(/\\/g, "/");
32+
} catch {
33+
return null;
34+
}
35+
} else {
36+
// Current opencode (#33580) emits a plain filesystem path. Normalize
37+
// OS-native backslashes to forward slashes so the tier/source
38+
// startsWith/includes checks below match on every platform.
39+
absDir = raw.replace(/\\/g, "/");
3640
}
3741

3842
const resolvedPath = `${absDir}/SKILL.md`;

0 commit comments

Comments
 (0)