fix(git-master): inject watermark only when enabled instead of overriding defaults

The watermark (commit footer and co-author) was inconsistently applied because:
1. The skill tool didn't receive gitMasterConfig
2. The approach was 'default ON, inject DISABLED override' which LLMs sometimes ignored

This refactors to 'inject only when enabled' approach:
- Remove hardcoded watermark section from base templates
- Dynamically inject section 5.5 based on config values
- Default is still ON (both true when no config)
- When both disabled, no injection occurs (clean prompt)

Also fixes missing config propagation to skill tool and createBuiltinAgents.
This commit is contained in:
Nguyen Khac Trung Kien 2026-01-16 08:01:04 +07:00
parent 837176d947
commit e36385e671
9 changed files with 157 additions and 86 deletions

View File

@ -1,6 +1,6 @@
import type { AgentConfig } from "@opencode-ai/sdk" import type { AgentConfig } from "@opencode-ai/sdk"
import type { BuiltinAgentName, AgentOverrideConfig, AgentOverrides, AgentFactory, AgentPromptMetadata } from "./types" import type { BuiltinAgentName, AgentOverrideConfig, AgentOverrides, AgentFactory, AgentPromptMetadata } from "./types"
import type { CategoriesConfig, CategoryConfig } from "../config/schema" import type { CategoriesConfig, CategoryConfig, GitMasterConfig } from "../config/schema"
import { createSisyphusAgent } from "./sisyphus" import { createSisyphusAgent } from "./sisyphus"
import { createOracleAgent, ORACLE_PROMPT_METADATA } from "./oracle" import { createOracleAgent, ORACLE_PROMPT_METADATA } from "./oracle"
import { createLibrarianAgent, LIBRARIAN_PROMPT_METADATA } from "./librarian" import { createLibrarianAgent, LIBRARIAN_PROMPT_METADATA } from "./librarian"
@ -51,7 +51,8 @@ function isFactory(source: AgentSource): source is AgentFactory {
export function buildAgent( export function buildAgent(
source: AgentSource, source: AgentSource,
model?: string, model?: string,
categories?: CategoriesConfig categories?: CategoriesConfig,
gitMasterConfig?: GitMasterConfig
): AgentConfig { ): AgentConfig {
const base = isFactory(source) ? source(model) : source const base = isFactory(source) ? source(model) : source
const categoryConfigs: Record<string, CategoryConfig> = categories const categoryConfigs: Record<string, CategoryConfig> = categories
@ -75,7 +76,7 @@ export function buildAgent(
} }
if (agentWithCategory.skills?.length) { if (agentWithCategory.skills?.length) {
const { resolved } = resolveMultipleSkills(agentWithCategory.skills) const { resolved } = resolveMultipleSkills(agentWithCategory.skills, { gitMasterConfig })
if (resolved.size > 0) { if (resolved.size > 0) {
const skillContent = Array.from(resolved.values()).join("\n\n") const skillContent = Array.from(resolved.values()).join("\n\n")
base.prompt = skillContent + (base.prompt ? "\n\n" + base.prompt : "") base.prompt = skillContent + (base.prompt ? "\n\n" + base.prompt : "")
@ -130,7 +131,8 @@ export function createBuiltinAgents(
agentOverrides: AgentOverrides = {}, agentOverrides: AgentOverrides = {},
directory?: string, directory?: string,
systemDefaultModel?: string, systemDefaultModel?: string,
categories?: CategoriesConfig categories?: CategoriesConfig,
gitMasterConfig?: GitMasterConfig
): Record<string, AgentConfig> { ): Record<string, AgentConfig> {
const result: Record<string, AgentConfig> = {} const result: Record<string, AgentConfig> = {}
const availableAgents: AvailableAgent[] = [] const availableAgents: AvailableAgent[] = []
@ -149,7 +151,7 @@ export function createBuiltinAgents(
const override = agentOverrides[agentName] const override = agentOverrides[agentName]
const model = override?.model const model = override?.model
let config = buildAgent(source, model, mergedCategories) let config = buildAgent(source, model, mergedCategories, gitMasterConfig)
if (agentName === "librarian" && directory && config.prompt) { if (agentName === "librarian" && directory && config.prompt) {
const envContext = createEnvContext() const envContext = createEnvContext()

View File

@ -529,33 +529,6 @@ IF style == SHORT:
3. Is it similar to examples from git log? 3. Is it similar to examples from git log?
If ANY check fails -> REWRITE message. If ANY check fails -> REWRITE message.
### 5.5 Commit Footer & Co-Author (Configurable)
**Check oh-my-opencode.json for these flags:**
- `git_master.commit_footer` (default: true) - adds footer message
- `git_master.include_co_authored_by` (default: true) - adds co-author trailer
If enabled, add Sisyphus attribution to EVERY commit:
1. **Footer in commit body (if `commit_footer: true`):**
```
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)
```
2. **Co-authored-by trailer (if `include_co_authored_by: true`):**
```
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
```
**Example (both enabled):**
```bash
git commit -m "{Commit Message}" -m "Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)" -m "Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>"
```
**To disable:** Set in oh-my-opencode.json:
```json
{ "git_master": { "commit_footer": false, "include_co_authored_by": false } }
``` ```
</execution> </execution>

View File

@ -622,35 +622,8 @@ IF style == SHORT:
3. Is it similar to examples from git log? 3. Is it similar to examples from git log?
If ANY check fails -> REWRITE message. If ANY check fails -> REWRITE message.
### 5.5 Commit Footer & Co-Author (Configurable)
**Check oh-my-opencode.json for these flags:**
- \`git_master.commit_footer\` (default: true) - adds footer message
- \`git_master.include_co_authored_by\` (default: true) - adds co-author trailer
If enabled, add Sisyphus attribution to EVERY commit:
1. **Footer in commit body (if \`commit_footer: true\`):**
\`\`\` \`\`\`
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) \</execution>
\`\`\`
2. **Co-authored-by trailer (if \`include_co_authored_by: true\`):**
\`\`\`
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
\`\`\`
**Example (both enabled):**
\`\`\`bash
git commit -m "{Commit Message}" -m "Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)" -m "Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>"
\`\`\`
**To disable:** Set in oh-my-opencode.json:
\`\`\`json
{ "git_master": { "commit_footer": false, "include_co_authored_by": false } }
\`\`\`
</execution>
--- ---

View File

@ -160,8 +160,8 @@ describe("resolveMultipleSkillsAsync", () => {
expect(result.resolved.get("playwright")).toContain("Playwright Browser Automation") expect(result.resolved.get("playwright")).toContain("Playwright Browser Automation")
}) })
it("should support git-master config injection", async () => { it("should NOT inject watermark when both options are disabled", async () => {
// #given: git-master skill with config override // #given: git-master skill with watermark disabled
const skillNames = ["git-master"] const skillNames = ["git-master"]
const options = { const options = {
gitMasterConfig: { gitMasterConfig: {
@ -173,12 +173,84 @@ describe("resolveMultipleSkillsAsync", () => {
// #when: resolving with git-master config // #when: resolving with git-master config
const result = await resolveMultipleSkillsAsync(skillNames, options) const result = await resolveMultipleSkillsAsync(skillNames, options)
// #then: config values injected into template // #then: no watermark section injected
expect(result.resolved.size).toBe(1) expect(result.resolved.size).toBe(1)
expect(result.notFound).toEqual([]) expect(result.notFound).toEqual([])
const gitMasterContent = result.resolved.get("git-master") const gitMasterContent = result.resolved.get("git-master")
expect(gitMasterContent).toContain("commit_footer") expect(gitMasterContent).not.toContain("Ultraworked with")
expect(gitMasterContent).toContain("DISABLED") expect(gitMasterContent).not.toContain("Co-authored-by: Sisyphus")
})
it("should inject watermark when enabled (default)", async () => {
// #given: git-master skill with default config (watermark enabled)
const skillNames = ["git-master"]
const options = {
gitMasterConfig: {
commit_footer: true,
include_co_authored_by: true,
},
}
// #when: resolving with git-master config
const result = await resolveMultipleSkillsAsync(skillNames, options)
// #then: watermark section is injected
expect(result.resolved.size).toBe(1)
const gitMasterContent = result.resolved.get("git-master")
expect(gitMasterContent).toContain("Ultraworked with [Sisyphus]")
expect(gitMasterContent).toContain("Co-authored-by: Sisyphus")
})
it("should inject only footer when co-author is disabled", async () => {
// #given: git-master skill with only footer enabled
const skillNames = ["git-master"]
const options = {
gitMasterConfig: {
commit_footer: true,
include_co_authored_by: false,
},
}
// #when: resolving with git-master config
const result = await resolveMultipleSkillsAsync(skillNames, options)
// #then: only footer is injected
const gitMasterContent = result.resolved.get("git-master")
expect(gitMasterContent).toContain("Ultraworked with [Sisyphus]")
expect(gitMasterContent).not.toContain("Co-authored-by: Sisyphus")
})
it("should inject watermark by default when no config provided", async () => {
// #given: git-master skill with NO config (default behavior)
const skillNames = ["git-master"]
// #when: resolving without any gitMasterConfig
const result = await resolveMultipleSkillsAsync(skillNames)
// #then: watermark is injected (default is ON)
expect(result.resolved.size).toBe(1)
const gitMasterContent = result.resolved.get("git-master")
expect(gitMasterContent).toContain("Ultraworked with [Sisyphus]")
expect(gitMasterContent).toContain("Co-authored-by: Sisyphus")
})
it("should inject only co-author when footer is disabled", async () => {
// #given: git-master skill with only co-author enabled
const skillNames = ["git-master"]
const options = {
gitMasterConfig: {
commit_footer: false,
include_co_authored_by: true,
},
}
// #when: resolving with git-master config
const result = await resolveMultipleSkillsAsync(skillNames, options)
// #then: only co-author is injected
const gitMasterContent = result.resolved.get("git-master")
expect(gitMasterContent).not.toContain("Ultraworked with [Sisyphus]")
expect(gitMasterContent).toContain("Co-authored-by: Sisyphus")
}) })
it("should handle empty array", async () => { it("should handle empty array", async () => {

View File

@ -59,22 +59,62 @@ async function extractSkillTemplate(skill: LoadedSkill): Promise<string> {
export { clearSkillCache, getAllSkills, extractSkillTemplate } export { clearSkillCache, getAllSkills, extractSkillTemplate }
function injectGitMasterConfig(template: string, config?: GitMasterConfig): string { export function injectGitMasterConfig(template: string, config?: GitMasterConfig): string {
if (!config) return template const commitFooter = config?.commit_footer ?? true
const includeCoAuthoredBy = config?.include_co_authored_by ?? true
const commitFooter = config.commit_footer ?? true if (!commitFooter && !includeCoAuthoredBy) {
const includeCoAuthoredBy = config.include_co_authored_by ?? true return template
}
const configHeader = `## Git Master Configuration (from oh-my-opencode.json) const sections: string[] = []
**IMPORTANT: These values override the defaults in section 5.5:** sections.push(`### 5.5 Commit Footer & Co-Author`)
- \`commit_footer\`: ${commitFooter} ${!commitFooter ? "(DISABLED - do NOT add footer)" : ""} sections.push(``)
- \`include_co_authored_by\`: ${includeCoAuthoredBy} ${!includeCoAuthoredBy ? "(DISABLED - do NOT add Co-authored-by)" : ""} sections.push(`Add Sisyphus attribution to EVERY commit:`)
sections.push(``)
--- if (commitFooter) {
sections.push(`1. **Footer in commit body:**`)
sections.push("```")
sections.push(`Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)`)
sections.push("```")
sections.push(``)
}
` if (includeCoAuthoredBy) {
return configHeader + template sections.push(`${commitFooter ? "2" : "1"}. **Co-authored-by trailer:**`)
sections.push("```")
sections.push(`Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>`)
sections.push("```")
sections.push(``)
}
if (commitFooter && includeCoAuthoredBy) {
sections.push(`**Example (both enabled):**`)
sections.push("```bash")
sections.push(`git commit -m "{Commit Message}" -m "Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)" -m "Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>"`)
sections.push("```")
} else if (commitFooter) {
sections.push(`**Example:**`)
sections.push("```bash")
sections.push(`git commit -m "{Commit Message}" -m "Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)"`)
sections.push("```")
} else if (includeCoAuthoredBy) {
sections.push(`**Example:**`)
sections.push("```bash")
sections.push(`git commit -m "{Commit Message}" -m "Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>"`)
sections.push("```")
}
const injection = sections.join("\n")
const insertionPoint = template.indexOf("```\n</execution>")
if (insertionPoint !== -1) {
return template.slice(0, insertionPoint) + "```\n\n" + injection + "\n</execution>" + template.slice(insertionPoint + "```\n</execution>".length)
}
return template + "\n\n" + injection
} }
export function resolveSkillContent(skillName: string, options?: SkillResolutionOptions): string | null { export function resolveSkillContent(skillName: string, options?: SkillResolutionOptions): string | null {
@ -82,8 +122,8 @@ export function resolveSkillContent(skillName: string, options?: SkillResolution
const skill = skills.find((s) => s.name === skillName) const skill = skills.find((s) => s.name === skillName)
if (!skill) return null if (!skill) return null
if (skillName === "git-master" && options?.gitMasterConfig) { if (skillName === "git-master") {
return injectGitMasterConfig(skill.template, options.gitMasterConfig) return injectGitMasterConfig(skill.template, options?.gitMasterConfig)
} }
return skill.template return skill.template
@ -102,8 +142,8 @@ export function resolveMultipleSkills(skillNames: string[], options?: SkillResol
for (const name of skillNames) { for (const name of skillNames) {
const template = skillMap.get(name) const template = skillMap.get(name)
if (template) { if (template) {
if (name === "git-master" && options?.gitMasterConfig) { if (name === "git-master") {
resolved.set(name, injectGitMasterConfig(template, options.gitMasterConfig)) resolved.set(name, injectGitMasterConfig(template, options?.gitMasterConfig))
} else { } else {
resolved.set(name, template) resolved.set(name, template)
} }
@ -125,8 +165,8 @@ export async function resolveSkillContentAsync(
const template = await extractSkillTemplate(skill) const template = await extractSkillTemplate(skill)
if (skillName === "git-master" && options?.gitMasterConfig) { if (skillName === "git-master") {
return injectGitMasterConfig(template, options.gitMasterConfig) return injectGitMasterConfig(template, options?.gitMasterConfig)
} }
return template return template
@ -152,8 +192,8 @@ export async function resolveMultipleSkillsAsync(
const skill = skillMap.get(name) const skill = skillMap.get(name)
if (skill) { if (skill) {
const template = await extractSkillTemplate(skill) const template = await extractSkillTemplate(skill)
if (name === "git-master" && options?.gitMasterConfig) { if (name === "git-master") {
resolved.set(name, injectGitMasterConfig(template, options.gitMasterConfig)) resolved.set(name, injectGitMasterConfig(template, options?.gitMasterConfig))
} else { } else {
resolved.set(name, template) resolved.set(name, template)
} }

View File

@ -281,6 +281,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
skills: mergedSkills, skills: mergedSkills,
mcpManager: skillMcpManager, mcpManager: skillMcpManager,
getSessionID: getSessionIDForMcp, getSessionID: getSessionIDForMcp,
gitMasterConfig: pluginConfig.git_master,
}); });
const skillMcpTool = createSkillMcpTool({ const skillMcpTool = createSkillMcpTool({
manager: skillMcpManager, manager: skillMcpManager,

View File

@ -104,7 +104,8 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
pluginConfig.agents, pluginConfig.agents,
ctx.directory, ctx.directory,
config.model as string | undefined, config.model as string | undefined,
pluginConfig.categories pluginConfig.categories,
pluginConfig.git_master
); );
// Claude Code agents: Do NOT apply permission migration // Claude Code agents: Do NOT apply permission migration

View File

@ -4,6 +4,7 @@ import { TOOL_DESCRIPTION_NO_SKILLS, TOOL_DESCRIPTION_PREFIX } from "./constants
import type { SkillArgs, SkillInfo, SkillLoadOptions } from "./types" import type { SkillArgs, SkillInfo, SkillLoadOptions } from "./types"
import type { LoadedSkill } from "../../features/opencode-skill-loader" import type { LoadedSkill } from "../../features/opencode-skill-loader"
import { getAllSkills, extractSkillTemplate } from "../../features/opencode-skill-loader/skill-content" import { getAllSkills, extractSkillTemplate } from "../../features/opencode-skill-loader/skill-content"
import { injectGitMasterConfig } from "../../features/opencode-skill-loader/skill-content"
import type { SkillMcpManager, SkillMcpClientInfo, SkillMcpServerContext } from "../../features/skill-mcp-manager" import type { SkillMcpManager, SkillMcpClientInfo, SkillMcpServerContext } from "../../features/skill-mcp-manager"
import type { Tool, Resource, Prompt } from "@modelcontextprotocol/sdk/types.js" import type { Tool, Resource, Prompt } from "@modelcontextprotocol/sdk/types.js"
@ -164,7 +165,12 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition
throw new Error(`Skill "${args.name}" not found. Available skills: ${available || "none"}`) throw new Error(`Skill "${args.name}" not found. Available skills: ${available || "none"}`)
} }
const body = await extractSkillBody(skill) let body = await extractSkillBody(skill)
if (args.name === "git-master") {
body = injectGitMasterConfig(body, options.gitMasterConfig)
}
const dir = skill.path ? dirname(skill.path) : skill.resolvedPath || process.cwd() const dir = skill.path ? dirname(skill.path) : skill.resolvedPath || process.cwd()
const output = [ const output = [

View File

@ -1,5 +1,6 @@
import type { SkillScope, LoadedSkill } from "../../features/opencode-skill-loader/types" import type { SkillScope, LoadedSkill } from "../../features/opencode-skill-loader/types"
import type { SkillMcpManager } from "../../features/skill-mcp-manager" import type { SkillMcpManager } from "../../features/skill-mcp-manager"
import type { GitMasterConfig } from "../../config/schema"
export interface SkillArgs { export interface SkillArgs {
name: string name: string
@ -25,4 +26,6 @@ export interface SkillLoadOptions {
mcpManager?: SkillMcpManager mcpManager?: SkillMcpManager
/** Session ID getter for MCP client identification */ /** Session ID getter for MCP client identification */
getSessionID?: () => string getSessionID?: () => string
/** Git master configuration for watermark/co-author settings */
gitMasterConfig?: GitMasterConfig
} }