@bridge_gpt/mcp-server 0.2.65 → 0.2.67

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 (132) hide show
  1. package/README.md +269 -82
  2. package/build/agent-capabilities/probes.js +15 -5
  3. package/build/agent-launchers/claude-executor-adapter.js +55 -34
  4. package/build/agent-launchers/executor-adapter.js +34 -4
  5. package/build/agents.generated.js +1 -1
  6. package/build/bounded-wait.js +67 -10
  7. package/build/bridge-config.js +104 -0
  8. package/build/codex-bridge-profile.js +10 -1
  9. package/build/commands.generated.js +12 -12
  10. package/build/conductor/bridge-api-client.js +372 -10
  11. package/build/conductor/bring-up-facts.js +1 -1
  12. package/build/conductor/doctor.js +3 -3
  13. package/build/conductor/epic-child-continuation.js +61 -34
  14. package/build/conductor/epic-implementer-cli.js +34 -6
  15. package/build/conductor/errors.js +4 -3
  16. package/build/conductor/guided-repair.js +327 -0
  17. package/build/conductor/hook-runtime-qualification.js +1 -1
  18. package/build/conductor/merge-execution-contract.js +27 -8
  19. package/build/conductor/merge-execution-eligibility.js +3 -2
  20. package/build/conductor/pr-ci-producer.js +1 -1
  21. package/build/conductor/readiness-cli.js +46 -9
  22. package/build/conductor/readiness-sections.js +41 -6
  23. package/build/conductor/readiness.js +204 -30
  24. package/build/conductor/recovery-cli.js +703 -20
  25. package/build/conductor/recovery-operations.js +392 -3
  26. package/build/conductor/repair-contract.js +112 -3
  27. package/build/conductor/repository-workflow-remedy.js +125 -0
  28. package/build/conductor/tools.js +2 -2
  29. package/build/conductor-bundle-artifacts.js +8 -0
  30. package/build/credential-store.js +5 -4
  31. package/build/diagnostic-text.js +36 -0
  32. package/build/docs.generated.js +8 -3
  33. package/build/doctor.js +70 -10
  34. package/build/drive-epic-key.js +6 -5
  35. package/build/drive-epic.js +479 -131
  36. package/build/epic-coherence-input.js +2 -1
  37. package/build/epic-implementer-bin.js +30 -26
  38. package/build/epic-implementer-bundle-cli.js +2 -2
  39. package/build/epic-launch-diagnostic.js +31 -0
  40. package/build/epic-launch-profile.js +44 -14
  41. package/build/executor/cli.js +3 -2
  42. package/build/executor/completion-contract.js +48 -5
  43. package/build/executor/completion-supervision.js +9 -0
  44. package/build/executor/correction-dispatch.js +60 -0
  45. package/build/executor/env.js +25 -0
  46. package/build/executor/heartbeat.js +1 -1
  47. package/build/executor/http-client.js +35 -0
  48. package/build/executor/integration-job.js +6 -1
  49. package/build/executor/job-errors.js +38 -4
  50. package/build/executor/job-log-registry.js +340 -73
  51. package/build/executor/job-runner.js +485 -74
  52. package/build/executor/job-types.js +8 -13
  53. package/build/executor/observation.js +6 -1
  54. package/build/executor/preflight.js +136 -10
  55. package/build/executor/prompt-spec.js +113 -9
  56. package/build/executor/required-work-ledger.js +6 -1
  57. package/build/executor/resume-pre-spawn.js +2 -0
  58. package/build/executor/runner.js +31 -2
  59. package/build/executor/service-unit.js +7 -1
  60. package/build/executor/stale-artifacts.js +5 -4
  61. package/build/executor/types.js +1 -0
  62. package/build/executor/watch-cli.js +35 -8
  63. package/build/executor/worker-command.js +43 -43
  64. package/build/executor/worker-finalization.js +161 -43
  65. package/build/executor/worker-guard-hook.js +152 -12
  66. package/build/executor/worker-log.js +137 -70
  67. package/build/executor/worktree-gc.js +30 -6
  68. package/build/executor/worktree.js +82 -1
  69. package/build/index.js +482 -660
  70. package/build/init.js +107 -4
  71. package/build/install-bridge-conductor.js +8 -6
  72. package/build/install-bridge.js +287 -23
  73. package/build/install-doctor.js +172 -30
  74. package/build/install-reexec.js +69 -7
  75. package/build/macos-terminal-permission-guidance.js +16 -0
  76. package/build/model-thresholds-tool.js +185 -0
  77. package/build/phase-context-git-probe.js +68 -0
  78. package/build/phase-result-artifacts.js +173 -5
  79. package/build/pipelines.generated.js +19 -25
  80. package/build/plane/alembic-head.js +12 -3
  81. package/build/plane/base-worktree-guard.js +147 -0
  82. package/build/plane/cli.js +217 -17
  83. package/build/plane/code-snapshot.js +468 -0
  84. package/build/plane/defaults.js +48 -3
  85. package/build/plane/endpoint.js +41 -0
  86. package/build/plane/manifest.js +120 -1
  87. package/build/plane/member-roster.js +94 -19
  88. package/build/plane/preflight.js +30 -2
  89. package/build/plane/process-cwd.js +67 -0
  90. package/build/plane/restart.js +542 -0
  91. package/build/plane/shutdown.js +31 -0
  92. package/build/plane/status.js +145 -6
  93. package/build/plane/supervisor.js +588 -177
  94. package/build/plane/types.js +54 -2
  95. package/build/plane/worker-exit-code.js +2 -0
  96. package/build/printed-remedy-commands.js +37 -0
  97. package/build/project-root.js +99 -0
  98. package/build/readiness-check.js +1 -1
  99. package/build/readme.generated.js +1 -1
  100. package/build/review-generation.js +22 -0
  101. package/build/review-tickets.js +156 -13
  102. package/build/setup-epic.js +224 -127
  103. package/build/sfcc/config.js +9 -0
  104. package/build/sfcc/credentials.js +100 -38
  105. package/build/sfcc/hostname.js +67 -0
  106. package/build/sfcc/permissions.js +17 -13
  107. package/build/sfcc/register.js +1 -1
  108. package/build/sfcc/setup-status.js +2 -0
  109. package/build/sfcc/tool-wrapper.js +41 -43
  110. package/build/sfcc/tools-configured.js +96 -0
  111. package/build/sfcc/write-grants.js +17 -11
  112. package/build/sfcc/write-result.js +4 -3
  113. package/build/sfcc/writes-site-preference.js +20 -8
  114. package/build/start-tickets.js +332 -79
  115. package/build/tool-error-envelope.js +61 -0
  116. package/build/upgrade-cli.js +109 -24
  117. package/build/version.generated.js +3 -3
  118. package/build/visual-diff.js +22 -1
  119. package/build/windows-npm-entry.js +103 -0
  120. package/build/worker-guard-hook-bin.js +3 -5
  121. package/build/worker-guard-hook-main.js +47 -0
  122. package/build/worktree-core.js +136 -9
  123. package/docs/EPIC-IMPLEMENTER-INSTALL.md +849 -0
  124. package/docs/EPIC-IMPLEMENTER.md +662 -0
  125. package/docs/install/mcp-tool-integrations.md +25 -15
  126. package/docs/install/sfcc-integration.md +21 -8
  127. package/package.json +1 -1
  128. package/pipelines/implement-ticket.json +1 -1
  129. package/pipelines/plan-epic.json +0 -5
  130. package/pipelines/review-ticket.json +1 -1
  131. package/docs/CONDUCTOR.md +0 -605
  132. package/pipelines/idea-to-pr.json +0 -49
@@ -7,6 +7,10 @@
7
7
  * "this operation has no local-backend equivalent" refusal, which the ticket
8
8
  * routes return as HTTP 409 with `detail.error = "UNSUPPORTED_IN_LOCAL_MODE"`.
9
9
  *
10
+ * BAPI-1382 adds a second trusted pair: the plan-entitlement denial, which the
11
+ * server returns as HTTP 403 with `detail.error = "ENTITLEMENT_REQUIRED"`. It
12
+ * tells a plan limit apart from a bad credential, which is also a 403.
13
+ *
10
14
  * Recognition is by the exact `(error, status)` PAIR — never by message text.
11
15
  * Message text is prose that gets reworded; matching on it would make a copy
12
16
  * edit silently change orchestration control flow.
@@ -18,6 +22,54 @@
18
22
  export const UNSUPPORTED_IN_LOCAL_MODE_ERROR = "UNSUPPORTED_IN_LOCAL_MODE";
