@biffo/cli 0.305.1 → 0.307.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.
@@ -127,6 +127,46 @@ runtime config rather than tenant-scoped table rows:
127
127
  > [ADR-0011](https://github.com/keiranholloway/biffo-template/blob/main/docs/ADR/0011-authorization-is-a-core-concern.md)
128
128
  > (authorization is a Core concern, not a plugin) and no longer exists.
129
129
 
130
+ ## User-facing frontend: the `user_frontend` contract (ADR-0021 §2)
131
+
132
+ If your plugin ships a founder-facing UI, it declares a `user_frontend` block
133
+ in `biffo.plugin.json`:
134
+
135
+ ```json
136
+ "user_frontend": {
137
+ "dir": "web/dist",
138
+ "required_group": "founder"
139
+ }
140
+ ```
141
+
142
+ and ships **`web/` and nothing else** — a built static bundle (`index.html`
143
+ plus hashed `assets/*`, the ordinary Vite build-output shape) at the
144
+ repo-relative path `dir` names. That is the entire contract. In particular a
145
+ `user_frontend` plugin ships **no** `terraform/` frontend resources and
146
+ **no** `deploy-frontend.yml` workflow of its own.
147
+
148
+ **Who serves it, and why there is nothing to provision.** The shared plugin
149
+ host serves the bundle directly — `GET /api/v1/plugins/<name>/ui` and
150
+ `GET /api/v1/plugins/<name>/ui/{proxy+}` on the API Gateway routes every
151
+ installed plugin already shares — the same way it already serves
152
+ `admin_ingress`'s admin shell. There is no per-plugin S3 origin, no per-plugin
153
+ CloudFront behaviour, and nothing for `biffo plugin install` to provision:
154
+ committing `web/dist/` is the whole deploy. See
155
+ [ADR-0021](../../docs/ADR/0021-shared-plugin-hosting.md) section 2 for the
156
+ full design and why this superseded the older, per-plugin hosting model.
157
+
158
+ **This retires ADR-0018 section 2's per-plugin frontend shape** — a dedicated
159
+ `aws_s3_bucket` behind the shared CloudFront distribution, wired through a
160
+ `cdn_distribution_arn` module variable and a `frontend_bucket_*` output. That
161
+ shape is fail-closed at install time: `biffo plugin install` and
162
+ `biffo plugin upgrade` **refuse** a plugin whose `terraform/` declares it —
163
+ an `aws_s3_bucket` whose name interpolates `-plugin-…-web`, an output named
164
+ `frontend_bucket_*`, or a `cdn_distribution_arn` variable — with an error
165
+ naming ADR-0021 section 2 and this contract. If you are updating an older
166
+ plugin that still ships one of these, delete `terraform/`'s frontend
167
+ resources (and `deploy-frontend.yml`, if present) and adopt `user_frontend`
168
+ above instead; there is nothing left to provision once you do.
169
+
130
170
  ## Standalone repo, not a monorepo package
131
171
 
132
172
  Unlike biffo-template's own in-monorepo plugins (e.g. `services/_plugins/orchestrator/`,
package/dist/index.js CHANGED
@@ -3504,6 +3504,45 @@ function declaredOutputs(moduleDir) {
3504
3504
  }
3505
3505
  return names;
3506
3506
  }
