@bensandee/tooling 0.89.1 → 0.91.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.mjs CHANGED
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { accessSync, appendFileSync, closeSync, constants, copyFileSync, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
3
- import * as path$1 from "node:path";
4
- import path from "node:path";
3
+ import path, * as path$1 from "node:path";
5
4
  import { parseArgs, parseEnv } from "node:util";
6
5
  import { FatalError, TransientError, UnexpectedError, describeError, describeError as describeError$1, redact, redact as redact$1 } from "@bensandee/common";
7
6
  import { defineCommand, renderUsage, runCommand, runMain } from "citty";
@@ -176,7 +175,7 @@ function loadEnvFileForBin(rawArgs, bin, io = processIo) {
176
175
  //#region src/env-file-preload.ts
177
176
  const envFileLoad = loadEnvFileForBin(process.argv.slice(2), {
178
177
  name: "bst",
179
- version: "0.89.1"
178
+ version: "0.91.0"
180
179
  });
181
180
  //#endregion
182
181
  //#region ../cli-kit/src/logger.ts
@@ -784,7 +783,7 @@ const LintConfigSchema = z.strictObject({ strictTypeAware: z.boolean().optional(
784
783
  const ChecksConfigSchema = z.strictObject({
785
784
  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." }),
786
785
  skip: z.array(z.string()).optional().meta({ description: "Check steps to skip, unioned with the built-in CI defaults (never replacing them). Supports glob patterns." }),
787
- runOnVersionPr: z.boolean().optional().meta({ description: "Run the CI check job on the changesets version PR (changeset-release/main). Default false: the generated check job is skipped there, because that branch is regenerated on every push to main and main's own run gates the release. Has no effect unless releaseStrategy is changesets." })
786
+ runOnVersionPr: z.boolean().optional().meta({ description: "Run the CI check job on the changesets version PR (changeset-release/<defaultBranch>). Default false: the generated check job is skipped there, because that branch is regenerated on every push to the default branch and that branch's own run gates the release. Has no effect unless releaseStrategy is changesets." })
788
787
  });
789
788
  const CoverageConfigSchema = z.strictObject({
790
789
  history: z.enum(["orphan-branch", "none"]).optional().meta({ description: "How coverage history is persisted. 'orphan-branch' (default) appends to a dedicated branch; 'none' disables CI history but keeps the local ratchet." }),
@@ -828,7 +827,7 @@ const DockerImageOverrideSchema = z.strictObject({
828
827
  buildArgs: z.record(z.string(), z.string()).optional().meta({ description: "Build args passed to `docker build` for this package. Values support ${VAR} and ${VAR:-default} expansion against process.env." }),
829
828
  imageName: z.string().min(1).optional().meta({ description: "Image name used for docker:build tags, docker:publish refs, and docker:check cross-referencing. Falls back to the derived name: the package.json name for the root package (npm scope stripped), or {root-name}-{package-name} for a monorepo package. A value that already carries a registry host (e.g. 'ghcr.io/me/app') is used as written by docker:publish, with no registryNamespace prefix added, and must name the registry this run logs in to -- one naming any other host refuses before login." }),
830
829
  version: z.string().optional().meta({ description: "Version used for docker:publish tags; overrides the version resolved from the enclosing package." }),
831
- publish: z.boolean().optional().meta({ description: "The publish decision either way: true keeps an image published -- including one its compose service is declared only in the check overlay, which a later release stops publishing by default -- and false excludes it while publishDocker stays true. Unset means the discovery default, which publishes everything except a check-only image once that rule is enforced." })
830
+ publish: z.boolean().optional().meta({ description: "The publish decision either way: true keeps an image published -- including one whose compose service is declared only in the check overlay, which is not published by default -- and false excludes it while publishDocker stays true. Unset means the discovery default, which publishes everything except a check-only image." })
832
831
  });
833
832
  const DockerMapSchema = z.record(z.string(), DockerImageOverrideSchema);
834
833
  const TolerantDockerMapSchema = z.record(z.string(), tolerantBlock(DockerImageOverrideSchema));
@@ -836,6 +835,16 @@ const TolerantDockerCheckConfigSchema = z.object({
836
835
  ...DockerCheckConfigSchema.shape,
837
836
  healthChecks: z.array(tolerantBlock(DeclarativeHealthCheckSchema)).optional()
838
837
  });
838
+ const DEFAULT_BRANCH_NAME_PATTERN = /^(?!.*\/\/)(?!.*\.\.)(?!.*\/$)(?!.*\.lock$)[A-Za-z0-9][A-Za-z0-9._/-]*$/;
839
+ const DefaultBranchNameSchema = z.string().regex(DEFAULT_BRANCH_NAME_PATTERN, "must be a plain branch name: ASCII letters, digits, '.', '_', '-' and '/', starting with a letter or digit, with no '//', '..', trailing '/', or '.lock' suffix");
840
+ const AgentEnabledSliceSchema = z.looseObject({ agent: z.looseObject({ enabled: z.boolean().optional() }).optional() });
841
+ function agentEnabledIn(raw) {
842
+ try {
843
+ return parseToolingConfigSlice(raw, AgentEnabledSliceSchema, { block: "agent" })?.agent?.enabled === true;
844
+ } catch {
845
+ return false;
846
+ }
847
+ }
839
848
  //#endregion
840
849
  //#region src/utils/stack.ts
841
850
  const IGNORED_DIRS = /* @__PURE__ */ new Set([
@@ -1701,6 +1710,7 @@ const LintSweepConfigSchema = z.strictObject({
1701
1710
  });
1702
1711
  const AgentEgressConfigSchema = z.strictObject({ browseHosts: z.array(z.string().min(1)).optional().meta({ description: "Host patterns this repo's agent containers may fetch from, on top of the hosts every run needs: an exact host (`fastdl.mongodb.org`) or a wildcard over its leftmost label (`*.example.com`). No ports. These join the browse lane, which serves GET and HEAD only." }) });
1703
1712
  const AgentConfigSchema = z.strictObject({
1713
+ enabled: z.boolean().optional().meta({ description: "The repo's explicit consent to the agent (default false). Only `true`, read from the repo's **default branch**, lets the agent work the repo: claim units, run sweeps, recover orphans, merge, act on a chat proposal or start a new spec. A missing key, `false`, a missing `.tooling.json` or a read that fails all mean not enabled, and the answer is re-read within about a minute. Every other `agent.*` key below has no effect without this one; `repo:sync` warns when the block sets one of them and this key is not `true`. Where this is `true`, `repo:sync` also installs `@bensandee/agent-cli`. `bst setup:agent` sets it to true and runs the sync. Provisioning the agent's labels is not consent, and is never gated by this key." }),
1704
1714
  autonomy: z.boolean().optional().meta({ description: "Opt into interior autonomy (default false). When a phase's latest rationale reports readiness \"ready\" with an empty openQuestions[] and the review phase is clean, the agent applies the existing plan -> implement -> review -> park-at-mergeable advance itself instead of parking for a human. Never applies to the design/spec gate or the final merge from the mergeable terminal -- those always wait on a human. Also arms the trunk-red detector: when this repo's default branch combined status settles red, the agent files a tracking issue naming the failing checks and labels it agent/spec, starting the fix through the ordinary spec/plan/implement/review pipeline. It also closes that issue, with a comment, if the default branch recovers on its own before a human acts on it. Also arms the world-changed retrigger sweep: a pull request labelled agent-flags/autopilot that is parked at a -stuck or -failed terminal is re-queued when the fact its park recorded stops being true (the default branch went green, or moved past a conflicted base)." }),
1705
1715
  preRunRebase: z.boolean().optional().meta({ description: "Merge the current default branch into the work branch before the implement (execute) phase's container starts (default true). A clean merge or an already-up-to-date branch runs the container as is; a conflicting merge is aborted, recorded on the run's status comment, and the run proceeds on the un-synced branch -- it never fails the run. Two consecutive conflicting attempts hand the unit to agent/repair before any container runs; a timeout or a failed merge never counts toward that pair. Set false to skip the attempt entirely for this repo: no fetch, no merge, no abort, and the branch is worked exactly as cloned." }),
1706
1716
  backlog: z.boolean().optional().meta({ description: "Whether the agent may self-trigger backlog runs consuming this repo's .agent/findings/ ledger (default: on wherever the ledger exists, which only happens in a repo whose own review loop wrote it). A backlog run opens a pull request that fixes or refutes the pending findings; false disarms it, leaving the ledger to accumulate for a human. A run starts only once at least three findings are pending or one is older than seven days." }),
@@ -1769,7 +1779,7 @@ const ToolingConfigSchema = z.strictObject({
1769
1779
  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`." }),
1770
1780
  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`." }),
1771
1781
  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)." }),
1772
- defaultBranch: z.string().optional().meta({ description: "Repository default branch name. Used by `checks:run` to decide when to enable coverage automatically. Default: detected via git (origin/HEAD), falling back to 'main'." }),
1782
+ 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 '/'." }),
1773
1783
  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." }),
1774
1784
  stack: StackSchema.optional().meta({ description: "Whether the repo has TS/JS source. Detected from source evidence; TypeScript is never inferred from absence." }),
1775
1785
  builder: z.enum([
@@ -1786,6 +1796,7 @@ const ToolingConfigSchema = z.strictObject({
1786
1796
  });
1787
1797
  const TolerantToolingConfigSchema = z.object({
1788
1798
  ...ToolingConfigSchema.shape,
1799
+ defaultBranch: z.string().optional(),
1789
1800
  docker: TolerantDockerMapSchema.optional(),
1790
1801
  dockerCheck: z.union([z.literal(false), TolerantDockerCheckConfigSchema]).optional(),
1791
1802
  coverage: z.union([z.literal(false), tolerantBlock(CoverageConfigSchema)]).optional(),
@@ -1831,10 +1842,16 @@ function loadToolingConfig(targetDir) {
1831
1842
  if (!file) return void 0;
1832
1843
  return parseConfigFile(file, TolerantToolingConfigSchema);
1833
1844
  }
1845
+ function parseToolingConfigStrict(raw, displayPath) {
1846
+ return parseConfigFile({
1847
+ raw,
1848
+ displayPath
1849
+ }, ToolingConfigSchema);
1850
+ }
1834
1851
  function loadToolingConfigStrict(targetDir) {
1835
1852
  const file = readConfigFile(targetDir);
1836
1853
  if (!file) return void 0;
1837
- return parseConfigFile(file, ToolingConfigSchema);
1854
+ return parseToolingConfigStrict(file.raw, file.displayPath);
1838
1855
  }
1839
1856
  const OVERRIDE_KEYS = [
1840
1857
  "structure",
@@ -3375,7 +3392,7 @@ function localTarget(call, entry) {
3375
3392
  }
3376
3393
  //#endregion
3377
3394
  //#region src/release/docker-check-only-phase.ts
3378
- const DOCKER_CHECK_ONLY_PHASE = "warn";
3395
+ const DOCKER_CHECK_ONLY_PHASE = "enforce";
3379
3396
  //#endregion
3380
3397
  //#region src/release/docker.ts
3381
3398
  function defaultPublish(checkOnlyIn, phase) {
@@ -3816,7 +3833,7 @@ function runDockerBuild(executor, config) {
3816
3833
  log$1.info(`Built ${pkg.imageName}:latest`);
3817
3834
  return { packages: [pkg] };
3818
3835
  }
3819
- const packages = detectDockerPackages(executor, config.cwd, repoName, config.checkOnlyPhase ?? "warn");
3836
+ const packages = detectDockerPackages(executor, config.cwd, repoName, config.checkOnlyPhase ?? "enforce");
3820
3837
  if (packages.length === 0) {
3821
3838
  log$1.info("No packages with docker config found");
3822
3839
  return { packages: [] };
@@ -3976,7 +3993,7 @@ function reportCheckOnlyImages(packages, phase) {
3976
3993
  }
3977
3994
  }
3978
3995
  function runDockerPublish(executor, config) {
3979
- const checkOnlyPhase = config.checkOnlyPhase ?? "warn";
3996
+ const checkOnlyPhase = config.checkOnlyPhase ?? "enforce";
3980
3997
  const { packages } = runDockerBuild(executor, {
3981
3998
  cwd: config.cwd,
3982
3999
  packageDir: void 0,
@@ -4439,8 +4456,8 @@ function buildWorkflowYaml(options) {
4439
4456
  }
4440
4457
  //#endregion
4441
4458
  //#region src/generators/mise-pin.ts
4442
- const MISE_VERSION = "2026.10.3";
4443
- const MISE_LINUX_X64_SHA256 = "8d48bc510b7d844fad0bc7156c0855c2a1e7230c51e498a7f6b63b021f8b57c5";
4459
+ const MISE_VERSION = "2026.10.8";
4460
+ const MISE_LINUX_X64_SHA256 = "c72477ae6c3a8e7d817a322f43da70e4643e75ee5c21265f1c19c894273b7428";
4444
4461
  const SETUP_MISE_ACTION_URL = "https://code.orangebikelabs.com/bensandee/common/actions/setup-mise";
4445
4462
  const SETUP_MISE_ACTION_TAG = "setup-mise@latest";
4446
4463
  const SETUP_MISE_ACTION_SHA = "ebe622ef0d24c7efec4a5f6093ddb18500900e24";
@@ -4449,13 +4466,22 @@ const SETUP_MISE_ACTION_SHA = "ebe622ef0d24c7efec4a5f6093ddb18500900e24";
4449
4466
  function actionsExpr(expr) {
4450
4467
  return `\${{ ${expr} }}`;
4451
4468
  }
4452
- const MAIN_BRANCH_REF = "refs/heads/main";
4453
- const IS_MAIN_BRANCH = `github.ref == '${MAIN_BRANCH_REF}'`;
4454
- const IS_NOT_MAIN_BRANCH = `github.ref != '${MAIN_BRANCH_REF}'`;
4455
- const IS_MAIN_JOB_ENV = { IS_MAIN: actionsExpr(IS_MAIN_BRANCH) };
4469
+ function branchNameLiteral(name) {
4470
+ if (DEFAULT_BRANCH_NAME_PATTERN.test(name)) return name;
4471
+ return name.replace(/'/g, "''");
4472
+ }
4473
+ function isDefaultBranch(name) {
4474
+ return `github.ref == 'refs/heads/${branchNameLiteral(name)}'`;
4475
+ }
4476
+ function isNotDefaultBranch(name) {
4477
+ return `github.ref != 'refs/heads/${branchNameLiteral(name)}'`;
4478
+ }
4479
+ function defaultBranchJobEnv(name) {
4480
+ return { IS_MAIN: actionsExpr(isDefaultBranch(name)) };
4481
+ }
4456
4482
  const IS_MAIN_ENV_GUARD = "env.IS_MAIN == 'true'";
4457
- function onlyOnDefaultBranch(condition) {
4458
- return `${IS_MAIN_BRANCH} && (${condition})`;
4483
+ function onlyOnDefaultBranch(name, condition) {
4484
+ return `${isDefaultBranch(name)} && (${condition})`;
4459
4485
  }
4460
4486
  function npmrcAuthKey(registryUrl) {
4461
4487
  return `${registryUrl.replace(/^https?:/i, "").replace(/\/+$/, "")}/`;
@@ -4858,6 +4884,38 @@ function diagnoseCheckScriptForwardingAt(targetDir) {
4858
4884
  return diagnoseCheckScriptForwarding(scripts);
4859
4885
  }
4860
4886
  //#endregion
4887
+ //#region src/generators/agent-cli-dep.ts
4888
+ const AGENT_CLI_PACKAGE = "@bensandee/agent-cli";
4889
+ function agentEnabled(ctx) {
4890
+ return agentEnabledIn(ctx.read(".tooling.json"));
4891
+ }
4892
+ function wantsAgentCli(ctx) {
4893
+ if (!agentEnabled(ctx)) return false;
4894
+ if (isWorkspacePackage(ctx, "@bensandee/agent-cli")) return false;
4895
+ return !readUnmanagedDevDepNames(ctx).has(AGENT_CLI_PACKAGE);
4896
+ }
4897
+ function isWorkspacePackage(ctx, packageName) {
4898
+ if (ctx.config.structure !== "monorepo") return false;
4899
+ const packagesDir = path.join(ctx.targetDir, "packages");
4900
+ if (!existsSync(packagesDir)) return false;
4901
+ try {
4902
+ for (const entry of readdirSync(packagesDir, { withFileTypes: true })) {
4903
+ if (!entry.isDirectory()) continue;
4904
+ const pkgJsonPath = path.join(packagesDir, entry.name, "package.json");
4905
+ if (!existsSync(pkgJsonPath)) continue;
4906
+ try {
4907
+ if (parsePackageJson(readFileSync(pkgJsonPath, "utf-8"))?.name === packageName) return true;
4908
+ } catch (_error) {}
4909
+ }
4910
+ } catch (_error) {}
4911
+ return false;
4912
+ }
4913
+ const UnmanagedSliceSchema = z.object({ unmanagedDevDependencies: UnmanagedDevDependenciesSchema.optional() });
4914
+ function readUnmanagedDevDepNames(ctx) {
4915
+ const slice = parseToolingConfigSlice(ctx.read(".tooling.json"), UnmanagedSliceSchema, { block: "unmanagedDevDependencies" });
4916
+ return new Set(slice?.unmanagedDevDependencies ?? []);
4917
+ }
4918
+ //#endregion
4861
4919
  //#region src/generators/package-json.ts
4862
4920
  const NODE_ENGINES_FLOOR = "24.13.0";
4863
4921
  const ENGINES_NODE = `>=${NODE_ENGINES_FLOOR}`;
@@ -4966,18 +5024,18 @@ function perPackageDevDeps(config) {
4966
5024
  "@types/node": "24.19.1",
4967
5025
  "@vitest/coverage-v8": "5.0.3",
4968
5026
  "commit-and-tag-version": "13.2.1",
4969
- "knip": "6.39.0",
4970
- "lefthook": "2.1.16",
4971
- "modern-web-guidance": "0.0.191",
4972
- "oxfmt": "0.71.0",
4973
- "oxlint": "1.86.0",
5027
+ "knip": "6.41.0",
5028
+ "lefthook": "2.2.1",
5029
+ "modern-web-guidance": "0.0.193",
5030
+ "oxfmt": "0.72.0",
5031
+ "oxlint": "1.87.0",
4974
5032
  "oxlint-tsgolint": "7.0.2003",
4975
5033
  "prettier": "3.9.9",
4976
- "release-it": "21.1.0",
5034
+ "release-it": "21.1.1",
4977
5035
  "tsdown": "0.23.0",
4978
5036
  "typescript": "6.0.3",
4979
5037
  "vitest": "5.0.3",
4980
- "pnpm": "11.28.3"
5038
+ "pnpm": "11.28.5"
4981
5039
  }["typescript"];
4982
5040
  deps["@types/node"] = {
4983
5041
  "@changesets/cli": "3.0.3",
@@ -4985,18 +5043,18 @@ function perPackageDevDeps(config) {
4985
5043
  "@types/node": "24.19.1",
4986
5044
  "@vitest/coverage-v8": "5.0.3",
4987
5045
  "commit-and-tag-version": "13.2.1",
4988
- "knip": "6.39.0",
4989
- "lefthook": "2.1.16",
4990
- "modern-web-guidance": "0.0.191",
4991
- "oxfmt": "0.71.0",
4992
- "oxlint": "1.86.0",
5046
+ "knip": "6.41.0",
5047
+ "lefthook": "2.2.1",
5048
+ "modern-web-guidance": "0.0.193",
5049
+ "oxfmt": "0.72.0",
5050
+ "oxlint": "1.87.0",
4993
5051
  "oxlint-tsgolint": "7.0.2003",
4994
5052
  "prettier": "3.9.9",
4995
- "release-it": "21.1.0",
5053
+ "release-it": "21.1.1",
4996
5054
  "tsdown": "0.23.0",
4997
5055
  "typescript": "6.0.3",
4998
5056
  "vitest": "5.0.3",
4999
- "pnpm": "11.28.3"
5057
+ "pnpm": "11.28.5"
5000
5058
  }["@types/node"];
5001
5059
  }
5002
5060
  if (config.builder === "tsdown") deps["tsdown"] = {
@@ -5005,18 +5063,18 @@ function perPackageDevDeps(config) {
5005
5063
  "@types/node": "24.19.1",
5006
5064
  "@vitest/coverage-v8": "5.0.3",
5007
5065
  "commit-and-tag-version": "13.2.1",
5008
- "knip": "6.39.0",
5009
- "lefthook": "2.1.16",
5010
- "modern-web-guidance": "0.0.191",
5011
- "oxfmt": "0.71.0",
5012
- "oxlint": "1.86.0",
5066
+ "knip": "6.41.0",
5067
+ "lefthook": "2.2.1",
5068
+ "modern-web-guidance": "0.0.193",
5069
+ "oxfmt": "0.72.0",
5070
+ "oxlint": "1.87.0",
5013
5071
  "oxlint-tsgolint": "7.0.2003",
5014
5072
  "prettier": "3.9.9",
5015
- "release-it": "21.1.0",
5073
+ "release-it": "21.1.1",
5016
5074
  "tsdown": "0.23.0",
5017
5075
  "typescript": "6.0.3",
5018
5076
  "vitest": "5.0.3",
5019
- "pnpm": "11.28.3"
5077
+ "pnpm": "11.28.5"
5020
5078
  }["tsdown"];
5021
5079
  return deps;
5022
5080
  }
@@ -5028,18 +5086,18 @@ function vitestDevDeps() {
5028
5086
  "@types/node": "24.19.1",
5029
5087
  "@vitest/coverage-v8": "5.0.3",
5030
5088
  "commit-and-tag-version": "13.2.1",
5031
- "knip": "6.39.0",
5032
- "lefthook": "2.1.16",
5033
- "modern-web-guidance": "0.0.191",
5034
- "oxfmt": "0.71.0",
5035
- "oxlint": "1.86.0",
5089
+ "knip": "6.41.0",
5090
+ "lefthook": "2.2.1",
5091
+ "modern-web-guidance": "0.0.193",
5092
+ "oxfmt": "0.72.0",
5093
+ "oxlint": "1.87.0",
5036
5094
  "oxlint-tsgolint": "7.0.2003",
5037
5095
  "prettier": "3.9.9",
5038
- "release-it": "21.1.0",
5096
+ "release-it": "21.1.1",
5039
5097
  "tsdown": "0.23.0",
5040
5098
  "typescript": "6.0.3",
5041
5099
  "vitest": "5.0.3",
5042
- "pnpm": "11.28.3"
5100
+ "pnpm": "11.28.5"
5043
5101
  }["vitest"],
5044
5102
  "@vitest/coverage-v8": {
5045
5103
  "@changesets/cli": "3.0.3",
@@ -5047,18 +5105,18 @@ function vitestDevDeps() {
5047
5105
  "@types/node": "24.19.1",
5048
5106
  "@vitest/coverage-v8": "5.0.3",
5049
5107
  "commit-and-tag-version": "13.2.1",
5050
- "knip": "6.39.0",
5051
- "lefthook": "2.1.16",
5052
- "modern-web-guidance": "0.0.191",
5053
- "oxfmt": "0.71.0",
5054
- "oxlint": "1.86.0",
5108
+ "knip": "6.41.0",
5109
+ "lefthook": "2.2.1",
5110
+ "modern-web-guidance": "0.0.193",
5111
+ "oxfmt": "0.72.0",
5112
+ "oxlint": "1.87.0",
5055
5113
  "oxlint-tsgolint": "7.0.2003",
5056
5114
  "prettier": "3.9.9",
5057
- "release-it": "21.1.0",
5115
+ "release-it": "21.1.1",
5058
5116
  "tsdown": "0.23.0",
5059
5117
  "typescript": "6.0.3",
5060
5118
  "vitest": "5.0.3",
5061
- "pnpm": "11.28.3"
5119
+ "pnpm": "11.28.5"
5062
5120
  }["@vitest/coverage-v8"]
5063
5121
  };
5064
5122
  }
@@ -5069,18 +5127,18 @@ function rootDevDeps(config) {
5069
5127
  "@types/node": "24.19.1",
5070
5128
  "@vitest/coverage-v8": "5.0.3",
5071
5129
  "commit-and-tag-version": "13.2.1",
5072
- "knip": "6.39.0",
5073
- "lefthook": "2.1.16",
5074
- "modern-web-guidance": "0.0.191",
5075
- "oxfmt": "0.71.0",
5076
- "oxlint": "1.86.0",
5130
+ "knip": "6.41.0",
5131
+ "lefthook": "2.2.1",
5132
+ "modern-web-guidance": "0.0.193",
5133
+ "oxfmt": "0.72.0",
5134
+ "oxlint": "1.87.0",
5077
5135
  "oxlint-tsgolint": "7.0.2003",
5078
5136
  "prettier": "3.9.9",
5079
- "release-it": "21.1.0",
5137
+ "release-it": "21.1.1",
5080
5138
  "tsdown": "0.23.0",
5081
5139
  "typescript": "6.0.3",
5082
5140
  "vitest": "5.0.3",
5083
- "pnpm": "11.28.3"
5141
+ "pnpm": "11.28.5"
5084
5142
  }["lefthook"] };
5085
5143
  if (config.stack !== "none") {
5086
5144
  deps["knip"] = {
@@ -5089,18 +5147,18 @@ function rootDevDeps(config) {
5089
5147
  "@types/node": "24.19.1",
5090
5148
  "@vitest/coverage-v8": "5.0.3",
5091
5149
  "commit-and-tag-version": "13.2.1",
5092
- "knip": "6.39.0",
5093
- "lefthook": "2.1.16",
5094
- "modern-web-guidance": "0.0.191",
5095
- "oxfmt": "0.71.0",
5096
- "oxlint": "1.86.0",
5150
+ "knip": "6.41.0",
5151
+ "lefthook": "2.2.1",
5152
+ "modern-web-guidance": "0.0.193",
5153
+ "oxfmt": "0.72.0",
5154
+ "oxlint": "1.87.0",
5097
5155
  "oxlint-tsgolint": "7.0.2003",
5098
5156
  "prettier": "3.9.9",
5099
- "release-it": "21.1.0",
5157
+ "release-it": "21.1.1",
5100
5158
  "tsdown": "0.23.0",
5101
5159
  "typescript": "6.0.3",
5102
5160
  "vitest": "5.0.3",
5103
- "pnpm": "11.28.3"
5161
+ "pnpm": "11.28.5"
5104
5162
  }["knip"];
5105
5163
  deps["oxlint"] = {
5106
5164
  "@changesets/cli": "3.0.3",
@@ -5108,18 +5166,18 @@ function rootDevDeps(config) {
5108
5166
  "@types/node": "24.19.1",
5109
5167
  "@vitest/coverage-v8": "5.0.3",
5110
5168
  "commit-and-tag-version": "13.2.1",
5111
- "knip": "6.39.0",
5112
- "lefthook": "2.1.16",
5113
- "modern-web-guidance": "0.0.191",
5114
- "oxfmt": "0.71.0",
5115
- "oxlint": "1.86.0",
5169
+ "knip": "6.41.0",
5170
+ "lefthook": "2.2.1",
5171
+ "modern-web-guidance": "0.0.193",
5172
+ "oxfmt": "0.72.0",
5173
+ "oxlint": "1.87.0",
5116
5174
  "oxlint-tsgolint": "7.0.2003",
5117
5175
  "prettier": "3.9.9",
5118
- "release-it": "21.1.0",
5176
+ "release-it": "21.1.1",
5119
5177
  "tsdown": "0.23.0",
5120
5178
  "typescript": "6.0.3",
5121
5179
  "vitest": "5.0.3",
5122
- "pnpm": "11.28.3"
5180
+ "pnpm": "11.28.5"
5123
5181
  }["oxlint"];
5124
5182
  Object.assign(deps, vitestDevDeps());
5125
5183
  }
@@ -5129,37 +5187,21 @@ function rootDevDeps(config) {
5129
5187
  "@types/node": "24.19.1",
5130
5188
  "@vitest/coverage-v8": "5.0.3",
5131
5189
  "commit-and-tag-version": "13.2.1",
5132
- "knip": "6.39.0",
5133
- "lefthook": "2.1.16",
5134
- "modern-web-guidance": "0.0.191",
5135
- "oxfmt": "0.71.0",
5136
- "oxlint": "1.86.0",
5190
+ "knip": "6.41.0",
5191
+ "lefthook": "2.2.1",
5192
+ "modern-web-guidance": "0.0.193",
5193
+ "oxfmt": "0.72.0",
5194
+ "oxlint": "1.87.0",
5137
5195
  "oxlint-tsgolint": "7.0.2003",
5138
5196
  "prettier": "3.9.9",
5139
- "release-it": "21.1.0",
5197
+ "release-it": "21.1.1",
5140
5198
  "tsdown": "0.23.0",
5141
5199
  "typescript": "6.0.3",
5142
5200
  "vitest": "5.0.3",
5143
- "pnpm": "11.28.3"
5201
+ "pnpm": "11.28.5"
5144
5202
  }["oxlint-tsgolint"];
5145
5203
  return deps;
5146
5204
  }
5147
- function isWorkspacePackage(ctx, packageName) {
5148
- if (ctx.config.structure !== "monorepo") return false;
5149
- const packagesDir = path.join(ctx.targetDir, "packages");
5150
- if (!existsSync(packagesDir)) return false;
5151
- try {
5152
- for (const entry of readdirSync(packagesDir, { withFileTypes: true })) {
5153
- if (!entry.isDirectory()) continue;
5154
- const pkgJsonPath = path.join(packagesDir, entry.name, "package.json");
5155
- if (!existsSync(pkgJsonPath)) continue;
5156
- try {
5157
- if (parsePackageJson(readFileSync(pkgJsonPath, "utf-8"))?.name === packageName) return true;
5158
- } catch (_error) {}
5159
- }
5160
- } catch (_error) {}
5161
- return false;
5162
- }
5163
5205
  function addReleaseDeps(deps, config) {
5164
5206
  switch (config.releaseStrategy) {
5165
5207
  case "release-it":
@@ -5169,18 +5211,18 @@ function addReleaseDeps(deps, config) {
5169
5211
  "@types/node": "24.19.1",
5170
5212
  "@vitest/coverage-v8": "5.0.3",
5171
5213
  "commit-and-tag-version": "13.2.1",
5172
- "knip": "6.39.0",
5173
- "lefthook": "2.1.16",
5174
- "modern-web-guidance": "0.0.191",
5175
- "oxfmt": "0.71.0",
5176
- "oxlint": "1.86.0",
5214
+ "knip": "6.41.0",
5215
+ "lefthook": "2.2.1",
5216
+ "modern-web-guidance": "0.0.193",
5217
+ "oxfmt": "0.72.0",
5218
+ "oxlint": "1.87.0",
5177
5219
  "oxlint-tsgolint": "7.0.2003",
5178
5220
  "prettier": "3.9.9",
5179
- "release-it": "21.1.0",
5221
+ "release-it": "21.1.1",
5180
5222
  "tsdown": "0.23.0",
5181
5223
  "typescript": "6.0.3",
5182
5224
  "vitest": "5.0.3",
5183
- "pnpm": "11.28.3"
5225
+ "pnpm": "11.28.5"
5184
5226
  }["release-it"];
5185
5227
  if (config.structure === "monorepo") deps["@release-it/bumper"] = {
5186
5228
  "@changesets/cli": "3.0.3",
@@ -5188,18 +5230,18 @@ function addReleaseDeps(deps, config) {
5188
5230
  "@types/node": "24.19.1",
5189
5231
  "@vitest/coverage-v8": "5.0.3",
5190
5232
  "commit-and-tag-version": "13.2.1",
5191
- "knip": "6.39.0",
5192
- "lefthook": "2.1.16",
5193
- "modern-web-guidance": "0.0.191",
5194
- "oxfmt": "0.71.0",
5195
- "oxlint": "1.86.0",
5233
+ "knip": "6.41.0",
5234
+ "lefthook": "2.2.1",
5235
+ "modern-web-guidance": "0.0.193",
5236
+ "oxfmt": "0.72.0",
5237
+ "oxlint": "1.87.0",
5196
5238
  "oxlint-tsgolint": "7.0.2003",
5197
5239
  "prettier": "3.9.9",
5198
- "release-it": "21.1.0",
5240
+ "release-it": "21.1.1",
5199
5241
  "tsdown": "0.23.0",
5200
5242
  "typescript": "6.0.3",
5201
5243
  "vitest": "5.0.3",
5202
- "pnpm": "11.28.3"
5244
+ "pnpm": "11.28.5"
5203
5245
  }["@release-it/bumper"];
5204
5246
  break;
5205
5247
  case "simple":
@@ -5209,18 +5251,18 @@ function addReleaseDeps(deps, config) {
5209
5251
  "@types/node": "24.19.1",
5210
5252
  "@vitest/coverage-v8": "5.0.3",
5211
5253
  "commit-and-tag-version": "13.2.1",
5212
- "knip": "6.39.0",
5213
- "lefthook": "2.1.16",
5214
- "modern-web-guidance": "0.0.191",
5215
- "oxfmt": "0.71.0",
5216
- "oxlint": "1.86.0",
5254
+ "knip": "6.41.0",
5255
+ "lefthook": "2.2.1",
5256
+ "modern-web-guidance": "0.0.193",
5257
+ "oxfmt": "0.72.0",
5258
+ "oxlint": "1.87.0",
5217
5259
  "oxlint-tsgolint": "7.0.2003",
5218
5260
  "prettier": "3.9.9",
5219
- "release-it": "21.1.0",
5261
+ "release-it": "21.1.1",
5220
5262
  "tsdown": "0.23.0",
5221
5263
  "typescript": "6.0.3",
5222
5264
  "vitest": "5.0.3",
5223
- "pnpm": "11.28.3"
5265
+ "pnpm": "11.28.5"
5224
5266
  }["commit-and-tag-version"];
5225
5267
  break;
5226
5268
  case "changesets": deps["@changesets/cli"] = {
@@ -5229,18 +5271,18 @@ function addReleaseDeps(deps, config) {
5229
5271
  "@types/node": "24.19.1",
5230
5272
  "@vitest/coverage-v8": "5.0.3",
5231
5273
  "commit-and-tag-version": "13.2.1",
5232
- "knip": "6.39.0",
5233
- "lefthook": "2.1.16",
5234
- "modern-web-guidance": "0.0.191",
5235
- "oxfmt": "0.71.0",
5236
- "oxlint": "1.86.0",
5274
+ "knip": "6.41.0",
5275
+ "lefthook": "2.2.1",
5276
+ "modern-web-guidance": "0.0.193",
5277
+ "oxfmt": "0.72.0",
5278
+ "oxlint": "1.87.0",
5237
5279
  "oxlint-tsgolint": "7.0.2003",
5238
5280
  "prettier": "3.9.9",
5239
- "release-it": "21.1.0",
5281
+ "release-it": "21.1.1",
5240
5282
  "tsdown": "0.23.0",
5241
5283
  "typescript": "6.0.3",
5242
5284
  "vitest": "5.0.3",
5243
- "pnpm": "11.28.3"
5285
+ "pnpm": "11.28.5"
5244
5286
  }["@changesets/cli"];
5245
5287
  }
5246
5288
  }
@@ -5307,10 +5349,6 @@ function reclaimManagedDevDeps(ctx, pkg, managedDevDeps, unmanaged, changes) {
5307
5349
  }
5308
5350
  pkg.devDependencies = existingDevDeps;
5309
5351
  }
5310
- function readUnmanagedDevDeps(ctx) {
5311
- const slice = parseToolingConfigSlice(ctx.read(".tooling.json"), z.object({ unmanagedDevDependencies: UnmanagedDevDependenciesSchema.optional() }), { block: "unmanagedDevDependencies" });
5312
- return new Set(slice?.unmanagedDevDependencies ?? []);
5313
- }
5314
5352
  function isAsciiCheckEnabled(ctx) {
5315
5353
  return parseToolingConfigSlice(ctx.read(".tooling.json"), z.object({ ascii: AsciiConfigSchema.optional() }), { block: "ascii" })?.ascii?.enabled === true;
5316
5354
  }
@@ -5345,18 +5383,18 @@ function applyMetadataDefaults(pkg, ctx, changes) {
5345
5383
  "@types/node": "24.19.1",
5346
5384
  "@vitest/coverage-v8": "5.0.3",
5347
5385
  "commit-and-tag-version": "13.2.1",
5348
- "knip": "6.39.0",
5349
- "lefthook": "2.1.16",
5350
- "modern-web-guidance": "0.0.191",
5351
- "oxfmt": "0.71.0",
5352
- "oxlint": "1.86.0",
5386
+ "knip": "6.41.0",
5387
+ "lefthook": "2.2.1",
5388
+ "modern-web-guidance": "0.0.193",
5389
+ "oxfmt": "0.72.0",
5390
+ "oxlint": "1.87.0",
5353
5391
  "oxlint-tsgolint": "7.0.2003",
5354
5392
  "prettier": "3.9.9",
5355
- "release-it": "21.1.0",
5393
+ "release-it": "21.1.1",
5356
5394
  "tsdown": "0.23.0",
5357
5395
  "typescript": "6.0.3",
5358
5396
  "vitest": "5.0.3",
5359
- "pnpm": "11.28.3"
5397
+ "pnpm": "11.28.5"
5360
5398
  }["pnpm"];
5361
5399
  const desiredPm = pinnedPnpm ? `pnpm@${pinnedPnpm}` : void 0;
5362
5400
  if (desiredPm) {
@@ -5395,26 +5433,27 @@ function buildManagedDevDeps(ctx) {
5395
5433
  const devDeps = { ...rootDevDeps(ctx.config) };
5396
5434
  if (!isMonorepo) Object.assign(devDeps, perPackageDevDeps(ctx.config));
5397
5435
  if (ctx.config.stack !== "none" || ctx.config.ci === "forgejo") devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.15.0";
5398
- if (!isWorkspacePackage(ctx, "@bensandee/tooling")) devDeps["@bensandee/tooling"] = "0.89.1";
5436
+ if (!isWorkspacePackage(ctx, "@bensandee/tooling")) devDeps["@bensandee/tooling"] = "0.91.0";
5399
5437
  if (ctx.config.useEslintPlugin && ctx.config.stack !== "none") devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.17.0";
5438
+ if (wantsAgentCli(ctx)) devDeps[AGENT_CLI_PACKAGE] = "0.1.2";
5400
5439
  if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
5401
5440
  "@changesets/cli": "3.0.3",
5402
5441
  "@release-it/bumper": "8.0.1",
5403
5442
  "@types/node": "24.19.1",
5404
5443
  "@vitest/coverage-v8": "5.0.3",
5405
5444
  "commit-and-tag-version": "13.2.1",
5406
- "knip": "6.39.0",
5407
- "lefthook": "2.1.16",
5408
- "modern-web-guidance": "0.0.191",
5409
- "oxfmt": "0.71.0",
5410
- "oxlint": "1.86.0",
5445
+ "knip": "6.41.0",
5446
+ "lefthook": "2.2.1",
5447
+ "modern-web-guidance": "0.0.193",
5448
+ "oxfmt": "0.72.0",
5449
+ "oxlint": "1.87.0",
5411
5450
  "oxlint-tsgolint": "7.0.2003",
5412
5451
  "prettier": "3.9.9",
5413
- "release-it": "21.1.0",
5452
+ "release-it": "21.1.1",
5414
5453
  "tsdown": "0.23.0",
5415
5454
  "typescript": "6.0.3",
5416
5455
  "vitest": "5.0.3",
5417
- "pnpm": "11.28.3"
5456
+ "pnpm": "11.28.5"
5418
5457
  }["oxfmt"];
5419
5458
  if (ctx.config.formatter === "prettier") devDeps["prettier"] = {
5420
5459
  "@changesets/cli": "3.0.3",
@@ -5422,18 +5461,18 @@ function buildManagedDevDeps(ctx) {
5422
5461
  "@types/node": "24.19.1",
5423
5462
  "@vitest/coverage-v8": "5.0.3",
5424
5463
  "commit-and-tag-version": "13.2.1",
5425
- "knip": "6.39.0",
5426
- "lefthook": "2.1.16",
5427
- "modern-web-guidance": "0.0.191",
5428
- "oxfmt": "0.71.0",
5429
- "oxlint": "1.86.0",
5464
+ "knip": "6.41.0",
5465
+ "lefthook": "2.2.1",
5466
+ "modern-web-guidance": "0.0.193",
5467
+ "oxfmt": "0.72.0",
5468
+ "oxlint": "1.87.0",
5430
5469
  "oxlint-tsgolint": "7.0.2003",
5431
5470
  "prettier": "3.9.9",
5432
- "release-it": "21.1.0",
5471
+ "release-it": "21.1.1",
5433
5472
  "tsdown": "0.23.0",
5434
5473
  "typescript": "6.0.3",
5435
5474
  "vitest": "5.0.3",
5436
- "pnpm": "11.28.3"
5475
+ "pnpm": "11.28.5"
5437
5476
  }["prettier"];
5438
5477
  addReleaseDeps(devDeps, ctx.config);
5439
5478
  return devDeps;
@@ -5442,7 +5481,7 @@ async function generatePackageJson(ctx) {
5442
5481
  const filePath = "package.json";
5443
5482
  const existing = ctx.read(filePath);
5444
5483
  const allScripts = buildManagedScripts(ctx);
5445
- const unmanaged = readUnmanagedDevDeps(ctx);
5484
+ const unmanaged = readUnmanagedDevDepNames(ctx);
5446
5485
  const devDeps = buildManagedDevDeps(ctx);
5447
5486
  if (existing) {
5448
5487
  const pkg = parsePackageJson(existing);
@@ -5492,18 +5531,18 @@ async function generatePackageJson(ctx) {
5492
5531
  "@types/node": "24.19.1",
5493
5532
  "@vitest/coverage-v8": "5.0.3",
5494
5533
  "commit-and-tag-version": "13.2.1",
5495
- "knip": "6.39.0",
5496
- "lefthook": "2.1.16",
5497
- "modern-web-guidance": "0.0.191",
5498
- "oxfmt": "0.71.0",
5499
- "oxlint": "1.86.0",
5534
+ "knip": "6.41.0",
5535
+ "lefthook": "2.2.1",
5536
+ "modern-web-guidance": "0.0.193",
5537
+ "oxfmt": "0.72.0",
5538
+ "oxlint": "1.87.0",
5500
5539
  "oxlint-tsgolint": "7.0.2003",
5501
5540
  "prettier": "3.9.9",
5502
- "release-it": "21.1.0",
5541
+ "release-it": "21.1.1",
5503
5542
  "tsdown": "0.23.0",
5504
5543
  "typescript": "6.0.3",
5505
5544
  "vitest": "5.0.3",
5506
- "pnpm": "11.28.3"
5545
+ "pnpm": "11.28.5"
5507
5546
  }["pnpm"]}`
5508
5547
  };
5509
5548
  ctx.write(filePath, JSON.stringify(pkg, null, 2) + "\n");
@@ -7318,6 +7357,59 @@ async function generateGitattributes(ctx) {
7318
7357
  };
7319
7358
  }
7320
7359
  //#endregion
7360
+ //#region src/default-branch.ts
7361
+ const DEFAULT_BRANCH = "main";
7362
+ const DefaultBranchSliceSchema = z.object({ defaultBranch: z.union([z.literal(""), DefaultBranchNameSchema]).optional() });
7363
+ function configuredDefaultBranch(raw) {
7364
+ const configured = parseToolingConfigSlice(raw, DefaultBranchSliceSchema, { block: "defaultBranch" })?.defaultBranch;
7365
+ if (configured === void 0 || configured === "") return DEFAULT_BRANCH;
7366
+ return configured;
7367
+ }
7368
+ function readConfiguredDefaultBranch(dir) {
7369
+ const configPath = path.join(dir, ".tooling.json");
7370
+ if (!existsSync(configPath)) return DEFAULT_BRANCH;
7371
+ let raw;
7372
+ try {
7373
+ raw = readFileSync(configPath, "utf-8");
7374
+ } catch (error) {
7375
+ throw new FatalError(`Cannot read ${configPath}: ${describeError$1(error)}`, { cause: error });
7376
+ }
7377
+ return configuredDefaultBranch(raw);
7378
+ }
7379
+ function processCiEventSource() {
7380
+ return {
7381
+ env: process.env,
7382
+ readFile: (absolutePath) => {
7383
+ try {
7384
+ return readFileSync(absolutePath, "utf-8");
7385
+ } catch (_error) {
7386
+ return;
7387
+ }
7388
+ }
7389
+ };
7390
+ }
7391
+ const CiEventSchema = z.object({ repository: z.object({ default_branch: z.string().min(1) }) });
7392
+ function ciDefaultBranch(source) {
7393
+ if (source === void 0 || source.env["GITHUB_ACTIONS"] !== "true") return void 0;
7394
+ const eventPath = source.env["GITHUB_EVENT_PATH"];
7395
+ if (eventPath === void 0 || eventPath === "") return void 0;
7396
+ const raw = source.readFile(eventPath);
7397
+ if (raw === void 0 || raw.trim() === "") return void 0;
7398
+ let json;
7399
+ try {
7400
+ json = JSON.parse(raw);
7401
+ } catch (_error) {
7402
+ return;
7403
+ }
7404
+ const result = CiEventSchema.safeParse(json);
7405
+ return result.success ? result.data.repository.default_branch : void 0;
7406
+ }
7407
+ function refuseForgeDefaultMismatch(configured, source, command) {
7408
+ const reported = ciDefaultBranch(source);
7409
+ if (reported === void 0 || reported === configured) return;
7410
+ throw new FatalError(`${command}: .tooling.json#defaultBranch is "${configured}", but the forge reports "${reported}" as this repository's default branch. Set .tooling.json#defaultBranch to "${reported}" and run bst repo:sync.`);
7411
+ }
7412
+ //#endregion
7321
7413
  //#region src/generators/release-ci.ts
7322
7414
  const DEFAULT_POST_RELEASE_COMMAND = [
7323
7415
  "pnpm exec bst webhook:send",
@@ -7337,7 +7429,7 @@ function hookSecretEnv(secrets) {
7337
7429
  };
7338
7430
  }
7339
7431
  function postReleaseHookStep(opts) {
7340
- const ifCondition = opts?.condition ?? (opts?.guardMain ? IS_MAIN_BRANCH : void 0);
7432
+ const ifCondition = opts?.condition ?? (opts?.guardMain ? isDefaultBranch(opts.defaultBranch) : void 0);
7341
7433
  return { step: {
7342
7434
  name: "Post-release hook",
7343
7435
  ...ifCondition && { if: ifCondition },
@@ -7348,7 +7440,7 @@ function postReleaseHookStep(opts) {
7348
7440
  function postReleaseHookJob(opts) {
7349
7441
  return {
7350
7442
  needs: opts.needsJob,
7351
- if: onlyOnDefaultBranch(opts.publishCondition),
7443
+ if: onlyOnDefaultBranch(opts.defaultBranch, opts.publishCondition),
7352
7444
  "continue-on-error": true,
7353
7445
  steps: [...commonSteps(opts.ci), { step: {
7354
7446
  name: "Post-release hook",
@@ -7492,6 +7584,7 @@ function releaseWorkflowPath(ci) {
7492
7584
  async function generateReleaseCi(ctx) {
7493
7585
  const filePath = "release-ci";
7494
7586
  const isGitHub = ctx.config.ci === "github";
7587
+ const defaultBranch = configuredDefaultBranch(ctx.read(".tooling.json"));
7495
7588
  if (ctx.config.ci === "none" || ctx.config.releaseStrategy === "none") return {
7496
7589
  filePath,
7497
7590
  action: "skipped",
@@ -7535,6 +7628,7 @@ async function generateReleaseCi(ctx) {
7535
7628
  let extraJobs;
7536
7629
  let jobOutputs;
7537
7630
  if (ctx.config.releaseStrategy === "release-it") steps.push(postReleaseHookStep({
7631
+ defaultBranch,
7538
7632
  command: getPostReleaseHookCommand(ctx),
7539
7633
  secrets: hookSecrets
7540
7634
  }));
@@ -7544,6 +7638,7 @@ async function generateReleaseCi(ctx) {
7544
7638
  tag: actionsExpr("steps.release.outputs.tag")
7545
7639
  };
7546
7640
  extraJobs = { "post-release-hook": postReleaseHookJob({
7641
+ defaultBranch,
7547
7642
  ci: ctx.config.ci,
7548
7643
  needsJob: "release",
7549
7644
  publishCondition: "needs.release.outputs.pushed == 'true'",
@@ -7559,7 +7654,7 @@ async function generateReleaseCi(ctx) {
7559
7654
  on,
7560
7655
  ...isGitHub && { permissions: { contents: "write" } },
7561
7656
  jobName: "release",
7562
- jobIf: IS_MAIN_BRANCH,
7657
+ jobIf: isDefaultBranch(defaultBranch),
7563
7658
  ...jobOutputs && { jobOutputs },
7564
7659
  steps,
7565
7660
  ...extraJobs && { extraJobs }
@@ -7590,7 +7685,9 @@ async function generateReleaseCi(ctx) {
7590
7685
  }
7591
7686
  //#endregion
7592
7687
  //#region src/release/version-branch.ts
7593
- const VERSION_PR_BRANCH = "changeset-release/main";
7688
+ function versionPrBranch(defaultBranch) {
7689
+ return `changeset-release/${defaultBranch}`;
7690
+ }
7594
7691
  //#endregion
7595
7692
  //#region src/generators/ci.ts
7596
7693
  function coverageHistorySteps() {
@@ -7624,7 +7721,7 @@ function healthSteps(includeHistory) {
7624
7721
  run: "pnpm exec bst health:status --limit 10"
7625
7722
  } }];
7626
7723
  }
7627
- function changesetsReleaseStep(ci, publishesNpm, npmAuth) {
7724
+ function changesetsReleaseStep(ci, publishesNpm, npmAuth, defaultBranch) {
7628
7725
  if (ci === "github") {
7629
7726
  const changesetsEnv = {
7630
7727
  GITHUB_TOKEN: actionsExpr("secrets.RELEASE_TOKEN"),
@@ -7633,7 +7730,7 @@ function changesetsReleaseStep(ci, publishesNpm, npmAuth) {
7633
7730
  return { step: {
7634
7731
  id: "release",
7635
7732
  uses: "changesets/action@v1",
7636
- if: IS_MAIN_BRANCH,
7733
+ if: isDefaultBranch(defaultBranch),
7637
7734
  with: {
7638
7735
  publish: "pnpm changeset publish",
7639
7736
  version: "pnpm changeset version"
@@ -7651,19 +7748,19 @@ function changesetsReleaseStep(ci, publishesNpm, npmAuth) {
7651
7748
  return { step: {
7652
7749
  id: "release",
7653
7750
  name: "Release",
7654
- if: IS_MAIN_BRANCH,
7751
+ if: isDefaultBranch(defaultBranch),
7655
7752
  env: releaseEnv,
7656
7753
  run: npmAuth.publishRun("pnpm exec bst release:changesets")
7657
7754
  } };
7658
7755
  }
7659
- function ciConcurrency(ci) {
7756
+ function ciConcurrency(ci, defaultBranch) {
7660
7757
  if (ci === "forgejo") return {
7661
- group: `ci-${actionsExpr(`${IS_MAIN_BRANCH} && github.sha || github.ref`)}`,
7662
- "cancel-in-progress": actionsExpr(IS_NOT_MAIN_BRANCH)
7758
+ group: `ci-${actionsExpr(`${isDefaultBranch(defaultBranch)} && github.sha || github.ref`)}`,
7759
+ "cancel-in-progress": actionsExpr(isNotDefaultBranch(defaultBranch))
7663
7760
  };
7664
7761
  return {
7665
7762
  group: `ci-${actionsExpr("github.ref")}`,
7666
- "cancel-in-progress": actionsExpr(IS_NOT_MAIN_BRANCH)
7763
+ "cancel-in-progress": actionsExpr(isNotDefaultBranch(defaultBranch))
7667
7764
  };
7668
7765
  }
7669
7766
  function checkSteps(ci, coverageEnabled, emitDockerSweepStep) {
@@ -7694,6 +7791,7 @@ async function generateCi(ctx) {
7694
7791
  action: "skipped",
7695
7792
  description: "CI workflow not requested"
7696
7793
  };
7794
+ const defaultBranch = configuredDefaultBranch(ctx.read(".tooling.json"));
7697
7795
  const isGitHub = ctx.config.ci === "github";
7698
7796
  const isForgejo = !isGitHub;
7699
7797
  const isChangesets = ctx.config.releaseStrategy === "changesets";
@@ -7708,7 +7806,7 @@ async function generateCi(ctx) {
7708
7806
  const hasAssets = isChangesets && (hasReleaseAssets(ctx) || hasVersionStamp(ctx));
7709
7807
  const host = hasDocker ? resolveGenTimeHost(ctx) : null;
7710
7808
  const publishedIf = "steps.release.outputs.published == 'true'";
7711
- const publishedOnMainIf = onlyOnDefaultBranch(publishedIf);
7809
+ const publishedOnDefaultIf = onlyOnDefaultBranch(defaultBranch, publishedIf);
7712
7810
  const githubChangesetsDeriveTagStep = { step: {
7713
7811
  name: "Derive release tag",
7714
7812
  id: "derive-tag",
@@ -7729,17 +7827,17 @@ async function generateCi(ctx) {
7729
7827
  ...checkSteps(ctx.config.ci, coverage.enabled, hasDockerPackages(ctx)),
7730
7828
  ...emitCoverageSteps ? coverageHistorySteps() : [],
7731
7829
  ...healthSteps(emitHealthHistory),
7732
- ...isChangesets ? [changesetsReleaseStep(ctx.config.ci, publishesNpm, npmAuth)] : [],
7830
+ ...isChangesets ? [changesetsReleaseStep(ctx.config.ci, publishesNpm, npmAuth, defaultBranch)] : [],
7733
7831
  ...isChangesets && isGitHub ? [githubChangesetsDeriveTagStep] : [],
7734
7832
  ...hasAssets ? [{ step: {
7735
7833
  name: "Upload Release Assets",
7736
- if: publishedOnMainIf,
7834
+ if: publishedOnDefaultIf,
7737
7835
  env: releaseEnv,
7738
7836
  run: "pnpm exec bst release:assets"
7739
7837
  } }] : [],
7740
7838
  ...hasDocker ? [{ step: {
7741
7839
  name: "Publish Docker images",
7742
- if: publishedOnMainIf,
7840
+ if: publishedOnDefaultIf,
7743
7841
  env: dockerPublishEnv(host),
7744
7842
  run: "pnpm exec bst docker:publish"
7745
7843
  } }] : [],
@@ -7747,7 +7845,7 @@ async function generateCi(ctx) {
7747
7845
  deploy,
7748
7846
  packageName: ctx.config.name,
7749
7847
  releaseEnv,
7750
- condition: publishedOnMainIf,
7848
+ condition: publishedOnDefaultIf,
7751
7849
  secrets: hookSecrets
7752
7850
  })] : []
7753
7851
  ];
@@ -7759,6 +7857,7 @@ async function generateCi(ctx) {
7759
7857
  tag: changesetsTagOutputExpr
7760
7858
  };
7761
7859
  extraJobs = { "post-release-hook": postReleaseHookJob({
7860
+ defaultBranch,
7762
7861
  ci: ctx.config.ci,
7763
7862
  needsJob: "check",
7764
7863
  publishCondition: "needs.check.outputs.published == 'true'",
@@ -7767,7 +7866,7 @@ async function generateCi(ctx) {
7767
7866
  secrets: hookSecrets
7768
7867
  }) };
7769
7868
  }
7770
- const checkJobEnv = emitCoverageSteps || emitHealthHistory ? IS_MAIN_JOB_ENV : {};
7869
+ const checkJobEnv = emitCoverageSteps || emitHealthHistory ? defaultBranchJobEnv(defaultBranch) : {};
7771
7870
  const checksSlice = parseToolingConfigSlice(ctx.read(".tooling.json"), z.object({ checks: ChecksConfigSchema.optional() }), { block: "checks" });
7772
7871
  const skipOnVersionPr = isChangesets && checksSlice?.checks?.runOnVersionPr !== true;
7773
7872
  const content = buildWorkflowYaml({
@@ -7775,16 +7874,16 @@ async function generateCi(ctx) {
7775
7874
  name: "CI",
7776
7875
  on: {
7777
7876
  push: {
7778
- branches: ["main"],
7877
+ branches: [defaultBranch],
7779
7878
  "tags-ignore": ["**"]
7780
7879
  },
7781
7880
  pull_request: null
7782
7881
  },
7783
7882
  ...isGitHub && (emitCoverageSteps || emitHealthHistory) && { permissions: { contents: "write" } },
7784
7883
  enableEmailNotifications: isForgejo,
7785
- ...isChangesets && { concurrency: ciConcurrency(ctx.config.ci) },
7884
+ ...isChangesets && { concurrency: ciConcurrency(ctx.config.ci, defaultBranch) },
7786
7885
  jobName: "check",
7787
- ...skipOnVersionPr && { jobIf: `github.head_ref != 'changeset-release/main'` },
7886
+ ...skipOnVersionPr && { jobIf: `github.head_ref != '${versionPrBranch(defaultBranch)}'` },
7788
7887
  ...Object.keys(checkJobEnv).length > 0 && { jobEnv: checkJobEnv },
7789
7888
  ...jobOutputs && { jobOutputs },
7790
7889
  steps,
@@ -7828,19 +7927,25 @@ function foreignOwnedBinaries(ctx) {
7828
7927
  function ignoreBinariesField(binaries) {
7829
7928
  return binaries.length > 0 ? `\n ignoreBinaries: ${JSON.stringify(binaries)},` : "";
7830
7929
  }
7831
- function buildKnipConfigSingle(ignoreBinaries) {
7930
+ function ignorableDependencies(ctx) {
7931
+ return wantsAgentCli(ctx) ? [AGENT_CLI_PACKAGE] : [];
7932
+ }
7933
+ function ignoreDependenciesField(dependencies) {
7934
+ return dependencies.length > 0 ? `\n ignoreDependencies: ${JSON.stringify(dependencies)},` : "";
7935
+ }
7936
+ function buildKnipConfigSingle(ignoreBinaries, ignoreDependencies) {
7832
7937
  return `import type { KnipConfig } from "knip";
7833
7938
 
7834
7939
  export default {
7835
7940
  entry: ["src/index.ts"],
7836
- project: ["src/**/*.ts"],${ignoreBinariesField(ignoreBinaries)}
7941
+ project: ["src/**/*.ts"],${ignoreBinariesField(ignoreBinaries)}${ignoreDependenciesField(ignoreDependencies)}
7837
7942
  } satisfies KnipConfig;
7838
7943
  `;
7839
7944
  }
7840
- function buildKnipConfigMonorepo(ignoreBinaries) {
7945
+ function buildKnipConfigMonorepo(ignoreBinaries, ignoreDependencies) {
7841
7946
  return `import type { KnipConfig } from "knip";
7842
7947
 
7843
- export default {${ignoreBinariesField(ignoreBinaries)}
7948
+ export default {${ignoreBinariesField(ignoreBinaries)}${ignoreDependenciesField(ignoreDependencies)}
7844
7949
  workspaces: {
7845
7950
  ".": {
7846
7951
  entry: [],
@@ -7901,16 +8006,17 @@ function hookIsInProjectScope(content, path) {
7901
8006
  if (patterns === void 0) return true;
7902
8007
  return patterns.some((pattern) => picomatch(pattern, { dot: true })(HOOK_FILE));
7903
8008
  }
7904
- function ensureIgnorePattern(content, path, ignoreBinaries) {
8009
+ function ensureIgnorePattern(content, path, ignoreBinaries, ignoreDependencies) {
7905
8010
  const needsHooksIgnore = hookIsInProjectScope(content, path);
7906
- if (isJsonConfig(path)) return ensureIgnoreInJson(content, ignoreBinaries, needsHooksIgnore);
7907
- return ensureIgnoreInTs(content, ignoreBinaries, needsHooksIgnore);
8011
+ if (isJsonConfig(path)) return ensureIgnoreInJson(content, ignoreBinaries, ignoreDependencies, needsHooksIgnore);
8012
+ return ensureIgnoreInTs(content, ignoreBinaries, ignoreDependencies, needsHooksIgnore);
7908
8013
  }
7909
8014
  const KnipJsonSchema = z.object({
7910
8015
  ignore: z.array(z.string()).optional(),
7911
- ignoreBinaries: z.array(z.string()).optional()
8016
+ ignoreBinaries: z.array(z.string()).optional(),
8017
+ ignoreDependencies: z.array(z.string()).optional()
7912
8018
  }).passthrough();
7913
- function ensureIgnoreInJson(content, ignoreBinaries, needsHooksIgnore) {
8019
+ function ensureIgnoreInJson(content, ignoreBinaries, ignoreDependencies, needsHooksIgnore) {
7914
8020
  let parsedJson;
7915
8021
  try {
7916
8022
  parsedJson = JSON.parse(content);
@@ -7931,6 +8037,12 @@ function ensureIgnoreInJson(content, ignoreBinaries, needsHooksIgnore) {
7931
8037
  parsed.ignoreBinaries = [...parsed.ignoreBinaries ?? [], ...missingBinaries];
7932
8038
  changed = true;
7933
8039
  }
8040
+ const existingDependencies = new Set(parsed.ignoreDependencies ?? []);
8041
+ const missingDependencies = ignoreDependencies.filter((dependency) => !existingDependencies.has(dependency));
8042
+ if (missingDependencies.length > 0) {
8043
+ parsed.ignoreDependencies = [...parsed.ignoreDependencies ?? [], ...missingDependencies];
8044
+ changed = true;
8045
+ }
7934
8046
  if (!changed) return void 0;
7935
8047
  return JSON.stringify(parsed, null, 2) + "\n";
7936
8048
  }
@@ -7940,7 +8052,30 @@ function missingIgnoreBinariesInTs(content, binaries) {
7940
8052
  const arrayContent = match[1] ?? "";
7941
8053
  return binaries.filter((binary) => !arrayContent.includes(`"${binary}"`));
7942
8054
  }
7943
- function ensureIgnoreInTs(content, ignoreBinaries, needsHooksIgnore) {
8055
+ function missingIgnoreDependenciesInTs(content, dependencies) {
8056
+ const match = /ignoreDependencies:\s*\[([^\]]*)\]/.exec(content);
8057
+ if (!match) return dependencies;
8058
+ const arrayContent = match[1] ?? "";
8059
+ return dependencies.filter((dependency) => !arrayContent.includes(`"${dependency}"`));
8060
+ }
8061
+ function insertIntoTsArray(content, field, entries) {
8062
+ const quoted = entries.map((entry) => `"${entry}"`).join(", ");
8063
+ const arrayPattern = new RegExp("(" + field + ":\\s*\\[)");
8064
+ if (arrayPattern.test(content)) return {
8065
+ content: content.replace(arrayPattern, `$1${quoted}, `),
8066
+ changed: true
8067
+ };
8068
+ const exportPattern = /(export default\s*\{)\n/;
8069
+ if (exportPattern.test(content)) return {
8070
+ content: content.replace(exportPattern, `$1\n ${field}: [${quoted}],\n`),
8071
+ changed: true
8072
+ };
8073
+ return {
8074
+ content,
8075
+ changed: false
8076
+ };
8077
+ }
8078
+ function ensureIgnoreInTs(content, ignoreBinaries, ignoreDependencies, needsHooksIgnore) {
7944
8079
  let result = content;
7945
8080
  let changed = false;
7946
8081
  if (needsHooksIgnore && !result.includes(".claude/hooks")) {
@@ -7958,18 +8093,15 @@ function ensureIgnoreInTs(content, ignoreBinaries, needsHooksIgnore) {
7958
8093
  }
7959
8094
  const missingBinaries = missingIgnoreBinariesInTs(result, ignoreBinaries);
7960
8095
  if (missingBinaries.length > 0) {
7961
- const quoted = missingBinaries.map((binary) => `"${binary}"`).join(", ");
7962
- const binariesArrayPattern = /(ignoreBinaries:\s*\[)/;
7963
- if (binariesArrayPattern.test(result)) {
7964
- result = result.replace(binariesArrayPattern, `$1${quoted}, `);
7965
- changed = true;
7966
- } else {
7967
- const exportPattern = /(export default\s*\{)\n/;
7968
- if (exportPattern.test(result)) {
7969
- result = result.replace(exportPattern, `$1\n ignoreBinaries: [${quoted}],\n`);
7970
- changed = true;
7971
- }
7972
- }
8096
+ const inserted = insertIntoTsArray(result, "ignoreBinaries", missingBinaries);
8097
+ result = inserted.content;
8098
+ changed = changed || inserted.changed;
8099
+ }
8100
+ const missingDependencies = missingIgnoreDependenciesInTs(result, ignoreDependencies);
8101
+ if (missingDependencies.length > 0) {
8102
+ const inserted = insertIntoTsArray(result, "ignoreDependencies", missingDependencies);
8103
+ result = inserted.content;
8104
+ changed = changed || inserted.changed;
7973
8105
  }
7974
8106
  return changed ? result : void 0;
7975
8107
  }
@@ -7985,6 +8117,7 @@ async function generateKnip(ctx) {
7985
8117
  };
7986
8118
  const isMonorepo = ctx.config.structure === "monorepo";
7987
8119
  const ignoreBinaries = foreignOwnedBinaries(ctx);
8120
+ const ignoreDependencies = ignorableDependencies(ctx);
7988
8121
  const buildConfig = isMonorepo ? buildKnipConfigMonorepo : buildKnipConfigSingle;
7989
8122
  const existingPath = KNIP_CONFIG_PATHS.find((p) => ctx.exists(p));
7990
8123
  if (existingPath) {
@@ -7995,7 +8128,7 @@ async function generateKnip(ctx) {
7995
8128
  description: "Could not read existing knip config"
7996
8129
  };
7997
8130
  if (content.trim() === "") {
7998
- const blank = isJsonConfig(existingPath) ? ensureIgnorePattern("{}", existingPath, ignoreBinaries) ?? "{}\n" : formattedTemplate(ctx, existingPath, buildConfig(ignoreBinaries));
8131
+ const blank = isJsonConfig(existingPath) ? ensureIgnorePattern("{}", existingPath, ignoreBinaries, ignoreDependencies) ?? "{}\n" : formattedTemplate(ctx, existingPath, buildConfig(ignoreBinaries, ignoreDependencies));
7999
8132
  ctx.write(existingPath, blank);
8000
8133
  return {
8001
8134
  filePath: existingPath,
@@ -8003,13 +8136,13 @@ async function generateKnip(ctx) {
8003
8136
  description: createdDescription(existingPath)
8004
8137
  };
8005
8138
  }
8006
- const updated = ensureIgnorePattern(content, existingPath, ignoreBinaries);
8139
+ const updated = ensureIgnorePattern(content, existingPath, ignoreBinaries, ignoreDependencies);
8007
8140
  if (updated) {
8008
8141
  ctx.write(existingPath, updated);
8009
8142
  return {
8010
8143
  filePath: existingPath,
8011
8144
  action: "updated",
8012
- description: "Added \".claude/hooks/**\" and/or foreign-owned ignoreBinaries"
8145
+ description: "Added \".claude/hooks/**\", foreign-owned ignoreBinaries, and/or ignored dependencies"
8013
8146
  };
8014
8147
  }
8015
8148
  return {
@@ -8018,7 +8151,7 @@ async function generateKnip(ctx) {
8018
8151
  description: existingPath === filePath ? "Already configured" : `Existing config found at ${existingPath}`
8019
8152
  };
8020
8153
  }
8021
- ctx.write(filePath, formattedTemplate(ctx, filePath, buildConfig(ignoreBinaries)));
8154
+ ctx.write(filePath, formattedTemplate(ctx, filePath, buildConfig(ignoreBinaries, ignoreDependencies)));
8022
8155
  return {
8023
8156
  filePath,
8024
8157
  action: "created",
@@ -8447,7 +8580,7 @@ async function generateReleaseIt(ctx) {
8447
8580
  }
8448
8581
  //#endregion
8449
8582
  //#region src/generators/changesets.ts
8450
- function buildConfig$2() {
8583
+ function buildConfig$2(defaultBranch) {
8451
8584
  return {
8452
8585
  $schema: "https://unpkg.com/@changesets/config@3.1.1/schema.json",
8453
8586
  changelog: "@changesets/cli/changelog",
@@ -8455,7 +8588,7 @@ function buildConfig$2() {
8455
8588
  fixed: [],
8456
8589
  linked: [],
8457
8590
  access: "public",
8458
- baseBranch: "main",
8591
+ baseBranch: defaultBranch,
8459
8592
  updateInternalDependencies: "patch",
8460
8593
  ignore: []
8461
8594
  };
@@ -8467,7 +8600,7 @@ async function generateChangesets(ctx) {
8467
8600
  action: "skipped",
8468
8601
  description: "Changesets not requested"
8469
8602
  };
8470
- const content = JSON.stringify(buildConfig$2(), null, 2) + "\n";
8603
+ const content = JSON.stringify(buildConfig$2(configuredDefaultBranch(ctx.read(".tooling.json"))), null, 2) + "\n";
8471
8604
  const existing = ctx.read(filePath);
8472
8605
  if (existing) return {
8473
8606
  filePath,
@@ -9095,6 +9228,7 @@ const AGENT_RULES_MID = [
9095
9228
  const TS_EXTENSIONLESS_RULE = "- Prefer extensionless imports; if an extension is required, use `.ts` over `.js`.";
9096
9229
  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.";
9097
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>`.";
9098
9232
  const FIND_COMMAND_RULE = "- Find a command with `bst --help`, its flags with `bst <command> --help`, before guessing.";
9099
9233
  const FORGEJO_CLI_RULE_SUFFIX = " For Forgejo API work, run `bst-fj --help`.";
9100
9234
  const AGENT_CLI_RULE_SUFFIX = " For the agent's runs and transcripts, run `bst-agent --help`.";
@@ -9115,20 +9249,22 @@ function declaresAgentCli(packageJson) {
9115
9249
  function findCommandRule(packageJson) {
9116
9250
  return FIND_COMMAND_RULE + (declaresForgejoCli(packageJson) ? FORGEJO_CLI_RULE_SUFFIX : "") + (declaresAgentCli(packageJson) ? AGENT_CLI_RULE_SUFFIX : "");
9117
9251
  }
9118
- const AGENT_RULES_TAIL = [
9119
- "- 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.",
9120
- "- 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.",
9121
- WORK_STATUS_RULE,
9122
- FIND_COMMAND_RULE,
9123
- "",
9124
- "#### Git discipline",
9125
- "",
9126
- "**The branch you are on is the mode switch.** The bst-hook refuses any command that would move a protected branch, so on one you leave changes unstaged; on a feature branch you commit, push, and open the PR yourself. Pick the mode, announce it, and only ask when it's genuinely ambiguous.",
9127
- "",
9128
- "**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.",
9129
- "",
9130
- "**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."
9131
- ];
9252
+ function agentRulesTail(agentWorked) {
9253
+ return [
9254
+ "- 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.",
9256
+ agentWorked ? WORK_START_RULE : WORK_STATUS_RULE,
9257
+ FIND_COMMAND_RULE,
9258
+ "",
9259
+ "#### Git discipline",
9260
+ "",
9261
+ "**The branch you are on is the mode switch.** The bst-hook refuses any command that would move a protected branch, so on one you leave changes unstaged; on a feature branch you commit, push, and open the PR yourself. Pick the mode, announce it, and only ask when it's genuinely ambiguous.",
9262
+ "",
9263
+ "**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
+ "",
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."
9266
+ ];
9267
+ }
9132
9268
  const COMMENTS_GUIDE_BODY = [
9133
9269
  "# Comments",
9134
9270
  "",
@@ -9136,7 +9272,7 @@ const COMMENTS_GUIDE_BODY = [
9136
9272
  "",
9137
9273
  "- Default to none. A comment earns its place by carrying what the code cannot -- a rejected alternative, the bug that forced this shape, an invariant enforced elsewhere -- and earns it one decision at a time, in a sentence or two each: what was rejected, and what it cost. Five decisions is five sentences, not five paragraphs. Recoverable from the code? Leave it unwritten.",
9138
9274
  "- Present tense, about what is. No `now`, `previously`, `changed to`: diff narration is stale on the next edit, and the commit message carries it. No running ledgers or changelogs: when adjusting a threshold, constant, or baseline, state the active invariant and prune past deltas.",
9139
- "- Never cite a lifecycle-managed doc, PR, or issue from code: no PR or issue numbers (`PR #123`, `closes #456`), no path under `docs/superpowers/` or `docs/design/`, none carrying a `retired/` or `completed/` segment, no `docs/plan-*.md`, and no bare decision or task ID (`D3`, `T2`). They move, retire, and renumber, nothing checks the reference, and the reader has no way to tell a dead one from a live one. State the constraint itself, or cite a maintained reference doc instead. A `docs/` path that is a glob or an elided example rather than a reference (`docs/*.md`) is vocabulary, not a citation, and is fine.",
9275
+ "- Never cite a lifecycle-managed doc, PR, or issue from code: no PR or issue numbers (`PR #123`, `closes #456`), no path under `docs/specs/`, `docs/plans/`, `docs/superpowers/` or `docs/design/`, none carrying a `retired/` or `completed/` segment, no `docs/plan-*.md`, and no bare decision or task ID (`D3`, `T2`). They move, retire, and renumber, nothing checks the reference, and the reader has no way to tell a dead one from a live one. State the constraint itself, or cite a maintained reference doc instead. A `docs/` path that is a glob or an elided example rather than a reference (`docs/*.md`) is vocabulary, not a citation, and is fine.",
9140
9276
  "- Code that exists only to accept an older shape -- a file, field, flag, environment variable, label or comment format -- carries `// @compat <id> until <expiry>: <reason>` on the line above it. The expiry is a date (`YYYY-MM-DD`) when the old shape lives with consumers who upgrade on their own schedule; a version of the code's own package (`v1.4.0`) when its consumer upgrades with each release; or `forge-clean` for old data on the forge, which has neither. Remove the code and every test that carries the same id together.",
9141
9277
  "- **The code is the fact.** When a comment disagrees with it, fix it in one line or delete it -- never grow one to argue the code is right."
9142
9278
  ].join("\n");
@@ -9167,7 +9303,11 @@ const GIT_GUIDE_BODY = [
9167
9303
  "",
9168
9304
  "## Claiming work by hand",
9169
9305
  "",
9170
- "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. First check its labels: an `agent/<stage>-running` label means a run is live -- do not start; wait or ask. Otherwise add `agent-flags/hand-authored` (on Forgejo, `bst-fj issue:label <n> agent-flags/hand-authored`, which takes a pull request number too). The flag on either an issue or its pull request holds both. Remove it when you hand the work back, before you add any `agent/` label. If the repo has no such label, no agent drives it; skip this step.",
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.",
9307
+ "",
9308
+ "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.",
9309
+ "",
9310
+ "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.",
9171
9311
  "",
9172
9312
  "## Issue linking",
9173
9313
  "",
@@ -9198,7 +9338,7 @@ const PLAN_LIFECYCLE_GUIDE_BODY = [
9198
9338
  "",
9199
9339
  "A plan's **directory** is the source of truth for whether it shipped -- not its `Status:` line, not its checkboxes. Read the source it names.",
9200
9340
  "",
9201
- "`.superpowers/` (no `docs/` prefix) is scratch state, not a plan's home -- a plan doc always lives under `docs/superpowers/plans/`, even one drafted while running a plan through subagents.",
9341
+ "`.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.",
9202
9342
  "",
9203
9343
  "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.",
9204
9344
  "",
@@ -9215,11 +9355,11 @@ const PLANNING_GUIDE_BODY = [
9215
9355
  "",
9216
9356
  "## The spec",
9217
9357
  "",
9218
- "Say why and what, not how: the problem, who it is for, the constraints, what done looks like, and the alternatives you rejected, each with its reason. Work with several independent parts gets one spec per part. Save it as `docs/superpowers/specs/<YYYY-MM-DD>-<topic>-design.md`.",
9358
+ "Say why and what, not how: the problem, who it is for, the constraints, what done looks like, and the alternatives you rejected, each with its reason. Work with several independent parts gets one spec per part. Save it as `docs/specs/<YYYY-MM-DD>-<topic>-design.md`.",
9219
9359
  "",
9220
9360
  "## The plan",
9221
9361
  "",
9222
- "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/superpowers/plans/`.",
9362
+ "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/`.",
9223
9363
  "",
9224
9364
  "- 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.",
9225
9365
  "- Label each task `Task <n>:` at the start of its heading or checkbox, numbered from 1, so `bst plan:brief` can find it. 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.",
@@ -9288,18 +9428,18 @@ function webUiGuideBody() {
9288
9428
  "@types/node": "24.19.1",
9289
9429
  "@vitest/coverage-v8": "5.0.3",
9290
9430
  "commit-and-tag-version": "13.2.1",
9291
- "knip": "6.39.0",
9292
- "lefthook": "2.1.16",
9293
- "modern-web-guidance": "0.0.191",
9294
- "oxfmt": "0.71.0",
9295
- "oxlint": "1.86.0",
9431
+ "knip": "6.41.0",
9432
+ "lefthook": "2.2.1",
9433
+ "modern-web-guidance": "0.0.193",
9434
+ "oxfmt": "0.72.0",
9435
+ "oxlint": "1.87.0",
9296
9436
  "oxlint-tsgolint": "7.0.2003",
9297
9437
  "prettier": "3.9.9",
9298
- "release-it": "21.1.0",
9438
+ "release-it": "21.1.1",
9299
9439
  "tsdown": "0.23.0",
9300
9440
  "typescript": "6.0.3",
9301
9441
  "vitest": "5.0.3",
9302
- "pnpm": "11.28.3"
9442
+ "pnpm": "11.28.5"
9303
9443
  }["modern-web-guidance"]}`;
9304
9444
  return [
9305
9445
  "# Web UI",
@@ -9548,13 +9688,13 @@ function guideIndex(guides) {
9548
9688
  for (const guide of guides.filter((g) => g.enabled)) lines.push(`- Before you ${guide.trigger}: \`${GUIDES_DIR}/${guide.name}\`.`);
9549
9689
  return lines.join("\n");
9550
9690
  }
9551
- function agentRulesBody(config, guides, packageJson) {
9691
+ function agentRulesBody(config, guides, packageJson, agentWorked) {
9552
9692
  const lines = [...AGENT_RULES_HEAD];
9553
9693
  if (config.stack === "typescript") lines.push(TS_TYPECAST_RULE);
9554
9694
  lines.push(...AGENT_RULES_MID);
9555
9695
  if (config.stack === "typescript") lines.push(TS_EXTENSIONLESS_RULE);
9556
9696
  if (config.releaseStrategy === "changesets") lines.push(CHANGESETS_RULE);
9557
- const tail = AGENT_RULES_TAIL.map((line) => line === FIND_COMMAND_RULE ? findCommandRule(packageJson) : line);
9697
+ const tail = agentRulesTail(agentWorked).map((line) => line === FIND_COMMAND_RULE ? findCommandRule(packageJson) : line);
9558
9698
  lines.push(...tail, "", guideIndex(guides));
9559
9699
  return lines.join("\n");
9560
9700
  }
@@ -9618,7 +9758,7 @@ function syncClaudeMdFile(ctx, guides) {
9618
9758
  name: "agent-rules",
9619
9759
  startMarker: "<!-- @tooling:agent-rules -->",
9620
9760
  endMarker: "<!-- @tooling:endagent-rules -->",
9621
- body: agentRulesBody(ctx.config, guides, ctx.packageJson),
9761
+ body: agentRulesBody(ctx.config, guides, ctx.packageJson, agentEnabled(ctx)),
9622
9762
  enabled: true
9623
9763
  }, ...RETIRED_BLOCK_NAMES.map((name) => ({
9624
9764
  name,
@@ -9859,7 +9999,7 @@ function reportFailures(failed, command) {
9859
9999
  function generateMigratePrompt(results, config, detected) {
9860
10000
  const sections = [];
9861
10001
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
9862
- sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.89.1 repo:sync\` on ${timestamp}_\n`);
10002
+ sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.91.0 repo:sync\` on ${timestamp}_\n`);
9863
10003
  const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
9864
10004
  if (changed.length > 0) {
9865
10005
  const items = changed.map((r) => {
@@ -9987,7 +10127,7 @@ function logDetectionSummary(ctx) {
9987
10127
  log$1.info(`Release assets convention detected in ${label} -- emit build artifacts to ${CONVENTION_OUTPUT_DIR}/ (gitignored).`);
9988
10128
  }
9989
10129
  }
9990
- async function runInit(config, options = {}) {
10130
+ async function runInitReport(config, options = {}) {
9991
10131
  const detected = detectProject(config.targetDir);
9992
10132
  const { ctx, archivedFiles, sawCrlf } = createContext(config, options.confirmOverwrite ?? (async (relativePath) => {
9993
10133
  const result = await select({
@@ -10024,7 +10164,10 @@ async function runInit(config, options = {}) {
10024
10164
  }
10025
10165
  if (!hasChanges && options.noPrompt) {
10026
10166
  if (failed.length === 0) log$1.success("Repository is up to date.");
10027
- return results;
10167
+ return {
10168
+ results,
10169
+ install: "not-run"
10170
+ };
10028
10171
  }
10029
10172
  if (results.some((r) => r.action === "archived" && r.filePath.startsWith(".husky/"))) try {
10030
10173
  execSync("git config --unset core.hooksPath", {
@@ -10043,16 +10186,19 @@ async function runInit(config, options = {}) {
10043
10186
  if (!options.noPrompt) log$1.info(`Migration prompt written to ${promptPath}`);
10044
10187
  const packageJsonChanged = results.some((r) => r.filePath === "package.json" && (r.action === "created" || r.action === "updated"));
10045
10188
  const hasLockfile = ctx.exists("pnpm-lock.yaml");
10189
+ let install = "not-run";
10046
10190
  if (packageJsonChanged && hasLockfile) {
10047
10191
  log$1.info("package.json changed -- running pnpm install...");
10048
10192
  try {
10049
10193
  execSync("pnpm install", {
10050
10194
  cwd: config.targetDir,
10051
- stdio: "inherit",
10195
+ stdio: options.installOutput === "capture" ? "ignore" : "inherit",
10052
10196
  timeout: 12e4
10053
10197
  });
10198
+ install = "ran";
10054
10199
  } catch (_error) {
10055
10200
  log$1.warn("pnpm install failed -- run it manually");
10201
+ install = "failed";
10056
10202
  }
10057
10203
  }
10058
10204
  if (hasChanges && ctx.exists("package.json")) try {
@@ -10067,7 +10213,13 @@ async function runInit(config, options = {}) {
10067
10213
  "Run: pnpm check",
10068
10214
  ...!options.noPrompt ? ["Run: claude \"Execute the migration steps in .tooling-migrate.md\""] : []
10069
10215
  ].map((step, i) => `${i + 1}. ${step}`).join("\n"), "Next steps");
10070
- return results;
10216
+ return {
10217
+ results,
10218
+ install
10219
+ };
10220
+ }
10221
+ async function runInit(config, options = {}) {
10222
+ return (await runInitReport(config, options)).results;
10071
10223
  }
10072
10224
  //#endregion
10073
10225
  //#region src/docker-check/compose-lint.ts
@@ -10806,34 +10958,6 @@ function repoPrefix(executor, cwd) {
10806
10958
  function ceilingFileNamesAt(prefix) {
10807
10959
  return HEALTH_CEILING_FILES.map((name) => `${prefix}${name}`);
10808
10960
  }
10809
- function processCiEventSource() {
10810
- return {
10811
- env: process.env,
10812
- readFile: (absolutePath) => {
10813
- try {
10814
- return readFileSync(absolutePath, "utf-8");
10815
- } catch (_error) {
10816
- return;
10817
- }
10818
- }
10819
- };
10820
- }
10821
- const CiEventSchema = z.object({ repository: z.object({ default_branch: z.string().min(1) }) });
10822
- function ciDefaultBranch(source) {
10823
- if (source === void 0 || source.env["GITHUB_ACTIONS"] !== "true") return void 0;
10824
- const eventPath = source.env["GITHUB_EVENT_PATH"];
10825
- if (eventPath === void 0 || eventPath === "") return void 0;
10826
- const raw = source.readFile(eventPath);
10827
- if (raw === void 0 || raw.trim() === "") return void 0;
10828
- let json;
10829
- try {
10830
- json = JSON.parse(raw);
10831
- } catch (_error) {
10832
- return;
10833
- }
10834
- const result = CiEventSchema.safeParse(json);
10835
- return result.success ? result.data.repository.default_branch : void 0;
10836
- }
10837
10961
  const DefaultBranchSchema = z.object({ defaultBranch: z.string().optional() });
10838
10962
  function parseDefaultBranchOverride(raw) {
10839
10963
  if (raw === void 0 || raw.trim() === "") return void 0;
@@ -10880,7 +11004,7 @@ function originHeadBranch(git, cwd) {
10880
11004
  return name === "" ? void 0 : name;
10881
11005
  }
10882
11006
  function unconfirmedDefaultWarning(branch, namedByTree) {
10883
- return `health: could not confirm the default branch, so the reference was read against origin/${branch}, while .tooling.json#defaultBranch names ${namedByTree}. Record it in this clone with \`git remote set-head origin ${namedByTree}\`, or run in GitHub or Forgejo Actions, where the event payload names it.`;
11007
+ return `health: could not confirm the default branch, so the reference was read against origin/${branch}, while .tooling.json#defaultBranch names ${namedByTree}. If ${namedByTree} is the repository's default branch, record it in this clone with \`git remote set-head origin --auto\`, or run in GitHub or Forgejo Actions, where the event payload names it.`;
10884
11008
  }
10885
11009
  function gitRunner(executor) {
10886
11010
  return (args, cwd) => {
@@ -11962,10 +12086,10 @@ function runComposeLintGate(targetDir, config, memo) {
11962
12086
  for (const warning of findDockerRebuildRisks(targetDir, config, memo)) log$1.warn(warning);
11963
12087
  return issues.length;
11964
12088
  }
11965
- function runCheckOnlyImageGate(config, targetDir, memo) {
12089
+ function runCheckOnlyImageGate(config, targetDir, memo, phase) {
11966
12090
  if (!config.publishDocker) return [];
11967
12091
  const images = [];
11968
- for (const risk of findCheckOnlyPublishRisks(targetDir, void 0, memo)) {
12092
+ for (const risk of findCheckOnlyPublishRisks(targetDir, phase, memo)) {
11969
12093
  log$1.warn(`Image "${risk.imageName}" is declared only in ${risk.declaredIn}, the check overlay, so a later tooling release stops publishing it with docker:publish by default. Keep it: set publish to true for "${risk.imageName}" under .tooling.json#docker. Settle it: set publish to false there, or move the service into a base compose file.`);
11970
12094
  images.push(risk.imageName);
11971
12095
  }
@@ -11979,6 +12103,18 @@ function runWorkflowAptGate(targetDir) {
11979
12103
  }
11980
12104
  return steps;
11981
12105
  }
12106
+ function findInertAgentKeys(saved) {
12107
+ const agent = saved?.agent;
12108
+ if (!agent) return [];
12109
+ if (agent.enabled === true) return [];
12110
+ return Object.keys(agent).filter((key) => key !== "enabled").toSorted();
12111
+ }
12112
+ function runInertAgentSettingsGate(saved) {
12113
+ const keys = findInertAgentKeys(saved);
12114
+ if (keys.length === 0) return [];
12115
+ log$1.warn(`.tooling.json sets ${keys.map((key) => `agent.${key}`).join(", ")}, but agent.enabled is not true, so the agent ignores them. Run \`bst setup:agent\` to enable the agent, or remove those keys.`);
12116
+ return keys;
12117
+ }
11982
12118
  function runPublishConfigGate(config, targetDir) {
11983
12119
  if (!config.publishNpm) return 0;
11984
12120
  const issues = lintPublishConfig(targetDir, config.structure);
@@ -12024,6 +12160,18 @@ function warnStaleChecksAdd(config) {
12024
12160
  if (stale.length === 0) return;
12025
12161
  log$1.warn(`.tooling.json#checks.add names built-in step(s) checks:run already runs: ${stale.join(", ")}. The entries are deduplicated at run time and do nothing -- remove them.`);
12026
12162
  }
12163
+ function warnUnconfiguredDefaultBranch(targetDir, saved, git = defaultGitRunner) {
12164
+ if (saved?.defaultBranch !== void 0) return;
12165
+ const head = git([
12166
+ "symbolic-ref",
12167
+ "--short",
12168
+ "refs/remotes/origin/HEAD"
12169
+ ], targetDir);
12170
+ if (head === void 0) return;
12171
+ const name = head.startsWith("origin/") ? head.slice(7) : head;
12172
+ if (name === "" || name === "main") return;
12173
+ log$1.warn(`origin/HEAD names "${name}", but .tooling.json sets no defaultBranch, so the generated CI workflow triggers on "${DEFAULT_BRANCH}" and gates its release steps there. Set .tooling.json#defaultBranch to "${name}" and run bst repo:sync.`);
12174
+ }
12027
12175
  function warnCoveredChecksAdd(config, targetDir) {
12028
12176
  const covered = coveredChecksAddEntries(config, readPackageJson(targetDir)?.scripts ?? {});
12029
12177
  for (const entry of covered) log$1.warn(`.tooling.json#checks.add entry "${entry.name}" is already run by the "${entry.coveredBy}" step's script ("${entry.fragment}"), so every pipeline run executes it twice. Remove the add entry, or drop the inline invocation from the "${entry.coveredBy}" script.`);
@@ -12143,26 +12291,14 @@ function checkDeclaredToolsActive(targetDir, declared) {
12143
12291
  }
12144
12292
  function runToolchainGate(targetDir) {
12145
12293
  const miseTomlPath = path.join(targetDir, "mise.toml");
12146
- if (!existsSync(miseTomlPath)) return {
12147
- failures: 0,
12148
- warned: false
12149
- };
12294
+ if (!existsSync(miseTomlPath)) return 0;
12150
12295
  if (miseIsAbsent(targetDir)) {
12151
- log$1.warn(`mise.toml declares this repo's toolchain, but mise is not on PATH. Install it (${MISE_INSTALL_URL}), then add \`eval "$(mise activate bash)"\` to ~/.bashrc for interactive shells and \`eval "$(mise activate bash --shims)"\` to ~/.profile, so editors, git hooks, and AI sessions get the same tools.`);
12152
- return {
12153
- failures: 0,
12154
- warned: true
12155
- };
12296
+ log$1.error(`mise.toml declares this repo's toolchain, but mise is not on PATH. Install it (${MISE_INSTALL_URL}), then add \`eval "$(mise activate bash)"\` to ~/.bashrc for interactive shells and \`eval "$(mise activate bash --shims)"\` to ~/.profile, so editors, git hooks, and AI sessions get the same tools. If this repo's mise.toml has an [env] block, also add the directory your checkouts live in to \`trusted_config_paths\` in ~/.config/mise/config.toml.`);
12297
+ return 1;
12156
12298
  }
12157
12299
  const declared = readDeclaredTools(miseTomlPath);
12158
- if (!declared) return {
12159
- failures: 0,
12160
- warned: false
12161
- };
12162
- return {
12163
- failures: checkDeclaredToolsActive(targetDir, declared),
12164
- warned: false
12165
- };
12300
+ if (!declared) return 0;
12301
+ return checkDeclaredToolsActive(targetDir, declared);
12166
12302
  }
12167
12303
  //#endregion
12168
12304
  //#region src/commands/repo-sync.ts
@@ -12204,7 +12340,7 @@ const syncCommand = defineCommand({
12204
12340
  },
12205
12341
  json: {
12206
12342
  type: "boolean",
12207
- description: "Emit one JSON document: status (up-to-date | drift | workflow-drift | failed), actionable, failed, toolchain, checkOnlyImages, aptSteps, warnCount, exitCode. --check only: the write mode prompts and inherits child streams."
12343
+ description: "Emit one JSON document: status (up-to-date | drift | workflow-drift | failed), actionable, failed, checkOnlyImages, aptSteps, inertAgentKeys, warnCount, exitCode. --check only: the write mode prompts and inherits child streams."
12208
12344
  }
12209
12345
  },
12210
12346
  async run({ args }) {
@@ -12242,47 +12378,71 @@ const syncCommand = defineCommand({
12242
12378
  })).length > 0) process.exitCode = 1;
12243
12379
  }
12244
12380
  });
12245
- async function runUpdate(targetDir, flags = {}) {
12381
+ async function runUpdateReport(targetDir, flags = {}, options = {}) {
12246
12382
  const dockerDiscovery = createDockerDiscoveryMemo();
12247
12383
  const saved = loadToolingConfigStrict(targetDir);
12248
12384
  if (!saved) {
12249
12385
  log$1.error("No .tooling.json found. Run `bst repo:sync` first to initialize the project.");
12250
12386
  process.exitCode = 1;
12251
- return [];
12387
+ return {
12388
+ status: "stopped",
12389
+ results: [],
12390
+ install: "not-run"
12391
+ };
12252
12392
  }
12253
12393
  warnStaleChecksAdd(saved);
12394
+ warnUnconfiguredDefaultBranch(targetDir, saved);
12254
12395
  warnCoveredChecksAdd(saved, targetDir);
12396
+ runInertAgentSettingsGate(saved);
12255
12397
  if (runComposeLintGate(targetDir, saved, dockerDiscovery) > 0) {
12256
12398
  process.exitCode = 1;
12257
- return [];
12399
+ return {
12400
+ status: "stopped",
12401
+ results: [],
12402
+ install: "not-run"
12403
+ };
12258
12404
  }
12259
12405
  const config = mergeWithSavedConfig(buildDefaultConfig(targetDir, flags), saved);
12260
12406
  runReleaseTargetGate(config, targetDir);
12261
12407
  runVersionStampGate(saved, targetDir, config.releaseStrategy);
12262
12408
  if (runPublishConfigGate(config, targetDir) > 0) {
12263
12409
  process.exitCode = 1;
12264
- return [];
12410
+ return {
12411
+ status: "stopped",
12412
+ results: [],
12413
+ install: "not-run"
12414
+ };
12265
12415
  }
12266
- return runInit(config, {
12267
- noPrompt: true,
12268
- confirmOverwrite: async () => "overwrite",
12269
- dockerDiscovery
12270
- });
12416
+ return {
12417
+ status: "ran",
12418
+ ...await runInitReport(config, {
12419
+ noPrompt: true,
12420
+ confirmOverwrite: async () => "overwrite",
12421
+ dockerDiscovery,
12422
+ installOutput: options.installOutput
12423
+ })
12424
+ };
12425
+ }
12426
+ async function runUpdate(targetDir, flags = {}) {
12427
+ return (await runUpdateReport(targetDir, flags)).results;
12271
12428
  }
12272
- function gateFailure(toolchainWarned = false) {
12429
+ function gateFailure() {
12273
12430
  return {
12274
12431
  status: "failed",
12275
12432
  actionable: [],
12276
12433
  failed: [],
12277
- toolchain: { warned: toolchainWarned },
12278
12434
  checkOnlyImages: [],
12279
12435
  aptSteps: [],
12436
+ inertAgentKeys: [],
12280
12437
  warnCount: 0,
12281
12438
  exitCode: 1
12282
12439
  };
12283
12440
  }
12284
- function upToDateReport(toolchain, checkOnlyImages, aptSteps) {
12285
- const warnCount = (toolchain.warned ? 1 : 0) + checkOnlyImages.length + aptSteps.length;
12441
+ function advisoryWarnCount(advisories) {
12442
+ return advisories.checkOnlyImages.length + advisories.aptSteps.length + (advisories.inertAgentKeys.length > 0 ? 1 : 0);
12443
+ }
12444
+ function upToDateReport(advisories) {
12445
+ const warnCount = advisoryWarnCount(advisories);
12286
12446
  if (warnCount > 0) {
12287
12447
  if (!isJsonMode()) rawLog$1(formatWarnMarker(warnCount));
12288
12448
  } else log$1.success("Repository is up to date.");
@@ -12290,9 +12450,9 @@ function upToDateReport(toolchain, checkOnlyImages, aptSteps) {
12290
12450
  status: "up-to-date",
12291
12451
  actionable: [],
12292
12452
  failed: [],
12293
- toolchain,
12294
- checkOnlyImages,
12295
- aptSteps,
12453
+ checkOnlyImages: advisories.checkOnlyImages,
12454
+ aptSteps: advisories.aptSteps,
12455
+ inertAgentKeys: advisories.inertAgentKeys,
12296
12456
  warnCount,
12297
12457
  exitCode: 0
12298
12458
  };
@@ -12300,6 +12460,7 @@ function upToDateReport(toolchain, checkOnlyImages, aptSteps) {
12300
12460
  async function runCheckReport(targetDir) {
12301
12461
  const dockerDiscovery = createDockerDiscoveryMemo();
12302
12462
  const saved = loadToolingConfigStrict(targetDir);
12463
+ warnUnconfiguredDefaultBranch(targetDir, saved);
12303
12464
  if (saved) {
12304
12465
  warnStaleChecksAdd(saved);
12305
12466
  warnCoveredChecksAdd(saved, targetDir);
@@ -12310,44 +12471,42 @@ async function runCheckReport(targetDir) {
12310
12471
  if (runReleaseTargetGate(config, targetDir)) return gateFailure();
12311
12472
  if (runVersionStampGate(saved, targetDir, config.releaseStrategy) > 0) return gateFailure();
12312
12473
  if (runPublishConfigGate(config, targetDir) > 0) return gateFailure();
12313
- const toolchain = runToolchainGate(targetDir);
12314
- if (toolchain.failures > 0) return {
12315
- ...gateFailure(),
12316
- toolchain: { warned: toolchain.warned }
12474
+ if (runToolchainGate(targetDir) > 0) return gateFailure();
12475
+ const advisories = {
12476
+ checkOnlyImages: runCheckOnlyImageGate(config, targetDir, dockerDiscovery),
12477
+ aptSteps: runWorkflowAptGate(targetDir),
12478
+ inertAgentKeys: runInertAgentSettingsGate(saved)
12317
12479
  };
12318
- const checkOnlyImages = runCheckOnlyImageGate(config, targetDir, dockerDiscovery);
12319
- const aptSteps = runWorkflowAptGate(targetDir);
12320
12480
  const { ctx, pendingWrites } = createDryRunContext(config, dockerDiscovery);
12321
12481
  logDetectionSummary(ctx);
12322
12482
  const results = await runGenerators(ctx);
12323
12483
  const failed = failedResults(results);
12324
12484
  reportFailures(failed, "repo:sync");
12325
12485
  const actionable = actionableResults(results, pendingWrites, targetDir);
12326
- const advisoryWarnCount = (toolchain.warned ? 1 : 0) + checkOnlyImages.length + aptSteps.length;
12327
- const toolchainState = { warned: toolchain.warned };
12328
- if (actionable.length === 0 && failed.length === 0) return upToDateReport(toolchainState, checkOnlyImages, aptSteps);
12486
+ const advisoryCount = advisoryWarnCount(advisories);
12487
+ if (actionable.length === 0 && failed.length === 0) return upToDateReport(advisories);
12329
12488
  if (actionable.length === 0) return {
12330
12489
  status: "failed",
12331
12490
  actionable: [],
12332
12491
  failed,
12333
- toolchain: toolchainState,
12334
12492
  checkOnlyImages: [],
12335
12493
  aptSteps: [],
12494
+ inertAgentKeys: [],
12336
12495
  warnCount: 0,
12337
12496
  exitCode: 1
12338
12497
  };
12339
12498
  log$1.warn(`${actionable.length} file(s) would be changed by repo:sync`);
12340
12499
  for (const r of actionable) logActionableChange(r, targetDir, pendingWrites);
12341
12500
  if (failed.length === 0 && actionable.every((r) => isWorkflowDirPath(r.filePath))) {
12342
- const warnCount = actionable.length + advisoryWarnCount;
12501
+ const warnCount = actionable.length + advisoryCount;
12343
12502
  if (!isJsonMode()) rawLog$1(formatWarnMarker(warnCount));
12344
12503
  return {
12345
12504
  status: "workflow-drift",
12346
12505
  actionable,
12347
12506
  failed: [],
12348
- toolchain: toolchainState,
12349
- checkOnlyImages,
12350
- aptSteps,
12507
+ checkOnlyImages: advisories.checkOnlyImages,
12508
+ aptSteps: advisories.aptSteps,
12509
+ inertAgentKeys: advisories.inertAgentKeys,
12351
12510
  warnCount,
12352
12511
  exitCode: 0
12353
12512
  };
@@ -12356,9 +12515,9 @@ async function runCheckReport(targetDir) {
12356
12515
  status: "drift",
12357
12516
  actionable,
12358
12517
  failed,
12359
- toolchain: toolchainState,
12360
12518
  checkOnlyImages: [],
12361
12519
  aptSteps: [],
12520
+ inertAgentKeys: [],
12362
12521
  warnCount: 0,
12363
12522
  exitCode: 1
12364
12523
  };
@@ -12715,8 +12874,9 @@ function childOutput(result) {
12715
12874
  }
12716
12875
  //#endregion
12717
12876
  //#region src/release/publish/version.ts
12718
- const MAIN_REF = "refs/heads/main";
12719
- const BRANCH_REF = `refs/heads/${VERSION_PR_BRANCH}`;
12877
+ function defaultBranchRef(config) {
12878
+ return `refs/heads/${config.defaultBranch}`;
12879
+ }
12720
12880
  function readRemoteRef(executor, config, auth, ref) {
12721
12881
  const result = executor.exec("git", [
12722
12882
  ...auth.args,
@@ -12728,13 +12888,13 @@ function readRemoteRef(executor, config, auth, ref) {
12728
12888
  return result.stdout.split("\n").find((entry) => entry.trim().endsWith(`\t${ref}`))?.split(" ")[0]?.trim() ?? "";
12729
12889
  }
12730
12890
  function isSuperseded(executor, config, auth, baseSha) {
12731
- const remoteMain = readRemoteRef(executor, config, auth, MAIN_REF);
12732
- if (remoteMain === null || remoteMain === "" || baseSha === "") {
12733
- debug(config, "Supersession check: main tip or base commit unreadable -- proceeding");
12891
+ const remoteDefault = readRemoteRef(executor, config, auth, defaultBranchRef(config));
12892
+ if (remoteDefault === null || remoteDefault === "" || baseSha === "") {
12893
+ debug(config, `Supersession check: ${config.defaultBranch} tip or base commit unreadable -- proceeding`);
12734
12894
  return false;
12735
12895
  }
12736
- if (remoteMain === baseSha) return false;
12737
- log$1.warn(`Superseded release run: this job versioned ${baseSha}, but main has advanced to ${remoteMain}. A newer run is versioning a superset of these changesets -- skipping to avoid overwriting its version commit on ${VERSION_PR_BRANCH}.`);
12896
+ if (remoteDefault === baseSha) return false;
12897
+ log$1.warn(`Superseded release run: this job versioned ${baseSha}, but ${config.defaultBranch} has advanced to ${remoteDefault}. A newer run is versioning a superset of these changesets -- skipping to avoid overwriting its version commit on ${versionPrBranch(config.defaultBranch)}.`);
12738
12898
  return true;
12739
12899
  }
12740
12900
  const DATELESS_VERSION_RE = /^## (\d+\.\d+\.\d+(?:-[^\s)]+)?)$/gm;
@@ -12821,12 +12981,14 @@ function buildPrContent(executor, cwd, packagesBefore) {
12821
12981
  async function runVersionMode(executor, config) {
12822
12982
  log$1.info("Changesets detected -- versioning packages");
12823
12983
  const auth = buildGitAuth(config);
12984
+ const branch = versionPrBranch(config.defaultBranch);
12985
+ const branchRef = `refs/heads/${branch}`;
12824
12986
  const baseShaResult = executor.exec("git", ["rev-parse", "HEAD"], { cwd: config.cwd });
12825
12987
  const baseSha = baseShaResult.exitCode === 0 ? baseShaResult.stdout.trim() : "";
12826
- const leaseTip = config.dryRun ? null : readRemoteRef(executor, config, auth, BRANCH_REF);
12827
- if (leaseTip === null && !config.dryRun) throw new FatalError(`Could not read the current tip of ${VERSION_PR_BRANCH} from the remote, so this run cannot take a push lease. Refusing to force-push without one -- an unleased push is what allows a concurrent run's version commit to be silently discarded.`);
12988
+ const leaseTip = config.dryRun ? null : readRemoteRef(executor, config, auth, branchRef);
12989
+ if (leaseTip === null && !config.dryRun) throw new FatalError(`Could not read the current tip of ${branch} from the remote, so this run cannot take a push lease. Refusing to force-push without one -- an unleased push is what allows a concurrent run's version commit to be silently discarded.`);
12828
12990
  debug(config, `Base commit: ${baseSha || "(unknown)"}`);
12829
- debug(config, `Lease tip for ${VERSION_PR_BRANCH}: ${leaseTip === null ? "(dry-run)" : leaseTip || "(absent)"}`);
12991
+ debug(config, `Lease tip for ${branch}: ${leaseTip === null ? "(dry-run)" : leaseTip || "(absent)"}`);
12830
12992
  const packagesBefore = executor.listWorkspacePackages(config.cwd);
12831
12993
  debug(config, `Packages before versioning: ${packagesBefore.map((pkg) => `${pkg.name}@${pkg.version}`).join(", ") || "(none)"}`);
12832
12994
  const changesetConfigPath = path.join(config.cwd, ".changeset", "config.json");
@@ -12888,28 +13050,28 @@ async function runVersionMode(executor, config) {
12888
13050
  pr: "none"
12889
13051
  };
12890
13052
  if (leaseTip === null) throw new UnexpectedError("Push lease was never observed on a non-dry-run release");
12891
- debug(config, `Force-pushing version commit to ${VERSION_PR_BRANCH} (lease: ${leaseTip || "(absent)"})`);
13053
+ debug(config, `Force-pushing version commit to ${branch} (lease: ${leaseTip || "(absent)"})`);
12892
13054
  const pushResult = executor.exec("git", [
12893
13055
  ...auth.args,
12894
13056
  "push",
12895
13057
  auth.url,
12896
- `HEAD:${BRANCH_REF}`,
12897
- `--force-with-lease=${BRANCH_REF}:${leaseTip}`
13058
+ `HEAD:${branchRef}`,
13059
+ `--force-with-lease=${branchRef}:${leaseTip}`
12898
13060
  ], { cwd: config.cwd });
12899
13061
  debugExec(config, "git push", pushResult);
12900
- if (pushResult.exitCode !== 0) throw new FatalError(`git push to ${VERSION_PR_BRANCH} failed (exit code ${String(pushResult.exitCode)}). If the lease was rejected, the branch moved after this run started (expected ${leaseTip || "the branch to be absent"}) -- another run pushed a version commit, and overwriting it would discard that release. This job fails rather than clobber it.\n${pushResult.stderr || pushResult.stdout}`);
13062
+ if (pushResult.exitCode !== 0) throw new FatalError(`git push to ${branch} failed (exit code ${String(pushResult.exitCode)}). If the lease was rejected, the branch moved after this run started (expected ${leaseTip || "the branch to be absent"}) -- another run pushed a version commit, and overwriting it would discard that release. This job fails rather than clobber it.\n${pushResult.stderr || pushResult.stdout}`);
12901
13063
  const conn = {
12902
13064
  serverUrl: config.serverUrl,
12903
13065
  repository: config.repository,
12904
13066
  token: config.token
12905
13067
  };
12906
- const existingPr = await findOpenPr(executor, conn, VERSION_PR_BRANCH);
12907
- debug(config, `Existing open PR for ${VERSION_PR_BRANCH}: ${existingPr === null ? "(none)" : `#${String(existingPr)}`}`);
13068
+ const existingPr = await findOpenPr(executor, conn, branch);
13069
+ debug(config, `Existing open PR for ${branch}: ${existingPr === null ? "(none)" : `#${String(existingPr)}`}`);
12908
13070
  if (existingPr === null) try {
12909
13071
  await createPr(executor, conn, {
12910
13072
  title,
12911
- head: VERSION_PR_BRANCH,
12912
- base: "main",
13073
+ head: branch,
13074
+ base: config.defaultBranch,
12913
13075
  body
12914
13076
  });
12915
13077
  log$1.info("Created version PR");
@@ -12919,7 +13081,7 @@ async function runVersionMode(executor, config) {
12919
13081
  };
12920
13082
  } catch (error) {
12921
13083
  debug(config, `createPr failed, retrying as update: ${describeError$1(error)}`);
12922
- const racePr = await findOpenPr(executor, conn, VERSION_PR_BRANCH);
13084
+ const racePr = await findOpenPr(executor, conn, branch);
12923
13085
  if (racePr === null) throw error;
12924
13086
  await updatePr(executor, conn, racePr, {
12925
13087
  title,
@@ -13404,7 +13566,7 @@ const releaseForgejoCommand = defineCommand({
13404
13566
  const result = await runRelease(buildReleaseConfig({
13405
13567
  dryRun,
13406
13568
  verbose: args.verbose === true || isEnvVerbose()
13407
- }), createRealExecutor$1());
13569
+ }), createRealExecutor$1(), processCiEventSource());
13408
13570
  out.json(releasePayload(result, dryRun));
13409
13571
  if (result.mode === "publish") {
13410
13572
  const outputFile = process.env["GITHUB_OUTPUT"];
@@ -13445,6 +13607,7 @@ function buildReleaseConfig(flags) {
13445
13607
  return {
13446
13608
  ...resolved.conn,
13447
13609
  cwd: process.cwd(),
13610
+ defaultBranch: readConfiguredDefaultBranch(process.cwd()),
13448
13611
  dryRun: flags.dryRun ?? false,
13449
13612
  verbose: flags.verbose ?? false
13450
13613
  };
@@ -13458,15 +13621,16 @@ function getCurrentBranch(executor, cwd) {
13458
13621
  "HEAD"
13459
13622
  ], { cwd }).stdout.trim();
13460
13623
  }
13461
- async function runRelease(config, executor) {
13624
+ async function runRelease(config, executor, ciEvent) {
13462
13625
  debug(config, `Repository: ${config.repository} on ${config.serverUrl}`);
13463
13626
  debug(config, `Config: dryRun=${String(config.dryRun)}`);
13464
13627
  const branch = getCurrentBranch(executor, config.cwd);
13465
13628
  debug(config, `Current branch: ${branch}`);
13466
- if (branch !== "main") {
13467
- debug(config, `Skipping release on non-main branch: ${branch}`);
13629
+ if (branch !== config.defaultBranch) {
13630
+ debug(config, `Skipping release: ${branch} is not the configured default branch (${config.defaultBranch})`);
13468
13631
  return { mode: "none" };
13469
13632
  }
13633
+ refuseForgeDefaultMismatch(config.defaultBranch, ciEvent, "release:changesets");
13470
13634
  const identity = await configureGitIdentity(executor, "forgejo", config.cwd, config);
13471
13635
  log$1.info(`Git identity: ${identity.name} <${identity.email}>`);
13472
13636
  const changesetFiles = executor.listChangesetFiles(config.cwd);
@@ -13668,7 +13832,7 @@ const releaseTriggerCommand = defineCommand({
13668
13832
  args: {
13669
13833
  ref: {
13670
13834
  type: "string",
13671
- description: "Git ref to trigger on (default: main)",
13835
+ description: "Git ref to trigger on (default: .tooling.json#defaultBranch, else main)",
13672
13836
  required: false
13673
13837
  },
13674
13838
  bump: {
@@ -13688,7 +13852,7 @@ const releaseTriggerCommand = defineCommand({
13688
13852
  },
13689
13853
  async run({ args }) {
13690
13854
  const out = createCliOutput(args.json === true);
13691
- const ref = args.ref ?? "main";
13855
+ const ref = args.ref ?? readConfiguredDefaultBranch(process.cwd());
13692
13856
  const inputs = {};
13693
13857
  if (args.bump) inputs["bump"] = args.bump;
13694
13858
  if (args.prerelease) inputs["prerelease"] = args.prerelease;
@@ -13705,33 +13869,131 @@ const releaseTriggerCommand = defineCommand({
13705
13869
  }
13706
13870
  });
13707
13871
  //#endregion
13708
- //#region ../forgejo/src/issue-comments.ts
13709
- const CommentResponseSchema = z.object({
13872
+ //#region ../forgejo/src/label-provisioning.ts
13873
+ const LabelSchema$1 = z.object({
13710
13874
  id: z.number(),
13711
- body: z.string().nullish(),
13712
- user: z.object({ login: z.string() }).nullish(),
13713
- created_at: z.string(),
13714
- updated_at: z.string().nullish(),
13715
- html_url: z.string().nullish(),
13716
- assets: AttachmentListSchema.nullish()
13875
+ name: z.string(),
13876
+ color: z.string().optional(),
13877
+ description: z.string().optional(),
13878
+ exclusive: z.boolean().optional()
13717
13879
  });
13718
- const CommentListSchema = z.array(CommentResponseSchema);
13719
- function parseCommentOrThrow(raw) {
13720
- const parsed = CommentResponseSchema.safeParse(raw);
13721
- if (!parsed.success) throw new UnexpectedError(`Unexpected comment response: ${parsed.error.message}`);
13722
- return parsed.data;
13880
+ const LabelListSchema = z.array(LabelSchema$1);
13881
+ const LABELS_PAGE_SIZE = 100;
13882
+ const LABELS_MAX_PAGES = 20;
13883
+ async function fetchAllForgejoLabels(executor, conn) {
13884
+ const labels = [];
13885
+ for (let page = 1; page <= LABELS_MAX_PAGES; page++) {
13886
+ const res = await executor.fetch(`${conn.serverUrl}/api/v1/repos/${conn.repository}/labels?limit=${String(LABELS_PAGE_SIZE)}&page=${String(page)}`, { headers: authHeaders$2(conn) });
13887
+ if (!res.ok) await throwForgejoFailure(res, {
13888
+ verb: "list labels",
13889
+ resource: conn.repository
13890
+ }, repoNotFoundMessage(conn));
13891
+ const parsed = LabelListSchema.safeParse(await res.json());
13892
+ if (!parsed.success) throw new UnexpectedError(`Unexpected label-list response: ${parsed.error.message}`);
13893
+ labels.push(...parsed.data);
13894
+ if (parsed.data.length < LABELS_PAGE_SIZE) break;
13895
+ }
13896
+ return labels;
13723
13897
  }
13724
- function parseCommentListOrThrow(raw) {
13725
- const parsed = CommentListSchema.safeParse(raw);
13726
- if (!parsed.success) throw new UnexpectedError(`Unexpected comment-list response: ${parsed.error.message}`);
13727
- return parsed.data;
13898
+ async function resolveLabelIds(executor, conn, names) {
13899
+ if (names.length === 0) return [];
13900
+ const labels = await fetchAllForgejoLabels(executor, conn);
13901
+ return names.map((name) => {
13902
+ const match = labels.find((label) => label.name === name);
13903
+ if (match === void 0) {
13904
+ const known = labels.map((label) => label.name).join(", ");
13905
+ throw new FatalError(`No label named "${name}" found among the repository's labels in ${conn.repository} (organization-level labels are not searched). Known labels: ${known}`);
13906
+ }
13907
+ return match.id;
13908
+ });
13728
13909
  }
13729
- function normalizeComment(raw) {
13730
- return {
13731
- id: raw.id,
13732
- author: raw.user?.login ?? "",
13733
- createdAt: raw.created_at,
13734
- updatedAt: raw.updated_at ?? raw.created_at,
13910
+ const LabelSpecSchema = z.strictObject({
13911
+ name: z.string().min(1),
13912
+ color: z.string().regex(/^#[0-9a-fA-F]{6}$/),
13913
+ description: z.string(),
13914
+ exclusive: z.boolean().optional()
13915
+ });
13916
+ z.record(z.string(), z.array(LabelSpecSchema));
13917
+ //#endregion
13918
+ //#region ../forgejo/src/issue-labels.ts
13919
+ const TimelineEntrySchema = z.object({
13920
+ id: z.number(),
13921
+ type: z.string(),
13922
+ body: z.string().nullish(),
13923
+ created_at: z.string(),
13924
+ label: z.object({ name: z.string() }).nullish(),
13925
+ user: z.object({ login: z.string() }).nullish()
13926
+ });
13927
+ z.array(TimelineEntrySchema).nullable();
13928
+ async function resolveLabelId(executor, conn, name, labelIds) {
13929
+ const cached = labelIds?.get(name);
13930
+ if (cached !== void 0) return cached;
13931
+ const [id] = await resolveLabelIds(executor, conn, [name]);
13932
+ if (id === void 0) throw new UnexpectedError(`Label id resolution returned nothing for "${name}"`);
13933
+ return id;
13934
+ }
13935
+ async function addForgejoIssueLabel(executor, conn, number, name, labelIds) {
13936
+ const id = await resolveLabelId(executor, conn, name, labelIds);
13937
+ const res = await executor.fetch(`${issuesBase(conn)}/${String(number)}/labels`, {
13938
+ method: "POST",
13939
+ headers: jsonHeaders(conn),
13940
+ body: JSON.stringify({ labels: [id] })
13941
+ }, { idempotent: true });
13942
+ if (!res.ok) await throwForgejoFailure(res, {
13943
+ verb: "add label",
13944
+ resource: `#${String(number)}`
13945
+ }, issueNotFoundMessage(conn, number));
13946
+ }
13947
+ async function removeForgejoIssueLabel(executor, conn, number, name, labelIds) {
13948
+ const id = await resolveLabelId(executor, conn, name, labelIds);
13949
+ const res = await executor.fetch(`${issuesBase(conn)}/${String(number)}/labels/${String(id)}`, {
13950
+ method: "DELETE",
13951
+ headers: authHeaders$2(conn)
13952
+ }, { idempotent: true });
13953
+ if (!res.ok) await throwForgejoFailure(res, {
13954
+ verb: "remove label",
13955
+ resource: `#${String(number)}`
13956
+ }, issueNotFoundMessage(conn, number));
13957
+ }
13958
+ async function replaceForgejoIssueLabels(executor, conn, number, labelIds) {
13959
+ const res = await executor.fetch(`${issuesBase(conn)}/${String(number)}/labels`, {
13960
+ method: "PUT",
13961
+ headers: jsonHeaders(conn),
13962
+ body: JSON.stringify({ labels: labelIds })
13963
+ }, { idempotent: true });
13964
+ if (!res.ok) await throwForgejoFailure(res, {
13965
+ verb: "replace labels",
13966
+ resource: `#${String(number)}`
13967
+ }, issueNotFoundMessage(conn, number));
13968
+ }
13969
+ //#endregion
13970
+ //#region ../forgejo/src/issue-comments.ts
13971
+ const CommentResponseSchema = z.object({
13972
+ id: z.number(),
13973
+ body: z.string().nullish(),
13974
+ user: z.object({ login: z.string() }).nullish(),
13975
+ created_at: z.string(),
13976
+ updated_at: z.string().nullish(),
13977
+ html_url: z.string().nullish(),
13978
+ assets: AttachmentListSchema.nullish()
13979
+ });
13980
+ const CommentListSchema = z.array(CommentResponseSchema);
13981
+ function parseCommentOrThrow(raw) {
13982
+ const parsed = CommentResponseSchema.safeParse(raw);
13983
+ if (!parsed.success) throw new UnexpectedError(`Unexpected comment response: ${parsed.error.message}`);
13984
+ return parsed.data;
13985
+ }
13986
+ function parseCommentListOrThrow(raw) {
13987
+ const parsed = CommentListSchema.safeParse(raw);
13988
+ if (!parsed.success) throw new UnexpectedError(`Unexpected comment-list response: ${parsed.error.message}`);
13989
+ return parsed.data;
13990
+ }
13991
+ function normalizeComment(raw) {
13992
+ return {
13993
+ id: raw.id,
13994
+ author: raw.user?.login ?? "",
13995
+ createdAt: raw.created_at,
13996
+ updatedAt: raw.updated_at ?? raw.created_at,
13735
13997
  body: raw.body ?? "",
13736
13998
  ...raw.html_url === void 0 || raw.html_url === null ? {} : { url: raw.html_url },
13737
13999
  attachments: (raw.assets ?? []).map(normalizeAttachment)
@@ -13780,52 +14042,6 @@ async function addForgejoComment(executor, conn, number, body) {
13780
14042
  return normalizeComment(parseCommentOrThrow(await res.json()));
13781
14043
  }
13782
14044
  //#endregion
13783
- //#region ../forgejo/src/label-provisioning.ts
13784
- const LabelSchema$1 = z.object({
13785
- id: z.number(),
13786
- name: z.string(),
13787
- color: z.string().optional(),
13788
- description: z.string().optional(),
13789
- exclusive: z.boolean().optional()
13790
- });
13791
- const LabelListSchema = z.array(LabelSchema$1);
13792
- const LABELS_PAGE_SIZE = 100;
13793
- const LABELS_MAX_PAGES = 20;
13794
- async function fetchAllForgejoLabels(executor, conn) {
13795
- const labels = [];
13796
- for (let page = 1; page <= LABELS_MAX_PAGES; page++) {
13797
- const res = await executor.fetch(`${conn.serverUrl}/api/v1/repos/${conn.repository}/labels?limit=${String(LABELS_PAGE_SIZE)}&page=${String(page)}`, { headers: authHeaders$2(conn) });
13798
- if (!res.ok) await throwForgejoFailure(res, {
13799
- verb: "list labels",
13800
- resource: conn.repository
13801
- }, repoNotFoundMessage(conn));
13802
- const parsed = LabelListSchema.safeParse(await res.json());
13803
- if (!parsed.success) throw new UnexpectedError(`Unexpected label-list response: ${parsed.error.message}`);
13804
- labels.push(...parsed.data);
13805
- if (parsed.data.length < LABELS_PAGE_SIZE) break;
13806
- }
13807
- return labels;
13808
- }
13809
- async function resolveLabelIds(executor, conn, names) {
13810
- if (names.length === 0) return [];
13811
- const labels = await fetchAllForgejoLabels(executor, conn);
13812
- return names.map((name) => {
13813
- const match = labels.find((label) => label.name === name);
13814
- if (match === void 0) {
13815
- const known = labels.map((label) => label.name).join(", ");
13816
- throw new FatalError(`No label named "${name}" found among the repository's labels in ${conn.repository} (organization-level labels are not searched). Known labels: ${known}`);
13817
- }
13818
- return match.id;
13819
- });
13820
- }
13821
- const LabelSpecSchema = z.strictObject({
13822
- name: z.string().min(1),
13823
- color: z.string().regex(/^#[0-9a-fA-F]{6}$/),
13824
- description: z.string(),
13825
- exclusive: z.boolean().optional()
13826
- });
13827
- z.record(z.string(), z.array(LabelSpecSchema));
13828
- //#endregion
13829
14045
  //#region ../forgejo/src/issues.ts
13830
14046
  const IssueResponseSchema = z.object({
13831
14047
  number: z.number(),
@@ -14271,6 +14487,178 @@ function resolveIssueTransport(executor, cwd, target, injectedToken, signal = nu
14271
14487
  };
14272
14488
  }
14273
14489
  //#endregion
14490
+ //#region src/work-status/labels.ts
14491
+ const SCOPE_ORDER = [
14492
+ "agent",
14493
+ "agent-flags",
14494
+ "agent-note",
14495
+ "priority",
14496
+ "other"
14497
+ ];
14498
+ function scopeOf(name) {
14499
+ const separator = name.indexOf("/");
14500
+ if (separator < 0) return "other";
14501
+ const prefix = name.slice(0, separator);
14502
+ if (prefix === "agent" || prefix === "agent-flags" || prefix === "agent-note" || prefix === "priority") return prefix;
14503
+ return "other";
14504
+ }
14505
+ function groupLabels(names, descriptions) {
14506
+ const groups = [];
14507
+ for (const scope of SCOPE_ORDER) {
14508
+ const inScope = names.filter((name) => scopeOf(name) === scope).toSorted((a, b) => a.localeCompare(b)).map((name) => ({
14509
+ name,
14510
+ description: descriptions.get(name) ?? null
14511
+ }));
14512
+ if (inScope.length === 0) continue;
14513
+ groups.push({
14514
+ scope,
14515
+ labels: inScope
14516
+ });
14517
+ }
14518
+ return groups;
14519
+ }
14520
+ const RUNNING_LABEL = /^agent\/[a-z-]+-running$/;
14521
+ const HAND_AUTHORED_LABEL = "agent-flags/hand-authored";
14522
+ function liftedFacts(names) {
14523
+ return {
14524
+ runLive: names.filter((name) => RUNNING_LABEL.test(name)).toSorted(),
14525
+ heldByHand: names.includes(HAND_AUTHORED_LABEL)
14526
+ };
14527
+ }
14528
+ function labelNamesOf(section) {
14529
+ const names = [];
14530
+ for (const group of [...section.pr, ...section.issue]) for (const label of group.labels) names.push(label.name);
14531
+ return names;
14532
+ }
14533
+ function ownerOf(names) {
14534
+ const agent = names.filter((name) => name.startsWith("agent/")).toSorted();
14535
+ const running = agent.find((name) => RUNNING_LABEL.test(name));
14536
+ if (running !== void 0) return {
14537
+ owner: "agent",
14538
+ label: running,
14539
+ running: true
14540
+ };
14541
+ if (names.includes("agent-flags/hand-authored")) return { owner: "person" };
14542
+ const held = agent[0];
14543
+ if (held !== void 0) return {
14544
+ owner: "agent",
14545
+ label: held,
14546
+ running: false
14547
+ };
14548
+ return { owner: "unclaimed" };
14549
+ }
14550
+ //#endregion
14551
+ //#region src/work-status/claim.ts
14552
+ const HANDOVER_ROUTES = "Claim it from the control panel's \"Claim for hand work\", or remove the agent label by hand and run bst work:start again.";
14553
+ function decideClaim(report, enabled) {
14554
+ if (!enabled) return {
14555
+ kind: "answer",
14556
+ exitCode: 0,
14557
+ result: {
14558
+ outcome: "not-agent-repo",
14559
+ reason: "Nothing was claimed: this repository's default branch does not set agent.enabled, so the agent does not work it and there is nothing to claim against."
14560
+ }
14561
+ };
14562
+ if (report.owner === null) return {
14563
+ kind: "answer",
14564
+ exitCode: 1,
14565
+ result: {
14566
+ outcome: "failed",
14567
+ reason: "A label read failed, so a run may be live on this object unseen. Nothing was claimed; retry once the forge read works."
14568
+ }
14569
+ };
14570
+ const held = ownershipOf(report);
14571
+ if (held.kind === "running") return {
14572
+ kind: "answer",
14573
+ exitCode: 1,
14574
+ result: {
14575
+ outcome: "refused",
14576
+ label: held.label,
14577
+ reason: `A run is live on this object (${held.label}); a claim does not stop it.`
14578
+ }
14579
+ };
14580
+ if (held.kind === "person") return {
14581
+ kind: "answer",
14582
+ exitCode: 0,
14583
+ result: { outcome: "already-claimed" }
14584
+ };
14585
+ if (held.kind === "agent") return {
14586
+ kind: "answer",
14587
+ exitCode: 1,
14588
+ result: {
14589
+ outcome: "refused",
14590
+ label: held.label,
14591
+ reason: `The agent holds this object (${held.label}). ${HANDOVER_ROUTES}`
14592
+ }
14593
+ };
14594
+ return { kind: "write" };
14595
+ }
14596
+ function ownershipOf(report) {
14597
+ if (report.labels.state === "ok") {
14598
+ const ownership = ownerOf(labelNamesOf(report.labels));
14599
+ if (ownership.owner === "agent") return ownership.running ? {
14600
+ kind: "running",
14601
+ label: ownership.label
14602
+ } : {
14603
+ kind: "agent",
14604
+ label: ownership.label
14605
+ };
14606
+ if (ownership.owner === "person") return { kind: "person" };
14607
+ return { kind: "unclaimed" };
14608
+ }
14609
+ if (report.owner === "agent") {
14610
+ const live = report.runLive?.[0];
14611
+ return live === void 0 ? {
14612
+ kind: "agent",
14613
+ label: "an agent label"
14614
+ } : {
14615
+ kind: "running",
14616
+ label: live
14617
+ };
14618
+ }
14619
+ return report.owner === "person" ? { kind: "person" } : { kind: "unclaimed" };
14620
+ }
14621
+ async function writeClaim(executor, cwd, target, number) {
14622
+ try {
14623
+ const { conn } = requireForgejoTransport(executor, cwd, target, "bst work:start claims work on Forgejo, the forge the agent drives; a GitHub checkout has no agent to hand work to.");
14624
+ const flag = (await fetchAllForgejoLabels(executor, conn)).find((label) => label.name === HAND_AUTHORED_LABEL);
14625
+ if (flag === void 0) return {
14626
+ outcome: "failed",
14627
+ reason: `The forge has no label named ${HAND_AUTHORED_LABEL}. Provision the agent's labels first with bst-agent agent:labels --repo ${conn.repository}, or the control panel's Provision labels.`
14628
+ };
14629
+ await addForgejoIssueLabel(executor, conn, number, HAND_AUTHORED_LABEL, /* @__PURE__ */ new Map([[HAND_AUTHORED_LABEL, flag.id]]));
14630
+ return {
14631
+ outcome: "claimed",
14632
+ label: HAND_AUTHORED_LABEL
14633
+ };
14634
+ } catch (error) {
14635
+ return {
14636
+ outcome: "failed",
14637
+ reason: describeError$1(error)
14638
+ };
14639
+ }
14640
+ }
14641
+ //#endregion
14642
+ //#region src/work-status/consent.ts
14643
+ async function readAgentConsent(git, defaultBranch) {
14644
+ const onBranch = await git.show(`origin/${defaultBranch}`, ".tooling.json");
14645
+ if (onBranch !== null) return {
14646
+ enabled: agentEnabledIn(onBranch),
14647
+ source: "default-branch"
14648
+ };
14649
+ return {
14650
+ enabled: agentEnabledIn(readWorkingTree(git.topLevel)),
14651
+ source: "working-tree"
14652
+ };
14653
+ }
14654
+ function readWorkingTree(topLevel) {
14655
+ try {
14656
+ return readFileSync(path.join(topLevel, ".tooling.json"), "utf8");
14657
+ } catch {
14658
+ return;
14659
+ }
14660
+ }
14661
+ //#endregion
14274
14662
  //#region ../forgejo/src/issue-run.ts
14275
14663
  var NothingToUpdateError = class extends FatalError {
14276
14664
  constructor() {
@@ -15162,6 +15550,42 @@ async function createWorkGit(executor, cwd) {
15162
15550
  };
15163
15551
  }
15164
15552
  //#endregion
15553
+ //#region src/work-status/prepare.ts
15554
+ function tryResolveWorkForge(executor, topLevel, target) {
15555
+ try {
15556
+ return resolveWorkForge(executor, topLevel, target);
15557
+ } catch (error) {
15558
+ return error instanceof Error ? error : new UnexpectedError(String(error));
15559
+ }
15560
+ }
15561
+ function sameRepository(one, other) {
15562
+ return one.toLowerCase() === other.toLowerCase();
15563
+ }
15564
+ function refNamesAnotherRepository(checkoutRepository, refRepository) {
15565
+ return new FatalError(`Usage: this command reads this checkout's repository (${checkoutRepository}); the ref names ${refRepository}`);
15566
+ }
15567
+ async function prepareWorkStatus(refText, cwd) {
15568
+ const ref = refText === "" ? null : parseIssueRef(refText);
15569
+ const executor = createRealExecutor$1();
15570
+ const git = await createWorkGit(executor, cwd);
15571
+ const target = ref?.target;
15572
+ if (target !== void 0) {
15573
+ const checkout = await git.originRepository();
15574
+ if (checkout !== null && !sameRepository(target.repository, checkout)) throw refNamesAnotherRepository(checkout, target.repository);
15575
+ }
15576
+ let forge = tryResolveWorkForge(executor, git.topLevel);
15577
+ if (target !== void 0 && !(forge instanceof Error)) {
15578
+ if (!sameRepository(target.repository, forge.repository)) throw refNamesAnotherRepository(forge.repository, target.repository);
15579
+ forge = tryResolveWorkForge(executor, git.topLevel, target);
15580
+ }
15581
+ return {
15582
+ ref,
15583
+ executor,
15584
+ git,
15585
+ forge
15586
+ };
15587
+ }
15588
+ //#endregion
15165
15589
  //#region src/docs-check/discover.ts
15166
15590
  const DOCS_ROOT = "docs";
15167
15591
  const RETIRED_DIR = "retired";
@@ -15251,7 +15675,7 @@ function retireDestination(src, verb) {
15251
15675
  if (dir === DOCS_ROOT && LEGACY_ACTIVE_PATTERN.test(base)) return `${DOCS_ROOT}/${RETIRED_DIR}/${verb}-${base.slice(5)}`;
15252
15676
  return `${dir}/${RETIRED_DIR}/${base}`;
15253
15677
  }
15254
- function discoverPairs(exec, config) {
15678
+ function candidatePairs(exec, config) {
15255
15679
  const candidates = [];
15256
15680
  for (const active of [...conventionalActiveDirs(exec), ...config.extraDirs]) candidates.push({
15257
15681
  active,
@@ -15266,8 +15690,16 @@ function discoverPairs(exec, config) {
15266
15690
  activePattern: LEGACY_ACTIVE_PATTERN,
15267
15691
  enforced: isEnforced(DOCS_ROOT, config.enforced)
15268
15692
  });
15693
+ return candidates;
15694
+ }
15695
+ function matchableActiveDirs(exec, config) {
15696
+ const seen = /* @__PURE__ */ new Set();
15697
+ for (const pair of candidatePairs(exec, config)) if (exec.readDir(pair.active) !== void 0) seen.add(pair.active);
15698
+ return [...seen].toSorted();
15699
+ }
15700
+ function discoverPairs(exec, config) {
15269
15701
  const seen = /* @__PURE__ */ new Set();
15270
- return candidates.filter((pair) => {
15702
+ return candidatePairs(exec, config).filter((pair) => {
15271
15703
  if (seen.has(pair.active)) return false;
15272
15704
  if (!(listDocs(exec, pair.active, pair.activePattern).length > 0 || pair.retiredDirs.some((dir) => listDocs(exec, dir).length > 0))) return false;
15273
15705
  seen.add(pair.active);
@@ -17416,45 +17848,6 @@ function feedbackSince(comments, headCommitDate, cap, agentLogin) {
17416
17848
  };
17417
17849
  }
17418
17850
  //#endregion
17419
- //#region src/work-status/labels.ts
17420
- const SCOPE_ORDER = [
17421
- "agent",
17422
- "agent-flags",
17423
- "agent-note",
17424
- "priority",
17425
- "other"
17426
- ];
17427
- function scopeOf(name) {
17428
- const separator = name.indexOf("/");
17429
- if (separator < 0) return "other";
17430
- const prefix = name.slice(0, separator);
17431
- if (prefix === "agent" || prefix === "agent-flags" || prefix === "agent-note" || prefix === "priority") return prefix;
17432
- return "other";
17433
- }
17434
- function groupLabels(names, descriptions) {
17435
- const groups = [];
17436
- for (const scope of SCOPE_ORDER) {
17437
- const inScope = names.filter((name) => scopeOf(name) === scope).toSorted((a, b) => a.localeCompare(b)).map((name) => ({
17438
- name,
17439
- description: descriptions.get(name) ?? null
17440
- }));
17441
- if (inScope.length === 0) continue;
17442
- groups.push({
17443
- scope,
17444
- labels: inScope
17445
- });
17446
- }
17447
- return groups;
17448
- }
17449
- const RUNNING_LABEL = /^agent\/[a-z-]+-running$/;
17450
- const HAND_AUTHORED_LABEL = "agent-flags/hand-authored";
17451
- function liftedFacts(names) {
17452
- return {
17453
- runLive: names.filter((name) => RUNNING_LABEL.test(name)).toSorted(),
17454
- heldByHand: names.includes(HAND_AUTHORED_LABEL)
17455
- };
17456
- }
17457
- //#endregion
17458
17851
  //#region src/work-status/report.ts
17459
17852
  const COMMIT_CAP = 20;
17460
17853
  const FEEDBACK_CAP = 20;
@@ -17690,11 +18083,14 @@ function objectSection({ resolved, forgeDown }) {
17690
18083
  return ok(facts);
17691
18084
  }
17692
18085
  function liftedFactsOf(resolved, forgeDown) {
17693
- const read = liftedFacts(labelNames([resolved.pr, resolved.issue]));
18086
+ const names = labelNames([resolved.pr, resolved.issue]);
18087
+ const read = liftedFacts(names);
18088
+ const ownership = ownerOf(names);
17694
18089
  const complete = forgeDown === null && resolved.prFailed === null && resolved.issueFailed === null;
17695
18090
  return {
17696
18091
  runLive: complete || read.runLive.length > 0 ? read.runLive : null,
17697
- heldByHand: complete || read.heldByHand ? read.heldByHand : null
18092
+ heldByHand: complete || read.heldByHand ? read.heldByHand : null,
18093
+ owner: complete || ownership.owner !== "unclaimed" ? ownership.owner : null
17698
18094
  };
17699
18095
  }
17700
18096
  async function docsSection({ git, docsExtraDirs, resolved }) {
@@ -17810,6 +18206,7 @@ async function buildWorkStatus(deps, ref) {
17810
18206
  },
17811
18207
  runLive: lifted.runLive,
17812
18208
  heldByHand: lifted.heldByHand,
18209
+ owner: lifted.owner,
17813
18210
  object: await section(() => objectSection(inputs)),
17814
18211
  docs: await section(() => docsSection(inputs)),
17815
18212
  labels: await section(() => labelsSection(inputs)),
@@ -17851,13 +18248,22 @@ function heading({ kind, branch, head }) {
17851
18248
  if (head !== null) named.push(`head ${short(head)}`);
17852
18249
  return `work:status ${named.join(", ")}`;
17853
18250
  }
17854
- function renderWorkStatus(report) {
18251
+ function claimableNumber(report) {
18252
+ if (report.object.state !== "ok") return null;
18253
+ if (report.target.kind === "issue") return report.object.issue?.number ?? null;
18254
+ return report.object.pr?.number ?? report.object.issue?.number ?? null;
18255
+ }
18256
+ function renderWorkStatus(report, options) {
17855
18257
  const quiet = nothingInFlight(report);
17856
18258
  if (quiet !== null) return [heading(report.target), quiet].join("\n");
17857
18259
  const lines = [heading(report.target)];
17858
18260
  for (const label of report.runLive ?? []) lines.push(`RUN LIVE: ${label} -- a run is working this object; do not start another.`);
17859
18261
  if (report.runLive === null || report.heldByHand === null) lines.push("UNKNOWN: whether a run is live or a human holds this -- not every label could be read.");
17860
18262
  if (report.heldByHand === true) lines.push("HELD BY HAND: agent-flags/hand-authored -- a human claimed this one; do not drive it.");
18263
+ if (options?.agentEnabled === true && report.owner === "unclaimed") {
18264
+ const number = claimableNumber(report);
18265
+ if (number !== null) lines.push(`UNCLAIMED: no agent label and no hand claim -- run \`bst work:start ${String(number)}\` before you work this.`);
18266
+ }
17861
18267
  lines.push(...block("object", report.object, objectRows));
17862
18268
  lines.push(...block("docs", report.docs, (value) => {
17863
18269
  const rows = [` base ${value.base}`];
@@ -17906,20 +18312,77 @@ function renderWorkStatus(report) {
17906
18312
  return lines.join("\n");
17907
18313
  }
17908
18314
  //#endregion
17909
- //#region src/commands/work-status.ts
17910
- function tryResolveWorkForge(executor, topLevel, target) {
17911
- try {
17912
- return resolveWorkForge(executor, topLevel, target);
17913
- } catch (error) {
17914
- return error instanceof Error ? error : new UnexpectedError(String(error));
17915
- }
17916
- }
17917
- function sameRepository(one, other) {
17918
- return one.toLowerCase() === other.toLowerCase();
18315
+ //#region src/commands/work-start.ts
18316
+ function claimLine(result) {
18317
+ if (result.outcome === "claimed") return `claim: claimed -- ${result.label ?? "agent-flags/hand-authored"} is on this object.`;
18318
+ if (result.outcome === "already-claimed") return "claim: already-claimed -- the flag is already on it; nothing was written.";
18319
+ if (result.outcome === "not-agent-repo") return `claim: not-agent-repo -- ${result.reason ?? "nothing was claimed."}`;
18320
+ return `claim: ${result.outcome} -- ${result.reason ?? "nothing was claimed."}`;
17919
18321
  }
17920
- function refNamesAnotherRepository(checkoutRepository, refRepository) {
17921
- return new FatalError(`Usage: bst work:status reads this checkout's repository (${checkoutRepository}); the ref names ${refRepository}`);
18322
+ function claimFailed(result) {
18323
+ return result.outcome === "refused" || result.outcome === "failed";
17922
18324
  }
18325
+ const workStartCommand = defineCommand({
18326
+ meta: {
18327
+ name: "work:start",
18328
+ description: "Claim an issue or pull request for hand work: put agent-flags/hand-authored on it so the agent leaves it alone"
18329
+ },
18330
+ args: {
18331
+ ref: {
18332
+ type: "positional",
18333
+ required: true,
18334
+ description: "Issue or pull request number or URL to claim"
18335
+ },
18336
+ json: {
18337
+ type: "boolean",
18338
+ description: "Emit one JSON document: the whole work:status report plus a claim member {outcome, label?, reason?}. outcome is claimed (the flag was written), already-claimed (it was already on the object, nothing written), not-agent-repo (the default branch does not set agent.enabled, nothing written), refused (a live run or an agent label holds it; exit 1), or failed (a read or write could not be done; exit 1). refused and failed carry ok: false and an error member of kind runtime, and the report survives beside them."
18339
+ },
18340
+ verbose: {
18341
+ type: "boolean",
18342
+ description: "Emit detailed debug logging"
18343
+ }
18344
+ },
18345
+ async run({ args }) {
18346
+ const { ref, executor, git, forge } = await prepareWorkStatus(typeof args.ref === "string" ? args.ref.trim() : "", process.cwd());
18347
+ const number = ref?.number;
18348
+ if (number === void 0) throw new FatalError("Usage: bst work:start needs an issue or pull request number or URL");
18349
+ const docs = resolveDocsConfig(git.topLevel);
18350
+ const defaultBranch = resolveDefaultBranch({ cwd: git.topLevel });
18351
+ const report = await buildWorkStatus({
18352
+ git,
18353
+ forge,
18354
+ docsExtraDirs: docs.extraDirs,
18355
+ defaultBranch
18356
+ }, ref);
18357
+ const consent = await readAgentConsent(git, defaultBranch);
18358
+ const decision = decideClaim(report, consent.enabled);
18359
+ let result;
18360
+ let exitCode;
18361
+ if (decision.kind === "write") {
18362
+ result = await writeClaim(executor, git.topLevel, ref?.target, number);
18363
+ exitCode = claimFailed(result) ? 1 : 0;
18364
+ } else {
18365
+ result = decision.result;
18366
+ exitCode = decision.exitCode;
18367
+ }
18368
+ const out = createCliOutput(args.json === true);
18369
+ const failed = claimFailed(result);
18370
+ out.json({
18371
+ ok: !failed,
18372
+ ...report,
18373
+ claim: result,
18374
+ ...failed ? { error: {
18375
+ kind: "runtime",
18376
+ message: result.reason ?? result.outcome
18377
+ } } : {}
18378
+ });
18379
+ out.text(renderWorkStatus(report, { agentEnabled: consent.enabled }));
18380
+ out.text(claimLine(result));
18381
+ if (exitCode !== 0) process.exitCode = exitCode;
18382
+ }
18383
+ });
18384
+ //#endregion
18385
+ //#region src/commands/work-status.ts
17923
18386
  const workStatusCommand = defineCommand({
17924
18387
  meta: {
17925
18388
  name: "work:status",
@@ -17933,7 +18396,7 @@ const workStatusCommand = defineCommand({
17933
18396
  },
17934
18397
  json: {
17935
18398
  type: "boolean",
17936
- description: "Emit one JSON document: target, runLive, heldByHand (each null when not every label could be read), and six sections (object, docs, labels, feedback, findings, commits), each {state: ok} with its fields, {state: absent, reason} or {state: unavailable, reason}. The output is advice, not a gate: the feedback cutoff is the newest agent run or review comment on the pull request and is approximate, and review comments on diff lines are not in the thread this reads."
18399
+ description: "Emit one JSON document: target, runLive, heldByHand, owner (agent, person or unclaimed; each null when not every label could be read), and six sections (object, docs, labels, feedback, findings, commits), each {state: ok} with its fields, {state: absent, reason} or {state: unavailable, reason}. The output is advice, not a gate: the feedback cutoff is the newest agent run or review comment on the pull request and is approximate, and review comments on diff lines are not in the thread this reads."
17937
18400
  },
17938
18401
  verbose: {
17939
18402
  type: "boolean",
@@ -17941,20 +18404,7 @@ const workStatusCommand = defineCommand({
17941
18404
  }
17942
18405
  },
17943
18406
  async run({ args }) {
17944
- const refText = typeof args.ref === "string" ? args.ref.trim() : "";
17945
- const ref = refText === "" ? null : parseIssueRef(refText);
17946
- const executor = createRealExecutor$1();
17947
- const git = await createWorkGit(executor, process.cwd());
17948
- const target = ref?.target;
17949
- if (target !== void 0) {
17950
- const checkout = await git.originRepository();
17951
- if (checkout !== null && !sameRepository(target.repository, checkout)) throw refNamesAnotherRepository(checkout, target.repository);
17952
- }
17953
- let forge = tryResolveWorkForge(executor, git.topLevel);
17954
- if (target !== void 0 && !(forge instanceof Error)) {
17955
- if (!sameRepository(target.repository, forge.repository)) throw refNamesAnotherRepository(forge.repository, target.repository);
17956
- forge = tryResolveWorkForge(executor, git.topLevel, target);
17957
- }
18407
+ const { ref, git, forge } = await prepareWorkStatus(typeof args.ref === "string" ? args.ref.trim() : "", process.cwd());
17958
18408
  const docs = resolveDocsConfig(git.topLevel);
17959
18409
  const defaultBranch = resolveDefaultBranch({ cwd: git.topLevel });
17960
18410
  const report = await buildWorkStatus({
@@ -17963,12 +18413,13 @@ const workStatusCommand = defineCommand({
17963
18413
  docsExtraDirs: docs.extraDirs,
17964
18414
  defaultBranch
17965
18415
  }, ref);
18416
+ const consent = await readAgentConsent(git, defaultBranch);
17966
18417
  const out = createCliOutput(args.json === true);
17967
18418
  out.json({
17968
18419
  ok: true,
17969
18420
  ...report
17970
18421
  });
17971
- out.text(renderWorkStatus(report));
18422
+ out.text(renderWorkStatus(report, { agentEnabled: consent.enabled }));
17972
18423
  }
17973
18424
  });
17974
18425
  //#endregion
@@ -18224,8 +18675,9 @@ const PASSED_VERDICT = {
18224
18675
  reason: null
18225
18676
  };
18226
18677
  async function mergeVersionPrForgejo(executor, conn, options) {
18227
- const prNumber = await findOpenPr(executor, conn, VERSION_PR_BRANCH);
18228
- if (prNumber === null) throw new FatalError(`No open PR found for branch ${VERSION_PR_BRANCH}`);
18678
+ const branch = versionPrBranch(options.defaultBranch);
18679
+ const prNumber = await findOpenPr(executor, conn, branch);
18680
+ if (prNumber === null) throw new FatalError(`No open PR found for branch ${branch}`);
18229
18681
  const sleep$1 = options.sleep ?? sleep;
18230
18682
  const recheckDelayMs = envMsOverride(process.env["BST_TEST_VERSION_MERGE_RECHECK_MS"], RECHECK_DELAY_MS);
18231
18683
  const confirmation = await confirmMergeable(executor, conn, prNumber, {
@@ -18246,12 +18698,12 @@ async function mergeVersionPrForgejo(executor, conn, options) {
18246
18698
  headSha,
18247
18699
  mergeBase: decidedFrom.mergeBase,
18248
18700
  changedFiles: decidedFrom.changedFiles,
18249
- branch: VERSION_PR_BRANCH,
18701
+ branch,
18250
18702
  rechecked,
18251
18703
  verdict: { ...PASSED_VERDICT }
18252
18704
  };
18253
18705
  if (options.dryRun) {
18254
- log$1.info(`[dry-run] Would merge PR #${String(prNumber)} at ${headSha} and delete branch ${VERSION_PR_BRANCH}`);
18706
+ log$1.info(`[dry-run] Would merge PR #${String(prNumber)} at ${headSha} and delete branch ${branch}`);
18255
18707
  return {
18256
18708
  ...outcome,
18257
18709
  status: "would-merge"
@@ -18262,7 +18714,7 @@ async function mergeVersionPrForgejo(executor, conn, options) {
18262
18714
  deleteBranch: true,
18263
18715
  expectedHeadSha: headSha
18264
18716
  });
18265
- log$1.info(`Merged PR #${String(prNumber)} and deleted branch ${VERSION_PR_BRANCH}`);
18717
+ log$1.info(`Merged PR #${String(prNumber)} and deleted branch ${branch}`);
18266
18718
  return {
18267
18719
  ...outcome,
18268
18720
  status: "merged"
@@ -18277,16 +18729,17 @@ const GitHubVersionPrSchema = z.object({
18277
18729
  changedFiles: z.number()
18278
18730
  });
18279
18731
  function mergeVersionPrGitHub(executor, options) {
18732
+ const branch = versionPrBranch(options.defaultBranch);
18280
18733
  const view = executor.exec("gh", [
18281
18734
  "pr",
18282
18735
  "view",
18283
- VERSION_PR_BRANCH,
18736
+ branch,
18284
18737
  "--json",
18285
18738
  "number,state,isDraft,mergeable,headRefOid,changedFiles"
18286
18739
  ], { cwd: process.cwd() });
18287
- if (view.exitCode !== 0) throw new FatalError(`Failed to read PR for branch ${VERSION_PR_BRANCH}: ${view.stderr || view.stdout || "unknown error"}`);
18740
+ if (view.exitCode !== 0) throw new FatalError(`Failed to read PR for branch ${branch}: ${view.stderr || view.stdout || "unknown error"}`);
18288
18741
  const parsed = GitHubVersionPrSchema.safeParse(JSON.parse(view.stdout));
18289
- if (!parsed.success) throw new FatalError(`Unexpected 'gh pr view' response for branch ${VERSION_PR_BRANCH}: ${parsed.error.message}`);
18742
+ if (!parsed.success) throw new FatalError(`Unexpected 'gh pr view' response for branch ${branch}: ${parsed.error.message}`);
18290
18743
  const data = parsed.data;
18291
18744
  const emptiness = data.changedFiles === 0 ? "empty" : "non-empty";
18292
18745
  const mergeable = data.mergeable === "CONFLICTING" ? "conflicted" : "mergeable";
@@ -18305,12 +18758,12 @@ function mergeVersionPrGitHub(executor, options) {
18305
18758
  headSha,
18306
18759
  mergeBase: null,
18307
18760
  changedFiles: data.changedFiles,
18308
- branch: VERSION_PR_BRANCH,
18761
+ branch,
18309
18762
  rechecked: false,
18310
18763
  verdict: { ...PASSED_VERDICT }
18311
18764
  };
18312
18765
  if (options.dryRun) {
18313
- log$1.info(`[dry-run] Would merge PR #${String(data.number)} at ${headSha} and delete branch ${VERSION_PR_BRANCH}`);
18766
+ log$1.info(`[dry-run] Would merge PR #${String(data.number)} at ${headSha} and delete branch ${branch}`);
18314
18767
  return {
18315
18768
  ...outcome,
18316
18769
  status: "would-merge"
@@ -18319,14 +18772,14 @@ function mergeVersionPrGitHub(executor, options) {
18319
18772
  const result = executor.exec("gh", [
18320
18773
  "pr",
18321
18774
  "merge",
18322
- VERSION_PR_BRANCH,
18775
+ branch,
18323
18776
  "--merge",
18324
18777
  "--delete-branch",
18325
18778
  "--match-head-commit",
18326
18779
  headSha
18327
18780
  ], { cwd: process.cwd() });
18328
18781
  if (result.exitCode !== 0) throw new FatalError(`Failed to merge PR: ${result.stderr || result.stdout || "unknown error"}`);
18329
- log$1.info(`Merged changesets PR and deleted branch ${VERSION_PR_BRANCH}`);
18782
+ log$1.info(`Merged changesets PR and deleted branch ${branch}`);
18330
18783
  return {
18331
18784
  ...outcome,
18332
18785
  status: "merged"
@@ -18354,7 +18807,14 @@ const releaseMergeCommand = defineCommand({
18354
18807
  const dryRun = args["dry-run"] === true;
18355
18808
  const resolved = resolveConnection(process.cwd());
18356
18809
  const executor = createRealExecutor$1();
18357
- const result = resolved.type === "forgejo" ? await mergeVersionPrForgejo(executor, resolved.conn, { dryRun }) : mergeVersionPrGitHub(executor, { dryRun });
18810
+ const defaultBranch = readConfiguredDefaultBranch(process.cwd());
18811
+ const result = resolved.type === "forgejo" ? await mergeVersionPrForgejo(executor, resolved.conn, {
18812
+ dryRun,
18813
+ defaultBranch
18814
+ }) : mergeVersionPrGitHub(executor, {
18815
+ dryRun,
18816
+ defaultBranch
18817
+ });
18358
18818
  out.json(result);
18359
18819
  }
18360
18820
  });
@@ -20115,7 +20575,7 @@ function composePs(executor, config) {
20115
20575
  //#region src/docker-check/health.ts
20116
20576
  const DOCKER_RUN_START_FAILURE_EXIT_CODE = 125;
20117
20577
  const PROBE_TIMEOUT_SECONDS = "5";
20118
- const PROBE_IMAGE = "curlimages/curl:8.21.0@sha256:7c12af72ceb38b7432ab85e1a265cff6ae58e06f95539d539b654f2cfa64bb13";
20578
+ const PROBE_IMAGE = "curlimages/curl:8.22.0@sha256:58adaa4e8dca9c988bae2aba4ab3434a0bb2da16bbe3f92dec39ec7785166777";
20119
20579
  function getContainerHealth(containers, serviceName) {
20120
20580
  return containers.find((c) => c.Service === serviceName)?.Health ?? "unknown";
20121
20581
  }
@@ -20814,6 +21274,102 @@ async function runHooksCheck(config) {
20814
21274
  };
20815
21275
  }
20816
21276
  //#endregion
21277
+ //#region src/commands/setup-agent.ts
21278
+ const RawToolingConfigSchema = z.record(z.string(), z.unknown());
21279
+ function previousState(saved) {
21280
+ const enabled = saved.agent?.enabled;
21281
+ if (enabled === void 0) return "absent";
21282
+ if (enabled === true) return "true";
21283
+ return "false";
21284
+ }
21285
+ function withAgentEnabled(raw) {
21286
+ const agent = RawToolingConfigSchema.safeParse(raw["agent"]);
21287
+ const block = agent.success ? { ...agent.data } : {};
21288
+ block["enabled"] = true;
21289
+ return {
21290
+ ...raw,
21291
+ agent: block
21292
+ };
21293
+ }
21294
+ const CHANGE_ACTIONS = /* @__PURE__ */ new Set([
21295
+ "created",
21296
+ "updated",
21297
+ "removed",
21298
+ "archived"
21299
+ ]);
21300
+ async function runSetupAgent(targetDir, options) {
21301
+ const saved = loadToolingConfigStrict(targetDir);
21302
+ if (saved === void 0) throw new FatalError(`No .tooling.json in ${targetDir}. Run bst repo:sync first to create one, so the repo's own conventions are chosen before the agent is enabled.`);
21303
+ const previous = previousState(saved);
21304
+ let consent = "already-enabled";
21305
+ if (previous !== "true") {
21306
+ const displayPath = path.join(targetDir, ".tooling.json");
21307
+ let parsedFile;
21308
+ try {
21309
+ parsedFile = JSON.parse(readFileSync(displayPath, "utf8"));
21310
+ } catch (error) {
21311
+ throw new FatalError(`Could not read ${displayPath} as JSON: ${describeError$1(error)}`);
21312
+ }
21313
+ const rawFile = RawToolingConfigSchema.safeParse(parsedFile);
21314
+ if (!rawFile.success) throw new FatalError(`Could not read ${displayPath} as a config object: ${rawFile.error.message}`);
21315
+ const edited = withAgentEnabled(rawFile.data);
21316
+ const content = `${JSON.stringify(edited, null, 2)}\n`;
21317
+ parseToolingConfigStrict(content, displayPath);
21318
+ const context = createContext(mergeWithSavedConfig(buildDefaultConfig(targetDir, {}), saved), async () => "overwrite");
21319
+ context.ctx.write(".tooling.json", context.ctx.format(".tooling.json", content));
21320
+ consent = "written";
21321
+ }
21322
+ if (consent === "written") log$1.success("Set agent.enabled to true in .tooling.json");
21323
+ else log$1.info("agent.enabled is already true");
21324
+ const report = await runUpdateReport(targetDir, {}, { installOutput: options.installOutput });
21325
+ const failed = failedResults(report.results);
21326
+ const changed = report.results.filter((result) => CHANGE_ACTIONS.has(result.action)).map((result) => ({
21327
+ filePath: result.filePath,
21328
+ action: result.action
21329
+ }));
21330
+ let status;
21331
+ if (report.status === "stopped" || failed.length > 0) status = "failed";
21332
+ else if (changed.length > 0) status = "changed";
21333
+ else status = "up-to-date";
21334
+ if (status === "failed") log$1.error("The sync failed. Fix what it reported, then run bst setup:agent again.");
21335
+ else log$1.info("Commit these changes and land them on the default branch; the agent reads consent there within about a minute.");
21336
+ return {
21337
+ dir: targetDir,
21338
+ consent,
21339
+ previous,
21340
+ sync: {
21341
+ status,
21342
+ changed,
21343
+ failed,
21344
+ install: report.install
21345
+ },
21346
+ exitCode: status === "failed" ? 1 : 0
21347
+ };
21348
+ }
21349
+ const setupAgentCommand = defineCommand({
21350
+ meta: {
21351
+ name: "setup:agent",
21352
+ description: "Enable the agent for this repository: set agent.enabled to true in .tooling.json, then run the same sync as repo:sync, which installs @bensandee/agent-cli and generates the files the agent reads. Commits nothing -- land the change on the default branch yourself."
21353
+ },
21354
+ args: {
21355
+ dir: {
21356
+ type: "positional",
21357
+ valueHint: "dir",
21358
+ description: "Directory to enable (default: current directory)",
21359
+ required: false
21360
+ },
21361
+ json: {
21362
+ type: "boolean",
21363
+ description: "Output the report as JSON: dir, consent, previous, sync { status, changed, failed, install }, exitCode"
21364
+ }
21365
+ },
21366
+ async run({ args }) {
21367
+ const report = await runSetupAgent(path.resolve(args.dir ?? "."), { installOutput: args.json === true || isJsonMode() ? "capture" : "inherit" });
21368
+ createCliOutput(args.json === true).json(report);
21369
+ process.exitCode = report.exitCode;
21370
+ }
21371
+ });
21372
+ //#endregion
20817
21373
  //#region src/commands/setup-secrets.ts
20818
21374
  function isCancelled(value) {
20819
21375
  return clack.isCancel(value);
@@ -21758,6 +22314,7 @@ const coverageRecordCommand = defineCommand({
21758
22314
  });
21759
22315
  return;
21760
22316
  }
22317
+ refuseForgeDefaultMismatch(readConfiguredDefaultBranch(targetDir), processCiEventSource(), "coverage:record");
21761
22318
  const summaryPath = path.join(targetDir, COVERAGE_SUMMARY_FILE);
21762
22319
  if (!existsSync(summaryPath)) throw new FatalError(`${COVERAGE_SUMMARY_FILE} not found. Run \`pnpm test:coverage\` before \`coverage:record\`.`);
21763
22320
  const summary = parseV8Summary(readFileSync(summaryPath, "utf-8"));
@@ -23230,6 +23787,7 @@ const healthRecordCommand = defineCommand({
23230
23787
  });
23231
23788
  return;
23232
23789
  }
23790
+ refuseForgeDefaultMismatch(readConfiguredDefaultBranch(targetDir), processCiEventSource(), "health:record");
23233
23791
  const executor = createRealHealthExecutor(targetDir);
23234
23792
  const headResult = executor.exec("git", ["rev-parse", "HEAD"], { cwd: targetDir });
23235
23793
  if (headResult.exitCode !== 0) throw new FatalError(`Failed to resolve HEAD sha: ${headResult.stderr.trim()}`);
@@ -24093,7 +24651,7 @@ function collectViolations(exec, config, opts) {
24093
24651
  retiredCount += result.retired;
24094
24652
  }
24095
24653
  violations.push(...collectSpecRetirementViolations(exec, pairs, config, opts));
24096
- const activeDirs = pairs.map((pair) => pair.active).toSorted();
24654
+ const activeDirs = matchableActiveDirs(exec, config);
24097
24655
  const unmatchedEnforced = Array.isArray(config.enforced) ? config.enforced.filter((entry) => !activeDirs.includes(entry)) : [];
24098
24656
  return {
24099
24657
  violations,
@@ -24236,7 +24794,7 @@ const docsCheckCommand = defineCommand({
24236
24794
  },
24237
24795
  json: {
24238
24796
  type: "boolean",
24239
- description: "Emit one JSON document: violations ([{file, rule, message, fix, severity (fatal | advisory)}]), activeCount, retiredCount, pairCount, pass, activeDirs, unmatchedEnforced. When docs are disabled in .tooling.json the document is only enabled (false)."
24797
+ description: "Emit one JSON document: violations ([{file, rule, message, fix, severity (fatal | advisory)}]), activeCount, retiredCount, pairCount, pass, activeDirs, unmatchedEnforced. activeDirs lists every existing doc directory an enforced entry can match, holding docs or not; unmatchedEnforced the .tooling.json#docs.enforced entries that match none of them. When docs are disabled in .tooling.json the document is only enabled (false)."
24240
24798
  },
24241
24799
  verbose: {
24242
24800
  type: "boolean",
@@ -25581,58 +26139,6 @@ const issueDownloadCommand = defineCommand({
25581
26139
  }
25582
26140
  });
25583
26141
  //#endregion
25584
- //#region ../forgejo/src/issue-labels.ts
25585
- const TimelineEntrySchema = z.object({
25586
- id: z.number(),
25587
- type: z.string(),
25588
- body: z.string().nullish(),
25589
- created_at: z.string(),
25590
- label: z.object({ name: z.string() }).nullish(),
25591
- user: z.object({ login: z.string() }).nullish()
25592
- });
25593
- z.array(TimelineEntrySchema).nullable();
25594
- async function resolveLabelId(executor, conn, name, labelIds) {
25595
- const cached = labelIds?.get(name);
25596
- if (cached !== void 0) return cached;
25597
- const [id] = await resolveLabelIds(executor, conn, [name]);
25598
- if (id === void 0) throw new UnexpectedError(`Label id resolution returned nothing for "${name}"`);
25599
- return id;
25600
- }
25601
- async function addForgejoIssueLabel(executor, conn, number, name, labelIds) {
25602
- const id = await resolveLabelId(executor, conn, name, labelIds);
25603
- const res = await executor.fetch(`${issuesBase(conn)}/${String(number)}/labels`, {
25604
- method: "POST",
25605
- headers: jsonHeaders(conn),
25606
- body: JSON.stringify({ labels: [id] })
25607
- }, { idempotent: true });
25608
- if (!res.ok) await throwForgejoFailure(res, {
25609
- verb: "add label",
25610
- resource: `#${String(number)}`
25611
- }, issueNotFoundMessage(conn, number));
25612
- }
25613
- async function removeForgejoIssueLabel(executor, conn, number, name, labelIds) {
25614
- const id = await resolveLabelId(executor, conn, name, labelIds);
25615
- const res = await executor.fetch(`${issuesBase(conn)}/${String(number)}/labels/${String(id)}`, {
25616
- method: "DELETE",
25617
- headers: authHeaders$2(conn)
25618
- }, { idempotent: true });
25619
- if (!res.ok) await throwForgejoFailure(res, {
25620
- verb: "remove label",
25621
- resource: `#${String(number)}`
25622
- }, issueNotFoundMessage(conn, number));
25623
- }
25624
- async function replaceForgejoIssueLabels(executor, conn, number, labelIds) {
25625
- const res = await executor.fetch(`${issuesBase(conn)}/${String(number)}/labels`, {
25626
- method: "PUT",
25627
- headers: jsonHeaders(conn),
25628
- body: JSON.stringify({ labels: labelIds })
25629
- }, { idempotent: true });
25630
- if (!res.ok) await throwForgejoFailure(res, {
25631
- verb: "replace labels",
25632
- resource: `#${String(number)}`
25633
- }, issueNotFoundMessage(conn, number));
25634
- }
25635
- //#endregion
25636
26142
  //#region ../forgejo/src/issue-run-labels.ts
25637
26143
  async function labelIssue(resolve, request) {
25638
26144
  const ref = parseIssueRef(request.ref);
@@ -25871,6 +26377,9 @@ var EmptyCommentError = class extends FatalError {
25871
26377
  super("The comment resolved to empty text.");
25872
26378
  }
25873
26379
  };
26380
+ function rejectEmptyComment(comment) {
26381
+ if (comment !== void 0 && comment.trim() === "") throw new EmptyCommentError();
26382
+ }
25874
26383
  var PullRequestStateError = class extends FatalError {
25875
26384
  number;
25876
26385
  action;
@@ -25880,20 +26389,35 @@ var PullRequestStateError = class extends FatalError {
25880
26389
  this.action = action;
25881
26390
  }
25882
26391
  };
26392
+ var NotAPullRequestError = class extends FatalError {
26393
+ number;
26394
+ constructor(number) {
26395
+ super(`#${String(number)} is an issue, not a pull request.`);
26396
+ this.number = number;
26397
+ }
26398
+ };
26399
+ var StateNotTakenError = class extends FatalError {
26400
+ number;
26401
+ expected;
26402
+ actual;
26403
+ constructor(number, expected, actual) {
26404
+ super(`The forge did not change #${String(number)}: it is still ${actual}, not ${expected}.`);
26405
+ this.number = number;
26406
+ this.expected = expected;
26407
+ this.actual = actual;
26408
+ }
26409
+ };
25883
26410
  const TARGET_STATE = {
25884
26411
  close: "closed",
25885
26412
  reopen: "open"
25886
26413
  };
25887
- async function setIssueState(resolve, request) {
25888
- const ref = parseIssueRef(request.ref);
25889
- const number = requireIssueNumber(ref);
25890
- if (request.comment !== void 0 && request.comment.trim() === "") throw new EmptyCommentError();
25891
- const commentGiven = request.comment !== void 0;
25892
- const { api } = resolve(resolveIssueTarget(ref.target, request.repo));
26414
+ async function changeState(api, number, request, subject) {
25893
26415
  const before = await api.get(number);
25894
- if (before.isPullRequest) throw new PullRequestStateError(number, request.action);
26416
+ if (subject === "issue" && before.isPullRequest) throw new PullRequestStateError(number, request.action);
26417
+ if (subject === "pull-request" && !before.isPullRequest) throw new NotAPullRequestError(number);
26418
+ const commentGiven = request.comment !== void 0;
25895
26419
  const targetState = TARGET_STATE[request.action];
25896
- const buildResult = (url, current, changed, posted) => ({
26420
+ const buildResult = (url, current, changed, posted, merged) => ({
25897
26421
  number,
25898
26422
  url,
25899
26423
  action: request.action,
@@ -25902,20 +26426,32 @@ async function setIssueState(resolve, request) {
25902
26426
  current
25903
26427
  },
25904
26428
  changed,
25905
- ...commentGiven ? { comment: { posted } } : {}
26429
+ ...commentGiven ? { comment: { posted } } : {},
26430
+ ...merged === void 0 ? {} : { merged }
25906
26431
  });
25907
- if (before.state === targetState) return buildResult(before.url, before.state, false, false);
26432
+ if (before.state === targetState) {
26433
+ const merged = subject === "pull-request" ? (await api.pullRequest(number)).merged : void 0;
26434
+ return buildResult(before.url, before.state, false, false, merged);
26435
+ }
25908
26436
  if (request.dryRun) return buildResult(before.url, targetState, true, true);
25909
26437
  if (commentGiven) await api.addComment(number, request.comment ?? "");
25910
26438
  let after;
25911
26439
  try {
25912
26440
  after = await api.setState(number, targetState);
26441
+ if (after.state !== targetState) throw new StateNotTakenError(number, targetState, after.state);
25913
26442
  } catch (error) {
25914
26443
  if (commentGiven && error instanceof Error) error.message = `The comment on #${String(number)} was already posted; a retry will duplicate it. ` + error.message;
25915
26444
  throw error;
25916
26445
  }
25917
26446
  return buildResult(after.url, after.state, true, true);
25918
26447
  }
26448
+ async function setIssueState(resolve, request) {
26449
+ const ref = parseIssueRef(request.ref);
26450
+ const number = requireIssueNumber(ref);
26451
+ rejectEmptyComment(request.comment);
26452
+ const { api } = resolve(resolveIssueTarget(ref.target, request.repo));
26453
+ return changeState(api, number, request, "issue");
26454
+ }
25919
26455
  const PAST_TENSE = {
25920
26456
  close: "Closed",
25921
26457
  reopen: "Reopened"
@@ -26808,7 +27344,7 @@ async function runCli(config) {
26808
27344
  //#region src/cli-main.ts
26809
27345
  setLogLevel(resolveLogLevel({}));
26810
27346
  setForgejoLogger(log$1);
26811
- const displayVersion = "0.89.1";
27347
+ const displayVersion = "0.91.0";
26812
27348
  const bstSubCommands = {
26813
27349
  "repo:sync": syncCommand,
26814
27350
  "checks:run": runChecksCommand,
@@ -26831,6 +27367,7 @@ const bstSubCommands = {
26831
27367
  "hooks:audit": hooksAuditCommand,
26832
27368
  "hooks:sync": hooksSyncCommand,
26833
27369
  "setup:secrets": setupSecretsCommand,
27370
+ "setup:agent": setupAgentCommand,
26834
27371
  "webhook:send": webhookSendCommand,
26835
27372
  "coverage:summary": coverageSummaryCommand,
26836
27373
  "coverage:check": coverageCheckCommand,
@@ -26864,7 +27401,8 @@ const bstSubCommands = {
26864
27401
  "plan:brief": planBriefCommand,
26865
27402
  "plan:review-package": planReviewPackageCommand,
26866
27403
  "plan:status": planStatusCommand,
26867
- "work:status": workStatusCommand
27404
+ "work:status": workStatusCommand,
27405
+ "work:start": workStartCommand
26868
27406
  };
26869
27407
  async function runBstCli(extraSubCommands = {}) {
26870
27408
  await runCli({