19
23
  /** The HTTP status that marker is only ever valid with. */
20
24
  export const UNSUPPORTED_IN_LOCAL_MODE_STATUS = 409;
25
+ /** The plan-entitlement marker (BAPI-1382): the account's plan lacks a feature. */
26
+ export const ENTITLEMENT_REQUIRED_ERROR = "ENTITLEMENT_REQUIRED";
27
+ /** The HTTP status that marker is only ever valid with. */
28
+ export const ENTITLEMENT_REQUIRED_STATUS = 403;
29
+ /**
30
+ * The request-time too-little-ticket-detail refusal (BAPI-1382): the generate
31
+ * routes answer HTTP 422 with `detail.error = "INSUFFICIENT_TICKET_DETAIL"`
32
+ * plus the measured length, threshold, and description source.
33
+ */
34
+ export const INSUFFICIENT_TICKET_DETAIL_ERROR = "INSUFFICIENT_TICKET_DETAIL";
35
+ /** The HTTP status that marker is only ever valid with. */
36
+ export const INSUFFICIENT_TICKET_DETAIL_STATUS = 422;
37
+ /**
38
+ * The server `(error, status)` pairs the MCP relay keeps as received. Every
39
+ * other server-provided `error` is replaced by the relay's own status-derived
40
+ * code, so an arbitrary detail object can never forge a code.
41
+ */
42
+ export const TRUSTED_UPSTREAM_ERROR_PAIRS = [
43
+ [UNSUPPORTED_IN_LOCAL_MODE_ERROR, UNSUPPORTED_IN_LOCAL_MODE_STATUS],
44
+ [ENTITLEMENT_REQUIRED_ERROR, ENTITLEMENT_REQUIRED_STATUS],
45
+ [INSUFFICIENT_TICKET_DETAIL_ERROR, INSUFFICIENT_TICKET_DETAIL_STATUS],
46
+ ];
47
+ /** True only for an exact trusted `(error, status)` pair. */
48
+ export function isTrustedUpstreamErrorPair(error, status) {
49
+ return TRUSTED_UPSTREAM_ERROR_PAIRS.some(([code, code_status]) => error === code && status === code_status);
50
+ }
51
+ /**
52
+ * Add the typed `operation` to a serialized error envelope (BAPI-1382). A text
53
+ * that is not an envelope is wrapped into one with the given status, so a
54
+ * refused request is always parseable and never a prose-prefixed string.
55
+ */
56
+ export function withRequestOperation(envelopeText, operation, status) {
57
+ let parsed = null;
58
+ try {
59
+ parsed = JSON.parse(envelopeText);
60
+ }
61
+ catch {
62
+ parsed = null;
63
+ }
64
+ if (parsed !== null &&
65
+ typeof parsed === "object" &&
66
+ !Array.isArray(parsed) &&
67
+ typeof parsed.error === "string" &&
68
+ typeof parsed.status === "number") {
69
+ return JSON.stringify({ ...parsed, operation });
70
+ }
71
+ return JSON.stringify({ error: "UNKNOWN_ERROR", status, message: envelopeText, operation });
72
+ }
21
73
  /**
22
74
  * Parse `text` as a tool-error envelope, or return `null`.
23
75
  *
@@ -52,8 +104,17 @@ export function parseToolErrorEnvelope(text) {
52
104
  if (Array.isArray(obj.unsupported_fields)) {
53
105
  envelope.unsupported_fields = obj.unsupported_fields.filter((v) => typeof v === "string");
54
106
  }
107
+ if (typeof obj.operation === "string") {
108
+ envelope.operation = obj.operation;
109
+ }
55
110
  return envelope;
56
111
  }
112
+ /** True for the exact plan-entitlement contract: the marker AND HTTP 403. */
113
+ export function isEntitlementRequiredEnvelope(envelope) {
114
+ return (envelope !== null &&
115
+ envelope.error === ENTITLEMENT_REQUIRED_ERROR &&
116
+ envelope.status === ENTITLEMENT_REQUIRED_STATUS);
117
+ }
57
118
  /** True for the exact unsupported-local contract: the marker AND HTTP 409. */
