@webpresso/opencode-plugin 3.1.30 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -1,5 +1,7 @@
1
- import { existsSync } from "node:fs";
2
- import { join } from "node:path";
1
+ import { accessSync, constants, existsSync, readFileSync, realpathSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
3
5
 
4
6
  const SESSION_START_HOOKS = ["sessionstart-routing"];
5
7
  const PRE_TOOL_HOOKS = ["pretool-guard"];
@@ -84,20 +86,112 @@ function shellQuote(value) {
84
86
  return `'${String(value).replaceAll("'", "'\\''")}'`;
85
87
  }
86
88
 
87
- async function resolveWpHookCommand($, directory, hookName) {
88
- const repoLauncher = join(directory, "bin", process.platform === "win32" ? "wp.cmd" : "wp");
89
- const candidates = [process.env.WEBPRESSO_WP_BIN, repoLauncher].filter(Boolean);
89
+ function isExecutableFile(candidate) {
90
+ try {
91
+ const stat = statSync(candidate);
92
+ if (!stat.isFile()) return false;
93
+ if (process.platform !== "win32") accessSync(candidate, constants.X_OK);
94
+ return true;
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
99
+
100
+ function isUsableWebpressoPackageRoot(packageRoot) {
101
+ try {
102
+ const pkg = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
103
+ return pkg.name === "@webpresso/agent-kit" && isExecutableFile(join(packageRoot, "bin", "wp"));
104
+ } catch {
105
+ return false;
106
+ }
107
+ }
108
+
109
+ function packageWpFromAdjacentNpmCmdShim(entryPath) {
110
+ if (!entryPath.toLowerCase().endsWith(".cmd")) return null;
111
+ const packageRoot = join(dirname(entryPath), "node_modules", "@webpresso", "agent-kit");
112
+ return isTrustedWebpressoPackageRoot(packageRoot) ? join(packageRoot, "bin", "wp") : null;
113
+ }
114
+
115
+ function isPathInsideOrEqual(parent, candidate) {
116
+ const parentRoot = realpathSync(parent);
117
+ const candidateRoot = realpathSync(candidate);
118
+ const rel = relative(resolve(parentRoot), resolve(candidateRoot));
119
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
120
+ }
121
+
122
+ function isAgentKitWp(candidate, _projectRoot) {
123
+ if (candidate.toLowerCase().endsWith(".cmd")) return false;
124
+ if (!isExecutableFile(candidate)) return false;
125
+ try {
126
+ const resolved = realpathSync(candidate);
127
+ const packageRoot = dirname(dirname(resolved));
128
+ if (resolved !== join(packageRoot, "bin", "wp")) return false;
129
+ return isTrustedWebpressoPackageRoot(packageRoot);
130
+ } catch {
131
+ return false;
132
+ }
133
+ }
90
134
 
91
- for (const candidate of candidates) {
92
- if (existsSync(candidate)) {
93
- return `${shellQuote(candidate)} hook ${hookName}`;
135
+ function currentAgentKitPackageRoot() {
136
+ let current = dirname(fileURLToPath(import.meta.url));
137
+ for (let depth = 0; depth < 12; depth += 1) {
138
+ const packagePath = join(current, "package.json");
139
+ if (existsSync(packagePath)) {
140
+ try {
141
+ const pkg = JSON.parse(readFileSync(packagePath, "utf8"));
142
+ if (pkg.name === "@webpresso/agent-kit") return current;
143
+ } catch {
144
+ return null;
145
+ }
94
146
  }
147
+ const parent = dirname(current);
148
+ if (parent === current) break;
149
+ current = parent;
150
+ }
151
+ return null;
152
+ }
153
+
154
+ function trustedHomeInstallPackageRoots() {
155
+ const home = process.env.HOME || homedir();
156
+ return [
157
+ join(home, ".claude", "plugins", "cache", "webpresso", "agent-kit"),
158
+ join(home, ".bun", "install", "global", "node_modules", "@webpresso", "agent-kit"),
159
+ ];
160
+ }
161
+
162
+ function isTrustedWebpressoPackageRoot(packageRoot) {
163
+ try {
164
+ if (!isUsableWebpressoPackageRoot(packageRoot)) return false;
165
+ const realPackageRoot = realpathSync(packageRoot);
166
+ const sourceRoot = currentAgentKitPackageRoot();
167
+ if (sourceRoot !== null && realPackageRoot === realpathSync(sourceRoot)) return true;
168
+ return trustedHomeInstallPackageRoots().some(
169
+ (anchor) => existsSync(anchor) && isPathInsideOrEqual(anchor, realPackageRoot),
170
+ );
171
+ } catch {
172
+ return false;
173
+ }
174
+ }
175
+
176
+ async function resolveWpHookCommand($, directory, hookName) {
177
+ const explicit = process.env.WEBPRESSO_WP_BIN;
178
+ if (explicit) {
179
+ if (isAgentKitWp(explicit, directory)) {
180
+ return `${shellQuote(explicit)} hook ${hookName}`;
181
+ }
182
+ throw new Error(
183
+ `WEBPRESSO_WP_BIN points to ${explicit}, but that executable is not @webpresso/agent-kit`,
184
+ );
95
185
  }
96
186
 
97
187
  const found = await $`command -v wp`.cwd(directory).quiet().nothrow();
98
188
  const resolved = found.stdout?.toString().trim().split(/\r?\n/u)[0] ?? "";
99
189
  if ((found.exitCode === undefined || found.exitCode === 0) && resolved.length > 0) {
100
- return `${shellQuote(resolved)} hook ${hookName}`;
190
+ const packageWp = packageWpFromAdjacentNpmCmdShim(resolved);
191
+ const command = packageWp ?? resolved;
192
+ if (isAgentKitWp(command, directory)) {
193
+ return `${shellQuote(command)} hook ${hookName}`;
194
+ }
101
195
  }
102
196
 
103
197
  throw new Error(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpresso/opencode-plugin",
3
- "version": "3.1.30",
3
+ "version": "3.3.0",
4
4
  "private": false,
5
5
  "description": "OpenCode plugin adapter package for Webpresso agent-kit.",
6
6
  "homepage": "https://github.com/webpresso/agent-kit#readme",
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": 1,
3
3
  "host": "opencode",
4
4
  "packageName": "@webpresso/opencode-plugin",
5
- "packageVersion": "3.1.30",
5
+ "packageVersion": "3.3.0",
6
6
  "runtimeDirs": [],
7
7
  "skills": {
8
8
  "ai-deslop": {
@@ -24,7 +24,7 @@
24
24
  "digest": "sha256:a97c243b11a25a893d3149a5545d3d111bc54e72c2e2acea9ce6fad3d333d2f1"
25
25
  },
26
26
  "codex": {
27
- "digest": "sha256:07804f1e139e1e2e76f34f35dd5471895072aea5d8b3f5f412cf1328a8180b4b"
27
+ "digest": "sha256:7093f390749bed751038ad480d68c12380cfa2630e61881c16be46da7f75e238"
28
28
  },
29
29
  "deep-interview": {
30
30
  "digest": "sha256:e80d7e12e486397103108bb473e98e8e8656519770fa20701f8b3e9e66991939"
@@ -90,7 +90,7 @@
90
90
  "digest": "sha256:953ab2625287866e5f27e602a89f1752e054d47522d04e1fdc11880867dc2fde"
91
91
  },
92
92
  "verify": {
93
- "digest": "sha256:60de27bef39fd24041e1b78a276fc7ed2f38bc6d7ac5af88beba7eb6770a0119"
93
+ "digest": "sha256:47c1d75ef5d1d47b94ef48ac91b0fd4bbf6ba1ab32563cfc28518bb75c7798f0"
94
94
  }
95
95
  }
96
96
  }
@@ -8,13 +8,37 @@ license: MIT
8
8
 
9
9
  Use this skill from Claude or another non-Codex host when the user wants Codex to independently review a diff, challenge a plan, or answer a repo question. Keep Codex read-only by default and treat its answer as external advice until independently verified.
10
10
 
11
+ ## Primary path: the `wp_review_run` MCP tool
12
+
13
+ When the webpresso MCP server is available, call `wp_review_run` directly instead of any bash block below — it is the same `wp review run` typed runtime, called in-process, with two ergonomic wins: it takes a `prompt` string (no `--prompt-file` bookkeeping) and it runs the probe stage and the review stage in **one call** by default (the review stage is skipped automatically if the probe fails, so you never waste a full review invocation on a cold/broken provider).
14
+
15
+ ```jsonc
16
+ // wp_review_run MCP tool call
17
+ {
18
+ "prompt": "<diff summary + what to look for>",
19
+ "provider": "codex",
20
+ // model, effort, artifactRoot, idleSeconds are all optional and default
21
+ // the same way the `wp review run` CLI does. Omit `stage` for automatic
22
+ // probe-then-review; pass stage: "probe" or stage: "review" only for
23
+ // manual single-stage control.
24
+ }
25
+ ```
26
+
27
+ **Provider fallback lives in this one call.** If your primary provider (e.g. Codex) is unavailable — out of credits, auth expired — switch the `provider` parameter to another supported value (`claude`, `opencode`, `grok`) in the SAME `wp_review_run` call. Never shell into a different provider's CLI directly; that bypasses artifact capture under `.webpresso/reviews`, availability tracking, and the probe-then-review handshake this tool exists to provide. The pretool guard hook actively blocks shelling straight into a provider CLI's review-launching subcommand and redirects here.
28
+
29
+ The bash blocks in this skill (below) are the **MCP-unavailable fallback only** — use them when the webpresso MCP server itself is not reachable in the current host, not as a provider-fallback mechanism.
30
+
11
31
  ## Single-shot budget (anti-stampede)
12
32
 
13
33
  - Default: **one** review invocation per request.
14
34
  - Do **not** fan out parallel multi-host review matrices unless the user set `review_budget`/`N` > 1.
15
- - Prefer `wp review run` / `wp review gate` over spawn/wait agent loops for review.
35
+ - Prefer `wp_review_run` (MCP) or `wp review run` / `wp review gate` (CLI fallback) over spawn/wait agent loops for review.
16
36
  - Keep prompts bounded; no whole-repo paste.
17
37
 
38
+ ## MCP-unavailable fallback (raw CLI)
39
+
40
+ Everything from here down is the manual `wp review run` CLI path documented for hosts or sessions where the webpresso MCP server is not reachable. Prefer `wp_review_run` above whenever MCP is available.
41
+
18
42
  ## Auth check
19
43
 
20
44
  ```bash
@@ -235,8 +235,10 @@ Reviewer preference (pick **one** path):
235
235
 
236
236
  Approval evidence requirements:
237
237
 
238
- - Blueprint lifecycle: `wp review gate <blueprint-slug> --target HEAD --json` first.
239
- Use `wp review log` / `wp review read` for inspection.
238
+ - Blueprint lifecycle: `wp review gate <blueprint-slug> --purpose delivery --target HEAD --json`
239
+ first. Delivery gates automatically run declared Promotion Gates from the
240
+ Trust Dossier before outside-review provider probes. Use `wp review log` /
241
+ `wp review read` for inspection.
240
242
  - Each reviewer must return clear `APPROVED`/`BLOCKED` with enough evidence.
241
243
  - Record as PR comments when a PR exists (model/tool, verdict, reviewed commit).
242
244
  - Unavailable reviewer → try next preferred; if still short, report gap and