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>
This commit is contained in:
garrytanandLockyer committed 2026-10-03 21:03:24 +00:00
1 parent f2838fd7d0
commit 82a79be3f2
2 files changed
+11 -3

No files matched your search

+5 -2
View File
@@ -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='<!-- gstack-managed command (./setup --host opencode): edits are overwritten -->'
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
+6 -1
View File
@@ -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);