58
119
  export function isUnsupportedInLocalModeEnvelope(envelope) {
59
120
  return (envelope !== null &&
@@ -42,26 +42,85 @@ import { fetchLatestVersion } from "./cli-release.js";
42
42
  import { CANONICAL_UPGRADE_COMMAND } from "./upgrade-advice.js";
43
43
  import { describeLauncherReason, inspectLauncherConfigs, isApplicableStandardLauncher, isPrerequisiteFailure, LAUNCHER_CONFIG_TARGETS, } from "./launcher-config-inspection.js";
44
44
  import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
45
+ import { defaultNpmEntryFileExists, resolveNpmEntry } from "./windows-npm-entry.js";
45
46
  /** The sentinel this CLI passes to the child it re-execs from `@latest`. */
46
47
  const INTERNAL_REEXEC_FLAG = "--internal-reexec";
47
- // ---------------------------------------------------------------------------
48
- // Default dependency construction
49
- // ---------------------------------------------------------------------------
50
- function defaultReexec(cwd, oldVersion) {
51
- const npxCmd = process.platform === "win32" ? "npx.cmd" : "npx";
48
+ /**
49
+ * The fixed, secret-free message printed when, on Windows, the latest CLI could
50
+ * not be STARTED. Upgrade stops here rather than continuing with the local copy.
51
+ */
52
+ export const UPGRADE_REEXEC_START_FAILURE_MESSAGE = "Bridge API upgrade failed: could not start the latest version of the CLI. " +
53
+ `Nothing was changed. Run ${CANONICAL_UPGRADE_COMMAND} to run the latest copy directly.`;
54
+ export function defaultReexec(cwd, oldVersion, options = {}) {
55
+ const platform = options.platform ?? process.platform;
56
+ const doSpawn = options.spawn ?? spawn;
57
+ const errorLog = options.errorLog ?? ((message) => console.error(message));
58
+ const reexecArgs = [
59
+ "-y",
60
+ `${MCP_PACKAGE_NAME}@latest`,
61
+ "upgrade",
62
+ INTERNAL_REEXEC_FLAG,
63
+ "--old-version",
64
+ oldVersion,
65
+ ];
66
+ if (platform !== "win32") {
67
+ return new Promise((resolve) => {
68
+ const child = doSpawn("npx", reexecArgs, { stdio: "inherit", cwd });
69
+ child.on("close", (code) => resolve(code ?? 0));
70
+ child.on("error", (err) => {
71
+ errorLog(`Bridge API upgrade failed: could not re-exec npx: ${err.message}`);
72
+ resolve(1);
73
+ });
74
+ });
75
+ }
76
+ // WINDOWS (BAPI-1319): run npm's `npx-cli.js` through Node, never `npx.cmd`
77
+ // (EINVAL without a shell since CVE-2024-27980). A launch that fails before the
78
+ // child starts STOPS the upgrade with a fixed message — it never continues with
79
+ // the local copy, and a sync throw can no longer escape as an unhandled rejection.
80
+ const resolved = resolveNpmEntry("npx", reexecArgs, {
81
+ platform,
82
+ env: options.env ?? process.env,
83
+ execPath: options.execPath ?? process.execPath,
84
+ fileExists: options.fileExists ?? defaultNpmEntryFileExists,
85
+ });
86
+ if (resolved.status !== "resolved") {
87
+ errorLog(UPGRADE_REEXEC_START_FAILURE_MESSAGE);
88
+ return Promise.resolve(1);
89
+ }
52
90
  return new Promise((resolve) => {
53
- const child = spawn(npxCmd, [
54
- "-y",
55
- `${MCP_PACKAGE_NAME}@latest`,
56
- "upgrade",
57
- INTERNAL_REEXEC_FLAG,
58
- "--old-version",
59
- oldVersion,
60
- ], { stdio: "inherit", cwd });
61
- child.on("close", (code) => resolve(code ?? 0));
91
+ let settled = false;
92
+ let started = false;
93
+ const settle = (code) => {
94
+ if (settled)
95
+ return;
96
+ settled = true;
97
+ resolve(code);
98
+ };
99
+ const failToStart = () => {
100
+ if (settled)
101
+ return;
102
+ errorLog(UPGRADE_REEXEC_START_FAILURE_MESSAGE);
103
+ settle(1);
104
+ };
105
+ let child;
106
+ try {
107
+ child = doSpawn(resolved.command, resolved.args, { shell: false, stdio: "inherit", cwd });
108
+ }
109
+ catch {
110
+ failToStart();
111
+ return;
112
+ }
113
+ child.on("spawn", () => {
114
+ started = true;
115
+ });
116
+ child.on("close", (code) => settle(code ?? 0));
62
117
  child.on("error", (err) => {
63
- console.error(`Bridge API upgrade failed: could not re-exec npx: ${err.message}`);
64
- resolve(1);
118
+ if (!started) {
119
+ failToStart();
120
+ return;
121
+ }
122
+ errorLog(`Bridge API upgrade failed: could not re-exec npx: ${err.message}`);
123
+ settle(1);
65
124
  });
66
125
  });
67
126
  }
@@ -77,13 +136,39 @@ async function defaultHasCompetingLocalInstall(cwd) {
77
136
  return false;
78
137
  }
79
138
  }
80
- function defaultRemoveCompetingLocalInstall(cwd) {
81
- const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
82
- return new Promise((resolve) => {
83
- const child = spawn(npmCmd, ["uninstall", MCP_PACKAGE_NAME], {
84
- stdio: "inherit",
85
- cwd,
139
+ export function defaultRemoveCompetingLocalInstall(cwd, options = {}) {
140
+ const platform = options.platform ?? process.platform;
141
+ const doSpawn = options.spawn ?? spawn;
142
+ const removeArgs = ["uninstall", MCP_PACKAGE_NAME];
143
+ if (platform !== "win32") {
144
+ return new Promise((resolve) => {
145
+ const child = doSpawn("npm", removeArgs, {
146
+ stdio: "inherit",
147
+ cwd,
148
+ });
149
+ child.on("close", (code) => resolve(code === 0));
150
+ child.on("error", () => resolve(false));
86
151
  });
152
+ }
153
+ // WINDOWS (BAPI-1319): `npm-cli.js` through Node. Every launch failure resolves
154
+ // false, which reaches the caller's existing "could not remove" message.
155
+ const resolved = resolveNpmEntry("npm", removeArgs, {
156
+ platform,
157
+ env: options.env ?? process.env,
158
+ execPath: options.execPath ?? process.execPath,
159
+ fileExists: options.fileExists ?? defaultNpmEntryFileExists,
160
+ });
161
+ if (resolved.status !== "resolved")
162
+ return Promise.resolve(false);
163
+ return new Promise((resolve) => {
164
+ let child;
165
+ try {
166
+ child = doSpawn(resolved.command, resolved.args, { shell: false, stdio: "inherit", cwd });
167
+ }
168
+ catch {
169
+ resolve(false);
170
+ return;
171
+ }
87
172
  child.on("close", (code) => resolve(code === 0));
88
173
  child.on("error", () => resolve(false));
89
174
  });
@@ -95,9 +180,9 @@ export function createDefaultUpgradeCliDeps(cwd) {
95
180
  fetchLatestVersion,
96
181
  readFile: (filePath) => readFile(filePath, "utf-8"),
97
182
  hasCompetingLocalInstall: defaultHasCompetingLocalInstall,
98
- removeCompetingLocalInstall: defaultRemoveCompetingLocalInstall,
183
+ removeCompetingLocalInstall: (installCwd) => defaultRemoveCompetingLocalInstall(installCwd),
99
184
  runInit,
100
- reexecFromLatest: defaultReexec,
185
+ reexecFromLatest: (reexecCwd, oldVersion) => defaultReexec(reexecCwd, oldVersion),
101
186
  stdout: (m) => console.log(m),
102
187
  stderr: (m) => console.error(m),
103
188
  env: process.env,
@@ -1,4 +1,4 @@
1
1
  // AUTO-GENERATED — do not edit manually. Regenerate with: npm run build
2
- export const VERSION = "0.2.65";
3
- export const BUILD_COMMIT = "a6ee899f43d8";
4
- export const LAUNCHER_ARGS = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.65", "serve"];
2
+ export const VERSION = "0.2.67";
3
+ export const BUILD_COMMIT = "8eb6ba563e5f";
4
+ export const LAUNCHER_ARGS = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.67", "serve"];
@@ -18,6 +18,7 @@
18
18
  * mismatch budget (never 0%, which AA/font rendering makes unreachable).
19
19
  */
20
20
  import path from "path";
21
+ import { createRequire } from "module";
21
22
  import { Worker } from "worker_threads";
22
23
  import { computePngVisualDiff, DEFAULT_PASS_MISMATCH_PCT, PIXELMATCH_COLOR_THRESHOLD, } from "./visual-diff-worker.js";
23
24
  // ---------------------------------------------------------------------------
@@ -247,6 +248,26 @@ export async function loadPlaywright() {
247
248
  return { ok: false };
248
249
  }
249
250
  }
251
+ /**
252
+ * The Chromium install command matching the bundled `playwright-core`.
253
+ *
254
+ * A bare `npx playwright install chromium` fetches whatever Playwright is
255
+ * newest, whose browser build may not match the one this server drives.
256
+ * Resolution is best-effort: any failure returns the historic unversioned
257
+ * command rather than throwing, since this only builds a hint.
258
+ */
259
+ export function chromiumInstallCommand(readMetadata = () => createRequire(import.meta.url)("playwright-core/package.json")) {
260
+ try {
261
+ const version = readMetadata()?.version;
262
+ if (typeof version === "string" && version.length > 0) {
263
+ return `npx playwright@${version} install chromium`;
264
+ }
265
+ }
266
+ catch {
267
+ // Not resolvable: fall through to the unversioned command.
268
+ }
269
+ return "npx playwright install chromium";
270
+ }
250
271
  export async function launchBrowser(playwright) {
251
272
  try {
252
273
  const browser = await playwright.chromium.launch({ headless: true });
@@ -258,7 +279,7 @@ export async function launchBrowser(playwright) {
258
279
  ok: false,
259
280
  error: "BROWSER_UNAVAILABLE",
260
281
  status: 503,
261
- message: `Chromium could not be launched: ${err instanceof Error ? err.message : "unknown error"}. Run "npx playwright install chromium".`,
282
+ message: `Chromium could not be launched: ${err instanceof Error ? err.message : "unknown error"}. Run "${chromiumInstallCommand()}".`,
262
283
  };
263
284
  }
264
285
  }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Windows `npx` / `npm` entry resolution (BAPI-1319).
3
+ *
4
+ * WHY THIS EXISTS. Since Node's April 2024 security releases (CVE-2024-27980),
5
+ * `child_process.spawn` throws `EINVAL` when handed a `.cmd` or `.bat` file
6
+ * without the `shell` option, and a bare `"npx"` is `ENOENT` on Windows because
7
+ * the real launcher is `npx.cmd`. `shell: true` is not an option: the install
8
+ * re-exec argv carries user input. So on `win32` this module skips the shim and
9
+ * runs npm's own JavaScript entry point (`npx-cli.js` / `npm-cli.js`) through
10
+ * the running Node executable, with no shell at all.
11
+ *
12
+ * LOOKUP ORDER MIRRORS THE SHELL. The first `npx.cmd` (or `npm.cmd`) on PATH
13
+ * wins, and its sibling `node_modules/npm/bin/<cli>.js` is the entry point. If
14
+ * that first shim has no sibling CLI the result is `unresolved` — a later PATH
15
+ * candidate is never tried, because that is not the npx the user's shell would
16
+ * run, and the test harnesses that put a fake `npx.cmd` first on PATH must still
17
+ * catch the call. Only when no shim is on PATH at all does the resolver fall
18
+ * back to the npm bundled beside `execPath`.
19
+ *
20
+ * On every platform other than `win32` the input is returned unchanged, so
21
+ * macOS and Linux spawn exactly what they spawned before this module existed.
22
+ *
23
+ * The resolver never throws and never carries exception text: every failure is
24
+ * the typed `unresolved` result.
25
+ */
26
+ import { statSync } from "fs";
27
+ import path from "path";
28
+ const DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
29
+ const CLI_FILE = {
30
+ npx: "npx-cli.js",
31
+ npm: "npm-cli.js",
32
+ };
33
+ /** Production `fileExists`: a regular file, never a throw. */
34
+ export function defaultNpmEntryFileExists(filePath) {
35
+ try {
36
+ return statSync(filePath).isFile();
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ }
42
+ /** Case-insensitive env read, as Windows environments are. */
43
+ function _readEnvCaseInsensitive(env, name) {
44
+ const target = name.toUpperCase();
45
+ for (const key of Object.keys(env)) {
46
+ if (key.toUpperCase() === target) {
47
+ const value = env[key];
48
+ if (value === undefined)
49
+ continue;
50
+ if (typeof value !== "string")
51
+ throw new TypeError("malformed environment value");
52
+ return value;
53
+ }
54
+ }
55
+ return undefined;
56
+ }
57
+ /** Find the first shim on PATH, or null when none exists. */
58
+ function _findFirstShim(command, env, fileExists) {
59
+ const rawPath = _readEnvCaseInsensitive(env, "PATH") ?? "";
60
+ const rawPathExt = _readEnvCaseInsensitive(env, "PATHEXT");
61
+ const pathExt = rawPathExt && rawPathExt.trim().length > 0 ? rawPathExt : DEFAULT_PATHEXT;
62
+ // Only the `.cmd` shim is the launcher npm installs beside its CLI. PATHEXT is
63
+ // matched case-insensitively and its own spelling is used for the lookup.
64
+ const cmdExt = pathExt
65
+ .split(";")
66
+ .map((ext) => ext.trim())
67
+ .find((ext) => ext.toLowerCase() === ".cmd");
68
+ if (!cmdExt)
69
+ return null;
70
+ for (const segment of rawPath.split(";")) {
71
+ const dir = segment.trim();
72
+ if (dir.length === 0)
73
+ continue;
74
+ const candidate = path.win32.join(dir, `${command}${cmdExt}`);
75
+ if (fileExists(candidate))
76
+ return candidate;
77
+ }
78
+ return null;
79
+ }
80
+ /**
81
+ * Map `npx`/`npm` plus args to a spawnable `{ command, args }`.
82
+ *
83
+ * Non-`win32`: `{ command, args }` unchanged. `win32`: `{ command: execPath,
84
+ * args: [<cliPath>, ...args] }`, or `unresolved`.
85
+ */
86
+ export function resolveNpmEntry(command, args, options) {
87
+ if (options.platform !== "win32") {
88
+ return { status: "resolved", command, args };
89
+ }
90
+ try {
91
+ const cliFile = CLI_FILE[command];
92
+ const shim = _findFirstShim(command, options.env, options.fileExists);
93
+ const cliPath = shim
94
+ ? path.win32.join(path.win32.dirname(shim), "node_modules", "npm", "bin", cliFile)
95
+ : path.win32.join(path.win32.dirname(options.execPath), "node_modules", "npm", "bin", cliFile);
96
+ if (!options.fileExists(cliPath))
97
+ return { status: "unresolved" };
98
+ return { status: "resolved", command: options.execPath, args: [cliPath, ...args] };
99
+ }
100
+ catch {
101
+ return { status: "unresolved" };
102
+ }
103
+ }
@@ -1,6 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import{readFileSync}from"node:fs";import path from"node:path";var WORKER_GUARD_REASON_PREFIX="bridge worker guard",WORKER_GUARD_MALFORMED_LINE=`${WORKER_GUARD_REASON_PREFIX}: payload could not be read; continuing fail-open`;function renderWorkerGuardReason(category,family){return category==="git-ambiguous"?`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). This shell form cannot be safely resolved: substitution, redirection, or stdin-fed arguments hide what git will run. Use literal arguments instead; for a commit message, write it to a file and run git commit -F <file>.`:`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). This command family is restricted to the worker's own branch, worktree, and read-only network access. Use a different approach and continue.`}var UNSAFE_UNQUOTED=new Set(["$","`",">","<"]),SEPARATORS=new Set([";","|","&"]);function tokenizeShellCommand(command){let commands=[],argv=[],current="",hasCurrent=!1,unsafe=!1,endToken=()=>{hasCurrent&&(argv.push(current),current="",hasCurrent=!1)},endCommand=()=>{endToken(),(argv.length>0||unsafe)&&commands.push({argv,unsafe}),argv=[],unsafe=!1};for(let i=0;i<command.length;i+=1){let ch=command[i];if(ch==="\\"){let next=command[i+1];next!==void 0&&(current+=next,hasCurrent=!0,i+=1);continue}if(ch==="'"){let close=command.indexOf("'",i+1);if(close===-1){unsafe=!0,current+=command.slice(i+1),hasCurrent=!0;break}current+=command.slice(i+1,close),hasCurrent=!0,i=close;continue}if(ch==='"'){let j=i+1,body="",closed=!1;for(;j<command.length;){let c=command[j];if(c==="\\"&&j+1<command.length){body+=command[j+1],j+=2;continue}if(c==='"'){closed=!0;break}(c==="$"||c==="`")&&(unsafe=!0),body+=c,j+=1}closed||(unsafe=!0),current+=body,hasCurrent=!0,i=j;continue}if(UNSAFE_UNQUOTED.has(ch)){unsafe=!0,current+=ch,hasCurrent=!0;continue}if(SEPARATORS.has(ch)){endCommand();continue}if(ch===`
3
- `){endCommand();continue}if(ch===" "||ch===" "||ch==="\r"){endToken();continue}current+=ch,hasCurrent=!0}return endCommand(),commands}var WORKER_GUARD_MAX_PAYLOAD_BYTES=1e6;function asRecord(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)?value:void 0}function isInside(root,candidate){let rel=path.relative(root,candidate);return rel===""?!0:rel.startsWith("..")?!1:!path.isAbsolute(rel)}function resolveEffectiveCwd(ctx,payloadCwd){let root=ctx.worktreePath;if(typeof root!="string"||!path.isAbsolute(root))return;let normalizedRoot=path.resolve(root);if(payloadCwd===void 0)return normalizedRoot;if(typeof payloadCwd!="string"||payloadCwd.length===0)return;let resolved=path.isAbsolute(payloadCwd)?path.resolve(payloadCwd):path.resolve(normalizedRoot,payloadCwd);return isInside(normalizedRoot,resolved)?resolved:void 0}function allTargetsContained(ctx,cwd,targets){let root=ctx.worktreePath;if(typeof root!="string"||!path.isAbsolute(root)||cwd===void 0)return!1;let normalizedRoot=path.resolve(root);return targets.every(target=>{if(target.length===0||target==="~"||target.startsWith("~/"))return!1;let resolved=path.isAbsolute(target)?path.resolve(target):path.resolve(cwd,target);return isInside(normalizedRoot,resolved)})}function parseGitInvocation(argv,baseCwd){let cwd=baseCwd,unsafe=!1,i=1;for(;i<argv.length;){let token=argv[i];if(token==="-C"){let dir=argv[i+1];dir===void 0||cwd===void 0?(unsafe=!0,cwd=void 0):cwd=path.isAbsolute(dir)?path.resolve(dir):path.resolve(cwd,dir),i+=2;continue}if(token==="-c"||token==="--namespace"||token==="--git-dir"||token==="--work-tree"){unsafe=!0,i+=2;continue}if(token.startsWith("-")){i+=1;continue}break}return{subcommand:argv[i]??"",args:argv.slice(i+1),cwd,unsafe}}function isOwnBranchDestination(destination,workerBranch){let colon=destination.indexOf(":"),dst=colon===-1?destination:destination.slice(colon+1);if(dst.length===0)return!1;let bare=dst.startsWith("+")?dst.slice(1):dst;return bare===workerBranch||bare===`refs/heads/${workerBranch}`}function evaluateGitPush(args,ctx){let deny={kind:"deny",category:"git-push",family:"git"},positional=[];for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="--"){positional.push(...args.slice(i+1));break}if(token.startsWith("-")){if(token==="--tags"||token==="--delete"||token==="-d"||token==="--mirror"||token==="--all"||token==="--follow-tags"||token.startsWith("--repo"))return deny;(token==="-o"||token==="--push-option"||token==="--receive-pack"||token==="--exec")&&(i+=1);continue}positional.push(token)}let workerBranch=ctx.workerBranch;return typeof workerBranch!="string"||workerBranch.length===0?deny:positional.length<=1?{kind:"allow"}:positional.slice(1).every(d=>isOwnBranchDestination(d,workerBranch))?{kind:"allow"}:deny}function evaluateGitReset(args,ctx){let deny={kind:"deny",category:"git-reset-hard",family:"git"};if(!args.includes("--hard"))return{kind:"allow"};let positional=[];for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="--"){positional.push(...args.slice(i+1));break}token.startsWith("-")||positional.push(token)}if(positional.length===0)return{kind:"allow"};let workerBranch=ctx.workerBranch;if(typeof workerBranch!="string"||workerBranch.length===0)return deny;let allowed=new Set([workerBranch,`origin/${workerBranch}`,`refs/heads/${workerBranch}`,`refs/remotes/origin/${workerBranch}`]);return positional.every(ref=>allowed.has(ref))?{kind:"allow"}:deny}function evaluateGitClean(args,ctx,cwd){let deny={kind:"deny",category:"filesystem-clean",family:"git"};if(cwd===void 0||!allTargetsContained(ctx,cwd,[cwd]))return deny;let pathspecs=[];for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="--"){pathspecs.push(...args.slice(i+1));break}if(token==="-e"||token==="--exclude"){i+=1;continue}token.startsWith("-")||pathspecs.push(token)}return pathspecs.length===0?{kind:"allow"}:allTargetsContained(ctx,cwd,pathspecs)?{kind:"allow"}:deny}function isRecursiveForcedRemove(args){let recursive=!1,force=!1;for(let token of args){if(token==="--")break;if(token==="--recursive")recursive=!0;else if(token==="--force")force=!0;else{if(token.startsWith("--"))continue;if(token.startsWith("-")&&token.length>1)for(let flag of token.slice(1))(flag==="r"||flag==="R")&&(recursive=!0),flag==="f"&&(force=!0)}}return recursive&&force}function evaluateRemove(argv,ctx,cwd){let deny={kind:"deny",category:"filesystem-remove",family:"rm"},args=argv.slice(1);if(!isRecursiveForcedRemove(args))return{kind:"allow"};let targets=[],sawDoubleDash=!1;for(let token of args){if(!sawDoubleDash&&token==="--"){sawDoubleDash=!0;continue}!sawDoubleDash&&token.startsWith("-")&&token.length>1||targets.push(token)}return targets.length===0?deny:allTargetsContained(ctx,cwd,targets)?{kind:"allow"}:deny}function isDatabaseCommand(executable){let base=path.basename(executable);return base==="psql"||base.startsWith("pg_")}var DESTRUCTIVE_SQL=/\b(?:DROP|TRUNCATE)\b/i;function evaluateDatabase(argv){let deny={kind:"deny",category:"database-destructive",family:"psql"};return argv.slice(1).some(arg=>DESTRUCTIVE_SQL.test(arg))?deny:{kind:"allow"}}var MUTATING_METHODS=new Set(["POST","PUT","PATCH","DELETE"]);function evaluateNetwork(argv){let deny={kind:"deny",category:"network-mutating",family:"curl"},base=path.basename(argv[0]??""),args=argv.slice(1);for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="-X"||token==="--request"||token==="--method"){let value=args[i+1];if(value===void 0||MUTATING_METHODS.has(value.toUpperCase()))return deny;i+=1;continue}if(token.startsWith("-X")&&token.length>2){if(MUTATING_METHODS.has(token.slice(2).toUpperCase()))return deny;continue}if(token.startsWith("--request=")||token.startsWith("--method=")){let value=token.slice(token.indexOf("=")+1);if(MUTATING_METHODS.has(value.toUpperCase()))return deny;continue}if(token==="--data"||token.startsWith("--data=")||token.startsWith("--data-")||token==="--form"||token==="-F"||token==="--upload-file"||token==="-T"||token==="--post-data"||token.startsWith("--post-data=")||token==="--post-file"||token.startsWith("--post-file=")||token==="--body-data"||token.startsWith("--body-data=")||token==="--body-file"||token.startsWith("--body-file=")||base==="curl"&&token==="-d")return deny}return{kind:"allow"}}function classify(executable){let base=path.basename(executable);return base==="git"?"git":base==="rm"?"rm":isDatabaseCommand(base)?"database":base==="curl"||base==="wget"?"network":null}function unsafeGitCategory(argv){let{subcommand}=parseGitInvocation(argv,void 0);return subcommand==="push"?"git-push":subcommand==="reset"?"git-reset-hard":subcommand==="clean"?"filesystem-clean":"git-ambiguous"}function unsafeSegmentDecision(family,argv){switch(family){case"git":return{kind:"deny",category:unsafeGitCategory(argv),family:"git"};case"rm":return{kind:"deny",category:"filesystem-remove",family:"rm"};case"database":return{kind:"deny",category:"database-destructive",family:"psql"};case"network":return{kind:"deny",category:"network-mutating",family:"curl"}}}var TRANSPARENT_PREFIXES=new Set(["sudo","env","command","nohup","time","nice","exec"]),STDIN_ARGUMENT_PREFIXES=new Set(["xargs"]),SHELL_EXECUTABLES=new Set(["sh","bash","zsh","dash","ksh"]),EVAL_BUILTINS=new Set(["eval"]);function unwrapTransparentPrefixes(argv){let rest=argv;for(let depth=0;depth<8;depth+=1){let head=rest[0];if(head===void 0||!TRANSPARENT_PREFIXES.has(path.basename(head)))return rest;let i=1;for(;i<rest.length;){let token=rest[i];if(path.basename(head)==="env"&&/^[A-Za-z_][A-Za-z0-9_]*=/.test(token)){i+=1;continue}if(token.startsWith("-")){i+=1;continue}break}rest=rest.slice(i)}return rest}function stripStdinPrefixOptions(argv){let VALUE_OPTS=new Set(["-I","-i","-L","-n","-P","-s","-d","-E","-e","-a","--replace","--max-lines","--max-args","--max-procs","--max-chars","--delimiter","--eof","--arg-file"]),i=1;for(;i<argv.length;){let token=argv[i];if(token==="--"){i+=1;break}if(!token.startsWith("-"))break;if(token.includes("=")){i+=1;continue}i+=VALUE_OPTS.has(token)?2:1}return argv.slice(i)}function evaluateSimpleCommand(segment,ctx,payloadCwd,depth=0,stdinFed=!1){let argv=unwrapTransparentPrefixes(segment.argv),executable=argv[0];if(executable===void 0)return{kind:"allow"};if(SHELL_EXECUTABLES.has(path.basename(executable))&&depth<4){let cIndex=argv.findIndex(t=>t==="-c"),inner=cIndex===-1?void 0:argv[cIndex+1];if(inner!==void 0){for(let nested of tokenizeShellCommand(inner)){let decision=evaluateSimpleCommand(nested,ctx,payloadCwd,depth+1,stdinFed);if(decision.kind==="deny")return decision}return{kind:"allow"}}}if(EVAL_BUILTINS.has(path.basename(executable))&&depth<4){for(let nested of tokenizeShellCommand(argv.slice(1).join(" "))){let decision=evaluateSimpleCommand(nested,ctx,payloadCwd,depth+1,stdinFed);if(decision.kind==="deny")return decision}return{kind:"allow"}}if(STDIN_ARGUMENT_PREFIXES.has(path.basename(executable))&&depth<4){let rest=stripStdinPrefixOptions(argv);return rest.length===0?{kind:"allow"}:evaluateSimpleCommand({argv:rest,unsafe:segment.unsafe},ctx,payloadCwd,depth+1,!0)}segment={argv,unsafe:segment.unsafe};let family=classify(executable);if(family===null)return{kind:"allow"};if(stdinFed||segment.unsafe)return unsafeSegmentDecision(family,segment.argv);let cwd=resolveEffectiveCwd(ctx,payloadCwd);if(family==="rm")return evaluateRemove(segment.argv,ctx,cwd);if(family==="database")return evaluateDatabase(segment.argv);if(family==="network")return evaluateNetwork(segment.argv);let git=parseGitInvocation(segment.argv,cwd);return git.subcommand==="push"?git.unsafe?{kind:"deny",category:"git-push",family:"git"}:evaluateGitPush(git.args,ctx):git.subcommand==="reset"?git.unsafe?{kind:"deny",category:"git-reset-hard",family:"git"}:evaluateGitReset(git.args,ctx):git.subcommand==="clean"?git.unsafe?{kind:"deny",category:"filesystem-clean",family:"git"}:evaluateGitClean(git.args,ctx,git.cwd):{kind:"allow"}}function evaluateWorkerGuardPayload(payload,ctx){let root=asRecord(payload);if(root===void 0)return{kind:"malformed"};let toolName=root.tool_name??root.toolName;if(typeof toolName!="string")return{kind:"malformed"};if(toolName!=="Bash")return{kind:"allow"};let input=asRecord(root.tool_input??root.toolInput);if(input===void 0)return{kind:"malformed"};let command=input.command;if(typeof command!="string")return{kind:"malformed"};let rawCwd=root.cwd??input.cwd,payloadCwd=typeof rawCwd=="string"?rawCwd:void 0;for(let segment of tokenizeShellCommand(command)){let decision=evaluateSimpleCommand(segment,ctx,payloadCwd);if(decision.kind==="deny")return decision}return{kind:"allow"}}function evaluateWorkerGuardInput(raw,ctx){if(typeof raw!="string")return{kind:"malformed"};if(raw.length>WORKER_GUARD_MAX_PAYLOAD_BYTES)return{kind:"malformed"};if(raw.trim().length===0)return{kind:"malformed"};let parsed;try{parsed=JSON.parse(raw)}catch{return{kind:"malformed"}}try{return evaluateWorkerGuardPayload(parsed,ctx)}catch{return{kind:"malformed"}}}var WORKER_GUARD_ROOT_FLAG="--worktree-root";function resolveWorkerGuardContext(env,cwd,argv=[]){let ctx={},root;for(let i=0;i<argv.length;i+=1){if(argv[i]===WORKER_GUARD_ROOT_FLAG){root=argv[i+1];break}if(argv[i].startsWith(`${WORKER_GUARD_ROOT_FLAG}=`)){root=argv[i].slice(WORKER_GUARD_ROOT_FLAG.length+1);break}}root===void 0&&typeof cwd=="string"&&(root=cwd),typeof root=="string"&&path.isAbsolute(root)&&(ctx.worktreePath=path.resolve(root));let branch=env.BAPI_WORKER_BRANCH;return typeof branch=="string"&&branch.trim().length>0&&(ctx.workerBranch=branch.trim()),ctx}function readStdinSync(){return readFileSync(0,"utf-8")}var exitCode=0;try{let raw=readStdinSync(),ctx=resolveWorkerGuardContext(process.env,process.cwd(),process.argv.slice(2)),decision=evaluateWorkerGuardInput(raw,ctx);decision.kind==="deny"?(process.stderr.write(`${renderWorkerGuardReason(decision.category,decision.family)}
4
- `),exitCode=2):decision.kind==="malformed"&&(process.stderr.write(`${WORKER_GUARD_MALFORMED_LINE}
5
- `),exitCode=0)}catch{try{process.stderr.write(`${WORKER_GUARD_MALFORMED_LINE}
6
- `)}catch{}exitCode=0}process.exit(exitCode);
2
+ import{readFileSync}from"node:fs";import path from"node:path";var WORKER_GUARD_REASON_PREFIX="bridge worker guard",WORKER_GUARD_MALFORMED_LINE=`${WORKER_GUARD_REASON_PREFIX}: payload could not be read; continuing fail-open`;function renderWorkerGuardReason(category,family){return category==="git-ambiguous"?`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). This shell form cannot be safely resolved: substitution, redirection, or stdin-fed arguments hide what git will run. Use literal arguments instead; for a commit message, write it to a file and run git commit -F <file>.`:category==="workflow-entry"||category==="workflow-launch-skill"||category==="workflow-launch-shell"||category==="workflow-planner-tool"?`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). A correction worker must not start a full workflow. Reading or editing files that mention a workflow is allowed; make the focused correction directly.`:category==="correction-context-invalid"?`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). The correction context for this worker is not valid, so no tool call can be checked. Stop and report the launch problem.`:category==="correction-payload-invalid"?`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). The tool call could not be read as a tool name with a tool input, so it cannot be checked. Retry with a well-formed call.`:`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). This command family is restricted to the worker's own branch, worktree, and read-only network access. Use a different approach and continue.`}var UNSAFE_UNQUOTED=new Set(["$","`",">","<"]),SEPARATORS=new Set([";","|","&"]);function tokenizeShellCommand(command){let commands=[],argv=[],current="",hasCurrent=!1,unsafe=!1,endToken=()=>{hasCurrent&&(argv.push(current),current="",hasCurrent=!1)},endCommand=()=>{endToken(),(argv.length>0||unsafe)&&commands.push({argv,unsafe}),argv=[],unsafe=!1};for(let i=0;i<command.length;i+=1){let ch=command[i];if(ch==="\\"){let next=command[i+1];next!==void 0&&(current+=next,hasCurrent=!0,i+=1);continue}if(ch==="'"){let close=command.indexOf("'",i+1);if(close===-1){unsafe=!0,current+=command.slice(i+1),hasCurrent=!0;break}current+=command.slice(i+1,close),hasCurrent=!0,i=close;continue}if(ch==='"'){let j=i+1,body="",closed=!1;for(;j<command.length;){let c=command[j];if(c==="\\"&&j+1<command.length){body+=command[j+1],j+=2;continue}if(c==='"'){closed=!0;break}(c==="$"||c==="`")&&(unsafe=!0),body+=c,j+=1}closed||(unsafe=!0),current+=body,hasCurrent=!0,i=j;continue}if(UNSAFE_UNQUOTED.has(ch)){unsafe=!0,current+=ch,hasCurrent=!0;continue}if(SEPARATORS.has(ch)){endCommand();continue}if(ch===`
3
+ `){endCommand();continue}if(ch===" "||ch===" "||ch==="\r"){endToken();continue}current+=ch,hasCurrent=!0}return endCommand(),commands}var WORKER_GUARD_MAX_PAYLOAD_BYTES=1e6;function asRecord(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)?value:void 0}function isInside(root,candidate){let rel=path.relative(root,candidate);return rel===""?!0:rel.startsWith("..")?!1:!path.isAbsolute(rel)}function resolveEffectiveCwd(ctx,payloadCwd){let root=ctx.worktreePath;if(typeof root!="string"||!path.isAbsolute(root))return;let normalizedRoot=path.resolve(root);if(payloadCwd===void 0)return normalizedRoot;if(typeof payloadCwd!="string"||payloadCwd.length===0)return;let resolved=path.isAbsolute(payloadCwd)?path.resolve(payloadCwd):path.resolve(normalizedRoot,payloadCwd);return isInside(normalizedRoot,resolved)?resolved:void 0}function allTargetsContained(ctx,cwd,targets){let root=ctx.worktreePath;if(typeof root!="string"||!path.isAbsolute(root)||cwd===void 0)return!1;let normalizedRoot=path.resolve(root);return targets.every(target=>{if(target.length===0||target==="~"||target.startsWith("~/"))return!1;let resolved=path.isAbsolute(target)?path.resolve(target):path.resolve(cwd,target);return isInside(normalizedRoot,resolved)})}function parseGitInvocation(argv,baseCwd){let cwd=baseCwd,unsafe=!1,i=1;for(;i<argv.length;){let token=argv[i];if(token==="-C"){let dir=argv[i+1];dir===void 0||cwd===void 0?(unsafe=!0,cwd=void 0):cwd=path.isAbsolute(dir)?path.resolve(dir):path.resolve(cwd,dir),i+=2;continue}if(token==="-c"||token==="--namespace"||token==="--git-dir"||token==="--work-tree"){unsafe=!0,i+=2;continue}if(token.startsWith("-")){i+=1;continue}break}return{subcommand:argv[i]??"",args:argv.slice(i+1),cwd,unsafe}}function isOwnBranchDestination(destination,workerBranch){let colon=destination.indexOf(":"),dst=colon===-1?destination:destination.slice(colon+1);if(dst.length===0)return!1;let bare=dst.startsWith("+")?dst.slice(1):dst;return bare===workerBranch||bare===`refs/heads/${workerBranch}`}function evaluateGitPush(args,ctx){let deny={kind:"deny",category:"git-push",family:"git"},positional=[];for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="--"){positional.push(...args.slice(i+1));break}if(token.startsWith("-")){if(token==="--tags"||token==="--delete"||token==="-d"||token==="--mirror"||token==="--all"||token==="--follow-tags"||token.startsWith("--repo"))return deny;(token==="-o"||token==="--push-option"||token==="--receive-pack"||token==="--exec")&&(i+=1);continue}positional.push(token)}let workerBranch=ctx.workerBranch;return typeof workerBranch!="string"||workerBranch.length===0?deny:positional.length<=1?{kind:"allow"}:positional.slice(1).every(d=>isOwnBranchDestination(d,workerBranch))?{kind:"allow"}:deny}function evaluateGitReset(args,ctx){let deny={kind:"deny",category:"git-reset-hard",family:"git"};if(!args.includes("--hard"))return{kind:"allow"};let positional=[];for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="--"){positional.push(...args.slice(i+1));break}token.startsWith("-")||positional.push(token)}if(positional.length===0)return{kind:"allow"};let workerBranch=ctx.workerBranch;if(typeof workerBranch!="string"||workerBranch.length===0)return deny;let allowed=new Set([workerBranch,`origin/${workerBranch}`,`refs/heads/${workerBranch}`,`refs/remotes/origin/${workerBranch}`]);return positional.every(ref=>allowed.has(ref))?{kind:"allow"}:deny}function evaluateGitClean(args,ctx,cwd){let deny={kind:"deny",category:"filesystem-clean",family:"git"};if(cwd===void 0||!allTargetsContained(ctx,cwd,[cwd]))return deny;let pathspecs=[];for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="--"){pathspecs.push(...args.slice(i+1));break}if(token==="-e"||token==="--exclude"){i+=1;continue}token.startsWith("-")||pathspecs.push(token)}return pathspecs.length===0?{kind:"allow"}:allTargetsContained(ctx,cwd,pathspecs)?{kind:"allow"}:deny}function isRecursiveForcedRemove(args){let recursive=!1,force=!1;for(let token of args){if(token==="--")break;if(token==="--recursive")recursive=!0;else if(token==="--force")force=!0;else{if(token.startsWith("--"))continue;if(token.startsWith("-")&&token.length>1)for(let flag of token.slice(1))(flag==="r"||flag==="R")&&(recursive=!0),flag==="f"&&(force=!0)}}return recursive&&force}function evaluateRemove(argv,ctx,cwd){let deny={kind:"deny",category:"filesystem-remove",family:"rm"},args=argv.slice(1);if(!isRecursiveForcedRemove(args))return{kind:"allow"};let targets=[],sawDoubleDash=!1;for(let token of args){if(!sawDoubleDash&&token==="--"){sawDoubleDash=!0;continue}!sawDoubleDash&&token.startsWith("-")&&token.length>1||targets.push(token)}return targets.length===0?deny:allTargetsContained(ctx,cwd,targets)?{kind:"allow"}:deny}function isDatabaseCommand(executable){let base=path.basename(executable);return base==="psql"||base.startsWith("pg_")}var DESTRUCTIVE_SQL=/\b(?:DROP|TRUNCATE)\b/i;function evaluateDatabase(argv){let deny={kind:"deny",category:"database-destructive",family:"psql"};return argv.slice(1).some(arg=>DESTRUCTIVE_SQL.test(arg))?deny:{kind:"allow"}}var MUTATING_METHODS=new Set(["POST","PUT","PATCH","DELETE"]);function evaluateNetwork(argv){let deny={kind:"deny",category:"network-mutating",family:"curl"},base=path.basename(argv[0]??""),args=argv.slice(1);for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="-X"||token==="--request"||token==="--method"){let value=args[i+1];if(value===void 0||MUTATING_METHODS.has(value.toUpperCase()))return deny;i+=1;continue}if(token.startsWith("-X")&&token.length>2){if(MUTATING_METHODS.has(token.slice(2).toUpperCase()))return deny;continue}if(token.startsWith("--request=")||token.startsWith("--method=")){let value=token.slice(token.indexOf("=")+1);if(MUTATING_METHODS.has(value.toUpperCase()))return deny;continue}if(token==="--data"||token.startsWith("--data=")||token.startsWith("--data-")||token==="--form"||token==="-F"||token==="--upload-file"||token==="-T"||token==="--post-data"||token.startsWith("--post-data=")||token==="--post-file"||token.startsWith("--post-file=")||token==="--body-data"||token.startsWith("--body-data=")||token==="--body-file"||token.startsWith("--body-file=")||base==="curl"&&token==="-d")return deny}return{kind:"allow"}}function classify(executable){let base=path.basename(executable);return base==="git"?"git":base==="rm"?"rm":isDatabaseCommand(base)?"database":base==="curl"||base==="wget"?"network":null}function unsafeGitCategory(argv){let{subcommand}=parseGitInvocation(argv,void 0);return subcommand==="push"?"git-push":subcommand==="reset"?"git-reset-hard":subcommand==="clean"?"filesystem-clean":"git-ambiguous"}function unsafeSegmentDecision(family,argv){switch(family){case"git":return{kind:"deny",category:unsafeGitCategory(argv),family:"git"};case"rm":return{kind:"deny",category:"filesystem-remove",family:"rm"};case"database":return{kind:"deny",category:"database-destructive",family:"psql"};case"network":return{kind:"deny",category:"network-mutating",family:"curl"}}}var TRANSPARENT_PREFIXES=new Set(["sudo","env","command","nohup","time","nice","exec"]),STDIN_ARGUMENT_PREFIXES=new Set(["xargs"]),SHELL_EXECUTABLES=new Set(["sh","bash","zsh","dash","ksh"]),EVAL_BUILTINS=new Set(["eval"]);function unwrapTransparentPrefixes(argv){let rest=argv;for(let depth=0;depth<8;depth+=1){let head=rest[0];if(head===void 0||!TRANSPARENT_PREFIXES.has(path.basename(head)))return rest;let i=1;for(;i<rest.length;){let token=rest[i];if(path.basename(head)==="env"&&/^[A-Za-z_][A-Za-z0-9_]*=/.test(token)){i+=1;continue}if(token.startsWith("-")){i+=1;continue}break}rest=rest.slice(i)}return rest}function stripStdinPrefixOptions(argv){let VALUE_OPTS=new Set(["-I","-i","-L","-n","-P","-s","-d","-E","-e","-a","--replace","--max-lines","--max-args","--max-procs","--max-chars","--delimiter","--eof","--arg-file"]),i=1;for(;i<argv.length;){let token=argv[i];if(token==="--"){i+=1;break}if(!token.startsWith("-"))break;if(token.includes("=")){i+=1;continue}i+=VALUE_OPTS.has(token)?2:1}return argv.slice(i)}var WORKFLOW_NAMES="implement-ticket|plan-ticket|idea-to-pr|review-and-implement",WORKFLOW_SLASH_LAUNCH=new RegExp(`(?:^|\\s)/(?:bridge-)?(?:${WORKFLOW_NAMES})(?=\\s|$)`,"i"),WORKFLOW_NAME_EXACT=new RegExp(`^/?(?:[A-Za-z0-9_-]+:)?(?:bridge-)?(?:${WORKFLOW_NAMES})$`,"i"),CLAUDE_PACKAGE_TOKEN=/^(?:@anthropic-ai\/)?claude(?:-code)?(?:@\S+)?$/;function claudeLaunchArgs(argv){let base=path.basename(argv[0]??"");if(base==="claude")return argv.slice(1);if(base==="npx"){let index=argv.findIndex((token,i)=>i>0&&CLAUDE_PACKAGE_TOKEN.test(token));return index===-1?void 0:argv.slice(index+1)}}function isClaudeWorkflowPrintLaunch(segment){let args=claudeLaunchArgs(segment.argv);return args===void 0||!args.some(token=>token==="-p"||token==="--print"||token.startsWith("--print="))?!1:segment.unsafe?!0:args.some(token=>{let value=token.startsWith("--print=")?token.slice(8):token;return WORKFLOW_SLASH_LAUNCH.test(value)})}function evaluateSimpleCommand(segment,ctx,payloadCwd,depth=0,stdinFed=!1){let argv=unwrapTransparentPrefixes(segment.argv),executable=argv[0];if(executable===void 0)return{kind:"allow"};if(SHELL_EXECUTABLES.has(path.basename(executable))&&depth<4){let cIndex=argv.findIndex(t=>t==="-c"),inner=cIndex===-1?void 0:argv[cIndex+1];if(inner!==void 0){for(let nested of tokenizeShellCommand(inner)){let decision=evaluateSimpleCommand(nested,ctx,payloadCwd,depth+1,stdinFed);if(decision.kind==="deny")return decision}return{kind:"allow"}}}if(EVAL_BUILTINS.has(path.basename(executable))&&depth<4){for(let nested of tokenizeShellCommand(argv.slice(1).join(" "))){let decision=evaluateSimpleCommand(nested,ctx,payloadCwd,depth+1,stdinFed);if(decision.kind==="deny")return decision}return{kind:"allow"}}if(STDIN_ARGUMENT_PREFIXES.has(path.basename(executable))&&depth<4){let rest=stripStdinPrefixOptions(argv);return rest.length===0?{kind:"allow"}:evaluateSimpleCommand({argv:rest,unsafe:segment.unsafe},ctx,payloadCwd,depth+1,!0)}if(segment={argv,unsafe:segment.unsafe},ctx.correctionEnforced&&isClaudeWorkflowPrintLaunch(segment))return{kind:"deny",category:"workflow-launch-shell",family:"workflow"};let family=classify(executable);if(family===null)return{kind:"allow"};if(stdinFed||segment.unsafe)return unsafeSegmentDecision(family,segment.argv);let cwd=resolveEffectiveCwd(ctx,payloadCwd);if(family==="rm")return evaluateRemove(segment.argv,ctx,cwd);if(family==="database")return evaluateDatabase(segment.argv);if(family==="network")return evaluateNetwork(segment.argv);let git=parseGitInvocation(segment.argv,cwd);return git.subcommand==="push"?git.unsafe?{kind:"deny",category:"git-push",family:"git"}:evaluateGitPush(git.args,ctx):git.subcommand==="reset"?git.unsafe?{kind:"deny",category:"git-reset-hard",family:"git"}:evaluateGitReset(git.args,ctx):git.subcommand==="clean"?git.unsafe?{kind:"deny",category:"filesystem-clean",family:"git"}:evaluateGitClean(git.args,ctx,git.cwd):{kind:"allow"}}function workflowDeny(category){return{kind:"deny",category,family:"workflow"}}var FILE_TOOLS=new Set(["Read","Edit","Write","MultiEdit","NotebookEdit","Grep","Glob"]),INVOCATION_FIELDS={Skill:["skill","args"],Task:["prompt","description"],Agent:["prompt","description"],SlashCommand:["command"]},PLANNER_TOOL_NAME=/(?:request_plan_generation|get_pipeline_recipe|plan_ticket|implement_ticket)/i;function evaluateCorrectionToolRole(toolName,input){if(FILE_TOOLS.has(toolName))return{kind:"allow"};if(toolName==="Bash")return;let fields=INVOCATION_FIELDS[toolName];if(fields!==void 0){for(let field of fields){let value=input[field];if(value===void 0)continue;if(typeof value!="string")return workflowDeny("workflow-launch-skill");let nameField=toolName==="Skill"?field==="skill":toolName==="SlashCommand";if(WORKFLOW_SLASH_LAUNCH.test(value)||nameField&&WORKFLOW_NAME_EXACT.test(value.trim().split(/\s+/)[0]??""))return workflowDeny("workflow-launch-skill")}return{kind:"allow"}}if(/^mcp__/i.test(toolName)&&PLANNER_TOOL_NAME.test(toolName))return workflowDeny("workflow-planner-tool");let args=Object.values(input).filter(v=>typeof v=="string").join(" ");return WORKFLOW_SLASH_LAUNCH.test(args)?workflowDeny("workflow-entry"):void 0}function evaluateWorkerGuardPayload(payload,ctx){if(ctx.correctionContextInvalid)return workflowDeny("correction-context-invalid");let root=asRecord(payload);if(root===void 0)return ctx.correctionEnforced?workflowDeny("correction-payload-invalid"):{kind:"malformed"};let toolName=root.tool_name??root.toolName;if(typeof toolName!="string")return ctx.correctionEnforced?workflowDeny("correction-payload-invalid"):{kind:"malformed"};let input=asRecord(root.tool_input??root.toolInput);if(ctx.correctionEnforced){if(input===void 0)return workflowDeny("correction-payload-invalid");let decision=evaluateCorrectionToolRole(toolName,input);if(decision!==void 0)return decision}if(toolName!=="Bash")return{kind:"allow"};if(input===void 0)return{kind:"malformed"};let command=input.command;if(typeof command!="string")return ctx.correctionEnforced?workflowDeny("correction-payload-invalid"):{kind:"malformed"};let rawCwd=root.cwd??input.cwd,payloadCwd=typeof rawCwd=="string"?rawCwd:void 0;for(let segment of tokenizeShellCommand(command)){let decision=evaluateSimpleCommand(segment,ctx,payloadCwd);if(decision.kind==="deny")return decision}return{kind:"allow"}}function evaluateWorkerGuardInput(raw,ctx){let unreadable=()=>ctx.correctionContextInvalid?workflowDeny("correction-context-invalid"):ctx.correctionEnforced?workflowDeny("correction-payload-invalid"):{kind:"malformed"};if(typeof raw!="string"||raw.length>WORKER_GUARD_MAX_PAYLOAD_BYTES||raw.trim().length===0)return unreadable();let parsed;try{parsed=JSON.parse(raw)}catch{return unreadable()}try{return evaluateWorkerGuardPayload(parsed,ctx)}catch{return unreadable()}}var WORKER_GUARD_ROOT_FLAG="--worktree-root";function resolveWorkerGuardContext(env,cwd,argv=[]){let ctx={},root;for(let i=0;i<argv.length;i+=1){if(argv[i]===WORKER_GUARD_ROOT_FLAG){root=argv[i+1];break}if(argv[i].startsWith(`${WORKER_GUARD_ROOT_FLAG}=`)){root=argv[i].slice(WORKER_GUARD_ROOT_FLAG.length+1);break}}root===void 0&&typeof cwd=="string"&&(root=cwd),typeof root=="string"&&path.isAbsolute(root)&&(ctx.worktreePath=path.resolve(root));let branch=env.BAPI_WORKER_BRANCH;typeof branch=="string"&&branch.trim().length>0&&(ctx.workerBranch=branch.trim());let flagIndex=argv.indexOf("--correction-intent-version"),flagValue=flagIndex>=0?argv[flagIndex+1]:void 0,envValue=env.BAPI_CORRECTION_INTENT_VERSION;return ctx.correctionEnforced=flagValue==="1"||envValue==="1",ctx.correctionContextInvalid=flagIndex>=0&&flagValue!=="1"||envValue!==void 0&&envValue!=="1"||flagIndex>=0&&envValue!==void 0&&envValue!==flagValue,ctx}function runWorkerGuardHook(io){let exitCode=0,correctionRequested=io.argv.includes("--correction-intent-version")||io.env.BAPI_CORRECTION_INTENT_VERSION!==void 0;try{let raw=io.readStdin(),ctx=resolveWorkerGuardContext(io.env,io.cwd,io.argv),decision=evaluateWorkerGuardInput(raw,ctx);decision.kind==="deny"?(io.writeStderr(renderWorkerGuardReason(decision.category,decision.family)),exitCode=2):decision.kind==="malformed"&&(io.writeStderr(WORKER_GUARD_MALFORMED_LINE),exitCode=0)}catch{try{io.writeStderr(correctionRequested?renderWorkerGuardReason("correction-payload-invalid","workflow"):WORKER_GUARD_MALFORMED_LINE)}catch{}exitCode=correctionRequested?2:0}return exitCode}function readStdinSync(){return readFileSync(0,"utf-8")}process.exit(runWorkerGuardHook({readStdin:readStdinSync,env:process.env,cwd:process.cwd(),argv:process.argv.slice(2),writeStderr:text=>{process.stderr.write(`${text}
4
+ `)}}));
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The worker-guard hook's whole decision-to-exit-code path, with its process
3
+ * boundaries injected (BAPI-1386).
4
+ *
5
+ * `worker-guard-hook-bin.ts` is the packaged entry point and calls `process.exit`,
6
+ * so a test cannot import it. This module holds everything that bin does, taking
7
+ * stdin, the environment, the working directory, argv, and stderr as parameters.
8
+ * The bin's behavior is unchanged: see its header for the exit-code contract.
9
+ *
10
+ * NOTHING is written to stdout: Claude Code reads a hook's stdout as structured
11
+ * control output.
12
+ */
13
+ import { WORKER_GUARD_MALFORMED_LINE, evaluateWorkerGuardInput, renderWorkerGuardReason, resolveWorkerGuardContext, } from "./executor/worker-guard-hook.js";
14
+ /** Run the hook once and return the exit code the process should use. */
15
+ export function runWorkerGuardHook(io) {
16
+ let exitCode = 0;
17
+ const correctionRequested = io.argv.includes("--correction-intent-version") || io.env.BAPI_CORRECTION_INTENT_VERSION !== undefined;
18
+ try {
19
+ const raw = io.readStdin();
20
+ const ctx = resolveWorkerGuardContext(io.env, io.cwd, io.argv);
21
+ const decision = evaluateWorkerGuardInput(raw, ctx);
22
+ if (decision.kind === "deny") {
23
+ io.writeStderr(renderWorkerGuardReason(decision.category, decision.family));
24
+ exitCode = 2;
25
+ }
26
+ else if (decision.kind === "malformed") {
27
+ io.writeStderr(WORKER_GUARD_MALFORMED_LINE);
28
+ exitCode = 0;
29
+ }
30
+ }
31
+ catch {
32
+ // Defensive top-level boundary. An unreadable stdin, an OOM on a pathological
33
+ // payload, anything at all: it becomes ONE fixed line, never raw exception text
34
+ // — an exception message can carry a path or an environment value and this line
35
+ // travels into worker logs.
36
+ try {
37
+ io.writeStderr(correctionRequested
38
+ ? renderWorkerGuardReason("correction-payload-invalid", "workflow")
39
+ : WORKER_GUARD_MALFORMED_LINE);
40
+ }
41
+ catch {
42
+ // stderr itself is unavailable; there is nothing further to report.
43
+ }
44
+ exitCode = correctionRequested ? 2 : 0;
45
+ }
46
+ return exitCode;
47
+ }