@biffo/cli 0.303.2 → 0.303.4

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.
@@ -137,7 +137,7 @@ repository with its own independent `uv sync` / `uv.lock`, not inside
137
137
  biffo-template's workspace. Once copied out, `uv sync && uv run pytest`
138
138
  works standalone with no dependency on the rest of biffo-template.
139
139
 
140
- ## The `biffo-plugin-sdk` dependency: PyPI pin, pending the first release
140
+ ## The `biffo-plugin-sdk` dependency: PyPI pin
141
141
 
142
142
  `pyproject.toml` declares:
143
143
 
@@ -148,41 +148,30 @@ dependencies = [
148
148
  ]
149
149
  ```
150
150
 
151
- This is a **PyPI-style version pin**, and it is the correct end state: the
152
- SDK is versioned `1.0.0` and biffo-template's
151
+ This is a **PyPI-style version pin**: the SDK is published via
152
+ biffo-template's
153
153
  [`.github/workflows/publish-sdk.yml`](https://github.com/keiranholloway/biffo-template/blob/main/.github/workflows/publish-sdk.yml)
154
- builds and publishes it to PyPI (via Trusted Publishing) on a pushed
155
- `sdk-v*` tag. `>=1.0,<2.0` matches the `"biffo-plugin-sdk": "^1.0"` that
154
+ (Trusted Publishing) on a pushed `sdk-v*` tag, and the first release has
155
+ already happened — this skeleton's own `uv.lock` resolves `biffo-plugin-sdk`
156
+ 1.4.0 from `https://pypi.org/simple` (see its `biffo-plugin-sdk` entry).
157
+ `>=1.0,<2.0` matches the `"biffo-plugin-sdk": "^1.0"` that
156
158
  `biffo.plugin.json` declares, and the SDK carries its own independent
157
159
  semver — it is **not** tied to the template's core version, so a major
158
160
  bump here means the plugin API broke and nothing else.
159
161
 
160
- **Ordering caveat.** The release _pipeline_ exists; the _release_ does not
161
- yet. `biffo-plugin-sdk` has never been uploaded — the PyPI project is
162
- unregistered until the owner configures the Trusted Publisher and pushes
163
- `sdk-v1.0.0`. Until that happens, `uv sync` in a freshly-copied plugin repo
164
- still cannot resolve this dependency, and you need one of the two local
165
- overrides below. Once 1.0.0 is live, **delete the override** — the
166
- `dependencies` entry above already points at the real thing.
167
-
168
- Two ways to make local development work before that happens:
169
-
170
- 1. **Path dependency** (if developing inside a biffo-template checkout,
171
- e.g. for a plugin you plan to upstream into `services/`): add
172
- ```toml
173
- [tool.uv.sources]
174
- biffo-plugin-sdk = { path = "../../packages/python-sdk", editable = true }
175
- ```
176
- 2. **Git dependency** (developing this plugin as a genuinely separate repo
177
- against an unpublished SDK):
178
- ```toml
179
- [tool.uv.sources]
180
- biffo-plugin-sdk = { git = "https://github.com/keiranholloway/biffo-template", subdirectory = "packages/python-sdk" }
181
- ```
182
-
183
- Either override goes in `[tool.uv.sources]` only — the PyPI-style
184
- `dependencies` entry above stays as-is, so removing the override is the
185
- only change needed once the SDK actually ships to PyPI.
162
+ `uv sync` in a freshly-copied plugin repo resolves this pin normally; no
163
+ `[tool.uv.sources]` override is needed. If you are developing this plugin
164
+ *inside* a `biffo-template` checkout (e.g. one you plan to upstream into
165
+ `services/`) and want to iterate against an unreleased SDK change before it
166
+ ships, a local override still works:
167
+
168
+ ```toml
169
+ [tool.uv.sources]
170
+ biffo-plugin-sdk = { path = "../../packages/python-sdk", editable = true }
171
+ ```
172
+
173
+ Keep it local (uncommitted, or removed before committing) — the PyPI-style
174
+ `dependencies` entry above is what every scaffolded plugin repo ships with.
186
175
 
187
176
  ## Manifest validation: why CI doesn't hard-gate on `registry-schema.json`
188
177
 
@@ -4,22 +4,14 @@ version = "0.1.0"
4
4
  description = "Example Biffo plugin — replace this with your own plugin's name and description"
5
5
  requires-python = ">=3.13"
