@biffo/cli 0.267.2 → 0.267.3

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.
Files changed (2) hide show
  1. package/dist/index.js +764 -120
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -377,12 +377,12 @@ function toPosix(p) {
377
377
  function listTemplateOwnedFiles(root, manifest, options = {}) {
378
378
  const out = [];
379
379
  const tracked = options.trackedOnly ? options.git ? gitTrackedFiles(root, options.git) : gitTrackedFiles(root) : null;
380
- function walk(dir) {
380
+ function walk2(dir) {
381
381
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
382
382
  if (entry.isDirectory() && HARD_EXCLUDED_DIRS.has(entry.name)) continue;
383
383
  const abs = join2(dir, entry.name);
384
384
  if (entry.isDirectory()) {
385
- walk(abs);
385
+ walk2(abs);
386
386
  } else if (entry.isFile()) {
387
387
  const rel = toPosix(relative(root, abs));
388
388
  if (tracked && !tracked.has(rel)) continue;
@@ -390,7 +390,7 @@ function listTemplateOwnedFiles(root, manifest, options = {}) {
390
390
  }
391
391
  }
392
392
  }
393
- walk(root);
393
+ walk2(root);
394
394
  return out.sort();
395
395
  }
396
396
  function sameFile(a, b) {
@@ -6030,12 +6030,12 @@ function formatStaleBuildError(result) {
6030
6030
  }
6031
6031
  function collectSourceFiles(srcDir) {
6032
6032
  const found = [];
6033
- const walk = (dir) => {
6033
+ const walk2 = (dir) => {
6034
6034
  for (const entry of readdirSync7(dir, { withFileTypes: true })) {
6035
6035
  const full = join19(dir, entry.name);
6036
6036
  if (entry.isDirectory()) {
6037
6037
  if (entry.name === "node_modules") continue;
6038
- walk(full);
6038
+ walk2(full);
6039
6039
  continue;
6040
6040
  }
6041
6041
  if (!entry.isFile()) continue;
@@ -6045,7 +6045,7 @@ function collectSourceFiles(srcDir) {
6045
6045
  found.push(full);
6046
6046
  }
6047
6047
  };
6048
- walk(srcDir);
6048
+ walk2(srcDir);
6049
6049
  return found;
6050
6050
  }
6051
6051
  function findPackageRoot(from) {
@@ -7834,7 +7834,7 @@ function scaffoldPlugin(skeletonRoot, destDir, names, options = {}) {
7834
7834
  }
7835
7835
  const skipped = [];
7836
7836
  const files = [];
7837
- const walk = (relDir) => {
7837
+ const walk2 = (relDir) => {
7838
7838
  const absDir = join25(skeletonRoot, relDir);
7839
7839
  for (const entry of readdirSync11(absDir, { withFileTypes: true }).sort(
7840
7840
  (a, b) => a.name.localeCompare(b.name)
@@ -7846,7 +7846,7 @@ function scaffoldPlugin(skeletonRoot, destDir, names, options = {}) {
7846
7846
  }
7847
7847
  const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
7848
7848
  if (entry.isDirectory()) {
7849
- walk(relPath);
7849
+ walk2(relPath);
7850
7850
  continue;
7851
7851
  }
7852
7852
  const destRel = applySubstitutions(relPath, names);
@@ -7863,7 +7863,7 @@ function scaffoldPlugin(skeletonRoot, destDir, names, options = {}) {
7863
7863
  files.push(destRel);
7864
7864
  }
7865
7865
  };
7866
- walk("");
7866
+ walk2("");
7867
7867
  if (!files.some((f) => f.startsWith("terraform/"))) {
7868
7868
  throw new Error(
7869
7869
  `Scaffold produced no terraform/ files from ${skeletonRoot} \u2014 refusing to leave a plugin whose event subscriptions could never fire (issue #194).`
@@ -9872,13 +9872,142 @@ async function runBranchProtectionCheck(explicitRepo, options = {}) {
9872
9872
  console.log(`\u2713 branch-protection guard: ${owner}/${repo} (${audited.join(", ")}) OK`);
9873
9873
  }
9874
9874
 
9875
- // src/scripts/check-core-direct-paths.ts
9876
- import { join as join37 } from "path";
9875
+ // src/scripts/check-cognito-invite-template.ts
9877
9876
  import { execa as execa7 } from "execa";
9878
9877
 
9879
- // src/lib/core-direct-paths-audit.ts
9880
- import { readFileSync as readFileSync27, readdirSync as readdirSync14, statSync as statSync7 } from "fs";
9878
+ // src/lib/cognito-invite-template-guard.ts
9879
+ import { readdirSync as readdirSync14, readFileSync as readFileSync27, statSync as statSync7 } from "fs";
9881
9880
  import { join as join36 } from "path";
9881
+ var REQUIRED_INVITE_MEMBERS = ["email_subject", "email_message", "sms_message"];
9882
+ var REQUIRED_INVITE_PLACEHOLDERS = ["{username}", "{####}"];
9883
+ var PLACEHOLDER_MEMBERS = ["email_message", "sms_message"];
9884
+ function stripHeredocs(source) {
9885
+ return source.replace(/<<-?(\w+)\r?\n[\s\S]*?\r?\n[ \t]*\1\b/g, '"HEREDOC"');
9886
+ }
9887
+ function stripHclComments(source) {
9888
+ return source.replace(/\/\*[\s\S]*?\*\//g, "").split("\n").map((line) => line.replace(/(^|\s)(#|\/\/).*$/, "$1")).join("\n");
9889
+ }
9890
+ function extractBlocks(source, name) {
9891
+ const blocks = [];
9892
+ const opener = new RegExp(`(?:^|[\\s])${name}\\s*\\{`, "g");
9893
+ let match;
9894
+ while ((match = opener.exec(source)) !== null) {
9895
+ const openBrace = source.indexOf("{", match.index);
9896
+ let depth = 1;
9897
+ let i = openBrace + 1;
9898
+ for (; i < source.length && depth > 0; i++) {
9899
+ if (source[i] === "{") depth++;
9900
+ else if (source[i] === "}") depth--;
9901
+ }
9902
+ blocks.push({
9903
+ line: source.slice(0, match.index).split("\n").length,
9904
+ body: source.slice(openBrace + 1, i - 1)
9905
+ });
9906
+ }
9907
+ return blocks;
9908
+ }
9909
+ function checkInviteTemplateSource(file, rawSource) {
9910
+ const source = stripHclComments(stripHeredocs(rawSource));
9911
+ const violations = [];
9912
+ for (const block of extractBlocks(source, "invite_message_template")) {
9913
+ for (const member of REQUIRED_INVITE_MEMBERS) {
9914
+ const assigned = new RegExp(`(?:^|[\\s{])${member}\\s*=`).test(block.body);
9915
+ if (!assigned) {
9916
+ violations.push({
9917
+ file,
9918
+ line: block.line,
9919
+ message: `invite_message_template is missing "${member}". Cognito's CreateUserPool validates all of ${REQUIRED_INVITE_MEMBERS.join(", ")} and rejects an empty member with InvalidParameterException ("length greater than or equal to 6"), so no user pool can be created and every fresh deploy fails (issue #356).`
9920
+ });
9921
+ }
9922
+ }
9923
+ }
9924
+ for (const block of extractBlocks(rawSource, "invite_message_template")) {
9925
+ for (const member of PLACEHOLDER_MEMBERS) {
9926
+ const body = memberBody(block.body, member);
9927
+ if (body === null) continue;
9928
+ for (const placeholder of REQUIRED_INVITE_PLACEHOLDERS) {
9929
+ if (!body.includes(placeholder)) {
9930
+ violations.push({
9931
+ file,
9932
+ line: block.line,
9933
+ message: `invite_message_template's "${member}" is missing the ${placeholder} placeholder. Cognito's CreateUserPool rejects the template outright ("${member === "sms_message" ? "SMS message" : "Email message body"} should have ${placeholder} which will be replaced by code"), so no user pool can be created. Note {username} is required even when the pool uses username_attributes and the value is an opaque UUID \u2014 keep it, but do not ask the recipient to type it.`
9934
+ });
9935
+ }
9936
+ }
9937
+ }
9938
+ }
9939
+ return violations;
9940
+ }
9941
+ function memberBody(blockBody, member) {
9942
+ const heredoc = new RegExp(
9943
+ `(?:^|[\\s{])${member}\\s*=\\s*<<-?([A-Za-z_]+)([\\s\\S]*?)^\\s*\\1`,
9944
+ "m"
9945
+ );
9946
+ const heredocMatch = heredoc.exec(blockBody);
9947
+ if (heredocMatch?.[2] !== void 0) return heredocMatch[2];
9948
+ const quoted = new RegExp(`(?:^|[\\s{])${member}\\s*=\\s*"((?:[^"\\\\]|\\\\.)*)"`);
9949
+ const quotedMatch = quoted.exec(blockBody);
9950
+ return quotedMatch?.[1] ?? null;
9951
+ }
9952
+ function findModuleTerraformFiles(repoRoot) {
9953
+ const found = [];
9954
+ const walk2 = (dir, relative8) => {
9955
+ let entries;
9956
+ try {
9957
+ entries = readdirSync14(dir);
9958
+ } catch {
9959
+ return;
9960
+ }
9961
+ for (const entry of entries) {
9962
+ if (entry === "node_modules" || entry === ".git" || entry === ".worktrees") continue;
9963
+ const full = join36(dir, entry);
9964
+ const rel = `${relative8}/${entry}`;
9965
+ if (statSync7(full).isDirectory()) {
9966
+ walk2(full, rel);
9967
+ } else if (entry.endsWith(".tf")) {
9968
+ found.push(rel);
9969
+ }
9970
+ }
9971
+ };
9972
+ walk2(join36(repoRoot, "modules"), "modules");
9973
+ return found.sort();
9974
+ }
9975
+ function checkCognitoInviteTemplates(repoRoot) {
9976
+ return findModuleTerraformFiles(repoRoot).flatMap(
9977
+ (file) => checkInviteTemplateSource(file, readFileSync27(join36(repoRoot, file), "utf8"))
9978
+ );
9979
+ }
9980
+
9981
+ // src/scripts/check-cognito-invite-template.ts
9982
+ async function runCognitoInviteTemplateCheck() {
9983
+ const root = (await execa7("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
9984
+ const files = findModuleTerraformFiles(root);
9985
+ console.log(`audited ${files.length} .tf file(s) under modules/ under ${root}`);
9986
+ if (files.length === 0) {
9987
+ console.error(
9988
+ "\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."
9989
+ );
9990
+ process.exit(1);
9991
+ }
9992
+ const violations = checkCognitoInviteTemplates(root);
9993
+ if (violations.length > 0) {
9994
+ console.error("\u2717 Cognito invite template guard: incomplete invite_message_template(s)\n");
9995
+ for (const v of violations) {
9996
+ console.error(` ${v.file}:${v.line} ${v.message}`);
9997
+ }
9998
+ console.error("\nSee biffo-template#356.");
9999
+ process.exit(1);
10000
+ }
10001
+ console.log(`\u2713 Cognito invite template guard: every invite_message_template block is complete`);
10002
+ }
10003
+
10004
+ // src/scripts/check-core-direct-paths.ts
10005
+ import { join as join38 } from "path";
10006
+ import { execa as execa8 } from "execa";
10007
+
10008
+ // src/lib/core-direct-paths-audit.ts
10009
+ import { readFileSync as readFileSync28, readdirSync as readdirSync15, statSync as statSync8 } from "fs";
10010
+ import { join as join37 } from "path";
9882
10011
  var EXTERNAL_BASE_IDENTIFIERS = ["CORE_API_URL"];
9883
10012
  var API_ROUTE_PREFIX = "/api/v1";
9884
10013
  var TEST_FILE_SUFFIXES = [".test.ts", ".test.tsx", ".spec.ts", ".spec.tsx"];
@@ -10034,30 +10163,30 @@ function countRawExternalOccurrences(rawText, externalBases = EXTERNAL_BASE_IDEN
10034
10163
  }
10035
10164
  function walkFiles(root, accept, skipDir) {
10036
10165
  const out = [];
10037
- const walk = (dir) => {
10166
+ const walk2 = (dir) => {
10038
10167
  let entries;
10039
10168
  try {
10040
- entries = readdirSync14(dir);
10169
+ entries = readdirSync15(dir);
10041
10170
  } catch {
10042
10171
  return;
10043
10172
  }
10044
10173
  for (const entry of entries) {
10045
- const p = join36(dir, entry);
10174
+ const p = join37(dir, entry);
10046
10175
  let st;
10047
10176
  try {
10048
- st = statSync7(p);
10177
+ st = statSync8(p);
10049
10178
  } catch {
10050
10179
  continue;
10051
10180
  }
10052
10181
  if (st.isDirectory()) {
10053
10182
  if (skipDir(entry)) continue;
10054
- walk(p);
10183
+ walk2(p);
10055
10184
  continue;
10056
10185
  }
10057
10186
  if (accept(entry)) out.push(p);
10058
10187
  }
10059
10188
  };
10060
- walk(root);
10189
+ walk2(root);
10061
10190
  return out.sort();
10062
10191
  }
10063
10192
  function frontendSourceFiles(frontendSrcDir) {
@@ -10072,7 +10201,7 @@ function auditFrontendExtraction(frontendSrcDir, externalBases = EXTERNAL_BASE_I
10072
10201
  const extracted = [];
10073
10202
  let rawTotal = 0;
10074
10203
  for (const file of files) {
10075
- const text = readFileSync27(file, "utf8");
10204
+ const text = readFileSync28(file, "utf8");
10076
10205
  rawTotal += countRawExternalOccurrences(text, externalBases);
10077
10206
  extracted.push(...extractCoreDirectPaths(text, file, externalBases));
10078
10207
  }
@@ -10121,7 +10250,7 @@ function auditCoreRouteExtraction(apiSrcDir) {
10121
10250
  const prefixSet = /* @__PURE__ */ new Set();
10122
10251
  let rawApiRouterCount = 0;
10123
10252
  for (const file of files) {
10124
- const text = readFileSync27(file, "utf8");
10253
+ const text = readFileSync28(file, "utf8");
10125
10254
  const extraction = extractCoreRoutePrefixes(text);
10126
10255
  rawApiRouterCount += extraction.rawApiRouterCount;
10127
10256
  for (const p of extraction.prefixes) prefixSet.add(normalizePrefix(p));
@@ -10169,10 +10298,10 @@ function auditSiblingCoreDirectPaths(params) {
10169
10298
 
10170
10299
  // src/scripts/check-core-direct-paths.ts
10171
10300
  async function runCoreDirectPathsCheck(opts = {}) {
10172
- const root = (await execa7("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
10301
+ const root = (await execa8("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
10173
10302
  const sibling = opts.sibling ?? "sibling-template (self-check)";
10174
- const frontendSrcDir = opts.frontendSrc ?? join37(root, "_skeletons", "sibling-template", "apps", "frontend", "src");
10175
- const coreApiSrcDir = opts.coreSrc ?? join37(root, "services", "api", "src");
10303
+ const frontendSrcDir = opts.frontendSrc ?? join38(root, "_skeletons", "sibling-template", "apps", "frontend", "src");
10304
+ const coreApiSrcDir = opts.coreSrc ?? join38(root, "services", "api", "src");
10176
10305
  const report = auditSiblingCoreDirectPaths({ sibling, frontendSrcDir, coreApiSrcDir });
10177
10306
  console.log(
10178
10307
  `audited ${report.extractedCount} core-direct call site(s) across ${report.frontendFiles} frontend file(s) under ${frontendSrcDir}, against ${report.corePrefixCount} route prefix(es) from ${report.coreFiles} core file(s) under ${coreApiSrcDir}`
@@ -10207,7 +10336,7 @@ async function runCoreDirectPathsCheck(opts = {}) {
10207
10336
  }
10208
10337
 
10209
10338
  // src/scripts/check-core-ownership.ts
10210
- import { execa as execa8 } from "execa";
10339
+ import { execa as execa9 } from "execa";
10211
10340
  var BOLD = "\x1B[1m";
10212
10341
  var DIM = "\x1B[2m";
10213
10342
  var RED = "\x1B[31m";
@@ -10218,7 +10347,7 @@ async function runOwnershipCheck(argv) {
10218
10347
  const stagedFlag = args.indexOf("--staged");
10219
10348
  const staged = stagedFlag !== -1;
10220
10349
  const messageFile = staged ? args[stagedFlag + 1] : void 0;
10221
- const root = (await execa8("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
10350
+ const root = (await execa9("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
10222
10351
  const ownership = classifyRepoOwnership(root);
10223
10352
  if (ownership === "template") {
10224
10353
  console.log("\u2713 core ownership guard: skipped \u2014 this is the template, which owns these paths.");
@@ -10234,11 +10363,11 @@ async function runOwnershipCheck(argv) {
10234
10363
  let deletedFiles = [];
10235
10364
  let commitMessage = "";
10236
10365
  if (staged) {
10237
- const { stdout } = await execa8("git", ["diff", "--cached", "--name-status"], { cwd: root });
10366
+ const { stdout } = await execa9("git", ["diff", "--cached", "--name-status"], { cwd: root });
10238
10367
  ({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
10239
10368
  if (messageFile) {
10240
- const { readFileSync: readFileSync32, existsSync: existsSync43 } = await import("fs");
10241
- if (existsSync43(messageFile)) commitMessage = readFileSync32(messageFile, "utf8");
10369
+ const { readFileSync: readFileSync37, existsSync: existsSync44 } = await import("fs");
10370
+ if (existsSync44(messageFile)) commitMessage = readFileSync37(messageFile, "utf8");
10242
10371
  }
10243
10372
  } else {
10244
10373
  const base = process.env["GITHUB_BASE_REF"] ?? args[0];
@@ -10246,18 +10375,18 @@ async function runOwnershipCheck(argv) {
10246
10375
  console.error("No base ref: set GITHUB_BASE_REF or pass a base branch as the first argument.");
10247
10376
  process.exit(2);
10248
10377
  }
10249
- await execa8("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
10250
- const { stdout } = await execa8("git", ["diff", "--name-status", `origin/${base}...HEAD`], {
10378
+ await execa9("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
10379
+ const { stdout } = await execa9("git", ["diff", "--name-status", `origin/${base}...HEAD`], {
10251
10380
  cwd: root
10252
10381
  });
10253
10382
  ({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
10254
- const { stdout: log2 } = await execa8("git", ["log", "--format=%B", `origin/${base}..HEAD`], {
10383
+ const { stdout: log2 } = await execa9("git", ["log", "--format=%B", `origin/${base}..HEAD`], {
10255
10384
  cwd: root,
10256
10385
  reject: false
10257
10386
  });
10258
10387
  commitMessage = log2;
10259
10388
  }
10260
- const { stdout: gitBranch } = await execa8("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
10389
+ const { stdout: gitBranch } = await execa9("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
10261
10390
  cwd: root,
10262
10391
  reject: false
10263
10392
  });
@@ -10339,11 +10468,11 @@ ${BOLD}If the divergence is deliberate${OFF}
10339
10468
  }
10340
10469
 
10341
10470
  // src/scripts/check-eventbridge-log-permissions.ts
10342
- import { execa as execa9 } from "execa";
10471
+ import { execa as execa10 } from "execa";
10343
10472
 
10344
10473
  // src/lib/eventbridge-log-permission-guard.ts
10345
- import { readFileSync as readFileSync28, readdirSync as readdirSync15, statSync as statSync8 } from "fs";
10346
- import { join as join38 } from "path";
10474
+ import { readFileSync as readFileSync29, readdirSync as readdirSync16, statSync as statSync9 } from "fs";
10475
+ import { join as join39 } from "path";
10347
10476
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".terraform", ".worktrees", "dist"]);
10348
10477
  var EVENT_TARGET_TYPE = "aws_cloudwatch_event_target";
10349
10478
  var LOG_RESOURCE_POLICY_TYPE = "aws_cloudwatch_log_resource_policy";
@@ -10412,30 +10541,30 @@ function countRawResourceDeclarations(text, resourceType) {
10412
10541
  }
10413
10542
  function walkTerraformFiles(root) {
10414
10543
  const out = [];
10415
- const walk = (dir) => {
10544
+ const walk2 = (dir) => {
10416
10545
  let entries;
10417
10546
  try {
10418
- entries = readdirSync15(dir);
10547
+ entries = readdirSync16(dir);
10419
10548
  } catch {
10420
10549
  return;
10421
10550
  }
10422
10551
  for (const entry of entries) {
10423
- const p = join38(dir, entry);
10552
+ const p = join39(dir, entry);
10424
10553
  let st;
10425
10554
  try {
10426
- st = statSync8(p);
10555
+ st = statSync9(p);
10427
10556
  } catch {
10428
10557
  continue;
10429
10558
  }
10430
10559
  if (st.isDirectory()) {
10431
10560
  if (SKIP_DIRS.has(entry)) continue;
10432
- walk(p);
10561
+ walk2(p);
10433
10562
  continue;
10434
10563
  }
10435
10564
  if (entry.endsWith(".tf")) out.push(p);
10436
10565
  }
10437
10566
  };
10438
- walk(root);
10567
+ walk2(root);
10439
10568
  return out.sort();
10440
10569
  }
10441
10570
  function resolveEventTargetLogGroup(body) {
@@ -10463,7 +10592,7 @@ function auditEventBridgeLogPermissions(root) {
10463
10592
  let rawEventTargetCount = 0;
10464
10593
  let rawLogPolicyCount = 0;
10465
10594
  for (const file of files) {
10466
- const text = readFileSync28(file, "utf8");
10595
+ const text = readFileSync29(file, "utf8");
10467
10596
  rawEventTargetCount += countRawResourceDeclarations(text, EVENT_TARGET_TYPE);
10468
10597
  rawLogPolicyCount += countRawResourceDeclarations(text, LOG_RESOURCE_POLICY_TYPE);
10469
10598
  eventTargetBlocks.push(...findResourceBlocks(text, file, EVENT_TARGET_TYPE));
@@ -10516,7 +10645,7 @@ function auditEventBridgeLogPermissions(root) {
10516
10645
 
10517
10646
  // src/scripts/check-eventbridge-log-permissions.ts
10518
10647
  async function runEventBridgeLogPermissionCheck() {
10519
- const root = (await execa9("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
10648
+ const root = (await execa10("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
10520
10649
  let report;
10521
10650
  try {
10522
10651
  report = auditEventBridgeLogPermissions(root);
@@ -10552,35 +10681,338 @@ async function runEventBridgeLogPermissionCheck() {
10552
10681
  console.log(`\u2713 EventBridge log permission guard: ${report.summary}`);
10553
10682
  }
10554
10683
 
10684
+ // src/scripts/check-lambda-output.ts
10685
+ import { execa as execa11 } from "execa";
10686
+
10687
+ // src/lib/lambda-output-guard.ts
10688
+ import { readFileSync as readFileSync31 } from "fs";
10689
+ import { join as join41 } from "path";
10690
+
10691
+ // src/lib/terraform-input-guard.ts
10692
+ import { readdirSync as readdirSync17, readFileSync as readFileSync30, statSync as statSync10 } from "fs";
10693
+ import { join as join40 } from "path";
10694
+ var GUARDED_SUBCOMMANDS = [
10695
+ "init",
10696
+ "plan",
10697
+ "apply",
10698
+ "destroy",
10699
+ "import",
10700
+ "refresh"
10701
+ ];
10702
+ var IGNORED_SUBCOMMANDS = ["output", "state", "fmt", "validate", "version", "workspace", "show"];
10703
+ function stripComments2(source) {
10704
+ return source.split("\n").map((line) => line.replace(/(^|\s)#.*$/, "$1")).join("\n");
10705
+ }
10706
+ function findWorkflowFiles(repoRoot) {
10707
+ const found = [];
10708
+ const walk2 = (dir, relative8) => {
10709
+ let entries;
10710
+ try {
10711
+ entries = readdirSync17(dir);
10712
+ } catch {
10713
+ return;
10714
+ }
10715
+ for (const entry of entries) {
10716
+ if (entry === "node_modules" || entry === ".git" || entry === ".worktrees") continue;
10717
+ const full = join40(dir, entry);
10718
+ const rel = relative8 ? `${relative8}/${entry}` : entry;
10719
+ if (statSync10(full).isDirectory()) {
10720
+ walk2(full, rel);
10721
+ } else if (/\.ya?ml$/.test(entry) && relative8.endsWith(".github/workflows")) {
10722
+ found.push(rel);
10723
+ }
10724
+ }
10725
+ };
10726
+ walk2(repoRoot, "");
10727
+ return found.sort();
10728
+ }
10729
+ function checkWorkflowSource(file, rawSource) {
10730
+ const violations = [];
10731
+ const source = stripComments2(rawSource);
10732
+ const lines = source.split("\n");
10733
+ let sawGuardedInvocation = false;
10734
+ lines.forEach((line, index) => {
10735
+ const match = /(?:^|[\s;&|(])terraform\s+([a-z-]+)/.exec(line);
10736
+ if (!match) return;
10737
+ const subcommand = match[1];
10738
+ if (subcommand === void 0) return;
10739
+ if (IGNORED_SUBCOMMANDS.includes(subcommand)) return;
10740
+ if (!GUARDED_SUBCOMMANDS.includes(subcommand)) return;
10741
+ sawGuardedInvocation = true;
10742
+ if (!/-input=false/.test(line)) {
10743
+ violations.push({
10744
+ file,
10745
+ line: index + 1,
10746
+ message: `terraform ${subcommand} is missing -input=false. Without it Terraform prompts on stdin for any unresolved variable and blocks forever in CI (issue #322). Note -auto-approve does not suppress variable prompts.`
10747
+ });
10748
+ }
10749
+ });
10750
+ if (sawGuardedInvocation && !/^\s*TF_INPUT\s*:/m.test(source)) {
10751
+ violations.push({
10752
+ file,
10753
+ line: 1,
10754
+ message: `workflow runs Terraform but does not set TF_INPUT. Add "TF_INPUT: '0'" to the workflow-level env block as a backstop (issue #322).`
10755
+ });
10756
+ }
10757
+ return violations;
10758
+ }
10759
+ function checkTerraformInput(repoRoot) {
10760
+ return findWorkflowFiles(repoRoot).flatMap(
10761
+ (file) => checkWorkflowSource(file, readFileSync30(join40(repoRoot, file), "utf8"))
10762
+ );
10763
+ }
10764
+
10765
+ // src/lib/lambda-output-guard.ts
10766
+ var GUARDED_LAMBDA_SUBCOMMANDS = [
10767
+ "update-function-code",
10768
+ "update-function-configuration"
10769
+ ];
10770
+ function joinContinuations(source) {
10771
+ const rawLines = source.split("\n");
10772
+ const logical = [];
10773
+ let buffer = "";
10774
+ let start = 0;
10775
+ rawLines.forEach((raw, index) => {
10776
+ const lineNo = index + 1;
10777
+ const trimmedEnd = raw.replace(/\s+$/, "");
10778
+ const continues = trimmedEnd.endsWith("\\");
10779
+ const piece = continues ? trimmedEnd.slice(0, -1) : trimmedEnd;
10780
+ if (buffer === "") start = lineNo;
10781
+ buffer = buffer === "" ? piece : `${buffer} ${piece.replace(/^\s+/, "")}`;
10782
+ if (!continues) {
10783
+ logical.push({ text: buffer, startLine: start });
10784
+ buffer = "";
10785
+ }
10786
+ });
10787
+ if (buffer !== "") logical.push({ text: buffer, startLine: start });
10788
+ return logical;
10789
+ }
10790
+ function isOutputSuppressed(command) {
10791
+ if (/>\s*\/dev\/null/.test(command)) return true;
10792
+ const hasTextOutput = /--output\s+text\b/.test(command);
10793
+ const queryMatch = /--query\s+(['"]?)([^\s'"]+)\1/.exec(command);
10794
+ if (hasTextOutput && queryMatch) {
10795
+ const projection = queryMatch[2] ?? "";
10796
+ if (!/environment/i.test(projection)) return true;
10797
+ }
10798
+ return false;
10799
+ }
10800
+ function checkWorkflowSource2(file, rawSource) {
10801
+ const violations = [];
10802
+ const source = stripComments2(rawSource);
10803
+ for (const { text, startLine } of joinContinuations(source)) {
10804
+ for (const subcommand of GUARDED_LAMBDA_SUBCOMMANDS) {
10805
+ const invocation = new RegExp(`(?:^|[\\s;&|(])aws\\s+lambda\\s+${subcommand}\\b`);
10806
+ if (!invocation.test(text)) continue;
10807
+ if (isOutputSuppressed(text)) continue;
10808
+ violations.push({
10809
+ file,
10810
+ line: startLine,
10811
+ message: `aws lambda ${subcommand} does not suppress its output. Its default is the full function configuration as JSON \u2014 Environment.Variables in plaintext \u2014 which leaks DB credentials into the Actions log (#334). Add --output text --query 'LastUpdateStatus' (or redirect to /dev/null).`
10812
+ });
10813
+ }
10814
+ }
10815
+ return violations;
10816
+ }
10817
+ function checkLambdaOutput(repoRoot) {
10818
+ return findWorkflowFiles(repoRoot).flatMap(
10819
+ (file) => checkWorkflowSource2(file, readFileSync31(join41(repoRoot, file), "utf8"))
10820
+ );
10821
+ }
10822
+
10823
+ // src/scripts/check-lambda-output.ts
10824
+ async function runLambdaOutputCheck() {
10825
+ const root = (await execa11("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
10826
+ const files = findWorkflowFiles(root);
10827
+ console.log(`audited ${files.length} workflow file(s) under ${root}`);
10828
+ if (files.length === 0) {
10829
+ console.error(
10830
+ "\u2717 Lambda output guard: found 0 workflow files \u2014 this looks like a broken scan, not a clean repo. Refusing to report success over zero input."
10831
+ );
10832
+ process.exit(1);
10833
+ }
10834
+ const violations = checkLambdaOutput(root);
10835
+ if (violations.length > 0) {
10836
+ console.error("\u2717 Lambda output guard: unsuppressed aws lambda update-function-* output found\n");
10837
+ for (const v of violations) {
10838
+ console.error(` ${v.file}:${v.line} ${v.message}`);
10839
+ }
10840
+ console.error("\nSee biffo-template#334.");
10841
+ process.exit(1);
10842
+ }
10843
+ console.log(`\u2713 Lambda output guard: every aws lambda update-function-* call suppresses output`);
10844
+ }
10845
+
10846
+ // src/scripts/check-pipe-trap.ts
10847
+ import { readFileSync as readFileSync32, readdirSync as readdirSync18 } from "fs";
10848
+ import { join as join42, relative as relative6 } from "path";
10849
+ import { execa as execa12 } from "execa";
10850
+
10851
+ // src/lib/pipe-trap-guard.ts
10852
+ var STATUS_BEARING = [
10853
+ // Three-valued: 0 green, 1 failed, 2 cannot tell — and 2 is never a pass.
10854
+ /\bwait-for-checks\b/,
10855
+ /\bbranch-health\b/,
10856
+ /\bclaim\.sh\b/,
10857
+ /\bbiffo\.sh\s+(claim|wait-for-checks|branch-health|verify)\b/,
10858
+ // Push rejection is how commits get silently lost (AGENTS.md §4).
10859
+ /\bgit\s+push\b/,
10860
+ /\bverify\.sh\b/
10861
+ ];
10862
+ var QUERY_FLAGS = [/--list\b/, /--help\b/, /--version\b/];
10863
+ function strip(source) {
10864
+ return source.split("\n").map((raw) => {
10865
+ let out = "";
10866
+ let quote = null;
10867
+ for (let i = 0; i < raw.length; i++) {
10868
+ const c = raw[i];
10869
+ if (quote) {
10870
+ if (c === "\\" && quote === '"') {
10871
+ i++;
10872
+ continue;
10873
+ }
10874
+ if (c === quote) {
10875
+ quote = null;
10876
+ continue;
10877
+ }
10878
+ if (c === "$" && quote === '"') {
10879
+ out += c + (raw[i + 1] ?? "");
10880
+ i++;
10881
+ }
10882
+ continue;
10883
+ }
10884
+ if (c === '"' || c === "'") {
10885
+ quote = c;
10886
+ continue;
10887
+ }
10888
+ if (c === "\\") {
10889
+ i++;
10890
+ continue;
10891
+ }
10892
+ if (c === "#" && (out === "" || /\s/.test(out[out.length - 1] ?? ""))) break;
10893
+ out += c;
10894
+ }
10895
+ return out;
10896
+ });
10897
+ }
10898
+ function isPipeline(line) {
10899
+ for (let i = 0; i < line.length; i++) {
10900
+ if (line[i] !== "|") continue;
10901
+ if (line[i + 1] === "|") {
10902
+ i++;
10903
+ continue;
10904
+ }
10905
+ if (line[i - 1] === "|") continue;
10906
+ return true;
10907
+ }
10908
+ return false;
10909
+ }
10910
+ function findPipeTraps(source) {
10911
+ const lines = strip(source);
10912
+ const traps = [];
10913
+ let lastWasPipeline = false;
10914
+ lines.forEach((line, index) => {
10915
+ const trimmed = line.trim();
10916
+ if (trimmed === "") return;
10917
+ if (isPipeline(line)) {
10918
+ const head = line.split("|")[0] ?? "";
10919
+ const matched = STATUS_BEARING.find((p) => p.test(head));
10920
+ const isQuery = QUERY_FLAGS.some((p) => p.test(head));
10921
+ if (matched && !isQuery) {
10922
+ traps.push({
10923
+ line: index + 1,
10924
+ text: trimmed,
10925
+ reason: `a status-bearing command upstream of a pipe \u2014 the pipeline reports the LAST command's status, so this can only ever read 0`
10926
+ });
10927
+ }
10928
+ }
10929
+ if (lastWasPipeline && /\$\?/.test(line)) {
10930
+ traps.push({
10931
+ line: index + 1,
10932
+ text: trimmed,
10933
+ reason: `$? read directly after a pipeline \u2014 that is the LAST command's status, not the one you meant. Use \${PIPESTATUS[0]}, or drop the pipe`
10934
+ });
10935
+ }
10936
+ lastWasPipeline = isPipeline(line);
10937
+ });
10938
+ return traps;
10939
+ }
10940
+
10941
+ // src/scripts/check-pipe-trap.ts
10942
+ function shellFiles(root) {
10943
+ const out = [];
10944
+ for (const dir of ["scripts", ".githooks"]) {
10945
+ const full = join42(root, dir);
10946
+ let entries;
10947
+ try {
10948
+ entries = readdirSync18(full, { withFileTypes: true });
10949
+ } catch {
10950
+ continue;
10951
+ }
10952
+ for (const entry of entries) {
10953
+ if (!entry.isFile()) continue;
10954
+ if (dir === "scripts" && !entry.name.endsWith(".sh")) continue;
10955
+ out.push(join42(full, entry.name));
10956
+ }
10957
+ }
10958
+ return out;
10959
+ }
10960
+ async function runPipeTrapCheck() {
10961
+ const root = (await execa12("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
10962
+ const files = shellFiles(root);
10963
+ console.log(`audited ${files.length} shell file(s) under scripts/ and .githooks/ under ${root}`);
10964
+ if (files.length === 0) {
10965
+ console.error(
10966
+ "\u2717 Pipe-trap guard: found 0 shell files under scripts/ or .githooks/ \u2014 this looks like a broken scan, not a clean repo. Refusing to report success over zero input."
10967
+ );
10968
+ process.exit(1);
10969
+ }
10970
+ const findings = files.flatMap(
10971
+ (file) => findPipeTraps(readFileSync32(file, "utf8")).map(
10972
+ (t) => `${relative6(root, file)}:${t.line} ${t.text}
10973
+ ${t.reason}`
10974
+ )
10975
+ );
10976
+ if (findings.length > 0) {
10977
+ console.error("\u2717 Pipe-trap guard: status-bearing pipeline(s) found\n");
10978
+ for (const f of findings) {
10979
+ console.error(` ${f}`);
10980
+ }
10981
+ console.error("\nSee biffo-template#1231 (AGENTS.md \xA74).");
10982
+ process.exit(1);
10983
+ }
10984
+ console.log(`\u2713 Pipe-trap guard: no status-bearing command is piped away`);
10985
+ }
10986
+
10555
10987
  // src/scripts/check-plugin-collisions.ts
10556
10988
  import { existsSync as existsSync37 } from "fs";
10557
- import { join as join40 } from "path";
10558
- import { execa as execa10 } from "execa";
10989
+ import { join as join44 } from "path";
10990
+ import { execa as execa13 } from "execa";
10559
10991
 
10560
10992
  // src/lib/plugin-collision-guard.ts
10561
- import { existsSync as existsSync36, readdirSync as readdirSync16, statSync as statSync9 } from "fs";
10562
- import { join as join39 } from "path";
10993
+ import { existsSync as existsSync36, readdirSync as readdirSync19, statSync as statSync11 } from "fs";
10994
+ import { join as join43 } from "path";
10563
10995
  var PYTEST_SPECIAL = /* @__PURE__ */ new Set(["conftest.py"]);
10564
10996
  var IGNORED_DIRS = /* @__PURE__ */ new Set([".venv", "node_modules", "__pycache__", ".git", "dist", "build"]);
10565
10997
  function subdirectories(dir) {
10566
10998
  if (!existsSync36(dir)) return [];
10567
- return readdirSync16(dir).filter((entry) => {
10999
+ return readdirSync19(dir).filter((entry) => {
10568
11000
  if (IGNORED_DIRS.has(entry) || entry.startsWith(".")) return false;
10569
11001
  try {
10570
- return statSync9(join39(dir, entry)).isDirectory();
11002
+ return statSync11(join43(dir, entry)).isDirectory();
10571
11003
  } catch {
10572
11004
  return false;
10573
11005
  }
10574
11006
  });
10575
11007
  }
10576
11008
  function regularPackagesOf(pluginDir2) {
10577
- return subdirectories(pluginDir2).filter((name) => existsSync36(join39(pluginDir2, name, "__init__.py"))).sort();
11009
+ return subdirectories(pluginDir2).filter((name) => existsSync36(join43(pluginDir2, name, "__init__.py"))).sort();
10578
11010
  }
10579
11011
  function bareTestModulesOf(pluginDir2) {
10580
- const testsDir = join39(pluginDir2, "tests");
11012
+ const testsDir = join43(pluginDir2, "tests");
10581
11013
  if (!existsSync36(testsDir)) return [];
10582
- if (existsSync36(join39(testsDir, "__init__.py"))) return [];
10583
- return readdirSync16(testsDir).filter((f) => f.endsWith(".py") && !PYTEST_SPECIAL.has(f)).sort();
11014
+ if (existsSync36(join43(testsDir, "__init__.py"))) return [];
11015
+ return readdirSync19(testsDir).filter((f) => f.endsWith(".py") && !PYTEST_SPECIAL.has(f)).sort();
10584
11016
  }
10585
11017
  function findCollisions(servicesDir, pluginDirs) {
10586
11018
  const plugins = (pluginDirs ?? subdirectories(servicesDir)).filter((name) => !name.startsWith("_")).filter((name) => name !== "api").sort();
@@ -10588,7 +11020,7 @@ function findCollisions(servicesDir, pluginDirs) {
10588
11020
  const gather = (kind, namesOf) => {
10589
11021
  const claims = /* @__PURE__ */ new Map();
10590
11022
  for (const plugin of plugins) {
10591
- for (const name of namesOf(join39(servicesDir, plugin))) {
11023
+ for (const name of namesOf(join43(servicesDir, plugin))) {
10592
11024
  claims.set(name, [...claims.get(name) ?? [], plugin]);
10593
11025
  }
10594
11026
  }
@@ -10625,8 +11057,8 @@ function formatCollisions(collisions) {
10625
11057
 
10626
11058
  // src/scripts/check-plugin-collisions.ts
10627
11059
  async function runPluginCollisionCheck() {
10628
- const root = (await execa10("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
10629
- const servicesDir = join40(root, "services");
11060
+ const root = (await execa13("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11061
+ const servicesDir = join44(root, "services");
10630
11062
  if (!existsSync37(servicesDir)) {
10631
11063
  console.log("\u2713 plugin collision guard: no services/ directory \u2014 nothing to compare");
10632
11064
  return;
@@ -10644,38 +11076,38 @@ async function runPluginCollisionCheck() {
10644
11076
  }
10645
11077
 
10646
11078
  // src/scripts/check-plugin-terraform.ts
10647
- import { execa as execa11 } from "execa";
11079
+ import { execa as execa14 } from "execa";
10648
11080
 
10649
11081
  // src/lib/plugin-terraform-guard.ts
10650
- import { existsSync as existsSync38, readFileSync as readFileSync29, readdirSync as readdirSync17 } from "fs";
10651
- import { dirname as dirname9, join as join41, relative as relative6, sep as sep3 } from "path";
11082
+ import { existsSync as existsSync38, readFileSync as readFileSync33, readdirSync as readdirSync20 } from "fs";
11083
+ import { dirname as dirname9, join as join45, relative as relative7, sep as sep3 } from "path";
10652
11084
  var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", ".worktrees", "dist", ".venv", "__pycache__"]);
10653
11085
  var PLUGIN_MANIFEST_FILE2 = "biffo.plugin.json";
10654
11086
  function findPluginManifests(root) {
10655
11087
  const found = [];
10656
- const walk = (dir) => {
11088
+ const walk2 = (dir) => {
10657
11089
  let entries;
10658
11090
  try {
10659
- entries = readdirSync17(dir, { withFileTypes: true });
11091
+ entries = readdirSync20(dir, { withFileTypes: true });
10660
11092
  } catch {
10661
11093
  return;
10662
11094
  }
10663
11095
  for (const entry of entries) {
10664
11096
  if (entry.isDirectory()) {
10665
11097
  if (SKIP_DIRS2.has(entry.name)) continue;
10666
- walk(join41(dir, entry.name));
11098
+ walk2(join45(dir, entry.name));
10667
11099
  } else if (entry.isFile() && entry.name === PLUGIN_MANIFEST_FILE2) {
10668
- found.push(relative6(root, join41(dir, entry.name)).split(sep3).join("/"));
11100
+ found.push(relative7(root, join45(dir, entry.name)).split(sep3).join("/"));
10669
11101
  }
10670
11102
  }
10671
11103
  };
10672
- walk(root);
11104
+ walk2(root);
10673
11105
  return found.sort();
10674
11106
  }
10675
11107
  function readSubscriptions(absManifestPath) {
10676
11108
  let parsed;
10677
11109
  try {
10678
- parsed = JSON.parse(readFileSync29(absManifestPath, "utf8"));
11110
+ parsed = JSON.parse(readFileSync33(absManifestPath, "utf8"));
10679
11111
  } catch {
10680
11112
  return null;
10681
11113
  }
@@ -10690,15 +11122,15 @@ function readSubscriptions(absManifestPath) {
10690
11122
  }
10691
11123
  function checkPluginTerraform(root) {
10692
11124
  const violations = [];
10693
- const coreManifest = existsSync38(join41(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
11125
+ const coreManifest = existsSync38(join45(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
10694
11126
  for (const manifest of findPluginManifests(root)) {
10695
11127
  if (coreManifest && !isTemplateOwned(manifest, coreManifest)) continue;
10696
- const absManifest = join41(root, manifest);
11128
+ const absManifest = join45(root, manifest);
10697
11129
  const subscriptions = readSubscriptions(absManifest);
10698
11130
  if (subscriptions === null) continue;
10699
11131
  const pluginDir2 = dirname9(absManifest);
10700
- if (existsSync38(join41(pluginDir2, "terraform"))) continue;
10701
- const relPluginDir = relative6(root, pluginDir2).split(sep3).join("/");
11132
+ if (existsSync38(join45(pluginDir2, "terraform"))) continue;
11133
+ const relPluginDir = relative7(root, pluginDir2).split(sep3).join("/");
10702
11134
  violations.push({
10703
11135
  manifest,
10704
11136
  expectedTerraformDir: relPluginDir ? `${relPluginDir}/terraform` : "terraform",
@@ -10717,7 +11149,7 @@ function formatViolations(violations) {
10717
11149
 
10718
11150
  // src/scripts/check-plugin-terraform.ts
10719
11151
  async function runPluginTerraformCheck() {
10720
- const root = (await execa11("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11152
+ const root = (await execa14("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
10721
11153
  const violations = checkPluginTerraform(root);
10722
11154
  if (violations.length > 0) {
10723
11155
  console.error("\u2717 plugin Terraform guard: event subscriptions with no infrastructure\n");
@@ -10729,12 +11161,12 @@ async function runPluginTerraformCheck() {
10729
11161
 
10730
11162
  // src/scripts/check-plugin-tool-supply.ts
10731
11163
  import { existsSync as existsSync40 } from "fs";
10732
- import { join as join43 } from "path";
10733
- import { execa as execa12 } from "execa";
11164
+ import { join as join47 } from "path";
11165
+ import { execa as execa15 } from "execa";
10734
11166
 
10735
11167
  // src/lib/plugin-tool-supply-audit.ts
10736
- import { existsSync as existsSync39, readFileSync as readFileSync30, readdirSync as readdirSync18, statSync as statSync10 } from "fs";
10737
- import { join as join42 } from "path";
11168
+ import { existsSync as existsSync39, readFileSync as readFileSync34, readdirSync as readdirSync21, statSync as statSync12 } from "fs";
11169
+ import { join as join46 } from "path";
10738
11170
 
10739
11171
  // src/lib/openrouter-model-snapshot.ts
10740
11172
  var OPENROUTER_MODEL_SNAPSHOT_FETCHED_AT = "2026-08-10T06:39:01Z";
@@ -11145,13 +11577,13 @@ var OPENROUTER_MODEL_IDS = [
11145
11577
  function listDirs(root) {
11146
11578
  let entries;
11147
11579
  try {
11148
- entries = readdirSync18(root);
11580
+ entries = readdirSync21(root);
11149
11581
  } catch {
11150
11582
  return [];
11151
11583
  }
11152
11584
  return entries.filter((e) => {
11153
11585
  try {
11154
- return statSync10(join42(root, e)).isDirectory();
11586
+ return statSync12(join46(root, e)).isDirectory();
11155
11587
  } catch {
11156
11588
  return false;
11157
11589
  }
@@ -11159,30 +11591,30 @@ function listDirs(root) {
11159
11591
  }
11160
11592
  function walkFiles2(root, accept, skipDir) {
11161
11593
  const out = [];
11162
- const walk = (dir) => {
11594
+ const walk2 = (dir) => {
11163
11595
  let entries;
11164
11596
  try {
11165
- entries = readdirSync18(dir);
11597
+ entries = readdirSync21(dir);
11166
11598
  } catch {
11167
11599
  return;
11168
11600
  }
11169
11601
  for (const entry of entries) {
11170
- const p = join42(dir, entry);
11602
+ const p = join46(dir, entry);
11171
11603
  let st;
11172
11604
  try {
11173
- st = statSync10(p);
11605
+ st = statSync12(p);
11174
11606
  } catch {
11175
11607
  continue;
11176
11608
  }
11177
11609
  if (st.isDirectory()) {
11178
11610
  if (skipDir(entry)) continue;
11179
- walk(p);
11611
+ walk2(p);
11180
11612
  continue;
11181
11613
  }
11182
11614
  if (accept(entry)) out.push(p);
11183
11615
  }
11184
11616
  };
11185
- walk(root);
11617
+ walk2(root);
11186
11618
  return out.sort();
11187
11619
  }
11188
11620
  function pluginPythonFiles(pluginDir2) {
@@ -11193,14 +11625,14 @@ function pluginPythonFiles(pluginDir2) {
11193
11625
  );
11194
11626
  }
11195
11627
  function pluginTerraformFiles(pluginDir2) {
11196
- const tfDir = join42(pluginDir2, "terraform");
11628
+ const tfDir = join46(pluginDir2, "terraform");
11197
11629
  let entries;
11198
11630
  try {
11199
- entries = readdirSync18(tfDir);
11631
+ entries = readdirSync21(tfDir);
11200
11632
  } catch {
11201
11633
  return [];
11202
11634
  }
11203
- return entries.filter((e) => e.endsWith(".tf")).map((e) => join42(tfDir, e)).sort();
11635
+ return entries.filter((e) => e.endsWith(".tf")).map((e) => join46(tfDir, e)).sort();
11204
11636
  }
11205
11637
  function extractManifestTools(manifestText) {
11206
11638
  let parsed;
@@ -11452,8 +11884,8 @@ function isSnapshotStale(fetchedAt, now) {
11452
11884
  function normalizeModelId(id) {
11453
11885
  return id.endsWith(":online") ? id.slice(0, -":online".length) : id;
11454
11886
  }
11455
- var CONFIG_PY_PATH = join42("services", "api", "src", "api", "config.py");
11456
- var ORCHESTRATION_SCHEMA_PATH = join42(
11887
+ var CONFIG_PY_PATH = join46("services", "api", "src", "api", "config.py");
11888
+ var ORCHESTRATION_SCHEMA_PATH = join46(
11457
11889
  "services",
11458
11890
  "api",
11459
11891
  "src",
@@ -11465,8 +11897,8 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
11465
11897
  const knownModelIds = options.knownModelIds ?? OPENROUTER_MODEL_IDS;
11466
11898
  const snapshotFetchedAt = options.snapshotFetchedAt ?? OPENROUTER_MODEL_SNAPSHOT_FETCHED_AT;
11467
11899
  const now = options.now ?? /* @__PURE__ */ new Date();
11468
- const configPath = join42(repoRoot, CONFIG_PY_PATH);
11469
- const orchestrationPath = join42(repoRoot, ORCHESTRATION_SCHEMA_PATH);
11900
+ const configPath = join46(repoRoot, CONFIG_PY_PATH);
11901
+ const orchestrationPath = join46(repoRoot, ORCHESTRATION_SCHEMA_PATH);
11470
11902
  const configMissing = !existsSync39(configPath);
11471
11903
  const orchestrationSchemaMissing = !existsSync39(orchestrationPath);
11472
11904
  const knownSet = new Set(knownModelIds);
@@ -11486,13 +11918,13 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
11486
11918
  };
11487
11919
  let settingsBlind = false;
11488
11920
  if (!configMissing) {
11489
- const settingsFields = extractSettingsModelFields(readFileSync30(configPath, "utf8"));
11921
+ const settingsFields = extractSettingsModelFields(readFileSync34(configPath, "utf8"));
11490
11922
  if (settingsFields.length === 0) settingsBlind = true;
11491
11923
  for (const { field, value } of settingsFields) record(`${CONFIG_PY_PATH}#${field}`, value);
11492
11924
  }
11493
11925
  let curatedFieldsBlind = false;
11494
11926
  if (!orchestrationSchemaMissing) {
11495
- const curated = extractCuratedModelFields(readFileSync30(orchestrationPath, "utf8"));
11927
+ const curated = extractCuratedModelFields(readFileSync34(orchestrationPath, "utf8"));
11496
11928
  if (curated.rawFieldCount > 0 && curated.fields.every((f) => f.defaultValue === null && f.optionValues.length === 0)) {
11497
11929
  curatedFieldsBlind = true;
11498
11930
  }
@@ -11539,7 +11971,7 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
11539
11971
  function discoverPluginDirs(pluginsRoot) {
11540
11972
  return listDirs(pluginsRoot).filter((name) => {
11541
11973
  try {
11542
- return statSync10(join42(pluginsRoot, name, "biffo.plugin.json")).isFile();
11974
+ return statSync12(join46(pluginsRoot, name, "biffo.plugin.json")).isFile();
11543
11975
  } catch {
11544
11976
  return false;
11545
11977
  }
@@ -11552,8 +11984,8 @@ function auditPluginToolSupply(pluginsRoot) {
11552
11984
  let terraformBlind = false;
11553
11985
  let totalDeclaredTools = 0;
11554
11986
  for (const name of pluginNames) {
11555
- const pluginDir2 = join42(pluginsRoot, name);
11556
- const manifestText = readFileSync30(join42(pluginDir2, "biffo.plugin.json"), "utf8");
11987
+ const pluginDir2 = join46(pluginsRoot, name);
11988
+ const manifestText = readFileSync34(join46(pluginDir2, "biffo.plugin.json"), "utf8");
11557
11989
  const manifest = extractManifestTools(manifestText);
11558
11990
  if (manifest.parseError) {
11559
11991
  findings.push({
@@ -11571,13 +12003,13 @@ function auditPluginToolSupply(pluginsRoot) {
11571
12003
  totalDeclaredTools += manifest.tools.length;
11572
12004
  const pySources = pluginPythonFiles(pluginDir2).map((f) => ({
11573
12005
  file: f,
11574
- text: readFileSync30(f, "utf8")
12006
+ text: readFileSync34(f, "utf8")
11575
12007
  }));
11576
12008
  const resolver = buildSymbolResolver(pySources);
11577
12009
  const registry = extractToolRegistryEntries(pySources, resolver);
11578
12010
  if (registry.rawToolDefinitionCount > 0 && registry.entries.length === 0) registryBlind = true;
11579
12011
  const tfFiles = pluginTerraformFiles(pluginDir2);
11580
- const tfText = tfFiles.map((f) => readFileSync30(f, "utf8")).join("\n");
12012
+ const tfText = tfFiles.map((f) => readFileSync34(f, "utf8")).join("\n");
11581
12013
  const terraform = extractTerraformEnvKeys(tfText);
11582
12014
  if (terraform.rawMarkerCount > 0 && terraform.resolvedBlockCount === 0) terraformBlind = true;
11583
12015
  for (const toolName of manifest.tools) {
@@ -11651,7 +12083,7 @@ function auditPluginToolSupply(pluginsRoot) {
11651
12083
  requiredEnvVars: envResult.envVars,
11652
12084
  missingEnvVars: anyWired ? [] : envResult.envVars,
11653
12085
  status: anyWired ? "ok" : "missing-env",
11654
- 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 ${join42(pluginDir2, "terraform")}, so this deployment can never supply it`
12086
+ 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 ${join46(pluginDir2, "terraform")}, so this deployment can never supply it`
11655
12087
  });
11656
12088
  }
11657
12089
  }
@@ -11682,9 +12114,9 @@ function auditPluginToolSupply(pluginsRoot) {
11682
12114
 
11683
12115
  // src/scripts/check-plugin-tool-supply.ts
11684
12116
  async function runPluginToolSupplyCheck() {
11685
- const root = (await execa12("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12117
+ const root = (await execa15("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11686
12118
  let allOk = true;
11687
- const pluginsRoot = join43(root, "services", "_plugins");
12119
+ const pluginsRoot = join47(root, "services", "_plugins");
11688
12120
  if (!existsSync40(pluginsRoot)) {
11689
12121
  console.log("\u2713 plugin tool-supply guard: no services/_plugins/ \u2014 nothing to audit");
11690
12122
  } else {
@@ -11715,7 +12147,7 @@ async function runPluginToolSupplyCheck() {
11715
12147
  console.log(`\u2713 plugin tool-supply guard: ${report.summary}`);
11716
12148
  }
11717
12149
  }
11718
- const servicesApiRoot = join43(root, "services", "api");
12150
+ const servicesApiRoot = join47(root, "services", "api");
11719
12151
  if (!existsSync40(servicesApiRoot)) {
11720
12152
  console.log("\u2713 plugin model-id guard: no services/api/ \u2014 nothing to audit");
11721
12153
  } else {
@@ -11762,7 +12194,7 @@ async function runPluginToolSupplyCheck() {
11762
12194
  }
11763
12195
 
11764
12196
  // src/scripts/check-release-subject.ts
11765
- import { execa as execa13 } from "execa";
12197
+ import { execa as execa16 } from "execa";
11766
12198
 
11767
12199
  // src/lib/release-version.ts
11768
12200
  var MINOR_TYPES = /* @__PURE__ */ new Set(["feat"]);
@@ -11799,7 +12231,7 @@ async function fetchPrTitleViaGh({
11799
12231
  PR_NUMBER,
11800
12232
  GH_REPO
11801
12233
  }) {
11802
- const { stdout } = await execa13(
12234
+ const { stdout } = await execa16(
11803
12235
  "gh",
11804
12236
  ["pr", "view", PR_NUMBER, "--repo", GH_REPO, "--json", "title", "--jq", ".title"],
11805
12237
  { env: { ...process.env, GH_TOKEN } }
@@ -11835,7 +12267,7 @@ async function resolveReleaseSubject({
11835
12267
  );
11836
12268
  }
11837
12269
  }
11838
- return (await execa13("git", ["log", "-1", "--format=%s"], { cwd })).stdout.trim();
12270
+ return (await execa16("git", ["log", "-1", "--format=%s"], { cwd })).stdout.trim();
11839
12271
  }
11840
12272
  async function runReleaseSubjectCheck(argv) {
11841
12273
  const base = process.env["GITHUB_BASE_REF"] ?? argv[0];
@@ -11843,9 +12275,9 @@ async function runReleaseSubjectCheck(argv) {
11843
12275
  console.error("No base ref: set GITHUB_BASE_REF or pass a base branch as the first argument.");
11844
12276
  process.exit(2);
11845
12277
  }
11846
- const root = (await execa13("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11847
- await execa13("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
11848
- const { stdout } = await execa13("git", ["diff", "--name-only", `origin/${base}...HEAD`], {
12278
+ const root = (await execa16("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12279
+ await execa16("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
12280
+ const { stdout } = await execa16("git", ["diff", "--name-only", `origin/${base}...HEAD`], {
11849
12281
  cwd: root
11850
12282
  });
11851
12283
  const changedFiles = stdout.split("\n").map((s) => s.trim()).filter(Boolean);
@@ -11892,9 +12324,196 @@ async function runReleaseSubjectCheck(argv) {
11892
12324
  );
11893
12325
  }
11894
12326
 
12327
+ // src/scripts/check-skeleton-drift.ts
12328
+ import { existsSync as existsSync41, readdirSync as readdirSync23 } from "fs";
12329
+ import { join as join49 } from "path";
12330
+ import { execa as execa17 } from "execa";
12331
+
12332
+ // src/lib/skeleton-drift-guard.ts
12333
+ import { readFileSync as readFileSync35, readdirSync as readdirSync22, statSync as statSync13 } from "fs";
12334
+ import { join as join48 } from "path";
12335
+ var isWorkflow = (rel) => rel.startsWith(".github/workflows/") && (rel.endsWith(".yml") || rel.endsWith(".yaml"));
12336
+ var isRootLayout = (rel) => rel.endsWith("src/app/layout.tsx");
12337
+ var uncommented = (contents) => contents.split("\n").filter((line) => !/^\s*(\/\/|\/\*|\*)/.test(line)).join("\n");
12338
+ var executableLines = (contents) => contents.split("\n").filter((line) => !/^\s*#/.test(line));
12339
+ var UNHARDENED_AUDITS = [
12340
+ { pattern: /\b(pnpm|npm|yarn)\s+audit\b/, replacement: "sh scripts/js-dependency-audit.sh" },
12341
+ { pattern: /\bpip-audit\b/, replacement: "sh scripts/py-dependency-audit.sh" }
12342
+ ];
12343
+ var SKELETON_RULES = [
12344
+ {
12345
+ id: "runner-label",
12346
+ rationale: "A hardcoded `ubuntu-latest` bills GitHub-hosted minutes and fails immediately on an account over its spending limit, which is the state this project is in \u2014 so a generated repo cannot reach the self-hosted fleet at all (#651).",
12347
+ appliesTo: isWorkflow,
12348
+ check: (_rel, contents) => {
12349
+ const hardcoded = [...contents.matchAll(/^\s*runs-on:\s*(.+)$/gm)].map((m) => m[1].trim()).filter((v) => !v.includes("RUNNER_LABEL"));
12350
+ return hardcoded.length > 0 ? `${hardcoded.length} job(s) pin a runner directly instead of \`\${{ vars.RUNNER_LABEL || 'ubuntu-latest' }}\`: ${[...new Set(hardcoded)].join(", ")}` : null;
12351
+ }
12352
+ },
12353
+ {
12354
+ id: "no-gitleaks-action",
12355
+ rationale: "gitleaks/gitleaks-action@v2 cannot pass here for two independent reasons: its SARIF upload assumes a GitHub-hosted $HOME layout and dies on self-hosted runners, and it requires a paid licence for organization-owned repos. Every generated repo was born with a permanently red Secret Scan (#649).",
12356
+ appliesTo: isWorkflow,
12357
+ check: (_rel, contents) => /^\s*(-\s*)?uses:\s*gitleaks\/gitleaks-action/m.test(contents) ? "uses gitleaks/gitleaks-action; install the free CLI directly, as the root ci.yml does" : null
12358
+ },
12359
+ {
12360
+ id: "hardened-dependency-audit",
12361
+ rationale: "A raw `pnpm audit --audit-level=high` or `uv run pip-audit` exits non-zero identically whether it found a vulnerability or simply could not parse the registry's response, so one npm/PyPI hiccup reds a required check on every open PR at once \u2014 for an infrastructure blip with nothing to do with the code (#591). This repo hardened its own audits in #592/#636/#717/#721 and went on shipping the raw commands into every generated repo for months, so six siblings and two plugin repos were born with the original defect (#743). The wrappers parse the output first: they fail only on a genuine finding, retry a transient error, and report INCONCLUSIVE loudly rather than passing or failing blind.",
12362
+ appliesTo: isWorkflow,
12363
+ check: (_rel, contents) => {
12364
+ const offenders = executableLines(contents).filter(
12365
+ (line) => UNHARDENED_AUDITS.some((a) => a.pattern.test(line))
12366
+ );
12367
+ if (offenders.length === 0) return null;
12368
+ const wanted = [
12369
+ ...new Set(
12370
+ UNHARDENED_AUDITS.filter((a) => offenders.some((line) => a.pattern.test(line))).map(
12371
+ (a) => a.replacement
12372
+ )
12373
+ )
12374
+ ];
12375
+ return `${offenders.length} step(s) run an unhardened dependency audit directly: ${offenders.map((l) => l.trim()).join(" | ")}. Call ${wanted.join(" / ")} instead.`;
12376
+ }
12377
+ },
12378
+ {
12379
+ id: "derived-app-title",
12380
+ rationale: "A root layout that hard-codes `metadata.title` ships that literal as every generated repo's browser title, and nothing at scaffold time ever touches it. The sibling skeleton carried `title: 'Sibling App'` for its whole life, so two of tabsii's five siblings were still serving it in their deployed out/index.html \u2014 visible to users, not just in source (#963). Derive it from the build-time `NEXT_PUBLIC_SIBLING_NAME` instead (see the skeleton's own `src/lib/branding.ts`), the way the portal derives `PORTAL_TITLE` (#389).",
12381
+ appliesTo: isRootLayout,
12382
+ check: (_rel, contents) => {
12383
+ const match = /\btitle:\s*(['"`])([^'"`]*)\1/.exec(uncommented(contents));
12384
+ return match ? `metadata.title is the hard-coded literal ${match[1]}${match[2]}${match[1]}; derive it from the generated app\u2019s own name instead` : null;
12385
+ }
12386
+ }
12387
+ ];
12388
+ function walk(dir, base = dir) {
12389
+ const out = [];
12390
+ let entries;
12391
+ try {
12392
+ entries = readdirSync22(dir);
12393
+ } catch {
12394
+ return out;
12395
+ }
12396
+ for (const entry of entries) {
12397
+ if (entry === ".venv" || entry === "node_modules" || entry === ".git") continue;
12398
+ const abs = join48(dir, entry);
12399
+ let isDir;
12400
+ try {
12401
+ isDir = statSync13(abs).isDirectory();
12402
+ } catch {
12403
+ continue;
12404
+ }
12405
+ if (isDir) out.push(...walk(abs, base));
12406
+ else
12407
+ out.push(
12408
+ abs.slice(base.length + 1).split("\\").join("/")
12409
+ );
12410
+ }
12411
+ return out;
12412
+ }
12413
+ function auditSkeleton(skeletonRoot, name, rules = SKELETON_RULES) {
12414
+ const violations = [];
12415
+ for (const rel of walk(skeletonRoot)) {
12416
+ for (const rule of rules) {
12417
+ if (!rule.appliesTo(rel)) continue;
12418
+ let contents;
12419
+ try {
12420
+ contents = readFileSync35(join48(skeletonRoot, rel), "utf8");
12421
+ } catch {
12422
+ continue;
12423
+ }
12424
+ const detail = rule.check(rel, contents);
12425
+ if (detail !== null) {
12426
+ violations.push({ skeleton: name, file: rel, rule: rule.id, detail });
12427
+ }
12428
+ }
12429
+ }
12430
+ return violations;
12431
+ }
12432
+ function formatViolations2(violations) {
12433
+ const byRule = /* @__PURE__ */ new Map();
12434
+ for (const v of violations) {
12435
+ byRule.set(v.rule, [...byRule.get(v.rule) ?? [], v]);
12436
+ }
12437
+ const lines = [];
12438
+ for (const [ruleId, group] of byRule) {
12439
+ const rule = SKELETON_RULES.find((r) => r.id === ruleId);
12440
+ lines.push(` ${ruleId}:`);
12441
+ for (const v of group) lines.push(` ${v.skeleton}/${v.file} \u2014 ${v.detail}`);
12442
+ if (rule) lines.push(` why: ${rule.rationale}`);
12443
+ }
12444
+ return lines.join("\n");
12445
+ }
12446
+
12447
+ // src/scripts/check-skeleton-drift.ts
12448
+ function discoverSkeletons(root) {
12449
+ const skeletonsDir = join49(root, "_skeletons");
12450
+ let entries;
12451
+ try {
12452
+ entries = readdirSync23(skeletonsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
12453
+ } catch {
12454
+ return [];
12455
+ }
12456
+ return entries.filter((name) => existsSync41(join49(skeletonsDir, name, ".github", "workflows", "ci.yml"))).sort();
12457
+ }
12458
+ async function runSkeletonDriftCheck() {
12459
+ const root = (await execa17("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12460
+ const skeletons = discoverSkeletons(root);
12461
+ let filesConsidered = 0;
12462
+ for (const name of skeletons) {
12463
+ const skeletonRoot = join49(root, "_skeletons", name);
12464
+ filesConsidered += findWorkflowFiles(skeletonRoot).length;
12465
+ if (existsSync41(join49(skeletonRoot, "apps", "frontend", "src", "app", "layout.tsx"))) {
12466
+ filesConsidered += 1;
12467
+ }
12468
+ }
12469
+ console.log(
12470
+ `audited ${skeletons.length} skeleton(s) (${skeletons.join(", ") || "none"}), ${filesConsidered} file(s) considered, under ${root}/_skeletons`
12471
+ );
12472
+ if (skeletons.length === 0) {
12473
+ console.error(
12474
+ "\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."
12475
+ );
12476
+ process.exit(1);
12477
+ }
12478
+ const violations = skeletons.flatMap(
12479
+ (name) => auditSkeleton(join49(root, "_skeletons", name), name)
12480
+ );
12481
+ if (violations.length > 0) {
12482
+ console.error("\u2717 Skeleton-drift guard: drift found between this repo and its scaffolding\n");
12483
+ console.error(formatViolations2(violations));
12484
+ console.error("\nSee skeleton-drift-guard.ts for why each rule exists.");
12485
+ process.exit(1);
12486
+ }
12487
+ console.log(`\u2713 Skeleton-drift guard: every skeleton holds every rule`);
12488
+ }
12489
+
12490
+ // src/scripts/check-terraform-input.ts
12491
+ import { execa as execa18 } from "execa";
12492
+ async function runTerraformInputCheck() {
12493
+ const root = (await execa18("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12494
+ const files = findWorkflowFiles(root);
12495
+ console.log(`audited ${files.length} workflow file(s) under ${root}`);
12496
+ if (files.length === 0) {
12497
+ console.error(
12498
+ "\u2717 Terraform-input guard: found 0 workflow files \u2014 this looks like a broken scan, not a clean repo. Refusing to report success over zero input."
12499
+ );
12500
+ process.exit(1);
12501
+ }
12502
+ const violations = checkTerraformInput(root);
12503
+ if (violations.length > 0) {
12504
+ console.error("\u2717 Terraform-input guard: interactive Terraform invocation(s) found\n");
12505
+ for (const v of violations) {
12506
+ console.error(` ${v.file}:${v.line} ${v.message}`);
12507
+ }
12508
+ console.error("\nSee biffo-template#322.");
12509
+ process.exit(1);
12510
+ }
12511
+ console.log(`\u2713 Terraform-input guard: every Terraform invocation is non-interactive`);
12512
+ }
12513
+
11895
12514
  // src/commands/check.ts
11896
12515
  var checkCommand = new Command23("check").description(
11897
- "Repo guards (ownership, release subject, plugin terraform, plugin collisions, eventbridge-log-permissions, plugin-tool-supply, core-direct-paths) run in CI and git hooks, plus out-of-band audits (branch protection)"
12516
+ "Repo guards (ownership, release subject, plugin terraform, plugin collisions, eventbridge-log-permissions, plugin-tool-supply, core-direct-paths, cognito-invite-template, lambda-output, pipe-trap, skeleton-drift, terraform-input) run in CI and git hooks, plus out-of-band audits (branch protection)"
11898
12517
  );
11899
12518
  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 () => {
11900
12519
  await runOwnershipCheck(rawArgsAfter("ownership"));
@@ -11928,6 +12547,31 @@ checkCommand.command("core-direct-paths").description(
11928
12547
  ).option("--sibling <name>", "Label for the report").option("--frontend-src <dir>", "Sibling's frontend source directory to scan").option("--core-src <dir>", "Core API's source directory (ground truth for route prefixes)").action(async (opts) => {
11929
12548
  await runCoreDirectPathsCheck(opts);
11930
12549
  });
12550
+ checkCommand.command("cognito-invite-template").description(
12551
+ "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"
12552
+ ).action(async () => {
12553
+ await runCognitoInviteTemplateCheck();
12554
+ });
12555
+ checkCommand.command("lambda-output").description(
12556
+ "Refuse an unsuppressed aws lambda update-function-* call (#334) \u2014 its default output is the full function configuration, env vars in plaintext, leaked into the Actions log"
12557
+ ).action(async () => {
12558
+ await runLambdaOutputCheck();
12559
+ });
12560
+ checkCommand.command("pipe-trap").description(
12561
+ "Refuse a status-bearing command (claim.sh, wait-for-checks, git push, ...) piped into another, or $? read after one (#1231) \u2014 both read the LAST command's exit status"
12562
+ ).action(async () => {
12563
+ await runPipeTrapCheck();
12564
+ });
12565
+ checkCommand.command("skeleton-drift").description(
12566
+ "Refuse a fix this repo made for itself that never reached _skeletons/ \u2014 a hardcoded runner, the paid gitleaks action, an unhardened dependency audit, a hard-coded app title"
12567
+ ).action(async () => {
12568
+ await runSkeletonDriftCheck();
12569
+ });
12570
+ checkCommand.command("terraform-input").description(
12571
+ "Refuse a Terraform invocation that can prompt on stdin without -input=false, or a workflow running Terraform without TF_INPUT set (#322) \u2014 a runner has no stdin to answer"
12572
+ ).action(async () => {
12573
+ await runTerraformInputCheck();
12574
+ });
11931
12575
  checkCommand.command("branch-protection").description(
11932
12576
  "Verify dev/staging/main are actually protected \u2014 scaffolding skips this on a 403 (#715)"
11933
12577
  ).option("--repo <owner/name>", "Repo to audit; defaults to this checkout's origin remote").option(
@@ -11942,8 +12586,8 @@ function rawArgsAfter(subcommand) {
11942
12586
  }
11943
12587
 
11944
12588
  // src/commands/doctor.ts
11945
- import { existsSync as existsSync41, readFileSync as readFileSync31 } from "fs";
11946
- import { join as join44, resolve as resolve18 } from "path";
12589
+ import { existsSync as existsSync42, readFileSync as readFileSync36 } from "fs";
12590
+ import { join as join50, resolve as resolve18 } from "path";
11947
12591
  import chalk21 from "chalk";
11948
12592
  import { Command as Command24 } from "commander";
11949
12593
 
@@ -12118,10 +12762,10 @@ async function runDoctor(options, deps = { git: new GitAdapter() }) {
12118
12762
  return runDoctorChecks(facts);
12119
12763
  }
12120
12764
  function readLocalCoreVersion(cwd) {
12121
- const path = join44(cwd, INSTANCE_CORE_FILE);
12122
- if (!existsSync41(path)) return null;
12765
+ const path = join50(cwd, INSTANCE_CORE_FILE);
12766
+ if (!existsSync42(path)) return null;
12123
12767
  try {
12124
- return parseCoreRecord(readFileSync31(path, "utf8"));
12768
+ return parseCoreRecord(readFileSync36(path, "utf8"));
12125
12769
  } catch {
12126
12770
  return null;
12127
12771
  }
@@ -12136,10 +12780,10 @@ function parseCoreRecord(contents) {
12136
12780
  }
12137
12781
  }
12138
12782
  function readFossil(cwd) {
12139
- const path = join44(cwd, CORE_VERSION_FILE);
12140
- if (!existsSync41(path)) return null;
12783
+ const path = join50(cwd, CORE_VERSION_FILE);
12784
+ if (!existsSync42(path)) return null;
12141
12785
  try {
12142
- const value = readFileSync31(path, "utf8").trim();
12786
+ const value = readFileSync36(path, "utf8").trim();
12143
12787
  return value === "" ? null : value;
12144
12788
  } catch {
12145
12789
  return null;
@@ -12588,13 +13232,13 @@ import { fileURLToPath as fileURLToPath6 } from "url";
12588
13232
  import { Command as Command26 } from "commander";
12589
13233
 
12590
13234
  // src/lib/packaged-scripts.ts
12591
- import { existsSync as existsSync42 } from "fs";
12592
- import { dirname as dirname10, join as join45 } from "path";
13235
+ import { existsSync as existsSync43 } from "fs";
13236
+ import { dirname as dirname10, join as join51 } from "path";
12593
13237
  function findPackagedScript(startDir, relativePath) {
12594
13238
  let dir = startDir;
12595
13239
  for (; ; ) {
12596
- const candidate = join45(dir, relativePath);
12597
- if (existsSync42(candidate)) return candidate;
13240
+ const candidate = join51(dir, relativePath);
13241
+ if (existsSync43(candidate)) return candidate;
12598
13242
  const parent = dirname10(dir);
12599
13243
  if (parent === dir) return null;
12600
13244
  dir = parent;