@posthog/wizard 2.44.0 → 2.46.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.
Files changed (121) hide show
  1. package/README.md +5 -1
  2. package/dist/{add-mcp-server-to-clients-D5qsqCkj.js → add-mcp-server-to-clients-Bv2CBSil.js} +53 -6
  3. package/dist/add-mcp-server-to-clients-Bv2CBSil.js.map +1 -0
  4. package/dist/{agent-interface-BvTxWiol.js → agent-interface-B8roB2UP.js} +1122 -157
  5. package/dist/agent-interface-B8roB2UP.js.map +1 -0
  6. package/dist/{agent-runner-BF6_f8Qh.js → agent-runner-CV3qh_2p.js} +298 -610
  7. package/dist/agent-runner-CV3qh_2p.js.map +1 -0
  8. package/dist/analytics-D7vUZgzf.js +2 -0
  9. package/dist/{analytics-DSlQuwOT.js → analytics-k76OWsl0.js} +20 -6
  10. package/dist/analytics-k76OWsl0.js.map +1 -0
  11. package/dist/{api-BXduJZrw.js → api-CmIWcW-U.js} +3 -3
  12. package/dist/{api-BXduJZrw.js.map → api-CmIWcW-U.js.map} +1 -1
  13. package/dist/bin.js +242 -1043
  14. package/dist/bin.js.map +1 -1
  15. package/dist/{ci-install-GirEJqu2.js → ci-install-CNEJxdHf.js} +4 -4
  16. package/dist/{ci-install-GirEJqu2.js.map → ci-install-CNEJxdHf.js.map} +1 -1
  17. package/dist/commandments-DSNrOeOC.js +42 -0
  18. package/dist/commandments-DSNrOeOC.js.map +1 -0
  19. package/dist/commandments-qTW7pwaT.js +2 -0
  20. package/dist/{debug-BOFxiVVs.js → debug-BwIo7nwn.js} +1 -1
  21. package/dist/{debug-DXfQFmcM.js → debug-C9HmIrwx.js} +36 -6
  22. package/dist/debug-C9HmIrwx.js.map +1 -0
  23. package/dist/{defaults-BWLIffh5.js → defaults-CtWMmC8s.js} +1 -1
  24. package/dist/{defaults-BWLIffh5.js.map → defaults-CtWMmC8s.js.map} +1 -1
  25. package/dist/{env-api-key-cjD4yfGT.js → env-api-key-C3LzH8Lr.js} +1 -1
  26. package/dist/{env-api-key-cjD4yfGT.js.map → env-api-key-C3LzH8Lr.js.map} +1 -1
  27. package/dist/{environment-F1yw21dG.js → environment-NJykOp33.js} +3 -3
  28. package/dist/{environment-F1yw21dG.js.map → environment-NJykOp33.js.map} +1 -1
  29. package/dist/{file-utils-D-7YfJ7A.js → file-utils-CLMcNIKi.js} +2 -2
  30. package/dist/{file-utils-D-7YfJ7A.js.map → file-utils-CLMcNIKi.js.map} +1 -1
  31. package/dist/file-watcher-CBDCNQVV.js +123 -0
  32. package/dist/file-watcher-CBDCNQVV.js.map +1 -0
  33. package/dist/{headless-ui-Dni0gPND.js → headless-ui-B3QKxbz8.js} +5 -4
  34. package/dist/headless-ui-B3QKxbz8.js.map +1 -0
  35. package/dist/{interactive-Bmi82rsO.js → interactive-CIJRex02.js} +2 -2
  36. package/dist/{interactive-Bmi82rsO.js.map → interactive-CIJRex02.js.map} +1 -1
  37. package/dist/{mcp-prompt-streaming-AtntLsJ8.js → mcp-prompt-streaming-D6OgW3lC.js} +3 -3
  38. package/dist/{mcp-prompt-streaming-AtntLsJ8.js.map → mcp-prompt-streaming-D6OgW3lC.js.map} +1 -1
  39. package/dist/{mcp-xKRDnrcq.js → mcp-qTjKHqXN.js} +3 -3
  40. package/dist/mcp-qTjKHqXN.js.map +1 -0
  41. package/dist/{non-interactive-BIHqcW8g.js → non-interactive-8Eypq_h-.js} +2 -2
  42. package/dist/{non-interactive-BIHqcW8g.js.map → non-interactive-8Eypq_h-.js.map} +1 -1
  43. package/dist/orchestrator-tools-DVXOV-kz.js +99 -0
  44. package/dist/orchestrator-tools-DVXOV-kz.js.map +1 -0
  45. package/dist/{package-json-CDs1ZVKu.js → package-json-DCuoye-H.js} +1 -1
  46. package/dist/{package-json-CDs1ZVKu.js.map → package-json-DCuoye-H.js.map} +1 -1
  47. package/dist/{package-manager-C6R_zpML.js → package-manager-CAD9Uklp.js} +2 -2
  48. package/dist/{package-manager-C6R_zpML.js.map → package-manager-CAD9Uklp.js.map} +1 -1
  49. package/dist/pi-C9iHv5uG.js +619 -0
  50. package/dist/pi-C9iHv5uG.js.map +1 -0
  51. package/dist/{playground-DwL36I2u.js → playground-C-9Q3ETa.js} +8 -9
  52. package/dist/{playground-DwL36I2u.js.map → playground-C-9Q3ETa.js.map} +1 -1
  53. package/dist/{posthog-DwKlq0HM.js → posthog-DZZbziJ1.js} +1 -1
  54. package/dist/{posthog-DwKlq0HM.js.map → posthog-DZZbziJ1.js.map} +1 -1
  55. package/dist/{posthog-integration-BhLKCFsI.js → posthog-integration-Do997QON.js} +2206 -112
  56. package/dist/posthog-integration-Do997QON.js.map +1 -0
  57. package/dist/{provisioning-jVZm2E-A.js → provisioning-DpqnGk3z.js} +3 -3
  58. package/dist/{provisioning-jVZm2E-A.js.map → provisioning-DpqnGk3z.js.map} +1 -1
  59. package/dist/queue-tools-CTUgbOU8.js +366 -0
  60. package/dist/queue-tools-CTUgbOU8.js.map +1 -0
  61. package/dist/{registry-DcoBCMQR.js → registry-DT58jEYr.js} +96 -18
  62. package/dist/registry-DT58jEYr.js.map +1 -0
  63. package/dist/{security-jxjVsOpd.js → security-CTigdAWm.js} +10 -8
  64. package/dist/security-CTigdAWm.js.map +1 -0
  65. package/dist/{setup-utils-BQCl28D8.js → setup-utils-Cb5uACEO.js} +31 -9
  66. package/dist/setup-utils-Cb5uACEO.js.map +1 -0
  67. package/dist/{start-tui-DgSKFMVh.js → start-tui-Cwc-PFma.js} +37 -92
  68. package/dist/start-tui-Cwc-PFma.js.map +1 -0
  69. package/dist/{steps-erepgD0V.js → steps-DyEKYM4I.js} +7 -7
  70. package/dist/{steps-erepgD0V.js.map → steps-DyEKYM4I.js.map} +1 -1
  71. package/dist/{store-C79GoHCQ.js → store-4sj-vWr5.js} +5 -6
  72. package/dist/store-4sj-vWr5.js.map +1 -0
  73. package/dist/{subagent-aDlD_Ltz.js → subagent-D8_yDgCB.js} +2 -2
  74. package/dist/{subagent-aDlD_Ltz.js.map → subagent-D8_yDgCB.js.map} +1 -1
  75. package/dist/task-D8Y8pkzd.js +304 -0
  76. package/dist/task-D8Y8pkzd.js.map +1 -0
  77. package/dist/{task-stream-0Pk0tPBN.js → task-stream-DQT3vjwS.js} +72 -7
  78. package/dist/task-stream-DQT3vjwS.js.map +1 -0
  79. package/dist/{tasks-ZkrluXCa.js → tasks-D2WKssA5.js} +2 -2
  80. package/dist/{tasks-ZkrluXCa.js.map → tasks-D2WKssA5.js.map} +1 -1
  81. package/dist/{telemetry-DZShaH-y.js → telemetry-CYr4HgS2.js} +3 -3
  82. package/dist/{telemetry-DZShaH-y.js.map → telemetry-CYr4HgS2.js.map} +1 -1
  83. package/dist/{terminal-CEhufbJ4.js → terminal-1RfyHQC2.js} +13 -13
  84. package/dist/{terminal-CEhufbJ4.js.map → terminal-1RfyHQC2.js.map} +1 -1
  85. package/dist/tools-DQCUOQZX.js +199 -0
  86. package/dist/tools-DQCUOQZX.js.map +1 -0
  87. package/dist/{wizard-abort-UCm4Pd5m.js → wizard-abort-D_-v1zvS.js} +1 -1
  88. package/dist/{wizard-abort-C14PzXZ8.js → wizard-abort-FuyIaI_0.js} +4 -4
  89. package/dist/{wizard-abort-C14PzXZ8.js.map → wizard-abort-FuyIaI_0.js.map} +1 -1
  90. package/dist/{wizard-session-C5lZPPEc.js → wizard-session-DWHe83uk.js} +1 -1
  91. package/dist/{wizard-session-BAZql11N.js → wizard-session-tp4Zdbwl.js} +1 -1
  92. package/dist/{wizard-session-BAZql11N.js.map → wizard-session-tp4Zdbwl.js.map} +1 -1
  93. package/dist/yara-hooks-6gNnPFPo.js +2 -0
  94. package/dist/{yara-hooks-CPH8N4yz.js → yara-hooks-DdYRRxfH.js} +82 -21
  95. package/dist/yara-hooks-DdYRRxfH.js.map +1 -0
  96. package/package.json +1 -1
  97. package/dist/add-mcp-server-to-clients-D5qsqCkj.js.map +0 -1
  98. package/dist/agent-interface-BvTxWiol.js.map +0 -1
  99. package/dist/agent-runner-BF6_f8Qh.js.map +0 -1
  100. package/dist/analytics-C7Xx6csw.js +0 -2
  101. package/dist/analytics-DSlQuwOT.js.map +0 -1
  102. package/dist/debug-DXfQFmcM.js.map +0 -1
  103. package/dist/headless-ui-Dni0gPND.js.map +0 -1
  104. package/dist/mcp-xKRDnrcq.js.map +0 -1
  105. package/dist/posthog-integration-BhLKCFsI.js.map +0 -1
  106. package/dist/registry-DcoBCMQR.js.map +0 -1
  107. package/dist/security-jxjVsOpd.js.map +0 -1
  108. package/dist/setup-utils-BQCl28D8.js.map +0 -1
  109. package/dist/start-tui-DgSKFMVh.js.map +0 -1
  110. package/dist/store-C79GoHCQ.js.map +0 -1
  111. package/dist/task-stream-0Pk0tPBN.js.map +0 -1
  112. package/dist/tools-BIPMan6l.js +0 -122
  113. package/dist/tools-BIPMan6l.js.map +0 -1
  114. package/dist/types-y3HkjtSq.js +0 -46
  115. package/dist/types-y3HkjtSq.js.map +0 -1
  116. package/dist/wizard-tools-B7SLjCic.js +0 -1106
  117. package/dist/wizard-tools-B7SLjCic.js.map +0 -1
  118. package/dist/wizard-ui-WZ48rUgr.js +0 -15
  119. package/dist/wizard-ui-WZ48rUgr.js.map +0 -1
  120. package/dist/yara-hooks-CPH8N4yz.js.map +0 -1
  121. package/dist/yara-hooks-VZlj4Lib.js +0 -2