3507
+ var RETIRED_FRONTEND_BUCKET_PATTERN = /bucket\s*=\s*"[^"]*-plugin-[^"]*-web"/;
3508
+ function declaresRetiredFrontendBucket(moduleDir) {
3509
+ let entries;
3510
+ try {
3511
+ entries = readdirSync3(moduleDir, { withFileTypes: true });
3512
+ } catch {
3513
+ return false;
3514
+ }
3515
+ for (const entry of entries) {
3516
+ if (!entry.isFile() || !entry.name.endsWith(".tf")) continue;
3517
+ let contents;
3518
+ try {
3519
+ contents = readFileSync10(join13(moduleDir, entry.name), "utf8");
3520
+ } catch {
3521
+ continue;
3522
+ }
3523
+ if (RETIRED_FRONTEND_BUCKET_PATTERN.test(contents)) return true;
3524
+ }
3525
+ return false;
3526
+ }
3527
+ function findRetiredFrontendShape(terraformDir) {
3528
+ const reasons = [];
3529
+ if (!existsSync12(terraformDir)) return reasons;
3530
+ if (declaresRetiredFrontendBucket(terraformDir)) {
3531
+ reasons.push('an aws_s3_bucket whose name interpolates "-plugin-...-web"');
3532
+ }
3533
+ for (const name of [...declaredOutputs(terraformDir)].sort()) {
3534
+ if (name.startsWith("frontend_bucket")) {
3535
+ reasons.push(`an output named "${name}"`);
3536
+ }
3537
+ }
3538
+ if (declaredVariables(terraformDir).has("cdn_distribution_arn")) {
3539
+ reasons.push('a "cdn_distribution_arn" variable');
3540
+ }
3541
+ return reasons;
3542
+ }
3543
+ function retiredFrontendShapeError(pluginName, reasons) {
3544
+ return `Plugin '${pluginName}' ships a terraform/ frontend on the retired ADR-0018 \xA72 per-plugin hosting shape: it declares ${reasons.join(", ")}. ADR-0021 \xA72 replaced this \u2014 a user-facing plugin's frontend is served by the shared plugin host directly from the manifest's \`user_frontend\` field (\`{ dir, required_group }\`); it ships \`web/\` and nothing else, with no terraform/ frontend and no deploy-frontend.yml. Update the plugin to the \`user_frontend\` contract (see _skeletons/plugin-template/README.md) before installing it here.`;
3545
+ }
3507
3546
  function renderArguments(args, indent) {
3508
3547
  const width = Math.max(...args.map(([key]) => key.length));
3509
3548
  return args.map(([key, value]) => `${indent}${key.padEnd(width)} = ${value}`).join("\n");
@@ -9488,6 +9527,10 @@ async function runPluginInstall(target, options, deps) {
9488
9527
  log.success(
9489
9528
  `Manifest valid \u2014 ${manifest.tables.length} table(s), ${manifest.api_routes.length} route(s)`
9490
9529
  );
9530
+ const retiredShapeReasons = findRetiredFrontendShape(join33(source.sourceDir, "terraform"));
9531
+ if (retiredShapeReasons.length > 0) {
9532
+ throw new Error(retiredFrontendShapeError(pluginName, retiredShapeReasons));
9533
+ }
9491
9534
  if (inTreeSource) {
9492
9535
  log.info(`${relTargetDir}/ is already in this checkout \u2014 installing in place.`);
9493
9536
  } else {
@@ -10256,6 +10299,10 @@ async function runPluginUpgrade(target, options, deps) {
10256
10299
  log.success(
10257
10300
  `Manifest valid \u2014 ${manifest.tables.length} table(s), ${manifest.api_routes.length} route(s)`
10258
10301
  );
10302
+ const retiredShapeReasons = findRetiredFrontendShape(join38(tmpDir, "terraform"));
10303
+ if (retiredShapeReasons.length > 0) {
10304
+ throw new Error(retiredFrontendShapeError(entry.name, retiredShapeReasons));
10305
+ }
10259
10306
  if (!existsSync36(join38(tmpDir, "terraform"))) {
10260
10307
  refuseIfModuleStillReferenced(options.cwd, modulesDir, entry.name);
10261
10308
  }
@@ -10370,6 +10417,10 @@ async function runLocalPluginRefresh(localPath, options, deps) {
10370
10417
  log.success(
10371
10418
  `Manifest valid \u2014 ${manifest.tables.length} table(s), ${manifest.api_routes.length} route(s)`
10372
10419
  );
10420
+ const retiredShapeReasons = findRetiredFrontendShape(join38(source.sourceDir, "terraform"));
10421
+ if (retiredShapeReasons.length > 0) {
10422
+ throw new Error(retiredFrontendShapeError(source.name, retiredShapeReasons));
10423
+ }
10373
10424
  if (!existsSync36(join38(source.sourceDir, "terraform"))) {
10374
10425
  refuseIfModuleStillReferenced(options.cwd, modulesDir, source.name);
10375
10426
  }
@@ -12335,8 +12386,8 @@ async function runOwnershipCheck(argv) {
12335
12386
  const { stdout } = await execa("git", ["diff", "--cached", "--name-status"], { cwd: root });
12336
12387
  ({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
12337
12388
  if (messageFile) {
12338
- const { readFileSync: readFileSync47, existsSync: existsSync59 } = await import("fs");
12339
- if (existsSync59(messageFile)) commitMessage = readFileSync47(messageFile, "utf8");
12389
+ const { readFileSync: readFileSync48, existsSync: existsSync59 } = await import("fs");
12390
+ if (existsSync59(messageFile)) commitMessage = readFileSync48(messageFile, "utf8");
12340
12391
  }
12341
12392
  } else {
12342
12393
  const base = process.env["GITHUB_BASE_REF"] ?? args[0];
@@ -13396,9 +13447,244 @@ baseline was ${String(ratchet.baseline)}, now ${String(ratchet.count)}. See biff
13396
13447
  }
13397
13448
  }
13398
13449
 
13450
+ // src/lib/ownership-header-claim-guard.ts
13451
+ import { readFileSync as readFileSync39 } from "fs";
13452
+ import { join as join58 } from "path";
13453
+ var OWNERSHIP_HEADER_SWEEP_DIRS = [
13454
+ "scripts",
13455
+ ".githooks",
13456
+ ".github",
13457
+ "services",
13458
+ "cli",
13459
+ "infra"
13460
+ ];
13461
+ var STRICT_MARKER = /(?<!`)(?:INSTANCE-OWNED|NOT a template file)(?!`)/;
13462
+ var SELF_REFERENTIAL_CLAIM = /\b(?:[Tt]his|[Ii]t)\b(?:\s+\S+){0,3}\s+(?:is|are|stays|remains)\s+(?:\*\*)?(template-owned|user-owned)\b/;
13463
+ var EM_DASH_FIRST_LINE_CLAIM = /[—–]\s*(template-owned|user-owned)\b/;
13464
+ var MAX_HEADER_LINES = 10;
13465
+ function claimFor(matchedPhrase) {
13466
+ return /^template-owned$/i.test(matchedPhrase) ? "template" : "user";
13467
+ }
13468
+ function claimInText(text) {
13469
+ const candidates = [];
13470
+ const strict = STRICT_MARKER.exec(text);
13471
+ if (strict)
13472
+ candidates.push({ matchedPhrase: strict[0], claim: claimFor(strict[0]), index: strict.index });
13473
+ const grammatical = SELF_REFERENTIAL_CLAIM.exec(text);
13474
+ if (grammatical) {
13475
+ const phrase = grammatical[1];
13476
+ candidates.push({ matchedPhrase: phrase, claim: claimFor(phrase), index: grammatical.index });
13477
+ }
13478
+ const firstLine = text.split("\n", 1)[0] ?? "";
13479
+ const emDash = EM_DASH_FIRST_LINE_CLAIM.exec(firstLine);
13480
+ if (emDash) {
13481
+ const phrase = emDash[1];
13482
+ candidates.push({ matchedPhrase: phrase, claim: claimFor(phrase), index: emDash.index });
13483
+ }
13484
+ if (candidates.length === 0) return null;
13485
+ candidates.sort((a, b) => a.index - b.index);
13486
+ return candidates[0] ?? null;
13487
+ }
13488
+ function fileExtension(path) {
13489
+ const dot = path.lastIndexOf(".");
13490
+ const slash = path.lastIndexOf("/");
13491
+ return dot > slash ? path.slice(dot).toLowerCase() : "";
13492
+ }
13493
+ function isCommentLine(line) {
13494
+ return line.trim().startsWith("#");
13495
+ }
13496
+ function lineAt(lines, i) {
13497
+ return lines[i] ?? "";
13498
+ }
13499
+ function leadingHashCommentEnd(lines, start) {
13500
+ let i = start;
13501
+ while (i < lines.length) {
13502
+ if (isCommentLine(lineAt(lines, i))) {
13503
+ i++;
13504
+ continue;
13505
+ }
13506
+ if (lineAt(lines, i).trim() === "") {
13507
+ let j = i + 1;
13508
+ while (j < lines.length && lineAt(lines, j).trim() === "") j++;
13509
+ if (j < lines.length && isCommentLine(lineAt(lines, j))) {
13510
+ i = j;
13511
+ continue;
13512
+ }
13513
+ return i;
13514
+ }
13515
+ return i;
13516
+ }
13517
+ return i;
13518
+ }
13519
+ function defaultHeaderRange(lines) {
13520
+ const start = lineAt(lines, 0).startsWith("#!") ? 1 : 0;
13521
+ const end = leadingHashCommentEnd(lines, start);
13522
+ return end > start ? [start, end] : null;
13523
+ }
13524
+ function pythonHeaderRange(lines) {
13525
+ let i = lineAt(lines, 0).startsWith("#!") ? 1 : 0;
13526
+ while (i < lines.length && lineAt(lines, i).trim() === "") i++;
13527
+ if (i >= lines.length) return null;
13528
+ const first = lineAt(lines, i);
13529
+ const trimmed = first.trimStart();
13530
+ const quote = trimmed.startsWith('"""') ? '"""' : trimmed.startsWith("'''") ? "'''" : null;
13531
+ if (quote) {
13532
+ const afterOpen = first.slice(first.indexOf(quote) + 3);
13533
+ if (afterOpen.includes(quote)) return [i, i + 1];
13534
+ let j = i + 1;
13535
+ while (j < lines.length && !lineAt(lines, j).includes(quote)) j++;
13536
+ return [i, Math.min(j + 1, lines.length)];
13537
+ }
13538
+ const end = leadingHashCommentEnd(lines, i);
13539
+ return end > i ? [i, end] : null;
13540
+ }
13541
+ function jsHeaderRange(lines) {
13542
+ let i = 0;
13543
+ while (i < lines.length && (lineAt(lines, i).trim() === "" || lineAt(lines, i).trim().startsWith("import "))) {
13544
+ i++;
13545
+ }
13546
+ if (i >= lines.length) return null;
13547
+ const first = lineAt(lines, i).trim();
13548
+ if (first.startsWith("/*")) {
13549
+ let j = i;
13550
+ while (j < lines.length && !lineAt(lines, j).includes("*/")) j++;
13551
+ return [i, Math.min(j + 1, lines.length)];
13552
+ }
13553
+ if (first.startsWith("//")) {
13554
+ let j = i;
13555
+ while (j < lines.length && (lineAt(lines, j).trim().startsWith("//") || lineAt(lines, j).trim() === ""))
13556
+ j++;
13557
+ return [i, j];
13558
+ }
13559
+ return null;
13560
+ }
13561
+ function headerRange(path, lines) {
13562
+ const ext = fileExtension(path);
13563
+ let range;
13564
+ if (ext === ".py") range = pythonHeaderRange(lines);
13565
+ else if (ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".mjs")
13566
+ range = jsHeaderRange(lines);
13567
+ else if (ext === ".md")
13568
+ range = [0, Math.min(15, lines.length)];
13569
+ else if (ext === ".json")
13570
+ return null;
13571
+ else range = defaultHeaderRange(lines);
13572
+ if (!range) return null;
13573
+ const [start, end] = range;
13574
+ return [start, Math.min(end, start + MAX_HEADER_LINES)];
13575
+ }
13576
+ function findJsonDescriptionClaim(path, content) {
13577
+ let data;
13578
+ try {
13579
+ data = JSON.parse(content);
13580
+ } catch {
13581
+ return null;
13582
+ }
13583
+ if (typeof data !== "object" || data === null) return null;
13584
+ const description = data["description"];
13585
+ if (typeof description !== "string") return null;
13586
+ const match = /INSTANCE-OWNED|NOT a template file|user-owned|template-owned/i.exec(description);
13587
+ if (!match) return null;
13588
+ const lineIndex = content.split("\n").findIndex((l) => l.includes('"description"'));
13589
+ return {
13590
+ path,
13591
+ claim: claimFor(match[0]),
13592
+ matchedPhrase: match[0],
13593
+ line: lineIndex === -1 ? 1 : lineIndex + 1
13594
+ };
13595
+ }
13596
+ function findHeaderClaim(path, content) {
13597
+ if (fileExtension(path) === ".json") return findJsonDescriptionClaim(path, content);
13598
+ const lines = content.split("\n");
13599
+ const range = headerRange(path, lines);
13600
+ if (!range) return null;
13601
+ const [start, end] = range;
13602
+ const windowText = lines.slice(start, end).join("\n");
13603
+ const found = claimInText(windowText);
13604
+ if (!found) return null;
13605
+ const line = start + windowText.slice(0, found.index).split("\n").length;
13606
+ return { path, claim: found.claim, matchedPhrase: found.matchedPhrase, line };
13607
+ }
13608
+ function sweepOwnershipHeaderClaims(root, options = {}) {
13609
+ const tracked = options.git ? gitTrackedFiles(root, options.git) : gitTrackedFiles(root);
13610
+ if (!tracked) return [];
13611
+ const files = [...tracked].filter((p) => OWNERSHIP_HEADER_SWEEP_DIRS.some((dir) => p === dir || p.startsWith(`${dir}/`))).sort();
13612
+ const hits = [];
13613
+ for (const rel of files) {
13614
+ let content;
13615
+ try {
13616
+ content = readFileSync39(join58(root, rel), "utf8");
13617
+ } catch {
13618
+ continue;
13619
+ }
13620
+ const hit = findHeaderClaim(rel, content);
13621
+ if (hit) hits.push(hit);
13622
+ }
13623
+ return hits;
13624
+ }
13625
+ function matchesReleased(relPath, manifest) {
13626
+ return (manifest.released ?? []).some((p) => relPath === p || relPath.startsWith(p));
13627
+ }
13628
+ function checkOwnershipHeaderClaims(hits, manifest) {
13629
+ const out = [];
13630
+ for (const hit of hits) {
13631
+ const manifestSaysTemplateOwned = isTemplateOwned(hit.path, manifest);
13632
+ const released = matchesReleased(hit.path, manifest);
13633
+ const agrees = hit.claim === "template" ? manifestSaysTemplateOwned || released : !manifestSaysTemplateOwned && !released;
13634
+ if (!agrees) {
13635
+ out.push({ ...hit, manifestSaysTemplateOwned });
13636
+ }
13637
+ }
13638
+ return out;
13639
+ }
13640
+
13641
+ // src/scripts/check-ownership-header-claim.ts
13642
+ async function runOwnershipHeaderClaimCheck() {
13643
+ const root = (await execa("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
13644
+ const ownership = classifyRepoOwnership(root);
13645
+ if (ownership === "satellite") {
13646
+ console.log(
13647
+ "\u2713 ownership-header-claim guard: skipped \u2014 this repo carries no core-manifest.json, so there is no ownership authority to check a header claim against."
13648
+ );
13649
+ return;
13650
+ }
13651
+ const manifest = readCoreManifest(root);
13652
+ const hits = sweepOwnershipHeaderClaims(root);
13653
+ console.log(
13654
+ `examined ${hits.length} file(s) carrying an ownership-style header claim under scripts/, .githooks/, .github/, services/, cli/, infra/`
13655
+ );
13656
+ if (hits.length === 0) {
13657
+ console.error(
13658
+ "\u2717 ownership-header-claim guard: swept zero files \u2014 refusing a false green rather than assuming every ownership-style header vanished."
13659
+ );
13660
+ process.exit(1);
13661
+ return;
13662
+ }
13663
+ const disagreements = checkOwnershipHeaderClaims(hits, manifest);
13664
+ if (disagreements.length > 0) {
13665
+ console.error(
13666
+ `\u2717 ownership-header-claim guard: ${disagreements.length} of ${hits.length} file(s) carry a header claim core-manifest.json disagrees with (#1911):
13667
+ `
13668
+ );
13669
+ for (const d of disagreements) {
13670
+ console.error(
13671
+ ` ${d.path}:${d.line} header says "${d.matchedPhrase}" (${d.claim}-owned), but core-manifest.json says ${d.manifestSaysTemplateOwned ? "template-owned" : "user-owned"}`
13672
+ );
13673
+ }
13674
+ console.error(
13675
+ "\nEither fix the header to match core-manifest.json, or fix core-manifest.json to match reality \u2014 never leave the two disagreeing."
13676
+ );
13677
+ process.exit(1);
13678
+ return;
13679
+ }
13680
+ console.log(
13681
+ `\u2713 ownership-header-claim guard: examined ${hits.length} file(s), all agree with core-manifest.json`
13682
+ );
13683
+ }
13684
+
13399
13685
  // src/scripts/check-pipe-trap.ts
13400
- import { readFileSync as readFileSync39, readdirSync as readdirSync23 } from "fs";
13401
- import { join as join58, relative as relative9 } from "path";
13686
+ import { readFileSync as readFileSync40, readdirSync as readdirSync23 } from "fs";
13687
+ import { join as join59, relative as relative9 } from "path";
13402
13688
 
13403
13689
  // src/lib/pipe-trap-guard.ts
13404
13690
  var STATUS_BEARING = [
@@ -13494,7 +13780,7 @@ function findPipeTraps(source) {
13494
13780
  function shellFiles(root) {
13495
13781
  const out = [];
13496
13782
  for (const dir of ["scripts", ".githooks"]) {
13497
- const full = join58(root, dir);
13783
+ const full = join59(root, dir);
13498
13784
  let entries;
13499
13785
  try {
13500
13786
  entries = readdirSync23(full, { withFileTypes: true });
@@ -13504,7 +13790,7 @@ function shellFiles(root) {
13504
13790
  for (const entry of entries) {
13505
13791
  if (!entry.isFile()) continue;
13506
13792
  if (dir === "scripts" && !entry.name.endsWith(".sh")) continue;
13507
- out.push(join58(full, entry.name));
13793
+ out.push(join59(full, entry.name));
13508
13794
  }
13509
13795
  }
13510
13796
  return out;
@@ -13520,7 +13806,7 @@ async function runPipeTrapCheck() {
13520
13806
  process.exit(1);
13521
13807
  }
13522
13808
  const findings = files.flatMap(
13523
- (file) => findPipeTraps(readFileSync39(file, "utf8")).map(
13809
+ (file) => findPipeTraps(readFileSync40(file, "utf8")).map(
13524
13810
  (t) => `${relative9(root, file)}:${t.line} ${t.text}
13525
13811
  ${t.reason}`
13526
13812
  )
@@ -13538,11 +13824,11 @@ async function runPipeTrapCheck() {
13538
13824
 
13539
13825
  // src/scripts/check-plugin-allowlist-convention.ts
13540
13826
  import { existsSync as existsSync50 } from "fs";
13541
- import { join as join60 } from "path";
13827
+ import { join as join61 } from "path";
13542
13828
 
13543
13829
  // src/lib/plugin-allowlist-convention.ts
13544
- import { readFileSync as readFileSync40 } from "fs";
13545
- import { join as join59 } from "path";
13830
+ import { readFileSync as readFileSync41 } from "fs";
13831
+ import { join as join60 } from "path";
13546
13832
  var COMPUTE_MAIN_TF = "modules/cloud/aws/compute/main.tf";
13547
13833
  var PLUGIN_TEMPLATE_MAIN_TF = "modules/plugins/_template/main.tf";
13548
13834
  var ALLOWLIST_MAIN_TF = "modules/cloud/aws/plugin-allowlist/main.tf";
@@ -13553,7 +13839,7 @@ var PLUGIN = "<plugin>";
13553
13839
  var ACCOUNT = "<account>";
13554
13840
  function read(repoRoot, relative11) {
13555
13841
  try {
13556
- return readFileSync40(join59(repoRoot, relative11), "utf8");
13842
+ return readFileSync41(join60(repoRoot, relative11), "utf8");
13557
13843
  } catch {
13558
13844
  throw new Error(`plugin-allowlist drift guard: cannot read ${relative11}`);
13559
13845
  }
@@ -13656,7 +13942,7 @@ var REQUIRED_SOURCES = [
13656
13942
  ];
13657
13943
  async function runPluginAllowlistConventionCheck() {
13658
13944
  const root = (await execa("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
13659
- const missing = REQUIRED_SOURCES.filter((relative11) => !existsSync50(join60(root, relative11)));
13945
+ const missing = REQUIRED_SOURCES.filter((relative11) => !existsSync50(join61(root, relative11)));
13660
13946
  if (missing.length === REQUIRED_SOURCES.length) {
13661
13947
  console.log(
13662
13948
  `\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.`
@@ -13688,11 +13974,11 @@ async function runPluginAllowlistConventionCheck() {
13688
13974
 
13689
13975
  // src/scripts/check-plugin-collisions.ts
13690
13976
  import { existsSync as existsSync52 } from "fs";
13691
- import { join as join62 } from "path";
13977
+ import { join as join63 } from "path";
13692
13978
 
13693
13979
  // src/lib/plugin-collision-guard.ts
13694
13980
  import { existsSync as existsSync51, readdirSync as readdirSync24, statSync as statSync15 } from "fs";
13695
- import { join as join61 } from "path";
13981
+ import { join as join62 } from "path";
13696
13982
  var PYTEST_SPECIAL = /* @__PURE__ */ new Set(["conftest.py"]);
13697
13983
  var IGNORED_DIRS = /* @__PURE__ */ new Set([".venv", "node_modules", "__pycache__", ".git", "dist", "build"]);
13698
13984
  function subdirectories(dir) {
@@ -13700,19 +13986,19 @@ function subdirectories(dir) {
13700
13986
  return readdirSync24(dir).filter((entry) => {
13701
13987
  if (IGNORED_DIRS.has(entry) || entry.startsWith(".")) return false;
13702
13988
  try {
13703
- return statSync15(join61(dir, entry)).isDirectory();
13989
+ return statSync15(join62(dir, entry)).isDirectory();
13704
13990
  } catch {
13705
13991
  return false;
13706
13992
  }
13707
13993
  });
13708
13994
  }
13709
13995
  function regularPackagesOf(pluginDir2) {
13710
- return subdirectories(pluginDir2).filter((name) => existsSync51(join61(pluginDir2, name, "__init__.py"))).sort();
13996
+ return subdirectories(pluginDir2).filter((name) => existsSync51(join62(pluginDir2, name, "__init__.py"))).sort();
13711
13997
  }
13712
13998
  function bareTestModulesOf(pluginDir2) {
13713
- const testsDir = join61(pluginDir2, "tests");
13999
+ const testsDir = join62(pluginDir2, "tests");
13714
14000
  if (!existsSync51(testsDir)) return [];
13715
- if (existsSync51(join61(testsDir, "__init__.py"))) return [];
14001
+ if (existsSync51(join62(testsDir, "__init__.py"))) return [];
13716
14002
  return readdirSync24(testsDir).filter((f) => f.endsWith(".py") && !PYTEST_SPECIAL.has(f)).sort();
13717
14003
  }
13718
14004
  function findCollisions(servicesDir, pluginDirs) {
@@ -13721,7 +14007,7 @@ function findCollisions(servicesDir, pluginDirs) {
13721
14007
  const gather = (kind, namesOf) => {
13722
14008
  const claims = /* @__PURE__ */ new Map();
13723
14009
  for (const plugin of plugins) {
13724
- for (const name of namesOf(join61(servicesDir, plugin))) {
14010
+ for (const name of namesOf(join62(servicesDir, plugin))) {
13725
14011
  claims.set(name, [...claims.get(name) ?? [], plugin]);
13726
14012
  }
13727
14013
  }
@@ -13759,7 +14045,7 @@ function formatCollisions(collisions) {
13759
14045
  // src/scripts/check-plugin-collisions.ts
13760
14046
  async function runPluginCollisionCheck() {
13761
14047
  const root = (await execa("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
13762
- const servicesDir = join62(root, "services");
14048
+ const servicesDir = join63(root, "services");
13763
14049
  if (!existsSync52(servicesDir)) {
13764
14050
  console.log("\u2713 plugin collision guard: no services/ directory \u2014 nothing to compare");
13765
14051
  return;
@@ -13777,8 +14063,8 @@ async function runPluginCollisionCheck() {
13777
14063
  }
13778
14064
 
13779
14065
  // src/lib/plugin-terraform-guard.ts
13780
- import { existsSync as existsSync53, readFileSync as readFileSync41, readdirSync as readdirSync25 } from "fs";
13781
- import { dirname as dirname10, join as join63, relative as relative10, sep as sep4 } from "path";
14066
+ import { existsSync as existsSync53, readFileSync as readFileSync42, readdirSync as readdirSync25 } from "fs";
14067
+ import { dirname as dirname10, join as join64, relative as relative10, sep as sep4 } from "path";
13782
14068
  var SKIP_DIRS4 = /* @__PURE__ */ new Set(["node_modules", ".git", ".worktrees", "dist", ".venv", "__pycache__"]);
13783
14069
  var PLUGIN_MANIFEST_FILE2 = "biffo.plugin.json";
13784
14070
  function findPluginManifests(root) {
@@ -13793,9 +14079,9 @@ function findPluginManifests(root) {
13793
14079
  for (const entry of entries) {
13794
14080
  if (entry.isDirectory()) {
13795
14081
  if (SKIP_DIRS4.has(entry.name)) continue;
13796
- walk2(join63(dir, entry.name));
14082
+ walk2(join64(dir, entry.name));
13797
14083
  } else if (entry.isFile() && entry.name === PLUGIN_MANIFEST_FILE2) {
13798
- found.push(relative10(root, join63(dir, entry.name)).split(sep4).join("/"));
14084
+ found.push(relative10(root, join64(dir, entry.name)).split(sep4).join("/"));
13799
14085
  }
13800
14086
  }
13801
14087
  };
@@ -13805,7 +14091,7 @@ function findPluginManifests(root) {
13805
14091
  function readSubscriptions(absManifestPath) {
13806
14092
  let parsed;
13807
14093
  try {
13808
- parsed = JSON.parse(readFileSync41(absManifestPath, "utf8"));
14094
+ parsed = JSON.parse(readFileSync42(absManifestPath, "utf8"));
13809
14095
  } catch {
13810
14096
  return null;
13811
14097
  }
@@ -13820,14 +14106,14 @@ function readSubscriptions(absManifestPath) {
13820
14106
  }
13821
14107
  function checkPluginTerraform(root) {
13822
14108
  const violations = [];
13823
- const coreManifest = existsSync53(join63(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
14109
+ const coreManifest = existsSync53(join64(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
13824
14110
  for (const manifest of findPluginManifests(root)) {
13825
14111
  if (coreManifest && !isTemplateOwned(manifest, coreManifest)) continue;
13826
- const absManifest = join63(root, manifest);
14112
+ const absManifest = join64(root, manifest);
13827
14113
  const subscriptions = readSubscriptions(absManifest);
13828
14114
  if (subscriptions === null) continue;
13829
14115
  const pluginDir2 = dirname10(absManifest);
13830
- if (existsSync53(join63(pluginDir2, "terraform"))) continue;
14116
+ if (existsSync53(join64(pluginDir2, "terraform"))) continue;
13831
14117
  const relPluginDir = relative10(root, pluginDir2).split(sep4).join("/");
13832
14118
  violations.push({
13833
14119
  manifest,
@@ -13859,11 +14145,11 @@ async function runPluginTerraformCheck() {
13859
14145
 
13860
14146
  // src/scripts/check-plugin-tool-supply.ts
13861
14147
  import { existsSync as existsSync55 } from "fs";
13862
- import { join as join65 } from "path";
14148
+ import { join as join66 } from "path";
13863
14149
 
13864
14150
  // src/lib/plugin-tool-supply-audit.ts
13865
- import { existsSync as existsSync54, readFileSync as readFileSync42, readdirSync as readdirSync26, statSync as statSync16 } from "fs";
13866
- import { join as join64 } from "path";
14151
+ import { existsSync as existsSync54, readFileSync as readFileSync43, readdirSync as readdirSync26, statSync as statSync16 } from "fs";
14152
+ import { join as join65 } from "path";
13867
14153
 
13868
14154
  // src/lib/openrouter-model-snapshot.ts
13869
14155
  var OPENROUTER_MODEL_SNAPSHOT_FETCHED_AT = "2026-08-10T06:39:01Z";
@@ -14280,7 +14566,7 @@ function listDirs(root) {
14280
14566
  }
14281
14567
  return entries.filter((e) => {
14282
14568
  try {
14283
- return statSync16(join64(root, e)).isDirectory();
14569
+ return statSync16(join65(root, e)).isDirectory();
14284
14570
  } catch {
14285
14571
  return false;
14286
14572
  }
@@ -14296,7 +14582,7 @@ function walkFiles2(root, accept, skipDir) {
14296
14582
  return;
14297
14583
  }
14298
14584
  for (const entry of entries) {
14299
- const p = join64(dir, entry);
14585
+ const p = join65(dir, entry);
14300
14586
  let st;
14301
14587
  try {
14302
14588
  st = statSync16(p);
@@ -14322,14 +14608,14 @@ function pluginPythonFiles(pluginDir2) {
14322
14608
  );
14323
14609
  }
14324
14610
  function pluginTerraformFiles(pluginDir2) {
14325
- const tfDir = join64(pluginDir2, "terraform");
14611
+ const tfDir = join65(pluginDir2, "terraform");
14326
14612
  let entries;
14327
14613
  try {
14328
14614
  entries = readdirSync26(tfDir);
14329
14615
  } catch {
14330
14616
  return [];
14331
14617
  }
14332
- return entries.filter((e) => e.endsWith(".tf")).map((e) => join64(tfDir, e)).sort();
14618
+ return entries.filter((e) => e.endsWith(".tf")).map((e) => join65(tfDir, e)).sort();
14333
14619
  }
14334
14620
  function extractManifestTools(manifestText) {
14335
14621
  let parsed;
@@ -14581,8 +14867,8 @@ function isSnapshotStale(fetchedAt, now) {
14581
14867
  function normalizeModelId(id) {
14582
14868
  return id.endsWith(":online") ? id.slice(0, -":online".length) : id;
14583
14869
  }
14584
- var CONFIG_PY_PATH = join64("services", "api", "src", "api", "config.py");
14585
- var ORCHESTRATION_SCHEMA_PATH = join64(
14870
+ var CONFIG_PY_PATH = join65("services", "api", "src", "api", "config.py");
14871
+ var ORCHESTRATION_SCHEMA_PATH = join65(
14586
14872
  "services",
14587
14873
  "api",
14588
14874
  "src",
@@ -14594,8 +14880,8 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
14594
14880
  const knownModelIds = options.knownModelIds ?? OPENROUTER_MODEL_IDS;
14595
14881
  const snapshotFetchedAt = options.snapshotFetchedAt ?? OPENROUTER_MODEL_SNAPSHOT_FETCHED_AT;
14596
14882
  const now = options.now ?? /* @__PURE__ */ new Date();
14597
- const configPath = join64(repoRoot, CONFIG_PY_PATH);
14598
- const orchestrationPath = join64(repoRoot, ORCHESTRATION_SCHEMA_PATH);
14883
+ const configPath = join65(repoRoot, CONFIG_PY_PATH);
14884
+ const orchestrationPath = join65(repoRoot, ORCHESTRATION_SCHEMA_PATH);
14599
14885
  const configMissing = !existsSync54(configPath);
14600
14886
  const orchestrationSchemaMissing = !existsSync54(orchestrationPath);
14601
14887
  const knownSet = new Set(knownModelIds);
@@ -14615,13 +14901,13 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
14615
14901
  };
14616
14902
  let settingsBlind = false;
14617
14903
  if (!configMissing) {
14618
- const settingsFields = extractSettingsModelFields(readFileSync42(configPath, "utf8"));
14904
+ const settingsFields = extractSettingsModelFields(readFileSync43(configPath, "utf8"));
14619
14905
  if (settingsFields.length === 0) settingsBlind = true;
14620
14906
  for (const { field, value } of settingsFields) record(`${CONFIG_PY_PATH}#${field}`, value);
14621
14907
  }
14622
14908
  let curatedFieldsBlind = false;
14623
14909
  if (!orchestrationSchemaMissing) {
14624
- const curated = extractCuratedModelFields(readFileSync42(orchestrationPath, "utf8"));
14910
+ const curated = extractCuratedModelFields(readFileSync43(orchestrationPath, "utf8"));
14625
14911
  if (curated.rawFieldCount > 0 && curated.fields.every((f) => f.defaultValue === null && f.optionValues.length === 0)) {
14626
14912
  curatedFieldsBlind = true;
14627
14913
  }
@@ -14668,7 +14954,7 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
14668
14954
  function discoverPluginDirs(pluginsRoot) {
14669
14955
  return listDirs(pluginsRoot).filter((name) => {
14670
14956
  try {
14671
- return statSync16(join64(pluginsRoot, name, "biffo.plugin.json")).isFile();
14957
+ return statSync16(join65(pluginsRoot, name, "biffo.plugin.json")).isFile();
14672
14958
  } catch {
14673
14959
  return false;
14674
14960
  }
@@ -14681,8 +14967,8 @@ function auditPluginToolSupply(pluginsRoot) {
14681
14967
  let terraformBlind = false;
14682
14968
  let totalDeclaredTools = 0;
14683
14969
  for (const name of pluginNames) {
14684
- const pluginDir2 = join64(pluginsRoot, name);
14685
- const manifestText = readFileSync42(join64(pluginDir2, "biffo.plugin.json"), "utf8");
14970
+ const pluginDir2 = join65(pluginsRoot, name);
14971
+ const manifestText = readFileSync43(join65(pluginDir2, "biffo.plugin.json"), "utf8");
14686
14972
  const manifest = extractManifestTools(manifestText);
14687
14973
  if (manifest.parseError) {
14688
14974
  findings.push({
@@ -14700,13 +14986,13 @@ function auditPluginToolSupply(pluginsRoot) {
14700
14986
  totalDeclaredTools += manifest.tools.length;
14701
14987
  const pySources = pluginPythonFiles(pluginDir2).map((f) => ({
14702
14988
  file: f,
14703
- text: readFileSync42(f, "utf8")
14989
+ text: readFileSync43(f, "utf8")
14704
14990
  }));
14705
14991
  const resolver = buildSymbolResolver(pySources);
14706
14992
  const registry = extractToolRegistryEntries(pySources, resolver);
14707
14993
  if (registry.rawToolDefinitionCount > 0 && registry.entries.length === 0) registryBlind = true;
14708
14994
  const tfFiles = pluginTerraformFiles(pluginDir2);
14709
- const tfText = tfFiles.map((f) => readFileSync42(f, "utf8")).join("\n");
14995
+ const tfText = tfFiles.map((f) => readFileSync43(f, "utf8")).join("\n");
14710
14996
  const terraform = extractTerraformEnvKeys(tfText);
14711
14997
  if (terraform.rawMarkerCount > 0 && terraform.resolvedBlockCount === 0) terraformBlind = true;
14712
14998
  for (const toolName of manifest.tools) {
@@ -14780,7 +15066,7 @@ function auditPluginToolSupply(pluginsRoot) {
14780
15066
  requiredEnvVars: envResult.envVars,
14781
15067
  missingEnvVars: anyWired ? [] : envResult.envVars,
14782
15068
  status: anyWired ? "ok" : "missing-env",
14783
- 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`
15069
+ 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 ${join65(pluginDir2, "terraform")}, so this deployment can never supply it`
14784
15070
  });
14785
15071
  }
14786
15072
  }
@@ -14813,7 +15099,7 @@ function auditPluginToolSupply(pluginsRoot) {
14813
15099
  async function runPluginToolSupplyCheck() {
14814
15100
  const root = (await execa("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
14815
15101
  let allOk = true;
14816
- const pluginsRoot = join65(root, "services", "_plugins");
15102
+ const pluginsRoot = join66(root, "services", "_plugins");
14817
15103
  if (!existsSync55(pluginsRoot)) {
14818
15104
  console.log("\u2713 plugin tool-supply guard: no services/_plugins/ \u2014 nothing to audit");
14819
15105
  } else {
@@ -14844,7 +15130,7 @@ async function runPluginToolSupplyCheck() {
14844
15130
  console.log(`\u2713 plugin tool-supply guard: ${report.summary}`);
14845
15131
  }
14846
15132
  }
14847
- const servicesApiRoot = join65(root, "services", "api");
15133
+ const servicesApiRoot = join66(root, "services", "api");
14848
15134
  if (!existsSync55(servicesApiRoot)) {
14849
15135
  console.log("\u2713 plugin model-id guard: no services/api/ \u2014 nothing to audit");
14850
15136
  } else {
@@ -15019,10 +15305,10 @@ async function runReleaseSubjectCheck(argv) {
15019
15305
  }
15020
15306
 
15021
15307
  // src/scripts/check-shared-file-reduction.ts
15022
- import { readFileSync as readFileSync44 } from "fs";
15308
+ import { readFileSync as readFileSync45 } from "fs";
15023
15309
 
15024
15310
  // src/lib/shared-file-reduction-guard.ts
15025
- import { readFileSync as readFileSync43 } from "fs";
15311
+ import { readFileSync as readFileSync44 } from "fs";
15026
15312
  var LEAF_TEST_CALLS = /* @__PURE__ */ new Set(["it", "test"]);
15027
15313
  var SUITE_CALLS = /* @__PURE__ */ new Set(["describe", "suite"]);
15028
15314
  var TEST_FILE_PATTERN = /\.(test|spec)\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/;
@@ -15149,7 +15435,7 @@ function formatReductionReport(report) {
15149
15435
  // src/scripts/check-shared-file-reduction.ts
15150
15436
  function readStdin() {
15151
15437
  try {
15152
- return readFileSync44(0, "utf8");
15438
+ return readFileSync45(0, "utf8");
15153
15439
  } catch {
15154
15440
  return "";
15155
15441
  }
@@ -15166,15 +15452,15 @@ function pairsFromTsv(tsv) {
15166
15452
  const [target, existingPath, incomingPath] = fields;
15167
15453
  pairs.push({
15168
15454
  target,
15169
- existing: readFileSync44(existingPath, "utf8"),
15170
- incoming: readFileSync44(incomingPath, "utf8")
15455
+ existing: readFileSync45(existingPath, "utf8"),
15456
+ incoming: readFileSync45(incomingPath, "utf8")
15171
15457
  });
15172
15458
  }
15173
15459
  return pairs;
15174
15460
  }
15175
15461
  function loadAccepted(manifestPath) {
15176
15462
  if (!manifestPath) return {};
15177
- const parsed = JSON.parse(readFileSync44(manifestPath, "utf8"));
15463
+ const parsed = JSON.parse(readFileSync45(manifestPath, "utf8"));
15178
15464
  return parsed.acceptedReductions ?? {};
15179
15465
  }
15180
15466
  async function runSharedFileReductionCheck(args) {
@@ -15182,13 +15468,13 @@ async function runSharedFileReductionCheck(args) {
15182
15468
  let accepted;
15183
15469
  try {
15184
15470
  if (args.pairs) {
15185
- pairs = pairsFromTsv(args.pairs === "-" ? readStdin() : readFileSync44(args.pairs, "utf8"));
15471
+ pairs = pairsFromTsv(args.pairs === "-" ? readStdin() : readFileSync45(args.pairs, "utf8"));
15186
15472
  } else if (args.target && args.existing && args.incoming) {
15187
15473
  pairs = [
15188
15474
  {
15189
15475
  target: args.target,
15190
- existing: readFileSync44(args.existing, "utf8"),
15191
- incoming: readFileSync44(args.incoming, "utf8")
15476
+ existing: readFileSync45(args.existing, "utf8"),
15477
+ incoming: readFileSync45(args.incoming, "utf8")
15192
15478
  }
15193
15479
  ];
15194
15480
  } else {
@@ -15223,11 +15509,11 @@ async function runSharedFileReductionCheck(args) {
15223
15509
 
15224
15510
  // src/scripts/check-skeleton-drift.ts
15225
15511
  import { existsSync as existsSync56, readdirSync as readdirSync28 } from "fs";
15226
- import { join as join67 } from "path";
15512
+ import { join as join68 } from "path";
15227
15513
 
15228
15514
  // src/lib/skeleton-drift-guard.ts
15229
- import { readFileSync as readFileSync45, readdirSync as readdirSync27, statSync as statSync17 } from "fs";
15230
- import { join as join66 } from "path";
15515
+ import { readFileSync as readFileSync46, readdirSync as readdirSync27, statSync as statSync17 } from "fs";
15516
+ import { join as join67 } from "path";
15231
15517
  var isWorkflow = (rel) => rel.startsWith(".github/workflows/") && (rel.endsWith(".yml") || rel.endsWith(".yaml"));
15232
15518
  var isRootLayout = (rel) => rel.endsWith("src/app/layout.tsx");
15233
15519
  var uncommented = (contents) => contents.split("\n").filter((line) => !/^\s*(\/\/|\/\*|\*)/.test(line)).join("\n");
@@ -15291,7 +15577,7 @@ function walk(dir, base = dir) {
15291
15577
  }
15292
15578
  for (const entry of entries) {
15293
15579
  if (entry === ".venv" || entry === "node_modules" || entry === ".git") continue;
15294
- const abs = join66(dir, entry);
15580
+ const abs = join67(dir, entry);
15295
15581
  let isDir;
15296
15582
  try {
15297
15583
  isDir = statSync17(abs).isDirectory();
@@ -15313,7 +15599,7 @@ function auditSkeleton(skeletonRoot, name, rules = SKELETON_RULES) {
15313
15599
  if (!rule.appliesTo(rel)) continue;
15314
15600
  let contents;
15315
15601
  try {
15316
- contents = readFileSync45(join66(skeletonRoot, rel), "utf8");
15602
+ contents = readFileSync46(join67(skeletonRoot, rel), "utf8");
15317
15603
  } catch {
15318
15604
  continue;
15319
15605
  }
@@ -15342,23 +15628,23 @@ function formatViolations2(violations) {
15342
15628
 
15343
15629
  // src/scripts/check-skeleton-drift.ts
15344
15630
  function discoverSkeletons(root) {
15345
- const skeletonsDir = join67(root, "_skeletons");
15631
+ const skeletonsDir = join68(root, "_skeletons");
15346
15632
  let entries;
15347
15633
  try {
15348
15634
  entries = readdirSync28(skeletonsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
15349
15635
  } catch {
15350
15636
  return [];
15351
15637
  }
15352
- return entries.filter((name) => existsSync56(join67(skeletonsDir, name, ".github", "workflows", "ci.yml"))).sort();
15638
+ return entries.filter((name) => existsSync56(join68(skeletonsDir, name, ".github", "workflows", "ci.yml"))).sort();
15353
15639
  }
15354
15640
  async function runSkeletonDriftCheck() {
15355
15641
  const root = (await execa("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
15356
15642
  const skeletons = discoverSkeletons(root);
15357
15643
  let filesConsidered = 0;
15358
15644
  for (const name of skeletons) {
15359
- const skeletonRoot = join67(root, "_skeletons", name);
15645
+ const skeletonRoot = join68(root, "_skeletons", name);
15360
15646
  filesConsidered += findWorkflowFiles(skeletonRoot).length;
15361
- if (existsSync56(join67(skeletonRoot, "apps", "frontend", "src", "app", "layout.tsx"))) {
15647
+ if (existsSync56(join68(skeletonRoot, "apps", "frontend", "src", "app", "layout.tsx"))) {
15362
15648
  filesConsidered += 1;
15363
15649
  }
15364
15650
  }
@@ -15366,7 +15652,7 @@ async function runSkeletonDriftCheck() {
15366
15652
  `audited ${skeletons.length} skeleton(s) (${skeletons.join(", ") || "none"}), ${filesConsidered} file(s) considered, under ${root}/_skeletons`
15367
15653
  );
15368
15654
  if (skeletons.length === 0) {
15369
- if (!existsSync56(join67(root, "_skeletons"))) {
15655
+ if (!existsSync56(join68(root, "_skeletons"))) {
15370
15656
  console.log(
15371
15657
  `\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.`
15372
15658
  );
@@ -15378,7 +15664,7 @@ async function runSkeletonDriftCheck() {
15378
15664
  process.exit(1);
15379
15665
  }
15380
15666
  const violations = skeletons.flatMap(
15381
- (name) => auditSkeleton(join67(root, "_skeletons", name), name)
15667
+ (name) => auditSkeleton(join68(root, "_skeletons", name), name)
15382
15668
  );
15383
15669
  if (violations.length > 0) {
15384
15670
  console.error("\u2717 Skeleton-drift guard: drift found between this repo and its scaffolding\n");
@@ -15414,7 +15700,7 @@ async function runTerraformInputCheck() {
15414
15700
 
15415
15701
  // src/commands/check.ts
15416
15702
  var checkCommand = new Command24("check").description(
15417
- "Repo guards (ownership, release subject, plugin terraform, plugin collisions, eventbridge-log-permissions, plugin-tool-supply, core-direct-paths, instance-adoption, orphan-ratchet, cognito-invite-template, lambda-output, pipe-trap, codeql-suppression, skeleton-drift, terraform-input, plugin-allowlist-convention, migration-body-change, distribution-inventory, distribution-remote-state, api-gateway-integration) run in CI and git hooks, plus out-of-band audits (branch protection, plugin-staleness)"
15703
+ "Repo guards (ownership, ownership-header-claim, release subject, plugin terraform, plugin collisions, eventbridge-log-permissions, plugin-tool-supply, core-direct-paths, instance-adoption, orphan-ratchet, cognito-invite-template, lambda-output, pipe-trap, codeql-suppression, skeleton-drift, terraform-input, plugin-allowlist-convention, migration-body-change, distribution-inventory, distribution-remote-state, api-gateway-integration) run in CI and git hooks, plus out-of-band audits (branch protection, plugin-staleness)"
15418
15704
  );
15419
15705
  checkCommand.command("ownership").description("Refuse changes to template-owned paths in an instance (#370)").argument("[base]", "Base branch to diff against; defaults to $GITHUB_BASE_REF").option("--staged <messageFile>", "Check staged changes instead of a branch diff (commit hook)").allowExcessArguments(true).action(async () => {
15420
15706
  await runOwnershipCheck(rawArgsAfter("ownership"));
@@ -15503,6 +15789,11 @@ checkCommand.command("orphan-ratchet").description(
15503
15789
  await runOrphanRatchetCheck(opts);
15504
15790
  }
15505
15791
  );
15792
+ checkCommand.command("ownership-header-claim").description(
15793
+ `Refuse a file whose own header comment claims INSTANCE-OWNED/template-owned/user-owned/"NOT a template file" while core-manifest.json's real longest-prefix-match answer disagrees (#1911) \u2014 scripts/verify-deployed.checks lived this exact gap (#1706/#1707) until a human happened to notice by hand; nothing else compared the two documents.`
15794
+ ).action(async () => {
15795
+ await runOwnershipHeaderClaimCheck();
15796
+ });
15506
15797
  checkCommand.command("cognito-invite-template").description(
15507
15798
  "Refuse a Cognito invite_message_template missing a required member or placeholder (#356) \u2014 terraform validate is silent on this, so a fresh deploy fails on its first apply"
15508
15799
  ).action(async () => {
@@ -15574,8 +15865,8 @@ function rawArgsAfter(subcommand) {
15574
15865
  }
15575
15866
 
15576
15867
  // src/commands/doctor.ts
15577
- import { existsSync as existsSync57, readFileSync as readFileSync46 } from "fs";
15578
- import { join as join68, resolve as resolve20 } from "path";
15868
+ import { existsSync as existsSync57, readFileSync as readFileSync47 } from "fs";
15869
+ import { join as join69, resolve as resolve20 } from "path";
15579
15870
  import chalk21 from "chalk";
15580
15871
  import { Command as Command25 } from "commander";
15581
15872
 
@@ -15970,10 +16261,10 @@ async function runDoctorFixBranches(cwd, facts, deps = { git: new GitAdapter(),
15970
16261
  return reapAllBareBranches(cwd, facts.branches, facts.worktrees, facts.currentBranch, deps);
15971
16262
  }
15972
16263
  function readLocalCoreVersion(cwd) {
15973
- const path = join68(cwd, INSTANCE_CORE_FILE);
16264
+ const path = join69(cwd, INSTANCE_CORE_FILE);
15974
16265
  if (!existsSync57(path)) return null;
15975
16266
  try {
15976
- return extractVersionField(readFileSync46(path, "utf8"));
16267
+ return extractVersionField(readFileSync47(path, "utf8"));
15977
16268
  } catch {
15978
16269
  return null;
15979
16270
  }
@@ -15993,10 +16284,10 @@ function extractVersionField(contents) {
15993
16284
  return match?.[1] ?? null;
15994
16285
  }
15995
16286
  function readFossil(cwd) {
15996
- const path = join68(cwd, CORE_VERSION_FILE);
16287
+ const path = join69(cwd, CORE_VERSION_FILE);
15997
16288
  if (!existsSync57(path)) return null;
15998
16289
  try {
15999
- const value = readFileSync46(path, "utf8").trim();
16290
+ const value = readFileSync47(path, "utf8").trim();
16000
16291
  return value === "" ? null : value;
16001
16292
  } catch {
16002
16293
  return null;
@@ -16526,11 +16817,11 @@ import { Command as Command27 } from "commander";
16526
16817
 
16527
16818
  // src/lib/packaged-scripts.ts
16528
16819
  import { existsSync as existsSync58 } from "fs";
16529
- import { dirname as dirname11, join as join69 } from "path";
16820
+ import { dirname as dirname11, join as join70 } from "path";
16530
16821
  function findPackagedScript(startDir, relativePath) {
16531
16822
  let dir = startDir;
16532
16823
  for (; ; ) {
16533
- const candidate = join69(dir, relativePath);
16824
+ const candidate = join70(dir, relativePath);
16534
16825
  if (existsSync58(candidate)) return candidate;
16535
16826
  const parent = dirname11(dir);
16536
16827
  if (parent === dir) return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.305.1",
3
+ "version": "0.307.0",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/scripts/verify.sh CHANGED
@@ -1449,6 +1449,7 @@ if [ -f scripts/biffo.sh ]; then
1449
1449
  run_check plugin-tool-supply sh scripts/biffo.sh check plugin-tool-supply
1450
1450
  run_check core-direct-paths sh scripts/biffo.sh check core-direct-paths
1451
1451
  run_check orphan-ratchet sh scripts/biffo.sh check orphan-ratchet
1452
+ run_check ownership-header-claim sh scripts/biffo.sh check ownership-header-claim
1452
1453
  # #1714 second remediation: the INSTANCE-mode caller, distinct from the
1453
1454
  # self-check above. It clones a real template tree pinned to
1454
1455
  # biffo.core.json's own version and compares against it -- but this repo