@patronage/software-factory 1.0.0-alpha.23 → 1.0.0-alpha.24

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
@@ -7,6 +7,7 @@ import path from "node:path";
7
7
  import crypto, { createHash, randomBytes, randomUUID } from "node:crypto";
8
8
  import { z } from "zod";
9
9
  import { execFileSync, spawnSync } from "node:child_process";
10
+ import { Buffer as Buffer$1 } from "node:buffer";
10
11
  import { link, lstat, mkdir, open, readFile, readdir, realpath, rename, stat, unlink } from "node:fs/promises";
11
12
  import os, { homedir, tmpdir } from "node:os";
12
13
  import { setImmediate } from "node:timers";
@@ -17,15 +18,15 @@ import { parse } from "yaml";
17
18
  import { FACTORY_PROOF_GATE_APP_ID, FACTORY_PROOF_GATE_STEP_NAME, GitHubApiError, mintInstallationToken, previewProofInventory, productionImpactTargetOutput, readVitestProfileDocument } from "@patronage/factory-ci";
18
19
  import { promisify } from "node:util";
19
20
  //#region package.json
20
- var version = "1.0.0-alpha.23";
21
+ var version = "1.0.0-alpha.24";
21
22
  //#endregion
22
23
  //#region src/review-rungs.ts
