@biffo/cli 0.309.2 → 0.309.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.
@@ -170,10 +170,18 @@ because `--auto` waits for ever on a check that cannot re-evaluate itself (a
170
170
 
171
171
  ## 6. Push honestly, and verify the remote has your commit
172
172
 
173
- - **Push with the exit status visible:** `git push origin HEAD; echo $?`. A pipe
174
- (`git push | tail`) reports the pipe's status, not git's — a rejected push then
175
- reads as success and the commit is silently lost. Never trust a "pushed"
176
- message printed unconditionally after a pipe.
173
+ - **Push with the exit status visible, and record the upstream:**
174
+ `git push -u origin HEAD; echo $?`. A pipe (`git push | tail`) reports the
175
+ pipe's status, not git's — a rejected push then reads as success and the
176
+ commit is silently lost. Never trust a "pushed" message printed
177
+ unconditionally after a pipe.
178
+ - **The `-u` is not optional (biffo-template#1954).** `git worktree add -b
179
+ <branch> <path> origin/dev` sets the new branch's upstream to `origin/dev`
180
+ by default — a plain `git push origin HEAD`, with no `-u`, never corrects
181
+ it. A branch left tracking `origin/dev` can be pushed, merged, and have its
182
+ own remote copy deleted, while `git branch -vv` never reports it `[gone]`
183
+ (the ref it is actually tracking never goes anywhere) — so `doctor --fix`
184
+ and every other tool reading that marker stays permanently blind to it.
177
185
  - **Confirm the remote actually has the commit before relying on it,** especially
178
186
  before merge: `git log origin/<branch> -1`. A green PR page is not proof your
179
187
  latest local commit reached it.
@@ -170,10 +170,18 @@ because `--auto` waits for ever on a check that cannot re-evaluate itself (a
170
170
 
171
171
  ## 6. Push honestly, and verify the remote has your commit
172
172
 
173
- - **Push with the exit status visible:** `git push origin HEAD; echo $?`. A pipe
174
- (`git push | tail`) reports the pipe's status, not git's — a rejected push then
175
- reads as success and the commit is silently lost. Never trust a "pushed"
176
- message printed unconditionally after a pipe.
173
+ - **Push with the exit status visible, and record the upstream:**
174
+ `git push -u origin HEAD; echo $?`. A pipe (`git push | tail`) reports the
175
+ pipe's status, not git's — a rejected push then reads as success and the
176
+ commit is silently lost. Never trust a "pushed" message printed
177
+ unconditionally after a pipe.
178
+ - **The `-u` is not optional (biffo-template#1954).** `git worktree add -b
179
+ <branch> <path> origin/dev` sets the new branch's upstream to `origin/dev`
180
+ by default — a plain `git push origin HEAD`, with no `-u`, never corrects
181
+ it. A branch left tracking `origin/dev` can be pushed, merged, and have its
182
+ own remote copy deleted, while `git branch -vv` never reports it `[gone]`
183
+ (the ref it is actually tracking never goes anywhere) — so `doctor --fix`
184
+ and every other tool reading that marker stays permanently blind to it.
177
185
  - **Confirm the remote actually has the commit before relying on it,** especially
178
186
  before merge: `git log origin/<branch> -1`. A green PR page is not proof your
179
187
  latest local commit reached it.
package/dist/index.js CHANGED
@@ -1449,6 +1449,39 @@ var GitAdapter = class {
1449
1449
  const { stdout, exitCode } = await execa("git", ["rev-parse", "HEAD"], { cwd, reject: false });
1450
1450
  return exitCode === 0 ? stdout.trim() : null;
1451
1451
  }
1452
+ /**
1453
+ * Every local branch name that currently has a same-named remote-tracking
1454
+ * ref under `refs/remotes/origin/` (biffo-template#1954) — i.e. `git
1455
+ * for-each-ref refs/remotes/origin`, stripped of the `refs/remotes/origin/`
1456
+ * prefix, with the `HEAD` symref excluded (it names no branch).
1457
+ *
1458
+ * This is the fact `doctor --fix`'s reap-candidate detection needs instead
1459
+ * of trusting a branch's own *configured* upstream (`%(upstream:track)`,
1460
+ * i.e. `git branch -vv`'s `[gone]` marker): `git worktree add -b <branch>
1461
+ * <path> origin/dev` sets the new branch's upstream to `origin/dev` by
1462
+ * default, and a plain `git push origin HEAD` (no `-u`) never corrects
1463
+ * it — so a branch can be pushed, merged, and have its OWN remote copy
1464
+ * deleted, while its `[gone]` marker never fires because the thing it is
1465
+ * actually tracking (`origin/dev`) never goes anywhere. Checking for a
1466
+ * same-named ref sidesteps whatever the branch happens to be configured to
1467
+ * track entirely.
1468
+ *
1469
+ * Absence from this set does NOT by itself prove a branch's remote copy
1470
+ * was deleted — a branch never pushed under its own name is equally
1471
+ * absent. See `doctor-reaper.ts`'s `hasProvenGoneRemote` for why that
1472
+ * distinction is safe to skip here (unlike in `upgrade-branch-reaper.ts`,
1473
+ * which has no downstream verification and must not skip it).
1474
+ */
1475
+ async listRemoteBranchNames(cwd) {
1476
+ const { stdout } = await execa(
1477
+ "git",
1478
+ ["for-each-ref", "--format=%(refname:lstrip=3)", "refs/remotes/origin"],
1479
+ { cwd }
1480
+ );
1481
+ return new Set(
1482
+ stdout.split("\n").map((line) => line.trim()).filter((name) => name !== "" && name !== "HEAD")
1483
+ );
1484
+ }
1452
1485
  /**
1453
1486
  * `branch`'s own tip SHA, without checking it out — the bare-branch
1454
1487
  * counterpart to `headSha` (#1682 milestone 2). A branch with no linked
@@ -12470,6 +12503,16 @@ function auditSiblingCoreDirectPaths(params) {
12470
12503
  // src/scripts/check-core-direct-paths.ts
12471
12504
  async function runCoreDirectPathsCheck(opts = {}) {
12472
12505
  const root = (await execa("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12506
+ const usingSelfCheckDefaults = !opts.sibling && !opts.frontendSrc && !opts.coreSrc;
12507
+ if (usingSelfCheckDefaults) {
12508
+ const ownership = classifyRepoOwnership(root);
12509
+ if (ownership === "satellite") {
12510
+ console.log(
12511
+ "\u2713 core-direct-paths guard: skipped \u2014 the self-check defaults compare this repo's own _skeletons/sibling-template scaffold against its own services/api/src, and a sibling/plugin repo has neither: no _skeletons/ at all, and its own services/api/ is an unrelated BFF, not core's route table. Pass --sibling/--frontend-src plus --estate (or --core-src) to audit a real sibling instead."
12512
+ );
12513
+ return;
12514
+ }
12515
+ }
12473
12516
  const sibling = opts.sibling ?? "sibling-template (self-check)";
12474
12517
  const frontendSrcDir = opts.frontendSrc ?? join48(root, "_skeletons", "sibling-template", "apps", "frontend", "src");
12475
12518
  let coreApiSrcDir;
@@ -12583,8 +12626,8 @@ async function runOwnershipCheck(argv) {
12583
12626
  const { stdout } = await execa("git", ["diff", "--cached", "--name-status"], { cwd: root });
12584
12627
  ({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
12585
12628
  if (messageFile) {
12586
- const { readFileSync: readFileSync48, existsSync: existsSync59 } = await import("fs");
12587
- if (existsSync59(messageFile)) commitMessage = readFileSync48(messageFile, "utf8");
12629
+ const { readFileSync: readFileSync48, existsSync: existsSync58 } = await import("fs");
12630
+ if (existsSync58(messageFile)) commitMessage = readFileSync48(messageFile, "utf8");
12588
12631
  }
12589
12632
  } else {
12590
12633
  const base = process.env["GITHUB_BASE_REF"] ?? args[0];
@@ -14019,10 +14062,6 @@ async function runPipeTrapCheck() {
14019
14062
  console.log(`\u2713 Pipe-trap guard: no status-bearing command is piped away`);
14020
14063
  }
14021
14064
 
14022
- // src/scripts/check-plugin-allowlist-convention.ts
14023
- import { existsSync as existsSync50 } from "fs";
14024
- import { join as join61 } from "path";
14025
-
14026
14065
  // src/lib/plugin-allowlist-convention.ts
14027
14066
  import { readFileSync as readFileSync41 } from "fs";
14028
14067
  import { join as join60 } from "path";
@@ -14131,18 +14170,12 @@ Plugins would be rejected by require_service_principal (ADR-0009). Fix the glob,
14131
14170
  }
14132
14171
 
14133
14172
  // src/scripts/check-plugin-allowlist-convention.ts
14134
- var REQUIRED_SOURCES = [
14135
- COMPUTE_MAIN_TF,
14136
- PLUGIN_TEMPLATE_MAIN_TF,
14137
- ALLOWLIST_MAIN_TF,
14138
- ALLOWLIST_VARIABLES_TF
14139
- ];
14140
14173
  async function runPluginAllowlistConventionCheck() {
14141
14174
  const root = (await execa("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
14142
- const missing = REQUIRED_SOURCES.filter((relative11) => !existsSync50(join61(root, relative11)));
14143
- if (missing.length === REQUIRED_SOURCES.length) {
14175
+ const ownership = classifyRepoOwnership(root);
14176
+ if (ownership === "satellite") {
14144
14177
  console.log(
14145
- `\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.`
14178
+ `\xB7 Plugin-allowlist convention guard: not applicable \u2014 this is not a template/instance tree (${root}), so there is no modules/plugins/_template or modules/cloud/aws/plugin-allowlist Terraform to audit. A sibling app may carry its own unrelated modules/cloud/aws/compute for its own BFF; that is not this guard's concern. Skipping.`
14146
14179
  );
14147
14180
  return;
14148
14181
  }
