From cb1ac6011a4a9823abe9beb8441d0190e301c2a2 Mon Sep 17 00:00:00 2001 From: walker1211 <13750528578@163.com> Date: Sun, 2 Aug 2026 12:51:59 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(skill-sync):=20=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=E5=B9=B6=E6=8C=89=E8=BF=90=E8=A1=8C=E6=97=B6=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=20imgen=20skill?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- {.claude => .agents}/skills/imgen/SKILL.md | 2 +- .../skills/imgen/evals/evals.json | 0 .../skills/imgen/references/imgen-usage.md | 0 .../imgen/references/prompt-patterns.md | 0 .../imgen/references/troubleshooting.md | 0 .github/workflows/release.yml | 6 +- .gitignore | 9 +- .openclaw/skills/imgen/SKILL.md | 2 +- README.md | 8 +- README.zh-CN.md | 8 +- docs/openclaw-imgen-integration.md | 2 +- docs/openclaw-imgen-integration.zh-CN.md | 2 +- internal/doctor/openclaw.go | 6 +- internal/doctor/openclaw_test.go | 14 +- internal/skillsync/command.go | 29 ++- internal/skillsync/command_test.go | 156 ++++++++++++++- internal/skillsync/sync.go | 85 +++++--- internal/skillsync/sync_test.go | 188 ++++++++++++++---- 18 files changed, 406 insertions(+), 111 deletions(-) rename {.claude => .agents}/skills/imgen/SKILL.md (95%) rename {.claude => .agents}/skills/imgen/evals/evals.json (100%) rename {.claude => .agents}/skills/imgen/references/imgen-usage.md (100%) rename {.claude => .agents}/skills/imgen/references/prompt-patterns.md (100%) rename {.claude => .agents}/skills/imgen/references/troubleshooting.md (100%) diff --git a/.claude/skills/imgen/SKILL.md b/.agents/skills/imgen/SKILL.md similarity index 95% rename from .claude/skills/imgen/SKILL.md rename to .agents/skills/imgen/SKILL.md index cc7d7b5..26381f2 100644 --- a/.claude/skills/imgen/SKILL.md +++ b/.agents/skills/imgen/SKILL.md @@ -1,6 +1,6 @@ --- name: imgen -description: Use this skill whenever the user wants to use codex-imgen, imgen, Codex CLI $imagegen, text-to-image, image-to-image, local asynchronous image generation jobs, image job troubleshooting, or OpenClaw integration for local image generation. This skill helps choose the right imgen command or service flow, ask only necessary questions, keep secrets and config in the right files, and produce safe platform-neutral CLI/service guidance when another agent such as OpenClaw will call imgen. +description: "Default image workflow for this user. Use for ordinary raster image generation or editing, text-to-image, image-to-image, codex-imgen/imgen CLI or service jobs, imgen troubleshooting, and OpenClaw integration. Prefer imgen over the system imagegen skill. Do not use for SVG/vector/code-native assets that are better edited directly." --- # imgen diff --git a/.claude/skills/imgen/evals/evals.json b/.agents/skills/imgen/evals/evals.json similarity index 100% rename from .claude/skills/imgen/evals/evals.json rename to .agents/skills/imgen/evals/evals.json diff --git a/.claude/skills/imgen/references/imgen-usage.md b/.agents/skills/imgen/references/imgen-usage.md similarity index 100% rename from .claude/skills/imgen/references/imgen-usage.md rename to .agents/skills/imgen/references/imgen-usage.md diff --git a/.claude/skills/imgen/references/prompt-patterns.md b/.agents/skills/imgen/references/prompt-patterns.md similarity index 100% rename from .claude/skills/imgen/references/prompt-patterns.md rename to .agents/skills/imgen/references/prompt-patterns.md diff --git a/.claude/skills/imgen/references/troubleshooting.md b/.agents/skills/imgen/references/troubleshooting.md similarity index 100% rename from .claude/skills/imgen/references/troubleshooting.md rename to .agents/skills/imgen/references/troubleshooting.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aa8c597..7b67208 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -79,11 +79,15 @@ jobs: run: | set -euo pipefail package="codex-imgen-${SUFFIX}" - mkdir -p "dist/${package}/configs" + mkdir -p "dist/${package}/configs" "dist/${package}/.agents/skills" "dist/${package}/.openclaw/skills" GOOS="${GOOS}" GOARCH="${GOARCH}" CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o "dist/${package}/imgen${EXT}" ./cmd/imgen GOOS="${GOOS}" GOARCH="${GOARCH}" CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o "dist/${package}/skill-sync${EXT}" ./cmd/skill-sync cp configs/config.example.yaml "dist/${package}/configs/config.example.yaml" + cp -R .agents/skills/imgen "dist/${package}/.agents/skills/" + cp -R .openclaw/skills/imgen "dist/${package}/.openclaw/skills/" cp .example.env README.md README.zh-CN.md LICENSE "dist/${package}/" + test -f "dist/${package}/.agents/skills/imgen/SKILL.md" + test -f "dist/${package}/.openclaw/skills/imgen/SKILL.md" tar -C dist -czf "dist/${package}.tar.gz" "${package}" - name: Upload release archive diff --git a/.gitignore b/.gitignore index 9fef04d..173f371 100644 --- a/.gitignore +++ b/.gitignore @@ -30,10 +30,11 @@ nohup.out # Local/private project assets /.claude/* -!/.claude/skills/ -/.claude/skills/* -!/.claude/skills/imgen/ -!/.claude/skills/imgen/** +/.agents/* +!/.agents/skills/ +/.agents/skills/* +!/.agents/skills/imgen/ +!/.agents/skills/imgen/** /.openclaw/* !/.openclaw/skills/ /.openclaw/skills/* diff --git a/.openclaw/skills/imgen/SKILL.md b/.openclaw/skills/imgen/SKILL.md index cc7d7b5..26381f2 100644 --- a/.openclaw/skills/imgen/SKILL.md +++ b/.openclaw/skills/imgen/SKILL.md @@ -1,6 +1,6 @@ --- name: imgen -description: Use this skill whenever the user wants to use codex-imgen, imgen, Codex CLI $imagegen, text-to-image, image-to-image, local asynchronous image generation jobs, image job troubleshooting, or OpenClaw integration for local image generation. This skill helps choose the right imgen command or service flow, ask only necessary questions, keep secrets and config in the right files, and produce safe platform-neutral CLI/service guidance when another agent such as OpenClaw will call imgen. +description: "Default image workflow for this user. Use for ordinary raster image generation or editing, text-to-image, image-to-image, codex-imgen/imgen CLI or service jobs, imgen troubleshooting, and OpenClaw integration. Prefer imgen over the system imagegen skill. Do not use for SVG/vector/code-native assets that are better edited directly." --- # imgen diff --git a/README.md b/README.md index e209785..0ef0aa5 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ cp .example.env .env On Windows, run `imgen.exe --help`. -Release archives include the `imgen` and `skill-sync` binaries, `configs/config.example.yaml`, `.example.env`, README files, and `LICENSE`. +Release archives include the `imgen` and `skill-sync` binaries, `.agents/skills/imgen`, `.openclaw/skills/imgen`, `configs/config.example.yaml`, `.example.env`, README files, and `LICENSE`. #### Option 2: Build from source @@ -81,7 +81,7 @@ Note: the binary reads `configs/config.yaml` from the current working directory. ## Skill Sync -`.claude/skills/imgen/` is the skill source; `.openclaw/skills/imgen/` is the repository OpenClaw mirror; `~/.claude/skills/imgen/`, `~/.openclaw/workspace/skills/imgen/`, and `~/.codex/skills/imgen/` are local install artifacts. +`.agents/skills/imgen/` is the canonical skill source, and `.openclaw/skills/imgen/` is the repository OpenClaw mirror. `skill-sync` always manages `~/.agents/skills/imgen/`. It manages `~/.openclaw/workspace/skills/imgen/` only when `~/.openclaw/` already exists, and `~/.claude/skills/imgen/` only when `~/.claude/` already exists. Missing optional runtime directories are skipped and are not created. Check whether local installs match the repository sources: @@ -89,7 +89,7 @@ Check whether local installs match the repository sources: go run ./cmd/skill-sync --check ``` -Copy repository sources into local Claude, OpenClaw, and Codex installs, and update the repository OpenClaw mirror: +Copy the repository source into the enabled local installs and update the repository OpenClaw mirror: ```bash go run ./cmd/skill-sync --apply @@ -104,6 +104,8 @@ You can also build first with `bash ./build.sh` and then use the local binary: The default behavior is drift checking only; local skill install directories are overwritten only when `--apply` is passed explicitly. +Use `--agents-dir`, `--openclaw-dir`, or `--claude-dir` to override an install directory. An explicit `--openclaw-dir` or `--claude-dir` enables that runtime sync even when its default root does not exist. The former `--codex-dir` option remains available as a compatibility alias for `--agents-dir`. + ## OpenClaw doctor Check whether the local OpenClaw setup satisfies the imgen / Telegram original-file delivery contract: diff --git a/README.zh-CN.md b/README.zh-CN.md index 9578a44..0c4c7c3 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -57,7 +57,7 @@ cp .example.env .env Windows 下运行 `imgen.exe --help`。 -Release 归档包含 `imgen`、`skill-sync` 二进制,`configs/config.example.yaml`、`.example.env`、README 文件和 `LICENSE`。 +Release 归档包含 `imgen`、`skill-sync` 二进制、`.agents/skills/imgen`、`.openclaw/skills/imgen`、`configs/config.example.yaml`、`.example.env`、README 文件和 `LICENSE`。 #### 方式二:从源码构建 @@ -81,7 +81,7 @@ bash ./build.sh ## Skill 同步 -`.claude/skills/imgen/` 是 skill 源文件;`.openclaw/skills/imgen/` 是仓库内 OpenClaw 镜像;`~/.claude/skills/imgen/`、`~/.openclaw/workspace/skills/imgen/` 与 `~/.codex/skills/imgen/` 是本机安装产物。 +`.agents/skills/imgen/` 是标准 skill 源目录,`.openclaw/skills/imgen/` 是仓库内 OpenClaw 镜像。`skill-sync` 始终管理 `~/.agents/skills/imgen/`。仅当 `~/.openclaw/` 已存在时才管理 `~/.openclaw/workspace/skills/imgen/`,仅当 `~/.claude/` 已存在时才管理 `~/.claude/skills/imgen/`;缺失的可选运行时目录会被跳过,也不会被创建。 检查本机安装是否与仓库源文件一致: @@ -89,7 +89,7 @@ bash ./build.sh go run ./cmd/skill-sync --check ``` -把仓库源文件同步到本机 Claude、OpenClaw 和 Codex,并更新仓库内 OpenClaw 镜像: +把仓库源文件同步到已启用的本机安装目录,并更新仓库内 OpenClaw 镜像: ```bash go run ./cmd/skill-sync --apply @@ -104,6 +104,8 @@ go run ./cmd/skill-sync --apply 默认只检查漂移;只有显式传入 `--apply` 时才会覆盖本机 skill 安装目录。 +可以使用 `--agents-dir`、`--openclaw-dir` 或 `--claude-dir` 覆盖对应安装目录。即使默认根目录不存在,显式传入 `--openclaw-dir` 或 `--claude-dir` 也会启用对应运行时同步。原来的 `--codex-dir` 仍作为 `--agents-dir` 的兼容别名保留。 + ## OpenClaw doctor 检查本机 OpenClaw 是否满足 imgen / Telegram 原图发送集成要求: diff --git a/docs/openclaw-imgen-integration.md b/docs/openclaw-imgen-integration.md index ab58a92..3b55282 100644 --- a/docs/openclaw-imgen-integration.md +++ b/docs/openclaw-imgen-integration.md @@ -36,7 +36,7 @@ Treat generation as successful only when the JSON output has `ok: true` and the ## Install or sync the imgen skill -The canonical skill source in this repository is `.claude/skills/imgen`. The `.openclaw/skills/imgen` tree is a repository mirror, and local runtime installs are populated by `skill-sync`. +The canonical skill source in this repository is `.agents/skills/imgen`. The `.openclaw/skills/imgen` tree is always maintained as a repository mirror. The local OpenClaw install is included only when `~/.openclaw` exists or `--openclaw-dir` is supplied explicitly; Claude follows the same rule with `~/.claude` and `--claude-dir`. ```bash go run ./cmd/skill-sync --check diff --git a/docs/openclaw-imgen-integration.zh-CN.md b/docs/openclaw-imgen-integration.zh-CN.md index 889b0c1..e36024c 100644 --- a/docs/openclaw-imgen-integration.zh-CN.md +++ b/docs/openclaw-imgen-integration.zh-CN.md @@ -36,7 +36,7 @@ bash ./build.sh ## 安装或同步 imgen skill -仓库中的标准 skill 源目录是 `.claude/skills/imgen`。`.openclaw/skills/imgen` 是仓库内 OpenClaw 镜像,本机运行时安装目录由 `skill-sync` 填充。 +仓库中的标准 skill 源目录是 `.agents/skills/imgen`。`.openclaw/skills/imgen` 始终作为仓库内 OpenClaw 镜像维护。只有 `~/.openclaw` 已存在,或显式传入 `--openclaw-dir` 时,才会同步本机 OpenClaw;Claude 以同样逻辑检查 `~/.claude` 和 `--claude-dir`。 ```bash go run ./cmd/skill-sync --check diff --git a/internal/doctor/openclaw.go b/internal/doctor/openclaw.go index c0ea9e3..1082688 100644 --- a/internal/doctor/openclaw.go +++ b/internal/doctor/openclaw.go @@ -254,7 +254,7 @@ func checkOpenClawSkill(path string, report *Report) { } func checkSkillSync(repoRoot string, installedOpenClawSkillPath string, report *Report) { - sourceDir := filepath.Join(repoRoot, ".claude", "skills", "imgen") + sourceDir := filepath.Join(repoRoot, ".agents", "skills", "imgen") repositoryOpenClawDir := filepath.Join(repoRoot, ".openclaw", "skills", "imgen") drift, err := skillsync.CompareSkillTrees(sourceDir, repositoryOpenClawDir, "repository OpenClaw skill mirror") @@ -263,7 +263,7 @@ func checkSkillSync(repoRoot string, installedOpenClawSkillPath string, report * } else if len(drift) > 0 { report.Items = append(report.Items, Item{Level: LevelFail, Message: "repository OpenClaw skill mirror drift: " + strings.Join(drift, "; ")}) } else { - report.Items = append(report.Items, Item{Level: LevelOK, Message: "repository OpenClaw skill mirror matches Claude source"}) + report.Items = append(report.Items, Item{Level: LevelOK, Message: "repository OpenClaw skill mirror matches canonical source"}) } installedOpenClawDir := filepath.Dir(installedOpenClawSkillPath) @@ -273,7 +273,7 @@ func checkSkillSync(repoRoot string, installedOpenClawSkillPath string, report * } else if len(drift) > 0 { report.Items = append(report.Items, Item{Level: LevelFail, Message: "installed OpenClaw imgen skill drift: " + strings.Join(drift, "; ")}) } else { - report.Items = append(report.Items, Item{Level: LevelOK, Message: "installed OpenClaw imgen skill matches Claude source"}) + report.Items = append(report.Items, Item{Level: LevelOK, Message: "installed OpenClaw imgen skill matches canonical source"}) } } diff --git a/internal/doctor/openclaw_test.go b/internal/doctor/openclaw_test.go index ca2f081..d18600a 100644 --- a/internal/doctor/openclaw_test.go +++ b/internal/doctor/openclaw_test.go @@ -31,10 +31,10 @@ func TestOpenClawCheckerReportsOKAndWarn(t *testing.T) { if !reportHas(report, LevelWarn, "active-memory targets main") { t.Fatalf("expected active-memory warning, got:\n%s", report.Render()) } - if !reportHas(report, LevelOK, "repository OpenClaw skill mirror matches Claude source") { + if !reportHas(report, LevelOK, "repository OpenClaw skill mirror matches canonical source") { t.Fatalf("expected repository sync OK, got:\n%s", report.Render()) } - if !reportHas(report, LevelOK, "installed OpenClaw imgen skill matches Claude source") { + if !reportHas(report, LevelOK, "installed OpenClaw imgen skill matches canonical source") { t.Fatalf("expected installed sync OK, got:\n%s", report.Render()) } if !strings.HasPrefix(report.Render(), "OpenClaw doctor\n") { @@ -224,7 +224,7 @@ func TestOpenClawCheckerReportsMessageSendForceDocumentSupport(t *testing.T) { if !reportHas(report, LevelOK, "openclaw CLI message send supports --force-document") { t.Fatalf("expected force-document OK, got:\n%s", report.Render()) } - if !reportHas(report, LevelOK, "installed OpenClaw imgen skill matches Claude source") { + if !reportHas(report, LevelOK, "installed OpenClaw imgen skill matches canonical source") { t.Fatalf("expected installed sync OK, got:\n%s", report.Render()) } } @@ -234,7 +234,7 @@ func TestOpenClawCheckerReportsRepositorySkillMirrorDrift(t *testing.T) { repoRoot := t.TempDir() writeOpenClawConfig(t, home, validOpenClawConfig(`"alsoAllow": ["message"]`)) writeOpenClawSkill(t, home, validOpenClawSkillText()) - writeRepoSkillFile(t, repoRoot, ".claude", "SKILL.md", validOpenClawSkillText()) + writeRepoSkillFile(t, repoRoot, ".agents", "SKILL.md", validOpenClawSkillText()) writeRepoSkillFile(t, repoRoot, ".openclaw", "SKILL.md", "changed") checker := checkerWithOpenClawSupport(home) checker.RepoRoot = repoRoot @@ -253,7 +253,7 @@ func TestOpenClawCheckerReportsInstalledOpenClawSkillDrift(t *testing.T) { repoRoot := t.TempDir() writeOpenClawConfig(t, home, validOpenClawConfig(`"alsoAllow": ["message"]`)) writeOpenClawSkill(t, home, "changed installed skill") - writeRepoSkillFile(t, repoRoot, ".claude", "SKILL.md", validOpenClawSkillText()) + writeRepoSkillFile(t, repoRoot, ".agents", "SKILL.md", validOpenClawSkillText()) writeRepoSkillFile(t, repoRoot, ".openclaw", "SKILL.md", validOpenClawSkillText()) checker := checkerWithOpenClawSupport(home) checker.RepoRoot = repoRoot @@ -302,7 +302,7 @@ func TestOpenClawCheckerWarnsWhenOpenClawCLIMissing(t *testing.T) { if !reportHas(report, LevelWarn, "openclaw CLI not found on PATH") { t.Fatalf("expected CLI warning, got:\n%s", report.Render()) } - if !reportHas(report, LevelOK, "repository OpenClaw skill mirror matches Claude source") { + if !reportHas(report, LevelOK, "repository OpenClaw skill mirror matches canonical source") { t.Fatalf("expected repository sync OK, got:\n%s", report.Render()) } } @@ -350,7 +350,7 @@ func checkerWithSyncedRepo(t *testing.T, home string) OpenClawChecker { func checkerWithRepoMirror(t *testing.T, home string, sourceSkill string) OpenClawChecker { t.Helper() repoRoot := t.TempDir() - writeRepoSkillFile(t, repoRoot, ".claude", "SKILL.md", sourceSkill) + writeRepoSkillFile(t, repoRoot, ".agents", "SKILL.md", sourceSkill) writeRepoSkillFile(t, repoRoot, ".openclaw", "SKILL.md", sourceSkill) checker := checkerWithOpenClawSupport(home) checker.RepoRoot = repoRoot diff --git a/internal/skillsync/command.go b/internal/skillsync/command.go index fb4d993..bedcf5a 100644 --- a/internal/skillsync/command.go +++ b/internal/skillsync/command.go @@ -1,6 +1,7 @@ package skillsync import ( + "errors" "flag" "fmt" "io" @@ -24,15 +25,21 @@ func Run(args []string, ctx CommandContext) int { } fs := flag.NewFlagSet("skill-sync", flag.ContinueOnError) - fs.SetOutput(io.Discard) + fs.SetOutput(stderr) apply := fs.Bool("apply", false, "copy repository skill sources to local installs") check := fs.Bool("check", false, "check whether local installs match repository skill sources") repoRootFlag := fs.String("repo-root", "", "repository root; defaults to nearest codex-imgen root") - claudeDir := fs.String("claude-dir", "", "Claude skill install directory") - openClawDir := fs.String("openclaw-dir", "", "OpenClaw workspace skill install directory") - codexDir := fs.String("codex-dir", "", "Codex skill install directory") + claudeDir := fs.String("claude-dir", "", "Claude skill install directory; enables Claude sync") + openClawDir := fs.String("openclaw-dir", "", "OpenClaw workspace skill install directory; enables OpenClaw sync") + agentsDir := fs.String("agents-dir", "", "Agents skill install directory") + codexDir := fs.String("codex-dir", "", "deprecated alias for --agents-dir") + if len(args) == 1 && args[0] == "help" { + args = []string{"--help"} + } if err := fs.Parse(args); err != nil { - fmt.Fprintln(stderr, err.Error()) + if errors.Is(err, flag.ErrHelp) { + return 0 + } return 2 } if fs.NArg() != 0 { @@ -43,6 +50,10 @@ func Run(args []string, ctx CommandContext) int { fmt.Fprintln(stderr, "choose either --check or --apply") return 2 } + if *agentsDir != "" && *codexDir != "" { + fmt.Fprintln(stderr, "choose either --agents-dir or --codex-dir") + return 2 + } repoRoot := *repoRootFlag if repoRoot == "" { @@ -79,8 +90,12 @@ func Run(args []string, ctx CommandContext) int { if *openClawDir != "" { paths = paths.WithOpenClawInstallDir(*openClawDir) } - if *codexDir != "" { - paths = paths.WithCodexInstallDir(*codexDir) + agentsInstallDir := *agentsDir + if agentsInstallDir == "" { + agentsInstallDir = *codexDir + } + if agentsInstallDir != "" { + paths = paths.WithAgentsInstallDir(agentsInstallDir) } if *apply { diff --git a/internal/skillsync/command_test.go b/internal/skillsync/command_test.go index 2961228..eac4ee6 100644 --- a/internal/skillsync/command_test.go +++ b/internal/skillsync/command_test.go @@ -2,6 +2,7 @@ package skillsync import ( "bytes" + "os" "path/filepath" "strings" "testing" @@ -33,7 +34,7 @@ func TestRunDefaultsToCheck(t *testing.T) { } } -func TestRunApplyCopiesSkills(t *testing.T) { +func TestRunApplyCopiesOnlyAgentsWhenOptionalRuntimesMissing(t *testing.T) { repoRoot := t.TempDir() home := t.TempDir() createSourceSkills(t, repoRoot) @@ -50,24 +51,52 @@ func TestRunApplyCopiesSkills(t *testing.T) { if exitCode != 0 { t.Fatalf("exitCode = %d stdout=%q stderr=%q", exitCode, stdout.String(), stderr.String()) } - assertFile(t, filepath.Join(home, ".claude", "skills", "imgen", "SKILL.md"), "claude skill") - assertFile(t, filepath.Join(home, ".openclaw", "workspace", "skills", "imgen", "SKILL.md"), "claude skill") - assertFile(t, filepath.Join(home, ".codex", "skills", "imgen", "SKILL.md"), "claude skill") + assertFile(t, filepath.Join(home, ".agents", "skills", "imgen", "SKILL.md"), "agents skill") + if _, err := os.Stat(filepath.Join(home, ".claude")); !os.IsNotExist(err) { + t.Fatalf("Claude directory stat error = %v, want not exist", err) + } + if _, err := os.Stat(filepath.Join(home, ".openclaw")); !os.IsNotExist(err) { + t.Fatalf("OpenClaw directory stat error = %v, want not exist", err) + } if !strings.Contains(stdout.String(), "copied") { t.Fatalf("stdout = %q", stdout.String()) } } -func TestRunApplyAcceptsCodexDirOverride(t *testing.T) { +func TestRunApplyCopiesOpenClawWhenOpenClawDirectoryExists(t *testing.T) { + repoRoot := t.TempDir() + home := t.TempDir() + createSourceSkills(t, repoRoot) + if err := os.MkdirAll(filepath.Join(home, ".openclaw"), 0o755); err != nil { + t.Fatalf("MkdirAll returned error: %v", err) + } + var stdout bytes.Buffer + var stderr bytes.Buffer + + exitCode := Run([]string{"--apply", "--repo-root", repoRoot}, CommandContext{ + Stdout: &stdout, + Stderr: &stderr, + Getwd: func() (string, error) { return repoRoot, nil }, + UserHomeDir: func() (string, error) { return home, nil }, + }) + + if exitCode != 0 { + t.Fatalf("exitCode = %d stdout=%q stderr=%q", exitCode, stdout.String(), stderr.String()) + } + assertFile(t, filepath.Join(home, ".openclaw", "workspace", "skills", "imgen", "SKILL.md"), "agents skill") +} + +func TestRunApplyCopiesClaudeWhenClaudeDirectoryExists(t *testing.T) { repoRoot := t.TempDir() home := t.TempDir() - codexParent := filepath.Join(t.TempDir(), "custom-codex", "skills") - codexDir := filepath.Join(codexParent, "imgen") createSourceSkills(t, repoRoot) + if err := os.MkdirAll(filepath.Join(home, ".claude"), 0o755); err != nil { + t.Fatalf("MkdirAll returned error: %v", err) + } var stdout bytes.Buffer var stderr bytes.Buffer - exitCode := Run([]string{"--apply", "--repo-root", repoRoot, "--codex-dir", codexDir}, CommandContext{ + exitCode := Run([]string{"--apply", "--repo-root", repoRoot}, CommandContext{ Stdout: &stdout, Stderr: &stderr, Getwd: func() (string, error) { return repoRoot, nil }, @@ -77,12 +106,81 @@ func TestRunApplyAcceptsCodexDirOverride(t *testing.T) { if exitCode != 0 { t.Fatalf("exitCode = %d stdout=%q stderr=%q", exitCode, stdout.String(), stderr.String()) } - assertFile(t, filepath.Join(codexParent, "imgen", "SKILL.md"), "claude skill") - if !strings.Contains(stdout.String(), codexDir) { + assertFile(t, filepath.Join(home, ".claude", "skills", "imgen", "SKILL.md"), "agents skill") +} + +func TestRunApplyAcceptsAgentsDirOverride(t *testing.T) { + repoRoot := t.TempDir() + home := t.TempDir() + agentsParent := filepath.Join(t.TempDir(), "custom-agents", "skills") + agentsDir := filepath.Join(agentsParent, "imgen") + createSourceSkills(t, repoRoot) + var stdout bytes.Buffer + var stderr bytes.Buffer + + exitCode := Run([]string{"--apply", "--repo-root", repoRoot, "--agents-dir", agentsDir}, CommandContext{ + Stdout: &stdout, + Stderr: &stderr, + Getwd: func() (string, error) { return repoRoot, nil }, + UserHomeDir: func() (string, error) { return home, nil }, + }) + + if exitCode != 0 { + t.Fatalf("exitCode = %d stdout=%q stderr=%q", exitCode, stdout.String(), stderr.String()) + } + assertFile(t, filepath.Join(agentsParent, "imgen", "SKILL.md"), "agents skill") + if !strings.Contains(stdout.String(), agentsDir) { t.Fatalf("stdout = %q", stdout.String()) } } +func TestRunApplyAcceptsOpenClawDirOverride(t *testing.T) { + repoRoot := t.TempDir() + home := t.TempDir() + openClawParent := filepath.Join(t.TempDir(), "custom-openclaw", "skills") + openClawDir := filepath.Join(openClawParent, "imgen") + createSourceSkills(t, repoRoot) + var stdout bytes.Buffer + var stderr bytes.Buffer + + exitCode := Run([]string{"--apply", "--repo-root", repoRoot, "--openclaw-dir", openClawDir}, CommandContext{ + Stdout: &stdout, + Stderr: &stderr, + Getwd: func() (string, error) { return repoRoot, nil }, + UserHomeDir: func() (string, error) { return home, nil }, + }) + + if exitCode != 0 { + t.Fatalf("exitCode = %d stdout=%q stderr=%q", exitCode, stdout.String(), stderr.String()) + } + assertFile(t, filepath.Join(openClawParent, "imgen", "SKILL.md"), "agents skill") + if !strings.Contains(stdout.String(), openClawDir) { + t.Fatalf("stdout = %q", stdout.String()) + } +} + +func TestRunApplyAcceptsCodexDirCompatibilityAlias(t *testing.T) { + repoRoot := t.TempDir() + home := t.TempDir() + agentsParent := filepath.Join(t.TempDir(), "custom-agents", "skills") + agentsDir := filepath.Join(agentsParent, "imgen") + createSourceSkills(t, repoRoot) + var stdout bytes.Buffer + var stderr bytes.Buffer + + exitCode := Run([]string{"--apply", "--repo-root", repoRoot, "--codex-dir", agentsDir}, CommandContext{ + Stdout: &stdout, + Stderr: &stderr, + Getwd: func() (string, error) { return repoRoot, nil }, + UserHomeDir: func() (string, error) { return home, nil }, + }) + + if exitCode != 0 { + t.Fatalf("exitCode = %d stdout=%q stderr=%q", exitCode, stdout.String(), stderr.String()) + } + assertFile(t, filepath.Join(agentsParent, "imgen", "SKILL.md"), "agents skill") +} + func TestRunRejectsApplyAndCheckTogether(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer @@ -101,3 +199,41 @@ func TestRunRejectsApplyAndCheckTogether(t *testing.T) { t.Fatalf("stderr = %q", stderr.String()) } } + +func TestRunRejectsAgentsAndCodexDirTogether(t *testing.T) { + var stdout bytes.Buffer + var stderr bytes.Buffer + + exitCode := Run([]string{"--agents-dir", "/tmp/agents/skills/imgen", "--codex-dir", "/tmp/codex/skills/imgen"}, CommandContext{ + Stdout: &stdout, + Stderr: &stderr, + }) + + if exitCode != 2 { + t.Fatalf("exitCode = %d", exitCode) + } + if !strings.Contains(stderr.String(), "choose either --agents-dir or --codex-dir") { + t.Fatalf("stderr = %q", stderr.String()) + } +} + +func TestRunHelp(t *testing.T) { + for _, args := range [][]string{{"--help"}, {"-h"}, {"help"}} { + t.Run(args[0], func(t *testing.T) { + var stdout bytes.Buffer + var stderr bytes.Buffer + + exitCode := Run(args, CommandContext{ + Stdout: &stdout, + Stderr: &stderr, + }) + + if exitCode != 0 { + t.Fatalf("exitCode = %d", exitCode) + } + if !strings.Contains(stderr.String(), "Usage of skill-sync") { + t.Fatalf("stderr = %q", stderr.String()) + } + }) + } +} diff --git a/internal/skillsync/sync.go b/internal/skillsync/sync.go index cd857b5..ddb5891 100644 --- a/internal/skillsync/sync.go +++ b/internal/skillsync/sync.go @@ -15,11 +15,13 @@ type Paths struct { RepoRoot string ClaudeInstallDir string OpenClawInstallDir string - CodexInstallDir string + AgentsInstallDir string claudeInstallParent string openClawInstallParent string - codexInstallParent string + agentsInstallParent string + syncClaude bool + syncOpenClaw bool } type Pair struct { @@ -35,17 +37,21 @@ type Result struct { } func DefaultPaths(repoRoot string, home string) Paths { - claudeInstallParent := filepath.Join(home, ".claude", "skills") - openClawInstallParent := filepath.Join(home, ".openclaw", "workspace", "skills") - codexInstallParent := filepath.Join(home, ".codex", "skills") + claudeRoot := filepath.Join(home, ".claude") + claudeInstallParent := filepath.Join(claudeRoot, "skills") + openClawRoot := filepath.Join(home, ".openclaw") + openClawInstallParent := filepath.Join(openClawRoot, "workspace", "skills") + agentsInstallParent := filepath.Join(home, ".agents", "skills") return Paths{ RepoRoot: repoRoot, ClaudeInstallDir: filepath.Join(claudeInstallParent, "imgen"), OpenClawInstallDir: filepath.Join(openClawInstallParent, "imgen"), - CodexInstallDir: filepath.Join(codexInstallParent, "imgen"), + AgentsInstallDir: filepath.Join(agentsInstallParent, "imgen"), claudeInstallParent: claudeInstallParent, openClawInstallParent: openClawInstallParent, - codexInstallParent: codexInstallParent, + agentsInstallParent: agentsInstallParent, + syncClaude: dirExists(claudeRoot), + syncOpenClaw: dirExists(openClawRoot), } } @@ -57,54 +63,67 @@ func (p Paths) openClawParent() string { return p.openClawInstallParent } -func (p Paths) codexParent() string { - return p.codexInstallParent +func (p Paths) agentsParent() string { + return p.agentsInstallParent } func (p Paths) WithClaudeInstallDir(path string) Paths { p.ClaudeInstallDir = path p.claudeInstallParent = filepath.Dir(filepath.Clean(path)) + p.syncClaude = true return p } func (p Paths) WithOpenClawInstallDir(path string) Paths { p.OpenClawInstallDir = path p.openClawInstallParent = filepath.Dir(filepath.Clean(path)) + p.syncOpenClaw = true return p } -func (p Paths) WithCodexInstallDir(path string) Paths { - p.CodexInstallDir = path - p.codexInstallParent = filepath.Dir(filepath.Clean(path)) +func (p Paths) WithAgentsInstallDir(path string) Paths { + p.AgentsInstallDir = path + p.agentsInstallParent = filepath.Dir(filepath.Clean(path)) return p } +// WithCodexInstallDir is kept as a compatibility alias for callers that used +// the previous Codex-specific name before personal skills moved to .agents. +func (p Paths) WithCodexInstallDir(path string) Paths { + return p.WithAgentsInstallDir(path) +} + func (p Paths) Pairs() []Pair { - sourceDir := p.claudeSourceDir() - return []Pair{ + sourceDir := p.agentsSourceDir() + pairs := []Pair{ { - Name: "claude", + Name: "agents", SourceDir: sourceDir, - DestinationDir: p.ClaudeInstallDir, - InstallParent: p.claudeParent(), + DestinationDir: p.AgentsInstallDir, + InstallParent: p.agentsParent(), }, - { + } + if p.syncOpenClaw { + pairs = append(pairs, Pair{ Name: "openclaw", SourceDir: sourceDir, DestinationDir: p.OpenClawInstallDir, InstallParent: p.openClawParent(), - }, - { - Name: "codex", + }) + } + if p.syncClaude { + pairs = append(pairs, Pair{ + Name: "claude", SourceDir: sourceDir, - DestinationDir: p.CodexInstallDir, - InstallParent: p.codexParent(), - }, + DestinationDir: p.ClaudeInstallDir, + InstallParent: p.claudeParent(), + }) } + return pairs } -func (p Paths) claudeSourceDir() string { - return filepath.Join(p.RepoRoot, ".claude", "skills", "imgen") +func (p Paths) agentsSourceDir() string { + return filepath.Join(p.RepoRoot, ".agents", "skills", "imgen") } func (p Paths) openClawRepositoryDir() string { @@ -113,9 +132,9 @@ func (p Paths) openClawRepositoryDir() string { func (p Paths) Check() (Result, error) { var result Result - sourceDir := p.claudeSourceDir() + sourceDir := p.agentsSourceDir() if err := validateSource(sourceDir); err != nil { - return Result{}, fmt.Errorf("claude source invalid: %w", err) + return Result{}, fmt.Errorf("agents source invalid: %w", err) } pairs := p.Pairs() for _, pair := range pairs { @@ -136,9 +155,9 @@ func (p Paths) Check() (Result, error) { func (p Paths) Apply() (Result, error) { var result Result - sourceDir := p.claudeSourceDir() + sourceDir := p.agentsSourceDir() if err := validateSource(sourceDir); err != nil { - return Result{}, fmt.Errorf("claude source invalid: %w", err) + return Result{}, fmt.Errorf("agents source invalid: %w", err) } pairs := p.Pairs() for _, pair := range pairs { @@ -172,7 +191,11 @@ func FindRepositoryRoot(cwd string) (string, error) { return "", err } for { - if fileExists(filepath.Join(dir, "go.mod")) && dirExists(filepath.Join(dir, ".claude", "skills", "imgen")) { + hasSkillSource := dirExists(filepath.Join(dir, ".agents", "skills", "imgen")) + hasProjectMarker := fileExists(filepath.Join(dir, "go.mod")) || + fileExists(filepath.Join(dir, "configs", "config.example.yaml")) || + fileExists(filepath.Join(dir, "configs", "config.yaml")) + if hasSkillSource && hasProjectMarker { return dir, nil } parent := filepath.Dir(dir) diff --git a/internal/skillsync/sync_test.go b/internal/skillsync/sync_test.go index 3756562..58ca5ec 100644 --- a/internal/skillsync/sync_test.go +++ b/internal/skillsync/sync_test.go @@ -7,7 +7,7 @@ import ( "testing" ) -func TestCheckReportsMissingInstallTargets(t *testing.T) { +func TestCheckReportsOnlyAgentsWhenOptionalRuntimesMissing(t *testing.T) { repoRoot := t.TempDir() home := t.TempDir() createSourceSkills(t, repoRoot) @@ -16,17 +16,63 @@ func TestCheckReportsMissingInstallTargets(t *testing.T) { if err != nil { t.Fatalf("Check returned error: %v", err) } - if len(result.Drift) != 3 { + if len(result.Drift) != 1 { + t.Fatalf("drift = %v", result.Drift) + } + if containsLine(result.Drift, "claude") { + t.Fatalf("expected Claude to be skipped, got %v", result.Drift) + } + if containsLine(result.Drift, "openclaw") { + t.Fatalf("expected OpenClaw to be skipped, got %v", result.Drift) + } + if !containsLine(result.Drift, "agents install missing") { + t.Fatalf("expected missing Agents install, got %v", result.Drift) + } +} + +func TestCheckReportsMissingClaudeWhenClaudeDirectoryExists(t *testing.T) { + repoRoot := t.TempDir() + home := t.TempDir() + createSourceSkills(t, repoRoot) + if err := os.MkdirAll(filepath.Join(home, ".claude"), 0o755); err != nil { + t.Fatalf("MkdirAll returned error: %v", err) + } + + result, err := DefaultPaths(repoRoot, home).Check() + if err != nil { + t.Fatalf("Check returned error: %v", err) + } + if len(result.Drift) != 2 { t.Fatalf("drift = %v", result.Drift) } if !containsLine(result.Drift, "claude install missing") { t.Fatalf("expected missing Claude install, got %v", result.Drift) } + if containsLine(result.Drift, "openclaw") { + t.Fatalf("expected OpenClaw to be skipped, got %v", result.Drift) + } +} + +func TestCheckReportsMissingOpenClawWhenOpenClawDirectoryExists(t *testing.T) { + repoRoot := t.TempDir() + home := t.TempDir() + createSourceSkills(t, repoRoot) + if err := os.MkdirAll(filepath.Join(home, ".openclaw"), 0o755); err != nil { + t.Fatalf("MkdirAll returned error: %v", err) + } + + result, err := DefaultPaths(repoRoot, home).Check() + if err != nil { + t.Fatalf("Check returned error: %v", err) + } + if len(result.Drift) != 2 { + t.Fatalf("drift = %v", result.Drift) + } if !containsLine(result.Drift, "openclaw install missing") { t.Fatalf("expected missing OpenClaw install, got %v", result.Drift) } - if !containsLine(result.Drift, "codex install missing") { - t.Fatalf("expected missing Codex install, got %v", result.Drift) + if containsLine(result.Drift, "claude") { + t.Fatalf("expected Claude to be skipped, got %v", result.Drift) } } @@ -34,9 +80,15 @@ func TestApplyCopiesSourcesAndRemovesStaleFiles(t *testing.T) { repoRoot := t.TempDir() home := t.TempDir() createSourceSkills(t, repoRoot) + if err := os.MkdirAll(filepath.Join(home, ".claude"), 0o755); err != nil { + t.Fatalf("MkdirAll returned error: %v", err) + } + if err := os.MkdirAll(filepath.Join(home, ".openclaw"), 0o755); err != nil { + t.Fatalf("MkdirAll returned error: %v", err) + } paths := DefaultPaths(repoRoot, home) writeFile(t, filepath.Join(paths.ClaudeInstallDir, "stale.md"), "old") - writeFile(t, filepath.Join(paths.CodexInstallDir, "stale.md"), "old") + writeFile(t, filepath.Join(paths.AgentsInstallDir, "stale.md"), "old") result, err := paths.Apply() if err != nil { @@ -45,14 +97,14 @@ func TestApplyCopiesSourcesAndRemovesStaleFiles(t *testing.T) { if len(result.Applied) != 4 { t.Fatalf("applied = %v", result.Applied) } - assertFile(t, filepath.Join(paths.ClaudeInstallDir, "SKILL.md"), "claude skill") - assertFile(t, filepath.Join(paths.OpenClawInstallDir, "SKILL.md"), "claude skill") - assertFile(t, filepath.Join(paths.CodexInstallDir, "SKILL.md"), "claude skill") + assertFile(t, filepath.Join(paths.ClaudeInstallDir, "SKILL.md"), "agents skill") + assertFile(t, filepath.Join(paths.OpenClawInstallDir, "SKILL.md"), "agents skill") + assertFile(t, filepath.Join(paths.AgentsInstallDir, "SKILL.md"), "agents skill") if _, err := os.Stat(filepath.Join(paths.ClaudeInstallDir, "stale.md")); !os.IsNotExist(err) { t.Fatalf("expected stale Claude install file to be removed, stat error = %v", err) } - if _, err := os.Stat(filepath.Join(paths.CodexInstallDir, "stale.md")); !os.IsNotExist(err) { - t.Fatalf("expected stale Codex install file to be removed, stat error = %v", err) + if _, err := os.Stat(filepath.Join(paths.AgentsInstallDir, "stale.md")); !os.IsNotExist(err) { + t.Fatalf("expected stale Agents install file to be removed, stat error = %v", err) } check, err := paths.Check() @@ -64,11 +116,36 @@ func TestApplyCopiesSourcesAndRemovesStaleFiles(t *testing.T) { } } -func TestApplySyncsRepositoryOpenClawFromClaudeSource(t *testing.T) { +func TestApplySkipsOptionalRuntimesWhenDirectoriesMissing(t *testing.T) { repoRoot := t.TempDir() home := t.TempDir() createSourceSkills(t, repoRoot) paths := DefaultPaths(repoRoot, home) + + result, err := paths.Apply() + if err != nil { + t.Fatalf("Apply returned error: %v", err) + } + if len(result.Applied) != 2 { + t.Fatalf("applied = %v", result.Applied) + } + if _, err := os.Stat(filepath.Join(home, ".claude")); !os.IsNotExist(err) { + t.Fatalf("Claude directory stat error = %v, want not exist", err) + } + if _, err := os.Stat(filepath.Join(home, ".openclaw")); !os.IsNotExist(err) { + t.Fatalf("OpenClaw directory stat error = %v, want not exist", err) + } + assertFile(t, filepath.Join(paths.AgentsInstallDir, "SKILL.md"), "agents skill") +} + +func TestApplySyncsRepositoryOpenClawFromAgentsSource(t *testing.T) { + repoRoot := t.TempDir() + home := t.TempDir() + createSourceSkills(t, repoRoot) + if err := os.MkdirAll(filepath.Join(home, ".openclaw"), 0o755); err != nil { + t.Fatalf("MkdirAll returned error: %v", err) + } + paths := DefaultPaths(repoRoot, home) writeFile(t, filepath.Join(repoRoot, ".openclaw", "skills", "imgen", "SKILL.md"), "stale openclaw skill") result, err := paths.Apply() @@ -78,8 +155,8 @@ func TestApplySyncsRepositoryOpenClawFromClaudeSource(t *testing.T) { if !containsLine(result.Applied, filepath.Join(repoRoot, ".openclaw", "skills", "imgen")) { t.Fatalf("expected repository openclaw skill to be applied, got %v", result.Applied) } - assertFile(t, filepath.Join(repoRoot, ".openclaw", "skills", "imgen", "SKILL.md"), "claude skill") - assertFile(t, filepath.Join(paths.OpenClawInstallDir, "SKILL.md"), "claude skill") + assertFile(t, filepath.Join(repoRoot, ".openclaw", "skills", "imgen", "SKILL.md"), "agents skill") + assertFile(t, filepath.Join(paths.OpenClawInstallDir, "SKILL.md"), "agents skill") } func TestCheckReportsChangedTargetFile(t *testing.T) { @@ -90,13 +167,13 @@ func TestCheckReportsChangedTargetFile(t *testing.T) { if _, err := paths.Apply(); err != nil { t.Fatalf("Apply returned error: %v", err) } - writeFile(t, filepath.Join(paths.ClaudeInstallDir, "SKILL.md"), "changed") + writeFile(t, filepath.Join(paths.AgentsInstallDir, "SKILL.md"), "changed") result, err := paths.Check() if err != nil { t.Fatalf("Check returned error: %v", err) } - if !containsLine(result.Drift, "claude file differs: SKILL.md") { + if !containsLine(result.Drift, "agents file differs: SKILL.md") { t.Fatalf("expected changed file drift, got %v", result.Drift) } } @@ -219,19 +296,19 @@ func TestCheckReportsMissingAndExtraInstallFiles(t *testing.T) { if _, err := paths.Apply(); err != nil { t.Fatalf("Apply returned error: %v", err) } - if err := os.Remove(filepath.Join(paths.ClaudeInstallDir, "references", "imgen-usage.md")); err != nil { + if err := os.Remove(filepath.Join(paths.AgentsInstallDir, "references", "imgen-usage.md")); err != nil { t.Fatalf("Remove returned error: %v", err) } - writeFile(t, filepath.Join(paths.ClaudeInstallDir, "extra.md"), "extra") + writeFile(t, filepath.Join(paths.AgentsInstallDir, "extra.md"), "extra") result, err := paths.Check() if err != nil { t.Fatalf("Check returned error: %v", err) } - if !containsLine(result.Drift, "claude file missing: references/imgen-usage.md") { + if !containsLine(result.Drift, "agents file missing: references/imgen-usage.md") { t.Fatalf("expected missing file drift, got %v", result.Drift) } - if !containsLine(result.Drift, "claude extra install file: extra.md") { + if !containsLine(result.Drift, "agents extra install file: extra.md") { t.Fatalf("expected extra file drift, got %v", result.Drift) } } @@ -240,6 +317,9 @@ func TestApplyRejectsDestinationOutsideExpectedSkillParent(t *testing.T) { repoRoot := t.TempDir() home := t.TempDir() createSourceSkills(t, repoRoot) + if err := os.MkdirAll(filepath.Join(home, ".claude"), 0o755); err != nil { + t.Fatalf("MkdirAll returned error: %v", err) + } outside := t.TempDir() paths := DefaultPaths(repoRoot, home) paths.ClaudeInstallDir = filepath.Join(outside, "skills", "imgen") @@ -255,23 +335,23 @@ func TestApplyRejectsDestinationOutsideExpectedSkillParent(t *testing.T) { assertFile(t, filepath.Join(paths.ClaudeInstallDir, "keep.md"), "keep") } -func TestApplyRejectsCodexDestinationOutsideExpectedSkillParent(t *testing.T) { +func TestApplyRejectsAgentsDestinationOutsideExpectedSkillParent(t *testing.T) { repoRoot := t.TempDir() home := t.TempDir() createSourceSkills(t, repoRoot) outside := t.TempDir() paths := DefaultPaths(repoRoot, home) - paths.CodexInstallDir = filepath.Join(outside, "skills", "imgen") - writeFile(t, filepath.Join(paths.CodexInstallDir, "keep.md"), "keep") + paths.AgentsInstallDir = filepath.Join(outside, "skills", "imgen") + writeFile(t, filepath.Join(paths.AgentsInstallDir, "keep.md"), "keep") _, err := paths.Apply() if err == nil { - t.Fatal("expected Apply to reject Codex destination outside expected skill parent") + t.Fatal("expected Apply to reject Agents destination outside expected skill parent") } if !strings.Contains(err.Error(), "outside expected install parent") { t.Fatalf("error = %v", err) } - assertFile(t, filepath.Join(paths.CodexInstallDir, "keep.md"), "keep") + assertFile(t, filepath.Join(paths.AgentsInstallDir, "keep.md"), "keep") } func TestApplyRejectsRelativeDestination(t *testing.T) { @@ -279,7 +359,7 @@ func TestApplyRejectsRelativeDestination(t *testing.T) { home := t.TempDir() createSourceSkills(t, repoRoot) paths := DefaultPaths(repoRoot, home) - paths.ClaudeInstallDir = filepath.Join("relative", "skills", "imgen") + paths.AgentsInstallDir = filepath.Join("relative", "skills", "imgen") _, err := paths.Apply() if err == nil { @@ -296,11 +376,11 @@ func TestApplyAcceptsExplicitInstallDirOverride(t *testing.T) { createSourceSkills(t, repoRoot) claudeParent := filepath.Join(t.TempDir(), "custom-claude", "skills") openClawParent := filepath.Join(t.TempDir(), "custom-openclaw", "skills") - codexParent := filepath.Join(t.TempDir(), "custom-codex", "skills") + agentsParent := filepath.Join(t.TempDir(), "custom-agents", "skills") paths := DefaultPaths(repoRoot, home). WithClaudeInstallDir(filepath.Join(claudeParent, "imgen")). WithOpenClawInstallDir(filepath.Join(openClawParent, "imgen")). - WithCodexInstallDir(filepath.Join(codexParent, "imgen")) + WithAgentsInstallDir(filepath.Join(agentsParent, "imgen")) result, err := paths.Apply() if err != nil { @@ -309,9 +389,9 @@ func TestApplyAcceptsExplicitInstallDirOverride(t *testing.T) { if len(result.Applied) != 4 { t.Fatalf("applied = %v", result.Applied) } - assertFile(t, filepath.Join(claudeParent, "imgen", "SKILL.md"), "claude skill") - assertFile(t, filepath.Join(openClawParent, "imgen", "SKILL.md"), "claude skill") - assertFile(t, filepath.Join(codexParent, "imgen", "SKILL.md"), "claude skill") + assertFile(t, filepath.Join(claudeParent, "imgen", "SKILL.md"), "agents skill") + assertFile(t, filepath.Join(openClawParent, "imgen", "SKILL.md"), "agents skill") + assertFile(t, filepath.Join(agentsParent, "imgen", "SKILL.md"), "agents skill") } func TestCheckReportsDestinationSymlinkAsDrift(t *testing.T) { @@ -322,12 +402,12 @@ func TestCheckReportsDestinationSymlinkAsDrift(t *testing.T) { if _, err := paths.Apply(); err != nil { t.Fatalf("Apply returned error: %v", err) } - if err := os.Remove(filepath.Join(paths.ClaudeInstallDir, "SKILL.md")); err != nil { + if err := os.Remove(filepath.Join(paths.AgentsInstallDir, "SKILL.md")); err != nil { t.Fatalf("Remove returned error: %v", err) } outside := filepath.Join(t.TempDir(), "outside.md") writeFile(t, outside, "outside") - if err := os.Symlink(outside, filepath.Join(paths.ClaudeInstallDir, "SKILL.md")); err != nil { + if err := os.Symlink(outside, filepath.Join(paths.AgentsInstallDir, "SKILL.md")); err != nil { t.Fatalf("Symlink returned error: %v", err) } @@ -335,7 +415,7 @@ func TestCheckReportsDestinationSymlinkAsDrift(t *testing.T) { if err != nil { t.Fatalf("Check returned error: %v", err) } - if !containsLine(result.Drift, "claude file is symlink: SKILL.md") { + if !containsLine(result.Drift, "agents file is symlink: SKILL.md") { t.Fatalf("expected destination symlink drift, got %v", result.Drift) } } @@ -346,7 +426,7 @@ func TestApplyRejectsSourceSymlink(t *testing.T) { createSourceSkills(t, repoRoot) outside := filepath.Join(t.TempDir(), "secret.txt") writeFile(t, outside, "secret") - link := filepath.Join(repoRoot, ".claude", "skills", "imgen", "linked-secret.txt") + link := filepath.Join(repoRoot, ".agents", "skills", "imgen", "linked-secret.txt") if err := os.Symlink(outside, link); err != nil { t.Fatalf("Symlink returned error: %v", err) } @@ -378,12 +458,44 @@ func TestFindRepositoryRoot(t *testing.T) { } } +func TestFindRepositoryRootFromReleaseLayout(t *testing.T) { + for _, configName := range []string{"config.example.yaml", "config.yaml"} { + t.Run(configName, func(t *testing.T) { + repoRoot := t.TempDir() + createSourceSkills(t, repoRoot) + writeFile(t, filepath.Join(repoRoot, "configs", configName), "backend: {}\n") + nested := filepath.Join(repoRoot, "configs") + + found, err := FindRepositoryRoot(nested) + if err != nil { + t.Fatalf("FindRepositoryRoot returned error: %v", err) + } + if found != repoRoot { + t.Fatalf("found = %q, want %q", found, repoRoot) + } + }) + } +} + +func TestFindRepositoryRootRejectsPersonalAgentsInstall(t *testing.T) { + home := t.TempDir() + writeFile(t, filepath.Join(home, ".agents", "skills", "imgen", "SKILL.md"), "agents skill") + nested := filepath.Join(home, "Projects", "example") + if err := os.MkdirAll(nested, 0o755); err != nil { + t.Fatalf("MkdirAll returned error: %v", err) + } + + if _, err := FindRepositoryRoot(nested); err == nil { + t.Fatal("expected personal Agents install without a project marker to be rejected") + } +} + func createSourceSkills(t *testing.T, repoRoot string) { t.Helper() - writeFile(t, filepath.Join(repoRoot, ".claude", "skills", "imgen", "SKILL.md"), "claude skill") - writeFile(t, filepath.Join(repoRoot, ".claude", "skills", "imgen", "references", "imgen-usage.md"), "usage") - writeFile(t, filepath.Join(repoRoot, ".claude", "skills", "imgen", "evals", "evals.json"), "{}") - writeFile(t, filepath.Join(repoRoot, ".openclaw", "skills", "imgen", "SKILL.md"), "claude skill") + writeFile(t, filepath.Join(repoRoot, ".agents", "skills", "imgen", "SKILL.md"), "agents skill") + writeFile(t, filepath.Join(repoRoot, ".agents", "skills", "imgen", "references", "imgen-usage.md"), "usage") + writeFile(t, filepath.Join(repoRoot, ".agents", "skills", "imgen", "evals", "evals.json"), "{}") + writeFile(t, filepath.Join(repoRoot, ".openclaw", "skills", "imgen", "SKILL.md"), "agents skill") writeFile(t, filepath.Join(repoRoot, ".openclaw", "skills", "imgen", "references", "imgen-usage.md"), "usage") writeFile(t, filepath.Join(repoRoot, ".openclaw", "skills", "imgen", "evals", "evals.json"), "{}") } From bd4c8311b94cbd84ce9c1dfca5ee29930625fc4e Mon Sep 17 00:00:00 2001 From: walker1211 <13750528578@163.com> Date: Sun, 2 Aug 2026 12:56:19 +0800 Subject: [PATCH 2/4] =?UTF-8?q?refactor(skill):=20=E7=B2=BE=E7=AE=80=20img?= =?UTF-8?q?en=20=E8=A7=84=E5=88=99=E4=B8=8E=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/skills/imgen/SKILL.md | 95 ++++--------------- .agents/skills/imgen/evals/evals.json | 6 ++ .../skills/imgen/references/imgen-usage.md | 58 ++++------- .openclaw/skills/imgen/SKILL.md | 95 ++++--------------- .openclaw/skills/imgen/evals/evals.json | 6 ++ .../skills/imgen/references/imgen-usage.md | 58 ++++------- 6 files changed, 86 insertions(+), 232 deletions(-) diff --git a/.agents/skills/imgen/SKILL.md b/.agents/skills/imgen/SKILL.md index 26381f2..19a12ad 100644 --- a/.agents/skills/imgen/SKILL.md +++ b/.agents/skills/imgen/SKILL.md @@ -5,111 +5,54 @@ description: "Default image workflow for this user. Use for ordinary raster imag # imgen -Use this skill to help users operate the `codex-imgen` project through its synchronous `imgen` CLI, local service jobs, and realtime WebSocket mode. +Use this skill to operate the `codex-imgen` CLI, service jobs, realtime mode, and OpenClaw integration. -## First decide the scenario +## Route the request -Classify the user's request into one of these scenarios: +Read only the references needed for the request: -1. **Synchronous text-to-image** — user has a prompt and wants image paths directly. -2. **Synchronous image-to-image** — user supplies one or more local reference image paths and wants image paths directly. -3. **Asynchronous service job** — user wants `serve`, `submit`, `status`, `get`, `list`, `cancel`, polling, recovery, or server-side job management. -4. **Realtime WebSocket** — user explicitly wants streaming per-item generation events over `/v1/realtime/generate/ws`. -5. **Troubleshooting** — user reports slowness, failed jobs, retries, missing images, or confusing output. -6. **Configuration** — user asks about `configs/config.yaml`, `.env`, backend, scheduler, storage, or email settings. -7. **OpenClaw or another agent** — user wants platform-neutral instructions for another agent or tool to call imgen. +- `references/imgen-usage.md`: commands, configuration, backend behavior, service/realtime selection, and the OpenClaw delivery contract. +- `references/prompt-patterns.md`: prompt wording for text-to-image or image-to-image requests. +- `references/troubleshooting.md`: failed, slow, retried, stuck, or missing-image runs. -Read only the reference file that matches the scenario: - -- Usage and OpenClaw calling contract: `references/imgen-usage.md` -- Prompt wording patterns: `references/prompt-patterns.md` -- Job and generation troubleshooting: `references/troubleshooting.md` +Combine references only when the request spans those concerns. ## Ask only necessary questions If the user gave enough information, provide the command or steps directly. -Ask one question when a required value is missing: +Ask one question only when a required value is missing: - Missing image prompt for generation. - Missing local image path for image-to-image. - Missing job id for `status`, `get`, `cancel`, or job-specific troubleshooting. -- Missing decision between synchronous CLI, service job, or realtime WebSocket only when the user's goal requires choosing one. +- Missing choice between synchronous CLI, service job, or realtime only when the goal does not determine it. Do not ask for preferences that are not needed to produce a safe minimal command. ## Command rules -Use the shortest command that satisfies the request. Default one-shot generation, including normal OpenClaw image generation, to synchronous `./imgen --json ...`; do not require `./imgen serve` unless the user needs job management or realtime streaming. - -Before giving local execution instructions for OpenClaw or another agent, do not assume `` is known. Tell the caller to discover a config cwd in this order: `IMGEN_REPO_ROOT`, upward search for `configs/config.yaml` plus `./imgen`/`build.sh`/`go.mod`, explicit user install paths, then `command -v imgen` paired with a discovered config cwd. If no executable or config cwd is found, return a clear "imgen is not installed or not discoverable" error instead of running from an arbitrary cwd. - -For OpenClaw or another local agent, resolve a repo/config root before running image requests. Check explicit common checkout paths such as `$HOME/Projects/codex-imgen` and `$HOME/codex-imgen` before falling back to `command -v imgen`; a PATH executable still needs a discovered config cwd. When a checkout root is found, run from that cwd. For normal text-to-image requests, use the synchronous CLI and run `./imgen --json --count N --concurrency 1 ""`; this does not require `./imgen serve`. For OpenClaw Telegram delivery, set `IMGEN_DELIVERY_DIR="${OPENCLAW_STATE_DIR:-$HOME/.openclaw}/workspace/imgen"` on each synchronous imgen process so returned `images[].path` values are copied under OpenClaw's allowed local media roots. For image-to-image requests, add repeated `--image ` flags before the prompt. Use `submit` only when the caller needs server-side job management, polling, cancellation, or recovery. Use `/v1/realtime/generate/ws` only when `imgen serve` is running and the caller needs live streaming events for a WebSocket session. For Telegram multi-image requests where distinct themes are useful, launch N independent `./imgen --json --count 1 --concurrency 1 ""` commands concurrently when the execution tool supports background sessions, each with the same `IMGEN_DELIVERY_DIR`, then poll all sessions and deliver each successful `images[].path` immediately as it completes. Do not serialize these independent theme generations unless the tool cannot run them concurrently. Do not wait for all requested images before sending earlier successes. Do not call OpenClaw's built-in `image_generate` tool. Do not fall back to direct `codex exec --json -- '$imagegen ...'`. Do not reuse old generated image paths unless the user explicitly asks for existing files. - -For multiple candidates, use `--count N` and `--concurrency M` to control quantity. Keep the prompt phrased as a single-image request such as `生成 1 张...` or `单图`; do not also ask for `生成 N 张` or `输出 N 张不同构图` inside the prompt. - -Common commands: - -```bash -./imgen "生成一张 Q 版小龙吉祥物,白底,单图" -./imgen --count 4 --concurrency 2 "五更琉璃,穿着女仆装在咖啡馆" -./imgen --image ./1.png "保留主体构图和姿态,把这张图改成高质量 3D 手办渲染风格,背景更干净,单图" -./imgen --image ./1.png --image ./2.png "以第一张为主体,采用第二张的风格和材质表现,输出单图" -./imgen serve -./imgen submit --count 4 --concurrency 2 "五更琉璃,穿着女仆装在咖啡馆" -./imgen status -./imgen get -./imgen list -./imgen cancel -``` - -Prefer `./imgen --json ...` as the stable verification and OpenClaw contract. Direct native Codex CLI usage is only a diagnostic path for humans who explicitly ask for it; it is not an OpenClaw fallback. For OpenClaw image requests, do not call `codex exec --json -- '$imagegen ...'` directly; use the discovered repo/config root with `./imgen --json ...` instead. - -If the local tool is `ccs codex` and `ccs codex exec --json` fails, do not reuse native Codex commands. Current `imgen` expects `backend.command` to accept `exec --json`; a plain `ccs codex '$imagegen ...'` exit code 0 without image paths is not a successful imgen-compatible result. +- Use the shortest command that satisfies the request. +- Default one-shot generation, including normal OpenClaw requests, to synchronous `./imgen --json ...` from a discovered config working directory. +- Use repeated `--image ` flags for references; URLs are not supported. +- Use `--count N` and `--concurrency M` for quantity while keeping the prompt about one image. +- Use `submit` only for job management, polling, cancellation, or recovery. Use realtime only for live WebSocket events while `imgen serve` is running. +- Treat success as returned image paths: text path lines, or `ok: true` with non-empty `images[].path`. Exit code 0 alone is insufficient. +- For OpenClaw, follow the full contract in `references/imgen-usage.md`; never substitute `image_generate`, direct `codex exec`, or stale paths. ## Safety and configuration rules - Treat `.env` as the only place for sensitive values such as `EMAIL_SMTP_AUTH_CODE`. - Treat `configs/config.yaml` as the local structured configuration file. - Do not suggest committing `configs/config.yaml`, `.env`, generated images, logs, or `.data/imgen.db`. -- Do not assume URL image input is supported; current image input is local file paths only. - Do not suggest `0.0.0.0` service binding unless the user explicitly asks for LAN access and understands the exposure. - Do not bypass Codex CLI login, permissions, or `$imagegen` availability checks. -- Do not claim generation succeeded from exit code alone; require text path lines or JSON `images[].path` values. -- Do not invent the image model; inspect `configs/config.yaml` `backend.model` when available, otherwise say it depends on the configured backend default. +- Inspect `backend.model` and `backend.reasoning_effort` before describing the active Codex agent configuration. The skill itself does not choose either value. +- Do not call `backend.model` the image-generation model; it selects the Codex agent that invokes `$imagegen`. ## Troubleshooting flow -For slow or failed service jobs, follow this order: - -1. Check the job summary with `./imgen status ` and `./imgen get `. -2. Check image attempts in SQLite. -3. Check phase timings in SQLite. -4. Check `logs/out.log` when the service was started by repository scripts. -5. Verify with `./imgen --json`; only verify the underlying Codex command directly when it supports `exec --json`. - -Use `references/troubleshooting.md` for exact SQL and phase interpretation. - -## OpenClaw guidance - -When the user wants OpenClaw support, produce platform-neutral instructions instead of Claude-specific tool steps. - -Prefer one of these outputs: - -- A synchronous CLI contract for normal image generation. It should say how to discover repo/config root, which `./imgen --json` command OpenClaw should run, what arguments it should supply, what output shape to expect, and how to report missing installation/configuration. This is the default OpenClaw contract and does not require `./imgen serve`. -- A local service job contract only when the caller needs server-side job management, polling, cancellation, or recovery. It should say to start `./imgen serve`, submit a job, poll or subscribe for completion, then read result paths. -- A realtime WebSocket contract only when the caller needs live streaming events. It should say to start `./imgen serve`, connect to `/v1/realtime/generate/ws`, send one `generate.start` frame, consume streamed events, and read image paths from `image.completed` events. - -Mention these constraints: - -- Image references are local file paths. -- `--count` / `--concurrency` control output quantity; prompt text should describe one image and its style constraints. -- Synchronous success means path lines in text mode, or `ok: true` plus non-empty `images[].path` in JSON mode. JSON image entries may use `status=done`; do not require per-image `status=completed`. -- For Telegram delivery after synchronous CLI success, use the OpenClaw `message` tool when available: run imgen with `IMGEN_DELIVERY_DIR="${OPENCLAW_STATE_DIR:-$HOME/.openclaw}/workspace/imgen"`, then use `action="send"` to the current/original chat and attach the generated local file with the exact `path` or `filePath` returned by imgen. For PNG wallpapers or any image where original quality matters, set `forceDocument: true` or `asDocument: true` so Telegram sends the original file instead of compressing it as a photo. A concise caption/status message on the delivered image is acceptable when useful. After direct delivery, reply only `NO_REPLY`; OpenClaw Telegram direct chats should allow this silent reply instead of rewriting it into visible fallback text such as `No extra answer from me.` For multi-image requests with distinct themes, start the independent `./imgen --json --count 1 --concurrency 1` commands concurrently, poll them all, and send each completed `images[].path` immediately when that session finishes. If the `message` tool is unavailable, reply immediately with one `MEDIA:/absolute/path/to/image.png` line for each completed image. -- `submit --json` returns a job id first; final paths come from `get --json ` after completion. Use it only for service-job workflows, not normal one-shot OpenClaw generation. -- Realtime WebSocket requires `imgen serve`, streams `session.started`, `item.started`, `image.completed`, `item.failed`, and terminal session events, and does not create submit jobs or store rows. -- Secrets remain outside the calling prompt and stay in `.env`. -- For OpenClaw, use the discovered repo/config root, preferring `$HOME/Projects/codex-imgen` or `$HOME/codex-imgen` when present; otherwise the caller should not assume public network exposure, ccs Codex compatibility, `imgen serve` for normal one-shot generation, or a known repo-root. +Follow `references/troubleshooting.md` in order: job summary, attempt rows, phase timings, service logs, then stable backend verification. Do not jump directly to native Codex commands. ## Before saying the work is ready diff --git a/.agents/skills/imgen/evals/evals.json b/.agents/skills/imgen/evals/evals.json index c171241..42421f6 100644 --- a/.agents/skills/imgen/evals/evals.json +++ b/.agents/skills/imgen/evals/evals.json @@ -72,6 +72,12 @@ "prompt": "OpenClaw 要给当前 Telegram 聊天生成一张 PNG Mac 壁纸并直接发回原图。请给可执行调用和回复规则,不要真的生图。", "expected_output": "Set IMGEN_DELIVERY_DIR=\"${OPENCLAW_STATE_DIR:-$HOME/.openclaw}/workspace/imgen\" and run ./imgen --json from the discovered repo/config root. Treat success as ok true with non-empty images[].path, allowing images[].status done, send the exact returned local path via OpenClaw message using forceDocument/asDocument for document/original PNG delivery, then reply NO_REPLY. Do not call image_generate, do not fall back to direct codex exec, and do not send stale generated_images paths.", "files": [] + }, + { + "id": 13, + "prompt": "本地 configs/config.yaml 配了 backend.model: gpt-5.6-terra 和 backend.reasoning_effort: high,但 ~/.codex/config.toml 是 model_reasoning_effort = \"max\"。imgen 实际用哪个 effort?skill 会不会再覆盖一次?", + "expected_output": "Inspect the local imgen config and explain that non-empty backend.reasoning_effort is passed as a per-invocation Codex override, so high overrides the global max for imgen calls. If backend.reasoning_effort is empty, imgen inherits the Codex CLI setting. State that the skill itself does not set or override model/effort, and that backend.model selects the Codex agent invoking imagegen rather than directly naming the image-generation model.", + "files": [] } ] } diff --git a/.agents/skills/imgen/references/imgen-usage.md b/.agents/skills/imgen/references/imgen-usage.md index 9399f98..cc3c790 100644 --- a/.agents/skills/imgen/references/imgen-usage.md +++ b/.agents/skills/imgen/references/imgen-usage.md @@ -157,7 +157,8 @@ Important config fields: - `storage.data_dir` and `storage.sqlite_path` control service data and SQLite paths. - `scheduler.global_max_concurrency` controls the serve-mode backend generation queue shared by async submit and WebSocket realtime; `scheduler.max_count_per_job` controls the per-job image count cap. - `backend.command` defaults to `codex` and must be a single executable that accepts `exec --json`. -- `backend.model` is passed to Codex CLI when set; when empty, the actual model is whatever the backend executable uses by default. +- `backend.model` selects the Codex agent that invokes `$imagegen`; it is passed with `--model` when set and otherwise inherits the backend default. Do not describe it as the image-generation model. +- `backend.reasoning_effort` is passed per invocation as `--config model_reasoning_effort=...`; when empty, imgen inherits the Codex CLI configuration. Supported levels depend on `backend.model`, and the skill does not set this value. - `backend.cwd` is passed to Codex CLI as `--cd` when set. - `backend.delivery_dir` copies generated images to a caller-visible delivery directory before returning `images[].path`; OpenClaw Telegram can also set `IMGEN_DELIVERY_DIR` per process. - `backend.prompt.prefix` normally remains `$imagegen` and is prepended to the prompt text. @@ -167,36 +168,20 @@ Important config fields: To confirm the current backend and model, inspect the local config from the discovered repo/config root: ```bash -grep -nE '^[[:space:]]*(command|model|cwd|prefix):' configs/config.yaml +grep -nE '^[[:space:]]*(command|model|reasoning_effort|cwd|prefix):' configs/config.yaml ``` -If `configs/config.yaml` is unavailable, do not invent a model. Say the model depends on the local `backend.model` setting or the default model of the configured Codex backend. +If `configs/config.yaml` is unavailable, do not invent a model or effort level. Say they depend on the local backend settings and the configured Codex CLI defaults. ## OpenClaw calling contract -OpenClaw should first resolve a config cwd using the discovery rules above. In a normal local checkout, `$HOME/Projects/codex-imgen` maps to the user's repo while avoiding a hardcoded username path. For normal image generation, default to the synchronous CLI route below; do not require `./imgen serve` unless the user needs service job management or realtime streaming. If the resolved root contains an `imgen` executable and `configs/config.yaml`, use this route: +Resolve a config cwd with the discovery rules above. Normal image generation uses the synchronous CLI and does not require `imgen serve`: ```bash cd && IMGEN_DELIVERY_DIR="${OPENCLAW_STATE_DIR:-$HOME/.openclaw}/workspace/imgen" ./imgen --json --count --concurrency 1 "" ``` -Use repeated `--image ` flags before the prompt for image-to-image requests. Do not call OpenClaw's built-in `image_generate` tool. Do not fall back to direct `codex exec --json -- '$imagegen ...'`. Do not reuse old generated image paths unless the user explicitly asks for existing files. - -For Telegram delivery after synchronous CLI success, run imgen with `IMGEN_DELIVERY_DIR="${OPENCLAW_STATE_DIR:-$HOME/.openclaw}/workspace/imgen"`, then use direct channel delivery when OpenClaw's `message` tool is available: use `action="send"` to the current/original chat and attach each generated local file with the exact `path` or `filePath` returned by imgen. For PNG wallpapers or any image where original quality matters, include `forceDocument: true` or `asDocument: true` so Telegram sends the original file instead of a compressed photo preview. A concise user-facing caption/status message on delivered images is acceptable when useful, then reply only `NO_REPLY` so OpenClaw does not send duplicate text; OpenClaw Telegram direct chats should allow this silent reply instead of rewriting it into visible fallback text such as `No extra answer from me.` If the `message` tool is unavailable, reply immediately with one `MEDIA:/absolute/path/to/image.png` line for the completed image. - -For Telegram multi-image requests, prefer separate one-image commands when distinct themes are useful: run N independent `./imgen --json --count 1 --concurrency 1 ""` calls. When the execution tool supports background sessions, launch these independent commands concurrently, keep their session ids, poll all sessions, and send each successful `images[].path` with the `message` tool as soon as that session completes. Do not serialize independent theme generations unless the tool cannot run concurrent sessions. Do not wait for all requested images before sending earlier successes; later generation retries or timeouts must not block already completed images from being delivered. - -Do not rely on OpenClaw heartbeat or async command-completion delivery for synchronous CLI image results. If an exec/process call becomes a background session, poll it until completion and deliver each successful `images[].path` in the same user-visible flow before starting more generation. - -Then use one of these stable contracts. - -For CLI usage, OpenClaw should provide: - -- `cwd`: the discovered repo/config root. -- `command`: `./imgen --json ...` when using a repository checkout, or an absolute `imgen` executable path only when paired with the discovered config cwd. -- Prompt string describing one image or one candidate. -- Optional local image paths via repeated `--image` flags. -- Optional `count`, `concurrency`, and `json` output choice. +If the execution tool supports a `cwd`, set it instead of using `cd`. Add repeated `--image ` flags before the prompt for image-to-image requests. References must be local files. Expected synchronous JSON result: @@ -212,28 +197,21 @@ Expected synchronous JSON result: } ``` -Treat the call as successful only when `ok` is true and the expected completed images have non-empty `path` values. In text mode, treat non-empty path lines as the result. - -If an execution tool cannot set `cwd`, use `cd && ./imgen ...` as the shell command. - -For service job usage, OpenClaw should use this only when it needs job management, polling, cancellation, or recovery: +Treat the call as successful only when `ok` is true and every expected completed image has a non-empty, existing `path`. Per-image `status: "done"` is valid; do not require `completed`. -1. Ensure `./imgen serve` is running locally from the discovered repo/config root. -2. Submit with `./imgen submit --json` or the local API exposed by the service. -3. Store the returned job id. -4. Poll `./imgen get --json ` or subscribe to `/ws?job_id=`. -5. Read final image paths from `images[].path` after completion. +### Telegram delivery -For realtime WebSocket usage, OpenClaw should use this only when it needs live streaming events: +- Send each exact returned `path` or `filePath` to the current/original chat with OpenClaw's `message` tool. +- Set `forceDocument: true` or `asDocument: true` for PNG wallpapers or other original-quality files. +- After direct delivery, reply only `NO_REPLY`. If `message` is unavailable, return one `MEDIA:/absolute/path` line per image. +- For distinct themes, run independent `--count 1 --concurrency 1` commands concurrently when supported, poll every background session, and deliver each successful path immediately. Do not make later retries block earlier results. +- Keep delivery in the same user-visible flow; do not rely on heartbeat or async process-completion messages. -1. Ensure `./imgen serve` is running locally from the discovered repo/config root. -2. Connect to `/v1/realtime/generate/ws`. -3. Send one `generate.start` frame with one or more items. -4. Stream status from `session.started`, `item.started`, `image.completed`, `item.failed`, and terminal session events. -5. Send each generated file path from `image.completed` as soon as it is available. +Do not call `image_generate`, fall back to direct `codex exec`, or reuse old paths unless the user explicitly requested existing files. -Realtime WebSocket does not return a job id, does not use `get `, and does not create submit job store rows. +### Service and realtime routes -For Telegram delivery after synchronous CLI success, use the OpenClaw `message` tool when available: send the exact generated local file path with `path` or `filePath`, set `forceDocument: true` or `asDocument: true` for PNG wallpapers/original-quality delivery, use concise captions/status text when useful, then reply only `NO_REPLY`. OpenClaw Telegram direct chats should allow this silent reply instead of rewriting it into visible fallback text such as `No extra answer from me.` For multi-image requests with distinct themes, run the one-image `imgen` commands concurrently when possible and send each completed image as soon as its session returns `images[].path`. If direct message-tool delivery is unavailable, reply immediately with one `MEDIA:/absolute/path/to/image.png` line for each completed image. +- Use `submit --json` only for server-side job management, polling, cancellation, or recovery. It returns a job id; poll `get --json ` or `/ws?job_id=` for final paths. +- Use `/v1/realtime/generate/ws` only when `imgen serve` is running and live per-item events are required. Read paths from `image.completed`; realtime creates neither submit jobs nor store rows. -OpenClaw should not assume URL image input, public network binding, public filesystem scans, ccs Codex compatibility, direct native Codex CLI fallback, `imgen serve` for normal one-shot generation, async heartbeat delivery for final images, or access to secrets inside prompts. +Do not assume public network binding, broad filesystem scans, ccs compatibility, secrets in prompts, or a known repository root. diff --git a/.openclaw/skills/imgen/SKILL.md b/.openclaw/skills/imgen/SKILL.md index 26381f2..19a12ad 100644 --- a/.openclaw/skills/imgen/SKILL.md +++ b/.openclaw/skills/imgen/SKILL.md @@ -5,111 +5,54 @@ description: "Default image workflow for this user. Use for ordinary raster imag # imgen -Use this skill to help users operate the `codex-imgen` project through its synchronous `imgen` CLI, local service jobs, and realtime WebSocket mode. +Use this skill to operate the `codex-imgen` CLI, service jobs, realtime mode, and OpenClaw integration. -## First decide the scenario +## Route the request -Classify the user's request into one of these scenarios: +Read only the references needed for the request: -1. **Synchronous text-to-image** — user has a prompt and wants image paths directly. -2. **Synchronous image-to-image** — user supplies one or more local reference image paths and wants image paths directly. -3. **Asynchronous service job** — user wants `serve`, `submit`, `status`, `get`, `list`, `cancel`, polling, recovery, or server-side job management. -4. **Realtime WebSocket** — user explicitly wants streaming per-item generation events over `/v1/realtime/generate/ws`. -5. **Troubleshooting** — user reports slowness, failed jobs, retries, missing images, or confusing output. -6. **Configuration** — user asks about `configs/config.yaml`, `.env`, backend, scheduler, storage, or email settings. -7. **OpenClaw or another agent** — user wants platform-neutral instructions for another agent or tool to call imgen. +- `references/imgen-usage.md`: commands, configuration, backend behavior, service/realtime selection, and the OpenClaw delivery contract. +- `references/prompt-patterns.md`: prompt wording for text-to-image or image-to-image requests. +- `references/troubleshooting.md`: failed, slow, retried, stuck, or missing-image runs. -Read only the reference file that matches the scenario: - -- Usage and OpenClaw calling contract: `references/imgen-usage.md` -- Prompt wording patterns: `references/prompt-patterns.md` -- Job and generation troubleshooting: `references/troubleshooting.md` +Combine references only when the request spans those concerns. ## Ask only necessary questions If the user gave enough information, provide the command or steps directly. -Ask one question when a required value is missing: +Ask one question only when a required value is missing: - Missing image prompt for generation. - Missing local image path for image-to-image. - Missing job id for `status`, `get`, `cancel`, or job-specific troubleshooting. -- Missing decision between synchronous CLI, service job, or realtime WebSocket only when the user's goal requires choosing one. +- Missing choice between synchronous CLI, service job, or realtime only when the goal does not determine it. Do not ask for preferences that are not needed to produce a safe minimal command. ## Command rules -Use the shortest command that satisfies the request. Default one-shot generation, including normal OpenClaw image generation, to synchronous `./imgen --json ...`; do not require `./imgen serve` unless the user needs job management or realtime streaming. - -Before giving local execution instructions for OpenClaw or another agent, do not assume `` is known. Tell the caller to discover a config cwd in this order: `IMGEN_REPO_ROOT`, upward search for `configs/config.yaml` plus `./imgen`/`build.sh`/`go.mod`, explicit user install paths, then `command -v imgen` paired with a discovered config cwd. If no executable or config cwd is found, return a clear "imgen is not installed or not discoverable" error instead of running from an arbitrary cwd. - -For OpenClaw or another local agent, resolve a repo/config root before running image requests. Check explicit common checkout paths such as `$HOME/Projects/codex-imgen` and `$HOME/codex-imgen` before falling back to `command -v imgen`; a PATH executable still needs a discovered config cwd. When a checkout root is found, run from that cwd. For normal text-to-image requests, use the synchronous CLI and run `./imgen --json --count N --concurrency 1 ""`; this does not require `./imgen serve`. For OpenClaw Telegram delivery, set `IMGEN_DELIVERY_DIR="${OPENCLAW_STATE_DIR:-$HOME/.openclaw}/workspace/imgen"` on each synchronous imgen process so returned `images[].path` values are copied under OpenClaw's allowed local media roots. For image-to-image requests, add repeated `--image ` flags before the prompt. Use `submit` only when the caller needs server-side job management, polling, cancellation, or recovery. Use `/v1/realtime/generate/ws` only when `imgen serve` is running and the caller needs live streaming events for a WebSocket session. For Telegram multi-image requests where distinct themes are useful, launch N independent `./imgen --json --count 1 --concurrency 1 ""` commands concurrently when the execution tool supports background sessions, each with the same `IMGEN_DELIVERY_DIR`, then poll all sessions and deliver each successful `images[].path` immediately as it completes. Do not serialize these independent theme generations unless the tool cannot run them concurrently. Do not wait for all requested images before sending earlier successes. Do not call OpenClaw's built-in `image_generate` tool. Do not fall back to direct `codex exec --json -- '$imagegen ...'`. Do not reuse old generated image paths unless the user explicitly asks for existing files. - -For multiple candidates, use `--count N` and `--concurrency M` to control quantity. Keep the prompt phrased as a single-image request such as `生成 1 张...` or `单图`; do not also ask for `生成 N 张` or `输出 N 张不同构图` inside the prompt. - -Common commands: - -```bash -./imgen "生成一张 Q 版小龙吉祥物,白底,单图" -./imgen --count 4 --concurrency 2 "五更琉璃,穿着女仆装在咖啡馆" -./imgen --image ./1.png "保留主体构图和姿态,把这张图改成高质量 3D 手办渲染风格,背景更干净,单图" -./imgen --image ./1.png --image ./2.png "以第一张为主体,采用第二张的风格和材质表现,输出单图" -./imgen serve -./imgen submit --count 4 --concurrency 2 "五更琉璃,穿着女仆装在咖啡馆" -./imgen status -./imgen get -./imgen list -./imgen cancel -``` - -Prefer `./imgen --json ...` as the stable verification and OpenClaw contract. Direct native Codex CLI usage is only a diagnostic path for humans who explicitly ask for it; it is not an OpenClaw fallback. For OpenClaw image requests, do not call `codex exec --json -- '$imagegen ...'` directly; use the discovered repo/config root with `./imgen --json ...` instead. - -If the local tool is `ccs codex` and `ccs codex exec --json` fails, do not reuse native Codex commands. Current `imgen` expects `backend.command` to accept `exec --json`; a plain `ccs codex '$imagegen ...'` exit code 0 without image paths is not a successful imgen-compatible result. +- Use the shortest command that satisfies the request. +- Default one-shot generation, including normal OpenClaw requests, to synchronous `./imgen --json ...` from a discovered config working directory. +- Use repeated `--image ` flags for references; URLs are not supported. +- Use `--count N` and `--concurrency M` for quantity while keeping the prompt about one image. +- Use `submit` only for job management, polling, cancellation, or recovery. Use realtime only for live WebSocket events while `imgen serve` is running. +- Treat success as returned image paths: text path lines, or `ok: true` with non-empty `images[].path`. Exit code 0 alone is insufficient. +- For OpenClaw, follow the full contract in `references/imgen-usage.md`; never substitute `image_generate`, direct `codex exec`, or stale paths. ## Safety and configuration rules - Treat `.env` as the only place for sensitive values such as `EMAIL_SMTP_AUTH_CODE`. - Treat `configs/config.yaml` as the local structured configuration file. - Do not suggest committing `configs/config.yaml`, `.env`, generated images, logs, or `.data/imgen.db`. -- Do not assume URL image input is supported; current image input is local file paths only. - Do not suggest `0.0.0.0` service binding unless the user explicitly asks for LAN access and understands the exposure. - Do not bypass Codex CLI login, permissions, or `$imagegen` availability checks. -- Do not claim generation succeeded from exit code alone; require text path lines or JSON `images[].path` values. -- Do not invent the image model; inspect `configs/config.yaml` `backend.model` when available, otherwise say it depends on the configured backend default. +- Inspect `backend.model` and `backend.reasoning_effort` before describing the active Codex agent configuration. The skill itself does not choose either value. +- Do not call `backend.model` the image-generation model; it selects the Codex agent that invokes `$imagegen`. ## Troubleshooting flow -For slow or failed service jobs, follow this order: - -1. Check the job summary with `./imgen status ` and `./imgen get `. -2. Check image attempts in SQLite. -3. Check phase timings in SQLite. -4. Check `logs/out.log` when the service was started by repository scripts. -5. Verify with `./imgen --json`; only verify the underlying Codex command directly when it supports `exec --json`. - -Use `references/troubleshooting.md` for exact SQL and phase interpretation. - -## OpenClaw guidance - -When the user wants OpenClaw support, produce platform-neutral instructions instead of Claude-specific tool steps. - -Prefer one of these outputs: - -- A synchronous CLI contract for normal image generation. It should say how to discover repo/config root, which `./imgen --json` command OpenClaw should run, what arguments it should supply, what output shape to expect, and how to report missing installation/configuration. This is the default OpenClaw contract and does not require `./imgen serve`. -- A local service job contract only when the caller needs server-side job management, polling, cancellation, or recovery. It should say to start `./imgen serve`, submit a job, poll or subscribe for completion, then read result paths. -- A realtime WebSocket contract only when the caller needs live streaming events. It should say to start `./imgen serve`, connect to `/v1/realtime/generate/ws`, send one `generate.start` frame, consume streamed events, and read image paths from `image.completed` events. - -Mention these constraints: - -- Image references are local file paths. -- `--count` / `--concurrency` control output quantity; prompt text should describe one image and its style constraints. -- Synchronous success means path lines in text mode, or `ok: true` plus non-empty `images[].path` in JSON mode. JSON image entries may use `status=done`; do not require per-image `status=completed`. -- For Telegram delivery after synchronous CLI success, use the OpenClaw `message` tool when available: run imgen with `IMGEN_DELIVERY_DIR="${OPENCLAW_STATE_DIR:-$HOME/.openclaw}/workspace/imgen"`, then use `action="send"` to the current/original chat and attach the generated local file with the exact `path` or `filePath` returned by imgen. For PNG wallpapers or any image where original quality matters, set `forceDocument: true` or `asDocument: true` so Telegram sends the original file instead of compressing it as a photo. A concise caption/status message on the delivered image is acceptable when useful. After direct delivery, reply only `NO_REPLY`; OpenClaw Telegram direct chats should allow this silent reply instead of rewriting it into visible fallback text such as `No extra answer from me.` For multi-image requests with distinct themes, start the independent `./imgen --json --count 1 --concurrency 1` commands concurrently, poll them all, and send each completed `images[].path` immediately when that session finishes. If the `message` tool is unavailable, reply immediately with one `MEDIA:/absolute/path/to/image.png` line for each completed image. -- `submit --json` returns a job id first; final paths come from `get --json ` after completion. Use it only for service-job workflows, not normal one-shot OpenClaw generation. -- Realtime WebSocket requires `imgen serve`, streams `session.started`, `item.started`, `image.completed`, `item.failed`, and terminal session events, and does not create submit jobs or store rows. -- Secrets remain outside the calling prompt and stay in `.env`. -- For OpenClaw, use the discovered repo/config root, preferring `$HOME/Projects/codex-imgen` or `$HOME/codex-imgen` when present; otherwise the caller should not assume public network exposure, ccs Codex compatibility, `imgen serve` for normal one-shot generation, or a known repo-root. +Follow `references/troubleshooting.md` in order: job summary, attempt rows, phase timings, service logs, then stable backend verification. Do not jump directly to native Codex commands. ## Before saying the work is ready diff --git a/.openclaw/skills/imgen/evals/evals.json b/.openclaw/skills/imgen/evals/evals.json index c171241..42421f6 100644 --- a/.openclaw/skills/imgen/evals/evals.json +++ b/.openclaw/skills/imgen/evals/evals.json @@ -72,6 +72,12 @@ "prompt": "OpenClaw 要给当前 Telegram 聊天生成一张 PNG Mac 壁纸并直接发回原图。请给可执行调用和回复规则,不要真的生图。", "expected_output": "Set IMGEN_DELIVERY_DIR=\"${OPENCLAW_STATE_DIR:-$HOME/.openclaw}/workspace/imgen\" and run ./imgen --json from the discovered repo/config root. Treat success as ok true with non-empty images[].path, allowing images[].status done, send the exact returned local path via OpenClaw message using forceDocument/asDocument for document/original PNG delivery, then reply NO_REPLY. Do not call image_generate, do not fall back to direct codex exec, and do not send stale generated_images paths.", "files": [] + }, + { + "id": 13, + "prompt": "本地 configs/config.yaml 配了 backend.model: gpt-5.6-terra 和 backend.reasoning_effort: high,但 ~/.codex/config.toml 是 model_reasoning_effort = \"max\"。imgen 实际用哪个 effort?skill 会不会再覆盖一次?", + "expected_output": "Inspect the local imgen config and explain that non-empty backend.reasoning_effort is passed as a per-invocation Codex override, so high overrides the global max for imgen calls. If backend.reasoning_effort is empty, imgen inherits the Codex CLI setting. State that the skill itself does not set or override model/effort, and that backend.model selects the Codex agent invoking imagegen rather than directly naming the image-generation model.", + "files": [] } ] } diff --git a/.openclaw/skills/imgen/references/imgen-usage.md b/.openclaw/skills/imgen/references/imgen-usage.md index 9399f98..cc3c790 100644 --- a/.openclaw/skills/imgen/references/imgen-usage.md +++ b/.openclaw/skills/imgen/references/imgen-usage.md @@ -157,7 +157,8 @@ Important config fields: - `storage.data_dir` and `storage.sqlite_path` control service data and SQLite paths. - `scheduler.global_max_concurrency` controls the serve-mode backend generation queue shared by async submit and WebSocket realtime; `scheduler.max_count_per_job` controls the per-job image count cap. - `backend.command` defaults to `codex` and must be a single executable that accepts `exec --json`. -- `backend.model` is passed to Codex CLI when set; when empty, the actual model is whatever the backend executable uses by default. +- `backend.model` selects the Codex agent that invokes `$imagegen`; it is passed with `--model` when set and otherwise inherits the backend default. Do not describe it as the image-generation model. +- `backend.reasoning_effort` is passed per invocation as `--config model_reasoning_effort=...`; when empty, imgen inherits the Codex CLI configuration. Supported levels depend on `backend.model`, and the skill does not set this value. - `backend.cwd` is passed to Codex CLI as `--cd` when set. - `backend.delivery_dir` copies generated images to a caller-visible delivery directory before returning `images[].path`; OpenClaw Telegram can also set `IMGEN_DELIVERY_DIR` per process. - `backend.prompt.prefix` normally remains `$imagegen` and is prepended to the prompt text. @@ -167,36 +168,20 @@ Important config fields: To confirm the current backend and model, inspect the local config from the discovered repo/config root: ```bash -grep -nE '^[[:space:]]*(command|model|cwd|prefix):' configs/config.yaml +grep -nE '^[[:space:]]*(command|model|reasoning_effort|cwd|prefix):' configs/config.yaml ``` -If `configs/config.yaml` is unavailable, do not invent a model. Say the model depends on the local `backend.model` setting or the default model of the configured Codex backend. +If `configs/config.yaml` is unavailable, do not invent a model or effort level. Say they depend on the local backend settings and the configured Codex CLI defaults. ## OpenClaw calling contract -OpenClaw should first resolve a config cwd using the discovery rules above. In a normal local checkout, `$HOME/Projects/codex-imgen` maps to the user's repo while avoiding a hardcoded username path. For normal image generation, default to the synchronous CLI route below; do not require `./imgen serve` unless the user needs service job management or realtime streaming. If the resolved root contains an `imgen` executable and `configs/config.yaml`, use this route: +Resolve a config cwd with the discovery rules above. Normal image generation uses the synchronous CLI and does not require `imgen serve`: ```bash cd && IMGEN_DELIVERY_DIR="${OPENCLAW_STATE_DIR:-$HOME/.openclaw}/workspace/imgen" ./imgen --json --count --concurrency 1 "" ``` -Use repeated `--image ` flags before the prompt for image-to-image requests. Do not call OpenClaw's built-in `image_generate` tool. Do not fall back to direct `codex exec --json -- '$imagegen ...'`. Do not reuse old generated image paths unless the user explicitly asks for existing files. - -For Telegram delivery after synchronous CLI success, run imgen with `IMGEN_DELIVERY_DIR="${OPENCLAW_STATE_DIR:-$HOME/.openclaw}/workspace/imgen"`, then use direct channel delivery when OpenClaw's `message` tool is available: use `action="send"` to the current/original chat and attach each generated local file with the exact `path` or `filePath` returned by imgen. For PNG wallpapers or any image where original quality matters, include `forceDocument: true` or `asDocument: true` so Telegram sends the original file instead of a compressed photo preview. A concise user-facing caption/status message on delivered images is acceptable when useful, then reply only `NO_REPLY` so OpenClaw does not send duplicate text; OpenClaw Telegram direct chats should allow this silent reply instead of rewriting it into visible fallback text such as `No extra answer from me.` If the `message` tool is unavailable, reply immediately with one `MEDIA:/absolute/path/to/image.png` line for the completed image. - -For Telegram multi-image requests, prefer separate one-image commands when distinct themes are useful: run N independent `./imgen --json --count 1 --concurrency 1 ""` calls. When the execution tool supports background sessions, launch these independent commands concurrently, keep their session ids, poll all sessions, and send each successful `images[].path` with the `message` tool as soon as that session completes. Do not serialize independent theme generations unless the tool cannot run concurrent sessions. Do not wait for all requested images before sending earlier successes; later generation retries or timeouts must not block already completed images from being delivered. - -Do not rely on OpenClaw heartbeat or async command-completion delivery for synchronous CLI image results. If an exec/process call becomes a background session, poll it until completion and deliver each successful `images[].path` in the same user-visible flow before starting more generation. - -Then use one of these stable contracts. - -For CLI usage, OpenClaw should provide: - -- `cwd`: the discovered repo/config root. -- `command`: `./imgen --json ...` when using a repository checkout, or an absolute `imgen` executable path only when paired with the discovered config cwd. -- Prompt string describing one image or one candidate. -- Optional local image paths via repeated `--image` flags. -- Optional `count`, `concurrency`, and `json` output choice. +If the execution tool supports a `cwd`, set it instead of using `cd`. Add repeated `--image ` flags before the prompt for image-to-image requests. References must be local files. Expected synchronous JSON result: @@ -212,28 +197,21 @@ Expected synchronous JSON result: } ``` -Treat the call as successful only when `ok` is true and the expected completed images have non-empty `path` values. In text mode, treat non-empty path lines as the result. - -If an execution tool cannot set `cwd`, use `cd && ./imgen ...` as the shell command. - -For service job usage, OpenClaw should use this only when it needs job management, polling, cancellation, or recovery: +Treat the call as successful only when `ok` is true and every expected completed image has a non-empty, existing `path`. Per-image `status: "done"` is valid; do not require `completed`. -1. Ensure `./imgen serve` is running locally from the discovered repo/config root. -2. Submit with `./imgen submit --json` or the local API exposed by the service. -3. Store the returned job id. -4. Poll `./imgen get --json ` or subscribe to `/ws?job_id=`. -5. Read final image paths from `images[].path` after completion. +### Telegram delivery -For realtime WebSocket usage, OpenClaw should use this only when it needs live streaming events: +- Send each exact returned `path` or `filePath` to the current/original chat with OpenClaw's `message` tool. +- Set `forceDocument: true` or `asDocument: true` for PNG wallpapers or other original-quality files. +- After direct delivery, reply only `NO_REPLY`. If `message` is unavailable, return one `MEDIA:/absolute/path` line per image. +- For distinct themes, run independent `--count 1 --concurrency 1` commands concurrently when supported, poll every background session, and deliver each successful path immediately. Do not make later retries block earlier results. +- Keep delivery in the same user-visible flow; do not rely on heartbeat or async process-completion messages. -1. Ensure `./imgen serve` is running locally from the discovered repo/config root. -2. Connect to `/v1/realtime/generate/ws`. -3. Send one `generate.start` frame with one or more items. -4. Stream status from `session.started`, `item.started`, `image.completed`, `item.failed`, and terminal session events. -5. Send each generated file path from `image.completed` as soon as it is available. +Do not call `image_generate`, fall back to direct `codex exec`, or reuse old paths unless the user explicitly requested existing files. -Realtime WebSocket does not return a job id, does not use `get `, and does not create submit job store rows. +### Service and realtime routes -For Telegram delivery after synchronous CLI success, use the OpenClaw `message` tool when available: send the exact generated local file path with `path` or `filePath`, set `forceDocument: true` or `asDocument: true` for PNG wallpapers/original-quality delivery, use concise captions/status text when useful, then reply only `NO_REPLY`. OpenClaw Telegram direct chats should allow this silent reply instead of rewriting it into visible fallback text such as `No extra answer from me.` For multi-image requests with distinct themes, run the one-image `imgen` commands concurrently when possible and send each completed image as soon as its session returns `images[].path`. If direct message-tool delivery is unavailable, reply immediately with one `MEDIA:/absolute/path/to/image.png` line for each completed image. +- Use `submit --json` only for server-side job management, polling, cancellation, or recovery. It returns a job id; poll `get --json ` or `/ws?job_id=` for final paths. +- Use `/v1/realtime/generate/ws` only when `imgen serve` is running and live per-item events are required. Read paths from `image.completed`; realtime creates neither submit jobs nor store rows. -OpenClaw should not assume URL image input, public network binding, public filesystem scans, ccs Codex compatibility, direct native Codex CLI fallback, `imgen serve` for normal one-shot generation, async heartbeat delivery for final images, or access to secrets inside prompts. +Do not assume public network binding, broad filesystem scans, ccs compatibility, secrets in prompts, or a known repository root. From 506c19e6367dfebb0938ca34fe6997130dba9cf2 Mon Sep 17 00:00:00 2001 From: walker1211 <13750528578@163.com> Date: Sun, 2 Aug 2026 12:57:19 +0800 Subject: [PATCH 3/4] =?UTF-8?q?refactor(skill-sync):=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=20canonical=20skill=20=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/doctor/openclaw.go | 2 +- internal/skillsync/sync.go | 16 ++++++++-------- internal/skillsync/sync_test.go | 8 ++++++++ 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/internal/doctor/openclaw.go b/internal/doctor/openclaw.go index 1082688..1a474f5 100644 --- a/internal/doctor/openclaw.go +++ b/internal/doctor/openclaw.go @@ -254,7 +254,7 @@ func checkOpenClawSkill(path string, report *Report) { } func checkSkillSync(repoRoot string, installedOpenClawSkillPath string, report *Report) { - sourceDir := filepath.Join(repoRoot, ".agents", "skills", "imgen") + sourceDir := skillsync.CanonicalSkillDir(repoRoot) repositoryOpenClawDir := filepath.Join(repoRoot, ".openclaw", "skills", "imgen") drift, err := skillsync.CompareSkillTrees(sourceDir, repositoryOpenClawDir, "repository OpenClaw skill mirror") diff --git a/internal/skillsync/sync.go b/internal/skillsync/sync.go index ddb5891..86d2ba7 100644 --- a/internal/skillsync/sync.go +++ b/internal/skillsync/sync.go @@ -36,6 +36,10 @@ type Result struct { Applied []string } +func CanonicalSkillDir(repoRoot string) string { + return filepath.Join(repoRoot, ".agents", "skills", "imgen") +} + func DefaultPaths(repoRoot string, home string) Paths { claudeRoot := filepath.Join(home, ".claude") claudeInstallParent := filepath.Join(claudeRoot, "skills") @@ -94,7 +98,7 @@ func (p Paths) WithCodexInstallDir(path string) Paths { } func (p Paths) Pairs() []Pair { - sourceDir := p.agentsSourceDir() + sourceDir := CanonicalSkillDir(p.RepoRoot) pairs := []Pair{ { Name: "agents", @@ -122,17 +126,13 @@ func (p Paths) Pairs() []Pair { return pairs } -func (p Paths) agentsSourceDir() string { - return filepath.Join(p.RepoRoot, ".agents", "skills", "imgen") -} - func (p Paths) openClawRepositoryDir() string { return filepath.Join(p.RepoRoot, ".openclaw", "skills", "imgen") } func (p Paths) Check() (Result, error) { var result Result - sourceDir := p.agentsSourceDir() + sourceDir := CanonicalSkillDir(p.RepoRoot) if err := validateSource(sourceDir); err != nil { return Result{}, fmt.Errorf("agents source invalid: %w", err) } @@ -155,7 +155,7 @@ func (p Paths) Check() (Result, error) { func (p Paths) Apply() (Result, error) { var result Result - sourceDir := p.agentsSourceDir() + sourceDir := CanonicalSkillDir(p.RepoRoot) if err := validateSource(sourceDir); err != nil { return Result{}, fmt.Errorf("agents source invalid: %w", err) } @@ -191,7 +191,7 @@ func FindRepositoryRoot(cwd string) (string, error) { return "", err } for { - hasSkillSource := dirExists(filepath.Join(dir, ".agents", "skills", "imgen")) + hasSkillSource := dirExists(CanonicalSkillDir(dir)) hasProjectMarker := fileExists(filepath.Join(dir, "go.mod")) || fileExists(filepath.Join(dir, "configs", "config.example.yaml")) || fileExists(filepath.Join(dir, "configs", "config.yaml")) diff --git a/internal/skillsync/sync_test.go b/internal/skillsync/sync_test.go index 58ca5ec..6f33290 100644 --- a/internal/skillsync/sync_test.go +++ b/internal/skillsync/sync_test.go @@ -7,6 +7,14 @@ import ( "testing" ) +func TestCanonicalSkillDir(t *testing.T) { + repoRoot := filepath.Join("testdata", "codex-imgen") + want := filepath.Join(repoRoot, ".agents", "skills", "imgen") + if got := CanonicalSkillDir(repoRoot); got != want { + t.Fatalf("CanonicalSkillDir() = %q, want %q", got, want) + } +} + func TestCheckReportsOnlyAgentsWhenOptionalRuntimesMissing(t *testing.T) { repoRoot := t.TempDir() home := t.TempDir() From d5157478dc1605e758dd9359279b1668080367da Mon Sep 17 00:00:00 2001 From: walker1211 <13750528578@163.com> Date: Sun, 2 Aug 2026 13:01:29 +0800 Subject: [PATCH 4/4] =?UTF-8?q?docs(release):=20=E6=B7=BB=E5=8A=A0=20v0.1.?= =?UTF-8?q?15=20=E5=8F=91=E5=B8=83=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/releases/v0.1.15.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 docs/releases/v0.1.15.md diff --git a/docs/releases/v0.1.15.md b/docs/releases/v0.1.15.md new file mode 100644 index 0000000..722dcd2 --- /dev/null +++ b/docs/releases/v0.1.15.md @@ -0,0 +1,16 @@ +## Highlights + +- Moved the canonical imgen skill source to `.agents/skills/imgen` and kept the repository OpenClaw mirror synchronized from that source. +- Made Agents skill installation unconditional while skipping Claude and OpenClaw installs when their runtime directories are absent; explicit directory flags still enable either optional target. +- Added `--agents-dir`, preserved `--codex-dir` as a compatibility alias, and improved help, repository discovery, doctor checks, release packaging, and regression coverage. +- Streamlined the imgen skill guidance and clarified that `backend.reasoning_effort` overrides the Codex CLI setting per invocation when configured. + +## Notes + +Running `./skill-sync --apply` always updates the personal Agents install. Claude and OpenClaw installs are updated only when their runtime directories already exist or their destination flags are supplied. + +## Validation + +- `./scripts/ci-local.sh clean` +- `./skill-sync --check` +- GitHub PR checks: `test`, `Analyze Go`, and `CodeQL`