@webpresso/agent-kit 0.26.0 → 0.26.2

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.
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Webpresso agent-kit Claude Code plugin: blueprints, skills, hooks, MCP server",
9
- "version": "0.26.0"
9
+ "version": "0.26.2"
10
10
  },
11
11
  "plugins": [
12
12
  {
@@ -23,5 +23,5 @@
23
23
  ]
24
24
  }
25
25
  ],
26
- "version": "0.26.0"
26
+ "version": "0.26.2"
27
27
  }
@@ -1,66 +1,9 @@
1
1
  {
2
2
  "name": "webpresso",
3
- "version": "0.26.0",
3
+ "version": "0.26.2",
4
4
  "description": "Webpresso agent-kit: blueprints, skills, lore commit protocol, tech-debt lifecycle",
5
5
  "skills": "./skills",
6
6
  "commands": "./commands",
7
- "hooks": {
8
- "PreToolUse": [
9
- {
10
- "matcher": "Bash|Edit|Write|MultiEdit|WebFetch|Read|Grep",
11
- "hooks": [
12
- {
13
- "type": "command",
14
- "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/wp-pretool-guard.js"
15
- }
16
- ]
17
- }
18
- ],
19
- "PostToolUse": [
20
- {
21
- "matcher": "Edit|Write",
22
- "hooks": [
23
- {
24
- "type": "command",
25
- "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/wp-post-tool.js",
26
- "timeout": 15
27
- }
28
- ]
29
- }
30
- ],
31
- "Stop": [
32
- {
33
- "hooks": [
34
- {
35
- "type": "command",
36
- "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/wp-stop-qa.js"
37
- }
38
- ]
39
- }
40
- ],
41
- "UserPromptSubmit": [
42
- {
43
- "hooks": [
44
- {
45
- "type": "command",
46
- "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/wp-guard-switch.js",
47
- "timeout": 5
48
- }
49
- ]
50
- }
51
- ],
52
- "SessionStart": [
53
- {
54
- "matcher": "startup|resume|compact",
55
- "hooks": [
56
- {
57
- "type": "command",
58
- "command": "node ${CLAUDE_PLUGIN_ROOT}/bin/wp-sessionstart-routing.js"
59
- }
60
- ]
61
- }
62
- ]
63
- },
64
7
  "mcpServers": {
65
8
  "webpresso": {
66
9
  "command": "node",
package/README.md CHANGED
@@ -44,6 +44,15 @@ Playwright, unit-test and file-based e2e smoke assets), wires `AGENTS.md` /
44
44
  execution-owned vs authoring-owned dependency migration guidance. Re-running
45
45
  refreshes the webpresso-owned pieces and preserves consumer-owned files.
46
46
 
47
+ > **`wp setup` is required for hooks.** The Claude Code hooks (PreToolUse guard,
48
+ > Stop-QA gate, SessionStart routing, …) are installed by `wp setup` into your
49
+ > repo's `.claude/settings.json`. They are intentionally **not** shipped in the
50
+ > plugin manifest — declaring them in both places double-fires every hook (Claude
51
+ > Code does not dedup across sources), and settings.json is the more reliable
52
+ > surface. So enabling the plugin alone does **not** activate hooks; run
53
+ > `wp setup`. Run `wp hooks doctor` to check — it warns if the managed hooks are
54
+ > missing from `.claude/settings.json`.
55
+
47
56
  `wp` owns **execution** for the generic tool lanes it manages (test / mutation /
48
57
  e2e / lint / format / typecheck). That does **not** mean every local
49
58
  devDependency disappears — keep dependencies your repo imports directly (e.g.
@@ -114,6 +114,11 @@ function shouldSkipDirectory(name) {
114
114
  '.omx',
115
115
  '.omc',
116
116
  '.codex',
117
+ // Gitignored Claude Code agent surface — agent worktree scratch under
118
+ // .claude/worktrees/* carries vendored package manifests that are not the
119
+ // repo's own packages; walking it produces false positives on local dev
120
+ // machines and in consumer repos that run agent worktrees.
121
+ '.claude',
117
122
  ].includes(name);
118
123
  }
119
124
  function readPackageJson(file) {
@@ -35,7 +35,7 @@ import { scaffoldAuditHooks } from './scaffolders/audit-hooks/index.js';
35
35
  import { ensureClaudeCodeUserPlugin } from './scaffolders/claude-plugin/index.js';
36
36
  import { scaffoldClaudeRules } from './scaffolders/claude-rules/index.js';
37
37
  import { ensureCodexCli } from './scaffolders/codex-cli/index.js';
38
- import { ensureCodexWebpressoMcp, ensureCodexPlaywrightMcp } from './scaffolders/codex-mcp/index.js';
38
+ import { ensureCodexWebpressoMcp, ensureCodexPlaywrightMcp, ensureClaudePlaywrightMcp, } from './scaffolders/codex-mcp/index.js';
39
39
  import { scaffoldExampleSkill } from './scaffolders/example-skill/index.js';
40
40
  import { ensureGstack } from './scaffolders/gstack/index.js';
41
41
  import { scaffoldLoreCommits } from './scaffolders/lore-commits/index.js';
@@ -437,6 +437,24 @@ export async function runInit(flags) {
437
437
  console.log(' codex playwright mcp: skipped (--dry-run)');
438
438
  break;
439
439
  }
440
+ const claudePlaywrightMcpResult = ensureClaudePlaywrightMcp({
441
+ options,
442
+ repoRoot: consumer.repoRoot,
443
+ });
444
+ switch (claudePlaywrightMcpResult.kind) {
445
+ case 'claude-playwright-mcp-written':
446
+ console.log(` claude playwright mcp: ✓ ${claudePlaywrightMcpResult.path}`);
447
+ break;
448
+ case 'claude-playwright-mcp-unchanged':
449
+ console.log(` claude playwright mcp: already configured at ${claudePlaywrightMcpResult.path}`);
450
+ break;
451
+ case 'claude-playwright-mcp-skipped-dry-run':
452
+ console.log(' claude playwright mcp: skipped (--dry-run)');
453
+ break;
454
+ case 'claude-playwright-mcp-invalid-json':
455
+ console.warn(` claude playwright mcp: ⚠ ${claudePlaywrightMcpResult.path} is not valid JSON; left unchanged`);
456
+ break;
457
+ }
440
458
  }
441
459
  if (presets.includes('omx')) {
442
460
  agentHooksResult = await scaffoldAgentHooks({
@@ -20,7 +20,11 @@ export function ensureCodexCli(input) {
20
20
  }
21
21
  }
22
22
  else if (!shouldSkipCodexRefresh()) {
23
- spawn('vp', ['update', '-g', '@openai/codex'], { stdio: 'inherit' });
23
+ // `--latest` ignores the recorded semver range so the global is pulled to
24
+ // the absolute newest published release, matching the force-to-latest
25
+ // guarantee `vp install -g <bare>` gives the agent-kit self-update. Plain
26
+ // `vp update -g` is range-bound and can strand the global on an old major.
27
+ spawn('vp', ['update', '-g', '--latest', '@openai/codex'], { stdio: 'inherit' });
24
28
  }
25
29
  return { kind: 'codex-cli-ok', installed };
26
30
  }
@@ -1,7 +1,7 @@
1
1
  import type { MergeOptions } from '#cli/commands/init/merge';
2
2
  export declare const PLAYWRIGHT_MCP_SERVER_NAME = "playwright";
3
3
  export declare const PLAYWRIGHT_MCP_HEADER = "[mcp_servers.playwright]";
4
- export declare const PLAYWRIGHT_MCP_BLOCK = "[mcp_servers.playwright]\ncommand = \"vp\"\nargs = [\"dlx\", \"@playwright/mcp@latest\", \"--caps=testing,storage,network,devtools\"]\nenabled = true\nstartup_timeout_sec = 30\n";
4
+ export declare const PLAYWRIGHT_MCP_BLOCK: string;
5
5
  export declare const WEBPRESSO_MCP_SERVER_NAME = "webpresso";
6
6
  export declare const WEBPRESSO_MCP_HEADER = "[mcp_servers.webpresso]";
7
7
  export interface EnsureCodexPlaywrightMcpInput {
@@ -21,6 +21,34 @@ export type EnsureCodexPlaywrightMcpResult = {
21
21
  };
22
22
  export declare function upsertPlaywrightMcpServer(raw: string): string;
23
23
  export declare function ensureCodexPlaywrightMcp(input: EnsureCodexPlaywrightMcpInput): EnsureCodexPlaywrightMcpResult;
24
+ /**
25
+ * Upsert the `playwright` server into a `.mcp.json` document, preserving every
26
+ * other server (e.g. `context7`, `exa`) and any non-server top-level keys.
27
+ * Output is normalized to 2-space JSON with a trailing newline so repeated runs
28
+ * converge — idempotent after the first write.
29
+ */
30
+ export declare function upsertClaudePlaywrightMcpServer(raw: string): string;
31
+ export interface EnsureClaudePlaywrightMcpInput {
32
+ options: MergeOptions;
33
+ /** Project root whose `.mcp.json` is managed. */
34
+ repoRoot: string;
35
+ /** Test seam. Defaults to `<repoRoot>/.mcp.json`. */
36
+ configPath?: string;
37
+ }
38
+ export type EnsureClaudePlaywrightMcpResult = {
39
+ kind: 'claude-playwright-mcp-written';
40
+ path: string;
41
+ } | {
42
+ kind: 'claude-playwright-mcp-unchanged';
43
+ path: string;
44
+ } | {
45
+ kind: 'claude-playwright-mcp-skipped-dry-run';
46
+ path: string;
47
+ } | {
48
+ kind: 'claude-playwright-mcp-invalid-json';
49
+ path: string;
50
+ };
51
+ export declare function ensureClaudePlaywrightMcp(input: EnsureClaudePlaywrightMcpInput): EnsureClaudePlaywrightMcpResult;
24
52
  export interface WebpressoInstallProbe {
25
53
  /** Test seam — override the candidate roots. Default: probe in fixed order. */
26
54
  candidates?: readonly string[];
@@ -22,10 +22,27 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
22
22
  import { homedir } from 'node:os';
23
23
  import { dirname, join } from 'node:path';
24
24
  export const PLAYWRIGHT_MCP_SERVER_NAME = 'playwright';
25
+ /**
26
+ * Single source of truth for how the Playwright MCP server is launched. Both
27
+ * the Codex TOML block and the Claude Code `.mcp.json` block render from these.
28
+ * The portable `vp dlx` facade fetches the npm-published server on demand, so
29
+ * there is no machine-specific bin path to rot — the failure mode a hand-
30
+ * authored `~/.bun/bin/playwright-mcp` entry hits the moment that global bin
31
+ * disappears (ENOENT on spawn).
32
+ */
33
+ const PLAYWRIGHT_MCP_COMMAND = 'vp';
34
+ const PLAYWRIGHT_MCP_ARGS = [
35
+ 'dlx',
36
+ '@playwright/mcp@latest',
37
+ '--caps=testing,storage,network,devtools',
38
+ ];
39
+ function tomlStringArray(values) {
40
+ return `[${values.map((value) => `"${value}"`).join(', ')}]`;
41
+ }
25
42
  export const PLAYWRIGHT_MCP_HEADER = `[mcp_servers.${PLAYWRIGHT_MCP_SERVER_NAME}]`;
26
43
  export const PLAYWRIGHT_MCP_BLOCK = `${PLAYWRIGHT_MCP_HEADER}
27
- command = "vp"
28
- args = ["dlx", "@playwright/mcp@latest", "--caps=testing,storage,network,devtools"]
44
+ command = "${PLAYWRIGHT_MCP_COMMAND}"
45
+ args = ${tomlStringArray(PLAYWRIGHT_MCP_ARGS)}
29
46
  enabled = true
30
47
  startup_timeout_sec = 30
31
48
  `;
@@ -70,6 +87,59 @@ export function ensureCodexPlaywrightMcp(input) {
70
87
  writeFileSync(configPath, next, 'utf8');
71
88
  return { kind: 'codex-playwright-mcp-written', path: configPath };
72
89
  }
90
+ function claudePlaywrightServer() {
91
+ return { command: PLAYWRIGHT_MCP_COMMAND, args: [...PLAYWRIGHT_MCP_ARGS] };
92
+ }
93
+ function isJsonRecord(value) {
94
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
95
+ }
96
+ function parseJson(raw) {
97
+ try {
98
+ return { ok: true, value: JSON.parse(raw) };
99
+ }
100
+ catch {
101
+ return { ok: false };
102
+ }
103
+ }
104
+ /**
105
+ * Upsert the `playwright` server into a `.mcp.json` document, preserving every
106
+ * other server (e.g. `context7`, `exa`) and any non-server top-level keys.
107
+ * Output is normalized to 2-space JSON with a trailing newline so repeated runs
108
+ * converge — idempotent after the first write.
109
+ */
110
+ export function upsertClaudePlaywrightMcpServer(raw) {
111
+ const parsed = raw.trim().length > 0 ? parseJson(raw) : { ok: true, value: {} };
112
+ if (!parsed.ok) {
113
+ throw new Error('cannot upsert playwright into .mcp.json: existing file is not valid JSON');
114
+ }
115
+ const root = isJsonRecord(parsed.value) ? parsed.value : {};
116
+ const servers = isJsonRecord(root.mcpServers) ? root.mcpServers : {};
117
+ const next = {
118
+ ...root,
119
+ mcpServers: {
120
+ ...servers,
121
+ [PLAYWRIGHT_MCP_SERVER_NAME]: claudePlaywrightServer(),
122
+ },
123
+ };
124
+ return `${JSON.stringify(next, null, 2)}\n`;
125
+ }
126
+ export function ensureClaudePlaywrightMcp(input) {
127
+ const configPath = input.configPath ?? join(input.repoRoot, '.mcp.json');
128
+ if (input.options.dryRun) {
129
+ return { kind: 'claude-playwright-mcp-skipped-dry-run', path: configPath };
130
+ }
131
+ const existing = existsSync(configPath) ? readFileSync(configPath, 'utf8') : '';
132
+ if (existing.trim().length > 0 && !parseJson(existing).ok) {
133
+ return { kind: 'claude-playwright-mcp-invalid-json', path: configPath };
134
+ }
135
+ const next = upsertClaudePlaywrightMcpServer(existing);
136
+ if (next === existing) {
137
+ return { kind: 'claude-playwright-mcp-unchanged', path: configPath };
138
+ }
139
+ mkdirSync(dirname(configPath), { recursive: true });
140
+ writeFileSync(configPath, next, 'utf8');
141
+ return { kind: 'claude-playwright-mcp-written', path: configPath };
142
+ }
73
143
  // ────────────────────────────────────────────────────────────────────────────
74
144
  // Agent-kit MCP server registration
75
145
  // ────────────────────────────────────────────────────────────────────────────
@@ -13,6 +13,8 @@ export interface DoctorCheck {
13
13
  name: string;
14
14
  ok: boolean;
15
15
  detail?: string;
16
+ /** Advisory checks surface a warning but do not flip the doctor's exit code. */
17
+ advisory?: boolean;
16
18
  }
17
19
  export interface DoctorResult {
18
20
  ok: boolean;
@@ -28,6 +30,16 @@ export interface RunHooksDoctorOptions {
28
30
  }
29
31
  export declare function findOwningPackageRoot(startDir: string): string | null;
30
32
  export declare function checkRtkOnPath(cwd?: string): Promise<DoctorCheck | null>;
33
+ /**
34
+ * Verify the consumer's `.claude/settings.json` carries the managed agent-kit
35
+ * hook launchers. Since the hooks are single-sourced there (not in the plugin
36
+ * manifest), a missing reference means a plugin-only install that never ran
37
+ * `wp setup` — i.e. no agent-kit hooks are active.
38
+ */
39
+ export declare function checkManagedHooksInstalled(cwd?: string): {
40
+ ok: boolean;
41
+ detail?: string;
42
+ };
31
43
  export declare function runHooksDoctor(opts?: RunHooksDoctorOptions): Promise<DoctorResult>;
32
44
  export declare function printHooksDoctor(opts?: RunHooksDoctorOptions): Promise<number>;
33
45
  export {};
@@ -590,6 +590,40 @@ function checkLiveSourceDevLink(cwd = process.cwd()) {
590
590
  detail: `${state.package} → ${state.linkedFrom}`,
591
591
  };
592
592
  }
593
+ // Marker for the managed hook launchers `wp setup` writes under
594
+ // `.claude/hooks/managed/` (CLAUDE_MANAGED_HOOK_SUBDIR in the agent-hooks
595
+ // scaffolder). The plugin manifest no longer ships hooks (they double-fired
596
+ // against these and were the less reliable surface), so settings.json is the
597
+ // single source — if it does not reference them, the hooks are not installed.
598
+ const MANAGED_HOOK_MARKER = 'hooks/managed/wp-pretool-guard';
599
+ /**
600
+ * Verify the consumer's `.claude/settings.json` carries the managed agent-kit
601
+ * hook launchers. Since the hooks are single-sourced there (not in the plugin
602
+ * manifest), a missing reference means a plugin-only install that never ran
603
+ * `wp setup` — i.e. no agent-kit hooks are active.
604
+ */
605
+ export function checkManagedHooksInstalled(cwd = process.cwd()) {
606
+ const settingsPath = join(cwd, '.claude', 'settings.json');
607
+ if (!tryAccess(settingsPath)) {
608
+ return {
609
+ ok: false,
610
+ detail: 'no .claude/settings.json — run `wp setup` to install the agent-kit hooks',
611
+ };
612
+ }
613
+ try {
614
+ const raw = readFileSync(settingsPath, 'utf-8');
615
+ if (!raw.includes(MANAGED_HOOK_MARKER)) {
616
+ return {
617
+ ok: false,
618
+ detail: 'agent-kit hooks not found in .claude/settings.json — run `wp setup`',
619
+ };
620
+ }
621
+ return { ok: true };
622
+ }
623
+ catch (err) {
624
+ return { ok: false, detail: `failed to read .claude/settings.json: ${String(err)}` };
625
+ }
626
+ }
593
627
  export async function runHooksDoctor(opts = {}) {
594
628
  const checks = [];
595
629
  const isWin = platform() === 'win32';
@@ -609,6 +643,11 @@ export async function runHooksDoctor(opts = {}) {
609
643
  }
610
644
  checks.push(checkConsumerCodexHookPaths(opts.cwd));
611
645
  checks.push({ name: 'plugin.json integrity', ...checkPluginJson() });
646
+ checks.push({
647
+ name: 'managed hooks installed (.claude/settings.json)',
648
+ advisory: true,
649
+ ...checkManagedHooksInstalled(opts.cwd),
650
+ });
612
651
  if (opts.skipMcp) {
613
652
  checks.push({ name: 'MCP server liveness', ok: true, detail: 'skipped (--skip-mcp)' });
614
653
  }
@@ -676,7 +715,7 @@ export async function runHooksDoctor(opts = {}) {
676
715
  }
677
716
  }
678
717
  }
679
- const nonMcpChecks = checks.filter((c) => !c.name.startsWith('MCP '));
718
+ const nonMcpChecks = checks.filter((c) => !c.name.startsWith('MCP ') && !c.advisory);
680
719
  const overallOk = nonMcpChecks.every((c) => c.ok);
681
720
  return { ok: overallOk, checks };
682
721
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpresso/agent-kit",
3
- "version": "0.26.0",
3
+ "version": "0.26.2",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -710,10 +710,10 @@
710
710
  "@stryker-mutator/typescript-checker": "^9.6.1",
711
711
  "@playwright/test": "^1.55.0",
712
712
  "wrangler": "^4.50.0",
713
- "@webpresso/agent-kit-runtime-darwin-arm64": "0.26.0",
714
- "@webpresso/agent-kit-runtime-darwin-x64": "0.26.0",
715
- "@webpresso/agent-kit-runtime-linux-x64": "0.26.0",
716
- "@webpresso/agent-kit-runtime-linux-arm64": "0.26.0",
717
- "@webpresso/agent-kit-runtime-windows-x64": "0.26.0"
713
+ "@webpresso/agent-kit-runtime-darwin-arm64": "0.26.2",
714
+ "@webpresso/agent-kit-runtime-darwin-x64": "0.26.2",
715
+ "@webpresso/agent-kit-runtime-linux-x64": "0.26.2",
716
+ "@webpresso/agent-kit-runtime-linux-arm64": "0.26.2",
717
+ "@webpresso/agent-kit-runtime-windows-x64": "0.26.2"
718
718
  }
719
719
  }