From 35709fa48dcf042488de71cdebeac731b0b416d9 Mon Sep 17 00:00:00 2001 From: pkuwkl Date: Fri, 2 Oct 2026 23:00:35 +0800 Subject: [PATCH 1/4] feat: add native OpenCode plugin support --- .github/workflows/consistency.yml | 2 +- README.md | 23 ++++- README.zh-CN.md | 25 ++++- docs/INSTALL_FOR_AGENTS.md | 30 +++++- docs/REFERENCE.md | 29 +++++- docs/REFERENCE.zh-CN.md | 36 +++++-- opencode-skills/agy-ask/SKILL.md | 46 +++++++++ opencode-skills/agy-implementer/SKILL.md | 56 +++++++++++ opencode-skills/agy-jobs/SKILL.md | 89 +++++++++++++++++ opencode-skills/agy-jobs/references/setup.md | 41 ++++++++ .../agy-jobs/references/troubleshooting.md | 29 ++++++ opencode-skills/agy-lead/SKILL.md | 39 ++++++++ opencode-skills/agy-researcher/SKILL.md | 51 ++++++++++ opencode-skills/agy-reviewer/SKILL.md | 62 ++++++++++++ .../agy-reviewer/references/code-review.md | 46 +++++++++ .../agy-reviewer/references/general-review.md | 22 +++++ opencode-skills/agy-staffer/SKILL.md | 53 +++++++++++ opencode.mjs | 14 +++ package.json | 17 +++- scripts/generate-pi-skills.mjs | 45 +++++---- tests/README.md | 15 ++- tests/opencode-packaging.test.mjs | 95 +++++++++++++++++++ tests/opencode.integration.mjs | 75 +++++++++++++++ 23 files changed, 897 insertions(+), 43 deletions(-) create mode 100644 opencode-skills/agy-ask/SKILL.md create mode 100644 opencode-skills/agy-implementer/SKILL.md create mode 100644 opencode-skills/agy-jobs/SKILL.md create mode 100644 opencode-skills/agy-jobs/references/setup.md create mode 100644 opencode-skills/agy-jobs/references/troubleshooting.md create mode 100644 opencode-skills/agy-lead/SKILL.md create mode 100644 opencode-skills/agy-researcher/SKILL.md create mode 100644 opencode-skills/agy-reviewer/SKILL.md create mode 100644 opencode-skills/agy-reviewer/references/code-review.md create mode 100644 opencode-skills/agy-reviewer/references/general-review.md create mode 100644 opencode-skills/agy-staffer/SKILL.md create mode 100644 opencode.mjs create mode 100644 tests/opencode-packaging.test.mjs create mode 100644 tests/opencode.integration.mjs diff --git a/.github/workflows/consistency.yml b/.github/workflows/consistency.yml index 39b8e11..5ee474f 100644 --- a/.github/workflows/consistency.yml +++ b/.github/workflows/consistency.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: '24' - - run: npm run check:pi + - run: npm run check:skills test-ubuntu: name: Tests (Ubuntu) diff --git a/README.md b/README.md index 3e64dcf..43b14c2 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@

Claude Code plugin Codex plugin license: MIT

