fix: resolve 5 remaining pre-publish blockers (14, 15, 17, 21, 25c)

- completion-promise-detector: restrict to assistant text parts only,
  remove tool_result from completion detection (blocker 14)
- ralph-loop tests: flip tool_result completion expectations to negative
  coverage, add false-positive rejection tests (blocker 15)
- skill tools: merge nativeSkills into initial cachedDescription
  synchronously before any execute() call (blocker 17)
- skill tools test: add assertion for initial description including
  native skills before execute() (blocker 25c)
- docs: sync all 4 fallback-chain docs with model-requirements.ts
  runtime source of truth (blocker 21)

Verified: bun test (4599 pass / 0 fail), tsc --noEmit clean
This commit is contained in:
YeonGyu-Kim
2026-03-28 15:57:27 +09:00
parent d2c576c510
commit 4a029258a4
11 changed files with 171 additions and 121 deletions
+4 -3
View File
@@ -621,7 +621,7 @@ describe("skill tool - nativeSkills integration", () => {
const tool = createSkillTool({
skills: [createMockSkill("seeded-skill")],
nativeSkills: {
async all() {
all() {
return [{
name: "native-visible-skill",
description: "Native skill exposed from config",
@@ -629,13 +629,14 @@ describe("skill tool - nativeSkills integration", () => {
content: "Native visible skill body",
}]
},
async get() { return undefined },
async dirs() { return [] },
get() { return undefined },
dirs() { return [] },
},
})
//#when
expect(tool.description).toContain("seeded-skill")
expect(tool.description).toContain("native-visible-skill")
await tool.execute({ name: "native-visible-skill" }, mockContext)
//#then
+64 -15
View File
@@ -11,6 +11,13 @@ import { sanitizeJsonSchema } from "../../plugin/normalize-tool-arg-schemas"
import { discoverCommandsSync } from "../slashcommand/command-discovery"
import type { CommandInfo } from "../slashcommand/types"
import { formatLoadedCommand } from "../slashcommand/command-output-formatter"
type NativeSkillEntry = {
name: string
description: string
location: string
content: string
}
// Priority: project > user > opencode/opencode-project > builtin/config
const scopePriority: Record<string, number> = {
project: 4,
@@ -35,6 +42,46 @@ function loadedSkillToInfo(skill: LoadedSkill): SkillInfo {
}
}
function nativeSkillToLoadedSkill(native: NativeSkillEntry): LoadedSkill {
return {
name: native.name,
path: native.location,
definition: {
name: native.name,
description: native.description,
template: native.content,
},
scope: "config",
}
}
function mergeNativeSkills(skills: LoadedSkill[], nativeSkills: NativeSkillEntry[]): void {
const knownNames = new Set(skills.map(skill => skill.name))
for (const native of nativeSkills) {
if (knownNames.has(native.name)) continue
skills.push(nativeSkillToLoadedSkill(native))
knownNames.add(native.name)
}
}
function mergeNativeSkillInfos(skillInfos: SkillInfo[], nativeSkills: NativeSkillEntry[]): void {
const knownNames = new Set(skillInfos.map(skill => skill.name))
for (const native of nativeSkills) {
if (knownNames.has(native.name)) continue
skillInfos.push({
name: native.name,
description: native.description,
location: native.location,
scope: "config",
})
knownNames.add(native.name)
}
}
function isPromiseLike<T>(value: T | Promise<T>): value is Promise<T> {
return typeof value === "object" && value !== null && "then" in value
}
function formatCombinedDescription(skills: SkillInfo[], commands: CommandInfo[]): string {
const lines: string[] = []
@@ -200,22 +247,9 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition
: [...discovered, ...options.skills.filter(s => !new Set(discovered.map(d => d.name)).has(s.name))]
if (options.nativeSkills) {
const knownNames = new Set(allSkills.map(s => s.name))
try {
const nativeAll = await options.nativeSkills.all()
for (const native of nativeAll) {
if (knownNames.has(native.name)) continue
allSkills.push({
name: native.name,
path: native.location,
definition: {
name: native.name,
description: native.description,
template: native.content,
},
scope: "config",
})
}
mergeNativeSkills(allSkills, nativeAll)
} catch {
// Native skill discovery may not be available
}
@@ -243,8 +277,23 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition
if (options.skills !== undefined) {
const skillInfos = options.skills.map(loadedSkillToInfo)
const commandsForDescription = options.commands ?? []
cachedDescription = formatCombinedDescription(skillInfos, commandsForDescription)
let needsAsyncRefresh = false
if (options.nativeSkills) {
try {
const nativeAll = options.nativeSkills.all()
if (isPromiseLike(nativeAll)) {
needsAsyncRefresh = true
} else {
mergeNativeSkillInfos(skillInfos, nativeAll)
}
} catch {
// Native skill discovery may not be available
}
}
cachedDescription = formatCombinedDescription(skillInfos, commandsForDescription)
if (needsAsyncRefresh) {
void buildDescription()
}
} else if (options.commands !== undefined) {
+3 -3
View File
@@ -41,8 +41,8 @@ export interface SkillLoadOptions {
enabledPluginsOverride?: Record<string, boolean>
/** Native skill accessor from PluginInput for discovering skills registered via config.skills.paths */
nativeSkills?: {
all(): Promise<{ name: string; description: string; location: string; content: string }[]>
get(name: string): Promise<{ name: string; description: string; location: string; content: string } | undefined>
dirs(): Promise<string[]>
all(): { name: string; description: string; location: string; content: string }[] | Promise<{ name: string; description: string; location: string; content: string }[]>
get(name: string): { name: string; description: string; location: string; content: string } | undefined | Promise<{ name: string; description: string; location: string; content: string } | undefined>
dirs(): string[] | Promise<string[]>
}
}