diff --git a/README.md b/README.md index 7919c19..c50c2eb 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,8 @@ and runs on [OpenCode](https://opencode.ai) and [Pi](https://github.com/earendil ```bash opencode ``` +> **Windows users:** `.opencode/{agents,commands,skills,rules}` are git symlinks and require `core.symlinks=true` (+ Developer Mode) to check out correctly — see [Known Issues](#-known-issues) if `/` shows no commands. + Type `/` to browse all 77 skills and 54 commands, or `/start` for onboarding. ### Pi @@ -374,6 +376,7 @@ skills, commands, rules, and plugins. | Issue | Impact | Workaround | |-------|--------|------------| | **Subagent model resolution via `task`** — Agent `model:` frontmatter fails with `ProviderModelNotFoundError` for models that work when used directly via `opencode -m `. Subagents inherit the caller's model per OpenCode docs, so the frontmatter model may only apply when the agent runs as a primary session. | Agents using `opencode-go/kimi-k2.6` and `opencode-go/deepseek-v4-flash` as subagents via `task` | Use `opencode-go/qwen3.6-plus` for subagent-heavy workflows, or start dedicated sessions with `opencode -m ` for director-level agents. Root cause being tracked upstream in OpenCode. | +| **`.opencode/*` symlinks inert on Windows** — `.opencode/{agents,commands,skills,rules}` are committed as git symlinks. With the Windows default `core.symlinks=false`, git checks them out as plain-text stub files instead of real symlinks, so OpenCode's `/`-command and skill/agent discovery finds nothing. | OpenCode is non-functional on a stock Windows clone (Pi is unaffected — it reads `.agents/` directly) | `git config --global core.symlinks true` + enable Developer Mode, then re-clone; or run `node tools/fix-opencode-symlinks.mjs` to repair an existing checkout. | --- diff --git a/docs/pi-compatibility.md b/docs/pi-compatibility.md index 2d67570..3205d20 100644 --- a/docs/pi-compatibility.md +++ b/docs/pi-compatibility.md @@ -72,3 +72,17 @@ If you have an existing OCGS project: 1. Run `node tools/migrate-to-agents.mjs` to migrate content from `.opencode/` to `.agents/` 2. Run with `--dry-run` first to preview 3. Use `--remove-old` to delete the original `.opencode/` content after verification + +## Windows Symlink Setup + +`.opencode/{agents,commands,skills,rules}` are git symlinks pointing at the +canonical `.agents/*` directories. On Windows, git only checks these out as +real symlinks if `core.symlinks=true` is set (and Developer Mode/admin rights +are enabled) **before** cloning. Otherwise they check out as inert stub files +containing the link-target text, and OpenCode's `.opencode/`-based discovery +silently finds nothing (Pi is unaffected — it reads `.agents/` directly). + +Fix: `git config --global core.symlinks true` + enable Developer Mode, then +re-clone (or run `git checkout -- .opencode` after enabling). Alternatively, +run `node tools/fix-opencode-symlinks.mjs` to detect and repair inert stub +files in an existing checkout. diff --git a/tools/fix-opencode-symlinks.mjs b/tools/fix-opencode-symlinks.mjs new file mode 100644 index 0000000..c4896d3 --- /dev/null +++ b/tools/fix-opencode-symlinks.mjs @@ -0,0 +1,113 @@ +#!/usr/bin/env node +/** + * Detects and repairs inert `.opencode/{agents,commands,skills,rules}` entries. + * + * These paths are committed as git symlinks pointing at `../.agents/`. + * On Windows, `git checkout` materializes symlinks as real OS symlinks only + * when `core.symlinks=true` is set (which itself requires Developer Mode or + * admin rights). With the default `core.symlinks=false`, git instead writes + * a small plain-text file containing the link target string — which silently + * breaks OpenCode's `.opencode/`-based agent/skill/command/rule discovery. + * + * This script detects that broken state and repairs it by replacing the stub + * file with a real directory symlink/junction pointing at the canonical + * `.agents/` directory. + * + * Usage: node tools/fix-opencode-symlinks.mjs [--dry-run] + */ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const ROOT = path.resolve(__dirname, ".."); +const LINKS = ["agents", "commands", "skills", "rules"]; +const STUB_RE = /^\.\.[\\/]\.agents[\\/][a-z-]+\s*$/i; + +const dryRun = process.argv.includes("--dry-run"); + +function classify(linkPath) { + if (!fs.existsSync(linkPath)) return "missing"; + const stat = fs.lstatSync(linkPath); + if (stat.isSymbolicLink()) { + try { + const real = fs.realpathSync(linkPath); + return fs.existsSync(real) && fs.statSync(real).isDirectory() + ? "ok-symlink" + : "broken-symlink"; + } catch { + return "broken-symlink"; + } + } + if (stat.isDirectory()) return "ok-directory"; // e.g. a real materialized copy + if (stat.isFile()) { + const content = fs.readFileSync(linkPath, "utf8").trim(); + if (STUB_RE.test(content)) return "stub-file"; + return "unknown-file"; + } + return "unknown"; +} + +function repair(name) { + const linkPath = path.join(ROOT, ".opencode", name); + const targetRel = path.join("..", ".agents", name); + const targetAbs = path.join(ROOT, ".agents", name); + + if (!fs.existsSync(targetAbs) || !fs.statSync(targetAbs).isDirectory()) { + console.error(` ✗ ${name}: canonical source .agents/${name} does not exist — skipping`); + return false; + } + + if (dryRun) { + console.log(` → would replace .opencode/${name} with a symlink/junction to .agents/${name}`); + return true; + } + + fs.rmSync(linkPath, { force: true }); + try { + // 'junction' works without elevated privileges on Windows; falls back + // to a regular symlink type on POSIX (junction is ignored there). + fs.symlinkSync(targetRel, linkPath, process.platform === "win32" ? "junction" : "dir"); + } catch (err) { + // Junctions require an absolute target on Windows. + if (process.platform === "win32") { + fs.symlinkSync(targetAbs, linkPath, "junction"); + } else { + throw err; + } + } + console.log(` ✓ ${name}: repaired -> .agents/${name}`); + return true; +} + +console.log("Checking .opencode/{agents,commands,skills,rules}...\n"); + +let anyBroken = false; +for (const name of LINKS) { + const linkPath = path.join(ROOT, ".opencode", name); + const state = classify(linkPath); + if (state === "ok-symlink" || state === "ok-directory") { + console.log(` ✓ ${name}: OK (${state})`); + continue; + } + anyBroken = true; + console.log(` ✗ ${name}: ${state} — this is broken, OpenCode cannot see .agents/${name}`); + repair(name); +} + +if (!anyBroken) { + console.log("\nAll .opencode/* links are healthy. Nothing to do."); + process.exit(0); +} + +console.log( + dryRun + ? "\nDry run complete. Re-run without --dry-run to apply fixes." + : "\nRepair complete. Re-run this script any time after a fresh clone/checkout." +); +console.log( + "\nTip: to avoid this permanently, enable symlink support before cloning:\n" + + " git config --global core.symlinks true\n" + + " (Windows also requires Developer Mode or running git as Administrator)\n" + + "then re-clone, or run `git checkout -- .opencode` after enabling it." +);