@@ -1,16 +1,21 @@
1
1
  import { n as __require } from "./rolldown-runtime-B_-DWIq7.js";
2
- import { G as POSTHOG_FLAG_HEADER_PREFIX, J as POSTHOG_PROPERTY_HEADER_PREFIX, a as getLogFilePath, at as WIZARD_ORCHESTRATOR_FLAG_KEY, f as getUI, lt as WIZARD_REMARK_EVENT_NAME, mt as wizardUserAgentForProgram, o as initLogFile, r as debug, s as logToFile, vt as runtimeEnv } from "./debug-DXfQFmcM.js";
3
- import { n as analytics } from "./analytics-DSlQuwOT.js";
4
- import { n as ADDITIONAL_FEATURE_PROMPTS } from "./wizard-session-BAZql11N.js";
5
- import { i as wizardAbort, n as registerCleanup, t as WizardError } from "./wizard-abort-C14PzXZ8.js";
6
- import { n as createWizardToolsServer, t as WIZARD_TOOL_NAMES } from "./wizard-tools-B7SLjCic.js";
7
- import { l as prewarmYaraScanner, n as createPostToolUseYaraHooks, r as createPreToolUseYaraHooks } from "./yara-hooks-CPH8N4yz.js";
2
+ import { Ct as runtimeEnv, J as POSTHOG_FLAG_HEADER_PREFIX, Z as POSTHOG_PROPERTY_HEADER_PREFIX, a as getLogFilePath, f as getUI, lt as WIZARD_ORCHESTRATOR_FLAG_KEY, o as initLogFile, pt as WIZARD_REMARK_EVENT_NAME, r as debug, s as logToFile, yt as wizardUserAgentForProgram } from "./debug-C9HmIrwx.js";
3
+ import { n as analytics } from "./analytics-k76OWsl0.js";
4
+ import { n as ADDITIONAL_FEATURE_PROMPTS } from "./wizard-session-tp4Zdbwl.js";
5
+ import { i as wizardAbort, n as registerCleanup, t as WizardError } from "./wizard-abort-FuyIaI_0.js";
6
+ import { c as makeMutex, i as buildOrchestratorTools, l as writeJsonAtomic } from "./queue-tools-CTUgbOU8.js";
7
+ import { _ as AUDIT_CHECKS_FILE, l as prewarmYaraScanner, n as createPostToolUseYaraHooks, r as createPreToolUseYaraHooks, x as coerceAuditChecks } from "./yara-hooks-DdYRRxfH.js";
8
+ import { t as getWizardCommandments } from "./commandments-DSNrOeOC.js";
8
9
  import { createRequire } from "node:module";
9
10
  import * as fs$1 from "fs";
11
+ import fs from "fs";
10
12
  import path from "path";
11
13
  import { spawnSync } from "node:child_process";
12
14
  import axios from "axios";
15
+ import { z } from "zod";
13
16
  import * as os from "os";
17
+ import { unzipSync } from "fflate";
18
+ import { randomUUID } from "crypto";
14
19
  //#region src/utils/custom-headers.ts