@@ -14170,32 +14203,32 @@ async function runPluginAllowlistConventionCheck() {
14170
14203
  }
14171
14204
 
14172
14205
  // src/scripts/check-plugin-collisions.ts
14173
- import { existsSync as existsSync52 } from "fs";
14174
- import { join as join63 } from "path";
14206
+ import { existsSync as existsSync51 } from "fs";
14207
+ import { join as join62 } from "path";
14175
14208
 
14176
14209
  // src/lib/plugin-collision-guard.ts
14177
- import { existsSync as existsSync51, readdirSync as readdirSync24, statSync as statSync15 } from "fs";
14178
- import { join as join62 } from "path";
14210
+ import { existsSync as existsSync50, readdirSync as readdirSync24, statSync as statSync15 } from "fs";
14211
+ import { join as join61 } from "path";
14179
14212
  var PYTEST_SPECIAL = /* @__PURE__ */ new Set(["conftest.py"]);
14180
14213
  var IGNORED_DIRS = /* @__PURE__ */ new Set([".venv", "node_modules", "__pycache__", ".git", "dist", "build"]);
14181
14214
  function subdirectories(dir) {
14182
- if (!existsSync51(dir)) return [];
14215
+ if (!existsSync50(dir)) return [];
14183
14216
  return readdirSync24(dir).filter((entry) => {
14184
14217
  if (IGNORED_DIRS.has(entry) || entry.startsWith(".")) return false;
14185
14218
  try {
14186
- return statSync15(join62(dir, entry)).isDirectory();
14219
+ return statSync15(join61(dir, entry)).isDirectory();
14187
14220
  } catch {
14188
14221
  return false;
14189
14222
  }
14190
14223
  });
14191
14224
  }
