@bensandee/tooling 0.92.1 → 0.93.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.mjs CHANGED
@@ -175,7 +175,7 @@ function loadEnvFileForBin(rawArgs, bin, io = processIo) {
175
175
  //#region src/env-file-preload.ts
176
176
  const envFileLoad = loadEnvFileForBin(process.argv.slice(2), {
177
177
  name: "bst",
178
- version: "0.92.1"
178
+ version: "0.93.1"
179
179
  });
180
180
  //#endregion
181
181
  //#region ../cli-kit/src/logger.ts
@@ -772,13 +772,16 @@ const HealthConfigSchema = z.strictObject({
772
772
  tolerance: z.number().int().nonnegative().optional().meta({ description: "Slack in offender counts before an increase is a regression: a metric fails when the current count exceeds baseline + tolerance (default 0, so any increase fails). The same allowance applies to every per-module ceiling in health-baseline.json's moduleCeilings, not only the headline metrics. A count, not a percentage -- coverage's percentage-point slack is `coverage.tolerancePp`. Raising it un-tracks every module beneath the new line at once, which is why a justified `exempt` entry is the better answer to one stubborn hub." }),
773
773
  exempt: z.partialRecord(z.enum(EXEMPTABLE_METRIC_KEYS), z.record(z.string(), z.string().min(1).refine((reason) => reason.trim() !== "", { message: "must not be blank" }))).optional().meta({ description: "Per-metric, per-module exemption, keyed first by metric name (fanInOutliers, fanOutOutliers or crowdedDirectories) then by the repo-relative path exactly as `health:summary --explain` prints it: a module path for the two fan metrics, a directory path for crowdedDirectories. Each path maps to a required, non-empty justification string. An exempt module or directory is listed in the summary with its count and reason, carries no ceiling in health-baseline.json, and is counted in no metric -- it is out of the gate, not suppressed inside it. Adding one is a raise: it travels alone in a ceiling-only pull request, never beside code. An exempt module stays fully in the graph -- its edges still count toward every other module's fan-in/fan-out." })
774
774
  });
775
- const AsciiConfigSchema = z.strictObject({
776
- enabled: z.boolean().optional().meta({ description: "Opt into the ascii:check gate (default false)" }),
777
- exclude: z.array(z.string()).optional().meta({ description: "Additional path globs to skip during scanning. Added to the built-in defaults (docs/**/retired/**, **/CHANGELOG.md, and the two generated .claude/hooks/bst-hook files), not a replacement for them." }),
775
+ const CharsConfigSchema = z.strictObject({
776
+ enabled: z.boolean().optional().meta({ description: "Run the chars:check gate (default true). Set false to opt out -- the only off-switch: the gate scans tracked text files for LLM-tell punctuation and invisible or deceptive characters, and passes letters in any language." }),
777
+ exclude: z.array(z.string()).optional().meta({ description: "Additional path globs the chars:check scan skips. Added to the built-in defaults (docs/**/retired/**, **/CHANGELOG.md, and the two generated .claude/hooks/bst-hook files), not a replacement for them." }),
778
778
  deny: z.array(z.string()).optional().meta({ description: "Named offender categories to add to the default deny-list (e.g. \"math\")" }),
779
779
  allow: z.array(z.string()).optional().meta({ description: "Codepoints (as U+XXXX) to drop from the effective deny-list" }),
780
780
  treatAsProse: z.array(z.string()).optional().meta({ description: "Path globs (matched the same way as exclude) whose files --fix classifies as prose wholesale regardless of extension, flattening rather than flagging. The opt-back-in for an extension with no dedicated lexical-context classifier where blind flattening is known safe. No built-in defaults." })
781
781
  });
782
+ function isCharsGateOn(block) {
783
+ return block?.enabled !== false;
784
+ }
782
785
  const LintConfigSchema = z.strictObject({ strictTypeAware: z.boolean().optional().meta({ description: "Make a missing oxlint-tsgolint binary a hard failure for `bst lint:types` (the type-aware oxlint gate) rather than a notice (default false): arming it is what turns a platform gap into a build break, deliberately, because the repo that armed it is the one that asked for it. Findings block in every repo regardless of this setting." }) });
783
786
  const ChecksConfigSchema = z.strictObject({
784
787
  add: z.array(z.string()).optional().meta({ description: "Extra package.json scripts to run as first-class check steps, appended to the built-in list (e.g. ['lint:php']). An added step with no matching script is a hard failure." }),
@@ -1775,19 +1778,20 @@ const ToolingConfigSchema = z.strictObject({
1775
1778
  coverage: z.union([z.literal(false), CoverageConfigSchema]).optional().meta({ description: "Coverage tracking configuration. Set to false to disable; omit for default-on (when a vitest config is present)." }),
1776
1779
  health: HealthConfigSchema.optional().meta({ description: "Codebase-health gate configuration. Opt-in: set enabled true to gate structural metrics (crowded directories, circular deps, fan-in/out) against health-baseline.json." }),
1777
1780
  egress: EgressConfigSchema.optional().meta({ description: "Egress-audit history configuration, consumed by the bst-agent egress:* commands. There is no enabled key: egress:summary and egress:check read the transcripts root unconditionally, so history: 'none' is the only off-switch, and it disables egress:record alone." }),
1778
- ascii: AsciiConfigSchema.optional().meta({ description: "ASCII-only gate (bst ascii:check) configuration. Opt-in: set enabled true to scan tracked text files for curated non-ASCII offenders (curly quotes, ellipsis, dashes, box-drawing) and report a suggested ASCII replacement for each hit." }),
1781
+ chars: CharsConfigSchema.optional().meta({ description: "Character gate (bst chars:check) configuration. On by default: the gate scans tracked text files for LLM-tell punctuation and invisible or deceptive characters, and passes letters in any language. Set enabled false to opt out." }),
1782
+ ascii: CharsConfigSchema.optional().meta({ description: "Deprecated alias of chars, read until 2026-12-15" }),
1779
1783
  docs: z.union([z.literal(false), DocsConfigSchema]).optional().meta({ description: "Plan/spec lifecycle gate (`bst docs:check`). Overrides only -- dirs are discovered by convention. Set to false to disable. Violations are advisory until a pair is named in `enforced`." }),
1780
1784
  deploy: DeployConfigSchema.optional().meta({ description: "Generate a deploy-dispatch step in the release workflow that triggers a workflow_dispatch event in another repo after a successful release. The resolved release token must have rights on `dispatchRepo`." }),
1781
1785
  pnpmWorkspace: z.literal(false).optional().meta({ description: "Set to false to opt out of pnpm-workspace.yaml management (skips both generation and the repo:sync --check validation)." }),
1782
1786
  defaultBranch: DefaultBranchNameSchema.optional().meta({ description: "Repository default branch name, B. Drives the generated CI `on.push` trigger and every default-branch gate in `ci.yml` and `release.yml`, the release path (`release:changesets`, the version PR, `release:trigger`) and `checks:run`'s coverage gate. Generated output uses 'main' when unset; `checks:run` still falls back to `origin/HEAD`. Must be a plain branch name: ASCII letters, digits, '.', '_', '-' and '/'." }),
1783
1787
  unmanagedDevDependencies: UnmanagedDevDependenciesSchema.optional().meta({ description: "devDependency names repo:sync must never add or re-pin. Escape hatch for holding a tooling-managed dep (oxlint, vitest, knip, ...) at a deliberately off-canonical version; without it, repo:sync raises any managed devDep whose version has drifted below the canonical pin." }),
1784
- stack: StackSchema.optional().meta({ description: "Whether the repo has TS/JS source. Detected from source evidence; TypeScript is never inferred from absence." }),
1788
+ stack: StackSchema.optional().meta({ description: "Whether the repo has TS/JS source. Detected from source evidence; TypeScript is never inferred from absence. Setting this key pins its value, even one that equals detection, and repo:sync keeps it until the key is deleted." }),
1785
1789
  builder: z.enum([
1786
1790
  "tsdown",
1787
1791
  "vite",
1788
1792
  "custom",
1789
1793
  "none"
1790
- ]).optional().meta({ description: "What compiles the project. Detected from the package.json build script, then config-file presence." }),
1794
+ ]).optional().meta({ description: "What compiles the project. Detected from the package.json build script, then config-file presence. Setting this key pins its value, even one that equals detection, and repo:sync keeps it until the key is deleted." }),
1791
1795
  checks: ChecksConfigSchema.optional().meta({ description: "Check-pipeline overrides: extra steps to add, and steps to skip (unioned with CI defaults)." }),
1792
1796
  lint: LintConfigSchema.optional().meta({ description: "bst lint:types (the type-aware oxlint gate) overrides. See lint.strictTypeAware." }),
1793
1797
  agent: AgentConfigSchema.optional().meta({ description: "agent:start behaviour overrides. Opt-in: see agent.autonomy for the interior-autonomy gate." }),
@@ -1801,7 +1805,8 @@ const TolerantToolingConfigSchema = z.object({
1801
1805
  dockerCheck: z.union([z.literal(false), TolerantDockerCheckConfigSchema]).optional(),
1802
1806
  coverage: z.union([z.literal(false), tolerantBlock(CoverageConfigSchema)]).optional(),
1803
1807
  health: tolerantBlock(HealthConfigSchema).optional(),
1804
- ascii: tolerantBlock(AsciiConfigSchema).optional(),
1808
+ chars: tolerantBlock(CharsConfigSchema).optional(),
1809
+ ascii: tolerantBlock(CharsConfigSchema).optional(),
1805
1810
  docs: z.union([z.literal(false), tolerantBlock(DocsConfigSchema)]).optional(),
1806
1811
  deploy: tolerantBlock(DeployConfigSchema).optional(),
1807
1812
  checks: tolerantBlock(ChecksConfigSchema).optional(),
@@ -1851,7 +1856,13 @@ function parseToolingConfigStrict(raw, displayPath) {
1851
1856
  function loadToolingConfigStrict(targetDir) {
1852
1857
  const file = readConfigFile(targetDir);
1853
1858
  if (!file) return void 0;
1854
- return parseToolingConfigStrict(file.raw, file.displayPath);
1859
+ const config = parseToolingConfigStrict(file.raw, file.displayPath);
1860
+ refuseRenamedCharsKey(config, file.displayPath);
1861
+ return config;
1862
+ }
1863
+ function refuseRenamedCharsKey(config, displayPath) {
1864
+ if (config.chars === void 0 || config.ascii === void 0) return;
1865
+ throw new FatalError(`${displayPath} sets both chars and ascii; rename ascii to chars`);
1855
1866
  }
1856
1867
  const OVERRIDE_KEYS = [
1857
1868
  "structure",
@@ -1879,6 +1890,18 @@ function parseExistingConfig(raw) {
1879
1890
  } catch (_error) {}
1880
1891
  return {};
1881
1892
  }
1893
+ function pruneCharsDefaults(value) {
1894
+ const parsed = RawJsonObjectSchema.safeParse(value);
1895
+ if (!parsed.success) return value;
1896
+ if (parsed.data["enabled"] !== true) return value;
1897
+ const kept = {};
1898
+ for (const [key, entry] of Object.entries(parsed.data)) {
1899
+ if (key === "enabled") continue;
1900
+ kept[key] = entry;
1901
+ }
1902
+ if (Object.keys(kept).length === 0) return void 0;
1903
+ return kept;
1904
+ }
1882
1905
  function saveToolingConfig(ctx, config) {
1883
1906
  const defaults = computeDefaults(config.targetDir);
1884
1907
  const isMonorepo = config.structure === "monorepo";
@@ -1888,10 +1911,16 @@ function saveToolingConfig(ctx, config) {
1888
1911
  const result = { $schema: SCHEMA_REF };
1889
1912
  for (const key of OVERRIDE_KEYS) {
1890
1913
  if (isMonorepo && MONOREPO_IGNORED_KEYS.has(key)) continue;
1891
- if (config[key] !== defaults[key]) result[key] = config[key];
1914
+ if (Object.hasOwn(existingJson, key) && existingJson[key] === config[key] || config[key] !== defaults[key]) result[key] = config[key];
1892
1915
  }
1893
1916
  for (const [key, value] of Object.entries(existingJson)) {
1894
1917
  if (key === "$schema" || overrideKeySet.has(key)) continue;
1918
+ if (key === "chars" || key === "ascii") {
1919
+ const pruned = pruneCharsDefaults(value);
1920
+ if (pruned === void 0) continue;
1921
+ result[key] = pruned;
1922
+ continue;
1923
+ }
1895
1924
  result[key] = value;
1896
1925
  }
1897
1926
  const content = JSON.stringify(result, null, 2) + "\n";
@@ -1900,7 +1929,7 @@ function saveToolingConfig(ctx, config) {
1900
1929
  action: "skipped",
1901
1930
  description: "Already up to date"
1902
1931
  };
1903
- ctx.write(CONFIG_FILE, content);
1932
+ ctx.write(CONFIG_FILE, ctx.format(CONFIG_FILE, content));
1904
1933
  return {
1905
1934
  filePath: CONFIG_FILE,
1906
1935
  action: existing ? "updated" : "created",
@@ -4915,6 +4944,11 @@ function readUnmanagedDevDepNames(ctx) {
4915
4944
  const slice = parseToolingConfigSlice(ctx.read(".tooling.json"), UnmanagedSliceSchema, { block: "unmanagedDevDependencies" });
4916
4945
  return new Set(slice?.unmanagedDevDependencies ?? []);
4917
4946
  }
4947
+ const COMMIT_AND_TAG_VERSION_PACKAGE = "commit-and-tag-version";
4948
+ function wantsCommitAndTagVersion(ctx) {
4949
+ if (ctx.config.releaseStrategy !== "simple") return false;
4950
+ return !readUnmanagedDevDepNames(ctx).has(COMMIT_AND_TAG_VERSION_PACKAGE);
4951
+ }
4918
4952
  //#endregion
4919
4953
  //#region src/generators/package-json.ts
4920
4954
  const NODE_ENGINES_FLOOR = "24.13.0";
@@ -5202,8 +5236,8 @@ function rootDevDeps(config) {
5202
5236
  }["oxlint-tsgolint"];
5203
5237
  return deps;
5204
5238
  }
5205
- function addReleaseDeps(deps, config) {
5206
- switch (config.releaseStrategy) {
5239
+ function addReleaseDeps(deps, ctx) {
5240
+ switch (ctx.config.releaseStrategy) {
5207
5241
  case "release-it":
5208
5242
  deps["release-it"] = {
5209
5243
  "@changesets/cli": "3.0.3",
@@ -5224,7 +5258,7 @@ function addReleaseDeps(deps, config) {
5224
5258
  "vitest": "5.0.3",
5225
5259
  "pnpm": "11.28.5"
5226
5260
  }["release-it"];
5227
- if (config.structure === "monorepo") deps["@release-it/bumper"] = {
5261
+ if (ctx.config.structure === "monorepo") deps["@release-it/bumper"] = {
5228
5262
  "@changesets/cli": "3.0.3",
5229
5263
  "@release-it/bumper": "8.0.1",
5230
5264
  "@types/node": "24.19.1",
@@ -5245,7 +5279,7 @@ function addReleaseDeps(deps, config) {
5245
5279
  }["@release-it/bumper"];
5246
5280
  break;
5247
5281
  case "simple":
5248
- deps["commit-and-tag-version"] = {
5282
+ if (wantsCommitAndTagVersion(ctx)) deps[COMMIT_AND_TAG_VERSION_PACKAGE] = {
5249
5283
  "@changesets/cli": "3.0.3",
5250
5284
  "@release-it/bumper": "8.0.1",
5251
5285
  "@types/node": "24.19.1",
@@ -5349,8 +5383,24 @@ function reclaimManagedDevDeps(ctx, pkg, managedDevDeps, unmanaged, changes) {
5349
5383
  }
5350
5384
  pkg.devDependencies = existingDevDeps;
5351
5385
  }
5352
- function isAsciiCheckEnabled(ctx) {
5353
- return parseToolingConfigSlice(ctx.read(".tooling.json"), z.object({ ascii: AsciiConfigSchema.optional() }), { block: "ascii" })?.ascii?.enabled === true;
5386
+ function migrateAsciiCheckScript(pkg, changes) {
5387
+ const scripts = pkg.scripts;
5388
+ if (scripts === void 0) return;
5389
+ const legacy = scripts["ascii:check"];
5390
+ if (legacy === void 0) return;
5391
+ if (legacy === "bst ascii:check") {
5392
+ delete scripts["ascii:check"];
5393
+ changes.push("renamed script: ascii:check -> chars:check");
5394
+ return;
5395
+ }
5396
+ log$1.warn(`package.json: the ascii:check script is not the generated one, so the rename to chars:check leaves it alone. Rename it by hand: "ascii:check": "${legacy}" becomes "chars:check": "bst chars:check".`);
5397
+ }
5398
+ function isCharsCheckEnabled(ctx) {
5399
+ const slice = parseToolingConfigSlice(ctx.read(".tooling.json"), z.object({
5400
+ chars: CharsConfigSchema.optional(),
5401
+ ascii: CharsConfigSchema.optional()
5402
+ }), { block: "chars" });
5403
+ return isCharsGateOn(slice?.chars ?? slice?.ascii);
5354
5404
  }
5355
5405
  function applyMetadataDefaults(pkg, ctx, changes) {
5356
5406
  const existingDeps = pkg.dependencies ?? {};
@@ -5425,15 +5475,15 @@ function buildManagedScripts(ctx) {
5425
5475
  allScripts["docker:check"] = "bst docker:check";
5426
5476
  }
5427
5477
  if (resolveHealthConfigFromContext(ctx, { isMonorepo: ctx.config.structure === "monorepo" }).enabled) allScripts["health:check"] = "bst health:check";
5428
- if (isAsciiCheckEnabled(ctx)) allScripts["ascii:check"] = "bst ascii:check";
5478
+ if (isCharsCheckEnabled(ctx)) allScripts["chars:check"] = "bst chars:check";
5429
5479
  return allScripts;
5430
5480
  }
5431
5481
  function buildManagedDevDeps(ctx) {
5432
5482
  const isMonorepo = ctx.config.structure === "monorepo";
5433
5483
  const devDeps = { ...rootDevDeps(ctx.config) };
5434
5484
  if (!isMonorepo) Object.assign(devDeps, perPackageDevDeps(ctx.config));
5435
- if (ctx.config.stack !== "none" || ctx.config.ci === "forgejo") devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.15.0";
5436
- if (!isWorkspacePackage(ctx, "@bensandee/tooling")) devDeps["@bensandee/tooling"] = "0.92.1";
5485
+ if (ctx.config.stack !== "none") devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.15.0";
5486
+ if (!isWorkspacePackage(ctx, "@bensandee/tooling")) devDeps["@bensandee/tooling"] = "0.93.1";
5437
5487
  if (ctx.config.useEslintPlugin && ctx.config.stack !== "none") devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.17.0";
5438
5488
  if (wantsAgentCli(ctx)) devDeps[AGENT_CLI_PACKAGE] = "0.1.2";
5439
5489
  if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
@@ -5474,7 +5524,7 @@ function buildManagedDevDeps(ctx) {
5474
5524
  "vitest": "5.0.3",
5475
5525
  "pnpm": "11.28.5"
5476
5526
  }["prettier"];
5477
- addReleaseDeps(devDeps, ctx.config);
5527
+ addReleaseDeps(devDeps, ctx);
5478
5528
  return devDeps;
5479
5529
  }
5480
5530
  async function generatePackageJson(ctx) {
@@ -5495,6 +5545,7 @@ async function generatePackageJson(ctx) {
5495
5545
  pkg.type = "module";
5496
5546
  changes.push("set type: \"module\"");
5497
5547
  }
5548
+ migrateAsciiCheckScript(pkg, changes);
5498
5549
  mergeManagedScripts(pkg, allScripts, changes);
5499
5550
  const forwardingHint = diagnoseCheckScriptForwarding(pkg.scripts ?? {});
5500
5551
  if (forwardingHint) log$1.warn(`package.json: pnpm-forwarded check flags may be dropped. ${forwardingHint}`);
@@ -5587,10 +5638,30 @@ function reportStaleConfig(filePath, reason) {
5587
5638
  }
5588
5639
  //#endregion
5589
5640
  //#region src/generators/tsconfig.ts
5590
- function buildGeneratedTsconfig(ctx, extendsValue) {
5641
+ const DEFAULT_INCLUDE = [
5642
+ "src",
5643
+ "test",
5644
+ "*.ts"
5645
+ ];
5646
+ const LEGACY_INCLUDE = ["src"];
5647
+ function formatInclude(include) {
5648
+ return `[${include.map((item) => JSON.stringify(item)).join(", ")}]`;
5649
+ }
5650
+ function describeInclude(include) {
5651
+ return `added include: ${formatInclude(include)}`;
5652
+ }
5653
+ function warnIfNarrowInclude(ctx, filePath, include) {
5654
+ if (!Array.isArray(include) || include.length !== 1 || include[0] !== "src") return;
5655
+ const testDir = path.join(path.dirname(filePath), "test");
5656
+ if (!ctx.exists(testDir)) return;
5657
+ log$1.warn(`${filePath}: "include" is ${formatInclude(include)}, so a test/ directory beside it sits outside tsc --noEmit and its types go unchecked. Consider "include": ${formatInclude(DEFAULT_INCLUDE)}.`);
5658
+ }
5659
+ function buildGeneratedTsconfig(ctx, extendsValue, options = {}) {
5660
+ const dir = options.dir ?? ".";
5661
+ const include = options.include ?? DEFAULT_INCLUDE;
5591
5662
  const config = {
5592
5663
  extends: extendsValue,
5593
- ...ctx.exists("src") ? { include: ["src"] } : {}
5664
+ ...ctx.exists(path.join(dir, "src")) ? { include: [...include] } : {}
5594
5665
  };
5595
5666
  return JSON.stringify(config, null, 2) + "\n";
5596
5667
  }
@@ -5600,7 +5671,9 @@ async function generateTsconfig(ctx) {
5600
5671
  if (ctx.config.structure === "monorepo") return [generateMonorepoRootTsconfig(ctx), ...ctx.config.detectPackageTypes ? generateMonorepoPackageTsconfigs(ctx) : []];
5601
5672
  const extendsValue = `@bensandee/config/tsconfig/${ctx.config.projectType}`;
5602
5673
  if (ctx.config.stack !== "typescript") {
5603
- if (existing !== void 0 && existing === buildGeneratedTsconfig(ctx, extendsValue)) return [reportStaleConfig(filePath, `stack is ${ctx.config.stack}`)];
5674
+ const current = buildGeneratedTsconfig(ctx, extendsValue);
5675
+ const legacy = buildGeneratedTsconfig(ctx, extendsValue, { include: LEGACY_INCLUDE });
5676
+ if (existing !== void 0 && (existing === current || existing === legacy)) return [reportStaleConfig(filePath, `stack is ${ctx.config.stack}`)];
5604
5677
  return [{
5605
5678
  filePath,
5606
5679
  action: "skipped",
@@ -5655,6 +5728,7 @@ function mergeSingleTsconfig(ctx, filePath, extendsValue) {
5655
5728
  description: "Ignored via tooling:ignore comment"
5656
5729
  };
5657
5730
  const parsed = parseTsconfig(existing);
5731
+ warnIfNarrowInclude(ctx, filePath, parsed.include);
5658
5732
  const changes = [];
5659
5733
  if (!parsed.extends) {
5660
5734
  parsed.extends = extendsValue;
@@ -5664,8 +5738,8 @@ function mergeSingleTsconfig(ctx, filePath, extendsValue) {
5664
5738
  const tsconfigDir = path.dirname(filePath);
5665
5739
  const srcDir = tsconfigDir === "." ? "src" : path.join(tsconfigDir, "src");
5666
5740
  if (ctx.exists(srcDir)) {
5667
- parsed.include = ["src"];
5668
- changes.push("added include: [\"src\"]");
5741
+ parsed.include = [...DEFAULT_INCLUDE];
5742
+ changes.push(describeInclude(DEFAULT_INCLUDE));
5669
5743
  }
5670
5744
  }
5671
5745
  if (changes.length === 0) return {
@@ -5724,6 +5798,7 @@ function generateMonorepoPackageTsconfigs(ctx) {
5724
5798
  }
5725
5799
  continue;
5726
5800
  }
5801
+ warnIfNarrowInclude(ctx, filePath, parsed.include);
5727
5802
  const changes = [];
5728
5803
  if (parsed.extends !== extendsValue) {
5729
5804
  const prev = parsed.extends;
@@ -5731,8 +5806,8 @@ function generateMonorepoPackageTsconfigs(ctx) {
5731
5806
  changes.push(prev ? `changed extends: ${String(prev)} -> ${extendsValue}` : `added extends: ${extendsValue}`);
5732
5807
  }
5733
5808
  if (!parsed.include && ctx.exists(path.join(relDir, "src"))) {
5734
- parsed.include = ["src"];
5735
- changes.push("added include: [\"src\"]");
5809
+ parsed.include = [...DEFAULT_INCLUDE];
5810
+ changes.push(describeInclude(DEFAULT_INCLUDE));
5736
5811
  }
5737
5812
  if (changes.length === 0) {
5738
5813
  results.push({
@@ -5749,11 +5824,7 @@ function generateMonorepoPackageTsconfigs(ctx) {
5749
5824
  description: changes.join(", ")
5750
5825
  });
5751
5826
  } else {
5752
- const config = {
5753
- extends: extendsValue,
5754
- ...ctx.exists(path.join(relDir, "src")) ? { include: ["src"] } : {}
5755
- };
5756
- ctx.write(filePath, JSON.stringify(config, null, 2) + "\n");
5827
+ ctx.write(filePath, buildGeneratedTsconfig(ctx, extendsValue, { dir: relDir }));
5757
5828
  results.push({
5758
5829
  filePath,
5759
5830
  action: "created",
@@ -7928,7 +7999,10 @@ function ignoreBinariesField(binaries) {
7928
7999
  return binaries.length > 0 ? `\n ignoreBinaries: ${JSON.stringify(binaries)},` : "";
7929
8000
  }
7930
8001
  function ignorableDependencies(ctx) {
7931
- return wantsAgentCli(ctx) ? [AGENT_CLI_PACKAGE] : [];
8002
+ const dependencies = [];
8003
+ if (wantsAgentCli(ctx)) dependencies.push(AGENT_CLI_PACKAGE);
8004
+ if (wantsCommitAndTagVersion(ctx)) dependencies.push(COMMIT_AND_TAG_VERSION_PACKAGE);
8005
+ return dependencies;
7932
8006
  }
7933
8007
  function ignoreDependenciesField(dependencies) {
7934
8008
  return dependencies.length > 0 ? `\n ignoreDependencies: ${JSON.stringify(dependencies)},` : "";
@@ -7937,7 +8011,6 @@ function buildKnipConfigSingle(ignoreBinaries, ignoreDependencies) {
7937
8011
  return `import type { KnipConfig } from "knip";
7938
8012
 
7939
8013
  export default {
7940
- entry: ["src/index.ts"],
7941
8014
  project: ["src/**/*.ts"],${ignoreBinariesField(ignoreBinaries)}${ignoreDependenciesField(ignoreDependencies)}
7942
8015
  } satisfies KnipConfig;
7943
8016
  `;
@@ -7952,7 +8025,6 @@ export default {${ignoreBinariesField(ignoreBinaries)}${ignoreDependenciesField(
7952
8025
  project: [],
7953
8026
  },
7954
8027
  "packages/*": {
7955
- entry: ["src/index.ts", "src/bin.ts"],
7956
8028
  project: ["src/**/*.ts"],
7957
8029
  },
7958
8030
  },
@@ -8349,8 +8421,8 @@ function writeEmbedded(ctx, file) {
8349
8421
  }
8350
8422
  const NOT_BUILT = "run `pnpm build` from the repo root";
8351
8423
  async function generateClaudeHooks(ctx, embedded = {
8352
- hook: "/**\n * bst-hook -- Claude Code PreToolUse hook\n * @bensandee/claude-hooks\n * https://github.com/bensandee/tooling\n *\n * Auto-generated by @bensandee/tooling repo:sync -- do not edit manually.\n */\nimport * as fs from \"node:fs\";\nimport { existsSync, readFileSync, readdirSync, realpathSync, statSync } from \"node:fs\";\nimport * as path from \"node:path\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { execFileSync } from \"node:child_process\";\n\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/util.js\nfunction joinValues(array, separator = \"|\") {\n return array.map((val) => stringifyPrimitive(val)).join(separator);\n}\nfunction jsonStringifyReplacer(_, value) {\n if (typeof value === \"bigint\") return value.toString();\n return value;\n}\nvar Cached = class {\n constructor(getter) {\n this._getter = getter;\n this._value = void 0;\n }\n get value() {\n const getter = this._getter;\n if (getter !== void 0) {\n this._value = getter();\n this._getter = void 0;\n }\n return this._value;\n }\n};\nfunction cached(getter) {\n return new Cached(getter);\n}\nfunction nullish(input) {\n return input === null || input === void 0;\n}\nfunction cleanRegex(source) {\n const start = source.startsWith(\"^\") ? 1 : 0;\n const end = source.endsWith(\"$\") ? source.length - 1 : source.length;\n return source.slice(start, end);\n}\nfunction assignProp(target, prop, value) {\n Object.defineProperty(target, prop, {\n value,\n writable: true,\n enumerable: true,\n configurable: true\n });\n}\nconst captureStackTrace = \"captureStackTrace\" in Error ? Error.captureStackTrace : (..._args) => {};\nfunction isObject(data) {\n return typeof data === \"object\" && data !== null && !Array.isArray(data);\n}\nfunction isPlainObject(o) {\n if (isObject(o) === false) return false;\n const ctor = o.constructor;\n if (ctor === void 0) return true;\n if (typeof ctor !== \"function\") return true;\n const prot = ctor.prototype;\n if (isObject(prot) === false) return false;\n if (Object.prototype.hasOwnProperty.call(prot, \"isPrototypeOf\") === false) return false;\n return true;\n}\nfunction shallowClone(o) {\n if (isPlainObject(o)) return { ...o };\n if (Array.isArray(o)) return [...o];\n if (o instanceof Map) return new Map(o);\n if (o instanceof Set) return new Set(o);\n return o;\n}\nfunction clone(inst, def, params) {\n const cl = new inst._zod.constr(def ?? inst._zod.def);\n if (!def || params?.parent) cl._zod.parent = inst;\n return cl;\n}\nfunction normalizeParams(_params) {\n const params = _params;\n if (!params) return {};\n if (typeof params === \"string\") return { error: () => params };\n if (params?.message !== void 0) {\n if (params?.error !== void 0) throw new Error(\"Cannot specify both `message` and `error` params\");\n params.error = params.message;\n }\n delete params.message;\n if (typeof params.error === \"string\") return {\n ...params,\n error: () => params.error\n };\n return params;\n}\nfunction stringifyPrimitive(value) {\n if (typeof value === \"bigint\") return value.toString() + \"n\";\n if (typeof value === \"string\") return `\"${value}\"`;\n return `${value}`;\n}\nfunction optionalKeys(shape) {\n return Object.keys(shape).filter((k) => {\n return shape[k]._zod.optin !== void 0 && shape[k]._zod.optout === \"optional\";\n });\n}\nfunction aborted(x, startIndex = 0) {\n if (x.aborted === true) return true;\n for (let i = startIndex; i < x.issues.length; i++) if (x.issues[i]?.continue !== true) return true;\n return false;\n}\nfunction explicitlyAborted(x, startIndex = 0) {\n if (x.aborted === true) return true;\n for (let i = startIndex; i < x.issues.length; i++) if (x.issues[i]?.continue === false) return true;\n return false;\n}\nfunction prefixIssues(path, issues) {\n return issues.map((iss) => {\n var _a;\n (_a = iss).path ?? (_a.path = []);\n iss.path.unshift(path);\n return iss;\n });\n}\nfunction unwrapMessage(message) {\n return typeof message === \"string\" ? message : message?.message;\n}\nfunction attachSchema(issues, start, inst) {\n var _a;\n for (let i = start; i < issues.length; i++) (_a = issues[i]).schema ?? (_a.schema = inst);\n}\nfunction finalizeIssue(iss, ctx, config) {\n var _a;\n const traits = iss.inst?._zod?.traits;\n if (traits?.has(\"$ZodType\")) {\n if (traits.has(\"$ZodCheck\")) (_a = iss).schema ?? (_a.schema = iss.inst);\n else iss.schema = iss.inst;\n }\n const schemaError = iss.schema !== iss.inst ? iss.schema?._zod.def?.error : void 0;\n const message = iss.message ? iss.message : unwrapMessage(iss.inst?._zod.def?.error?.(iss)) ?? unwrapMessage(schemaError?.(iss)) ?? unwrapMessage(ctx?.error?.(iss)) ?? unwrapMessage(config.customError?.(iss)) ?? unwrapMessage(config.localeError?.(iss)) ?? \"Invalid input\";\n const full = {};\n for (const k of Object.keys(iss)) {\n if (k === \"inst\" || k === \"schema\" || k === \"continue\" || k === \"input\" || k === \"__proto__\") continue;\n full[k] = iss[k];\n }\n full.path ?? (full.path = []);\n full.message = message;\n if (ctx?.reportInput) full.input = iss.input;\n return full;\n}\nconst highSurrogate = /[\\uD800-\\uDBFF]/;\nfunction codePointLength(str) {\n const units = str.length;\n if (!highSurrogate.test(str)) return units;\n let count = units;\n for (let i = 0; i < units - 1; i++) if ((str.charCodeAt(i) & 64512) === 55296 && (str.charCodeAt(i + 1) & 64512) === 56320) {\n count--;\n i++;\n }\n return count;\n}\nfunction getLengthableOrigin(input) {\n if (Array.isArray(input)) return \"array\";\n if (typeof input === \"string\") return \"string\";\n return \"unknown\";\n}\nfunction parsedType(data) {\n const t = typeof data;\n switch (t) {\n case \"number\": return Number.isNaN(data) ? \"nan\" : \"number\";\n case \"object\": {\n if (data === null) return \"null\";\n if (Array.isArray(data)) return \"array\";\n const obj = data;\n if (obj && Object.getPrototypeOf(obj) !== Object.prototype && \"constructor\" in obj && obj.constructor) return obj.constructor.name;\n }\n }\n return t;\n}\nfunction members(proto, table) {\n for (const key in table) {\n const desc = Object.getOwnPropertyDescriptor(table, key);\n if (desc.get) Object.defineProperty(proto, key, {\n ...desc,\n enumerable: false\n });\n else defineBound(proto, key, desc.value);\n }\n}\nfunction own(inst, key, value, enumerable = true) {\n Object.defineProperty(inst, key, {\n configurable: true,\n writable: true,\n enumerable,\n value\n });\n return value;\n}\nfunction hide(inst, key, value) {\n return own(inst, key, value, false);\n}\nfunction defineBound(proto, key, fn) {\n Object.defineProperty(proto, key, {\n configurable: true,\n get() {\n return this == null ? fn : own(this, key, fn.bind(this));\n },\n set(value) {\n own(this, key, value);\n }\n });\n}\nfunction claim(inst, sentinel) {\n const proto = Object.getPrototypeOf(inst);\n return sentinel in proto ? void 0 : proto;\n}\nlet installing;\nlet broke = false;\nconst breaker = {\n configurable: true,\n get() {\n broke = true;\n }\n};\nfunction defineLazyInternal(inst, key, compute) {\n const proto = Object.getPrototypeOf(inst._zod);\n if (key in proto && installing !== inst._zod) {\n installing = void 0;\n return;\n }\n installing = inst._zod;\n Object.defineProperty(proto, key, {\n configurable: true,\n get() {\n Object.defineProperty(this, key, breaker);\n const outer = broke;\n broke = false;\n try {\n const value = compute(this);\n if (broke) delete this[key];\n else Object.defineProperty(this, key, {\n configurable: true,\n writable: true,\n value\n });\n broke = broke || outer;\n return value;\n } catch (err) {\n delete this[key];\n broke = broke || outer;\n throw err;\n }\n },\n set(value) {\n Object.defineProperty(this, key, {\n configurable: true,\n writable: true,\n value\n });\n }\n });\n}\nfunction installLazyProp(inst, key, make, enumerable) {\n const proto = claim(inst, key);\n if (!proto) return;\n Object.defineProperty(proto, key, {\n configurable: true,\n get() {\n const desc = {\n configurable: true,\n writable: true,\n enumerable,\n value: void 0\n };\n Object.defineProperty(this, key, desc);\n desc.value = make(this);\n Object.defineProperty(this, key, desc);\n return desc.value;\n },\n set(value) {\n Object.defineProperty(this, key, {\n configurable: true,\n writable: true,\n enumerable,\n value\n });\n }\n });\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/core.js\nvar _a;\nconst _zodDesc = {\n value: void 0,\n enumerable: false\n};\nlet _E = \"captureStackTrace\" in Error ? Error : null;\nfunction newError(Definition) {\n const E = _E;\n if (E) {\n const saved = E.stackTraceLimit;\n if (typeof saved === \"number\") {\n try {\n E.stackTraceLimit = 0;\n } catch {\n _E = null;\n return new Definition();\n }\n try {\n return new Definition();\n } finally {\n E.stackTraceLimit = saved;\n }\n }\n }\n return new Definition();\n}\nfunction $constructor(name, initializer, proto, params) {\n const zodProto = {};\n function Internals(def) {\n this.def = def;\n this.constr = _;\n this.traits = /* @__PURE__ */ new Set();\n }\n Internals.prototype = zodProto;\n const protoMembers = proto;\n const initialized = protoMembers && /* @__PURE__ */ new WeakSet();\n function init(inst, def) {\n if (!inst._zod) {\n _zodDesc.value = new Internals(def);\n try {\n Object.defineProperty(inst, \"_zod\", _zodDesc);\n } finally {\n _zodDesc.value = void 0;\n }\n } else if (inst._zod.traits.has(name)) return;\n inst._zod.traits.add(name);\n initializer(inst, def);\n if (initialized) {\n const own = Object.getPrototypeOf(inst);\n const ctorProto = inst._zod.constr.prototype;\n let up = own;\n while (up && up !== ctorProto) up = Object.getPrototypeOf(up);\n const target = up ?? own;\n if (!initialized.has(target)) {\n initialized.add(target);\n members(target, protoMembers);\n }\n }\n const proto = _.prototype;\n for (const k in proto) {\n if (!Object.prototype.hasOwnProperty.call(proto, k)) continue;\n if (!(k in inst)) inst[k] = proto[k].bind(inst);\n }\n }\n const Parent = params?.Parent ?? Object;\n class Definition extends Parent {}\n Object.defineProperty(Definition, \"name\", { value: name });\n function _(def) {\n const inst = params?.Parent ? newError(Definition) : this;\n init(inst, def);\n const deferred = inst._zod.deferred;\n if (deferred) {\n for (const fn of deferred) fn();\n inst._zod.deferred = void 0;\n }\n const pp = globalThis.__zod_globalConfig?.postProcessor;\n if (pp) pp(inst);\n return inst;\n }\n Object.defineProperty(_, \"init\", { value: init });\n Object.defineProperty(_, Symbol.hasInstance, { value: (inst) => {\n if (params?.Parent && inst instanceof params.Parent) return true;\n return inst?._zod?.traits?.has(name);\n } });\n Object.defineProperty(_, \"name\", { value: name });\n return _;\n}\nvar $ZodAsyncError = class extends Error {\n constructor() {\n super(`Encountered Promise during synchronous parse. Use .parseAsync() instead.`);\n }\n};\n(_a = globalThis).__zod_globalConfig ?? (_a.__zod_globalConfig = {});\nconst globalConfig = globalThis.__zod_globalConfig;\nfunction config(newConfig) {\n if (newConfig) Object.assign(globalConfig, newConfig);\n return globalConfig;\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/errors.js\nfunction _getMessage() {\n const internals = this._zod;\n internals.message ?? (internals.message = JSON.stringify(internals.def, jsonStringifyReplacer, 2));\n return internals.message;\n}\nfunction _setMessage(value) {\n this._zod.message = value;\n}\nconst _messageDesc = {\n get: _getMessage,\n set: _setMessage,\n enumerable: true,\n configurable: true\n};\nconst _issuesDesc = {\n value: void 0,\n enumerable: false\n};\nconst _installedToString = /* @__PURE__ */ new WeakSet([Object.prototype, Error.prototype]);\nconst initializer = (inst, def) => {\n inst.name = \"$ZodError\";\n _issuesDesc.value = def;\n Object.defineProperty(inst, \"issues\", _issuesDesc);\n _issuesDesc.value = void 0;\n Object.defineProperty(inst, \"message\", _messageDesc);\n const proto = Object.getPrototypeOf(inst);\n if (!_installedToString.has(proto)) {\n _installedToString.add(proto);\n Object.defineProperty(proto, \"toString\", {\n configurable: true,\n enumerable: false,\n get() {\n const value = () => this.message;\n Object.defineProperty(this, \"toString\", {\n value,\n configurable: true,\n writable: true\n });\n return value;\n },\n set(value) {\n Object.defineProperty(this, \"toString\", {\n value,\n configurable: true,\n writable: true\n });\n }\n });\n }\n};\nconst $ZodError = $constructor(\"$ZodError\", initializer);\nconst $ZodRealError = $constructor(\"$ZodError\", initializer, void 0, { Parent: Error });\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/parse.js\nconst _parse = (_Err) => {\n const fn = (schema, value, _ctx, _params) => {\n const ctx = _ctx ? {\n ..._ctx,\n async: false\n } : { async: false };\n const result = schema._zod.run({\n value,\n issues: []\n }, ctx);\n if (result instanceof Promise) throw new $ZodAsyncError();\n if (result.issues.length) {\n const e = new ((_params?.Err) ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())));\n captureStackTrace(e, _params?.callee ?? fn);\n throw e;\n }\n return result.value;\n };\n return fn;\n};\nconst parse = /* @__PURE__*/ _parse($ZodRealError);\nconst _parseAsync = (_Err) => {\n const fn = async (schema, value, _ctx, params) => {\n const ctx = _ctx ? {\n ..._ctx,\n async: true\n } : { async: true };\n let result = schema._zod.run({\n value,\n issues: []\n }, ctx);\n if (result instanceof Promise) result = await result;\n if (result.issues.length) {\n const e = new ((params?.Err) ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())));\n captureStackTrace(e, params?.callee ?? fn);\n throw e;\n }\n return result.value;\n };\n return fn;\n};\nconst parseAsync = /* @__PURE__*/ _parseAsync($ZodRealError);\nconst _safeParse = (_Err) => (schema, value, _ctx) => {\n const ctx = _ctx ? {\n ..._ctx,\n async: false\n } : { async: false };\n const result = schema._zod.run({\n value,\n issues: []\n }, ctx);\n if (result instanceof Promise) throw new $ZodAsyncError();\n return result.issues.length ? failure(_Err, result.issues, ctx) : {\n success: true,\n data: result.value\n };\n};\nconst safeParse = /* @__PURE__*/ _safeParse($ZodRealError);\nfunction failure(Err, issues, ctx) {\n let error;\n return {\n success: false,\n get error() {\n if (!error) {\n error = new Err(issues.map((iss) => finalizeIssue(iss, ctx, config())));\n issues = void 0;\n ctx = void 0;\n }\n return error;\n },\n set error(e) {\n error = e;\n issues = void 0;\n ctx = void 0;\n }\n };\n}\nconst _safeParseAsync = (_Err) => async (schema, value, _ctx) => {\n const ctx = _ctx ? {\n ..._ctx,\n async: true\n } : { async: true };\n let result = schema._zod.run({\n value,\n issues: []\n }, ctx);\n if (result instanceof Promise) result = await result;\n return result.issues.length ? failure(_Err, result.issues, ctx) : {\n success: true,\n data: result.value\n };\n};\nconst safeParseAsync = /* @__PURE__*/ _safeParseAsync($ZodRealError);\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/regexes.js\nconst anyString = /^[\\s\\S]{0,}$/;\nconst number = /^-?\\d+(?:\\.\\d+)?$/;\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/checks.js\nconst $ZodCheck = /*@__PURE__*/ $constructor(\"$ZodCheck\", (inst, def) => {\n var _a;\n inst._zod ?? (inst._zod = {});\n inst._zod.def = def;\n (_a = inst._zod).onattach ?? (_a.onattach = []);\n});\nconst _whenHasLength = (payload) => {\n const val = payload.value;\n return !nullish(val) && val.length !== void 0;\n};\nconst $ZodCheckMinLength = /*@__PURE__*/ $constructor(\"$ZodCheckMinLength\", (inst, def) => {\n var _a;\n $ZodCheck.init(inst, def);\n (_a = inst._zod.def).when ?? (_a.when = _whenHasLength);\n inst._zod.check = (payload) => {\n const input = payload.value;\n const units = input.length;\n if ((typeof input === \"string\" && units >= def.minimum && units < def.minimum * 2 ? codePointLength(input) : units) >= def.minimum) return;\n const origin = getLengthableOrigin(input);\n payload.issues.push({\n origin,\n code: \"too_small\",\n minimum: def.minimum,\n inclusive: true,\n input,\n inst,\n continue: !def.abort\n });\n };\n});\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/versions.js\nconst version = {\n major: 4,\n minor: 6,\n patch: 5\n};\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/schemas.js\nconst $ZodType = /*@__PURE__*/ $constructor(\"$ZodType\", (inst, def) => {\n var _a;\n inst ?? (inst = {});\n inst._zod.def = def;\n inst._zod.bag = inst._zod.bag || {};\n inst._zod.version = version;\n const defChecks = inst._zod.def.checks;\n const checks = inst._zod.traits.has(\"$ZodCheck\") ? [inst, ...defChecks ?? []] : defChecks?.length ? [...defChecks] : [];\n for (const ch of checks) for (const fn of ch._zod.onattach) fn(inst);\n if (checks.length === 0) {\n (_a = inst._zod).deferred ?? (_a.deferred = []);\n inst._zod.deferred?.push(() => {\n inst._zod.run = inst._zod.parse;\n });\n } else {\n const runChecks = (payload, checks, ctx) => {\n if (payload.memo) return payload;\n let isAborted = aborted(payload);\n let asyncResult;\n for (const ch of checks) {\n if (ch._zod.def.when) {\n if (explicitlyAborted(payload)) continue;\n if (!ch._zod.def.when(payload)) continue;\n } else if (isAborted) continue;\n const currLen = payload.issues.length;\n const _ = ch._zod.check(payload);\n if (_ instanceof Promise && ctx?.async === false) throw new $ZodAsyncError();\n if (asyncResult || _ instanceof Promise) asyncResult = (asyncResult ?? Promise.resolve()).then(async () => {\n await _;\n if (payload.issues.length === currLen) return;\n attachSchema(payload.issues, currLen, inst);\n if (!isAborted) isAborted = aborted(payload, currLen);\n });\n else {\n if (payload.issues.length === currLen) continue;\n attachSchema(payload.issues, currLen, inst);\n if (!isAborted) isAborted = aborted(payload, currLen);\n }\n }\n if (asyncResult) return asyncResult.then(() => {\n return payload;\n });\n return payload;\n };\n const handleCanaryResult = (canary, payload, ctx) => {\n if (aborted(canary)) {\n canary.aborted = true;\n return canary;\n }\n const checkResult = runChecks(payload, checks, ctx);\n if (checkResult instanceof Promise) {\n if (ctx.async === false) throw new $ZodAsyncError();\n return checkResult.then((checkResult) => inst._zod.parse(checkResult, ctx));\n }\n return inst._zod.parse(checkResult, ctx);\n };\n inst._zod.run = (payload, ctx) => {\n if (ctx.skipChecks) return inst._zod.parse(payload, ctx);\n if (ctx.direction === \"backward\") {\n const canary = inst._zod.parse({\n value: payload.value,\n issues: []\n }, {\n ...ctx,\n skipChecks: true\n });\n if (canary instanceof Promise) return canary.then((canary) => {\n return handleCanaryResult(canary, payload, ctx);\n });\n return handleCanaryResult(canary, payload, ctx);\n }\n const result = inst._zod.parse(payload, ctx);\n if (result instanceof Promise) {\n if (ctx.async === false) throw new $ZodAsyncError();\n return result.then((result) => runChecks(result, checks, ctx));\n }\n return runChecks(result, checks, ctx);\n };\n }\n}, {\n get \"~standard\"() {\n return hide(this, \"~standard\", standardProps(this));\n },\n set \"~standard\"(value) {\n own(this, \"~standard\", value);\n }\n});\nconst toStandardResult = (r, ctx) => r.issues.length ? { issues: r.issues.map((iss) => finalizeIssue(iss, ctx, config())) } : { value: r.value };\nasync function validateAsync(inst, value) {\n const ctx = { async: true };\n return toStandardResult(await inst._zod.run({\n value,\n issues: []\n }, ctx), ctx);\n}\nfunction standardProps(inst) {\n return {\n validate: (value) => {\n const ctx = { async: false };\n try {\n const r = inst._zod.run({\n value,\n issues: []\n }, ctx);\n if (!(r instanceof Promise)) return toStandardResult(r, ctx);\n } catch (_) {}\n return validateAsync(inst, value);\n },\n vendor: \"zod\",\n version: 1\n };\n}\nconst $ZodString = /*@__PURE__*/ $constructor(\"$ZodString\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.pattern = def.pattern ?? anyString;\n inst._zod.parse = (payload, _) => {\n if (def.coerce) try {\n payload.value = String(payload.value);\n } catch (_) {}\n if (typeof payload.value === \"string\") return payload;\n payload.issues.push({\n expected: \"string\",\n code: \"invalid_type\",\n input: payload.value,\n inst\n });\n return payload;\n };\n});\nconst $ZodUnknown = /*@__PURE__*/ $constructor(\"$ZodUnknown\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload) => payload;\n});\nfunction handleArrayResult(result, final, index) {\n if (result.issues.length) final.issues.push(...prefixIssues(index, result.issues));\n final.value[index] = result.value;\n}\nconst $ZodArray = /*@__PURE__*/ $constructor(\"$ZodArray\", (inst, def) => {\n $ZodType.init(inst, def);\n const memo = globalConfig.memoizer;\n memo?.attach(inst);\n inst._zod.parse = (payload, ctx) => {\n const input = payload.value;\n if (!Array.isArray(input)) {\n payload.issues.push({\n expected: \"array\",\n code: \"invalid_type\",\n input,\n inst\n });\n return payload;\n }\n payload.value = memo ? memo.alloc(inst, payload, Array(input.length), ctx) : Array(input.length);\n const proms = [];\n const abortEarly = ctx?.abortEarly;\n for (let i = 0; i < input.length; i++) {\n const item = input[i];\n const result = def.element._zod.run({\n value: item,\n issues: []\n }, ctx);\n if (result instanceof Promise) proms.push(result.then((result) => handleArrayResult(result, payload, i)));\n else {\n handleArrayResult(result, payload, i);\n if (abortEarly && result.issues.length !== 0 && aborted(result)) break;\n }\n }\n if (proms.length) return Promise.all(proms).then(() => payload);\n return payload;\n };\n});\nfunction handlePropertyResult(result, final, key, input, optin, optout) {\n const isPresent = key in input;\n const isOptionalOut = optout === \"optional\";\n if (!isPresent && isOptionalOut && optin === \"optional\") return;\n if (result.issues.length) {\n if (optin !== void 0 && isOptionalOut && !isPresent) return;\n final.issues.push(...prefixIssues(key, result.issues));\n }\n if (!isPresent && optin === void 0) {\n if (!result.issues.length) final.issues.push({\n code: \"invalid_type\",\n expected: \"nonoptional\",\n input: void 0,\n path: [key]\n });\n return;\n }\n if (result.value === void 0) {\n if (isPresent || optin === \"defaulted\" && !isOptionalOut) final.value[key] = void 0;\n } else final.value[key] = result.value;\n}\nconst NO_SYMBOL_KEYS = [];\nfunction normalizeDef(def) {\n const keys = Object.keys(def.shape);\n const ownSymbols = Object.getOwnPropertySymbols(def.shape);\n const symbolKeys = ownSymbols.length ? ownSymbols : NO_SYMBOL_KEYS;\n const allKeys = symbolKeys.length ? [...keys, ...symbolKeys] : keys;\n for (const k of allKeys) if (!def.shape?.[k]?._zod?.traits?.has(\"$ZodType\")) throw new Error(`Invalid element at key \"${String(k)}\": expected a Zod schema`);\n const okeys = optionalKeys(def.shape);\n return {\n ...def,\n allKeys,\n symbolKeys,\n keySet: new Set(keys),\n numKeys: keys.length,\n optionalKeys: new Set(okeys)\n };\n}\nfunction handleCatchall(proms, input, payload, ctx, def, inst, abortEarly) {\n const unrecognized = [];\n const keySet = def.keySet;\n const _catchall = def.catchall._zod;\n const t = _catchall.def.type;\n const optin = _catchall.optin;\n const optout = _catchall.optout;\n let seen = 0;\n for (const key in input) {\n if (abortEarly && payload.issues.length !== seen) {\n if (aborted(payload, seen)) break;\n seen = payload.issues.length;\n }\n if (keySet.has(key)) continue;\n if (key === \"__proto__\") {\n if (t === \"never\") unrecognized.push(key);\n continue;\n }\n if (t === \"never\") {\n unrecognized.push(key);\n continue;\n }\n const r = _catchall.run({\n value: input[key],\n issues: []\n }, ctx);\n if (r instanceof Promise) proms.push(r.then((r) => handlePropertyResult(r, payload, key, input, optin, optout)));\n else handlePropertyResult(r, payload, key, input, optin, optout);\n }\n if (unrecognized.length) payload.issues.push({\n code: \"unrecognized_keys\",\n keys: unrecognized,\n input,\n inst,\n continue: true\n });\n if (!proms.length) return payload;\n return Promise.all(proms).then(() => {\n return payload;\n });\n}\nconst $ZodObject = /*@__PURE__*/ $constructor(\"$ZodObject\", (inst, def) => {\n $ZodType.init(inst, def);\n const desc = Object.getOwnPropertyDescriptor(def, \"shape\");\n const sh = desc?.get ? desc.get.raw : def.shape ?? {};\n if (sh) {\n const get = () => {\n const newSh = { ...sh };\n Object.defineProperty(def, \"shape\", { value: newSh });\n get.raw = newSh;\n return newSh;\n };\n get.raw = sh;\n Object.defineProperty(def, \"shape\", { get });\n }\n const _normalized = cached(() => normalizeDef(def));\n defineLazyInternal(inst, \"propValues\", (zod) => {\n const shape = zod.def.shape;\n const propValues = {};\n for (const key in shape) {\n const field = shape[key]._zod;\n if (field.values) {\n if (!Object.prototype.hasOwnProperty.call(propValues, key)) assignProp(propValues, key, /* @__PURE__ */ new Set());\n for (const v of field.values) propValues[key].add(v);\n if (field.optin !== void 0) propValues[key].add(void 0);\n }\n }\n return propValues;\n });\n const isObject$2 = isObject;\n const catchall = def.catchall;\n let value;\n const memo = globalConfig.memoizer;\n memo?.attach(inst);\n inst._zod.parse = (payload, ctx) => {\n value ?? (value = _normalized.value);\n const input = payload.value;\n if (!isObject$2(input)) {\n payload.issues.push({\n expected: \"object\",\n code: \"invalid_type\",\n input,\n inst\n });\n return payload;\n }\n payload.value = memo ? memo.alloc(inst, payload, {}, ctx) : {};\n const proms = [];\n const shape = value.shape;\n const abortEarly = ctx?.abortEarly;\n let seen = payload.issues.length;\n for (const key of value.allKeys) {\n if (abortEarly && payload.issues.length !== seen) {\n if (aborted(payload, seen)) break;\n seen = payload.issues.length;\n }\n if (key === \"__proto__\") continue;\n const el = shape[key];\n const optin = el._zod.optin;\n const optout = el._zod.optout;\n const r = el._zod.run({\n value: input[key],\n issues: []\n }, ctx);\n if (r instanceof Promise) proms.push(r.then((r) => handlePropertyResult(r, payload, key, input, optin, optout)));\n else handlePropertyResult(r, payload, key, input, optin, optout);\n }\n if (!catchall) return proms.length ? Promise.all(proms).then(() => payload) : payload;\n return handleCatchall(proms, input, payload, ctx, _normalized.value, inst, abortEarly === true);\n };\n});\nconst $ZodRecord = /*@__PURE__*/ $constructor(\"$ZodRecord\", (inst, def) => {\n $ZodType.init(inst, def);\n const memo = globalConfig.memoizer;\n memo?.attach(inst);\n inst._zod.parse = (payload, ctx) => {\n const input = payload.value;\n if (!isPlainObject(input)) {\n payload.issues.push({\n expected: \"record\",\n code: \"invalid_type\",\n input,\n inst\n });\n return payload;\n }\n const proms = [];\n const values = def.keyType._zod.values;\n if (values && !def.partial) {\n payload.value = memo ? memo.alloc(inst, payload, {}, ctx) : {};\n const recordKeys = /* @__PURE__ */ new Set();\n for (const key of values) if (typeof key === \"string\" || typeof key === \"number\" || typeof key === \"symbol\") {\n recordKeys.add(typeof key === \"number\" ? key.toString() : key);\n if (key === \"__proto__\") continue;\n const keyResult = def.keyType._zod.run({\n value: key,\n issues: []\n }, ctx);\n if (keyResult instanceof Promise) throw new Error(\"Async schemas not supported in object keys currently\");\n if (keyResult.issues.length) {\n payload.issues.push({\n code: \"invalid_key\",\n origin: \"record\",\n issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config())),\n input: key,\n path: [key],\n inst\n });\n continue;\n }\n const outKey = keyResult.value;\n if (outKey === \"__proto__\") continue;\n const result = def.valueType._zod.run({\n value: input[key],\n issues: []\n }, ctx);\n if (result instanceof Promise) proms.push(result.then((result) => {\n if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));\n payload.value[outKey] = result.value;\n }));\n else {\n if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));\n payload.value[outKey] = result.value;\n }\n }\n let unrecognized;\n for (const key in input) if (!recordKeys.has(key)) {\n if (def.mode === \"loose\") {\n if (key === \"__proto__\") continue;\n payload.value[key] = input[key];\n } else {\n unrecognized = unrecognized ?? [];\n unrecognized.push(key);\n }\n }\n if (unrecognized && unrecognized.length > 0) payload.issues.push({\n code: \"unrecognized_keys\",\n input,\n inst,\n keys: unrecognized,\n continue: true\n });\n } else {\n payload.value = memo ? memo.alloc(inst, payload, {}, ctx) : {};\n let unrecognized;\n for (const key of Reflect.ownKeys(input)) {\n if (key === \"__proto__\") continue;\n if (!Object.prototype.propertyIsEnumerable.call(input, key)) continue;\n let keyResult = def.keyType._zod.run({\n value: key,\n issues: []\n }, ctx);\n if (keyResult instanceof Promise) throw new Error(\"Async schemas not supported in object keys currently\");\n if (typeof key === \"string\" && number.test(key) && keyResult.issues.length) {\n const retryResult = def.keyType._zod.run({\n value: Number(key),\n issues: []\n }, ctx);\n if (retryResult instanceof Promise) throw new Error(\"Async schemas not supported in object keys currently\");\n if (retryResult.issues.length === 0) keyResult = retryResult;\n }\n if (keyResult.issues.length) {\n if (def.mode === \"loose\") payload.value[key] = input[key];\n else if (values) {\n unrecognized = unrecognized ?? [];\n unrecognized.push(key);\n } else payload.issues.push({\n code: \"invalid_key\",\n origin: \"record\",\n issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config())),\n input: key,\n path: [key],\n inst\n });\n continue;\n }\n const outKey = keyResult.value;\n if (outKey === \"__proto__\") continue;\n const result = def.valueType._zod.run({\n value: input[key],\n issues: []\n }, ctx);\n if (result instanceof Promise) proms.push(result.then((result) => {\n if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));\n payload.value[outKey] = result.value;\n }));\n else {\n if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));\n payload.value[outKey] = result.value;\n }\n }\n if (unrecognized && unrecognized.length > 0) payload.issues.push({\n code: \"unrecognized_keys\",\n input,\n inst,\n keys: unrecognized,\n continue: true\n });\n }\n if (proms.length) return Promise.all(proms).then(() => payload);\n return payload;\n };\n});\nfunction handleOptionalResult(payload, result) {\n payload.value = result.issues.length ? void 0 : result.value;\n return payload;\n}\nconst $ZodOptional = /*@__PURE__*/ $constructor(\"$ZodOptional\", (inst, def) => {\n $ZodType.init(inst, def);\n defineLazyInternal(inst, \"optin\", (zod) => zod.def.innerType._zod.optin === \"defaulted\" ? \"defaulted\" : \"optional\");\n inst._zod.optout = \"optional\";\n defineLazyInternal(inst, \"values\", (zod) => {\n const values = zod.def.innerType._zod.values;\n return values ? /* @__PURE__ */ new Set([...values, void 0]) : void 0;\n });\n defineLazyInternal(inst, \"pattern\", (zod) => {\n const pattern = zod.def.innerType._zod.pattern;\n return pattern ? new RegExp(`^(${cleanRegex(pattern.source)})?$`) : void 0;\n });\n inst._zod.parse = (payload, ctx) => {\n if (payload.value === void 0) {\n if (def.innerType._zod.optin !== \"defaulted\") return payload;\n const result = def.innerType._zod.run({\n value: payload.value,\n issues: []\n }, ctx);\n if (result instanceof Promise) return result.then((result) => handleOptionalResult(payload, result));\n return handleOptionalResult(payload, result);\n }\n return def.innerType._zod.run(payload, ctx);\n };\n});\nconst $ZodDefault = /*@__PURE__*/ $constructor(\"$ZodDefault\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.optin = \"defaulted\";\n defineLazyInternal(inst, \"values\", (zod) => zod.def.innerType._zod.values);\n inst._zod.parse = (payload, ctx) => {\n if (ctx.direction === \"backward\") return def.innerType._zod.run(payload, ctx);\n if (payload.value === void 0) {\n payload.value = def.defaultValue;\n return payload;\n }\n const result = def.innerType._zod.run(payload, ctx);\n if (result instanceof Promise) return result.then((result) => handleDefaultResult(result, def));\n return handleDefaultResult(result, def);\n };\n});\nfunction handleDefaultResult(payload, def) {\n if (payload.value === void 0) payload.value = def.defaultValue;\n return payload;\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/locales/en.js\nconst error = () => {\n const Sizable = {\n string: {\n unit: \"characters\",\n verb: \"to have\"\n },\n file: {\n unit: \"bytes\",\n verb: \"to have\"\n },\n array: {\n unit: \"items\",\n verb: \"to have\"\n },\n set: {\n unit: \"items\",\n verb: \"to have\"\n },\n map: {\n unit: \"entries\",\n verb: \"to have\"\n }\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const FormatDictionary = {\n regex: \"input\",\n email: \"email address\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO datetime\",\n date: \"ISO date\",\n time: \"ISO time\",\n duration: \"ISO duration\",\n ipv4: \"IPv4 address\",\n ipv6: \"IPv6 address\",\n mac: \"MAC address\",\n cidrv4: \"IPv4 range\",\n cidrv6: \"IPv6 range\",\n base64: \"base64-encoded string\",\n base64url: \"base64url-encoded string\",\n json_string: \"JSON string\",\n e164: \"E.164 number\",\n currency_code: \"currency code\",\n credit_card: \"credit card number\",\n iban: \"IBAN\",\n jwt: \"JWT\",\n template_literal: \"input\"\n };\n const TypeDictionary = { nan: \"NaN\" };\n function getTypeName(type, input) {\n if (type === \"number\" && typeof input === \"number\" && !Number.isFinite(input)) return String(input);\n return TypeDictionary[type] ?? type;\n }\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\": return `Invalid input: expected ${getTypeName(issue.expected)}, received ${getTypeName(parsedType(issue.input), issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1) return `Invalid input: expected ${stringifyPrimitive(issue.values[0])}`;\n return `Invalid option: expected one of ${joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.exact ? \"exactly \" : issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing) return `Too big: expected ${issue.origin ?? \"value\"} to have ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elements\"}`;\n return `Too big: expected ${issue.origin ?? \"value\"} to be ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.exact ? \"exactly \" : issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) return `Too small: expected ${issue.origin} to have ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n return `Too small: expected ${issue.origin} to be ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") return `Invalid string: must start with \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\") return `Invalid string: must end with \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\") return `Invalid string: must include \"${_issue.includes}\"`;\n if (_issue.format === \"regex\") return `Invalid string: must match pattern ${_issue.pattern}`;\n return `Invalid ${FormatDictionary[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\": return `Invalid number: must be a multiple of ${issue.divisor}`;\n case \"unrecognized_keys\": return `Unrecognized key${issue.keys.length > 1 ? \"s\" : \"\"}: ${joinValues(issue.keys, \", \")}`;\n case \"invalid_key\": return `Invalid key in ${issue.origin}`;\n case \"invalid_union\":\n if (issue.options && Array.isArray(issue.options) && issue.options.length > 0) return `Invalid discriminator value. Expected ${issue.options.map((o) => `'${o}'`).join(\" | \")}`;\n if (issue.inclusive === false) return \"Invalid input: more than one option matched\";\n return \"Invalid input\";\n case \"invalid_element\": return `Invalid value in ${issue.origin}`;\n default: return `Invalid input`;\n }\n };\n};\nfunction en_default() {\n return { localeError: error() };\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/api.js\nfunction snapshotChecks(def) {\n if (def.checks) def.checks = [...def.checks];\n return def;\n}\n// @__NO_SIDE_EFFECTS__\nfunction _string(Class, params) {\n return new Class(snapshotChecks({\n type: \"string\",\n ...normalizeParams(params)\n }));\n}\n// @__NO_SIDE_EFFECTS__\nfunction _unknown(Class) {\n return new Class({ type: \"unknown\" });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _minLength(minimum, params) {\n return new $ZodCheckMinLength({\n check: \"min_length\",\n ...normalizeParams(params),\n minimum\n });\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/mini/schemas.js\nconst ZodMiniType = /*@__PURE__*/ $constructor(\"ZodMiniType\", (inst, def) => {\n if (!inst._zod) throw new Error(\"Uninitialized schema in ZodMiniType.\");\n $ZodType.init(inst, def);\n inst.def = def;\n inst.type = def.type;\n}, {\n get with() {\n return this.check;\n },\n set with(value) {\n own(this, \"with\", value);\n },\n parse(data, params) {\n return parse(this, data, params, { callee: this.parse });\n },\n parseAsync(data, params) {\n return parseAsync(this, data, params, { callee: this.parseAsync });\n },\n safeParse(data, params) {\n return safeParse(this, data, params);\n },\n safeParseAsync(data, params) {\n return safeParseAsync(this, data, params);\n },\n check(...checks) {\n const def = this.def;\n return this.clone({\n ...def,\n checks: [...def.checks ?? [], ...checks.map((ch) => typeof ch === \"function\" ? { _zod: {\n check: ch,\n def: { check: \"custom\" },\n onattach: []\n } } : ch)]\n }, { parent: true });\n },\n clone(_def, params) {\n return clone(this, _def, params);\n },\n brand() {\n return this;\n },\n register(reg, meta) {\n reg.add(this, meta);\n return this;\n },\n apply(fn, ...args) {\n return args.length === 0 ? fn(this) : fn(this, ...args);\n }\n});\nconst ZodMiniString = /*@__PURE__*/ $constructor(\"ZodMiniString\", (inst, def) => {\n $ZodString.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction string(params) {\n return _string(ZodMiniString, params);\n}\nconst ZodMiniUnknown = /*@__PURE__*/ $constructor(\"ZodMiniUnknown\", (inst, def) => {\n $ZodUnknown.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction unknown() {\n return _unknown(ZodMiniUnknown);\n}\nconst ZodMiniArray = /*@__PURE__*/ $constructor(\"ZodMiniArray\", (inst, def) => {\n $ZodArray.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction array(element, params) {\n return new ZodMiniArray({\n type: \"array\",\n element,\n ...normalizeParams(params)\n });\n}\nconst ZodMiniObject = /*@__PURE__*/ $constructor(\"ZodMiniObject\", (inst, def) => {\n $ZodObject.init(inst, def);\n ZodMiniType.init(inst, def);\n installLazyProp(inst, \"shape\", (self) => self._zod.def.shape, false);\n});\n// @__NO_SIDE_EFFECTS__\nfunction object(shape, params) {\n const def = {\n type: \"object\",\n shape: shape ?? {},\n ...normalizeParams(params)\n };\n return new ZodMiniObject(def);\n}\nconst ZodMiniRecord = /*@__PURE__*/ $constructor(\"ZodMiniRecord\", (inst, def) => {\n $ZodRecord.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction record(keyType, valueType, params) {\n if (!valueType || !valueType._zod) return new ZodMiniRecord({\n type: \"record\",\n keyType: /* @__PURE__ */ string(),\n valueType: keyType,\n ...normalizeParams(valueType)\n });\n return new ZodMiniRecord({\n type: \"record\",\n keyType,\n valueType,\n ...normalizeParams(params)\n });\n}\nconst ZodMiniOptional = /*@__PURE__*/ $constructor(\"ZodMiniOptional\", (inst, def) => {\n $ZodOptional.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction optional(innerType) {\n return new ZodMiniOptional({\n type: \"optional\",\n innerType\n });\n}\nconst ZodMiniDefault = /*@__PURE__*/ $constructor(\"ZodMiniDefault\", (inst, def) => {\n $ZodDefault.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction _default(innerType, defaultValue) {\n return new ZodMiniDefault({\n type: \"default\",\n innerType,\n get defaultValue() {\n return typeof defaultValue === \"function\" ? defaultValue() : shallowClone(defaultValue);\n }\n });\n}\n\n//#endregion\n//#region src/lib/hook-io.ts\nconfig(en_default());\nconst HookInputSchema = object({\n session_id: string(),\n transcript_path: string(),\n cwd: string(),\n permission_mode: string(),\n hook_event_name: string(),\n tool_name: string(),\n tool_input: record(string(), unknown())\n});\nasync function readHookInput() {\n const chunks = [];\n for await (const chunk of process.stdin) chunks.push(typeof chunk === \"string\" ? chunk : String(chunk));\n let parsed;\n try {\n parsed = JSON.parse(chunks.join(\"\"));\n } catch (_error) {\n return {\n ok: false,\n raw: chunks.join(\"\").slice(0, 200),\n error: \"invalid JSON\"\n };\n }\n const result = HookInputSchema.safeParse(parsed);\n if (!result.success) return {\n ok: false,\n raw: parsed,\n error: result.error.message\n };\n return {\n ok: true,\n data: result.data\n };\n}\nconst BashInputSchema = object({\n command: string(),\n description: optional(string())\n});\nconst FilePathInputSchema = object({ file_path: string() });\nconst SearchInputSchema = object({\n pattern: string(),\n path: optional(string())\n});\nfunction parseBashInput(toolInput) {\n const result = BashInputSchema.safeParse(toolInput);\n return result.success ? result.data : null;\n}\nfunction parseFilePathInput(toolInput) {\n const result = FilePathInputSchema.safeParse(toolInput);\n return result.success ? result.data : null;\n}\nfunction parseSearchInput(toolInput) {\n const result = SearchInputSchema.safeParse(toolInput);\n return result.success ? result.data : null;\n}\nfunction writeDeny(reason) {\n const decision = { hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n permissionDecision: \"deny\",\n ...reason ? { permissionDecisionReason: reason } : {}\n } };\n process.stdout.write(JSON.stringify(decision));\n}\nfunction writeAdvice(advice) {\n const decision = { hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n additionalContext: advice\n } };\n process.stdout.write(JSON.stringify(decision));\n}\n\n//#endregion\n//#region src/lib/audit.ts\nconst MAX_LOG_SIZE = 1048576;\nfunction getLogPath() {\n const hookDir = path.dirname(fileURLToPath(import.meta.url));\n return path.join(hookDir, \"bst-hook.audit.local.jsonl\");\n}\nfunction rotateIfNeeded(logPath) {\n try {\n if (fs.statSync(logPath).size > MAX_LOG_SIZE) {\n const rotatedPath = `${logPath}.1`;\n fs.renameSync(logPath, rotatedPath);\n }\n } catch (_error) {}\n}\nlet rotationChecked = false;\nfunction audit(entry) {\n try {\n const logPath = getLogPath();\n if (!rotationChecked) {\n rotateIfNeeded(logPath);\n rotationChecked = true;\n }\n const record = {\n timestamp: (/* @__PURE__ */ new Date()).toISOString(),\n ...entry\n };\n fs.appendFileSync(logPath, JSON.stringify(record) + \"\\n\");\n } catch (err) {\n process.stderr.write(`[bst-hook] audit write failed: ${err instanceof Error ? err.message : String(err)}\\n`);\n }\n}\n\n//#endregion\n//#region src/lib/shell-tokenizer.ts\nfunction scanDoubleQuoted(input, start) {\n let content = \"\";\n let i = start;\n while (i < input.length && input[i] !== \"\\\"\") {\n if (input[i] === \"\\\\\" && i + 1 < input.length) {\n const next = input[i + 1];\n if (next === \"\\\"\" || next === \"\\\\\" || next === \"$\" || next === \"`\") {\n content += next;\n i += 2;\n continue;\n }\n }\n content += input[i];\n i++;\n }\n if (i >= input.length) return null;\n return {\n content,\n next: i + 1\n };\n}\nfunction tokenize(input) {\n const tokens = [];\n let current = \"\";\n let i = 0;\n let inToken = false;\n while (i < input.length) {\n const ch = input[i];\n if (ch === \"'\") {\n inToken = true;\n i++;\n while (i < input.length && input[i] !== \"'\") {\n current += input[i];\n i++;\n }\n if (i >= input.length) return {\n ok: false,\n error: \"unterminated single quote\"\n };\n i++;\n } else if (ch === \"\\\"\") {\n inToken = true;\n const scanned = scanDoubleQuoted(input, i + 1);\n if (!scanned) return {\n ok: false,\n error: \"unterminated double quote\"\n };\n current += scanned.content;\n i = scanned.next;\n } else if (ch === \"\\\\\" && i + 1 < input.length) {\n inToken = true;\n current += input[i + 1];\n i += 2;\n } else if (ch === \" \" || ch === \" \") {\n if (inToken) {\n tokens.push(current);\n current = \"\";\n inToken = false;\n }\n i++;\n } else {\n inToken = true;\n current += ch;\n i++;\n }\n }\n if (inToken) tokens.push(current);\n return {\n ok: true,\n tokens\n };\n}\nconst SAFE_UNQUOTED_RE = /^[A-Za-z0-9_\\-./=:@%+,]+$/;\nfunction quoteToken(token) {\n if (token.length > 0 && SAFE_UNQUOTED_RE.test(token)) return token;\n return \"'\" + token.replace(/'/g, \"'\\\\''\") + \"'\";\n}\nfunction joinTokens(tokens) {\n return tokens.map(quoteToken).join(\" \");\n}\n\n//#endregion\n//#region src/lib/shell-splitter.ts\nfunction splitCommand(input) {\n if (!input || !input.trim()) return [];\n const segments = [];\n let current = \"\";\n let i = 0;\n let inSingleQuote = false;\n let inDoubleQuote = false;\n while (i < input.length) {\n const ch = input[i];\n if (ch === \"'\" && !inDoubleQuote) {\n inSingleQuote = !inSingleQuote;\n current += ch;\n i++;\n continue;\n }\n if (ch === \"\\\"\" && !inSingleQuote) {\n inDoubleQuote = !inDoubleQuote;\n current += ch;\n i++;\n continue;\n }\n if (ch === \"\\\\\" && !inSingleQuote && i + 1 < input.length) {\n current += ch + input[i + 1];\n i += 2;\n continue;\n }\n if (!inSingleQuote && !inDoubleQuote) {\n if (ch === \"&\" && i + 1 < input.length && input[i + 1] === \"&\") {\n pushSegment(segments, current, \"&&\");\n current = \"\";\n i += 2;\n continue;\n }\n if (ch === \"&\" && !(i + 1 < input.length && input[i + 1] === \">\") && !(i > 0 && (input[i - 1] === \">\" || input[i - 1] === \"<\"))) {\n pushSegment(segments, current, \"&\");\n current = \"\";\n i++;\n continue;\n }\n if (ch === \"|\" && i + 1 < input.length && input[i + 1] === \"|\") {\n pushSegment(segments, current, \"||\");\n current = \"\";\n i += 2;\n continue;\n }\n if (ch === \"|\") {\n pushSegment(segments, current, \"|\");\n current = \"\";\n i++;\n continue;\n }\n if (ch === \";\") {\n pushSegment(segments, current, \";\");\n current = \"\";\n i++;\n continue;\n }\n }\n current += ch;\n i++;\n }\n const trimmed = current.trim();\n if (trimmed) segments.push({\n command: trimmed,\n operator: null\n });\n return segments;\n}\nfunction pushSegment(segments, text, operator) {\n const trimmed = text.trim();\n if (trimmed) segments.push({\n command: trimmed,\n operator\n });\n}\n\n//#endregion\n//#region src/lib/injection-guard.ts\nfunction hasBraceExpansion(token) {\n let depth = 0;\n for (let i = 0; i < token.length; i++) {\n const ch = token[i];\n if (ch === \"{\") depth++;\n else if (ch === \"}\") {\n if (depth > 0) depth--;\n } else if (depth >= 1 && (ch === \",\" || ch === \".\" && token[i + 1] === \".\")) return true;\n }\n return false;\n}\nfunction checkInjection(tokens, rawSegment) {\n if (/[\\n\\r]/.test(rawSegment)) return {\n safe: false,\n reason: \"unquoted newline in command\"\n };\n for (const token of tokens) {\n if (token === \"`\") return {\n safe: false,\n reason: \"backtick command substitution\"\n };\n if (token.includes(\"`\")) return {\n safe: false,\n reason: \"backtick command substitution\"\n };\n if (token.includes(\"$\")) return {\n safe: false,\n reason: \"unresolved dollar-expansion\"\n };\n if (hasBraceExpansion(token)) return {\n safe: false,\n reason: \"brace expansion\"\n };\n if (token.includes(\"<(\")) return {\n safe: false,\n reason: \"process substitution\"\n };\n if (token.includes(\">(\")) return {\n safe: false,\n reason: \"process substitution\"\n };\n }\n return { safe: true };\n}\n\n//#endregion\n//#region src/lib/env-file.ts\nconst ENV_SAFE_SUFFIXES = [\n \".sample\",\n \".example\",\n \".template\"\n];\nconst ENV_TOOL_OWNED_SUFFIXES = [\".check.env\"];\nconst ENV_FILE_PATTERN = /(^|[/\\\\])(\\.env(\\..*)?|.*\\.env(\\..*)?|\\.env)$/;\nfunction isEnvSafeSuffix(filename) {\n return [...ENV_SAFE_SUFFIXES, ...ENV_TOOL_OWNED_SUFFIXES].some((suffix) => filename.endsWith(suffix));\n}\nfunction isEnvFile(filePath) {\n if (isEnvSafeSuffix(path.basename(filePath))) return false;\n return ENV_FILE_PATTERN.test(filePath);\n}\n\n//#endregion\n//#region src/lib/git-state.ts\nconst GIT_TIMEOUT_MS = 5e3;\nfunction getCurrentBranch(cwd) {\n try {\n const out = execFileSync(\"git\", [\n \"rev-parse\",\n \"--abbrev-ref\",\n \"HEAD\"\n ], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n if (!out || out === \"HEAD\") return null;\n return out;\n } catch {\n return null;\n }\n}\nfunction getRepoTopLevel(cwd) {\n try {\n const out = execFileSync(\"git\", [\"rev-parse\", \"--show-toplevel\"], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n return out === \"\" ? null : out;\n } catch {\n return null;\n }\n}\nfunction getRemoteDefaultBranch(cwd) {\n try {\n const out = execFileSync(\"git\", [\n \"symbolic-ref\",\n \"--short\",\n \"refs/remotes/origin/HEAD\"\n ], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n if (!out) return null;\n return out.startsWith(\"origin/\") ? out.slice(7) : out;\n } catch {\n return null;\n }\n}\nconst DefaultBranchSchema = object({ defaultBranch: optional(string().check(_minLength(1))) });\nfunction getConfiguredDefaultBranch(topLevel) {\n let parsed;\n try {\n parsed = JSON.parse(readFileSync(join(topLevel, \".tooling.json\"), \"utf-8\"));\n } catch {\n return null;\n }\n const result = DefaultBranchSchema.safeParse(parsed);\n return result.success ? result.data.defaultBranch ?? null : null;\n}\nconst ALWAYS_PROTECTED = [\"main\", \"master\"];\nfunction getProtectedBranches(cwd, extra) {\n const set = new Set(ALWAYS_PROTECTED);\n const remote = getRemoteDefaultBranch(cwd);\n if (remote) set.add(remote);\n for (const b of extra) set.add(b);\n return set;\n}\nfunction getUpstream(cwd, branch) {\n try {\n return execFileSync(\"git\", [\n \"rev-parse\",\n \"--abbrev-ref\",\n `${branch}@{upstream}`\n ], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim() || null;\n } catch {\n return null;\n }\n}\nfunction resolveCommit(cwd, rev) {\n try {\n return execFileSync(\"git\", [\n \"rev-parse\",\n \"--verify\",\n \"--quiet\",\n `${rev}^{commit}`\n ], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim() || null;\n } catch {\n return null;\n }\n}\nfunction branchCarriesOtherFiles(cwd, allowed) {\n const toplevel = getRepoTopLevel(cwd);\n if (toplevel === null) return null;\n try {\n const defaultBranch = getConfiguredDefaultBranch(toplevel) ?? getRemoteDefaultBranch(toplevel) ?? \"main\";\n const mergeBase = execFileSync(\"git\", [\n \"merge-base\",\n \"HEAD\",\n `origin/${defaultBranch}`\n ], {\n cwd: toplevel,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n if (!mergeBase) return null;\n const names = (args) => execFileSync(\"git\", args, {\n cwd: toplevel,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().split(\"\\n\").filter((line) => line.length > 0);\n return [...names([\n \"diff\",\n \"--name-only\",\n mergeBase\n ]), ...names([\n \"ls-files\",\n \"--others\",\n \"--exclude-standard\"\n ])].some((name) => !allowed.includes(name));\n } catch {\n return null;\n }\n}\nfunction isPrimaryWorktree(cwd) {\n try {\n const gitDir = execFileSync(\"git\", [\"rev-parse\", \"--absolute-git-dir\"], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n const commonRaw = execFileSync(\"git\", [\"rev-parse\", \"--git-common-dir\"], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n if (!gitDir || !commonRaw) return false;\n const commonDir = resolve(cwd, commonRaw);\n return realpathSync(gitDir) === realpathSync(commonDir);\n } catch {\n return false;\n }\n}\n\n//#endregion\n//#region src/lib/git-short-options.ts\nconst VALUE_TAKING_LETTERS = {\n switch: /* @__PURE__ */ new Set([\"c\", \"C\"]),\n checkout: /* @__PURE__ */ new Set([\"b\", \"B\"]),\n branch: /* @__PURE__ */ new Set([\"u\"]),\n fetch: /* @__PURE__ */ new Set([\"o\", \"j\"])\n};\nfunction splitShortOptionCluster(token, sub) {\n if (!token.startsWith(\"-\") || token.startsWith(\"--\") || token.length <= 1) return;\n const valueLetters = VALUE_TAKING_LETTERS[sub];\n const letters = [];\n const chars = token.slice(1);\n for (let i = 0; i < chars.length; i++) {\n const ch = chars[i];\n letters.push(ch);\n if (valueLetters.has(ch)) return {\n letters,\n attachedValue: chars.slice(i + 1)\n };\n }\n return { letters };\n}\n\n//#endregion\n//#region src/lib/path-scope.ts\nfunction pathFlavorFor(base) {\n return /^[a-zA-Z]:[\\\\/]/.test(base) ? path.win32 : path.posix;\n}\nfunction resolveIn(base, target) {\n return pathFlavorFor(base).resolve(base, target);\n}\nfunction isWithin(dir, target) {\n const pp = pathFlavorFor(dir);\n const rel = pp.relative(dir, target);\n if (rel === \"\") return true;\n if (rel === \"..\" || rel.startsWith(\"..\" + pp.sep)) return false;\n return !pp.isAbsolute(rel);\n}\nfunction samePath(a, b) {\n return pathFlavorFor(a).relative(a, b) === \"\";\n}\nfunction targetsGitDir(filePath, cwd) {\n const pp = pathFlavorFor(cwd);\n const resolved = resolveIn(cwd, filePath);\n if (!isWithin(cwd, resolved)) return false;\n const rel = pp.relative(cwd, resolved);\n if (rel === \"\") return false;\n return rel.split(pp.sep).includes(\".git\");\n}\nfunction targetsNodeModules(filePath, cwd) {\n return resolveIn(cwd, filePath).split(/[/\\\\]/).includes(\"node_modules\");\n}\n\n//#endregion\n//#region src/rules.ts\nconst GIT_FLAGS_WITH_ARG = /* @__PURE__ */ new Set([\n \"-C\",\n \"-c\",\n \"--git-dir\",\n \"--work-tree\"\n]);\nfunction getGitSubcommand(tokens) {\n const args = tokens.slice(1);\n for (let i = 0; i < args.length; i++) {\n const t = args[i];\n if (GIT_FLAGS_WITH_ARG.has(t)) {\n i++;\n continue;\n }\n if (t.startsWith(\"-\")) continue;\n return t;\n }\n}\nconst WORKTREE_ADVICE = \"You're creating a feature branch in the primary worktree. An isolated worktree keeps this work independent of other windows, and with no commits on this branch yet, converting is cheap -- `git worktree add ../<dir> -b <branch>` (a sibling outside the repo, with its own dependency install). Claude Code keeps this session inside its own working tree, so the work continues in a new session opened in the sibling worktree. If you cannot open a session there, stay in the primary checkout on this branch and check `git branch --show-current` before each commit.\";\nfunction isGitBranchCreation(tokens) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"checkout\" && sub !== \"switch\") return false;\n const after = tokens.slice(tokens.indexOf(sub) + 1);\n const creationLetters = sub === \"checkout\" ? [\"b\", \"B\"] : [\"c\", \"C\"];\n return after.some((t) => {\n const cluster = splitShortOptionCluster(t, sub);\n return cluster !== void 0 && cluster.letters.some((l) => creationLetters.includes(l));\n });\n}\nfunction worktreeAdvice(tokens, ctx) {\n return isGitBranchCreation(tokens) && isPrimaryWorktree(ctx.cwd) ? WORKTREE_ADVICE : void 0;\n}\nconst GIT_FORCE_CREATE_LETTER = {\n switch: \"C\",\n checkout: \"B\"\n};\nconst GIT_FORCE_CREATE_LONG = {\n flag: \"--force-create\",\n shortest: \"--force-\"\n};\nfunction gitForceCreateTargets(after, letterSub) {\n const letter = GIT_FORCE_CREATE_LETTER[letterSub];\n const longFlag = GIT_FORCE_CREATE_LONG;\n const targets = [];\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (t === void 0) continue;\n if (letterSub === \"switch\") {\n const [flag, ...rest] = t.split(\"=\");\n if (flag !== void 0 && flag.length >= longFlag.shortest.length && longFlag.flag.startsWith(flag)) {\n const target = rest.length > 0 ? rest.join(\"=\") : after[i + 1];\n if (target !== void 0) targets.push(target);\n continue;\n }\n }\n const cluster = splitShortOptionCluster(t, letterSub);\n if (cluster === void 0 || !cluster.letters.includes(letter)) continue;\n if (cluster.attachedValue !== void 0 && cluster.attachedValue !== \"\") {\n targets.push(cluster.attachedValue);\n continue;\n }\n const next = after[i + 1];\n if (next !== void 0) targets.push(next);\n }\n return targets;\n}\nfunction gitCommandForceCreateTargets(tokens) {\n const sub = getGitSubcommand(tokens);\n if (sub === \"switch\" || sub === \"checkout\") return gitForceCreateTargets(tokens.slice(tokens.indexOf(sub) + 1), sub);\n if (sub === \"worktree\") {\n const worktreeIdx = tokens.indexOf(\"worktree\");\n if (tokens[worktreeIdx + 1] !== \"add\") return [];\n return gitForceCreateTargets(tokens.slice(worktreeIdx + 2), \"checkout\");\n }\n return [];\n}\nfunction isGitProtectedBranchForceCreate(tokens, ctx) {\n const targets = gitCommandForceCreateTargets(tokens);\n if (targets.length === 0) return false;\n const protectedSet = getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []);\n return targets.some((t) => protectedSet.has(t));\n}\nfunction isGitProtectedBranchRefWrite(tokens, ctx) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"branch\") return false;\n const { flags, positionals } = splitGitBranchArgs(tokens.slice(tokens.indexOf(sub) + 1));\n if (positionals.length === 0) return false;\n let deleteMode = false;\n let renameMode = false;\n let copyMode = false;\n let forceMode = false;\n for (const flag of flags) {\n if (flag.letters !== void 0) {\n if (flag.letters.includes(\"d\") || flag.letters.includes(\"D\")) deleteMode = true;\n if (flag.letters.includes(\"m\") || flag.letters.includes(\"M\")) renameMode = true;\n if (flag.letters.includes(\"c\") || flag.letters.includes(\"C\")) copyMode = true;\n if (flag.letters.includes(\"f\") || flag.letters.includes(\"M\") || flag.letters.includes(\"C\")) forceMode = true;\n continue;\n }\n if (flag.name === \"delete\") deleteMode = true;\n if (flag.name === \"move\") renameMode = true;\n if (flag.name === \"copy\") copyMode = true;\n if (flag.name === \"force\") forceMode = true;\n }\n const protectedSet = getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []);\n if (deleteMode && positionals.some((p) => protectedSet.has(p))) return true;\n if (renameMode && positionals.length >= 2 && protectedSet.has(positionals[0])) return true;\n if (forceMode) {\n const destination = renameMode || copyMode ? positionals[positionals.length - 1] : positionals[0];\n if (destination !== void 0 && protectedSet.has(destination)) return true;\n }\n return false;\n}\nfunction refspecDestination(refspec) {\n const spec = refspec.startsWith(\"+\") ? refspec.slice(1) : refspec;\n const colon = spec.lastIndexOf(\":\");\n return (colon === -1 ? spec : spec.slice(colon + 1)).replace(/^refs\\/heads\\//, \"\");\n}\nconst GIT_BRANCH_VALUE_FLAG_NAMES = /* @__PURE__ */ new Set([\n \"contains\",\n \"no-contains\",\n \"merged\",\n \"no-merged\",\n \"points-at\",\n \"sort\",\n \"format\",\n \"set-upstream-to\"\n]);\nfunction splitGitBranchArgs(after) {\n const flags = [];\n const positionals = [];\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (t === void 0) continue;\n if (!t.startsWith(\"-\")) {\n positionals.push(t);\n continue;\n }\n if (t.startsWith(\"--\") && t !== \"--\") {\n const eq = t.indexOf(\"=\");\n const name = eq > 0 ? t.slice(2, eq) : t.slice(2);\n flags.push({\n raw: t,\n name\n });\n if (eq < 0 && GIT_BRANCH_VALUE_FLAG_NAMES.has(name)) i++;\n continue;\n }\n const cluster = splitShortOptionCluster(t, \"branch\");\n if (cluster === void 0) {\n flags.push({ raw: t });\n continue;\n }\n flags.push({\n raw: t,\n letters: cluster.letters\n });\n if (cluster.attachedValue === \"\") i++;\n }\n return {\n flags,\n positionals\n };\n}\nconst GIT_FETCH_VALUE_FLAG_NAMES = /* @__PURE__ */ new Set([\n \"depth\",\n \"deepen\",\n \"shallow-since\",\n \"shallow-exclude\",\n \"negotiation-tip\",\n \"filter\",\n \"jobs\",\n \"server-option\",\n \"upload-pack\",\n \"refmap\",\n \"submodule-prefix\",\n \"recurse-submodules-default\"\n]);\nfunction splitGitFetchArgs(after) {\n const flags = [];\n const positionals = [];\n const refmaps = [];\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (t === void 0) continue;\n if (!t.startsWith(\"-\")) {\n positionals.push(t);\n continue;\n }\n if (t === \"--\") {\n flags.push({ raw: t });\n continue;\n }\n if (t.startsWith(\"--\")) {\n const eq = t.indexOf(\"=\");\n const name = eq > 0 ? t.slice(2, eq) : t.slice(2);\n flags.push({\n raw: t,\n name\n });\n if (name === \"refmap\") {\n if (eq > 0) refmaps.push(t.slice(eq + 1));\n else if (after[i + 1] !== void 0) refmaps.push(after[i + 1]);\n }\n if (eq < 0 && GIT_FETCH_VALUE_FLAG_NAMES.has(name)) i++;\n continue;\n }\n const cluster = splitShortOptionCluster(t, \"fetch\");\n if (cluster === void 0) {\n flags.push({ raw: t });\n continue;\n }\n flags.push({\n raw: t,\n letters: cluster.letters\n });\n if (cluster.attachedValue === \"\") i++;\n }\n return {\n flags,\n positionals,\n refmaps\n };\n}\nfunction fetchRefspecTarget(spec) {\n const body = spec.startsWith(\"+\") ? spec.slice(1) : spec;\n if (body.startsWith(\"^\")) return { kind: \"none\" };\n if (!body.includes(\":\")) return { kind: \"none\" };\n const destination = refspecDestination(body);\n if (destination === \"\") return { kind: \"none\" };\n if (destination.startsWith(\"refs/remotes/\")) return { kind: \"tracking\" };\n if (destination.startsWith(\"refs/\")) return { kind: \"other\" };\n return {\n kind: \"branch\",\n name: destination\n };\n}\nfunction globToAnchoredRegex(glob) {\n const escaped = glob.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n return new RegExp(`^${escaped.replace(/\\\\\\*/g, \".*\")}$`);\n}\nfunction isProtectedFetchBranch(name, protectedSet) {\n if (protectedSet.has(name)) return true;\n if (!name.includes(\"*\")) return false;\n const pattern = globToAnchoredRegex(name);\n for (const member of protectedSet) if (pattern.test(member)) return true;\n return false;\n}\nfunction isGitProtectedBranchFetchWrite(tokens, ctx) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"fetch\") return false;\n const { positionals, refmaps } = splitGitFetchArgs(tokens.slice(tokens.indexOf(sub) + 1));\n const protectedSet = getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []);\n return [...positionals, ...refmaps].some((spec) => {\n const target = fetchRefspecTarget(spec);\n if (target.kind !== \"branch\" || target.name === void 0) return false;\n return isProtectedFetchBranch(target.name, protectedSet);\n });\n}\nfunction isGitProtectedUpdateRef(tokens, ctx) {\n const after = tokens.slice(tokens.indexOf(\"update-ref\") + 1);\n let ref;\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (t === \"-m\") {\n i++;\n continue;\n }\n if (t.startsWith(\"-\")) continue;\n ref = t;\n break;\n }\n if (ref === void 0) return false;\n return getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []).has(ref.replace(/^refs\\/heads\\//, \"\"));\n}\nfunction gitEffectiveCwd(tokens, cwd) {\n let dir = cwd;\n const args = tokens.slice(1);\n for (let i = 0; i < args.length; i++) {\n const t = args[i];\n if (t === \"-C\") {\n const next = args[i + 1];\n if (next !== void 0) dir = path.resolve(dir, next);\n i++;\n continue;\n }\n if (GIT_FLAGS_WITH_ARG.has(t)) {\n i++;\n continue;\n }\n if (!t.startsWith(\"-\")) break;\n }\n return dir;\n}\nconst GIT_SWITCH_NEUTRAL_FLAGS = /* @__PURE__ */ new Set([\n \"-q\",\n \"--quiet\",\n \"-f\",\n \"--force\",\n \"--discard-changes\",\n \"-m\",\n \"--merge\",\n \"--no-track\",\n \"--progress\",\n \"--no-progress\",\n \"--guess\",\n \"--no-guess\",\n \"--ignore-other-worktrees\",\n \"--recurse-submodules\",\n \"--no-recurse-submodules\"\n]);\nconst GIT_SWITCH_CREATE_FLAGS = /* @__PURE__ */ new Set([\n \"--create\",\n \"--force-create\",\n \"--orphan\"\n]);\nconst GIT_CHECKOUT_PATH_FLAGS = /* @__PURE__ */ new Set([\n \"--ours\",\n \"--theirs\",\n \"-p\",\n \"--patch\",\n \"--conflict\",\n \"--pathspec-from-file\",\n \"--pathspec-file-nul\",\n \"--overlay\",\n \"--no-overlay\"\n]);\nconst GIT_SWITCH_NEUTRAL_LETTERS = /* @__PURE__ */ new Set([\n \"q\",\n \"f\",\n \"m\"\n]);\nfunction switchTarget(after, sub, isCheckoutBranch) {\n if (sub === \"checkout\" && after.includes(\"--\")) return void 0;\n const createLetters = sub === \"checkout\" ? [\"b\", \"B\"] : [\"c\", \"C\"];\n const known = (l) => GIT_SWITCH_NEUTRAL_LETTERS.has(l) || createLetters.includes(l);\n const positionals = [];\n let created;\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (!t.startsWith(\"-\")) {\n positionals.push(t);\n continue;\n }\n if (GIT_SWITCH_NEUTRAL_FLAGS.has(t)) continue;\n const eq = t.indexOf(\"=\");\n if (sub === \"checkout\" && GIT_CHECKOUT_PATH_FLAGS.has(eq === -1 ? t : t.slice(0, eq))) return;\n if (GIT_SWITCH_CREATE_FLAGS.has(eq === -1 ? t : t.slice(0, eq))) {\n created = eq === -1 ? after[++i] : t.slice(eq + 1);\n continue;\n }\n const cluster = splitShortOptionCluster(t, sub);\n if (cluster === void 0 || !cluster.letters.every(known)) return null;\n if (cluster.letters.some((l) => createLetters.includes(l))) created = cluster.attachedValue ? cluster.attachedValue : after[++i];\n }\n if (created !== void 0) return created;\n if (positionals.length !== 1) return void 0;\n const target = positionals[0];\n if (target === \"HEAD\" || target === \"@\") return void 0;\n if (sub === \"checkout\" && !isCheckoutBranch(target)) return void 0;\n return target;\n}\nfunction branchSwitch(tokens, ctx, resolved) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"switch\" && sub !== \"checkout\") return void 0;\n const dir = gitEffectiveCwd(tokens, ctx.cwd);\n const isCheckoutBranch = (name) => !resolved || getProtectedBranches(dir, ctx.repoProtectedBranches ?? []).has(name) || resolveCommit(dir, name) !== null;\n const target = switchTarget(tokens.slice(tokens.indexOf(sub) + 1), sub, isCheckoutBranch);\n if (!resolved) return target === void 0 ? void 0 : {\n dir,\n branch: null\n };\n return target === void 0 ? void 0 : {\n dir,\n branch: target\n };\n}\nconst GIT_CHECKED_OUT_WRITES = /* @__PURE__ */ new Set([\n \"commit\",\n \"merge\",\n \"rebase\",\n \"cherry-pick\",\n \"revert\",\n \"am\",\n \"pull\"\n]);\nfunction subcommandArgs(tokens, sub, valueFlags = /* @__PURE__ */ new Set()) {\n const after = tokens.slice(tokens.indexOf(sub) + 1);\n const flags = [];\n const positionals = [];\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (!t.startsWith(\"-\")) {\n positionals.push(t);\n continue;\n }\n flags.push(t);\n if (valueFlags.has(t)) i++;\n }\n return {\n flags,\n positionals\n };\n}\nconst GIT_PUSH_VALUE_FLAGS = /* @__PURE__ */ new Set([\n \"-o\",\n \"--push-option\",\n \"--repo\",\n \"--receive-pack\",\n \"--exec\"\n]);\nfunction isMergeFastForwardFromUpstream(tokens, upstream) {\n const { flags, positionals } = subcommandArgs(tokens, \"merge\");\n if (!flags.includes(\"--ff-only\") || positionals.length !== 1) return false;\n const ref = positionals[0];\n return ref === \"@{u}\" || ref === \"@{upstream}\" || upstream !== null && ref === upstream;\n}\nconst GIT_PULL_NO_REBASE = /* @__PURE__ */ new Set([\n \"--rebase=false\",\n \"--rebase=no\",\n \"--rebase=off\",\n \"--rebase=0\"\n]);\nfunction isPullFromUpstream(tokens, upstream) {\n const { flags, positionals } = subcommandArgs(tokens, \"pull\");\n if (flags.some((t) => t.startsWith(\"--rebase\") && !GIT_PULL_NO_REBASE.has(t) || t === \"--no-ff\" || !t.startsWith(\"--\") && t.slice(1).includes(\"r\"))) return false;\n if (positionals.length === 0) return true;\n if (positionals.length !== 2 || upstream === null) return false;\n const [remote, branch] = positionals;\n return `${remote}/${branch.replace(/^refs\\/heads\\//, \"\")}` === upstream;\n}\nfunction isRefMovingReset(tokens, cwd, head) {\n if (tokens.slice(tokens.indexOf(\"reset\") + 1).includes(\"--\")) return false;\n const { positionals } = subcommandArgs(tokens, \"reset\");\n if (positionals.length !== 1) return false;\n if (head !== \"HEAD\" && /^(HEAD|@)/.test(positionals[0])) return false;\n const target = resolveCommit(cwd, positionals[0]);\n if (target === null) return false;\n return target !== resolveCommit(cwd, head);\n}\nfunction pushDestinations(tokens, current) {\n const { positionals } = subcommandArgs(tokens, \"push\", GIT_PUSH_VALUE_FLAGS);\n const refspecs = positionals.slice(1);\n if (refspecs.length === 0) return null;\n const names = [];\n for (const spec of refspecs) {\n const dst = refspecDestination(spec);\n if (dst === \"HEAD\" || dst === \"@\") {\n if (current !== null) names.push(current);\n continue;\n }\n names.push(dst);\n }\n return names;\n}\nconst GIT_PUSH_EVERY_BRANCH = /* @__PURE__ */ new Set([\n \"--all\",\n \"--branches\",\n \"--mirror\"\n]);\nfunction leavesRefInPlace(tokens, sub) {\n const { flags } = subcommandArgs(tokens, sub);\n if (flags.includes(\"--abort\") || flags.includes(\"--quit\")) return true;\n return sub === \"commit\" && flags.includes(\"--dry-run\");\n}\nfunction movesProtectedRef(tokens, ctx) {\n const sub = getGitSubcommand(tokens);\n if (sub === void 0) return false;\n if (sub === \"switch\" || sub === \"checkout\" || sub === \"worktree\") return isGitProtectedBranchForceCreate(tokens, ctx);\n if (sub === \"branch\") return isGitProtectedBranchRefWrite(tokens, ctx);\n if (sub === \"fetch\") return isGitProtectedBranchFetchWrite(tokens, ctx);\n if (sub === \"update-ref\") return isGitProtectedUpdateRef(tokens, ctx);\n if (sub !== \"push\" && sub !== \"reset\" && !GIT_CHECKED_OUT_WRITES.has(sub)) return false;\n const cwd = gitEffectiveCwd(tokens, ctx.cwd);\n const pending = ctx.pendingBranches?.[cwd];\n const current = pending ? pending.branch : getCurrentBranch(cwd);\n const protectedSet = getProtectedBranches(cwd, ctx.repoProtectedBranches ?? []);\n const onProtected = current !== null && protectedSet.has(current);\n if (sub === \"push\") {\n const { flags } = subcommandArgs(tokens, \"push\", GIT_PUSH_VALUE_FLAGS);\n if (flags.some((f) => GIT_PUSH_EVERY_BRANCH.has(f))) return true;\n const destinations = pushDestinations(tokens, current);\n if (destinations === null) {\n if (flags.includes(\"--tags\")) return false;\n return onProtected;\n }\n return destinations.some((d) => protectedSet.has(d));\n }\n if (!onProtected) return false;\n if (leavesRefInPlace(tokens, sub)) return false;\n if (sub === \"reset\") return isRefMovingReset(tokens, cwd, pending ? current : \"HEAD\");\n if (sub === \"merge\" || sub === \"pull\") {\n const upstream = getUpstream(cwd, current);\n if (sub === \"merge\" && isMergeFastForwardFromUpstream(tokens, upstream)) return false;\n if (sub === \"pull\" && isPullFromUpstream(tokens, upstream)) return false;\n }\n return true;\n}\nconst PNPM_BUILTINS = /* @__PURE__ */ new Set([\n \"install\",\n \"i\",\n \"add\",\n \"remove\",\n \"rm\",\n \"uninstall\",\n \"un\",\n \"update\",\n \"up\",\n \"upgrade\",\n \"list\",\n \"ls\",\n \"ll\",\n \"la\",\n \"why\",\n \"view\",\n \"info\",\n \"show\",\n \"outdated\",\n \"audit\",\n \"licenses\",\n \"init\",\n \"create\",\n \"link\",\n \"unlink\",\n \"pack\",\n \"publish\",\n \"config\",\n \"set\",\n \"get\",\n \"store\",\n \"cache\",\n \"rebuild\",\n \"rb\",\n \"prune\",\n \"dedupe\",\n \"dlx\",\n \"exec\",\n \"run\",\n \"patch\",\n \"patch-commit\",\n \"recursive\",\n \"-r\"\n]);\nconst NPM_READ_OPS = /* @__PURE__ */ new Set([\n \"view\",\n \"info\",\n \"show\"\n]);\nfunction isNpmReadOp(tokens) {\n const sub = tokens[1];\n return sub !== void 0 && NPM_READ_OPS.has(sub);\n}\nfunction getNpmRedirectReason(tokens) {\n const sub = tokens[1];\n const BASE = \"This project uses pnpm.\";\n if (!sub) return `${BASE} Use \\`pnpm\\` instead of npm.`;\n if (sub === \"install\" || sub === \"i\" || sub === \"ci\") {\n if (tokens.slice(2).some((t) => !t.startsWith(\"-\"))) return `${BASE} Use \\`pnpm add <pkg>\\` instead of \\`npm install <pkg>\\`.`;\n return `${BASE} Use \\`pnpm install\\` instead of npm.`;\n }\n if (sub === \"add\") return `${BASE} Use \\`pnpm add <pkg>\\` instead of npm.`;\n if (sub === \"uninstall\" || sub === \"remove\" || sub === \"rm\" || sub === \"un\" || sub === \"r\") return `${BASE} Use \\`pnpm remove <pkg>\\` instead of npm.`;\n if (sub === \"run\" || sub === \"run-script\") return `${BASE} Use \\`pnpm run <script>\\` or \\`pnpm <script>\\` instead of npm.`;\n if (sub === \"test\" || sub === \"t\" || sub === \"tst\") return `${BASE} Use \\`pnpm test\\` instead of npm.`;\n if (sub === \"start\") return `${BASE} Use \\`pnpm start\\` instead of npm.`;\n if (sub === \"stop\") return `${BASE} Use \\`pnpm stop\\` instead of npm.`;\n if (sub === \"restart\") return `${BASE} Use \\`pnpm restart\\` instead of npm.`;\n if (sub === \"exec\") return `${BASE} Use \\`pnpm exec <cmd>\\` instead of npm.`;\n if (sub === \"list\" || sub === \"ls\" || sub === \"ll\" || sub === \"la\") return `${BASE} Use \\`pnpm list\\` instead of npm.`;\n if (sub === \"why\") return `${BASE} Use \\`pnpm why\\` instead of npm.`;\n if (sub === \"outdated\") return `${BASE} Use \\`pnpm outdated\\` instead of npm.`;\n if (sub === \"audit\") return `${BASE} Use \\`pnpm audit\\` instead of npm.`;\n if (sub === \"update\" || sub === \"up\" || sub === \"upgrade\") return `${BASE} Use \\`pnpm update\\` instead of npm.`;\n if (sub === \"init\") return `${BASE} Use \\`pnpm init\\` instead of npm.`;\n if (sub === \"publish\") return `${BASE} Use \\`pnpm publish\\` instead of npm.`;\n if (sub === \"link\") return `${BASE} Use \\`pnpm link\\` instead of npm.`;\n if (sub === \"unlink\") return `${BASE} Use \\`pnpm unlink\\` instead of npm.`;\n if (sub === \"pack\") return `${BASE} Use \\`pnpm pack\\` instead of npm.`;\n if (sub === \"cache\") return `${BASE} Use \\`pnpm store\\` instead of \\`npm cache\\`.`;\n if (sub === \"config\" || sub === \"set\" || sub === \"get\") return `${BASE} Use \\`pnpm config\\` instead of npm.`;\n return `${BASE} Use the equivalent pnpm command instead of npm.`;\n}\nconst LAST_POSITIONAL_DEST = /* @__PURE__ */ new Set([\n \"cp\",\n \"mv\",\n \"rsync\",\n \"install\",\n \"ln\"\n]);\nconst ALL_POSITIONAL_DEST = /* @__PURE__ */ new Set([\"tee\", \"truncate\"]);\nfunction explicitTargetDirectory(rest) {\n for (const [i, t] of rest.entries()) {\n if (t === \"-t\" || t === \"--target-directory\") return rest[i + 1] ?? null;\n if (t.startsWith(\"--target-directory=\")) return t.slice(19);\n }\n return null;\n}\nconst OUTPUT_REDIRECT = /^(?:\\d*|&)>{1,2}$/;\nconst OUTPUT_REDIRECT_ATTACHED = /^(?:\\d*|&)>{1,2}(.+)$/;\nconst INPUT_REDIRECT = /^\\d*<$/;\nconst INPUT_REDIRECT_ATTACHED = /^\\d*<([^<].*)$/;\nfunction spacedRedirectDirection(token) {\n if (OUTPUT_REDIRECT.test(token)) return \"out\";\n if (INPUT_REDIRECT.test(token)) return \"in\";\n return null;\n}\nfunction splitRedirects(tokens) {\n const redirects = [];\n const rest = [];\n for (let i = 0; i < tokens.length; i++) {\n const t = tokens[i];\n const spaced = spacedRedirectDirection(t);\n if (spaced !== null) {\n const next = tokens[i + 1];\n if (next !== void 0) redirects.push({\n direction: spaced,\n target: next\n });\n i++;\n continue;\n }\n const out = OUTPUT_REDIRECT_ATTACHED.exec(t);\n if (out) {\n redirects.push({\n direction: \"out\",\n target: out[1]\n });\n continue;\n }\n const input = INPUT_REDIRECT_ATTACHED.exec(t);\n if (input) {\n redirects.push({\n direction: \"in\",\n target: input[1]\n });\n continue;\n }\n rest.push(t);\n }\n return {\n redirects,\n rest\n };\n}\nfunction redirectTargets(tokens, direction) {\n return splitRedirects(tokens).redirects.filter((r) => r.direction === direction).map((r) => r.target);\n}\nfunction writeDestinations(tokens) {\n const cmd = path.basename(tokens[0] ?? \"\");\n const rest = tokens.slice(1);\n const dests = redirectTargets(tokens, \"out\");\n if (cmd === \"dd\") dests.push(...rest.filter((t) => t.startsWith(\"of=\")).map((t) => t.slice(3)));\n else if (cmd === \"sed\") {\n if (rest.some((t) => t === \"--in-place\" || /^-[a-zA-Z]*i/.test(t))) dests.push(...rest.filter((t) => !t.startsWith(\"-\")));\n } else if (ALL_POSITIONAL_DEST.has(cmd)) dests.push(...rest.filter((t) => !t.startsWith(\"-\")));\n else if (LAST_POSITIONAL_DEST.has(cmd)) {\n if (cmd === \"cp\" && rest.includes(\"--remove-destination\")) return dests;\n const targetDir = explicitTargetDirectory(rest);\n if (targetDir !== null) dests.push(targetDir);\n else {\n const last = rest.findLast((t) => !t.startsWith(\"-\"));\n if (last !== void 0) dests.push(last);\n }\n }\n return dests;\n}\nfunction writesIntoNodeModules(tokens, ctx) {\n return writeDestinations(tokens).some((d) => targetsNodeModules(d, ctx.cwd));\n}\nconst NODE_MODULES_WRITE_REASON = \"Don't write into node_modules/. Its files are hardlinks into pnpm's global store, so overwriting one in place (plain `cp`, `>` redirect, `sed -i`, ...) writes through the link and corrupts that package for every project on this machine -- silently, since the directory you wrote into ends up fine. To test a local build as an installed dependency, run it directly (`node packages/<pkg>/dist/bin.mjs <cmd>`), or install into a scratch tree with an isolated store (`pnpm add --store-dir <scratch> <pkg>`). If you must replace a file in place, `cp --remove-destination` is safe -- it unlinks the target first. Run `pnpm store status` to check whether the store is already damaged.\";\nfunction isChmod777(tokens) {\n return tokens.some((t) => t === \"777\" || t === \"0777\");\n}\nfunction isGitForcePush(tokens) {\n const pushIdx = tokens.indexOf(\"push\");\n if (pushIdx === -1) return false;\n return tokens.slice(pushIdx + 1).some((t) => t === \"--force\" || t === \"-f\");\n}\nconst ENV_READ_FILE_COMMANDS = /* @__PURE__ */ new Set([\n \"cat\",\n \"head\",\n \"tail\",\n \"less\",\n \"more\",\n \"source\",\n \".\"\n]);\nconst GREP_FAMILY_COMMANDS = /* @__PURE__ */ new Set([\n \"grep\",\n \"rg\",\n \"ag\"\n]);\nfunction grepFileOperands(rest) {\n const files = [];\n let patternAccountedFor = false;\n let optionsEnded = false;\n for (let i = 0; i < rest.length; i++) {\n const t = rest[i];\n if (!optionsEnded && t === \"--\") {\n optionsEnded = true;\n continue;\n }\n if (!optionsEnded) {\n if (t === \"-f\" || t === \"--file\") {\n const value = rest[i + 1];\n if (value !== void 0) {\n files.push(value);\n i++;\n }\n patternAccountedFor = true;\n continue;\n }\n if (t.startsWith(\"--file=\")) {\n files.push(t.slice(7));\n patternAccountedFor = true;\n continue;\n }\n if (t.startsWith(\"-f\") && t !== \"-f\" && !t.startsWith(\"--\")) {\n files.push(t.slice(2));\n patternAccountedFor = true;\n continue;\n }\n if (t === \"-e\" || t === \"--regexp\") {\n i++;\n patternAccountedFor = true;\n continue;\n }\n if (t.startsWith(\"--regexp=\") || t.startsWith(\"-e\") && t !== \"-e\" && !t.startsWith(\"--\")) {\n patternAccountedFor = true;\n continue;\n }\n if (t.startsWith(\"-\")) continue;\n }\n if (!patternAccountedFor) {\n patternAccountedFor = true;\n continue;\n }\n files.push(t);\n }\n return files;\n}\nfunction envFileOperands(tokens) {\n const cmd = path.basename(tokens[0] ?? \"\");\n const { redirects, rest } = splitRedirects(tokens.slice(1));\n const operands = redirects.filter((r) => r.direction === \"in\").map((r) => r.target);\n if (GREP_FAMILY_COMMANDS.has(cmd)) operands.push(...grepFileOperands(rest));\n else if (ENV_READ_FILE_COMMANDS.has(cmd)) operands.push(...rest.filter((t) => !t.startsWith(\"-\")));\n return operands;\n}\nconst HEALTH_CEILING_FILES = [\"health-baseline.json\", \".tooling.json\"];\nconst HEALTH_BASELINE_FILE = \"health-baseline.json\";\nconst HEALTH_RAISE_REASON = \"A health ceiling raise travels alone: open a ceiling-only pull request -- only health-baseline.json and .tooling.json changed -- for a human to merge, or split the module. `bst health:check` refuses the raise beside code either way. To restore the default branch's copy run `git show origin/<default>:health-baseline.json > health-baseline.json`, and to adopt lower counts run `bst health:check --update-baseline`.\";\nfunction writesHealthBaseline(tokens, cwd) {\n const destinations = writeDestinations(tokens);\n if (!destinations.some((dest) => baseName(dest) === \"health-baseline.json\")) return false;\n const root = getRepoTopLevel(cwd) ?? cwd;\n return destinations.some((dest) => samePath(resolveIn(cwd, dest), path.join(root, HEALTH_BASELINE_FILE)));\n}\nfunction baseName(target) {\n const segments = target.split(/[/\\\\]/);\n return segments[segments.length - 1] ?? \"\";\n}\nfunction isBaselineRestore(tokens) {\n if (tokens[0] !== \"git\") return false;\n let i = 1;\n while (tokens[i]?.startsWith(\"-\") === true) i += GIT_FLAGS_WITH_ARG.has(tokens[i] ?? \"\") ? 2 : 1;\n if (tokens[i] !== \"show\") return false;\n return tokens.slice(i + 1).some((token) => token.includes(\":\") && baseName(token.slice(token.lastIndexOf(\":\") + 1)) === \"health-baseline.json\");\n}\nfunction isHealthBaselineReset(tokens) {\n return tokens.includes(\"health:check\") && tokens.includes(\"--update-baseline\") && tokens.includes(\"--reset\");\n}\nfunction raiseTravelsWithCode(cwd) {\n return branchCarriesOtherFiles(cwd, HEALTH_CEILING_FILES) === true;\n}\nconst denyRules = [\n {\n name: \"sudo\",\n match: /^sudo$/,\n action: \"deny\",\n reason: \"Don't use sudo. If you need elevated permissions, ask me to do it manually.\"\n },\n {\n name: \"git-force-push\",\n match: /^git$/,\n action: \"deny\",\n when: (tokens) => isGitForcePush(tokens),\n reason: \"Use `git push --force-with-lease` instead of `--force`.\"\n },\n {\n name: \"git-protected-branch-write\",\n match: /^git$/,\n action: \"deny\",\n when: (tokens, ctx) => movesProtectedRef(tokens, ctx),\n reason: \"This would move a protected branch (main, master, the remote's default branch, or one listed in .claude/hooks/bst-hook.settings.json#protectedBranches). Work on a feature branch in a worktree: `git worktree add ../<repo>-<topic> -b feat/<topic>`, then commit and push there. To bring `main` up to date, use `git pull --ff-only`.\"\n },\n {\n name: \"chmod-777\",\n match: /^chmod$/,\n action: \"deny\",\n when: (tokens) => isChmod777(tokens),\n reason: \"Don't use 777 permissions. Use more restrictive permissions.\"\n },\n {\n name: \"node-modules-write\",\n match: /./,\n action: \"deny\",\n when: (tokens, ctx) => writesIntoNodeModules(tokens, ctx),\n reason: NODE_MODULES_WRITE_REASON\n },\n {\n name: \"env-file-read\",\n match: /./,\n action: \"deny\",\n when: (tokens) => envFileOperands(tokens).some(isEnvFile),\n reason: \"Don't read .env files. These contain secrets that should not appear in the conversation.\"\n },\n {\n name: \"env-file-write\",\n match: /./,\n action: \"deny\",\n when: (tokens) => redirectTargets(tokens, \"out\").some(isEnvFile),\n reason: \"Don't write to .env files. These contain secrets.\"\n },\n {\n name: \"health-baseline-write\",\n match: /./,\n action: \"deny\",\n when: (tokens, ctx) => writesHealthBaseline(tokens, ctx.cwd) && !isBaselineRestore(tokens) && raiseTravelsWithCode(ctx.cwd),\n reason: HEALTH_RAISE_REASON\n },\n {\n name: \"health-baseline-reset\",\n match: /^bst$/,\n action: \"deny\",\n when: (tokens, ctx) => isHealthBaselineReset(tokens) && raiseTravelsWithCode(ctx.cwd),\n reason: HEALTH_RAISE_REASON\n },\n {\n name: \"npx-redirect\",\n match: /^npx$/,\n action: \"deny\",\n reason: \"This project uses pnpm. Use `pnpm dlx <pkg>` or `pnpm exec <cmd>` instead of npx.\"\n },\n {\n name: \"mise-self-update\",\n match: /^mise$/,\n action: \"deny\",\n when: (tokens) => tokens[1] === \"self-update\",\n reason: \"Don't self-update mise. The version is Renovate-managed; ask a human to bump the pin.\"\n }\n];\n\n//#endregion\n//#region src/lib/script-resolver.ts\nconst PackageJsonSchema = object({ scripts: record(string(), unknown()) });\nfunction resolveScript(scriptName, cwd) {\n const scripts = readScripts(cwd);\n if (!scripts) return null;\n const main = scripts[scriptName];\n if (typeof main !== \"string\") return null;\n const pre = scripts[`pre${scriptName}`];\n const post = scripts[`post${scriptName}`];\n return {\n pre: typeof pre === \"string\" ? pre : null,\n main,\n post: typeof post === \"string\" ? post : null\n };\n}\nfunction resolveFilteredScript(scriptName, filterPkg, cwd) {\n const pkgDir = findWorkspacePackageDir(filterPkg, cwd);\n if (!pkgDir) return null;\n const script = resolveScript(scriptName, pkgDir);\n if (!script) return null;\n return {\n ...script,\n pkgDir\n };\n}\nfunction resolveRecursiveScripts(scriptName, rootDir) {\n const pkgDirs = listWorkspacePackageDirs(rootDir);\n if (!pkgDirs) return null;\n const results = [];\n for (const pkgDir of pkgDirs) {\n const resolved = resolveScript(scriptName, pkgDir);\n if (resolved) results.push({\n ...resolved,\n pkgDir\n });\n }\n return results;\n}\nfunction listWorkspacePackageDirs(rootDir) {\n const patterns = readWorkspacePatterns(rootDir);\n if (!patterns) return null;\n const dirs = [];\n for (const pattern of patterns) {\n if (!pattern.endsWith(\"/*\")) continue;\n const baseDir = pattern.slice(0, -2);\n const parentDir = resolve(rootDir, baseDir);\n let entries;\n try {\n entries = readdirSync(parentDir);\n } catch {\n continue;\n }\n for (const entry of entries) {\n const entryDir = join(parentDir, entry);\n try {\n if (!statSync(entryDir).isDirectory()) continue;\n } catch {\n continue;\n }\n dirs.push(entryDir);\n }\n }\n return dirs.length > 0 ? dirs : null;\n}\nfunction findWorkspacePackageDir(pkgName, rootDir) {\n const pkgDirs = listWorkspacePackageDirs(rootDir);\n if (!pkgDirs) return null;\n for (const entryDir of pkgDirs) {\n const name = readPackageName(entryDir);\n if (name === pkgName || name === `@${pkgName}`) return entryDir;\n if (entryDir.split(\"/\").pop() === pkgName) return entryDir;\n if (name) {\n if ((name.startsWith(\"@\") ? name.split(\"/\")[1] : name) === pkgName) return entryDir;\n }\n }\n return null;\n}\nfunction readWorkspacePatterns(rootDir) {\n try {\n const lines = readFileSync(join(rootDir, \"pnpm-workspace.yaml\"), \"utf-8\").split(\"\\n\");\n const packagesIdx = lines.findIndex((l) => /^packages\\s*:/.test(l));\n if (packagesIdx === -1) return null;\n const patterns = [];\n for (let i = packagesIdx + 1; i < lines.length; i++) {\n const line = lines[i];\n const match = /^\\s+-\\s+(.+)$/.exec(line);\n if (!match) break;\n patterns.push(match[1].trim().replace(/['\"`]/g, \"\"));\n }\n return patterns.length > 0 ? patterns : null;\n } catch {\n return null;\n }\n}\nconst PackageNameSchema = object({ name: string() });\nfunction readPackageName(dir) {\n try {\n const raw = readFileSync(join(dir, \"package.json\"), \"utf-8\");\n const result = PackageNameSchema.safeParse(JSON.parse(raw));\n if (!result.success) return null;\n return result.data.name;\n } catch {\n return null;\n }\n}\nfunction readScripts(cwd) {\n try {\n const raw = readFileSync(join(cwd, \"package.json\"), \"utf-8\");\n const result = PackageJsonSchema.safeParse(JSON.parse(raw));\n if (!result.success) return null;\n return result.data.scripts;\n } catch {\n return null;\n }\n}\n\n//#endregion\n//#region src/lib/workspace-root.ts\nfunction findWorkspaceRoot(cwd) {\n let dir = resolve(cwd);\n while (true) {\n if (existsSync(join(dir, \"pnpm-workspace.yaml\"))) return dir;\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\n//#endregion\n//#region src/evaluate.ts\nconst MAX_EVAL_DEPTH = 5;\nfunction subshellBody(command) {\n if (!command.startsWith(\"(\") || !command.endsWith(\")\")) return null;\n let depth = 0;\n let quote = null;\n for (let i = 0; i < command.length; i++) {\n const ch = command[i];\n if (quote !== null) {\n if (ch === quote) quote = null;\n else if (ch === \"\\\\\" && quote === \"\\\"\") i++;\n continue;\n }\n if (ch === \"\\\\\") i++;\n else if (ch === \"'\" || ch === \"\\\"\") quote = ch;\n else if (ch === \"(\") depth++;\n else if (ch === \")\") {\n depth--;\n if (depth === 0) return i === command.length - 1 ? command.slice(1, -1) : null;\n }\n }\n return null;\n}\nfunction extractCdDir(segment, ctx) {\n const tokenResult = tokenize(segment);\n if (!tokenResult.ok || tokenResult.tokens.length !== 2) return null;\n const tokens = tokenResult.tokens;\n if (tokens[0] !== \"cd\") return null;\n const dir = tokens[1];\n if (path.isAbsolute(dir)) return dir;\n return path.resolve(ctx.cwd, dir);\n}\nfunction extractBranchSwitch(segment, ctx) {\n const tokenResult = tokenize(segment);\n if (!tokenResult.ok) return void 0;\n let tokens = tokenResult.tokens;\n let envSkipped = 0;\n while (envSkipped < tokens.length && /^\\w+=/.test(tokens[envSkipped])) envSkipped++;\n tokens = tokens.slice(envSkipped);\n if (tokens.length === 0 || path.basename(tokens[0]) !== \"git\") return void 0;\n return branchSwitch(tokens, ctx, checkInjection(tokens, segment).safe);\n}\nfunction evaluateCommand(command, ctx) {\n if (ctx.depth >= MAX_EVAL_DEPTH) return {\n action: \"none\",\n rule: null,\n reason: \"max evaluation depth exceeded\"\n };\n const trimmed = command.trim();\n if (!trimmed) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n const body = subshellBody(trimmed);\n if (body !== null) return evaluateCommand(body, {\n ...ctx,\n depth: ctx.depth + 1\n });\n const segments = splitCommand(trimmed);\n if (segments.length === 0) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n let firstNone = null;\n const currentCtx = { ...ctx };\n let unknownAfterNext;\n for (const segment of segments) {\n if (segment.operator === \"&&\") {\n const cdDir = extractCdDir(segment.command, currentCtx);\n if (cdDir) currentCtx.cwd = cdDir;\n }\n const result = evaluateSegment(segment.command, currentCtx);\n if (result.action === \"deny\") return result;\n if (!firstNone) firstNone = result;\n if (unknownAfterNext) {\n currentCtx.pendingBranches = {\n ...currentCtx.pendingBranches,\n [unknownAfterNext.dir]: unknownAfterNext\n };\n unknownAfterNext = void 0;\n }\n const switched = extractBranchSwitch(segment.command, currentCtx);\n if (switched) {\n const unknown = {\n dir: switched.dir,\n branch: null\n };\n if (segment.operator === \"&&\") currentCtx.pendingBranches = {\n ...currentCtx.pendingBranches,\n [switched.dir]: switched\n };\n else if (segment.operator === \"||\") unknownAfterNext = unknown;\n else currentCtx.pendingBranches = {\n ...currentCtx.pendingBranches,\n [switched.dir]: unknown\n };\n }\n }\n return firstNone ?? {\n action: \"none\",\n rule: null,\n reason: null\n };\n}\nfunction evaluateResolvedScript(resolved, ctx) {\n if (resolved.pre) {\n const preResult = evaluateCommand(resolved.pre, ctx);\n if (preResult.action === \"deny\") return preResult;\n }\n const mainResult = evaluateCommand(resolved.main, ctx);\n if (mainResult.action === \"deny\") return mainResult;\n if (resolved.post) {\n const postResult = evaluateCommand(resolved.post, ctx);\n if (postResult.action === \"deny\") return postResult;\n }\n return {\n action: \"none\",\n rule: null,\n reason: null\n };\n}\nfunction evaluateSegment(rawSegment, ctx) {\n const trimmed = rawSegment.trim();\n const body = subshellBody(trimmed);\n if (body !== null) return evaluateCommand(body, {\n ...ctx,\n depth: ctx.depth + 1\n });\n if (trimmed.startsWith(\"(\") && trimmed.endsWith(\")\")) return {\n action: \"none\",\n rule: null,\n reason: \"subshell\"\n };\n const tokenResult = tokenize(rawSegment);\n if (!tokenResult.ok) return {\n action: \"none\",\n rule: null,\n reason: tokenResult.error\n };\n let tokens = tokenResult.tokens;\n if (tokens.length === 0) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n let envSkipped = 0;\n while (envSkipped < tokens.length && /^\\w+=/.test(tokens[envSkipped])) envSkipped++;\n if (envSkipped >= tokens.length) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n tokens = tokens.slice(envSkipped);\n if (writesIntoNodeModules(tokens, ctx)) return {\n action: \"deny\",\n rule: \"node-modules-write\",\n reason: NODE_MODULES_WRITE_REASON\n };\n const injection = checkInjection(tokens, rawSegment);\n if (!injection.safe) return {\n action: \"none\",\n rule: null,\n reason: injection.reason\n };\n const SAFE_REDIRECT_TOKENS = /* @__PURE__ */ new Set([\n \"2>&1\",\n \"2>/dev/null\",\n \">/dev/null\",\n \"1>/dev/null\"\n ]);\n tokens = tokens.filter((t, i) => {\n if (SAFE_REDIRECT_TOKENS.has(t)) return false;\n if ((t === \">\" || t === \">>\") && tokens[i + 1] === \"/dev/null\") return false;\n if (t === \"/dev/null\" && i > 0 && (tokens[i - 1] === \">\" || tokens[i - 1] === \">>\")) return false;\n return true;\n });\n const cmdName = path.basename(tokens[0]);\n if (cmdName === \"command\" && tokens.length > 1) return evaluateCommand(joinTokens(tokens.slice(1)), {\n ...ctx,\n depth: ctx.depth + 1\n });\n if (cmdName === \"env\") {\n let envIdx = 1;\n while (envIdx < tokens.length && /^\\w+=/.test(tokens[envIdx])) envIdx++;\n if (envIdx >= tokens.length) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n return evaluateCommand(joinTokens(tokens.slice(envIdx)), {\n ...ctx,\n depth: ctx.depth + 1\n });\n }\n if (cmdName === \"pnpm\") return evaluatePnpm(tokens, ctx);\n if (cmdName === \"npm\") return evaluateNpm(tokens);\n if (cmdName === \"mise\" && tokens[1] === \"exec\") {\n const inner = miseExecInner(tokens);\n if (!inner) return {\n action: \"none\",\n rule: null,\n reason: \"bare mise exec\"\n };\n return evaluateCommand(joinTokens(inner), {\n ...ctx,\n depth: ctx.depth + 1\n });\n }\n if (cmdName === \"bash\" || cmdName === \"sh\" || cmdName === \"zsh\") return evaluateShellC(tokens, ctx);\n if (cmdName === \"xargs\") return evaluateXargs(tokens, ctx);\n if (cmdName === \"node\" && (tokens.includes(\"-e\") || tokens.includes(\"-p\") || tokens.includes(\"--eval\") || tokens.includes(\"--print\"))) return {\n action: \"none\",\n rule: null,\n reason: \"node eval\"\n };\n for (const rule of denyRules) if (rule.match.test(cmdName)) {\n if (!rule.when || rule.when(tokens, ctx)) return {\n action: \"deny\",\n rule: rule.name,\n reason: rule.reason ?? null\n };\n }\n const advice = cmdName === \"git\" ? worktreeAdvice(tokens, ctx) : void 0;\n return {\n action: \"none\",\n rule: null,\n reason: `no rule matched: ${cmdName}`,\n ...advice ? { advice } : {}\n };\n}\nfunction miseExecInner(tokens) {\n const dashIdx = tokens.indexOf(\"--\");\n if (dashIdx === -1) return void 0;\n const inner = tokens.slice(dashIdx + 1);\n return inner.length > 0 ? inner : void 0;\n}\nfunction scanPnpmFlags(subTokens, from, state) {\n for (let i = from; i < subTokens.length; i++) {\n const t = subTokens[i];\n if (t === \"--filter\" || t.startsWith(\"--filter=\") || t === \"-F\" || t.startsWith(\"-F=\")) return {\n name: void 0,\n filterIdx: i\n };\n if (t === \"-r\" || t === \"--recursive\") {\n state.isRecursive = true;\n continue;\n }\n if (t === \"-w\" || t === \"--workspace-root\") {\n state.isWorkspaceRoot = true;\n continue;\n }\n if (t === \"-C\" || t === \"--dir\") {\n state.dirOverride = subTokens[++i];\n continue;\n }\n if (t.startsWith(\"-C=\") || t.startsWith(\"--dir=\")) {\n state.dirOverride = t.split(\"=\", 2)[1];\n continue;\n }\n if (t.startsWith(\"-\") && !PNPM_BUILTINS.has(t)) continue;\n return { name: t };\n }\n return { name: void 0 };\n}\nfunction evaluatePnpm(tokens, ctx) {\n const subTokens = tokens.slice(1);\n const state = {\n isRecursive: false,\n isWorkspaceRoot: false,\n dirOverride: void 0\n };\n const scan = scanPnpmFlags(subTokens, 0, state);\n if (scan.filterIdx !== void 0) return evaluatePnpmFilter(tokens, subTokens, scan.filterIdx, ctx);\n const subcommand = scan.name;\n const subcommandIdx = subcommand ? subTokens.indexOf(subcommand) : -1;\n if (!subcommand) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n if (subcommand === \"dlx\") return {\n action: \"none\",\n rule: null,\n reason: \"pnpm dlx\"\n };\n if (subcommand === \"add\") return {\n action: \"none\",\n rule: null,\n reason: \"pnpm add\"\n };\n if (subcommand === \"exec\") {\n const execArgs = subTokens.slice(subcommandIdx + 1);\n if (execArgs.length === 0) return {\n action: \"none\",\n rule: null,\n reason: \"bare pnpm exec\"\n };\n return evaluateCommand(joinTokens(execArgs), {\n ...ctx,\n depth: ctx.depth + 1\n });\n }\n let scriptName;\n let { isRecursive, isWorkspaceRoot, dirOverride } = state;\n if (subcommand === \"run\") {\n const runScan = scanPnpmFlags(subTokens, subcommandIdx + 1, state);\n ({isRecursive, isWorkspaceRoot, dirOverride} = state);\n if (runScan.filterIdx !== void 0) return evaluatePnpmFilter(tokens, subTokens, runScan.filterIdx, ctx);\n if (!runScan.name) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm run without script name\"\n };\n scriptName = runScan.name;\n } else if (!PNPM_BUILTINS.has(subcommand)) scriptName = subcommand;\n else return {\n action: \"none\",\n rule: null,\n reason: null\n };\n if (isRecursive) return evaluatePnpmRecursive(scriptName, ctx);\n if (isWorkspaceRoot) {\n const root = findWorkspaceRoot(ctx.cwd);\n if (!root) return {\n action: \"none\",\n rule: null,\n reason: \"workspace root not found\"\n };\n return evaluatePnpmScript(scriptName, {\n ...ctx,\n cwd: root\n });\n }\n if (dirOverride) {\n const resolved = path.resolve(ctx.cwd, dirOverride);\n return evaluatePnpmScript(scriptName, {\n ...ctx,\n cwd: resolved\n });\n }\n return evaluatePnpmScript(scriptName, ctx);\n}\nfunction evaluatePnpmScript(scriptName, ctx) {\n const resolved = resolveScript(scriptName, ctx.cwd);\n if (!resolved) return {\n action: \"none\",\n rule: null,\n reason: \"script resolution failed\"\n };\n const res = evaluateResolvedScript(resolved, {\n ...ctx,\n depth: ctx.depth + 1\n });\n if (res.action === \"deny\") return res;\n return {\n action: \"none\",\n rule: `pnpm-script:${scriptName}`,\n reason: null\n };\n}\nfunction evaluatePnpmRecursive(scriptName, ctx) {\n const resolved = resolveRecursiveScripts(scriptName, ctx.cwd);\n if (!resolved || resolved.length === 0) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm recursive\"\n };\n for (const pkg of resolved) {\n const res = evaluateResolvedScript(pkg, {\n ...ctx,\n depth: ctx.depth + 1,\n cwd: pkg.pkgDir ?? ctx.cwd\n });\n if (res.action === \"deny\") return res;\n }\n return {\n action: \"none\",\n rule: `pnpm-recursive:${scriptName}`,\n reason: null\n };\n}\nfunction evaluateNpm(tokens) {\n if (isNpmReadOp(tokens)) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n return {\n action: \"deny\",\n rule: \"npm-redirect\",\n reason: getNpmRedirectReason(tokens)\n };\n}\nfunction evaluatePnpmFilter(_tokens, subTokens, filterIdx, ctx) {\n const filterPkgs = [];\n let cursor = filterIdx;\n while (cursor < subTokens.length) {\n const t = subTokens[cursor];\n if (t === \"--filter\" || t === \"-F\") {\n const pkg = subTokens[cursor + 1];\n if (!pkg || pkg.startsWith(\"-\")) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm --filter without package\"\n };\n filterPkgs.push(pkg);\n cursor += 2;\n } else if (t.startsWith(\"--filter=\") || t.startsWith(\"-F=\")) {\n const pkg = t.startsWith(\"-F=\") ? t.slice(3) : t.slice(9);\n if (!pkg) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm --filter without package\"\n };\n filterPkgs.push(pkg);\n cursor += 1;\n } else break;\n }\n if (filterPkgs.length === 0) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm --filter without package\"\n };\n let filterSubcommand;\n let subcmdIdx = -1;\n for (let i = cursor; i < subTokens.length; i++) {\n const t = subTokens[i];\n if (t === \"--filter\" || t === \"-F\") {\n i += 1;\n continue;\n }\n if (t.startsWith(\"--filter=\") || t.startsWith(\"-F=\")) continue;\n if (t.startsWith(\"-\") && !PNPM_BUILTINS.has(t)) continue;\n filterSubcommand = t;\n subcmdIdx = i;\n break;\n }\n if (!filterSubcommand) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm --filter without subcommand\"\n };\n if (PNPM_BUILTINS.has(filterSubcommand) && filterSubcommand !== \"run\" && filterSubcommand !== \"exec\") return {\n action: \"none\",\n rule: null,\n reason: null\n };\n if (filterSubcommand === \"exec\") {\n const execArgs = subTokens.slice(subcmdIdx + 1);\n if (execArgs.length === 0) return {\n action: \"none\",\n rule: null,\n reason: \"bare pnpm exec\"\n };\n return evaluateCommand(joinTokens(execArgs), {\n ...ctx,\n depth: ctx.depth + 1\n });\n }\n const scriptName = filterSubcommand === \"run\" ? subTokens[subcmdIdx + 1] : filterSubcommand;\n if (!scriptName || scriptName.startsWith(\"-\")) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm --filter run without script name\"\n };\n if (PNPM_BUILTINS.has(scriptName) && scriptName !== \"run\") return {\n action: \"none\",\n rule: null,\n reason: null\n };\n for (const pkg of filterPkgs) {\n const resolved = resolveFilteredScript(scriptName, pkg, ctx.cwd);\n if (!resolved) return {\n action: \"none\",\n rule: null,\n reason: \"filtered script resolution failed\"\n };\n const res = evaluateResolvedScript(resolved, {\n ...ctx,\n depth: ctx.depth + 1,\n cwd: resolved.pkgDir ?? ctx.cwd\n });\n if (res.action === \"deny\") return res;\n }\n return {\n action: \"none\",\n rule: `pnpm-filter:${filterPkgs.join(\",\")}:${scriptName}`,\n reason: null\n };\n}\nconst SHELL_LOGIN_FLAGS = /* @__PURE__ */ new Set([\"-l\", \"--login\"]);\nfunction evaluateShellC(tokens, ctx) {\n let i = 1;\n while (i < tokens.length && SHELL_LOGIN_FLAGS.has(tokens[i])) i++;\n const flag = tokens[i];\n if (flag === void 0 || !/^-[lc]+$/.test(flag) || !flag.includes(\"c\")) return {\n action: \"none\",\n rule: null,\n reason: \"shell without a plain -c\"\n };\n const cmdArg = tokens[i + 1];\n if (!cmdArg) return {\n action: \"none\",\n rule: null,\n reason: \"shell -c without command\"\n };\n if (tokens.length > i + 2) return {\n action: \"none\",\n rule: null,\n reason: \"shell -c with positional args\"\n };\n return evaluateCommand(cmdArg, {\n ...ctx,\n depth: ctx.depth + 1\n });\n}\nconst XARGS_ARG_FLAGS = /* @__PURE__ */ new Set([\n \"-I\",\n \"-d\",\n \"-n\",\n \"-P\",\n \"-s\",\n \"-L\",\n \"-E\",\n \"-a\",\n \"--max-args\",\n \"--max-procs\",\n \"--delimiter\",\n \"--replace\",\n \"--max-lines\",\n \"--max-chars\",\n \"--arg-file\",\n \"--process-slot-var\"\n]);\nconst XARGS_BOOL_FLAGS = /* @__PURE__ */ new Set([\n \"-0\",\n \"--null\",\n \"-t\",\n \"--verbose\",\n \"-r\",\n \"--no-run-if-empty\",\n \"-p\",\n \"--interactive\",\n \"-x\",\n \"--exit\",\n \"--show-limits\"\n]);\nfunction evaluateXargs(tokens, ctx) {\n let i = 1;\n while (i < tokens.length) {\n const t = tokens[i];\n if (!t.startsWith(\"-\")) break;\n if (XARGS_ARG_FLAGS.has(t)) {\n i += 2;\n continue;\n }\n const eqIdx = t.indexOf(\"=\");\n if (eqIdx !== -1) {\n const flagName = t.slice(0, eqIdx);\n if (XARGS_ARG_FLAGS.has(flagName)) {\n i++;\n continue;\n }\n }\n if (t.length > 2 && XARGS_ARG_FLAGS.has(t.slice(0, 2))) {\n i++;\n continue;\n }\n if (XARGS_BOOL_FLAGS.has(t)) {\n i++;\n continue;\n }\n return {\n action: \"none\",\n rule: null,\n reason: \"xargs with unrecognised flags\"\n };\n }\n if (i >= tokens.length) return {\n action: \"none\",\n rule: null,\n reason: \"xargs without command\"\n };\n return evaluateCommand(joinTokens(tokens.slice(i)), {\n ...ctx,\n depth: ctx.depth + 1\n });\n}\n\n//#endregion\n//#region src/file-rules.ts\nconst READ_TOOLS = /* @__PURE__ */ new Set([\n \"Read\",\n \"Glob\",\n \"Grep\"\n]);\nconst WRITE_TOOLS = /* @__PURE__ */ new Set([\"Write\", \"Edit\"]);\nconst fileDenyRules = [\n {\n name: \"env-file-read\",\n tools: READ_TOOLS,\n action: \"deny\",\n when: (filePath) => isEnvFile(filePath),\n reason: \"Don't read .env files. These contain secrets that should not appear in the conversation.\"\n },\n {\n name: \"env-file-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath) => isEnvFile(filePath),\n reason: \"Don't write to .env files. These contain secrets.\"\n },\n {\n name: \"hooks-folder-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx) => {\n const resolved = resolveIn(ctx.cwd, filePath);\n const hooksDir = path.join(ctx.cwd, \".claude\", \"hooks\");\n return isWithin(hooksDir, resolved);\n },\n reason: \"Don't write to .claude/hooks/. These files are managed by the project and should not be modified directly.\"\n },\n {\n name: \"git-dir-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx) => targetsGitDir(filePath, ctx.cwd),\n reason: \"Don't write into .git/ directly. Its internals (config, hooks, refs) are not plain project files -- a config edit can redirect remotes or push destinations that other rules trust, and a hooks/ script runs automatically on the next commit/push. Use git subcommands instead, or ask the user.\"\n },\n {\n name: \"node-modules-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx) => targetsNodeModules(filePath, ctx.cwd),\n reason: \"Don't write into node_modules/. Its files are hardlinks into pnpm's global store -- editing one in place corrupts that package for every project on this machine. Edit the source package and rebuild, or install into a scratch tree with an isolated store (`pnpm add --store-dir <scratch> <pkg>`).\"\n },\n {\n name: \"claude-settings-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx) => {\n const resolved = resolveIn(ctx.cwd, filePath);\n return samePath(resolved, path.join(ctx.cwd, \".claude\", \"settings.json\"));\n },\n reason: \"Don't write to .claude/settings.json. This file is managed by the project and should not be modified directly.\"\n },\n {\n name: \"health-baseline-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx, toolInput) => {\n const resolved = resolveIn(ctx.cwd, filePath);\n if (path.basename(resolved) !== \"health-baseline.json\") return false;\n const root = getRepoTopLevel(ctx.cwd) ?? ctx.cwd;\n if (!samePath(resolved, path.join(root, \"health-baseline.json\"))) return false;\n if (!writeRaisesBaseline(resolved, toolInput)) return false;\n return branchCarriesOtherFiles(ctx.cwd, HEALTH_CEILING_FILES) === true;\n },\n reason: HEALTH_RAISE_REASON\n }\n];\nfunction writeRaisesBaseline(file, toolInput) {\n let before;\n try {\n before = readFileSync(file, \"utf8\");\n } catch (_error) {\n return false;\n }\n const after = textAfterWrite(before, toolInput);\n if (after === void 0) return false;\n const beforeJson = parseJson(before);\n const afterJson = parseJson(after);\n if (beforeJson === void 0 || afterJson === void 0) return false;\n return numberRises(beforeJson, afterJson);\n}\nfunction textAfterWrite(before, toolInput) {\n const { content, old_string: oldText, new_string: newText, replace_all: all } = toolInput;\n if (typeof content === \"string\") return content;\n if (typeof oldText !== \"string\" || typeof newText !== \"string\" || oldText === \"\") return void 0;\n const at = before.indexOf(oldText);\n if (at === -1) return void 0;\n if (all === true) return before.split(oldText).join(newText);\n return before.slice(0, at) + newText + before.slice(at + oldText.length);\n}\nfunction parseJson(text) {\n try {\n const result = record(string(), unknown()).safeParse(JSON.parse(text));\n return result.success ? result.data : void 0;\n } catch (_error) {\n return;\n }\n}\nfunction numberRises(before, after) {\n if (typeof after === \"number\") return typeof before !== \"number\" || after > before;\n if (typeof after !== \"object\" || after === null) return false;\n const beforeObject = typeof before === \"object\" && before !== null ? before : {};\n return Object.entries(after).some(([key, value]) => numberRises(Object.entries(beforeObject).find(([k]) => k === key)?.[1], value));\n}\n\n//#endregion\n//#region src/evaluate-tool.ts\nconst FILE_PATH_TOOLS = /* @__PURE__ */ new Set([\n \"Read\",\n \"Write\",\n \"Edit\"\n]);\nconst SEARCH_TOOLS = /* @__PURE__ */ new Set([\"Glob\", \"Grep\"]);\nfunction extractPath(toolName, toolInput, cwd) {\n if (FILE_PATH_TOOLS.has(toolName)) return parseFilePathInput(toolInput)?.file_path ?? null;\n if (SEARCH_TOOLS.has(toolName)) return parseSearchInput(toolInput)?.path ?? cwd;\n return null;\n}\nfunction evaluateToolUse(toolName, toolInput, ctx) {\n const filePath = extractPath(toolName, toolInput, ctx.cwd);\n if (filePath === null) return {\n action: \"none\",\n rule: null,\n reason: \"unknown tool input shape\"\n };\n for (const rule of fileDenyRules) if (rule.tools.has(toolName) && rule.when(filePath, ctx, toolInput)) return {\n action: \"deny\",\n rule: rule.name,\n reason: rule.reason ?? null\n };\n return {\n action: \"none\",\n rule: null,\n reason: \"no file rule matched\"\n };\n}\n\n//#endregion\n//#region src/lib/settings.ts\nconst SETTINGS_PATH = \".claude/hooks/bst-hook.settings.json\";\nconst BstHookSettingsSchema = object({ protectedBranches: _default(array(string().check(_minLength(1))), []) });\nfunction isFileClean(cwd, relativePath) {\n try {\n execFileSync(\"git\", [\n \"ls-files\",\n \"--error-unmatch\",\n relativePath\n ], {\n cwd,\n stdio: \"ignore\"\n });\n execFileSync(\"git\", [\n \"diff\",\n \"--quiet\",\n \"HEAD\",\n \"--\",\n relativePath\n ], {\n cwd,\n stdio: \"ignore\"\n });\n return true;\n } catch {\n return false;\n }\n}\nfunction loadRepoSettings(cwd) {\n const root = findWorkspaceRoot(cwd) ?? cwd;\n const settingsFile = join(root, SETTINGS_PATH);\n const empty = { protectedBranches: [] };\n if (!existsSync(settingsFile)) return empty;\n if (!isFileClean(root, SETTINGS_PATH)) return empty;\n try {\n const raw = readFileSync(settingsFile, \"utf-8\");\n const parsed = JSON.parse(raw);\n const result = BstHookSettingsSchema.safeParse(parsed);\n if (!result.success) return empty;\n return { protectedBranches: result.data.protectedBranches };\n } catch {\n return empty;\n }\n}\n\n//#endregion\n//#region src/index.ts\nconst inputResult = await readHookInput();\nif (!inputResult.ok) {\n process.stderr.write(`[bst-hook] invalid hook input: ${inputResult.error}\\n`);\n audit({\n sessionId: \"unknown\",\n cwd: \"unknown\",\n command: `<invalid input: ${inputResult.error}>`,\n decision: \"none\",\n rule: null,\n reason: `validation failed: ${inputResult.error}`,\n permissionMode: void 0\n });\n process.exit(0);\n}\nconst input = inputResult.data;\nif (input.hook_event_name !== \"PreToolUse\") process.exit(0);\nif (!(/* @__PURE__ */ new Set([\n \"Bash\",\n \"Read\",\n \"Write\",\n \"Edit\",\n \"Glob\",\n \"Grep\"\n])).has(input.tool_name)) process.exit(0);\nconst repoSettings = loadRepoSettings(input.cwd);\nlet decision;\nlet auditCommand;\nif (input.tool_name === \"Bash\") {\n const bashInput = parseBashInput(input.tool_input);\n if (!bashInput) {\n audit({\n sessionId: input.session_id,\n cwd: input.cwd,\n toolName: input.tool_name,\n command: `<invalid Bash input: ${JSON.stringify(input.tool_input).slice(0, 200)}>`,\n decision: \"none\",\n rule: null,\n reason: \"Bash tool_input missing or invalid 'command' field\",\n permissionMode: input.permission_mode\n });\n process.exit(0);\n }\n decision = evaluateCommand(bashInput.command, {\n cwd: input.cwd,\n depth: 0,\n repoProtectedBranches: repoSettings.protectedBranches\n });\n auditCommand = bashInput.command;\n} else {\n const projectDir = findWorkspaceRoot(input.cwd) ?? process.env[\"CLAUDE_PROJECT_DIR\"] ?? input.cwd;\n decision = evaluateToolUse(input.tool_name, input.tool_input, {\n cwd: projectDir,\n sessionId: input.session_id\n });\n const filePath = String(input.tool_input[\"file_path\"] ?? input.tool_input[\"path\"] ?? \"\");\n auditCommand = `${input.tool_name} ${filePath}`;\n}\naudit({\n sessionId: input.session_id,\n cwd: input.cwd,\n toolName: input.tool_name,\n command: auditCommand,\n decision: decision.action,\n rule: decision.rule,\n reason: decision.reason,\n permissionMode: input.permission_mode\n});\nif (decision.action === \"deny\") writeDeny(decision.reason);\nelse if (decision.advice) writeAdvice(decision.advice);\n\n//#endregion\nexport { };",
8353
- guide: "# bst-hook Permission Guide\n\nThis project uses a PreToolUse hook (`bst-hook`) that checks each Bash command\nand file tool call before it runs. The hook only ever blocks a command; it does\nnot approve anything on its own. A command that matches a rule below is denied,\nwith a message that says what to do instead.\n\n## Denied commands\n\nThese are blocked. The hook tells you what to do instead.\n\n### Bash\n\n- **sudo**: Don't use sudo. If you need elevated permissions, ask me to do it manually.\n- **git-force-push**: Use `git push --force-with-lease` instead of `--force`.\n- **git-protected-branch-write**: This would move a protected branch (main, master, the remote's default branch, or one listed in .claude/hooks/bst-hook.settings.json#protectedBranches). Work on a feature branch in a worktree: `git worktree add ../<repo>-<topic> -b feat/<topic>`, then commit and push there. To bring `main` up to date, use `git pull --ff-only`.\n- **chmod-777**: Don't use 777 permissions. Use more restrictive permissions.\n- **node-modules-write**: Don't write into node_modules/. Its files are hardlinks into pnpm's global store, so overwriting one in place (plain `cp`, `>` redirect, `sed -i`, ...) writes through the link and corrupts that package for every project on this machine -- silently, since the directory you wrote into ends up fine. To test a local build as an installed dependency, run it directly (`node packages/<pkg>/dist/bin.mjs <cmd>`), or install into a scratch tree with an isolated store (`pnpm add --store-dir <scratch> <pkg>`). If you must replace a file in place, `cp --remove-destination` is safe -- it unlinks the target first. Run `pnpm store status` to check whether the store is already damaged.\n- **env-file-read**: Don't read .env files. These contain secrets that should not appear in the conversation.\n- **env-file-write**: Don't write to .env files. These contain secrets.\n- **health-baseline-write**: A health ceiling raise travels alone: open a ceiling-only pull request -- only health-baseline.json and .tooling.json changed -- for a human to merge, or split the module. `bst health:check` refuses the raise beside code either way. To restore the default branch's copy run `git show origin/<default>:health-baseline.json > health-baseline.json`, and to adopt lower counts run `bst health:check --update-baseline`.\n- **health-baseline-reset**: A health ceiling raise travels alone: open a ceiling-only pull request -- only health-baseline.json and .tooling.json changed -- for a human to merge, or split the module. `bst health:check` refuses the raise beside code either way. To restore the default branch's copy run `git show origin/<default>:health-baseline.json > health-baseline.json`, and to adopt lower counts run `bst health:check --update-baseline`.\n- **npx-redirect**: This project uses pnpm. Use `pnpm dlx <pkg>` or `pnpm exec <cmd>` instead of npx.\n- **mise-self-update**: Don't self-update mise. The version is Renovate-managed; ask a human to bump the pin.\n\n### File tools (Read, Write, Edit, Glob, Grep)\n\n- **env-file-read**: Don't read .env files. These contain secrets that should not appear in the conversation.\n- **env-file-write**: Don't write to .env files. These contain secrets.\n- **hooks-folder-write**: Don't write to .claude/hooks/. These files are managed by the project and should not be modified directly.\n- **git-dir-write**: Don't write into .git/ directly. Its internals (config, hooks, refs) are not plain project files -- a config edit can redirect remotes or push destinations that other rules trust, and a hooks/ script runs automatically on the next commit/push. Use git subcommands instead, or ask the user.\n- **node-modules-write**: Don't write into node_modules/. Its files are hardlinks into pnpm's global store -- editing one in place corrupts that package for every project on this machine. Edit the source package and rebuild, or install into a scratch tree with an isolated store (`pnpm add --store-dir <scratch> <pkg>`).\n- **claude-settings-write**: Don't write to .claude/settings.json. This file is managed by the project and should not be modified directly.\n- **health-baseline-write**: A health ceiling raise travels alone: open a ceiling-only pull request -- only health-baseline.json and .tooling.json changed -- for a human to merge, or split the module. `bst health:check` refuses the raise beside code either way. To restore the default branch's copy run `git show origin/<default>:health-baseline.json > health-baseline.json`, and to adopt lower counts run `bst health:check --update-baseline`.\n\n## How commands are evaluated\n\n- **Pipelines and chains**: every segment is checked; a deny on any segment blocks the whole command.\n- **pnpm scripts**: `pnpm <script>` resolves against `package.json` (including pre/post hooks and `--filter` workspaces) and evaluates the resolved command recursively.\n- **cd propagation**: `cd dir && cmd` evaluates `cmd` relative to the new directory.\n- **Branch switch propagation**: `git switch <branch> && cmd` (or `git checkout <branch>`) evaluates a git write in `cmd` against the branch it switched to, not the one checked out before. This holds only across `&&` and only in the same directory.\n- **Audit log**: all decisions are logged to `.claude/hooks/bst-hook.audit.local.jsonl`.\n\n## Everything else\n\nA command that matches none of the rules above is judged by Claude Code's own\nauto mode, not by this hook.\n"
8424
+ hook: "/**\n * bst-hook -- Claude Code PreToolUse hook\n * @bensandee/claude-hooks\n * https://github.com/bensandee/tooling\n *\n * Auto-generated by @bensandee/tooling repo:sync -- do not edit manually.\n */\nimport * as fs from \"node:fs\";\nimport { existsSync, readFileSync, readdirSync, realpathSync, statSync } from \"node:fs\";\nimport * as path from \"node:path\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { execFileSync } from \"node:child_process\";\n\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/util.js\nfunction joinValues(array, separator = \"|\") {\n return array.map((val) => stringifyPrimitive(val)).join(separator);\n}\nfunction jsonStringifyReplacer(_, value) {\n if (typeof value === \"bigint\") return value.toString();\n return value;\n}\nvar Cached = class {\n constructor(getter) {\n this._getter = getter;\n this._value = void 0;\n }\n get value() {\n const getter = this._getter;\n if (getter !== void 0) {\n this._value = getter();\n this._getter = void 0;\n }\n return this._value;\n }\n};\nfunction cached(getter) {\n return new Cached(getter);\n}\nfunction nullish(input) {\n return input === null || input === void 0;\n}\nfunction cleanRegex(source) {\n const start = source.startsWith(\"^\") ? 1 : 0;\n const end = source.endsWith(\"$\") ? source.length - 1 : source.length;\n return source.slice(start, end);\n}\nfunction assignProp(target, prop, value) {\n Object.defineProperty(target, prop, {\n value,\n writable: true,\n enumerable: true,\n configurable: true\n });\n}\nconst captureStackTrace = \"captureStackTrace\" in Error ? Error.captureStackTrace : (..._args) => {};\nfunction isObject(data) {\n return typeof data === \"object\" && data !== null && !Array.isArray(data);\n}\nfunction isPlainObject(o) {\n if (isObject(o) === false) return false;\n const ctor = o.constructor;\n if (ctor === void 0) return true;\n if (typeof ctor !== \"function\") return true;\n const prot = ctor.prototype;\n if (isObject(prot) === false) return false;\n if (Object.prototype.hasOwnProperty.call(prot, \"isPrototypeOf\") === false) return false;\n return true;\n}\nfunction shallowClone(o) {\n if (isPlainObject(o)) return { ...o };\n if (Array.isArray(o)) return [...o];\n if (o instanceof Map) return new Map(o);\n if (o instanceof Set) return new Set(o);\n return o;\n}\nfunction clone(inst, def, params) {\n const cl = new inst._zod.constr(def ?? inst._zod.def);\n if (!def || params?.parent) cl._zod.parent = inst;\n return cl;\n}\nfunction normalizeParams(_params) {\n const params = _params;\n if (!params) return {};\n if (typeof params === \"string\") return { error: () => params };\n if (params?.message !== void 0) {\n if (params?.error !== void 0) throw new Error(\"Cannot specify both `message` and `error` params\");\n params.error = params.message;\n }\n delete params.message;\n if (typeof params.error === \"string\") return {\n ...params,\n error: () => params.error\n };\n return params;\n}\nfunction stringifyPrimitive(value) {\n if (typeof value === \"bigint\") return value.toString() + \"n\";\n if (typeof value === \"string\") return `\"${value}\"`;\n return `${value}`;\n}\nfunction optionalKeys(shape) {\n return Object.keys(shape).filter((k) => {\n return shape[k]._zod.optin !== void 0 && shape[k]._zod.optout === \"optional\";\n });\n}\nfunction aborted(x, startIndex = 0) {\n if (x.aborted === true) return true;\n for (let i = startIndex; i < x.issues.length; i++) if (x.issues[i]?.continue !== true) return true;\n return false;\n}\nfunction explicitlyAborted(x, startIndex = 0) {\n if (x.aborted === true) return true;\n for (let i = startIndex; i < x.issues.length; i++) if (x.issues[i]?.continue === false) return true;\n return false;\n}\nfunction prefixIssues(path, issues) {\n return issues.map((iss) => {\n var _a;\n (_a = iss).path ?? (_a.path = []);\n iss.path.unshift(path);\n return iss;\n });\n}\nfunction unwrapMessage(message) {\n return typeof message === \"string\" ? message : message?.message;\n}\nfunction attachSchema(issues, start, inst) {\n var _a;\n for (let i = start; i < issues.length; i++) (_a = issues[i]).schema ?? (_a.schema = inst);\n}\nfunction finalizeIssue(iss, ctx, config) {\n var _a;\n const traits = iss.inst?._zod?.traits;\n if (traits?.has(\"$ZodType\")) {\n if (traits.has(\"$ZodCheck\")) (_a = iss).schema ?? (_a.schema = iss.inst);\n else iss.schema = iss.inst;\n }\n const schemaError = iss.schema !== iss.inst ? iss.schema?._zod.def?.error : void 0;\n const message = iss.message ? iss.message : unwrapMessage(iss.inst?._zod.def?.error?.(iss)) ?? unwrapMessage(schemaError?.(iss)) ?? unwrapMessage(ctx?.error?.(iss)) ?? unwrapMessage(config.customError?.(iss)) ?? unwrapMessage(config.localeError?.(iss)) ?? \"Invalid input\";\n const full = {};\n for (const k of Object.keys(iss)) {\n if (k === \"inst\" || k === \"schema\" || k === \"continue\" || k === \"input\" || k === \"__proto__\") continue;\n full[k] = iss[k];\n }\n full.path ?? (full.path = []);\n full.message = message;\n if (ctx?.reportInput) full.input = iss.input;\n return full;\n}\nconst highSurrogate = /[\\uD800-\\uDBFF]/;\nfunction codePointLength(str) {\n const units = str.length;\n if (!highSurrogate.test(str)) return units;\n let count = units;\n for (let i = 0; i < units - 1; i++) if ((str.charCodeAt(i) & 64512) === 55296 && (str.charCodeAt(i + 1) & 64512) === 56320) {\n count--;\n i++;\n }\n return count;\n}\nfunction getLengthableOrigin(input) {\n if (Array.isArray(input)) return \"array\";\n if (typeof input === \"string\") return \"string\";\n return \"unknown\";\n}\nfunction parsedType(data) {\n const t = typeof data;\n switch (t) {\n case \"number\": return Number.isNaN(data) ? \"nan\" : \"number\";\n case \"object\": {\n if (data === null) return \"null\";\n if (Array.isArray(data)) return \"array\";\n const obj = data;\n if (obj && Object.getPrototypeOf(obj) !== Object.prototype && \"constructor\" in obj && obj.constructor) return obj.constructor.name;\n }\n }\n return t;\n}\nfunction members(proto, table) {\n for (const key in table) {\n const desc = Object.getOwnPropertyDescriptor(table, key);\n if (desc.get) Object.defineProperty(proto, key, {\n ...desc,\n enumerable: false\n });\n else defineBound(proto, key, desc.value);\n }\n}\nfunction own(inst, key, value, enumerable = true) {\n Object.defineProperty(inst, key, {\n configurable: true,\n writable: true,\n enumerable,\n value\n });\n return value;\n}\nfunction hide(inst, key, value) {\n return own(inst, key, value, false);\n}\nfunction defineBound(proto, key, fn) {\n Object.defineProperty(proto, key, {\n configurable: true,\n get() {\n return this == null ? fn : own(this, key, fn.bind(this));\n },\n set(value) {\n own(this, key, value);\n }\n });\n}\nfunction claim(inst, sentinel) {\n const proto = Object.getPrototypeOf(inst);\n return sentinel in proto ? void 0 : proto;\n}\nlet installing;\nlet broke = false;\nconst breaker = {\n configurable: true,\n get() {\n broke = true;\n }\n};\nfunction defineLazyInternal(inst, key, compute) {\n const proto = Object.getPrototypeOf(inst._zod);\n if (key in proto && installing !== inst._zod) {\n installing = void 0;\n return;\n }\n installing = inst._zod;\n Object.defineProperty(proto, key, {\n configurable: true,\n get() {\n Object.defineProperty(this, key, breaker);\n const outer = broke;\n broke = false;\n try {\n const value = compute(this);\n if (broke) delete this[key];\n else Object.defineProperty(this, key, {\n configurable: true,\n writable: true,\n value\n });\n broke = broke || outer;\n return value;\n } catch (err) {\n delete this[key];\n broke = broke || outer;\n throw err;\n }\n },\n set(value) {\n Object.defineProperty(this, key, {\n configurable: true,\n writable: true,\n value\n });\n }\n });\n}\nfunction installLazyProp(inst, key, make, enumerable) {\n const proto = claim(inst, key);\n if (!proto) return;\n Object.defineProperty(proto, key, {\n configurable: true,\n get() {\n const desc = {\n configurable: true,\n writable: true,\n enumerable,\n value: void 0\n };\n Object.defineProperty(this, key, desc);\n desc.value = make(this);\n Object.defineProperty(this, key, desc);\n return desc.value;\n },\n set(value) {\n Object.defineProperty(this, key, {\n configurable: true,\n writable: true,\n enumerable,\n value\n });\n }\n });\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/core.js\nvar _a;\nconst _zodDesc = {\n value: void 0,\n enumerable: false\n};\nlet _E = \"captureStackTrace\" in Error ? Error : null;\nfunction newError(Definition) {\n const E = _E;\n if (E) {\n const saved = E.stackTraceLimit;\n if (typeof saved === \"number\") {\n try {\n E.stackTraceLimit = 0;\n } catch {\n _E = null;\n return new Definition();\n }\n try {\n return new Definition();\n } finally {\n E.stackTraceLimit = saved;\n }\n }\n }\n return new Definition();\n}\nfunction $constructor(name, initializer, proto, params) {\n const zodProto = {};\n function Internals(def) {\n this.def = def;\n this.constr = _;\n this.traits = /* @__PURE__ */ new Set();\n }\n Internals.prototype = zodProto;\n const protoMembers = proto;\n const initialized = protoMembers && /* @__PURE__ */ new WeakSet();\n function init(inst, def) {\n if (!inst._zod) {\n _zodDesc.value = new Internals(def);\n try {\n Object.defineProperty(inst, \"_zod\", _zodDesc);\n } finally {\n _zodDesc.value = void 0;\n }\n } else if (inst._zod.traits.has(name)) return;\n inst._zod.traits.add(name);\n initializer(inst, def);\n if (initialized) {\n const own = Object.getPrototypeOf(inst);\n const ctorProto = inst._zod.constr.prototype;\n let up = own;\n while (up && up !== ctorProto) up = Object.getPrototypeOf(up);\n const target = up ?? own;\n if (!initialized.has(target)) {\n initialized.add(target);\n members(target, protoMembers);\n }\n }\n const proto = _.prototype;\n for (const k in proto) {\n if (!Object.prototype.hasOwnProperty.call(proto, k)) continue;\n if (!(k in inst)) inst[k] = proto[k].bind(inst);\n }\n }\n const Parent = params?.Parent ?? Object;\n class Definition extends Parent {}\n Object.defineProperty(Definition, \"name\", { value: name });\n function _(def) {\n const inst = params?.Parent ? newError(Definition) : this;\n init(inst, def);\n const deferred = inst._zod.deferred;\n if (deferred) {\n for (const fn of deferred) fn();\n inst._zod.deferred = void 0;\n }\n const pp = globalThis.__zod_globalConfig?.postProcessor;\n if (pp) pp(inst);\n return inst;\n }\n Object.defineProperty(_, \"init\", { value: init });\n Object.defineProperty(_, Symbol.hasInstance, { value: (inst) => {\n if (params?.Parent && inst instanceof params.Parent) return true;\n return inst?._zod?.traits?.has(name);\n } });\n Object.defineProperty(_, \"name\", { value: name });\n return _;\n}\nvar $ZodAsyncError = class extends Error {\n constructor() {\n super(`Encountered Promise during synchronous parse. Use .parseAsync() instead.`);\n }\n};\n(_a = globalThis).__zod_globalConfig ?? (_a.__zod_globalConfig = {});\nconst globalConfig = globalThis.__zod_globalConfig;\nfunction config(newConfig) {\n if (newConfig) Object.assign(globalConfig, newConfig);\n return globalConfig;\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/errors.js\nfunction _getMessage() {\n const internals = this._zod;\n internals.message ?? (internals.message = JSON.stringify(internals.def, jsonStringifyReplacer, 2));\n return internals.message;\n}\nfunction _setMessage(value) {\n this._zod.message = value;\n}\nconst _messageDesc = {\n get: _getMessage,\n set: _setMessage,\n enumerable: true,\n configurable: true\n};\nconst _issuesDesc = {\n value: void 0,\n enumerable: false\n};\nconst _installedToString = /* @__PURE__ */ new WeakSet([Object.prototype, Error.prototype]);\nconst initializer = (inst, def) => {\n inst.name = \"$ZodError\";\n _issuesDesc.value = def;\n Object.defineProperty(inst, \"issues\", _issuesDesc);\n _issuesDesc.value = void 0;\n Object.defineProperty(inst, \"message\", _messageDesc);\n const proto = Object.getPrototypeOf(inst);\n if (!_installedToString.has(proto)) {\n _installedToString.add(proto);\n Object.defineProperty(proto, \"toString\", {\n configurable: true,\n enumerable: false,\n get() {\n const value = () => this.message;\n Object.defineProperty(this, \"toString\", {\n value,\n configurable: true,\n writable: true\n });\n return value;\n },\n set(value) {\n Object.defineProperty(this, \"toString\", {\n value,\n configurable: true,\n writable: true\n });\n }\n });\n }\n};\nconst $ZodError = $constructor(\"$ZodError\", initializer);\nconst $ZodRealError = $constructor(\"$ZodError\", initializer, void 0, { Parent: Error });\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/parse.js\nconst _parse = (_Err) => {\n const fn = (schema, value, _ctx, _params) => {\n const ctx = _ctx ? {\n ..._ctx,\n async: false\n } : { async: false };\n const result = schema._zod.run({\n value,\n issues: []\n }, ctx);\n if (result instanceof Promise) throw new $ZodAsyncError();\n if (result.issues.length) {\n const e = new ((_params?.Err) ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())));\n captureStackTrace(e, _params?.callee ?? fn);\n throw e;\n }\n return result.value;\n };\n return fn;\n};\nconst parse = /* @__PURE__*/ _parse($ZodRealError);\nconst _parseAsync = (_Err) => {\n const fn = async (schema, value, _ctx, params) => {\n const ctx = _ctx ? {\n ..._ctx,\n async: true\n } : { async: true };\n let result = schema._zod.run({\n value,\n issues: []\n }, ctx);\n if (result instanceof Promise) result = await result;\n if (result.issues.length) {\n const e = new ((params?.Err) ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())));\n captureStackTrace(e, params?.callee ?? fn);\n throw e;\n }\n return result.value;\n };\n return fn;\n};\nconst parseAsync = /* @__PURE__*/ _parseAsync($ZodRealError);\nconst _safeParse = (_Err) => (schema, value, _ctx) => {\n const ctx = _ctx ? {\n ..._ctx,\n async: false\n } : { async: false };\n const result = schema._zod.run({\n value,\n issues: []\n }, ctx);\n if (result instanceof Promise) throw new $ZodAsyncError();\n return result.issues.length ? failure(_Err, result.issues, ctx) : {\n success: true,\n data: result.value\n };\n};\nconst safeParse = /* @__PURE__*/ _safeParse($ZodRealError);\nfunction failure(Err, issues, ctx) {\n let error;\n return {\n success: false,\n get error() {\n if (!error) {\n error = new Err(issues.map((iss) => finalizeIssue(iss, ctx, config())));\n issues = void 0;\n ctx = void 0;\n }\n return error;\n },\n set error(e) {\n error = e;\n issues = void 0;\n ctx = void 0;\n }\n };\n}\nconst _safeParseAsync = (_Err) => async (schema, value, _ctx) => {\n const ctx = _ctx ? {\n ..._ctx,\n async: true\n } : { async: true };\n let result = schema._zod.run({\n value,\n issues: []\n }, ctx);\n if (result instanceof Promise) result = await result;\n return result.issues.length ? failure(_Err, result.issues, ctx) : {\n success: true,\n data: result.value\n };\n};\nconst safeParseAsync = /* @__PURE__*/ _safeParseAsync($ZodRealError);\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/regexes.js\nconst anyString = /^[\\s\\S]{0,}$/;\nconst number = /^-?\\d+(?:\\.\\d+)?$/;\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/checks.js\nconst $ZodCheck = /*@__PURE__*/ $constructor(\"$ZodCheck\", (inst, def) => {\n var _a;\n inst._zod ?? (inst._zod = {});\n inst._zod.def = def;\n (_a = inst._zod).onattach ?? (_a.onattach = []);\n});\nconst _whenHasLength = (payload) => {\n const val = payload.value;\n return !nullish(val) && val.length !== void 0;\n};\nconst $ZodCheckMinLength = /*@__PURE__*/ $constructor(\"$ZodCheckMinLength\", (inst, def) => {\n var _a;\n $ZodCheck.init(inst, def);\n (_a = inst._zod.def).when ?? (_a.when = _whenHasLength);\n inst._zod.check = (payload) => {\n const input = payload.value;\n const units = input.length;\n if ((typeof input === \"string\" && units >= def.minimum && units < def.minimum * 2 ? codePointLength(input) : units) >= def.minimum) return;\n const origin = getLengthableOrigin(input);\n payload.issues.push({\n origin,\n code: \"too_small\",\n minimum: def.minimum,\n inclusive: true,\n input,\n inst,\n continue: !def.abort\n });\n };\n});\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/versions.js\nconst version = {\n major: 4,\n minor: 6,\n patch: 5\n};\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/schemas.js\nconst $ZodType = /*@__PURE__*/ $constructor(\"$ZodType\", (inst, def) => {\n var _a;\n inst ?? (inst = {});\n inst._zod.def = def;\n inst._zod.bag = inst._zod.bag || {};\n inst._zod.version = version;\n const defChecks = inst._zod.def.checks;\n const checks = inst._zod.traits.has(\"$ZodCheck\") ? [inst, ...defChecks ?? []] : defChecks?.length ? [...defChecks] : [];\n for (const ch of checks) for (const fn of ch._zod.onattach) fn(inst);\n if (checks.length === 0) {\n (_a = inst._zod).deferred ?? (_a.deferred = []);\n inst._zod.deferred?.push(() => {\n inst._zod.run = inst._zod.parse;\n });\n } else {\n const runChecks = (payload, checks, ctx) => {\n if (payload.memo) return payload;\n let isAborted = aborted(payload);\n let asyncResult;\n for (const ch of checks) {\n if (ch._zod.def.when) {\n if (explicitlyAborted(payload)) continue;\n if (!ch._zod.def.when(payload)) continue;\n } else if (isAborted) continue;\n const currLen = payload.issues.length;\n const _ = ch._zod.check(payload);\n if (_ instanceof Promise && ctx?.async === false) throw new $ZodAsyncError();\n if (asyncResult || _ instanceof Promise) asyncResult = (asyncResult ?? Promise.resolve()).then(async () => {\n await _;\n if (payload.issues.length === currLen) return;\n attachSchema(payload.issues, currLen, inst);\n if (!isAborted) isAborted = aborted(payload, currLen);\n });\n else {\n if (payload.issues.length === currLen) continue;\n attachSchema(payload.issues, currLen, inst);\n if (!isAborted) isAborted = aborted(payload, currLen);\n }\n }\n if (asyncResult) return asyncResult.then(() => {\n return payload;\n });\n return payload;\n };\n const handleCanaryResult = (canary, payload, ctx) => {\n if (aborted(canary)) {\n canary.aborted = true;\n return canary;\n }\n const checkResult = runChecks(payload, checks, ctx);\n if (checkResult instanceof Promise) {\n if (ctx.async === false) throw new $ZodAsyncError();\n return checkResult.then((checkResult) => inst._zod.parse(checkResult, ctx));\n }\n return inst._zod.parse(checkResult, ctx);\n };\n inst._zod.run = (payload, ctx) => {\n if (ctx.skipChecks) return inst._zod.parse(payload, ctx);\n if (ctx.direction === \"backward\") {\n const canary = inst._zod.parse({\n value: payload.value,\n issues: []\n }, {\n ...ctx,\n skipChecks: true\n });\n if (canary instanceof Promise) return canary.then((canary) => {\n return handleCanaryResult(canary, payload, ctx);\n });\n return handleCanaryResult(canary, payload, ctx);\n }\n const result = inst._zod.parse(payload, ctx);\n if (result instanceof Promise) {\n if (ctx.async === false) throw new $ZodAsyncError();\n return result.then((result) => runChecks(result, checks, ctx));\n }\n return runChecks(result, checks, ctx);\n };\n }\n}, {\n get \"~standard\"() {\n return hide(this, \"~standard\", standardProps(this));\n },\n set \"~standard\"(value) {\n own(this, \"~standard\", value);\n }\n});\nconst toStandardResult = (r, ctx) => r.issues.length ? { issues: r.issues.map((iss) => finalizeIssue(iss, ctx, config())) } : { value: r.value };\nasync function validateAsync(inst, value) {\n const ctx = { async: true };\n return toStandardResult(await inst._zod.run({\n value,\n issues: []\n }, ctx), ctx);\n}\nfunction standardProps(inst) {\n return {\n validate: (value) => {\n const ctx = { async: false };\n try {\n const r = inst._zod.run({\n value,\n issues: []\n }, ctx);\n if (!(r instanceof Promise)) return toStandardResult(r, ctx);\n } catch (_) {}\n return validateAsync(inst, value);\n },\n vendor: \"zod\",\n version: 1\n };\n}\nconst $ZodString = /*@__PURE__*/ $constructor(\"$ZodString\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.pattern = def.pattern ?? anyString;\n inst._zod.parse = (payload, _) => {\n if (def.coerce) try {\n payload.value = String(payload.value);\n } catch (_) {}\n if (typeof payload.value === \"string\") return payload;\n payload.issues.push({\n expected: \"string\",\n code: \"invalid_type\",\n input: payload.value,\n inst\n });\n return payload;\n };\n});\nconst $ZodUnknown = /*@__PURE__*/ $constructor(\"$ZodUnknown\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload) => payload;\n});\nfunction handleArrayResult(result, final, index) {\n if (result.issues.length) final.issues.push(...prefixIssues(index, result.issues));\n final.value[index] = result.value;\n}\nconst $ZodArray = /*@__PURE__*/ $constructor(\"$ZodArray\", (inst, def) => {\n $ZodType.init(inst, def);\n const memo = globalConfig.memoizer;\n memo?.attach(inst);\n inst._zod.parse = (payload, ctx) => {\n const input = payload.value;\n if (!Array.isArray(input)) {\n payload.issues.push({\n expected: \"array\",\n code: \"invalid_type\",\n input,\n inst\n });\n return payload;\n }\n payload.value = memo ? memo.alloc(inst, payload, Array(input.length), ctx) : Array(input.length);\n const proms = [];\n const abortEarly = ctx?.abortEarly;\n for (let i = 0; i < input.length; i++) {\n const item = input[i];\n const result = def.element._zod.run({\n value: item,\n issues: []\n }, ctx);\n if (result instanceof Promise) proms.push(result.then((result) => handleArrayResult(result, payload, i)));\n else {\n handleArrayResult(result, payload, i);\n if (abortEarly && result.issues.length !== 0 && aborted(result)) break;\n }\n }\n if (proms.length) return Promise.all(proms).then(() => payload);\n return payload;\n };\n});\nfunction handlePropertyResult(result, final, key, input, optin, optout) {\n const isPresent = key in input;\n const isOptionalOut = optout === \"optional\";\n if (!isPresent && isOptionalOut && optin === \"optional\") return;\n if (result.issues.length) {\n if (optin !== void 0 && isOptionalOut && !isPresent) return;\n final.issues.push(...prefixIssues(key, result.issues));\n }\n if (!isPresent && optin === void 0) {\n if (!result.issues.length) final.issues.push({\n code: \"invalid_type\",\n expected: \"nonoptional\",\n input: void 0,\n path: [key]\n });\n return;\n }\n if (result.value === void 0) {\n if (isPresent || optin === \"defaulted\" && !isOptionalOut) final.value[key] = void 0;\n } else final.value[key] = result.value;\n}\nconst NO_SYMBOL_KEYS = [];\nfunction normalizeDef(def) {\n const keys = Object.keys(def.shape);\n const ownSymbols = Object.getOwnPropertySymbols(def.shape);\n const symbolKeys = ownSymbols.length ? ownSymbols : NO_SYMBOL_KEYS;\n const allKeys = symbolKeys.length ? [...keys, ...symbolKeys] : keys;\n for (const k of allKeys) if (!def.shape?.[k]?._zod?.traits?.has(\"$ZodType\")) throw new Error(`Invalid element at key \"${String(k)}\": expected a Zod schema`);\n const okeys = optionalKeys(def.shape);\n return {\n ...def,\n allKeys,\n symbolKeys,\n keySet: new Set(keys),\n numKeys: keys.length,\n optionalKeys: new Set(okeys)\n };\n}\nfunction handleCatchall(proms, input, payload, ctx, def, inst, abortEarly) {\n const unrecognized = [];\n const keySet = def.keySet;\n const _catchall = def.catchall._zod;\n const t = _catchall.def.type;\n const optin = _catchall.optin;\n const optout = _catchall.optout;\n let seen = 0;\n for (const key in input) {\n if (abortEarly && payload.issues.length !== seen) {\n if (aborted(payload, seen)) break;\n seen = payload.issues.length;\n }\n if (keySet.has(key)) continue;\n if (key === \"__proto__\") {\n if (t === \"never\") unrecognized.push(key);\n continue;\n }\n if (t === \"never\") {\n unrecognized.push(key);\n continue;\n }\n const r = _catchall.run({\n value: input[key],\n issues: []\n }, ctx);\n if (r instanceof Promise) proms.push(r.then((r) => handlePropertyResult(r, payload, key, input, optin, optout)));\n else handlePropertyResult(r, payload, key, input, optin, optout);\n }\n if (unrecognized.length) payload.issues.push({\n code: \"unrecognized_keys\",\n keys: unrecognized,\n input,\n inst,\n continue: true\n });\n if (!proms.length) return payload;\n return Promise.all(proms).then(() => {\n return payload;\n });\n}\nconst $ZodObject = /*@__PURE__*/ $constructor(\"$ZodObject\", (inst, def) => {\n $ZodType.init(inst, def);\n const desc = Object.getOwnPropertyDescriptor(def, \"shape\");\n const sh = desc?.get ? desc.get.raw : def.shape ?? {};\n if (sh) {\n const get = () => {\n const newSh = { ...sh };\n Object.defineProperty(def, \"shape\", { value: newSh });\n get.raw = newSh;\n return newSh;\n };\n get.raw = sh;\n Object.defineProperty(def, \"shape\", { get });\n }\n const _normalized = cached(() => normalizeDef(def));\n defineLazyInternal(inst, \"propValues\", (zod) => {\n const shape = zod.def.shape;\n const propValues = {};\n for (const key in shape) {\n const field = shape[key]._zod;\n if (field.values) {\n if (!Object.prototype.hasOwnProperty.call(propValues, key)) assignProp(propValues, key, /* @__PURE__ */ new Set());\n for (const v of field.values) propValues[key].add(v);\n if (field.optin !== void 0) propValues[key].add(void 0);\n }\n }\n return propValues;\n });\n const isObject$2 = isObject;\n const catchall = def.catchall;\n let value;\n const memo = globalConfig.memoizer;\n memo?.attach(inst);\n inst._zod.parse = (payload, ctx) => {\n value ?? (value = _normalized.value);\n const input = payload.value;\n if (!isObject$2(input)) {\n payload.issues.push({\n expected: \"object\",\n code: \"invalid_type\",\n input,\n inst\n });\n return payload;\n }\n payload.value = memo ? memo.alloc(inst, payload, {}, ctx) : {};\n const proms = [];\n const shape = value.shape;\n const abortEarly = ctx?.abortEarly;\n let seen = payload.issues.length;\n for (const key of value.allKeys) {\n if (abortEarly && payload.issues.length !== seen) {\n if (aborted(payload, seen)) break;\n seen = payload.issues.length;\n }\n if (key === \"__proto__\") continue;\n const el = shape[key];\n const optin = el._zod.optin;\n const optout = el._zod.optout;\n const r = el._zod.run({\n value: input[key],\n issues: []\n }, ctx);\n if (r instanceof Promise) proms.push(r.then((r) => handlePropertyResult(r, payload, key, input, optin, optout)));\n else handlePropertyResult(r, payload, key, input, optin, optout);\n }\n if (!catchall) return proms.length ? Promise.all(proms).then(() => payload) : payload;\n return handleCatchall(proms, input, payload, ctx, _normalized.value, inst, abortEarly === true);\n };\n});\nconst $ZodRecord = /*@__PURE__*/ $constructor(\"$ZodRecord\", (inst, def) => {\n $ZodType.init(inst, def);\n const memo = globalConfig.memoizer;\n memo?.attach(inst);\n inst._zod.parse = (payload, ctx) => {\n const input = payload.value;\n if (!isPlainObject(input)) {\n payload.issues.push({\n expected: \"record\",\n code: \"invalid_type\",\n input,\n inst\n });\n return payload;\n }\n const proms = [];\n const values = def.keyType._zod.values;\n if (values && !def.partial) {\n payload.value = memo ? memo.alloc(inst, payload, {}, ctx) : {};\n const recordKeys = /* @__PURE__ */ new Set();\n for (const key of values) if (typeof key === \"string\" || typeof key === \"number\" || typeof key === \"symbol\") {\n recordKeys.add(typeof key === \"number\" ? key.toString() : key);\n if (key === \"__proto__\") continue;\n const keyResult = def.keyType._zod.run({\n value: key,\n issues: []\n }, ctx);\n if (keyResult instanceof Promise) throw new Error(\"Async schemas not supported in object keys currently\");\n if (keyResult.issues.length) {\n payload.issues.push({\n code: \"invalid_key\",\n origin: \"record\",\n issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config())),\n input: key,\n path: [key],\n inst\n });\n continue;\n }\n const outKey = keyResult.value;\n if (outKey === \"__proto__\") continue;\n const result = def.valueType._zod.run({\n value: input[key],\n issues: []\n }, ctx);\n if (result instanceof Promise) proms.push(result.then((result) => {\n if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));\n payload.value[outKey] = result.value;\n }));\n else {\n if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));\n payload.value[outKey] = result.value;\n }\n }\n let unrecognized;\n for (const key in input) if (!recordKeys.has(key)) {\n if (def.mode === \"loose\") {\n if (key === \"__proto__\") continue;\n payload.value[key] = input[key];\n } else {\n unrecognized = unrecognized ?? [];\n unrecognized.push(key);\n }\n }\n if (unrecognized && unrecognized.length > 0) payload.issues.push({\n code: \"unrecognized_keys\",\n input,\n inst,\n keys: unrecognized,\n continue: true\n });\n } else {\n payload.value = memo ? memo.alloc(inst, payload, {}, ctx) : {};\n let unrecognized;\n for (const key of Reflect.ownKeys(input)) {\n if (key === \"__proto__\") continue;\n if (!Object.prototype.propertyIsEnumerable.call(input, key)) continue;\n let keyResult = def.keyType._zod.run({\n value: key,\n issues: []\n }, ctx);\n if (keyResult instanceof Promise) throw new Error(\"Async schemas not supported in object keys currently\");\n if (typeof key === \"string\" && number.test(key) && keyResult.issues.length) {\n const retryResult = def.keyType._zod.run({\n value: Number(key),\n issues: []\n }, ctx);\n if (retryResult instanceof Promise) throw new Error(\"Async schemas not supported in object keys currently\");\n if (retryResult.issues.length === 0) keyResult = retryResult;\n }\n if (keyResult.issues.length) {\n if (def.mode === \"loose\") payload.value[key] = input[key];\n else if (values) {\n unrecognized = unrecognized ?? [];\n unrecognized.push(key);\n } else payload.issues.push({\n code: \"invalid_key\",\n origin: \"record\",\n issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config())),\n input: key,\n path: [key],\n inst\n });\n continue;\n }\n const outKey = keyResult.value;\n if (outKey === \"__proto__\") continue;\n const result = def.valueType._zod.run({\n value: input[key],\n issues: []\n }, ctx);\n if (result instanceof Promise) proms.push(result.then((result) => {\n if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));\n payload.value[outKey] = result.value;\n }));\n else {\n if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));\n payload.value[outKey] = result.value;\n }\n }\n if (unrecognized && unrecognized.length > 0) payload.issues.push({\n code: \"unrecognized_keys\",\n input,\n inst,\n keys: unrecognized,\n continue: true\n });\n }\n if (proms.length) return Promise.all(proms).then(() => payload);\n return payload;\n };\n});\nfunction handleOptionalResult(payload, result) {\n payload.value = result.issues.length ? void 0 : result.value;\n return payload;\n}\nconst $ZodOptional = /*@__PURE__*/ $constructor(\"$ZodOptional\", (inst, def) => {\n $ZodType.init(inst, def);\n defineLazyInternal(inst, \"optin\", (zod) => zod.def.innerType._zod.optin === \"defaulted\" ? \"defaulted\" : \"optional\");\n inst._zod.optout = \"optional\";\n defineLazyInternal(inst, \"values\", (zod) => {\n const values = zod.def.innerType._zod.values;\n return values ? /* @__PURE__ */ new Set([...values, void 0]) : void 0;\n });\n defineLazyInternal(inst, \"pattern\", (zod) => {\n const pattern = zod.def.innerType._zod.pattern;\n return pattern ? new RegExp(`^(${cleanRegex(pattern.source)})?$`) : void 0;\n });\n inst._zod.parse = (payload, ctx) => {\n if (payload.value === void 0) {\n if (def.innerType._zod.optin !== \"defaulted\") return payload;\n const result = def.innerType._zod.run({\n value: payload.value,\n issues: []\n }, ctx);\n if (result instanceof Promise) return result.then((result) => handleOptionalResult(payload, result));\n return handleOptionalResult(payload, result);\n }\n return def.innerType._zod.run(payload, ctx);\n };\n});\nconst $ZodDefault = /*@__PURE__*/ $constructor(\"$ZodDefault\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.optin = \"defaulted\";\n defineLazyInternal(inst, \"values\", (zod) => zod.def.innerType._zod.values);\n inst._zod.parse = (payload, ctx) => {\n if (ctx.direction === \"backward\") return def.innerType._zod.run(payload, ctx);\n if (payload.value === void 0) {\n payload.value = def.defaultValue;\n return payload;\n }\n const result = def.innerType._zod.run(payload, ctx);\n if (result instanceof Promise) return result.then((result) => handleDefaultResult(result, def));\n return handleDefaultResult(result, def);\n };\n});\nfunction handleDefaultResult(payload, def) {\n if (payload.value === void 0) payload.value = def.defaultValue;\n return payload;\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/locales/en.js\nconst error = () => {\n const Sizable = {\n string: {\n unit: \"characters\",\n verb: \"to have\"\n },\n file: {\n unit: \"bytes\",\n verb: \"to have\"\n },\n array: {\n unit: \"items\",\n verb: \"to have\"\n },\n set: {\n unit: \"items\",\n verb: \"to have\"\n },\n map: {\n unit: \"entries\",\n verb: \"to have\"\n }\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const FormatDictionary = {\n regex: \"input\",\n email: \"email address\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO datetime\",\n date: \"ISO date\",\n time: \"ISO time\",\n duration: \"ISO duration\",\n ipv4: \"IPv4 address\",\n ipv6: \"IPv6 address\",\n mac: \"MAC address\",\n cidrv4: \"IPv4 range\",\n cidrv6: \"IPv6 range\",\n base64: \"base64-encoded string\",\n base64url: \"base64url-encoded string\",\n json_string: \"JSON string\",\n e164: \"E.164 number\",\n currency_code: \"currency code\",\n credit_card: \"credit card number\",\n iban: \"IBAN\",\n jwt: \"JWT\",\n template_literal: \"input\"\n };\n const TypeDictionary = { nan: \"NaN\" };\n function getTypeName(type, input) {\n if (type === \"number\" && typeof input === \"number\" && !Number.isFinite(input)) return String(input);\n return TypeDictionary[type] ?? type;\n }\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\": return `Invalid input: expected ${getTypeName(issue.expected)}, received ${getTypeName(parsedType(issue.input), issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1) return `Invalid input: expected ${stringifyPrimitive(issue.values[0])}`;\n return `Invalid option: expected one of ${joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.exact ? \"exactly \" : issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing) return `Too big: expected ${issue.origin ?? \"value\"} to have ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elements\"}`;\n return `Too big: expected ${issue.origin ?? \"value\"} to be ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.exact ? \"exactly \" : issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) return `Too small: expected ${issue.origin} to have ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n return `Too small: expected ${issue.origin} to be ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") return `Invalid string: must start with \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\") return `Invalid string: must end with \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\") return `Invalid string: must include \"${_issue.includes}\"`;\n if (_issue.format === \"regex\") return `Invalid string: must match pattern ${_issue.pattern}`;\n return `Invalid ${FormatDictionary[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\": return `Invalid number: must be a multiple of ${issue.divisor}`;\n case \"unrecognized_keys\": return `Unrecognized key${issue.keys.length > 1 ? \"s\" : \"\"}: ${joinValues(issue.keys, \", \")}`;\n case \"invalid_key\": return `Invalid key in ${issue.origin}`;\n case \"invalid_union\":\n if (issue.options && Array.isArray(issue.options) && issue.options.length > 0) return `Invalid discriminator value. Expected ${issue.options.map((o) => `'${o}'`).join(\" | \")}`;\n if (issue.inclusive === false) return \"Invalid input: more than one option matched\";\n return \"Invalid input\";\n case \"invalid_element\": return `Invalid value in ${issue.origin}`;\n default: return `Invalid input`;\n }\n };\n};\nfunction en_default() {\n return { localeError: error() };\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/api.js\nfunction snapshotChecks(def) {\n if (def.checks) def.checks = [...def.checks];\n return def;\n}\n// @__NO_SIDE_EFFECTS__\nfunction _string(Class, params) {\n return new Class(snapshotChecks({\n type: \"string\",\n ...normalizeParams(params)\n }));\n}\n// @__NO_SIDE_EFFECTS__\nfunction _unknown(Class) {\n return new Class({ type: \"unknown\" });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _minLength(minimum, params) {\n return new $ZodCheckMinLength({\n check: \"min_length\",\n ...normalizeParams(params),\n minimum\n });\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/mini/schemas.js\nconst ZodMiniType = /*@__PURE__*/ $constructor(\"ZodMiniType\", (inst, def) => {\n if (!inst._zod) throw new Error(\"Uninitialized schema in ZodMiniType.\");\n $ZodType.init(inst, def);\n inst.def = def;\n inst.type = def.type;\n}, {\n get with() {\n return this.check;\n },\n set with(value) {\n own(this, \"with\", value);\n },\n parse(data, params) {\n return parse(this, data, params, { callee: this.parse });\n },\n parseAsync(data, params) {\n return parseAsync(this, data, params, { callee: this.parseAsync });\n },\n safeParse(data, params) {\n return safeParse(this, data, params);\n },\n safeParseAsync(data, params) {\n return safeParseAsync(this, data, params);\n },\n check(...checks) {\n const def = this.def;\n return this.clone({\n ...def,\n checks: [...def.checks ?? [], ...checks.map((ch) => typeof ch === \"function\" ? { _zod: {\n check: ch,\n def: { check: \"custom\" },\n onattach: []\n } } : ch)]\n }, { parent: true });\n },\n clone(_def, params) {\n return clone(this, _def, params);\n },\n brand() {\n return this;\n },\n register(reg, meta) {\n reg.add(this, meta);\n return this;\n },\n apply(fn, ...args) {\n return args.length === 0 ? fn(this) : fn(this, ...args);\n }\n});\nconst ZodMiniString = /*@__PURE__*/ $constructor(\"ZodMiniString\", (inst, def) => {\n $ZodString.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction string(params) {\n return _string(ZodMiniString, params);\n}\nconst ZodMiniUnknown = /*@__PURE__*/ $constructor(\"ZodMiniUnknown\", (inst, def) => {\n $ZodUnknown.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction unknown() {\n return _unknown(ZodMiniUnknown);\n}\nconst ZodMiniArray = /*@__PURE__*/ $constructor(\"ZodMiniArray\", (inst, def) => {\n $ZodArray.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction array(element, params) {\n return new ZodMiniArray({\n type: \"array\",\n element,\n ...normalizeParams(params)\n });\n}\nconst ZodMiniObject = /*@__PURE__*/ $constructor(\"ZodMiniObject\", (inst, def) => {\n $ZodObject.init(inst, def);\n ZodMiniType.init(inst, def);\n installLazyProp(inst, \"shape\", (self) => self._zod.def.shape, false);\n});\n// @__NO_SIDE_EFFECTS__\nfunction object(shape, params) {\n const def = {\n type: \"object\",\n shape: shape ?? {},\n ...normalizeParams(params)\n };\n return new ZodMiniObject(def);\n}\nconst ZodMiniRecord = /*@__PURE__*/ $constructor(\"ZodMiniRecord\", (inst, def) => {\n $ZodRecord.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction record(keyType, valueType, params) {\n if (!valueType || !valueType._zod) return new ZodMiniRecord({\n type: \"record\",\n keyType: /* @__PURE__ */ string(),\n valueType: keyType,\n ...normalizeParams(valueType)\n });\n return new ZodMiniRecord({\n type: \"record\",\n keyType,\n valueType,\n ...normalizeParams(params)\n });\n}\nconst ZodMiniOptional = /*@__PURE__*/ $constructor(\"ZodMiniOptional\", (inst, def) => {\n $ZodOptional.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction optional(innerType) {\n return new ZodMiniOptional({\n type: \"optional\",\n innerType\n });\n}\nconst ZodMiniDefault = /*@__PURE__*/ $constructor(\"ZodMiniDefault\", (inst, def) => {\n $ZodDefault.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction _default(innerType, defaultValue) {\n return new ZodMiniDefault({\n type: \"default\",\n innerType,\n get defaultValue() {\n return typeof defaultValue === \"function\" ? defaultValue() : shallowClone(defaultValue);\n }\n });\n}\n\n//#endregion\n//#region src/lib/hook-io.ts\nconfig(en_default());\nconst HookInputSchema = object({\n session_id: string(),\n transcript_path: string(),\n cwd: string(),\n permission_mode: string(),\n hook_event_name: string(),\n tool_name: string(),\n tool_input: record(string(), unknown())\n});\nasync function readHookInput() {\n const chunks = [];\n for await (const chunk of process.stdin) chunks.push(typeof chunk === \"string\" ? chunk : String(chunk));\n let parsed;\n try {\n parsed = JSON.parse(chunks.join(\"\"));\n } catch (_error) {\n return {\n ok: false,\n raw: chunks.join(\"\").slice(0, 200),\n error: \"invalid JSON\"\n };\n }\n const result = HookInputSchema.safeParse(parsed);\n if (!result.success) return {\n ok: false,\n raw: parsed,\n error: result.error.message\n };\n return {\n ok: true,\n data: result.data\n };\n}\nconst BashInputSchema = object({\n command: string(),\n description: optional(string())\n});\nconst FilePathInputSchema = object({ file_path: string() });\nconst SearchInputSchema = object({\n pattern: string(),\n path: optional(string())\n});\nfunction parseBashInput(toolInput) {\n const result = BashInputSchema.safeParse(toolInput);\n return result.success ? result.data : null;\n}\nfunction parseFilePathInput(toolInput) {\n const result = FilePathInputSchema.safeParse(toolInput);\n return result.success ? result.data : null;\n}\nfunction parseSearchInput(toolInput) {\n const result = SearchInputSchema.safeParse(toolInput);\n return result.success ? result.data : null;\n}\nfunction writeDeny(reason) {\n const decision = { hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n permissionDecision: \"deny\",\n ...reason ? { permissionDecisionReason: reason } : {}\n } };\n process.stdout.write(JSON.stringify(decision));\n}\nfunction writeAdvice(advice) {\n const decision = { hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n additionalContext: advice\n } };\n process.stdout.write(JSON.stringify(decision));\n}\n\n//#endregion\n//#region src/lib/audit.ts\nconst MAX_LOG_SIZE = 1048576;\nfunction getLogPath() {\n const hookDir = path.dirname(fileURLToPath(import.meta.url));\n return path.join(hookDir, \"bst-hook.audit.local.jsonl\");\n}\nfunction rotateIfNeeded(logPath) {\n try {\n if (fs.statSync(logPath).size > MAX_LOG_SIZE) {\n const rotatedPath = `${logPath}.1`;\n fs.renameSync(logPath, rotatedPath);\n }\n } catch (_error) {}\n}\nlet rotationChecked = false;\nfunction audit(entry) {\n try {\n const logPath = getLogPath();\n if (!rotationChecked) {\n rotateIfNeeded(logPath);\n rotationChecked = true;\n }\n const record = {\n timestamp: (/* @__PURE__ */ new Date()).toISOString(),\n ...entry\n };\n fs.appendFileSync(logPath, JSON.stringify(record) + \"\\n\");\n } catch (err) {\n process.stderr.write(`[bst-hook] audit write failed: ${err instanceof Error ? err.message : String(err)}\\n`);\n }\n}\n\n//#endregion\n//#region src/lib/shell-tokenizer.ts\nfunction scanDoubleQuoted(input, start) {\n let content = \"\";\n let i = start;\n while (i < input.length && input[i] !== \"\\\"\") {\n if (input[i] === \"\\\\\" && i + 1 < input.length) {\n const next = input[i + 1];\n if (next === \"\\\"\" || next === \"\\\\\" || next === \"$\" || next === \"`\") {\n content += next;\n i += 2;\n continue;\n }\n }\n content += input[i];\n i++;\n }\n if (i >= input.length) return null;\n return {\n content,\n next: i + 1\n };\n}\nfunction tokenize(input) {\n const tokens = [];\n let current = \"\";\n let i = 0;\n let inToken = false;\n while (i < input.length) {\n const ch = input[i];\n if (ch === \"'\") {\n inToken = true;\n i++;\n while (i < input.length && input[i] !== \"'\") {\n current += input[i];\n i++;\n }\n if (i >= input.length) return {\n ok: false,\n error: \"unterminated single quote\"\n };\n i++;\n } else if (ch === \"\\\"\") {\n inToken = true;\n const scanned = scanDoubleQuoted(input, i + 1);\n if (!scanned) return {\n ok: false,\n error: \"unterminated double quote\"\n };\n current += scanned.content;\n i = scanned.next;\n } else if (ch === \"\\\\\" && i + 1 < input.length) {\n inToken = true;\n current += input[i + 1];\n i += 2;\n } else if (ch === \" \" || ch === \" \") {\n if (inToken) {\n tokens.push(current);\n current = \"\";\n inToken = false;\n }\n i++;\n } else {\n inToken = true;\n current += ch;\n i++;\n }\n }\n if (inToken) tokens.push(current);\n return {\n ok: true,\n tokens\n };\n}\nconst SAFE_UNQUOTED_RE = /^[A-Za-z0-9_\\-./=:@%+,]+$/;\nfunction quoteToken(token) {\n if (token.length > 0 && SAFE_UNQUOTED_RE.test(token)) return token;\n return \"'\" + token.replace(/'/g, \"'\\\\''\") + \"'\";\n}\nfunction joinTokens(tokens) {\n return tokens.map(quoteToken).join(\" \");\n}\n\n//#endregion\n//#region src/lib/shell-splitter.ts\nfunction splitCommand(input) {\n if (!input || !input.trim()) return [];\n const segments = [];\n let current = \"\";\n let i = 0;\n let inSingleQuote = false;\n let inDoubleQuote = false;\n while (i < input.length) {\n const ch = input[i];\n if (ch === \"'\" && !inDoubleQuote) {\n inSingleQuote = !inSingleQuote;\n current += ch;\n i++;\n continue;\n }\n if (ch === \"\\\"\" && !inSingleQuote) {\n inDoubleQuote = !inDoubleQuote;\n current += ch;\n i++;\n continue;\n }\n if (ch === \"\\\\\" && !inSingleQuote && i + 1 < input.length) {\n current += ch + input[i + 1];\n i += 2;\n continue;\n }\n if (!inSingleQuote && !inDoubleQuote) {\n if (ch === \"&\" && i + 1 < input.length && input[i + 1] === \"&\") {\n pushSegment(segments, current, \"&&\");\n current = \"\";\n i += 2;\n continue;\n }\n if (ch === \"&\" && !(i + 1 < input.length && input[i + 1] === \">\") && !(i > 0 && (input[i - 1] === \">\" || input[i - 1] === \"<\"))) {\n pushSegment(segments, current, \"&\");\n current = \"\";\n i++;\n continue;\n }\n if (ch === \"|\" && i + 1 < input.length && input[i + 1] === \"|\") {\n pushSegment(segments, current, \"||\");\n current = \"\";\n i += 2;\n continue;\n }\n if (ch === \"|\") {\n pushSegment(segments, current, \"|\");\n current = \"\";\n i++;\n continue;\n }\n if (ch === \";\") {\n pushSegment(segments, current, \";\");\n current = \"\";\n i++;\n continue;\n }\n }\n current += ch;\n i++;\n }\n const trimmed = current.trim();\n if (trimmed) segments.push({\n command: trimmed,\n operator: null\n });\n return segments;\n}\nfunction pushSegment(segments, text, operator) {\n const trimmed = text.trim();\n if (trimmed) segments.push({\n command: trimmed,\n operator\n });\n}\n\n//#endregion\n//#region src/lib/injection-guard.ts\nfunction hasBraceExpansion(token) {\n let depth = 0;\n for (let i = 0; i < token.length; i++) {\n const ch = token[i];\n if (ch === \"{\") depth++;\n else if (ch === \"}\") {\n if (depth > 0) depth--;\n } else if (depth >= 1 && (ch === \",\" || ch === \".\" && token[i + 1] === \".\")) return true;\n }\n return false;\n}\nfunction checkInjection(tokens, rawSegment) {\n if (/[\\n\\r]/.test(rawSegment)) return {\n safe: false,\n reason: \"unquoted newline in command\"\n };\n for (const token of tokens) {\n if (token === \"`\") return {\n safe: false,\n reason: \"backtick command substitution\"\n };\n if (token.includes(\"`\")) return {\n safe: false,\n reason: \"backtick command substitution\"\n };\n if (token.includes(\"$\")) return {\n safe: false,\n reason: \"unresolved dollar-expansion\"\n };\n if (hasBraceExpansion(token)) return {\n safe: false,\n reason: \"brace expansion\"\n };\n if (token.includes(\"<(\")) return {\n safe: false,\n reason: \"process substitution\"\n };\n if (token.includes(\">(\")) return {\n safe: false,\n reason: \"process substitution\"\n };\n }\n return { safe: true };\n}\n\n//#endregion\n//#region src/lib/env-file.ts\nconst ENV_SAFE_SUFFIXES = [\n \".sample\",\n \".example\",\n \".template\"\n];\nconst ENV_TOOL_OWNED_SUFFIXES = [\".check.env\"];\nconst ENV_FILE_PATTERN = /(^|[/\\\\])(\\.env(\\..*)?|.*\\.env(\\..*)?|\\.env)$/;\nfunction isEnvSafeSuffix(filename) {\n return [...ENV_SAFE_SUFFIXES, ...ENV_TOOL_OWNED_SUFFIXES].some((suffix) => filename.endsWith(suffix));\n}\nfunction isEnvFile(filePath) {\n if (isEnvSafeSuffix(path.basename(filePath))) return false;\n return ENV_FILE_PATTERN.test(filePath);\n}\n\n//#endregion\n//#region src/lib/git-state.ts\nconst GIT_TIMEOUT_MS = 5e3;\nfunction getCurrentBranch(cwd) {\n try {\n const out = execFileSync(\"git\", [\n \"rev-parse\",\n \"--abbrev-ref\",\n \"HEAD\"\n ], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n if (!out || out === \"HEAD\") return null;\n return out;\n } catch {\n return null;\n }\n}\nfunction getRepoTopLevel(cwd) {\n try {\n const out = execFileSync(\"git\", [\"rev-parse\", \"--show-toplevel\"], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n return out === \"\" ? null : out;\n } catch {\n return null;\n }\n}\nfunction getRemoteDefaultBranch(cwd) {\n try {\n const out = execFileSync(\"git\", [\n \"symbolic-ref\",\n \"--short\",\n \"refs/remotes/origin/HEAD\"\n ], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n if (!out) return null;\n return out.startsWith(\"origin/\") ? out.slice(7) : out;\n } catch {\n return null;\n }\n}\nconst DefaultBranchSchema = object({ defaultBranch: optional(string().check(_minLength(1))) });\nfunction getConfiguredDefaultBranch(topLevel) {\n let parsed;\n try {\n parsed = JSON.parse(readFileSync(join(topLevel, \".tooling.json\"), \"utf-8\"));\n } catch {\n return null;\n }\n const result = DefaultBranchSchema.safeParse(parsed);\n return result.success ? result.data.defaultBranch ?? null : null;\n}\nconst ALWAYS_PROTECTED = [\"main\", \"master\"];\nfunction getProtectedBranches(cwd, extra) {\n const set = new Set(ALWAYS_PROTECTED);\n const remote = getRemoteDefaultBranch(cwd);\n if (remote) set.add(remote);\n for (const b of extra) set.add(b);\n return set;\n}\nfunction getUpstream(cwd, branch) {\n try {\n return execFileSync(\"git\", [\n \"rev-parse\",\n \"--abbrev-ref\",\n `${branch}@{upstream}`\n ], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim() || null;\n } catch {\n return null;\n }\n}\nfunction resolveCommit(cwd, rev) {\n try {\n return execFileSync(\"git\", [\n \"rev-parse\",\n \"--verify\",\n \"--quiet\",\n `${rev}^{commit}`\n ], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim() || null;\n } catch {\n return null;\n }\n}\nfunction branchCarriesOtherFiles(cwd, allowed) {\n const toplevel = getRepoTopLevel(cwd);\n if (toplevel === null) return null;\n try {\n const defaultBranch = getConfiguredDefaultBranch(toplevel) ?? getRemoteDefaultBranch(toplevel) ?? \"main\";\n const mergeBase = execFileSync(\"git\", [\n \"merge-base\",\n \"HEAD\",\n `origin/${defaultBranch}`\n ], {\n cwd: toplevel,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n if (!mergeBase) return null;\n const names = (args) => execFileSync(\"git\", args, {\n cwd: toplevel,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().split(\"\\n\").filter((line) => line.length > 0);\n return [...names([\n \"diff\",\n \"--name-only\",\n mergeBase\n ]), ...names([\n \"ls-files\",\n \"--others\",\n \"--exclude-standard\"\n ])].some((name) => !allowed.includes(name));\n } catch {\n return null;\n }\n}\nfunction isPrimaryWorktree(cwd) {\n try {\n const gitDir = execFileSync(\"git\", [\"rev-parse\", \"--absolute-git-dir\"], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n const commonRaw = execFileSync(\"git\", [\"rev-parse\", \"--git-common-dir\"], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n if (!gitDir || !commonRaw) return false;\n const commonDir = resolve(cwd, commonRaw);\n return realpathSync(gitDir) === realpathSync(commonDir);\n } catch {\n return false;\n }\n}\n\n//#endregion\n//#region src/lib/git-short-options.ts\nconst VALUE_TAKING_LETTERS = {\n switch: /* @__PURE__ */ new Set([\"c\", \"C\"]),\n checkout: /* @__PURE__ */ new Set([\"b\", \"B\"]),\n branch: /* @__PURE__ */ new Set([\"u\"]),\n fetch: /* @__PURE__ */ new Set([\"o\", \"j\"])\n};\nfunction splitShortOptionCluster(token, sub) {\n if (!token.startsWith(\"-\") || token.startsWith(\"--\") || token.length <= 1) return;\n const valueLetters = VALUE_TAKING_LETTERS[sub];\n const letters = [];\n const chars = token.slice(1);\n for (let i = 0; i < chars.length; i++) {\n const ch = chars[i];\n letters.push(ch);\n if (valueLetters.has(ch)) return {\n letters,\n attachedValue: chars.slice(i + 1)\n };\n }\n return { letters };\n}\n\n//#endregion\n//#region src/lib/path-scope.ts\nfunction pathFlavorFor(base) {\n return /^[a-zA-Z]:[\\\\/]/.test(base) ? path.win32 : path.posix;\n}\nfunction resolveIn(base, target) {\n return pathFlavorFor(base).resolve(base, target);\n}\nfunction isWithin(dir, target) {\n const pp = pathFlavorFor(dir);\n const rel = pp.relative(dir, target);\n if (rel === \"\") return true;\n if (rel === \"..\" || rel.startsWith(\"..\" + pp.sep)) return false;\n return !pp.isAbsolute(rel);\n}\nfunction samePath(a, b) {\n return pathFlavorFor(a).relative(a, b) === \"\";\n}\nfunction targetsGitDir(filePath, cwd) {\n const pp = pathFlavorFor(cwd);\n const resolved = resolveIn(cwd, filePath);\n if (!isWithin(cwd, resolved)) return false;\n const rel = pp.relative(cwd, resolved);\n if (rel === \"\") return false;\n return rel.split(pp.sep).includes(\".git\");\n}\nfunction targetsNodeModules(filePath, cwd) {\n return resolveIn(cwd, filePath).split(/[/\\\\]/).includes(\"node_modules\");\n}\n\n//#endregion\n//#region src/rules.ts\nconst GIT_FLAGS_WITH_ARG = /* @__PURE__ */ new Set([\n \"-C\",\n \"-c\",\n \"--git-dir\",\n \"--work-tree\"\n]);\nfunction getGitSubcommand(tokens) {\n const args = tokens.slice(1);\n for (let i = 0; i < args.length; i++) {\n const t = args[i];\n if (GIT_FLAGS_WITH_ARG.has(t)) {\n i++;\n continue;\n }\n if (t.startsWith(\"-\")) continue;\n return t;\n }\n}\nconst WORKTREE_ADVICE = \"You're creating a feature branch in the primary worktree. An isolated worktree keeps this work independent of other windows, and with no commits on this branch yet, converting is cheap -- `git worktree add ../<dir> -b <branch>` (a sibling outside the repo, with its own dependency install). Claude Code keeps this session inside its own working tree, so the work continues in a new session opened in the sibling worktree. If you cannot open a session there, stay in the primary checkout on this branch and check `git branch --show-current` before each commit.\";\nfunction isGitBranchCreation(tokens) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"checkout\" && sub !== \"switch\") return false;\n const after = tokens.slice(tokens.indexOf(sub) + 1);\n const creationLetters = sub === \"checkout\" ? [\"b\", \"B\"] : [\"c\", \"C\"];\n return after.some((t) => {\n const cluster = splitShortOptionCluster(t, sub);\n return cluster !== void 0 && cluster.letters.some((l) => creationLetters.includes(l));\n });\n}\nfunction worktreeAdvice(tokens, ctx) {\n return isGitBranchCreation(tokens) && isPrimaryWorktree(ctx.cwd) ? WORKTREE_ADVICE : void 0;\n}\nconst GIT_FORCE_CREATE_LETTER = {\n switch: \"C\",\n checkout: \"B\"\n};\nconst GIT_FORCE_CREATE_LONG = {\n flag: \"--force-create\",\n shortest: \"--force-\"\n};\nfunction gitForceCreateTargets(after, letterSub) {\n const letter = GIT_FORCE_CREATE_LETTER[letterSub];\n const longFlag = GIT_FORCE_CREATE_LONG;\n const targets = [];\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (t === void 0) continue;\n if (letterSub === \"switch\") {\n const [flag, ...rest] = t.split(\"=\");\n if (flag !== void 0 && flag.length >= longFlag.shortest.length && longFlag.flag.startsWith(flag)) {\n const target = rest.length > 0 ? rest.join(\"=\") : after[i + 1];\n if (target !== void 0) targets.push(target);\n continue;\n }\n }\n const cluster = splitShortOptionCluster(t, letterSub);\n if (cluster === void 0 || !cluster.letters.includes(letter)) continue;\n if (cluster.attachedValue !== void 0 && cluster.attachedValue !== \"\") {\n targets.push(cluster.attachedValue);\n continue;\n }\n const next = after[i + 1];\n if (next !== void 0) targets.push(next);\n }\n return targets;\n}\nfunction gitCommandForceCreateTargets(tokens) {\n const sub = getGitSubcommand(tokens);\n if (sub === \"switch\" || sub === \"checkout\") return gitForceCreateTargets(tokens.slice(tokens.indexOf(sub) + 1), sub);\n if (sub === \"worktree\") {\n const worktreeIdx = tokens.indexOf(\"worktree\");\n if (tokens[worktreeIdx + 1] !== \"add\") return [];\n return gitForceCreateTargets(tokens.slice(worktreeIdx + 2), \"checkout\");\n }\n return [];\n}\nfunction isGitProtectedBranchForceCreate(tokens, ctx) {\n const targets = gitCommandForceCreateTargets(tokens);\n if (targets.length === 0) return false;\n const protectedSet = getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []);\n return targets.some((t) => protectedSet.has(t));\n}\nfunction isGitProtectedBranchRefWrite(tokens, ctx) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"branch\") return false;\n const { flags, positionals } = splitGitBranchArgs(tokens.slice(tokens.indexOf(sub) + 1));\n if (positionals.length === 0) return false;\n let deleteMode = false;\n let renameMode = false;\n let copyMode = false;\n let forceMode = false;\n for (const flag of flags) {\n if (flag.letters !== void 0) {\n if (flag.letters.includes(\"d\") || flag.letters.includes(\"D\")) deleteMode = true;\n if (flag.letters.includes(\"m\") || flag.letters.includes(\"M\")) renameMode = true;\n if (flag.letters.includes(\"c\") || flag.letters.includes(\"C\")) copyMode = true;\n if (flag.letters.includes(\"f\") || flag.letters.includes(\"M\") || flag.letters.includes(\"C\")) forceMode = true;\n continue;\n }\n if (flag.name === \"delete\") deleteMode = true;\n if (flag.name === \"move\") renameMode = true;\n if (flag.name === \"copy\") copyMode = true;\n if (flag.name === \"force\") forceMode = true;\n }\n const protectedSet = getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []);\n if (deleteMode && positionals.some((p) => protectedSet.has(p))) return true;\n if (renameMode && positionals.length >= 2 && protectedSet.has(positionals[0])) return true;\n if (forceMode) {\n const destination = renameMode || copyMode ? positionals[positionals.length - 1] : positionals[0];\n if (destination !== void 0 && protectedSet.has(destination)) return true;\n }\n return false;\n}\nfunction refspecDestination(refspec) {\n const spec = refspec.startsWith(\"+\") ? refspec.slice(1) : refspec;\n const colon = spec.lastIndexOf(\":\");\n return (colon === -1 ? spec : spec.slice(colon + 1)).replace(/^refs\\/heads\\//, \"\");\n}\nconst GIT_BRANCH_VALUE_FLAG_NAMES = /* @__PURE__ */ new Set([\n \"contains\",\n \"no-contains\",\n \"merged\",\n \"no-merged\",\n \"points-at\",\n \"sort\",\n \"format\",\n \"set-upstream-to\"\n]);\nfunction splitGitBranchArgs(after) {\n const flags = [];\n const positionals = [];\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (t === void 0) continue;\n if (!t.startsWith(\"-\")) {\n positionals.push(t);\n continue;\n }\n if (t.startsWith(\"--\") && t !== \"--\") {\n const eq = t.indexOf(\"=\");\n const name = eq > 0 ? t.slice(2, eq) : t.slice(2);\n flags.push({\n raw: t,\n name\n });\n if (eq < 0 && GIT_BRANCH_VALUE_FLAG_NAMES.has(name)) i++;\n continue;\n }\n const cluster = splitShortOptionCluster(t, \"branch\");\n if (cluster === void 0) {\n flags.push({ raw: t });\n continue;\n }\n flags.push({\n raw: t,\n letters: cluster.letters\n });\n if (cluster.attachedValue === \"\") i++;\n }\n return {\n flags,\n positionals\n };\n}\nconst GIT_FETCH_VALUE_FLAG_NAMES = /* @__PURE__ */ new Set([\n \"depth\",\n \"deepen\",\n \"shallow-since\",\n \"shallow-exclude\",\n \"negotiation-tip\",\n \"filter\",\n \"jobs\",\n \"server-option\",\n \"upload-pack\",\n \"refmap\",\n \"submodule-prefix\",\n \"recurse-submodules-default\"\n]);\nfunction splitGitFetchArgs(after) {\n const flags = [];\n const positionals = [];\n const refmaps = [];\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (t === void 0) continue;\n if (!t.startsWith(\"-\")) {\n positionals.push(t);\n continue;\n }\n if (t === \"--\") {\n flags.push({ raw: t });\n continue;\n }\n if (t.startsWith(\"--\")) {\n const eq = t.indexOf(\"=\");\n const name = eq > 0 ? t.slice(2, eq) : t.slice(2);\n flags.push({\n raw: t,\n name\n });\n if (name === \"refmap\") {\n if (eq > 0) refmaps.push(t.slice(eq + 1));\n else if (after[i + 1] !== void 0) refmaps.push(after[i + 1]);\n }\n if (eq < 0 && GIT_FETCH_VALUE_FLAG_NAMES.has(name)) i++;\n continue;\n }\n const cluster = splitShortOptionCluster(t, \"fetch\");\n if (cluster === void 0) {\n flags.push({ raw: t });\n continue;\n }\n flags.push({\n raw: t,\n letters: cluster.letters\n });\n if (cluster.attachedValue === \"\") i++;\n }\n return {\n flags,\n positionals,\n refmaps\n };\n}\nfunction fetchRefspecTarget(spec) {\n const body = spec.startsWith(\"+\") ? spec.slice(1) : spec;\n if (body.startsWith(\"^\")) return { kind: \"none\" };\n if (!body.includes(\":\")) return { kind: \"none\" };\n const destination = refspecDestination(body);\n if (destination === \"\") return { kind: \"none\" };\n if (destination.startsWith(\"refs/remotes/\")) return { kind: \"tracking\" };\n if (destination.startsWith(\"refs/\")) return { kind: \"other\" };\n return {\n kind: \"branch\",\n name: destination\n };\n}\nfunction globToAnchoredRegex(glob) {\n const escaped = glob.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n return new RegExp(`^${escaped.replace(/\\\\\\*/g, \".*\")}$`);\n}\nfunction isProtectedFetchBranch(name, protectedSet) {\n if (protectedSet.has(name)) return true;\n if (!name.includes(\"*\")) return false;\n const pattern = globToAnchoredRegex(name);\n for (const member of protectedSet) if (pattern.test(member)) return true;\n return false;\n}\nfunction isGitProtectedBranchFetchWrite(tokens, ctx) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"fetch\") return false;\n const { positionals, refmaps } = splitGitFetchArgs(tokens.slice(tokens.indexOf(sub) + 1));\n const protectedSet = getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []);\n return [...positionals, ...refmaps].some((spec) => {\n const target = fetchRefspecTarget(spec);\n if (target.kind !== \"branch\" || target.name === void 0) return false;\n return isProtectedFetchBranch(target.name, protectedSet);\n });\n}\nfunction isGitProtectedUpdateRef(tokens, ctx) {\n const after = tokens.slice(tokens.indexOf(\"update-ref\") + 1);\n let ref;\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (t === \"-m\") {\n i++;\n continue;\n }\n if (t.startsWith(\"-\")) continue;\n ref = t;\n break;\n }\n if (ref === void 0) return false;\n return getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []).has(ref.replace(/^refs\\/heads\\//, \"\"));\n}\nfunction gitEffectiveCwd(tokens, cwd) {\n let dir = cwd;\n const args = tokens.slice(1);\n for (let i = 0; i < args.length; i++) {\n const t = args[i];\n if (t === \"-C\") {\n const next = args[i + 1];\n if (next !== void 0) dir = path.resolve(dir, next);\n i++;\n continue;\n }\n if (GIT_FLAGS_WITH_ARG.has(t)) {\n i++;\n continue;\n }\n if (!t.startsWith(\"-\")) break;\n }\n return dir;\n}\nconst GIT_SWITCH_NEUTRAL_FLAGS = /* @__PURE__ */ new Set([\n \"-q\",\n \"--quiet\",\n \"-f\",\n \"--force\",\n \"--discard-changes\",\n \"-m\",\n \"--merge\",\n \"--no-track\",\n \"--progress\",\n \"--no-progress\",\n \"--guess\",\n \"--no-guess\",\n \"--ignore-other-worktrees\",\n \"--recurse-submodules\",\n \"--no-recurse-submodules\"\n]);\nconst GIT_SWITCH_CREATE_FLAGS = /* @__PURE__ */ new Set([\n \"--create\",\n \"--force-create\",\n \"--orphan\"\n]);\nconst GIT_CHECKOUT_PATH_FLAGS = /* @__PURE__ */ new Set([\n \"--ours\",\n \"--theirs\",\n \"-p\",\n \"--patch\",\n \"--conflict\",\n \"--pathspec-from-file\",\n \"--pathspec-file-nul\",\n \"--overlay\",\n \"--no-overlay\"\n]);\nconst GIT_SWITCH_NEUTRAL_LETTERS = /* @__PURE__ */ new Set([\n \"q\",\n \"f\",\n \"m\"\n]);\nfunction switchTarget(after, sub, isCheckoutBranch) {\n if (sub === \"checkout\" && after.includes(\"--\")) return void 0;\n const createLetters = sub === \"checkout\" ? [\"b\", \"B\"] : [\"c\", \"C\"];\n const known = (l) => GIT_SWITCH_NEUTRAL_LETTERS.has(l) || createLetters.includes(l);\n const positionals = [];\n let created;\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (!t.startsWith(\"-\")) {\n positionals.push(t);\n continue;\n }\n if (GIT_SWITCH_NEUTRAL_FLAGS.has(t)) continue;\n const eq = t.indexOf(\"=\");\n if (sub === \"checkout\" && GIT_CHECKOUT_PATH_FLAGS.has(eq === -1 ? t : t.slice(0, eq))) return;\n if (GIT_SWITCH_CREATE_FLAGS.has(eq === -1 ? t : t.slice(0, eq))) {\n created = eq === -1 ? after[++i] : t.slice(eq + 1);\n continue;\n }\n const cluster = splitShortOptionCluster(t, sub);\n if (cluster === void 0 || !cluster.letters.every(known)) return null;\n if (cluster.letters.some((l) => createLetters.includes(l))) created = cluster.attachedValue ? cluster.attachedValue : after[++i];\n }\n if (created !== void 0) return created;\n if (positionals.length !== 1) return void 0;\n const target = positionals[0];\n if (target === \"HEAD\" || target === \"@\") return void 0;\n if (sub === \"checkout\" && !isCheckoutBranch(target)) return void 0;\n return target;\n}\nfunction branchSwitch(tokens, ctx, resolved) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"switch\" && sub !== \"checkout\") return void 0;\n const dir = gitEffectiveCwd(tokens, ctx.cwd);\n const isCheckoutBranch = (name) => !resolved || getProtectedBranches(dir, ctx.repoProtectedBranches ?? []).has(name) || resolveCommit(dir, name) !== null;\n const target = switchTarget(tokens.slice(tokens.indexOf(sub) + 1), sub, isCheckoutBranch);\n if (!resolved) return target === void 0 ? void 0 : {\n dir,\n branch: null\n };\n return target === void 0 ? void 0 : {\n dir,\n branch: target\n };\n}\nconst GIT_CHECKED_OUT_WRITES = /* @__PURE__ */ new Set([\n \"commit\",\n \"merge\",\n \"rebase\",\n \"cherry-pick\",\n \"revert\",\n \"am\",\n \"pull\"\n]);\nfunction subcommandArgs(tokens, sub, valueFlags = /* @__PURE__ */ new Set()) {\n const after = tokens.slice(tokens.indexOf(sub) + 1);\n const flags = [];\n const positionals = [];\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (!t.startsWith(\"-\")) {\n positionals.push(t);\n continue;\n }\n flags.push(t);\n if (valueFlags.has(t)) i++;\n }\n return {\n flags,\n positionals\n };\n}\nconst GIT_PUSH_VALUE_FLAGS = /* @__PURE__ */ new Set([\n \"-o\",\n \"--push-option\",\n \"--repo\",\n \"--receive-pack\",\n \"--exec\"\n]);\nfunction isMergeFastForwardFromUpstream(tokens, upstream) {\n const { flags, positionals } = subcommandArgs(tokens, \"merge\");\n if (!flags.includes(\"--ff-only\") || positionals.length !== 1) return false;\n const ref = positionals[0];\n return ref === \"@{u}\" || ref === \"@{upstream}\" || upstream !== null && ref === upstream;\n}\nconst GIT_PULL_NO_REBASE = /* @__PURE__ */ new Set([\n \"--rebase=false\",\n \"--rebase=no\",\n \"--rebase=off\",\n \"--rebase=0\"\n]);\nfunction isPullFromUpstream(tokens, upstream) {\n const { flags, positionals } = subcommandArgs(tokens, \"pull\");\n if (flags.some((t) => t.startsWith(\"--rebase\") && !GIT_PULL_NO_REBASE.has(t) || t === \"--no-ff\" || !t.startsWith(\"--\") && t.slice(1).includes(\"r\"))) return false;\n if (positionals.length === 0) return true;\n if (positionals.length !== 2 || upstream === null) return false;\n const [remote, branch] = positionals;\n return `${remote}/${branch.replace(/^refs\\/heads\\//, \"\")}` === upstream;\n}\nfunction isRefMovingReset(tokens, cwd, head) {\n if (tokens.slice(tokens.indexOf(\"reset\") + 1).includes(\"--\")) return false;\n const { positionals } = subcommandArgs(tokens, \"reset\");\n if (positionals.length !== 1) return false;\n if (head !== \"HEAD\" && /^(HEAD|@)/.test(positionals[0])) return false;\n const target = resolveCommit(cwd, positionals[0]);\n if (target === null) return false;\n return target !== resolveCommit(cwd, head);\n}\nfunction pushDestinations(tokens, current) {\n const { positionals } = subcommandArgs(tokens, \"push\", GIT_PUSH_VALUE_FLAGS);\n const refspecs = positionals.slice(1);\n if (refspecs.length === 0) return null;\n const names = [];\n for (const spec of refspecs) {\n const dst = refspecDestination(spec);\n if (dst === \"HEAD\" || dst === \"@\") {\n if (current !== null) names.push(current);\n continue;\n }\n names.push(dst);\n }\n return names;\n}\nconst GIT_PUSH_EVERY_BRANCH = /* @__PURE__ */ new Set([\n \"--all\",\n \"--branches\",\n \"--mirror\"\n]);\nfunction leavesRefInPlace(tokens, sub) {\n const { flags } = subcommandArgs(tokens, sub);\n if (flags.includes(\"--abort\") || flags.includes(\"--quit\")) return true;\n return sub === \"commit\" && flags.includes(\"--dry-run\");\n}\nfunction movesProtectedRef(tokens, ctx) {\n const sub = getGitSubcommand(tokens);\n if (sub === void 0) return false;\n if (sub === \"switch\" || sub === \"checkout\" || sub === \"worktree\") return isGitProtectedBranchForceCreate(tokens, ctx);\n if (sub === \"branch\") return isGitProtectedBranchRefWrite(tokens, ctx);\n if (sub === \"fetch\") return isGitProtectedBranchFetchWrite(tokens, ctx);\n if (sub === \"update-ref\") return isGitProtectedUpdateRef(tokens, ctx);\n if (sub !== \"push\" && sub !== \"reset\" && !GIT_CHECKED_OUT_WRITES.has(sub)) return false;\n const cwd = gitEffectiveCwd(tokens, ctx.cwd);\n const pending = ctx.pendingBranches?.[cwd];\n const current = pending ? pending.branch : getCurrentBranch(cwd);\n const protectedSet = getProtectedBranches(cwd, ctx.repoProtectedBranches ?? []);\n const onProtected = current !== null && protectedSet.has(current);\n if (sub === \"push\") {\n const { flags } = subcommandArgs(tokens, \"push\", GIT_PUSH_VALUE_FLAGS);\n if (flags.some((f) => GIT_PUSH_EVERY_BRANCH.has(f))) return true;\n const destinations = pushDestinations(tokens, current);\n if (destinations === null) {\n if (flags.includes(\"--tags\")) return false;\n return onProtected;\n }\n return destinations.some((d) => protectedSet.has(d));\n }\n if (!onProtected) return false;\n if (leavesRefInPlace(tokens, sub)) return false;\n if (sub === \"reset\") return isRefMovingReset(tokens, cwd, pending ? current : \"HEAD\");\n if (sub === \"merge\" || sub === \"pull\") {\n const upstream = getUpstream(cwd, current);\n if (sub === \"merge\" && isMergeFastForwardFromUpstream(tokens, upstream)) return false;\n if (sub === \"pull\" && isPullFromUpstream(tokens, upstream)) return false;\n }\n return true;\n}\nconst PNPM_BUILTINS = /* @__PURE__ */ new Set([\n \"install\",\n \"i\",\n \"add\",\n \"remove\",\n \"rm\",\n \"uninstall\",\n \"un\",\n \"update\",\n \"up\",\n \"upgrade\",\n \"list\",\n \"ls\",\n \"ll\",\n \"la\",\n \"why\",\n \"view\",\n \"info\",\n \"show\",\n \"outdated\",\n \"audit\",\n \"licenses\",\n \"init\",\n \"create\",\n \"link\",\n \"unlink\",\n \"pack\",\n \"publish\",\n \"config\",\n \"set\",\n \"get\",\n \"store\",\n \"cache\",\n \"rebuild\",\n \"rb\",\n \"prune\",\n \"dedupe\",\n \"dlx\",\n \"exec\",\n \"run\",\n \"patch\",\n \"patch-commit\",\n \"recursive\",\n \"-r\"\n]);\nconst NPM_READ_OPS = /* @__PURE__ */ new Set([\n \"view\",\n \"info\",\n \"show\"\n]);\nfunction isNpmReadOp(tokens) {\n const sub = tokens[1];\n return sub !== void 0 && NPM_READ_OPS.has(sub);\n}\nfunction getNpmRedirectReason(tokens) {\n const sub = tokens[1];\n const BASE = \"This project uses pnpm.\";\n if (!sub) return `${BASE} Use \\`pnpm\\` instead of npm.`;\n if (sub === \"install\" || sub === \"i\" || sub === \"ci\") {\n if (tokens.slice(2).some((t) => !t.startsWith(\"-\"))) return `${BASE} Use \\`pnpm add <pkg>\\` instead of \\`npm install <pkg>\\`.`;\n return `${BASE} Use \\`pnpm install\\` instead of npm.`;\n }\n if (sub === \"add\") return `${BASE} Use \\`pnpm add <pkg>\\` instead of npm.`;\n if (sub === \"uninstall\" || sub === \"remove\" || sub === \"rm\" || sub === \"un\" || sub === \"r\") return `${BASE} Use \\`pnpm remove <pkg>\\` instead of npm.`;\n if (sub === \"run\" || sub === \"run-script\") return `${BASE} Use \\`pnpm run <script>\\` or \\`pnpm <script>\\` instead of npm.`;\n if (sub === \"test\" || sub === \"t\" || sub === \"tst\") return `${BASE} Use \\`pnpm test\\` instead of npm.`;\n if (sub === \"start\") return `${BASE} Use \\`pnpm start\\` instead of npm.`;\n if (sub === \"stop\") return `${BASE} Use \\`pnpm stop\\` instead of npm.`;\n if (sub === \"restart\") return `${BASE} Use \\`pnpm restart\\` instead of npm.`;\n if (sub === \"exec\") return `${BASE} Use \\`pnpm exec <cmd>\\` instead of npm.`;\n if (sub === \"list\" || sub === \"ls\" || sub === \"ll\" || sub === \"la\") return `${BASE} Use \\`pnpm list\\` instead of npm.`;\n if (sub === \"why\") return `${BASE} Use \\`pnpm why\\` instead of npm.`;\n if (sub === \"outdated\") return `${BASE} Use \\`pnpm outdated\\` instead of npm.`;\n if (sub === \"audit\") return `${BASE} Use \\`pnpm audit\\` instead of npm.`;\n if (sub === \"update\" || sub === \"up\" || sub === \"upgrade\") return `${BASE} Use \\`pnpm update\\` instead of npm.`;\n if (sub === \"init\") return `${BASE} Use \\`pnpm init\\` instead of npm.`;\n if (sub === \"publish\") return `${BASE} Use \\`pnpm publish\\` instead of npm.`;\n if (sub === \"link\") return `${BASE} Use \\`pnpm link\\` instead of npm.`;\n if (sub === \"unlink\") return `${BASE} Use \\`pnpm unlink\\` instead of npm.`;\n if (sub === \"pack\") return `${BASE} Use \\`pnpm pack\\` instead of npm.`;\n if (sub === \"cache\") return `${BASE} Use \\`pnpm store\\` instead of \\`npm cache\\`.`;\n if (sub === \"config\" || sub === \"set\" || sub === \"get\") return `${BASE} Use \\`pnpm config\\` instead of npm.`;\n return `${BASE} Use the equivalent pnpm command instead of npm.`;\n}\nconst LAST_POSITIONAL_DEST = /* @__PURE__ */ new Set([\n \"cp\",\n \"mv\",\n \"rsync\",\n \"install\",\n \"ln\"\n]);\nconst ALL_POSITIONAL_DEST = /* @__PURE__ */ new Set([\"tee\", \"truncate\"]);\nfunction explicitTargetDirectory(rest) {\n for (const [i, t] of rest.entries()) {\n if (t === \"-t\" || t === \"--target-directory\") return rest[i + 1] ?? null;\n if (t.startsWith(\"--target-directory=\")) return t.slice(19);\n }\n return null;\n}\nconst OUTPUT_REDIRECT = /^(?:\\d*|&)>{1,2}$/;\nconst OUTPUT_REDIRECT_ATTACHED = /^(?:\\d*|&)>{1,2}(.+)$/;\nconst INPUT_REDIRECT = /^\\d*<$/;\nconst INPUT_REDIRECT_ATTACHED = /^\\d*<([^<].*)$/;\nfunction spacedRedirectDirection(token) {\n if (OUTPUT_REDIRECT.test(token)) return \"out\";\n if (INPUT_REDIRECT.test(token)) return \"in\";\n return null;\n}\nfunction splitRedirects(tokens) {\n const redirects = [];\n const rest = [];\n for (let i = 0; i < tokens.length; i++) {\n const t = tokens[i];\n const spaced = spacedRedirectDirection(t);\n if (spaced !== null) {\n const next = tokens[i + 1];\n if (next !== void 0) redirects.push({\n direction: spaced,\n target: next\n });\n i++;\n continue;\n }\n const out = OUTPUT_REDIRECT_ATTACHED.exec(t);\n if (out) {\n redirects.push({\n direction: \"out\",\n target: out[1]\n });\n continue;\n }\n const input = INPUT_REDIRECT_ATTACHED.exec(t);\n if (input) {\n redirects.push({\n direction: \"in\",\n target: input[1]\n });\n continue;\n }\n rest.push(t);\n }\n return {\n redirects,\n rest\n };\n}\nfunction redirectTargets(tokens, direction) {\n return splitRedirects(tokens).redirects.filter((r) => r.direction === direction).map((r) => r.target);\n}\nfunction writeDestinations(tokens) {\n const cmd = path.basename(tokens[0] ?? \"\");\n const rest = tokens.slice(1);\n const dests = redirectTargets(tokens, \"out\");\n if (cmd === \"dd\") dests.push(...rest.filter((t) => t.startsWith(\"of=\")).map((t) => t.slice(3)));\n else if (cmd === \"sed\") {\n if (rest.some((t) => t === \"--in-place\" || /^-[a-zA-Z]*i/.test(t))) dests.push(...rest.filter((t) => !t.startsWith(\"-\")));\n } else if (ALL_POSITIONAL_DEST.has(cmd)) dests.push(...rest.filter((t) => !t.startsWith(\"-\")));\n else if (LAST_POSITIONAL_DEST.has(cmd)) {\n if (cmd === \"cp\" && rest.includes(\"--remove-destination\")) return dests;\n const targetDir = explicitTargetDirectory(rest);\n if (targetDir !== null) dests.push(targetDir);\n else {\n const last = rest.findLast((t) => !t.startsWith(\"-\"));\n if (last !== void 0) dests.push(last);\n }\n }\n return dests;\n}\nfunction writesIntoNodeModules(tokens, ctx) {\n return writeDestinations(tokens).some((d) => targetsNodeModules(d, ctx.cwd));\n}\nconst NODE_MODULES_WRITE_REASON = \"Don't write into node_modules/. Its files are hardlinks into pnpm's global store, so overwriting one in place (plain `cp`, `>` redirect, `sed -i`, ...) writes through the link and corrupts that package for every project on this machine -- silently, since the directory you wrote into ends up fine. To test a local build as an installed dependency, run it directly (`node packages/<pkg>/dist/bin.mjs <cmd>`), or install into a scratch tree with an isolated store (`pnpm add --store-dir <scratch> <pkg>`). If you must replace a file in place, `cp --remove-destination` is safe -- it unlinks the target first. Run `pnpm store status` to check whether the store is already damaged.\";\nfunction isChmod777(tokens) {\n return tokens.some((t) => t === \"777\" || t === \"0777\");\n}\nfunction isGitForcePush(tokens) {\n const pushIdx = tokens.indexOf(\"push\");\n if (pushIdx === -1) return false;\n return tokens.slice(pushIdx + 1).some((t) => t === \"--force\" || t === \"-f\");\n}\nconst ENV_READ_FILE_COMMANDS = /* @__PURE__ */ new Set([\n \"cat\",\n \"head\",\n \"tail\",\n \"less\",\n \"more\",\n \"source\",\n \".\"\n]);\nconst GREP_FAMILY_COMMANDS = /* @__PURE__ */ new Set([\n \"grep\",\n \"rg\",\n \"ag\"\n]);\nfunction grepFileOperands(rest) {\n const files = [];\n let patternAccountedFor = false;\n let optionsEnded = false;\n for (let i = 0; i < rest.length; i++) {\n const t = rest[i];\n if (!optionsEnded && t === \"--\") {\n optionsEnded = true;\n continue;\n }\n if (!optionsEnded) {\n if (t === \"-f\" || t === \"--file\") {\n const value = rest[i + 1];\n if (value !== void 0) {\n files.push(value);\n i++;\n }\n patternAccountedFor = true;\n continue;\n }\n if (t.startsWith(\"--file=\")) {\n files.push(t.slice(7));\n patternAccountedFor = true;\n continue;\n }\n if (t.startsWith(\"-f\") && t !== \"-f\" && !t.startsWith(\"--\")) {\n files.push(t.slice(2));\n patternAccountedFor = true;\n continue;\n }\n if (t === \"-e\" || t === \"--regexp\") {\n i++;\n patternAccountedFor = true;\n continue;\n }\n if (t.startsWith(\"--regexp=\") || t.startsWith(\"-e\") && t !== \"-e\" && !t.startsWith(\"--\")) {\n patternAccountedFor = true;\n continue;\n }\n if (t.startsWith(\"-\")) continue;\n }\n if (!patternAccountedFor) {\n patternAccountedFor = true;\n continue;\n }\n files.push(t);\n }\n return files;\n}\nfunction envFileOperands(tokens) {\n const cmd = path.basename(tokens[0] ?? \"\");\n const { redirects, rest } = splitRedirects(tokens.slice(1));\n const operands = redirects.filter((r) => r.direction === \"in\").map((r) => r.target);\n if (GREP_FAMILY_COMMANDS.has(cmd)) operands.push(...grepFileOperands(rest));\n else if (ENV_READ_FILE_COMMANDS.has(cmd)) operands.push(...rest.filter((t) => !t.startsWith(\"-\")));\n return operands;\n}\nconst HEALTH_CEILING_FILES = [\"health-baseline.json\", \".tooling.json\"];\nconst HEALTH_BASELINE_FILE = \"health-baseline.json\";\nconst HEALTH_RAISE_REASON = \"A health ceiling raise travels alone: open a ceiling-only pull request -- only health-baseline.json and .tooling.json changed -- for a human to merge, or split the module. `health:check` refuses the raise beside code either way. To restore the default branch's copy run `git show origin/<default>:health-baseline.json > health-baseline.json`, and to adopt lower counts run `pnpm exec bst health:check --update-baseline`.\";\nfunction writesHealthBaseline(tokens, cwd) {\n const destinations = writeDestinations(tokens);\n if (!destinations.some((dest) => baseName(dest) === \"health-baseline.json\")) return false;\n const root = getRepoTopLevel(cwd) ?? cwd;\n return destinations.some((dest) => samePath(resolveIn(cwd, dest), path.join(root, HEALTH_BASELINE_FILE)));\n}\nfunction baseName(target) {\n const segments = target.split(/[/\\\\]/);\n return segments[segments.length - 1] ?? \"\";\n}\nfunction isBaselineRestore(tokens) {\n if (tokens[0] !== \"git\") return false;\n let i = 1;\n while (tokens[i]?.startsWith(\"-\") === true) i += GIT_FLAGS_WITH_ARG.has(tokens[i] ?? \"\") ? 2 : 1;\n if (tokens[i] !== \"show\") return false;\n return tokens.slice(i + 1).some((token) => token.includes(\":\") && baseName(token.slice(token.lastIndexOf(\":\") + 1)) === \"health-baseline.json\");\n}\nfunction isHealthBaselineReset(tokens) {\n return tokens.includes(\"health:check\") && tokens.includes(\"--update-baseline\") && tokens.includes(\"--reset\");\n}\nfunction raiseTravelsWithCode(cwd) {\n return branchCarriesOtherFiles(cwd, HEALTH_CEILING_FILES) === true;\n}\nconst denyRules = [\n {\n name: \"sudo\",\n match: /^sudo$/,\n action: \"deny\",\n reason: \"Don't use sudo. If you need elevated permissions, ask me to do it manually.\"\n },\n {\n name: \"git-force-push\",\n match: /^git$/,\n action: \"deny\",\n when: (tokens) => isGitForcePush(tokens),\n reason: \"Use `git push --force-with-lease` instead of `--force`.\"\n },\n {\n name: \"git-protected-branch-write\",\n match: /^git$/,\n action: \"deny\",\n when: (tokens, ctx) => movesProtectedRef(tokens, ctx),\n reason: \"This would move a protected branch (main, master, the remote's default branch, or one listed in .claude/hooks/bst-hook.settings.json#protectedBranches). Work on a feature branch in a worktree: `git worktree add ../<repo>-<topic> -b feat/<topic>`, then commit and push there. To bring `main` up to date, use `git pull --ff-only`.\"\n },\n {\n name: \"chmod-777\",\n match: /^chmod$/,\n action: \"deny\",\n when: (tokens) => isChmod777(tokens),\n reason: \"Don't use 777 permissions. Use more restrictive permissions.\"\n },\n {\n name: \"node-modules-write\",\n match: /./,\n action: \"deny\",\n when: (tokens, ctx) => writesIntoNodeModules(tokens, ctx),\n reason: NODE_MODULES_WRITE_REASON\n },\n {\n name: \"env-file-read\",\n match: /./,\n action: \"deny\",\n when: (tokens) => envFileOperands(tokens).some(isEnvFile),\n reason: \"Don't read .env files. These contain secrets that should not appear in the conversation.\"\n },\n {\n name: \"env-file-write\",\n match: /./,\n action: \"deny\",\n when: (tokens) => redirectTargets(tokens, \"out\").some(isEnvFile),\n reason: \"Don't write to .env files. These contain secrets.\"\n },\n {\n name: \"health-baseline-write\",\n match: /./,\n action: \"deny\",\n when: (tokens, ctx) => writesHealthBaseline(tokens, ctx.cwd) && !isBaselineRestore(tokens) && raiseTravelsWithCode(ctx.cwd),\n reason: HEALTH_RAISE_REASON\n },\n {\n name: \"health-baseline-reset\",\n match: /^bst$/,\n action: \"deny\",\n when: (tokens, ctx) => isHealthBaselineReset(tokens) && raiseTravelsWithCode(ctx.cwd),\n reason: HEALTH_RAISE_REASON\n },\n {\n name: \"npx-redirect\",\n match: /^npx$/,\n action: \"deny\",\n reason: \"This project uses pnpm. Use `pnpm dlx <pkg>` or `pnpm exec <cmd>` instead of npx.\"\n },\n {\n name: \"mise-self-update\",\n match: /^mise$/,\n action: \"deny\",\n when: (tokens) => tokens[1] === \"self-update\",\n reason: \"Don't self-update mise. The version is Renovate-managed; ask a human to bump the pin.\"\n }\n];\n\n//#endregion\n//#region src/lib/script-resolver.ts\nconst PackageJsonSchema = object({ scripts: record(string(), unknown()) });\nfunction resolveScript(scriptName, cwd) {\n const scripts = readScripts(cwd);\n if (!scripts) return null;\n const main = scripts[scriptName];\n if (typeof main !== \"string\") return null;\n const pre = scripts[`pre${scriptName}`];\n const post = scripts[`post${scriptName}`];\n return {\n pre: typeof pre === \"string\" ? pre : null,\n main,\n post: typeof post === \"string\" ? post : null\n };\n}\nfunction resolveFilteredScript(scriptName, filterPkg, cwd) {\n const pkgDir = findWorkspacePackageDir(filterPkg, cwd);\n if (!pkgDir) return null;\n const script = resolveScript(scriptName, pkgDir);\n if (!script) return null;\n return {\n ...script,\n pkgDir\n };\n}\nfunction resolveRecursiveScripts(scriptName, rootDir) {\n const pkgDirs = listWorkspacePackageDirs(rootDir);\n if (!pkgDirs) return null;\n const results = [];\n for (const pkgDir of pkgDirs) {\n const resolved = resolveScript(scriptName, pkgDir);\n if (resolved) results.push({\n ...resolved,\n pkgDir\n });\n }\n return results;\n}\nfunction listWorkspacePackageDirs(rootDir) {\n const patterns = readWorkspacePatterns(rootDir);\n if (!patterns) return null;\n const dirs = [];\n for (const pattern of patterns) {\n if (!pattern.endsWith(\"/*\")) continue;\n const baseDir = pattern.slice(0, -2);\n const parentDir = resolve(rootDir, baseDir);\n let entries;\n try {\n entries = readdirSync(parentDir);\n } catch {\n continue;\n }\n for (const entry of entries) {\n const entryDir = join(parentDir, entry);\n try {\n if (!statSync(entryDir).isDirectory()) continue;\n } catch {\n continue;\n }\n dirs.push(entryDir);\n }\n }\n return dirs.length > 0 ? dirs : null;\n}\nfunction findWorkspacePackageDir(pkgName, rootDir) {\n const pkgDirs = listWorkspacePackageDirs(rootDir);\n if (!pkgDirs) return null;\n for (const entryDir of pkgDirs) {\n const name = readPackageName(entryDir);\n if (name === pkgName || name === `@${pkgName}`) return entryDir;\n if (entryDir.split(\"/\").pop() === pkgName) return entryDir;\n if (name) {\n if ((name.startsWith(\"@\") ? name.split(\"/\")[1] : name) === pkgName) return entryDir;\n }\n }\n return null;\n}\nfunction readWorkspacePatterns(rootDir) {\n try {\n const lines = readFileSync(join(rootDir, \"pnpm-workspace.yaml\"), \"utf-8\").split(\"\\n\");\n const packagesIdx = lines.findIndex((l) => /^packages\\s*:/.test(l));\n if (packagesIdx === -1) return null;\n const patterns = [];\n for (let i = packagesIdx + 1; i < lines.length; i++) {\n const line = lines[i];\n const match = /^\\s+-\\s+(.+)$/.exec(line);\n if (!match) break;\n patterns.push(match[1].trim().replace(/['\"`]/g, \"\"));\n }\n return patterns.length > 0 ? patterns : null;\n } catch {\n return null;\n }\n}\nconst PackageNameSchema = object({ name: string() });\nfunction readPackageName(dir) {\n try {\n const raw = readFileSync(join(dir, \"package.json\"), \"utf-8\");\n const result = PackageNameSchema.safeParse(JSON.parse(raw));\n if (!result.success) return null;\n return result.data.name;\n } catch {\n return null;\n }\n}\nfunction readScripts(cwd) {\n try {\n const raw = readFileSync(join(cwd, \"package.json\"), \"utf-8\");\n const result = PackageJsonSchema.safeParse(JSON.parse(raw));\n if (!result.success) return null;\n return result.data.scripts;\n } catch {\n return null;\n }\n}\n\n//#endregion\n//#region src/lib/workspace-root.ts\nfunction findWorkspaceRoot(cwd) {\n let dir = resolve(cwd);\n while (true) {\n if (existsSync(join(dir, \"pnpm-workspace.yaml\"))) return dir;\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\n//#endregion\n//#region src/evaluate.ts\nconst MAX_EVAL_DEPTH = 5;\nfunction subshellBody(command) {\n if (!command.startsWith(\"(\") || !command.endsWith(\")\")) return null;\n let depth = 0;\n let quote = null;\n for (let i = 0; i < command.length; i++) {\n const ch = command[i];\n if (quote !== null) {\n if (ch === quote) quote = null;\n else if (ch === \"\\\\\" && quote === \"\\\"\") i++;\n continue;\n }\n if (ch === \"\\\\\") i++;\n else if (ch === \"'\" || ch === \"\\\"\") quote = ch;\n else if (ch === \"(\") depth++;\n else if (ch === \")\") {\n depth--;\n if (depth === 0) return i === command.length - 1 ? command.slice(1, -1) : null;\n }\n }\n return null;\n}\nfunction extractCdDir(segment, ctx) {\n const tokenResult = tokenize(segment);\n if (!tokenResult.ok || tokenResult.tokens.length !== 2) return null;\n const tokens = tokenResult.tokens;\n if (tokens[0] !== \"cd\") return null;\n const dir = tokens[1];\n if (path.isAbsolute(dir)) return dir;\n return path.resolve(ctx.cwd, dir);\n}\nfunction extractBranchSwitch(segment, ctx) {\n const tokenResult = tokenize(segment);\n if (!tokenResult.ok) return void 0;\n let tokens = tokenResult.tokens;\n let envSkipped = 0;\n while (envSkipped < tokens.length && /^\\w+=/.test(tokens[envSkipped])) envSkipped++;\n tokens = tokens.slice(envSkipped);\n if (tokens.length === 0 || path.basename(tokens[0]) !== \"git\") return void 0;\n return branchSwitch(tokens, ctx, checkInjection(tokens, segment).safe);\n}\nfunction evaluateCommand(command, ctx) {\n if (ctx.depth >= MAX_EVAL_DEPTH) return {\n action: \"none\",\n rule: null,\n reason: \"max evaluation depth exceeded\"\n };\n const trimmed = command.trim();\n if (!trimmed) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n const body = subshellBody(trimmed);\n if (body !== null) return evaluateCommand(body, {\n ...ctx,\n depth: ctx.depth + 1\n });\n const segments = splitCommand(trimmed);\n if (segments.length === 0) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n let firstNone = null;\n const currentCtx = { ...ctx };\n let unknownAfterNext;\n for (const segment of segments) {\n if (segment.operator === \"&&\") {\n const cdDir = extractCdDir(segment.command, currentCtx);\n if (cdDir) currentCtx.cwd = cdDir;\n }\n const result = evaluateSegment(segment.command, currentCtx);\n if (result.action === \"deny\") return result;\n if (!firstNone) firstNone = result;\n if (unknownAfterNext) {\n currentCtx.pendingBranches = {\n ...currentCtx.pendingBranches,\n [unknownAfterNext.dir]: unknownAfterNext\n };\n unknownAfterNext = void 0;\n }\n const switched = extractBranchSwitch(segment.command, currentCtx);\n if (switched) {\n const unknown = {\n dir: switched.dir,\n branch: null\n };\n if (segment.operator === \"&&\") currentCtx.pendingBranches = {\n ...currentCtx.pendingBranches,\n [switched.dir]: switched\n };\n else if (segment.operator === \"||\") unknownAfterNext = unknown;\n else currentCtx.pendingBranches = {\n ...currentCtx.pendingBranches,\n [switched.dir]: unknown\n };\n }\n }\n return firstNone ?? {\n action: \"none\",\n rule: null,\n reason: null\n };\n}\nfunction evaluateResolvedScript(resolved, ctx) {\n if (resolved.pre) {\n const preResult = evaluateCommand(resolved.pre, ctx);\n if (preResult.action === \"deny\") return preResult;\n }\n const mainResult = evaluateCommand(resolved.main, ctx);\n if (mainResult.action === \"deny\") return mainResult;\n if (resolved.post) {\n const postResult = evaluateCommand(resolved.post, ctx);\n if (postResult.action === \"deny\") return postResult;\n }\n return {\n action: \"none\",\n rule: null,\n reason: null\n };\n}\nfunction evaluateSegment(rawSegment, ctx) {\n const trimmed = rawSegment.trim();\n const body = subshellBody(trimmed);\n if (body !== null) return evaluateCommand(body, {\n ...ctx,\n depth: ctx.depth + 1\n });\n if (trimmed.startsWith(\"(\") && trimmed.endsWith(\")\")) return {\n action: \"none\",\n rule: null,\n reason: \"subshell\"\n };\n const tokenResult = tokenize(rawSegment);\n if (!tokenResult.ok) return {\n action: \"none\",\n rule: null,\n reason: tokenResult.error\n };\n let tokens = tokenResult.tokens;\n if (tokens.length === 0) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n let envSkipped = 0;\n while (envSkipped < tokens.length && /^\\w+=/.test(tokens[envSkipped])) envSkipped++;\n if (envSkipped >= tokens.length) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n tokens = tokens.slice(envSkipped);\n if (writesIntoNodeModules(tokens, ctx)) return {\n action: \"deny\",\n rule: \"node-modules-write\",\n reason: NODE_MODULES_WRITE_REASON\n };\n const injection = checkInjection(tokens, rawSegment);\n if (!injection.safe) return {\n action: \"none\",\n rule: null,\n reason: injection.reason\n };\n const SAFE_REDIRECT_TOKENS = /* @__PURE__ */ new Set([\n \"2>&1\",\n \"2>/dev/null\",\n \">/dev/null\",\n \"1>/dev/null\"\n ]);\n tokens = tokens.filter((t, i) => {\n if (SAFE_REDIRECT_TOKENS.has(t)) return false;\n if ((t === \">\" || t === \">>\") && tokens[i + 1] === \"/dev/null\") return false;\n if (t === \"/dev/null\" && i > 0 && (tokens[i - 1] === \">\" || tokens[i - 1] === \">>\")) return false;\n return true;\n });\n const cmdName = path.basename(tokens[0]);\n if (cmdName === \"command\" && tokens.length > 1) return evaluateCommand(joinTokens(tokens.slice(1)), {\n ...ctx,\n depth: ctx.depth + 1\n });\n if (cmdName === \"env\") {\n let envIdx = 1;\n while (envIdx < tokens.length && /^\\w+=/.test(tokens[envIdx])) envIdx++;\n if (envIdx >= tokens.length) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n return evaluateCommand(joinTokens(tokens.slice(envIdx)), {\n ...ctx,\n depth: ctx.depth + 1\n });\n }\n if (cmdName === \"pnpm\") return evaluatePnpm(tokens, ctx);\n if (cmdName === \"npm\") return evaluateNpm(tokens);\n if (cmdName === \"mise\" && tokens[1] === \"exec\") {\n const inner = miseExecInner(tokens);\n if (!inner) return {\n action: \"none\",\n rule: null,\n reason: \"bare mise exec\"\n };\n return evaluateCommand(joinTokens(inner), {\n ...ctx,\n depth: ctx.depth + 1\n });\n }\n if (cmdName === \"bash\" || cmdName === \"sh\" || cmdName === \"zsh\") return evaluateShellC(tokens, ctx);\n if (cmdName === \"xargs\") return evaluateXargs(tokens, ctx);\n if (cmdName === \"node\" && (tokens.includes(\"-e\") || tokens.includes(\"-p\") || tokens.includes(\"--eval\") || tokens.includes(\"--print\"))) return {\n action: \"none\",\n rule: null,\n reason: \"node eval\"\n };\n for (const rule of denyRules) if (rule.match.test(cmdName)) {\n if (!rule.when || rule.when(tokens, ctx)) return {\n action: \"deny\",\n rule: rule.name,\n reason: rule.reason ?? null\n };\n }\n const advice = cmdName === \"git\" ? worktreeAdvice(tokens, ctx) : void 0;\n return {\n action: \"none\",\n rule: null,\n reason: `no rule matched: ${cmdName}`,\n ...advice ? { advice } : {}\n };\n}\nfunction miseExecInner(tokens) {\n const dashIdx = tokens.indexOf(\"--\");\n if (dashIdx === -1) return void 0;\n const inner = tokens.slice(dashIdx + 1);\n return inner.length > 0 ? inner : void 0;\n}\nfunction scanPnpmFlags(subTokens, from, state) {\n for (let i = from; i < subTokens.length; i++) {\n const t = subTokens[i];\n if (t === \"--filter\" || t.startsWith(\"--filter=\") || t === \"-F\" || t.startsWith(\"-F=\")) return {\n name: void 0,\n filterIdx: i\n };\n if (t === \"-r\" || t === \"--recursive\") {\n state.isRecursive = true;\n continue;\n }\n if (t === \"-w\" || t === \"--workspace-root\") {\n state.isWorkspaceRoot = true;\n continue;\n }\n if (t === \"-C\" || t === \"--dir\") {\n state.dirOverride = subTokens[++i];\n continue;\n }\n if (t.startsWith(\"-C=\") || t.startsWith(\"--dir=\")) {\n state.dirOverride = t.split(\"=\", 2)[1];\n continue;\n }\n if (t.startsWith(\"-\") && !PNPM_BUILTINS.has(t)) continue;\n return { name: t };\n }\n return { name: void 0 };\n}\nfunction evaluatePnpm(tokens, ctx) {\n const subTokens = tokens.slice(1);\n const state = {\n isRecursive: false,\n isWorkspaceRoot: false,\n dirOverride: void 0\n };\n const scan = scanPnpmFlags(subTokens, 0, state);\n if (scan.filterIdx !== void 0) return evaluatePnpmFilter(tokens, subTokens, scan.filterIdx, ctx);\n const subcommand = scan.name;\n const subcommandIdx = subcommand ? subTokens.indexOf(subcommand) : -1;\n if (!subcommand) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n if (subcommand === \"dlx\") return {\n action: \"none\",\n rule: null,\n reason: \"pnpm dlx\"\n };\n if (subcommand === \"add\") return {\n action: \"none\",\n rule: null,\n reason: \"pnpm add\"\n };\n if (subcommand === \"exec\") {\n const execArgs = subTokens.slice(subcommandIdx + 1);\n if (execArgs.length === 0) return {\n action: \"none\",\n rule: null,\n reason: \"bare pnpm exec\"\n };\n return evaluateCommand(joinTokens(execArgs), {\n ...ctx,\n depth: ctx.depth + 1\n });\n }\n let scriptName;\n let { isRecursive, isWorkspaceRoot, dirOverride } = state;\n if (subcommand === \"run\") {\n const runScan = scanPnpmFlags(subTokens, subcommandIdx + 1, state);\n ({isRecursive, isWorkspaceRoot, dirOverride} = state);\n if (runScan.filterIdx !== void 0) return evaluatePnpmFilter(tokens, subTokens, runScan.filterIdx, ctx);\n if (!runScan.name) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm run without script name\"\n };\n scriptName = runScan.name;\n } else if (!PNPM_BUILTINS.has(subcommand)) scriptName = subcommand;\n else return {\n action: \"none\",\n rule: null,\n reason: null\n };\n if (isRecursive) return evaluatePnpmRecursive(scriptName, ctx);\n if (isWorkspaceRoot) {\n const root = findWorkspaceRoot(ctx.cwd);\n if (!root) return {\n action: \"none\",\n rule: null,\n reason: \"workspace root not found\"\n };\n return evaluatePnpmScript(scriptName, {\n ...ctx,\n cwd: root\n });\n }\n if (dirOverride) {\n const resolved = path.resolve(ctx.cwd, dirOverride);\n return evaluatePnpmScript(scriptName, {\n ...ctx,\n cwd: resolved\n });\n }\n return evaluatePnpmScript(scriptName, ctx);\n}\nfunction evaluatePnpmScript(scriptName, ctx) {\n const resolved = resolveScript(scriptName, ctx.cwd);\n if (!resolved) return {\n action: \"none\",\n rule: null,\n reason: \"script resolution failed\"\n };\n const res = evaluateResolvedScript(resolved, {\n ...ctx,\n depth: ctx.depth + 1\n });\n if (res.action === \"deny\") return res;\n return {\n action: \"none\",\n rule: `pnpm-script:${scriptName}`,\n reason: null\n };\n}\nfunction evaluatePnpmRecursive(scriptName, ctx) {\n const resolved = resolveRecursiveScripts(scriptName, ctx.cwd);\n if (!resolved || resolved.length === 0) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm recursive\"\n };\n for (const pkg of resolved) {\n const res = evaluateResolvedScript(pkg, {\n ...ctx,\n depth: ctx.depth + 1,\n cwd: pkg.pkgDir ?? ctx.cwd\n });\n if (res.action === \"deny\") return res;\n }\n return {\n action: \"none\",\n rule: `pnpm-recursive:${scriptName}`,\n reason: null\n };\n}\nfunction evaluateNpm(tokens) {\n if (isNpmReadOp(tokens)) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n return {\n action: \"deny\",\n rule: \"npm-redirect\",\n reason: getNpmRedirectReason(tokens)\n };\n}\nfunction evaluatePnpmFilter(_tokens, subTokens, filterIdx, ctx) {\n const filterPkgs = [];\n let cursor = filterIdx;\n while (cursor < subTokens.length) {\n const t = subTokens[cursor];\n if (t === \"--filter\" || t === \"-F\") {\n const pkg = subTokens[cursor + 1];\n if (!pkg || pkg.startsWith(\"-\")) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm --filter without package\"\n };\n filterPkgs.push(pkg);\n cursor += 2;\n } else if (t.startsWith(\"--filter=\") || t.startsWith(\"-F=\")) {\n const pkg = t.startsWith(\"-F=\") ? t.slice(3) : t.slice(9);\n if (!pkg) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm --filter without package\"\n };\n filterPkgs.push(pkg);\n cursor += 1;\n } else break;\n }\n if (filterPkgs.length === 0) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm --filter without package\"\n };\n let filterSubcommand;\n let subcmdIdx = -1;\n for (let i = cursor; i < subTokens.length; i++) {\n const t = subTokens[i];\n if (t === \"--filter\" || t === \"-F\") {\n i += 1;\n continue;\n }\n if (t.startsWith(\"--filter=\") || t.startsWith(\"-F=\")) continue;\n if (t.startsWith(\"-\") && !PNPM_BUILTINS.has(t)) continue;\n filterSubcommand = t;\n subcmdIdx = i;\n break;\n }\n if (!filterSubcommand) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm --filter without subcommand\"\n };\n if (PNPM_BUILTINS.has(filterSubcommand) && filterSubcommand !== \"run\" && filterSubcommand !== \"exec\") return {\n action: \"none\",\n rule: null,\n reason: null\n };\n if (filterSubcommand === \"exec\") {\n const execArgs = subTokens.slice(subcmdIdx + 1);\n if (execArgs.length === 0) return {\n action: \"none\",\n rule: null,\n reason: \"bare pnpm exec\"\n };\n return evaluateCommand(joinTokens(execArgs), {\n ...ctx,\n depth: ctx.depth + 1\n });\n }\n const scriptName = filterSubcommand === \"run\" ? subTokens[subcmdIdx + 1] : filterSubcommand;\n if (!scriptName || scriptName.startsWith(\"-\")) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm --filter run without script name\"\n };\n if (PNPM_BUILTINS.has(scriptName) && scriptName !== \"run\") return {\n action: \"none\",\n rule: null,\n reason: null\n };\n for (const pkg of filterPkgs) {\n const resolved = resolveFilteredScript(scriptName, pkg, ctx.cwd);\n if (!resolved) return {\n action: \"none\",\n rule: null,\n reason: \"filtered script resolution failed\"\n };\n const res = evaluateResolvedScript(resolved, {\n ...ctx,\n depth: ctx.depth + 1,\n cwd: resolved.pkgDir ?? ctx.cwd\n });\n if (res.action === \"deny\") return res;\n }\n return {\n action: \"none\",\n rule: `pnpm-filter:${filterPkgs.join(\",\")}:${scriptName}`,\n reason: null\n };\n}\nconst SHELL_LOGIN_FLAGS = /* @__PURE__ */ new Set([\"-l\", \"--login\"]);\nfunction evaluateShellC(tokens, ctx) {\n let i = 1;\n while (i < tokens.length && SHELL_LOGIN_FLAGS.has(tokens[i])) i++;\n const flag = tokens[i];\n if (flag === void 0 || !/^-[lc]+$/.test(flag) || !flag.includes(\"c\")) return {\n action: \"none\",\n rule: null,\n reason: \"shell without a plain -c\"\n };\n const cmdArg = tokens[i + 1];\n if (!cmdArg) return {\n action: \"none\",\n rule: null,\n reason: \"shell -c without command\"\n };\n if (tokens.length > i + 2) return {\n action: \"none\",\n rule: null,\n reason: \"shell -c with positional args\"\n };\n return evaluateCommand(cmdArg, {\n ...ctx,\n depth: ctx.depth + 1\n });\n}\nconst XARGS_ARG_FLAGS = /* @__PURE__ */ new Set([\n \"-I\",\n \"-d\",\n \"-n\",\n \"-P\",\n \"-s\",\n \"-L\",\n \"-E\",\n \"-a\",\n \"--max-args\",\n \"--max-procs\",\n \"--delimiter\",\n \"--replace\",\n \"--max-lines\",\n \"--max-chars\",\n \"--arg-file\",\n \"--process-slot-var\"\n]);\nconst XARGS_BOOL_FLAGS = /* @__PURE__ */ new Set([\n \"-0\",\n \"--null\",\n \"-t\",\n \"--verbose\",\n \"-r\",\n \"--no-run-if-empty\",\n \"-p\",\n \"--interactive\",\n \"-x\",\n \"--exit\",\n \"--show-limits\"\n]);\nfunction evaluateXargs(tokens, ctx) {\n let i = 1;\n while (i < tokens.length) {\n const t = tokens[i];\n if (!t.startsWith(\"-\")) break;\n if (XARGS_ARG_FLAGS.has(t)) {\n i += 2;\n continue;\n }\n const eqIdx = t.indexOf(\"=\");\n if (eqIdx !== -1) {\n const flagName = t.slice(0, eqIdx);\n if (XARGS_ARG_FLAGS.has(flagName)) {\n i++;\n continue;\n }\n }\n if (t.length > 2 && XARGS_ARG_FLAGS.has(t.slice(0, 2))) {\n i++;\n continue;\n }\n if (XARGS_BOOL_FLAGS.has(t)) {\n i++;\n continue;\n }\n return {\n action: \"none\",\n rule: null,\n reason: \"xargs with unrecognised flags\"\n };\n }\n if (i >= tokens.length) return {\n action: \"none\",\n rule: null,\n reason: \"xargs without command\"\n };\n return evaluateCommand(joinTokens(tokens.slice(i)), {\n ...ctx,\n depth: ctx.depth + 1\n });\n}\n\n//#endregion\n//#region src/file-rules.ts\nconst READ_TOOLS = /* @__PURE__ */ new Set([\n \"Read\",\n \"Glob\",\n \"Grep\"\n]);\nconst WRITE_TOOLS = /* @__PURE__ */ new Set([\"Write\", \"Edit\"]);\nconst fileDenyRules = [\n {\n name: \"env-file-read\",\n tools: READ_TOOLS,\n action: \"deny\",\n when: (filePath) => isEnvFile(filePath),\n reason: \"Don't read .env files. These contain secrets that should not appear in the conversation.\"\n },\n {\n name: \"env-file-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath) => isEnvFile(filePath),\n reason: \"Don't write to .env files. These contain secrets.\"\n },\n {\n name: \"hooks-folder-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx) => {\n const resolved = resolveIn(ctx.cwd, filePath);\n const hooksDir = path.join(ctx.cwd, \".claude\", \"hooks\");\n return isWithin(hooksDir, resolved);\n },\n reason: \"Don't write to .claude/hooks/. These files are managed by the project and should not be modified directly.\"\n },\n {\n name: \"git-dir-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx) => targetsGitDir(filePath, ctx.cwd),\n reason: \"Don't write into .git/ directly. Its internals (config, hooks, refs) are not plain project files -- a config edit can redirect remotes or push destinations that other rules trust, and a hooks/ script runs automatically on the next commit/push. Use git subcommands instead, or ask the user.\"\n },\n {\n name: \"node-modules-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx) => targetsNodeModules(filePath, ctx.cwd),\n reason: \"Don't write into node_modules/. Its files are hardlinks into pnpm's global store -- editing one in place corrupts that package for every project on this machine. Edit the source package and rebuild, or install into a scratch tree with an isolated store (`pnpm add --store-dir <scratch> <pkg>`).\"\n },\n {\n name: \"claude-settings-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx) => {\n const resolved = resolveIn(ctx.cwd, filePath);\n return samePath(resolved, path.join(ctx.cwd, \".claude\", \"settings.json\"));\n },\n reason: \"Don't write to .claude/settings.json. This file is managed by the project and should not be modified directly.\"\n },\n {\n name: \"health-baseline-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx, toolInput) => {\n const resolved = resolveIn(ctx.cwd, filePath);\n if (path.basename(resolved) !== \"health-baseline.json\") return false;\n const root = getRepoTopLevel(ctx.cwd) ?? ctx.cwd;\n if (!samePath(resolved, path.join(root, \"health-baseline.json\"))) return false;\n if (!writeRaisesBaseline(resolved, toolInput)) return false;\n return branchCarriesOtherFiles(ctx.cwd, HEALTH_CEILING_FILES) === true;\n },\n reason: HEALTH_RAISE_REASON\n }\n];\nfunction writeRaisesBaseline(file, toolInput) {\n let before;\n try {\n before = readFileSync(file, \"utf8\");\n } catch (_error) {\n return false;\n }\n const after = textAfterWrite(before, toolInput);\n if (after === void 0) return false;\n const beforeJson = parseJson(before);\n const afterJson = parseJson(after);\n if (beforeJson === void 0 || afterJson === void 0) return false;\n return numberRises(beforeJson, afterJson);\n}\nfunction textAfterWrite(before, toolInput) {\n const { content, old_string: oldText, new_string: newText, replace_all: all } = toolInput;\n if (typeof content === \"string\") return content;\n if (typeof oldText !== \"string\" || typeof newText !== \"string\" || oldText === \"\") return void 0;\n const at = before.indexOf(oldText);\n if (at === -1) return void 0;\n if (all === true) return before.split(oldText).join(newText);\n return before.slice(0, at) + newText + before.slice(at + oldText.length);\n}\nfunction parseJson(text) {\n try {\n const result = record(string(), unknown()).safeParse(JSON.parse(text));\n return result.success ? result.data : void 0;\n } catch (_error) {\n return;\n }\n}\nfunction numberRises(before, after) {\n if (typeof after === \"number\") return typeof before !== \"number\" || after > before;\n if (typeof after !== \"object\" || after === null) return false;\n const beforeObject = typeof before === \"object\" && before !== null ? before : {};\n return Object.entries(after).some(([key, value]) => numberRises(Object.entries(beforeObject).find(([k]) => k === key)?.[1], value));\n}\n\n//#endregion\n//#region src/evaluate-tool.ts\nconst FILE_PATH_TOOLS = /* @__PURE__ */ new Set([\n \"Read\",\n \"Write\",\n \"Edit\"\n]);\nconst SEARCH_TOOLS = /* @__PURE__ */ new Set([\"Glob\", \"Grep\"]);\nfunction extractPath(toolName, toolInput, cwd) {\n if (FILE_PATH_TOOLS.has(toolName)) return parseFilePathInput(toolInput)?.file_path ?? null;\n if (SEARCH_TOOLS.has(toolName)) return parseSearchInput(toolInput)?.path ?? cwd;\n return null;\n}\nfunction evaluateToolUse(toolName, toolInput, ctx) {\n const filePath = extractPath(toolName, toolInput, ctx.cwd);\n if (filePath === null) return {\n action: \"none\",\n rule: null,\n reason: \"unknown tool input shape\"\n };\n for (const rule of fileDenyRules) if (rule.tools.has(toolName) && rule.when(filePath, ctx, toolInput)) return {\n action: \"deny\",\n rule: rule.name,\n reason: rule.reason ?? null\n };\n return {\n action: \"none\",\n rule: null,\n reason: \"no file rule matched\"\n };\n}\n\n//#endregion\n//#region src/lib/settings.ts\nconst SETTINGS_PATH = \".claude/hooks/bst-hook.settings.json\";\nconst BstHookSettingsSchema = object({ protectedBranches: _default(array(string().check(_minLength(1))), []) });\nfunction isFileClean(cwd, relativePath) {\n try {\n execFileSync(\"git\", [\n \"ls-files\",\n \"--error-unmatch\",\n relativePath\n ], {\n cwd,\n stdio: \"ignore\"\n });\n execFileSync(\"git\", [\n \"diff\",\n \"--quiet\",\n \"HEAD\",\n \"--\",\n relativePath\n ], {\n cwd,\n stdio: \"ignore\"\n });\n return true;\n } catch {\n return false;\n }\n}\nfunction loadRepoSettings(cwd) {\n const root = findWorkspaceRoot(cwd) ?? cwd;\n const settingsFile = join(root, SETTINGS_PATH);\n const empty = { protectedBranches: [] };\n if (!existsSync(settingsFile)) return empty;\n if (!isFileClean(root, SETTINGS_PATH)) return empty;\n try {\n const raw = readFileSync(settingsFile, \"utf-8\");\n const parsed = JSON.parse(raw);\n const result = BstHookSettingsSchema.safeParse(parsed);\n if (!result.success) return empty;\n return { protectedBranches: result.data.protectedBranches };\n } catch {\n return empty;\n }\n}\n\n//#endregion\n//#region src/index.ts\nconst inputResult = await readHookInput();\nif (!inputResult.ok) {\n process.stderr.write(`[bst-hook] invalid hook input: ${inputResult.error}\\n`);\n audit({\n sessionId: \"unknown\",\n cwd: \"unknown\",\n command: `<invalid input: ${inputResult.error}>`,\n decision: \"none\",\n rule: null,\n reason: `validation failed: ${inputResult.error}`,\n permissionMode: void 0\n });\n process.exit(0);\n}\nconst input = inputResult.data;\nif (input.hook_event_name !== \"PreToolUse\") process.exit(0);\nif (!(/* @__PURE__ */ new Set([\n \"Bash\",\n \"Read\",\n \"Write\",\n \"Edit\",\n \"Glob\",\n \"Grep\"\n])).has(input.tool_name)) process.exit(0);\nconst repoSettings = loadRepoSettings(input.cwd);\nlet decision;\nlet auditCommand;\nif (input.tool_name === \"Bash\") {\n const bashInput = parseBashInput(input.tool_input);\n if (!bashInput) {\n audit({\n sessionId: input.session_id,\n cwd: input.cwd,\n toolName: input.tool_name,\n command: `<invalid Bash input: ${JSON.stringify(input.tool_input).slice(0, 200)}>`,\n decision: \"none\",\n rule: null,\n reason: \"Bash tool_input missing or invalid 'command' field\",\n permissionMode: input.permission_mode\n });\n process.exit(0);\n }\n decision = evaluateCommand(bashInput.command, {\n cwd: input.cwd,\n depth: 0,\n repoProtectedBranches: repoSettings.protectedBranches\n });\n auditCommand = bashInput.command;\n} else {\n const projectDir = findWorkspaceRoot(input.cwd) ?? process.env[\"CLAUDE_PROJECT_DIR\"] ?? input.cwd;\n decision = evaluateToolUse(input.tool_name, input.tool_input, {\n cwd: projectDir,\n sessionId: input.session_id\n });\n const filePath = String(input.tool_input[\"file_path\"] ?? input.tool_input[\"path\"] ?? \"\");\n auditCommand = `${input.tool_name} ${filePath}`;\n}\naudit({\n sessionId: input.session_id,\n cwd: input.cwd,\n toolName: input.tool_name,\n command: auditCommand,\n decision: decision.action,\n rule: decision.rule,\n reason: decision.reason,\n permissionMode: input.permission_mode\n});\nif (decision.action === \"deny\") writeDeny(decision.reason);\nelse if (decision.advice) writeAdvice(decision.advice);\n\n//#endregion\nexport { };",
8425
+ guide: "# bst-hook Permission Guide\n\nThis project uses a PreToolUse hook (`bst-hook`) that checks each Bash command\nand file tool call before it runs. The hook only ever blocks a command; it does\nnot approve anything on its own. A command that matches a rule below is denied,\nwith a message that says what to do instead.\n\n## Denied commands\n\nThese are blocked. The hook tells you what to do instead.\n\n### Bash\n\n- **sudo**: Don't use sudo. If you need elevated permissions, ask me to do it manually.\n- **git-force-push**: Use `git push --force-with-lease` instead of `--force`.\n- **git-protected-branch-write**: This would move a protected branch (main, master, the remote's default branch, or one listed in .claude/hooks/bst-hook.settings.json#protectedBranches). Work on a feature branch in a worktree: `git worktree add ../<repo>-<topic> -b feat/<topic>`, then commit and push there. To bring `main` up to date, use `git pull --ff-only`.\n- **chmod-777**: Don't use 777 permissions. Use more restrictive permissions.\n- **node-modules-write**: Don't write into node_modules/. Its files are hardlinks into pnpm's global store, so overwriting one in place (plain `cp`, `>` redirect, `sed -i`, ...) writes through the link and corrupts that package for every project on this machine -- silently, since the directory you wrote into ends up fine. To test a local build as an installed dependency, run it directly (`node packages/<pkg>/dist/bin.mjs <cmd>`), or install into a scratch tree with an isolated store (`pnpm add --store-dir <scratch> <pkg>`). If you must replace a file in place, `cp --remove-destination` is safe -- it unlinks the target first. Run `pnpm store status` to check whether the store is already damaged.\n- **env-file-read**: Don't read .env files. These contain secrets that should not appear in the conversation.\n- **env-file-write**: Don't write to .env files. These contain secrets.\n- **health-baseline-write**: A health ceiling raise travels alone: open a ceiling-only pull request -- only health-baseline.json and .tooling.json changed -- for a human to merge, or split the module. `health:check` refuses the raise beside code either way. To restore the default branch's copy run `git show origin/<default>:health-baseline.json > health-baseline.json`, and to adopt lower counts run `pnpm exec bst health:check --update-baseline`.\n- **health-baseline-reset**: A health ceiling raise travels alone: open a ceiling-only pull request -- only health-baseline.json and .tooling.json changed -- for a human to merge, or split the module. `health:check` refuses the raise beside code either way. To restore the default branch's copy run `git show origin/<default>:health-baseline.json > health-baseline.json`, and to adopt lower counts run `pnpm exec bst health:check --update-baseline`.\n- **npx-redirect**: This project uses pnpm. Use `pnpm dlx <pkg>` or `pnpm exec <cmd>` instead of npx.\n- **mise-self-update**: Don't self-update mise. The version is Renovate-managed; ask a human to bump the pin.\n\n### File tools (Read, Write, Edit, Glob, Grep)\n\n- **env-file-read**: Don't read .env files. These contain secrets that should not appear in the conversation.\n- **env-file-write**: Don't write to .env files. These contain secrets.\n- **hooks-folder-write**: Don't write to .claude/hooks/. These files are managed by the project and should not be modified directly.\n- **git-dir-write**: Don't write into .git/ directly. Its internals (config, hooks, refs) are not plain project files -- a config edit can redirect remotes or push destinations that other rules trust, and a hooks/ script runs automatically on the next commit/push. Use git subcommands instead, or ask the user.\n- **node-modules-write**: Don't write into node_modules/. Its files are hardlinks into pnpm's global store -- editing one in place corrupts that package for every project on this machine. Edit the source package and rebuild, or install into a scratch tree with an isolated store (`pnpm add --store-dir <scratch> <pkg>`).\n- **claude-settings-write**: Don't write to .claude/settings.json. This file is managed by the project and should not be modified directly.\n- **health-baseline-write**: A health ceiling raise travels alone: open a ceiling-only pull request -- only health-baseline.json and .tooling.json changed -- for a human to merge, or split the module. `health:check` refuses the raise beside code either way. To restore the default branch's copy run `git show origin/<default>:health-baseline.json > health-baseline.json`, and to adopt lower counts run `pnpm exec bst health:check --update-baseline`.\n\n## How commands are evaluated\n\n- **Pipelines and chains**: every segment is checked; a deny on any segment blocks the whole command.\n- **pnpm scripts**: `pnpm <script>` resolves against `package.json` (including pre/post hooks and `--filter` workspaces) and evaluates the resolved command recursively.\n- **cd propagation**: `cd dir && cmd` evaluates `cmd` relative to the new directory.\n- **Branch switch propagation**: `git switch <branch> && cmd` (or `git checkout <branch>`) evaluates a git write in `cmd` against the branch it switched to, not the one checked out before. This holds only across `&&` and only in the same directory.\n- **Audit log**: all decisions are logged to `.claude/hooks/bst-hook.audit.local.jsonl`.\n\n## Everything else\n\nA command that matches none of the rules above is judged by Claude Code's own\nauto mode, not by this hook.\n"
8354
8426
  }) {
8355
8427
  const results = [{
8356
8428
  filePath: HOOK_PATH,
@@ -8785,16 +8857,10 @@ async function generateLefthook(ctx) {
8785
8857
  }
8786
8858
  //#endregion
8787
8859
  //#region src/generators/vscode-settings.ts
8788
- const SCHEMA_NPM_PATH = "@bensandee/config/schemas/forgejo-workflow.schema.json";
8789
8860
  const SCHEMA_LOCAL_PATH = ".vscode/forgejo-workflow.schema.json";
8790
8861
  const SETTINGS_PATH = ".vscode/settings.json";
8791
8862
  const SCHEMA_GLOB = ".forgejo/workflows/*.{yml,yaml}";
8792
8863
  const VscodeSettingsSchema = z.looseObject({ "yaml.schemas": z.record(z.string(), z.unknown()).default({}) });
8793
- function readSchemaFromNodeModules(targetDir) {
8794
- const candidate = path.join(targetDir, "node_modules", SCHEMA_NPM_PATH);
8795
- if (!existsSync(candidate)) return void 0;
8796
- return readFileSync(candidate, "utf-8");
8797
- }
8798
8864
  function serializeSettings(settings) {
8799
8865
  return JSON.stringify(settings, null, 2) + "\n";
8800
8866
  }
@@ -8861,15 +8927,7 @@ async function generateVscodeSettings(ctx) {
8861
8927
  });
8862
8928
  return results;
8863
8929
  }
8864
- const schemaContent = readSchemaFromNodeModules(ctx.targetDir);
8865
- if (!schemaContent) {
8866
- results.push({
8867
- filePath: SCHEMA_LOCAL_PATH,
8868
- action: "skipped",
8869
- description: "Could not find @bensandee/config schema in node_modules"
8870
- });
8871
- return results;
8872
- }
8930
+ const schemaContent = "{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"$comment\": \"GitHub Actions workflow schema extended with Forgejo-specific properties\",\n \"additionalProperties\": false,\n \"definitions\": {\n \"architecture\": {\n \"type\": \"string\",\n \"enum\": [\"ARM32\", \"x64\", \"x86\"]\n },\n \"branch\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#onpushpull_requestbranchestags\",\n \"$ref\": \"#/definitions/globs\",\n \"description\": \"When using the push and pull_request events, you can configure a workflow to run on specific branches or tags. If you only define only tags or only branches, the workflow won't run for events affecting the undefined Git ref.\\nThe branches, branches-ignore, tags, and tags-ignore keywords accept glob patterns that use the * and ** wildcard characters to match more than one branch or tag name. For more information, see https://help.github.com/en/github/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet.\\nThe patterns defined in branches and tags are evaluated against the Git ref's name. For example, defining the pattern mona/octocat in branches will match the refs/heads/mona/octocat Git ref. The pattern releases/** will match the refs/heads/releases/10 Git ref.\\nYou can use two types of filters to prevent a workflow from running on pushes and pull requests to tags and branches:\\n- branches or branches-ignore - You cannot use both the branches and branches-ignore filters for the same event in a workflow. Use the branches filter when you need to filter branches for positive matches and exclude branches. Use the branches-ignore filter when you only need to exclude branch names.\\n- tags or tags-ignore - You cannot use both the tags and tags-ignore filters for the same event in a workflow. Use the tags filter when you need to filter tags for positive matches and exclude tags. Use the tags-ignore filter when you only need to exclude tag names.\\nYou can exclude tags and branches using the ! character. The order that you define patterns matters.\\n- A matching negative pattern (prefixed with !) after a positive match will exclude the Git ref.\\n- A matching positive pattern after a negative match will include the Git ref again.\"\n },\n \"concurrency\": {\n \"type\": \"object\",\n \"properties\": {\n \"group\": {\n \"$comment\": \"https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#example-using-concurrency-to-cancel-any-in-progress-job-or-run-1\",\n \"description\": \"When a concurrent job or workflow is queued, if another job or workflow using the same concurrency group in the repository is in progress, the queued job or workflow will be pending. Any previously pending job or workflow in the concurrency group will be canceled.\",\n \"type\": \"string\"\n },\n \"cancel-in-progress\": {\n \"$comment\": \"https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#example-using-concurrency-to-cancel-any-in-progress-job-or-run-1\",\n \"description\": \"To cancel any currently running job or workflow in the same concurrency group, specify cancel-in-progress: true.\",\n \"oneOf\": [\n {\n \"type\": \"boolean\"\n },\n {\n \"$ref\": \"#/definitions/expressionSyntax\"\n }\n ]\n }\n },\n \"required\": [\"group\"],\n \"additionalProperties\": false\n },\n \"configuration\": {\n \"oneOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"number\"\n },\n {\n \"type\": \"boolean\"\n },\n {\n \"type\": \"object\",\n \"additionalProperties\": {\n \"$ref\": \"#/definitions/configuration\"\n }\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/configuration\"\n }\n }\n ]\n },\n \"container\": {\n \"type\": \"object\",\n \"properties\": {\n \"image\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idcontainerimage\",\n \"description\": \"The Docker image to use as the container to run the action. The value can be the Docker Hub image name or a registry name.\",\n \"type\": \"string\"\n },\n \"credentials\": {\n \"$comment\": \"https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#jobsjob_idcontainercredentials\",\n \"description\": \"If the image's container registry requires authentication to pull the image, you can use credentials to set a map of the username and password. The credentials are the same values that you would provide to the `docker login` command.\",\n \"type\": \"object\",\n \"properties\": {\n \"username\": {\n \"type\": \"string\"\n },\n \"password\": {\n \"type\": \"string\"\n }\n }\n },\n \"env\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idcontainerenv\",\n \"$ref\": \"#/definitions/env\",\n \"description\": \"Sets an array of environment variables in the container.\"\n },\n \"ports\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idcontainerports\",\n \"description\": \"Sets an array of ports to expose on the container.\",\n \"type\": \"array\",\n \"items\": {\n \"oneOf\": [\n {\n \"type\": \"number\"\n },\n {\n \"type\": \"string\"\n }\n ]\n },\n \"minItems\": 1\n },\n \"volumes\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idcontainervolumes\",\n \"description\": \"Sets an array of volumes for the container to use. You can use volumes to share data between services or other steps in a job. You can specify named Docker volumes, anonymous Docker volumes, or bind mounts on the host.\\nTo specify a volume, you specify the source and destination path: <source>:<destinationPath>\\nThe <source> is a volume name or an absolute path on the host machine, and <destinationPath> is an absolute path in the container.\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n },\n \"minItems\": 1\n },\n \"options\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idcontaineroptions\",\n \"description\": \"Additional Docker container resource options. For a list of options, see https://docs.docker.com/engine/reference/commandline/create/#options.\",\n \"type\": \"string\"\n }\n },\n \"required\": [\"image\"],\n \"additionalProperties\": false\n },\n \"defaults\": {\n \"type\": \"object\",\n \"properties\": {\n \"run\": {\n \"type\": \"object\",\n \"properties\": {\n \"shell\": {\n \"$ref\": \"#/definitions/shell\"\n },\n \"working-directory\": {\n \"$ref\": \"#/definitions/working-directory\"\n }\n },\n \"minProperties\": 1,\n \"additionalProperties\": false\n }\n },\n \"minProperties\": 1,\n \"additionalProperties\": false\n },\n \"permissions\": {\n \"$comment\": \"https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#permissions\",\n \"description\": \"You can modify the default permissions granted to the GITHUB_TOKEN, adding or removing access as required, so that you only allow the minimum required access.\",\n \"oneOf\": [\n {\n \"type\": \"string\",\n \"enum\": [\"read-all\", \"write-all\"]\n },\n {\n \"$ref\": \"#/definitions/permissions-event\"\n }\n ]\n },\n \"permissions-event\": {\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"properties\": {\n \"actions\": {\n \"$ref\": \"#/definitions/permissions-level\"\n },\n \"artifact-metadata\": {\n \"$ref\": \"#/definitions/permissions-level\"\n },\n \"attestations\": {\n \"$ref\": \"#/definitions/permissions-level\"\n },\n \"checks\": {\n \"$ref\": \"#/definitions/permissions-level\"\n },\n \"contents\": {\n \"$ref\": \"#/definitions/permissions-level\"\n },\n \"deployments\": {\n \"$ref\": \"#/definitions/permissions-level\"\n },\n \"discussions\": {\n \"$ref\": \"#/definitions/permissions-level\"\n },\n \"id-token\": {\n \"$ref\": \"#/definitions/permissions-level\"\n },\n \"issues\": {\n \"$ref\": \"#/definitions/permissions-level\"\n },\n \"models\": {\n \"type\": \"string\",\n \"enum\": [\"read\", \"none\"]\n },\n \"packages\": {\n \"$ref\": \"#/definitions/permissions-level\"\n },\n \"pages\": {\n \"$ref\": \"#/definitions/permissions-level\"\n },\n \"pull-requests\": {\n \"$ref\": \"#/definitions/permissions-level\"\n },\n \"repository-projects\": {\n \"$ref\": \"#/definitions/permissions-level\"\n },\n \"security-events\": {\n \"$ref\": \"#/definitions/permissions-level\"\n },\n \"statuses\": {\n \"$ref\": \"#/definitions/permissions-level\"\n }\n }\n },\n \"permissions-level\": {\n \"type\": \"string\",\n \"enum\": [\"read\", \"write\", \"none\"]\n },\n \"env\": {\n \"$comment\": \"https://docs.github.com/en/actions/learn-github-actions/environment-variables\",\n \"description\": \"To set custom environment variables, you need to specify the variables in the workflow file. You can define environment variables for a step, job, or entire workflow using the jobs.<job_id>.steps[*].env, jobs.<job_id>.env, and env keywords. For more information, see https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#jobsjob_idstepsenv\",\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"additionalProperties\": {\n \"oneOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"number\"\n },\n {\n \"type\": \"boolean\"\n }\n ]\n }\n },\n {\n \"$ref\": \"#/definitions/stringContainingExpressionSyntax\"\n }\n ]\n },\n \"environment\": {\n \"$comment\": \"https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#jobsjob_idenvironment\",\n \"description\": \"The environment that the job references\",\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"$comment\": \"https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#example-using-a-single-environment-name\",\n \"description\": \"The name of the environment configured in the repo.\",\n \"type\": \"string\"\n },\n \"url\": {\n \"$comment\": \"https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#example-using-environment-name-and-url\",\n \"description\": \"A deployment URL\",\n \"type\": \"string\"\n }\n },\n \"required\": [\"name\"],\n \"additionalProperties\": false\n },\n \"event\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows\",\n \"type\": \"string\",\n \"enum\": [\n \"branch_protection_rule\",\n \"check_run\",\n \"check_suite\",\n \"create\",\n \"delete\",\n \"deployment\",\n \"deployment_status\",\n \"discussion\",\n \"discussion_comment\",\n \"fork\",\n \"gollum\",\n \"issue_comment\",\n \"issues\",\n \"label\",\n \"merge_group\",\n \"milestone\",\n \"page_build\",\n \"project\",\n \"project_card\",\n \"project_column\",\n \"public\",\n \"pull_request\",\n \"pull_request_review\",\n \"pull_request_review_comment\",\n \"pull_request_target\",\n \"push\",\n \"registry_package\",\n \"release\",\n \"status\",\n \"watch\",\n \"workflow_call\",\n \"workflow_dispatch\",\n \"workflow_run\",\n \"repository_dispatch\"\n ]\n },\n \"eventObject\": {\n \"oneOf\": [\n {\n \"type\": \"object\"\n },\n {\n \"type\": \"null\"\n }\n ],\n \"additionalProperties\": true\n },\n \"expressionSyntax\": {\n \"$comment\": \"escape `{` and `}` in pattern to be unicode compatible (#1360)\",\n \"type\": \"string\",\n \"pattern\": \"^\\\\$\\\\{\\\\{(.|[\\r\\n])*\\\\}\\\\}$\"\n },\n \"stringContainingExpressionSyntax\": {\n \"$comment\": \"escape `{` and `}` in pattern to be unicode compatible (#1360)\",\n \"type\": \"string\",\n \"pattern\": \"^.*\\\\$\\\\{\\\\{(.|[\\r\\n])*\\\\}\\\\}.*$\"\n },\n \"globs\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"minItems\": 1\n },\n \"machine\": {\n \"type\": \"string\",\n \"enum\": [\"linux\", \"macos\", \"windows\"]\n },\n \"name\": {\n \"type\": \"string\",\n \"pattern\": \"^[_a-zA-Z][a-zA-Z0-9_-]*$\"\n },\n \"path\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#onpushpull_requestpaths\",\n \"$ref\": \"#/definitions/globs\",\n \"description\": \"When using the push and pull_request events, you can configure a workflow to run when at least one file does not match paths-ignore or at least one modified file matches the configured paths. Path filters are not evaluated for pushes to tags.\\nThe paths-ignore and paths keywords accept glob patterns that use the * and ** wildcard characters to match more than one path name. For more information, see https://help.github.com/en/github/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet.\\nYou can exclude paths using two types of filters. You cannot use both of these filters for the same event in a workflow.\\n- paths-ignore - Use the paths-ignore filter when you only need to exclude path names.\\n- paths - Use the paths filter when you need to filter paths for positive matches and exclude paths.\"\n },\n \"shell\": {\n \"$comment\": \"https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsshell\",\n \"description\": \"You can override the default shell settings in the runner's operating system using the shell keyword. You can use built-in shell keywords, or you can define a custom set of shell options.\",\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"$comment\": \"https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#custom-shell\",\n \"type\": \"string\",\n \"enum\": [\"bash\", \"pwsh\", \"python\", \"sh\", \"cmd\", \"powershell\"]\n }\n ]\n },\n \"snapshot\": {\n \"$comment\": \"https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idsnapshot\",\n \"description\": \"You can use `jobs.<job_id>.snapshot` to generate a custom image.\\nAdd the snapshot keyword to the job, using either the string syntax or mapping syntax as shown in https://docs.github.com/en/actions/how-tos/manage-runners/larger-runners/use-custom-images#generating-a-custom-image.\\nEach job that includes the snapshot keyword creates a separate image. To generate only one image or image version, include all workflow steps in a single job. Each successful run of a job that includes the snapshot keyword creates a new version of that image.\\nFor more information, see https://docs.github.com/en/actions/how-tos/manage-runners/larger-runners/use-custom-images.\",\n \"oneOf\": [\n {\n \"$comment\": \"https://docs.github.com/en/actions/how-tos/manage-runners/larger-runners/use-custom-images#string-syntax\",\n \"type\": \"string\",\n \"description\": \"You can use the string syntax with `snapshot` to define the image name. This method creates a new image or adds a new version to an existing image with the same name. You cannot specify a version number using this syntax.\"\n },\n {\n \"$comment\": \"https://docs.github.com/en/actions/how-tos/manage-runners/larger-runners/use-custom-images#mapping-syntax\",\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"description\": \"You can use the mapping syntax with `snapshot` to define both the `image-name` and the optional `version`. When you specify a major version, the minor versioning automatically increments if that major version already exists. Patch versions are not supported.\",\n \"properties\": {\n \"image-name\": {\n \"type\": \"string\"\n },\n \"version\": {\n \"$comment\": \"https://docs.github.com/en/actions/how-tos/manage-runners/larger-runners/use-custom-images#specifying-a-version-in-your-workflow\",\n \"type\": \"string\",\n \"pattern\": \"^\\\\d+(\\\\.\\\\d+|\\\\*)?$\"\n }\n },\n \"required\": [\"image-name\"]\n }\n ]\n },\n \"step\": {\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"dependencies\": {\n \"working-directory\": [\"run\"],\n \"shell\": [\"run\"]\n },\n \"oneOf\": [\n {\n \"required\": [\"uses\"]\n },\n {\n \"required\": [\"run\"]\n }\n ],\n \"properties\": {\n \"id\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idstepsid\",\n \"description\": \"A unique identifier for the step. You can use the id to reference the step in contexts. For more information, see https://help.github.com/en/articles/contexts-and-expression-syntax-for-github-actions.\",\n \"type\": \"string\"\n },\n \"if\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idstepsif\",\n \"description\": \"You can use the if conditional to prevent a step from running unless a condition is met. You can use any supported context and expression to create a conditional.\\nExpressions in an if conditional do not require the ${{ }} syntax. For more information, see https://help.github.com/en/articles/contexts-and-expression-syntax-for-github-actions.\",\n \"type\": [\"boolean\", \"number\", \"string\"]\n },\n \"name\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idstepsname\",\n \"description\": \"A name for your step to display on GitHub.\",\n \"type\": \"string\"\n },\n \"uses\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idstepsuses\",\n \"description\": \"Selects an action to run as part of a step in your job. An action is a reusable unit of code. You can use an action defined in the same repository as the workflow, a public repository, or in a published Docker container image (https://hub.docker.com/).\\nWe strongly recommend that you include the version of the action you are using by specifying a Git ref, SHA, or Docker tag number. If you don't specify a version, it could break your workflows or cause unexpected behavior when the action owner publishes an update.\\n- Using the commit SHA of a released action version is the safest for stability and security.\\n- Using the specific major action version allows you to receive critical fixes and security patches while still maintaining compatibility. It also assures that your workflow should still work.\\n- Using the master branch of an action may be convenient, but if someone releases a new major version with a breaking change, your workflow could break.\\nSome actions require inputs that you must set using the with keyword. Review the action's README file to determine the inputs required.\\nActions are either JavaScript files or Docker containers. If the action you're using is a Docker container you must run the job in a Linux virtual environment. For more details, see https://help.github.com/en/articles/virtual-environments-for-github-actions.\",\n \"type\": \"string\"\n },\n \"run\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idstepsrun\",\n \"description\": \"Runs command-line programs using the operating system's shell. If you do not provide a name, the step name will default to the text specified in the run command.\\nCommands run using non-login shells by default. You can choose a different shell and customize the shell used to run commands. For more information, see https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#using-a-specific-shell.\\nEach run keyword represents a new process and shell in the virtual environment. When you provide multi-line commands, each line runs in the same shell.\",\n \"type\": \"string\"\n },\n \"working-directory\": {\n \"$ref\": \"#/definitions/working-directory\"\n },\n \"shell\": {\n \"$ref\": \"#/definitions/shell\"\n },\n \"with\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idstepswith\",\n \"$ref\": \"#/definitions/env\",\n \"description\": \"A map of the input parameters defined by the action. Each input parameter is a key/value pair. Input parameters are set as environment variables. The variable is prefixed with INPUT_ and converted to upper case.\",\n \"properties\": {\n \"args\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idstepswithargs\",\n \"type\": \"string\"\n },\n \"entrypoint\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idstepswithentrypoint\",\n \"type\": \"string\"\n }\n }\n },\n \"env\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idstepsenv\",\n \"$ref\": \"#/definitions/env\",\n \"description\": \"Sets environment variables for steps to use in the virtual environment. You can also set environment variables for the entire workflow or a job.\"\n },\n \"continue-on-error\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idstepscontinue-on-error\",\n \"description\": \"Prevents a job from failing when a step fails. Set to true to allow a job to pass when this step fails.\",\n \"oneOf\": [\n {\n \"type\": \"boolean\"\n },\n {\n \"$ref\": \"#/definitions/expressionSyntax\"\n }\n ],\n \"default\": false\n },\n \"timeout-minutes\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idstepstimeout-minutes\",\n \"description\": \"The maximum number of minutes to run the step before killing the process.\",\n \"oneOf\": [\n {\n \"type\": \"number\"\n },\n {\n \"$ref\": \"#/definitions/expressionSyntax\"\n }\n ]\n }\n }\n },\n \"types\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#onevent_nametypes\",\n \"description\": \"Selects the types of activity that will trigger a workflow run. Most GitHub events are triggered by more than one type of activity. For example, the event for the release resource is triggered when a release is published, unpublished, created, edited, deleted, or prereleased. The types keyword enables you to narrow down activity that causes the workflow to run. When only one activity type triggers a webhook event, the types keyword is unnecessary.\\nYou can use an array of event types. For more information about each event and their activity types, see https://help.github.com/en/articles/events-that-trigger-workflows#webhook-events.\",\n \"oneOf\": [\n {\n \"type\": \"array\",\n \"minItems\": 1\n },\n {\n \"type\": \"string\"\n }\n ]\n },\n \"working-directory\": {\n \"$comment\": \"https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepsrun\",\n \"description\": \"Using the working-directory keyword, you can specify the working directory of where to run the command.\",\n \"type\": \"string\"\n },\n \"jobNeeds\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idneeds\",\n \"description\": \"Identifies any jobs that must complete successfully before this job will run. It can be a string or array of strings. If a job fails, all jobs that need it are skipped unless the jobs use a conditional statement that causes the job to continue.\",\n \"oneOf\": [\n {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/name\"\n },\n \"minItems\": 1\n },\n {\n \"$ref\": \"#/definitions/name\"\n }\n ]\n },\n \"matrix\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idstrategymatrix\",\n \"description\": \"A build matrix is a set of different configurations of the virtual environment. For example you might run a job against more than one supported version of a language, operating system, or tool. Each configuration is a copy of the job that runs and reports a status.\\nYou can specify a matrix by supplying an array for the configuration options. For example, if the GitHub virtual environment supports Node.js versions 6, 8, and 10 you could specify an array of those versions in the matrix.\\nWhen you define a matrix of operating systems, you must set the required runs-on keyword to the operating system of the current job, rather than hard-coding the operating system name. To access the operating system name, you can use the matrix.os context parameter to set runs-on. For more information, see https://help.github.com/en/articles/contexts-and-expression-syntax-for-github-actions.\",\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"patternProperties\": {\n \"^(in|ex)clude$\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#example-including-configurations-in-a-matrix-build\",\n \"oneOf\": [\n {\n \"$ref\": \"#/definitions/expressionSyntax\"\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"additionalProperties\": {\n \"$ref\": \"#/definitions/configuration\"\n }\n },\n \"minItems\": 1\n }\n ]\n }\n },\n \"additionalProperties\": {\n \"oneOf\": [\n {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/configuration\"\n },\n \"minItems\": 1\n },\n {\n \"$ref\": \"#/definitions/expressionSyntax\"\n }\n ]\n },\n \"minProperties\": 1\n },\n {\n \"$ref\": \"#/definitions/expressionSyntax\"\n }\n ]\n },\n \"reusableWorkflowCallJob\": {\n \"$comment\": \"https://docs.github.com/en/actions/learn-github-actions/reusing-workflows#calling-a-reusable-workflow\",\n \"description\": \"Each job must have an id to associate with the job. The key job_id is a string and its value is a map of the job's configuration data. You must replace <job_id> with a string that is unique to the jobs object. The <job_id> must start with a letter or _ and contain only alphanumeric characters, -, or _.\",\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idname\",\n \"description\": \"The name of the job displayed on GitHub.\",\n \"type\": \"string\"\n },\n \"needs\": {\n \"$ref\": \"#/definitions/jobNeeds\"\n },\n \"permissions\": {\n \"$ref\": \"#/definitions/permissions\"\n },\n \"if\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idif\",\n \"description\": \"You can use the if conditional to prevent a job from running unless a condition is met. You can use any supported context and expression to create a conditional.\\nExpressions in an if conditional do not require the ${{ }} syntax. For more information, see https://help.github.com/en/articles/contexts-and-expression-syntax-for-github-actions.\",\n \"type\": [\"boolean\", \"number\", \"string\"]\n },\n \"uses\": {\n \"$comment\": \"https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#jobsjob_iduses\",\n \"description\": \"The location and version of a reusable workflow file to run as a job, of the form './{path/to}/{localfile}.yml' or '{owner}/{repo}/{path}/{filename}@{ref}'. {ref} can be a SHA, a release tag, or a branch name. Using the commit SHA is the safest for stability and security.\",\n \"type\": \"string\",\n \"pattern\": \"^(.+\\\\/)+(.+)\\\\.(ya?ml)(@.+)?$\"\n },\n \"with\": {\n \"$comment\": \"https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#jobsjob_idwith\",\n \"$ref\": \"#/definitions/env\",\n \"description\": \"A map of inputs that are passed to the called workflow. Any inputs that you pass must match the input specifications defined in the called workflow. Unlike 'jobs.<job_id>.steps[*].with', the inputs you pass with 'jobs.<job_id>.with' are not be available as environment variables in the called workflow. Instead, you can reference the inputs by using the inputs context.\"\n },\n \"secrets\": {\n \"$comment\": \"https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#jobsjob_idsecrets\",\n \"description\": \"When a job is used to call a reusable workflow, you can use 'secrets' to provide a map of secrets that are passed to the called workflow. Any secrets that you pass must match the names defined in the called workflow.\",\n \"oneOf\": [\n {\n \"$ref\": \"#/definitions/env\"\n },\n {\n \"type\": \"string\",\n \"enum\": [\"inherit\"]\n }\n ]\n },\n \"strategy\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idstrategy\",\n \"description\": \"A strategy creates a build matrix for your jobs. You can define different variations of an environment to run each job in.\",\n \"type\": \"object\",\n \"properties\": {\n \"matrix\": {\n \"$ref\": \"#/definitions/matrix\"\n },\n \"fail-fast\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idstrategyfail-fast\",\n \"description\": \"When set to true, GitHub cancels all in-progress jobs if any matrix job fails. Default: true\",\n \"type\": [\"boolean\", \"string\"],\n \"default\": true\n },\n \"max-parallel\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idstrategymax-parallel\",\n \"description\": \"The maximum number of jobs that can run simultaneously when using a matrix job strategy. By default, GitHub will maximize the number of jobs run in parallel depending on the available runners on GitHub-hosted virtual machines.\",\n \"type\": [\"number\", \"string\"]\n }\n },\n \"required\": [\"matrix\"],\n \"additionalProperties\": false\n },\n \"concurrency\": {\n \"$comment\": \"https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idconcurrency\",\n \"description\": \"Concurrency ensures that only a single job or workflow using the same concurrency group will run at a time. A concurrency group can be any string or expression. The expression can use any context except for the secrets context. \\nYou can also specify concurrency at the workflow level. \\nWhen a concurrent job or workflow is queued, if another job or workflow using the same concurrency group in the repository is in progress, the queued job or workflow will be pending. Any previously pending job or workflow in the concurrency group will be canceled. To also cancel any currently running job or workflow in the same concurrency group, specify cancel-in-progress: true.\",\n \"oneOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"$ref\": \"#/definitions/concurrency\"\n }\n ]\n }\n },\n \"required\": [\"uses\"],\n \"additionalProperties\": false\n },\n \"normalJob\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_id\",\n \"description\": \"Each job must have an id to associate with the job. The key job_id is a string and its value is a map of the job's configuration data. You must replace <job_id> with a string that is unique to the jobs object. The <job_id> must start with a letter or _ and contain only alphanumeric characters, -, or _.\",\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idname\",\n \"description\": \"The name of the job displayed on GitHub.\",\n \"type\": \"string\"\n },\n \"needs\": {\n \"$ref\": \"#/definitions/jobNeeds\"\n },\n \"snapshot\": {\n \"$ref\": \"#/definitions/snapshot\"\n },\n \"permissions\": {\n \"$ref\": \"#/definitions/permissions\"\n },\n \"runs-on\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idruns-on\",\n \"description\": \"The type of machine to run the job on. The machine can be either a GitHub-hosted runner, or a self-hosted runner.\",\n \"anyOf\": [\n {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#github-hosted-runners\",\n \"type\": \"string\"\n },\n {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#self-hosted-runners\",\n \"type\": \"array\",\n \"anyOf\": [\n {\n \"items\": [\n {\n \"type\": \"string\"\n }\n ],\n \"minItems\": 1,\n \"additionalItems\": {\n \"type\": \"string\"\n }\n }\n ]\n },\n {\n \"$comment\": \"https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#choosing-runners-in-a-group\",\n \"type\": \"object\",\n \"properties\": {\n \"group\": {\n \"type\": \"string\"\n },\n \"labels\": {\n \"oneOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n ]\n }\n }\n },\n {\n \"$ref\": \"#/definitions/stringContainingExpressionSyntax\"\n },\n {\n \"$ref\": \"#/definitions/expressionSyntax\"\n }\n ]\n },\n \"environment\": {\n \"$comment\": \"https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#jobsjob_idenvironment\",\n \"description\": \"The environment that the job references.\",\n \"oneOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"$ref\": \"#/definitions/environment\"\n }\n ]\n },\n \"outputs\": {\n \"$comment\": \"https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idoutputs\",\n \"description\": \"A map of outputs for a job. Job outputs are available to all downstream jobs that depend on this job.\",\n \"type\": \"object\",\n \"additionalProperties\": {\n \"type\": \"string\"\n },\n \"minProperties\": 1\n },\n \"env\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idenv\",\n \"$ref\": \"#/definitions/env\",\n \"description\": \"A map of environment variables that are available to all steps in the job.\"\n },\n \"defaults\": {\n \"$comment\": \"https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_iddefaults\",\n \"$ref\": \"#/definitions/defaults\",\n \"description\": \"A map of default settings that will apply to all steps in the job.\"\n },\n \"if\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idif\",\n \"description\": \"You can use the if conditional to prevent a job from running unless a condition is met. You can use any supported context and expression to create a conditional.\\nExpressions in an if conditional do not require the ${{ }} syntax. For more information, see https://help.github.com/en/articles/contexts-and-expression-syntax-for-github-actions.\",\n \"type\": [\"boolean\", \"number\", \"string\"]\n },\n \"steps\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idsteps\",\n \"description\": \"A job contains a sequence of tasks called steps. Steps can run commands, run setup tasks, or run an action in your repository, a public repository, or an action published in a Docker registry. Not all steps run actions, but all actions run as a step. Each step runs in its own process in the virtual environment and has access to the workspace and filesystem. Because steps run in their own process, changes to environment variables are not preserved between steps. GitHub provides built-in steps to set up and complete a job.\\nMust contain either `uses` or `run`\\n\",\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/step\"\n },\n \"minItems\": 1\n },\n \"timeout-minutes\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes\",\n \"description\": \"The maximum number of minutes to let a workflow run before GitHub automatically cancels it. Default: 360\",\n \"oneOf\": [\n {\n \"type\": \"number\"\n },\n {\n \"$ref\": \"#/definitions/expressionSyntax\"\n }\n ],\n \"default\": 360\n },\n \"strategy\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idstrategy\",\n \"description\": \"A strategy creates a build matrix for your jobs. You can define different variations of an environment to run each job in.\",\n \"type\": \"object\",\n \"properties\": {\n \"matrix\": {\n \"$ref\": \"#/definitions/matrix\"\n },\n \"fail-fast\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idstrategyfail-fast\",\n \"description\": \"When set to true, GitHub cancels all in-progress jobs if any matrix job fails. Default: true\",\n \"type\": [\"boolean\", \"string\"],\n \"default\": true\n },\n \"max-parallel\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idstrategymax-parallel\",\n \"description\": \"The maximum number of jobs that can run simultaneously when using a matrix job strategy. By default, GitHub will maximize the number of jobs run in parallel depending on the available runners on GitHub-hosted virtual machines.\",\n \"type\": [\"number\", \"string\"]\n }\n },\n \"required\": [\"matrix\"],\n \"additionalProperties\": false\n },\n \"continue-on-error\": {\n \"$comment\": \"https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idcontinue-on-error\",\n \"description\": \"Prevents a workflow run from failing when a job fails. Set to true to allow a workflow run to pass when this job fails.\",\n \"oneOf\": [\n {\n \"type\": \"boolean\"\n },\n {\n \"$ref\": \"#/definitions/expressionSyntax\"\n }\n ]\n },\n \"container\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idcontainer\",\n \"description\": \"A container to run any steps in a job that don't already specify a container. If you have steps that use both script and container actions, the container actions will run as sibling containers on the same network with the same volume mounts.\\nIf you do not set a container, all steps will run directly on the host specified by runs-on unless a step refers to an action configured to run in a container.\",\n \"oneOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"$ref\": \"#/definitions/container\"\n }\n ]\n },\n \"services\": {\n \"$comment\": \"https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idservices\",\n \"description\": \"Additional containers to host services for a job in a workflow. These are useful for creating databases or cache services like redis. The runner on the virtual machine will automatically create a network and manage the life cycle of the service containers.\\nWhen you use a service container for a job or your step uses container actions, you don't need to set port information to access the service. Docker automatically exposes all ports between containers on the same network.\\nWhen both the job and the action run in a container, you can directly reference the container by its hostname. The hostname is automatically mapped to the service name.\\nWhen a step does not use a container action, you must access the service using localhost and bind the ports.\",\n \"type\": \"object\",\n \"additionalProperties\": {\n \"$ref\": \"#/definitions/container\"\n }\n },\n \"concurrency\": {\n \"$comment\": \"https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idconcurrency\",\n \"description\": \"Concurrency ensures that only a single job or workflow using the same concurrency group will run at a time. A concurrency group can be any string or expression. The expression can use any context except for the secrets context. \\nYou can also specify concurrency at the workflow level. \\nWhen a concurrent job or workflow is queued, if another job or workflow using the same concurrency group in the repository is in progress, the queued job or workflow will be pending. Any previously pending job or workflow in the concurrency group will be canceled. To also cancel any currently running job or workflow in the same concurrency group, specify cancel-in-progress: true.\",\n \"oneOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"$ref\": \"#/definitions/concurrency\"\n }\n ]\n }\n },\n \"required\": [\"runs-on\"],\n \"additionalProperties\": false\n },\n \"workflowDispatchInput\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/metadata-syntax-for-github-actions#inputsinput_id\",\n \"description\": \"A string identifier to associate with the input. The value of <input_id> is a map of the input's metadata. The <input_id> must be a unique identifier within the inputs object. The <input_id> must start with a letter or _ and contain only alphanumeric characters, -, or _.\",\n \"type\": \"object\",\n \"properties\": {\n \"description\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/metadata-syntax-for-github-actions#inputsinput_iddescription\",\n \"description\": \"A string description of the input parameter.\",\n \"type\": \"string\"\n },\n \"deprecationMessage\": {\n \"description\": \"A string shown to users using the deprecated input.\",\n \"type\": \"string\"\n },\n \"required\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/metadata-syntax-for-github-actions#inputsinput_idrequired\",\n \"description\": \"A boolean to indicate whether the action requires the input parameter. Set to true when the parameter is required.\",\n \"type\": \"boolean\"\n },\n \"default\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/metadata-syntax-for-github-actions#inputsinput_iddefault\",\n \"description\": \"A string representing the default value. The default value is used when an input parameter isn't specified in a workflow file.\"\n },\n \"type\": {\n \"$comment\": \"https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onworkflow_dispatchinputsinput_idtype\",\n \"description\": \"A string representing the type of the input.\",\n \"type\": \"string\",\n \"enum\": [\"string\", \"choice\", \"boolean\", \"number\", \"environment\"]\n },\n \"options\": {\n \"$comment\": \"https://github.blog/changelog/2021-11-10-github-actions-input-types-for-manual-workflows\",\n \"description\": \"The options of the dropdown list, if the type is a choice.\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n },\n \"minItems\": 1\n }\n },\n \"allOf\": [\n {\n \"if\": {\n \"properties\": {\n \"type\": {\n \"const\": \"string\"\n }\n },\n \"required\": [\"type\"]\n },\n \"then\": {\n \"properties\": {\n \"default\": {\n \"type\": \"string\"\n }\n }\n }\n },\n {\n \"if\": {\n \"properties\": {\n \"type\": {\n \"const\": \"boolean\"\n }\n },\n \"required\": [\"type\"]\n },\n \"then\": {\n \"properties\": {\n \"default\": {\n \"type\": \"boolean\"\n }\n }\n }\n },\n {\n \"if\": {\n \"properties\": {\n \"type\": {\n \"const\": \"number\"\n }\n },\n \"required\": [\"type\"]\n },\n \"then\": {\n \"properties\": {\n \"default\": {\n \"type\": \"number\"\n }\n }\n }\n },\n {\n \"if\": {\n \"properties\": {\n \"type\": {\n \"const\": \"environment\"\n }\n },\n \"required\": [\"type\"]\n },\n \"then\": {\n \"properties\": {\n \"default\": {\n \"type\": \"string\"\n }\n }\n }\n },\n {\n \"if\": {\n \"properties\": {\n \"type\": {\n \"const\": \"choice\"\n }\n },\n \"required\": [\"type\"]\n },\n \"then\": {\n \"required\": [\"options\"]\n }\n }\n ],\n \"additionalProperties\": false\n }\n },\n \"properties\": {\n \"name\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#name\",\n \"description\": \"The name of your workflow. GitHub displays the names of your workflows on your repository's actions page. If you omit this field, GitHub sets the name to the workflow's filename.\",\n \"type\": \"string\"\n },\n \"on\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#on\",\n \"description\": \"The name of the GitHub event that triggers the workflow. You can provide a single event string, array of events, array of event types, or an event configuration map that schedules a workflow or restricts the execution of a workflow to specific files, tags, or branch changes. For a list of available events, see https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows.\",\n \"oneOf\": [\n {\n \"$ref\": \"#/definitions/event\"\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/event\"\n },\n \"minItems\": 1\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"branch_protection_rule\": {\n \"$comment\": \"https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#branch_protection_rule\",\n \"$ref\": \"#/definitions/eventObject\",\n \"description\": \"Runs your workflow anytime the branch_protection_rule event occurs. More than one activity type triggers this event.\",\n \"properties\": {\n \"types\": {\n \"$ref\": \"#/definitions/types\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\"created\", \"edited\", \"deleted\"]\n },\n \"default\": [\"created\", \"edited\", \"deleted\"]\n }\n }\n },\n \"check_run\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows#check-run-event-check_run\",\n \"$ref\": \"#/definitions/eventObject\",\n \"description\": \"Runs your workflow anytime the check_run event occurs. More than one activity type triggers this event. For information about the REST API, see https://developer.github.com/v3/checks/runs.\",\n \"properties\": {\n \"types\": {\n \"$ref\": \"#/definitions/types\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\"created\", \"rerequested\", \"completed\", \"requested_action\"]\n },\n \"default\": [\"created\", \"rerequested\", \"completed\", \"requested_action\"]\n }\n }\n },\n \"check_suite\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows#check-suite-event-check_suite\",\n \"$ref\": \"#/definitions/eventObject\",\n \"description\": \"Runs your workflow anytime the check_suite event occurs. More than one activity type triggers this event. For information about the REST API, see https://developer.github.com/v3/checks/suites/.\",\n \"properties\": {\n \"types\": {\n \"$ref\": \"#/definitions/types\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\"completed\", \"requested\", \"rerequested\"]\n },\n \"default\": [\"completed\", \"requested\", \"rerequested\"]\n }\n }\n },\n \"create\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows#create-event-create\",\n \"$ref\": \"#/definitions/eventObject\",\n \"description\": \"Runs your workflow anytime someone creates a branch or tag, which triggers the create event. For information about the REST API, see https://developer.github.com/v3/git/refs/#create-a-reference.\"\n },\n \"delete\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows#delete-event-delete\",\n \"$ref\": \"#/definitions/eventObject\",\n \"description\": \"Runs your workflow anytime someone deletes a branch or tag, which triggers the delete event. For information about the REST API, see https://developer.github.com/v3/git/refs/#delete-a-reference.\"\n },\n \"deployment\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows#deployment-event-deployment\",\n \"$ref\": \"#/definitions/eventObject\",\n \"description\": \"Runs your workflow anytime someone creates a deployment, which triggers the deployment event. Deployments created with a commit SHA may not have a Git ref. For information about the REST API, see https://developer.github.com/v3/repos/deployments/.\"\n },\n \"deployment_status\": {\n \"$comment\": \"https://docs.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows\",\n \"$ref\": \"#/definitions/eventObject\",\n \"description\": \"Runs your workflow anytime a third party provides a deployment status, which triggers the deployment_status event. Deployments created with a commit SHA may not have a Git ref. For information about the REST API, see https://developer.github.com/v3/repos/deployments/#create-a-deployment-status.\"\n },\n \"discussion\": {\n \"$comment\": \"https://docs.github.com/en/actions/reference/events-that-trigger-workflows#discussion\",\n \"$ref\": \"#/definitions/eventObject\",\n \"description\": \"Runs your workflow anytime the discussion event occurs. More than one activity type triggers this event. For information about the GraphQL API, see https://docs.github.com/en/graphql/guides/using-the-graphql-api-for-discussions\",\n \"properties\": {\n \"types\": {\n \"$ref\": \"#/definitions/types\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"created\",\n \"edited\",\n \"deleted\",\n \"transferred\",\n \"pinned\",\n \"unpinned\",\n \"labeled\",\n \"unlabeled\",\n \"locked\",\n \"unlocked\",\n \"category_changed\",\n \"answered\",\n \"unanswered\"\n ]\n },\n \"default\": [\n \"created\",\n \"edited\",\n \"deleted\",\n \"transferred\",\n \"pinned\",\n \"unpinned\",\n \"labeled\",\n \"unlabeled\",\n \"locked\",\n \"unlocked\",\n \"category_changed\",\n \"answered\",\n \"unanswered\"\n ]\n }\n }\n },\n \"discussion_comment\": {\n \"$comment\": \"https://docs.github.com/en/actions/reference/events-that-trigger-workflows#discussion_comment\",\n \"$ref\": \"#/definitions/eventObject\",\n \"description\": \"Runs your workflow anytime the discussion_comment event occurs. More than one activity type triggers this event. For information about the GraphQL API, see https://docs.github.com/en/graphql/guides/using-the-graphql-api-for-discussions\",\n \"properties\": {\n \"types\": {\n \"$ref\": \"#/definitions/types\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\"created\", \"edited\", \"deleted\"]\n },\n \"default\": [\"created\", \"edited\", \"deleted\"]\n }\n }\n },\n \"fork\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows#fork-event-fork\",\n \"$ref\": \"#/definitions/eventObject\",\n \"description\": \"Runs your workflow anytime when someone forks a repository, which triggers the fork event. For information about the REST API, see https://developer.github.com/v3/repos/forks/#create-a-fork.\"\n },\n \"gollum\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows#gollum-event-gollum\",\n \"$ref\": \"#/definitions/eventObject\",\n \"description\": \"Runs your workflow when someone creates or updates a Wiki page, which triggers the gollum event.\"\n },\n \"issue_comment\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows#issue-comment-event-issue_comment\",\n \"$ref\": \"#/definitions/eventObject\",\n \"description\": \"Runs your workflow anytime the issue_comment event occurs. More than one activity type triggers this event. For information about the REST API, see https://developer.github.com/v3/issues/comments/.\",\n \"properties\": {\n \"types\": {\n \"$ref\": \"#/definitions/types\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\"created\", \"edited\", \"deleted\"]\n },\n \"default\": [\"created\", \"edited\", \"deleted\"]\n }\n }\n },\n \"issues\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows#issues-event-issues\",\n \"$ref\": \"#/definitions/eventObject\",\n \"description\": \"Runs your workflow anytime the issues event occurs. More than one activity type triggers this event. For information about the REST API, see https://developer.github.com/v3/issues.\",\n \"properties\": {\n \"types\": {\n \"$ref\": \"#/definitions/types\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"opened\",\n \"edited\",\n \"deleted\",\n \"transferred\",\n \"pinned\",\n \"unpinned\",\n \"closed\",\n \"reopened\",\n \"assigned\",\n \"unassigned\",\n \"labeled\",\n \"unlabeled\",\n \"locked\",\n \"unlocked\",\n \"milestoned\",\n \"demilestoned\"\n ]\n },\n \"default\": [\n \"opened\",\n \"edited\",\n \"deleted\",\n \"transferred\",\n \"pinned\",\n \"unpinned\",\n \"closed\",\n \"reopened\",\n \"assigned\",\n \"unassigned\",\n \"labeled\",\n \"unlabeled\",\n \"locked\",\n \"unlocked\",\n \"milestoned\",\n \"demilestoned\"\n ]\n }\n }\n },\n \"label\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows#label-event-label\",\n \"$ref\": \"#/definitions/eventObject\",\n \"description\": \"Runs your workflow anytime the label event occurs. More than one activity type triggers this event. For information about the REST API, see https://developer.github.com/v3/issues/labels/.\",\n \"properties\": {\n \"types\": {\n \"$ref\": \"#/definitions/types\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\"created\", \"edited\", \"deleted\"]\n },\n \"default\": [\"created\", \"edited\", \"deleted\"]\n }\n }\n },\n \"merge_group\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows#merge_group\",\n \"$ref\": \"#/definitions/eventObject\",\n \"description\": \"Runs your workflow when a pull request is added to a merge queue, which adds the pull request to a merge group. For information about the merge queue, see https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/merging-a-pull-request-with-a-merge-queue .\",\n \"properties\": {\n \"types\": {\n \"$ref\": \"#/definitions/types\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\"checks_requested\"]\n },\n \"default\": [\"checks_requested\"]\n }\n }\n },\n \"milestone\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows#milestone-event-milestone\",\n \"$ref\": \"#/definitions/eventObject\",\n \"description\": \"Runs your workflow anytime the milestone event occurs. More than one activity type triggers this event. For information about the REST API, see https://developer.github.com/v3/issues/milestones/.\",\n \"properties\": {\n \"types\": {\n \"$ref\": \"#/definitions/types\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\"created\", \"closed\", \"opened\", \"edited\", \"deleted\"]\n },\n \"default\": [\"created\", \"closed\", \"opened\", \"edited\", \"deleted\"]\n }\n }\n },\n \"page_build\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows#page-build-event-page_build\",\n \"$ref\": \"#/definitions/eventObject\",\n \"description\": \"Runs your workflow anytime someone pushes to a GitHub Pages-enabled branch, which triggers the page_build event. For information about the REST API, see https://developer.github.com/v3/repos/pages/.\"\n },\n \"project\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows#project-event-project\",\n \"$ref\": \"#/definitions/eventObject\",\n \"description\": \"Runs your workflow anytime the project event occurs. More than one activity type triggers this event. For information about the REST API, see https://developer.github.com/v3/projects/.\",\n \"properties\": {\n \"types\": {\n \"$ref\": \"#/definitions/types\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\"created\", \"updated\", \"closed\", \"reopened\", \"edited\", \"deleted\"]\n },\n \"default\": [\"created\", \"updated\", \"closed\", \"reopened\", \"edited\", \"deleted\"]\n }\n }\n },\n \"project_card\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows#project-card-event-project_card\",\n \"$ref\": \"#/definitions/eventObject\",\n \"description\": \"Runs your workflow anytime the project_card event occurs. More than one activity type triggers this event. For information about the REST API, see https://developer.github.com/v3/projects/cards.\",\n \"properties\": {\n \"types\": {\n \"$ref\": \"#/definitions/types\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\"created\", \"moved\", \"converted\", \"edited\", \"deleted\"]\n },\n \"default\": [\"created\", \"moved\", \"converted\", \"edited\", \"deleted\"]\n }\n }\n },\n \"project_column\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows#project-column-event-project_column\",\n \"$ref\": \"#/definitions/eventObject\",\n \"description\": \"Runs your workflow anytime the project_column event occurs. More than one activity type triggers this event. For information about the REST API, see https://developer.github.com/v3/projects/columns.\",\n \"properties\": {\n \"types\": {\n \"$ref\": \"#/definitions/types\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\"created\", \"updated\", \"moved\", \"deleted\"]\n },\n \"default\": [\"created\", \"updated\", \"moved\", \"deleted\"]\n }\n }\n },\n \"public\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows#public-event-public\",\n \"$ref\": \"#/definitions/eventObject\",\n \"description\": \"Runs your workflow anytime someone makes a private repository public, which triggers the public event. For information about the REST API, see https://developer.github.com/v3/repos/#edit.\"\n },\n \"pull_request\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows#pull-request-event-pull_request\",\n \"description\": \"Runs your workflow anytime the pull_request event occurs. More than one activity type triggers this event. For information about the REST API, see https://developer.github.com/v3/pulls.\\nNote: Workflows do not run on private base repositories when you open a pull request from a forked repository.\\nWhen you create a pull request from a forked repository to the base repository, GitHub sends the pull_request event to the base repository and no pull request events occur on the forked repository.\\nWorkflows don't run on forked repositories by default. You must enable GitHub Actions in the Actions tab of the forked repository.\\nThe permissions for the GITHUB_TOKEN in forked repositories is read-only. For more information about the GITHUB_TOKEN, see https://help.github.com/en/articles/virtual-environments-for-github-actions.\",\n \"oneOf\": [\n {\n \"type\": \"null\"\n },\n {\n \"allOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"types\": {\n \"$ref\": \"#/definitions/types\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"assigned\",\n \"unassigned\",\n \"labeled\",\n \"unlabeled\",\n \"opened\",\n \"edited\",\n \"closed\",\n \"reopened\",\n \"synchronize\",\n \"converted_to_draft\",\n \"ready_for_review\",\n \"locked\",\n \"unlocked\",\n \"milestoned\",\n \"demilestoned\",\n \"review_requested\",\n \"review_request_removed\",\n \"auto_merge_enabled\",\n \"auto_merge_disabled\",\n \"enqueued\",\n \"dequeued\"\n ]\n },\n \"default\": [\"opened\", \"synchronize\", \"reopened\"]\n },\n \"branches\": {\n \"$ref\": \"#/definitions/branch\"\n },\n \"branches-ignore\": {\n \"$ref\": \"#/definitions/branch\"\n },\n \"tags\": {\n \"$ref\": \"#/definitions/branch\"\n },\n \"tags-ignore\": {\n \"$ref\": \"#/definitions/branch\"\n },\n \"paths\": {\n \"$ref\": \"#/definitions/path\"\n },\n \"paths-ignore\": {\n \"$ref\": \"#/definitions/path\"\n }\n },\n \"additionalProperties\": false\n },\n {\n \"not\": {\n \"required\": [\"branches\", \"branches-ignore\"]\n }\n },\n {\n \"not\": {\n \"required\": [\"tags\", \"tags-ignore\"]\n }\n },\n {\n \"not\": {\n \"required\": [\"paths\", \"paths-ignore\"]\n }\n }\n ]\n }\n ]\n },\n \"pull_request_review\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows#pull-request-review-event-pull_request_review\",\n \"$ref\": \"#/definitions/eventObject\",\n \"description\": \"Runs your workflow anytime the pull_request_review event occurs. More than one activity type triggers this event. For information about the REST API, see https://developer.github.com/v3/pulls/reviews.\\nNote: Workflows do not run on private base repositories when you open a pull request from a forked repository.\\nWhen you create a pull request from a forked repository to the base repository, GitHub sends the pull_request event to the base repository and no pull request events occur on the forked repository.\\nWorkflows don't run on forked repositories by default. You must enable GitHub Actions in the Actions tab of the forked repository.\\nThe permissions for the GITHUB_TOKEN in forked repositories is read-only. For more information about the GITHUB_TOKEN, see https://help.github.com/en/articles/virtual-environments-for-github-actions.\",\n \"properties\": {\n \"types\": {\n \"$ref\": \"#/definitions/types\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\"submitted\", \"edited\", \"dismissed\"]\n },\n \"default\": [\"submitted\", \"edited\", \"dismissed\"]\n }\n }\n },\n \"pull_request_review_comment\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows#pull-request-review-comment-event-pull_request_review_comment\",\n \"$ref\": \"#/definitions/eventObject\",\n \"description\": \"Runs your workflow anytime a comment on a pull request's unified diff is modified, which triggers the pull_request_review_comment event. More than one activity type triggers this event. For information about the REST API, see https://developer.github.com/v3/pulls/comments.\\nNote: Workflows do not run on private base repositories when you open a pull request from a forked repository.\\nWhen you create a pull request from a forked repository to the base repository, GitHub sends the pull_request event to the base repository and no pull request events occur on the forked repository.\\nWorkflows don't run on forked repositories by default. You must enable GitHub Actions in the Actions tab of the forked repository.\\nThe permissions for the GITHUB_TOKEN in forked repositories is read-only. For more information about the GITHUB_TOKEN, see https://help.github.com/en/articles/virtual-environments-for-github-actions.\",\n \"properties\": {\n \"types\": {\n \"$ref\": \"#/definitions/types\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\"created\", \"edited\", \"deleted\"]\n },\n \"default\": [\"created\", \"edited\", \"deleted\"]\n }\n }\n },\n \"pull_request_target\": {\n \"$comment\": \"https://docs.github.com/en/actions/reference/events-that-trigger-workflows#pull_request_target\",\n \"description\": \"This event is similar to pull_request, except that it runs in the context of the base repository of the pull request, rather than in the merge commit. This means that you can more safely make your secrets available to the workflows triggered by the pull request, because only workflows defined in the commit on the base repository are run. For example, this event allows you to create workflows that label and comment on pull requests, based on the contents of the event payload.\",\n \"oneOf\": [\n {\n \"type\": \"null\"\n },\n {\n \"allOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"types\": {\n \"$ref\": \"#/definitions/types\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"assigned\",\n \"unassigned\",\n \"labeled\",\n \"unlabeled\",\n \"opened\",\n \"edited\",\n \"closed\",\n \"reopened\",\n \"synchronize\",\n \"converted_to_draft\",\n \"ready_for_review\",\n \"locked\",\n \"unlocked\",\n \"review_requested\",\n \"review_request_removed\",\n \"auto_merge_enabled\",\n \"auto_merge_disabled\"\n ]\n },\n \"default\": [\"opened\", \"synchronize\", \"reopened\"]\n },\n \"branches\": {\n \"$ref\": \"#/definitions/branch\"\n },\n \"branches-ignore\": {\n \"$ref\": \"#/definitions/branch\"\n },\n \"tags\": {\n \"$ref\": \"#/definitions/branch\"\n },\n \"tags-ignore\": {\n \"$ref\": \"#/definitions/branch\"\n },\n \"paths\": {\n \"$ref\": \"#/definitions/path\"\n },\n \"paths-ignore\": {\n \"$ref\": \"#/definitions/path\"\n }\n },\n \"additionalProperties\": false\n },\n {\n \"not\": {\n \"required\": [\"branches\", \"branches-ignore\"]\n }\n },\n {\n \"not\": {\n \"required\": [\"tags\", \"tags-ignore\"]\n }\n },\n {\n \"not\": {\n \"required\": [\"paths\", \"paths-ignore\"]\n }\n }\n ]\n }\n ]\n },\n \"push\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows#push-event-push\",\n \"description\": \"Runs your workflow when someone pushes to a repository branch, which triggers the push event.\\nNote: The webhook payload available to GitHub Actions does not include the added, removed, and modified attributes in the commit object. You can retrieve the full commit object using the REST API. For more information, see https://developer.github.com/v3/repos/commits/#get-a-single-commit.\",\n \"oneOf\": [\n {\n \"type\": \"null\"\n },\n {\n \"allOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"branches\": {\n \"$ref\": \"#/definitions/branch\"\n },\n \"branches-ignore\": {\n \"$ref\": \"#/definitions/branch\"\n },\n \"tags\": {\n \"$ref\": \"#/definitions/branch\"\n },\n \"tags-ignore\": {\n \"$ref\": \"#/definitions/branch\"\n },\n \"paths\": {\n \"$ref\": \"#/definitions/path\"\n },\n \"paths-ignore\": {\n \"$ref\": \"#/definitions/path\"\n }\n },\n \"additionalProperties\": false\n },\n {\n \"not\": {\n \"required\": [\"branches\", \"branches-ignore\"]\n }\n },\n {\n \"not\": {\n \"required\": [\"tags\", \"tags-ignore\"]\n }\n },\n {\n \"not\": {\n \"required\": [\"paths\", \"paths-ignore\"]\n }\n }\n ]\n }\n ]\n },\n \"registry_package\": {\n \"$comment\": \"https://help.github.com/en/actions/reference/events-that-trigger-workflows#registry-package-event-registry_package\",\n \"$ref\": \"#/definitions/eventObject\",\n \"description\": \"Runs your workflow anytime a package is published or updated. For more information, see https://help.github.com/en/github/managing-packages-with-github-packages.\",\n \"properties\": {\n \"types\": {\n \"$ref\": \"#/definitions/types\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\"published\", \"updated\"]\n },\n \"default\": [\"published\", \"updated\"]\n }\n }\n },\n \"release\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows#release-event-release\",\n \"$ref\": \"#/definitions/eventObject\",\n \"description\": \"Runs your workflow anytime the release event occurs. More than one activity type triggers this event. For information about the REST API, see https://developer.github.com/v3/repos/releases/ in the GitHub Developer documentation.\",\n \"properties\": {\n \"types\": {\n \"$ref\": \"#/definitions/types\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"published\",\n \"unpublished\",\n \"created\",\n \"edited\",\n \"deleted\",\n \"prereleased\",\n \"released\"\n ]\n },\n \"default\": [\n \"published\",\n \"unpublished\",\n \"created\",\n \"edited\",\n \"deleted\",\n \"prereleased\",\n \"released\"\n ]\n }\n }\n },\n \"status\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows#status-event-status\",\n \"$ref\": \"#/definitions/eventObject\",\n \"description\": \"Runs your workflow anytime the status of a Git commit changes, which triggers the status event. For information about the REST API, see https://developer.github.com/v3/repos/statuses/.\"\n },\n \"watch\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows#watch-event-watch\",\n \"$ref\": \"#/definitions/eventObject\",\n \"description\": \"Runs your workflow anytime the watch event occurs. More than one activity type triggers this event. For information about the REST API, see https://developer.github.com/v3/activity/starring/.\"\n },\n \"workflow_call\": {\n \"$comment\": \"https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#workflow_call\",\n \"description\": \"Allows workflows to be reused by other workflows.\",\n \"properties\": {\n \"inputs\": {\n \"$comment\": \"https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#onworkflow_callinputs\",\n \"description\": \"When using the workflow_call keyword, you can optionally specify inputs that are passed to the called workflow from the caller workflow.\",\n \"type\": \"object\",\n \"patternProperties\": {\n \"^[_a-zA-Z][a-zA-Z0-9_-]*$\": {\n \"$comment\": \"https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#inputsinput_id\",\n \"description\": \"A string identifier to associate with the input. The value of <input_id> is a map of the input's metadata. The <input_id> must be a unique identifier within the inputs object. The <input_id> must start with a letter or _ and contain only alphanumeric characters, -, or _.\",\n \"type\": \"object\",\n \"properties\": {\n \"description\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/metadata-syntax-for-github-actions#inputsinput_iddescription\",\n \"description\": \"A string description of the input parameter.\",\n \"type\": \"string\"\n },\n \"required\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/metadata-syntax-for-github-actions#inputsinput_idrequired\",\n \"description\": \"A boolean to indicate whether the action requires the input parameter. Set to true when the parameter is required.\",\n \"type\": \"boolean\"\n },\n \"type\": {\n \"$comment\": \"https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#onworkflow_callinput_idtype\",\n \"description\": \"Required if input is defined for the on.workflow_call keyword. The value of this parameter is a string specifying the data type of the input. This must be one of: boolean, number, or string.\",\n \"type\": \"string\",\n \"enum\": [\"boolean\", \"number\", \"string\"]\n },\n \"default\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/metadata-syntax-for-github-actions#inputsinput_iddefault\",\n \"description\": \"The default value is used when an input parameter isn't specified in a workflow file.\",\n \"type\": [\"boolean\", \"number\", \"string\"]\n }\n },\n \"required\": [\"type\"],\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"outputs\": {\n \"$comment\": \"https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#onworkflow_calloutputs\",\n \"description\": \"When using the workflow_call keyword, you can optionally specify inputs that are passed to the called workflow from the caller workflow.\",\n \"type\": \"object\",\n \"patternProperties\": {\n \"^[_a-zA-Z][a-zA-Z0-9_-]*$\": {\n \"$comment\": \"https://docs.github.com/en/actions/sharing-automations/creating-actions/metadata-syntax-for-github-actions#outputsoutput_id\",\n \"description\": \"A string identifier to associate with the output. The value of <output_id> is a map of the output's metadata. The <output_id> must be a unique identifier within the outputs object. The <output_id> must start with a letter or _ and contain only alphanumeric characters, -, or _.\",\n \"type\": \"object\",\n \"properties\": {\n \"description\": {\n \"$comment\": \"https://docs.github.com/en/actions/sharing-automations/creating-actions/metadata-syntax-for-github-actions#outputsoutput_iddescription\",\n \"description\": \"A string description of the output parameter.\",\n \"type\": \"string\"\n },\n \"value\": {\n \"$comment\": \"https://docs.github.com/en/actions/sharing-automations/creating-actions/metadata-syntax-for-github-actions#outputsoutput_idvalue\",\n \"description\": \"The value that the output parameter will be mapped to. You can set this to a string or an expression with context. For example, you can use the steps context to set the value of an output to the output value of a step.\",\n \"type\": \"string\"\n }\n },\n \"required\": [\"value\"],\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"secrets\": {\n \"$comment\": \"https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#onworkflow_callsecrets\",\n \"description\": \"A map of the secrets that can be used in the called workflow. Within the called workflow, you can use the secrets context to refer to a secret.\",\n \"patternProperties\": {\n \"^[_a-zA-Z][a-zA-Z0-9_-]*$\": {\n \"$comment\": \"https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#onworkflow_callsecretssecret_id\",\n \"description\": \"A string identifier to associate with the secret.\",\n \"properties\": {\n \"description\": {\n \"description\": \"A string description of the secret parameter.\",\n \"type\": \"string\"\n },\n \"required\": {\n \"$comment\": \"https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#onworkflow_callsecretssecret_idrequired\",\n \"description\": \"A boolean specifying whether the secret must be supplied.\",\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n }\n }\n },\n \"workflow_dispatch\": {\n \"$comment\": \"https://github.blog/changelog/2020-07-06-github-actions-manual-triggers-with-workflow_dispatch/\",\n \"description\": \"You can now create workflows that are manually triggered with the new workflow_dispatch event. You will then see a 'Run workflow' button on the Actions tab, enabling you to easily trigger a run.\",\n \"properties\": {\n \"inputs\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/metadata-syntax-for-github-actions#inputs\",\n \"description\": \"Input parameters allow you to specify data that the action expects to use during runtime. GitHub stores input parameters as environment variables. Input ids with uppercase letters are converted to lowercase during runtime. We recommended using lowercase input ids.\",\n \"type\": \"object\",\n \"patternProperties\": {\n \"^[_a-zA-Z][a-zA-Z0-9_-]*$\": {\n \"$ref\": \"#/definitions/workflowDispatchInput\"\n }\n },\n \"additionalProperties\": false\n }\n },\n \"additionalProperties\": false\n },\n \"workflow_run\": {\n \"$comment\": \"https://docs.github.com/en/actions/reference/events-that-trigger-workflows#workflow_run\",\n \"$ref\": \"#/definitions/eventObject\",\n \"description\": \"This event occurs when a workflow run is requested or completed, and allows you to execute a workflow based on the finished result of another workflow. For example, if your pull_request workflow generates build artifacts, you can create a new workflow that uses workflow_run to analyze the results and add a comment to the original pull request.\",\n \"properties\": {\n \"types\": {\n \"$ref\": \"#/definitions/types\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\"requested\", \"completed\", \"in_progress\"]\n },\n \"default\": [\"requested\", \"completed\"]\n },\n \"workflows\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n },\n \"minItems\": 1\n }\n },\n \"patternProperties\": {\n \"^branches(-ignore)?$\": {}\n }\n },\n \"repository_dispatch\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows#external-events-repository_dispatch\",\n \"$ref\": \"#/definitions/eventObject\",\n \"description\": \"You can use the GitHub API to trigger a webhook event called repository_dispatch when you want to trigger a workflow for activity that happens outside of GitHub. For more information, see https://developer.github.com/v3/repos/#create-a-repository-dispatch-event.\\nTo trigger the custom repository_dispatch webhook event, you must send a POST request to a GitHub API endpoint and provide an event_type name to describe the activity type. To trigger a workflow run, you must also configure your workflow to use the repository_dispatch event.\"\n },\n \"schedule\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows#scheduled-events-schedule\",\n \"description\": \"You can schedule a workflow to run at specific UTC times using POSIX cron syntax (https://pubs.opengroup.org/onlinepubs/9699919799/utilities/crontab.html#tag_20_25_07). Scheduled workflows run on the latest commit on the default or base branch. The shortest interval you can run scheduled workflows is once every 5 minutes.\\nNote: GitHub Actions does not support the non-standard syntax @yearly, @monthly, @weekly, @daily, @hourly, and @reboot.\\nYou can use crontab guru (https://crontab.guru/). to help generate your cron syntax and confirm what time it will run. To help you get started, there is also a list of crontab guru examples (https://crontab.guru/examples.html).\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"cron\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n },\n \"minItems\": 1\n }\n },\n \"additionalProperties\": false\n }\n ]\n },\n \"env\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#env\",\n \"$ref\": \"#/definitions/env\",\n \"description\": \"A map of environment variables that are available to all jobs and steps in the workflow.\"\n },\n \"defaults\": {\n \"$comment\": \"https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#defaults\",\n \"$ref\": \"#/definitions/defaults\",\n \"description\": \"A map of default settings that will apply to all jobs in the workflow.\"\n },\n \"concurrency\": {\n \"$comment\": \"https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#concurrency\",\n \"description\": \"Concurrency ensures that only a single job or workflow using the same concurrency group will run at a time. A concurrency group can be any string or expression. The expression can use any context except for the secrets context. \\nYou can also specify concurrency at the workflow level. \\nWhen a concurrent job or workflow is queued, if another job or workflow using the same concurrency group in the repository is in progress, the queued job or workflow will be pending. Any previously pending job or workflow in the concurrency group will be canceled. To also cancel any currently running job or workflow in the same concurrency group, specify cancel-in-progress: true.\",\n \"oneOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"$ref\": \"#/definitions/concurrency\"\n }\n ]\n },\n \"jobs\": {\n \"$comment\": \"https://help.github.com/en/github/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobs\",\n \"description\": \"A workflow run is made up of one or more jobs. Jobs run in parallel by default. To run jobs sequentially, you can define dependencies on other jobs using the jobs.<job_id>.needs keyword.\\nEach job runs in a fresh instance of the virtual environment specified by runs-on.\\nYou can run an unlimited number of jobs as long as you are within the workflow usage limits. For more information, see https://help.github.com/en/github/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#usage-limits.\",\n \"type\": \"object\",\n \"patternProperties\": {\n \"^[_a-zA-Z][a-zA-Z0-9_-]*$\": {\n \"oneOf\": [\n {\n \"$ref\": \"#/definitions/normalJob\"\n },\n {\n \"$ref\": \"#/definitions/reusableWorkflowCallJob\"\n }\n ]\n }\n },\n \"minProperties\": 1,\n \"additionalProperties\": false\n },\n \"run-name\": {\n \"$comment\": \"https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#run-name\",\n \"description\": \"The name for workflow runs generated from the workflow. GitHub displays the workflow run name in the list of workflow runs on your repository's 'Actions' tab.\",\n \"type\": \"string\"\n },\n \"permissions\": {\n \"$ref\": \"#/definitions/permissions\"\n },\n \"enable-email-notifications\": {\n \"type\": \"boolean\",\n \"description\": \"Send an email notification when a workflow run fails or recovers from a failure. (Forgejo-specific)\"\n }\n },\n \"required\": [\"on\", \"jobs\"],\n \"type\": \"object\"\n}\n";
8873
8931
  const existingSchema = ctx.read(SCHEMA_LOCAL_PATH);
8874
8932
  if (existingSchema !== void 0 && contentEqual(SCHEMA_LOCAL_PATH, existingSchema, schemaContent)) results.push({
8875
8933
  filePath: SCHEMA_LOCAL_PATH,
@@ -8881,7 +8939,7 @@ async function generateVscodeSettings(ctx) {
8881
8939
  results.push({
8882
8940
  filePath: SCHEMA_LOCAL_PATH,
8883
8941
  action: existingSchema ? "updated" : "created",
8884
- description: "Copied Forgejo workflow schema from @bensandee/config"
8942
+ description: "Wrote the Forgejo workflow schema"
8885
8943
  });
8886
8944
  }
8887
8945
  results.push(writeSchemaToSettings(ctx));
@@ -9217,21 +9275,29 @@ const AGENT_RULES_HEAD = [
9217
9275
  "### Agent rules",
9218
9276
  "",
9219
9277
  "- Use pnpm, not npm/yarn/npx. Run binaries with `pnpm exec`.",
9220
- "- Tools come from `mise install` (`mise.toml`); run commands via `mise exec` or an activated shell. A non-interactive session gets them only if mise's shims are on PATH (`mise activate --shims` in the login profile)."
9278
+ "- `mise install` provides the toolchain (`mise.toml`); a non-interactive session finds it only through mise's shims (`mise activate --shims` in the login profile)."
9221
9279
  ];
9222
9280
  const TS_TYPECAST_RULE = "- No typecasts (`as`/`any`). Use zod schemas, type guards, or narrowing.";
9223
9281
  const AGENT_RULES_MID = [
9224
9282
  "- Fix lint violations; don't suppress them. For `no-empty-function`, use a `{ /* no-op */ }` body, not a disable comment.",
9225
- "- ASCII source bytes only. A character that must survive to rendered output stays, escaped for its lexical context: `\\uXXXX` in a JS/TS/Kotlin/Java string, char, or regex; `&#xXXXX;` in JSX or XML text and attributes; a spliced `${'\\uXXXX'}` inside a Kotlin raw string. In comments, prose, and log text prefer plain ASCII -- `...` not ellipsis, `-`/`--` not en/em dashes, straight not curly quotes, `-`/`|`/`+` not box-drawing (`--` is illegal inside an XML comment). `bst ascii:check` reports each offender with its context-correct fix; `--fix` applies the safe ones (`--fix-ambiguous` for em dashes and status glyphs in prose) and flags what it cannot rewrite safely for hand-editing.",
9283
+ "- Write text in any language; letters, accents and other scripts are fine. Keep out what `chars:check` denies: LLM-tell punctuation (curly quotes, ellipsis, en and em dashes, arrows, box-drawing, status glyphs) and invisible or deceptive characters (bidi controls, zero-width and tag characters, odd spaces). Write `...`, `-`/`--`, straight quotes, `->` and `-`/`|`/`+` instead. Where a denied character must survive to output, escape it for its context: `\\uXXXX` in a JS/TS/Kotlin/Java string, char or regex; `&#xXXXX;` in JSX or XML; a spliced `${'\\uXXXX'}` in a Kotlin raw string. `pnpm exec bst chars:check --fix` applies the safe rewrites (`--fix-ambiguous` for em dashes and status glyphs) and flags the rest.",
9226
9284
  "- Write in plain language, per ISO 24495-1 -- every word you produce, code comments and commit bodies included."
9227
9285
  ];
9228
- const TS_EXTENSIONLESS_RULE = "- Prefer extensionless imports; if an extension is required, use `.ts` over `.js`.";
9286
+ const TS_IMPORT_RULE_BUNDLER = "- Prefer extensionless imports; if an extension is required, use `.ts` over `.js`.";
9287
+ const TS_IMPORT_RULE_ALWAYS_TS = "- Relative imports always carry the `.ts` extension.";
9288
+ const TS_IMPORT_RULE_CONDITIONAL = "- Relative imports carry `.ts` with no build step or `nodenext` resolution; otherwise prefer extensionless.";
9289
+ function importRule(config) {
9290
+ if (config.structure === "monorepo") return TS_IMPORT_RULE_CONDITIONAL;
9291
+ if (config.builder === "none" || config.projectType === "node") return TS_IMPORT_RULE_ALWAYS_TS;
9292
+ if (config.builder === "custom") return TS_IMPORT_RULE_CONDITIONAL;
9293
+ return TS_IMPORT_RULE_BUNDLER;
9294
+ }
9229
9295
  const CHANGESETS_RULE = "- A user-facing change owes a changeset in `.changeset/`, written with the change: `patch` for a fix, `minor` for a feature, naming every package it affects. One file per smallest unit of work.";
9230
- const WORK_STATUS_RULE = "- Before starting or resuming work, run `bst work:status`.";
9231
- const WORK_START_RULE = "- Before starting or resuming work, run `bst work:start <n>`.";
9232
- const FIND_COMMAND_RULE = "- Find a command with `bst --help`, its flags with `bst <command> --help`, before guessing.";
9233
- const FORGEJO_CLI_RULE_SUFFIX = " For Forgejo API work, run `bst-fj --help`.";
9234
- const AGENT_CLI_RULE_SUFFIX = " For the agent's runs and transcripts, run `bst-agent --help`.";
9296
+ const WORK_STATUS_RULE = "- Before starting or resuming work, run `pnpm exec bst work:status`.";
9297
+ const WORK_START_RULE = "- Before starting or resuming work, run `pnpm exec bst work:start <n>`.";
9298
+ const FIND_COMMAND_RULE = "- Find a command with `pnpm exec bst --help`, its flags with `pnpm exec bst <command> --help`, before guessing.";
9299
+ const FORGEJO_CLI_RULE_SUFFIX = " For Forgejo API work, run `pnpm exec bst-fj --help`.";
9300
+ const AGENT_CLI_RULE_SUFFIX = " For the agent's runs and transcripts, run `pnpm exec bst-agent --help`.";
9235
9301
  function declaresAny(packageJson, names) {
9236
9302
  if (packageJson === void 0) return false;
9237
9303
  return [
@@ -9252,7 +9318,7 @@ function findCommandRule(packageJson) {
9252
9318
  function agentRulesTail(agentWorked) {
9253
9319
  return [
9254
9320
  "- The bst-hook blocks a short list of dangerous commands (`.claude/hooks/bst-hook-guide.md` lists them); Claude Code's auto mode judges everything else.",
9255
- "- Machine-reading a `bst` command? Pass `--json` after the command name (`bst <cmd> --json`, not `bst --json <cmd>`); capture `2>&1` and parse it as one JSON document, failures included as `{ok: false, error}`. `--verbose` breaks this.",
9321
+ "- Machine-reading a bst command? Run `pnpm exec bst <cmd> --json`, never `--json` before `<cmd>`; capture `2>&1` and parse it as one JSON document, failures included as `{ok: false, error}`. `--verbose` breaks this.",
9256
9322
  agentWorked ? WORK_START_RULE : WORK_STATUS_RULE,
9257
9323
  FIND_COMMAND_RULE,
9258
9324
  "",
@@ -9262,7 +9328,7 @@ function agentRulesTail(agentWorked) {
9262
9328
  "",
9263
9329
  "**Small change** -- a fix, a doc edit, a one-file tweak: work on the default branch and leave it unstaged. Don't commit; surface the commit message as text for the user to run.",
9264
9330
  "",
9265
- "**Large effort** -- anything that warranted a written plan or spec (spec, then plan, then code, each approved before the next): start it in a worktree (see the Git guide), commit as you go, finish with `bst pr:create`. Push later commits with `bst pr:update`, which rewrites nothing unless given `--title`/`--body`. The one exception: a headless agent run (a spec, plan, execute, or review container) never starts a worktree -- it works and commits on the branch it was started on."
9331
+ "**Large effort** -- anything that warranted a written plan or spec (spec, then plan, then code, each approved before the next): start it in a worktree (see the Git guide), commit as you go, finish with `pnpm exec bst pr:create`. Push later commits with `pnpm exec bst pr:update`, which rewrites nothing unless given `--title`/`--body`. The one exception: a headless agent run (a spec, plan, execute, or review container) never starts a worktree -- it works and commits on the branch it was started on."
9266
9332
  ];
9267
9333
  }
9268
9334
  const COMMENTS_GUIDE_BODY = [
@@ -9303,11 +9369,11 @@ const GIT_GUIDE_BODY = [
9303
9369
  "",
9304
9370
  "## Claiming work by hand",
9305
9371
  "",
9306
- "If the bst agent drives this repo, claim an issue or pull request before you work it by hand, so the agent does not start a run beside you: run `bst work:start <n>`. It prints the same orientation `bst work:status` does and then puts `agent-flags/hand-authored` on the object. It claims only where the repo's default branch sets `agent.enabled: true`, and does nothing anywhere else. **Exit 1 means do not start**: a run is live on the pair, or the agent holds it -- the output names which. Exit 0 means the pair is yours, whether the command wrote the flag or found it there already.",
9372
+ "If the bst agent drives this repo, claim an issue or pull request before you work it by hand, so the agent does not start a run beside you: run `pnpm exec bst work:start <n>`. It prints the same orientation `work:status` does and then puts `agent-flags/hand-authored` on the object. It claims only where the repo's default branch sets `agent.enabled: true`, and does nothing anywhere else. **Exit 1 means do not start**: a run is live on the pair, or the agent holds it -- the output names which. Exit 0 means the pair is yours, whether the command wrote the flag or found it there already.",
9307
9373
  "",
9308
9374
  "The flag records who is working an object now, not who wrote it. Do not claim an issue or pull request you only file or open for later, even one a person or a coding session wrote by hand: leave it unclaimed so the agent can take it if asked. Claim it when you are about to work it.",
9309
9375
  "",
9310
- "A pull request you open yourself that closes no claimed issue needs its own `bst work:start <n>` once it exists -- the flag on either half of a pair holds both, but there is no pair until something closes something.",
9376
+ "A pull request you open yourself that closes no claimed issue needs its own `pnpm exec bst work:start <n>` once it exists -- the flag on either half of a pair holds both, but there is no pair until something closes something.",
9311
9377
  "",
9312
9378
  "Hand the work back with the control panel's **Release to the agent**, or by removing `agent-flags/hand-authored` from both halves. Removing it does not queue anything: the agent picks the object up on its next cycle only if a label or a comment asks it to.",
9313
9379
  "",
@@ -9327,7 +9393,7 @@ const GIT_GUIDE_BODY = [
9327
9393
  "",
9328
9394
  "**Drop `WIP:` and fix the stem when the work is done.** Once the pull request carries finished work you are handing over for review -- a spec or a plan ready to approve, or an implementation whose check passes -- remove the `WIP:` marker and make the stem match what it now carries: no stem for an implementation. Do it in the same step that reports the work done, without being asked. A marker left behind tells a reviewer the work is still moving.",
9329
9395
  "",
9330
- "`pr:update` never changes a pull request's draft state or its base. `bst pr:ready <n>` marks a draft ready and drops its `WIP:` marker on either forge; `--title \"<title>\"` fixes the stem in the same step. `bst pr:retarget <n> --base <branch>` repoints a pull request, such as a stacked one whose parent has merged. Both confirm the change by re-reading the forge.",
9396
+ "`pr:update` never changes a pull request's draft state or its base. `pnpm exec bst pr:ready <n>` marks a draft ready and drops its `WIP:` marker on either forge; `--title \"<title>\"` fixes the stem in the same step. `pnpm exec bst pr:retarget <n> --base <branch>` repoints a pull request, such as a stacked one whose parent has merged. Both confirm the change by re-reading the forge.",
9331
9397
  "",
9332
9398
  "## Merging the default branch in",
9333
9399
  "",
@@ -9342,13 +9408,13 @@ const PLAN_LIFECYCLE_GUIDE_BODY = [
9342
9408
  "",
9343
9409
  "`.superpowers/` (no `docs/` prefix) is scratch state, not a plan's home -- a plan doc lives under `docs/plans/` (older ones under `docs/superpowers/plans/`), even one drafted while running a plan through subagents.",
9344
9410
  "",
9345
- "Retiring the plan is the last task of the plan, done in the PR that delivers the work: \"in the tree\" means committed on this branch, not merged to a default branch. Run `bst docs:retire <plan.md> --status <verb> --shipped \"<text>\"` -- it moves the doc, relinks what the move breaks, de-links paths pointing outside `docs/`, and writes the dated terminal `Status:` line, the historical banner, and the \"Shipped:\" paragraph. You supply the judgement: the honest verb -- `completed` / `superseded` / `abandoned` / `rejected` -- and a truthful `--shipped` naming the delivering PR; never fabricate it for uncommitted code or claim a merge that has not happened. A plan that lands one phase of a larger spec retires the plan and leaves the spec open.",
9411
+ "Retiring the plan is the last task of the plan, done in the PR that delivers the work: \"in the tree\" means committed on this branch, not merged to a default branch. Run `pnpm exec bst docs:retire <plan.md> --status <verb> --shipped \"<text>\"` -- it moves the doc, relinks what the move breaks, de-links paths pointing outside `docs/`, and writes the dated terminal `Status:` line, the historical banner, and the \"Shipped:\" paragraph. You supply the judgement: the honest verb -- `completed` / `superseded` / `abandoned` / `rejected` -- and a truthful `--shipped` naming the delivering PR; never fabricate it for uncommitted code or claim a merge that has not happened. A plan that lands one phase of a larger spec retires the plan and leaves the spec open.",
9346
9412
  "",
9347
- "`bst docs:check` enforces the mechanical half (status <-> directory, dated retirements, links, staleness). It cannot tell whether the code shipped -- that's your job.",
9413
+ "`docs:check` enforces the mechanical half (status <-> directory, dated retirements, links, staleness). It cannot tell whether the code shipped -- that's your job.",
9348
9414
  "",
9349
- `An active doc's \`Status:\` may say anything that is not a retirement word; \`draft\`, \`approved\`, and \`planned\` are the usual ones before anything ships. \`bst docs:check\` reads a status as retired when it contains one of ${codeList(TERMINAL_STATUS_WORDS)} and none of ${codeList(OPEN_STATUS_WORDS)}, so a status naming a shipped phase stays active while it also says an open word. A spec whose status says ${codeList(PARTIAL_STATUS_WORDS)} is read as partly delivered, and \`docs:check\` stops warning that every plan implementing it has retired.`,
9415
+ `An active doc's \`Status:\` may say anything that is not a retirement word; \`draft\`, \`approved\`, and \`planned\` are the usual ones before anything ships. \`docs:check\` reads a status as retired when it contains one of ${codeList(TERMINAL_STATUS_WORDS)} and none of ${codeList(OPEN_STATUS_WORDS)}, so a status naming a shipped phase stays active while it also says an open word. A spec whose status says ${codeList(PARTIAL_STATUS_WORDS)} is read as partly delivered, and \`docs:check\` stops warning that every plan implementing it has retired.`,
9350
9416
  "",
9351
- "**A retired doc is a historical record, not documentation you maintain.** Do not read one for staleness or check its claims against the tree -- that misreading has already caused real defects -- and keeping one up to date is not your job. One question decides whether an edit is allowed: would it have been right the day the doc retired? Yes means the doc misrecords its own moment and you may fix it where you already are -- an unchecked box for work that PR delivered, a `Shipped:` naming the wrong PR. No means the tree moved afterwards and the doc is merely old -- a renamed path, a superseded claim -- so leave it. The move, status line, banner, and delinking stay `bst docs:retire`/`bst docs:delink`'s; never hand-write those."
9417
+ "**A retired doc is a historical record, not documentation you maintain.** Do not read one for staleness or check its claims against the tree -- that misreading has already caused real defects -- and keeping one up to date is not your job. One question decides whether an edit is allowed: would it have been right the day the doc retired? Yes means the doc misrecords its own moment and you may fix it where you already are -- an unchecked box for work that PR delivered, a `Shipped:` naming the wrong PR. No means the tree moved afterwards and the doc is merely old -- a renamed path, a superseded claim -- so leave it. The move, status line, banner, and delinking stay `docs:retire`/`docs:delink`'s; never hand-write those."
9352
9418
  ].join("\n");
9353
9419
  const PLANNING_GUIDE_BODY = [
9354
9420
  "# Specs and plans",
@@ -9361,10 +9427,22 @@ const PLANNING_GUIDE_BODY = [
9361
9427
  "",
9362
9428
  "## The plan",
9363
9429
  "",
9364
- "Write for an executor who has not seen this repo or the spec. They write idiomatic code once they know the exact interface and the exact test, and make a reasonable choice wherever the plan leaves one open; what they cannot know is what you decided. A plan records those decisions -- it is not a transcript of the code. Save it under `docs/plans/`.",
9430
+ "Write for an executor who has not seen this repo or the spec. They write idiomatic code once they know the exact interface and the exact test, and make a reasonable choice wherever the plan leaves one open; what they cannot know is what you decided. A plan records those decisions -- it is not a transcript of the code. Save it under `docs/plans/`, named by the rules below.",
9431
+ "",
9432
+ "### Which plan a branch works",
9433
+ "",
9434
+ "A plan may be written by you, by another session, or by a headless agent, and the run that executes it may be any of these. That run never asks which plan is meant: it finds the plan from the branch alone, so the name and the git history must say it.",
9435
+ "",
9436
+ "- A branch has one live plan. Tooling looks first for a path under `docs/` that carries the marker of the issue or pull request the run works (`issue-<n>` or `pr-<n>`), then for the one live plan the branch **added**. A plan the branch only edited or renamed is not an added file, so a plan that has neither the marker nor the add is invisible to the run that should execute it, however clearly its text names the work.",
9437
+ "- Name a new plan `docs/plans/<YYYY-MM-DD>-<topic>.md`, and when the work has an issue or pull request, put its marker in the name: `docs/plans/<YYYY-MM-DD>-issue-<n>-<topic>.md`. The marker is what keeps the plan findable when a second plan joins the branch.",
9438
+ "- To refine a plan this branch already added, edit it in place and keep its name, so the branch never carries two plans for one piece of work.",
9439
+ "- A plan the branch inherited from the default branch -- a deferred plan, or a plan that landed in an earlier pull request -- is found only by its marker. To work one, whether you rewrite it or execute it, rename it with `git mv` to a name that carries the marker, in the first commit that touches it. Use the marker of the object the run works: when an agent executes the plan on a pull request, that is the pull request's `pr-<n>`, not the issue's. Do not edit an inherited plan under its old name.",
9440
+ "- After committing the plan, check that it is findable: `git diff --name-status <default branch>...HEAD -- docs/plans` lists it with `A`, or its path carries the marker.",
9441
+ "",
9442
+ "### Writing the plan",
9365
9443
  "",
9366
9444
  "- Check the plan's premises against the tree before building on them. \"Today X is broken\" is a claim: read the code that handles X, not just the line that throws.",
9367
- "- Label each task `Task <n>:` at the start of its heading or checkbox, numbered from 1, so `bst plan:brief` can find it. To split a task after the plan is written, give the parts one lowercase letter (`Task 18a:`, `Task 18b:`) rather than renumbering, so later tasks and every cross-reference keep their numbers. A checkbox's first sentence is the task's name on the control panel: end it with a period inside one short line, and put the files, commands, and checks in the sentences after it.",
9445
+ "- Label each task `Task <n>:` at the start of its heading or checkbox, numbered from 1, so `plan:brief` can find it. To split a task after the plan is written, give the parts one lowercase letter (`Task 18a:`, `Task 18b:`) rather than renumbering, so later tasks and every cross-reference keep their numbers. A checkbox's first sentence is the task's name on the control panel: end it with a period inside one short line, and put the files, commands, and checks in the sentences after it.",
9368
9446
  `- Give each task what makes it unambiguous and nothing more: a test's name and its assertions with the spec's exact values and why that test fails before the change, a code change's exact signature and file, a command with the output that means it passed. Write out a body only for an algorithm the signature and tests do not determine. Work that uses another task's result names that task's interfaces, never repeats its code. A task with no test to write says \`${TEST_EVIDENCE_PREFIXES.noTest} <reason>\` in the plan, so the executor inherits the call instead of making it cold.`,
9369
9447
  "- Keep tasks small. Each one runs its own test cycle and ends in a result that can be checked on its own: write the failing test, watch it fail, make it pass, commit. A run cut off partway then loses one task, not the plan. A large plan is fine; a large task is not.",
9370
9448
  "- A plan that builds on earlier work opens with a task that checks the prerequisite exists, and records a `**Blocked:**` note if it does not, instead of rebuilding it.",
@@ -9402,10 +9480,10 @@ const SUBAGENTS_GUIDE_BODY = [
9402
9480
  "",
9403
9481
  "## Running a plan through subagents",
9404
9482
  "",
9405
- "- The plan's checkboxes are the progress record. Tick a task's box and commit once its review is clean. After a context reset, `bst plan:status <plan>` names the next open task; trust it and `git log` over your memory, and never re-run a ticked task.",
9406
- "- `bst plan:workspace <plan>` prints the plan's own git-ignored scratch directory for briefs, reports, review packages, and fix-round notes. Nothing that must outlive the run goes there.",
9483
+ "- The plan's checkboxes are the progress record. Tick a task's box and commit once its review is clean. After a context reset, `pnpm exec bst plan:status <plan>` names the next open task; trust it and `git log` over your memory, and never re-run a ticked task.",
9484
+ "- `pnpm exec bst plan:workspace <plan>` prints the plan's own git-ignored scratch directory for briefs, reports, review packages, and fix-round notes. Nothing that must outlive the run goes there.",
9407
9485
  "- Before the first task, check the plan for tasks that contradict each other or the spec. Rule on each (the spec wins) and record the ruling.",
9408
- "- Per task: note the base commit, run `bst plan:brief <plan> <n>`, and give one implementer the brief file it writes plus a report file to write back. Then run `bst plan:review-package <plan> <base> <head>` and have a separate reviewer judge that file against the brief, for both spec compliance and quality. Never tell a reviewer what not to flag.",
9486
+ "- Per task: note the base commit, run `pnpm exec bst plan:brief <plan> <n>`, and give one implementer the brief file it writes plus a report file to write back. Then run `pnpm exec bst plan:review-package <plan> <base> <head>` and have a separate reviewer judge that file against the brief, for both spec compliance and quality. Never tell a reviewer what not to flag.",
9409
9487
  "- Findings go back to the same implementer, followed by a review of the fix diff only. Cap it at five rounds, switching to a stronger model from round four. At the cap, rule on each open finding and record why. Never fix in your own session: that skips review. A minor finding nobody fixes in this run goes to the findings ledger, not a scratch note.",
9410
9488
  "- Do not stop between tasks to ask. Stop only for something irreversible or destructive, a security-sensitive action, a side effect outside the working tree (a push, a merge, a publish), or a plan so broken that every way forward is a guess. Anything else, decide, and record the ruling with what it costs if wrong.",
9411
9489
  "- Keep every ruling where the human will read it: list them all, each with what it costs if wrong, in your final message and in the pull request body.",
@@ -9488,14 +9566,14 @@ const CODE_REVIEW_GUIDE_BODY = [
9488
9566
  const FINDINGS_LEDGER_BODY = [
9489
9567
  "# Findings ledger",
9490
9568
  "",
9491
- "A defect you notice but are not fixing in the current change -- a nit, or something pre-existing -- goes in `.agent/findings/`, one file per finding, via `bst findings:add` (never hand-write the filename: a name the parser cannot read back is silently dropped from every listing). A finding names the PR or issue that surfaced it. The file IS the pending state: merging it enqueues it for the automated backlog run, fixing or refuting it means deleting the file in the same commit (`bst findings:resolve`), and deferring it appends a note (`bst findings:resolve --defer`). A finding only a human can decide leaves the ledger as an issue: the backlog run escalates it through the `human-decision` follow-up route and deletes the file; by hand, file the issue yourself and `bst findings:resolve` the entry. `bst findings:list` shows what is pending."
9569
+ "A defect you notice but are not fixing in the current change -- a nit, or something pre-existing -- goes in `.agent/findings/`, one file per finding, via `pnpm exec bst findings:add` (never hand-write the filename: a name the parser cannot read back is silently dropped from every listing). A finding names the PR or issue that surfaced it. The file IS the pending state: merging it enqueues it for the automated backlog run, fixing or refuting it means deleting the file in the same commit (`pnpm exec bst findings:resolve`), and deferring it appends a note (`pnpm exec bst findings:resolve --defer`). A finding only a human can decide leaves the ledger as an issue: the backlog run escalates it through the `human-decision` follow-up route and deletes the file; by hand, file the issue yourself and `pnpm exec bst findings:resolve` the entry. `pnpm exec bst findings:list` shows what is pending."
9492
9570
  ].join("\n");
9493
9571
  const COVERAGE_RULE_BODY = [
9494
9572
  "# Coverage baseline",
9495
9573
  "",
9496
- "Touched `src/`? Run `pnpm test:coverage`, then `pnpm exec bst coverage:check`. If a total regressed beyond tolerance, add tests until parity -- or exclude the file via `coverage.exclude` in `.tooling.json` and say why in the commit message. If all four are flat-or-up, run `bst coverage:check --update-baseline` and stage `coverage-baseline.json`. Skip for docs-, config-, dependency-, or test-only changes.",
9574
+ "Touched `src/`? Run `pnpm test:coverage`, then `pnpm exec bst coverage:check`. If a total regressed beyond tolerance, add tests until parity -- or exclude the file via `coverage.exclude` in `.tooling.json` and say why in the commit. If no total dropped, run `pnpm exec bst coverage:check --update-baseline` and stage `coverage-baseline.json`. Skip for docs-, config-, dependency-, or test-only changes.",
9497
9575
  "",
9498
- "If the total dropped only because previously-untested code entered the denominator -- a first test, or `coverage.include` newly enabled -- that is expected, not a regression: do not exclude the file you just tested. Adopt the baseline instead: `bst coverage:check --update-baseline --reset` (refuses a downward move without `--reset`).",
9576
+ "If the total dropped only because previously-untested code entered the denominator -- a first test, or `coverage.include` newly enabled -- that is expected, not a regression: do not exclude the file you just tested. Adopt the baseline instead: `pnpm exec bst coverage:check --update-baseline --reset` (a downward move needs `--reset`).",
9499
9577
  "",
9500
9578
  "Totals at or above `coverage.floorPp` pass and re-baseline freely; the ratchet applies only below the floor."
9501
9579
  ].join("\n");
@@ -9526,7 +9604,7 @@ function checkCadenceBody(config) {
9526
9604
  const HEALTH_RULE_HEAD = [
9527
9605
  "# Codebase health",
9528
9606
  "",
9529
- "`bst health:check` gates four structural metrics against `health-baseline.json`: any increase fails, as does a module or directory past its recorded ceiling. `bst health:summary --explain` names the offenders.",
9607
+ "`health:check` gates four structural metrics against `health-baseline.json`: any increase fails, as does a module or directory past its recorded ceiling. `pnpm exec bst health:summary --explain` names the offenders.",
9530
9608
  ""
9531
9609
  ];
9532
9610
  const HEALTH_RULE_SAME_PACKAGE = "**The fan-in, fan-out, and cycle metrics count value edges only.** A type-only import is erased before the graph is built and never moves a metric. An import across a workspace package boundary counts at the `health.crossPackage` default and is invisible when it is off.";
@@ -9535,11 +9613,11 @@ const HEALTH_RULE_TAIL = [
9535
9613
  "",
9536
9614
  "So never duplicate a type, or fork a helper, to protect a fan budget -- import it. Nor move a module out of its home directory to dodge crowding. The converse holds: turning a type-only import into a value import adds an edge, so a module that already imports another for its types gains a real edge the first time it imports a value from it.",
9537
9615
  "",
9538
- "**An exemption takes a module or directory out of the gate.** It is listed with its count and reason in `bst health:summary --explain`, carries no ceiling, and is counted in no metric. `crowdedDirectories` is exemptable by directory path. Adding one is a raise too.",
9616
+ "**An exemption takes a module or directory out of the gate.** It is listed with its count and reason in `health:summary --explain`, carries no ceiling, and is counted in no metric. `crowdedDirectories` is exemptable by directory path. Adding one is a raise too.",
9539
9617
  "",
9540
- "**A raise travels alone.** A raise is a higher count or ceiling, a ceiling for a module that has none, a new exemption, or a higher limit. It lands only as a ceiling-only pull request -- one whose diff changes nothing but `health-baseline.json` and `.tooling.json` -- which a human merges. When `bst health:check` refuses, split the module or open that pull request, and stop. Never run `bst health:check --update-baseline --reset` on a branch that carries code.",
9618
+ "**A raise travels alone.** A raise is a higher count or ceiling, a ceiling for a module that has none, a new exemption, or a higher limit. It lands only as a ceiling-only pull request -- one whose diff changes nothing but `health-baseline.json` and `.tooling.json` -- which a human merges. When `health:check` refuses, split the module or open that pull request, and stop. Never run `pnpm exec bst health:check --update-baseline --reset` on a branch that carries code.",
9541
9619
  "",
9542
- "Prefer an exemption to raising `fanInLimit` or `fanOutLimit`: an exemption names the hub in `bst health:summary --explain`, while a higher limit silently un-tracks every module beneath it. A per-module ceiling has no such shadow -- it ratchets one module at one count and fails again one edge later."
9620
+ "Prefer an exemption to raising `fanInLimit` or `fanOutLimit`: an exemption names the hub in `health:summary --explain`, while a higher limit silently un-tracks every module beneath it. A per-module ceiling has no such shadow -- it ratchets one module at one count and fails again one edge later."
9543
9621
  ];
9544
9622
  function healthRuleBody(config) {
9545
9623
  const lines = [...HEALTH_RULE_HEAD];
@@ -9674,7 +9752,7 @@ function agentGuides(config, coverageEnabled, healthEnabled, webUiEnabled) {
9674
9752
  },
9675
9753
  {
9676
9754
  name: "health.md",
9677
- trigger: "add a cross-module import, or when `bst health:check` fails",
9755
+ trigger: "add a cross-module import, or when `health:check` fails",
9678
9756
  body: healthRuleBody(config),
9679
9757
  enabled: healthEnabled
9680
9758
  }
@@ -9684,7 +9762,7 @@ function guideIndex(guides) {
9684
9762
  const lines = [
9685
9763
  "#### Guides",
9686
9764
  "",
9687
- "Each guide below is a file `bst repo:sync` keeps current. Read it at the moment it names, before you act. Where an installed plugin or skill covers the same ground as a guide, the guide wins.",
9765
+ "Each guide below is a file `repo:sync` keeps current. Read it at the moment it names, before you act. Where an installed plugin or skill covers the same ground as a guide, the guide wins.",
9688
9766
  ""
9689
9767
  ];
9690
9768
  for (const guide of guides.filter((g) => g.enabled)) lines.push(`- Before you ${guide.trigger}: \`${GUIDES_DIR}/${guide.name}\`.`);
@@ -9694,7 +9772,7 @@ function agentRulesBody(config, guides, packageJson, agentWorked) {
9694
9772
  const lines = [...AGENT_RULES_HEAD];
9695
9773
  if (config.stack === "typescript") lines.push(TS_TYPECAST_RULE);
9696
9774
  lines.push(...AGENT_RULES_MID);
9697
- if (config.stack === "typescript") lines.push(TS_EXTENSIONLESS_RULE);
9775
+ if (config.stack === "typescript") lines.push(importRule(config));
9698
9776
  if (config.releaseStrategy === "changesets") lines.push(CHANGESETS_RULE);
9699
9777
  const tail = agentRulesTail(agentWorked).map((line) => line === FIND_COMMAND_RULE ? findCommandRule(packageJson) : line);
9700
9778
  lines.push(...tail, "", guideIndex(guides));
@@ -10001,7 +10079,7 @@ function reportFailures(failed, command) {
10001
10079
  function generateMigratePrompt(results, config, detected) {
10002
10080
  const sections = [];
10003
10081
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
10004
- sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.92.1 repo:sync\` on ${timestamp}_\n`);
10082
+ sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.93.1 repo:sync\` on ${timestamp}_\n`);
10005
10083
  const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
10006
10084
  if (changed.length > 0) {
10007
10085
  const items = changed.map((r) => {
@@ -11659,19 +11737,32 @@ function withHealthCheck(checks, step = HEALTH_CHECK) {
11659
11737
  ...checks.slice(at)
11660
11738
  ];
11661
11739
  }
11662
- const ASCII_CHECK = { name: "ascii:check" };
11663
- function withAsciiCheck(checks) {
11740
+ const CHARS_CHECK = { name: "chars:check" };
11741
+ const LEGACY_CHARS_CHECK_SCRIPT = "ascii:check";
11742
+ function legacyCharsAlias(name) {
11743
+ return name === LEGACY_CHARS_CHECK_SCRIPT ? CHARS_CHECK.name : void 0;
11744
+ }
11745
+ function withLegacyCharsSkip(entries) {
11746
+ const skip = new Set(entries);
11747
+ const aliases = [...skip].map(legacyCharsAlias).filter((name) => name !== void 0);
11748
+ for (const alias of aliases) skip.add(alias);
11749
+ return skip;
11750
+ }
11751
+ function withCharsCheck(checks) {
11664
11752
  const idx = checks.findIndex((c) => c.name === "lint");
11665
11753
  const at = idx === -1 ? checks.length : idx + 1;
11666
11754
  return [
11667
11755
  ...checks.slice(0, at),
11668
- ASCII_CHECK,
11756
+ CHARS_CHECK,
11669
11757
  ...checks.slice(at)
11670
11758
  ];
11671
11759
  }
11760
+ function charsBlock(config) {
11761
+ return config?.chars ?? config?.ascii;
11762
+ }
11672
11763
  function resolveBuiltinChecks(config) {
11673
11764
  let builtin = CHECKS;
11674
- if (config?.ascii?.enabled === true) builtin = withAsciiCheck(builtin);
11765
+ if (isCharsGateOn(charsBlock(config))) builtin = withCharsCheck(builtin);
11675
11766
  if (config?.health?.enabled === true) builtin = withHealthCheck(builtin);
11676
11767
  return builtin;
11677
11768
  }
@@ -11690,7 +11781,7 @@ function resolveRunBuiltins(targetDir, config, referenceHealthOn = referenceEnab
11690
11781
  function staleChecksAddNames(config, add = config?.checks?.add ?? []) {
11691
11782
  if (add.length === 0) return [];
11692
11783
  const names = new Set(resolveBuiltinChecks(config).map((check) => check.name));
11693
- return add.filter((name) => names.has(name));
11784
+ return add.filter((name) => names.has(name) || names.has(legacyCharsAlias(name) ?? ""));
11694
11785
  }
11695
11786
  function shouldSkip(name, patterns) {
11696
11787
  if (patterns.size === 0) return false;
@@ -11846,8 +11937,8 @@ async function runRunChecks(targetDir, options = {}) {
11846
11937
  const getScripts = options.getScripts ?? defaultGetScripts;
11847
11938
  const toolingConfig = options.toolingConfig ?? loadToolingConfig(targetDir);
11848
11939
  const configChecks = toolingConfig?.checks;
11849
- const flagSkip = new Set(options.skip ?? []);
11850
- const configSkip = new Set(configChecks?.skip ?? []);
11940
+ const flagSkip = withLegacyCharsSkip(options.skip ?? []);
11941
+ const configSkip = withLegacyCharsSkip(configChecks?.skip ?? []);
11851
11942
  const skip = /* @__PURE__ */ new Set([...flagSkip, ...configSkip]);
11852
11943
  const add = [.../* @__PURE__ */ new Set([...options.add ?? [], ...configChecks?.add ?? []])];
11853
11944
  const isCI = Boolean(process.env["CI"]);
@@ -11855,17 +11946,24 @@ async function runRunChecks(targetDir, options = {}) {
11855
11946
  const reporter = isCI ? ciReporter : localReporter;
11856
11947
  const vc = { verbose: options.verbose ?? false };
11857
11948
  const definedScripts = getScripts(targetDir);
11949
+ const definedCheckScripts = new Set(definedScripts);
11950
+ if (!definedCheckScripts.has(CHARS_CHECK.name) && definedScripts.has(LEGACY_CHARS_CHECK_SCRIPT)) definedCheckScripts.add(CHARS_CHECK.name);
11858
11951
  const addedNames = new Set(add);
11859
11952
  const builtinChecks = resolveRunBuiltins(targetDir, toolingConfig, options.referenceEnablesHealth);
11860
11953
  const builtinNames = new Set(builtinChecks.map((check) => check.name));
11861
- const allChecks = [...builtinChecks, ...add.filter((name) => !builtinNames.has(name)).map((name) => ({ name }))];
11954
+ function isBuiltInName(name) {
11955
+ if (builtinNames.has(name)) return true;
11956
+ const alias = legacyCharsAlias(name);
11957
+ return alias !== void 0 && builtinNames.has(alias);
11958
+ }
11959
+ const allChecks = [...builtinChecks, ...add.filter((name) => !isBuiltInName(name)).map((name) => ({ name }))];
11862
11960
  const failures = [];
11863
11961
  const warnedSteps = [];
11864
11962
  const notDefined = [];
11865
11963
  const coverage = decideCoverage(targetDir, options, toolingConfig);
11866
11964
  const useCoverageScript = coverage.enabled && definedScripts.has("test:coverage");
11867
11965
  logCoverageChoice(vc, coverage, useCoverageScript, options.coverage);
11868
- const skippedSteps = reportSkippedUnlessRefused(reporter, allChecks, definedScripts, flagSkip, configSkip);
11966
+ const skippedSteps = reportSkippedUnlessRefused(reporter, allChecks, definedCheckScripts, flagSkip, configSkip);
11869
11967
  for (const check of allChecks) {
11870
11968
  if (shouldSkip(check.name, skip)) {
11871
11969
  if (requiredSkipIsRefused(check, flagSkip, configSkip)) {
@@ -11875,7 +11973,7 @@ async function runRunChecks(targetDir, options = {}) {
11875
11973
  }
11876
11974
  continue;
11877
11975
  }
11878
- if (!definedScripts.has(check.name)) {
11976
+ if (!definedCheckScripts.has(check.name)) {
11879
11977
  if (addedNames.has(check.name) || check.required === true) {
11880
11978
  reporter.undefinedCheck(check.name);
11881
11979
  failures.push(check.name);
@@ -11885,6 +11983,7 @@ async function runRunChecks(targetDir, options = {}) {
11885
11983
  let scriptName = check.name;
11886
11984
  if (check.name === "test" && useCoverageScript) scriptName = "test:coverage";
11887
11985
  if (check.name === "lint" && definedScripts.has("lint:report")) scriptName = "lint:report";
11986
+ if (check.name === CHARS_CHECK.name && !definedScripts.has(CHARS_CHECK.name) && definedScripts.has(LEGACY_CHARS_CHECK_SCRIPT)) scriptName = LEGACY_CHARS_CHECK_SCRIPT;
11888
11987
  const cmd = check.args ? `pnpm run ${scriptName} ${check.args}` : `pnpm run ${scriptName}`;
11889
11988
  debug(vc, `Running: ${cmd} (in ${targetDir})`);
11890
11989
  reporter.groupStart(check.name);
@@ -11917,11 +12016,11 @@ async function runRunChecks(targetDir, options = {}) {
11917
12016
  reporter.allPassed(warnedSteps, skippedSteps);
11918
12017
  return 0;
11919
12018
  }
11920
- const HELP_CHECK_NAMES = withAsciiCheck(withHealthCheck(CHECKS)).map((check) => check.name).join(", ");
12019
+ const HELP_CHECK_NAMES = withCharsCheck(withHealthCheck(CHECKS)).map((check) => check.name).join(", ");
11921
12020
  const runChecksCommand = defineCommand({
11922
12021
  meta: {
11923
12022
  name: "checks:run",
11924
- description: `Run the repo's standard check pipeline. The steps are ${HELP_CHECK_NAMES} (ascii:check and health:check only where .tooling.json enables them). A step that exits 0 and prints ::bst-checks:: warn <count> on stdout is reported as warned; no exit code changes.`
12023
+ description: `Run the repo's standard check pipeline. The steps are ${HELP_CHECK_NAMES} (chars:check runs unless .tooling.json#chars.enabled is false, and health:check only where .tooling.json enables it). A step that exits 0 and prints ::bst-checks:: warn <count> on stdout is reported as warned; no exit code changes.`
11925
12024
  },
11926
12025
  args: {
11927
12026
  dir: {
@@ -15715,7 +15814,7 @@ function describeForgeError(error) {
15715
15814
  if (error instanceof RepoTargetMismatchError) return `--repo disagrees with the issue URL: --repo names ${error.given}, the issue URL names ${error.fromReference}.`;
15716
15815
  }
15717
15816
  //#endregion
15718
- //#region src/ascii-check/offenders.ts
15817
+ //#region src/chars-check/offenders.ts
15719
15818
  const TIER1_OFFENDERS = [
15720
15819
  {
15721
15820
  codepoint: "U+2018",
@@ -15989,6 +16088,27 @@ const OUTPUT_GLYPH_OFFENDERS = [
15989
16088
  char: "▶",
15990
16089
  replacement: ">"
15991
16090
  },
16091
+ {
16092
+ codepoint: "U+25BA",
16093
+ name: "BLACK RIGHT-POINTING POINTER",
16094
+ tier: 2,
16095
+ char: "►",
16096
+ replacement: ">"
16097
+ },
16098
+ {
16099
+ codepoint: "U+25C4",
16100
+ name: "BLACK LEFT-POINTING POINTER",
16101
+ tier: 2,
16102
+ char: "◄",
16103
+ replacement: "<"
16104
+ },
16105
+ {
16106
+ codepoint: "U+25C0",
16107
+ name: "BLACK LEFT-POINTING TRIANGLE",
16108
+ tier: 2,
16109
+ char: "◀",
16110
+ replacement: "<"
16111
+ },
15992
16112
  {
15993
16113
  codepoint: "U+0394",
15994
16114
  name: "GREEK CAPITAL LETTER DELTA",
@@ -15997,12 +16117,349 @@ const OUTPUT_GLYPH_OFFENDERS = [
15997
16117
  replacement: "delta"
15998
16118
  }
15999
16119
  ];
16120
+ function rangeOffenders(first, last, name, group) {
16121
+ const offenders = [];
16122
+ for (let codePoint = first; codePoint <= last; codePoint += 1) offenders.push({
16123
+ codepoint: `U+${codePoint.toString(16).toUpperCase().padStart(4, "0")}`,
16124
+ name,
16125
+ tier: 1,
16126
+ char: String.fromCodePoint(codePoint),
16127
+ group
16128
+ });
16129
+ return offenders;
16130
+ }
16131
+ const INVISIBLE_OFFENDERS = [
16132
+ {
16133
+ codepoint: "U+202A",
16134
+ name: "LEFT-TO-RIGHT EMBEDDING",
16135
+ tier: 1,
16136
+ char: "‪",
16137
+ group: "invisible"
16138
+ },
16139
+ {
16140
+ codepoint: "U+202B",
16141
+ name: "RIGHT-TO-LEFT EMBEDDING",
16142
+ tier: 1,
16143
+ char: "‫",
16144
+ group: "invisible"
16145
+ },
16146
+ {
16147
+ codepoint: "U+202C",
16148
+ name: "POP DIRECTIONAL FORMATTING",
16149
+ tier: 1,
16150
+ char: "‬",
16151
+ group: "invisible"
16152
+ },
16153
+ {
16154
+ codepoint: "U+202D",
16155
+ name: "LEFT-TO-RIGHT OVERRIDE",
16156
+ tier: 1,
16157
+ char: "‭",
16158
+ group: "invisible"
16159
+ },
16160
+ {
16161
+ codepoint: "U+202E",
16162
+ name: "RIGHT-TO-LEFT OVERRIDE",
16163
+ tier: 1,
16164
+ char: "‮",
16165
+ group: "invisible"
16166
+ },
16167
+ {
16168
+ codepoint: "U+2066",
16169
+ name: "LEFT-TO-RIGHT ISOLATE",
16170
+ tier: 1,
16171
+ char: "⁦",
16172
+ group: "invisible"
16173
+ },
16174
+ {
16175
+ codepoint: "U+2067",
16176
+ name: "RIGHT-TO-LEFT ISOLATE",
16177
+ tier: 1,
16178
+ char: "⁧",
16179
+ group: "invisible"
16180
+ },
16181
+ {
16182
+ codepoint: "U+2068",
16183
+ name: "FIRST STRONG ISOLATE",
16184
+ tier: 1,
16185
+ char: "⁨",
16186
+ group: "invisible"
16187
+ },
16188
+ {
16189
+ codepoint: "U+2069",
16190
+ name: "POP DIRECTIONAL ISOLATE",
16191
+ tier: 1,
16192
+ char: "⁩",
16193
+ group: "invisible"
16194
+ },
16195
+ {
16196
+ codepoint: "U+200B",
16197
+ name: "ZERO WIDTH SPACE",
16198
+ tier: 1,
16199
+ char: "​",
16200
+ group: "invisible"
16201
+ },
16202
+ {
16203
+ codepoint: "U+2060",
16204
+ name: "WORD JOINER",
16205
+ tier: 1,
16206
+ char: "⁠",
16207
+ group: "invisible"
16208
+ },
16209
+ {
16210
+ codepoint: "U+2061",
16211
+ name: "FUNCTION APPLICATION",
16212
+ tier: 1,
16213
+ char: "⁡",
16214
+ group: "invisible"
16215
+ },
16216
+ {
16217
+ codepoint: "U+2062",
16218
+ name: "INVISIBLE TIMES",
16219
+ tier: 1,
16220
+ char: "⁢",
16221
+ group: "invisible"
16222
+ },
16223
+ {
16224
+ codepoint: "U+2063",
16225
+ name: "INVISIBLE SEPARATOR",
16226
+ tier: 1,
16227
+ char: "⁣",
16228
+ group: "invisible"
16229
+ },
16230
+ {
16231
+ codepoint: "U+2064",
16232
+ name: "INVISIBLE PLUS",
16233
+ tier: 1,
16234
+ char: "⁤",
16235
+ group: "invisible"
16236
+ },
16237
+ {
16238
+ codepoint: "U+FEFF",
16239
+ name: "ZERO WIDTH NO-BREAK SPACE",
16240
+ tier: 1,
16241
+ char: "",
16242
+ group: "invisible"
16243
+ },
16244
+ {
16245
+ codepoint: "U+00AD",
16246
+ name: "SOFT HYPHEN",
16247
+ tier: 1,
16248
+ char: "­",
16249
+ group: "invisible"
16250
+ },
16251
+ {
16252
+ codepoint: "U+115F",
16253
+ name: "HANGUL CHOSEONG FILLER",
16254
+ tier: 1,
16255
+ char: "ᅟ",
16256
+ group: "invisible"
16257
+ },
16258
+ {
16259
+ codepoint: "U+1160",
16260
+ name: "HANGUL JUNGSEONG FILLER",
16261
+ tier: 1,
16262
+ char: "ᅠ",
16263
+ group: "invisible"
16264
+ },
16265
+ {
16266
+ codepoint: "U+3164",
16267
+ name: "HANGUL FILLER",
16268
+ tier: 1,
16269
+ char: "ㅤ",
16270
+ group: "invisible"
16271
+ },
16272
+ {
16273
+ codepoint: "U+FFA0",
16274
+ name: "HALFWIDTH HANGUL FILLER",
16275
+ tier: 1,
16276
+ char: "ᅠ",
16277
+ group: "invisible"
16278
+ },
16279
+ ...rangeOffenders(917504, 917631, "TAG CHARACTER", "invisible")
16280
+ ];
16281
+ const SCRIPT_DEPENDENT_OFFENDERS = [
16282
+ {
16283
+ codepoint: "U+200C",
16284
+ name: "ZERO WIDTH NON-JOINER",
16285
+ tier: 1,
16286
+ char: "‌",
16287
+ group: "script"
16288
+ },
16289
+ {
16290
+ codepoint: "U+200D",
16291
+ name: "ZERO WIDTH JOINER",
16292
+ tier: 1,
16293
+ char: "‍",
16294
+ group: "script"
16295
+ },
16296
+ {
16297
+ codepoint: "U+200E",
16298
+ name: "LEFT-TO-RIGHT MARK",
16299
+ tier: 1,
16300
+ char: "‎",
16301
+ group: "script"
16302
+ },
16303
+ {
16304
+ codepoint: "U+200F",
16305
+ name: "RIGHT-TO-LEFT MARK",
16306
+ tier: 1,
16307
+ char: "‏",
16308
+ group: "script"
16309
+ },
16310
+ {
16311
+ codepoint: "U+061C",
16312
+ name: "ARABIC LETTER MARK",
16313
+ tier: 1,
16314
+ char: "؜",
16315
+ group: "script"
16316
+ },
16317
+ {
16318
+ codepoint: "U+3000",
16319
+ name: "IDEOGRAPHIC SPACE",
16320
+ tier: 1,
16321
+ char: " ",
16322
+ group: "script"
16323
+ }
16324
+ ];
16325
+ const ODD_SPACE_OFFENDERS = [
16326
+ {
16327
+ codepoint: "U+2000",
16328
+ name: "EN QUAD",
16329
+ tier: 1,
16330
+ char: " ",
16331
+ group: "space",
16332
+ replacement: " "
16333
+ },
16334
+ {
16335
+ codepoint: "U+2001",
16336
+ name: "EM QUAD",
16337
+ tier: 1,
16338
+ char: " ",
16339
+ group: "space",
16340
+ replacement: " "
16341
+ },
16342
+ {
16343
+ codepoint: "U+2002",
16344
+ name: "EN SPACE",
16345
+ tier: 1,
16346
+ char: " ",
16347
+ group: "space",
16348
+ replacement: " "
16349
+ },
16350
+ {
16351
+ codepoint: "U+2003",
16352
+ name: "EM SPACE",
16353
+ tier: 1,
16354
+ char: " ",
16355
+ group: "space",
16356
+ replacement: " "
16357
+ },
16358
+ {
16359
+ codepoint: "U+2004",
16360
+ name: "THREE-PER-EM SPACE",
16361
+ tier: 1,
16362
+ char: " ",
16363
+ group: "space",
16364
+ replacement: " "
16365
+ },
16366
+ {
16367
+ codepoint: "U+2005",
16368
+ name: "FOUR-PER-EM SPACE",
16369
+ tier: 1,
16370
+ char: " ",
16371
+ group: "space",
16372
+ replacement: " "
16373
+ },
16374
+ {
16375
+ codepoint: "U+2006",
16376
+ name: "SIX-PER-EM SPACE",
16377
+ tier: 1,
16378
+ char: " ",
16379
+ group: "space",
16380
+ replacement: " "
16381
+ },
16382
+ {
16383
+ codepoint: "U+2007",
16384
+ name: "FIGURE SPACE",
16385
+ tier: 1,
16386
+ char: " ",
16387
+ group: "space",
16388
+ replacement: " "
16389
+ },
16390
+ {
16391
+ codepoint: "U+2008",
16392
+ name: "PUNCTUATION SPACE",
16393
+ tier: 1,
16394
+ char: " ",
16395
+ group: "space",
16396
+ replacement: " "
16397
+ },
16398
+ {
16399
+ codepoint: "U+2009",
16400
+ name: "THIN SPACE",
16401
+ tier: 1,
16402
+ char: " ",
16403
+ group: "space",
16404
+ replacement: " "
16405
+ },
16406
+ {
16407
+ codepoint: "U+200A",
16408
+ name: "HAIR SPACE",
16409
+ tier: 1,
16410
+ char: " ",
16411
+ group: "space",
16412
+ replacement: " "
16413
+ },
16414
+ {
16415
+ codepoint: "U+205F",
16416
+ name: "MEDIUM MATHEMATICAL SPACE",
16417
+ tier: 1,
16418
+ char: " ",
16419
+ group: "space",
16420
+ replacement: " "
16421
+ }
16422
+ ];
16423
+ const HYPHEN_OFFENDERS = [
16424
+ {
16425
+ codepoint: "U+2010",
16426
+ name: "HYPHEN",
16427
+ tier: 1,
16428
+ char: "‐",
16429
+ replacement: "-"
16430
+ },
16431
+ {
16432
+ codepoint: "U+2011",
16433
+ name: "NON-BREAKING HYPHEN",
16434
+ tier: 1,
16435
+ char: "‑",
16436
+ replacement: "-"
16437
+ },
16438
+ {
16439
+ codepoint: "U+2012",
16440
+ name: "FIGURE DASH",
16441
+ tier: 1,
16442
+ char: "‒",
16443
+ replacement: "-"
16444
+ },
16445
+ {
16446
+ codepoint: "U+2015",
16447
+ name: "HORIZONTAL BAR",
16448
+ tier: 2,
16449
+ char: "―",
16450
+ replacement: "--"
16451
+ }
16452
+ ];
16000
16453
  const DEFAULT_OFFENDERS = [
16001
16454
  ...TIER1_OFFENDERS,
16002
16455
  ...BOX_DRAWING_OFFENDERS,
16003
16456
  ...ARROW_OFFENDERS,
16004
16457
  ...OUTPUT_GLYPH_OFFENDERS,
16005
- ...TIER2_OFFENDERS
16458
+ ...TIER2_OFFENDERS,
16459
+ ...INVISIBLE_OFFENDERS,
16460
+ ...SCRIPT_DEPENDENT_OFFENDERS,
16461
+ ...ODD_SPACE_OFFENDERS,
16462
+ ...HYPHEN_OFFENDERS
16006
16463
  ];
16007
16464
  const OFFENDER_CATEGORIES = { math: [
16008
16465
  {
@@ -16054,7 +16511,7 @@ function offenderNameByCodepoint(codepoint) {
16054
16511
  return nameByCodepoint.get(codepoint);
16055
16512
  }
16056
16513
  //#endregion
16057
- //#region src/ascii-check/context-js-ts.ts
16514
+ //#region src/chars-check/context-js-ts.ts
16058
16515
  const EXPRESSION_START_PUNCTUATION = /* @__PURE__ */ new Set([
16059
16516
  "(",
16060
16517
  "[",
@@ -16445,7 +16902,7 @@ const jsxFamily = {
16445
16902
  definition: { classify: (text) => classifyJsTs(text, true) }
16446
16903
  };
16447
16904
  //#endregion
16448
- //#region src/ascii-check/context-kt-java.ts
16905
+ //#region src/chars-check/context-kt-java.ts
16449
16906
  var KtJavaLexer = class {
16450
16907
  text;
16451
16908
  kotlin;
@@ -16601,7 +17058,7 @@ const javaFamily = {
16601
17058
  definition: { classify: (text) => classifyKtJava(text, false) }
16602
17059
  };
16603
17060
  //#endregion
16604
- //#region src/ascii-check/context-markup.ts
17061
+ //#region src/chars-check/context-markup.ts
16605
17062
  const TAG_START = /[A-Za-z!?]/;
16606
17063
  const TAG_NAME_CHAR = /[A-Za-z0-9:_-]/;
16607
17064
  var MarkupLexer = class {
@@ -16762,7 +17219,7 @@ var MarkupLexer = class {
16762
17219
  this.i += 1;
16763
17220
  }
16764
17221
  stepIsland() {
16765
- this.flushTo(this.i, "code");
17222
+ this.flushTo(this.i, "unclassified");
16766
17223
  if (this.char() === "<" && this.char(1) === "/" && this.matchesIslandClose()) return this.closeIsland();
16767
17224
  this.i += 1;
16768
17225
  }
@@ -16772,6 +17229,7 @@ var MarkupLexer = class {
16772
17229
  return this.text.slice(start, end).toLowerCase() === this.islandCloseTag;
16773
17230
  }
16774
17231
  closeIsland() {
17232
+ this.flushTo(this.i, "code");
16775
17233
  let j = this.i;
16776
17234
  while (j < this.n && this.text[j] !== ">") j += 1;
16777
17235
  this.i = Math.min(j + 1, this.n);
@@ -16790,7 +17248,7 @@ const htmlFamily = {
16790
17248
  definition: { classify: (text) => classifyMarkup(text, true) }
16791
17249
  };
16792
17250
  //#endregion
16793
- //#region src/ascii-check/context-structured.ts
17251
+ //#region src/chars-check/context-structured.ts
16794
17252
  function triesJsonParse(text) {
16795
17253
  try {
16796
17254
  JSON.parse(text);
@@ -16936,7 +17394,7 @@ function classifyAsProse(text) {
16936
17394
  }];
16937
17395
  }
16938
17396
  //#endregion
16939
- //#region src/ascii-check/families.ts
17397
+ //#region src/chars-check/families.ts
16940
17398
  const FAMILIES = [
16941
17399
  jsTsFamily,
16942
17400
  jsxFamily,
@@ -16974,7 +17432,7 @@ const FAMILIES = [
16974
17432
  }
16975
17433
  ];
16976
17434
  //#endregion
16977
- //#region src/ascii-check/context.ts
17435
+ //#region src/chars-check/context.ts
16978
17436
  const proseSpan = (text) => [{
16979
17437
  start: 0,
16980
17438
  end: text.length,
@@ -17011,6 +17469,12 @@ function contextAt(spans, offset) {
17011
17469
  function spanCovering(spans, offset) {
17012
17470
  for (const span of spans) if (offset >= span.start && offset < span.end) return span;
17013
17471
  }
17472
+ //#endregion
17473
+ //#region src/chars-check/scan.ts
17474
+ const IGNORE_LINE_MARKERS = ["chars-ignore-line", "ascii-ignore-line"];
17475
+ function hasIgnoreMarker(line) {
17476
+ return IGNORE_LINE_MARKERS.some((marker) => line.includes(marker));
17477
+ }
17014
17478
  const BINARY_SNIFF_BYTES = 8e3;
17015
17479
  function looksBinary(buf) {
17016
17480
  const end = Math.min(buf.length, BINARY_SNIFF_BYTES);
@@ -17024,14 +17488,14 @@ function scanWithOffsets(rel, text, offenders) {
17024
17488
  let cumulative = 0;
17025
17489
  for (let lineIdx = 0; lineIdx < lines.length; lineIdx += 1) {
17026
17490
  const line = lines[lineIdx] ?? "";
17027
- if (!line.includes("ascii-ignore-line")) for (let col = 0; col < line.length; col += 1) {
17028
- const offender = byChar.get(line[col] ?? "");
17029
- if (offender) out.push({
17030
- offset: cumulative + col,
17491
+ if (!hasIgnoreMarker(line)) for (const { column, offender } of lineOffenders(line, byChar)) {
17492
+ if (cumulative + column === 0 && offender.codepoint === "U+FEFF") continue;
17493
+ out.push({
17494
+ offset: cumulative + column,
17031
17495
  hit: {
17032
17496
  path: rel,
17033
17497
  line: lineIdx + 1,
17034
- column: col + 1,
17498
+ column: column + 1,
17035
17499
  offender
17036
17500
  }
17037
17501
  });
@@ -17040,6 +17504,20 @@ function scanWithOffsets(rel, text, offenders) {
17040
17504
  }
17041
17505
  return out;
17042
17506
  }
17507
+ function lineOffenders(line, byChar) {
17508
+ const found = [];
17509
+ let column = 0;
17510
+ while (column < line.length) {
17511
+ const char = String.fromCodePoint(line.codePointAt(column) ?? 0);
17512
+ const offender = byChar.get(char);
17513
+ if (offender !== void 0) found.push({
17514
+ column,
17515
+ offender
17516
+ });
17517
+ column += char.length;
17518
+ }
17519
+ return found;
17520
+ }
17043
17521
  function createRealScanExecutor(rootDir) {
17044
17522
  return {
17045
17523
  listTrackedFiles() {
@@ -17081,7 +17559,7 @@ function createRealScanExecutor(rootDir) {
17081
17559
  };
17082
17560
  }
17083
17561
  //#endregion
17084
- //#region src/ascii-check/placement.ts
17562
+ //#region src/chars-check/placement.ts
17085
17563
  const ESCAPE_TOKEN_RE = /\\u\{[0-9a-fA-F]{1,6}\}|\\u[0-9a-fA-F]{4}|\\x[0-9a-fA-F]{2}/g;
17086
17564
  const ENTITY_TOKEN_RE = /&#(?:[xX][0-9a-fA-F]{1,6}|[0-9]{1,7});/g;
17087
17565
  const MARKUP_CONTEXTS = /* @__PURE__ */ new Set([
@@ -17146,7 +17624,7 @@ function scanPlacement(rel, text, treatAsProse = []) {
17146
17624
  let cumulative = 0;
17147
17625
  for (let lineIdx = 0; lineIdx < lines.length; lineIdx += 1) {
17148
17626
  const line = lines[lineIdx] ?? "";
17149
- if (!line.includes("ascii-ignore-line")) {
17627
+ if (!hasIgnoreMarker(line)) {
17150
17628
  const site = {
17151
17629
  rel,
17152
17630
  spans,
@@ -17241,9 +17719,18 @@ function finishFinding(site, parts, rule) {
17241
17719
  };
17242
17720
  }
17243
17721
  //#endregion
17244
- //#region src/ascii-check/rewrite.ts
17722
+ //#region src/chars-check/rewrite.ts
17245
17723
  function unicodeEscape(offender) {
17246
- return `\\u${offender.codepoint.slice(2)}`;
17724
+ let out = "";
17725
+ for (let i = 0; i < offender.char.length; i += 1) {
17726
+ const unit = offender.char.charCodeAt(i).toString(16).toUpperCase().padStart(4, "0");
17727
+ out += `\\u${unit}`;
17728
+ }
17729
+ return out;
17730
+ }
17731
+ function kotlinSplice(offender) {
17732
+ const quote = offender.char.length > 1 ? "\"" : "'";
17733
+ return `\${${quote}${unicodeEscape(offender)}${quote}}`;
17247
17734
  }
17248
17735
  function xmlEntity(offender) {
17249
17736
  return `&#x${offender.codepoint.slice(2)};`;
@@ -17266,14 +17753,23 @@ function decideAction(offender, context) {
17266
17753
  case "prose":
17267
17754
  case "comment":
17268
17755
  case "xml-comment":
17269
- case "cdata": return {
17270
- action: "flatten",
17271
- replacement: offender.replacement
17272
- };
17756
+ case "cdata":
17757
+ if (offender.group === "invisible") return {
17758
+ action: "flag",
17759
+ reason: "an invisible or deceptive character; removing it can change what the line means"
17760
+ };
17761
+ if (offender.replacement === void 0) return {
17762
+ action: "flag",
17763
+ reason: "no ASCII replacement is safe to pick unattended; edit it by hand"
17764
+ };
17765
+ return {
17766
+ action: "flatten",
17767
+ replacement: offender.replacement
17768
+ };
17273
17769
  case "raw-string": return {
17274
17770
  action: "flag",
17275
17771
  reason: "Kotlin raw strings do not process \\u escapes",
17276
- suggestion: `\${'${unicodeEscape(offender)}'}`
17772
+ suggestion: kotlinSplice(offender)
17277
17773
  };
17278
17774
  case "text-block": return {
17279
17775
  action: "flag",
@@ -17283,13 +17779,18 @@ function decideAction(offender, context) {
17283
17779
  action: "flag",
17284
17780
  reason: "a tagged template's tag may read .raw, where an escape is not the same value"
17285
17781
  };
17286
- case "code": return {
17287
- action: "flag",
17288
- reason: "non-ASCII character in a code position (e.g. an identifier); not this tool's job"
17289
- };
17782
+ case "code":
17783
+ if (offender.group === "script") return {
17784
+ action: "flag",
17785
+ reason: "a character some scripts need, in a code position; not this tool's job"
17786
+ };
17787
+ return {
17788
+ action: "flag",
17789
+ reason: "non-ASCII character in a code position (e.g. an identifier); not this tool's job"
17790
+ };
17290
17791
  case "unclassified": return {
17291
17792
  action: "flag",
17292
- reason: "file could not be classified with confidence; hand-edit required"
17793
+ reason: "this position was not classified with confidence; hand-edit required"
17293
17794
  };
17294
17795
  default: throw new UnexpectedError(`unhandled ascii context: ${String(context)}`);
17295
17796
  }
@@ -17304,7 +17805,7 @@ function violatesXmlCommentInvariant(commentBody) {
17304
17805
  return commentBody.includes("--") || commentBody.endsWith("-");
17305
17806
  }
17306
17807
  //#endregion
17307
- //#region src/ascii-check/fix.ts
17808
+ //#region src/chars-check/fix.ts
17308
17809
  function precedingBackslashRun(text, offset) {
17309
17810
  let count = 0;
17310
17811
  for (let i = offset - 1; i >= 0 && text[i] === "\\"; i -= 1) count += 1;
@@ -17335,7 +17836,7 @@ function splice(text, planned) {
17335
17836
  for (const p of planned) {
17336
17837
  if (!p.applied || p.replacement === void 0) continue;
17337
17838
  result += text.slice(cursor, p.offset) + p.replacement;
17338
- cursor = p.offset + 1;
17839
+ cursor = p.offset + p.hit.offender.char.length;
17339
17840
  }
17340
17841
  result += text.slice(cursor);
17341
17842
  return result;
@@ -17359,8 +17860,10 @@ var XmlCommentGuard = class {
17359
17860
  if (!span || span.context !== "xml-comment") return decision;
17360
17861
  const prior = this.committed.get(span) ?? [];
17361
17862
  const before = spliceWithin(text, span.start, span.end, prior);
17863
+ const length = String.fromCodePoint(text.codePointAt(offset) ?? 0).length;
17362
17864
  const tentative = [...prior, {
17363
17865
  offset,
17866
+ length,
17364
17867
  replacement: decision.replacement
17365
17868
  }].toSorted((a, b) => a.offset - b.offset);
17366
17869
  if (violatesXmlCommentInvariant(spliceWithin(text, span.start, span.end, tentative)) && !violatesXmlCommentInvariant(before)) {
@@ -17380,7 +17883,7 @@ function spliceWithin(text, start, end, replacements) {
17380
17883
  let cursor = start;
17381
17884
  for (const r of replacements) {
17382
17885
  result += text.slice(cursor, r.offset) + r.replacement;
17383
- cursor = r.offset + 1;
17886
+ cursor = r.offset + r.length;
17384
17887
  }
17385
17888
  return result + text.slice(cursor, end);
17386
17889
  }
@@ -17398,6 +17901,7 @@ function fixText(text, options) {
17398
17901
  const pending = [];
17399
17902
  for (const { offset, hit } of rawHits) {
17400
17903
  const context = contextAt(spans, offset);
17904
+ if (hit.offender.group === "script" && context !== "code") continue;
17401
17905
  const preGuardDecision = decideAction(hit.offender, context);
17402
17906
  const willApply = isFixedNow(hit.offender, preGuardDecision, fixAmbiguous);
17403
17907
  const decision = guardAgainstDoubleEscape(text, offset, xmlCommentGuard.check(spans, text, offset, preGuardDecision, willApply));
@@ -17419,7 +17923,7 @@ function fixText(text, options) {
17419
17923
  } : p.decision;
17420
17924
  const applied = isFixedNow(p.hit.offender, decision, fixAmbiguous);
17421
17925
  const newOffset = p.offset + delta;
17422
- if (applied && decision.replacement !== void 0) delta += decision.replacement.length - 1;
17926
+ if (applied && decision.replacement !== void 0) delta += decision.replacement.length - p.hit.offender.char.length;
17423
17927
  planned.push({
17424
17928
  offset: p.offset,
17425
17929
  newOffset,
@@ -17456,6 +17960,34 @@ function fixText(text, options) {
17456
17960
  decisions: planned.map(toDecision)
17457
17961
  };
17458
17962
  }
17963
+ const strictUtf8 = new TextDecoder("utf-8", { fatal: true });
17964
+ function isValidUtf8(buf) {
17965
+ try {
17966
+ strictUtf8.decode(buf);
17967
+ return true;
17968
+ } catch {
17969
+ return false;
17970
+ }
17971
+ }
17972
+ const INVALID_UTF8_OFFENDER = {
17973
+ codepoint: "U+FFFD",
17974
+ name: "REPLACEMENT CHARACTER (invalid UTF-8 byte)",
17975
+ tier: 1,
17976
+ char: "�"
17977
+ };
17978
+ function refuseLossyDecode(result) {
17979
+ return {
17980
+ text: result.text,
17981
+ changed: false,
17982
+ decisions: result.decisions.map((d) => ({
17983
+ ...d,
17984
+ action: "flag",
17985
+ applied: false,
17986
+ replacement: void 0,
17987
+ reason: "the file is not valid UTF-8, so no rewrite of it is safe; fix its encoding first"
17988
+ }))
17989
+ };
17990
+ }
17459
17991
  function fixFiles(executor, options) {
17460
17992
  const isExcluded = picomatch(options.exclude ?? []);
17461
17993
  const results = [];
@@ -17466,12 +17998,14 @@ function fixFiles(executor, options) {
17466
17998
  if (!buf || looksBinary(buf)) continue;
17467
17999
  const original = buf.toString("utf-8");
17468
18000
  const placement = scanPlacement(rel, original, options.treatAsProse);
17469
- const { text, changed, decisions } = fixText(original, {
17470
- offenders: options.offenders,
18001
+ const validUtf8 = isValidUtf8(buf);
18002
+ const fixed = fixText(original, {
18003
+ offenders: validUtf8 ? options.offenders : [...options.offenders, INVALID_UTF8_OFFENDER],
17471
18004
  path: rel,
17472
18005
  fixAmbiguous: options.fixAmbiguous,
17473
18006
  treatAsProse: options.treatAsProse
17474
18007
  });
18008
+ const { text, changed, decisions } = validUtf8 ? fixed : refuseLossyDecode(fixed);
17475
18009
  if (changed && options.dryRun !== true) executor.writeFileBuffer(rel, Buffer.from(text, "utf-8"));
17476
18010
  const remainingHits = decisions.filter((d) => !d.applied).map((d) => d.hit);
17477
18011
  results.push({
@@ -17523,22 +18057,29 @@ function isManual(provenance) {
17523
18057
  function composeFindingFile(entry, provenance) {
17524
18058
  const sourceLabel = provenance.sourceKind === "pr" ? "PR" : "issue";
17525
18059
  const lines = [
17526
- `# ${toAscii(entry.title)}`,
18060
+ `# ${toGateText(entry.title)}`,
17527
18061
  "",
17528
18062
  `- Class: ${entry.class}`,
17529
18063
  `- Phase: ${provenance.phase}`
17530
18064
  ];
17531
18065
  if (isManual(provenance)) lines.push(`- Source ${sourceLabel}: #${String(provenance.sourceIssueNumber)}`);
17532
18066
  else lines.push(`- Source ${sourceLabel}: #${String(provenance.sourceIssueNumber)} (${provenance.sourceIssueUrl})`, `- Pull request: ${provenance.prUrl ?? "none -- the run produced no commits"}`, `- Transcript directory: ${provenance.transcriptDir}`);
17533
- lines.push("", toAscii(entry.body));
18067
+ lines.push("", toGateText(entry.body));
17534
18068
  return lines.join("\n") + "\n";
17535
18069
  }
17536
- function toAscii(text) {
17537
- return fixText(text, {
18070
+ function toGateText(text) {
18071
+ const flattened = fixText(text, {
17538
18072
  offenders: DEFAULT_OFFENDERS,
17539
18073
  path: "finding.md",
17540
18074
  fixAmbiguous: true
17541
18075
  }).text;
18076
+ return nameInvisibleCharacters(flattened);
18077
+ }
18078
+ function nameInvisibleCharacters(text) {
18079
+ const labels = new Map(DEFAULT_OFFENDERS.filter((o) => o.group === "invisible").map((o) => [o.char, o.codepoint]));
18080
+ let out = "";
18081
+ for (const char of Array.from(text)) out += labels.get(char) ?? char;
18082
+ return out;
17542
18083
  }
17543
18084
  const TITLE_RE = /^#\s+(.+)$/m;
17544
18085
  const CLASS_RE = /^-\s*Class:\s*(.+)$/m;
@@ -23857,32 +24398,40 @@ const healthStatusCommand = defineCommand({
23857
24398
  }
23858
24399
  });
23859
24400
  //#endregion
23860
- //#region src/ascii-check/config.ts
23861
- const ToolingShallowSchema = z.object({ ascii: tolerantBlock(AsciiConfigSchema).optional() });
23862
- const DEFAULT_ASCII_EXCLUDE = [
24401
+ //#region src/chars-check/config.ts
24402
+ const ToolingShallowSchema = z.object({
24403
+ chars: tolerantBlock(CharsConfigSchema).optional(),
24404
+ ascii: tolerantBlock(CharsConfigSchema).optional()
24405
+ });
24406
+ const DEFAULT_CHARS_EXCLUDE = [
23863
24407
  "docs/**/retired/**",
23864
24408
  "**/CHANGELOG.md",
23865
24409
  ".claude/hooks/bst-hook.mjs",
23866
24410
  ".claude/hooks/bst-hook-guide.md"
23867
24411
  ];
23868
- function parseAsciiConfig(raw, displayPath) {
23869
- const ascii = parseToolingConfigSlice(raw, ToolingShallowSchema, {
23870
- block: "ascii",
24412
+ function parseCharsConfig(raw, displayPath) {
24413
+ const blocks = parseToolingConfigSlice(raw, ToolingShallowSchema, {
24414
+ block: "chars",
23871
24415
  path: displayPath
23872
- })?.ascii;
24416
+ });
24417
+ const chars = blocks?.chars ?? blocks?.ascii;
23873
24418
  return {
23874
- exclude: [.../* @__PURE__ */ new Set([...DEFAULT_ASCII_EXCLUDE, ...ascii?.exclude ?? []])],
23875
- deny: ascii?.deny ?? [],
23876
- allow: ascii?.allow ?? [],
23877
- treatAsProse: ascii?.treatAsProse ?? []
24419
+ exclude: [.../* @__PURE__ */ new Set([...DEFAULT_CHARS_EXCLUDE, ...chars?.exclude ?? []])],
24420
+ deny: chars?.deny ?? [],
24421
+ allow: chars?.allow ?? [],
24422
+ treatAsProse: chars?.treatAsProse ?? []
23878
24423
  };
23879
24424
  }
23880
- function resolveAsciiConfig(targetDir) {
24425
+ function resolveCharsConfig(targetDir) {
23881
24426
  const full = path.join(targetDir, ".tooling.json");
23882
- return parseAsciiConfig(existsSync(full) ? readFileSync(full, "utf-8") : void 0, path.relative(process.cwd(), full) || full);
24427
+ return parseCharsConfig(existsSync(full) ? readFileSync(full, "utf-8") : void 0, path.relative(process.cwd(), full) || full);
23883
24428
  }
23884
24429
  //#endregion
23885
- //#region src/ascii-check/report.ts
24430
+ //#region src/chars-check/report.ts
24431
+ function displayChar(offender) {
24432
+ if (offender.group === void 0) return offender.char;
24433
+ return `<${offender.codepoint}>`;
24434
+ }
23886
24435
  function formatHitLine(decision) {
23887
24436
  const { hit, context, action } = decision;
23888
24437
  const { path, line, column, offender } = hit;
@@ -23890,18 +24439,18 @@ function formatHitLine(decision) {
23890
24439
  if (action === "flag") {
23891
24440
  const reason = decision.reason ?? "no safe mechanical rewrite for this context";
23892
24441
  const suggestionPart = decision.suggestion !== void 0 ? `; suggested: ${decision.suggestion}` : "";
23893
- return `${head} '${offender.char}' [${context}] flagged: ${reason}${suggestionPart}`;
24442
+ return `${head} '${displayChar(offender)}' [${context}] flagged: ${reason}${suggestionPart}`;
23894
24443
  }
23895
- const replacement = decision.replacement ?? offender.replacement;
24444
+ const replacement = decision.replacement ?? offender.replacement ?? "";
23896
24445
  const ambiguous = offender.tier === 2 && action === "flatten" ? " (ambiguous)" : "";
23897
- return `${head} '${offender.char}' -> '${replacement}' [${context}/${action}]${ambiguous}`;
24446
+ return `${head} '${displayChar(offender)}' -> '${replacement}' [${context}/${action}]${ambiguous}`;
23898
24447
  }
23899
24448
  function formatPlacementLine(finding) {
23900
24449
  const { path, line, column, codepoint, name, found, context, expected } = finding;
23901
24450
  return `${`${path}:${String(line)}:${String(column)} ${codepoint}${name !== void 0 ? ` ${name}` : ""}`} '${found}' in ${context} is not decoded there; write '${expected}'`;
23902
24451
  }
23903
24452
  function formatReport$1(decisions, placement = []) {
23904
- if (decisions.length === 0 && placement.length === 0) return "ascii:check: no offenders found.";
24453
+ if (decisions.length === 0 && placement.length === 0) return "chars:check: no offenders found.";
23905
24454
  return [...decisions.map(formatHitLine), ...placement.map(formatPlacementLine)].join("\n");
23906
24455
  }
23907
24456
  function toJsonReport(decisions, placement = []) {
@@ -23931,7 +24480,7 @@ function toJsonReport(decisions, placement = []) {
23931
24480
  };
23932
24481
  }
23933
24482
  //#endregion
23934
- //#region src/commands/ascii-check.ts
24483
+ //#region src/commands/chars-check.ts
23935
24484
  function reportDecisions(decisions, placement = []) {
23936
24485
  return {
23937
24486
  payload: toJsonReport(decisions, placement),
@@ -23939,7 +24488,7 @@ function reportDecisions(decisions, placement = []) {
23939
24488
  exitCode: decisions.length + placement.length === 0 ? 0 : 1
23940
24489
  };
23941
24490
  }
23942
- function runAsciiCheck(executor, offenders, exclude, opts, treatAsProse = []) {
24491
+ function runCharsCheck(executor, offenders, exclude, opts, treatAsProse = []) {
23943
24492
  const writes = opts.fix === true && opts.dryRun !== true;
23944
24493
  const results = fixFiles(executor, {
23945
24494
  offenders,
@@ -23955,7 +24504,7 @@ function runAsciiCheck(executor, offenders, exclude, opts, treatAsProse = []) {
23955
24504
  if (opts.dryRun !== true) return reportDecisions(remaining, placement);
23956
24505
  const planned = decisions.filter((d) => d.applied);
23957
24506
  const exitCode = planned.length === 0 && remaining.length === 0 && placement.length === 0 ? 0 : 1;
23958
- const lines = [planned.length === 0 ? "ascii:check --fix --dry-run: no fixable offenders found." : `ascii:check --fix --dry-run: would fix ${String(planned.length)} offender(s):`, ...planned.map(formatHitLine)];
24507
+ const lines = [planned.length === 0 ? "chars:check --fix --dry-run: no fixable offenders found." : `chars:check --fix --dry-run: would fix ${String(planned.length)} offender(s):`, ...planned.map(formatHitLine)];
23959
24508
  if (remaining.length > 0 || placement.length > 0) lines.push("remaining (not fixed):", ...remaining.map(formatHitLine), ...placement.map(formatPlacementLine));
23960
24509
  return {
23961
24510
  payload: {
@@ -23969,54 +24518,70 @@ function runAsciiCheck(executor, offenders, exclude, opts, treatAsProse = []) {
23969
24518
  exitCode
23970
24519
  };
23971
24520
  }
23972
- const asciiCheckCommand = defineCommand({
24521
+ const CHARS_CHECK_ARGS = {
24522
+ dir: {
24523
+ type: "positional",
24524
+ valueHint: "dir",
24525
+ description: "Project directory (default: current directory)",
24526
+ required: false
24527
+ },
24528
+ fix: {
24529
+ type: "boolean",
24530
+ description: "Rewrite tier-1 offenders in place"
24531
+ },
24532
+ "fix-ambiguous": {
24533
+ type: "boolean",
24534
+ description: "With --fix, also rewrite tier-2 offenders (e.g. em dash -> --)"
24535
+ },
24536
+ "dry-run": {
24537
+ type: "boolean",
24538
+ description: "With --fix, print the planned edits without writing"
24539
+ },
24540
+ json: {
24541
+ type: "boolean",
24542
+ description: "Emit one JSON document: status (clean | violations), count, hits ([{path, line, column, codepoint, name, char, replacement?, tier (1 | 2), ambiguous, context, action (flatten | escape | flag), reason?, suggestion?}]), placementCount, placement ([{path, line, column, rule (js-escape-in-markup | entity-in-js-string), context, found, codepoint, expected, name?}]). context is one of prose | comment | xml-comment | string | template | tagged-template | raw-string | text-block | regex | jsx-text | jsx-attribute | markup-text | markup-attribute | cdata | code | unclassified. With --fix --dry-run the document is instead dryRun (true), plannedFixes (hits), remaining (hits), placementCount, placement."
24543
+ },
24544
+ verbose: {
24545
+ type: "boolean",
24546
+ description: "Emit detailed debug logging"
24547
+ }
24548
+ };
24549
+ function runCharsCheckCli(args) {
24550
+ const out = createCliOutput(args.json === true);
24551
+ const targetDir = path.resolve(args.dir ?? ".");
24552
+ const vc = { verbose: args.verbose === true || isEnvVerbose() };
24553
+ const config = resolveCharsConfig(targetDir);
24554
+ const offenders = resolveOffenders(config);
24555
+ debug(vc, `chars:check: ${String(offenders.length)} offender(s) in the effective deny-list`);
24556
+ const { payload, text, exitCode } = runCharsCheck(createRealFixExecutor(targetDir), offenders, config.exclude, {
24557
+ fix: args.fix === true,
24558
+ fixAmbiguous: args["fix-ambiguous"] === true,
24559
+ dryRun: args["dry-run"] === true
24560
+ }, config.treatAsProse);
24561
+ out.json(payload);
24562
+ if (text !== "") out.text(text);
24563
+ process.exitCode = exitCode;
24564
+ }
24565
+ const charsCheckCommand = defineCommand({
23973
24566
  meta: {
23974
- name: "ascii:check",
23975
- description: "Scan tracked text files for a curated non-ASCII deny-list (curly quotes, ellipsis, dashes, box-drawing, arrows) and report each hit with a suggested ASCII replacement. It also reports an escape (such as \\uXXXX or an entity) written where its context does not decode it; that report always exits 1 and is never auto-fixed. Opt-in via .tooling.json#ascii.enabled for checks:run; always runs directly. To skip files, add globs to .tooling.json#ascii.exclude. To skip one line, put the text ascii-ignore-line on it, in any comment syntax."
24567
+ name: "chars:check",
24568
+ description: "Scan tracked text files for the characters this gate denies: LLM-tell punctuation (curly quotes, ellipsis, en and em dashes, arrows, box-drawing, status glyphs) and invisible or deceptive characters (bidi controls, zero-width and tag characters, odd spaces). Letters in any language pass -- an accented e, Persian, Hindi, an emoji sequence and CJK text are all fine. Each hit is reported with its context-correct fix: an escape in a string, an entity in markup, an ASCII lookalike in prose, or a flag naming why no mechanical rewrite is safe. Skipped on purpose: excluded globs (the built-in docs/**/retired/**, **/CHANGELOG.md and the generated bst-hook files, plus .tooling.json#chars.exclude), files .gitattributes marks binary, and files with a null byte in their first 8000 bytes. It also reports an escape (such as \\uXXXX or an entity) written where its context does not decode it; that report always exits 1 and is never auto-fixed. It runs in checks:run unless .tooling.json#chars.enabled is false, and always runs when invoked directly. To keep one character, add its code point to .tooling.json#chars.allow. To skip files, add globs to .tooling.json#chars.exclude. To skip one line, put the text chars-ignore-line on it, in any comment syntax."
23976
24569
  },
23977
- args: {
23978
- dir: {
23979
- type: "positional",
23980
- valueHint: "dir",
23981
- description: "Project directory (default: current directory)",
23982
- required: false
23983
- },
23984
- fix: {
23985
- type: "boolean",
23986
- description: "Rewrite tier-1 offenders in place"
23987
- },
23988
- "fix-ambiguous": {
23989
- type: "boolean",
23990
- description: "With --fix, also rewrite tier-2 offenders (e.g. em dash -> --)"
23991
- },
23992
- "dry-run": {
23993
- type: "boolean",
23994
- description: "With --fix, print the planned edits without writing"
23995
- },
23996
- json: {
23997
- type: "boolean",
23998
- description: "Emit one JSON document: status (clean | violations), count, hits ([{path, line, column, codepoint, name, char, replacement?, tier (1 | 2), ambiguous, context, action (flatten | escape | flag), reason?, suggestion?}]), placementCount, placement ([{path, line, column, rule (js-escape-in-markup | entity-in-js-string), context, found, codepoint, expected, name?}]). context is one of prose | comment | xml-comment | string | template | tagged-template | raw-string | text-block | regex | jsx-text | jsx-attribute | markup-text | markup-attribute | cdata | code | unclassified. With --fix --dry-run the document is instead dryRun (true), plannedFixes (hits), remaining (hits), placementCount, placement."
23999
- },
24000
- verbose: {
24001
- type: "boolean",
24002
- description: "Emit detailed debug logging"
24003
- }
24570
+ args: CHARS_CHECK_ARGS,
24571
+ run({ args }) {
24572
+ runCharsCheckCli(args);
24573
+ }
24574
+ });
24575
+ const asciiCheckAliasCommand = defineCommand({
24576
+ meta: {
24577
+ name: "ascii:check",
24578
+ description: "Deprecated alias of chars:check",
24579
+ hidden: true
24004
24580
  },
24581
+ args: CHARS_CHECK_ARGS,
24005
24582
  run({ args }) {
24006
- const out = createCliOutput(args.json === true);
24007
- const targetDir = path.resolve(args.dir ?? ".");
24008
- const vc = { verbose: args.verbose === true || isEnvVerbose() };
24009
- const config = resolveAsciiConfig(targetDir);
24010
- const offenders = resolveOffenders(config);
24011
- debug(vc, `ascii:check: ${String(offenders.length)} offender(s) in the effective deny-list`);
24012
- const { payload, text, exitCode } = runAsciiCheck(createRealFixExecutor(targetDir), offenders, config.exclude, {
24013
- fix: args.fix === true,
24014
- fixAmbiguous: args["fix-ambiguous"] === true,
24015
- dryRun: args["dry-run"] === true
24016
- }, config.treatAsProse);
24017
- out.json(payload);
24018
- if (text !== "") out.text(text);
24019
- process.exitCode = exitCode;
24583
+ createCliOutput(args.json === true).text("ascii:check is now chars:check; the old name works until 2026-12-15.");
24584
+ runCharsCheckCli(args);
24020
24585
  }
24021
24586
  });
24022
24587
  z.object({
@@ -27326,7 +27891,7 @@ async function runCli(config) {
27326
27891
  //#region src/cli-main.ts
27327
27892
  setLogLevel(resolveLogLevel({}));
27328
27893
  setForgejoLogger(log$1);
27329
- const displayVersion = "0.92.1";
27894
+ const displayVersion = "0.93.1";
27330
27895
  const bstSubCommands = {
27331
27896
  "repo:sync": syncCommand,
27332
27897
  "checks:run": runChecksCommand,
@@ -27359,7 +27924,8 @@ const bstSubCommands = {
27359
27924
  "health:summary": healthSummaryCommand,
27360
27925
  "health:record": healthRecordCommand,
27361
27926
  "health:status": healthStatusCommand,
27362
- "ascii:check": asciiCheckCommand,
27927
+ "chars:check": charsCheckCommand,
27928
+ "ascii:check": asciiCheckAliasCommand,
27363
27929
  "branch:prune": branchPruneCommand,
27364
27930
  "docs:check": docsCheckCommand,
27365
27931
  "docs:delink": docsDelinkCommand,