15
20
  /**
16
21
  * Builds a list of custom headers for ANTHROPIC_CUSTOM_HEADERS.
@@ -160,6 +165,7 @@ const LINTING_TOOLS = [
160
165
  "sql-formatter",
161
166
  "sqlfmt",
162
167
  "squawk",
168
+ "svelte-check",
163
169
  "svgo",
164
170
  "stringslint",
165
171
  "swiftformat",
@@ -175,6 +181,1103 @@ const LINTING_TOOLS = [
175
181
  "yamllint"
176
182
  ];
177
183
  //#endregion
184
+ //#region src/lib/agent/bash-fence.ts
185
+ /**
186
+ * Bash command fence: exact per-manager allowlists + shell-shape gates.
187
+ *
188
+ * In effect this gates the pi harness only — on the Anthropic arm `Bash` is
189
+ * pre-allowed (BASE_ALLOWED_TOOLS), so canUseTool never sees it and the SDK's
190
+ * OS sandbox + YARA hooks gate execution instead. pi has no OS sandbox yet,
191
+ * so this string fence is its only execution gate.
192
+ *
193
+ * Threat model: the agent can already Write project files, so commands that
194
+ * execute project-defined code (installs, builds) are equivalent risk by
195
+ * construction. The fence must prevent: direct arbitrary shell, outward
196
+ * registry actions (publish/push/deploy), arbitrary-package execution
197
+ * (`npx <anything>` downloads and runs it), and shell injection. Matching is
198
+ * token-exact per manager — keyword prefixes admitted `npm publish` via `pub`.
199
+ */
200
+ const NODE_MANAGERS = new Set([
201
+ "npm",
202
+ "pnpm",
203
+ "yarn",
204
+ "bun"
205
+ ]);
206
+ const GRADLE_MANAGERS = new Set([
207
+ "gradle",
208
+ "gradlew",
209
+ "./gradlew"
210
+ ]);
211
+ const MAVEN_MANAGERS = new Set([
212
+ "mvn",
213
+ "mvnw",
214
+ "./mvnw"
215
+ ]);
216
+ const NODE_SUBCOMMANDS = [
217
+ "install",
218
+ "i",
219
+ "ci",
220
+ "add",
221
+ "remove",
222
+ "uninstall",
223
+ "update",
224
+ "view"
225
+ ];
226
+ const NODE_SCRIPT_NAMES = [
227
+ "build",
228
+ "lint",
229
+ "format",
230
+ "tsc",
231
+ "typecheck",
232
+ "type-check",
233
+ "check-types",
234
+ "check",
235
+ "types"
236
+ ];
237
+ const NPX_TOOLS = new Set([
238
+ ...LINTING_TOOLS,
239
+ "tsc",
240
+ "expo",
241
+ "pod-install",
242
+ "cap"
243
+ ]);
244
+ const PIP_SUBCOMMANDS = [
245
+ "install",
246
+ "uninstall",
247
+ "show",
248
+ "list",
249
+ "index"
250
+ ];
251
+ const BUNDLE_SUBCOMMANDS = [
252
+ "install",
253
+ "add",
254
+ "remove",
255
+ "update",
256
+ "show"
257
+ ];
258
+ const MAVEN_GOALS = [
259
+ "install",
260
+ "compile",
261
+ "package",
262
+ "verify",
263
+ "dependency:tree"
264
+ ];
265
+ /** Managers whose first subcommand is checked exactly against a fixed set. */
266
+ const SIMPLE_MANAGERS = {
267
+ pip: PIP_SUBCOMMANDS,
268
+ pip3: PIP_SUBCOMMANDS,
269
+ poetry: [
270
+ "install",
271
+ "add",
272
+ "remove",
273
+ "show",
274
+ "lock",
275
+ "update"
276
+ ],
277
+ pipenv: ["install", "uninstall"],
278
+ uv: [
279
+ "add",
280
+ "remove",
281
+ "sync"
282
+ ],
283
+ pdm: [
284
+ "install",
285
+ "add",
286
+ "remove"
287
+ ],
288
+ conda: ["install", "remove"],
289
+ composer: [
290
+ "install",
291
+ "require",
292
+ "update",
293
+ "remove",
294
+ "show"
295
+ ],
296
+ bundle: BUNDLE_SUBCOMMANDS,
297
+ bundler: BUNDLE_SUBCOMMANDS,
298
+ gem: [
299
+ "install",
300
+ "uninstall",
301
+ "list",
302
+ "search"
303
+ ],
304
+ swift: ["package", "build"],
305
+ pod: [
306
+ "install",
307
+ "update",
308
+ "search"
309
+ ],
310
+ carthage: ["bootstrap", "update"]
311
+ };
312
+ const GRADLE_EXACT_TASKS = new Set([
313
+ "build",
314
+ "clean",
315
+ "dependencies"
316
+ ]);
317
+ const GRADLE_TASK_VERBS = [
318
+ "assemble",
319
+ "compile",
320
+ "bundle",
321
+ "lint"
322
+ ];
323
+ const XCODEBUILD_DENIED_ACTIONS = new Set(["test", "test-without-building"]);
324
+ const DANGEROUS_OPERATORS = /[;`$()]/;
325
+ const ALLOWED_TOOLS_SUMMARY = "Allowed: npm/pnpm/yarn/bun (install|i|ci|add|remove|uninstall|update|view, run <build/lint/typecheck script>), npx <lint tool|tsc|expo|pod-install|cap>, pip/pip3/poetry/pipenv/uv/pdm/conda (install/add/remove/...), composer (install|require|update|remove|show), bundle (install|add|remove|update|show|exec <lint tool>), gem (install|uninstall|list|search), swift (package|build), pod (install|update|search), carthage (bootstrap|update), xcodebuild (build/clean/archive actions), gradle/gradlew (build|clean|dependencies|assemble*/compile*/bundle*/lint* tasks), mvn (install|compile|package|verify|dependency:tree).";
326
+ function deny(analyticsReason, message) {
327
+ return {
328
+ allowed: false,
329
+ message,
330
+ analyticsReason
331
+ };
332
+ }
333
+ function denyCommand(command, feedback) {
334
+ return deny("not in allowlist", `Bash command not allowed: \`${command.length > 120 ? command.slice(0, 117) + "..." : command}\`. ${feedback}`);
335
+ }
336
+ function isNodeScriptName(token) {
337
+ return NODE_SCRIPT_NAMES.some((name) => token === name || token.startsWith(name + ":"));
338
+ }
339
+ function isLintingTool(token) {
340
+ return LINTING_TOOLS.includes(token) || token === "tsc";
341
+ }
342
+ function nodeDecision(parts, command) {
343
+ const bin = parts[0];
344
+ let i = 1;
345
+ if (bin === "yarn" && parts[i] === "workspace" && parts[i + 1]) i += 2;
346
+ for (;;) {
347
+ const t = parts[i];
348
+ if (t === "-r" || t === "--recursive") i++;
349
+ else if (t && (t.startsWith("--filter=") || t.startsWith("--workspace="))) i++;
350
+ else if ((t === "--filter" || t === "--workspace" || t === "-w" && bin === "npm") && parts[i + 1]) i += 2;
351
+ else if (t === "-w" && bin === "pnpm") i++;
352
+ else break;
353
+ }
354
+ const sub = parts[i];
355
+ const feedback = `Allowed ${bin} usage: ${bin} ${NODE_SUBCOMMANDS.join("|")} [args], ${bin} run <script named ${NODE_SCRIPT_NAMES.join("/")}>, ${bin} exec <lint tool>, or ${bin} <that script name or lint tool> directly.`;
356
+ if (!sub) return denyCommand(command, feedback);
357
+ if (sub === "run") {
358
+ const target = parts[i + 1];
359
+ if (target && isNodeScriptName(target)) return { allowed: true };
360
+ return denyCommand(command, feedback);
361
+ }
362
+ if (sub === "exec") {
363
+ const target = parts[i + 1];
364
+ if (target && isLintingTool(target)) return { allowed: true };
365
+ return denyCommand(command, feedback);
366
+ }
367
+ if (NODE_SUBCOMMANDS.includes(sub)) return { allowed: true };
368
+ if (isNodeScriptName(sub) || isLintingTool(sub)) return { allowed: true };
369
+ return denyCommand(command, feedback);
370
+ }
371
+ function npxDecision(parts, command) {
372
+ const target = parts[1];
373
+ if (!target || target.startsWith("-")) return denyCommand(command, "npx may only run a known tool directly: tsc, expo, pod-install, cap, or a lint/format tool (eslint, prettier, biome, ...).");
374
+ const name = target.replace(/@[^@/]*$/, "");
375
+ if (NPX_TOOLS.has(name)) return { allowed: true };
376
+ return denyCommand(command, "npx may only run a known tool directly: tsc, expo, pod-install, cap, or a lint/format tool (eslint, prettier, biome, ...).");
377
+ }
378
+ function gradleDecision(parts, command) {
379
+ let sawTask = false;
380
+ for (const raw of parts.slice(1)) {
381
+ if (raw.startsWith("-")) continue;
382
+ const task = raw.replace(/^(?::[\w.-]+)+:/, "");
383
+ if (!(GRADLE_EXACT_TASKS.has(task) || GRADLE_TASK_VERBS.some((verb) => task.startsWith(verb) && /^[A-Z]/.test(task.slice(verb.length))))) return denyCommand(command, "Allowed gradle tasks: build, clean, dependencies, or assemble*/compile*/bundle*/lint* variants (flags OK).");
384
+ sawTask = true;
385
+ }
386
+ return sawTask ? { allowed: true } : denyCommand(command, "Give gradle an allowed task: build, clean, dependencies, assembleDebug, ...");
387
+ }
388
+ function mavenDecision(parts, command) {
389
+ let sawGoal = false;
390
+ for (const raw of parts.slice(1)) {
391
+ if (raw.startsWith("-")) continue;
392
+ if (!MAVEN_GOALS.includes(raw)) return denyCommand(command, `Allowed mvn goals: ${MAVEN_GOALS.join(", ")} (flags OK).`);
393
+ sawGoal = true;
394
+ }
395
+ return sawGoal ? { allowed: true } : denyCommand(command, `Give mvn an allowed goal: ${MAVEN_GOALS.join(", ")}.`);
396
+ }
397
+ function xcodebuildDecision(parts, command) {
398
+ for (const t of parts.slice(1)) if (XCODEBUILD_DENIED_ACTIONS.has(t)) return denyCommand(command, "xcodebuild test actions are not allowed; build, clean, and archive are.");
399
+ return { allowed: true };
400
+ }
401
+ /** Grammar decision for a single operator-free, pipe-free command. */
402
+ function commandDecision(command) {
403
+ const parts = command.split(/\s+/).filter(Boolean);
404
+ const bin = parts[0];
405
+ if (!bin) return denyCommand(command, ALLOWED_TOOLS_SUMMARY);
406
+ if (NODE_MANAGERS.has(bin)) return nodeDecision(parts, command);
407
+ if (bin === "npx") return npxDecision(parts, command);
408
+ if (GRADLE_MANAGERS.has(bin)) return gradleDecision(parts, command);
409
+ if (MAVEN_MANAGERS.has(bin)) return mavenDecision(parts, command);
410
+ if (bin === "xcodebuild") return xcodebuildDecision(parts, command);
411
+ if ((bin === "python" || bin === "python3") && parts[1] === "manage.py" && parts[2] === "check") return { allowed: true };
412
+ if (bin === "uv" && parts[1] === "pip") {
413
+ if (parts[2] && PIP_SUBCOMMANDS.includes(parts[2])) return { allowed: true };
414
+ return denyCommand(command, `Allowed uv pip subcommands: ${PIP_SUBCOMMANDS.join(", ")}.`);
415
+ }
416
+ if ((bin === "bundle" || bin === "bundler") && parts[1] === "exec") {
417
+ if (parts[2] && isLintingTool(parts[2])) return { allowed: true };
418
+ return denyCommand(command, `bundle exec may only run a lint/format tool (rubocop, ...). Allowed bundle subcommands: ${BUNDLE_SUBCOMMANDS.join(", ")}, exec <lint tool>.`);
419
+ }
420
+ const subs = SIMPLE_MANAGERS[bin];
421
+ if (subs) {
422
+ if (parts[1] && subs.includes(parts[1])) return { allowed: true };
423
+ const exec = bin === "bundle" || bin === "bundler" ? ", exec <lint tool>" : "";
424
+ return denyCommand(command, `Allowed ${bin} subcommands: ${subs.join(", ")}${exec}.`);
425
+ }
426
+ return denyCommand(command, `\`${bin}\` is not an allowed tool. ${ALLOWED_TOOLS_SUMMARY}`);
427
+ }
428
+ function tailArgsAreSafe(argStr) {
429
+ const args = argStr.trim().split(/\s+/).filter(Boolean);
430
+ for (let i = 0; i < args.length; i++) {
431
+ const a = args[i];
432
+ if (/^-[nc]$/.test(a)) {
433
+ if (!/^[+-]?\d+$/.test(args[i + 1] ?? "")) return false;
434
+ i++;
435
+ } else if (!/^-[nc]?\+?\d+$/.test(a)) return false;
436
+ }
437
+ return true;
438
+ }
439
+ /**
440
+ * Full fence decision for a Bash command: shell-shape gates (separators,
441
+ * redirects, pipes) first, then the per-manager grammar.
442
+ */
443
+ function evaluateBashCommand(rawCommand) {
444
+ const command = rawCommand.trim();
445
+ if (/[\r\n]/.test(command)) return deny("dangerous operators", "Bash command not allowed. Multi-line commands are not permitted — run one command at a time.");
446
+ if (DANGEROUS_OPERATORS.test(command)) return deny("dangerous operators", "Bash command not allowed. Shell operators like ; ` $ ( ) are not permitted.");
447
+ const normalized = command.replace(/\s*\d*>&\d+\s*/g, " ").replace(/\s*\d*>\s*\/dev\/null\s*/g, " ").trim();
448
+ if (/[<>]/.test(normalized)) return deny("disallowed redirect", "Bash command not allowed. Redirects are not permitted (only 2>&1 and >/dev/null).");
449
+ const pipeMatch = normalized.match(/^(.+?)\s*\|\s*(?:tail|head)((?:\s+\S+)*)\s*$/);
450
+ if (pipeMatch) {
451
+ const base = pipeMatch[1].trim();
452
+ if (/[|&]/.test(base)) return deny("multiple pipes", "Bash command not allowed. Only a single pipe to tail/head is permitted.");
453
+ if (!tailArgsAreSafe(pipeMatch[2])) return deny("disallowed pipe", "Bash command not allowed. tail/head may only take numeric flags (-n 50, -c 200) — no file arguments.");
454
+ return commandDecision(base);
455
+ }
456
+ if (/[|&]/.test(normalized)) return deny("disallowed pipe", "Bash command not allowed. Pipes are only permitted as a single | tail/head for output limiting.");
457
+ return commandDecision(normalized);
458
+ }
459
+ //#endregion
460
+ //#region src/lib/wizard-ask-bridge.ts
461
+ /**
462
+ * WizardAskBridge — host-side promise broker for the `wizard_ask` MCP tool.
463
+ *
464
+ * The `wizard_ask` tool needs to (a) read information from the wizard
465
+ * session (the active skill id, used as the analytics `source`) and
466
+ * (b) drive the TUI overlay. Wiring `wizard-tools.ts` directly to either
467
+ * would couple our pure-data MCP server to the runtime UI layer.
468
+ *
469
+ * The bridge is the seam: `wizard-tools.ts` depends on this interface,
470
+ * and `agent-runner.ts` constructs an implementation that knows about
471
+ * both the session and `getUI()`.
472
+ */
473
+ /** Sentinel returned for unanswered fields on cancellation or timeout. */
474
+ const CANCELLED_SENTINEL = "__cancelled__";
475
+ function buildCancelledAnswers(questions) {
476
+ const out = {};
477
+ for (const q of questions) out[q.id] = CANCELLED_SENTINEL;
478
+ return out;
479
+ }
480
+ function isFullyCancelled(answers) {
481
+ const values = Object.values(answers);
482
+ if (values.length === 0) return false;
483
+ return values.every((v) => v === CANCELLED_SENTINEL);
484
+ }
485
+ function createWizardAskBridge(opts) {
486
+ const timeoutMs = opts.timeoutMs ?? 3e5;
487
+ return { async request({ questions }) {
488
+ const pending = {
489
+ id: randomUUID(),
490
+ questions,
491
+ source: opts.getSource(),
492
+ richLinks: opts.richLinks ?? false
493
+ };
494
+ const startedAt = Date.now();
495
+ let timer;
496
+ const timeoutPromise = new Promise((resolve) => {
497
+ timer = setTimeout(() => {
498
+ opts.cancelQuestion?.();
499
+ resolve(buildCancelledAnswers(questions));
500
+ }, timeoutMs);
501
+ });
502
+ try {
503
+ const answers = await Promise.race([opts.showQuestion(pending), timeoutPromise]);
504
+ const durationMs = Date.now() - startedAt;
505
+ if (isFullyCancelled(answers)) analytics.wizardCapture("wizard_ask cancelled", {
506
+ source: pending.source,
507
+ question_count: questions.length,
508
+ duration_ms: durationMs,
509
+ timed_out: durationMs >= timeoutMs
510
+ });
511
+ else analytics.wizardCapture("wizard_ask answered", {
512
+ source: pending.source,
513
+ question_count: questions.length,
514
+ duration_ms: durationMs
515
+ });
516
+ return answers;
517
+ } finally {
518
+ if (timer) clearTimeout(timer);
519
+ }
520
+ } };
521
+ }
522
+ //#endregion
523
+ //#region src/lib/fetch-retry.ts
524
+ /**
525
+ * Retry wrapper for fetching from the skills/agents server (GitHub releases in
526
+ * production). GitHub releases blips transiently, so every fetch on the run's
527
+ * critical path — skill menu, skill zips, agent menu, agent prompt bodies —
528
+ * goes through here rather than a bare `fetch`.
529
+ */
530
+ const DEFAULT_TIMEOUT_MS = 6e4;
531
+ const DEFAULT_MAX_ATTEMPTS = 3;
532
+ const DEFAULT_BACKOFF_MS = 500;
533
+ function sleep(ms) {
534
+ return new Promise((resolve) => setTimeout(resolve, ms));
535
+ }
536
+ /** Fetch a URL, retrying transient failures (network error or non-ok HTTP) with backoff. */
537
+ async function fetchWithRetry(url, opts = {}) {
538
+ const { fetchImpl = fetch, sleepImpl = sleep, timeoutMs = DEFAULT_TIMEOUT_MS, maxAttempts = DEFAULT_MAX_ATTEMPTS, backoffMs = DEFAULT_BACKOFF_MS } = opts;
539
+ const failures = [];
540
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) try {
541
+ const resp = await fetchImpl(url, { signal: AbortSignal.timeout(timeoutMs) });
542
+ if (!resp.ok) throw new Error(`HTTP ${resp.status} ${resp.statusText}`);
543
+ return resp;
544
+ } catch (err) {
545
+ failures.push(`attempt ${attempt}: ${err.message}`);
546
+ if (attempt < maxAttempts) await sleepImpl(backoffMs * 2 ** (attempt - 1));
547
+ }
548
+ throw new Error(`fetch ${url} failed — ${failures.join("; ")}`);
549
+ }
550
+ //#endregion
551
+ //#region src/lib/wizard-tools/tools.ts
552
+ /**
553
+ * Shared wizard-tool behavior — skill discovery/install, env-file helpers,
554
+ * the wizard_ask cap policy, secret-vault plumbing, and the audit ledger.
555
+ * One implementation consumed by both protocol facades: the MCP server
556
+ * (`./mcp`) and the pi-native tools (`harness/pi/tools.ts`) — tool behavior
557
+ * cannot drift between harnesses.
558
+ */
559
+ /** Expand a bundle entry into one entry per variant, so the menu reads the same whether a group ships bundled or as zips. */
560
+ function expandBundleEntry(entry) {
561
+ if (!entry.bundle || !entry.variants) return [entry];
562
+ return entry.variants.map((variant) => ({
563
+ ...variant,
564
+ name: entry.name,
565
+ group: entry.group,
566
+ bundle: true,
567
+ downloadUrl: entry.downloadUrl
568
+ }));
569
+ }
570
+ /**
571
+ * Fetch the skill menu from the skills server.
572
+ * Returns parsed data on success, `null` on failure.
573
+ */
574
+ async function fetchSkillMenu(skillsBaseUrl, opts = {}) {
575
+ const menuUrl = `${skillsBaseUrl}/skill-menu.json`;
576
+ try {
577
+ logToFile(`fetchSkillMenu: fetching from ${menuUrl}`);
578
+ const data = await (await fetchWithRetry(menuUrl, opts)).json();
579
+ for (const [category, entries] of Object.entries(data.categories)) data.categories[category] = entries.flatMap(expandBundleEntry);
580
+ logToFile(`fetchSkillMenu: loaded (${Object.keys(data.categories).length} categories)`);
581
+ return data;
582
+ } catch (err) {
583
+ logToFile(`fetchSkillMenu: error: ${err.message}`);
584
+ return null;
585
+ }
586
+ }
587
+ /** Extract a zip buffer, refusing entries that escape destDir (zip-slip). */
588
+ function extractZipArchive(zip, destDir) {
589
+ const root = path.resolve(destDir);
590
+ let written = 0;
591
+ for (const [entryPath, data] of Object.entries(unzipSync(zip))) {
592
+ const target = path.resolve(root, entryPath);
593
+ if (target !== root && !target.startsWith(root + path.sep)) throw new Error(`zip entry escapes destination: ${entryPath}`);
594
+ if (entryPath.endsWith("/")) {
595
+ fs.mkdirSync(target, { recursive: true });
596
+ continue;
597
+ }
598
+ fs.mkdirSync(path.dirname(target), { recursive: true });
599
+ fs.writeFileSync(target, data);
600
+ written++;
601
+ }
602
+ return written;
603
+ }
604
+ /** Unpack the one variant this entry names out of a bundle; the rest is noise and never hits disk. */
605
+ function extractBundle(bundle, destDir, entryId) {
606
+ if (typeof bundle?.id !== "string" || typeof bundle?.variants !== "object" || bundle.variants === null) throw new Error("malformed bundle: expected { id, variants }");
607
+ const files = bundle.variants[entryId.slice(bundle.id.length + 1)];
608
+ if (!files) throw new Error(`bundle ${bundle.id} has no variant "${entryId}"`);
609
+ const root = path.resolve(destDir);
610
+ let written = 0;
611
+ for (const [entryPath, contents] of Object.entries(files)) {
612
+ const target = path.resolve(root, entryPath);
613
+ if (target !== root && !target.startsWith(root + path.sep)) throw new Error(`bundle entry escapes destination: ${entryPath}`);
614
+ fs.mkdirSync(path.dirname(target), { recursive: true });
615
+ fs.writeFileSync(target, contents);
616
+ written++;
617
+ }
618
+ return written;
619
+ }
620
+ /** Download a URL to a buffer, retrying transient failures with backoff. */
621
+ async function downloadWithRetry(url, opts = {}) {
622
+ const resp = await fetchWithRetry(url, opts);
623
+ return new Uint8Array(await resp.arrayBuffer());
624
+ }
625
+ /**
626
+ * Download and extract a skill.
627
+ * By default installs to `<installDir>/.claude/skills/<id>/`.
628
+ * Pass `skillsRoot` to override the base directory (e.g. `.posthog/skills`).
629
+ */
630
+ async function downloadSkill(skillEntry, installDir, skillsRoot) {
631
+ const skillDir = skillsRoot ? path.join(installDir, skillsRoot, skillEntry.id) : path.join(installDir, ".claude", "skills", skillEntry.id);
632
+ let step = "download";
633
+ try {
634
+ fs.mkdirSync(skillDir, { recursive: true });
635
+ const data = await downloadWithRetry(skillEntry.downloadUrl);
636
+ step = "extract";
637
+ const fileCount = skillEntry.bundle ? extractBundle(JSON.parse(Buffer.from(data).toString("utf8")), skillDir, skillEntry.id) : extractZipArchive(data, skillDir);
638
+ fs.writeFileSync(path.join(skillDir, ".posthog-wizard"), "");
639
+ logToFile(`downloadSkill: installed ${skillEntry.id} from ${skillEntry.downloadUrl} (${fileCount} files)`);
640
+ return { success: true };
641
+ } catch (err) {
642
+ logToFile(`downloadSkill: error: ${err.message}`);
643
+ analytics.wizardCapture("skill install failed", {
644
+ skill_id: skillEntry.id,
645
+ step,
646
+ platform: process.platform,
647
+ error: String(err.message).slice(0, 500)
648
+ });
649
+ return {
650
+ success: false,
651
+ error: err.message
652
+ };
653
+ }
654
+ }
655
+ /**
656
+ * High-level "install a skill by ID" helper. Fetches the skill menu,
657
+ * finds the skill, downloads and extracts it. Programs should use this
658
+ * instead of composing fetchSkillMenu + downloadSkill themselves.
659
+ */
660
+ async function installSkillById(skillId, installDir, skillsBaseUrl, skillsRoot) {
661
+ const menu = await fetchSkillMenu(skillsBaseUrl);
662
+ if (!menu) return { kind: "menu-fetch-failed" };
663
+ const skill = Object.values(menu.categories).flat().find((s) => s.id === skillId);
664
+ if (!skill) return {
665
+ kind: "skill-not-found",
666
+ skillId
667
+ };
668
+ const result = await downloadSkill(skill, installDir, skillsRoot);
669
+ if (!result.success) return {
670
+ kind: "download-failed",
671
+ message: result.error ?? "unknown"
672
+ };
673
+ return {
674
+ kind: "ok",
675
+ path: skillsRoot ? `${skillsRoot}/${skillId}` : `.claude/skills/${skillId}`
676
+ };
677
+ }
678
+ /**
679
+ * Pure decision function for the wizard_ask caps. Returns whether the
680
+ * upcoming call should proceed and, if not, the error message to surface
681
+ * to the agent. Extracted so the policy can be unit-tested without
682
+ * spinning up an MCP server.
683
+ *
684
+ * The adjacency nudge fires exactly once per run (the caller records it
685
+ * via `adjacencyNudged`) — flows that legitimately need several
686
+ * sequential, answer-dependent asks then proceed up to `maxQuestions`.
687
+ * Without the flag the rejected call would never advance the counter and
688
+ * every later call would be rejected, making caps above the threshold
689
+ * unreachable.
690
+ */
691
+ function evaluateAskCap(callCount, maxQuestions, adjacencyNudged = false) {
692
+ if (callCount >= maxQuestions) return {
693
+ kind: "capped",
694
+ reason: "max_questions",
695
+ message: `Error: wizard_ask cap reached (${maxQuestions} calls in this run). Proceed with sensible defaults using the answers you already have, or emit [ABORT] requirements-incomplete.`
696
+ };
697
+ if (!adjacencyNudged && callCount >= 3) return {
698
+ kind: "capped",
699
+ reason: "adjacency",
700
+ message: `Error: too many wizard_ask calls in a row (${callCount} so far). Batch the remaining questions into a single call — the schema accepts up to 8 questions per invocation. If the remaining questions truly depend on earlier answers, ask again and they will go through.`
701
+ };
702
+ return { kind: "ok" };
703
+ }
704
+ const ENV_FILE_PATH_DESCRIPTION = "Path to the .env file, relative to the wizard working directory. Pass \".env\" for a file in that directory, or include the selected subproject path (for example, \"packages/app/.env\") for a nested project. Never prefix it with the wizard working directory's path inside an ancestor repository.";
705
+ /**
706
+ * Resolve filePath relative to workingDirectory, rejecting path traversal.
707
+ */
708
+ function resolveEnvPath(workingDirectory, filePath) {
709
+ const resolved = path.resolve(workingDirectory, filePath);
710
+ if (!resolved.startsWith(workingDirectory + path.sep) && resolved !== workingDirectory) throw new Error(`Path traversal rejected: "${filePath}" resolves outside working directory`);
711
+ return resolved;
712
+ }
713
+ /**
714
+ * Ensure the given env file basename is covered by .gitignore in the working directory.
715
+ * Creates .gitignore if it doesn't exist; appends the entry if missing.
716
+ */
717
+ function ensureGitignoreCoverage(workingDirectory, envFileName) {
718
+ const gitignorePath = path.join(workingDirectory, ".gitignore");
719
+ if (fs.existsSync(gitignorePath)) {
720
+ const content = fs.readFileSync(gitignorePath, "utf8");
721
+ if (content.split("\n").some((line) => line.trim() === envFileName)) return;
722
+ const newContent = content.endsWith("\n") ? `${content}${envFileName}\n` : `${content}\n${envFileName}\n`;
723
+ fs.writeFileSync(gitignorePath, newContent, "utf8");
724
+ } else fs.writeFileSync(gitignorePath, `${envFileName}\n`, "utf8");
725
+ }
726
+ /**
727
+ * Parse a .env file's content and return the set of defined key names.
728
+ */
729
+ function parseEnvKeys(content) {
730
+ const keys = /* @__PURE__ */ new Set();
731
+ for (const line of content.split("\n")) {
732
+ const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/);
733
+ if (match) keys.add(match[1]);
734
+ }
735
+ return keys;
736
+ }
737
+ /**
738
+ * Merge key-value pairs into existing .env content.
739
+ * Updates existing keys in-place, appends new keys at the end.
740
+ */
741
+ function mergeEnvValues(content, values) {
742
+ let result = content;
743
+ const updatedKeys = /* @__PURE__ */ new Set();
744
+ for (const [key, value] of Object.entries(values)) {
745
+ const regex = new RegExp(`^(\\s*${key}\\s*=).*$`, "m");
746
+ if (regex.test(result)) {
747
+ result = result.replace(regex, `$1${value}`);
748
+ updatedKeys.add(key);
749
+ }
750
+ }
751
+ const newKeys = Object.entries(values).filter(([key]) => !updatedKeys.has(key));
752
+ if (newKeys.length > 0) {
753
+ if (result.length > 0 && !result.endsWith("\n")) result += "\n";
754
+ for (const [key, value] of newKeys) result += `${key}=${value}\n`;
755
+ }
756
+ return result;
757
+ }
758
+ /**
759
+ * Swap sensitive text answers for opaque vault refs before they return to
760
+ * the agent — the raw value never enters the LLM conversation. Cancelled
761
+ * answers pass through as the sentinel, unvaulted.
762
+ */
763
+ function vaultSensitiveAnswers(questions, answers, vault) {
764
+ const sensitiveById = new Map(questions.filter((q) => q.sensitive).map((q) => [q.id, q.prompt]));
765
+ const sanitised = {};
766
+ for (const [id, answer] of Object.entries(answers)) {
767
+ const label = sensitiveById.get(id);
768
+ if (label !== void 0 && typeof answer === "string" && answer !== "__cancelled__") {
769
+ const ref = vault.put(answer, {
770
+ label,
771
+ source: "wizard_ask"
772
+ });
773
+ sanitised[id] = { secretRef: ref };
774
+ logToFile(`wizard_ask: vaulted answer for "${id}" as ${ref}`);
775
+ } else sanitised[id] = answer;
776
+ }
777
+ return sanitised;
778
+ }
779
+ /**
780
+ * Resolve `{secretRef}` entries host-side, so the value is written but never
781
+ * returned to the agent. Callers format their own error envelope from the
782
+ * failing key/ref.
783
+ */
784
+ function resolveEnvSecretRefs(values, vault) {
785
+ const resolved = {};
786
+ const refKeys = [];
787
+ for (const [key, val] of Object.entries(values)) {
788
+ if (typeof val === "string") {
789
+ resolved[key] = val;
790
+ continue;
791
+ }
792
+ const secret = vault.get(val.secretRef);
793
+ if (secret === void 0) return {
794
+ ok: false,
795
+ key,
796
+ secretRef: val.secretRef
797
+ };
798
+ resolved[key] = secret;
799
+ refKeys.push(key);
800
+ }
801
+ return {
802
+ ok: true,
803
+ values: resolved,
804
+ refKeys
805
+ };
806
+ }
807
+ const AUDIT_STATUSES = [
808
+ "pending",
809
+ "pass",
810
+ "error",
811
+ "warning",
812
+ "suggestion"
813
+ ];
814
+ /** Atomically write the audit ledger. Thin typed wrapper over writeJsonAtomic. */
815
+ function writeLedgerAtomic(targetPath, checks) {
816
+ writeJsonAtomic(targetPath, checks);
817
+ }
818
+ /**
819
+ * Apply a batch of patches to the ledger by id. Returns the new array and the
820
+ * list of update ids that didn't match any existing check.
821
+ */
822
+ function applyAuditUpdates(current, updates) {
823
+ const byId = new Map(current.map((c) => [c.id, c]));
824
+ const unknown = [];
825
+ for (const u of updates) {
826
+ const existing = byId.get(u.id);
827
+ if (!existing) {
828
+ unknown.push(u.id);
829
+ continue;
830
+ }
831
+ byId.set(u.id, {
832
+ ...existing,
833
+ status: u.status,
834
+ ...u.file !== void 0 ? { file: u.file } : {},
835
+ ...u.details !== void 0 ? { details: u.details } : {}
836
+ });
837
+ }
838
+ return {
839
+ next: current.map((c) => byId.get(c.id) ?? c),
840
+ unknown
841
+ };
842
+ }
843
+ /**
844
+ * Append new checks to a seeded ledger. Duplicate ids are reported without
845
+ * mutating the current ledger, including duplicates inside the additions.
846
+ */
847
+ function applyAuditAdditions(current, additions) {
848
+ const existingIds = new Set(current.map((c) => c.id));
849
+ const additionIds = /* @__PURE__ */ new Set();
850
+ const duplicates = [];
851
+ for (const check of additions) {
852
+ if (existingIds.has(check.id) || additionIds.has(check.id)) {
853
+ duplicates.push(check.id);
854
+ continue;
855
+ }
856
+ additionIds.add(check.id);
857
+ }
858
+ if (duplicates.length > 0) return {
859
+ next: current,
860
+ duplicates
861
+ };
862
+ return {
863
+ next: [...current, ...additions],
864
+ duplicates: []
865
+ };
866
+ }
867
+ function readLedger(targetPath) {
868
+ if (!fs.existsSync(targetPath)) return [];
869
+ try {
870
+ return coerceAuditChecks(JSON.parse(fs.readFileSync(targetPath, "utf8")));
871
+ } catch {
872
+ return [];
873
+ }
874
+ }
875
+ function appendAuditChecksToLedger(targetPath, additions) {
876
+ if (!fs.existsSync(targetPath)) return {
877
+ ok: false,
878
+ reason: "missing-ledger"
879
+ };
880
+ const { next, duplicates } = applyAuditAdditions(readLedger(targetPath), additions);
881
+ if (duplicates.length > 0) return {
882
+ ok: false,
883
+ reason: "duplicate-ids",
884
+ ids: duplicates
885
+ };
886
+ writeLedgerAtomic(targetPath, next);
887
+ return {
888
+ ok: true,
889
+ added: additions.length
890
+ };
891
+ }
892
+ const SERVER_NAME = "wizard-tools";
893
+ /** Tool names exposed by the wizard-tools server, keyed for selective use. */
894
+ const WIZARD_TOOL_NAMES = {
895
+ checkEnvKeys: `mcp__${SERVER_NAME}__check_env_keys`,
896
+ setEnvValues: `mcp__${SERVER_NAME}__set_env_values`,
897
+ detectPackageManager: `mcp__${SERVER_NAME}__detect_package_manager`,
898
+ loadSkillMenu: `mcp__${SERVER_NAME}__load_skill_menu`,
899
+ installSkill: `mcp__${SERVER_NAME}__install_skill`,
900
+ auditSeedChecks: `mcp__${SERVER_NAME}__audit_seed_checks`,
901
+ auditAddChecks: `mcp__${SERVER_NAME}__audit_add_checks`,
902
+ auditResolveChecks: `mcp__${SERVER_NAME}__audit_resolve_checks`,
903
+ wizardAsk: `mcp__${SERVER_NAME}__wizard_ask`,
904
+ enqueueTask: `mcp__${SERVER_NAME}__enqueue_task`,
905
+ completeTask: `mcp__${SERVER_NAME}__complete_task`,
906
+ readHandoffs: `mcp__${SERVER_NAME}__read_handoffs`
907
+ };
908
+ //#endregion
909
+ //#region src/lib/secret-vault.ts
910
+ /**
911
+ * Session-scoped secret vault.
912
+ *
913
+ * Tools that handle sensitive values (personal API keys pasted by the user
914
+ * or minted by the wizard, OAuth tokens, etc.) store them here and return
915
+ * an opaque `secret:<uuid>` ref to the agent. The agent passes the ref
916
+ * around to subsequent tools — `set_env_values` resolves it host-side
917
+ * before writing — but the raw value never enters the LLM conversation.
918
+ *
919
+ * The vault is created once per `createWizardToolsServer` call and lives
920
+ * for the duration of a single wizard run. There is no persistence and
921
+ * no cross-session sharing; refs minted in one run cannot be resolved in
922
+ * another.
923
+ */
924
+ const REF_PREFIX = "secret:";
925
+ function createSecretVault() {
926
+ const store = /* @__PURE__ */ new Map();
927
+ return {
928
+ put(value, meta) {
929
+ const ref = `${REF_PREFIX}${randomUUID()}`;
930
+ store.set(ref, {
931
+ value,
932
+ meta: {
933
+ ref,
934
+ label: meta.label,
935
+ source: meta.source,
936
+ createdAt: Date.now()
937
+ }
938
+ });
939
+ return ref;
940
+ },
941
+ get(ref) {
942
+ return store.get(ref)?.value;
943
+ },
944
+ has(ref) {
945
+ return store.has(ref);
946
+ },
947
+ list() {
948
+ return [...store.values()].map((s) => s.meta);
949
+ },
950
+ clear() {
951
+ store.clear();
952
+ }
953
+ };
954
+ }
955
+ //#endregion
956
+ //#region src/lib/wizard-tools/mcp.ts
957
+ /**
958
+ * MCP facade over `./tools` — the unified in-process server the anthropic
959
+ * harness mounts. Declarations (zod schemas, descriptions) and the MCP result
960
+ * envelope live here; all behavior is imported from the shared core.
961
+ *
962
+ * Provides: check_env_keys, set_env_values, detect_package_manager,
963
+ * load_skill_menu / install_skill, audit_* ledger tools, wizard_ask, and the
964
+ * orchestrator queue tools. Secret values never leave the machine.
965
+ */
966
+ const auditCheckSchema = z.object({
967
+ id: z.string().min(1),
968
+ area: z.string().min(1),
969
+ label: z.string().min(1),
970
+ status: z.enum(AUDIT_STATUSES),
971
+ file: z.string().optional(),
972
+ details: z.string().optional()
973
+ });
974
+ const auditUpdateSchema = z.object({
975
+ id: z.string().min(1),
976
+ status: z.enum(AUDIT_STATUSES),
977
+ file: z.string().optional(),
978
+ details: z.string().optional()
979
+ });
980
+ let _sdkModule$1 = null;
981
+ async function getSDKModule$1() {
982
+ if (!_sdkModule$1) _sdkModule$1 = await import("@anthropic-ai/claude-agent-sdk");
983
+ return _sdkModule$1;
984
+ }
985
+ /** Default per-run cap on wizard_ask calls when no override is provided. */
986
+ /**
987
+ * Create the unified in-process MCP server with all wizard tools.
988
+ * Must be called asynchronously because the SDK is an ESM module loaded via dynamic import.
989
+ */
990
+ async function createWizardToolsServer(options) {
991
+ const { workingDirectory, detectPackageManager, skillsBaseUrl, askBridge, askMaxQuestions = 10, secretVault = createSecretVault(), orchestrator } = options;
992
+ const { tool, createSdkMcpServer } = await getSDKModule$1();
993
+ let askCallCount = 0;
994
+ let askAdjacencyNudged = false;
995
+ let cachedSkillMenu = {};
996
+ let categoryNames = ["integration"];
997
+ const menu = await fetchSkillMenu(skillsBaseUrl);
998
+ if (menu) cachedSkillMenu = menu.categories;
999
+ const keys = Object.keys(cachedSkillMenu);
1000
+ if (keys.length > 0) categoryNames = keys;
1001
+ const checkEnvKeys = tool("check_env_keys", "Check which environment variable keys are present or missing in a .env file. Never reveals values.", {
1002
+ filePath: z.string().describe(ENV_FILE_PATH_DESCRIPTION),
1003
+ keys: z.array(z.string()).describe("Environment variable key names to check")
1004
+ }, (args) => {
1005
+ const resolved = resolveEnvPath(workingDirectory, args.filePath);
1006
+ logToFile(`check_env_keys: ${resolved}, keys: ${args.keys.join(", ")}`);
1007
+ const existingKeys = fs.existsSync(resolved) ? parseEnvKeys(fs.readFileSync(resolved, "utf8")) : /* @__PURE__ */ new Set();
1008
+ const results = {};
1009
+ for (const key of args.keys) results[key] = existingKeys.has(key) ? "present" : "missing";
1010
+ return { content: [{
1011
+ type: "text",
1012
+ text: JSON.stringify(results, null, 2)
1013
+ }] };
1014
+ });
1015
+ const setEnvValues = tool("set_env_values", "Create or update environment variable keys in a .env file. Creates the file if it does not exist. Ensures .gitignore coverage. Each value can be either a literal string or a secret reference of the form `{ \"secretRef\": \"secret:...\" }` returned by another tool (e.g. wizard_ask). Secret references are resolved locally — the actual value is written to the file but never returned to the agent.", {
1016
+ filePath: z.string().describe(ENV_FILE_PATH_DESCRIPTION),
1017
+ values: z.record(z.string(), z.union([z.string(), z.object({ secretRef: z.string() })])).describe("Key → (literal string OR { secretRef } pointing to a vaulted secret)")
1018
+ }, (args) => {
1019
+ const forbidden = Object.keys(args.values).find((k) => k.toUpperCase() === "POSTHOG_KEY");
1020
+ if (forbidden) return {
1021
+ content: [{
1022
+ type: "text",
1023
+ text: `Error: "${forbidden}" is not a valid PostHog env var name. Use the project-specific key name from your framework's integration guide (e.g. NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN).`
1024
+ }],
1025
+ isError: true
1026
+ };
1027
+ const resolution = resolveEnvSecretRefs(args.values, secretVault);
1028
+ if (!resolution.ok) return {
1029
+ content: [{
1030
+ type: "text",
1031
+ text: `Error: secret reference "${resolution.secretRef}" for key "${resolution.key}" is not known to the vault. The ref may have expired, been minted in a different run, or been mistyped.`
1032
+ }],
1033
+ isError: true
1034
+ };
1035
+ const { values: resolvedValues, refKeys: resolvedRefKeys } = resolution;
1036
+ const resolved = resolveEnvPath(workingDirectory, args.filePath);
1037
+ logToFile(`set_env_values: ${resolved}, keys: ${Object.keys(resolvedValues).join(", ")}${resolvedRefKeys.length > 0 ? ` (secret refs: ${resolvedRefKeys.join(", ")})` : ""}`);
1038
+ const content = mergeEnvValues(fs.existsSync(resolved) ? fs.readFileSync(resolved, "utf8") : "", resolvedValues);
1039
+ const dir = path.dirname(resolved);
1040
+ if (!fs.existsSync(dir)) {
1041
+ analytics.wizardCapture("set_env_values parent dir missing", { platform: process.platform });
1042
+ return {
1043
+ content: [{
1044
+ type: "text",
1045
+ text: `Error: parent directory does not exist: "${path.dirname(args.filePath)}". filePath is resolved against the wizard working directory — pass ".env" for a file there, or "<subproject>/.env" for an existing nested project.`
1046
+ }],
1047
+ isError: true
1048
+ };
1049
+ }
1050
+ fs.writeFileSync(resolved, content, "utf8");
1051
+ ensureGitignoreCoverage(workingDirectory, path.basename(resolved));
1052
+ return { content: [{
1053
+ type: "text",
1054
+ text: `Updated ${Object.keys(args.values).length} key(s) in ${args.filePath}`
1055
+ }] };
1056
+ });
1057
+ const detectPM = tool("detect_package_manager", "Detect which package manager(s) the project uses. Returns the name, install command, and run command for each detected package manager. Call this before running any install commands.", {}, async () => {
1058
+ logToFile(`detect_package_manager: scanning ${workingDirectory}`);
1059
+ const result = await detectPackageManager(workingDirectory);
1060
+ logToFile(`detect_package_manager: detected ${result.detected.length} package manager(s)`);
1061
+ return { content: [{
1062
+ type: "text",
1063
+ text: JSON.stringify(result, null, 2)
1064
+ }] };
1065
+ });
1066
+ const loadSkillMenu = tool("load_skill_menu", "Load available PostHog skills for a category. Returns skill IDs and names. Call this first, then use install_skill with the chosen ID.", { category: z.enum(categoryNames).describe("Skill category") }, (args) => {
1067
+ const skills = cachedSkillMenu[args.category];
1068
+ if (!skills || skills.length === 0) return {
1069
+ content: [{
1070
+ type: "text",
1071
+ text: `No skills found for category "${args.category}".`
1072
+ }],
1073
+ isError: true
1074
+ };
1075
+ const menuText = skills.map((s) => `- ${s.id}: ${s.name}`).join("\n");
1076
+ logToFile(`load_skill_menu: returning ${skills.length} skills for "${args.category}"`);
1077
+ return { content: [{
1078
+ type: "text",
1079
+ text: menuText
1080
+ }] };
1081
+ });
1082
+ const installSkill = tool("install_skill", "Download and install a PostHog skill by ID. Call load_skill_menu first to see available skills. Extracts the skill to .claude/skills/<skillId>/.", { skillId: z.string().describe("Skill ID from the skill menu (e.g., \"integration-nextjs-app-router\")") }, async (args) => {
1083
+ if (!/^[a-z0-9][a-z0-9_-]*$/.test(args.skillId)) return {
1084
+ content: [{
1085
+ type: "text",
1086
+ text: "Error: skillId must be lowercase alphanumeric with hyphens."
1087
+ }],
1088
+ isError: true
1089
+ };
1090
+ const skill = Object.values(cachedSkillMenu).flat().find((s) => s.id === args.skillId);
1091
+ if (!skill) return {
1092
+ content: [{
1093
+ type: "text",
1094
+ text: `Error: skill "${args.skillId}" not found. Use load_skill_menu to see available skills.`
1095
+ }],
1096
+ isError: true
1097
+ };
1098
+ const result = await downloadSkill(skill, workingDirectory);
1099
+ if (result.success) return { content: [{
1100
+ type: "text",
1101
+ text: `Skill installed to .claude/skills/${args.skillId}/`
1102
+ }] };
1103
+ else {
1104
+ analytics.captureException(/* @__PURE__ */ new Error("Skill install failed: download-failed"), {
1105
+ source: "install_skill_tool",
1106
+ skill_id: args.skillId,
1107
+ error_detail: String(result.error).slice(0, 500),
1108
+ platform: process.platform
1109
+ });
1110
+ return {
1111
+ content: [{
1112
+ type: "text",
1113
+ text: `Error installing skill: ${result.error}`
1114
+ }],
1115
+ isError: true
1116
+ };
1117
+ }
1118
+ });
1119
+ const auditLedgerPath = path.join(workingDirectory, AUDIT_CHECKS_FILE);
1120
+ const auditMutex = makeMutex();
1121
+ const auditSeedChecks = tool("audit_seed_checks", "Seed the audit ledger at .posthog-audit-checks.json with the full set of pending checks. Call this once at the start of the audit. Atomically replaces any existing ledger.", { checks: z.array(auditCheckSchema).describe("Full pending checklist to write to the ledger") }, async (args) => {
1122
+ return auditMutex(() => {
1123
+ writeLedgerAtomic(auditLedgerPath, args.checks);
1124
+ logToFile(`audit_seed_checks: wrote ${args.checks.length} entries`);
1125
+ return { content: [{
1126
+ type: "text",
1127
+ text: `Seeded ${args.checks.length} audit checks.`
1128
+ }] };
1129
+ });
1130
+ });
1131
+ const auditAddChecks = tool("audit_add_checks", "Append one or more pending checks to the existing audit ledger at .posthog-audit-checks.json. Call audit_seed_checks first. Atomically rejects duplicate ids without changing the ledger.", { checks: z.array(auditCheckSchema).min(1).describe("Additional checks to append to the existing ledger") }, async (args) => {
1132
+ return auditMutex(() => {
1133
+ const result = appendAuditChecksToLedger(auditLedgerPath, args.checks);
1134
+ if (!result.ok) {
1135
+ if (result.reason === "missing-ledger") return {
1136
+ content: [{
1137
+ type: "text",
1138
+ text: "Error: audit ledger does not exist. Run audit_seed_checks first."
1139
+ }],
1140
+ isError: true
1141
+ };
1142
+ return {
1143
+ content: [{
1144
+ type: "text",
1145
+ text: `Error: duplicate check id(s): ${result.ids.join(", ")}. Check ids must be unique.`
1146
+ }],
1147
+ isError: true
1148
+ };
1149
+ }
1150
+ logToFile(`audit_add_checks: added ${result.added} entries`);
1151
+ return { content: [{
1152
+ type: "text",
1153
+ text: `Added ${result.added} audit check(s).`
1154
+ }] };
1155
+ });
1156
+ });
1157
+ const auditResolveChecks = tool("audit_resolve_checks", "Resolve one or more audit checks by id. Patches each entry's status (and optional file/details) and writes the ledger back atomically. Concurrent calls serialize.", { updates: z.array(auditUpdateSchema).min(1).describe("Patches to apply, keyed by check id") }, async (args) => {
1158
+ return auditMutex(() => {
1159
+ const { next, unknown } = applyAuditUpdates(readLedger(auditLedgerPath), args.updates);
1160
+ if (unknown.length > 0) return {
1161
+ content: [{
1162
+ type: "text",
1163
+ text: `Error: unknown check id(s): ${unknown.join(", ")}. Run audit_seed_checks first or check the id.`
1164
+ }],
1165
+ isError: true
1166
+ };
1167
+ writeLedgerAtomic(auditLedgerPath, next);
1168
+ logToFile(`audit_resolve_checks: applied ${args.updates.length} update(s)`);
1169
+ return { content: [{
1170
+ type: "text",
1171
+ text: `Resolved ${args.updates.length} check(s).`
1172
+ }] };
1173
+ });
1174
+ });
1175
+ const askQuestionSchema = z.object({
1176
+ id: z.string().min(1).describe("Stable key for the answer in the response map"),
1177
+ prompt: z.string().min(1).describe("Question text shown to the user"),
1178
+ kind: z.enum([
1179
+ "single",
1180
+ "multi",
1181
+ "text"
1182
+ ]).describe("'single' = pick one option, 'multi' = pick any, 'text' = free-form single-line answer"),
1183
+ options: z.array(z.object({
1184
+ label: z.string(),
1185
+ value: z.string(),
1186
+ description: z.string().optional().describe("Optional secondary line shown dimmed and wrapped beneath the label (multi-select only). Use when a choice needs more than a title — e.g. what a custom scout watches and what makes it speak up.")
1187
+ })).optional().describe("Required for kind=single|multi; ignored for kind=text"),
1188
+ required: z.boolean().optional().describe("Defaults to true"),
1189
+ sensitive: z.boolean().optional().describe("Only valid for kind='text'. When true, the user's answer is stored in the wizard's secret vault and returned to you as { secretRef: 'secret:...' } instead of the raw string. Use for API keys, tokens, and any other secret the user types in. The secretRef is only resolved by wizard-tools that accept it (e.g. set_env_values) — it is NOT resolved when passed to other MCP tools (e.g. PostHog data-warehouse tools), which will reject it. For a secret that must reach another tool, write it to the env with set_env_values first, or use that tool's own credential-reference flow.")
1190
+ });
1191
+ return createSdkMcpServer({
1192
+ name: SERVER_NAME,
1193
+ version: "1.0.0",
1194
+ tools: [
1195
+ checkEnvKeys,
1196
+ setEnvValues,
1197
+ detectPM,
1198
+ loadSkillMenu,
1199
+ installSkill,
1200
+ auditSeedChecks,
1201
+ auditAddChecks,
1202
+ auditResolveChecks,
1203
+ tool("wizard_ask", "Ask the user one or more structured questions and wait for their answers. Use this whenever you would otherwise inline a question in your text output. Batch related questions into a single call (up to 8) rather than asking one at a time; sequential calls are fine when later questions genuinely depend on earlier answers. A fully cancelled or timed-out response does NOT count against the per-run cap — treat it as \"the user declined\" and fall back gracefully (e.g. hand over a deep link) without worrying about a wasted call.", { questions: z.array(askQuestionSchema).min(1).max(8) }, async (args) => {
1204
+ if (!askBridge) return {
1205
+ content: [{
1206
+ type: "text",
1207
+ text: "Error: wizard_ask is not available in this environment (CI / non-interactive). Proceed with sensible defaults or emit [ABORT] requirements-incomplete."
1208
+ }],
1209
+ isError: true
1210
+ };
1211
+ const capDecision = evaluateAskCap(askCallCount, askMaxQuestions, askAdjacencyNudged);
1212
+ if (capDecision.kind === "capped") {
1213
+ if (capDecision.reason === "adjacency") askAdjacencyNudged = true;
1214
+ analytics.wizardCapture("wizard_ask capped", {
1215
+ reason: capDecision.reason,
1216
+ call_count: askCallCount,
1217
+ max_questions: askMaxQuestions
1218
+ });
1219
+ return {
1220
+ content: [{
1221
+ type: "text",
1222
+ text: capDecision.message
1223
+ }],
1224
+ isError: true
1225
+ };
1226
+ }
1227
+ for (const q of args.questions) {
1228
+ if ((q.kind === "single" || q.kind === "multi") && (!q.options || q.options.length === 0)) return {
1229
+ content: [{
1230
+ type: "text",
1231
+ text: `Error: question "${q.id}" has kind="${q.kind}" but no options. Provide at least one { label, value } option, or change kind to "text".`
1232
+ }],
1233
+ isError: true
1234
+ };
1235
+ if (q.sensitive && q.kind !== "text") return {
1236
+ content: [{
1237
+ type: "text",
1238
+ text: `Error: question "${q.id}" sets sensitive=true but kind="${q.kind}". Only kind="text" answers can be vaulted as secrets.`
1239
+ }],
1240
+ isError: true
1241
+ };
1242
+ }
1243
+ const ids = /* @__PURE__ */ new Set();
1244
+ for (const q of args.questions) {
1245
+ if (ids.has(q.id)) return {
1246
+ content: [{
1247
+ type: "text",
1248
+ text: `Error: duplicate question id "${q.id}". Each question must have a unique id.`
1249
+ }],
1250
+ isError: true
1251
+ };
1252
+ ids.add(q.id);
1253
+ }
1254
+ askCallCount += 1;
1255
+ try {
1256
+ const answers = await askBridge.request({ questions: args.questions });
1257
+ if (isFullyCancelled(answers)) askCallCount -= 1;
1258
+ const sanitised = vaultSensitiveAnswers(args.questions, answers, secretVault);
1259
+ logToFile(`wizard_ask: resolved ${Object.keys(answers).length} answer(s) for ${args.questions.length} question(s)`);
1260
+ return { content: [{
1261
+ type: "text",
1262
+ text: JSON.stringify({ answers: sanitised }, null, 2)
1263
+ }] };
1264
+ } catch (err) {
1265
+ askCallCount -= 1;
1266
+ logToFile(`wizard_ask: error: ${err?.message ?? err}`);
1267
+ return {
1268
+ content: [{
1269
+ type: "text",
1270
+ text: `Error: wizard_ask failed: ${err?.message ?? String(err)}`
1271
+ }],
1272
+ isError: true
1273
+ };
1274
+ }
1275
+ }),
1276
+ ...orchestrator ? buildOrchestratorTools(tool, orchestrator) : []
1277
+ ]
1278
+ });
1279
+ }
1280
+ //#endregion
178
1281
  //#region src/lib/agent/triage-provider.ts
