From 82a79be3f2424cb645b30481fb86d18a176fc179 Mon Sep 17 00:00:00 2001 From: garrytan Date: Sat, 3 Oct 2026 21:03:24 +0000 Subject: [PATCH] C5: OpenCode /gstack-* commands load the skill by its frontmatter name OpenCode's skill tool knows gstack-review as `review` (its frontmatter name), and OpenCode's builtin /review shadows that name as a command. The managed /gstack-review command told the agent to load `gstack-review`, a name the skill tool does not list. Every managed command now names the frontmatter name. OpenCode's AskUserQuestion/ExitPlanMode mapping (#2759) already shipped in v1.91.16.0. Test migration: the #2629 commands test pinned the directory name. Co-authored-by: Lockyer <135391289+Lockyer228@users.noreply.github.com> --- setup | 7 +++++-- test/host-conformance.test.ts | 7 ++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/setup b/setup index b841efd42..be80537ac 100755 --- a/setup +++ b/setup @@ -2942,7 +2942,7 @@ link_cursor_skill_dirs() { # marker is the user's and is never touched; managed files whose skill is gone # or disabled are removed. write_opencode_commands() { - local skills_dir="$1/.opencode/skills" cmd_dir="$2" d name file desc marker + local skills_dir="$1/.opencode/skills" cmd_dir="$2" d name file desc marker skill marker='' mkdir -p "$cmd_dir" for d in "$skills_dir"/gstack-*/; do @@ -2955,8 +2955,11 @@ write_opencode_commands() { fi case "${_DISABLED_SKILLS:- }" in *" ${name#gstack-} "*) rm -f "$file"; continue ;; esac desc="$(awk '/^description:/ { f = 1; next } f && /^[^ ]/ { exit } f && NF { sub(/^ +/, ""); print; exit }' "$d/SKILL.md" | sed 's/\\/\\\\/g; s/"/\\"/g')" + # The skill tool knows a skill by its frontmatter name (gstack-review's is + # `review`, which OpenCode's builtin /review shadows as a command, #2651). + skill="$(awk '/^---$/ { fm++; next } fm == 1 && $1 == "name:" { print $2; exit }' "$d/SKILL.md")" printf -- '---\ndescription: "%s"\nagent: build\nsubtask: false\n---\n%s\nLoad the `%s` skill with the skill tool and follow its instructions.\n\n$ARGUMENTS\n' \ - "${desc:-gstack $name}" "$marker" "$name" > "$file.tmp.$$" && mv -f "$file.tmp.$$" "$file" + "${desc:-gstack $name}" "$marker" "${skill:-$name}" > "$file.tmp.$$" && mv -f "$file.tmp.$$" "$file" done for file in "$cmd_dir"/gstack-*.md; do [ -f "$file" ] && grep -qF "$marker" "$file" 2>/dev/null || continue diff --git a/test/host-conformance.test.ts b/test/host-conformance.test.ts index 95be95858..953b70263 100644 --- a/test/host-conformance.test.ts +++ b/test/host-conformance.test.ts @@ -131,6 +131,10 @@ describe.skipIf(process.platform === 'win32')('host conformance kit', () => { expect(status.status).toBe(0); expect(status.stdout).toMatch(new RegExp(`\\b${host}\\s+${ALL_HOST_CONFIGS.find(c => c.name === host)!.tier}\\s+global\\s+current`)); + // C5 (#2651): OpenCode's builtin /review shadows the review skill, so + // /gstack-review loads it by the name the skill tool knows. + if (host === 'opencode') expect(readFileSync(join(f.home, '.config/opencode/commands/gstack-review.md'), 'utf8')).toContain('Load the `review` skill with the skill tool'); + // INV-3 install-level checks (C2, C3, ENG-1, ENG-14). if (host !== 'claude') { const runtimeRoot = rows[0][4]; @@ -154,7 +158,8 @@ describe.skipIf(process.platform === 'win32')('host conformance kit', () => { expect(runSetup(f, join(src, 'setup'), ['--host', 'opencode']).status).toBe(0); const ship = readFileSync(join(commands, 'gstack-ship.md'), 'utf8'); expect(ship).toMatch(/^---\ndescription: "[^"]+"\nagent: build\nsubtask: false\n---\n/); - expect(ship).toContain('Load the `gstack-ship` skill with the skill tool'); + // The skill tool loads by frontmatter name, not directory name (#2651). + expect(ship).toContain('Load the `ship` skill with the skill tool'); expect(ship).toContain('$ARGUMENTS'); expect(readFileSync(join(commands, 'gstack-review.md'), 'utf8')).toBe('my own review command\n'); expect(existsSync(join(commands, 'gstack-retired.md'))).toBe(false);