@webpresso/opencode-plugin 3.1.28 → 3.2.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.28",
3
+ "version": "3.2.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.28",
5
+ "packageVersion": "3.2.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"
@@ -39,7 +39,7 @@
39
39
  "digest": "sha256:306355888da0827e3523022077f731845726bbaaf595ca12515cc91d8605b5e6"
40
40
  },
41
41
  "fix": {
42
- "digest": "sha256:6761e335ca1327d0dd12822886d5ea7f1b683f27caba62046cd0529c7cda7e44"
42
+ "digest": "sha256:2508fbad15af4ef828b668084cac950c98b8a232ce25c8152135788738e5f243"
43
43
  },
44
44
  "grok": {
45
45
  "digest": "sha256:07f2cdc58889f13f0869ddc153d465423cf2f4aae7b9deb4c4e427d2dba78a34"
@@ -90,7 +90,7 @@
90
90
  "digest": "sha256:953ab2625287866e5f27e602a89f1752e054d47522d04e1fdc11880867dc2fde"
91
91
  },
92
92
  "verify": {
93
- "digest": "sha256:4c9d534c358456c1d34f9b4b919e70dd013164c7c89ca8229181b77e31bdc914"
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
@@ -122,12 +122,15 @@ Run the narrowest checks that prove the repaired behavior on the real repo surfa
122
122
  - `wp_test` / targeted tests for the repaired path
123
123
  - `wp_lint` / `wp_typecheck` for changed surfaces
124
124
  - `wp_qa` only when the blast radius needs the bookend
125
+ - Before push: `wp ci-preflight` (local pre-push owner). Do not use remote CI
126
+ re-runs as the primary fix loop (`ci-cost-local-first`).
125
127
 
126
128
  Rules:
127
129
 
128
130
  - Prefer MCP `wp_*` over raw host Bash.
129
131
  - Reuse fresh logs / session-memory indexed output; do not re-run long commands just to re-read.
130
132
  - Read the exit code and summary before making a claim.
133
+ - Never `git push --no-verify` to land an unproven fix.
131
134
 
132
135
  Escalate to `/verify <target>` (local) or `/verify <target> --merge-ready` when any of these are true:
133
136
 
@@ -109,6 +109,9 @@ workflow:
109
109
  1. Identify target type (file / package / blueprint slug / all).
110
110
  2. Map each claim you plan to make to the exact command or log that proves it.
111
111
  3. If the target is a blueprint slug, run the repo's blueprint show/audit surface and record the acceptance boxes that still need proof.
112
+ 4. **CI cost / local-first:** treat remote CI as confirmation, not the first
113
+ debugger. Load `ci-cost-local-first` expectations: no hook skips, no
114
+ blind `gh run rerun`, Ubicloud for private non-release jobs.
112
115
 
113
116
  ## Phase 1 — Governance gates
114
117
 
@@ -130,12 +133,27 @@ Run the narrowest checks that prove the touched behavior:
130
133
  - `wp_test` / targeted tests
131
134
  - `wp_qa`, `wp_e2e`, build / package checks only when the change requires them
132
135
 
136
+ **Before push or PR open/update**, run the pre-push owner end-to-end:
137
+
138
+ ```bash
139
+ wp ci-preflight
140
+ ```
141
+
142
+ That is fail-fast branch-scoped format → guardrails → typecheck → lint →
143
+ test. Agent-kit itself enforces it from `.husky/pre-push`; consumer repos may
144
+ keep pre-push checks user-owned, so run the command explicitly before pushing.
145
+ Do **not** claim push-ready if preflight failed. Do **not** bypass hooks to ship
146
+ red work.
147
+
133
148
  Rules:
134
149
 
135
150
  - Prefer MCP `wp_*` over raw underlying tools.
136
151
  - Reuse fresh logs / session-memory indexed output if the runner auto-saves them.
137
152
  - Never claim broader correctness than the commands actually proved.
138
153
  - If the repo documents a "full QA" bookend, reserve `wp_qa` / `--full` for the final broad pass rather than every iteration.
154
+ - If CI is already red: read the failed job log, reproduce with the matching
155
+ local `wp` command, fix, re-run `wp ci-preflight`, then push **one**
156
+ corrective commit — do not burn another full matrix “to check.”
139
157
 
140
158
  ## Phase 3 — Cross-surface impact scan
141
159
 
@@ -217,8 +235,10 @@ Reviewer preference (pick **one** path):
217
235
 
218
236
  Approval evidence requirements:
219
237
 
220
- - Blueprint lifecycle: `wp review gate <blueprint-slug> --target HEAD --json` first.
221
- 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.
222
242
  - Each reviewer must return clear `APPROVED`/`BLOCKED` with enough evidence.
223
243
  - Record as PR comments when a PR exists (model/tool, verdict, reviewed commit).
224
244
  - Unavailable reviewer → try next preferred; if still short, report gap and