179
1282
  /**
180
1283
  * LLM provider for warlock security-scan triage.
@@ -231,44 +1334,6 @@ function createTriageLLMProvider(auth) {
231
1334
  };
232
1335
  }
233
1336
  //#endregion
234
- //#region src/lib/agent/commandments.ts
235
- /**
236
- * Wizard-wide commandments that are always appended as a system prompt.
237
- *
238
- * Keep this as a simple string so it can be inlined into the compiled bundle
239
- * without extra files, copying, or runtime I/O.
240
- */
241
- const WIZARD_COMMANDMENTS = [
242
- "Never hallucinate a PostHog project token, host, or any other secret. Always use the real values that have been configured for this project (for example via environment variables).",
243
- "Never write API keys, access tokens, or other secrets directly into source code. Always reference environment variables instead, and rely on the wizard-tools MCP server (check_env_keys / set_env_values) to create or update .env files.",
244
- "Always use the detect_package_manager tool from the wizard-tools MCP server to determine the package manager. Do not guess based on lockfiles or hard-code npm, yarn, pnpm, bun, pip, etc.",
245
- "Before writing to any file, you MUST read that exact file immediately beforehand using the Read tool, even if you have already read it earlier in the run. This avoids tool failures and stale edits.",
246
- "Treat feature flags, custom properties, and event names as part of an analytics contract. Prefer reusing existing names and patterns in the project. When you must introduce new ones, make them clear, descriptive, and consistent with existing conventions, and avoid scattering the same flag or property across many unrelated callsites.",
247
- "Keep PostHog data capture at its defaults unless the user explicitly asks otherwise. Do not disable autocapture, do not disable session recording, and never set opt_out_capturing (or opted_out) to true in the SDK init config — these turn off data the user almost always wants. Note: posthog.opt_out_capturing() called at runtime for GDPR consent flows is legitimate; the rule is about the init configuration.",
248
- "Never put personally identifiable information — emails, full or partial names, phone numbers, physical or IP addresses, or other user-entered PII — in capture() event properties. PII belongs on the PERSON: send it via identify()/$set (or the SDK's person-property API), and capture events with a stable distinct id derived from the authenticated user or session, never a raw email or name. This holds for every SDK, client and server.",
249
- "Prefer minimal, targeted edits that achieve the requested behavior while preserving existing structure and style. Avoid large refactors, broad reformatting, or unrelated changes unless explicitly requested.",
250
- "When you change a file that already exists, edit it in place. If an edit fails because the text you are matching is not unique, add just enough surrounding context to make it unique, or apply the change to each occurrence — never fall back to rewriting the whole file. Write a file in full only when you are creating it, never to modify one that already exists.",
251
- "Before you overwrite or delete anything that already exists, look at what you would remove. This integration only adds instrumentation, so keep every part of the project unrelated to PostHog exactly as it was — never drop existing code, comments, or markup to make room for a change.",
252
- "Do not spawn subagents unless explicitly instructed to do so.",
253
- "Create tasks as soon as you understand the work you are going to carry out. Break the list into distinct stages of work that the user can follow through. Create all tasks in a single tool call, in the order you will be performing them. Drive the work with TaskUpdate: status in_progress when you begin a task, completed when done.",
254
- "Keep task titles broad and stage-oriented — describe the purpose or area of work, not the specific files, paths, or symbols involved. Do not name individual files, modules, or directories inside task titles, and do not include illustrative examples within a task title.",
255
- "Do not assume \"PostHog provider\" or \"PostHogProvider\" in task labels, status messages, or code. Only use a provider if the skill workflow explicitly requires one for the specific framework.",
256
- "If you see a [YARA WARNING] or [YARA VIOLATION] message, handle it silently. Fix the issue (revert the write, avoid the flagged pattern) without mentioning security warnings or YARA to the user. These are internal guardrails, not user-facing alerts. Never show scary security language to the user.",
257
- [
258
- "When a skill instructs you to gather information from the user, use the `wizard_ask` MCP tool from the wizard-tools server. Never inline questions in your text output expecting a reply — the user has no way to answer text.",
259
- "When a skill provides a numbered or bulleted list of questions, translate the entire list into a single `wizard_ask` tool call:",
260
- " - One tool call per skill step. Batch every question from that step into the `questions` array — never split into multiple calls.",
261
- " - Infer `kind` from the question phrasing: comma-separated alternatives (\"React, Vue, or vanilla JS?\") → `single`; phrasing like \"all that apply\" or \"any of\" → `multi`; everything else → `text`.",
262
- " - For `single` and `multi`, extract the alternatives from the prose into `options` as `{ label, value }` pairs. Use the human phrase as `label` and a lowercase-hyphenated form as `value` (e.g., `label: \"Vanilla JS\"`, `value: \"vanilla-js\"`).",
263
- " - Use a kebab-case slug of the question label as `id` (e.g., \"Tech stack\" → `tech-stack`, \"Show frequency\" → `show-frequency`).",
264
- " - Do not invent fields the schema does not define (no `source`, `category`, `priority`, etc.) — the tool rejects unknown fields and the wizard already knows which skill is running.",
265
- "After `wizard_ask` returns, use the answers directly — do not re-ask in text or call `wizard_ask` again for the same fields."
266
- ].join("\n")
267
- ].join("\n");
268
- function getWizardCommandments() {
269
- return WIZARD_COMMANDMENTS;
270
- }
271
- //#endregion
272
1337
  //#region src/lib/agent/agent-phase.ts