6
6
  dependencies = [
7
- # biffo-plugin-sdk has a real release path as of biffo-template's
8
- # .github/workflows/publish-sdk.yml: the package is versioned 1.0.0 and
9
- # publishes to PyPI (via Trusted Publishing) when an `sdk-v*` tag is
10
- # pushed. This pin is therefore the *correct end state* and is left as-is.
11
- #
12
- # ORDERING: the pin only resolves once the owner has actually pushed
13
- # `sdk-v1.0.0` and PyPI has the release. Until that first publish,
14
- # `uv sync` in a freshly-copied plugin repo cannot resolve it — the same
15
- # as before this comment was written, so nothing regressed here; what
16
- # changed is that the fix is now one tag push away rather than unbuilt.
17
- # Deliberately NOT worked around with a git/path source in the committed
18
- # file: baking one in would mean every plugin repo forked from this
19
- # skeleton silently keeps building against a moving git ref long after
20
- # the real release exists. Use a local `[tool.uv.sources]` override until
21
- # then — see the README's "biffo-plugin-sdk dependency" section, which
22
- # documents both options and says which to delete once 1.0.0 is live.
7
+ # biffo-plugin-sdk has a real release path via biffo-template's
8
+ # .github/workflows/publish-sdk.yml: it publishes to PyPI (via Trusted
9
+ # Publishing) when an `sdk-v*` tag is pushed. That first publish has
10
+ # already happened — this skeleton's own uv.lock resolves biffo-plugin-sdk
11
+ # 1.4.0 from https://pypi.org/simple (see uv.lock's own `biffo-plugin-sdk`
12
+ # entry) — so `uv sync` in a freshly-copied plugin repo resolves this pin
13
+ # normally, with no `[tool.uv.sources]` override needed (biffo-template#1769
14
+ # side finding).
23
15
  "biffo-plugin-sdk>=1.0,<2.0",
24
16
  "aws-lambda-powertools[tracer]>=3.4.0",
25
17
  "httpx>=0.28.1",
package/dist/index.js CHANGED
@@ -8273,7 +8273,8 @@ import {
8273
8273
  import { dirname as dirname7, join as join26 } from "path";
8274
8274
  var STANDALONE_ONLY_ENTRIES = {
8275
8275
  ".github": "standalone-repo CI/release workflows \u2014 the host monorepo already runs lint/type/test/security over services/",
8276
- "registry-schema.json": "the plugin-registry publishing schema, used when submitting a *published* plugin to the registry repo, not by an in-tree plugin"
8276
+ "registry-schema.json": "the plugin-registry publishing schema, used when submitting a *published* plugin to the registry repo, not by an in-tree plugin",
8277
+ "uv.lock": "a standalone repo's own lockfile, substituted like any other text file (issue #1769) \u2014 a nested lock inside a monorepo's services/ would be wrong there, since the host resolves its own dependency tree"
8277
8278
  };
8278
8279
  var NEVER_COPY = /* @__PURE__ */ new Set([
8279
8280
  ".git",
@@ -8284,7 +8285,6 @@ var NEVER_COPY = /* @__PURE__ */ new Set([
8284
8285
  ".pytest_cache",
8285
8286
  ".mypy_cache",
8286
8287
  "dist",
8287
- "uv.lock",
8288
8288
  ".DS_Store"
8289
8289
  ]);
8290
8290
  var PLUGIN_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
@@ -8537,6 +8537,11 @@ async function runStandaloneCreate(names, options, deps) {
8537
8537
  `
8538
8538
  );
8539
8539
  if (options.commit) {
8540
+ if (!existsSync27(join27(destDir, "uv.lock"))) {
8541
+ throw new Error(
8542
+ `Scaffolded plugin at ${destDir} has no uv.lock. The standalone layout must carry a substituted uv.lock (see plugin-scaffold.ts STANDALONE_ONLY_ENTRIES) so the first commit's CI run can satisfy 'uv sync --locked'. Refusing to commit a repo that would ship red on its very first push (issue #1769) \u2014 run 'uv lock' in ${destDir} first.`
8543
+ );
8544
+ }
8540
8545
  await deps.git.init(destDir);
8541
8546
  await deps.git.add(destDir, ["."]);
8542
8547
  await deps.git.commit(destDir, `feat: scaffold ${names.slug} plugin`);
@@ -11122,8 +11127,15 @@ async function runApiGatewayIntegrationCheck() {
11122
11127
  try {
11123
11128
  report = auditApiGatewayIntegrations(root);
11124
11129
  } catch (err) {
11130
+ const message = err instanceof Error ? err.message : String(err);
11131
+ if (message.includes("no .tf files found under")) {
11132
+ console.log(
11133
+ `\xB7 API Gateway integration-qualifier guard: not applicable \u2014 ${message} This repo carries no Terraform at all, which is expected for a non-instance satellite. Skipping.`
11134
+ );
11135
+ return;
11136
+ }
11125
11137
  console.error("\u2717 API Gateway integration-qualifier guard: could not run\n");
11126
- console.error(err instanceof Error ? err.message : String(err));
11138
+ console.error(message);
11127
11139
  process.exit(1);
11128
11140
  }
11129
11141
  console.log(`scanned ${report.filesScanned} .tf file(s) under ${root}`);
@@ -11653,6 +11665,10 @@ async function runCodeqlSuppressionCheck() {
11653
11665
  console.log("\u2713 codeql-suppression guard: no dead `codeql[...]` suppression comment found");
11654
11666
  }
11655
11667
 
11668
+ // src/scripts/check-cognito-invite-template.ts
11669
+ import { existsSync as existsSync42 } from "fs";
11670
+ import { join as join46 } from "path";
11671
+
11656
11672
  // src/lib/cognito-invite-template-guard.ts
11657
11673
  import { readdirSync as readdirSync19, readFileSync as readFileSync33, statSync as statSync11 } from "fs";
11658
11674
  import { join as join45 } from "path";
@@ -11769,6 +11785,12 @@ async function runCognitoInviteTemplateCheck() {
11769
11785
  const files = findModuleTerraformFiles(root);
11770
11786
  console.log(`audited ${files.length} .tf file(s) under modules/ under ${root}`);
11771
11787
  if (files.length === 0) {
11788
+ if (!existsSync42(join46(root, "modules"))) {
11789
+ console.log(
11790
+ `\xB7 Cognito invite template guard: not applicable \u2014 no modules/ directory under ${root} \u2014 this is not a template/instance tree (a satellite repo never carries the template-owned Terraform modules). Skipping.`
11791
+ );
11792
+ return;
11793
+ }
11772
11794
  console.error(
11773
11795
  "\u2717 Cognito invite template guard: found 0 .tf files under modules/ \u2014 this looks like a broken scan, not a clean repo. Refusing to report success over zero input."
11774
11796
  );
@@ -11787,11 +11809,11 @@ async function runCognitoInviteTemplateCheck() {
11787
11809
  }
11788
11810
 
11789
11811
  // src/scripts/check-core-direct-paths.ts
11790
- import { join as join47 } from "path";
11812
+ import { join as join48 } from "path";
11791
11813
 
11792
11814
  // src/lib/core-direct-paths-audit.ts
11793
- import { existsSync as existsSync42, readFileSync as readFileSync34, readdirSync as readdirSync20, statSync as statSync12 } from "fs";
11794
- import { join as join46 } from "path";
11815
+ import { existsSync as existsSync43, readFileSync as readFileSync34, readdirSync as readdirSync20, statSync as statSync12 } from "fs";
11816
+ import { join as join47 } from "path";
11795
11817
  var EXTERNAL_BASE_IDENTIFIERS = ["CORE_API_URL"];
11796
11818
  var API_ROUTE_PREFIX = "/api/v1";
11797
11819
  var TEST_FILE_SUFFIXES = [".test.ts", ".test.tsx", ".spec.ts", ".spec.tsx"];
@@ -11955,7 +11977,7 @@ function walkFiles(root, accept, skipDir) {
11955
11977
  return;
11956
11978
  }
11957
11979
  for (const entry of entries) {
11958
- const p = join46(dir, entry);
11980
+ const p = join47(dir, entry);
11959
11981
  let st;
11960
11982
  try {
11961
11983
  st = statSync12(p);
@@ -12050,7 +12072,7 @@ function pathMatchesAnyCorePrefix(normalized, corePrefixes, apiRoutePrefix = API
12050
12072
  }
12051
12073
  function resolveSiblingCoreSrc(params) {
12052
12074
  const { estateDir, sibling } = params;
12053
- const configPath = join46(estateDir, sibling, "biffo.sibling.json");
12075
+ const configPath = join47(estateDir, sibling, "biffo.sibling.json");
12054
12076
  let raw;
12055
12077
  try {
12056
12078
  raw = readFileSync34(configPath, "utf8");
@@ -12073,8 +12095,8 @@ function resolveSiblingCoreSrc(params) {
12073
12095
  `cannot resolve ${sibling}'s core: ${configPath} has no non-empty "core_project" field.`
12074
12096
  );
12075
12097
  }
12076
- const coreApiSrcDir = join46(estateDir, coreProject, "services", "api", "src");
12077
- if (!existsSync42(coreApiSrcDir)) {
12098
+ const coreApiSrcDir = join47(estateDir, coreProject, "services", "api", "src");
12099
+ if (!existsSync43(coreApiSrcDir)) {
12078
12100
  throw new Error(
12079
12101
  `cannot resolve ${sibling}'s core: biffo.sibling.json names core_project "${coreProject}", but ${coreApiSrcDir} does not exist -- the instance is missing from this estate checkout, not merely unmatched. Refusing to silently skip ${sibling} and shrink the audit's denominator.`
12080
12102
  );
@@ -12117,7 +12139,7 @@ function auditSiblingCoreDirectPaths(params) {
12117
12139
  async function runCoreDirectPathsCheck(opts = {}) {
12118
12140
  const root = (await execa("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12119
12141
  const sibling = opts.sibling ?? "sibling-template (self-check)";
12120
- const frontendSrcDir = opts.frontendSrc ?? join47(root, "_skeletons", "sibling-template", "apps", "frontend", "src");
12142
+ const frontendSrcDir = opts.frontendSrc ?? join48(root, "_skeletons", "sibling-template", "apps", "frontend", "src");
12121
12143
  let coreApiSrcDir;
12122
12144
  let coreProject = null;
12123
12145
  if (opts.coreSrc) {
@@ -12133,7 +12155,7 @@ async function runCoreDirectPathsCheck(opts = {}) {
12133
12155
  coreApiSrcDir = resolution.coreApiSrcDir;
12134
12156
  coreProject = resolution.coreProject;
12135
12157
  } else {
12136
- coreApiSrcDir = join47(root, "services", "api", "src");
12158
+ coreApiSrcDir = join48(root, "services", "api", "src");
12137
12159
  }
12138
12160
  const report = auditSiblingCoreDirectPaths({ sibling, frontendSrcDir, coreApiSrcDir });
12139
12161
  console.log(
@@ -12198,8 +12220,8 @@ async function runOwnershipCheck(argv) {
12198
12220
  const { stdout } = await execa("git", ["diff", "--cached", "--name-status"], { cwd: root });
12199
12221
  ({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
12200
12222
  if (messageFile) {
12201
- const { readFileSync: readFileSync47, existsSync: existsSync57 } = await import("fs");
12202
- if (existsSync57(messageFile)) commitMessage = readFileSync47(messageFile, "utf8");
12223
+ const { readFileSync: readFileSync47, existsSync: existsSync59 } = await import("fs");
12224
+ if (existsSync59(messageFile)) commitMessage = readFileSync47(messageFile, "utf8");
12203
12225
  }
12204
12226
  } else {
12205
12227
  const base = process.env["GITHUB_BASE_REF"] ?? args[0];
@@ -12300,16 +12322,16 @@ ${BOLD}If the divergence is deliberate${OFF}
12300
12322
  }
12301
12323
 
12302
12324
  // src/scripts/check-distribution-inventory.ts
12303
- import { existsSync as existsSync44 } from "fs";
12304
- import { join as join49 } from "path";
12325
+ import { existsSync as existsSync45 } from "fs";
12326
+ import { join as join50 } from "path";
12305
12327
 
12306
12328
  // src/lib/distribution-inventory.ts
12307
- import { existsSync as existsSync43, readFileSync as readFileSync35 } from "fs";
12308
- import { join as join48 } from "path";
12329
+ import { existsSync as existsSync44, readFileSync as readFileSync35 } from "fs";
12330
+ import { join as join49 } from "path";
12309
12331
  var INVENTORY_FILENAME = "distribution-inventory.json";
12310
12332
  function loadDistributionInventory(root) {
12311
- const path = join48(root, INVENTORY_FILENAME);
12312
- if (!existsSync43(path)) {
12333
+ const path = join49(root, INVENTORY_FILENAME);
12334
+ if (!existsSync44(path)) {
12313
12335
  throw new Error(
12314
12336
  `${INVENTORY_FILENAME} not found at ${root} -- expected it beside core-manifest.json`
12315
12337
  );
@@ -12474,7 +12496,7 @@ async function runDistributionInventoryCheck(root) {
12474
12496
  );
12475
12497
  return;
12476
12498
  }
12477
- if (!existsSync44(join49(repoRoot, INVENTORY_FILENAME))) {
12499
+ if (!existsSync45(join50(repoRoot, INVENTORY_FILENAME))) {
12478
12500
  console.log(
12479
12501
  `\u2713 distribution-inventory: skipped \u2014 no ${INVENTORY_FILENAME} at ${repoRoot}. This is a template-only registry; a checkout that legitimately has none has nothing for this guard to check.`
12480
12502
  );
@@ -12500,7 +12522,7 @@ async function runDistributionInventoryCheck(root) {
12500
12522
  }
12501
12523
 
12502
12524
  // src/scripts/check-distribution-remote-state.ts
12503
- import { join as join50 } from "path";
12525
+ import { join as join51 } from "path";
12504
12526
  async function ghExecCommand(file, args) {
12505
12527
  const result = await execa(file, args, { reject: false });
12506
12528
  return { stdout: String(result.stdout ?? ""), exitCode: result.exitCode ?? null };
@@ -12515,7 +12537,7 @@ async function runDistributionRemoteStateCheck(root) {
12515
12537
  }
12516
12538
  }
12517
12539
  console.log(
12518
- `distribution-remote-state: examined ${assertions.length} remote content assertion(s) declared across ${inventory.entries.length} inventory entries (${join50(repoRoot, "distribution-inventory.json")})`
12540
+ `distribution-remote-state: examined ${assertions.length} remote content assertion(s) declared across ${inventory.entries.length} inventory entries (${join51(repoRoot, "distribution-inventory.json")})`
12519
12541
  );
12520
12542
  if (assertions.length === 0) {
12521
12543
  console.log("\u2713 distribution-remote-state: nothing declared to check");
@@ -12561,7 +12583,7 @@ ${assertion.ref}`;
12561
12583
 
12562
12584
  // src/lib/eventbridge-log-permission-guard.ts
12563
12585
  import { readFileSync as readFileSync36, readdirSync as readdirSync21, statSync as statSync13 } from "fs";
12564
- import { join as join51 } from "path";
12586
+ import { join as join52 } from "path";
12565
12587
  var SKIP_DIRS3 = /* @__PURE__ */ new Set(["node_modules", ".git", ".terraform", ".worktrees", "dist"]);
12566
12588
  var EVENT_TARGET_TYPE = "aws_cloudwatch_event_target";
12567
12589
  var LOG_RESOURCE_POLICY_TYPE = "aws_cloudwatch_log_resource_policy";
@@ -12638,7 +12660,7 @@ function walkTerraformFiles2(root) {
12638
12660
  return;
12639
12661
  }
12640
12662
  for (const entry of entries) {
12641
- const p = join51(dir, entry);
12663
+ const p = join52(dir, entry);
12642
12664
  let st;
12643
12665
  try {
12644
12666
  st = statSync13(p);
@@ -12739,8 +12761,15 @@ async function runEventBridgeLogPermissionCheck() {
12739
12761
  try {
12740
12762
  report = auditEventBridgeLogPermissions(root);
12741
12763
  } catch (err) {
12764
+ const message = err instanceof Error ? err.message : String(err);
12765
+ if (message.includes("no .tf files found under")) {
12766
+ console.log(
12767
+ `\xB7 EventBridge log permission guard: not applicable \u2014 ${message} This repo carries no Terraform at all, which is expected for a non-instance satellite. Skipping.`
12768
+ );
12769
+ return;
12770
+ }
12742
12771
  console.error("\u2717 EventBridge log permission guard: could not run\n");
12743
- console.error(err instanceof Error ? err.message : String(err));
12772
+ console.error(message);
12744
12773
  process.exit(1);
12745
12774
  }
12746
12775
  console.log(`audited ${report.filesScanned} .tf file(s) under ${root}`);
@@ -12771,8 +12800,8 @@ async function runEventBridgeLogPermissionCheck() {
12771
12800
  }
12772
12801
 
12773
12802
  // src/scripts/check-instance-adoption.ts
12774
- import { existsSync as existsSync45 } from "fs";
12775
- import { join as join52 } from "path";
12803
+ import { existsSync as existsSync46 } from "fs";
12804
+ import { join as join53 } from "path";
12776
12805
  async function runInstanceAdoptionCheck(opts = {}) {
12777
12806
  if (!opts.instanceDir) {
12778
12807
  console.error(
@@ -12780,7 +12809,7 @@ async function runInstanceAdoptionCheck(opts = {}) {
12780
12809
  );
12781
12810
  process.exit(2);
12782
12811
  }
12783
- if (!existsSync45(opts.instanceDir)) {
12812
+ if (!existsSync46(opts.instanceDir)) {
12784
12813
  console.error(
12785
12814
  `\u2717 instance-adoption guard: --instance-dir ${opts.instanceDir} does not exist \u2014 cannot tell whether it is adopted, and that is not the same as a clean pass.`
12786
12815
  );
@@ -12788,7 +12817,7 @@ async function runInstanceAdoptionCheck(opts = {}) {
12788
12817
  }
12789
12818
  const root = (await execa("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12790
12819
  const theirsDir = opts.theirsDir ?? root;
12791
- const instanceLabel = opts.instance ?? join52(opts.instanceDir).split("/").filter(Boolean).pop();
12820
+ const instanceLabel = opts.instance ?? join53(opts.instanceDir).split("/").filter(Boolean).pop();
12792
12821
  const report = checkInstanceAdoption(theirsDir, opts.instanceDir);
12793
12822
  console.log(
12794
12823
  `examined ${report.examinedInstances} instance (${instanceLabel}) against ${report.registeredPairs} registered pair(s), ${report.applicablePairs} applicable to this instance, against template tree ${theirsDir}`
@@ -12811,11 +12840,11 @@ async function runInstanceAdoptionCheck(opts = {}) {
12811
12840
 
12812
12841
  // src/lib/lambda-output-guard.ts
12813
12842
  import { readFileSync as readFileSync38 } from "fs";
12814
- import { join as join54 } from "path";
12843
+ import { join as join55 } from "path";
12815
12844
 
12816
12845
  // src/lib/terraform-input-guard.ts
12817
- import { existsSync as existsSync46, readdirSync as readdirSync22, readFileSync as readFileSync37, statSync as statSync14 } from "fs";
12818
- import { join as join53 } from "path";
12846
+ import { existsSync as existsSync47, readdirSync as readdirSync22, readFileSync as readFileSync37, statSync as statSync14 } from "fs";
12847
+ import { join as join54 } from "path";
12819
12848
  var GUARDED_SUBCOMMANDS = [
12820
12849
  "init",
12821
12850
  "plan",
@@ -12829,7 +12858,7 @@ function stripComments2(source) {
12829
12858
  return source.split("\n").map((line) => line.replace(/(^|\s)#.*$/, "$1")).join("\n");
12830
12859
  }
12831
12860
  function vendoredPluginServiceDirs(repoRoot) {
12832
- const servicesDir = join53(repoRoot, "services");
12861
+ const servicesDir = join54(repoRoot, "services");
12833
12862
  const result = /* @__PURE__ */ new Set();
12834
12863
  let entries;
12835
12864
  try {
@@ -12838,9 +12867,9 @@ function vendoredPluginServiceDirs(repoRoot) {
12838
12867
  return result;
12839
12868
  }
12840
12869
  for (const entry of entries) {
12841
- const full = join53(servicesDir, entry);
12842
- if (!existsSync46(full) || !statSync14(full).isDirectory()) continue;
12843
- if (existsSync46(join53(full, "biffo.plugin.json"))) {
12870
+ const full = join54(servicesDir, entry);
12871
+ if (!existsSync47(full) || !statSync14(full).isDirectory()) continue;
12872
+ if (existsSync47(join54(full, "biffo.plugin.json"))) {
12844
12873
  result.add(entry);
12845
12874
  }
12846
12875
  }
@@ -12862,7 +12891,7 @@ function findWorkflowFiles(repoRoot) {
12862
12891
  if (entry === ".github" && relative11.startsWith("services/") && vendoredPluginDirs.has(relative11.slice("services/".length))) {
12863
12892
  continue;
12864
12893
  }
12865
- const full = join53(dir, entry);
12894
+ const full = join54(dir, entry);
12866
12895
  const rel = relative11 ? `${relative11}/${entry}` : entry;
12867
12896
  let isDir;
12868
12897
  try {
@@ -12912,7 +12941,7 @@ function checkWorkflowSource(file, rawSource) {
12912
12941
  }
12913
12942
  function checkTerraformInput(repoRoot) {
12914
12943
  return findWorkflowFiles(repoRoot).flatMap(
12915
- (file) => checkWorkflowSource(file, readFileSync37(join53(repoRoot, file), "utf8"))
12944
+ (file) => checkWorkflowSource(file, readFileSync37(join54(repoRoot, file), "utf8"))
12916
12945
  );
12917
12946
  }
12918
12947
 
@@ -12970,7 +12999,7 @@ function checkWorkflowSource2(file, rawSource) {
12970
12999
  }
12971
13000
  function checkLambdaOutput(repoRoot) {
12972
13001
  return findWorkflowFiles(repoRoot).flatMap(
12973
- (file) => checkWorkflowSource2(file, readFileSync38(join54(repoRoot, file), "utf8"))
13002
+ (file) => checkWorkflowSource2(file, readFileSync38(join55(repoRoot, file), "utf8"))
12974
13003
  );
12975
13004
  }
12976
13005
 
@@ -12998,8 +13027,8 @@ async function runLambdaOutputCheck() {
12998
13027
  }
12999
13028
 
13000
13029
  // src/scripts/check-migration-body-change.ts
13001
- import { existsSync as existsSync47 } from "fs";
13002
- import { join as join55 } from "path";
13030
+ import { existsSync as existsSync48 } from "fs";
13031
+ import { join as join56 } from "path";
13003
13032
 
13004
13033
  // src/lib/migration-body-change-guard.ts
13005
13034
  function checkMigrationBodyChangeMarkers(diffs) {
@@ -13065,7 +13094,7 @@ async function runMigrationBodyChangeCheck(argv) {
13065
13094
  );
13066
13095
  return;
13067
13096
  }
13068
- if (!existsSync47(join55(root, MIGRATIONS_VERSIONS_DIR))) {
13097
+ if (!existsSync48(join56(root, MIGRATIONS_VERSIONS_DIR))) {
13069
13098
  console.log(`\u2713 migration body-change guard: no ${MIGRATIONS_VERSIONS_DIR} in this repo.`);
13070
13099
  return;
13071
13100
  }
@@ -13167,8 +13196,8 @@ ${BOLD2}What to do${OFF2}
13167
13196
  }
13168
13197
 
13169
13198
  // src/scripts/check-orphan-ratchet.ts
13170
- import { existsSync as existsSync48 } from "fs";
13171
- import { basename as basename3 } from "path";
13199
+ import { existsSync as existsSync49 } from "fs";
13200
+ import { basename as basename3, join as join57 } from "path";
13172
13201
  function reportOrphan(entry, manifest) {
13173
13202
  console.error(` ${entry.path}`);
13174
13203
  const { templateOwnedMatch, nearestUserOwnedEntries } = explainOwnership(entry.path, manifest);
@@ -13190,7 +13219,7 @@ function reportOrphan(entry, manifest) {
13190
13219
  }
13191
13220
  }
13192
13221
  async function runOrphanRatchetCheck(opts = {}) {
13193
- if (opts.instanceDir !== void 0 && !existsSync48(opts.instanceDir)) {
13222
+ if (opts.instanceDir !== void 0 && !existsSync49(opts.instanceDir)) {
13194
13223
  console.error(
13195
13224
  `\u2717 orphan-ratchet guard: --instance-dir ${opts.instanceDir} does not exist \u2014 cannot tell whether it carries unsanctioned files, and that is not the same as a clean pass.`
13196
13225
  );
@@ -13207,6 +13236,12 @@ async function runOrphanRatchetCheck(opts = {}) {
13207
13236
  "self-check mode: --instance-dir was not given, so it defaulted to --theirs-dir (this repo's own root) alongside --base-dir \u2014 all three trees are the same, so this run can only ever find zero orphans by construction. See this script's doc comment. Pass a real instance tree with --instance-dir for a check that can actually find something. biffo-template#1714."
13208
13237
  );
13209
13238
  }
13239
+ if (!existsSync49(join57(theirsDir, CORE_MANIFEST_FILE))) {
13240
+ console.log(
13241
+ `orphan-ratchet guard (${label}): no ${CORE_MANIFEST_FILE} in ${theirsDir} \u2014 this is not a Biffo template/instance tree (a satellite repo never carries one), so there is no ownership manifest to classify paths against. Not applicable; skipping.`
13242
+ );
13243
+ return;
13244
+ }
13210
13245
  const manifest = readCoreManifest(theirsDir);
13211
13246
  const plan = await planCoreUpgrade({ baseDir, oursDir: instanceDir, theirsDir, manifest });
13212
13247
  const baseline = readOrphanBaseline(instanceDir);
@@ -13233,7 +13268,7 @@ baseline was ${String(ratchet.baseline)}, now ${String(ratchet.count)}. See biff
13233
13268
 
13234
13269
  // src/scripts/check-pipe-trap.ts
13235
13270
  import { readFileSync as readFileSync39, readdirSync as readdirSync23 } from "fs";
13236
- import { join as join56, relative as relative9 } from "path";
13271
+ import { join as join58, relative as relative9 } from "path";
13237
13272
 
13238
13273
  // src/lib/pipe-trap-guard.ts
13239
13274
  var STATUS_BEARING = [
@@ -13329,7 +13364,7 @@ function findPipeTraps(source) {
13329
13364
  function shellFiles(root) {
13330
13365
  const out = [];
13331
13366
  for (const dir of ["scripts", ".githooks"]) {
13332
- const full = join56(root, dir);
13367
+ const full = join58(root, dir);
13333
13368
  let entries;
13334
13369
  try {
13335
13370
  entries = readdirSync23(full, { withFileTypes: true });
@@ -13339,7 +13374,7 @@ function shellFiles(root) {
13339
13374
  for (const entry of entries) {
13340
13375
  if (!entry.isFile()) continue;
13341
13376
  if (dir === "scripts" && !entry.name.endsWith(".sh")) continue;
13342
- out.push(join56(full, entry.name));
13377
+ out.push(join58(full, entry.name));
13343
13378
  }
13344
13379
  }
13345
13380
  return out;
@@ -13371,9 +13406,13 @@ async function runPipeTrapCheck() {
13371
13406
  console.log(`\u2713 Pipe-trap guard: no status-bearing command is piped away`);
13372
13407
  }
13373
13408
 
13409
+ // src/scripts/check-plugin-allowlist-convention.ts
13410
+ import { existsSync as existsSync50 } from "fs";
13411
+ import { join as join60 } from "path";
13412
+
13374
13413
  // src/lib/plugin-allowlist-convention.ts
13375
13414
  import { readFileSync as readFileSync40 } from "fs";
13376
- import { join as join57 } from "path";
13415
+ import { join as join59 } from "path";
13377
13416
  var COMPUTE_MAIN_TF = "modules/cloud/aws/compute/main.tf";
13378
13417
  var PLUGIN_TEMPLATE_MAIN_TF = "modules/plugins/_template/main.tf";
13379
13418
  var ALLOWLIST_MAIN_TF = "modules/cloud/aws/plugin-allowlist/main.tf";
@@ -13384,7 +13423,7 @@ var PLUGIN = "<plugin>";
13384
13423
  var ACCOUNT = "<account>";
13385
13424
  function read(repoRoot, relative11) {
13386
13425
  try {
13387
- return readFileSync40(join57(repoRoot, relative11), "utf8");
13426
+ return readFileSync40(join59(repoRoot, relative11), "utf8");
13388
13427
  } catch {
13389
13428
  throw new Error(`plugin-allowlist drift guard: cannot read ${relative11}`);
13390
13429
  }
@@ -13479,8 +13518,21 @@ Plugins would be rejected by require_service_principal (ADR-0009). Fix the glob,
13479
13518
  }
13480
13519
 
13481
13520
  // src/scripts/check-plugin-allowlist-convention.ts
13521
+ var REQUIRED_SOURCES = [
13522
+ COMPUTE_MAIN_TF,
13523
+ PLUGIN_TEMPLATE_MAIN_TF,
13524
+ ALLOWLIST_MAIN_TF,
13525
+ ALLOWLIST_VARIABLES_TF
13526
+ ];
13482
13527
  async function runPluginAllowlistConventionCheck() {
13483
13528
  const root = (await execa("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
13529
+ const missing = REQUIRED_SOURCES.filter((relative11) => !existsSync50(join60(root, relative11)));
13530
+ if (missing.length === REQUIRED_SOURCES.length) {
13531
+ console.log(
13532
+ `\xB7 Plugin-allowlist convention guard: not applicable \u2014 ${missing.join(", ")} not found under ${root}. This is not a template/instance tree (a satellite repo never carries the plugin-allowlist Terraform modules). Skipping.`
13533
+ );
13534
+ return;
13535
+ }
13484
13536
  let violations;
13485
13537
  try {
13486
13538
  violations = checkAllowlistConvention(root);
@@ -13505,32 +13557,32 @@ async function runPluginAllowlistConventionCheck() {
13505
13557
  }
13506
13558
 
13507
13559
  // src/scripts/check-plugin-collisions.ts
13508
- import { existsSync as existsSync50 } from "fs";
13509
- import { join as join59 } from "path";
13560
+ import { existsSync as existsSync52 } from "fs";
13561
+ import { join as join62 } from "path";
13510
13562
 
13511
13563
  // src/lib/plugin-collision-guard.ts
13512
- import { existsSync as existsSync49, readdirSync as readdirSync24, statSync as statSync15 } from "fs";
13513
- import { join as join58 } from "path";
13564
+ import { existsSync as existsSync51, readdirSync as readdirSync24, statSync as statSync15 } from "fs";
13565
+ import { join as join61 } from "path";
13514
13566
  var PYTEST_SPECIAL = /* @__PURE__ */ new Set(["conftest.py"]);
13515
13567
  var IGNORED_DIRS = /* @__PURE__ */ new Set([".venv", "node_modules", "__pycache__", ".git", "dist", "build"]);
13516
13568
  function subdirectories(dir) {
13517
- if (!existsSync49(dir)) return [];
13569
+ if (!existsSync51(dir)) return [];
13518
13570
  return readdirSync24(dir).filter((entry) => {
13519
13571
  if (IGNORED_DIRS.has(entry) || entry.startsWith(".")) return false;
13520
13572
  try {
13521
- return statSync15(join58(dir, entry)).isDirectory();
13573
+ return statSync15(join61(dir, entry)).isDirectory();
13522
13574
  } catch {
13523
13575
  return false;
13524
13576
  }
13525
13577
  });
13526
13578
  }
13527
13579
  function regularPackagesOf(pluginDir2) {
13528
- return subdirectories(pluginDir2).filter((name) => existsSync49(join58(pluginDir2, name, "__init__.py"))).sort();
13580
+ return subdirectories(pluginDir2).filter((name) => existsSync51(join61(pluginDir2, name, "__init__.py"))).sort();
13529
13581
  }
13530
13582
  function bareTestModulesOf(pluginDir2) {
13531
- const testsDir = join58(pluginDir2, "tests");
13532
- if (!existsSync49(testsDir)) return [];
13533
- if (existsSync49(join58(testsDir, "__init__.py"))) return [];
13583
+ const testsDir = join61(pluginDir2, "tests");
13584
+ if (!existsSync51(testsDir)) return [];
13585
+ if (existsSync51(join61(testsDir, "__init__.py"))) return [];
13534
13586
  return readdirSync24(testsDir).filter((f) => f.endsWith(".py") && !PYTEST_SPECIAL.has(f)).sort();
13535
13587
  }
13536
13588
  function findCollisions(servicesDir, pluginDirs) {
@@ -13539,7 +13591,7 @@ function findCollisions(servicesDir, pluginDirs) {
13539
13591
  const gather = (kind, namesOf) => {
13540
13592
  const claims = /* @__PURE__ */ new Map();
13541
13593
  for (const plugin of plugins) {
13542
- for (const name of namesOf(join58(servicesDir, plugin))) {
13594
+ for (const name of namesOf(join61(servicesDir, plugin))) {
13543
13595
  claims.set(name, [...claims.get(name) ?? [], plugin]);
13544
13596
  }
13545
13597
  }
@@ -13577,8 +13629,8 @@ function formatCollisions(collisions) {
13577
13629
  // src/scripts/check-plugin-collisions.ts
13578
13630
  async function runPluginCollisionCheck() {
13579
13631
  const root = (await execa("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
13580
- const servicesDir = join59(root, "services");
13581
- if (!existsSync50(servicesDir)) {
13632
+ const servicesDir = join62(root, "services");
13633
+ if (!existsSync52(servicesDir)) {
13582
13634
  console.log("\u2713 plugin collision guard: no services/ directory \u2014 nothing to compare");
13583
13635
  return;
13584
13636
  }
@@ -13595,8 +13647,8 @@ async function runPluginCollisionCheck() {
13595
13647
  }
13596
13648
 
13597
13649
  // src/lib/plugin-terraform-guard.ts
13598
- import { existsSync as existsSync51, readFileSync as readFileSync41, readdirSync as readdirSync25 } from "fs";
13599
- import { dirname as dirname10, join as join60, relative as relative10, sep as sep4 } from "path";
13650
+ import { existsSync as existsSync53, readFileSync as readFileSync41, readdirSync as readdirSync25 } from "fs";
13651
+ import { dirname as dirname10, join as join63, relative as relative10, sep as sep4 } from "path";
13600
13652
  var SKIP_DIRS4 = /* @__PURE__ */ new Set(["node_modules", ".git", ".worktrees", "dist", ".venv", "__pycache__"]);
13601
13653
  var PLUGIN_MANIFEST_FILE2 = "biffo.plugin.json";
13602
13654
  function findPluginManifests(root) {
@@ -13611,9 +13663,9 @@ function findPluginManifests(root) {
13611
13663
  for (const entry of entries) {
13612
13664
  if (entry.isDirectory()) {
13613
13665
  if (SKIP_DIRS4.has(entry.name)) continue;
13614
- walk2(join60(dir, entry.name));
13666
+ walk2(join63(dir, entry.name));
13615
13667
  } else if (entry.isFile() && entry.name === PLUGIN_MANIFEST_FILE2) {
13616
- found.push(relative10(root, join60(dir, entry.name)).split(sep4).join("/"));
13668
+ found.push(relative10(root, join63(dir, entry.name)).split(sep4).join("/"));
13617
13669
  }
13618
13670
  }
13619
13671
  };
@@ -13638,14 +13690,14 @@ function readSubscriptions(absManifestPath) {
13638
13690
  }
13639
13691
  function checkPluginTerraform(root) {
13640
13692
  const violations = [];
13641
- const coreManifest = existsSync51(join60(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
13693
+ const coreManifest = existsSync53(join63(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
13642
13694
  for (const manifest of findPluginManifests(root)) {
13643
13695
  if (coreManifest && !isTemplateOwned(manifest, coreManifest)) continue;
13644
- const absManifest = join60(root, manifest);
13696
+ const absManifest = join63(root, manifest);
13645
13697
  const subscriptions = readSubscriptions(absManifest);
13646
13698
  if (subscriptions === null) continue;
13647
13699
  const pluginDir2 = dirname10(absManifest);
13648
- if (existsSync51(join60(pluginDir2, "terraform"))) continue;
13700
+ if (existsSync53(join63(pluginDir2, "terraform"))) continue;
13649
13701
  const relPluginDir = relative10(root, pluginDir2).split(sep4).join("/");
13650
13702
  violations.push({
13651
13703
  manifest,
@@ -13676,12 +13728,12 @@ async function runPluginTerraformCheck() {
13676
13728
  }
13677
13729
 
13678
13730
  // src/scripts/check-plugin-tool-supply.ts
13679
- import { existsSync as existsSync53 } from "fs";
13680
- import { join as join62 } from "path";
13731
+ import { existsSync as existsSync55 } from "fs";
13732
+ import { join as join65 } from "path";
13681
13733
 
13682
13734
  // src/lib/plugin-tool-supply-audit.ts
13683
- import { existsSync as existsSync52, readFileSync as readFileSync42, readdirSync as readdirSync26, statSync as statSync16 } from "fs";
13684
- import { join as join61 } from "path";
13735
+ import { existsSync as existsSync54, readFileSync as readFileSync42, readdirSync as readdirSync26, statSync as statSync16 } from "fs";
13736
+ import { join as join64 } from "path";
13685
13737
 
13686
13738
  // src/lib/openrouter-model-snapshot.ts
13687
13739
  var OPENROUTER_MODEL_SNAPSHOT_FETCHED_AT = "2026-08-10T06:39:01Z";
@@ -14098,7 +14150,7 @@ function listDirs(root) {
14098
14150
  }
14099
14151
  return entries.filter((e) => {
14100
14152
  try {
14101
- return statSync16(join61(root, e)).isDirectory();
14153
+ return statSync16(join64(root, e)).isDirectory();
14102
14154
  } catch {
14103
14155
  return false;
14104
14156
  }
@@ -14114,7 +14166,7 @@ function walkFiles2(root, accept, skipDir) {
14114
14166
  return;
14115
14167
  }
14116
14168
  for (const entry of entries) {
14117
- const p = join61(dir, entry);
14169
+ const p = join64(dir, entry);
14118
14170
  let st;
14119
14171
  try {
14120
14172
  st = statSync16(p);
@@ -14140,14 +14192,14 @@ function pluginPythonFiles(pluginDir2) {
14140
14192
  );
14141
14193
  }
14142
14194
  function pluginTerraformFiles(pluginDir2) {
14143
- const tfDir = join61(pluginDir2, "terraform");
14195
+ const tfDir = join64(pluginDir2, "terraform");
14144
14196
  let entries;
14145
14197
  try {
14146
14198
  entries = readdirSync26(tfDir);
14147
14199
  } catch {
14148
14200
  return [];
14149
14201
  }
14150
- return entries.filter((e) => e.endsWith(".tf")).map((e) => join61(tfDir, e)).sort();
14202
+ return entries.filter((e) => e.endsWith(".tf")).map((e) => join64(tfDir, e)).sort();
14151
14203
  }
14152
14204
  function extractManifestTools(manifestText) {
14153
14205
  let parsed;
@@ -14399,8 +14451,8 @@ function isSnapshotStale(fetchedAt, now) {
14399
14451
  function normalizeModelId(id) {
14400
14452
  return id.endsWith(":online") ? id.slice(0, -":online".length) : id;
14401
14453
  }
14402
- var CONFIG_PY_PATH = join61("services", "api", "src", "api", "config.py");
14403
- var ORCHESTRATION_SCHEMA_PATH = join61(
14454
+ var CONFIG_PY_PATH = join64("services", "api", "src", "api", "config.py");
14455
+ var ORCHESTRATION_SCHEMA_PATH = join64(
14404
14456
  "services",
14405
14457
  "api",
14406
14458
  "src",
@@ -14412,10 +14464,10 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
14412
14464
  const knownModelIds = options.knownModelIds ?? OPENROUTER_MODEL_IDS;
14413
14465
  const snapshotFetchedAt = options.snapshotFetchedAt ?? OPENROUTER_MODEL_SNAPSHOT_FETCHED_AT;
14414
14466
  const now = options.now ?? /* @__PURE__ */ new Date();
14415
- const configPath = join61(repoRoot, CONFIG_PY_PATH);
14416
- const orchestrationPath = join61(repoRoot, ORCHESTRATION_SCHEMA_PATH);
14417
- const configMissing = !existsSync52(configPath);
14418
- const orchestrationSchemaMissing = !existsSync52(orchestrationPath);
14467
+ const configPath = join64(repoRoot, CONFIG_PY_PATH);
14468
+ const orchestrationPath = join64(repoRoot, ORCHESTRATION_SCHEMA_PATH);
14469
+ const configMissing = !existsSync54(configPath);
14470
+ const orchestrationSchemaMissing = !existsSync54(orchestrationPath);
14419
14471
  const knownSet = new Set(knownModelIds);
14420
14472
  const snapshotEmpty = knownModelIds.length === 0;
14421
14473
  const snapshotStale = isSnapshotStale(snapshotFetchedAt, now);
@@ -14486,7 +14538,7 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
14486
14538
  function discoverPluginDirs(pluginsRoot) {
14487
14539
  return listDirs(pluginsRoot).filter((name) => {
14488
14540
  try {
14489
- return statSync16(join61(pluginsRoot, name, "biffo.plugin.json")).isFile();
14541
+ return statSync16(join64(pluginsRoot, name, "biffo.plugin.json")).isFile();
14490
14542
  } catch {
14491
14543
  return false;
14492
14544
  }
@@ -14499,8 +14551,8 @@ function auditPluginToolSupply(pluginsRoot) {
14499
14551
  let terraformBlind = false;
14500
14552
  let totalDeclaredTools = 0;
14501
14553
  for (const name of pluginNames) {
14502
- const pluginDir2 = join61(pluginsRoot, name);
14503
- const manifestText = readFileSync42(join61(pluginDir2, "biffo.plugin.json"), "utf8");
14554
+ const pluginDir2 = join64(pluginsRoot, name);
14555
+ const manifestText = readFileSync42(join64(pluginDir2, "biffo.plugin.json"), "utf8");
14504
14556
  const manifest = extractManifestTools(manifestText);
14505
14557
  if (manifest.parseError) {
14506
14558
  findings.push({
@@ -14598,7 +14650,7 @@ function auditPluginToolSupply(pluginsRoot) {
14598
14650
  requiredEnvVars: envResult.envVars,
14599
14651
  missingEnvVars: anyWired ? [] : envResult.envVars,
14600
14652
  status: anyWired ? "ok" : "missing-env",
14601
- detail: anyWired ? `${entry.predicate}() is satisfiable: at least one of ${JSON.stringify(envResult.envVars)} is wired in Terraform` : `${entry.predicate}() reads ${JSON.stringify(envResult.envVars)} \u2014 NONE of these are wired by any environment_variables block under ${join61(pluginDir2, "terraform")}, so this deployment can never supply it`
14653
+ detail: anyWired ? `${entry.predicate}() is satisfiable: at least one of ${JSON.stringify(envResult.envVars)} is wired in Terraform` : `${entry.predicate}() reads ${JSON.stringify(envResult.envVars)} \u2014 NONE of these are wired by any environment_variables block under ${join64(pluginDir2, "terraform")}, so this deployment can never supply it`
14602
14654
  });
14603
14655
  }
14604
14656
  }
@@ -14631,8 +14683,8 @@ function auditPluginToolSupply(pluginsRoot) {
14631
14683
  async function runPluginToolSupplyCheck() {
14632
14684
  const root = (await execa("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
14633
14685
  let allOk = true;
14634
- const pluginsRoot = join62(root, "services", "_plugins");
14635
- if (!existsSync53(pluginsRoot)) {
14686
+ const pluginsRoot = join65(root, "services", "_plugins");
14687
+ if (!existsSync55(pluginsRoot)) {
14636
14688
  console.log("\u2713 plugin tool-supply guard: no services/_plugins/ \u2014 nothing to audit");
14637
14689
  } else {
14638
14690
  const report = auditPluginToolSupply(pluginsRoot);
@@ -14662,8 +14714,8 @@ async function runPluginToolSupplyCheck() {
14662
14714
  console.log(`\u2713 plugin tool-supply guard: ${report.summary}`);
14663
14715
  }
14664
14716
  }
14665
- const servicesApiRoot = join62(root, "services", "api");
14666
- if (!existsSync53(servicesApiRoot)) {
14717
+ const servicesApiRoot = join65(root, "services", "api");
14718
+ if (!existsSync55(servicesApiRoot)) {
14667
14719
  console.log("\u2713 plugin model-id guard: no services/api/ \u2014 nothing to audit");
14668
14720
  } else {
14669
14721
  const modelReport = auditDeclaredModelIds(root);
@@ -15040,12 +15092,12 @@ async function runSharedFileReductionCheck(args) {
15040
15092
  }
15041
15093
 
15042
15094
  // src/scripts/check-skeleton-drift.ts
15043
- import { existsSync as existsSync54, readdirSync as readdirSync28 } from "fs";
15044
- import { join as join64 } from "path";
15095
+ import { existsSync as existsSync56, readdirSync as readdirSync28 } from "fs";
15096
+ import { join as join67 } from "path";
15045
15097
 
15046
15098
  // src/lib/skeleton-drift-guard.ts
15047
15099
  import { readFileSync as readFileSync45, readdirSync as readdirSync27, statSync as statSync17 } from "fs";
15048
- import { join as join63 } from "path";
15100
+ import { join as join66 } from "path";
15049
15101
  var isWorkflow = (rel) => rel.startsWith(".github/workflows/") && (rel.endsWith(".yml") || rel.endsWith(".yaml"));
15050
15102
  var isRootLayout = (rel) => rel.endsWith("src/app/layout.tsx");
15051
15103
  var uncommented = (contents) => contents.split("\n").filter((line) => !/^\s*(\/\/|\/\*|\*)/.test(line)).join("\n");
@@ -15109,7 +15161,7 @@ function walk(dir, base = dir) {
15109
15161
  }
15110
15162
  for (const entry of entries) {
15111
15163
  if (entry === ".venv" || entry === "node_modules" || entry === ".git") continue;
15112
- const abs = join63(dir, entry);
15164
+ const abs = join66(dir, entry);
15113
15165
  let isDir;
15114
15166
  try {
15115
15167
  isDir = statSync17(abs).isDirectory();
@@ -15131,7 +15183,7 @@ function auditSkeleton(skeletonRoot, name, rules = SKELETON_RULES) {
15131
15183
  if (!rule.appliesTo(rel)) continue;
15132
15184
  let contents;
15133
15185
  try {
15134
- contents = readFileSync45(join63(skeletonRoot, rel), "utf8");
15186
+ contents = readFileSync45(join66(skeletonRoot, rel), "utf8");
15135
15187
  } catch {
15136
15188
  continue;
15137
15189
  }
@@ -15160,23 +15212,23 @@ function formatViolations2(violations) {
15160
15212
 
15161
15213
  // src/scripts/check-skeleton-drift.ts
15162
15214
  function discoverSkeletons(root) {
15163
- const skeletonsDir = join64(root, "_skeletons");
15215
+ const skeletonsDir = join67(root, "_skeletons");
15164
15216
  let entries;
15165
15217
  try {
15166
15218
  entries = readdirSync28(skeletonsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
15167
15219
  } catch {
15168
15220
  return [];
15169
15221
  }
15170
- return entries.filter((name) => existsSync54(join64(skeletonsDir, name, ".github", "workflows", "ci.yml"))).sort();
15222
+ return entries.filter((name) => existsSync56(join67(skeletonsDir, name, ".github", "workflows", "ci.yml"))).sort();
15171
15223
  }
15172
15224
  async function runSkeletonDriftCheck() {
15173
15225
  const root = (await execa("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
15174
15226
  const skeletons = discoverSkeletons(root);
15175
15227
  let filesConsidered = 0;
15176
15228
  for (const name of skeletons) {
15177
- const skeletonRoot = join64(root, "_skeletons", name);
15229
+ const skeletonRoot = join67(root, "_skeletons", name);
15178
15230
  filesConsidered += findWorkflowFiles(skeletonRoot).length;
15179
- if (existsSync54(join64(skeletonRoot, "apps", "frontend", "src", "app", "layout.tsx"))) {
15231
+ if (existsSync56(join67(skeletonRoot, "apps", "frontend", "src", "app", "layout.tsx"))) {
15180
15232
  filesConsidered += 1;
15181
15233
  }
15182
15234
  }
@@ -15184,13 +15236,19 @@ async function runSkeletonDriftCheck() {
15184
15236
  `audited ${skeletons.length} skeleton(s) (${skeletons.join(", ") || "none"}), ${filesConsidered} file(s) considered, under ${root}/_skeletons`
15185
15237
  );
15186
15238
  if (skeletons.length === 0) {
15239
+ if (!existsSync56(join67(root, "_skeletons"))) {
15240
+ console.log(
15241
+ `\xB7 Skeleton-drift guard: no _skeletons/ directory under ${root} \u2014 this is not a repo that ships scaffolding (a satellite repo never carries one). Not applicable; skipping.`
15242
+ );
15243
+ return;
15244
+ }
15187
15245
  console.error(
15188
15246
  "\u2717 Skeleton-drift guard: found 0 repo skeletons under _skeletons/ \u2014 this looks like a broken scan, not a repo with no scaffolding. Refusing to report success over zero input."
15189
15247
  );
15190
15248
  process.exit(1);
15191
15249
  }
15192
15250
  const violations = skeletons.flatMap(
15193
- (name) => auditSkeleton(join64(root, "_skeletons", name), name)
15251
+ (name) => auditSkeleton(join67(root, "_skeletons", name), name)
15194
15252
  );
15195
15253
  if (violations.length > 0) {
15196
15254
  console.error("\u2717 Skeleton-drift guard: drift found between this repo and its scaffolding\n");
@@ -15386,8 +15444,8 @@ function rawArgsAfter(subcommand) {
15386
15444
  }
15387
15445
 
15388
15446
  // src/commands/doctor.ts
15389
- import { existsSync as existsSync55, readFileSync as readFileSync46 } from "fs";
15390
- import { join as join65, resolve as resolve20 } from "path";
15447
+ import { existsSync as existsSync57, readFileSync as readFileSync46 } from "fs";
15448
+ import { join as join68, resolve as resolve20 } from "path";
15391
15449
  import chalk21 from "chalk";
15392
15450
  import { Command as Command25 } from "commander";
15393
15451
 
@@ -15782,8 +15840,8 @@ async function runDoctorFixBranches(cwd, facts, deps = { git: new GitAdapter(),
15782
15840
  return reapAllBareBranches(cwd, facts.branches, facts.worktrees, facts.currentBranch, deps);
15783
15841
  }
15784
15842
  function readLocalCoreVersion(cwd) {
15785
- const path = join65(cwd, INSTANCE_CORE_FILE);
15786
- if (!existsSync55(path)) return null;
15843
+ const path = join68(cwd, INSTANCE_CORE_FILE);
15844
+ if (!existsSync57(path)) return null;
15787
15845
  try {
15788
15846
  return extractVersionField(readFileSync46(path, "utf8"));
15789
15847
  } catch {
@@ -15805,8 +15863,8 @@ function extractVersionField(contents) {
15805
15863
  return match?.[1] ?? null;
15806
15864
  }
15807
15865
  function readFossil(cwd) {
15808
- const path = join65(cwd, CORE_VERSION_FILE);
15809
- if (!existsSync55(path)) return null;
15866
+ const path = join68(cwd, CORE_VERSION_FILE);
15867
+ if (!existsSync57(path)) return null;
15810
15868
  try {
15811
15869
  const value = readFileSync46(path, "utf8").trim();
15812
15870
  return value === "" ? null : value;
@@ -16337,13 +16395,13 @@ import { fileURLToPath as fileURLToPath6 } from "url";
16337
16395
  import { Command as Command27 } from "commander";
16338
16396
 
16339
16397
  // src/lib/packaged-scripts.ts
16340
- import { existsSync as existsSync56 } from "fs";
16341
- import { dirname as dirname11, join as join66 } from "path";
16398
+ import { existsSync as existsSync58 } from "fs";
16399
+ import { dirname as dirname11, join as join69 } from "path";
16342
16400
  function findPackagedScript(startDir, relativePath) {
16343
16401
  let dir = startDir;
16344
16402
  for (; ; ) {
16345
- const candidate = join66(dir, relativePath);
16346
- if (existsSync56(candidate)) return candidate;
16403
+ const candidate = join69(dir, relativePath);
16404
+ if (existsSync58(candidate)) return candidate;
16347
16405
  const parent = dirname11(dir);
16348
16406
  if (parent === dir) return null;
16349
16407
  dir = parent;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.303.2",
3
+ "version": "0.303.4",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",