@patronage/software-factory 1.0.0-alpha.26 → 1.0.0-alpha.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,12 +1,10 @@
1
- #!/usr/bin/env node
2
1
  import { t as __exportAll } from "./chunk-pbuEa-1d.js";
3
- import { appendFileSync, constants, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
4
- import { pathToFileURL } from "node:url";
5
2
  import { Command, InvalidArgumentError } from "commander";
6
3
  import path from "node:path";
7
4
  import crypto, { createHash, randomBytes, randomUUID } from "node:crypto";
8
5
  import { z } from "zod";
9
6
  import { execFileSync, spawnSync } from "node:child_process";
7
+ import { appendFileSync, constants, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
10
8
  import { Buffer as Buffer$1 } from "node:buffer";
11
9
  import picomatch from "picomatch";
12
10
  import { link, lstat, mkdir, open, readFile, readdir, realpath, rename, stat, unlink } from "node:fs/promises";
@@ -16,9 +14,27 @@ import { setImmediate as setImmediate$1, setTimeout as setTimeout$1 } from "node
16
14
  import { Worker } from "node:worker_threads";
17
15
  import { parse } from "yaml";
18
16
  import { promisify } from "node:util";
19
- import { FACTORY_PROOF_GATE_APP_ID, FACTORY_PROOF_GATE_STEP_NAME, GitHubApiError, mintInstallationToken, previewProofInventory, productionImpactTargetOutput, readVitestProfileDocument } from "@patronage/factory-ci";
17
+ import { FACTORY_APP_BROKER_ORIGIN, FACTORY_APP_BROKER_PATH, FACTORY_CURSOR_OIDC_AUDIENCE, FACTORY_PROOF_GATE_APP_ID, FACTORY_PROOF_GATE_STEP_NAME, GitHubApiError, mintInstallationToken, previewProofInventory, productionImpactTargetOutput, readVitestProfileDocument } from "@patronage/factory-ci";
18
+ import http from "node:http";
19
+ import { pathToFileURL } from "node:url";
20
20
  //#region package.json
21
- var version = "1.0.0-alpha.26";
21
+ var version = "1.0.0-alpha.27";
22
+ //#endregion
23
+ //#region src/cli-entry.ts
24
+ /**
25
+ * The path of the module that runs a factory command.
26
+ *
27
+ * The package publishes two dist entries (#386). `index.js` is the library
28
+ * export: consumers import it, so it runs nothing. `cli.js` is the bin, and it
29
+ * is the only entry that runs a command. A spawned follow-up that names
30
+ * `index.js` therefore exits 0 having done nothing.
31
+ *
32
+ * Every caller that needs the runnable entry resolves it here, so there is one
33
+ * place to get it wrong rather than one per call site. The entry is this
34
+ * module's sibling and carries this module's extension — `cli.ts` under tsx in
35
+ * development, `cli.js` in the built package.
36
+ */
37
+ const factoryCliEntry = () => path.join(import.meta.dirname, `cli${path.extname(import.meta.filename)}`);
22
38
  //#endregion
23
39
  //#region src/review-rungs.ts
24
40
  const EVIDENCE_REVIEW_RUNGS = [
@@ -1261,11 +1277,52 @@ function currentHeadSha(cwd) {
1261
1277
  function repositoryRoot(cwd) {
1262
1278
  return runCapture("git", ["rev-parse", "--show-toplevel"], cwd).stdout.trim();
1263
1279
  }
1264
- function showFileAtRef(cwd, ref, absolutePath) {
1265
- const rel = path.relative(cwd, absolutePath);
1266
- const spec = rel.startsWith(".") ? rel : `./${rel}`;
1280
+ /** A path handed to a git object read that is not repository-relative. */
1281
+ var RepositoryRelativePathError = class extends Error {
1282
+ name = "RepositoryRelativePathError";
1283
+ };
1284
+ /**
1285
+ * The repository-relative contract, enforced before a path reaches git.
1286
+ *
1287
+ * git resolves `<ref>:<path>` from the repository root ONLY for a bare name.
1288
+ * Measured against real git from a package subdirectory, `HEAD:./package.json`
1289
+ * and `HEAD:../software-factory/package.json` both resolve — the first against
1290
+ * the working directory, the second by traversal. So a caller that passes
1291
+ * either form silently addresses a different file depending on where the
1292
+ * command ran, which is the whole defect class this contract exists to close
1293
+ * (#922, #1004 review cycles 2 to 4).
1294
+ *
1295
+ * Every current call site already passes a bare name; this makes the promise
1296
+ * enforced rather than documented. It throws instead of returning a value,
1297
+ * because a control that cannot answer must fail loudly, and callers that
1298
+ * tolerate a missing file must not silently tolerate a mis-addressed one.
1299
+ */
1300
+ function assertRepositoryRelativePath(repositoryRelativePath, caller) {
1301
+ const refuse = (reason) => {
1302
+ throw new RepositoryRelativePathError(`${caller} requires a repository-relative path, but received ${JSON.stringify(repositoryRelativePath)}: ${reason}. git resolves "<ref>:<path>" from the repository root only for a bare name, so a "./", "../", or absolute form addresses a different file depending on the working directory.`);
1303
+ };
1304
+ if (repositoryRelativePath.length === 0) return refuse("it is empty");
1305
+ if (repositoryRelativePath.startsWith("/") || path.isAbsolute(repositoryRelativePath)) return refuse("it is absolute");
1306
+ if (repositoryRelativePath.split("/").some((segment) => segment === "." || segment === "..")) return refuse("it contains a \".\" or \"..\" segment");
1307
+ return repositoryRelativePath;
1308
+ }
1309
+ /**
1310
+ * A file's content at a ref, addressed the way git addresses it (#922).
1311
+ *
1312
+ * `repositoryRelativePath` is repository-relative, NOT relative to `cwd`.
1313
+ * git resolves `<ref>:<path>` from the repository ROOT unless the path starts
1314
+ * with `./` or `../`, so a repository-relative name is correct from every
1315
+ * working directory inside the checkout, including one reached through a
1316
+ * symlink. `cwd` is still passed so git discovers the same repository.
1317
+ *
1318
+ * This used to relativize an absolute path against `cwd` and prefix `./`,
1319
+ * which made the address cwd-relative and therefore wrong from any
1320
+ * subdirectory (#1004 review cycles 2 and 3).
1321
+ */
1322
+ function showFileAtRef(cwd, ref, repositoryRelativePath) {
1323
+ assertRepositoryRelativePath(repositoryRelativePath, "showFileAtRef");
1267
1324
  try {
1268
- return runCapture("git", ["show", `${ref}:${spec}`], cwd).stdout;
1325
+ return runCapture("git", ["show", `${ref}:${repositoryRelativePath}`], cwd).stdout;
1269
1326
  } catch {
1270
1327
  return;
1271
1328
  }
@@ -5483,7 +5540,7 @@ const analyzeSourcePaths = (sourceFiles, targets, profilePath, inertGlobs) => {
5483
5540
  };
5484
5541
  if (file === profilePath) return {
5485
5542
  kind: "conservative",
5486
- reason: `changed source path "${file}" is the impact profile being evaluated; fail closed to full impact`
5543
+ reason: `changed source path "${file}" is the impact profile being evaluated; fail closed to full impact. A profile migration waives each retired target's required-check demand (see the admission skill).`
5487
5544
  };
5488
5545
  }
5489
5546
  return {
@@ -6009,6 +6066,75 @@ function generationOrderMs(value) {
6009
6066
  return Number.isFinite(parsed) ? parsed : Number.NEGATIVE_INFINITY;
6010
6067
  }
6011
6068
  //#endregion
6069
+ //#region src/cursor-oidc-mint.ts
6070
+ const DEFAULT_CURSOR_AGENT_SOCKET = "/run/cursor/api.sock";
6071
+ const DEFAULT_TIMEOUT_MS = 5e3;
6072
+ const cursorAgentSocketPath = (env = process.env) => {
6073
+ const fromEnv = env.CURSOR_AGENT_SOCKET;
6074
+ return fromEnv !== void 0 && fromEnv.length > 0 ? fromEnv : DEFAULT_CURSOR_AGENT_SOCKET;
6075
+ };
6076
+ const isCursorManagedVm = (env = process.env, exists = existsSync) => {
6077
+ if (env.CURSOR_AGENT_SOCKET !== void 0 && env.CURSOR_AGENT_SOCKET.length > 0) return true;
6078
+ return exists(DEFAULT_CURSOR_AGENT_SOCKET);
6079
+ };
6080
+ var CursorOidcMintError = class extends Error {
6081
+ constructor(message) {
6082
+ super(message);
6083
+ this.name = "CursorOidcMintError";
6084
+ }
6085
+ };
6086
+ /**
6087
+ * Mint a Cursor OIDC JWT from the local agent socket. No nonce: the broker
6088
+ * does not run a nonce protocol (#949).
6089
+ */
6090
+ const mintCursorOidcToken = (options = {}) => new Promise((resolve, reject) => {
6091
+ const socketPath = options.socketPath ?? cursorAgentSocketPath();
6092
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
6093
+ const body = JSON.stringify({ aud: options.audience ?? FACTORY_CURSOR_OIDC_AUDIENCE });
6094
+ const req = http.request({
6095
+ headers: {
6096
+ "Content-Length": String(Buffer.byteLength(body)),
6097
+ "Content-Type": "application/json"
6098
+ },
6099
+ method: "POST",
6100
+ path: "/v1/tokens/oidc",
6101
+ socketPath
6102
+ }, (response) => {
6103
+ const chunks = [];
6104
+ response.on("data", (chunk) => {
6105
+ chunks.push(chunk);
6106
+ });
6107
+ response.on("end", () => {
6108
+ const text = Buffer.concat(chunks).toString("utf-8");
6109
+ if (response.statusCode !== 200) {
6110
+ reject(new CursorOidcMintError(`Cursor OIDC mint failed: ${String(response.statusCode)}`));
6111
+ return;
6112
+ }
6113
+ let parsed;
6114
+ try {
6115
+ parsed = JSON.parse(text);
6116
+ } catch {
6117
+ reject(new CursorOidcMintError("Cursor OIDC mint returned invalid JSON"));
6118
+ return;
6119
+ }
6120
+ if (parsed === null || typeof parsed !== "object" || typeof parsed.token !== "string") {
6121
+ reject(new CursorOidcMintError("Cursor OIDC mint omitted token"));
6122
+ return;
6123
+ }
6124
+ resolve(parsed.token);
6125
+ });
6126
+ });
6127
+ req.on("error", (error) => {
6128
+ reject(new CursorOidcMintError(`Cursor OIDC mint failed: ${error instanceof Error ? error.message : String(error)}`));
6129
+ });
6130
+ req.setTimeout(timeoutMs, () => {
6131
+ req.destroy();
6132
+ reject(new CursorOidcMintError("Cursor OIDC mint timed out"));
6133
+ });
6134
+ req.write(body);
6135
+ req.end();
6136
+ });
6137
+ //#endregion
6012
6138
  //#region src/pr-verify-status.ts
6013
6139
  const PR_VERIFY_STATUS_CONTEXT = "patronage-factory/pr-verify";
6014
6140
  const GITHUB_PUBLISH_TIMEOUT_MS = 5e3;
@@ -6067,6 +6193,143 @@ function createGhCommitStatusPoster(output, run = runGhStatus) {
6067
6193
  };
6068
6194
  }
6069
6195
  //#endregion
6196
+ //#region src/factory-app-broker-client.ts
6197
+ const FACTORY_APP_BROKER_URL = `${FACTORY_APP_BROKER_ORIGIN}${FACTORY_APP_BROKER_PATH}`;
6198
+ var FactoryAppBrokerError = class extends Error {
6199
+ status;
6200
+ constructor(message, status) {
6201
+ super(message);
6202
+ this.name = "FactoryAppBrokerError";
6203
+ if (status !== void 0) this.status = status;
6204
+ }
6205
+ };
6206
+ const factoryAppBrokerUrl = (env = process.env) => {
6207
+ const override = env.FACTORY_APP_BROKER_URL;
6208
+ return override !== void 0 && override.length > 0 ? override : FACTORY_APP_BROKER_URL;
6209
+ };
6210
+ /**
6211
+ * Exchange a Cursor OIDC JWT for a repo-scoped Factory App installation token.
6212
+ * Missing or refused exchange fails closed — no token is invented.
6213
+ */
6214
+ const exchangeFactoryAppInstallationToken = async (input) => {
6215
+ const request = input.fetch ?? fetch;
6216
+ const url = input.url ?? FACTORY_APP_BROKER_URL;
6217
+ let response;
6218
+ try {
6219
+ response = await request(url, {
6220
+ body: JSON.stringify({
6221
+ owner: input.owner,
6222
+ repo: input.repo
6223
+ }),
6224
+ headers: {
6225
+ Authorization: `Bearer ${input.oidc}`,
6226
+ "Content-Type": "application/json"
6227
+ },
6228
+ method: "POST",
6229
+ signal: AbortSignal.timeout(input.timeoutMs ?? 5e3)
6230
+ });
6231
+ } catch (error) {
6232
+ throw new FactoryAppBrokerError(`Factory App broker exchange failed: ${error instanceof Error ? error.message : String(error)}`);
6233
+ }
6234
+ if (!response.ok) throw new FactoryAppBrokerError(`Factory App broker exchange failed: ${String(response.status)}`, response.status);
6235
+ const body = await response.json();
6236
+ if (body === null || typeof body !== "object" || typeof body.token !== "string" || body.token.length === 0) throw new FactoryAppBrokerError("Factory App broker exchange omitted token");
6237
+ return body.token;
6238
+ };
6239
+ //#endregion
6240
+ //#region src/github-app-access.ts
6241
+ const NO_APP_SKIP_MESSAGE = "no factory GitHub App configured; skipping durable check-run republish.\n";
6242
+ const exchangeFromCursorVm = async (repository, dependencies, env, request, timeoutMs) => {
6243
+ const mint = dependencies.mintCursorOidc ?? mintCursorOidcToken;
6244
+ const exchange = dependencies.exchangeBroker ?? exchangeFactoryAppInstallationToken;
6245
+ let oidc;
6246
+ try {
6247
+ oidc = await mint({
6248
+ socketPath: cursorAgentSocketPath(env),
6249
+ timeoutMs
6250
+ });
6251
+ } catch (error) {
6252
+ return {
6253
+ failClosed: true,
6254
+ kind: "unavailable",
6255
+ message: `Cursor OIDC mint failed: ${error instanceof Error ? error.message : String(error)}\n`
6256
+ };
6257
+ }
6258
+ try {
6259
+ return {
6260
+ appId: FACTORY_PROOF_GATE_APP_ID,
6261
+ kind: "ok",
6262
+ token: await exchange({
6263
+ fetch: request,
6264
+ oidc,
6265
+ owner: repository.owner,
6266
+ repo: repository.repo,
6267
+ timeoutMs,
6268
+ url: factoryAppBrokerUrl(env)
6269
+ })
6270
+ };
6271
+ } catch (error) {
6272
+ return {
6273
+ failClosed: true,
6274
+ kind: "unavailable",
6275
+ message: `Factory App broker exchange failed: ${error instanceof Error ? error.message : String(error)}\n`
6276
+ };
6277
+ }
6278
+ };
6279
+ /**
6280
+ * Resolve a Factory App installation token for App-owned GitHub writes.
6281
+ *
6282
+ * Order: caller token, `FACTORY_GITHUB_APP_TOKEN` (Actions), local PEM mint,
6283
+ * Cursor OIDC broker exchange. A Cursor-managed VM with no PEM must succeed
6284
+ * the exchange; missing mint or publication fails closed (#949).
6285
+ */
6286
+ const resolveFactoryAppAccess = async (repository, dependencies = {}) => {
6287
+ const env = dependencies.env ?? process.env;
6288
+ if (dependencies.token) return {
6289
+ appId: FACTORY_PROOF_GATE_APP_ID,
6290
+ kind: "ok",
6291
+ token: dependencies.token
6292
+ };
6293
+ const workflowToken = env.FACTORY_GITHUB_APP_TOKEN;
6294
+ if (workflowToken !== void 0 && workflowToken.length > 0) return {
6295
+ appId: FACTORY_PROOF_GATE_APP_ID,
6296
+ kind: "ok",
6297
+ token: workflowToken
6298
+ };
6299
+ const config = dependencies.githubApp ?? tryUserConfig({ env })?.config.githubApp;
6300
+ const request = dependencies.fetch ?? fetch;
6301
+ const timeoutMs = dependencies.timeoutMs ?? 5e3;
6302
+ if (config) try {
6303
+ const token = await mintInstallationToken({
6304
+ credentials: config,
6305
+ owner: repository.owner,
6306
+ repo: repository.repo
6307
+ }, {
6308
+ fetch: request,
6309
+ now: dependencies.now ?? Date.now,
6310
+ timeoutMs,
6311
+ ...dependencies.transportAttempts === void 0 ? {} : { transportAttempts: dependencies.transportAttempts }
6312
+ });
6313
+ return {
6314
+ appId: String(config.appId),
6315
+ kind: "ok",
6316
+ token
6317
+ };
6318
+ } catch (error) {
6319
+ return {
6320
+ failClosed: true,
6321
+ kind: "unavailable",
6322
+ message: `unable to mint Factory App token: ${error instanceof Error ? error.message : String(error)}\n`
6323
+ };
6324
+ }
6325
+ if (isCursorManagedVm(env, dependencies.existsSync)) return exchangeFromCursorVm(repository, dependencies, env, request, timeoutMs);
6326
+ return {
6327
+ failClosed: false,
6328
+ kind: "unavailable",
6329
+ message: NO_APP_SKIP_MESSAGE
6330
+ };
6331
+ };
6332
+ //#endregion
6070
6333
  //#region src/github-app-client.ts
6071
6334
  const readExchange = async (request, url, init, timeoutMs) => {
6072
6335
  const response = await request(url, {
@@ -6159,14 +6422,10 @@ const OUTCOMES = ["aborted", "passed"];
6159
6422
  const isOutcome = (value) => OUTCOMES.includes(value);
6160
6423
  /**
6161
6424
  * The payload's outcome must agree with the check run's own conclusion, which
6162
- * comes from `verifyProofPassed`: a schemaVersion 1 proof predates the
6163
- * `outcome` field and counts as passing for the compatibility window. Any other
6164
- * proof with no recorded outcome is unfinished (fail closed).
6425
+ * comes from `verifyProofPassed`: a proof with no recorded outcome is
6426
+ * unfinished (fail closed).
6165
6427
  */
6166
- const outcomeFor = (value) => {
6167
- if (isOutcome(value.outcome)) return value.outcome;
6168
- return value.schemaVersion === 1 ? "passed" : "aborted";
6169
- };
6428
+ const outcomeFor = (value) => isOutcome(value.outcome) ? value.outcome : "aborted";
6170
6429
  const asClassification = (value) => DIFF_CLASSIFICATIONS.includes(value) ? value : "unknown";
6171
6430
  /**
6172
6431
  * Project a local pr:verify proof onto the durable check-run payload. Accepts
@@ -6394,7 +6653,7 @@ async function postFactoryCheckRun({ config, deadlineMs, dependencies, detailsUr
6394
6653
  const request = dependencies.fetch ?? fetch;
6395
6654
  const timeoutMs = dependencies.timeoutMs ?? 5e3;
6396
6655
  try {
6397
- const token = await installationToken(input, config, request, (dependencies.now ?? Date.now)(), {
6656
+ const token = dependencies.token ?? await installationToken(input, config, request, (dependencies.now ?? Date.now)(), {
6398
6657
  timeoutMs,
6399
6658
  transportAttempts: 1
6400
6659
  });
@@ -6488,7 +6747,7 @@ async function resolveDetailsUrlSafely(input, dependencies, onDiagnostic) {
6488
6747
  async function servedFactoryCheckRun(input, config, dependencies) {
6489
6748
  const request = dependencies.fetch ?? fetch;
6490
6749
  const timeoutMs = dependencies.timeoutMs ?? 5e3;
6491
- const token = await installationToken(input, config, request, (dependencies.now ?? Date.now)(), { timeoutMs });
6750
+ const token = dependencies.token ?? await installationToken(input, config, request, (dependencies.now ?? Date.now)(), { timeoutMs });
6492
6751
  const query = new URLSearchParams({
6493
6752
  app_id: String(config.appId),
6494
6753
  check_name: FACTORY_CHECK_NAMES[input.gate],
@@ -6621,10 +6880,12 @@ async function confirmPublishedCheckRun({ config, createdId, dependencies, input
6621
6880
  * the head SHA visible on GitHub (pushed the branch, created the PR) can make
6622
6881
  * the proof reliably present for that SHA before it returns (#247).
6623
6882
  *
6624
- * Never throws and never posts a commit-status fallback: the commit status is a
6625
- * human-readable mirror, not a proof surface, so a caller that needs an
6626
- * App-verified check run must be told plainly whether it got one. Returns
6627
- * `true` only when the App-owned check run landed.
6883
+ * Never posts a commit-status fallback: the commit status is a human-readable
6884
+ * mirror, not a proof surface, so a caller that needs an App-verified check
6885
+ * run must be told plainly whether it got one. Returns `true` only when the
6886
+ * App-owned check run landed. Throws when Factory App access is fail-closed
6887
+ * (missing mint or Cursor OIDC exchange, #949): `pr:ready` must not notice
6888
+ * that miss and exit 0. An optional-App skip still returns `false`.
6628
6889
  *
6629
6890
  * "Landed" means GitHub serves it, not that the POST was accepted (#520). On
6630
6891
  * PR #519 the POST was accepted, `pr:ready` reported ready and armed, and the
@@ -6650,18 +6911,28 @@ async function confirmPublishedCheckRun({ config, createdId, dependencies, input
6650
6911
  */
6651
6912
  async function ensureFactoryCheckRunPublished(input, dependencies = {}) {
6652
6913
  const { onDiagnostic } = dependencies;
6914
+ const checkName = FACTORY_CHECK_NAMES[input.gate];
6915
+ const access = await resolveFactoryAppAccess(input, dependencies);
6916
+ if (access.kind !== "ok") {
6917
+ onDiagnostic?.(`${checkName}: ${access.message}`);
6918
+ if (access.failClosed) throw new Error(`${checkName}: ${access.message.trim()}`);
6919
+ return false;
6920
+ }
6653
6921
  try {
6654
- const config = dependencies.githubApp ?? tryUserConfig()?.config.githubApp;
6655
- if (!config) {
6656
- onDiagnostic?.(`${FACTORY_CHECK_NAMES[input.gate]}: no factory GitHub App configured; skipping durable check-run republish.\n`);
6657
- return false;
6658
- }
6922
+ const config = dependencies.githubApp ?? tryUserConfig()?.config.githubApp ?? {
6923
+ appId: access.appId,
6924
+ privateKeyPath: "factory-app-broker"
6925
+ };
6926
+ const tokenDependencies = {
6927
+ ...dependencies,
6928
+ token: access.token
6929
+ };
6659
6930
  const detailsUrl = await resolveDetailsUrlSafely(input, dependencies, onDiagnostic);
6660
6931
  const created = await postFactoryCheckRun({
6661
6932
  config,
6662
6933
  dependencies: {
6663
6934
  retry: DURABLE_CHECK_RUN_RETRY,
6664
- ...dependencies
6935
+ ...tokenDependencies
6665
6936
  },
6666
6937
  detailsUrl,
6667
6938
  input
@@ -6669,7 +6940,7 @@ async function ensureFactoryCheckRunPublished(input, dependencies = {}) {
6669
6940
  return await confirmPublishedCheckRun({
6670
6941
  config,
6671
6942
  createdId: typeof created.id === "number" ? created.id : void 0,
6672
- dependencies,
6943
+ dependencies: tokenDependencies,
6673
6944
  input,
6674
6945
  onDiagnostic
6675
6946
  });
@@ -6708,15 +6979,26 @@ function createFactoryCheckPublisher(output, dependencies = {}) {
6708
6979
  output.stderr.write(`${checkName}: unable to post fallback status: ${message}\n`);
6709
6980
  }
6710
6981
  };
6711
- const config = dependencies.githubApp ?? tryUserConfig()?.config.githubApp;
6712
- if (!config) {
6713
- postFallback();
6982
+ const access = await resolveFactoryAppAccess(input, {
6983
+ ...dependencies,
6984
+ transportAttempts: 1
6985
+ });
6986
+ if (access.kind !== "ok") {
6987
+ output.stderr.write(`${checkName}: ${access.message}`);
6988
+ if (!access.failClosed) postFallback();
6714
6989
  return;
6715
6990
  }
6991
+ const config = dependencies.githubApp ?? tryUserConfig()?.config.githubApp ?? {
6992
+ appId: access.appId,
6993
+ privateKeyPath: "factory-app-broker"
6994
+ };
6716
6995
  try {
6717
6996
  await postFactoryCheckRun({
6718
6997
  config,
6719
- dependencies,
6998
+ dependencies: {
6999
+ ...dependencies,
7000
+ token: access.token
7001
+ },
6720
7002
  detailsUrl,
6721
7003
  input
6722
7004
  });
@@ -6733,6 +7015,77 @@ function createFactoryCheckPublisher(output, dependencies = {}) {
6733
7015
  };
6734
7016
  }
6735
7017
  //#endregion
7018
+ //#region src/repository-profile-path.ts
7019
+ /**
7020
+ * `git rev-parse --show-toplevel` answers with a real path, while a caller's
7021
+ * `cwd` may reach the same directory through a symlink (every macOS temporary
7022
+ * directory does). Both sides are resolved before they are compared, so a
7023
+ * symlinked checkout is not mistaken for a profile outside the repository.
7024
+ */
7025
+ const realPath = (value) => {
7026
+ try {
7027
+ return realpathSync(value);
7028
+ } catch {
7029
+ return value;
7030
+ }
7031
+ };
7032
+ /**
7033
+ * The repository top level for `cwd`, or `cwd` itself when it is not a git
7034
+ * checkout. Never throws: a caller rooting a path has a usable answer either
7035
+ * way, and the checkout-less case keeps the previous cwd-rooted behavior.
7036
+ */
7037
+ const repositoryRootOrCwd = ({ cwd, readRepositoryRoot = repositoryRoot }) => {
7038
+ try {
7039
+ return readRepositoryRoot(cwd);
7040
+ } catch {
7041
+ return cwd;
7042
+ }
7043
+ };
7044
+ /**
7045
+ * The repository-relative, POSIX-separated path of `profilePath`.
7046
+ *
7047
+ * A cwd that is not a git checkout, and a profile the repository top level
7048
+ * does not contain, both fall back to the cwd-relative path: this function
7049
+ * makes a path more portable, and it must never invent one. Callers that
7050
+ * require the profile to be inside the trusted checkout keep enforcing that
7051
+ * themselves.
7052
+ */
7053
+ const repositoryRelativeProfilePath = ({ cwd, profilePath, readRepositoryRoot = repositoryRoot }) => {
7054
+ const cwdRelative = path.relative(cwd, profilePath).split(path.sep).join("/");
7055
+ const root = repositoryRootOrCwd({
7056
+ cwd,
7057
+ readRepositoryRoot
7058
+ });
7059
+ const rootRelative = path.relative(realPath(root), realPath(profilePath)).split(path.sep).join("/");
7060
+ return rootRelative.length > 0 && !rootRelative.startsWith("../") ? rootRelative : cwdRelative;
7061
+ };
7062
+ /** A path that names no file inside the root it was measured from. */
7063
+ const escapesRoot = (relativePath) => relativePath.length === 0 || relativePath === ".." || relativePath.startsWith("../") || path.isAbsolute(relativePath);
7064
+ /**
7065
+ * The same path, for a caller that must REFUSE a profile the repository does
7066
+ * not contain.
7067
+ *
7068
+ * Containment is measured from the repository top level, not from `cwd`.
7069
+ * Measuring it from `cwd` refuses the repository-root profile whenever the
7070
+ * command runs in a package subdirectory — a profile that is plainly inside
7071
+ * the trusted checkout — and it refused before the conversion above could run
7072
+ * (cycle-1 review finding on PR #1004).
7073
+ *
7074
+ * Fail-closed is unchanged. A profile genuinely outside the repository root
7075
+ * falls back to the cwd-relative path, which still escapes, and throws here.
7076
+ * A cwd that is not a git checkout has no root to measure from and keeps the
7077
+ * previous cwd-relative rule exactly.
7078
+ */
7079
+ const repositoryContainedProfilePath = ({ cwd, profilePath, readRepositoryRoot = repositoryRoot }) => {
7080
+ const relativePath = repositoryRelativeProfilePath({
7081
+ cwd,
7082
+ profilePath,
7083
+ readRepositoryRoot
7084
+ });
7085
+ if (escapesRoot(relativePath)) throw new Error("The readiness profile must be inside the trusted checkout.");
7086
+ return relativePath;
7087
+ };
7088
+ //#endregion
6736
7089
  //#region src/pr-classification.ts
6737
7090
  const MISSING_STAMP_REASON = "impact stamp is missing; fail closed to non-trivial";
6738
7091
  function ownershipFromStamp(stamp, file) {
@@ -6758,7 +7111,10 @@ function reasonForOwnership(file, ownership) {
6758
7111
  }
6759
7112
  }
6760
7113
  function relativeProfilePath(options) {
6761
- return path.relative(options.cwd, options.profilePath).split(path.sep).join("/");
7114
+ return repositoryRelativeProfilePath({
7115
+ cwd: options.cwd,
7116
+ profilePath: options.profilePath
7117
+ });
6762
7118
  }
6763
7119
  function applyProfileEditGuard(files, relativeProfile, profilePath, base) {
6764
7120
  if (!files.some((file) => file === relativeProfile || file === profilePath)) return base;
@@ -6990,9 +7346,7 @@ const planVerificationBattery = ({ commands, stamp, vetoedTargets }) => {
6990
7346
  * skip this epic forbids.
6991
7347
  *
6992
7348
  * Rule 4 is scoped to `outcome: "passed"` because an ABORTED proof legitimately
6993
- * records partial execution — the run stopped at the failing command. It is
6994
- * also scoped to schemaVersion >= 4: `executedCommands` postdates v1–v3, so a
6995
- * legacy proof that never recorded it is not making a false completeness claim.
7349
+ * records partial execution — the run stopped at the failing command.
6996
7350
  *
6997
7351
  * The check is one-directional on purpose. Every SELECTED command must be
6998
7352
  * accounted for, but `executedCommands` may legitimately carry MORE than the
@@ -7035,7 +7389,7 @@ const assertBatteryCompleteness = (proof, context) => {
7035
7389
  });
7036
7390
  seen.add(entry.name);
7037
7391
  }
7038
- if (proof.outcome !== "passed" || proof.schemaVersion < 4) return;
7392
+ if (proof.outcome !== "passed") return;
7039
7393
  const disposed = new Set([...executed, ...seen]);
7040
7394
  const undisposed = selected.filter((name) => !disposed.has(name));
7041
7395
  if (undisposed.length > 0) context.addIssue({
@@ -7045,7 +7399,7 @@ const assertBatteryCompleteness = (proof, context) => {
7045
7399
  });
7046
7400
  };
7047
7401
  /**
7048
- * The stamp-authorization control: a passed v4 proof may only claim a command
7402
+ * The stamp-authorization control: a passed proof may only claim a command
7049
7403
  * was NOT REQUIRED if its own recorded stamp says so.
7050
7404
  *
7051
7405
  * `assertBatteryCompleteness` proves the two lists partition the selected
@@ -7068,8 +7422,8 @@ const assertBatteryCompleteness = (proof, context) => {
7068
7422
  * provably released.
7069
7423
  *
7070
7424
  * The proof is one artifact and a determined forger controls all of it; this
7071
- * is internal-coherence belt-and-braces in the same trust domain as the
7072
- * version guards, and write-time truth stays `pr:verify`'s job.
7425
+ * is internal-coherence belt-and-braces in the same trust domain as the rest
7426
+ * of the proof, and write-time truth stays `pr:verify`'s job.
7073
7427
  *
7074
7428
  * The authorizing evidence is the proof's OWN `impactStamp` — the same stamp
7075
7429
  * `pr:verify` computed for this candidate's identity triple and recorded here,
@@ -7082,14 +7436,13 @@ const assertBatteryCompleteness = (proof, context) => {
7082
7436
  * self-contradictory stamp, an unclassified name, or an `affected` verdict all
7083
7437
  * fail the same way scoping itself would.
7084
7438
  *
7085
- * Scoped to `outcome: "passed"` and schemaVersion >= 4 for the same reasons as
7086
- * the completeness rule: an aborted run records partial execution, and a
7087
- * legacy proof predates both fields, so neither is making a false completeness
7439
+ * Scoped to `outcome: "passed"` for the same reason as the completeness rule:
7440
+ * an aborted run records partial execution, so it makes no false completeness
7088
7441
  * claim.
7089
7442
  */
7090
7443
  const assertNotRequiredStampAuthorization = (proof, context) => {
7091
7444
  const notRequired = proof.notRequiredCommands ?? [];
7092
- if (proof.outcome !== "passed" || proof.schemaVersion < 4 || notRequired.length === 0) return;
7445
+ if (proof.outcome !== "passed" || notRequired.length === 0) return;
7093
7446
  const { impactStamp } = proof;
7094
7447
  if (impactStamp === void 0) {
7095
7448
  context.addIssue({
@@ -7138,36 +7491,6 @@ const batteryScopeSummaryLines = (plan) => {
7138
7491
  //#endregion
7139
7492
  //#region src/pr-verify-proof-rules.ts
7140
7493
  const assertPrVerifyProofRules = (proof, context) => {
7141
- if (proof.schemaVersion >= 4 && proof.authoringSession === void 0) context.addIssue({
7142
- code: "custom",
7143
- message: "schemaVersion 4 pr:verify proof requires authoringSession (the recorded authoring identity, or the `unknown` sentinel).",
7144
- path: ["authoringSession"]
7145
- });
7146
- if (proof.schemaVersion < 4 && proof.authoringSession !== void 0) context.addIssue({
7147
- code: "custom",
7148
- message: "authoringSession is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
7149
- path: ["authoringSession"]
7150
- });
7151
- if (proof.schemaVersion < 4 && proof.impactStamp !== void 0) context.addIssue({
7152
- code: "custom",
7153
- message: "impactStamp is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
7154
- path: ["impactStamp"]
7155
- });
7156
- if (proof.schemaVersion < 4 && proof.notRequiredCommands !== void 0) context.addIssue({
7157
- code: "custom",
7158
- message: "notRequiredCommands is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
7159
- path: ["notRequiredCommands"]
7160
- });
7161
- if (proof.schemaVersion < 4 && proof.catchUpRecognition !== void 0) context.addIssue({
7162
- code: "custom",
7163
- message: "catchUpRecognition is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
7164
- path: ["catchUpRecognition"]
7165
- });
7166
- if (proof.schemaVersion < 4 && proof.notDemandedRecords !== void 0) context.addIssue({
7167
- code: "custom",
7168
- message: "notDemandedRecords is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
7169
- path: ["notDemandedRecords"]
7170
- });
7171
7494
  if (proof.notDemandedRecords !== void 0 && proof.impactStamp === void 0) context.addIssue({
7172
7495
  code: "custom",
7173
7496
  message: "notDemandedRecords requires impactStamp; package-level not-demanded records are bound to the stamp identity.",
@@ -7176,14 +7499,15 @@ const assertPrVerifyProofRules = (proof, context) => {
7176
7499
  assertBatteryCompleteness(proof, context);
7177
7500
  assertNotRequiredStampAuthorization(proof, context);
7178
7501
  };
7179
- //#endregion
7180
- //#region src/pr-verify-proof-contract.ts
7181
- const SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS = [
7182
- 1,
7183
- 2,
7184
- 3,
7185
- 4
7186
- ];
7502
+ /**
7503
+ * The pr:verify proof versions a reader accepts: the current one only (#920
7504
+ * item 1). #917 narrowed the pr:ready reader the same way. A pr:verify proof
7505
+ * is per-candidate and short-lived — it is rewritten on every head — so no
7506
+ * stored v1–v3 proof can outlive the release that drops it. Pre-1.0 posture
7507
+ * applies: one current contract, no compat reader. A consumer on an older
7508
+ * factory must adopt this release before HQ ingests its pr:verify proofs.
7509
+ */
7510
+ const SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS = [4];
7187
7511
  const executedCommandSchema = z.object({
7188
7512
  command: z.string().min(1),
7189
7513
  counts: z.object({
@@ -7194,7 +7518,6 @@ const executedCommandSchema = z.object({
7194
7518
  exitCode: z.number(),
7195
7519
  name: z.string().min(1),
7196
7520
  scope: z.enum([
7197
- "always",
7198
7521
  "docs-only",
7199
7522
  "trivial",
7200
7523
  "full"
@@ -7209,9 +7532,9 @@ const notDemandedRecordSchema = z.object({
7209
7532
  basis: z.string().min(1),
7210
7533
  name: z.string().min(1)
7211
7534
  });
7212
- const prVerifySchemaVersionSchema = z.number().refine((value) => SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS.includes(value), { message: `schemaVersion must be one of: ${SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS.join(", ")}` });
7535
+ const prVerifySchemaVersionSchema = z.literal(4);
7213
7536
  const prVerifyProofSchema = z.object({
7214
- authoringSession: z.string().trim().min(1).optional(),
7537
+ authoringSession: z.string().trim().min(1),
7215
7538
  base: z.string().min(1),
7216
7539
  baselineFullProofs: z.array(z.object({
7217
7540
  base: z.string().min(1),
@@ -7257,7 +7580,6 @@ const prVerifyProofSchema = z.object({
7257
7580
  impactTarget: z.string().min(1).optional(),
7258
7581
  name: z.string().min(1),
7259
7582
  scope: z.enum([
7260
- "always",
7261
7583
  "docs-only",
7262
7584
  "trivial",
7263
7585
  "full"
@@ -7288,7 +7610,7 @@ var verification_proof_exports = /* @__PURE__ */ __exportAll({
7288
7610
  function readPrVerifyProof(filePath) {
7289
7611
  return readProofFromPath(validatePrVerifyProof, filePath);
7290
7612
  }
7291
- const verifyProofPassed = (proof) => proof.outcome === "passed" || proof.outcome === void 0 && proof.schemaVersion === 1;
7613
+ const verifyProofPassed = (proof) => proof.outcome === "passed";
7292
7614
  const prVerifyProofHeadShas = (proof, creditedHeadSha = proof?.headSha) => {
7293
7615
  if (!proof || !verifyProofPassed(proof)) return {
7294
7616
  docsOnlyVerifiedHeadShas: [],
@@ -7317,7 +7639,7 @@ const verifyProofHeadMatches = ({ headSha, proof }) => Boolean(proof && evidence
7317
7639
  recorded: { headSha: proof.headSha },
7318
7640
  rule: "patch-id-or-head-sha"
7319
7641
  }));
7320
- const verifyProofIdentityMatches = ({ currentIdentity, proof }) => Boolean(proof && verifyProofPassed(proof) && proof.schemaVersion >= 3 && evidenceFresh({
7642
+ const verifyProofIdentityMatches = ({ currentIdentity, proof }) => Boolean(proof && verifyProofPassed(proof) && evidenceFresh({
7321
7643
  candidate: { patchId: currentIdentity?.patchId },
7322
7644
  recorded: { patchId: proof.patchId },
7323
7645
  rule: "patch-id"
@@ -7336,7 +7658,6 @@ const verifyProofIdentityMatches = ({ currentIdentity, proof }) => Boolean(proof
7336
7658
  */
7337
7659
  const trustedImpactStamp = ({ candidate, proof }) => {
7338
7660
  if (!proof || !verifyProofPassed(proof)) return;
7339
- if (proof.schemaVersion < 4) return;
7340
7661
  const headBound = sameHeadSha(candidate.headSha, proof.headSha);
7341
7662
  const patchBound = evidenceFresh({
7342
7663
  candidate: { patchId: candidate.patchId },
@@ -7348,13 +7669,13 @@ const trustedImpactStamp = ({ candidate, proof }) => {
7348
7669
  /**
7349
7670
  * #833: the recorded catch-up recognition, consumed ONLY when it is
7350
7671
  * identity-bound to the current candidate under exactly the rules
7351
- * `trustedImpactStamp` applies — the proof passed, is v4+, and its recorded
7672
+ * `trustedImpactStamp` applies — the proof passed, and its recorded
7352
7673
  * headSha AND patchId both name this candidate. The recognition withdraws
7353
7674
  * review and preview demands, so it earns the same strict double binding and
7354
7675
  * the same fail-closed answer (`undefined`) on every other state.
7355
7676
  */
7356
7677
  const trustedCatchUpRecognition = ({ candidate, proof }) => {
7357
- if (!proof || !verifyProofPassed(proof) || proof.schemaVersion < 4) return;
7678
+ if (!proof || !verifyProofPassed(proof)) return;
7358
7679
  const headBound = sameHeadSha(candidate.headSha, proof.headSha);
7359
7680
  const patchBound = evidenceFresh({
7360
7681
  candidate: { patchId: candidate.patchId },
@@ -7693,13 +8014,12 @@ const assertCleanInstallResolution = (repoRoot, dependencies = {}) => {
7693
8014
  };
7694
8015
  //#endregion
7695
8016
  //#region src/pr-verify.ts
7696
- const PR_VERIFY_SCHEMA_VERSION = 4;
7697
8017
  const DEFAULT_PR_VERIFY_PROOF_PATH = ".factory-memory/pr-verify.json";
7698
8018
  const PR_VERIFY_PROOF_DESCRIPTOR$1 = {
7699
8019
  defaultPath: DEFAULT_PR_VERIFY_PROOF_PATH,
7700
8020
  label: "pr:verify proof",
7701
8021
  parse: validatePrVerifyProof,
7702
- schemaVersion: PR_VERIFY_SCHEMA_VERSION
8022
+ schemaVersion: 4
7703
8023
  };
7704
8024
  const VERIFY_SCOPE_RANK = {
7705
8025
  "docs-only": 0,
@@ -7786,7 +8106,7 @@ function buildPrVerifyProof({ authoringSession, base, baselineFullProofs, catchU
7786
8106
  profilePath,
7787
8107
  projectKey,
7788
8108
  repository,
7789
- schemaVersion: PR_VERIFY_SCHEMA_VERSION,
8109
+ schemaVersion: 4,
7790
8110
  startedAt: startedAt.toISOString(),
7791
8111
  verificationCommands: commands
7792
8112
  };
@@ -7845,7 +8165,10 @@ function runPrVerify(args, dependencies = {}) {
7845
8165
  profilePath: args.profilePath
7846
8166
  });
7847
8167
  const files = git.changedFiles(cwd, args.base);
7848
- const relativeProfile = path.relative(cwd, profilePath).split(path.sep).join("/");
8168
+ const relativeProfile = repositoryRelativeProfilePath({
8169
+ cwd,
8170
+ profilePath
8171
+ });
7849
8172
  const { headSha, mergeBaseSha: proofMergeBaseSha, patchId } = resolveCandidateIdentity({
7850
8173
  base: args.base,
7851
8174
  cwd,
@@ -7863,7 +8186,7 @@ function runPrVerify(args, dependencies = {}) {
7863
8186
  changedFiles: files,
7864
8187
  profile,
7865
8188
  profilePath: relativeProfile,
7866
- readLockfile: (side) => readLockfileAtRef(cwd, side === "base" ? proofMergeBaseSha : headSha, path.resolve(cwd, LOCKFILE_PATH))
8189
+ readLockfile: (side) => readLockfileAtRef(cwd, side === "base" ? proofMergeBaseSha : headSha, LOCKFILE_PATH)
7867
8190
  });
7868
8191
  } catch (error) {
7869
8192
  if (error instanceof ImpactStampConfigError) throw error;
@@ -10415,35 +10738,38 @@ const writeGithubOutputs$1 = (outputPath, decision) => {
10415
10738
  appendFileSync(outputPath, `${lines.join("\n")}\n`, "utf-8");
10416
10739
  };
10417
10740
  /**
10418
- * Classify candidate impact from one exact base/head identity. The delta is
10419
- * the merge-base-to-head diff, and the lockfile sides are read at exactly the
10420
- * merge base and head — the same inputs `pr:verify` gives the impact stamp
10421
- * for the same candidate commits. The result may withdraw hosted routing
10422
- * work only; every doubt path returns a refused decision whose known targets
10423
- * remain demanded.
10741
+ * The candidate impact classifier: one exact base/head identity in, one impact
10742
+ * stamp out. The delta is the merge-base-to-head diff, and the lockfile sides
10743
+ * are read at exactly the merge base and head — the same inputs `pr:verify`
10744
+ * gives the impact stamp for the same candidate commits.
10745
+ *
10746
+ * It is the authority on which required checks a candidate owes (#963), so
10747
+ * every surface that states that demand calls this rather than matching paths
10748
+ * itself. Every doubt path refuses with its reason; a refusal never withdraws
10749
+ * anything.
10424
10750
  */
10425
- const runCandidateImpact = (args) => {
10751
+ const classifyCandidateImpact = (args) => {
10426
10752
  const cwd = path.resolve(args.cwd ?? process.cwd());
10427
10753
  let profile;
10428
- let profilePath;
10429
10754
  let relativeProfile;
10430
- let decision;
10431
10755
  try {
10432
- const { path: loadedPath, profile: loadedProfile } = loadProjectProfile({
10756
+ const { path: profilePath, profile: loadedProfile } = loadProjectProfile({
10433
10757
  cwd,
10434
10758
  profilePath: args.profilePath
10435
10759
  });
10436
10760
  profile = loadedProfile;
10437
- profilePath = loadedPath;
10438
- relativeProfile = path.relative(cwd, profilePath).split(path.sep).join("/");
10761
+ relativeProfile = repositoryRelativeProfilePath({
10762
+ cwd,
10763
+ profilePath
10764
+ });
10439
10765
  const base = assertCommitIdentity$1(args.base, "base");
10440
10766
  const head = assertCommitIdentity$1(args.head, "head");
10441
10767
  assertReachableCommit$1(cwd, base, "base");
10442
10768
  assertReachableCommit$1(cwd, head, "head");
10443
10769
  const checkedOutHead = runCapture("git", ["rev-parse", "HEAD"], cwd).stdout.trim().toLowerCase();
10444
10770
  if (checkedOutHead !== head) throw new Error(`head commit ${head} is not the checked-out HEAD ${checkedOutHead}; refusing to mix commit identities with declarations from another tree`);
10445
- const relativeProfilePath = path.relative(cwd, profilePath);
10446
- const committedProfile = showFileAtRef(cwd, head, profilePath);
10771
+ const relativeProfilePath = relativeProfile;
10772
+ const committedProfile = showFileAtRef(cwd, head, relativeProfile);
10447
10773
  if (committedProfile === void 0) throw new Error(`profile ${relativeProfilePath} is not readable at head commit ${head}; refusing to classify from declarations the head does not carry`);
10448
10774
  if (canonicalJson(committedProfile) === void 0 || canonicalJson(committedProfile) !== canonicalJson(readFileSync(profilePath, "utf-8"))) throw new Error(`profile ${relativeProfilePath} on disk differs from head commit ${head}; refusing to classify from declarations the head does not carry`);
10449
10775
  const mergeBase = runCapture("git", [
@@ -10462,12 +10788,43 @@ const runCandidateImpact = (args) => {
10462
10788
  const stamp = computeImpactStamp({
10463
10789
  changedFiles,
10464
10790
  profile,
10465
- profilePath: path.relative(cwd, profilePath),
10466
- readLockfile: (side) => showFileAtRef(cwd, side === "base" ? mergeBase : head, path.join(cwd, LOCKFILE_PATH))
10791
+ profilePath: relativeProfile,
10792
+ readLockfile: (side) => showFileAtRef(cwd, side === "base" ? mergeBase : head, LOCKFILE_PATH)
10467
10793
  });
10468
- const core = {
10794
+ return {
10469
10795
  baseSha: base,
10796
+ changedFiles,
10470
10797
  headSha: head,
10798
+ kind: "classified",
10799
+ mergeBaseSha: mergeBase,
10800
+ profile,
10801
+ relativeProfilePath: relativeProfile,
10802
+ stamp
10803
+ };
10804
+ } catch (error) {
10805
+ return {
10806
+ kind: "refused",
10807
+ ...profile ? { profile } : {},
10808
+ reason: `candidate impact classification refused: ${error instanceof Error ? error.message : String(error)}`,
10809
+ ...relativeProfile ? { relativeProfilePath: relativeProfile } : {}
10810
+ };
10811
+ }
10812
+ };
10813
+ /**
10814
+ * Classify candidate impact from one exact base/head identity, as the routing
10815
+ * decision the hosted plane consumes. The result may withdraw hosted routing
10816
+ * work only; every doubt path returns a refused decision whose known targets
10817
+ * remain demanded.
10818
+ */
10819
+ const runCandidateImpact = (args) => {
10820
+ const cwd = path.resolve(args.cwd ?? process.cwd());
10821
+ const classification = classifyCandidateImpact(args);
10822
+ let decision;
10823
+ if (classification.kind === "classified") {
10824
+ const { baseSha, changedFiles, headSha, mergeBaseSha: mergeBase, stamp } = classification;
10825
+ const core = {
10826
+ baseSha,
10827
+ headSha,
10471
10828
  inertPaths: stamp.inertPaths,
10472
10829
  mergeBaseSha: mergeBase,
10473
10830
  schemaVersion: CANDIDATE_IMPACT_DECISION_SCHEMA_VERSION,
@@ -10500,16 +10857,15 @@ const runCandidateImpact = (args) => {
10500
10857
  state: "demanded"
10501
10858
  }))
10502
10859
  };
10503
- } catch (error) {
10504
- decision = refusal$1({
10505
- base: args.base,
10506
- head: args.head,
10507
- profile,
10508
- reason: `candidate impact classification refused: ${error instanceof Error ? error.message : String(error)}`
10509
- });
10510
- }
10860
+ } else decision = refusal$1({
10861
+ base: args.base,
10862
+ head: args.head,
10863
+ ...classification.profile ? { profile: classification.profile } : {},
10864
+ reason: classification.reason
10865
+ });
10511
10866
  if (args.emitRunContext) {
10512
10867
  const fullSuiteBaseRef = emptyTreeHash(cwd);
10868
+ const relativeProfile = classification.relativeProfilePath;
10513
10869
  const { env } = createVerificationRunContext({
10514
10870
  baseRef: decision.mergeBaseSha ?? fullSuiteBaseRef,
10515
10871
  changedFiles: decision.basis.changedFiles,
@@ -13067,6 +13423,60 @@ const reviewProofCheck = ({ candidate, candidateError, proof, reviewModes }) =>
13067
13423
  status: accepted ? "ok" : "warning"
13068
13424
  };
13069
13425
  };
13426
+ /**
13427
+ * Resolve the demand this candidate actually owes.
13428
+ *
13429
+ * The `pr:verify` stamp wins when one binds this candidate: readiness admits
13430
+ * on that stamp and nothing else, so the checklist must not tell a different
13431
+ * story about an already-verified candidate. Before `pr:verify` has stamped
13432
+ * anything there is no trusted stamp to read, and that is exactly the state
13433
+ * #963 reports from — so preflight runs the classifier itself. Preflight
13434
+ * never gates, so a stamp it computed can only inform; it never becomes
13435
+ * evidence, and readiness still requires the verified one.
13436
+ */
13437
+ const resolveRequiredCheckDemand = ({ candidate, cwd, profile, profilePath, verifyProof }) => {
13438
+ const stamped = trustedImpactStamp({
13439
+ candidate: {
13440
+ headSha: candidate.headSha,
13441
+ patchId: candidate.patchId
13442
+ },
13443
+ proof: verifyProof
13444
+ });
13445
+ if (stamped) return {
13446
+ basis: `the pr:verify proof's impact stamp for this candidate — ${impactStampSummaryLines(stamped)[0]}`,
13447
+ kind: "resolved",
13448
+ stamp: stamped
13449
+ };
13450
+ if ((profile.impact?.targets ?? []).length === 0) return {
13451
+ basis: "the profile declares no impact targets, so every required check is unconditional",
13452
+ kind: "resolved"
13453
+ };
13454
+ const classification = classifyCandidateImpact({
13455
+ base: candidate.mergeBaseSha,
13456
+ cwd,
13457
+ head: candidate.headSha,
13458
+ profilePath
13459
+ });
13460
+ if (classification.kind === "refused") return {
13461
+ kind: "unresolved",
13462
+ reason: classification.reason
13463
+ };
13464
+ return {
13465
+ basis: `candidate:impact over ${candidate.mergeBaseSha}..${candidate.headSha} — ${impactStampSummaryLines(classification.stamp)[0]}`,
13466
+ kind: "resolved",
13467
+ stamp: classification.stamp
13468
+ };
13469
+ };
13470
+ /** The row that names the authority on what this candidate owes (#963). */
13471
+ const requiredCheckDemandCheck = (resolution) => resolution.kind === "resolved" ? {
13472
+ message: `Required-check demand is resolved against this candidate by ${resolution.basis}. Rows below state their own resolution; a check reported as not demanded is not owed on this candidate.`,
13473
+ name: "admission:required-check-demand",
13474
+ status: "ok"
13475
+ } : {
13476
+ message: `Required-check demand is NOT resolvable for this candidate (${resolution.reason}), so every declared required check below is shown in force (fail closed). Resolve the refusal, or read candidate:impact directly, before treating the rows below as the demand set.`,
13477
+ name: "admission:required-check-demand",
13478
+ status: "warning"
13479
+ };
13070
13480
  const requiredCheckMessage = (outcome) => {
13071
13481
  if (outcome.status === "out-of-scope") return `Not demanded for this candidate: ${outcome.scopeReason}.`;
13072
13482
  if (outcome.status === "satisfied") return `Satisfied by a ${outcome.checkType}-type evidence envelope${outcome.matchedPath ? ` (${outcome.matchedPath})` : ""}.`;
@@ -13078,7 +13488,7 @@ const requiredCheckMessage = (outcome) => {
13078
13488
  * readiness's own `evaluateRequiredChecks`, so scoping, freshness and
13079
13489
  * independence stay exactly one implementation.
13080
13490
  */
13081
- const requiredCheckChecks = ({ candidate, candidateError, cwd, profile, verifyProof }) => {
13491
+ const requiredCheckChecks = ({ candidate, candidateError, cwd, profile, profilePath, verifyProof }) => {
13082
13492
  const requiredChecks = resolveProfileRequiredChecks(profile);
13083
13493
  if (requiredChecks.length === 0) return [{
13084
13494
  message: "Profile declares no requiredChecks; no external evidence envelope (including a preview-deploy proof) is demanded.",
@@ -13090,13 +13500,14 @@ const requiredCheckChecks = ({ candidate, candidateError, cwd, profile, verifyPr
13090
13500
  name: "admission:required-checks",
13091
13501
  status: "warning"
13092
13502
  }];
13093
- const impactStamp = trustedImpactStamp({
13094
- candidate: {
13095
- headSha: candidate.headSha,
13096
- patchId: candidate.patchId
13097
- },
13098
- proof: verifyProof
13503
+ const demand = resolveRequiredCheckDemand({
13504
+ candidate,
13505
+ cwd,
13506
+ profile,
13507
+ profilePath,
13508
+ verifyProof
13099
13509
  });
13510
+ const impactStamp = demand.kind === "resolved" ? demand.stamp : void 0;
13100
13511
  const { outcomes } = evaluateRequiredChecks({
13101
13512
  authoringSessionIds: resolveAuthoringSessionIds({ recorded: verifyProof?.authoringSession }),
13102
13513
  candidate: {
@@ -13111,11 +13522,11 @@ const requiredCheckChecks = ({ candidate, candidateError, cwd, profile, verifyPr
13111
13522
  ...impactStamp ? { impactStamp } : {}
13112
13523
  }
13113
13524
  });
13114
- return outcomes.map((outcome) => ({
13525
+ return [requiredCheckDemandCheck(demand), ...outcomes.map((outcome) => ({
13115
13526
  message: requiredCheckMessage(outcome),
13116
13527
  name: `admission:required-check:${outcome.name}`,
13117
13528
  status: outcome.status === "unmet" ? "warning" : "ok"
13118
- }));
13529
+ }))];
13119
13530
  };
13120
13531
  /**
13121
13532
  * The full admission checklist for the current repository and candidate, in the
@@ -13124,7 +13535,7 @@ const requiredCheckChecks = ({ candidate, candidateError, cwd, profile, verifyPr
13124
13535
  * requirement is reported by the doctor `github-app` check, which states the
13125
13536
  * consequence of its absence.
13126
13537
  */
13127
- const admissionPreflightChecks = ({ base = "origin/main", cwd, env = process.env, hqIngestCredentials, profile, reviewProofPath = DEFAULT_PR_REVIEW_PROOF_PATH, verifyProofPath = DEFAULT_PR_VERIFY_PROOF_PATH }) => {
13538
+ const admissionPreflightChecks = ({ base = "origin/main", cwd, env = process.env, hqIngestCredentials, profile, profilePath, reviewProofPath = DEFAULT_PR_REVIEW_PROOF_PATH, verifyProofPath = DEFAULT_PR_VERIFY_PROOF_PATH }) => {
13128
13539
  const resolvedVerifyProofPath = path.resolve(cwd, verifyProofPath);
13129
13540
  const preread = tryReadProof(readPrVerifyProof, resolvedVerifyProofPath);
13130
13541
  const resolved = resolveCandidate({
@@ -13154,6 +13565,7 @@ const admissionPreflightChecks = ({ base = "origin/main", cwd, env = process.env
13154
13565
  candidateError,
13155
13566
  cwd,
13156
13567
  profile,
13568
+ profilePath,
13157
13569
  verifyProof: preread.proof
13158
13570
  }),
13159
13571
  hqIngestCredentialCheck({
@@ -13239,6 +13651,7 @@ async function doctorProjectProfile(input = {}) {
13239
13651
  }, input.hqSpoolDependencies);
13240
13652
  const checks = [
13241
13653
  ...buildProfileChecks(profile, cwd, {
13654
+ env,
13242
13655
  userConfig: userConfig.loaded?.config,
13243
13656
  userConfigCheck: userConfig.check
13244
13657
  }),
@@ -13317,17 +13730,24 @@ function buildProfileChecks(profile, cwd, userConfigInput) {
13317
13730
  },
13318
13731
  reviewLadderCheck(),
13319
13732
  userConfigInput.userConfigCheck,
13320
- githubAppCheck(userConfigInput.userConfig),
13733
+ githubAppCheck(userConfigInput.userConfig, userConfigInput.env),
13321
13734
  gitRemoteCheck(profile, cwd)
13322
13735
  ];
13323
13736
  }
13324
- function githubAppCheck(userConfig) {
13737
+ function githubAppCheck(userConfig, env) {
13325
13738
  const app = userConfig?.githubApp;
13326
- if (!app) return {
13327
- message: "Patronage Factory GitHub App credentials are not configured; gate publishing will use user-token commit statuses with Details links. Proof bindings cannot publish, so hosted required checks cannot be satisfied by local proof and must go green by their own means.",
13328
- name: "github-app",
13329
- status: "warning"
13330
- };
13739
+ if (!app) {
13740
+ if (env.CURSOR_AGENT_SOCKET !== void 0 && env.CURSOR_AGENT_SOCKET.length > 0) return {
13741
+ message: "Patronage Factory GitHub App PEM is not configured; this Cursor-managed VM publishes patronage-factory/pr-ready through the Factory App broker (Cursor OIDC, audience Factory). Missing exchange fails closed.",
13742
+ name: "github-app",
13743
+ status: "warning"
13744
+ };
13745
+ return {
13746
+ message: "Patronage Factory GitHub App credentials are not configured; gate publishing will use user-token commit statuses with Details links. Proof bindings cannot publish, so hosted required checks cannot be satisfied by local proof and must go green by their own means.",
13747
+ name: "github-app",
13748
+ status: "warning"
13749
+ };
13750
+ }
13331
13751
  if (!existsSync(app.privateKeyPath)) return {
13332
13752
  message: `Patronage Factory GitHub App id ${app.appId} is configured, but private key ${app.privateKeyPath} was not found.`,
13333
13753
  name: "github-app",
@@ -13391,7 +13811,7 @@ function repositoryUrlMatches(origin, profile) {
13391
13811
  //#endregion
13392
13812
  //#region src/commands/doctor.ts
13393
13813
  function createDoctorCommand(output) {
13394
- return markCwdOptionDefault(new Command("doctor").description("Validate a project profile without mutating GitHub or local files").option("--base <ref>", "base branch or ref for --preflight", "origin/main").option("--cwd <path>", "working directory to validate", collectCwdOption).option("--json", "print the doctor report as JSON").option("--preflight", "also list the pre-checkable admission requirements for the current candidate (read-only; never blocks); findings-file shape and wave membership have no read-only pre-check").option("--profile <path>", "path to the project profile JSON file").action(async (options) => {
13814
+ return markCwdOptionDefault(new Command("doctor").description("Validate a project profile without mutating GitHub or local files").option("--base <ref>", "base branch or ref for --preflight", "origin/main").option("--cwd <path>", "working directory to validate", collectCwdOption).option("--json", "print the doctor report as JSON").option("--preflight", "also list the pre-checkable admission requirements for the current candidate (read-only; never blocks); each required check states whether this candidate is demanded to satisfy it, resolved by candidate:impact; findings-file shape and wave membership have no read-only pre-check").option("--profile <path>", "path to the project profile JSON file").action(async (options) => {
13395
13815
  const report = await doctorProjectProfile({
13396
13816
  base: options.base,
13397
13817
  cwd: resolveCwdOption(options.cwd),
@@ -16187,10 +16607,10 @@ function defaultPrReadyGit() {
16187
16607
  ], cwd);
16188
16608
  },
16189
16609
  mergeBaseSha,
16190
- objectIdAtRef: (cwd, ref, absolutePath) => {
16191
- const relativePath = path.relative(cwd, absolutePath);
16610
+ objectIdAtRef: (cwd, ref, repositoryRelativePath) => {
16611
+ assertRepositoryRelativePath(repositoryRelativePath, "objectIdAtRef");
16192
16612
  try {
16193
- return runCapture("git", ["rev-parse", `${ref}:${relativePath}`], cwd).stdout.trim();
16613
+ return runCapture("git", ["rev-parse", `${ref}:${repositoryRelativePath}`], cwd).stdout.trim();
16194
16614
  } catch {}
16195
16615
  },
16196
16616
  resolveCommitSha: (cwd, ref) => {
@@ -16425,9 +16845,9 @@ const resolveExternalEvidenceInputs = ({ args, cwd, recordedAuthoringSession, re
16425
16845
  externalRequiredChecks: requiredChecks
16426
16846
  };
16427
16847
  };
16428
- function policyProfileAtCommittedHead({ cwd, git, headSha, profilePath }) {
16429
- const content = git.showFileAtRef(cwd, headSha, profilePath);
16430
- const blobSha = git.objectIdAtRef(cwd, headSha, profilePath);
16848
+ function policyProfileAtCommittedHead({ cwd, git, headSha, repositoryRelativeProfile }) {
16849
+ const content = git.showFileAtRef(cwd, headSha, repositoryRelativeProfile);
16850
+ const blobSha = git.objectIdAtRef(cwd, headSha, repositoryRelativeProfile);
16431
16851
  const committedProfile = content ? tryParseProfileContent(content) : void 0;
16432
16852
  if (!(committedProfile && blobSha && /^[0-9a-f]{40}$/u.test(blobSha))) throw new Error("Could not read, validate, and identify readiness policy from the committed PR head; fetch the head and re-run pr:ready.");
16433
16853
  return {
@@ -16435,11 +16855,6 @@ function policyProfileAtCommittedHead({ cwd, git, headSha, profilePath }) {
16435
16855
  profile: committedProfile
16436
16856
  };
16437
16857
  }
16438
- function checkoutRelativeProfilePath(cwd, profilePath) {
16439
- const relativePath = path.relative(cwd, profilePath);
16440
- if (relativePath.length === 0 || relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath)) throw new Error("The readiness profile must be inside the trusted checkout.");
16441
- return relativePath.split(path.sep).join("/");
16442
- }
16443
16858
  function readinessProfilePath(cwd, requestedPath) {
16444
16859
  return path.resolve(cwd, requestedPath ?? "software-factory.profile.json");
16445
16860
  }
@@ -16499,15 +16914,18 @@ async function runPrReady(args, dependencies = {}) {
16499
16914
  hudMergeBaseSha = resolvedMergeBaseSha;
16500
16915
  hudPatchId = patchId;
16501
16916
  const files = git.changedFiles(cwd, authoritativeBaseSha, pr.headRefOid);
16917
+ const committedProfilePath = repositoryContainedProfilePath({
16918
+ cwd,
16919
+ profilePath
16920
+ });
16502
16921
  const committedPolicy = policyProfileAtCommittedHead({
16503
16922
  cwd,
16504
16923
  git,
16505
16924
  headSha: pr.headRefOid,
16506
- profilePath
16925
+ repositoryRelativeProfile: committedProfilePath
16507
16926
  });
16508
16927
  const policyProfile = committedPolicy.profile;
16509
16928
  if (repositorySlug(policyProfile.repository) !== checkoutRepositorySlug) throw new Error(`Committed readiness profile repository ${repositorySlug(policyProfile.repository)} does not match trusted checkout repository ${checkoutRepositorySlug}.`);
16510
- const committedProfilePath = checkoutRelativeProfilePath(cwd, profilePath);
16511
16929
  const policy = resolveReviewDemand({
16512
16930
  authority: dependencies.basePolicy ?? defaultBaseReviewPolicyAuthority(),
16513
16931
  callerBaseRef: pr.baseRefName,
@@ -16740,7 +17158,12 @@ async function runPrReady(args, dependencies = {}) {
16740
17158
  repo: repository.name,
16741
17159
  sha: presentedHeadSha
16742
17160
  };
16743
- if (!await (dependencies.publishFinalCheckRun ? dependencies.publishFinalCheckRun(failureCheckInput) : ensureFactoryCheckRunPublished(failureCheckInput, { onDiagnostic: (diagnostic) => process.stderr.write(diagnostic) }))) console.warn(`pr:ready could not publish the failing ${FACTORY_CHECK_NAMES["pr-ready"]} check run for ${presentedHeadSha}; the required check is absent rather than red.`);
17161
+ try {
17162
+ if (!await (dependencies.publishFinalCheckRun ? dependencies.publishFinalCheckRun(failureCheckInput) : ensureFactoryCheckRunPublished(failureCheckInput, { onDiagnostic: (diagnostic) => process.stderr.write(diagnostic) }))) console.warn(`pr:ready could not publish the failing ${FACTORY_CHECK_NAMES["pr-ready"]} check run for ${presentedHeadSha}; the required check is absent rather than red.`);
17163
+ } catch (publishError) {
17164
+ const publishMessage = publishError instanceof Error ? publishError.message : String(publishError);
17165
+ console.warn(`pr:ready could not publish the failing ${FACTORY_CHECK_NAMES["pr-ready"]} check run for ${presentedHeadSha}; the required check is absent rather than red. ${publishMessage}`);
17166
+ }
16744
17167
  await presentHud();
16745
17168
  }
16746
17169
  throw error;
@@ -17149,7 +17572,7 @@ const readCodingStandardsAtHead = ({ cwd, git, headSha }) => {
17149
17572
  } catch {
17150
17573
  return;
17151
17574
  }
17152
- return (git?.showFileAtRef ?? showFileAtRef)(root, headSha, path.join(root, CODING_STANDARDS_FILE));
17575
+ return (git?.showFileAtRef ?? showFileAtRef)(root, headSha, CODING_STANDARDS_FILE);
17153
17576
  };
17154
17577
  const codingStandardsSegment = ({ contents, headSha }) => {
17155
17578
  const text = contents?.replace(/\n+$/u, "");
@@ -17474,7 +17897,10 @@ const reviewPolicyIdentity = ({ args, changedFiles: files, cwd, dependencies, gi
17474
17897
  changedFiles: files,
17475
17898
  cwd,
17476
17899
  lookupPullRequest: dependencies.lookupPullRequest ?? lookupCandidatePullRequest,
17477
- profilePath: path.relative(cwd, profilePath).split(path.sep).join("/"),
17900
+ profilePath: repositoryRelativeProfilePath({
17901
+ cwd,
17902
+ profilePath
17903
+ }),
17478
17904
  readCheckoutRepository: git.checkoutRepository ?? checkoutRepository
17479
17905
  });
17480
17906
  /**
@@ -17839,11 +18265,7 @@ const defaultFollowUpRunner = (factoryCliInvocation) => (followUp, cwd) => {
17839
18265
  const [command, ...args] = resolveFollowUpArgv(followUp, factoryCliInvocation);
17840
18266
  runCapture(command, args, cwd);
17841
18267
  };
17842
- const defaultPrPublishCliInvocation = () => {
17843
- const moduleExtension = path.extname(import.meta.filename);
17844
- return defaultFactoryCliInvocation(path.join(import.meta.dirname, `index${moduleExtension}`));
17845
- };
17846
- const prPublishCliInvocation = (explicit) => explicit ?? defaultPrPublishCliInvocation();
18268
+ const prPublishCliInvocation = (explicit) => explicit ?? defaultFactoryCliInvocation(factoryCliEntry());
17847
18269
  const errorMessage = (error) => error instanceof Error ? error.message : String(error);
17848
18270
  /**
17849
18271
  * GitHub creates a pull request against a branch name, while the rest of the
@@ -18704,8 +19126,11 @@ const runProductionImpact = (args) => {
18704
19126
  const stamp = computeImpactStamp({
18705
19127
  changedFiles,
18706
19128
  profile,
18707
- profilePath: path.relative(cwd, profilePath),
18708
- readLockfile: (side) => showFileAtRef(cwd, side === "base" ? before : after, path.join(cwd, LOCKFILE_PATH))
19129
+ profilePath: repositoryRelativeProfilePath({
19130
+ cwd,
19131
+ profilePath
19132
+ }),
19133
+ readLockfile: (side) => showFileAtRef(cwd, side === "base" ? before : after, LOCKFILE_PATH)
18709
19134
  });
18710
19135
  const core = {
18711
19136
  afterSha: after,
@@ -19496,7 +19921,7 @@ function formatScratchFileCheckSummary(check) {
19496
19921
  //#endregion
19497
19922
  //#region src/index.ts
19498
19923
  function createProgram(options = {}) {
19499
- const factoryCliInvocation = options.factoryCliInvocation ? FactoryCliInvocationSchema.parse(options.factoryCliInvocation) : defaultFactoryCliInvocation(import.meta.filename);
19924
+ const factoryCliInvocation = options.factoryCliInvocation ? FactoryCliInvocationSchema.parse(options.factoryCliInvocation) : defaultFactoryCliInvocation(factoryCliEntry());
19500
19925
  const output = options.output ?? {
19501
19926
  stderr: process.stderr,
19502
19927
  stdout: process.stdout
@@ -19526,24 +19951,18 @@ function createProgram(options = {}) {
19526
19951
  program.addCommand(createDemandWaiveCommand(output, options.actions?.demandWaive));
19527
19952
  return program;
19528
19953
  }
19529
- async function run(argv = process.argv) {
19954
+ /**
19955
+ * Parse `argv` and run the matching command. `cliEntry` is the path of the
19956
+ * runnable bin module, which follow-up commands spawn; the bin entry passes its
19957
+ * own path. This function never runs itself — `./cli.js` is the only caller
19958
+ * that does (#386).
19959
+ */
19960
+ async function run(argv = process.argv, cliEntry = factoryCliEntry()) {
19530
19961
  try {
19531
- await createProgram({ factoryCliInvocation: defaultFactoryCliInvocation(import.meta.filename) }).parseAsync(argv);
19962
+ await createProgram({ factoryCliInvocation: defaultFactoryCliInvocation(cliEntry) }).parseAsync(argv);
19532
19963
  } finally {
19533
19964
  await awaitPendingHqIngest();
19534
19965
  }
19535
19966
  }
19536
- function isDirectCliExecution() {
19537
- const [, entryPath] = process.argv;
19538
- if (!entryPath) return false;
19539
- return import.meta.url === pathToFileURL(realpathSync(entryPath)).href;
19540
- }
19541
- if (isDirectCliExecution()) try {
19542
- await run();
19543
- } catch (error) {
19544
- if (error instanceof PrPublishFollowUpError) console.error(JSON.stringify(error.result, null, 2));
19545
- else console.error("\nError:", error instanceof Error ? error.message : String(error));
19546
- process.exit(1);
19547
- }
19548
19967
  //#endregion
19549
19968
  export { DEFAULT_DEMAND_WAIVER_PATH, DEFAULT_FACTORY_REPOSITORY, DEFAULT_ROOT_SHARED_GLOBS, DemandWaiveRefusalError, EMPTY_TREE_OBJECT_HASH, EPIC_STRUCTURE_NODE_STATUSES, EPIC_STRUCTURE_SCHEMA_VERSION, EpicStructureValidationError, FACTORY_BASE_REF_ENV, FACTORY_CHANGED_FILES_ENV, FACTORY_CHANGED_FILES_FILE_ENV, FactoryCliInvocationSchema, FollowUpActionSchema, MERGE_FREEZE_APP_SLUG, MERGE_FREEZE_CHECK_NAME, PrPublishFollowUpError, REVIEW_FOCUS_SECTION, WorkerCheckoutGuardError, appendReviewLadderStageTraceEvent, appendWithTraceSinks, applyDemandWaiver, assembleReviewPrompt, assertFactoryPrStatusIdentity, assertWorkerCheckoutAllowed, authorizeDemandWaiver, batteryScopeSummaryLines, blockingLadderFindings, boundary_manifest_exports as boundaryManifest, boundary_review_proof_exports as boundaryReviewProof, buildEpicStructureEvent, buildEpicStructurePayload, buildEvidenceEnvelope, buildReviewGateNotRequiredTraceEvent, buildReviewLadderStageTraceEvent, comment_provenance_exports as commentProvenance, createGitHubCheckRunMergeFreezeStore, createLocalJsonlTraceSink, createProgram, createVerificationRunContext, doctorProjectProfile, epicStructureEventId, evaluateReviewLadder, evidenceEnvelopeFilename, findingKey, followUpFromArgv, impactStampScopeDecision, inferFixedInThreadDispositions, isProductionHqUrl, loadProjectProfile, normalizeIssueComments, openLadderFindings, planFactoryPrStatusHud, planPreviewLifecycle, planPublishFollowUp, planVerificationBattery, readiness_evaluation_exports as prReadinessEvaluation, external_evidence_exports as prReadinessExternalEvidence, pr_body_renderer_exports as prReadinessPrBodyRenderer, proof_identity_exports as prReadinessProofIdentity, review_proof_exports as prReadinessReviewProof, status_check_rollup_exports as prReadinessStatusChecks, verification_proof_exports as prReadinessVerificationProof, presentFactoryPrStatusHud, previewLifecycleTargets, publishEpicStructure, readDemandWaivers, readFactoryPrStatusSources, readPrReadyProof, readPrReviewProof, refreshFactoryPrStatusHud, refreshFactoryPrStatusHudSafely, renderFactoryPrStatusHud, resolveFactoryRepository, resolveFindingBlocking, resolveReviewFindingCategory, resolveReviewFindingSeverity, resolveReviewLadderPolicy, resolveTraceWriteSinks, reviewCycleStateFor, reviewFocusFromIssueBody, reviewPromptSectionSchema, reviewPromptSectionsSchema, run, runBoundaryCheck, runDemandWaive, runEvidenceEmit, runPrPublish, runPrReady, runPrReview, runPrVerify, scanFactoryTraceDiagnostics, scanFactoryTraceEvents, selectWaiversForCandidate, staleRepeatLadderFindings, toFactoryTraceEnvelope, tryAppendReviewGateNotRequiredTraceEvent, tryAppendReviewLadderStageTraceEvent, validateBoundaryCheckProof, validateDagDocument, validateDemandWaiverStore, validateFactoryTraceEvent, validateMergeFreezeState, validatePrReadyProof, validatePrReviewProof, validatePrVerifyProof, validatePreviewLifecyclePlan, waivedDemandNotice, waivedDemandSchema, worktree_scratch_files_exports as worktreeScratchFiles };