273
1338
  /** Maps a Claude SDK tool name to the phase that tool implies. Returns null
274
1339
  * when the tool doesn't drive the visualizer (Task*, TodoWrite, etc.). */
@@ -959,62 +2024,9 @@ function buildAgentEnv(wizardMetadata, wizardFlags) {
959
2024
  return encoded;
960
2025
  }
961
2026
  /**
962
- * Package managers that can be used to run commands.
963
- */
964
- const PACKAGE_MANAGERS = [
965
- "npm",
966
- "pnpm",
967
- "yarn",
968
- "bun",
969
- "npx",
970
- "pip",
971
- "pip3",
972
- "poetry",
973
- "pipenv",
974
- "uv"
975
- ];
976
- /**
977
- * Safe scripts/commands that can be run with any package manager.
978
- * Uses startsWith matching, so 'build' matches 'build', 'build:prod', etc.
979
- * Note: Linting tools are in LINTING_TOOLS and checked separately.
980
- */
981
- const SAFE_SCRIPTS = [
982
- "install",
983
- "add",
984
- "ci",
985
- "build",
986
- "tsc",
987
- "typecheck",
988
- "type-check",
989
- "check-types",
990
- "types",
991
- "lint",
992
- "format"
993
- ];
994
- /**
995
- * Dangerous shell operators that could allow command injection.
996
- * Note: We handle `2>&1` and `| tail/head` separately as safe patterns.
997
- */
998
- const DANGEROUS_OPERATORS = /[;`$()]/;
999
- /**
1000
- * Check if command is an allowed package manager command.
1001
- * Matches: <pkg-manager> [run|exec] <safe-script> [args...]
1002
- */
1003
- function matchesAllowedPrefix(command) {
1004
- const parts = command.split(/\s+/);
1005
- if (parts.length === 0 || !PACKAGE_MANAGERS.includes(parts[0])) return false;
1006
- let scriptIndex = 1;
1007
- if (parts[scriptIndex] === "run" || parts[scriptIndex] === "exec") scriptIndex++;
1008
- if (parts[scriptIndex] === "i") return true;
1009
- const scriptPart = parts.slice(scriptIndex).join(" ");
1010
- return SAFE_SCRIPTS.some((safe) => scriptPart.startsWith(safe)) || LINTING_TOOLS.some((tool) => scriptPart.startsWith(tool));
1011
- }
1012
- /**
1013
- * Permission hook that allows only safe commands.
1014
- * - Package manager install commands
1015
- * - Build/typecheck/lint commands for verification
1016
- * - Piping to tail/head for output limiting is allowed
1017
- * - Stderr redirection (2>&1) is allowed
2027
+ * Permission hook that allows only safe commands. Bash commands are gated by
2028
+ * the exact per-manager fence in bash-fence.ts (install/build/typecheck/lint
2029
+ * commands, single | tail/head, 2>&1).
1018
2030
  *
1019
2031
  * `wizardAskPending` is true while a wizard_ask overlay is open — when set,
1020
2032
  * Write/Edit calls are denied as a defense-in-depth measure against a
@@ -1040,7 +2052,8 @@ function wizardCanUseTool(toolName, input, context = {}) {
1040
2052
  if (toolName === "Read" || toolName === "Write" || toolName === "Edit") {
1041
2053
  const filePath = typeof input.file_path === "string" ? input.file_path : "";
1042
2054
  const basename = path.basename(filePath);
1043
- if (basename.startsWith(".env")) {
2055
+ const isEnvExample = /^\.env\.(example|sample|template|dist)$/.test(basename);
2056
+ if (basename.startsWith(".env") && !isEnvExample) {
1044
2057
  logToFile(`Denying ${toolName} on env file: ${filePath}`);
1045
2058
  return {
1046
2059
  behavior: "deny",
@@ -1071,56 +2084,8 @@ function wizardCanUseTool(toolName, input, context = {}) {
1071
2084
  updatedInput: input
1072
2085
  };
1073
2086
  const command = (typeof input.command === "string" ? input.command : "").trim();
1074
- if (DANGEROUS_OPERATORS.test(command)) {
1075
- logToFile(`Denying bash command with dangerous operators: ${command}`);
1076
- debug(`Denying bash command with dangerous operators: ${command}`);
1077
- analytics.wizardCapture("bash denied", {
1078
- reason: "dangerous operators",
1079
- command
1080
- });
1081
- return {
1082
- behavior: "deny",
1083
- message: `Bash command not allowed. Shell operators like ; \` $ ( ) are not permitted.`
1084
- };
1085
- }
1086
- const normalized = command.replace(/\s*\d*>&\d+\s*/g, " ").trim();
1087
- const pipeMatch = normalized.match(/^(.+?)\s*\|\s*(tail|head)(\s+\S+)*\s*$/);
1088
- if (pipeMatch) {
1089
- const baseCommand = pipeMatch[1].trim();
1090
- if (/[|&]/.test(baseCommand)) {
1091
- logToFile(`Denying bash command with multiple pipes: ${command}`);
1092
- debug(`Denying bash command with multiple pipes: ${command}`);
1093
- analytics.wizardCapture("bash denied", {
1094
- reason: "multiple pipes",
1095
- command
1096
- });
1097
- return {
1098
- behavior: "deny",
1099
- message: `Bash command not allowed. Only single pipe to tail/head is permitted.`
1100
- };
1101
- }
1102
- if (matchesAllowedPrefix(baseCommand)) {
1103
- logToFile(`Allowing bash command with output limiter: ${command}`);
1104
- debug(`Allowing bash command with output limiter: ${command}`);
1105
- return {
1106
- behavior: "allow",
1107
- updatedInput: input
1108
- };
1109
- }
1110
- }
1111
- if (/[|&]/.test(normalized)) {
1112
- logToFile(`Denying bash command with pipe/&: ${command}`);
1113
- debug(`Denying bash command with pipe/&: ${command}`);
1114
- analytics.wizardCapture("bash denied", {
1115
- reason: "disallowed pipe",
1116
- command
1117
- });
1118
- return {
1119
- behavior: "deny",
1120
- message: `Bash command not allowed. Pipes are only permitted with tail/head for output limiting.`
1121
- };
1122
- }
1123
- if (matchesAllowedPrefix(normalized)) {
2087
+ const decision = evaluateBashCommand(command);
2088
+ if (decision.allowed) {
1124
2089
  logToFile(`Allowing bash command: ${command}`);
1125
2090
  debug(`Allowing bash command: ${command}`);
1126
2091
  return {
@@ -1128,15 +2093,15 @@ function wizardCanUseTool(toolName, input, context = {}) {
1128
2093
  updatedInput: input
1129
2094
  };
1130
2095
  }
1131
- logToFile(`Denying bash command: ${command}`);
1132
- debug(`Denying bash command: ${command}`);
2096
+ logToFile(`Denying bash command (${decision.analyticsReason}): ${command}`);
2097
+ debug(`Denying bash command (${decision.analyticsReason}): ${command}`);
1133
2098
  analytics.wizardCapture("bash denied", {
1134
- reason: "not in allowlist",
2099
+ reason: decision.analyticsReason,
1135
2100
  command
1136
2101
  });
1137
2102
  return {
1138
2103
  behavior: "deny",
1139
- message: `Bash command not allowed. Only install, build, typecheck, lint, and formatting commands are permitted.`
2104
+ message: decision.message
1140
2105
  };
1141
2106
  }
1142
2107
  /**
@@ -1807,6 +2772,6 @@ function handleSDKMessage(message, options, spinner, signals, receivedSuccessRes
1807
2772
  }
1808
2773
  }
1809
2774
  //#endregion
1810
- export { wizardCanUseTool as a, classifySettingsConflicts as c, sanitizeAgentSubprocessEnv as d, AgentOutputSignals as f, createTriageLLMProvider as g, getWizardCommandments as h, runAgent as i, recoverOrphanedSettingsBackups as l, REMARK_INSTRUCTION as m, buildRunTags as n, backupAndFixClaudeSettings as o, AgentSignals as p, initializeAgent as r, checkAllSettingsConflicts as s, buildAgentEnv as t, restoreClaudeSettings as u };
2775
+ export { isFullyCancelled as A, mergeEnvValues as C, vaultSensitiveAnswers as D, resolveEnvSecretRefs as E, fetchWithRetry as O, installSkillById as S, resolveEnvPath as T, ENV_FILE_PATH_DESCRIPTION as _, wizardCanUseTool as a, evaluateAskCap as b, classifySettingsConflicts as c, sanitizeAgentSubprocessEnv as d, AgentOutputSignals as f, createSecretVault as g, createTriageLLMProvider as h, runAgent as i, createWizardAskBridge as k, recoverOrphanedSettingsBackups as l, REMARK_INSTRUCTION as m, buildRunTags as n, backupAndFixClaudeSettings as o, AgentSignals as p, initializeAgent as r, checkAllSettingsConflicts as s, buildAgentEnv as t, restoreClaudeSettings as u, WIZARD_TOOL_NAMES as v, parseEnvKeys as w, fetchSkillMenu as x, downloadSkill as y };
1811
2776
 
1812
- //# sourceMappingURL=agent-interface-BvTxWiol.js.map
2777
+ //# sourceMappingURL=agent-interface-B8roB2UP.js.map