23
- const EVIDENCE_REVIEW_RUNGS$1 = [
24
+ const EVIDENCE_REVIEW_RUNGS = [
24
25
  "independent-model",
25
26
  "oracle",
26
27
  "human"
27
28
  ];
28
- const RUNG_ORDER = Object.fromEntries(EVIDENCE_REVIEW_RUNGS$1.map((rung, index) => [rung, index]));
29
+ const RUNG_ORDER = Object.fromEntries(EVIDENCE_REVIEW_RUNGS.map((rung, index) => [rung, index]));
29
30
  //#endregion
30
31
  //#region src/boundary-manifest.ts
31
32
  var boundary_manifest_exports = /* @__PURE__ */ __exportAll({
@@ -43,7 +44,7 @@ var boundary_manifest_exports = /* @__PURE__ */ __exportAll({
43
44
  const FACTORY_BOUNDARY_FENCE_LANG = "factory-boundary";
44
45
  const BOUNDARY_CLOSEOUT_DEFAULT_RUNG = "oracle";
45
46
  const rungMeetsMinimum = (rung, minimum) => RUNG_ORDER[rung] >= RUNG_ORDER[minimum];
46
- const rungSchema = z.enum(EVIDENCE_REVIEW_RUNGS$1);
47
+ const rungSchema = z.enum(EVIDENCE_REVIEW_RUNGS);
47
48
  const BOUNDARY_TOPOLOGIES = [
48
49
  "each-to-main",
49
50
  "flagged",
@@ -358,7 +359,7 @@ const boundaryReviewProofSchema$1 = z.object({
358
359
  }).passthrough(),
359
360
  outcome: z.enum(["pass", "fail"]),
360
361
  producer: z.string().min(1),
361
- rung: z.enum(EVIDENCE_REVIEW_RUNGS$1),
362
+ rung: z.enum(EVIDENCE_REVIEW_RUNGS),
362
363
  schemaVersion: z.literal(1),
363
364
  sessionId: z.string().min(1).optional(),
364
365
  specHash: z.object({
@@ -999,6 +1000,9 @@ function runInheritedTee(command, args, cwd, extraEnv = {}) {
999
1000
  function currentHeadSha(cwd) {
1000
1001
  return runCapture("git", ["rev-parse", "HEAD"], cwd).stdout.trim();
1001
1002
  }
1003
+ function repositoryRoot(cwd) {
1004
+ return runCapture("git", ["rev-parse", "--show-toplevel"], cwd).stdout.trim();
1005
+ }
1002
1006
  function showFileAtRef(cwd, ref, absolutePath) {
1003
1007
  const rel = path.relative(cwd, absolutePath);
1004
1008
  const spec = rel.startsWith(".") ? rel : `./${rel}`;
@@ -1199,7 +1203,33 @@ const normalizeIssueComments = (value) => {
1199
1203
  const runGhCapture = (args, cwd = process.cwd(), acceptableExitCodes = [0]) => runCapture("gh", args, cwd, { acceptableExitCodes });
1200
1204
  const runGhJsonAt = (args, cwd) => JSON.parse(runGhCapture(args, cwd).stdout);
1201
1205
  const runGhJson = (args) => runGhJsonAt(args, process.cwd());
1202
- z.object({ object: z.object({ sha: z.string().regex(/^[0-9a-f]{40}$/u) }) });
1206
+ const gitReferenceSchema = z.object({ object: z.object({ sha: z.string().regex(/^[0-9a-f]{40}$/u) }) });
1207
+ /** Read a branch tip from GitHub rather than a checkout's cached remote ref. */
1208
+ const fetchGitHubBranchTipSha = (repository, branch, cwd) => {
1209
+ return gitReferenceSchema.parse(runGhJsonAt(["api", `/repos/${repository.owner}/${repository.name}/git/ref/heads/${encodeURIComponent(branch)}`], cwd)).object.sha;
1210
+ };
1211
+ const repositoryFileSchema = z.object({
1212
+ content: z.string(),
1213
+ encoding: z.literal("base64"),
1214
+ type: z.literal("file")
1215
+ });
1216
+ /**
1217
+ * Read one file's content at an exact commit from GitHub.
1218
+ *
1219
+ * #922: the authoritative base policy is the profile GitHub serves at the base
1220
+ * tip. A checkout is candidate-controlled, so it is not an authority for it.
1221
+ * There is exactly one implementation on purpose — no local-first read with an
1222
+ * API fallback, which would let a checkout answer the authority's question.
1223
+ *
1224
+ * Throws for every failure — an absent path, a directory, a file too large for
1225
+ * the contents endpoint to inline, an unreachable or unauthorized API. The
1226
+ * caller decides what an unreadable authority means; it is never an absent one.
1227
+ */
1228
+ const fetchGitHubFileAtRef = (repository, filePath, ref, cwd) => {
1229
+ const encodedPath = filePath.split("/").map((segment) => encodeURIComponent(segment)).join("/");
1230
+ const response = repositoryFileSchema.parse(runGhJsonAt(["api", `/repos/${repository.owner}/${repository.name}/contents/${encodedPath}?ref=${encodeURIComponent(ref)}`], cwd));
1231
+ return Buffer$1.from(response.content, "base64").toString("utf-8");
1232
+ };
1203
1233
  /**
1204
1234
  * Fetch and normalize the issue/PR comment thread for `<owner>/<repo>`.
1205
1235
  *
@@ -1260,15 +1290,15 @@ function matchesAnyGlob(patterns, file) {
1260
1290
  }
1261
1291
  //#endregion
1262
1292
  //#region src/diff-classification.ts
1263
- const DIFF_CLASSIFICATIONS$1 = [
1293
+ const DIFF_CLASSIFICATIONS = [
1264
1294
  "docs/process-only",
1265
1295
  "trivial",
1266
1296
  "non-trivial"
1267
1297
  ];
1268
1298
  //#endregion
1269
1299
  //#region src/required-check-policy.ts
1270
- const EVIDENCE_CHECK_TYPES$1 = ["review", "verify"];
1271
- const requiredCheckScopeSchema$1 = z.object({ classifications: z.array(z.enum(DIFF_CLASSIFICATIONS$1)).min(1) }).strict();
1300
+ const EVIDENCE_CHECK_TYPES = ["review", "verify"];
1301
+ const requiredCheckScopeSchema = z.object({ classifications: z.array(z.enum(DIFF_CLASSIFICATIONS)).min(1) }).strict();
1272
1302
  const requiredCheckInScope = ({ context, scope }) => {
1273
1303
  if (!scope) return {
1274
1304
  inScope: true,
@@ -1316,6 +1346,20 @@ const commandSchema = z.object({
1316
1346
  ]).default("full")
1317
1347
  });
1318
1348
  const conditionalReviewSchema = z.object({ paths: z.array(z.string().min(1).refine(isValidGlob, "must be a valid glob")).min(1) }).strict();
1349
+ /**
1350
+ * The `review` subtree: the repository-declared paths that additionally demand
1351
+ * a security review.
1352
+ *
1353
+ * Exported so the authoritative-base-policy reader (#922) can validate the
1354
+ * subtree WITHOUT the rest of the profile. That reader needs this fact and only
1355
+ * this fact, and it reads a profile committed on the base ref, which may sit at
1356
+ * the previous `schemaVersion` while a bump is in flight. Validating the whole
1357
+ * document there would refuse every open pull request in a repository for the
1358
+ * length of a schema bump, with no in-factory route to land the fix (the sf#489
1359
+ * class: a guardrail must not lock the operator out). One definition, two parse
1360
+ * scopes.
1361
+ */
1362
+ const reviewPolicySchema = z.object({ conditional: z.array(conditionalReviewSchema).min(1) }).strict();
1319
1363
  const hqEndpointDeclarationSchema = z.intersection(z.string().url(), z.string().regex(/^https:\/\/[^/@\s]+(?:[/?#]|$)/u, "Must use HTTPS without embedded credentials"));
1320
1364
  const hqIngestConfigSchema = z.object({
1321
1365
  enabled: z.boolean(),
@@ -1328,7 +1372,7 @@ const hqIngestConfigSchema = z.object({
1328
1372
  }
1329
1373
  }, "Must be an HTTPS origin without a path, query, fragment, or embedded credentials").describe("Repository-declared HQ origin (scheme and host only; the factory composes the ingest path). Credential delivery additionally requires exact-origin authorization in the operator user config hqAllowedOrigins list.")
1330
1374
  }).strict();
1331
- const profileRequiredCheckScopeSchema = requiredCheckScopeSchema$1.extend({ classifications: z.array(z.enum(["docs/process-only", "non-trivial"])).min(1) }).strict();
1375
+ const profileRequiredCheckScopeSchema = requiredCheckScopeSchema.extend({ classifications: z.array(z.enum(["docs/process-only", "non-trivial"])).min(1) }).strict();
1332
1376
  const requiredCheckSchema = z.object({
1333
1377
  name: z.string().min(1),
1334
1378
  scope: profileRequiredCheckScopeSchema.optional()
@@ -1369,7 +1413,7 @@ const factoryProjectProfileSchema = z.object({
1369
1413
  message: "requiredChecks entries must have distinct names."
1370
1414
  });
1371
1415
  }).optional(),
1372
- review: z.object({ conditional: z.array(conditionalReviewSchema).min(1) }).strict().optional(),
1416
+ review: reviewPolicySchema.optional(),
1373
1417
  schemaVersion: z.literal(6),
1374
1418
  verification: z.object({ commands: z.array(commandSchema).min(1) })
1375
1419
  }).strict().superRefine((profile, context) => {
@@ -3765,7 +3809,7 @@ const qualifierIsMintable = (family, qualifier) => {
3765
3809
  if (!QUALIFIER_PATTERN.test(qualifier)) return false;
3766
3810
  const decoded = decodeQualifier(qualifier);
3767
3811
  if (decoded === void 0 || demandQualifier(decoded) !== qualifier) return false;
3768
- if (family === "review-rung") return EVIDENCE_REVIEW_RUNGS$1.includes(decoded);
3812
+ if (family === "review-rung") return EVIDENCE_REVIEW_RUNGS.includes(decoded);
3769
3813
  if (family === "review-mode") return REVIEW_MODE_DEMAND_VALUES.includes(decoded);
3770
3814
  return true;
3771
3815
  };
@@ -3928,8 +3972,64 @@ const authorizeDemandWaiver = ({ authenticatedLogin, session }) => {
3928
3972
  } };
3929
3973
  };
3930
3974
  //#endregion
3975
+ //#region src/policy-resolution.ts
3976
+ const COMMIT_SHA_PATTERN$2 = /^[0-9a-f]{40}$/u;
3977
+ const digestSchema = z.string().regex(/^[0-9a-f]{64}$/u);
3978
+ /**
3979
+ * The authority resolved: the live tip of the PR's own base ref served a
3980
+ * parseable profile, and demand was resolved from the union of that policy and
3981
+ * the candidate's.
3982
+ *
3983
+ * Every identity field is REQUIRED. A cross-stage reader compares these digests
3984
+ * to decide whether two stages judged the same candidate under the same policy;
3985
+ * an `authoritative` record with a digest missing would read to that reader as
3986
+ * "nothing disagrees", which is a silent downgrade of exactly the demand this
3987
+ * record exists to protect. `reason` is forbidden here — a resolved authority
3988
+ * has nothing to explain.
3989
+ */
3990
+ const authoritativePolicyResolutionSchema = z.object({
3991
+ /** sha256 of the canonical base `review` subtree. */
3992
+ basePolicyDigest: digestSchema,
3993
+ /** The PR's own base ref, whose live tip is the authority. */
3994
+ baseRefName: z.string().min(1),
3995
+ /** sha256 of the canonical candidate `review` subtree. */
3996
+ candidatePolicyDigest: digestSchema,
3997
+ /** sha256 of the merged (base ∪ candidate) protected-path set. */
3998
+ effectiveDigest: digestSchema,
3999
+ /** The base-ref tip the base policy was read at. */
4000
+ policyBaseSha: z.string().regex(COMMIT_SHA_PATTERN$2),
4001
+ /**
4002
+ * Whether the candidate's `review` subtree differs from the base's. The
4003
+ * digests prove which policies were read; this says whether they agreed,
4004
+ * which is the fact that explains a demand the candidate's own profile
4005
+ * would not have produced.
4006
+ */
4007
+ reviewPolicyChanged: z.boolean(),
4008
+ status: z.literal("authoritative")
4009
+ }).strict();
4010
+ /**
4011
+ * The authority could not be read, and `reason` says why — required, because an
4012
+ * unresolved record with no reason is an unactionable refusal. `pr:ready`
4013
+ * refuses before it writes a proof, so a Slice A ready proof never carries one;
4014
+ * this member is the recorded form of that refusal for the stages that carry it
4015
+ * forward.
4016
+ */
4017
+ const unresolvedPolicyResolutionSchema = z.object({
4018
+ /** The base ref whose policy could not be read. */
4019
+ baseRefName: z.string().min(1),
4020
+ /** Why the authority is unresolved. */
4021
+ reason: z.string().min(1),
4022
+ status: z.literal("unresolved")
4023
+ }).strict();
4024
+ /** How a gate resolved the review policy it evaluated a candidate under. */
4025
+ const policyResolutionSchema = z.discriminatedUnion("status", [authoritativePolicyResolutionSchema, unresolvedPolicyResolutionSchema]);
4026
+ //#endregion
3931
4027
  //#region src/packages/review-prompt-sections/schema.ts
3932
- const REVIEW_PROMPT_SECTION_PROVENANCES = ["prior-review-ledger", "issue-review-focus"];
4028
+ const REVIEW_PROMPT_SECTION_PROVENANCES = [
4029
+ "coding-standards",
4030
+ "prior-review-ledger",
4031
+ "issue-review-focus"
4032
+ ];
3933
4033
  const nonBlankString$2 = z.string().refine((value) => value.trim().length > 0, { message: "must not be blank" });
3934
4034
  const reviewPromptSectionSchema = z.object({
3935
4035
  provenance: z.enum(REVIEW_PROMPT_SECTION_PROVENANCES),
@@ -4780,7 +4880,7 @@ const prReviewResultSchema = z.object({
4780
4880
  ]),
4781
4881
  producer: nonBlankString$1.optional(),
4782
4882
  promptSections: reviewPromptSectionsSchema.optional(),
4783
- rung: z.enum(EVIDENCE_REVIEW_RUNGS$1).optional(),
4883
+ rung: z.enum(EVIDENCE_REVIEW_RUNGS).optional(),
4784
4884
  sessionId: nonBlankString$1.optional(),
4785
4885
  startedAt: parseableDateString,
4786
4886
  summary: z.string().refine((value) => value.trim().length > 0 && value !== "<one sentence>", { message: "summary must not be blank or a placeholder" }),
@@ -4867,6 +4967,47 @@ const PR_REVIEW_PROOF_DESCRIPTOR = {
4867
4967
  schemaVersion: 2
4868
4968
  };
4869
4969
  //#endregion
4970
+ //#region src/review-proof-applicability.ts
4971
+ const REVIEW_STATUS_VALUES = [
4972
+ "not-required",
4973
+ "current",
4974
+ "stale",
4975
+ "missing",
4976
+ "blocked"
4977
+ ];
4978
+ const reviewStatusSchema = z.enum(REVIEW_STATUS_VALUES);
4979
+ const isProofCurrent = ({ reviewedHeadSha, reviewedPatchId, headSha, patchId }) => evidenceFresh({
4980
+ candidate: {
4981
+ headSha,
4982
+ patchId
4983
+ },
4984
+ recorded: {
4985
+ headSha: reviewedHeadSha,
4986
+ patchId: reviewedPatchId
4987
+ },
4988
+ rule: "patch-id-or-head-sha"
4989
+ });
4990
+ const reviewStatus = ({ blocked, required, reviewedHeadSha, reviewedPatchId, headSha, patchId }) => {
4991
+ if (blocked) {
4992
+ if ((reviewedHeadSha || reviewedPatchId) && !isProofCurrent({
4993
+ headSha,
4994
+ patchId,
4995
+ reviewedHeadSha,
4996
+ reviewedPatchId
4997
+ })) return "stale";
4998
+ return "blocked";
4999
+ }
5000
+ if (!required) return "not-required";
5001
+ if (!(reviewedHeadSha || reviewedPatchId)) return "missing";
5002
+ if (isProofCurrent({
5003
+ headSha,
5004
+ patchId,
5005
+ reviewedHeadSha,
5006
+ reviewedPatchId
5007
+ })) return "current";
5008
+ return "stale";
5009
+ };
5010
+ //#endregion
4870
5011
  //#region src/review-state.ts
4871
5012
  const reviewStateIssue = (message, field) => [{
4872
5013
  code: "custom",
@@ -5783,24 +5924,12 @@ const closeoutArtifactSchema = z.object({
5783
5924
  }).strict();
5784
5925
  //#endregion
5785
5926
  //#region src/schemas.ts
5786
- const EVIDENCE_CHECK_TYPES = ["review", "verify"];
5787
- const EVIDENCE_REVIEW_RUNGS = [
5788
- "independent-model",
5789
- "oracle",
5790
- "human"
5791
- ];
5792
- const DIFF_CLASSIFICATIONS = [
5793
- "docs/process-only",
5794
- "trivial",
5795
- "non-trivial"
5796
- ];
5797
- const REVIEW_STATUS_VALUES$1 = [
5798
- "not-required",
5799
- "current",
5800
- "stale",
5801
- "missing",
5802
- "blocked"
5803
- ];
5927
+ const evidenceCheckTypeSchema = z.enum(EVIDENCE_CHECK_TYPES);
5928
+ const evidenceReviewRungSchema = z.enum(EVIDENCE_REVIEW_RUNGS);
5929
+ const diffClassificationSchema = z.enum(DIFF_CLASSIFICATIONS);
5930
+ const ladderStageSchema = z.enum(LADDER_STAGES);
5931
+ const ladderNextActionSchema = z.enum(LADDER_NEXT_ACTION_KINDS);
5932
+ const ladderForcedTransitionSchema = z.enum(LADDER_FORCED_TRANSITION_KINDS);
5804
5933
  const followUpActionSchema = z.object({
5805
5934
  argv: z.array(z.string()),
5806
5935
  command: z.string()
@@ -5808,7 +5937,7 @@ const followUpActionSchema = z.object({
5808
5937
  const evidenceShaSchema = z.string().regex(/^[0-9a-f]{7,40}$/u, { message: "must be a 7-40 char lowercase hex git SHA" });
5809
5938
  z.object({
5810
5939
  check: z.string().min(1),
5811
- checkType: z.enum(EVIDENCE_CHECK_TYPES),
5940
+ checkType: evidenceCheckTypeSchema,
5812
5941
  findingsPointer: z.string().min(1).optional(),
5813
5942
  headSha: evidenceShaSchema,
5814
5943
  mergeBaseSha: evidenceShaSchema,
@@ -5818,7 +5947,7 @@ z.object({
5818
5947
  policyVersion: z.string().min(1).optional(),
5819
5948
  producer: z.string().min(1),
5820
5949
  requestId: z.string().min(1).optional(),
5821
- rung: z.enum(EVIDENCE_REVIEW_RUNGS).optional(),
5950
+ rung: evidenceReviewRungSchema.optional(),
5822
5951
  schemaVersion: z.literal(1),
5823
5952
  sessionId: z.string().trim().min(1).optional(),
5824
5953
  timestamp: z.iso.datetime().optional()
@@ -5881,7 +6010,7 @@ z.object({
5881
6010
  }).passthrough(),
5882
6011
  outcome: z.enum(["pass", "fail"]),
5883
6012
  producer: z.string().min(1),
5884
- rung: z.enum(EVIDENCE_REVIEW_RUNGS),
6013
+ rung: evidenceReviewRungSchema,
5885
6014
  schemaVersion: z.literal(1),
5886
6015
  sessionId: z.string().min(1).optional(),
5887
6016
  specHash: z.object({
@@ -5901,7 +6030,7 @@ const boundaryCheckProofSchema = z.object({
5901
6030
  state: z.enum(["merged", "open"]),
5902
6031
  wave: z.string().min(1)
5903
6032
  })),
5904
- demandedRung: z.enum(EVIDENCE_REVIEW_RUNGS).optional(),
6033
+ demandedRung: evidenceReviewRungSchema.optional(),
5905
6034
  epicIssue: z.number().int().positive(),
5906
6035
  manifestHash: z.object({
5907
6036
  algo: z.literal("sha256"),
@@ -5913,7 +6042,7 @@ const boundaryCheckProofSchema = z.object({
5913
6042
  commentId: z.union([z.string(), z.number()]).optional(),
5914
6043
  commentUrl: z.string().optional(),
5915
6044
  producer: z.string().min(1),
5916
- rung: z.enum(EVIDENCE_REVIEW_RUNGS),
6045
+ rung: evidenceReviewRungSchema,
5917
6046
  sessionId: z.string().min(1).optional(),
5918
6047
  specHash: z.object({
5919
6048
  algo: z.literal("sha256"),
@@ -5928,7 +6057,6 @@ const boundaryCheckProofSchema = z.object({
5928
6057
  }).optional(),
5929
6058
  status: z.enum(["ready", "blocked"])
5930
6059
  });
5931
- const requiredCheckScopeSchema = z.object({ classifications: z.array(z.enum(DIFF_CLASSIFICATIONS)).min(1) }).strict();
5932
6060
  const readinessRepairSchema$1 = z.object({
5933
6061
  action: z.string().min(1),
5934
6062
  code: z.enum([
@@ -5942,7 +6070,7 @@ const reviewStateSchema$1 = z.object({
5942
6070
  required: z.boolean(),
5943
6071
  reviewedHeadSha: z.string().optional(),
5944
6072
  reviewedPatchId: z.string().optional(),
5945
- status: z.enum(REVIEW_STATUS_VALUES$1)
6073
+ status: reviewStatusSchema
5946
6074
  }).strict().superRefine((state, context) => {
5947
6075
  for (const issue of reviewStateIssues(state)) context.addIssue(issue);
5948
6076
  });
@@ -5963,9 +6091,9 @@ const historicalHandledHumanCommentSchema = z.object({
5963
6091
  const managedReadinessLedgerSchema$1 = z.object({
5964
6092
  baseSha: z.string().regex(/^[0-9a-f]{40}$/u),
5965
6093
  blockingReasons: z.array(z.string()),
5966
- classification: z.enum(DIFF_CLASSIFICATIONS),
6094
+ classification: diffClassificationSchema,
5967
6095
  externalChecks: z.array(z.object({
5968
- checkType: z.enum(EVIDENCE_CHECK_TYPES),
6096
+ checkType: evidenceCheckTypeSchema,
5969
6097
  name: z.string().min(1),
5970
6098
  reason: z.string().optional(),
5971
6099
  scope: requiredCheckScopeSchema.optional(),
@@ -6038,25 +6166,9 @@ const managedReadinessLedgerSchema$1 = z.object({
6038
6166
  gate: z.number().int().nonnegative(),
6039
6167
  interior: z.number().int().nonnegative()
6040
6168
  }),
6041
- forcedTransition: z.enum([
6042
- "gate-cap-exhausted",
6043
- "interior-cap-reached",
6044
- "interior-takeover"
6045
- ]).optional(),
6046
- nextAction: z.enum([
6047
- "run-interior-cycle",
6048
- "advance-to-gate",
6049
- "run-gate-cycle",
6050
- "accept-nonblocking-findings",
6051
- "escalate-to-triage",
6052
- "ready-for-human"
6053
- ]),
6054
- stage: z.enum([
6055
- "interior",
6056
- "interior-takeover",
6057
- "interior-complete",
6058
- "gate"
6059
- ])
6169
+ forcedTransition: ladderForcedTransitionSchema.optional(),
6170
+ nextAction: ladderNextActionSchema,
6171
+ stage: ladderStageSchema
6060
6172
  }).optional(),
6061
6173
  reviewRuns: z.array(prReviewResultSchema).optional(),
6062
6174
  reviewTerminalState: z.enum([
@@ -6122,6 +6234,7 @@ z.object({
6122
6234
  humanBlockingReasons: z.array(z.string()).default([]),
6123
6235
  ledger: managedReadinessLedgerSchema$1,
6124
6236
  notices: z.array(z.string().min(1)).optional(),
6237
+ policyResolution: policyResolutionSchema.optional(),
6125
6238
  profileBlobSha: z.string().regex(/^[0-9a-f]{40}$/u).optional(),
6126
6239
  profilePath: z.string().min(1).optional(),
6127
6240
  repairs: z.array(readinessRepairSchema$1).default([]),
@@ -9687,10 +9800,10 @@ const recognizeCatchUpCandidate = ({ base, cwd }) => {
9687
9800
  //#endregion
9688
9801
  //#region src/pr-readiness/external-evidence.ts
9689
9802
  var external_evidence_exports = /* @__PURE__ */ __exportAll({
9690
- EVIDENCE_CHECK_TYPES: () => EVIDENCE_CHECK_TYPES$1,
9803
+ EVIDENCE_CHECK_TYPES: () => EVIDENCE_CHECK_TYPES,
9691
9804
  EVIDENCE_DIR: () => EVIDENCE_DIR,
9692
9805
  EVIDENCE_ENVELOPE_SCHEMA_VERSION: () => 1,
9693
- EVIDENCE_REVIEW_RUNGS: () => EVIDENCE_REVIEW_RUNGS$1,
9806
+ EVIDENCE_REVIEW_RUNGS: () => EVIDENCE_REVIEW_RUNGS,
9694
9807
  boundFailingCheckNames: () => boundFailingCheckNames,
9695
9808
  evaluateRequiredChecks: () => evaluateRequiredChecks,
9696
9809
  evidenceBindingFresh: () => evidenceBindingFresh,
@@ -9699,13 +9812,13 @@ var external_evidence_exports = /* @__PURE__ */ __exportAll({
9699
9812
  loadEvidenceEnvelopes: () => loadEvidenceEnvelopes,
9700
9813
  parseEvidenceEnvelope: () => parseEvidenceEnvelope,
9701
9814
  requiredCheckInScope: () => requiredCheckInScope,
9702
- requiredCheckScopeSchema: () => requiredCheckScopeSchema$1,
9815
+ requiredCheckScopeSchema: () => requiredCheckScopeSchema,
9703
9816
  resolveAuthoringSessionIds: () => resolveAuthoringSessionIds
9704
9817
  });
9705
9818
  const shaSchema$4 = z.string().regex(/^[0-9a-f]{7,40}$/u, { message: "must be a 7-40 char lowercase hex git SHA" });
9706
9819
  const evidenceEnvelopeSchema = z.object({
9707
9820
  check: z.string().min(1),
9708
- checkType: z.enum(EVIDENCE_CHECK_TYPES$1),
9821
+ checkType: z.enum(EVIDENCE_CHECK_TYPES),
9709
9822
  findingsPointer: z.string().min(1).optional(),
9710
9823
  headSha: shaSchema$4,
9711
9824
  mergeBaseSha: shaSchema$4,
@@ -9715,7 +9828,7 @@ const evidenceEnvelopeSchema = z.object({
9715
9828
  policyVersion: z.string().min(1).optional(),
9716
9829
  producer: z.string().min(1),
9717
9830
  requestId: z.string().min(1).optional(),
9718
- rung: z.enum(EVIDENCE_REVIEW_RUNGS$1).optional(),
9831
+ rung: z.enum(EVIDENCE_REVIEW_RUNGS).optional(),
9719
9832
  schemaVersion: z.literal(1),
9720
9833
  sessionId: z.string().trim().min(1).optional(),
9721
9834
  timestamp: z.iso.datetime().optional()
@@ -10207,21 +10320,29 @@ const readExchange = async (request, url, init, timeoutMs) => {
10207
10320
  async function requestGitHubJson(request, url, init, timeoutMs = GITHUB_PUBLISH_TIMEOUT_MS) {
10208
10321
  return (await readExchange(request, url, init, timeoutMs)).json;
10209
10322
  }
10210
- /** Mint one repository-scoped installation token for the Patronage Factory App. */
10211
- const mintFactoryInstallationToken = (repository, config, request, now, timeoutMs = GITHUB_PUBLISH_TIMEOUT_MS) => mintInstallationToken({
10323
+ /**
10324
+ * Mint one repository-scoped installation token for the Patronage Factory App.
10325
+ *
10326
+ * `budget.transportAttempts` is the caller's, not this function's: a proof
10327
+ * registration must survive a stale keep-alive socket, while a best-effort
10328
+ * publish with a fallback must give up at once. Omitted, it stays at the
10329
+ * package default of three.
10330
+ */
10331
+ const mintFactoryInstallationToken = (repository, config, request, now, budget = {}) => mintInstallationToken({
10212
10332
  credentials: config,
10213
10333
  owner: repository.owner,
10214
10334
  repo: repository.repo
10215
10335
  }, {
10216
10336
  fetch: request,
10217
10337
  now: () => now,
10218
- timeoutMs
10338
+ timeoutMs: budget.timeoutMs ?? 5e3,
10339
+ ...budget.transportAttempts === void 0 ? {} : { transportAttempts: budget.transportAttempts }
10219
10340
  });
10220
10341
  const resolveFactoryAppToken = async (input, dependencies, request, timeoutMs) => {
10221
10342
  if (dependencies.token) return dependencies.token;
10222
10343
  const config = dependencies.githubApp ?? tryUserConfig()?.config.githubApp;
10223
10344
  if (!config) throw new Error("The Patronage Factory GitHub App is required for App-owned GitHub requests.");
10224
- return mintFactoryInstallationToken(input, config, request, (dependencies.now ?? Date.now)(), timeoutMs);
10345
+ return mintFactoryInstallationToken(input, config, request, (dependencies.now ?? Date.now)(), { timeoutMs });
10225
10346
  };
10226
10347
  const factoryAppInit = (input, token) => ({
10227
10348
  ...input.body === void 0 ? {} : { body: JSON.stringify(input.body) },
@@ -10272,7 +10393,7 @@ const outcomeFor = (value) => {
10272
10393
  if (isOutcome(value.outcome)) return value.outcome;
10273
10394
  return value.schemaVersion === 1 ? "passed" : "aborted";
10274
10395
  };
10275
- const asClassification = (value) => DIFF_CLASSIFICATIONS$1.includes(value) ? value : "unknown";
10396
+ const asClassification = (value) => DIFF_CLASSIFICATIONS.includes(value) ? value : "unknown";
10276
10397
  /**
10277
10398
  * Project a local pr:verify proof onto the durable check-run payload. Accepts
10278
10399
  * `unknown` because the publisher is handed an opaque proof value; anything
@@ -10402,8 +10523,16 @@ Re-verify only if the tree changes: a new commit, an amend, or a rebase.
10402
10523
  * had grown a second copy of it. What stays here is what is this repository's:
10403
10524
  * where the credentials come from (`user-config`), and the publish timeout the
10404
10525
  * rest of these calls are bound by. Nothing is cached, as before.
10526
+ *
10527
+ * Retry budget is the caller's. The fire-and-forget publisher passes one
10528
+ * attempt because it already has a commit-status fallback (#938). The
10529
+ * confirmation read-back leaves the default, because it has no fallback and
10530
+ * a stale keep-alive is the class #923 covers.
10405
10531
  */
10406
- const installationToken = (repository, config, request, now, timeoutMs = GITHUB_PUBLISH_TIMEOUT_MS) => mintFactoryInstallationToken(repository, config, request, now, timeoutMs);
10532
+ const installationToken = (repository, config, request, now, budget = {}) => mintFactoryInstallationToken(repository, config, request, now, {
10533
+ timeoutMs: budget.timeoutMs ?? 5e3,
10534
+ ...budget.transportAttempts === void 0 ? {} : { transportAttempts: budget.transportAttempts }
10535
+ });
10407
10536
  function verifyOutput(proof) {
10408
10537
  const value = proof;
10409
10538
  const commands = value.executedCommands?.length ?? 0;
@@ -10491,7 +10620,10 @@ async function postFactoryCheckRun({ config, deadlineMs, dependencies, detailsUr
10491
10620
  const request = dependencies.fetch ?? fetch;
10492
10621
  const timeoutMs = dependencies.timeoutMs ?? 5e3;
10493
10622
  try {
10494
- const token = await installationToken(input, config, request, (dependencies.now ?? Date.now)(), timeoutMs);
10623
+ const token = await installationToken(input, config, request, (dependencies.now ?? Date.now)(), {
10624
+ timeoutMs,
10625
+ transportAttempts: 1
10626
+ });
10495
10627
  return await requestGitHubJson(request, `https://api.github.com/repos/${input.owner}/${input.repo}/check-runs`, {
10496
10628
  body: JSON.stringify(checkRunRequestBody(input, detailsUrl)),
10497
10629
  headers: {
@@ -10582,7 +10714,7 @@ async function resolveDetailsUrlSafely(input, dependencies, onDiagnostic) {
10582
10714
  async function servedFactoryCheckRun(input, config, dependencies) {
10583
10715
  const request = dependencies.fetch ?? fetch;
10584
10716
  const timeoutMs = dependencies.timeoutMs ?? 5e3;
10585
- const token = await installationToken(input, config, request, (dependencies.now ?? Date.now)(), timeoutMs);
10717
+ const token = await installationToken(input, config, request, (dependencies.now ?? Date.now)(), { timeoutMs });
10586
10718
  const query = new URLSearchParams({
10587
10719
  app_id: String(config.appId),
10588
10720
  check_name: FACTORY_CHECK_NAMES[input.gate],
@@ -11817,6 +11949,204 @@ function createDemandWaiveCommand(_output, action = runDemandWaive) {
11817
11949
  const resolveApplicableReviewModes = (profile, changedFiles) => {
11818
11950
  return (profile.review?.conditional ?? []).some((scope) => changedFiles.some((file) => matchesAnyGlob(scope.paths, file))) ? ["correctness", "security"] : ["correctness"];
11819
11951
  };
11952
+ const compareStrings = (left, right) => {
11953
+ if (left === right) return 0;
11954
+ return left < right ? -1 : 1;
11955
+ };
11956
+ /**
11957
+ * The `review` subtree as one comparable string.
11958
+ *
11959
+ * Groups are anonymous in schema v6 (`paths` only, `additionalProperties:
11960
+ * false`), so a group has no identity beyond its path list. Paths are sorted
11961
+ * inside each group and groups are sorted by their path list, so a pure
11962
+ * reordering is the same policy and only a real change reads as a change.
11963
+ * `undefined` (no `review` object at all) canonicalizes to `null`, which is
11964
+ * distinct from every declared policy including an empty one.
11965
+ */
11966
+ const canonicalReviewPolicy = (review) => {
11967
+ if (review === void 0) return JSON.stringify(null);
11968
+ const groups = review.conditional.map((group) => ({ paths: [...group.paths].toSorted(compareStrings) }));
11969
+ return JSON.stringify({ conditional: groups.toSorted((left, right) => compareStrings(JSON.stringify(left.paths), JSON.stringify(right.paths))) });
11970
+ };
11971
+ /** Every protected path a policy declares, across all of its groups. */
11972
+ const reviewPolicyPaths = (review) => (review?.conditional ?? []).flatMap((group) => group.paths);
11973
+ /**
11974
+ * Resolve review demand from the base policy and the candidate policy.
11975
+ *
11976
+ * Two terms, both decided:
11977
+ *
11978
+ * 1. The merge rule is the UNION of the flattened protected-path sets, matched
11979
+ * by the same globs as before. Policy may add demand; it can never remove
11980
+ * demand the base established for this candidate.
11981
+ * 2. Self-protection: a candidate whose `review` subtree differs from the
11982
+ * base's demands a security review whatever else it touches. It is scoped
11983
+ * to `review` and not to the whole profile deliberately — widening it would
11984
+ * make every `verification.commands` edit owe a security review.
11985
+ *
11986
+ * The demand is not scoped by classification. A docs-only candidate that
11987
+ * touches a protected path still owes the security review: classification
11988
+ * scopes verification work, not review authority.
11989
+ */
11990
+ const resolveAuthoritativeReviewDemand = ({ basePolicy, candidatePolicy, changedFiles }) => {
11991
+ const baseCanonical = canonicalReviewPolicy(basePolicy);
11992
+ const candidateCanonical = canonicalReviewPolicy(candidatePolicy);
11993
+ const reviewPolicyChanged = baseCanonical !== candidateCanonical;
11994
+ const candidatePaths = reviewPolicyPaths(candidatePolicy);
11995
+ const effectivePaths = [...new Set([...reviewPolicyPaths(basePolicy), ...candidatePaths])].toSorted(compareStrings);
11996
+ const candidateOwned = new Set(candidatePaths);
11997
+ const baseOnlyMatchedPaths = effectivePaths.filter((protectedPath) => !candidateOwned.has(protectedPath) && changedFiles.some((file) => matchesAnyGlob([protectedPath], file)));
11998
+ const candidateAloneDemandsSecurity = changedFiles.some((file) => matchesAnyGlob(candidatePaths, file));
11999
+ const securityApplies = reviewPolicyChanged || candidateAloneDemandsSecurity || baseOnlyMatchedPaths.length > 0;
12000
+ return {
12001
+ baseOnlyMatchedPaths,
12002
+ basePolicyDigest: sha256Hex(baseCanonical),
12003
+ candidateAloneDemandsSecurity,
12004
+ candidatePolicyDigest: sha256Hex(candidateCanonical),
12005
+ effectiveDigest: sha256Hex(JSON.stringify(effectivePaths)),
12006
+ effectivePaths,
12007
+ reviewModes: securityApplies ? ["correctness", "security"] : ["correctness"],
12008
+ reviewPolicyChanged
12009
+ };
12010
+ };
12011
+ /**
12012
+ * Why the security demand rose, when the candidate's own profile would not have
12013
+ * produced it. `undefined` when the candidate's own policy already demanded the
12014
+ * review, or when nothing demanded it — an operator learns nothing from being
12015
+ * told their own profile did what they wrote it to do.
12016
+ */
12017
+ const baseDerivedSecurityNotice = ({ baseRefName, demand }) => {
12018
+ if (!demand.reviewModes.includes("security") || demand.candidateAloneDemandsSecurity) return;
12019
+ return `Security review is demanded by the base policy, not by this candidate's own profile: ${[...demand.baseOnlyMatchedPaths.length > 0 ? [`${baseRefName} protects ${demand.baseOnlyMatchedPaths.join(", ")}, and this candidate changes a file it matches`] : [], ...demand.reviewPolicyChanged ? [`this candidate's review policy differs from the one ${baseRefName} declares`] : []].join("; ")}. Policy may add review demand; it cannot remove demand the base established (#922).`;
12020
+ };
12021
+ const defaultBaseReviewPolicyAuthority = () => ({
12022
+ branchTipSha: ({ branch, cwd, repository }) => fetchGitHubBranchTipSha(repository, branch, cwd),
12023
+ profileContentAtRef: ({ cwd, profilePath, ref, repository }) => fetchGitHubFileAtRef(repository, profilePath, ref, cwd)
12024
+ });
12025
+ const failureText = (error) => error instanceof Error ? error.message : String(error);
12026
+ /**
12027
+ * The base document, read at exactly the scope of the fact being read — and no
12028
+ * wider, but no narrower either.
12029
+ *
12030
+ * NARROW, because the reader needs the `review` subtree and nothing else. It
12031
+ * does NOT pin `schemaVersion` to the CLI's own constant. A base profile one
12032
+ * version behind still answers the question it is being asked, which is what
12033
+ * lets the pull request that bumps the schema — and every pull request open
12034
+ * beside it — reach `pr:ready` at all.
12035
+ *
12036
+ * BOUNDED, because `review` is optional and "no protected paths" is a real,
12037
+ * demand-lowering answer. A schema that accepted any JSON object would read
12038
+ * `{}`, a truncated write, or an unrelated JSON file at that path as a valid
12039
+ * statement that the base protects nothing — a degrade-to-no-base-demand path,
12040
+ * which is the failure this whole change exists to close. So the document must
12041
+ * be RECOGNIZABLE as a factory profile before its `review` subtree is trusted:
12042
+ * `schemaVersion` must be a positive integer, and `repository` must be present
12043
+ * (the caller then checks it against the trusted checkout).
12044
+ *
12045
+ * Nothing about the refusal relaxes: unreadable JSON, a non-object document,
12046
+ * and a `review` value that violates the review shape all still refuse.
12047
+ */
12048
+ const baseReviewDocumentSchema = z.object({
12049
+ repository: z.object({
12050
+ name: z.string().min(1),
12051
+ owner: z.string().min(1)
12052
+ }).strict(),
12053
+ review: reviewPolicySchema.optional(),
12054
+ schemaVersion: z.number().int().positive()
12055
+ });
12056
+ const parseBaseReviewPolicy = (content) => {
12057
+ let document;
12058
+ try {
12059
+ document = JSON.parse(content);
12060
+ } catch (error) {
12061
+ return {
12062
+ ok: false,
12063
+ reason: `it is not valid JSON (${failureText(error)})`
12064
+ };
12065
+ }
12066
+ const parsed = baseReviewDocumentSchema.safeParse(document);
12067
+ if (!parsed.success) return {
12068
+ ok: false,
12069
+ reason: parsed.error.issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ")
12070
+ };
12071
+ return {
12072
+ ok: true,
12073
+ policy: parsed.data.review,
12074
+ repository: parsed.data.repository
12075
+ };
12076
+ };
12077
+ /**
12078
+ * Read the review policy the PR's base ref actually declares, at its live tip.
12079
+ *
12080
+ * The base ref name comes from GitHub, the repository from the trusted
12081
+ * checkout, and the tip from GitHub's ref API — never from the candidate
12082
+ * profile and never from a caller-supplied ref, both of which the candidate
12083
+ * controls. Every failure returns `unresolved` with a reason; the caller
12084
+ * refuses on it. An absent or unparseable profile at the base tip is a
12085
+ * refusal, never "no base demand".
12086
+ */
12087
+ const readAuthoritativeBasePolicy = ({ authority, baseRefName, crossRepository, cwd, profilePath, repository }) => {
12088
+ if (crossRepository !== false) return {
12089
+ baseRefName,
12090
+ kind: "unresolved",
12091
+ reason: crossRepository === true ? `this pull request is opened from a fork, so its base repository is not the trusted checkout repository ${repositorySlug(repository)}, and base policy must never be resolved from a candidate-controlled fork.` : `GitHub did not state whether this pull request comes from a fork (isCrossRepository was ${JSON.stringify(crossRepository)}), and an unstated answer is not a "no": base policy must never be resolved from a repository that may be a candidate-controlled fork.`
12092
+ };
12093
+ let policyBaseSha;
12094
+ try {
12095
+ policyBaseSha = authority.branchTipSha({
12096
+ branch: baseRefName,
12097
+ cwd,
12098
+ repository
12099
+ });
12100
+ } catch (error) {
12101
+ return {
12102
+ baseRefName,
12103
+ kind: "unresolved",
12104
+ reason: `the live tip of ${baseRefName} could not be read from GitHub: ${failureText(error)}`
12105
+ };
12106
+ }
12107
+ let content;
12108
+ try {
12109
+ content = authority.profileContentAtRef({
12110
+ cwd,
12111
+ profilePath,
12112
+ ref: policyBaseSha,
12113
+ repository
12114
+ });
12115
+ } catch (error) {
12116
+ return {
12117
+ baseRefName,
12118
+ kind: "unresolved",
12119
+ reason: `${profilePath} could not be read at base tip ${policyBaseSha}: ${failureText(error)}`
12120
+ };
12121
+ }
12122
+ const parsed = parseBaseReviewPolicy(content);
12123
+ if (!parsed.ok) return {
12124
+ baseRefName,
12125
+ kind: "unresolved",
12126
+ reason: `${profilePath} at base tip ${policyBaseSha} does not carry a readable review policy: ${parsed.reason}`
12127
+ };
12128
+ if (repositorySlug(parsed.repository) !== repositorySlug(repository)) return {
12129
+ baseRefName,
12130
+ kind: "unresolved",
12131
+ reason: `${profilePath} at base tip ${policyBaseSha} declares repository ${repositorySlug(parsed.repository)}, which is not the trusted checkout repository ${repositorySlug(repository)}.`
12132
+ };
12133
+ return {
12134
+ baseRefName,
12135
+ kind: "resolved",
12136
+ policy: parsed.policy,
12137
+ policyBaseSha
12138
+ };
12139
+ };
12140
+ /** The policy identity a gate records once the authority resolved (#922). */
12141
+ const authoritativePolicyResolution = ({ base, demand }) => ({
12142
+ basePolicyDigest: demand.basePolicyDigest,
12143
+ baseRefName: base.baseRefName,
12144
+ candidatePolicyDigest: demand.candidatePolicyDigest,
12145
+ effectiveDigest: demand.effectiveDigest,
12146
+ policyBaseSha: base.policyBaseSha,
12147
+ reviewPolicyChanged: demand.reviewPolicyChanged,
12148
+ status: "authoritative"
12149
+ });
11820
12150
  /**
11821
12151
  * Convert schema-v5 profile requiredChecks into the executable evidence
11822
12152
  * demands consumed by readiness and its preflight presentation. Schema-v5
@@ -11866,7 +12196,7 @@ const defaultFetchClosingPullRequests = ({ issue, owner, repo }) => {
11866
12196
  `issue=${issue}`
11867
12197
  ]).data?.repository?.issue?.closedByPullRequestsReferences?.nodes ?? [];
11868
12198
  };
11869
- const defaultFetchIssueBody = ({ issue, owner, repo }) => runGhJson([
12199
+ const defaultFetchIssueBody$1 = ({ issue, owner, repo }) => runGhJson([
11870
12200
  "issue",
11871
12201
  "view",
11872
12202
  String(issue),
@@ -11891,7 +12221,7 @@ const wrapperBoundaryNotices = ({ epic, manifest }) => {
11891
12221
  * closed: an absent, unparseable, or unmatched manifest binds nothing and
11892
12222
  * names its refusal. The boundary issue may be the work issue itself (#789).
11893
12223
  */
11894
- const resolveWaveDemand = ({ baseRefName, epic, fetchClosingPullRequests = defaultFetchClosingPullRequests, fetchIssueBody = defaultFetchIssueBody, headRefName, owner, pr, repo }) => {
12224
+ const resolveWaveDemand = ({ baseRefName, epic, fetchClosingPullRequests = defaultFetchClosingPullRequests, fetchIssueBody = defaultFetchIssueBody$1, headRefName, owner, pr, repo }) => {
11895
12225
  const parsed = parseBoundaryManifest(fetchIssueBody({
11896
12226
  issue: epic,
11897
12227
  owner,
@@ -12152,47 +12482,6 @@ function hqIngestCredentialCheck({ env, resolve = defaultSecretReferenceProbe, r
12152
12482
  };
12153
12483
  }
12154
12484
  //#endregion
12155
- //#region src/review-proof-applicability.ts
12156
- const REVIEW_STATUS_VALUES = [
12157
- "not-required",
12158
- "current",
12159
- "stale",
12160
- "missing",
12161
- "blocked"
12162
- ];
12163
- const reviewStatusSchema = z.enum(REVIEW_STATUS_VALUES);
12164
- const isProofCurrent = ({ reviewedHeadSha, reviewedPatchId, headSha, patchId }) => evidenceFresh({
12165
- candidate: {
12166
- headSha,
12167
- patchId
12168
- },
12169
- recorded: {
12170
- headSha: reviewedHeadSha,
12171
- patchId: reviewedPatchId
12172
- },
12173
- rule: "patch-id-or-head-sha"
12174
- });
12175
- const reviewStatus = ({ blocked, required, reviewedHeadSha, reviewedPatchId, headSha, patchId }) => {
12176
- if (blocked) {
12177
- if ((reviewedHeadSha || reviewedPatchId) && !isProofCurrent({
12178
- headSha,
12179
- patchId,
12180
- reviewedHeadSha,
12181
- reviewedPatchId
12182
- })) return "stale";
12183
- return "blocked";
12184
- }
12185
- if (!required) return "not-required";
12186
- if (!(reviewedHeadSha || reviewedPatchId)) return "missing";
12187
- if (isProofCurrent({
12188
- headSha,
12189
- patchId,
12190
- reviewedHeadSha,
12191
- reviewedPatchId
12192
- })) return "current";
12193
- return "stale";
12194
- };
12195
- //#endregion
12196
12485
  //#region src/admission-preflight.ts
12197
12486
  const errorMessage$1 = (error) => error instanceof Error ? error.message : String(error);
12198
12487
  const resolveCandidate = ({ base, cwd, verifyProof }) => {
@@ -13963,10 +14252,10 @@ const managedReadinessLedgerSchema = z.object({
13963
14252
  "non-trivial"
13964
14253
  ]),
13965
14254
  externalChecks: z.array(z.object({
13966
- checkType: z.enum(EVIDENCE_CHECK_TYPES$1),
14255
+ checkType: z.enum(EVIDENCE_CHECK_TYPES),
13967
14256
  name: z.string().min(1),
13968
14257
  reason: z.string().optional(),
13969
- scope: requiredCheckScopeSchema$1.optional(),
14258
+ scope: requiredCheckScopeSchema.optional(),
13970
14259
  scopeReason: z.string().min(1),
13971
14260
  status: z.enum([
13972
14261
  "satisfied",
@@ -15422,6 +15711,7 @@ const prReadyProofSchema = z.object({
15422
15711
  humanBlockingReasons: z.array(z.string()).default([]),
15423
15712
  ledger: managedReadinessLedgerSchema,
15424
15713
  notices: z.array(z.string().min(1)).optional(),
15714
+ policyResolution: policyResolutionSchema.optional(),
15425
15715
  profileBlobSha: z.string().regex(/^[0-9a-f]{40}$/u).optional(),
15426
15716
  profilePath: z.string().min(1).optional(),
15427
15717
  repairs: z.array(readinessRepairSchema).default([]),
@@ -15444,6 +15734,7 @@ const PR_VIEW_JSON_FIELDS = [
15444
15734
  "body",
15445
15735
  "headRefName",
15446
15736
  "headRefOid",
15737
+ "isCrossRepository",
15447
15738
  "isDraft",
15448
15739
  "mergeStateStatus",
15449
15740
  "mergeable",
@@ -15647,6 +15938,20 @@ async function runPrReady(args, dependencies = {}) {
15647
15938
  const policyProfile = committedPolicy.profile;
15648
15939
  if (repositorySlug(policyProfile.repository) !== checkoutRepositorySlug) throw new Error(`Committed readiness profile repository ${repositorySlug(policyProfile.repository)} does not match trusted checkout repository ${checkoutRepositorySlug}.`);
15649
15940
  const committedProfilePath = checkoutRelativeProfilePath(cwd, profilePath);
15941
+ const basePolicyReading = readAuthoritativeBasePolicy({
15942
+ authority: dependencies.basePolicy ?? defaultBaseReviewPolicyAuthority(),
15943
+ baseRefName: pr.baseRefName,
15944
+ crossRepository: pr.isCrossRepository,
15945
+ cwd,
15946
+ profilePath: committedProfilePath,
15947
+ repository
15948
+ });
15949
+ if (basePolicyReading.kind === "unresolved") throw new Error(`pr:ready could not read the authoritative review policy from the live tip of base ref ${basePolicyReading.baseRefName}: ${basePolicyReading.reason} Review demand is resolved from the base policy as well as the candidate's, so an unreadable base authority refuses admission rather than falling back to the candidate's own profile. Restore ${committedProfilePath} on ${basePolicyReading.baseRefName}, or re-run once GitHub is reachable.`);
15950
+ const authoritativeReview = resolveAuthoritativeReviewDemand({
15951
+ basePolicy: basePolicyReading.policy,
15952
+ candidatePolicy: policyProfile.review,
15953
+ changedFiles: files
15954
+ });
15650
15955
  const verifyProofPath = path.resolve(cwd, args.verifyProof ?? ".factory-memory/pr-verify.json");
15651
15956
  const reviewProofPath = path.resolve(cwd, args.reviewProof ?? ".factory-memory/pr-review.json");
15652
15957
  const verifyProofRead = tryReadProof(readPrVerifyProof, verifyProofPath);
@@ -15655,11 +15960,10 @@ async function runPrReady(args, dependencies = {}) {
15655
15960
  recorded: { patchId: verifyProofRead.proof.patchId },
15656
15961
  rule: "patch-id"
15657
15962
  }) ? verifyProofRead.proof.classification : "non-trivial";
15658
- const demands = resolveCandidateDemands({
15963
+ const { ladderPolicy, requiredChecks } = resolveCandidateDemands({
15659
15964
  changedFiles: files,
15660
15965
  profile: policyProfile
15661
15966
  });
15662
- const { ladderPolicy } = demands;
15663
15967
  const waveDemand = args.epic === void 0 ? void 0 : resolveWaveDemand({
15664
15968
  baseRefName: pr.baseRefName,
15665
15969
  epic: args.epic,
@@ -15693,7 +15997,7 @@ async function runPrReady(args, dependencies = {}) {
15693
15997
  args,
15694
15998
  cwd,
15695
15999
  recordedAuthoringSession: verifyProofRead.proof?.authoringSession,
15696
- requiredChecks: demands.requiredChecks
16000
+ requiredChecks
15697
16001
  });
15698
16002
  const output = path.resolve(cwd, args.output ?? ".factory-memory/pr-ready.json");
15699
16003
  const externalCheckState = statusCheckState(pr.statusCheckRollup, isFactoryReadyCheck);
@@ -15729,7 +16033,7 @@ async function runPrReady(args, dependencies = {}) {
15729
16033
  prTitle: pr.title,
15730
16034
  requiredChecks: externalCheckState,
15731
16035
  reviewDecision: pr.reviewDecision,
15732
- reviewModes: demands.reviewModes,
16036
+ reviewModes: authoritativeReview.reviewModes,
15733
16037
  reviewProof,
15734
16038
  unresolvedReviewThreads: pr.unresolvedReviewThreads,
15735
16039
  verificationProof: verification.verificationProof,
@@ -15768,8 +16072,13 @@ async function runPrReady(args, dependencies = {}) {
15768
16072
  if (evaluation.status === "ready") followUp = arming?.outcome === "not-armed" ? readyRedispatch() : void 0;
15769
16073
  else if (undraftRepair && evaluation.humanBlockingReasons.length === 0) followUp = prUndraftFollowUp(args.pr);
15770
16074
  else if (!pendingExternalChecksOnly) followUp = prVerifyFollowUp(authoringSession);
16075
+ const basePolicyNotice = baseDerivedSecurityNotice({
16076
+ baseRefName: basePolicyReading.baseRefName,
16077
+ demand: authoritativeReview
16078
+ });
15771
16079
  const notices = [
15772
16080
  ...evaluation.notices,
16081
+ ...basePolicyNotice && evaluation.ledger.reviews.security ? [basePolicyNotice] : [],
15773
16082
  ...arming ? [armingNotice(arming)] : [],
15774
16083
  ...waveDemand?.notices ?? [],
15775
16084
  ...evaluation.status === "ready" && !boundary ? ["No boundary wave authorized this candidate, so nothing was armed: pr:ready admits it but schedules no merge. Re-run with --epic <issue> to resolve the wave demand and arm. That issue can be the work issue itself, carrying its own fence; do not file a second boundary issue for one candidate."] : [],
@@ -15784,6 +16093,10 @@ async function runPrReady(args, dependencies = {}) {
15784
16093
  humanBlockingReasons: evaluation.humanBlockingReasons,
15785
16094
  ledger: evaluation.ledger,
15786
16095
  ...notices.length > 0 ? { notices } : {},
16096
+ policyResolution: authoritativePolicyResolution({
16097
+ base: basePolicyReading,
16098
+ demand: authoritativeReview
16099
+ }),
15787
16100
  profileBlobSha: committedPolicy.blobSha,
15788
16101
  profilePath: committedProfilePath,
15789
16102
  repairs: evaluation.repairs,
@@ -15930,7 +16243,7 @@ const reviewFindingsFileSchema = z.object({
15930
16243
  patchId: nonBlankString,
15931
16244
  producer: nonBlankString.optional(),
15932
16245
  reviews: z.array(findingsReviewSchema),
15933
- rung: z.enum(EVIDENCE_REVIEW_RUNGS$1).optional(),
16246
+ rung: z.enum(EVIDENCE_REVIEW_RUNGS).optional(),
15934
16247
  schemaVersion: z.union([z.literal(1), z.literal(2)]),
15935
16248
  sessionId: nonBlankString
15936
16249
  }).strict().superRefine((file, context) => {
@@ -15981,40 +16294,92 @@ const parseReviewFindingsFile = (findingsPath, cwd = process.cwd()) => {
15981
16294
  }
15982
16295
  };
15983
16296
  //#endregion
15984
- //#region src/review-ladder-trace.ts
15985
- const eventTime = () => (/* @__PURE__ */ new Date()).toISOString();
15986
- const buildReviewLadderStageTraceEvent = ({ createdAt = eventTime(), event, headSha, identity, patchId }) => {
15987
- const trace = {
15988
- createdAt,
15989
- cycle: event.cycle,
15990
- engine: event.engine,
15991
- eventId: `review-ladder-stage-${crypto.randomUUID()}`,
15992
- eventType: "review-ladder-stage",
15993
- findingsSummary: event.findingsSummary,
15994
- observedAt: createdAt,
15995
- repo: identity.repo,
15996
- schemaVersion: 1,
15997
- stage: event.stage,
15998
- typeVersion: 1
15999
- };
16000
- if (event.cyclesToClean !== void 0) trace.cyclesToClean = event.cyclesToClean;
16001
- if (event.forcedTransition !== void 0) trace.forcedTransition = event.forcedTransition;
16002
- if (event.model !== void 0) trace.model = event.model;
16003
- if (event.session !== void 0) trace.session = event.session;
16004
- if (event.usage !== void 0) trace.usage = event.usage;
16005
- if (headSha !== void 0) trace.headSha = headSha;
16006
- if (patchId !== void 0) trace.patchId = patchId;
16007
- if (typeof identity.issue === "number") trace.issue = identity.issue;
16008
- if (typeof identity.pr === "number") trace.pr = identity.pr;
16009
- if (identity.threadId !== void 0) trace.threadId = identity.threadId;
16010
- if (identity.workerId !== void 0) trace.workerId = identity.workerId;
16011
- return trace;
16012
- };
16013
- const appendReviewLadderStageTraceEvent = ({ createdAt, event, headSha, identity, mirror, patchId, repoRoot }) => appendFactoryTraceEvent(repoRoot, buildReviewLadderStageTraceEvent({
16014
- createdAt,
16015
- event,
16016
- headSha,
16017
- identity,
16297
+ //#region src/issue-body-sections.ts
16298
+ const HEADING_PATTERN = /^#{2,6}\s+(?<title>.+?)\s*$/u;
16299
+ const FENCE_PATTERN = /^\s*(?<fence>`{3,}|~{3,})(?<rest>.*)$/u;
16300
+ /**
16301
+ * Extracts the body of a markdown section (heading depth `##`–`######`,
16302
+ * case-insensitive title match) from an issue body. Duplicate sections
16303
+ * resolve to the first occurrence.
16304
+ *
16305
+ * The scan is fence-aware: heading-like lines inside fenced code blocks
16306
+ * (``` or ~~~, including indented and language-tagged fences) neither start
16307
+ * nor terminate a section. Fences are matched by marker character (backtick
16308
+ * vs. tilde) and require a closing fence with at least as many marker
16309
+ * characters as the opener, per CommonMark; an unclosed fence extends to the
16310
+ * end of the body.
16311
+ */
16312
+ const sectionBody = (body, heading) => {
16313
+ const lines = body.split(/\r?\n/u);
16314
+ const target = heading.toLowerCase();
16315
+ let openFence;
16316
+ let startIndex = -1;
16317
+ for (const [index, line] of lines.entries()) {
16318
+ const fence = FENCE_PATTERN.exec(line)?.groups;
16319
+ const marker = fence?.fence;
16320
+ if (openFence) {
16321
+ if (marker && marker[0] === openFence.char && marker.length >= openFence.length && (fence?.rest ?? "").trim() === "") openFence = void 0;
16322
+ continue;
16323
+ }
16324
+ if (marker) {
16325
+ openFence = {
16326
+ char: marker[0],
16327
+ length: marker.length
16328
+ };
16329
+ continue;
16330
+ }
16331
+ const title = HEADING_PATTERN.exec(line)?.groups?.title;
16332
+ if (title === void 0) continue;
16333
+ if (startIndex === -1) {
16334
+ if (title.toLowerCase() === target) startIndex = index;
16335
+ continue;
16336
+ }
16337
+ return lines.slice(startIndex + 1, index).join("\n").trim();
16338
+ }
16339
+ if (startIndex === -1) return;
16340
+ return lines.slice(startIndex + 1).join("\n").trim();
16341
+ };
16342
+ //#endregion
16343
+ //#region src/review-focus.ts
16344
+ const REVIEW_FOCUS_SECTION = "Review focus";
16345
+ const reviewFocusFromIssueBody = (body) => {
16346
+ return sectionBody(body, "Review focus") || void 0;
16347
+ };
16348
+ //#endregion
16349
+ //#region src/review-ladder-trace.ts
16350
+ const eventTime = () => (/* @__PURE__ */ new Date()).toISOString();
16351
+ const buildReviewLadderStageTraceEvent = ({ createdAt = eventTime(), event, headSha, identity, patchId }) => {
16352
+ const trace = {
16353
+ createdAt,
16354
+ cycle: event.cycle,
16355
+ engine: event.engine,
16356
+ eventId: `review-ladder-stage-${crypto.randomUUID()}`,
16357
+ eventType: "review-ladder-stage",
16358
+ findingsSummary: event.findingsSummary,
16359
+ observedAt: createdAt,
16360
+ repo: identity.repo,
16361
+ schemaVersion: 1,
16362
+ stage: event.stage,
16363
+ typeVersion: 1
16364
+ };
16365
+ if (event.cyclesToClean !== void 0) trace.cyclesToClean = event.cyclesToClean;
16366
+ if (event.forcedTransition !== void 0) trace.forcedTransition = event.forcedTransition;
16367
+ if (event.model !== void 0) trace.model = event.model;
16368
+ if (event.session !== void 0) trace.session = event.session;
16369
+ if (event.usage !== void 0) trace.usage = event.usage;
16370
+ if (headSha !== void 0) trace.headSha = headSha;
16371
+ if (patchId !== void 0) trace.patchId = patchId;
16372
+ if (typeof identity.issue === "number") trace.issue = identity.issue;
16373
+ if (typeof identity.pr === "number") trace.pr = identity.pr;
16374
+ if (identity.threadId !== void 0) trace.threadId = identity.threadId;
16375
+ if (identity.workerId !== void 0) trace.workerId = identity.workerId;
16376
+ return trace;
16377
+ };
16378
+ const appendReviewLadderStageTraceEvent = ({ createdAt, event, headSha, identity, mirror, patchId, repoRoot }) => appendFactoryTraceEvent(repoRoot, buildReviewLadderStageTraceEvent({
16379
+ createdAt,
16380
+ event,
16381
+ headSha,
16382
+ identity,
16018
16383
  patchId
16019
16384
  }), { mirror });
16020
16385
  const tryAppendReviewLadderStageTraceEvent = (input) => {
@@ -16128,15 +16493,189 @@ function emitLadderStageTrace({ cwd, evaluation, headSha, issue, patchId, profil
16128
16493
  else console.error(`review-ladder-stage trace event not written: ${traceResult.errorMessage}`);
16129
16494
  }
16130
16495
  //#endregion
16496
+ //#region src/review-contract.ts
16497
+ const REVIEW_FOOTER_INSTRUCTIONS = `Do not print a machine-readable footer in your chat response. Instead, write your findings to the typed findings file at the path given in your dispatch/task instructions (the file pr:review reads via --findings <path>). The file must be valid JSON matching review-findings-file.ts's schema (schemaVersion 1):
16498
+ {
16499
+ "schemaVersion": 1,
16500
+ "model": "<your model identifier>",
16501
+ "sessionId": "<this session's id>",
16502
+ "patchId": "<the stable patch-id from Review scope above>",
16503
+ "reviews": [
16504
+ {
16505
+ "kind": "correctness" or "security" (this review's mode, matching the mode-floor instructions below),
16506
+ "outcome": "passed" or "failed",
16507
+ "issuesFlagged": an integer count of actionable findings (must equal findings.length),
16508
+ "summary": one concise sentence; use "No actionable findings." only when issuesFlagged is 0,
16509
+ "findings": [] when issuesFlagged is 0, otherwise an array of objects with title, body, and optional file, line, priority, category, blockingAfterCap, citedSpan, prescribedAction, and supersedes fields
16510
+ }
16511
+ ]
16512
+ }
16513
+ Use category "correctness", "safety", "coordination", or "maintainability"; set blockingAfterCap true for correctness, safety, or coordination findings. citedSpan must be the exact code text the finding is about, quoted verbatim from the diff (identity is derived from this, not from the title, so quote precisely). prescribedAction must be a concise, standalone statement of the required fix (identity is derived from this text too). If this finding is a restatement of a prior finding shown in the prior review ledger below, set supersedes to {"title": "<exact prior title>", "file": "<its file>"} instead of minting a new title for the same theme.`;
16514
+ //#endregion
16515
+ //#region src/review-prompt.ts
16516
+ const reviewScope = ({ base, changedFiles: files, headSha, patchId, profile }) => `Review scope:
16517
+ - Repository: ${profile.repository.owner}/${profile.repository.name}
16518
+ - Base ref: ${base}
16519
+ - Head SHA: ${headSha}
16520
+ - Stable patch-id: ${patchId}
16521
+ - Changed files:
16522
+ ${files.length > 0 ? files.map((file) => ` - ${file}`).join("\n") : " - None"}
16523
+
16524
+ Required constraints:
16525
+ - Review only the PR-owned diff against ${base} and the changed files listed above.
16526
+ - Prefer git diff, git grep, and file-specific reads.
16527
+ - Do not inspect ignored generated output such as dist, build, coverage, or sourcemaps.
16528
+ - Do not rerun broad build or verification commands. If pr:verify proof already exists, treat build verification as out of scope for this review runner.
16529
+ - Do not request elevated or unsandboxed approvals.
16530
+ - When you flag a finding, report every other instance of the same class in this diff in the same response, and include a one-line coverage note.
16531
+
16532
+ ${REVIEW_FOOTER_INSTRUCTIONS}`;
16533
+ const MODE_FLOOR_FILES = {
16534
+ correctness: "correctness.md",
16535
+ security: "security/SKILL.md"
16536
+ };
16537
+ const reviewPromptsDir = path.join(import.meta.dirname, "review-prompts");
16538
+ const modeFloorCache = /* @__PURE__ */ new Map();
16539
+ const securityReferenceFiles = (changedFiles) => {
16540
+ const references = /* @__PURE__ */ new Set();
16541
+ for (const file of changedFiles) {
16542
+ if (/^\.github\/(?:actions|workflows)\//u.test(file)) references.add("security/references/github-workflows.md");
16543
+ if (/\.(?:[cm]?[jt]sx?)$/u.test(file)) references.add("security/references/javascript-typescript.md");
16544
+ if (/\.py(?:i)?$/u.test(file)) references.add("security/references/python.md");
16545
+ }
16546
+ return [...references];
16547
+ };
16548
+ function modeFloorInstructions(kind, changedFiles = []) {
16549
+ const referenceFiles = kind === "security" ? securityReferenceFiles(changedFiles) : [];
16550
+ const cacheKey = `${kind}:${referenceFiles.join(",")}`;
16551
+ const cached = modeFloorCache.get(cacheKey);
16552
+ if (cached !== void 0) return cached;
16553
+ const text = [MODE_FLOOR_FILES[kind], ...referenceFiles].map((file) => readFileSync(path.join(reviewPromptsDir, file), "utf-8")).map((contents) => contents.replace(/\n+$/u, "")).join("\n\n---\n\n");
16554
+ modeFloorCache.set(cacheKey, text);
16555
+ return text;
16556
+ }
16557
+ const CODING_STANDARDS_FILE = "CODING_STANDARDS.md";
16558
+ const readCodingStandardsAtHead = ({ cwd, git, headSha }) => {
16559
+ let root;
16560
+ try {
16561
+ root = (git?.repositoryRoot ?? repositoryRoot)(cwd);
16562
+ } catch {
16563
+ return;
16564
+ }
16565
+ return (git?.showFileAtRef ?? showFileAtRef)(root, headSha, path.join(root, CODING_STANDARDS_FILE));
16566
+ };
16567
+ const codingStandardsSegment = ({ contents, headSha }) => {
16568
+ const text = contents?.replace(/\n+$/u, "");
16569
+ if (!text?.trim()) return;
16570
+ const source = `${CODING_STANDARDS_FILE} at head ${headSha}`;
16571
+ return {
16572
+ section: {
16573
+ provenance: "coding-standards",
16574
+ source,
16575
+ text
16576
+ },
16577
+ text: `Repository coding standards (source: ${source}, appended verbatim). Check the diff against these rules:
16578
+ ${text}`
16579
+ };
16580
+ };
16581
+ const issueFocusSegment = (focus) => ({
16582
+ section: {
16583
+ provenance: "issue-review-focus",
16584
+ source: `issue #${focus.issueNumber} "${REVIEW_FOCUS_SECTION}" section`,
16585
+ text: focus.text
16586
+ },
16587
+ text: `Per-issue review focus (source: issue #${focus.issueNumber} "${REVIEW_FOCUS_SECTION}" section, appended verbatim):
16588
+ ${focus.text}`
16589
+ });
16590
+ const priorLedgerEntryLine = (entry) => {
16591
+ const location = entry.file ? ` (${entry.file})` : "";
16592
+ const reference = entry.reference ? ` — ${entry.reference}` : "";
16593
+ return `- [${entry.disposition}] "${entry.title}"${location}${reference} (flagged ${entry.flagCount}x)`;
16594
+ };
16595
+ const priorLedgerSegment = (entries) => ({
16596
+ section: {
16597
+ provenance: "prior-review-ledger",
16598
+ source: "persisted pr:review disposition ledger (prior cycles, #233)",
16599
+ text: entries.map(priorLedgerEntryLine).join("\n")
16600
+ },
16601
+ text: `Prior review ledger (source: persisted pr:review disposition ledger from earlier cycles on this PR; already adjudicated — do not re-litigate):
16602
+ ${entries.map(priorLedgerEntryLine).join("\n")}
16603
+
16604
+ Do not re-raise any of the findings above under a new title: they are already adjudicated (waived, filed as a follow-up, or fixed in this thread). If new evidence genuinely supersedes one of them, say so explicitly and name the exact prior finding (its title and file) that it supersedes, rather than minting an unrelated title for the same underlying theme.`
16605
+ });
16606
+ const assembleReviewPrompt = ({ base, changedFiles, cwd = process.cwd(), headSha, issueFocus, kind, patchId, priorLedgerEntries, profile }, dependencies = {}) => {
16607
+ if (!resolveApplicableReviewModes(profile, changedFiles).includes(kind)) throw new Error(`${kind} review mode does not apply to the changed files.`);
16608
+ const priorLedger = priorLedgerEntries ? nonOpenLadderFindings(priorLedgerEntries) : [];
16609
+ const optionalSegments = {
16610
+ "coding-standards": codingStandardsSegment({
16611
+ contents: kind === "correctness" ? readCodingStandardsAtHead({
16612
+ cwd,
16613
+ git: dependencies.git,
16614
+ headSha
16615
+ }) : void 0,
16616
+ headSha
16617
+ }),
16618
+ "issue-review-focus": issueFocus ? issueFocusSegment(issueFocus) : void 0,
16619
+ "prior-review-ledger": priorLedger.length > 0 ? priorLedgerSegment(priorLedger) : void 0
16620
+ };
16621
+ const segments = [
16622
+ { text: reviewScope({
16623
+ base,
16624
+ changedFiles,
16625
+ headSha,
16626
+ patchId,
16627
+ profile
16628
+ }) },
16629
+ { text: modeFloorInstructions(kind, changedFiles) },
16630
+ ...REVIEW_PROMPT_SECTION_PROVENANCES.flatMap((provenance) => {
16631
+ const segment = optionalSegments[provenance];
16632
+ return segment ? [segment] : [];
16633
+ })
16634
+ ];
16635
+ return {
16636
+ prompt: segments.map((segment) => segment.text).join("\n\n"),
16637
+ sections: segments.flatMap((segment) => segment.section ? [segment.section] : [])
16638
+ };
16639
+ };
16640
+ //#endregion
16131
16641
  //#region src/pr-review.ts
16642
+ const DEFAULT_ASSEMBLED_REVIEW_PROMPT_PATH = ".factory-memory/pr-review-prompt.json";
16643
+ const MISSING_ASSEMBLED_PROMPT_NOTICE = "pr:review notice: assembled review prompt not found at .factory-memory/pr-review-prompt.json; accepting findings without promptSections.";
16644
+ const STALE_ASSEMBLED_PROMPT_NOTICE = "pr:review notice: assembled review prompt patch-id does not match the candidate; accepting findings without promptSections.";
16645
+ const UNREADABLE_ASSEMBLED_PROMPT_NOTICE = "pr:review notice: assembled review prompt at .factory-memory/pr-review-prompt.json is unreadable; accepting findings without promptSections.";
16646
+ const ASSEMBLE_NO_KIND_NOTICE = "pr:review --assemble: no review kind applies; prompt not written.";
16647
+ const ASSEMBLE_FAITHFUL_MERGE_NOTICE = "pr:review --assemble: faithful-merge; prompt not written.";
16648
+ const ASSEMBLED_REVIEW_PROMPT_SCHEMA_VERSION = 1;
16649
+ const assembledReviewPromptKindSchema = z.object({
16650
+ kind: z.enum(["correctness", "security"]),
16651
+ prompt: z.string().min(1),
16652
+ sections: z.array(reviewPromptSectionSchema)
16653
+ });
16654
+ const assembledReviewPromptArtifactSchema = z.object({
16655
+ headSha: z.string().regex(/^[0-9a-f]{40}$/u),
16656
+ patchId: z.string().regex(/^[0-9a-f]{40,64}$/u),
16657
+ reviews: z.array(assembledReviewPromptKindSchema),
16658
+ schemaVersion: z.literal(ASSEMBLED_REVIEW_PROMPT_SCHEMA_VERSION)
16659
+ });
16132
16660
  const defaultPrReviewGit = () => ({
16133
16661
  changedFiles,
16134
16662
  currentHeadSha,
16135
16663
  isAncestor,
16136
16664
  recognizeCatchUp: recognizeCatchUpCandidate,
16665
+ repositoryRoot,
16666
+ showFileAtRef,
16137
16667
  stablePatchId: runStablePatchId,
16138
16668
  statusPorcelain
16139
16669
  });
16670
+ const defaultFetchIssueBody = ({ issue, owner, repo }) => runGhJson([
16671
+ "issue",
16672
+ "view",
16673
+ String(issue),
16674
+ "--repo",
16675
+ `${owner}/${repo}`,
16676
+ "--json",
16677
+ "body"
16678
+ ]).body;
16140
16679
  const assertReviewCycleInWindow = (cycle, maxCycles) => {
16141
16680
  if (cycle < 1 || cycle > maxCycles) throw new Error(`--cycle must be between 1 and ${maxCycles}.`);
16142
16681
  };
@@ -16231,7 +16770,99 @@ const assertReviewOutcomes = (proof, policy, cycle, maxCycles) => {
16231
16770
  console.error(cycle >= maxCycles ? `Review cycle limit reached (${cycle}/${maxCycles}). Stop and request human direction before rerunning.` : `Fix ${state.openFindings} actionable finding(s) before rerunning pr:review as cycle ${cycle + 1}/${maxCycles}.`);
16232
16771
  throw new Error("pr:review found actionable review findings.");
16233
16772
  };
16773
+ const loadIssueFocus = (issue, profile, fetchIssueBody) => {
16774
+ const text = reviewFocusFromIssueBody(fetchIssueBody({
16775
+ issue,
16776
+ owner: profile.repository.owner,
16777
+ repo: profile.repository.name
16778
+ }));
16779
+ return text === void 0 ? void 0 : {
16780
+ issueNumber: issue,
16781
+ text
16782
+ };
16783
+ };
16784
+ const assembleGitDependencies = (git) => git.repositoryRoot !== void 0 && git.showFileAtRef !== void 0 ? {
16785
+ repositoryRoot: git.repositoryRoot,
16786
+ showFileAtRef: git.showFileAtRef
16787
+ } : void 0;
16788
+ const writeAssembledReviewPrompt = ({ args, cwd, fetchIssueBody, files, git, headSha, kinds, output, patchId, profile }) => {
16789
+ const priorLedger = priorLedgerFor(priorLadderStateFor({
16790
+ base: args.base,
16791
+ cwd,
16792
+ headSha,
16793
+ isAncestor: (ancestor, descendant) => git.isAncestor(cwd, ancestor, descendant),
16794
+ output
16795
+ }));
16796
+ const issueFocus = args.issue === void 0 ? void 0 : loadIssueFocus(args.issue, profile, fetchIssueBody);
16797
+ const assembleGit = assembleGitDependencies(git);
16798
+ const assembledReviews = kinds.map((kind) => {
16799
+ const assembled = assembleReviewPrompt({
16800
+ base: args.base,
16801
+ changedFiles: files,
16802
+ cwd,
16803
+ headSha,
16804
+ ...issueFocus === void 0 ? {} : { issueFocus },
16805
+ kind,
16806
+ patchId,
16807
+ priorLedgerEntries: priorLedger,
16808
+ profile
16809
+ }, assembleGit === void 0 ? {} : { git: assembleGit });
16810
+ return {
16811
+ kind,
16812
+ prompt: assembled.prompt,
16813
+ sections: assembled.sections
16814
+ };
16815
+ });
16816
+ const artifactPath = path.resolve(cwd, DEFAULT_ASSEMBLED_REVIEW_PROMPT_PATH);
16817
+ const existing = readAssembledReviewPrompt(artifactPath);
16818
+ writeProofJson({
16819
+ headSha,
16820
+ patchId,
16821
+ reviews: existing.status === "ok" && existing.artifact.patchId === patchId && existing.artifact.headSha === headSha ? [...new Map([...existing.artifact.reviews, ...assembledReviews].map((review) => [review.kind, review])).values()] : assembledReviews,
16822
+ schemaVersion: ASSEMBLED_REVIEW_PROMPT_SCHEMA_VERSION
16823
+ }, artifactPath);
16824
+ console.log(`pr:review assembled prompt written to ${artifactPath}`);
16825
+ return artifactPath;
16826
+ };
16827
+ const readAssembledReviewPrompt = (artifactPath) => {
16828
+ if (!existsSync(artifactPath)) return { status: "missing" };
16829
+ try {
16830
+ return {
16831
+ artifact: assembledReviewPromptArtifactSchema.parse(JSON.parse(readFileSync(artifactPath, "utf-8"))),
16832
+ status: "ok"
16833
+ };
16834
+ } catch {
16835
+ return { status: "unreadable" };
16836
+ }
16837
+ };
16838
+ const promptSectionsFromAssembledArtifact = ({ cwd, patchId }) => {
16839
+ const read = readAssembledReviewPrompt(path.resolve(cwd, DEFAULT_ASSEMBLED_REVIEW_PROMPT_PATH));
16840
+ if (read.status === "missing") {
16841
+ console.error(MISSING_ASSEMBLED_PROMPT_NOTICE);
16842
+ return;
16843
+ }
16844
+ if (read.status === "unreadable") {
16845
+ console.error(UNREADABLE_ASSEMBLED_PROMPT_NOTICE);
16846
+ return;
16847
+ }
16848
+ if (read.artifact.patchId !== patchId) {
16849
+ console.error(STALE_ASSEMBLED_PROMPT_NOTICE);
16850
+ return;
16851
+ }
16852
+ return new Map(read.artifact.reviews.flatMap((review) => review.sections.length > 0 ? [[review.kind, review.sections]] : []));
16853
+ };
16854
+ const withAssembledPromptSections = (reviews, sectionsByKind) => {
16855
+ if (sectionsByKind === void 0) return reviews;
16856
+ return reviews.map((review) => {
16857
+ const promptSections = sectionsByKind.get(review.kind);
16858
+ return promptSections === void 0 ? review : {
16859
+ ...review,
16860
+ promptSections
16861
+ };
16862
+ });
16863
+ };
16234
16864
  async function runPrReview(args, dependencies = {}) {
16865
+ if (args.assemble === true && args.findings !== void 0) throw new Error("--assemble and --findings cannot be combined; they are sequential steps.");
16235
16866
  const cwd = path.resolve(args.cwd ?? process.cwd());
16236
16867
  const git = dependencies.git ?? defaultPrReviewGit();
16237
16868
  const files = git.changedFiles(cwd, args.base);
@@ -16268,6 +16899,40 @@ async function runPrReview(args, dependencies = {}) {
16268
16899
  const startedAt = /* @__PURE__ */ new Date();
16269
16900
  const findingsFile = args.findings ? parseReviewFindingsFile(args.findings, cwd) : void 0;
16270
16901
  const kinds = enabledKinds(profile, args.mode, files, findingsFile?.reviews.map((review) => review.kind) ?? []);
16902
+ if (args.assemble === true) {
16903
+ const assembleProof = () => emptyProof({
16904
+ base: args.base,
16905
+ cycle: args.cycle,
16906
+ files,
16907
+ headSha,
16908
+ maxCycles,
16909
+ patchId
16910
+ });
16911
+ if (kinds.length === 0) {
16912
+ console.log(ASSEMBLE_NO_KIND_NOTICE);
16913
+ return assembleProof();
16914
+ }
16915
+ if ((git.recognizeCatchUp ?? recognizeCatchUpCandidate)({
16916
+ base: args.base,
16917
+ cwd
16918
+ }).recognized) {
16919
+ console.log(ASSEMBLE_FAITHFUL_MERGE_NOTICE);
16920
+ return assembleProof();
16921
+ }
16922
+ writeAssembledReviewPrompt({
16923
+ args,
16924
+ cwd,
16925
+ fetchIssueBody: dependencies.fetchIssueBody ?? defaultFetchIssueBody,
16926
+ files,
16927
+ git,
16928
+ headSha,
16929
+ kinds,
16930
+ output,
16931
+ patchId,
16932
+ profile
16933
+ });
16934
+ return assembleProof();
16935
+ }
16271
16936
  const mintNotRequiredProof = (reviewRequirement) => {
16272
16937
  const proof = emptyProof({
16273
16938
  base: args.base,
@@ -16335,7 +17000,10 @@ async function runPrReview(args, dependencies = {}) {
16335
17000
  const priorLedger = priorLedgerFor(priorLadder);
16336
17001
  const declaredDispositions = declaredDispositionsFor(args.dispositions, cwd);
16337
17002
  assertDeclaredDispositionsMatch(priorLedger, declaredDispositions);
16338
- const reviews = reviewResults(findingsFile, kinds, startedAt, /* @__PURE__ */ new Date());
17003
+ const reviews = withAssembledPromptSections(reviewResults(findingsFile, kinds, startedAt, /* @__PURE__ */ new Date()), promptSectionsFromAssembledArtifact({
17004
+ cwd,
17005
+ patchId
17006
+ }));
16339
17007
  const runs = reviews.map((review) => ({ review }));
16340
17008
  const { evaluation, ladder } = ladderStateForRun({
16341
17009
  cycle: args.cycle,
@@ -17070,7 +17738,7 @@ function assertForegroundGate({ gate, env = process.env }) {
17070
17738
  //#endregion
17071
17739
  //#region src/commands/pr-review.ts
17072
17740
  function createPrReviewCommand(output, action) {
17073
- return markCwdOptionDefault(new Command("pr:review").description("Validate clean-session findings and write typed review proof").option("--base <ref>", "base branch or ref", "origin/main").option("--cwd <path>", "working directory to review", collectCwdOption).option("--cycle <number>", "review cycle number", positiveInteger$1("--cycle"), 1).option("--mode <mode>", "correctness, security, or all", "all").option("--findings <path>", "typed clean-session findings JSON file").option("--dispositions <path>", "JSON file of ladder disposition declarations (waived / follow-up-filed / fixed-in-thread) for previously flagged findings").option("--issue <number>", "issue number recorded on the review ladder trace", positiveInteger$1("--issue")).option("--output <path>", "write proof JSON to a file").option("--profile <path>", "path to the project profile JSON file").option("--verify-proof <path>", "pr:verify proof JSON path").action(withGateTiming({
17741
+ return markCwdOptionDefault(new Command("pr:review").description("Assemble a factory-owned review prompt, or validate clean-session findings and write typed review proof").option("--base <ref>", "base branch or ref", "origin/main").option("--cwd <path>", "working directory to review", collectCwdOption).option("--cycle <number>", "review cycle number", positiveInteger$1("--cycle"), 1).option("--mode <mode>", "correctness, security, or all", "all").option("--assemble", "write the factory-owned review prompt for a clean session; do not validate findings").option("--findings <path>", "typed clean-session findings JSON file").option("--dispositions <path>", "JSON file of ladder disposition declarations (waived / follow-up-filed / fixed-in-thread) for previously flagged findings").option("--issue <number>", "issue number recorded on the review ladder trace; --assemble also loads Review focus from this issue", positiveInteger$1("--issue")).option("--output <path>", "write proof JSON to a file").option("--profile <path>", "path to the project profile JSON file").option("--verify-proof <path>", "pr:verify proof JSON path").action(withGateTiming({
17074
17742
  gate: "pr:review",
17075
17743
  resolveCycle: (options) => options.cycle,
17076
17744
  resolveLedgerRoot: (options) => resolveCwdOption(options.cwd),
@@ -17083,6 +17751,7 @@ function createPrReviewCommand(output, action) {
17083
17751
  "all"
17084
17752
  ].includes(options.mode)) throw new Error("--mode requires correctness, security, or all");
17085
17753
  const args = {
17754
+ ...options.assemble === true ? { assemble: true } : {},
17086
17755
  base: options.base,
17087
17756
  cwd: resolveCwdOption(options.cwd),
17088
17757
  cycle: options.cycle,
@@ -17399,171 +18068,6 @@ function formatScratchFileCheckSummary(check) {
17399
18068
  return "Scratch files: not checked (no readable worktree path)";
17400
18069
  }
17401
18070
  //#endregion
17402
- //#region src/issue-body-sections.ts
17403
- const HEADING_PATTERN = /^#{2,6}\s+(?<title>.+?)\s*$/u;
17404
- const FENCE_PATTERN = /^\s*(?<fence>`{3,}|~{3,})(?<rest>.*)$/u;
17405
- /**
17406
- * Extracts the body of a markdown section (heading depth `##`–`######`,
17407
- * case-insensitive title match) from an issue body. Duplicate sections
17408
- * resolve to the first occurrence.
17409
- *
17410
- * The scan is fence-aware: heading-like lines inside fenced code blocks
17411
- * (``` or ~~~, including indented and language-tagged fences) neither start
17412
- * nor terminate a section. Fences are matched by marker character (backtick
17413
- * vs. tilde) and require a closing fence with at least as many marker
17414
- * characters as the opener, per CommonMark; an unclosed fence extends to the
17415
- * end of the body.
17416
- */
17417
- const sectionBody = (body, heading) => {
17418
- const lines = body.split(/\r?\n/u);
17419
- const target = heading.toLowerCase();
17420
- let openFence;
17421
- let startIndex = -1;
17422
- for (const [index, line] of lines.entries()) {
17423
- const fence = FENCE_PATTERN.exec(line)?.groups;
17424
- const marker = fence?.fence;
17425
- if (openFence) {
17426
- if (marker && marker[0] === openFence.char && marker.length >= openFence.length && (fence?.rest ?? "").trim() === "") openFence = void 0;
17427
- continue;
17428
- }
17429
- if (marker) {
17430
- openFence = {
17431
- char: marker[0],
17432
- length: marker.length
17433
- };
17434
- continue;
17435
- }
17436
- const title = HEADING_PATTERN.exec(line)?.groups?.title;
17437
- if (title === void 0) continue;
17438
- if (startIndex === -1) {
17439
- if (title.toLowerCase() === target) startIndex = index;
17440
- continue;
17441
- }
17442
- return lines.slice(startIndex + 1, index).join("\n").trim();
17443
- }
17444
- if (startIndex === -1) return;
17445
- return lines.slice(startIndex + 1).join("\n").trim();
17446
- };
17447
- //#endregion
17448
- //#region src/review-focus.ts
17449
- const REVIEW_FOCUS_SECTION = "Review focus";
17450
- const reviewFocusFromIssueBody = (body) => {
17451
- return sectionBody(body, "Review focus") || void 0;
17452
- };
17453
- //#endregion
17454
- //#region src/review-contract.ts
17455
- const REVIEW_FOOTER_INSTRUCTIONS = `Do not print a machine-readable footer in your chat response. Instead, write your findings to the typed findings file at the path given in your dispatch/task instructions (the file pr:review reads via --findings <path>). The file must be valid JSON matching review-findings-file.ts's schema (schemaVersion 1):
17456
- {
17457
- "schemaVersion": 1,
17458
- "model": "<your model identifier>",
17459
- "sessionId": "<this session's id>",
17460
- "patchId": "<the stable patch-id from Review scope above>",
17461
- "reviews": [
17462
- {
17463
- "kind": "correctness" or "security" (this review's mode, matching the mode-floor instructions below),
17464
- "outcome": "passed" or "failed",
17465
- "issuesFlagged": an integer count of actionable findings (must equal findings.length),
17466
- "summary": one concise sentence; use "No actionable findings." only when issuesFlagged is 0,
17467
- "findings": [] when issuesFlagged is 0, otherwise an array of objects with title, body, and optional file, line, priority, category, blockingAfterCap, citedSpan, prescribedAction, and supersedes fields
17468
- }
17469
- ]
17470
- }
17471
- Use category "correctness", "safety", "coordination", or "maintainability"; set blockingAfterCap true for correctness, safety, or coordination findings. citedSpan must be the exact code text the finding is about, quoted verbatim from the diff (identity is derived from this, not from the title, so quote precisely). prescribedAction must be a concise, standalone statement of the required fix (identity is derived from this text too). If this finding is a restatement of a prior finding shown in the prior review ledger below, set supersedes to {"title": "<exact prior title>", "file": "<its file>"} instead of minting a new title for the same theme.`;
17472
- //#endregion
17473
- //#region src/review-prompt.ts
17474
- const reviewScope = ({ base, changedFiles: files, headSha, patchId, profile }) => `Review scope:
17475
- - Repository: ${profile.repository.owner}/${profile.repository.name}
17476
- - Base ref: ${base}
17477
- - Head SHA: ${headSha}
17478
- - Stable patch-id: ${patchId}
17479
- - Changed files:
17480
- ${files.length > 0 ? files.map((file) => ` - ${file}`).join("\n") : " - None"}
17481
-
17482
- Required constraints:
17483
- - Review only the PR-owned diff against ${base} and the changed files listed above.
17484
- - Prefer git diff, git grep, and file-specific reads.
17485
- - Do not inspect ignored generated output such as dist, build, coverage, or sourcemaps.
17486
- - Do not rerun broad build or verification commands. If pr:verify proof already exists, treat build verification as out of scope for this review runner.
17487
- - Do not request elevated or unsandboxed approvals.
17488
- - When you flag a finding, report every other instance of the same class in this diff in the same response, and include a one-line coverage note.
17489
-
17490
- ${REVIEW_FOOTER_INSTRUCTIONS}`;
17491
- const MODE_FLOOR_FILES = {
17492
- correctness: "correctness.md",
17493
- security: "security/SKILL.md"
17494
- };
17495
- const reviewPromptsDir = path.join(import.meta.dirname, "review-prompts");
17496
- const modeFloorCache = /* @__PURE__ */ new Map();
17497
- const securityReferenceFiles = (changedFiles) => {
17498
- const references = /* @__PURE__ */ new Set();
17499
- for (const file of changedFiles) {
17500
- if (/^\.github\/(?:actions|workflows)\//u.test(file)) references.add("security/references/github-workflows.md");
17501
- if (/\.(?:[cm]?[jt]sx?)$/u.test(file)) references.add("security/references/javascript-typescript.md");
17502
- if (/\.py(?:i)?$/u.test(file)) references.add("security/references/python.md");
17503
- }
17504
- return [...references];
17505
- };
17506
- function modeFloorInstructions(kind, changedFiles = []) {
17507
- const referenceFiles = kind === "security" ? securityReferenceFiles(changedFiles) : [];
17508
- const cacheKey = `${kind}:${referenceFiles.join(",")}`;
17509
- const cached = modeFloorCache.get(cacheKey);
17510
- if (cached !== void 0) return cached;
17511
- const text = [MODE_FLOOR_FILES[kind], ...referenceFiles].map((file) => readFileSync(path.join(reviewPromptsDir, file), "utf-8")).map((contents) => contents.replace(/\n+$/u, "")).join("\n\n---\n\n");
17512
- modeFloorCache.set(cacheKey, text);
17513
- return text;
17514
- }
17515
- const issueFocusSegment = (focus) => ({
17516
- section: {
17517
- provenance: "issue-review-focus",
17518
- source: `issue #${focus.issueNumber} "${REVIEW_FOCUS_SECTION}" section`,
17519
- text: focus.text
17520
- },
17521
- text: `Per-issue review focus (source: issue #${focus.issueNumber} "${REVIEW_FOCUS_SECTION}" section, appended verbatim):
17522
- ${focus.text}`
17523
- });
17524
- const priorLedgerEntryLine = (entry) => {
17525
- const location = entry.file ? ` (${entry.file})` : "";
17526
- const reference = entry.reference ? ` — ${entry.reference}` : "";
17527
- return `- [${entry.disposition}] "${entry.title}"${location}${reference} (flagged ${entry.flagCount}x)`;
17528
- };
17529
- const priorLedgerSegment = (entries) => ({
17530
- section: {
17531
- provenance: "prior-review-ledger",
17532
- source: "persisted pr:review disposition ledger (prior cycles, #233)",
17533
- text: entries.map(priorLedgerEntryLine).join("\n")
17534
- },
17535
- text: `Prior review ledger (source: persisted pr:review disposition ledger from earlier cycles on this PR; already adjudicated — do not re-litigate):
17536
- ${entries.map(priorLedgerEntryLine).join("\n")}
17537
-
17538
- Do not re-raise any of the findings above under a new title: they are already adjudicated (waived, filed as a follow-up, or fixed in this thread). If new evidence genuinely supersedes one of them, say so explicitly and name the exact prior finding (its title and file) that it supersedes, rather than minting an unrelated title for the same underlying theme.`
17539
- });
17540
- const assembleReviewPrompt = ({ base, changedFiles, headSha, issueFocus, kind, patchId, priorLedgerEntries, profile }) => {
17541
- if (!resolveApplicableReviewModes(profile, changedFiles).includes(kind)) throw new Error(`${kind} review mode does not apply to the changed files.`);
17542
- const priorLedger = priorLedgerEntries ? nonOpenLadderFindings(priorLedgerEntries) : [];
17543
- const optionalSegments = {
17544
- "issue-review-focus": issueFocus ? issueFocusSegment(issueFocus) : void 0,
17545
- "prior-review-ledger": priorLedger.length > 0 ? priorLedgerSegment(priorLedger) : void 0
17546
- };
17547
- const segments = [
17548
- { text: reviewScope({
17549
- base,
17550
- changedFiles,
17551
- headSha,
17552
- patchId,
17553
- profile
17554
- }) },
17555
- { text: modeFloorInstructions(kind, changedFiles) },
17556
- ...REVIEW_PROMPT_SECTION_PROVENANCES.flatMap((provenance) => {
17557
- const segment = optionalSegments[provenance];
17558
- return segment ? [segment] : [];
17559
- })
17560
- ];
17561
- return {
17562
- prompt: segments.map((segment) => segment.text).join("\n\n"),
17563
- sections: segments.flatMap((segment) => segment.section ? [segment.section] : [])
17564
- };
17565
- };
17566
- //#endregion
17567
18071
  //#region src/index.ts
17568
18072
  function createProgram(options = {}) {
17569
18073
  const factoryCliInvocation = options.factoryCliInvocation ? FactoryCliInvocationSchema.parse(options.factoryCliInvocation) : defaultFactoryCliInvocation(import.meta.filename);