-Hire Google's Antigravity CLI (`agy`) as a staffer for **Claude Code**, **OpenAI Codex**, and **Pi**. +Hire Google's Antigravity CLI (`agy`) as a staffer for **Claude Code**, **OpenAI Codex**, **Pi**, and **OpenCode**. ![agy-staff design](assets/design.png) @@ -63,6 +63,21 @@ Update with `pi update --extension git:github.com/keli-wen/agy-staff`, then run +
+Using OpenCode? + +With OpenCode V1 (verified on 1.18.34), install the package through its native plugin manager: + +```bash +opencode plugin 'agy-staff@git+https://github.com/keli-wen/agy-staff.git' --global +``` + +Restart OpenCode, then run `/agy-ask reply with OK`. The plugin automatically registers all seven `agy-*` skills and their native slash commands, including `/agy-lead` and `/agy-jobs`. No separate skills path or command wrappers are needed. Node.js and an authenticated `agy` are required, as above. + +OpenCode caches the complete package spec. Restarting does not refresh an unchanged Git spec. To upgrade, choose a newer Git tag or commit and run `opencode plugin 'agy-staff@git+https://github.com/keli-wen/agy-staff.git#' --global --force`, then restart. See [the reference](docs/REFERENCE.md#opencode) for config and local development. OpenCode V2 is not covered by this adapter. + +
+ Restart Claude Code or Codex afterwards. First run: `/agy:ask reply with OK` (Claude Code) or `$agy:ask reply with OK` (Codex). Ask is tool-free and needs no setup. > [!IMPORTANT] @@ -95,7 +110,7 @@ Claude Code and Codex cache per version directory, so an upgrade lands only if t ### CUJs -Examples below use Claude Code's `/agy:…`; in Codex use `$agy:…`. +Examples below use Claude Code's `/agy:…`; in Codex use `$agy:…`, in Pi `/skill:agy-…`, and in OpenCode `/agy-…`. | Use case | Invocation | |---|---| @@ -116,7 +131,7 @@ Examples below use Claude Code's `/agy:…`; in Codex use `$agy:…`. ## Core design -`lead` adds task orchestration guidance for your current agent. Within lead, orient enough to frame the assignment, delegate substantive work to `staffer` by default, wait for the result, then assess it and integrate or follow up. Specialists provide dedicated guidance when useful, while `ask` is reserved for testing. The host owns cross-task decisions, acceptance, integration, and delivery, using the existing jobs workflow. Invoke `/agy:lead` in Claude Code, `$agy:lead` in Codex, or `/skill:agy-lead` in Pi. +`lead` adds task orchestration guidance for your current agent. Within lead, orient enough to frame the assignment, delegate substantive work to `staffer` by default, wait for the result, then assess it and integrate or follow up. Specialists provide dedicated guidance when useful, while `ask` is reserved for testing. The host owns cross-task decisions, acceptance, integration, and delivery, using the existing jobs workflow. Invoke `/agy:lead` in Claude Code, `$agy:lead` in Codex, `/skill:agy-lead` in Pi, or `/agy-lead` in OpenCode. `ask` answers in the same call. The other personas return a job id and a collection command, such as `wait --timeout 10m`. Your agent waits using the host's available capabilities, with one independent background wait per job where supported. @@ -143,7 +158,7 @@ A few things worth knowing before you open a PR: - **Run the tests**: `npm test`. The standard suite uses temporary repos and HOME directories with fake `agy`, plus focused module tests. Keep regression tests offline and independent of personal settings. Real AGY validation is a separate opt-in suite described in [tests/README.md](tests/README.md). - **Docs come in pairs**: `README.md` / `README.zh-CN.md` and `docs/REFERENCE.md` / `docs/REFERENCE.zh-CN.md` are kept in sync. Change one, change its counterpart. - **Runtime code lives in `companion/`**: the entrypoint handles modes and job commands; separate modules handle streaming execution, observations and state locking. Skills call the companion, and `templates/` holds the shared prompts. -- **Canonical skills are the source of truth**: edit personas in `skills/`, never in `pi-skills/`. Run `npm run generate:pi` to generate Pi entrypoints, and `npm run check:pi` to verify consistency. +- **Canonical skills are the source of truth**: edit personas in `skills/`, never in `pi-skills/` or `opencode-skills/`. Run `npm run generate:skills` to generate both host entrypoints, and `npm run check:skills` to verify consistency. The existing `generate:pi` and `check:pi` commands remain available. Adding a mode or a flag changes the public surface, so please open an issue first and we can agree on the shape. diff --git a/README.zh-CN.md b/README.zh-CN.md index 0685b25..bed3b22 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -6,7 +6,7 @@

Claude Code plugin Codex plugin license: MIT

-把 Google 的 Antigravity CLI(`agy`)雇来当 **Claude Code**、**OpenAI Codex** 和 **Pi** 的「agy 员工」。 +把 Google 的 Antigravity CLI(`agy`)雇来当 **Claude Code**、**OpenAI Codex**、**Pi** 和 **OpenCode** 的「agy 员工」。 ![agy-staff 设计图](assets/design.png) @@ -53,6 +53,21 @@ codex plugin add agy@agy-staff +
+在 OpenCode 中安装 + +使用 OpenCode V1(已在 1.18.34 验证)的原生插件管理命令安装: + +```bash +opencode plugin 'agy-staff@git+https://github.com/keli-wen/agy-staff.git' --global +``` + +重启 OpenCode 后运行 `/agy-ask reply with OK`。插件会自动注册七个 `agy-*` 技能及其原生斜杠命令,包括 `/agy-lead` 和 `/agy-jobs`,无需单独配置技能路径或命令包装。仍需安装 Node.js,并完成 `agy` 登录。 + +OpenCode 按完整包规格缓存;仅重启不会刷新相同的 Git 规格。升级时选择新的 Git 标签或提交,运行 `opencode plugin 'agy-staff@git+https://github.com/keli-wen/agy-staff.git#' --global --force`,然后重启。配置与本地开发方式见[参考手册](docs/REFERENCE.zh-CN.md#opencode)。此适配器不覆盖 OpenCode V2。 + +
+ 安装完成后,重启 Claude Code 或 Codex,再做一次简单的验证:在 Claude Code 中输入 `/agy:ask reply with OK`,在 Codex 中输入 `$agy:ask reply with OK`。`ask` 不调用工具,也不需要额外的权限配置。 > [!IMPORTANT] @@ -80,7 +95,7 @@ install and verify the agy-staff plugin for the harness you are running in. Resp ![Codex 中的 $agy 技能选择器](assets/codex-desktop-screenshot.png) -下面的示例使用 Claude Code 的 `/agy:…` 写法。在 Codex 中把它换成 `$agy:…` 即可;Pi 使用 `/skill:agy-…`。 +下面的示例使用 Claude Code 的 `/agy:…` 写法。在 Codex 中把它换成 `$agy:…` 即可;Pi 使用 `/skill:agy-…`,OpenCode 使用 `/agy-…`。 | 想做的事 | 示例 | | --- | --- | @@ -101,7 +116,7 @@ install and verify the agy-staff plugin for the harness you are running in. Resp ## 核心设计 -`lead` 为当前主 agent 增加任务编排指导。在 lead 工作流中,主 agent 了解至足以明确任务后,默认用 `staffer` 承担实质性工作,等待结果返回后再验收、整合或追加任务;专门指导有帮助时再选择 specialist,`ask` 仅用于测试。主 agent 负责跨任务决策、验收、整合和交付,复用现有 jobs 工作流。Claude Code 使用 `/agy:lead`,Codex 使用 `$agy:lead`,Pi 使用 `/skill:agy-lead`。 +`lead` 为当前主 agent 增加任务编排指导。在 lead 工作流中,主 agent 了解至足以明确任务后,默认用 `staffer` 承担实质性工作,等待结果返回后再验收、整合或追加任务;专门指导有帮助时再选择 specialist,`ask` 仅用于测试。主 agent 负责跨任务决策、验收、整合和交付,复用现有 jobs 工作流。Claude Code 使用 `/agy:lead`,Codex 使用 `$agy:lead`,Pi 使用 `/skill:agy-lead`,OpenCode 使用 `/agy-lead`。 `ask` 会在同一次调用中返回答案。其他角色启动后会先返回任务 ID,并给出收取结果的命令,例如 `wait --timeout 10m`。主 agent 根据所在环境的能力等待任务;如果支持后台命令,就为每个任务保留一个独立的等待命令。 @@ -131,7 +146,7 @@ Codex 的更新命令是: codex plugin marketplace upgrade && codex plugin add agy@agy-staff ``` -这两个环境都按版本号管理插件缓存。如果更新后仍然看到旧行为,请先确认是否已重启应用,再参考[升级说明](docs/REFERENCE.zh-CN.md#升级)检查版本和实际安装的提交。Pi 的更新方式见上方安装说明。 +这两个环境都按版本号管理插件缓存。如果更新后仍然看到旧行为,请先确认是否已重启应用,再参考[升级说明](docs/REFERENCE.zh-CN.md#升级)检查版本和实际安装的提交。Pi 和 OpenCode 的更新方式见上方安装说明。 ## 社区 @@ -143,7 +158,7 @@ codex plugin marketplace upgrade && codex plugin add agy@agy-staff 提交代码前请运行 `npm test`。标准测试使用临时仓库、临时 HOME 和假的 agy,不会调用真实模型或改动你的个人配置。新增回归测试也应保持这一点。需要验证真实 AGY 时,请使用[测试说明](tests/README.md)中单独启用的集成测试。 -运行逻辑位于 `companion/`:入口负责模式和任务命令,独立模块负责流式执行、进度快照和状态锁。角色技能位于 `skills/`,共享的提示词模板位于 `templates/`。修改技能时请以 `skills/` 为准,再运行 `npm run generate:pi` 生成 `pi-skills/`,用 `npm run check:pi` 检查两者是否一致。 +运行逻辑位于 `companion/`:入口负责模式和任务命令,独立模块负责流式执行、进度快照和状态锁。角色技能位于 `skills/`,共享的提示词模板位于 `templates/`。修改技能时请以 `skills/` 为准,再运行 `npm run generate:skills` 生成 `pi-skills/` 与 `opencode-skills/`,用 `npm run check:skills` 检查一致性。不要直接编辑生成文件;原有 `generate:pi` 和 `check:pi` 命令仍然可用。 README 和参考手册都有中英文版本。修改使用方法或行为说明时,请同步更新对应版本,让两种语言的读者得到一致的信息。 diff --git a/docs/INSTALL_FOR_AGENTS.md b/docs/INSTALL_FOR_AGENTS.md index 961b5ae..ff6607c 100644 --- a/docs/INSTALL_FOR_AGENTS.md +++ b/docs/INSTALL_FOR_AGENTS.md @@ -17,9 +17,10 @@ You normally know which product you are. If unsure, check: - **Claude Code** — you have `/plugin` slash commands, project instructions arrive via `CLAUDE.md`, and your Bash tool typically has `CLAUDECODE=1` in the environment. → Follow section 2a. - **Codex** — you invoke skills with `$name` syntax, follow `AGENTS.md`, and plugins are managed through the `codex` CLI. → Follow section 2b. - **Pi** — you invoke skills with `/skill:name` and manage packages with `pi install`. → Follow section 2c. -- **Anything else** — this package documents Claude Code, Codex, and Pi. Report the unsupported harness and stop. +- **OpenCode V1** — you use `opencode`, its `plugin` config or `opencode plugin` installer, and native `/agy-*` commands. Verified with OpenCode 1.18.34; V2 is not covered. → Follow section 2d. +- **Anything else** — this package documents Claude Code, Codex, Pi, and OpenCode V1. Report the unsupported harness and stop. -Follow exactly one of the three sections below. +Follow exactly one of the four sections below. ## 2a. Claude Code — install / upgrade @@ -78,6 +79,26 @@ pi update --extension git:github.com/keli-wen/agy-staff Restart Pi or run `/reload` afterwards. Use `pi list` to verify the package is registered, then check Pi's skill picker for `agy-lead`, `agy-ask`, `agy-staffer`, `agy-researcher`, `agy-reviewer`, `agy-implementer`, and `agy-jobs`. +## 2d. OpenCode V1 — install / upgrade / local development + +Use the native plugin installer (verified with OpenCode 1.18.34): + +```bash +opencode plugin 'agy-staff@git+https://github.com/keli-wen/agy-staff.git' --global +``` + +This installs the full package and adds it to the `plugin` array in OpenCode's config, preserving other settings. Do not replace the user's config file. The plugin registers its bundled skills automatically; do not manually clone the repository or add `skills.paths` as an installation workaround. For an explicitly provided local checkout, use `opencode plugin /absolute/path/to/checkout --global`. Regenerate modified canonical skills with `npm run generate:skills` in that checkout. + +Restart OpenCode, then verify with `opencode debug skill`: expect `agy-ask`, `agy-staffer`, `agy-researcher`, `agy-reviewer`, `agy-implementer`, `agy-jobs`, and `agy-lead` alongside any other installed skills. OpenCode exposes these directly as `/agy-*` commands. Existing skill permissions remain in effect. Install the entire package, since its companion, templates and references are relative resources. + +OpenCode 1.18.34 caches the full package spec. Restarting or reinstalling the same Git spec does not guarantee an update, even with `--force`. Select a newer tag or commit containing the adapter, replace the placeholder below, then restart: + +```bash +opencode plugin 'agy-staff@git+https://github.com/keli-wen/agy-staff.git#' --global --force +``` + +Changing the ref creates a fresh cache key; `--force` replaces the configured plugin entry. Do not clear unrelated caches. This adapter covers V1, not V2. + ## 3. Smoke test Run the zero-setup ask mode — it needs no allowlist and answers in ~3 seconds: @@ -85,15 +106,18 @@ Run the zero-setup ask mode — it needs no allowlist and answers in ~3 seconds: - Claude Code: `/agy:ask "reply with OK"` — **after the restart**, otherwise you are testing the old copy or nothing at all - Codex: `$agy:ask reply with OK` - Pi: `/skill:agy-ask reply with OK` — after restart or `/reload` +- OpenCode: `/agy-ask reply with OK` — after restart If you cannot restart the session, call the companion of the freshly installed copy directly from the shell. It is the same code path the skill takes, so a pass here means the install is sound: +For Claude Code, resolve its installed copy with: + ```bash AGY_ROOT=$(node -p 'require(process.env.HOME+"/.claude/plugins/installed_plugins.json").plugins["agy@agy-staff"][0].installPath') node "$AGY_ROOT/companion/agy-companion.mjs" ask --prompt "reply with OK" ``` -Resolve the root that way rather than globbing `cache/agy-staff/agy/*/`: superseded version directories are left behind after an upgrade, so the glob expands to several paths and the command fails with `unknown subcommand`. `installPath` is always the copy in use. (Codex's equivalent root is printed by `codex plugin list`.) A fallback pass still leaves the restart outstanding — report it as "installed and verified, restart Claude Code to use it". +Resolve the root that way rather than globbing `cache/agy-staff/agy/*/`: superseded version directories are left behind after an upgrade, so the glob expands to several paths and the command fails with `unknown subcommand`. `installPath` is always the copy in use. (Codex's equivalent root is printed by `codex plugin list`.) For OpenCode, `opencode debug skill` reports each skill location; resolve `../../companion/agy-companion.mjs` from the installed `agy-ask` skill directory and invoke it with `node` from the user’s project directory. For Pi, use the registered package root from `pi list`. A fallback pass still leaves the restart outstanding — report it as "installed and verified, restart the current harness to use it". Expect a short answer on stdout with no telemetry mixed in, plus an `[agy-staff]` telemetry line on stderr (mode, profile, model, duration, tokens, conversation id — for you, not for the user). If it errors, relay the error verbatim; the usual causes are expired agy auth (user runs `agy` interactively once to re-login) or an invalid model id (`agy models` lists valid ids). Do not improvise flags to work around errors. diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 4f3bd76..7416ba7 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -12,11 +12,11 @@ Back to the [README](../README.md). See the [Chinese reference](REFERENCE.zh-CN. | `reviewer` | `review` | Second-opinion verifier, two flavors routed by subject: code review (severity-ranked findings with `file:line` refs) and general review (multi-angle challenge of a plan, design, or decision) | `gemini-3.8-flash-medium` | unrestricted | background job — returns a job id | | `implementer` | `implement` | Well-scoped coding task; agy edits the working tree and can perform explicitly requested Git delivery | `gemini-3.8-flash-high` | unrestricted | background job — returns a job id | -`lead` provides task orchestration guidance for the current agent, reusing the existing companion modes without adding a mode of its own; invoke `/agy:lead` in Claude Code, `$agy:lead` in Codex, or `/skill:agy-lead` in Pi. +`lead` provides task orchestration guidance for the current agent, reusing the existing companion modes without adding a mode of its own; invoke `/agy:lead` in Claude Code, `$agy:lead` in Codex, `/skill:agy-lead` in Pi, or `/agy-lead` in OpenCode. Execution style is fixed per mode and cannot be overridden by a flag. `continue` inherits the resolved mode's style (continuing an `ask` stays synchronous; continuing the others returns a job id). -Claude Code, Codex, and Pi surface the same personas, backed by one companion script (`companion/agy-companion.mjs`, Node stdlib only) and shared prompt templates (`templates/`). Invocation tokens: `/agy:` on Claude Code, `$agy:` on Codex, and `/skill:agy-` on Pi. Pi's manifest exposes only `pi-skills/`, generated mechanically from canonical `skills/` via `npm run generate:pi`. Generated skills use `agy-` prefixes, rewrite sibling references, and append `templates/harness-compatibility.md` (directing the host to adapt missing tools to equivalent methods without dropping requirements, or ask for help). Job management (`wait`/`status`/`result`/`cancel`/`continue`/`setup`) lives in `jobs` (`agy-jobs` on Pi) plus the companion CLI — ask for it in natural language ("is the agy job done?"). +Claude Code, Codex, Pi, and OpenCode surface the same personas, backed by one companion script (`companion/agy-companion.mjs`, Node stdlib only) and shared prompt templates (`templates/`). Invocation tokens: `/agy:` on Claude Code, `$agy:` on Codex, `/skill:agy-` on Pi, and `/agy-` on OpenCode. Pi's manifest exposes only `pi-skills/`, generated mechanically from canonical `skills/` via `npm run generate:pi`. Generated skills use `agy-` prefixes, rewrite sibling references, and append `templates/harness-compatibility.md` (directing the host to adapt missing tools to equivalent methods without dropping requirements, or ask for help). Job management (`wait`/`status`/`result`/`cancel`/`continue`/`setup`) lives in `jobs` (`agy-jobs` on Pi and OpenCode) plus the companion CLI — ask for it in natural language ("is the agy job done?"). ## The two-profile permission model @@ -255,12 +255,31 @@ If the installed `agy` CLI does not support Gemini 3.8 Flash, the companion fail Windows is supported on a best-effort basis and exercised by the `Tests (Windows)` CI job; it has not yet been validated against a real Windows `agy` installation. Subprocesses are spawned with `windowsHide: true` so no console windows appear during background execution. Job cancellation and process cleanup discover descendant processes via PowerShell (`Get-CimInstance Win32_Process`, with `CreationDate` in round-trip precision) and terminate each identified member individually; the leader falls to `taskkill /PID /F`, never `/T`. A parent link is only followed when the child was created after its parent: Windows keeps a dead parent's PID in `ParentProcessId`, so once that PID is reused an unrelated orphan (typically another job's detached worker) would otherwise look like a descendant and be killed. State locking retries transient Windows errors (`EPERM`/`EBUSY`/`EACCES`) when renaming or unlinking lock directories and marker files. +## OpenCode + +The dependency-free `opencode.mjs` entrypoint implements the OpenCode V1 plugin config hook (verified with 1.18.34). Install the full package with `opencode plugin 'agy-staff@git+https://github.com/keli-wen/agy-staff.git' --global`, then restart OpenCode and run `/agy-ask reply with OK`. The companion requires Node.js and an authenticated `agy` on PATH. OpenCode V2 is not covered. + +The native installer adds a package entry to the `plugin` array in the OpenCode config. The equivalent configuration is: + +```json +{ + "plugin": ["agy-staff@git+https://github.com/keli-wen/agy-staff.git"] +} +``` + +The plugin appends its absolute bundled `opencode-skills/` directory to `skills.paths`, preserving existing paths and avoiding duplicate registrations. OpenCode discovers seven branded skills and exposes them as same-named slash commands: `/agy-ask`, `/agy-staffer`, `/agy-researcher`, `/agy-reviewer`, `/agy-implementer`, `/agy-jobs`, and `/agy-lead`. No command wrappers are installed. Existing OpenCode skill permissions still apply. + +`npm run generate:opencode` generates these entrypoints from canonical `skills/`, copying all assets, rewriting invocation syntax and sibling paths, and appending the same host compatibility context as Pi. The companion and prompt templates remain shared. Install the whole package; copying only the plugin file would lose its relative resources. + +For local development, run `opencode plugin /absolute/path/to/checkout --global`, generate skills with `npm run generate:skills`, and restart OpenCode. This reads the checkout directly. For Git installs, OpenCode 1.18.34 caches by the full package spec; restart or `--force` alone does not refresh an unchanged spec. Upgrade by choosing a newer tag or commit and running `opencode plugin 'agy-staff@git+https://github.com/keli-wen/agy-staff.git#' --global --force`, then restart. The selected ref must contain this adapter. `--force` replaces the configured plugin entry; changing the ref gives the new install a distinct cache key. + ## Upgrading Claude Code and Codex cache the plugin under a per-**version** directory (e.g. `cache/agy-staff/agy/0.4.0`) and key "is it current?" on that version string, not on the commit. Bump their manifests and `package.json` together when preparing a release. Pi's Git source instead follows the configured ref; local sources read the checkout directly. - **Claude Code** — `claude plugin marketplace update agy-staff` refreshes the marketplace clone, then `claude plugin update agy@agy-staff` re-copies it into the cache. `install` is **not** the upgrade command: on an already-installed plugin it answers "already installed" and does nothing, whatever the version. And `update` only moves if the version string changed — on an unchanged version it answers "already at the latest version" and leaves the old commit in place. Force the current commit in with `claude plugin uninstall agy@agy-staff && claude plugin install agy@agy-staff`. Restart Claude Code afterwards either way — skills are registered at session start. - **Codex** — bump the version, run `codex plugin marketplace upgrade` (or remove and re-add the marketplace entry), then restart the app. +- **OpenCode** — use a newer Git tag/commit in the native plugin installer with `--force`, then restart; see [OpenCode](#opencode). - **Pi** — for an unpinned Git install, run `pi update --extension git:github.com/keli-wen/agy-staff`, then `/reload`. For local development, regenerate Pi skills (`npm run generate:pi`) and run `/reload`; no push is needed. You can check which commit is actually installed: the `gitCommitSha` in `~/.claude/plugins/installed_plugins.json`, versus `git -C ~/.claude/plugins/marketplaces/agy-staff log -1` for what the marketplace clone has fetched. @@ -277,8 +296,10 @@ templates/ shared prompt templates (staffer/ask/research/revi .codex-plugin/plugin.json Codex plugin manifest .agents/plugins/ Codex marketplace manifest pi-skills/ generated agy-* entrypoints/resources for Pi; do not hand-edit -scripts/generate-pi-skills.mjs generates Pi skills and checks for drift -package.json Pi manifest, npm file allowlist, and verification commands +opencode-skills/ generated agy-* entrypoints/resources for OpenCode; do not hand-edit +opencode.mjs OpenCode V1 package plugin; registers bundled skills +scripts/generate-pi-skills.mjs generates Pi/OpenCode skills and checks for drift +package.json Pi manifest, OpenCode entrypoint, npm file allowlist, and verification commands skills/ canonical personas + jobs (Claude/Codex entrypoints; reviewer/ and jobs/ carry references/ for on-demand detail) assets/ design diagram + logo + badges diff --git a/docs/REFERENCE.zh-CN.md b/docs/REFERENCE.zh-CN.md index 5197982..9a7bcf4 100644 --- a/docs/REFERENCE.zh-CN.md +++ b/docs/REFERENCE.zh-CN.md @@ -14,17 +14,17 @@ | `reviewer` | `review` | 审查代码、方案或决策 | `gemini-3.8-flash-medium` | 返回后台任务 ID | | `implementer` | `implement` | 完成范围明确的编码任务 | `gemini-3.8-flash-high` | 返回后台任务 ID | -`lead` 为当前主 agent 提供任务编排指导,复用现有 companion 模式,没有自己的运行模式;Claude Code 使用 `/agy:lead`,Codex 使用 `$agy:lead`,Pi 使用 `/skill:agy-lead`。 +`lead` 为当前主 agent 提供任务编排指导,复用现有 companion 模式,没有自己的运行模式;Claude Code 使用 `/agy:lead`,Codex 使用 `$agy:lead`,Pi 使用 `/skill:agy-lead`,OpenCode 使用 `/agy-lead`。 `staffer` 不预设专业分工或固定的报告格式,但仍遵守共享的操作约定。`reviewer` 会根据对象选择审查方式:代码问题按严重程度列出,并附上 `file:line` 位置;方案和决策审查则检查假设、风险和取舍。`implementer` 可以直接修改工作区,也可以完成任务明确要求的提交、推送或 PR 操作。 执行方式由模式决定,不能通过参数切换。继续一个 `ask` 会话时,答案仍在同一次调用中返回;继续其他模式时,会创建新的后台任务。 -Claude Code 使用 `/agy:`,Codex 使用 `$agy:`,Pi 使用 `/skill:agy-`。三者共用 `companion/` 中的运行逻辑和 `templates/` 中的提示词模板。companion 只依赖 Node.js 标准库。 +Claude Code 使用 `/agy:`,Codex 使用 `$agy:`,Pi 使用 `/skill:agy-`,OpenCode 使用 `/agy-`。四者共用 `companion/` 中的运行逻辑和 `templates/` 中的提示词模板。companion 只依赖 Node.js 标准库。 Pi 加载的入口位于 `pi-skills/`,由 `npm run generate:pi` 根据 `skills/` 自动生成。生成过程会添加 `agy-` 前缀、调整技能之间的相对路径,并附上 `templates/harness-compatibility.md`。这份兼容说明要求主 agent 在工具不可用时寻找等价方法,保留原有要求;无法做到时再向用户求助。 -任务管理由 `jobs` 技能和 companion CLI 共同完成,在 Pi 中对应 `agy-jobs`。通常直接对主 agent 说“agy 的任务进展如何”或“继续刚才的任务”即可,不需要手动记住管理命令。 +任务管理由 `jobs` 技能和 companion CLI 共同完成,在 Pi 和 OpenCode 中对应 `agy-jobs`。通常直接对主 agent 说“agy 的任务进展如何”或“继续刚才的任务”即可,不需要手动记住管理命令。 @@ -345,6 +345,24 @@ AGY 会读取工作区中的 `AGENTS.md`、`GEMINI.md` 和 `.agents/rules/*.md` Windows 为尽力支持,由 CI 的 `Tests (Windows)` 任务覆盖,尚未在真实的 Windows `agy` 安装上验证。子进程均以 `windowsHide: true` 启动,避免后台执行期间弹出控制台窗口。任务取消与进程清理通过 PowerShell(`Get-CimInstance Win32_Process`,`CreationDate` 使用往返精度)发现子孙进程,并逐个终止已确认身份的成员;组长进程使用 `taskkill /PID /F`,不再使用 `/T`。父子链接只有在子进程创建时间晚于父进程时才被采信:Windows 会在 `ParentProcessId` 中保留已退出父进程的 PID,该 PID 被复用后,一个无关的孤儿进程(通常是另一个任务的后台 worker)否则会被误认为子孙而被杀掉。状态锁针对 Windows 目录与标记文件的重命名和删除瞬态错误(`EPERM`/`EBUSY`/`EACCES`)进行了自动重试。 +## OpenCode + +`opencode.mjs` 是无额外依赖的 OpenCode V1 插件,使用 config hook 注册技能,已在 1.18.34 验证。运行 `opencode plugin 'agy-staff@git+https://github.com/keli-wen/agy-staff.git' --global` 安装完整包,重启后运行 `/agy-ask reply with OK`。companion 需要 Node.js 以及 PATH 中已完成登录的 `agy`。此适配器不覆盖 OpenCode V2。 + +原生安装器会在 OpenCode 配置的 `plugin` 数组中添加包条目,等价配置为: + +```json +{ + "plugin": ["agy-staff@git+https://github.com/keli-wen/agy-staff.git"] +} +``` + +插件将包内 `opencode-skills/` 的绝对路径追加到 `skills.paths`,保留已有路径并避免重复注册。OpenCode 会发现七个带品牌前缀的技能,并提供同名原生斜杠命令:`/agy-ask`、`/agy-staffer`、`/agy-researcher`、`/agy-reviewer`、`/agy-implementer`、`/agy-jobs` 和 `/agy-lead`。无需额外命令包装,已有 OpenCode 技能权限仍然适用。 + +`npm run generate:opencode` 从唯一方法源 `skills/` 生成入口,复制所有资源、改写调用方式与相对路径,并附上与 Pi 相同的主环境兼容说明。companion 与提示词模板继续共用。请安装完整包;单独复制插件文件会缺失相对路径引用的资源。 + +本地开发时运行 `opencode plugin /absolute/path/to/checkout --global`,修改源文件后运行 `npm run generate:skills` 并重启 OpenCode;本地安装直接读取检出目录。Git 安装按完整包规格缓存,仅重启或对相同规格使用 `--force` 不会刷新缓存。升级时选择新的标签或提交,运行 `opencode plugin 'agy-staff@git+https://github.com/keli-wen/agy-staff.git#' --global --force`,然后重启。所选引用必须包含此适配器。`--force` 替换配置中的插件条目,变更引用则提供新的缓存键。 + ## 升级 Claude Code 和 Codex 按版本号缓存插件,例如 `cache/agy-staff/agy/0.4.0`。缓存是否需要更新取决于版本号,而不是仓库的最新提交。因此,准备发布时需要同步更新两个插件 manifest 和 `package.json` 中的版本。 @@ -361,6 +379,10 @@ Claude Code 和 Codex 按版本号缓存插件,例如 `cache/agy-staff/agy/0.4 发布新版本后,运行 `codex plugin marketplace upgrade` 更新插件市场,再按安装流程更新插件并重启应用。必要时也可以移除并重新添加插件市场条目。若仍然出现旧行为,应先确认插件版本和当前会话加载的副本。 +### OpenCode + +使用带新 Git 标签或提交的原生插件安装命令,加上 `--force`,然后重启;详见 [OpenCode](#opencode)。 + ### Pi Pi 的 Git 安装跟随所配置的分支或引用,本地路径安装则直接读取检出目录。没有固定版本的 Git 安装可以运行 `pi update --extension git:github.com/keli-wen/agy-staff`,然后在 Pi 中执行 `/reload`。 @@ -369,7 +391,7 @@ Pi 的 Git 安装跟随所配置的分支或引用,本地路径安装则直接 ## 仓库结构 -`skills/` 是角色技能的源文件,`pi-skills/` 是为 Pi 生成的入口。两者共用 `templates/` 中的提示词和 `companion/` 中的运行逻辑。 +`skills/` 是角色技能的源文件,`pi-skills/` 和 `opencode-skills/` 是为对应环境生成的入口。它们共用 `templates/` 中的提示词和 `companion/` 中的运行逻辑。 ```text companion/agy-companion.mjs 命令入口、模式选择、任务管理与 setup @@ -379,11 +401,13 @@ companion/state-lock.mjs 状态更新与锁回收 skills/ 角色技能与 jobs 管理技能,以及按需加载的参考文件 pi-skills/ 自动生成的 Pi 入口与参考文件,不应手动编辑 templates/ 共享提示词模板与宿主兼容说明 -scripts/generate-pi-skills.mjs 生成 Pi 技能并检查一致性 +opencode-skills/ 自动生成的 OpenCode 入口与参考文件,不应手动编辑 +opencode.mjs OpenCode V1 包入口,注册包内技能 +scripts/generate-pi-skills.mjs 生成 Pi/OpenCode 技能并检查一致性 .claude-plugin/ Claude Code 插件与插件市场配置 .codex-plugin/plugin.json Codex 插件配置 .agents/plugins/ Codex 插件市场配置 -package.json Pi 包配置、npm 打包范围与验证命令 +package.json Pi 包配置、OpenCode 入口、npm 打包范围与验证命令 tests/ 离线回归测试,以及单独启用的集成测试 assets/ 图片、徽标与徽章 docs/ 参考手册、安装说明和发布记录 diff --git a/opencode-skills/agy-ask/SKILL.md b/opencode-skills/agy-ask/SKILL.md new file mode 100644 index 0000000..ce7c872 --- /dev/null +++ b/opencode-skills/agy-ask/SKILL.md @@ -0,0 +1,46 @@ +--- +name: agy-ask +description: Ask Google's Antigravity CLI (agy staffer, fast Gemini) a cheap one-shot question - the fast zero-tool mode and the post-install smoke test. Use when the user says /agy-ask, "ask agy", "quick second opinion from agy", or right after installing to verify the plugin works. +--- + + + +# agy ask + +The quick mode: one question in, one answer out, ~3 seconds on the default `gemini-3.8-flash-low`. Zero tools by design (restricted profile, question-only prompt), so it needs no setup and works on a fresh install — run it first as the smoke test: `ask --prompt "reply with OK"`. + +ask is the only persona that runs in the foreground: the call blocks and the answer comes back on stdout. staffer, researcher, reviewer, and implementer instead return a background job id (see the jobs skill, `../agy-jobs/SKILL.md`). + +## Locating the companion + +This skill file lives at `/opencode-skills/agy-ask/SKILL.md`; resolve the companion path relative to this skill directory: + +```bash +node "/../../companion/agy-companion.mjs" ask [flags] --prompt "question" +``` + +Pass the user's question verbatim via `--prompt`; use `--prompt-file ` or `--stdin` for a long question. + +> [!IMPORTANT] +> Run this command **unsandboxed** — agy needs a localhost port and its OAuth token file, which harness sandboxes hide. In Codex, request escalated permissions for the command. Details: `../agy-jobs/references/troubleshooting.md`. + +## Flags (all optional) + +- `--prompt ` / `--prompt-file ` / `--stdin` — the question, from exactly one of these three sources. Use file/stdin for a long question. +- `--continue` — reuse the last ask conversation; `--conversation ` targets a specific one. +- `--model ` or `--effort low|medium|high` — default model is `gemini-3.8-flash-low`. +- `--timeout ` — default 2m. + +ask is always restricted (it is tool-free, so there is nothing to unrestrict); `--unrestricted` is ignored with a note on stderr. That is fixed for ask alone — the tool-using personas default to unrestricted, where `--restricted` is an opt-in hardening flag. Execution style is likewise fixed per mode and no flag changes it. + +## Rules + +- Pass the user's question through verbatim and return the answer verbatim. The `[agy-staff]` telemetry line arrives on stderr and is metadata for you, the calling agent — do not show it to the user; mention the follow-up ability in natural language when relevant, and give model/duration/token numbers only if asked. +- If the answer says "not sure", relay it as-is; do not silently substitute your own answer. +- Exit 5 means a resumable response timeout: explain it and ask whether the user wants to continue with the suggested timeout or stop. Continue only after explicit user confirmation, using the recorded conversation and configuration. For other companion errors, quote the error and add a concise diagnosis. Full failure protocol: `../agy-jobs/SKILL.md`. + +## Host compatibility + +When this skill or its referenced instructions require a tool that the current environment does not provide, use available capabilities to achieve an equivalent result. Adapt only the tool-specific execution method; preserve the task goal, authorization requirements, explicit confirmation steps, result delivery, and stopping conditions. + +If an equivalent result cannot be achieved, or you cannot establish that an alternative is equivalent, explain the missing capability and its impact, and ask the user for help. Do not silently skip requirements or bypass the environment's restrictions. diff --git a/opencode-skills/agy-implementer/SKILL.md b/opencode-skills/agy-implementer/SKILL.md new file mode 100644 index 0000000..0771f25 --- /dev/null +++ b/opencode-skills/agy-implementer/SKILL.md @@ -0,0 +1,56 @@ +--- +name: agy-implementer +description: Delegate a coding task to Google's Antigravity CLI (agy staffer, fast Gemini), which edits the working tree directly and can perform explicitly requested Git delivery. Use when the user says /agy-implementer, "have agy fix/build X", or wants to hand a well-scoped coding task to the agy staffer instead of doing it in the host model. +--- + + + +# agy implementer + +Hand a coding task to the agy staffer. agy edits the real working tree under its unrestricted permission profile; the companion is a thin launcher and job collector. Pass the user's requested delivery through in the task text instead of doing Git work yourself. + +## Locating the companion + +This skill file lives at `/opencode-skills/agy-implementer/SKILL.md`; resolve the companion path relative to this skill directory: + +```bash +node "/../../companion/agy-companion.mjs" implement [flags] --prompt "task description" +``` + +Pass the user's task description verbatim via `--prompt`; use `--prompt-file ` or `--stdin` for long text. + +> [!IMPORTANT] +> Run this command **unsandboxed** — agy needs a localhost port and its OAuth token file, which harness sandboxes hide. In Codex, request escalated permissions for the command. Details: `../agy-jobs/references/troubleshooting.md`. (The companion passes `--dangerously-skip-permissions` to agy in this mode — that is the unrestricted profile working as designed.) + +## Workspace and delivery + +- Inside a git repository, dirty workspaces are allowed. When `git status --porcelain` is not clean, the companion injects a bounded pre-run status summary into the implement prompt so agy treats those paths as user-owned context. +- Outside a git repository the companion warns that agy's edits cannot be reviewed or rolled back via git, and proceeds. Relay that warning; there is no diff to fall back on. +- By default, agy leaves a working-tree diff. If the user explicitly asks for a commit, push, or PR, include that request verbatim in the task text and let agy do that exact Git delivery. +- After the run, surface agy's summary and the current workspace state. Do not add your own commit/PR step unless the user separately asks you to do it. + +## Collecting the result + +The command returns a job id. Read `../agy-jobs/SKILL.md` for result collection and recovery: dispatch, wait for the final result, then validate as needed. Do not proactively observe progress, read logs or inspect intermediate artifacts while running. Observe only when the user explicitly asks for progress; diagnose a failure or a result requiring intervention under the jobs protocol. + +## Flags (all optional) + +- `--restricted` / `--unrestricted` — permission profile. implement defaults to unrestricted, so it works out of the box with no setup. `--restricted` is the opt-in hardening path: agy may then only use allowlisted tools, so it can usually only propose rather than edit, and it needs the setup flow's evidence-gathering allowlist to be useful. +- `--continue` (or `--conversation `), `--model ` / `--effort low|medium|high` (default `gemini-3.8-flash-high`), `--timeout ` (default 60m, maximum 120m hard execution limit). +- `--prompt ` / `--prompt-file ` / `--stdin` — the task, from exactly one of these three sources. Use file/stdin for long prompts. + +## Rules + +- Do not pre-implement, extend, or "clean up" agy's changes without user confirmation. +- Return agy's summary verbatim before presenting the diff. +- Pass the user's explicit authorizations through to the task string verbatim. The prompt template default-denies costly or irreversible side effects; that default opens only when the request itself asks for the operation — so keep "open a draft PR", "run the e2e tests", or "call the staging API" in the prompt instead of trimming it. +- Never commit agy's changes yourself unless the user explicitly asks you, the host agent, to do it. +- For errors and recovery, follow `../agy-jobs/SKILL.md`. + +For an existing conversation, `--continue` / `--conversation ` inherit its recorded model and permission profile unless explicitly overridden. The unrestricted defaults above apply to new tasks. + +## Host compatibility + +When this skill or its referenced instructions require a tool that the current environment does not provide, use available capabilities to achieve an equivalent result. Adapt only the tool-specific execution method; preserve the task goal, authorization requirements, explicit confirmation steps, result delivery, and stopping conditions. + +If an equivalent result cannot be achieved, or you cannot establish that an alternative is equivalent, explain the missing capability and its impact, and ask the user for help. Do not silently skip requirements or bypass the environment's restrictions. diff --git a/opencode-skills/agy-jobs/SKILL.md b/opencode-skills/agy-jobs/SKILL.md new file mode 100644 index 0000000..b068024 --- /dev/null +++ b/opencode-skills/agy-jobs/SKILL.md @@ -0,0 +1,89 @@ +--- +name: agy-jobs +description: Manage agy staffer background jobs - collect results, check status, cancel, follow-up conversation, and setup. Use when an agy job needs collecting, when the user asks "is the agy job done", "show agy's result", "cancel the agy job", "continue the agy conversation", or "set up agy". This is the orchestrator's skill; the persona skills (staffer/researcher/reviewer/implementer) point here. +--- + + + +# agy jobs + +Manage background staffer/research/review/implement jobs. State is per repository in `.agy-staff/`. Only ask runs synchronously. + +This file lives at `/opencode-skills/agy-jobs/SKILL.md`: + +```bash +node "/../../companion/agy-companion.mjs" [args] +``` + +## Collect the result + +Default flow: prepare the prompt → dispatch → wait for the final result → validate as needed. While a job is running, do not proactively call `observe`/`status`, read logs or inspect intermediate artifacts. Do not query progress for routine updates or create sleep/observe loops. Observe only when the user explicitly asks for progress; diagnose after receiving a failure or a result requiring intervention. + +1. Keep the returned job id. Start `wait --timeout 10m` in the background, using the same unsandboxed context as launch. Use a separate wait for each job; never wait for several jobs serially in one shell. +2. For **wait**, branch on the exit code: + +| Code | Meaning | Next action | +| --- | --- | --- | +| 0 | Invocation ended; response text delivered | Assess whether it satisfies the task. Review attached diagnostics; inspect further only as needed. | +| 2 | Still running; wait soft-expired | Wait again for the same job. The attached snapshot is not a request to inspect progress or intervene. | +| 3 | Error or crash | Read the error and recovery information below. | +| 4 | Canceled | Complete any already-authorized follow-up; otherwise report cancellation. | +| 5 | Attention: resumable timeout | Inspect partial workspace changes; ask whether to continue with the suggested timeout or stop. Continue only after explicit user confirmation. | +| 1 | Invalid command or other command error | Quote the error and correct the named problem. | + +`done` describes invocation and response delivery, not task acceptance. Preserve agy-cli response text and diagnostics; a nonempty response may only acknowledge launched background work. Successful calls with warnings include a bounded log tail on stderr and a full-log pointer. The orchestrator assesses the response and artifacts, uses `observe` or diagnostics if the returned result needs investigation, and decides whether to propose continuation. Keep the recovery confirmation rules below; do not infer timeout from response wording or add routine progress polling. + +When the user explicitly asks for progress, use `observe ` and answer from that snapshot, keeping any pending wait open. An already returned snapshot may answer the question; do not duplicate it or turn one question into recurring observation. + +Collecting a pending wait command is necessary result collection, not active observation. Host collectors (for example, Codex `write_stdin`) return that command's output; an outer `functions.wait` resumes a yielded `functions.exec` call. They do not independently read AGY progress. Prefer background completion delivery; if host collection requires polling, use a long supported blocking wait rather than short empty polls or sleep loops. Keep the same pending command until it returns; only restart `wait` for the same job after exit 2. + +A wait expires without stopping the worker. Cancel only when the task calls for stopping; silence or soft expiry alone is not a reason. For a user-requested progress answer or diagnosis after failure/required intervention, read a bounded `details` excerpt only if the returned information leaves a specific question unanswered. Never inspect logs or intermediate artifacts for routine reassurance. + +When using lead, let the lead assess and synthesize the results. For direct persona invocations, deliver short results verbatim; summarize long results with their file path. Keep quoted verdicts, numbers and errors exact. For implement, also report the workspace state and inspect changes with `git diff`; verify any Git delivery that the user explicitly requested. + +Follow through to a result unless the user asked only to launch. If the host cannot deliver background command results, use the longest practical wait within its tool-call limit (bare `wait` defaults to 100s). The host controls when the model receives a tool result; this plugin cannot schedule a future model invocation by itself. + +## Other commands + +| Command | Purpose | +| --- | --- | +| `observe [id]` | On an explicit user progress request, or for diagnosis after failure/required intervention: bounded JSON with progress or terminal metadata and recovery pointers. Never returns report text. | +| `status [id]` | List jobs or show one job's state and log tail. | +| `result [id]` | Reprint stored output; default to the latest finished job. | +| `cancel ` | Stop that job's execution. Interrupting wait does not cancel it. | +| `continue --job --prompt "..."` | Resume the job's conversation with its original mode/model/profile; create a linked new job. | +| `continue --prompt "..."` | Continue the latest conversation; `--conversation ` selects a known older one. | +| `restart ` | Start the original task/configuration again without its conversation; create a linked new job. | +| `setup [--apply] [--restrict ]` | Optional permission setup; read `references/setup.md` first. | + +Wait/observe default to the latest job. Observe uses the same status exit codes, but exit 0 means **finished, not full result delivered**. Collect the existing wait session, or use `result ` if none is pending; do not start another wait or expect observe to consume the pending session. Failed/canceled/attention observations provide bounded recovery metadata; wait/result deliver the full report. A continued ask remains synchronous. `result` also returns exit 5 for attention (its legacy exit behavior for other terminal states is unchanged). + +Continuation and restart may be invoked from the root or any subdirectory of the same worktree; execution returns to the original cwd. Generic `continue` fails for an unrecorded conversation ID. It does not search other worktrees or infer configuration from an unrelated conversation. For continuation, explicit model/profile flags override the inherited values. + +Cancel records a request first and returns success only after the worker has stored the cancellation report and published `canceled`. A crashed job keeps its crash diagnostics. A cancellation error requires inspection; it does not mean execution has stopped. + +## Follow-up instructions + +Use `continue --job ` to target an existing AGY conversation. It starts a new invocation once the current execution has stopped. While that job is still running, the companion refuses the follow-up with exit 1, reporting the job ID and status; nothing is queued. Decide whether to wait or cancel. + +- **Finished:** continue directly with the next assignment or revision. +- **Running, feedback can wait:** collect the current result, then continue. +- **Running, direction must change now:** cancel the active job, confirm termination, then continue with the updated brief. An already-authorized change of direction does not require another confirmation solely for this sequence. + +Include relevant decisions made in the host conversation, what changed, and what the worker should do next. After interruption, reconcile partial artifacts before repeating work: cancellation does not roll back edits, and the conversation may not contain the last interrupted step. If no usable conversation exists, start a fresh task with the updated brief and retained work. Timeout recovery still follows the rules below. + +## Progress and recovery + +Progress contains up to five recent tool calls, input/output excerpts, and the latest response text. Timestamps, incomplete text and truncation are labeled. It is a snapshot, not a judgment of useful progress. Reads do not consume history or reset deadlines. Payload limits and file layout are in `../../docs/REFERENCE.md`. + +The worker has a separate hard limit: default 60m, configurable with launch `--timeout` up to 120m. AGY receives the same response timeout; the worker independently enforces the overall budget, including initialization. At that limit it stops execution. If response text has already arrived, it delivers that text with a warning for the orchestrator to assess; otherwise it reports `hard_timeout`, the last snapshot, logs, known conversation ID and original configuration. Before recovery, inspect `git status` and `git diff` so partial changes are accounted for. Prefer `continue --job` when a conversation exists; otherwise use `restart`. Each creates a fresh 60m budget unless `--timeout` is specified, and preserves the old terminal record. Restart refreshes workspace context; older specifications explicitly label historical snapshots and append current context. An empty response at either the AGY response deadline or worker hard limit becomes `attention` (exit 5) when a conversation ID is known, otherwise `error`. The response-timeout classifier accepts explicit TIMEOUT statuses and AGY's exact `ERROR` / `timeout waiting for response` payload; unrelated tool/network/auth/quota errors keep their own failure path. The report includes pre-run/current workspace status, original configuration and an exact continuation command with a doubled timeout capped at 120m for background jobs. At that ceiling, offer a narrower task. Ask the user whether to continue or stop and inspect; do not automatically retry, restart or continue after a timeout. Run the proposed recovery only after explicit user confirmation. A wait soft expiry is still exit 2 and requires no new execution. + +Warning-free success removes intermediate stream/snapshot files after results are stored. Errors, cancellation, hard timeout and warning results retain them; results, logs and conversation metadata remain available. Older jobs may have no progress files. + +Quote errors and add a concise diagnosis. For sandbox/permission errors or an apparent crash without a result, check that collection uses the same unsandboxed context as launch; see `references/troubleshooting.md`. For restricted empty responses, relay the companion's permission guidance. Do not switch repositories to bypass a precondition. + +## Host compatibility + +When this skill or its referenced instructions require a tool that the current environment does not provide, use available capabilities to achieve an equivalent result. Adapt only the tool-specific execution method; preserve the task goal, authorization requirements, explicit confirmation steps, result delivery, and stopping conditions. + +If an equivalent result cannot be achieved, or you cannot establish that an alternative is equivalent, explain the missing capability and its impact, and ask the user for help. Do not silently skip requirements or bypass the environment's restrictions. diff --git a/opencode-skills/agy-jobs/references/setup.md b/opencode-skills/agy-jobs/references/setup.md new file mode 100644 index 0000000..786fbd2 --- /dev/null +++ b/opencode-skills/agy-jobs/references/setup.md @@ -0,0 +1,41 @@ + + +# setup — optional hardening for restricted runs + +staffer, research, review, and implement default to unrestricted and work with no setup. Run setup only when the user wants restricted runs — per call (`--restricted`) or by default in this repository. Restricted runs keep AGY's permission engine active (ask is tool-free and needs no command rules). + +## Flow + +**Step 1 — status + dry run.** Shows the current per-repo policy, the allow/deny rules state, and the plan without writing anything: + +```bash +node "/../../companion/agy-companion.mjs" setup +``` + +If it reports the agy CLI itself is missing, stop and relay its install guidance — do not attempt to install agy yourself. + +**Step 2 — per-repo policy.** Ask the user with `AskUserQuestion` (multiSelect over `staffer`, `research`, `review`, `implement`, plus a "None — keep the defaults (Recommended)" option): which modes should default to the **restricted** profile in this repository? Make clear this is a per-repo, per-machine preference (`.agy-staff/config.json`, git-ignored, not shared with the team), that a `--restricted`/`--unrestricted` flag on a call still overrides it, and that it is a run policy, not a security boundary — untrusted input still calls for an isolated checkout. Then apply their answer: + +```bash +node "/../../companion/agy-companion.mjs" setup --restrict review,research # their selection +node "/../../companion/agy-companion.mjs" setup --restrict none # if they chose none +``` + +If they chose none AND the step-1 output shows the allow/deny rules already installed, you are done — skip to the final report. + +**Step 3 — allow/deny rules.** If any mode is restricted (by policy or because the user wants `--restricted` runs) and rules are missing, present the step-1 dry-run output in full: which rules would be added, to which file, and that the file is backed up first. Be explicit about two things before asking for confirmation: + +- **Scope is global.** The rules go into agy's global settings file, so they apply to every project on this machine, not just this repository. +- **AGY owns rule enforcement.** Setup allows broad `git`/`gh` commands and adds five deny prefixes: `git push`, `git reset --hard`, `git clean`, `gh pr merge`, and `gh release delete`. AGY evaluates deny before ask before allow. This avoids maintaining every task's allowed subcommands, but does not cover other command forms, scripts or APIs and is not a read-only boundary. Existing allow/deny/ask rules are preserved; adding broad grants to an earlier narrow setup is visible in the dry run. A denied operation remains denied even if the task requests it; changing policy requires an explicit settings change. + +Then use `AskUserQuestion` exactly once: `Apply the allow/deny rules (Recommended)` / `Skip for now`. + +**Step 4 — only if the user chose apply:** + +```bash +node "/../../companion/agy-companion.mjs" setup --apply +``` + +## Final report + +Tell the user what is now in effect: the per-repo policy (if any) and where to change it later (`setup --restrict ...` / `setup --restrict none`), whether the allow/deny rules were applied (including the backup path), and the caveats: prefix matching, global scope, the headless caveat that some agy tools ignore allow-rules entirely so a restricted run can still come back empty, and the honest note that agy's project-scoped settings path is undocumented and unverified — so only the global file is edited. Security-sensitive users can scope permissions to a single project themselves, but do not guess a path for them. diff --git a/opencode-skills/agy-jobs/references/troubleshooting.md b/opencode-skills/agy-jobs/references/troubleshooting.md new file mode 100644 index 0000000..e859192 --- /dev/null +++ b/opencode-skills/agy-jobs/references/troubleshooting.md @@ -0,0 +1,29 @@ + + +# Troubleshooting agy runs + +## The harness command sandbox (`operation not permitted`) + +agy cannot run inside a harness command sandbox (e.g. Codex workspace-write): it binds a localhost port for its internal language server and reads its OAuth token file, which sandbox secret-protection hides. No writable_roots/network_access knob fixes the hidden token — the run dies with `operation not permitted` on `~/.gemini/...` or on binding `127.0.0.1`, or with a bogus "authentication failed". + +Fix: run the companion command **unsandboxed**. In Codex, request escalated permissions for the command or have the user grant the workspace full access. Do not retry the command as-is; the sandbox will block it the same way every time. + +## False crash reports across permission or sandbox contexts + +If a background job was started unsandboxed but a management command (`wait`, `status`, `result`) is later run from a sandboxed or different permission context, the collector process may not see the running worker process. Because the liveness check fails and no result file has been written yet, the command reports the job as `crashed` with no stored result. + +Fix: run job management commands (`wait`, `status`, `result`, `cancel`) in the same unsandboxed permission context as the job start. Rerunning `wait`/`status`/`result` from the unsandboxed context sees the live worker PID and resumes waiting or reporting normal running status. + +## Empty response with status SUCCESS + +The fail-closed signature of a restricted run: headless agy auto-denies every unlisted tool call, so agy finishes "successfully" with nothing to say. The companion's error message carries the exact guidance — run `setup` once to install the evidence-gathering allowlist (see `setup.md`), or pass `--unrestricted` explicitly when authorized (continuations inherit the previous profile; new tool-using tasks default to unrestricted). Note that some agy tools ignore allow-rules in headless mode entirely, so even a complete allowlist cannot make them work; those need an unrestricted run. An empty response from an *unrestricted* run is not a permission issue — report it. + +## done_with_warnings (error status, response text) + +When agy reports an error but response text came back, the companion delivers the response anyway: exit 0, response on stdout, warning on stderr (retained in the job log and included as a bounded tail during background result collection). Assess the response against the task and review the diagnostics before deciding whether more work is needed. An empty response at a deadline with a known conversation instead needs attention (exit 5); other empty responses remain failures. + +## Retry rules + +- Do not retry with different flags unless the error message itself names the exact flag. +- For a timeout, inspect the retained result and workspace. Exit 5 means the conversation is resumable: ask the user whether to continue with the suggested timeout or stop, and recover only after explicit confirmation. Background `--timeout` defaults to 60m and accepts at most 120m; increase it only below that ceiling, otherwise narrow the task. A known conversation ID lets `continue` resume with its recorded configuration. A response already received before the hard deadline is delivered with a warning, so do not restart a completed job merely because cleanup timed out. +- Model-id errors fail pre-flight with the valid ids in the message (`agy models` lists them); expired auth means running `agy` interactively once to re-login. diff --git a/opencode-skills/agy-lead/SKILL.md b/opencode-skills/agy-lead/SKILL.md new file mode 100644 index 0000000..10b000d --- /dev/null +++ b/opencode-skills/agy-lead/SKILL.md @@ -0,0 +1,39 @@ +--- +name: agy-lead +description: Orchestrate an ongoing task with AGY while the current agent owns key decisions, review, and delivery. Use when the user invokes /agy-lead or asks you to coordinate a task using AGY. +--- + + + +# agy lead + +Task orchestration with AGY. You are the lead in the current harness. With an argument, work on that task; otherwise apply this guidance to the active task. Extend it across the session only when the user asks. + +## Working with AGY + +1. **Frame the assignment.** Orient just enough to state the outcome and completion criteria. Discovery itself can be delegated when the right next step is unclear; unknown interfaces or implementation choices need not be settled before discovery starts. Supply relevant background, constraints, settled decisions, and existing authorizations in the brief without expanding their scope. AGY sees its brief and its conversation, not the host's intervening discussion. +2. **Default to delegating substantive work.** Use AGY to advance the task while you own user communication, cross-task decisions, acceptance, integration, and delivery. Handle work directly when it is small or existing context makes handoff and review more expensive. Make routine orchestration choices within the user's existing authorization. Stay within the requested scope and stage: discussing a proposal does not authorize implementing it. +3. **Choose persona and worker scope by outcome.** Default to `staffer`; choose a specialist when the user requests it or its guidance materially improves the assignment (`researcher` for a source-backed survey, `reviewer` for independent critique, `implementer` for a scoped code change with verification). Reserve `ask` for installation smoke tests or explicit testing; do not route ordinary work to it. Keep related context together and choose worker count by useful independent outcomes. Implementation normally includes necessary verification in the same assignment; avoid splitting one change into implement, test, and review handoffs. Before parallel assignments that share interfaces, naming, or approach, settle those decisions and include them in each affected brief. Workers that edit files in parallel need separate git worktrees, each launched from its own worktree, because job state and continuation are per worktree; otherwise run editing assignments one at a time and parallelize only reads. +4. **Wait by default.** After dispatch, wait for the result through `jobs`. Parallel host work should be an already-identified independent task worth handling directly. Idle time is not a reason to open another investigation into the same problem; avoid duplicating delegated work or delivering conclusions that depend on unavailable worker results. +5. **Require an assessable result.** Specify the artifacts and evidence needed for acceptance. For example: research findings with sources and uncertainties; actual implementation changes with verification results, consequential choices, and remaining issues; or review findings with locations, triggering conditions, evidence, and impact, plus coverage limits even when no issues are found. Adapt these requirements to the task. The worker may analyze and recommend; you retain final judgment. +6. **Assess, then decide the next step.** Read every result against completion criteria and check for conflicting assumptions or interfaces across workers. Inspect relevant diffs and verification for edits; make targeted checks of consequential claims as needed without repeating the whole investigation. Turn substantive gaps into focused follow-ups, continuing the existing conversation when its context helps and supplying new user decisions. Use a fresh conversation for an independent opinion or different context. Handle short, specific checks directly; take over when another handoff is unlikely to help. Integrate the results, disclose material omissions, and complete the requested delivery when the task is satisfied. Add review rounds only when they resolve meaningful uncertainty. + +## Dispatch and follow through + +Read `../agy-jobs/SKILL.md` for result collection, cancellation, continuation, and recovery. This skill lives at `/opencode-skills/agy-lead/SKILL.md`. Write the brief to a temporary file and call the shared companion: + +```bash +node "/../../companion/agy-companion.mjs" staffer --prompt-file "" +``` + +For a specialist, replace `staffer` with `research`, `review`, or `implement`. When requesting code review, use the review-brief guidance in `../agy-reviewer/references/code-review.md`. Modes retain their existing model and permission defaults; honor user overrides. Run unsandboxed as described in `../agy-jobs/references/troubleshooting.md` (escalated execution in Codex). + +Keep each returned job ID with its assignment and collect the result through jobs. Prefer `continue --job ` for follow-ups; the companion refuses it while that job is still running. Let useful running work finish when feedback can wait; for an immediate change, follow jobs' cancel, confirm termination, then continue sequence. Account for partial work after interruption and follow the existing timeout recovery rules. + +In this workflow you compose briefs and synthesize results. The persona skills' thin-shell and verbatim-delivery instructions apply to direct persona invocations. Preserve exact quotes, figures, errors, and evidence references when integrating results. The lead skill uses existing companion modes; it adds no scheduler. + +## Host compatibility + +When this skill or its referenced instructions require a tool that the current environment does not provide, use available capabilities to achieve an equivalent result. Adapt only the tool-specific execution method; preserve the task goal, authorization requirements, explicit confirmation steps, result delivery, and stopping conditions. + +If an equivalent result cannot be achieved, or you cannot establish that an alternative is equivalent, explain the missing capability and its impact, and ask the user for help. Do not silently skip requirements or bypass the environment's restrictions. diff --git a/opencode-skills/agy-researcher/SKILL.md b/opencode-skills/agy-researcher/SKILL.md new file mode 100644 index 0000000..9b9d621 --- /dev/null +++ b/opencode-skills/agy-researcher/SKILL.md @@ -0,0 +1,51 @@ +--- +name: agy-researcher +description: Delegate a deep research or survey task to Google's Antigravity CLI (agy staffer, fast Gemini). Use when the user says /agy-researcher, "ask agy to research", "have the agy staffer survey X", or wants a second, independent deep-dive on a topic or codebase without spending the host model's quota. +--- + + + +# agy researcher + +Delegate a research task to the agy staffer via the shared companion script. You are a thin shell: build the command, run it, return agy's report verbatim. + +## Locating the companion + +This skill file lives at `/opencode-skills/agy-researcher/SKILL.md`; resolve the companion path relative to this skill directory: + +```bash +node "/../../companion/agy-companion.mjs" research [flags] --prompt "what to research" +``` + +Pass the user's research topic verbatim via `--prompt`; use `--prompt-file ` or `--stdin` for a long brief. + +> [!IMPORTANT] +> Run this command **unsandboxed** — agy needs a localhost port and its OAuth token file, which harness sandboxes hide. In Codex, request escalated permissions for the command. Details: `../agy-jobs/references/troubleshooting.md`. + +## Collecting the result + +The command returns a job id. Read `../agy-jobs/SKILL.md` for result collection and recovery: dispatch, wait for the final result, then validate as needed. Do not proactively observe progress, read logs or inspect intermediate artifacts while running. Observe only when the user explicitly asks for progress; diagnose a failure or a result requiring intervention under the jobs protocol. + +## Flags (all optional) + +- `--continue` — reuse the last research conversation (quota-friendly, served largely from cache); `--conversation ` targets a specific one. +- `--model ` or `--effort low|medium|high` — default model is `gemini-3.8-flash-high`. +- `--restricted` / `--unrestricted` — permission profile. research defaults to unrestricted, so it works out of the box with no setup. `--restricted` is the opt-in hardening path: agy runs without `--dangerously-skip-permissions` and may only use allowlisted tools, so it needs the setup flow's evidence-gathering allowlist to be useful — and some native agy tools ignore allow-rules headless, so restricted runs can still come back empty. +- `--prompt ` / `--prompt-file ` / `--stdin` — the task, from exactly one of these three sources. Use file/stdin for long prompts. +- `--timeout ` — default 60m, maximum 120m hard execution limit. + +## Rules + +- Do not do the research yourself and do not re-verify agy's findings. +- Return the companion stdout verbatim. The `[agy-staff]` telemetry line goes to stderr (and into `jobs/.log` for background runs) — it is metadata for you, the calling agent, not something to show the user. +- Pass the user's explicit authorizations through to the task string verbatim. The prompt template default-denies costly or irreversible side effects (commits/pushes, deleting files outside the workspace, side-effectful network calls, commands that burn paid API quota); that default opens only when the request itself asks for the operation — so keep "run the e2e tests" or "call the staging API" in the prompt instead of trimming it. +- If a `--restricted` run reports an empty response due to denied permissions, relay the companion's guidance: run the setup flow once (see `../agy-jobs/references/setup.md`), or drop `--restricted`. +- For errors and recovery, follow `../agy-jobs/SKILL.md`. + +For an existing conversation, `--continue` / `--conversation ` inherit its recorded model and permission profile unless explicitly overridden. The unrestricted defaults above apply to new tasks. + +## Host compatibility + +When this skill or its referenced instructions require a tool that the current environment does not provide, use available capabilities to achieve an equivalent result. Adapt only the tool-specific execution method; preserve the task goal, authorization requirements, explicit confirmation steps, result delivery, and stopping conditions. + +If an equivalent result cannot be achieved, or you cannot establish that an alternative is equivalent, explain the missing capability and its impact, and ask the user for help. Do not silently skip requirements or bypass the environment's restrictions. diff --git a/opencode-skills/agy-reviewer/SKILL.md b/opencode-skills/agy-reviewer/SKILL.md new file mode 100644 index 0000000..2c26012 --- /dev/null +++ b/opencode-skills/agy-reviewer/SKILL.md @@ -0,0 +1,62 @@ +--- +name: agy-reviewer +description: Get a second-opinion review from Google's Antigravity CLI (agy staffer, fast Gemini) - of code (a diff, PR, working tree) or of a decision, plan, or design. Use when the user says /agy-reviewer, "have agy review this", "second opinion on my diff/PR/plan", or after finishing work and wanting an independent verifier that does not share the host model's blind spots. +--- + + + +# agy reviewer + +Run a second-opinion review through the agy staffer. You are a thin shell: compose the task, run the companion, return agy's review verbatim. Never fix the issues it finds. + +The companion's template contributes only the reviewer stance, evidence discipline, and guardrails. Everything flavor-specific travels in the task string you compose — so pick the flavor first: + +- **Code review** — the subject is code: a PR, a branch/ref, the working tree, a patch file, specific files. Read `references/code-review.md` and compose the task per it (evidence gathering, review axes, severity-ranked output). `--json` belongs to this flavor only. +- **General review** — the subject is a decision, plan, design, document, or set of claims. Read `references/general-review.md` and compose the task per it (multi-angle challenge). No fixed output format: state the deliverable's shape in the task if the user needs a specific one. + +## Locating the companion + +This skill file lives at `/opencode-skills/agy-reviewer/SKILL.md`; resolve the companion path relative to this skill directory: + +```bash +node "/../../companion/agy-companion.mjs" review [flags] --prompt "what to review" +``` + +Pass the review subject verbatim via `--prompt`; use `--prompt-file ` or `--stdin` for long text, which a composed task usually needs. + +> [!IMPORTANT] +> Run this command **unsandboxed** — agy needs a localhost port and its OAuth token file, which harness sandboxes hide. In Codex, request escalated permissions for the command. Details: `../agy-jobs/references/troubleshooting.md`. + +## The review subject is the prompt + +review is prompt-based: the user's request plus the flavor's framing is the task string. Do not gather diffs, write patch files, or translate the request into flags — agy collects the evidence itself. If the subject is ambiguous, agy reports the ambiguity instead of guessing; relay that and let the user sharpen the request. A task string is required; review with no subject exits with an error. + +## Collecting the result + +The command returns a job id. Read `../agy-jobs/SKILL.md` for result collection and recovery: dispatch, wait for the final result, then validate as needed. Do not proactively observe progress, read logs or inspect intermediate artifacts while running. Observe only when the user explicitly asks for progress; diagnose a failure or a result requiring intervention under the jobs protocol. + +## Flags (all optional) + +- `--json` — schema-enforced JSON findings (verdict/summary/findings/could_not_verify) instead of markdown. Code-review flavor only, and only when the user asks for machine-readable output. +- `--restricted` / `--unrestricted` — permission profile. review defaults to unrestricted, so it works out of the box and can run tests or reproduce a bug when the request asks for it. `--restricted` is the opt-in hardening path: agy may then only use allowlisted tools, so it needs the setup flow's evidence-gathering allowlist to be useful — and some native agy tools ignore allow-rules headless, so restricted runs can still come back empty. +- `--model ` / `--effort low|medium|high` (default `gemini-3.8-flash-medium`), `--continue` (or `--conversation `), `--timeout ` (default 60m, maximum 120m hard execution limit). +- `--prompt ` / `--prompt-file ` / `--stdin` — the task, from exactly one of these three sources. Use file/stdin for long prompts (a composed task with the flavor framing usually is one). + +## Reviewing untrusted content + +An unrestricted review of code from an untrusted author (a PR from a stranger, a patch from an unknown source) means prompt injection in that content could run arbitrary commands. For those reviews, consider `--restricted` (it may fail closed, and some native tools ignore allow-rules) or run the review in an isolated checkout. + +## Rules + +- Return the companion stdout verbatim — no commentary, no fixes, no softening of findings. +- Pass the user's explicit authorizations through to the task string verbatim. The prompt template default-denies costly or irreversible side effects (commits/pushes, deleting files outside the workspace, side-effectful network calls, commands that burn paid API quota); that default opens only when the request itself asks for the operation — so keep "run the e2e tests" or "call the staging API" in the prompt instead of trimming it. +- Empty responses from a `--restricted` run: relay the companion's guidance (run the setup flow once, or drop `--restricted`). +- For errors and recovery, follow `../agy-jobs/SKILL.md`. + +For an existing conversation, `--continue` / `--conversation ` inherit its recorded model and permission profile unless explicitly overridden. The unrestricted defaults above apply to new tasks. + +## Host compatibility + +When this skill or its referenced instructions require a tool that the current environment does not provide, use available capabilities to achieve an equivalent result. Adapt only the tool-specific execution method; preserve the task goal, authorization requirements, explicit confirmation steps, result delivery, and stopping conditions. + +If an equivalent result cannot be achieved, or you cannot establish that an alternative is equivalent, explain the missing capability and its impact, and ask the user for help. Do not silently skip requirements or bypass the environment's restrictions. diff --git a/opencode-skills/agy-reviewer/references/code-review.md b/opencode-skills/agy-reviewer/references/code-review.md new file mode 100644 index 0000000..2ce5176 --- /dev/null +++ b/opencode-skills/agy-reviewer/references/code-review.md @@ -0,0 +1,46 @@ + + +# Composing a code-review task + +Read this when the review subject is code: a PR, a branch or ref, the working tree, a patch file, or specific files. The companion's template carries only the reviewer stance and guardrails; the code-review contract below travels in the task string. Compose the task as the user's request, verbatim, followed by this contract — adjusted only where the request explicitly overrides it. A composed task is usually long: pass it with `--prompt-file` or `--stdin` rather than `--prompt` (the three are the only task sources, and exactly one per call). + +The standards and spec-alignment axes are adapted from `code-review` in [mattpocock/skills](https://github.com/mattpocock/skills) (MIT). + +## The contract to append to the task + +### Gathering the evidence + +No diff is inlined. Identify the review subject from the request, then fetch the evidence yourself with the tools you have: + +- A pull request → `gh pr view ` for title, description, and discussion, then `gh pr diff ` for the change. +- A git ref or branch → `git diff ` for the change and `git log --oneline ..HEAD` for the commit trail. +- The current working tree → `git status` plus `git diff` and `git diff --staged`. +- A patch or diff file → read the file at the given path. +- Specific files or a directory → read them directly. + +Read surrounding source files wherever the diff alone is ambiguous — the review is about the code the change lands in, not just the changed lines. + +### Review axes + +Cover each axis; drop one only when the diff plainly has none of that surface, and say in the report that you dropped it: + +- **correctness** — does the code do what it claims: logic, edge cases, error and retry paths, concurrency, resource lifetimes, and tests that only appear to test something. +- **standards** — does it follow *this* repo's conventions, documented (lint config, CONTRIBUTING, CLAUDE.md / AGENTS.md) and observed (how neighbouring modules do it). Establish the convention from the repo before judging, and cite the file that establishes it — a convention you cannot cite is a preference, not a finding. +- **spec alignment** — does it do what the originating issue, ticket, or request actually asked: requirements left unmet, cases silently dropped, scope added on its own. +- **security** — untrusted input reaching a sink, authz checks missing or in the wrong layer, secrets and logging, injection, unsafe defaults. + +### Findings + +- **Rank by severity**: `critical` (data loss, security, corruption), `high` (incorrect behavior on realistic input), `medium` (bug in edge case, resource leak), `low` (robustness, maintainability), `nit` (style). Report in that order. +- **Every finding needs `file:line`** (or `file:hunk` if line numbers are unavailable) plus a one-line title and a concrete explanation of the failure mode — what input or sequence triggers it. +- **Check what the change touches, not just what it shows**: callers of changed functions, invariants the change might break, error paths, and concurrency. + +### Output format + +- `## Verdict` — one of: approve / request changes / comment, with one sentence of justification. +- `## Findings` — severity-ranked list as specified above. If none: say so explicitly and state what was checked. +- `## What I could not verify` — mandatory. Evidence that could not be fetched, tests that could not run, callers not seen, assumptions made. + +## --json + +`--json` makes the companion enforce a findings schema (verdict / summary / findings with severity, file, line, title, detail / could_not_verify) instead of markdown. Opt-in only, when the user wants machine-readable output; it matches the output format above, so no extra task text is needed for it. diff --git a/opencode-skills/agy-reviewer/references/general-review.md b/opencode-skills/agy-reviewer/references/general-review.md new file mode 100644 index 0000000..609587e --- /dev/null +++ b/opencode-skills/agy-reviewer/references/general-review.md @@ -0,0 +1,22 @@ + + +# Composing a general review task + +Read this when the review subject is not code: a decision, a plan, a design, a document, or a set of claims. The companion's template carries the reviewer stance (find real problems, no speculative findings) and the guardrails — nothing else. The task string defines everything specific, including the output's shape. + +Two composition rules: + +1. **The user's request goes through verbatim.** Their numbering, their wording, their scope — the task is theirs, the framing below is scaffolding around it, never a replacement for it. +2. **State the deliverable's shape in the task when the user needs a specific one.** The template imposes no output format by design; if nothing is stated, agy chooses its own structure. + +## The framing to append to the task + +Challenge the subject from independent angles rather than summarizing or grading it. Angles that earn their place for most subjects: + +- **First principles** — rebuild the reasoning from the problem, not from the proposal. Does the conclusion still fall out? +- **Hidden assumptions** — what must be true for this to work that is nowhere stated? Which assumption, if wrong, sinks it? +- **Simpler alternatives** — what cheaper or smaller option was not considered, and why would it not suffice (Occam's razor)? +- **Failure modes** — under what realistic conditions does this break, and what is the blast radius when it does? +- **Evidence** — which claims are backed by something checkable in the environment, and which are conviction? Check the checkable ones. + +Drop an angle that plainly has no surface for the subject, and add one the subject demands (cost, timeline, reversibility, security). For each angle, report concrete objections grounded in the subject's own text or the environment — generic caution is noise. Where everything holds up, say so plainly; manufacturing objections is the same failure as rubber-stamping. diff --git a/opencode-skills/agy-staffer/SKILL.md b/opencode-skills/agy-staffer/SKILL.md new file mode 100644 index 0000000..b0fb04f --- /dev/null +++ b/opencode-skills/agy-staffer/SKILL.md @@ -0,0 +1,53 @@ +--- +name: agy-staffer +description: Delegate a general-purpose task to Google's Antigravity CLI (agy staffer, fast Gemini) with a minimal, unopinionated prompt. Use when the user says /agy-staffer, "have agy do/handle X", "have agy generate an image", or the task fits none of the specialist personas (researcher / reviewer / implementer / ask) — the template adds no role, rules, or output format, so the task text alone shapes the output. Also the route to agy-native tools no specialist covers, notably image generation (generate_image). +--- + + + +# agy staffer + +The general-purpose persona: a clean entry point for tasks that none of the specialists fit. Its prompt template is deliberately minimal — the task text, the environment (cwd, branch, date), and the safety guardrails, nothing else. No role framing, no rules, no output format: the task defines its own output, and no unrelated template context can pull the run off course. + +Prefer a specialist when one fits: `researcher` for surveys and deep dives, `reviewer` for second opinions on code or plans, `implementer` for edits to the working tree, `ask` for a cheap one-shot question. + +staffer is also the route to agy-native tools no specialist covers — notably **image generation**: agy ships a `generate_image` tool (verified on v1.1.15; a 1024×1024 PNG in ~30s). Name the output path in the task, e.g. `staffer --prompt "generate a pixel-art robot mascot, save it as assets/mascot.png"`. + +## Locating the companion + +This skill file lives at `/opencode-skills/agy-staffer/SKILL.md`; resolve the companion path relative to this skill directory: + +```bash +node "/../../companion/agy-companion.mjs" staffer [flags] --prompt "task" +``` + +Pass the user's task text verbatim via `--prompt`; use `--prompt-file ` or `--stdin` for long text. + +> [!IMPORTANT] +> Run this command **unsandboxed** — agy needs a localhost port and its OAuth token file, which harness sandboxes hide. In Codex, request escalated permissions for the command. Details: `../agy-jobs/references/troubleshooting.md`. + +## Collecting the result + +The command returns a job id. Read `../agy-jobs/SKILL.md` for result collection and recovery: dispatch, wait for the final result, then validate as needed. Do not proactively observe progress, read logs or inspect intermediate artifacts while running. Observe only when the user explicitly asks for progress; diagnose a failure or a result requiring intervention under the jobs protocol. + +## Flags (all optional) + +- `--prompt ` / `--prompt-file ` / `--stdin` — the task, from exactly one of these three sources. Use file/stdin for long prompts instead of shell quoting. +- `--model ` or `--effort low|medium|high` — default model is `gemini-3.8-flash-medium`. +- `--restricted` / `--unrestricted` — permission profile; staffer defaults to unrestricted like the other tool-using personas, `--restricted` is the opt-in hardening path. +- `--continue` (or `--conversation `), `--timeout ` (default 60m, maximum 120m hard execution limit). + +## Rules + +- Pass the user's task through verbatim. Because the template imposes no output format, state the desired format in the task text when the caller needs a specific one. +- Pass the user's explicit authorizations through verbatim. The template default-denies costly or irreversible side effects (commits/pushes, deleting files outside the workspace, side-effectful network calls, paid-quota commands); that default opens only when the task itself asks for the operation. +- A general task may legitimately edit files. The companion reports any working-tree delta with the result — inspect it (`git diff`) and confirm it is what the task asked for before building on it. +- For errors and recovery, follow `../agy-jobs/SKILL.md`. + +For an existing conversation, `--continue` / `--conversation ` inherit its recorded model and permission profile unless explicitly overridden. The unrestricted defaults above apply to new tasks. + +## Host compatibility + +When this skill or its referenced instructions require a tool that the current environment does not provide, use available capabilities to achieve an equivalent result. Adapt only the tool-specific execution method; preserve the task goal, authorization requirements, explicit confirmation steps, result delivery, and stopping conditions. + +If an equivalent result cannot be achieved, or you cannot establish that an alternative is equivalent, explain the missing capability and its impact, and ask the user for help. Do not silently skip requirements or bypass the environment's restrictions. diff --git a/opencode.mjs b/opencode.mjs new file mode 100644 index 0000000..008dcea --- /dev/null +++ b/opencode.mjs @@ -0,0 +1,14 @@ +// OpenCode V1 plugin. The host discovers commands directly from these skills. +// Keep this module dependency-free and export only plugin initializers: V1's +// loader invokes every export as a plugin. +import { fileURLToPath } from 'node:url'; + +const bundledSkills = fileURLToPath(new URL('./opencode-skills', import.meta.url)); + +export const AgyStaffPlugin = async () => ({ + config: async config => { + config.skills ??= {}; + config.skills.paths ??= []; + if (!config.skills.paths.includes(bundledSkills)) config.skills.paths.push(bundledSkills); + }, +}); diff --git a/package.json b/package.json index fb8ec9d..fe61366 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "version": "0.7.3", "description": "Delegate work to Google's Antigravity CLI (agy) with fast Gemini access.", "keywords": [ - "pi-package" + "pi-package", + "opencode-plugin" ], "license": "MIT", "repository": { @@ -15,6 +16,8 @@ "files": [ "skills/", "pi-skills/", + "opencode-skills/", + "opencode.mjs", "companion/", "templates/", "scripts/generate-pi-skills.mjs", @@ -27,11 +30,21 @@ "check:pi": "node scripts/generate-pi-skills.mjs --check", "test": "node --test tests/*.test.mjs", "test:pi": "node --test tests/pi.integration.mjs", - "prepack": "npm run check:pi" + "prepack": "npm run check:skills", + "generate:opencode": "node scripts/generate-pi-skills.mjs --target=opencode", + "check:opencode": "node scripts/generate-pi-skills.mjs --target=opencode --check", + "generate:skills": "npm run generate:pi && npm run generate:opencode", + "check:skills": "npm run check:pi && npm run check:opencode", + "test:opencode": "node --test tests/opencode.integration.mjs" }, "pi": { "skills": [ "./pi-skills" ] + }, + "main": "./opencode.mjs", + "exports": "./opencode.mjs", + "engines": { + "opencode": ">=1.18.34 <2" } } diff --git a/scripts/generate-pi-skills.mjs b/scripts/generate-pi-skills.mjs index 1be906c..359133c 100644 --- a/scripts/generate-pi-skills.mjs +++ b/scripts/generate-pi-skills.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -// Pi has a flat skill namespace. Keep the canonical skills for Claude/Codex, -// and generate namespaced entrypoints at the same depth for Pi. No runtime deps. +// Generate flat, branded skill entrypoints from the canonical methods. +// Both targets stay at the same depth so shared runtime paths remain valid. import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -18,13 +18,21 @@ function filesUnder(dir) { }); } -export function piFiles(root = ROOT) { +const targets = { + pi: { label: 'Pi', invocation: '/skill:agy-' }, + opencode: { label: 'OpenCode', invocation: '/agy-' }, +}; + +export function skillFiles(root = ROOT, target = 'pi') { + const host = targets[target]; + if (!host) throw new Error(`Unknown skill target: ${target}`); + const outputDir = `${target}-skills`; const source = path.join(root, 'skills'); const names = fs.readdirSync(source).filter(name => fs.existsSync(path.join(source, name, 'SKILL.md'))).sort(); const outputs = new Map(); for (const name of names) { if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name) || `agy-${name}`.length > 64) { - throw new Error(`Invalid Pi skill name: agy-${name}`); + throw new Error(`Invalid ${host.label} skill name: agy-${name}`); } for (const file of filesUnder(path.join(source, name))) { let content = fs.readFileSync(file); @@ -33,19 +41,19 @@ export function piFiles(root = ROOT) { // Change only skill invocation syntax and skill-directory paths, never // companion subcommands such as `review`, `research`, or `wait`. for (const peer of names) { - content = content.replace(new RegExp(`(?:/agy:|\\$agy:)${peer}(?![a-z0-9-])`, 'g'), `/skill:agy-${peer}`) + content = content.replace(new RegExp(`(?:/agy:|\\$agy:)${peer}(?![a-z0-9-])`, 'g'), `${host.invocation}${peer}`) .replaceAll(`../${peer}/`, `../agy-${peer}/`) - .replaceAll(`/skills/${peer}/`, `/pi-skills/agy-${peer}/`); + .replaceAll(`/skills/${peer}/`, `/${outputDir}/agy-${peer}/`); } const canonicalRel = path.relative(root, file).split(path.sep).join('/'); - const notice = ``; + const notice = ``; const match = /^---\n([\s\S]*?)\n---\n/.exec(content); if (path.basename(file) === 'SKILL.md') { if (!match || !match[1].split('\n').includes(`name: ${name}`)) { throw new Error(`Expected name: ${name} in ${file}`); } const frontmatter = match[1].split('\n') - // These are Claude-specific UI/permission fields, not Pi policy. + // These are Claude-specific UI/permission fields, not host policy. .filter(line => !/^(allowed-tools|argument-hint|user-invocable):/.test(line)) .map(line => line === `name: ${name}` ? `name: agy-${name}` : line).join('\n'); content = `---\n${frontmatter}\n---\n\n${notice}\n` @@ -57,15 +65,15 @@ export function piFiles(root = ROOT) { } content = Buffer.from(content); } - outputs.set(path.join('pi-skills', `agy-${name}`, path.relative(path.join(source, name), file)), content); + outputs.set(path.join(outputDir, `agy-${name}`, path.relative(path.join(source, name), file)), content); } } return outputs; } -export function generatePiSkills({ root = ROOT, check = false } = {}) { - const expected = piFiles(root); - const actual = filesUnder(path.join(root, 'pi-skills')); +export function generateSkills({ root = ROOT, check = false, target = 'pi' } = {}) { + const expected = skillFiles(root, target); + const actual = filesUnder(path.join(root, `${target}-skills`)); const unexpected = actual.filter(file => !expected.has(path.relative(root, file))); // Fail instead of deleting stale files automatically: a maintainer may have // edited them. Renames/removals must explicitly remove the obsolete output. @@ -81,17 +89,22 @@ export function generatePiSkills({ root = ROOT, check = false } = {}) { } } if (check && changed.length) { - throw new Error(`Stale Pi skills; edit canonical sources in skills/ (do not edit pi-skills/) and run npm run generate:pi:\n${changed.join('\n')}`); + throw new Error(`Stale ${targets[target].label} skills; edit canonical sources in skills/ (do not edit ${target}-skills/) and run npm run generate:${target}:\n${changed.join('\n')}`); } return { count: expected.size, changed }; } +// Preserve the original Pi API for consumers and tests. +export const piFiles = (root = ROOT) => skillFiles(root, 'pi'); +export const generatePiSkills = (options = {}) => generateSkills({ ...options, target: 'pi' }); + if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { try { - if (process.argv.slice(2).some(arg => arg !== '--check')) throw new Error('Usage: generate-pi-skills.mjs [--check]'); + if (process.argv.slice(2).some(arg => !['--check', '--target=pi', '--target=opencode'].includes(arg))) throw new Error('Usage: generate-pi-skills.mjs [--check] [--target=pi|opencode]'); + const target = process.argv.find(arg => arg.startsWith('--target='))?.slice(9) || 'pi'; const check = process.argv.includes('--check'); - const result = generatePiSkills({ check }); - console.log(`Pi skills ${check ? 'verified' : 'generated'}: ${result.count} files (${result.changed.length} changed).`); + const result = generateSkills({ check, target }); + console.log(`${targets[target].label} skills ${check ? 'verified' : 'generated'}: ${result.count} files (${result.changed.length} changed).`); } catch (error) { console.error(error.message); process.exitCode = 1; diff --git a/tests/README.md b/tests/README.md index 0894f4f..47cd8b6 100644 --- a/tests/README.md +++ b/tests/README.md @@ -3,7 +3,7 @@ Regression tests for the 0.6.1 companion interface: black-box CLI tests for `companion/agy-companion.mjs`, plus focused tests for observation parsing, byte budgets and state locking. `issue-regressions.test.mjs` covers #8 response-timeout attention and conversation configuration history, #9 workspace attachment across execution/recovery paths, and #10 broad allow plus targeted deny setup rules, preserving existing settings and upgrading deny-only gaps. Timeout tests distinguish complete answers, unrelated errors, absent conversation IDs and the background timeout ceiling; terminal-observation tests cover attention publication races. All use fake AGY and temporary workspaces/settings. -The standard suite uses Node's built-in test runner and assertions, with no test dependencies or model/network calls. Run unsandboxed when the host restricts process inspection/signals: lifecycle tests use `ps` to verify detached descendant cleanup. Packaging tests also use npm and tar. The optional Pi integration suite uses a separately installed Pi CLI, never a model provider; the opt-in real AGY suite below does make model calls. +The standard suite uses Node's built-in test runner and assertions, with no test dependencies or model/network calls. Run unsandboxed when the host restricts process inspection/signals: lifecycle tests use `ps` to verify detached descendant cleanup. Packaging tests also use npm and tar. The optional Pi and OpenCode integration suites use separately installed CLIs, never a model provider; the opt-in real AGY suite below does make model calls. ## Run @@ -17,7 +17,18 @@ Pi packaging checks run in the standard suite (`pi-packaging.test.mjs`). They ch With Pi installed, `npm run test:pi` exercises its real package loader, skill-command expansion, and Bash tool, using disposable settings and fake agy. The suite discovers Pi from PATH, or accepts `AGY_PI_PACKAGE_ROOT`. It never reads credentials or calls a model; offline success does not prove LLM behavior. For manual testing in Pi, use `pi -e /path/to/checkout` (temporary session) or `pi install /path/to/checkout`. After editing canonical skills in `skills/`, re-run `npm run generate:pi` and run `/reload` in Pi. -GitHub CI has three jobs on pushes and pull requests, all on Node 24. `Generated skills consistency` runs `npm run check:pi` automatically and keeps the required status-check name. `Tests (Ubuntu)` runs `npm test` automatically. `Tests (Windows)` runs the same suite with a 60s per-test timeout (`node --test --test-timeout=60000 tests/*.test.mjs`) and a 30-minute job limit, gated by the `manual-tests` environment: a repository maintainer must approve the pending deployment before it starts. Windows is expected to fail until the companion gains Windows process handling (see issue #19). Pi integration and real AGY smoke tests remain opt-in; CI does not install Pi or run a Node version matrix. +OpenCode packaging checks (`opencode-packaging.test.mjs`) run offline in the standard suite. They verify generated policy and resources, idempotent config registration, the actual npm artifact, and fake-agy ask plus background dispatch/collection from another project directory. Run `npm run generate:skills` after editing canonical skills; `npm run check:skills` checks both hosts without writing. + +For the opt-in real OpenCode V1 suite, install a pinned host into a disposable directory and point the test at its binary: + +```bash +npm install --prefix /tmp/agy-opencode-host --no-audit --no-fund opencode-ai@1.18.34 +AGY_OPENCODE_BIN=/tmp/agy-opencode-host/node_modules/.bin/opencode npm run test:opencode +``` + +The test checks the host version, installs the actual npm tarball using `opencode plugin`, checks real skill discovery, and checks the native `/command` server endpoint. It isolates HOME, OpenCode test home and all XDG config/data/cache/state directories, clears inherited OpenCode config/auth overrides, disables model-list fetching and external skill discovery, and uses no model credentials. The host/package installation may need network access; a passing smoke proves package loading and command discovery, not model compliance with skill instructions. OpenCode V2 is not tested. On Windows, set `AGY_OPENCODE_BIN` with the shell's environment syntax and use the platform's installed executable path. + +GitHub CI has three jobs on pushes and pull requests, all on Node 24. `Generated skills consistency` runs `npm run check:skills` automatically and keeps the required status-check name. `Tests (Ubuntu)` runs `npm test` automatically. `Tests (Windows)` runs the same suite with a 60s per-test timeout (`node --test --test-timeout=60000 tests/*.test.mjs`) and a 30-minute job limit, gated by the `manual-tests` environment: a repository maintainer must approve the pending deployment before it starts. Windows is expected to fail until the companion gains Windows process handling (see issue #19). Pi/OpenCode integration and real AGY smoke tests remain opt-in; CI does not install Pi or OpenCode or run a Node version matrix. Note: `node --test tests/` does **not** work on Node >= 22 — positional arguments are glob patterns there, and a bare directory matches the directory diff --git a/tests/opencode-packaging.test.mjs b/tests/opencode-packaging.test.mjs new file mode 100644 index 0000000..3904d60 --- /dev/null +++ b/tests/opencode-packaging.test.mjs @@ -0,0 +1,95 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { spawnSync } from 'node:child_process'; +import { generateSkills, skillFiles, ROOT, COMPATIBILITY_CONTEXT } from '../scripts/generate-pi-skills.mjs'; +import { sandbox, FAKE_AGY, jobIdOf } from './helpers.mjs'; +import { pack } from './pi-pack-helpers.mjs'; + +const names = ['ask', 'implementer', 'jobs', 'lead', 'researcher', 'reviewer', 'staffer']; + +test('OpenCode output preserves canonical policy, rewrites invocations, and resolves every resource', () => { + assert.deepEqual(generateSkills({ target: 'opencode', check: true }).changed, []); + assert.deepEqual(fs.readdirSync(path.join(ROOT, 'opencode-skills')).sort(), names.map(name => `agy-${name}`)); + for (const name of names) { + const skillDir = path.join(ROOT, 'opencode-skills', `agy-${name}`); + const canonical = fs.readFileSync(path.join(ROOT, 'skills', name, 'SKILL.md'), 'utf8'); + const generated = fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf8'); + assert.match(generated, new RegExp(`^name: agy-${name}$`, 'm')); + assert.doesNotMatch(generated, /^(?:allowed-tools|argument-hint|user-invocable):/m); + assert.doesNotMatch(generated, /\/agy:|\$agy:|\/skill:agy-/); + let body = canonical.replace(/^---\n[\s\S]*?\n---\n/, ''); + for (const peer of names) { + body = body.replaceAll(`/agy:${peer}`, `/agy-${peer}`).replaceAll(`$agy:${peer}`, `/agy-${peer}`) + .replaceAll(`../${peer}/`, `../agy-${peer}/`) + .replaceAll(`/skills/${peer}/`, `/opencode-skills/agy-${peer}/`); + } + assert.equal(generated.replace(/^---\n[\s\S]*?\n---\n\n\n/, ''), body + '\n' + COMPATIBILITY_CONTEXT); + for (const match of generated.matchAll(/`((?:\.\.\/|references\/)[^`]*\.md)`/g)) { + assert.ok(fs.existsSync(path.resolve(skillDir, match[1])), `${name}: ${match[1]}`); + } + assert.ok(fs.existsSync(path.resolve(skillDir, '../../companion/agy-companion.mjs'))); + } +}); + +test('OpenCode generation copies assets, detects drift and never overwrites in check mode', t => { + const sb = sandbox('opencode-generation'); + t.after(() => fs.rmSync(sb.root, { recursive: true, force: true })); + fs.cpSync(path.join(ROOT, 'skills'), path.join(sb.root, 'skills'), { recursive: true }); + const asset = Buffer.from([0, 128, 255]); + fs.writeFileSync(path.join(sb.root, 'skills/ask/asset.bin'), asset); + const options = { root: sb.root, target: 'opencode' }; + generateSkills(options); + assert.deepEqual(generateSkills(options).changed, []); + assert.deepEqual(fs.readFileSync(path.join(sb.root, 'opencode-skills/agy-ask/asset.bin')), asset); + const output = path.join(sb.root, 'opencode-skills/agy-ask/SKILL.md'); + fs.appendFileSync(output, '\nhand edit\n'); + assert.throws(() => generateSkills({ ...options, check: true }), /Stale OpenCode skills/); + assert.match(fs.readFileSync(output, 'utf8'), /hand edit/); +}); + +test('packed OpenCode plugin registers only bundled branded skills idempotently and runs jobs from another cwd', async t => { + const sb = sandbox('opencode-pack'); + t.after(() => fs.rmSync(sb.root, { recursive: true, force: true })); + const { metadata, dir } = pack(sb.root); + const packed = new Set(metadata.files.map(file => file.path)); + for (const file of skillFiles(ROOT, 'opencode').keys()) assert.ok(packed.has(file.split(path.sep).join('/')), file); + for (const file of ['opencode.mjs', 'templates/harness-compatibility.md', 'companion/agy-companion.mjs', 'docs/INSTALL_FOR_AGENTS.md']) assert.ok(packed.has(file), file); + const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')); + assert.equal(manifest.main, './opencode.mjs'); + assert.equal(manifest.exports, './opencode.mjs'); + const plugin = await import(pathToFileURL(path.join(dir, manifest.main))); + assert.deepEqual(Object.keys(plugin), ['AgyStaffPlugin']); + const hooks = await plugin.AgyStaffPlugin({}); + const config = { skills: { paths: ['/existing/skills'], urls: ['https://example.invalid/skills'] }, permission: { skill: { '*': 'ask' } } }; + await hooks.config(config); + await hooks.config(config); + assert.deepEqual(config, { skills: { paths: ['/existing/skills', path.join(dir, 'opencode-skills')], urls: ['https://example.invalid/skills'] }, permission: { skill: { '*': 'ask' } } }); + const empty = {}; + await hooks.config(empty); + assert.deepEqual(empty, { skills: { paths: [path.join(dir, 'opencode-skills')] } }); + const invoke = (skill, args, sleep = '150') => { + const skillDir = path.join(empty.skills.paths[0], `agy-${skill}`); + const command = /node "\/([^"\n]+)"/.exec(fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf8')); + assert.ok(command, `companion command missing from ${skill}`); + const result = spawnSync(process.execPath, [path.resolve(skillDir, command[1]), ...args], { + cwd: sb.repo, encoding: 'utf8', timeout: 60_000, + env: { ...process.env, HOME: sb.home, USERPROFILE: sb.home, AGY_BIN: FAKE_AGY, FAKE_AGY_RESPONSE: 'OpenCode package OK', FAKE_AGY_SLEEP_MS: sleep }, + }); + if (result.error) throw result.error; + return result; + }; + const ask = invoke('ask', ['ask', '--prompt', 'reply with OK']); + assert.equal(ask.status, 0, ask.stderr); + assert.match(ask.stdout, /OpenCode package OK/); + const start = invoke('staffer', ['staffer', '--prompt', 'read-only test'], '1200'); + assert.equal(start.status, 0, start.stderr); + const id = jobIdOf(start.stdout); + const pending = invoke('jobs', ['wait', id, '--timeout', '1ms']); + assert.equal(pending.status, 2, pending.stderr); + const done = invoke('jobs', ['wait', id, '--timeout', process.platform === 'win32' ? '20s' : '5s']); + assert.equal(done.status, 0, done.stderr); + assert.match(done.stdout, /OpenCode package OK/); +}); diff --git a/tests/opencode.integration.mjs b/tests/opencode.integration.mjs new file mode 100644 index 0000000..3564124 --- /dev/null +++ b/tests/opencode.integration.mjs @@ -0,0 +1,75 @@ +// Opt-in real OpenCode V1 package/skill/command discovery. No model calls. +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import { spawn } from 'node:child_process'; +import { sandbox } from './helpers.mjs'; +import { exec, pack } from './pi-pack-helpers.mjs'; + +const names = ['agy-ask', 'agy-implementer', 'agy-jobs', 'agy-lead', 'agy-researcher', 'agy-reviewer', 'agy-staffer']; + +test('OpenCode V1: native package install registers seven skills and same-named commands', { timeout: 180_000 }, async t => { + const binary = process.env.AGY_OPENCODE_BIN || 'opencode'; + const sb = sandbox('opencode-host'); + t.after(() => fs.rmSync(sb.root, { recursive: true, force: true })); + const env = { + ...process.env, HOME: sb.home, USERPROFILE: sb.home, OPENCODE_TEST_HOME: sb.home, + XDG_CONFIG_HOME: path.join(sb.root, 'config'), XDG_CACHE_HOME: path.join(sb.root, 'cache'), + XDG_DATA_HOME: path.join(sb.root, 'data'), XDG_STATE_HOME: path.join(sb.root, 'state'), + OPENCODE_DISABLE_DEFAULT_PLUGINS: '1', OPENCODE_DISABLE_EXTERNAL_SKILLS: '1', + OPENCODE_DISABLE_MODELS_FETCH: '1', OPENCODE_DISABLE_AUTOUPDATE: '1', + }; + // Do not inherit host config, auth, server credentials, or inline overrides. + for (const key of Object.keys(env)) { + if (/^(OPENCODE_CONFIG|OPENCODE_AUTH|OPENCODE_SERVER_|OPENCODE_EXPERIMENTAL|OPENCODE_CLIENT|AGY_.*TOKEN)/.test(key)) delete env[key]; + if (/(?:API_KEY|AUTH_TOKEN|ACCESS_TOKEN)$/.test(key)) delete env[key]; + } + const version = exec(binary, ['--version'], { cwd: sb.repo, env }).trim(); + assert.equal(version, '1.18.34', 'This integration contract is pinned; set AGY_OPENCODE_BIN to OpenCode 1.18.34.'); + t.diagnostic(`OpenCode ${version}`); + const unrelated = path.join(sb.repo, '.opencode/skills/reviewer'); + fs.mkdirSync(unrelated, { recursive: true }); + fs.writeFileSync(path.join(unrelated, 'SKILL.md'), '---\nname: reviewer\ndescription: An unrelated review workflow.\n---\nNot agy.\n'); + const { metadata } = pack(sb.root); + const spec = `agy-staff@file:${path.join(sb.root, metadata.filename)}`; + exec(binary, ['plugin', spec, '--global'], { cwd: sb.repo, env, timeout: 120_000 }); + const discovered = JSON.parse(exec(binary, ['debug', 'skill'], { cwd: sb.repo, env, timeout: 120_000 })); + // OpenCode also ships its own built-in skills. + const skills = discovered.filter(skill => skill.name.startsWith('agy-')); + assert.deepEqual(skills.map(skill => skill.name).sort(), names); + assert.ok(discovered.some(skill => skill.name === 'reviewer'), 'unrelated reviewer skill survives'); + for (const skill of skills) { + assert.ok(skill.location.includes('opencode-skills'), skill.location); + assert.ok(fs.existsSync(path.resolve(path.dirname(skill.location), '../../companion/agy-companion.mjs'))); + } + // Native skill -> slash-command mapping is exposed by the real server. + const server = spawn(binary, ['serve', '--hostname', '127.0.0.1', '--port', '0'], { cwd: sb.repo, env, stdio: ['ignore', 'pipe', 'pipe'] }); + let logs = ''; + t.after(async () => { + if (server.exitCode !== null) return; + await new Promise(resolve => { server.once('exit', resolve); server.kill(); }); + }); + const url = await new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error(`OpenCode server startup timed out: ${logs}`)), 60_000); + const collect = chunk => { + logs += chunk.toString(); + const match = /https?:\/\/127\.0\.0\.1:\d+/.exec(logs); + if (match) { clearTimeout(timer); resolve(match[0]); } + }; + server.stdout.on('data', collect); + server.stderr.on('data', collect); + server.once('error', error => { clearTimeout(timer); reject(error); }); + server.once('exit', code => { clearTimeout(timer); reject(new Error(`OpenCode server exited ${code}: ${logs}`)); }); + }); + const response = await fetch(`${url}/command`, { signal: AbortSignal.timeout(60_000) }); + assert.equal(response.status, 200); + const commands = await response.json(); + assert.deepEqual(commands.filter(command => command.name.startsWith('agy-')).map(command => command.name).sort(), names); + assert.ok(commands.some(command => command.name === 'reviewer' && command.source === 'skill'), 'unrelated reviewer command survives'); + for (const name of names) { + const command = commands.find(item => item.name === name); + assert.equal(command.source, 'skill'); + assert.match(command.template, /|companion/); + } +}); From 4ef5946bb2d1587630320df13e431a6f70ffd042 Mon Sep 17 00:00:00 2001 From: pkuwkl Date: Fri, 2 Oct 2026 23:11:08 +0800 Subject: [PATCH 2/4] fix(opencode): keep native Git installation free of preparation hooks --- .gitattributes | 3 ++- README.md | 2 +- README.zh-CN.md | 2 +- package.json | 5 +++-- tests/README.md | 4 ++-- tests/opencode-packaging.test.mjs | 18 ++++++++++++++++++ tests/opencode.integration.mjs | 14 +++++++++++++- 7 files changed, 40 insertions(+), 8 deletions(-) diff --git a/.gitattributes b/.gitattributes index d9cc024..bf952e8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,6 @@ -# Check out text files with LF on every platform: the Pi skill generator and +# Check out text files with LF on every platform: the skill generator and # its tests parse SKILL.md frontmatter with \n, which CRLF breaks. * text=auto eol=lf pi-skills/** linguist-generated=true +opencode-skills/** linguist-generated=true diff --git a/README.md b/README.md index 43b14c2..e8b0bd9 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ A few things worth knowing before you open a PR: - **Run the tests**: `npm test`. The standard suite uses temporary repos and HOME directories with fake `agy`, plus focused module tests. Keep regression tests offline and independent of personal settings. Real AGY validation is a separate opt-in suite described in [tests/README.md](tests/README.md). - **Docs come in pairs**: `README.md` / `README.zh-CN.md` and `docs/REFERENCE.md` / `docs/REFERENCE.zh-CN.md` are kept in sync. Change one, change its counterpart. - **Runtime code lives in `companion/`**: the entrypoint handles modes and job commands; separate modules handle streaming execution, observations and state locking. Skills call the companion, and `templates/` holds the shared prompts. -- **Canonical skills are the source of truth**: edit personas in `skills/`, never in `pi-skills/` or `opencode-skills/`. Run `npm run generate:skills` to generate both host entrypoints, and `npm run check:skills` to verify consistency. The existing `generate:pi` and `check:pi` commands remain available. +- **Canonical skills are the source of truth**: edit personas in `skills/`, never in `pi-skills/` or `opencode-skills/`. Run `npm run generate:skills` to generate both host entrypoints, and `npm run check:skills` to verify consistency. The existing `generate:pi` and `check:pi` commands remain available. Use `npm run pack:checked` to check freshness before packing; publishing also runs the check. Adding a mode or a flag changes the public surface, so please open an issue first and we can agree on the shape. diff --git a/README.zh-CN.md b/README.zh-CN.md index bed3b22..c83b291 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -158,7 +158,7 @@ codex plugin marketplace upgrade && codex plugin add agy@agy-staff 提交代码前请运行 `npm test`。标准测试使用临时仓库、临时 HOME 和假的 agy,不会调用真实模型或改动你的个人配置。新增回归测试也应保持这一点。需要验证真实 AGY 时,请使用[测试说明](tests/README.md)中单独启用的集成测试。 -运行逻辑位于 `companion/`:入口负责模式和任务命令,独立模块负责流式执行、进度快照和状态锁。角色技能位于 `skills/`,共享的提示词模板位于 `templates/`。修改技能时请以 `skills/` 为准,再运行 `npm run generate:skills` 生成 `pi-skills/` 与 `opencode-skills/`,用 `npm run check:skills` 检查一致性。不要直接编辑生成文件;原有 `generate:pi` 和 `check:pi` 命令仍然可用。 +运行逻辑位于 `companion/`:入口负责模式和任务命令,独立模块负责流式执行、进度快照和状态锁。角色技能位于 `skills/`,共享的提示词模板位于 `templates/`。修改技能时请以 `skills/` 为准,再运行 `npm run generate:skills` 生成 `pi-skills/` 与 `opencode-skills/`,用 `npm run check:skills` 检查一致性。不要直接编辑生成文件;原有 `generate:pi` 和 `check:pi` 命令仍然可用。打包时使用 `npm run pack:checked`,先检查一致性再生成归档;发布前也会自动检查。 README 和参考手册都有中英文版本。修改使用方法或行为说明时,请同步更新对应版本,让两种语言的读者得到一致的信息。 diff --git a/package.json b/package.json index fe61366..e301c22 100644 --- a/package.json +++ b/package.json @@ -30,12 +30,13 @@ "check:pi": "node scripts/generate-pi-skills.mjs --check", "test": "node --test tests/*.test.mjs", "test:pi": "node --test tests/pi.integration.mjs", - "prepack": "npm run check:skills", "generate:opencode": "node scripts/generate-pi-skills.mjs --target=opencode", "check:opencode": "node scripts/generate-pi-skills.mjs --target=opencode --check", "generate:skills": "npm run generate:pi && npm run generate:opencode", "check:skills": "npm run check:pi && npm run check:opencode", - "test:opencode": "node --test tests/opencode.integration.mjs" + "test:opencode": "node --test tests/opencode.integration.mjs", + "pack:checked": "npm run check:skills && npm pack --ignore-scripts", + "prepublishOnly": "npm run check:skills" }, "pi": { "skills": [ diff --git a/tests/README.md b/tests/README.md index 47cd8b6..f188414 100644 --- a/tests/README.md +++ b/tests/README.md @@ -17,7 +17,7 @@ Pi packaging checks run in the standard suite (`pi-packaging.test.mjs`). They ch With Pi installed, `npm run test:pi` exercises its real package loader, skill-command expansion, and Bash tool, using disposable settings and fake agy. The suite discovers Pi from PATH, or accepts `AGY_PI_PACKAGE_ROOT`. It never reads credentials or calls a model; offline success does not prove LLM behavior. For manual testing in Pi, use `pi -e /path/to/checkout` (temporary session) or `pi install /path/to/checkout`. After editing canonical skills in `skills/`, re-run `npm run generate:pi` and run `/reload` in Pi. -OpenCode packaging checks (`opencode-packaging.test.mjs`) run offline in the standard suite. They verify generated policy and resources, idempotent config registration, the actual npm artifact, and fake-agy ask plus background dispatch/collection from another project directory. Run `npm run generate:skills` after editing canonical skills; `npm run check:skills` checks both hosts without writing. +OpenCode packaging checks (`opencode-packaging.test.mjs`) run offline in the standard suite. They verify generated policy and resources, idempotent config registration, the actual npm artifact, and fake-agy ask plus background dispatch/collection from another project directory. Run `npm run generate:skills` after editing canonical skills; `npm run check:skills` checks both hosts without writing. Use `npm run pack:checked` to validate before creating an archive; `prepublishOnly` also validates before publishing. The package intentionally has no `prepack` or `prepare` hook: their presence makes pacote invoke nested npm during Git installation, which fails inside OpenCode’s compiled runtime even with lifecycle scripts disabled. CI and checked packing retain the generation gate; bare `npm pack` does not validate freshness. For the opt-in real OpenCode V1 suite, install a pinned host into a disposable directory and point the test at its binary: @@ -26,7 +26,7 @@ npm install --prefix /tmp/agy-opencode-host --no-audit --no-fund opencode-ai@1.1 AGY_OPENCODE_BIN=/tmp/agy-opencode-host/node_modules/.bin/opencode npm run test:opencode ``` -The test checks the host version, installs the actual npm tarball using `opencode plugin`, checks real skill discovery, and checks the native `/command` server endpoint. It isolates HOME, OpenCode test home and all XDG config/data/cache/state directories, clears inherited OpenCode config/auth overrides, disables model-list fetching and external skill discovery, and uses no model credentials. The host/package installation may need network access; a passing smoke proves package loading and command discovery, not model compliance with skill instructions. OpenCode V2 is not tested. On Windows, set `AGY_OPENCODE_BIN` with the shell's environment syntax and use the platform's installed executable path. +The test checks the host version, installs the actual npm tarball and a disposable Git repository containing the packed files using `opencode plugin`, checks real skill discovery, and checks the native `/command` server endpoint. The Git fixture exercises the installer’s separate dependency-preparation path. It isolates HOME, OpenCode test home and all XDG config/data/cache/state directories, clears inherited OpenCode config/auth overrides, disables model-list fetching and external skill discovery, and uses no model credentials. The host/package installation may need network access; a passing smoke proves package loading and command discovery, not model compliance with skill instructions. OpenCode V2 is not tested. On Windows, set `AGY_OPENCODE_BIN` with the shell's environment syntax and use the platform's installed executable path. GitHub CI has three jobs on pushes and pull requests, all on Node 24. `Generated skills consistency` runs `npm run check:skills` automatically and keeps the required status-check name. `Tests (Ubuntu)` runs `npm test` automatically. `Tests (Windows)` runs the same suite with a 60s per-test timeout (`node --test --test-timeout=60000 tests/*.test.mjs`) and a 30-minute job limit, gated by the `manual-tests` environment: a repository maintainer must approve the pending deployment before it starts. Windows is expected to fail until the companion gains Windows process handling (see issue #19). Pi/OpenCode integration and real AGY smoke tests remain opt-in; CI does not install Pi or OpenCode or run a Node version matrix. diff --git a/tests/opencode-packaging.test.mjs b/tests/opencode-packaging.test.mjs index 3904d60..380ea92 100644 --- a/tests/opencode-packaging.test.mjs +++ b/tests/opencode-packaging.test.mjs @@ -60,6 +60,24 @@ test('packed OpenCode plugin registers only bundled branded skills idempotently const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')); assert.equal(manifest.main, './opencode.mjs'); assert.equal(manifest.exports, './opencode.mjs'); + // Pacote invokes npm to prepare Git dependencies when any of these hooks + // exist, even with ignoreScripts. OpenCode's compiled runtime cannot run + // that nested npm; checked packing and prepublishOnly keep release checks. + for (const hook of ['prepack', 'prepare', 'preinstall', 'install', 'postinstall', 'build']) { + assert.equal(manifest.scripts[hook], undefined, `${hook} breaks native Git installation`); + } + const output = path.join(dir, 'opencode-skills/agy-ask/SKILL.md'); + const original = fs.readFileSync(output); + fs.appendFileSync(output, '\nstale generated content\n'); + const stalePack = spawnSync('npm', ['run', 'pack:checked'], { + cwd: dir, encoding: 'utf8', timeout: 60_000, shell: process.platform === 'win32', + env: { ...process.env, npm_config_cache: path.join(sb.root, 'npm-cache'), npm_config_update_notifier: 'false' }, + }); + assert.equal(stalePack.status, 1, stalePack.stderr); + assert.match(stalePack.stderr, /Stale OpenCode skills/); + assert.equal(fs.readdirSync(dir).some(file => file.endsWith('.tgz')), false, 'drift must fail before creating an archive'); + fs.writeFileSync(output, original); + const plugin = await import(pathToFileURL(path.join(dir, manifest.main))); assert.deepEqual(Object.keys(plugin), ['AgyStaffPlugin']); const hooks = await plugin.AgyStaffPlugin({}); diff --git a/tests/opencode.integration.mjs b/tests/opencode.integration.mjs index 3564124..b6ab707 100644 --- a/tests/opencode.integration.mjs +++ b/tests/opencode.integration.mjs @@ -4,6 +4,7 @@ import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import { spawn } from 'node:child_process'; +import { pathToFileURL } from 'node:url'; import { sandbox } from './helpers.mjs'; import { exec, pack } from './pi-pack-helpers.mjs'; @@ -31,9 +32,20 @@ test('OpenCode V1: native package install registers seven skills and same-named const unrelated = path.join(sb.repo, '.opencode/skills/reviewer'); fs.mkdirSync(unrelated, { recursive: true }); fs.writeFileSync(path.join(unrelated, 'SKILL.md'), '---\nname: reviewer\ndescription: An unrelated review workflow.\n---\nNot agy.\n'); - const { metadata } = pack(sb.root); + const { metadata, dir: fixture } = pack(sb.root); const spec = `agy-staff@file:${path.join(sb.root, metadata.filename)}`; exec(binary, ['plugin', spec, '--global'], { cwd: sb.repo, env, timeout: 120_000 }); + // Exercise the separate Git preparation path too. A prepack/prepare hook + // makes pacote spawn npm inside OpenCode's compiled runtime and breaks + // Git installs even when the same package works as a tarball. + exec('git', ['init', '-q'], { cwd: fixture, env }); + exec('git', ['add', '.'], { cwd: fixture, env }); + exec('git', ['-c', 'user.name=AGY Test', '-c', 'user.email=agy-test@example.invalid', '-c', 'commit.gpgsign=false', 'commit', '-qm', 'Packed fixture'], { cwd: fixture, env }); + const revision = exec('git', ['rev-parse', 'HEAD'], { cwd: fixture, env }).trim(); + const gitSpec = `agy-staff@git+${pathToFileURL(fixture).href}#${revision}`; + exec(binary, ['plugin', gitSpec, '--global', '--force'], { cwd: sb.repo, env, timeout: 120_000 }); + const configured = JSON.parse(exec(binary, ['debug', 'config'], { cwd: sb.repo, env, timeout: 120_000 })); + assert.ok(configured.plugin.some(entry => (Array.isArray(entry) ? entry[0] : entry) === gitSpec), 'Git install must replace the tarball entry'); const discovered = JSON.parse(exec(binary, ['debug', 'skill'], { cwd: sb.repo, env, timeout: 120_000 })); // OpenCode also ships its own built-in skills. const skills = discovered.filter(skill => skill.name.startsWith('agy-')); From 373b7ccfae753b47638ffd61d2b42627c7a89977 Mon Sep 17 00:00:00 2001 From: pkuwkl Date: Sat, 3 Oct 2026 12:41:13 +0800 Subject: [PATCH 3/4] refactor: generalize derived skill generation --- docs/REFERENCE.md | 2 +- docs/REFERENCE.zh-CN.md | 2 +- package.json | 14 ++-- ...rate-pi-skills.mjs => generate-skills.mjs} | 72 +++++++++++-------- tests/README.md | 2 +- tests/opencode-packaging.test.mjs | 2 +- tests/pi-pack-helpers.mjs | 2 +- tests/pi-packaging.test.mjs | 4 +- tests/pi.integration.mjs | 2 +- 9 files changed, 58 insertions(+), 44 deletions(-) rename scripts/{generate-pi-skills.mjs => generate-skills.mjs} (57%) diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 7416ba7..1c3f961 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -298,7 +298,7 @@ templates/ shared prompt templates (staffer/ask/research/revi pi-skills/ generated agy-* entrypoints/resources for Pi; do not hand-edit opencode-skills/ generated agy-* entrypoints/resources for OpenCode; do not hand-edit opencode.mjs OpenCode V1 package plugin; registers bundled skills -scripts/generate-pi-skills.mjs generates Pi/OpenCode skills and checks for drift +scripts/generate-skills.mjs generates Pi/OpenCode skills and checks for drift package.json Pi manifest, OpenCode entrypoint, npm file allowlist, and verification commands skills/ canonical personas + jobs (Claude/Codex entrypoints; reviewer/ and jobs/ carry references/ for on-demand detail) diff --git a/docs/REFERENCE.zh-CN.md b/docs/REFERENCE.zh-CN.md index 9a7bcf4..5ecc881 100644 --- a/docs/REFERENCE.zh-CN.md +++ b/docs/REFERENCE.zh-CN.md @@ -403,7 +403,7 @@ pi-skills/ 自动生成的 Pi 入口与参考文件,不应 templates/ 共享提示词模板与宿主兼容说明 opencode-skills/ 自动生成的 OpenCode 入口与参考文件,不应手动编辑 opencode.mjs OpenCode V1 包入口,注册包内技能 -scripts/generate-pi-skills.mjs 生成 Pi/OpenCode 技能并检查一致性 +scripts/generate-skills.mjs 生成 Pi/OpenCode 技能并检查一致性 .claude-plugin/ Claude Code 插件与插件市场配置 .codex-plugin/plugin.json Codex 插件配置 .agents/plugins/ Codex 插件市场配置 diff --git a/package.json b/package.json index e301c22..cfc5316 100644 --- a/package.json +++ b/package.json @@ -20,20 +20,20 @@ "opencode.mjs", "companion/", "templates/", - "scripts/generate-pi-skills.mjs", + "scripts/generate-skills.mjs", "docs/INSTALL_FOR_AGENTS.md", "docs/REFERENCE*.md", "README.zh-CN.md" ], "scripts": { - "generate:pi": "node scripts/generate-pi-skills.mjs", - "check:pi": "node scripts/generate-pi-skills.mjs --check", + "generate:pi": "node scripts/generate-skills.mjs --target=pi", + "check:pi": "node scripts/generate-skills.mjs --target=pi --check", "test": "node --test tests/*.test.mjs", "test:pi": "node --test tests/pi.integration.mjs", - "generate:opencode": "node scripts/generate-pi-skills.mjs --target=opencode", - "check:opencode": "node scripts/generate-pi-skills.mjs --target=opencode --check", - "generate:skills": "npm run generate:pi && npm run generate:opencode", - "check:skills": "npm run check:pi && npm run check:opencode", + "generate:opencode": "node scripts/generate-skills.mjs --target=opencode", + "check:opencode": "node scripts/generate-skills.mjs --target=opencode --check", + "generate:skills": "node scripts/generate-skills.mjs", + "check:skills": "node scripts/generate-skills.mjs --check", "test:opencode": "node --test tests/opencode.integration.mjs", "pack:checked": "npm run check:skills && npm pack --ignore-scripts", "prepublishOnly": "npm run check:skills" diff --git a/scripts/generate-pi-skills.mjs b/scripts/generate-skills.mjs similarity index 57% rename from scripts/generate-pi-skills.mjs rename to scripts/generate-skills.mjs index 359133c..e69e3ec 100644 --- a/scripts/generate-pi-skills.mjs +++ b/scripts/generate-skills.mjs @@ -19,20 +19,27 @@ function filesUnder(dir) { } const targets = { - pi: { label: 'Pi', invocation: '/skill:agy-' }, - opencode: { label: 'OpenCode', invocation: '/agy-' }, + pi: { + label: 'Pi', outputDir: 'pi-skills', namePrefix: 'agy-', invocationPrefix: '/skill:', + dropMetadata: ['allowed-tools', 'argument-hint', 'user-invocable'], appendCompatibility: true, + }, + opencode: { + label: 'OpenCode', outputDir: 'opencode-skills', namePrefix: 'agy-', invocationPrefix: '/', + dropMetadata: ['allowed-tools', 'argument-hint', 'user-invocable'], appendCompatibility: true, + }, }; -export function skillFiles(root = ROOT, target = 'pi') { +export function skillFiles(root = ROOT, target) { + if (!Object.hasOwn(targets, target)) throw new Error(`Unknown skill target: ${target}`); const host = targets[target]; - if (!host) throw new Error(`Unknown skill target: ${target}`); - const outputDir = `${target}-skills`; + const { outputDir, namePrefix, invocationPrefix } = host; const source = path.join(root, 'skills'); const names = fs.readdirSync(source).filter(name => fs.existsSync(path.join(source, name, 'SKILL.md'))).sort(); const outputs = new Map(); for (const name of names) { - if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name) || `agy-${name}`.length > 64) { - throw new Error(`Invalid ${host.label} skill name: agy-${name}`); + const generatedName = `${namePrefix}${name}`; + if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name) || generatedName.length > 64) { + throw new Error(`Invalid ${host.label} skill name: ${generatedName}`); } for (const file of filesUnder(path.join(source, name))) { let content = fs.readFileSync(file); @@ -41,9 +48,9 @@ export function skillFiles(root = ROOT, target = 'pi') { // Change only skill invocation syntax and skill-directory paths, never // companion subcommands such as `review`, `research`, or `wait`. for (const peer of names) { - content = content.replace(new RegExp(`(?:/agy:|\\$agy:)${peer}(?![a-z0-9-])`, 'g'), `${host.invocation}${peer}`) - .replaceAll(`../${peer}/`, `../agy-${peer}/`) - .replaceAll(`/skills/${peer}/`, `/${outputDir}/agy-${peer}/`); + content = content.replace(new RegExp(`(?:/agy:|\\$agy:)${peer}(?![a-z0-9-])`, 'g'), `${invocationPrefix}${namePrefix}${peer}`) + .replaceAll(`../${peer}/`, `../${namePrefix}${peer}/`) + .replaceAll(`/skills/${peer}/`, `/${outputDir}/${namePrefix}${peer}/`); } const canonicalRel = path.relative(root, file).split(path.sep).join('/'); const notice = ``; @@ -54,10 +61,11 @@ export function skillFiles(root = ROOT, target = 'pi') { } const frontmatter = match[1].split('\n') // These are Claude-specific UI/permission fields, not host policy. - .filter(line => !/^(allowed-tools|argument-hint|user-invocable):/.test(line)) - .map(line => line === `name: ${name}` ? `name: agy-${name}` : line).join('\n'); + .filter(line => !host.dropMetadata.includes(line.split(':', 1)[0])) + .map(line => line === `name: ${name}` ? `name: ${generatedName}` : line).join('\n'); content = `---\n${frontmatter}\n---\n\n${notice}\n` - + content.slice(match[0].length) + '\n' + COMPATIBILITY_CONTEXT; + + content.slice(match[0].length) + + (host.appendCompatibility ? '\n' + COMPATIBILITY_CONTEXT : ''); } else if (match) { content = `---\n${match[1]}\n---\n\n${notice}\n` + content.slice(match[0].length); } else { @@ -65,46 +73,50 @@ export function skillFiles(root = ROOT, target = 'pi') { } content = Buffer.from(content); } - outputs.set(path.join(outputDir, `agy-${name}`, path.relative(path.join(source, name), file)), content); + outputs.set(path.join(outputDir, generatedName, path.relative(path.join(source, name), file)), content); } } return outputs; } -export function generateSkills({ root = ROOT, check = false, target = 'pi' } = {}) { +export function generateSkills({ root = ROOT, check = false, target } = {}) { const expected = skillFiles(root, target); - const actual = filesUnder(path.join(root, `${target}-skills`)); + const host = targets[target]; + const actual = filesUnder(path.join(root, host.outputDir)); const unexpected = actual.filter(file => !expected.has(path.relative(root, file))); // Fail instead of deleting stale files automatically: a maintainer may have // edited them. Renames/removals must explicitly remove the obsolete output. if (unexpected.length) throw new Error(`Unexpected generated files; inspect and remove explicitly:\n${unexpected.join('\n')}`); const changed = []; for (const [relative, content] of expected) { - const target = path.join(root, relative); - if (fs.existsSync(target) && fs.readFileSync(target).equals(content)) continue; + const output = path.join(root, relative); + if (fs.existsSync(output) && fs.readFileSync(output).equals(content)) continue; changed.push(relative); if (!check) { - fs.mkdirSync(path.dirname(target), { recursive: true }); - fs.writeFileSync(target, content); + fs.mkdirSync(path.dirname(output), { recursive: true }); + fs.writeFileSync(output, content); } } if (check && changed.length) { - throw new Error(`Stale ${targets[target].label} skills; edit canonical sources in skills/ (do not edit ${target}-skills/) and run npm run generate:${target}:\n${changed.join('\n')}`); + throw new Error(`Stale ${host.label} skills; edit canonical sources in skills/ (do not edit ${host.outputDir}/) and run npm run generate:${target}:\n${changed.join('\n')}`); } return { count: expected.size, changed }; } -// Preserve the original Pi API for consumers and tests. -export const piFiles = (root = ROOT) => skillFiles(root, 'pi'); -export const generatePiSkills = (options = {}) => generateSkills({ ...options, target: 'pi' }); - if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { try { - if (process.argv.slice(2).some(arg => !['--check', '--target=pi', '--target=opencode'].includes(arg))) throw new Error('Usage: generate-pi-skills.mjs [--check] [--target=pi|opencode]'); - const target = process.argv.find(arg => arg.startsWith('--target='))?.slice(9) || 'pi'; - const check = process.argv.includes('--check'); - const result = generateSkills({ check, target }); - console.log(`${targets[target].label} skills ${check ? 'verified' : 'generated'}: ${result.count} files (${result.changed.length} changed).`); + const args = process.argv.slice(2); + const supported = ['all', ...Object.keys(targets)]; + const selectors = args.filter(arg => arg.startsWith('--target=')); + if (selectors.length > 1 || args.some(arg => arg !== '--check' && !supported.some(target => arg === `--target=${target}`))) { + throw new Error(`Usage: generate-skills.mjs [--check] [--target=${supported.join('|')}]`); + } + const selected = selectors[0]?.slice(9) || 'all'; + const check = args.includes('--check'); + for (const target of selected === 'all' ? Object.keys(targets) : [selected]) { + const result = generateSkills({ check, target }); + console.log(`${targets[target].label} skills ${check ? 'verified' : 'generated'}: ${result.count} files (${result.changed.length} changed).`); + } } catch (error) { console.error(error.message); process.exitCode = 1; diff --git a/tests/README.md b/tests/README.md index f188414..93b546a 100644 --- a/tests/README.md +++ b/tests/README.md @@ -17,7 +17,7 @@ Pi packaging checks run in the standard suite (`pi-packaging.test.mjs`). They ch With Pi installed, `npm run test:pi` exercises its real package loader, skill-command expansion, and Bash tool, using disposable settings and fake agy. The suite discovers Pi from PATH, or accepts `AGY_PI_PACKAGE_ROOT`. It never reads credentials or calls a model; offline success does not prove LLM behavior. For manual testing in Pi, use `pi -e /path/to/checkout` (temporary session) or `pi install /path/to/checkout`. After editing canonical skills in `skills/`, re-run `npm run generate:pi` and run `/reload` in Pi. -OpenCode packaging checks (`opencode-packaging.test.mjs`) run offline in the standard suite. They verify generated policy and resources, idempotent config registration, the actual npm artifact, and fake-agy ask plus background dispatch/collection from another project directory. Run `npm run generate:skills` after editing canonical skills; `npm run check:skills` checks both hosts without writing. Use `npm run pack:checked` to validate before creating an archive; `prepublishOnly` also validates before publishing. The package intentionally has no `prepack` or `prepare` hook: their presence makes pacote invoke nested npm during Git installation, which fails inside OpenCode’s compiled runtime even with lifecycle scripts disabled. CI and checked packing retain the generation gate; bare `npm pack` does not validate freshness. +OpenCode packaging checks (`opencode-packaging.test.mjs`) run offline in the standard suite. They verify generated policy and resources, idempotent config registration, the actual npm artifact, and fake-agy ask plus background dispatch/collection from another project directory. Run `npm run generate:skills` after editing canonical skills; `npm run check:skills` checks both hosts without writing. Both commands use `scripts/generate-skills.mjs`, whose target map defines each host’s output directory, skill prefix, invocation syntax, metadata filtering and compatibility context. The CLI defaults to all targets; `--target=pi` and `--target=opencode` select one host, as the existing host-specific npm commands do. Use `npm run pack:checked` to validate before creating an archive; `prepublishOnly` also validates before publishing. The package intentionally has no `prepack` or `prepare` hook: their presence makes pacote invoke nested npm during Git installation, which fails inside OpenCode’s compiled runtime even with lifecycle scripts disabled. CI and checked packing retain the generation gate; bare `npm pack` does not validate freshness. For the opt-in real OpenCode V1 suite, install a pinned host into a disposable directory and point the test at its binary: diff --git a/tests/opencode-packaging.test.mjs b/tests/opencode-packaging.test.mjs index 380ea92..51880a1 100644 --- a/tests/opencode-packaging.test.mjs +++ b/tests/opencode-packaging.test.mjs @@ -4,7 +4,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; import { spawnSync } from 'node:child_process'; -import { generateSkills, skillFiles, ROOT, COMPATIBILITY_CONTEXT } from '../scripts/generate-pi-skills.mjs'; +import { generateSkills, skillFiles, ROOT, COMPATIBILITY_CONTEXT } from '../scripts/generate-skills.mjs'; import { sandbox, FAKE_AGY, jobIdOf } from './helpers.mjs'; import { pack } from './pi-pack-helpers.mjs'; diff --git a/tests/pi-pack-helpers.mjs b/tests/pi-pack-helpers.mjs index ea0e38a..96913b7 100644 --- a/tests/pi-pack-helpers.mjs +++ b/tests/pi-pack-helpers.mjs @@ -1,7 +1,7 @@ import { spawnSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; -import { ROOT } from '../scripts/generate-pi-skills.mjs'; +import { ROOT } from '../scripts/generate-skills.mjs'; export function exec(command, args, options = {}) { const result = spawnSync(command, args, { encoding: 'utf8', timeout: 60_000, ...options }); diff --git a/tests/pi-packaging.test.mjs b/tests/pi-packaging.test.mjs index ea358c3..5f56f59 100644 --- a/tests/pi-packaging.test.mjs +++ b/tests/pi-packaging.test.mjs @@ -4,11 +4,13 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { spawnSync } from 'node:child_process'; -import { generatePiSkills, piFiles, ROOT, COMPATIBILITY_CONTEXT } from '../scripts/generate-pi-skills.mjs'; +import { generateSkills, skillFiles, ROOT, COMPATIBILITY_CONTEXT } from '../scripts/generate-skills.mjs'; import { sandbox, FAKE_AGY, jobIdOf } from './helpers.mjs'; import { pack } from './pi-pack-helpers.mjs'; const json = file => JSON.parse(fs.readFileSync(file, 'utf8')); +const generatePiSkills = options => generateSkills({ ...options, target: 'pi' }); +const piFiles = () => skillFiles(ROOT, 'pi'); const names = ['ask', 'implementer', 'jobs', 'lead', 'researcher', 'reviewer', 'staffer']; test('Pi adapters are current; canonical names and all relative resources remain valid', () => { diff --git a/tests/pi.integration.mjs b/tests/pi.integration.mjs index 76b52da..e26dc9a 100644 --- a/tests/pi.integration.mjs +++ b/tests/pi.integration.mjs @@ -4,7 +4,7 @@ import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; -import { ROOT } from '../scripts/generate-pi-skills.mjs'; +import { ROOT } from '../scripts/generate-skills.mjs'; import { sandbox, FAKE_AGY, jobIdOf } from './helpers.mjs'; import { exec, pack } from './pi-pack-helpers.mjs'; From 5cca91fed6a99f3cceb30f37c60844d99289f777 Mon Sep 17 00:00:00 2001 From: pkuwkl Date: Sun, 4 Oct 2026 00:43:32 +0800 Subject: [PATCH 4/4] chore(release): 0.7.4 --- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- docs/releases/v0.7.4.md | 31 +++++++++++++++++++++++++++++++ package.json | 2 +- 4 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 docs/releases/v0.7.4.md diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 90aceca..57c3344 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agy", - "version": "0.7.3", + "version": "0.7.4", "description": "Delegate work to Google's Antigravity CLI (agy) with fast Gemini access - five personas: staffer (general), researcher, reviewer (code and plans), implementer, ask.", "author": { "name": "Keli Wen", diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 3d5919f..bc6aec8 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agy", - "version": "0.7.3", + "version": "0.7.4", "description": "Delegate work to Google's Antigravity CLI (agy) with fast Gemini access - five personas: staffer (general), researcher, reviewer (code and plans), implementer, ask.", "author": { "name": "Keli Wen", diff --git a/docs/releases/v0.7.4.md b/docs/releases/v0.7.4.md new file mode 100644 index 0000000..8aa5ed7 --- /dev/null +++ b/docs/releases/v0.7.4.md @@ -0,0 +1,31 @@ +# agy-staff 0.7.4 — OpenCode plugin support + +## Changes since 0.7.3 + +[#27](https://github.com/keli-wen/agy-staff/pull/27) adds native OpenCode V1 plugin installation with seven branded skills: `agy-ask`, `agy-staffer`, `agy-researcher`, `agy-reviewer`, `agy-implementer`, `agy-jobs`, and `agy-lead`. OpenCode discovers their slash commands directly from the installed skills; users do not need to clone the repository or configure skill paths. + +The shared `scripts/generate-skills.mjs` derives Pi and OpenCode entrypoints from canonical `skills/`. Target configuration controls output directories, naming, invocation syntax, metadata removal, and the shared compatibility appendix. Canonical workflows, existing Pi output, companion behavior, and prompt templates are unchanged. No additional OpenCode-specific workflow instructions are introduced. + +The package includes a small OpenCode loader and all referenced resources. Native Git installation required replacing the `prepack` hook: its presence triggers nested npm preparation in OpenCode's compiled runtime. Generated content is checked by CI, `npm run pack:checked` before packing, and `prepublishOnly` before publishing. Bare `npm pack` does not check generated-file freshness. + +Package, Claude Code, and Codex manifests are aligned at 0.7.4. OpenCode support is limited to V1 (`>=1.18.34 <2`). + +## Validation + +- The implementation passed all 198 offline tests, including actual npm archive execution and generated-file consistency checks. Ubuntu CI and the generated-skills check passed. +- Real OpenCode 1.18.34 installed npm and Git packages, discovered all seven skills and commands, and retained an unrelated skill. Installation from a public GitHub commit also passed. +- Live tests used OpenCode 1.18.34, Antigravity CLI 1.2.11, and DeepSeek Flash: ask returned the requested response, staffer dispatched and collected a real background job, and implementer fixed a fixture and passed its test without changing the test or unrelated file. +- The live host model was current DeepSeek V4.1 Flash (`deepseek-flash`). The requested legacy `deepseek-v4-flash` ID was not available in OpenCode's model catalog; this is not a test of the retired V4 weights. +- Live cancellation, timeout recovery, and permission rejection were not covered by these smoke tests. Windows CI remains separately gated by the repository's existing approval setting. + +## Installing and upgrading + +With Node.js and an authenticated Antigravity CLI available, install this version into OpenCode: + +```bash +opencode plugin 'agy-staff@git+https://github.com/keli-wen/agy-staff.git#v0.7.4' --global --force +``` + +Restart OpenCode, then run `/agy-ask reply with OK`. OpenCode caches Git package specs; changing the tag or commit selects a new install, while `--force` replaces the configured entry. + +Claude Code and Codex users should update the plugin and restart their host to load the versioned copy. Pi installations pinned to a Git ref can select `v0.7.4`. See the [upgrade instructions](../REFERENCE.md#upgrading). diff --git a/package.json b/package.json index cfc5316..105d016 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agy-staff", - "version": "0.7.3", + "version": "0.7.4", "description": "Delegate work to Google's Antigravity CLI (agy) with fast Gemini access.", "keywords": [ "pi-package",