14192
14225
  function regularPackagesOf(pluginDir2) {
14193
- return subdirectories(pluginDir2).filter((name) => existsSync51(join62(pluginDir2, name, "__init__.py"))).sort();
14226
+ return subdirectories(pluginDir2).filter((name) => existsSync50(join61(pluginDir2, name, "__init__.py"))).sort();
14194
14227
  }
14195
14228
  function bareTestModulesOf(pluginDir2) {
14196
- const testsDir = join62(pluginDir2, "tests");
14197
- if (!existsSync51(testsDir)) return [];
14198
- if (existsSync51(join62(testsDir, "__init__.py"))) return [];
14229
+ const testsDir = join61(pluginDir2, "tests");
14230
+ if (!existsSync50(testsDir)) return [];
14231
+ if (existsSync50(join61(testsDir, "__init__.py"))) return [];
14199
14232
  return readdirSync24(testsDir).filter((f) => f.endsWith(".py") && !PYTEST_SPECIAL.has(f)).sort();
14200
14233
  }
14201
14234
  function findCollisions(servicesDir, pluginDirs) {
@@ -14204,7 +14237,7 @@ function findCollisions(servicesDir, pluginDirs) {
14204
14237
  const gather = (kind, namesOf) => {
14205
14238
  const claims = /* @__PURE__ */ new Map();
14206
14239
  for (const plugin of plugins) {
14207
- for (const name of namesOf(join62(servicesDir, plugin))) {
14240
+ for (const name of namesOf(join61(servicesDir, plugin))) {
14208
14241
  claims.set(name, [...claims.get(name) ?? [], plugin]);
14209
14242
  }
14210
14243
  }
@@ -14242,8 +14275,8 @@ function formatCollisions(collisions) {
14242
14275
  // src/scripts/check-plugin-collisions.ts
14243
14276
  async function runPluginCollisionCheck() {
14244
14277
  const root = (await execa("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
14245
- const servicesDir = join63(root, "services");
14246
- if (!existsSync52(servicesDir)) {
14278
+ const servicesDir = join62(root, "services");
14279
+ if (!existsSync51(servicesDir)) {
14247
14280
  console.log("\u2713 plugin collision guard: no services/ directory \u2014 nothing to compare");
14248
14281
  return;
14249
14282
  }
@@ -14260,8 +14293,8 @@ async function runPluginCollisionCheck() {
14260
14293
  }
14261
14294
 
14262
14295
  // src/lib/plugin-terraform-guard.ts
14263
- import { existsSync as existsSync53, readFileSync as readFileSync42, readdirSync as readdirSync25 } from "fs";
14264
- import { dirname as dirname10, join as join64, relative as relative10, sep as sep4 } from "path";
14296
+ import { existsSync as existsSync52, readFileSync as readFileSync42, readdirSync as readdirSync25 } from "fs";
14297
+ import { dirname as dirname10, join as join63, relative as relative10, sep as sep4 } from "path";
14265
14298
  var SKIP_DIRS4 = /* @__PURE__ */ new Set(["node_modules", ".git", ".worktrees", "dist", ".venv", "__pycache__"]);
14266
14299
  var PLUGIN_MANIFEST_FILE2 = "biffo.plugin.json";
14267
14300
  function findPluginManifests(root) {
@@ -14276,9 +14309,9 @@ function findPluginManifests(root) {
14276
14309
  for (const entry of entries) {
14277
14310
  if (entry.isDirectory()) {
14278
14311
  if (SKIP_DIRS4.has(entry.name)) continue;
14279
- walk2(join64(dir, entry.name));
14312
+ walk2(join63(dir, entry.name));
14280
14313
  } else if (entry.isFile() && entry.name === PLUGIN_MANIFEST_FILE2) {
14281
- found.push(relative10(root, join64(dir, entry.name)).split(sep4).join("/"));
14314
+ found.push(relative10(root, join63(dir, entry.name)).split(sep4).join("/"));
14282
14315
  }
14283
14316
  }
14284
14317
  };
@@ -14303,14 +14336,14 @@ function readSubscriptions(absManifestPath) {
14303
14336
  }
14304
14337
  function checkPluginTerraform(root) {
14305
14338
  const violations = [];
14306
- const coreManifest = existsSync53(join64(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
14339
+ const coreManifest = existsSync52(join63(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
14307
14340
  for (const manifest of findPluginManifests(root)) {
14308
14341
  if (coreManifest && !isTemplateOwned(manifest, coreManifest)) continue;
14309
- const absManifest = join64(root, manifest);
14342
+ const absManifest = join63(root, manifest);
14310
14343
  const subscriptions = readSubscriptions(absManifest);
14311
14344
  if (subscriptions === null) continue;
14312
14345
  const pluginDir2 = dirname10(absManifest);
14313
- if (existsSync53(join64(pluginDir2, "terraform"))) continue;
14346
+ if (existsSync52(join63(pluginDir2, "terraform"))) continue;
14314
14347
  const relPluginDir = relative10(root, pluginDir2).split(sep4).join("/");
14315
14348
  violations.push({
14316
14349
  manifest,
@@ -14341,12 +14374,12 @@ async function runPluginTerraformCheck() {
14341
14374
  }
14342
14375
 
14343
14376
  // src/scripts/check-plugin-tool-supply.ts
14344
- import { existsSync as existsSync55 } from "fs";
14345
- import { join as join66 } from "path";
14377
+ import { existsSync as existsSync54 } from "fs";
14378
+ import { join as join65 } from "path";
14346
14379
 
14347
14380
  // src/lib/plugin-tool-supply-audit.ts
14348
- import { existsSync as existsSync54, readFileSync as readFileSync43, readdirSync as readdirSync26, statSync as statSync16 } from "fs";
14349
- import { join as join65 } from "path";
14381
+ import { existsSync as existsSync53, readFileSync as readFileSync43, readdirSync as readdirSync26, statSync as statSync16 } from "fs";
14382
+ import { join as join64 } from "path";
14350
14383
 
14351
14384
  // src/lib/openrouter-model-snapshot.ts
14352
14385
  var OPENROUTER_MODEL_SNAPSHOT_FETCHED_AT = "2026-08-10T06:39:01Z";
@@ -14763,7 +14796,7 @@ function listDirs(root) {
14763
14796
  }
14764
14797
  return entries.filter((e) => {
14765
14798
  try {
14766
- return statSync16(join65(root, e)).isDirectory();
14799
+ return statSync16(join64(root, e)).isDirectory();
14767
14800
  } catch {
14768
14801
  return false;
14769
14802
  }
@@ -14779,7 +14812,7 @@ function walkFiles2(root, accept, skipDir) {
14779
14812
  return;
14780
14813
  }
14781
14814
  for (const entry of entries) {
14782
- const p = join65(dir, entry);
14815
+ const p = join64(dir, entry);
14783
14816
  let st;
14784
14817
  try {
14785
14818
  st = statSync16(p);
@@ -14805,14 +14838,14 @@ function pluginPythonFiles(pluginDir2) {
14805
14838
  );
14806
14839
  }
14807
14840
  function pluginTerraformFiles(pluginDir2) {
14808
- const tfDir = join65(pluginDir2, "terraform");
14841
+ const tfDir = join64(pluginDir2, "terraform");
14809
14842
  let entries;
14810
14843
  try {
14811
14844
  entries = readdirSync26(tfDir);
14812
14845
  } catch {
14813
14846
  return [];
14814
14847
  }
14815
- return entries.filter((e) => e.endsWith(".tf")).map((e) => join65(tfDir, e)).sort();
14848
+ return entries.filter((e) => e.endsWith(".tf")).map((e) => join64(tfDir, e)).sort();
14816
14849
  }
14817
14850
  function extractManifestTools(manifestText) {
14818
14851
  let parsed;
@@ -15064,8 +15097,8 @@ function isSnapshotStale(fetchedAt, now) {
15064
15097
  function normalizeModelId(id) {
15065
15098
  return id.endsWith(":online") ? id.slice(0, -":online".length) : id;
15066
15099
  }
15067
- var CONFIG_PY_PATH = join65("services", "api", "src", "api", "config.py");
15068
- var ORCHESTRATION_SCHEMA_PATH = join65(
15100
+ var CONFIG_PY_PATH = join64("services", "api", "src", "api", "config.py");
15101
+ var ORCHESTRATION_SCHEMA_PATH = join64(
15069
15102
  "services",
15070
15103
  "api",
15071
15104
  "src",
@@ -15077,10 +15110,10 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
15077
15110
  const knownModelIds = options.knownModelIds ?? OPENROUTER_MODEL_IDS;
15078
15111
  const snapshotFetchedAt = options.snapshotFetchedAt ?? OPENROUTER_MODEL_SNAPSHOT_FETCHED_AT;
15079
15112
  const now = options.now ?? /* @__PURE__ */ new Date();
15080
- const configPath = join65(repoRoot, CONFIG_PY_PATH);
15081
- const orchestrationPath = join65(repoRoot, ORCHESTRATION_SCHEMA_PATH);
15082
- const configMissing = !existsSync54(configPath);
15083
- const orchestrationSchemaMissing = !existsSync54(orchestrationPath);
15113
+ const configPath = join64(repoRoot, CONFIG_PY_PATH);
15114
+ const orchestrationPath = join64(repoRoot, ORCHESTRATION_SCHEMA_PATH);
15115
+ const configMissing = !existsSync53(configPath);
15116
+ const orchestrationSchemaMissing = !existsSync53(orchestrationPath);
15084
15117
  const knownSet = new Set(knownModelIds);
15085
15118
  const snapshotEmpty = knownModelIds.length === 0;
15086
15119
  const snapshotStale = isSnapshotStale(snapshotFetchedAt, now);
@@ -15151,7 +15184,7 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
15151
15184
  function discoverPluginDirs(pluginsRoot) {
15152
15185
  return listDirs(pluginsRoot).filter((name) => {
15153
15186
  try {
15154
- return statSync16(join65(pluginsRoot, name, "biffo.plugin.json")).isFile();
15187
+ return statSync16(join64(pluginsRoot, name, "biffo.plugin.json")).isFile();
15155
15188
  } catch {
15156
15189
  return false;
15157
15190
  }
@@ -15164,8 +15197,8 @@ function auditPluginToolSupply(pluginsRoot) {
15164
15197
  let terraformBlind = false;
15165
15198
  let totalDeclaredTools = 0;
15166
15199
  for (const name of pluginNames) {
15167
- const pluginDir2 = join65(pluginsRoot, name);
15168
- const manifestText = readFileSync43(join65(pluginDir2, "biffo.plugin.json"), "utf8");
15200
+ const pluginDir2 = join64(pluginsRoot, name);
15201
+ const manifestText = readFileSync43(join64(pluginDir2, "biffo.plugin.json"), "utf8");
15169
15202
  const manifest = extractManifestTools(manifestText);
15170
15203
  if (manifest.parseError) {
15171
15204
  findings.push({
@@ -15263,7 +15296,7 @@ function auditPluginToolSupply(pluginsRoot) {
15263
15296
  requiredEnvVars: envResult.envVars,
15264
15297
  missingEnvVars: anyWired ? [] : envResult.envVars,
15265
15298
  status: anyWired ? "ok" : "missing-env",
15266
- 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`
15299
+ 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`
15267
15300
  });
15268
15301
  }
15269
15302
  }
@@ -15295,9 +15328,16 @@ function auditPluginToolSupply(pluginsRoot) {
15295
15328
  // src/scripts/check-plugin-tool-supply.ts
15296
15329
  async function runPluginToolSupplyCheck() {
15297
15330
  const root = (await execa("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
15331
+ const ownership = classifyRepoOwnership(root);
15332
+ if (ownership === "satellite") {
15333
+ console.log(
15334
+ "\u2713 plugin tool-supply guard: skipped \u2014 this repo is not the template or an instance, so it holds no services/_plugins/ tool grants and no services/api/ model-id catalogue to audit (a sibling/plugin repo may carry an unrelated services/api/ of its own \u2014 a BFF, not the ADR-0002 Core API this guard checks)."
15335
+ );
15336
+ return;
15337
+ }
15298
15338
  let allOk = true;
15299
- const pluginsRoot = join66(root, "services", "_plugins");
15300
- if (!existsSync55(pluginsRoot)) {
15339
+ const pluginsRoot = join65(root, "services", "_plugins");
15340
+ if (!existsSync54(pluginsRoot)) {
15301
15341
  console.log("\u2713 plugin tool-supply guard: no services/_plugins/ \u2014 nothing to audit");
15302
15342
  } else {
15303
15343
  const report = auditPluginToolSupply(pluginsRoot);
@@ -15327,8 +15367,8 @@ async function runPluginToolSupplyCheck() {
15327
15367
  console.log(`\u2713 plugin tool-supply guard: ${report.summary}`);
15328
15368
  }
15329
15369
  }
15330
- const servicesApiRoot = join66(root, "services", "api");
15331
- if (!existsSync55(servicesApiRoot)) {
15370
+ const servicesApiRoot = join65(root, "services", "api");
15371
+ if (!existsSync54(servicesApiRoot)) {
15332
15372
  console.log("\u2713 plugin model-id guard: no services/api/ \u2014 nothing to audit");
15333
15373
  } else {
15334
15374
  const modelReport = auditDeclaredModelIds(root);
@@ -15705,12 +15745,12 @@ async function runSharedFileReductionCheck(args) {
15705
15745
  }
15706
15746
 
15707
15747
  // src/scripts/check-skeleton-drift.ts
15708
- import { existsSync as existsSync56, readdirSync as readdirSync28 } from "fs";
15709
- import { join as join68 } from "path";
15748
+ import { existsSync as existsSync55, readdirSync as readdirSync28 } from "fs";
15749
+ import { join as join67 } from "path";
15710
15750
 
15711
15751
  // src/lib/skeleton-drift-guard.ts
15712
15752
  import { readFileSync as readFileSync46, readdirSync as readdirSync27, statSync as statSync17 } from "fs";
15713
- import { join as join67 } from "path";
15753
+ import { join as join66 } from "path";
15714
15754
  var isWorkflow = (rel) => rel.startsWith(".github/workflows/") && (rel.endsWith(".yml") || rel.endsWith(".yaml"));
15715
15755
  var isRootLayout = (rel) => rel.endsWith("src/app/layout.tsx");
15716
15756
  var uncommented = (contents) => contents.split("\n").filter((line) => !/^\s*(\/\/|\/\*|\*)/.test(line)).join("\n");
@@ -15774,7 +15814,7 @@ function walk(dir, base = dir) {
15774
15814
  }
15775
15815
  for (const entry of entries) {
15776
15816
  if (entry === ".venv" || entry === "node_modules" || entry === ".git") continue;
15777
- const abs = join67(dir, entry);
15817
+ const abs = join66(dir, entry);
15778
15818
  let isDir;
15779
15819
  try {
15780
15820
  isDir = statSync17(abs).isDirectory();
@@ -15796,7 +15836,7 @@ function auditSkeleton(skeletonRoot, name, rules = SKELETON_RULES) {
15796
15836
  if (!rule.appliesTo(rel)) continue;
15797
15837
  let contents;
15798
15838
  try {
15799
- contents = readFileSync46(join67(skeletonRoot, rel), "utf8");
15839
+ contents = readFileSync46(join66(skeletonRoot, rel), "utf8");
15800
15840
  } catch {
15801
15841
  continue;
15802
15842
  }
@@ -15825,23 +15865,23 @@ function formatViolations2(violations) {
15825
15865
 
15826
15866
  // src/scripts/check-skeleton-drift.ts
15827
15867
  function discoverSkeletons(root) {
15828
- const skeletonsDir = join68(root, "_skeletons");
15868
+ const skeletonsDir = join67(root, "_skeletons");
15829
15869
  let entries;
15830
15870
  try {
15831
15871
  entries = readdirSync28(skeletonsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
15832
15872
  } catch {
15833
15873
  return [];
15834
15874
  }
15835
- return entries.filter((name) => existsSync56(join68(skeletonsDir, name, ".github", "workflows", "ci.yml"))).sort();
15875
+ return entries.filter((name) => existsSync55(join67(skeletonsDir, name, ".github", "workflows", "ci.yml"))).sort();
15836
15876
  }
15837
15877
  async function runSkeletonDriftCheck() {
15838
15878
  const root = (await execa("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
15839
15879
  const skeletons = discoverSkeletons(root);
15840
15880
  let filesConsidered = 0;
15841
15881
  for (const name of skeletons) {
15842
- const skeletonRoot = join68(root, "_skeletons", name);
15882
+ const skeletonRoot = join67(root, "_skeletons", name);
15843
15883
  filesConsidered += findWorkflowFiles(skeletonRoot).length;
15844
- if (existsSync56(join68(skeletonRoot, "apps", "frontend", "src", "app", "layout.tsx"))) {
15884
+ if (existsSync55(join67(skeletonRoot, "apps", "frontend", "src", "app", "layout.tsx"))) {
15845
15885
  filesConsidered += 1;
15846
15886
  }
15847
15887
  }
@@ -15849,7 +15889,7 @@ async function runSkeletonDriftCheck() {
15849
15889
  `audited ${skeletons.length} skeleton(s) (${skeletons.join(", ") || "none"}), ${filesConsidered} file(s) considered, under ${root}/_skeletons`
15850
15890
  );
15851
15891
  if (skeletons.length === 0) {
15852
- if (!existsSync56(join68(root, "_skeletons"))) {
15892
+ if (!existsSync55(join67(root, "_skeletons"))) {
15853
15893
  console.log(
15854
15894
  `\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.`
15855
15895
  );
@@ -15861,7 +15901,7 @@ async function runSkeletonDriftCheck() {
15861
15901
  process.exit(1);
15862
15902
  }
15863
15903
  const violations = skeletons.flatMap(
15864
- (name) => auditSkeleton(join68(root, "_skeletons", name), name)
15904
+ (name) => auditSkeleton(join67(root, "_skeletons", name), name)
15865
15905
  );
15866
15906
  if (violations.length > 0) {
15867
15907
  console.error("\u2717 Skeleton-drift guard: drift found between this repo and its scaffolding\n");
@@ -16062,8 +16102,8 @@ function rawArgsAfter(subcommand) {
16062
16102
  }
16063
16103
 
16064
16104
  // src/commands/doctor.ts
16065
- import { existsSync as existsSync57, readFileSync as readFileSync47 } from "fs";
16066
- import { join as join69, resolve as resolve20 } from "path";
16105
+ import { existsSync as existsSync56, readFileSync as readFileSync47 } from "fs";
16106
+ import { join as join68, resolve as resolve20 } from "path";
16067
16107
  import chalk21 from "chalk";
16068
16108
  import { Command as Command25 } from "commander";
16069
16109
 
@@ -16300,8 +16340,13 @@ function classifyReapCandidate(facts) {
16300
16340
  return { action: "keep", reason: "unknown-pr-verdict" };
16301
16341
  }
16302
16342
  }
16303
- function findReapCandidates(branches, worktrees) {
16304
- const goneBranches = new Set(branches.filter((b) => b.track.includes("gone")).map((b) => b.name));
16343
+ function hasProvenGoneRemote(branch, remoteBranchNames) {
16344
+ return !remoteBranchNames.has(branch.name);
16345
+ }
16346
+ function findReapCandidates(branches, worktrees, remoteBranchNames) {
16347
+ const goneBranches = new Set(
16348
+ branches.filter((b) => hasProvenGoneRemote(b, remoteBranchNames)).map((b) => b.name)
16349
+ );
16305
16350
  return worktrees.filter((w) => goneBranches.has(w.branch)).map((w) => ({ branch: w.branch, worktreePath: w.path }));
16306
16351
  }
16307
16352
  async function reapCandidate(cwd, candidate, deps) {
@@ -16345,8 +16390,8 @@ async function reapCandidate(cwd, candidate, deps) {
16345
16390
  const worktreeRemoved = await git.removeWorktree(cwd, candidate.worktreePath);
16346
16391
  return { candidate, verdict, worktreeRemoved, staleClaimCleared };
16347
16392
  }
16348
- async function reapAll(cwd, branches, worktrees, currentBranch, deps) {
16349
- const candidates = findReapCandidates(branches, worktrees).filter(
16393
+ async function reapAll(cwd, branches, worktrees, currentBranch, deps, remoteBranchNames) {
16394
+ const candidates = findReapCandidates(branches, worktrees, remoteBranchNames).filter(
16350
16395
  (c) => c.branch !== currentBranch
16351
16396
  );
16352
16397
  const outcomes = [];
@@ -16355,9 +16400,9 @@ async function reapAll(cwd, branches, worktrees, currentBranch, deps) {
16355
16400
  }
16356
16401
  return outcomes;
16357
16402
  }
16358
- function findBareBranchCandidates(branches, worktrees) {
16403
+ function findBareBranchCandidates(branches, worktrees, remoteBranchNames) {
16359
16404
  const worktreeBranches = new Set(worktrees.map((w) => w.branch));
16360
- return branches.filter((b) => b.track.includes("gone") && !worktreeBranches.has(b.name)).map((b) => ({ branch: b.name }));
16405
+ return branches.filter((b) => hasProvenGoneRemote(b, remoteBranchNames) && !worktreeBranches.has(b.name)).map((b) => ({ branch: b.name }));
16361
16406
  }
16362
16407
  async function reapBareBranch(cwd, candidate, deps) {
16363
16408
  const { git, github } = deps;
@@ -16386,8 +16431,8 @@ async function reapBareBranch(cwd, candidate, deps) {
16386
16431
  const branchDeleted = await git.deleteBranch(cwd, candidate.branch);
16387
16432
  return { candidate, verdict, branchDeleted };
16388
16433
  }
16389
- async function reapAllBareBranches(cwd, branches, worktrees, currentBranch, deps) {
16390
- const candidates = findBareBranchCandidates(branches, worktrees).filter(
16434
+ async function reapAllBareBranches(cwd, branches, worktrees, currentBranch, deps, remoteBranchNames) {
16435
+ const candidates = findBareBranchCandidates(branches, worktrees, remoteBranchNames).filter(
16391
16436
  (c) => c.branch !== currentBranch
16392
16437
  );
16393
16438
  const outcomes = [];
@@ -16464,14 +16509,23 @@ async function gatherRepoFacts(options, deps) {
16464
16509
  };
16465
16510
  }
16466
16511
  async function runDoctorFix(cwd, facts, deps = { git: new GitAdapter(), github: new GithubCliAdapter() }) {
16467
- return reapAll(cwd, facts.branches, facts.worktrees, facts.currentBranch, deps);
16512
+ const remoteBranchNames = await deps.git.listRemoteBranchNames(cwd);
16513
+ return reapAll(cwd, facts.branches, facts.worktrees, facts.currentBranch, deps, remoteBranchNames);
16468
16514
  }
16469
16515
  async function runDoctorFixBranches(cwd, facts, deps = { git: new GitAdapter(), github: new GithubCliAdapter() }) {
16470
- return reapAllBareBranches(cwd, facts.branches, facts.worktrees, facts.currentBranch, deps);
16516
+ const remoteBranchNames = await deps.git.listRemoteBranchNames(cwd);
16517
+ return reapAllBareBranches(
16518
+ cwd,
16519
+ facts.branches,
16520
+ facts.worktrees,
16521
+ facts.currentBranch,
16522
+ deps,
16523
+ remoteBranchNames
16524
+ );
16471
16525
  }
16472
16526
  function readLocalCoreVersion(cwd) {
16473
- const path = join69(cwd, INSTANCE_CORE_FILE);
16474
- if (!existsSync57(path)) return null;
16527
+ const path = join68(cwd, INSTANCE_CORE_FILE);
16528
+ if (!existsSync56(path)) return null;
16475
16529
  try {
16476
16530
  return extractVersionField(readFileSync47(path, "utf8"));
16477
16531
  } catch {
@@ -16493,8 +16547,8 @@ function extractVersionField(contents) {
16493
16547
  return match?.[1] ?? null;
16494
16548
  }
16495
16549
  function readFossil(cwd) {
16496
- const path = join69(cwd, CORE_VERSION_FILE);
16497
- if (!existsSync57(path)) return null;
16550
+ const path = join68(cwd, CORE_VERSION_FILE);
16551
+ if (!existsSync56(path)) return null;
16498
16552
  try {
16499
16553
  const value = readFileSync47(path, "utf8").trim();
16500
16554
  return value === "" ? null : value;
@@ -17033,13 +17087,13 @@ import { fileURLToPath as fileURLToPath6 } from "url";
17033
17087
  import { Command as Command27 } from "commander";
17034
17088
 
17035
17089
  // src/lib/packaged-scripts.ts
17036
- import { existsSync as existsSync58 } from "fs";
17037
- import { dirname as dirname11, join as join70 } from "path";
17090
+ import { existsSync as existsSync57 } from "fs";
17091
+ import { dirname as dirname11, join as join69 } from "path";
17038
17092
  function findPackagedScript(startDir, relativePath) {
17039
17093
  let dir = startDir;
17040
17094
  for (; ; ) {
17041
- const candidate = join70(dir, relativePath);
17042
- if (existsSync58(candidate)) return candidate;
17095
+ const candidate = join69(dir, relativePath);
17096
+ if (existsSync57(candidate)) return candidate;
17043
17097
  const parent = dirname11(dir);
17044
17098
  if (parent === dir) return null;
17045
17099
  dir = parent;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.309.2",
3
+ "version": "0.309.4",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/scripts/verify.sh CHANGED
@@ -1455,11 +1455,30 @@ if [ -f scripts/biffo.sh ]; then
1455
1455
  # biffo.core.json's own version and compares against it -- but this repo
1456
1456
  # never carries biffo.core.json (it is the template, not an instance), so
1457
1457
  # the script itself detects that and exits 0 as a genuine skip (see its own
1458
- # doc comment) rather than attempting a comparison it has no tree for. Kept
1459
- # unconditional, exactly like the line above, for the same reason: ci.yml's
1460
- # exact command text must appear in `verify.sh --list`'s output or the
1461
- # parity test (verify-parity.test.ts, #1773) reports it uncategorised.
1462
- run_check orphan-ratchet-instance sh scripts/check-orphan-ratchet-instance.sh
1458
+ # doc comment) rather than attempting a comparison it has no tree for.
1459
+ #
1460
+ # Guarded with `[ -f ... ] &&`, unlike the line above, because this script
1461
+ # is a raw file path rather than a `scripts/biffo.sh check <name>` call
1462
+ # through the versioned CLI bridge -- every other check in this block goes
1463
+ # through that bridge, which resolves against the pinned, PACKAGED CLI and
1464
+ # so works from any repo. This one does not, and `scripts/
1465
+ # check-orphan-ratchet-instance.sh` is template-owned but never packaged
1466
+ # for a satellite: not in shared-files.json's `files` (never copied to a
1467
+ # sibling/plugin repo) and not in `PACKAGED_ROOT_ASSETS` (never bundled
1468
+ # into the npm tarball) -- it only ever reaches a real INSTANCE, via
1469
+ # `biffo core upgrade`'s ordinary three-way merge. A satellite running the
1470
+ # packaged `verify.sh` through `scripts/biffo.sh verify` therefore had no
1471
+ # such file on disk at all, and a bare `sh scripts/
1472
+ # check-orphan-ratchet-instance.sh` failed at the shell -- "cannot open
1473
+ # ... No such file" -- before the script's own biffo.core.json skip ever
1474
+ # got a chance to run (biffo-template#1943). The guard is true in every
1475
+ # checkout that actually carries the file (this repo, and any instance
1476
+ # that has upgraded far enough to have received it), so `verify.sh --list`'s
1477
+ # output here is unchanged and parity with ci.yml's own unconditional step
1478
+ # holds exactly as before; it is false only in a satellite, where this now
1479
+ # skips instead of crashing the rest of the gate.
1480
+ [ -f scripts/check-orphan-ratchet-instance.sh ] &&
1481
+ run_check orphan-ratchet-instance sh scripts/check-orphan-ratchet-instance.sh
1463
1482
  run_check cognito-invite sh scripts/biffo.sh check cognito-invite-template
1464
1483
  run_check lambda-output sh scripts/biffo.sh check lambda-output
1465
1484
  run_check pipe-trap sh scripts/biffo.sh check pipe-trap