@bensandee/tooling 0.81.1 → 0.82.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/README.md CHANGED
@@ -28,12 +28,12 @@ The tool auto-detects project structure, CI platform, project type, and Docker p
28
28
 
29
29
  ### Project management
30
30
 
31
- | Command | Description |
32
- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
33
- | `tooling repo:sync [dir]` | Detect, generate, and sync project tooling (idempotent). First run prompts for release strategy, CI platform (if not detected), and formatter (if Prettier found). Subsequent runs are non-interactive. |
31
+ | Command | Description |
32
+ | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
33
+ | `tooling repo:sync [dir]` | Detect, generate, and sync project tooling (idempotent). First run prompts for release strategy, CI platform (if not detected), and formatter (if Prettier found). Subsequent runs are non-interactive. |
34
34
  | `tooling repo:sync --check [dir]` | Dry-run drift detection. Exits 1 if files would change, except drift confined to `.forgejo/workflows/`/`.github/workflows/`, which is reported on the `::bst-checks:: warn` marker at exit 0. CI-friendly. |
35
- | `tooling checks:run` | Run project checks -- `bst checks:run --help` lists the ordered steps. Flags: `--skip`, `--add`, `--fail-fast`, `--verbose`, `--with-coverage`, `--coverage`, `--no-coverage`. |
36
- | `tooling lint:types` | Type-aware linting (`oxlint --type-aware`, backed by `oxlint-tsgolint`). Flags: `--json`. See [Type-aware linting](#type-aware-linting) below. |
35
+ | `tooling checks:run` | Run project checks -- `bst checks:run --help` lists the ordered steps. Flags: `--skip`, `--add`, `--fail-fast`, `--verbose`, `--with-coverage`, `--coverage`, `--no-coverage`. |
36
+ | `tooling lint:types` | Type-aware linting (`oxlint --type-aware`, backed by `oxlint-tsgolint`). Flags: `--json`. See [Type-aware linting](#type-aware-linting) below. |
37
37
 
38
38
  **Flags:** `--yes` (accept all defaults), `--no-ci` (negates `--ci`), `--no-prompt` (negates `--prompt`), `--eslint-plugin`
39
39
 
@@ -172,15 +172,15 @@ Renovate moves the pinned `node` version, but only within its current major -- a
172
172
 
173
173
  ### Release management
174
174
 
175
- | Command | Description |
176
- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
177
- | `tooling release:changesets` | Changesets version/publish for Forgejo CI. A prerelease package moves only its own `<name>@<version>` tag; `<name>@latest`, `<name>@vX` and `<name>@vX.Y` move only for a release (see `release:simple`). Flags: `--dry-run`, `--verbose`. Env: `FORGEJO_SERVER_URL`, `FORGEJO_REPOSITORY`, `RELEASE_TOKEN`. |
178
- | `tooling release:simple` | Streamlined release using commit-and-tag-version. Flags: `--release-as`, `--first-release`, `--prerelease`, `--verbose`. Env: `RELEASE_TOKEN` (required on every platform). |
179
- | `tooling release:trigger` | Trigger a release workflow. |
180
- | `tooling workflow:dispatch` | Dispatch a `workflow_dispatch` event for any workflow in any repo on the same git host. Flags: `--repo`, `--workflow` (required), `--ref`, `--input key=value` (repeatable). |
181
- | `tooling forgejo:create-release` | Create a Forgejo release from a tag. |
182
- | `tooling changesets:merge` | Merge a changesets version PR. Gated on a live read of the PR; refuses an empty version PR. On Forgejo, a first `mergeable: false` re-reads once after a short delay before refusing, since the forge reports `false` both for a conflict and while still checking. |
183
- | `tooling webhook:send <url>` | POST a structured JSON release payload with HMAC-SHA256 auth. Best-effort by default: a delivery failure is logged, annotated `::error::` under CI, and the command exits 0; `--strict` restores a non-zero exit. Flags: `--tag`, `--repository`, `--server-url`, `--actor`, `--prerelease`, `--strict`. Secret from `WEBHOOK_SECRET` env var. |
175
+ | Command | Description |
176
+ | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
177
+ | `tooling release:changesets` | Changesets version/publish for Forgejo CI. A prerelease package moves only its own `<name>@<version>` tag; `<name>@latest`, `<name>@vX` and `<name>@vX.Y` move only for a release (see `release:simple`). Flags: `--dry-run`, `--verbose`. Env: `FORGEJO_SERVER_URL`, `FORGEJO_REPOSITORY`, `RELEASE_TOKEN`. |
178
+ | `tooling release:simple` | Streamlined release using commit-and-tag-version. Flags: `--release-as`, `--first-release`, `--prerelease`, `--verbose`. Env: `RELEASE_TOKEN` (required on every platform). |
179
+ | `tooling release:trigger` | Trigger a release workflow. |
180
+ | `tooling workflow:dispatch` | Dispatch a `workflow_dispatch` event for any workflow in any repo on the same git host. Flags: `--repo`, `--workflow` (required), `--ref`, `--input key=value` (repeatable). |
181
+ | `tooling forgejo:create-release` | Create a Forgejo release from a tag. |
182
+ | `tooling changesets:merge` | Merge a changesets version PR. Gated on a live read of the PR; refuses an empty version PR. On Forgejo, a first `mergeable: false` re-reads once after a short delay before refusing, since the forge reports `false` both for a conflict and while still checking. |
183
+ | `tooling webhook:send <url>` | POST a structured JSON release payload with HMAC-SHA256 auth. Best-effort by default: a delivery failure is logged, annotated `::error::` under CI, and the command exits 0; `--strict` restores a non-zero exit. Flags: `--tag`, `--repository`, `--server-url`, `--actor`, `--prerelease`, `--strict`. Secret from `WEBHOOK_SECRET` env var. |
184
184
 
185
185
  #### `release:simple`
186
186
 
@@ -296,22 +296,22 @@ For a repo whose released artifact carries its version somewhere other than `pac
296
296
  }
297
297
  ```
298
298
 
299
- | Field | Description |
300
- | -------- | ------------------------------------------------------------------------------------- |
299
+ | Field | Description |
300
+ | -------- | ------------------------------------------------------------------------------------------ |
301
301
  | `file` | Path to the file whose version line gets stamped, relative to the project root (required). |
302
- | `prefix` | Literal string (never a pattern) the target line must start with (required). |
303
- | `suffix` | Literal string (never a pattern) the target line must end with (optional; default: none). |
302
+ | `prefix` | Literal string (never a pattern) the target line must start with (required). |
303
+ | `suffix` | Literal string (never a pattern) the target line must end with (optional; default: none). |
304
304
 
305
305
  `prefix` and `suffix` are literal strings, not regexes -- the target line is read straight off `prefix + version + suffix`:
306
306
 
307
307
  | Target | `prefix` | `suffix` |
308
- | ------------------------------------ | ---------------------------- | -------------- |
309
- | `Version: 1.2.3` (WordPress theme) | `"Version: "` | -- |
310
- | ` * Version: 1.2.3` (plugin header) | `" * Version: "` | -- |
311
- | `Version: v1.2.3` | `"Version: v"` | -- |
312
- | `__version__ = "1.2.3"` | `"__version__ = \""` | `"\""` |
313
- | `<version>1.2.3</version>` | `"<version>"` | `"</version>"` |
314
- | `define('FOO_VERSION', '1.2.3');` | `"define('FOO_VERSION', '"` | `"');"` |
308
+ | ----------------------------------- | --------------------------- | -------------- |
309
+ | `Version: 1.2.3` (WordPress theme) | `"Version: "` | -- |
310
+ | ` * Version: 1.2.3` (plugin header) | `" * Version: "` | -- |
311
+ | `Version: v1.2.3` | `"Version: v"` | -- |
312
+ | `__version__ = "1.2.3"` | `"__version__ = \""` | `"\""` |
313
+ | `<version>1.2.3</version>` | `"<version>"` | `"</version>"` |
314
+ | `define('FOO_VERSION', '1.2.3');` | `"define('FOO_VERSION', '"` | `"');"` |
315
315
 
316
316
  `release:assets` stamps every configured file before building assets, then verifies the stamp survived the build. Exactly one line in the file must match `prefix`/`suffix` or the release fails loudly -- zero matches means the config is broken, and more than one is an ambiguous config that needs a more specific `prefix` or a second entry. The stamp is never committed: the tracked copy of the file keeps its placeholder version on purpose, so there is only ever one source of truth (`package.json#version`) and nothing to drift. Run `bst release:assets --dry-run` to rehearse a `versionStamp` config locally, with no forge access and no release -- the rehearsal does still write the stamp, so it leaves the tracked file modified in your working tree. Discard it with `git checkout -- <file>`; committing it is exactly the drift this design exists to prevent.
317
317
 
@@ -619,7 +619,7 @@ There is exactly one credential to be careful with, and its lifetime is the part
619
619
 
620
620
  | | Board token | Session cookie it mints |
621
621
  | --------- | ----------------------------------------------------------------- | ----------------------- |
622
- | Lifetime | the agent **process** -- a service, so weeks | 12 hours |
622
+ | Lifetime | the agent **process** -- a service, so weeks | 12 hours |
623
623
  | Scope | label transitions + comments + kill, every repo the bot can write | the board, nothing else |
624
624
  | Holders | one, shared | one browser |
625
625
  | Persisted | never | never |
package/dist/bin.mjs CHANGED
@@ -407,7 +407,7 @@ function loadEnvFileForBin(rawArgs, io = processIo) {
407
407
  if (resolveJsonMode(rawArgs)) {
408
408
  const document = buildErrorDocument({
409
409
  command: resolveCommandName(rawArgs),
410
- version: "0.81.1",
410
+ version: "0.82.0",
411
411
  kind: "usage",
412
412
  message
413
413
  });
@@ -1964,8 +1964,13 @@ const DeployConfigSchema = z.strictObject({
1964
1964
  });
1965
1965
  const AgentConfigSchema = z.strictObject({
1966
1966
  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." }),
1967
- preRunRebase: z.boolean().optional().meta({ description: "Attempt a clean-only rebase of the work branch onto the default branch before the implement (execute) phase's container starts (default true). A clean rebase runs the container on the rebased branch; a conflicting one is aborted, reported on the run's ledger comment, and the run still proceeds on the un-rebased branch for a later agent/repair cycle to resolve -- it never fails the run. Set false to skip the attempt entirely for this repo: no fetch, no rebase, no abort, and the branch is worked exactly as cloned." }),
1967
+ preRunRebase: z.boolean().optional().meta({ description: "Attempt a clean-only rebase of the work branch onto the default branch before the implement (execute) phase's container starts (default true). A clean rebase runs the container on the rebased branch; a conflicting one is aborted, reported on the run's status comment, and the run still proceeds on the un-rebased branch -- it never fails the run. Two consecutive conflicting attempts hand the unit to agent/repair before any container runs, unless the branch already contains the default branch (a hand-made merge, which leaves a repair nothing to rebase), and a timeout or a failed rebase never counts toward that pair. Set false to skip the attempt entirely for this repo: no fetch, no rebase, no abort, and the branch is worked exactly as cloned." }),
1968
1968
  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." }),
1969
+ lintSweep: z.strictObject({
1970
+ enabled: z.boolean().optional().meta({ description: "Whether the agent's lint-warning sweep may open pull requests in this repo (default: on). false disarms it; the repo is then never cloned or measured by it." }),
1971
+ minWarnings: z.number().int().nonnegative().optional().meta({ description: "How many oxlint warnings the default branch must carry before the sweep opens a pull request (default 5). Below the floor the warn marker on checks:run is the whole response." }),
1972
+ cooldownDays: z.number().int().nonnegative().optional().meta({ description: "Days after a lint-sweep pull request merged or closed before the sweep may open another (default 7): a merged sweep whose declined warnings survive must not re-file them the next morning." })
1973
+ }).optional().meta({ description: "The lint-warning sweep: a periodic agent job that measures this repo's oxlint warnings on its default branch in a container and, past minWarnings, opens a pull request carrying a generated plan for an agent/implement run to work off." }),
1969
1974
  priority: z.record(z.string(), z.enum([
1970
1975
  "critical",
1971
1976
  "high",
@@ -1974,6 +1979,14 @@ const AgentConfigSchema = z.strictObject({
1974
1979
  ])).optional().meta({ description: "Map this repo's own issue-label names onto the agent's four priority levels, e.g. {\"P1\": \"critical\", \"P4\": \"low\"}. The map UNIONS with the built-in priority/<level> names rather than replacing them, and the agent does not create aliased labels -- the repo owns names the agent did not choose. Priority affects claim order only: it never changes a run's model, effort, or any budget." }),
1975
1980
  triageAnalysis: z.boolean().optional().meta({ description: "Whether the agent may route its own agent-note/human-triage-needed issues through the agent/analyze stage (default: off). Unlike agent.backlog, which defaults on, this defaults off: a backlog run batches every pending finding into one container, while triage routing spends one container per issue -- arming it is the reversible direction, not disarming it." })
1976
1981
  });
1982
+ const TolerantAgentConfigSchema = z.object({
1983
+ ...AgentConfigSchema.shape,
1984
+ lintSweep: tolerantBlock(z.strictObject({
1985
+ enabled: z.boolean().optional(),
1986
+ minWarnings: z.number().int().nonnegative().optional(),
1987
+ cooldownDays: z.number().int().nonnegative().optional()
1988
+ })).optional()
1989
+ });
1977
1990
  const BranchPruneConfigSchema = z.strictObject({
1978
1991
  mergedDays: z.number().int().nonnegative().optional().meta({ description: "Days after a work branch's PR merged before bst branch:prune deletes it (default 7). Zero is valid on purpose (an explicit 'delete everything merged already'), matching the --merged-days CLI flag's contract." }),
1979
1992
  backupDays: z.number().int().nonnegative().optional().meta({ description: "Days before bst branch:prune deletes an agent/(repair|rebase)-backup/* ref whose parent branch has no merged PR to key on (default 30). Deliberately longer than mergedDays: a backup ref is the only surviving copy of history rebased away from its parent. Zero is valid on purpose, matching the --backup-days CLI flag's contract." }),
@@ -2081,7 +2094,7 @@ const TolerantToolingConfigSchema = z.object({
2081
2094
  deploy: tolerantBlock(DeployConfigSchema).optional(),
2082
2095
  checks: tolerantBlock(ChecksConfigSchema).optional(),
2083
2096
  lint: tolerantBlock(LintConfigSchema).optional(),
2084
- agent: tolerantBlock(AgentConfigSchema).optional(),
2097
+ agent: TolerantAgentConfigSchema.optional(),
2085
2098
  branchPrune: tolerantBlock(BranchPruneConfigSchema).optional(),
2086
2099
  releaseAssets: z.array(tolerantBlock(ReleaseAssetSchema)).optional(),
2087
2100
  versionStamp: z.array(tolerantBlock(VersionStampSchema)).optional(),
@@ -5495,8 +5508,8 @@ function declaredPublishRegistries(targetDir, structure, rootPackageJson) {
5495
5508
  * shape of custom regex manager in the root `renovate.json` can rewrite both this version and this
5496
5509
  * digest (`customManagers` entries scoped to this file).
5497
5510
  */
5498
- const MISE_VERSION = "2026.9.14";
5499
- const MISE_LINUX_X64_SHA256 = "2b289d1b3074e0b1d3f95bad0bd78bbc517c1a5bcb020cbc1643d260f5d1a351";
5511
+ const MISE_VERSION = "2026.9.16";
5512
+ const MISE_LINUX_X64_SHA256 = "b6f8757201f6a2ee799f45f3f52ef7ca0b4071523637dc3b0b24264dd3333518";
5500
5513
  /** Where generated Forgejo workflows fetch the shared mise setup action: a composite action in
5501
5514
  * `bensandee/common`, on the forge every fleet repo lives on. GitHub cannot reach it; the
5502
5515
  * generators keep inline steps there. */
@@ -6343,17 +6356,17 @@ function perPackageDevDeps(config) {
6343
6356
  "@types/node": "24.13.5",
6344
6357
  "@vitest/coverage-v8": "5.0.1",
6345
6358
  "commit-and-tag-version": "13.2.1",
6346
- "knip": "6.37.0",
6359
+ "knip": "6.38.0",
6347
6360
  "lefthook": "2.1.14",
6348
- "oxfmt": "0.68.0",
6349
- "oxlint": "1.83.0",
6350
- "oxlint-tsgolint": "7.0.2002",
6351
- "prettier": "3.9.8",
6361
+ "oxfmt": "0.71.0",
6362
+ "oxlint": "1.85.0",
6363
+ "oxlint-tsgolint": "7.0.2003",
6364
+ "prettier": "3.9.9",
6352
6365
  "release-it": "21.1.0",
6353
6366
  "tsdown": "0.23.0",
6354
6367
  "typescript": "6.0.3",
6355
6368
  "vitest": "5.0.1",
6356
- "pnpm": "11.27.0"
6369
+ "pnpm": "11.27.1"
6357
6370
  }["typescript"];
6358
6371
  deps["@types/node"] = {
6359
6372
  "@changesets/cli": "3.0.3",
@@ -6361,17 +6374,17 @@ function perPackageDevDeps(config) {
6361
6374
  "@types/node": "24.13.5",
6362
6375
  "@vitest/coverage-v8": "5.0.1",
6363
6376
  "commit-and-tag-version": "13.2.1",
6364
- "knip": "6.37.0",
6377
+ "knip": "6.38.0",
6365
6378
  "lefthook": "2.1.14",
6366
- "oxfmt": "0.68.0",
6367
- "oxlint": "1.83.0",
6368
- "oxlint-tsgolint": "7.0.2002",
6369
- "prettier": "3.9.8",
6379
+ "oxfmt": "0.71.0",
6380
+ "oxlint": "1.85.0",
6381
+ "oxlint-tsgolint": "7.0.2003",
6382
+ "prettier": "3.9.9",
6370
6383
  "release-it": "21.1.0",
6371
6384
  "tsdown": "0.23.0",
6372
6385
  "typescript": "6.0.3",
6373
6386
  "vitest": "5.0.1",
6374
- "pnpm": "11.27.0"
6387
+ "pnpm": "11.27.1"
6375
6388
  }["@types/node"];
6376
6389
  }
6377
6390
  if (config.builder === "tsdown") deps["tsdown"] = {
@@ -6380,17 +6393,17 @@ function perPackageDevDeps(config) {
6380
6393
  "@types/node": "24.13.5",
6381
6394
  "@vitest/coverage-v8": "5.0.1",
6382
6395
  "commit-and-tag-version": "13.2.1",
6383
- "knip": "6.37.0",
6396
+ "knip": "6.38.0",
6384
6397
  "lefthook": "2.1.14",
6385
- "oxfmt": "0.68.0",
6386
- "oxlint": "1.83.0",
6387
- "oxlint-tsgolint": "7.0.2002",
6388
- "prettier": "3.9.8",
6398
+ "oxfmt": "0.71.0",
6399
+ "oxlint": "1.85.0",
6400
+ "oxlint-tsgolint": "7.0.2003",
6401
+ "prettier": "3.9.9",
6389
6402
  "release-it": "21.1.0",
6390
6403
  "tsdown": "0.23.0",
6391
6404
  "typescript": "6.0.3",
6392
6405
  "vitest": "5.0.1",
6393
- "pnpm": "11.27.0"
6406
+ "pnpm": "11.27.1"
6394
6407
  }["tsdown"];
6395
6408
  return deps;
6396
6409
  }
@@ -6410,17 +6423,17 @@ function vitestDevDeps() {
6410
6423
  "@types/node": "24.13.5",
6411
6424
  "@vitest/coverage-v8": "5.0.1",
6412
6425
  "commit-and-tag-version": "13.2.1",
6413
- "knip": "6.37.0",
6426
+ "knip": "6.38.0",
6414
6427
  "lefthook": "2.1.14",
6415
- "oxfmt": "0.68.0",
6416
- "oxlint": "1.83.0",
6417
- "oxlint-tsgolint": "7.0.2002",
6418
- "prettier": "3.9.8",
6428
+ "oxfmt": "0.71.0",
6429
+ "oxlint": "1.85.0",
6430
+ "oxlint-tsgolint": "7.0.2003",
6431
+ "prettier": "3.9.9",
6419
6432
  "release-it": "21.1.0",
6420
6433
  "tsdown": "0.23.0",
6421
6434
  "typescript": "6.0.3",
6422
6435
  "vitest": "5.0.1",
6423
- "pnpm": "11.27.0"
6436
+ "pnpm": "11.27.1"
6424
6437
  }["vitest"],
6425
6438
  "@vitest/coverage-v8": {
6426
6439
  "@changesets/cli": "3.0.3",
@@ -6428,17 +6441,17 @@ function vitestDevDeps() {
6428
6441
  "@types/node": "24.13.5",
6429
6442
  "@vitest/coverage-v8": "5.0.1",
6430
6443
  "commit-and-tag-version": "13.2.1",
6431
- "knip": "6.37.0",
6444
+ "knip": "6.38.0",
6432
6445
  "lefthook": "2.1.14",
6433
- "oxfmt": "0.68.0",
6434
- "oxlint": "1.83.0",
6435
- "oxlint-tsgolint": "7.0.2002",
6436
- "prettier": "3.9.8",
6446
+ "oxfmt": "0.71.0",
6447
+ "oxlint": "1.85.0",
6448
+ "oxlint-tsgolint": "7.0.2003",
6449
+ "prettier": "3.9.9",
6437
6450
  "release-it": "21.1.0",
6438
6451
  "tsdown": "0.23.0",
6439
6452
  "typescript": "6.0.3",
6440
6453
  "vitest": "5.0.1",
6441
- "pnpm": "11.27.0"
6454
+ "pnpm": "11.27.1"
6442
6455
  }["@vitest/coverage-v8"]
6443
6456
  };
6444
6457
  }
@@ -6455,17 +6468,17 @@ function rootDevDeps(config) {
6455
6468
  "@types/node": "24.13.5",
6456
6469
  "@vitest/coverage-v8": "5.0.1",
6457
6470
  "commit-and-tag-version": "13.2.1",
6458
- "knip": "6.37.0",
6471
+ "knip": "6.38.0",
6459
6472
  "lefthook": "2.1.14",
6460
- "oxfmt": "0.68.0",
6461
- "oxlint": "1.83.0",
6462
- "oxlint-tsgolint": "7.0.2002",
6463
- "prettier": "3.9.8",
6473
+ "oxfmt": "0.71.0",
6474
+ "oxlint": "1.85.0",
6475
+ "oxlint-tsgolint": "7.0.2003",
6476
+ "prettier": "3.9.9",
6464
6477
  "release-it": "21.1.0",
6465
6478
  "tsdown": "0.23.0",
6466
6479
  "typescript": "6.0.3",
6467
6480
  "vitest": "5.0.1",
6468
- "pnpm": "11.27.0"
6481
+ "pnpm": "11.27.1"
6469
6482
  }["lefthook"] };
6470
6483
  if (config.stack !== "none") {
6471
6484
  deps["knip"] = {
@@ -6474,17 +6487,17 @@ function rootDevDeps(config) {
6474
6487
  "@types/node": "24.13.5",
6475
6488
  "@vitest/coverage-v8": "5.0.1",
6476
6489
  "commit-and-tag-version": "13.2.1",
6477
- "knip": "6.37.0",
6490
+ "knip": "6.38.0",
6478
6491
  "lefthook": "2.1.14",
6479
- "oxfmt": "0.68.0",
6480
- "oxlint": "1.83.0",
6481
- "oxlint-tsgolint": "7.0.2002",
6482
- "prettier": "3.9.8",
6492
+ "oxfmt": "0.71.0",
6493
+ "oxlint": "1.85.0",
6494
+ "oxlint-tsgolint": "7.0.2003",
6495
+ "prettier": "3.9.9",
6483
6496
  "release-it": "21.1.0",
6484
6497
  "tsdown": "0.23.0",
6485
6498
  "typescript": "6.0.3",
6486
6499
  "vitest": "5.0.1",
6487
- "pnpm": "11.27.0"
6500
+ "pnpm": "11.27.1"
6488
6501
  }["knip"];
6489
6502
  deps["oxlint"] = {
6490
6503
  "@changesets/cli": "3.0.3",
@@ -6492,17 +6505,17 @@ function rootDevDeps(config) {
6492
6505
  "@types/node": "24.13.5",
6493
6506
  "@vitest/coverage-v8": "5.0.1",
6494
6507
  "commit-and-tag-version": "13.2.1",
6495
- "knip": "6.37.0",
6508
+ "knip": "6.38.0",
6496
6509
  "lefthook": "2.1.14",
6497
- "oxfmt": "0.68.0",
6498
- "oxlint": "1.83.0",
6499
- "oxlint-tsgolint": "7.0.2002",
6500
- "prettier": "3.9.8",
6510
+ "oxfmt": "0.71.0",
6511
+ "oxlint": "1.85.0",
6512
+ "oxlint-tsgolint": "7.0.2003",
6513
+ "prettier": "3.9.9",
6501
6514
  "release-it": "21.1.0",
6502
6515
  "tsdown": "0.23.0",
6503
6516
  "typescript": "6.0.3",
6504
6517
  "vitest": "5.0.1",
6505
- "pnpm": "11.27.0"
6518
+ "pnpm": "11.27.1"
6506
6519
  }["oxlint"];
6507
6520
  Object.assign(deps, vitestDevDeps());
6508
6521
  }
@@ -6512,17 +6525,17 @@ function rootDevDeps(config) {
6512
6525
  "@types/node": "24.13.5",
6513
6526
  "@vitest/coverage-v8": "5.0.1",
6514
6527
  "commit-and-tag-version": "13.2.1",
6515
- "knip": "6.37.0",
6528
+ "knip": "6.38.0",
6516
6529
  "lefthook": "2.1.14",
6517
- "oxfmt": "0.68.0",
6518
- "oxlint": "1.83.0",
6519
- "oxlint-tsgolint": "7.0.2002",
6520
- "prettier": "3.9.8",
6530
+ "oxfmt": "0.71.0",
6531
+ "oxlint": "1.85.0",
6532
+ "oxlint-tsgolint": "7.0.2003",
6533
+ "prettier": "3.9.9",
6521
6534
  "release-it": "21.1.0",
6522
6535
  "tsdown": "0.23.0",
6523
6536
  "typescript": "6.0.3",
6524
6537
  "vitest": "5.0.1",
6525
- "pnpm": "11.27.0"
6538
+ "pnpm": "11.27.1"
6526
6539
  }["oxlint-tsgolint"];
6527
6540
  return deps;
6528
6541
  }
@@ -6556,17 +6569,17 @@ function addReleaseDeps(deps, config) {
6556
6569
  "@types/node": "24.13.5",
6557
6570
  "@vitest/coverage-v8": "5.0.1",
6558
6571
  "commit-and-tag-version": "13.2.1",
6559
- "knip": "6.37.0",
6572
+ "knip": "6.38.0",
6560
6573
  "lefthook": "2.1.14",
6561
- "oxfmt": "0.68.0",
6562
- "oxlint": "1.83.0",
6563
- "oxlint-tsgolint": "7.0.2002",
6564
- "prettier": "3.9.8",
6574
+ "oxfmt": "0.71.0",
6575
+ "oxlint": "1.85.0",
6576
+ "oxlint-tsgolint": "7.0.2003",
6577
+ "prettier": "3.9.9",
6565
6578
  "release-it": "21.1.0",
6566
6579
  "tsdown": "0.23.0",
6567
6580
  "typescript": "6.0.3",
6568
6581
  "vitest": "5.0.1",
6569
- "pnpm": "11.27.0"
6582
+ "pnpm": "11.27.1"
6570
6583
  }["release-it"];
6571
6584
  if (config.structure === "monorepo") deps["@release-it/bumper"] = {
6572
6585
  "@changesets/cli": "3.0.3",
@@ -6574,17 +6587,17 @@ function addReleaseDeps(deps, config) {
6574
6587
  "@types/node": "24.13.5",
6575
6588
  "@vitest/coverage-v8": "5.0.1",
6576
6589
  "commit-and-tag-version": "13.2.1",
6577
- "knip": "6.37.0",
6590
+ "knip": "6.38.0",
6578
6591
  "lefthook": "2.1.14",
6579
- "oxfmt": "0.68.0",
6580
- "oxlint": "1.83.0",
6581
- "oxlint-tsgolint": "7.0.2002",
6582
- "prettier": "3.9.8",
6592
+ "oxfmt": "0.71.0",
6593
+ "oxlint": "1.85.0",
6594
+ "oxlint-tsgolint": "7.0.2003",
6595
+ "prettier": "3.9.9",
6583
6596
  "release-it": "21.1.0",
6584
6597
  "tsdown": "0.23.0",
6585
6598
  "typescript": "6.0.3",
6586
6599
  "vitest": "5.0.1",
6587
- "pnpm": "11.27.0"
6600
+ "pnpm": "11.27.1"
6588
6601
  }["@release-it/bumper"];
6589
6602
  break;
6590
6603
  case "simple":
@@ -6594,17 +6607,17 @@ function addReleaseDeps(deps, config) {
6594
6607
  "@types/node": "24.13.5",
6595
6608
  "@vitest/coverage-v8": "5.0.1",
6596
6609
  "commit-and-tag-version": "13.2.1",
6597
- "knip": "6.37.0",
6610
+ "knip": "6.38.0",
6598
6611
  "lefthook": "2.1.14",
6599
- "oxfmt": "0.68.0",
6600
- "oxlint": "1.83.0",
6601
- "oxlint-tsgolint": "7.0.2002",
6602
- "prettier": "3.9.8",
6612
+ "oxfmt": "0.71.0",
6613
+ "oxlint": "1.85.0",
6614
+ "oxlint-tsgolint": "7.0.2003",
6615
+ "prettier": "3.9.9",
6603
6616
  "release-it": "21.1.0",
6604
6617
  "tsdown": "0.23.0",
6605
6618
  "typescript": "6.0.3",
6606
6619
  "vitest": "5.0.1",
6607
- "pnpm": "11.27.0"
6620
+ "pnpm": "11.27.1"
6608
6621
  }["commit-and-tag-version"];
6609
6622
  break;
6610
6623
  case "changesets": deps["@changesets/cli"] = {
@@ -6613,17 +6626,17 @@ function addReleaseDeps(deps, config) {
6613
6626
  "@types/node": "24.13.5",
6614
6627
  "@vitest/coverage-v8": "5.0.1",
6615
6628
  "commit-and-tag-version": "13.2.1",
6616
- "knip": "6.37.0",
6629
+ "knip": "6.38.0",
6617
6630
  "lefthook": "2.1.14",
6618
- "oxfmt": "0.68.0",
6619
- "oxlint": "1.83.0",
6620
- "oxlint-tsgolint": "7.0.2002",
6621
- "prettier": "3.9.8",
6631
+ "oxfmt": "0.71.0",
6632
+ "oxlint": "1.85.0",
6633
+ "oxlint-tsgolint": "7.0.2003",
6634
+ "prettier": "3.9.9",
6622
6635
  "release-it": "21.1.0",
6623
6636
  "tsdown": "0.23.0",
6624
6637
  "typescript": "6.0.3",
6625
6638
  "vitest": "5.0.1",
6626
- "pnpm": "11.27.0"
6639
+ "pnpm": "11.27.1"
6627
6640
  }["@changesets/cli"];
6628
6641
  }
6629
6642
  }
@@ -6820,17 +6833,17 @@ function applyMetadataDefaults(pkg, ctx, changes) {
6820
6833
  "@types/node": "24.13.5",
6821
6834
  "@vitest/coverage-v8": "5.0.1",
6822
6835
  "commit-and-tag-version": "13.2.1",
6823
- "knip": "6.37.0",
6836
+ "knip": "6.38.0",
6824
6837
  "lefthook": "2.1.14",
6825
- "oxfmt": "0.68.0",
6826
- "oxlint": "1.83.0",
6827
- "oxlint-tsgolint": "7.0.2002",
6828
- "prettier": "3.9.8",
6838
+ "oxfmt": "0.71.0",
6839
+ "oxlint": "1.85.0",
6840
+ "oxlint-tsgolint": "7.0.2003",
6841
+ "prettier": "3.9.9",
6829
6842
  "release-it": "21.1.0",
6830
6843
  "tsdown": "0.23.0",
6831
6844
  "typescript": "6.0.3",
6832
6845
  "vitest": "5.0.1",
6833
- "pnpm": "11.27.0"
6846
+ "pnpm": "11.27.1"
6834
6847
  }["pnpm"];
6835
6848
  const desiredPm = pinnedPnpm ? `pnpm@${pinnedPnpm}` : void 0;
6836
6849
  if (desiredPm) {
@@ -6871,7 +6884,7 @@ function buildManagedDevDeps(ctx) {
6871
6884
  const devDeps = { ...rootDevDeps(ctx.config) };
6872
6885
  if (!isMonorepo) Object.assign(devDeps, perPackageDevDeps(ctx.config));
6873
6886
  if (ctx.config.stack !== "none" || ctx.config.ci === "forgejo") devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.14.5";
6874
- if (!isWorkspacePackage(ctx, "@bensandee/tooling")) devDeps["@bensandee/tooling"] = "0.81.1";
6887
+ if (!isWorkspacePackage(ctx, "@bensandee/tooling")) devDeps["@bensandee/tooling"] = "0.82.0";
6875
6888
  if (ctx.config.useEslintPlugin && ctx.config.stack !== "none") devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.15.2";
6876
6889
  if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
6877
6890
  "@changesets/cli": "3.0.3",
@@ -6879,17 +6892,17 @@ function buildManagedDevDeps(ctx) {
6879
6892
  "@types/node": "24.13.5",
6880
6893
  "@vitest/coverage-v8": "5.0.1",
6881
6894
  "commit-and-tag-version": "13.2.1",
6882
- "knip": "6.37.0",
6895
+ "knip": "6.38.0",
6883
6896
  "lefthook": "2.1.14",
6884
- "oxfmt": "0.68.0",
6885
- "oxlint": "1.83.0",
6886
- "oxlint-tsgolint": "7.0.2002",
6887
- "prettier": "3.9.8",
6897
+ "oxfmt": "0.71.0",
6898
+ "oxlint": "1.85.0",
6899
+ "oxlint-tsgolint": "7.0.2003",
6900
+ "prettier": "3.9.9",
6888
6901
  "release-it": "21.1.0",
6889
6902
  "tsdown": "0.23.0",
6890
6903
  "typescript": "6.0.3",
6891
6904
  "vitest": "5.0.1",
6892
- "pnpm": "11.27.0"
6905
+ "pnpm": "11.27.1"
6893
6906
  }["oxfmt"];
6894
6907
  if (ctx.config.formatter === "prettier") devDeps["prettier"] = {
6895
6908
  "@changesets/cli": "3.0.3",
@@ -6897,17 +6910,17 @@ function buildManagedDevDeps(ctx) {
6897
6910
  "@types/node": "24.13.5",
6898
6911
  "@vitest/coverage-v8": "5.0.1",
6899
6912
  "commit-and-tag-version": "13.2.1",
6900
- "knip": "6.37.0",
6913
+ "knip": "6.38.0",
6901
6914
  "lefthook": "2.1.14",
6902
- "oxfmt": "0.68.0",
6903
- "oxlint": "1.83.0",
6904
- "oxlint-tsgolint": "7.0.2002",
6905
- "prettier": "3.9.8",
6915
+ "oxfmt": "0.71.0",
6916
+ "oxlint": "1.85.0",
6917
+ "oxlint-tsgolint": "7.0.2003",
6918
+ "prettier": "3.9.9",
6906
6919
  "release-it": "21.1.0",
6907
6920
  "tsdown": "0.23.0",
6908
6921
  "typescript": "6.0.3",
6909
6922
  "vitest": "5.0.1",
6910
- "pnpm": "11.27.0"
6923
+ "pnpm": "11.27.1"
6911
6924
  }["prettier"];
6912
6925
  addReleaseDeps(devDeps, ctx.config);
6913
6926
  return devDeps;
@@ -6966,17 +6979,17 @@ async function generatePackageJson(ctx) {
6966
6979
  "@types/node": "24.13.5",
6967
6980
  "@vitest/coverage-v8": "5.0.1",
6968
6981
  "commit-and-tag-version": "13.2.1",
6969
- "knip": "6.37.0",
6982
+ "knip": "6.38.0",
6970
6983
  "lefthook": "2.1.14",
6971
- "oxfmt": "0.68.0",
6972
- "oxlint": "1.83.0",
6973
- "oxlint-tsgolint": "7.0.2002",
6974
- "prettier": "3.9.8",
6984
+ "oxfmt": "0.71.0",
6985
+ "oxlint": "1.85.0",
6986
+ "oxlint-tsgolint": "7.0.2003",
6987
+ "prettier": "3.9.9",
6975
6988
  "release-it": "21.1.0",
6976
6989
  "tsdown": "0.23.0",
6977
6990
  "typescript": "6.0.3",
6978
6991
  "vitest": "5.0.1",
6979
- "pnpm": "11.27.0"
6992
+ "pnpm": "11.27.1"
6980
6993
  }["pnpm"]}`
6981
6994
  };
6982
6995
  ctx.write(filePath, JSON.stringify(pkg, null, 2) + "\n");
@@ -7987,17 +8000,36 @@ async function generateOxlint(ctx) {
7987
8000
  * format gate on the branch it was filed against -- blocking the pull request over a file no
7988
8001
  * human wrote and nobody maintains.
7989
8002
  *
7990
- * `**\/*.md` is excluded because oxfmt's markdown printer, inherited from prettier, is not
7991
- * idempotent: formatting an already-formatted file changes it again (task-list indentation grows
7992
- * four spaces per pass), so a format-then-check gate fails on its own output. The fix landed on
7993
- * prettier's `main` (prettier/prettier#19647, closing prettier/prettier#19644) but as of 2026-08-26
7994
- * has not shipped in a release oxfmt can port from. Drop this entry once oxfmt ships the ported fix.
8003
+ * `docs/**\/retired/` is excluded because of what those files are, not only because of the
8004
+ * formatter bug below: the plan lifecycle defines a retired doc as a historical record nobody
8005
+ * maintains, so a reformat would rewrite hundreds of them for no reader and bury any real change
8006
+ * in the churn. It is also where every still-unstable file in the measured corpus lives. The
8007
+ * pattern covers both retired layouts the docs lifecycle knows: `docs/superpowers/*\/retired/`
8008
+ * and the legacy `docs/retired/`.
8009
+ *
8010
+ * oxfmt's markdown printer is still not idempotent, in one bounded way: an inline code span
8011
+ * broken across a line inside a list item whose continuation lines are indented deeper than the
8012
+ * item's content moves two columns left per pass until it reaches column 0, settling within four
8013
+ * passes. Active docs reach a fixed point in one pass; a `format --check` that fails right after
8014
+ * `pnpm format` is fixed by running `pnpm format` again until it changes nothing.
7995
8015
  */
7996
8016
  const BASE_IGNORE_PATTERNS = [
7997
8017
  ".claude",
7998
8018
  ".agent/findings",
7999
- "**/*.md"
8019
+ "docs/**/retired/"
8000
8020
  ];
8021
+ /**
8022
+ * Exact patterns bst required in earlier versions and now removes on sync, because the merge in
8023
+ * `generateOxfmt` only ever appends: dropping a pattern from the base list would leave every
8024
+ * existing .oxfmtrc.json holding it forever.
8025
+ *
8026
+ * `**\/*.md` is here because bst wrote it from 2026-08-26 until markdown formatting was
8027
+ * re-enabled, and the 42 files it was excluding were all retired docs -- which the base list
8028
+ * still excludes by directory. A repo that wants its markdown left unformatted opts out with the
8029
+ * different spelling `*.md`, which matches every markdown file at any depth but is not an exact
8030
+ * match for a superseded entry, so the strip leaves it alone.
8031
+ */
8032
+ const SUPERSEDED_IGNORE_PATTERNS = /* @__PURE__ */ new Set(["**/*.md"]);
8001
8033
  /** Extra ignore patterns when a release strategy generates CHANGELOGs. */
8002
8034
  const RELEASE_IGNORE_PATTERNS = ["**/CHANGELOG.md"];
8003
8035
  /**
@@ -8073,19 +8105,30 @@ async function generateOxfmt(ctx) {
8073
8105
  }
8074
8106
  const parsed = parseResult.data;
8075
8107
  const ignorePatterns = Array.isArray(parsed["ignorePatterns"]) ? parsed["ignorePatterns"] : [];
8076
- const existingSet = new Set(ignorePatterns.filter((p) => typeof p === "string"));
8108
+ const strings = ignorePatterns.filter((p) => typeof p === "string");
8109
+ const existingSet = new Set(strings);
8077
8110
  const missing = required.filter((p) => !existingSet.has(p));
8078
- if (missing.length === 0) return {
8111
+ const kept = strings.filter((p) => !SUPERSEDED_IGNORE_PATTERNS.has(p));
8112
+ const removed = strings.filter((p) => SUPERSEDED_IGNORE_PATTERNS.has(p));
8113
+ const nonStringPatterns = ignorePatterns.filter((p) => typeof p !== "string");
8114
+ if (missing.length === 0 && removed.length === 0) return {
8079
8115
  filePath,
8080
8116
  action: "skipped",
8081
8117
  description: "Existing oxfmt config already has required ignorePatterns"
8082
8118
  };
8083
- parsed["ignorePatterns"] = [...ignorePatterns, ...missing];
8119
+ parsed["ignorePatterns"] = [
8120
+ ...kept,
8121
+ ...nonStringPatterns,
8122
+ ...missing
8123
+ ];
8084
8124
  ctx.write(filePath, JSON.stringify(parsed, null, 2) + "\n");
8125
+ const changeParts = [];
8126
+ if (missing.length > 0) changeParts.push(`Added ignorePatterns: ${missing.join(", ")}`);
8127
+ if (removed.length > 0) changeParts.push(`removed: ${removed.join(", ")}`);
8085
8128
  return {
8086
8129
  filePath,
8087
8130
  action: "updated",
8088
- description: `Added ignorePatterns: ${missing.join(", ")}`
8131
+ description: changeParts.join("; ")
8089
8132
  };
8090
8133
  }
8091
8134
  async function generatePrettier(ctx) {
@@ -11537,9 +11580,18 @@ const COMMENTS_GUIDE_BODY = [
11537
11580
  "- **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."
11538
11581
  ].join("\n");
11539
11582
  /**
11583
+ * Exported because the agent's execute prompt names the same tokens, and a shared constant is
11584
+ * what keeps a guide and a prompt from naming different ones.
11585
+ */
11586
+ const TEST_EVIDENCE_PREFIXES = {
11587
+ red: "Red:",
11588
+ noTest: "No test:"
11589
+ };
11590
+ /**
11540
11591
  * The test rules, measured like the comment rules: an audit of this repo's suite found about
11541
11592
  * 290 cases that could not fail, echoed a constant, pinned a sentence, or duplicated a sibling.
11542
- * Each bullet names one of those shapes, because the repo's general "test behaviour, not
11593
+ * The opening bullets state the test-first cycle and its commit-body evidence line; each bullet
11594
+ * after them names one of those audited shapes, because the repo's general "test behaviour, not
11543
11595
  * wording" rule had not prevented them.
11544
11596
  */
11545
11597
  const TESTING_GUIDE_BODY = [
@@ -11548,6 +11600,8 @@ const TESTING_GUIDE_BODY = [
11548
11600
  "**A test earns its place by failing when behaviour changes.** Prove each new one red by undoing the change it covers: a test that cannot fail reads as coverage, which is worse than none.",
11549
11601
  "",
11550
11602
  "- Write the test before the code it covers, and watch it fail for the reason you expect. A test that passes on its first run is testing something that already works.",
11603
+ `- Carry the evidence in the commit body: \`${TEST_EVIDENCE_PREFIXES.red} <test name> failed: <reason>\` when the test ran red first, or \`${TEST_EVIDENCE_PREFIXES.noTest} <reason>\` when there was none to write; the reason is one clause (a missing export, a wrong value, a thrown error) so a reviewer can tell a run that followed the cycle from one that did not.`,
11604
+ `- A change with no test to write is allowed, but the \`${TEST_EVIDENCE_PREFIXES.noTest}\` line always states the reason -- a plan's retire-the-plan task and a docs-only change are the named examples. "It is small" is not a reason -- a small change has a small test.`,
11551
11605
  "- Never assert that a constant equals its own literal (a default, threshold, model name, label). Test the logic that reads it.",
11552
11606
  "- Never pin prose with `toContain`/`toBe`. Snapshot it, or assert a property: ordering, absence, output that changes with an input, a character set or length.",
11553
11607
  "- One case per branch. A new literal down a path already tested, or the same call through a one-line wrapper, is a duplicate.",
@@ -11617,7 +11671,7 @@ const PLANNING_GUIDE_BODY = [
11617
11671
  "",
11618
11672
  "- 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.",
11619
11673
  "- Label each task `Task <n>:` at the start of its heading or checkbox, numbered from 1, so `bst plan:brief` can find it.",
11620
- "- Give each task what makes it unambiguous and nothing more: a test's name and its assertions with the spec's exact values, a code change's exact signature and file, a command with the output that means it passed. Write out a body only for an algorithm the signature and tests do not determine. Work that uses another task's result names that task's interfaces, never repeats its code.",
11674
+ `- Give each task what makes it unambiguous and nothing more: a test's name and its assertions with the spec's exact values and why that test fails before the change, a code change's exact signature and file, a command with the output that means it passed. Write out a body only for an algorithm the signature and tests do not determine. Work that uses another task's result names that task's interfaces, never repeats its code. A task with no test to write says \`${TEST_EVIDENCE_PREFIXES.noTest} <reason>\` in the plan, so the executor inherits the call instead of making it cold.`,
11621
11675
  "- Keep tasks small. Each one runs its own test cycle and ends in a result that can be checked on its own: write the failing test, watch it fail, make it pass, commit. A run cut off partway then loses one task, not the plan. A large plan is fine; a large task is not.",
11622
11676
  "- A plan that builds on earlier work opens with a task that checks the prerequisite exists, and records a `**Blocked:**` note if it does not, instead of rebuilding it.",
11623
11677
  "- State a judgement that recurs across many places once, as a rule in the plan, so every task decides it the same way.",
@@ -11627,7 +11681,7 @@ const PLANNING_GUIDE_BODY = [
11627
11681
  "",
11628
11682
  "## Working through a plan",
11629
11683
  "",
11630
- "Do the tasks in order, commit after each, and tick a box only once its result is verified. A box you cannot finish keeps its text exactly as written and gets one note underneath:",
11684
+ `Do the tasks in order, commit after each, and tick a box only once its result is verified. Each task starts red -- write its test, watch it fail, make it pass -- and its commit body carries the testing guide's \`${TEST_EVIDENCE_PREFIXES.red}\` or \`${TEST_EVIDENCE_PREFIXES.noTest}\` line. A box you cannot finish keeps its text exactly as written and gets one note underneath:`,
11631
11685
  "",
11632
11686
  "- `**Progress:**` -- partly done: what landed, what remains, where to pick up. The next pass resumes it.",
11633
11687
  "- `**Blocked:**` -- tried and failing: the error. The next pass retries it.",
@@ -11784,6 +11838,8 @@ const JS_PRECOMMIT_RUNG = "- **Before a commit** -- `pnpm exec oxlint <the files
11784
11838
  const CHECK_CADENCE_TAIL = [
11785
11839
  "- **Once per task or plan phase** -- `pnpm check --skip 'docker:*'`. This is the gate.",
11786
11840
  "",
11841
+ "- **When `format --check` still fails after `pnpm format`** -- run `pnpm format` again until it changes nothing. A code span broken across a line inside a list item can take up to four passes to settle.",
11842
+ "",
11787
11843
  "Batch the fixes, not the runs: re-running a checker after every single fix turns one report into eight."
11788
11844
  ];
11789
11845
  function checkCadenceBody(config) {
@@ -11833,6 +11889,7 @@ const HEALTH_RULE_TAIL = [
11833
11889
  "**An exemption drops a module from one outlier list and nothing else.** Every module over a fan limit keeps a per-module ceiling in `health-baseline.json`, exempt or not, and growing past that ceiling fails the gate.",
11834
11890
  "",
11835
11891
  "When a real value edge trips the gate, the remedy depends on the module:",
11892
+ "",
11836
11893
  "- No `health.exempt` entry: split the module, or record a justified entry. The justification names what the hub wires by construction, which is what a copy would hide.",
11837
11894
  "- An exempt hub gaining an edge inside its justification's scope: adopt the growth with `bst health:check --update-baseline --reset` in the pull request that adds the edge, and say why in that PR's plan or commit. The justification is the durable record of what the hub may wire; the raise needs no second record when the new edge is already named there.",
11838
11895
  "- An exempt hub gaining an edge outside that scope: split the module, or widen the justification in `.tooling.json` as a reviewed edit of its own. A ceiling raised without either is the thing a reviewer should question.",
@@ -11895,7 +11952,7 @@ function agentGuides(config, coverageEnabled, healthEnabled) {
11895
11952
  return [
11896
11953
  {
11897
11954
  name: "testing.md",
11898
- trigger: "write or change a test",
11955
+ trigger: "change source, or write or change a test",
11899
11956
  body: TESTING_GUIDE_BODY,
11900
11957
  enabled: true
11901
11958
  },
@@ -12011,7 +12068,7 @@ function writeGuide(ctx, guide) {
12011
12068
  action: "skipped",
12012
12069
  description: "Ignored via tooling:ignore comment"
12013
12070
  };
12014
- const content = `${GUIDE_MANAGED_HEADER}\n${guide.body}\n`;
12071
+ const content = formattedTemplate(ctx, filePath, `${GUIDE_MANAGED_HEADER}\n${guide.body}\n`);
12015
12072
  if (existing === content) return {
12016
12073
  filePath,
12017
12074
  action: "skipped",
@@ -12070,6 +12127,7 @@ function syncClaudeMdFile(ctx, guides) {
12070
12127
  const existing = ctx.read(FILE);
12071
12128
  let updated = existing ?? STARTER_CLAUDE_MD;
12072
12129
  for (const block of blocks) updated = applyBlock(updated, block);
12130
+ updated = formattedTemplate(ctx, FILE, updated);
12073
12131
  if (!existing) {
12074
12132
  ctx.write(FILE, updated);
12075
12133
  return {
@@ -12262,7 +12320,7 @@ function reportFailures(failed, command) {
12262
12320
  function generateMigratePrompt(results, config, detected) {
12263
12321
  const sections = [];
12264
12322
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
12265
- sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.81.1 repo:sync\` on ${timestamp}_\n`);
12323
+ sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.82.0 repo:sync\` on ${timestamp}_\n`);
12266
12324
  const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
12267
12325
  if (changed.length > 0) {
12268
12326
  const items = changed.map((r) => {
@@ -22245,14 +22303,15 @@ function resolveLabelSet(sets, name) {
22245
22303
  * same reason its neighbour does: it parses the CLI's own `.tooling.json` config schema, which the
22246
22304
  * forge-generic package must not know about.
22247
22305
  */
22248
- /** Tolerant of an unknown sibling key (`z.looseObject`) and of a key `agent` itself does not
22249
- * declare (`tolerantBlock`): this backs a report (the autonomy-ladder startup report and the
22250
- * priority-label sweep), so a newer repo's `agent.<newKey>` must report that repo's real
22251
- * autonomy setting instead of `unknown`. Its neighbour, `branch-prune-reads.ts`'s
22252
- * `ToolingConfigBranchPruneShapeSchema`, stays strict on the `branchPrune` block itself --
22253
- * that asymmetry is deliberate: this read backs a report, where a silently-ignored key costs
22254
- * nothing; that one backs a delete, where it would cost an un-excluded branch. */
22255
- const ToolingConfigAgentShapeSchema = z.looseObject({ agent: tolerantBlock(AgentConfigSchema).optional() });
22306
+ /** Tolerant of an unknown sibling key (`z.looseObject`) and of a key that `agent` or one of its
22307
+ * nested blocks does not declare (`TolerantAgentConfigSchema`): this backs a report (the
22308
+ * autonomy-ladder startup report and the priority-label sweep), so a newer repo's
22309
+ * `agent.<newKey>` must report that repo's real autonomy setting instead of `unknown`. Its
22310
+ * neighbour, `branch-prune-reads.ts`'s `ToolingConfigBranchPruneShapeSchema`, stays strict on the
22311
+ * `branchPrune` block itself -- that asymmetry is deliberate: this read backs a report, where a
22312
+ * silently-ignored key costs nothing; that one backs a delete, where it would cost an un-excluded
22313
+ * branch. */
22314
+ const ToolingConfigAgentShapeSchema = z.looseObject({ agent: TolerantAgentConfigSchema.optional() });
22256
22315
  /**
22257
22316
  * Fetch and validate `.tooling.json#agent` straight from the forge at `ref`. Resolves rather than
22258
22317
  * throws in every case -- a transport failure, a non-404 status, unparsable JSON, or a schema
@@ -31453,7 +31512,7 @@ setForgejoLogger(log$1);
31453
31512
  * `local <sha>[-dirty]` otherwise -- so an artifact built from the working tree
31454
31513
  * can't pass itself off as the published release whose version it carries.
31455
31514
  */
31456
- const displayVersion = "0.81.1";
31515
+ const displayVersion = "0.82.0";
31457
31516
  /**
31458
31517
  * Wait until a stream's internally-buffered data has been flushed to the OS.
31459
31518
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bensandee/tooling",
3
- "version": "0.81.1",
3
+ "version": "0.82.0",
4
4
  "description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,11 +34,11 @@
34
34
  "@clack/prompts": "^1.8.1",
35
35
  "citty": "^0.2.2",
36
36
  "cross-spawn": "^7.0.6",
37
- "dependency-cruiser": "^18.3.1",
37
+ "dependency-cruiser": "^18.4.0",
38
38
  "json5": "^2.2.3",
39
39
  "jsonc-parser": "^3.3.1",
40
40
  "picomatch": "^4.0.7",
41
- "smol-toml": "^1.8.0",
41
+ "smol-toml": "^1.9.0",
42
42
  "yaml": "^2.9.1",
43
43
  "zod": "^4.6.5"
44
44
  },
@@ -51,8 +51,8 @@
51
51
  "@types/node": "24.13.5",
52
52
  "@types/picomatch": "4.0.3",
53
53
  "commit-and-tag-version": "13.2.1",
54
- "oxfmt": "0.68.0",
55
- "prettier": "3.9.8",
54
+ "oxfmt": "0.71.0",
55
+ "prettier": "3.9.9",
56
56
  "release-it": "21.1.0",
57
57
  "tsdown": "0.23.0"
58
58
  },
@@ -605,13 +605,36 @@
605
605
  "type": "boolean"
606
606
  },
607
607
  "preRunRebase": {
608
- "description": "Attempt a clean-only rebase of the work branch onto the default branch before the implement (execute) phase's container starts (default true). A clean rebase runs the container on the rebased branch; a conflicting one is aborted, reported on the run's ledger comment, and the run still proceeds on the un-rebased branch for a later agent/repair cycle to resolve -- it never fails the run. Set false to skip the attempt entirely for this repo: no fetch, no rebase, no abort, and the branch is worked exactly as cloned.",
608
+ "description": "Attempt a clean-only rebase of the work branch onto the default branch before the implement (execute) phase's container starts (default true). A clean rebase runs the container on the rebased branch; a conflicting one is aborted, reported on the run's status comment, and the run still proceeds on the un-rebased branch -- it never fails the run. Two consecutive conflicting attempts hand the unit to agent/repair before any container runs, unless the branch already contains the default branch (a hand-made merge, which leaves a repair nothing to rebase), and a timeout or a failed rebase never counts toward that pair. Set false to skip the attempt entirely for this repo: no fetch, no rebase, no abort, and the branch is worked exactly as cloned.",
609
609
  "type": "boolean"
610
610
  },
611
611
  "backlog": {
612
612
  "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.",
613
613
  "type": "boolean"
614
614
  },
615
+ "lintSweep": {
616
+ "description": "The lint-warning sweep: a periodic agent job that measures this repo's oxlint warnings on its default branch in a container and, past minWarnings, opens a pull request carrying a generated plan for an agent/implement run to work off.",
617
+ "type": "object",
618
+ "properties": {
619
+ "enabled": {
620
+ "description": "Whether the agent's lint-warning sweep may open pull requests in this repo (default: on). false disarms it; the repo is then never cloned or measured by it.",
621
+ "type": "boolean"
622
+ },
623
+ "minWarnings": {
624
+ "description": "How many oxlint warnings the default branch must carry before the sweep opens a pull request (default 5). Below the floor the warn marker on checks:run is the whole response.",
625
+ "type": "integer",
626
+ "minimum": 0,
627
+ "maximum": 9007199254740991
628
+ },
629
+ "cooldownDays": {
630
+ "description": "Days after a lint-sweep pull request merged or closed before the sweep may open another (default 7): a merged sweep whose declined warnings survive must not re-file them the next morning.",
631
+ "type": "integer",
632
+ "minimum": 0,
633
+ "maximum": 9007199254740991
634
+ }
635
+ },
636
+ "additionalProperties": false
637
+ },
615
638
  "priority": {
616
639
  "description": "Map this repo's own issue-label names onto the agent's four priority levels, e.g. {\"P1\": \"critical\", \"P4\": \"low\"}. The map UNIONS with the built-in priority/<level> names rather than replacing them, and the agent does not create aliased labels -- the repo owns names the agent did not choose. Priority affects claim order only: it never changes a run's model, effort, or any budget.",
617
640
  "type": "object",