@patronage/software-factory 1.0.0-alpha.40 → 1.0.0-alpha.42

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.d.ts CHANGED
@@ -138,8 +138,8 @@ declare const boundaryManifestBaseSchema: z.ZodObject<{
138
138
  integrationBranch: z.ZodOptional<z.ZodString>;
139
139
  schemaVersion: z.ZodLiteral<1>;
140
140
  topology: z.ZodEnum<{
141
- flagged: "flagged";
142
141
  "each-to-main": "each-to-main";
142
+ flagged: "flagged";
143
143
  stacked: "stacked";
144
144
  "each-to-epic": "each-to-epic";
145
145
  }>;
@@ -167,8 +167,8 @@ declare const boundaryManifestSchema: z.ZodObject<{
167
167
  integrationBranch: z.ZodOptional<z.ZodString>;
168
168
  schemaVersion: z.ZodLiteral<1>;
169
169
  topology: z.ZodEnum<{
170
- flagged: "flagged";
171
170
  "each-to-main": "each-to-main";
171
+ flagged: "flagged";
172
172
  stacked: "stacked";
173
173
  "each-to-epic": "each-to-epic";
174
174
  }>;
@@ -228,7 +228,7 @@ interface GithubIssueComment {
228
228
  }
229
229
  declare const normalizeIssueComments: (value: GithubIssueCommentApi[] | GithubIssueCommentApi[][]) => GithubIssueComment[];
230
230
  declare namespace boundary_review_proof_d_exports {
231
- export { BOUNDARY_REVIEW_PROOF_KIND, BoundaryReviewProof, BoundaryReviewProofComment, boundaryReviewProofAuthorsSeen, boundaryReviewProofSchema, selectBoundaryReviewProof, undispositionedBlockingFindings };
231
+ export { BOUNDARY_REVIEW_PROOF_KIND, BoundaryReviewProof, BoundaryReviewProofComment, BoundaryReviewProofParseFailure, boundaryReviewProofAuthorsSeen, boundaryReviewProofParseFailures, boundaryReviewProofSchema, selectBoundaryReviewProof, undispositionedBlockingFindings };
232
232
  }
233
233
  declare const BOUNDARY_REVIEW_PROOF_KIND = "boundary-review-proof";
234
234
  declare const boundaryReviewProofBaseSchema: z.ZodObject<{
@@ -239,8 +239,8 @@ declare const boundaryReviewProofBaseSchema: z.ZodObject<{
239
239
  mergedSha: z.ZodOptional<z.ZodString>;
240
240
  pr: z.ZodNumber;
241
241
  state: z.ZodEnum<{
242
- merged: "merged";
243
242
  open: "open";
243
+ merged: "merged";
244
244
  }>;
245
245
  wave: z.ZodOptional<z.ZodString>;
246
246
  }, z.core.$loose>>;
@@ -283,8 +283,8 @@ declare const boundaryReviewProofSchema: z.ZodObject<{
283
283
  mergedSha: z.ZodOptional<z.ZodString>;
284
284
  pr: z.ZodNumber;
285
285
  state: z.ZodEnum<{
286
- merged: "merged";
287
286
  open: "open";
287
+ merged: "merged";
288
288
  }>;
289
289
  wave: z.ZodOptional<z.ZodString>;
290
290
  }, z.core.$loose>>;
@@ -325,8 +325,14 @@ interface BoundaryReviewProofComment {
325
325
  commentId?: string | number;
326
326
  commentUrl?: string;
327
327
  }
328
+ interface BoundaryReviewProofParseFailure {
329
+ commentAuthor: string;
330
+ commentUrl?: string;
331
+ parseError: string;
332
+ }
328
333
  declare const selectBoundaryReviewProof: (comments: GithubIssueComment[], boundary: string, declaredBy: string) => BoundaryReviewProofComment | undefined;
329
334
  declare const boundaryReviewProofAuthorsSeen: (comments: GithubIssueComment[], boundary: string) => string[];
335
+ declare const boundaryReviewProofParseFailures: (comments: GithubIssueComment[], boundary: string) => BoundaryReviewProofParseFailure[];
330
336
  declare const undispositionedBlockingFindings: (proof: BoundaryReviewProof) => string[];
331
337
  //#endregion
332
338
  //#region src/boundary-membership-graph.d.ts
@@ -796,7 +802,7 @@ interface LadderCycleRef {
796
802
  stage: LadderCycleStage;
797
803
  }
798
804
  type LadderFindingReport = ReviewFindingAttributes;
799
- /** One finding as recorded in one cycle (CONTEXT.md: finding occurrence). */
805
+ /** One finding as recorded in one cycle (GLOSSARY.md: finding occurrence). */
800
806
  interface ReviewOccurrenceRecord {
801
807
  address: string;
802
808
  /** The ladder-assigned cycle number the occurrence was recorded in. */
@@ -813,7 +819,7 @@ declare const POLICY_DECLARATION_RULES: readonly ["cycle-2-severity-floor", "ide
813
819
  type PolicyDeclarationRule = (typeof POLICY_DECLARATION_RULES)[number];
814
820
  type DeclarationAuthority = "operator" | "reviewer" | "policy";
815
821
  /**
816
- * A disposition declaration as stored (CONTEXT.md: disposition declaration):
822
+ * A disposition declaration as stored (GLOSSARY.md: disposition declaration):
817
823
  * one occurrence, one disposition, the authority that made it and its
818
824
  * reference (operator, reviewer) or rule (policy).
819
825
  */
@@ -1735,14 +1741,19 @@ interface ExactHeadRunSource {
1735
1741
  //#endregion
1736
1742
  //#region src/merge-freeze.d.ts
1737
1743
  declare const MERGE_FREEZE_CHECK_NAME = "patronage-factory/merge-freeze";
1744
+ /**
1745
+ * The Factory App's slug, as GitHub serves it on the runs the writer posts.
1746
+ * It is not a trust predicate: an App chooses its own slug, so the reader
1747
+ * trusts the pinned numeric App id instead (ADR 0022, #1393).
1748
+ */
1738
1749
  declare const MERGE_FREEZE_APP_SLUG = "patronage-factory";
1739
1750
  declare const mergeFreezeStateSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
1740
1751
  active: z.ZodLiteral<true>;
1741
1752
  generationId: z.ZodNumber;
1742
1753
  headSha: z.ZodString;
1743
1754
  outcome: z.ZodEnum<{
1744
- stale: "stale";
1745
1755
  active: "active";
1756
+ stale: "stale";
1746
1757
  }>;
1747
1758
  reason: z.ZodString;
1748
1759
  recordedAt: z.ZodISODateTime;
@@ -2029,10 +2040,10 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
2029
2040
  reviewedHeadSha: z.ZodOptional<z.ZodString>;
2030
2041
  reviewedPatchId: z.ZodOptional<z.ZodString>;
2031
2042
  status: z.ZodEnum<{
2043
+ stale: "stale";
2032
2044
  blocked: "blocked";
2033
2045
  "not-required": "not-required";
2034
2046
  current: "current";
2035
- stale: "stale";
2036
2047
  missing: "missing";
2037
2048
  }>;
2038
2049
  }, z.core.$strip>;
@@ -2041,10 +2052,10 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
2041
2052
  reviewedHeadSha: z.ZodOptional<z.ZodString>;
2042
2053
  reviewedPatchId: z.ZodOptional<z.ZodString>;
2043
2054
  status: z.ZodEnum<{
2055
+ stale: "stale";
2044
2056
  blocked: "blocked";
2045
2057
  "not-required": "not-required";
2046
2058
  current: "current";
2047
- stale: "stale";
2048
2059
  missing: "missing";
2049
2060
  }>;
2050
2061
  }, z.core.$strip>>;
@@ -2495,6 +2506,12 @@ interface CompletedFactoryCheckSnapshot {
2495
2506
  readonly detailsUrl?: string;
2496
2507
  readonly headSha: string;
2497
2508
  readonly name: FactoryPrStatusCheckName;
2509
+ /**
2510
+ * Merge freeze only (#1501): the cancellation-only generations, newest
2511
+ * first, that the freeze selection read past to reach this snapshot's
2512
+ * generation (#1158). Absent when the newest generation stands.
2513
+ */
2514
+ readonly readPastRunIds?: readonly number[];
2498
2515
  }
2499
2516
  interface FailedPreviewSnapshot {
2500
2517
  readonly completedAt: string;
@@ -3114,10 +3131,10 @@ declare const loadEvidenceEnvelopes: (cwd: string) => LoadedEvidenceEnvelope[];
3114
3131
  //#region src/review-proof-applicability.d.ts
3115
3132
  declare const REVIEW_STATUS_VALUES: readonly ["not-required", "current", "stale", "missing", "blocked"];
3116
3133
  declare const reviewStatusSchema: z.ZodEnum<{
3134
+ stale: "stale";
3117
3135
  blocked: "blocked";
3118
3136
  "not-required": "not-required";
3119
3137
  current: "current";
3120
- stale: "stale";
3121
3138
  missing: "missing";
3122
3139
  }>;
3123
3140
  type ReviewStatus = z.infer<typeof reviewStatusSchema>;
@@ -3225,8 +3242,8 @@ declare const boundaryCheckProofSchema: z.ZodObject<{
3225
3242
  pr: z.ZodNumber;
3226
3243
  sha: z.ZodString;
3227
3244
  state: z.ZodEnum<{
3228
- merged: "merged";
3229
3245
  open: "open";
3246
+ merged: "merged";
3230
3247
  }>;
3231
3248
  wave: z.ZodString;
3232
3249
  }, z.core.$strip>>;
@@ -3628,6 +3645,12 @@ interface PrVerifyDependencies {
3628
3645
  }
3629
3646
  declare function runPrVerify(args: PrVerifyArgs, dependencies?: PrVerifyDependencies): PrVerifyProof;
3630
3647
  //#endregion
3648
+ //#region src/pull-request-fill.d.ts
3649
+ interface CommitMessage {
3650
+ body: string;
3651
+ subject: string;
3652
+ }
3653
+ //#endregion
3631
3654
  //#region src/pr-publish.d.ts
3632
3655
  type PrPublishArgs = Omit<PrReadyArgs, "pr" | "throwWhenBlocked"> & {
3633
3656
  /** A clean-session findings file to validate into the review proof. */findings?: string;
@@ -3726,6 +3749,12 @@ interface PrPublishDependencies extends PrReadyDependencies {
3726
3749
  ensureVerifyCheckRun?: typeof ensureFactoryCheckRunPublished;
3727
3750
  factoryCliInvocation?: FactoryCliInvocation;
3728
3751
  gh?: GhRunner;
3752
+ /**
3753
+ * The branch's commits into its base, oldest first, that a pull request
3754
+ * publish creates is titled and described from (#1366). Tests only;
3755
+ * production reads them from Git.
3756
+ */
3757
+ pullRequestCommits?: PullRequestCommits;
3729
3758
  github?: PrReadyDependencies["github"] & {
3730
3759
  editPullRequestBody?: (input: {
3731
3760
  body: string;
@@ -3741,7 +3770,10 @@ interface PrPublishDependencies extends PrReadyDependencies {
3741
3770
  * carries the publisher for the verdict it records (#1323).
3742
3771
  */
3743
3772
  runPrReview?: (args: PrReviewArgs, dependencies?: ProducerPublisher) => Promise<PrReviewProof>;
3744
- /** Settles asynchronously scheduled sink work before the handoff drain. */
3773
+ /**
3774
+ * Settles asynchronously scheduled sink work: after a review cycle publish
3775
+ * records from `--findings` (#1382), and before the handoff drain.
3776
+ */
3745
3777
  awaitPendingIngest?: () => Promise<void>;
3746
3778
  /**
3747
3779
  * The handoff drain (#390). Advisory everywhere: publish reports what it
@@ -3767,6 +3799,7 @@ interface PrPublishDependencies extends PrReadyDependencies {
3767
3799
  */
3768
3800
  sleep?: (ms: number) => Promise<void>;
3769
3801
  }
3802
+ type PullRequestCommits = (cwd: string, base: string) => CommitMessage[];
3770
3803
  /**
3771
3804
  * What became of the durable `pr:verify` binding for this publish. Reported in
3772
3805
  * the publish summary: a binding that silently failed to land is
@@ -4647,13 +4680,13 @@ declare const evaluateReadiness: (input: EvaluationInput) => {
4647
4680
  reviews: {
4648
4681
  correctness: {
4649
4682
  required: boolean;
4650
- status: "blocked" | "not-required" | "current" | "stale" | "missing";
4683
+ status: "stale" | "blocked" | "not-required" | "current" | "missing";
4651
4684
  reviewedHeadSha?: string | undefined;
4652
4685
  reviewedPatchId?: string | undefined;
4653
4686
  };
4654
4687
  security?: {
4655
4688
  required: boolean;
4656
- status: "blocked" | "not-required" | "current" | "stale" | "missing";
4689
+ status: "stale" | "blocked" | "not-required" | "current" | "missing";
4657
4690
  reviewedHeadSha?: string | undefined;
4658
4691
  reviewedPatchId?: string | undefined;
4659
4692
  } | undefined;
@@ -4766,13 +4799,13 @@ declare const evaluateReadiness: (input: EvaluationInput) => {
4766
4799
  reviews: {
4767
4800
  correctness: {
4768
4801
  required: boolean;
4769
- status: "blocked" | "not-required" | "current" | "stale" | "missing";
4802
+ status: "stale" | "blocked" | "not-required" | "current" | "missing";
4770
4803
  reviewedHeadSha?: string | undefined;
4771
4804
  reviewedPatchId?: string | undefined;
4772
4805
  };
4773
4806
  security?: {
4774
4807
  required: boolean;
4775
- status: "blocked" | "not-required" | "current" | "stale" | "missing";
4808
+ status: "stale" | "blocked" | "not-required" | "current" | "missing";
4776
4809
  reviewedHeadSha?: string | undefined;
4777
4810
  reviewedPatchId?: string | undefined;
4778
4811
  } | undefined;
@@ -4927,7 +4960,7 @@ declare const EPIC_STRUCTURE_SCHEMA_VERSION = 1;
4927
4960
  /**
4928
4961
  * DAG-node PLANNING statuses — the planning-plane vocabulary a planner authors
4929
4962
  * on a `dag.yml` node. Carried forward by ADR 0018 and defined by the epic
4930
- * skill (`reference/epic-artifacts.md`) and both `CONTEXT.md` glossaries.
4963
+ * skill (`reference/epic-artifacts.md`) and both `GLOSSARY.md` glossaries.
4931
4964
  *
4932
4965
  * A DAG node is a unit of *plan*; an HQ lane is a unit of *runtime execution*.
4933
4966
  * These are two planes with two vocabularies and must not be conflated — the
package/dist/index.js CHANGED
@@ -22,7 +22,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
22
22
  import { pathToFileURL } from "node:url";
23
23
  //#region package.json
24
24
  var name = "@patronage/software-factory";
25
- var version = "1.0.0-alpha.40";
25
+ var version = "1.0.0-alpha.42";
26
26
  //#endregion
27
27
  //#region src/git-globals-parse.ts
28
28
  const VALUE_TAKING_GIT_OPTIONS = new Set([
@@ -2507,6 +2507,7 @@ const coveredSetDivergences = (computed, proof) => {
2507
2507
  var boundary_review_proof_exports = /* @__PURE__ */ __exportAll({
2508
2508
  BOUNDARY_REVIEW_PROOF_KIND: () => BOUNDARY_REVIEW_PROOF_KIND$1,
2509
2509
  boundaryReviewProofAuthorsSeen: () => boundaryReviewProofAuthorsSeen,
2510
+ boundaryReviewProofParseFailures: () => boundaryReviewProofParseFailures,
2510
2511
  boundaryReviewProofSchema: () => boundaryReviewProofSchema$1,
2511
2512
  selectBoundaryReviewProof: () => selectBoundaryReviewProof,
2512
2513
  undispositionedBlockingFindings: () => undispositionedBlockingFindings
@@ -2561,8 +2562,11 @@ const boundaryReviewProofSchema$1 = z.object({
2561
2562
  }).passthrough().optional()
2562
2563
  }).passthrough();
2563
2564
  const FENCED_JSON_PATTERN = /```json[ \t]*\r?\n(?<inner>[\s\S]*?)```/gu;
2564
- const collectValidBoundaryReviewProofs = (comments, boundary) => {
2565
- const entries = [];
2565
+ const scanBoundaryReviewProofs = (comments, boundary) => {
2566
+ const scan = {
2567
+ failed: [],
2568
+ valid: []
2569
+ };
2566
2570
  for (const comment of comments) {
2567
2571
  const commentAuthor = comment.author.login;
2568
2572
  if (!commentAuthor) continue;
@@ -2576,8 +2580,17 @@ const collectValidBoundaryReviewProofs = (comments, boundary) => {
2576
2580
  }
2577
2581
  if (typeof raw !== "object" || raw === null || raw.kind !== "boundary-review-proof") continue;
2578
2582
  const parsed = boundaryReviewProofSchema$1.safeParse(raw);
2579
- if (!parsed.success || parsed.data.boundary !== boundary) continue;
2580
- entries.push({
2583
+ if (!parsed.success) {
2584
+ const rawBoundary = raw.boundary;
2585
+ if (typeof rawBoundary !== "string" || rawBoundary === boundary) scan.failed.push({
2586
+ commentAuthor,
2587
+ ...comment.url === void 0 ? {} : { commentUrl: comment.url },
2588
+ parseError: formatZodIssues(parsed.error)
2589
+ });
2590
+ continue;
2591
+ }
2592
+ if (parsed.data.boundary !== boundary) continue;
2593
+ scan.valid.push({
2581
2594
  commentAuthor,
2582
2595
  ...comment.id === void 0 ? {} : { commentId: comment.id },
2583
2596
  ...comment.url === void 0 ? {} : { commentUrl: comment.url },
@@ -2585,12 +2598,12 @@ const collectValidBoundaryReviewProofs = (comments, boundary) => {
2585
2598
  });
2586
2599
  }
2587
2600
  }
2588
- return entries;
2601
+ return scan;
2589
2602
  };
2590
2603
  const selectBoundaryReviewProof = (comments, boundary, declaredBy) => {
2591
2604
  let selected;
2592
2605
  const declaredByLower = declaredBy.toLowerCase();
2593
- for (const entry of collectValidBoundaryReviewProofs(comments, boundary)) {
2606
+ for (const entry of scanBoundaryReviewProofs(comments, boundary).valid) {
2594
2607
  if (entry.commentAuthor.toLowerCase() !== declaredByLower) continue;
2595
2608
  selected = {
2596
2609
  ...entry.commentId === void 0 ? {} : { commentId: entry.commentId },
@@ -2602,9 +2615,10 @@ const selectBoundaryReviewProof = (comments, boundary, declaredBy) => {
2602
2615
  };
2603
2616
  const boundaryReviewProofAuthorsSeen = (comments, boundary) => {
2604
2617
  const authors = /* @__PURE__ */ new Set();
2605
- for (const entry of collectValidBoundaryReviewProofs(comments, boundary)) authors.add(entry.commentAuthor);
2618
+ for (const entry of scanBoundaryReviewProofs(comments, boundary).valid) authors.add(entry.commentAuthor);
2606
2619
  return [...authors];
2607
2620
  };
2621
+ const boundaryReviewProofParseFailures = (comments, boundary) => scanBoundaryReviewProofs(comments, boundary).failed;
2608
2622
  const undispositionedBlockingFindings = (proof) => proof.findings.filter((finding) => (finding.category !== "maintainability" || finding.blockingAfterCap === true) && finding.disposition === void 0).map((finding) => `boundary finding "${finding.title}" (category ${finding.category ?? "unknown"}) has no disposition; fix it or record waived / follow-up-filed`);
2609
2623
  //#endregion
2610
2624
  //#region src/boundary-topology.ts
@@ -3030,6 +3044,42 @@ const waivedDemandSchema = z.object({
3030
3044
  session: z.string().trim().min(1).optional(),
3031
3045
  unmetReasons: z.array(z.string().min(1)).min(1)
3032
3046
  });
3047
+ /**
3048
+ * The demands an operator waiver can reach (#354; moved out of `pr:ready` by
3049
+ * #1492 so `demand:waive` refuses by the same contract every reader folds by,
3050
+ * and can never record an authorization nothing applies).
3051
+ *
3052
+ * `pr:ready` folds the merge freeze, the profile's external required checks
3053
+ * and the boundary wave's review rung — the demands the resolver puts *in
3054
+ * force* for one candidate — plus the review-round limit (#1280), which exists
3055
+ * only to be waived and which `pr:review --assemble` folds too.
3056
+ * `boundary:check --final` folds the closeout pin `child-issue-open` (#901);
3057
+ * readiness never raises that demand, so naming it here widens nothing at
3058
+ * `pr:ready`.
3059
+ *
3060
+ * Everything else stays unwaivable. Structural refusals — candidate identity,
3061
+ * proof binding, an unreadable demand authority, wave membership — were never
3062
+ * waivable at merge time and are not waivable now; in `pr:ready` most of them
3063
+ * refuse by throwing before any blocker list exists. The evidence gates
3064
+ * readiness has always owned (verify, review modes, draft, human blockers) and
3065
+ * GitHub's own check and merge-state rollups are satisfied or not; a waiver is
3066
+ * not a second way to pass them.
3067
+ */
3068
+ const WAIVABLE_DEMAND_KEYS = [
3069
+ DEMAND_KEYS.mergeFreeze,
3070
+ DEMAND_KEYS.reviewRoundLimit,
3071
+ DEMAND_KEYS.childIssueOpen
3072
+ ];
3073
+ /** Every instance of these qualified families is waivable at `pr:ready`. */
3074
+ const WAIVABLE_QUALIFIED_DEMAND_FAMILIES = ["required-check", "review-rung"];
3075
+ const isWaivableDemand = (demand) => WAIVABLE_DEMAND_KEYS.includes(demand) || WAIVABLE_QUALIFIED_DEMAND_FAMILIES.some((family) => demand.startsWith(`${family}:`));
3076
+ /**
3077
+ * Why `demand:waive` refuses a well-formed key outside that contract. It names
3078
+ * the key and the waivable set, built from the same lists the predicate reads,
3079
+ * and says nothing about the demand itself: whether it is in force or met is
3080
+ * the reader's to decide, and a refused waiver changes neither.
3081
+ */
3082
+ const ineligibleWaiverRefusal = (demand) => `demand:waive refuses demand key "${demand}": no factory command applies a waiver of it, so a recorded waiver would authorize nothing. A waiver can name ${WAIVABLE_DEMAND_KEYS.join(", ")}, or a ${WAIVABLE_QUALIFIED_DEMAND_FAMILIES.map((family) => `${family}:<value>`).join(" or ")} key.`;
3033
3083
  /** The waivers that bind one candidate: same PR, same head. */
3034
3084
  const selectWaiversForCandidate = ({ headSha, pr, waivers }) => headSha === void 0 ? [] : waivers.filter((waiver) => waiver.candidate.pr === pr && waiver.candidate.headSha === headSha);
3035
3085
  /**
@@ -4353,7 +4403,7 @@ function resolveCandidateIdentity({ base, cwd, git }) {
4353
4403
  * and `resolveCandidateIdentity`'s own 40-character object-id check would
4354
4404
  * reject a legitimate 64-character patch id from a SHA-256 repository, which
4355
4405
  * the original direct call never rejected. This is a narrower view over the
4356
- * same owned machine — not a second primitive and not a new `CONTEXT.md`
4406
+ * same owned machine — not a second primitive and not a new `GLOSSARY.md`
4357
4407
  * entry.
4358
4408
  *
4359
4409
  * Takes the primitive itself, not a `git` object to read it off: the original
@@ -7305,7 +7355,7 @@ const headShaMatches = (recorded, candidate) => Boolean(recorded.headSha && cand
7305
7355
  * describes the candidate.
7306
7356
  *
7307
7357
  * Time is not an input and never will be: adding one would contradict the
7308
- * `freshness` definition in root `CONTEXT.md`.
7358
+ * `freshness` definition in root `GLOSSARY.md`.
7309
7359
  */
7310
7360
  function evidenceFresh({ candidate, recorded, rule }) {
7311
7361
  if (patchIdMatches(recorded, candidate)) return true;
@@ -8835,7 +8885,7 @@ const readTransportedVerifyProof = (value, text) => {
8835
8885
  */
8836
8886
  const parsePrVerifyCheckPayload = (text) => {
8837
8887
  if (!text?.trim()) return;
8838
- const candidate = (text.match(/```(?:json)?\s*(?<body>[\s\S]*?)```/u)?.groups?.body ?? text).trim();
8888
+ const candidate = (text.match(/```(?:json)?\s*(?<body>[\s\S]*?)\n```/u)?.groups?.body ?? text).trim();
8839
8889
  try {
8840
8890
  const parsed = JSON.parse(candidate);
8841
8891
  if (parsed.schemaVersion !== PR_VERIFY_CHECK_PAYLOAD_SCHEMA_VERSION || parsed.kind !== "pr-verify-proof-binding" || typeof parsed.headSha !== "string" || parsed.headSha.length === 0 || !isResolvedPrVerifyMode(parsed.mode) || !isOutcome(parsed.outcome)) return;
@@ -9632,6 +9682,200 @@ const completeCheckRunPages = (pages) => {
9632
9682
  const runs = pages.flatMap((page) => page.check_runs);
9633
9683
  return pages.every((page) => page.total_count === runs.length) ? runs : void 0;
9634
9684
  };
9685
+ /** The only instant shape either pr-verify reader takes from a typed proof. */
9686
+ const CANONICAL_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/u;
9687
+ /** An instant in epoch milliseconds, rounded down to its whole second. */
9688
+ const wholeSecondMs = (ms) => Math.floor(ms / 1e3) * 1e3;
9689
+ /**
9690
+ * When the verification a transported typed proof records started: its
9691
+ * `endedAt` minus its `durationMs`, rounded down to the whole second. Nothing
9692
+ * when `endedAt` is not canonical UTC or `durationMs` is not a finite
9693
+ * non-negative number; the run is then ordered like one without a typed proof.
9694
+ */
9695
+ const proofStartMs = (proof) => {
9696
+ if (proof === void 0 || !CANONICAL_UTC.test(proof.endedAt)) return;
9697
+ const { durationMs } = proof;
9698
+ if (!(Number.isFinite(durationMs) && durationMs >= 0)) return;
9699
+ const startMs = Date.parse(proof.endedAt) - durationMs;
9700
+ return Number.isFinite(startMs) ? wholeSecondMs(startMs) : void 0;
9701
+ };
9702
+ /**
9703
+ * Whether a run is a pass: completed with conclusion `success`, and its bound
9704
+ * payload has outcome `passed`. The hosted gate's jq calls this `passing`.
9705
+ */
9706
+ const isPassingRun = (run, payload) => payload !== void 0 && run.status === "completed" && run.conclusion === "success" && payload.outcome === "passed";
9707
+ /**
9708
+ * When a passing run's verification started, by its transported typed proof
9709
+ * (`proofStartMs`). Nothing for any other run, which is ordered at its
9710
+ * `started_at` whatever proof it carries (#1506).
9711
+ */
9712
+ const passingProofStartMs = (run, payload) => isPassingRun(run, payload) ? proofStartMs(payload.verificationProof) : void 0;
9713
+ /** The same bound payload with the same status and conclusion. */
9714
+ const sameVerdict = (one, other) => one.payload !== void 0 && (one.run.status ?? "") === (other.run.status ?? "") && (one.run.conclusion ?? "") === (other.run.conclusion ?? "") && isDeepStrictEqual(one.payload, other.payload);
9715
+ /**
9716
+ * Every generation among the served runs, ordered and grouped by the rule
9717
+ * `newestVerifyProofGeneration` states, and every run's post with the
9718
+ * generation it belongs to.
9719
+ */
9720
+ const verifyProofGenerations = (runs, bound) => {
9721
+ const generations = [];
9722
+ const posts = [];
9723
+ for (const run of runs) {
9724
+ const parsed = parsePrVerifyCheckPayload(run.output?.text) ?? parsePrVerifyCheckPayload(run.output?.summary);
9725
+ const payload = run.head_sha === bound.sha && parsed?.headSha === bound.sha && parsed.repository === bound.repository ? parsed : void 0;
9726
+ const postedMs = wholeSecondMs(generationOrderMs(run.started_at));
9727
+ const startMs = passingProofStartMs(run, payload);
9728
+ if (payload === void 0 || startMs === void 0) {
9729
+ const generation = {
9730
+ orderMs: postedMs,
9731
+ ...payload === void 0 ? {} : { payload },
9732
+ recorded: false,
9733
+ run
9734
+ };
9735
+ generations.push(generation);
9736
+ posts.push({
9737
+ generation,
9738
+ postedMs
9739
+ });
9740
+ continue;
9741
+ }
9742
+ const generation = {
9743
+ orderMs: Math.min(startMs, postedMs),
9744
+ payload,
9745
+ recorded: true,
9746
+ run
9747
+ };
9748
+ const earlier = generations.find((candidate) => candidate.recorded && sameVerdict(candidate, generation));
9749
+ if (earlier === void 0) generations.push(generation);
9750
+ else earlier.orderMs = Math.min(earlier.orderMs, generation.orderMs);
9751
+ posts.push({
9752
+ generation: earlier ?? generation,
9753
+ postedMs
9754
+ });
9755
+ }
9756
+ return {
9757
+ generations,
9758
+ posts
9759
+ };
9760
+ };
9761
+ /**
9762
+ * The newest generation of the `pr:verify` proof at one head, or nothing when
9763
+ * no single generation can be trusted. A passing generation is ordered by when
9764
+ * its verification started, never later than when its run reached GitHub and
9765
+ * never by when its verification ended (#1409, #1490). Every other run is
9766
+ * ordered by when it reached GitHub (#1506):
9767
+ *
9768
+ * - A passing run is completed with conclusion `success`, and its bound
9769
+ * payload has outcome `passed`. When that payload transports a typed proof
9770
+ * whose start is computable (`proofStartMs`: its `endedAt` minus its
9771
+ * `durationMs`), the run is ordered at that start, or at its own
9772
+ * `started_at` when that is earlier. Runs that carry the same such payload
9773
+ * with the same status and conclusion are one generation posted more than
9774
+ * once (`pr:verify` posts, `pr:publish` re-posts), ordered at the earliest of
9775
+ * its members' instants, whatever order GitHub serves them in. A re-post
9776
+ * does not move the proof's start, so an older pass re-posted after a newer
9777
+ * failure does not outrank it, and the normal post plus re-post is not a
9778
+ * tie.
9779
+ * - Any other run is its own generation at its `started_at`: one still
9780
+ * running, a failure or cancellation whether or not it carries a proof, one
9781
+ * with no bound payload (including one whose transported proof fails the
9782
+ * proof schema, which drops the whole payload), or a pass with no usable
9783
+ * typed proof (over the text cap, or with no computable start). A run is
9784
+ * created no earlier than its verification began, so `started_at` is an
9785
+ * upper bound on that start. Ranking there can only make such a run newer
9786
+ * than it is, so it vetoes more, never less.
9787
+ * - Neither `endedAt` nor a passing run's `started_at` is its start. Every
9788
+ * writer posts its `pr-verify` run only once verification has finished
9789
+ * (`pr:verify` on success or failure, `pr:publish` on re-post), so both say
9790
+ * when it ended, and an older pass ordered there would hide a run that
9791
+ * started after the pass began: still running, or failed (#1490). A pass's
9792
+ * `started_at` only caps its proof's start. A failure's `started_at` is when
9793
+ * it was posted, not when it started, and that is deliberate: a veto's
9794
+ * instant only has to be no earlier than its start.
9795
+ * - Only a pass is ordered by its proof because the hosted gate cannot run the
9796
+ * proof schema this reader runs (#1506). Ordered by its proof's claimed
9797
+ * start there, a failure carrying a malformed proof ranked under an older
9798
+ * pass, and the gate reused the pass where this reader refused. For an
9799
+ * honest proof, `started_at` is no earlier than `endedAt`, which is no
9800
+ * earlier than the start, so the cap changes nothing for a valid pass, and
9801
+ * it also caps a schema-valid proof from a skewed clock.
9802
+ * - A pass is reused only when its verification started strictly after every
9803
+ * other run at this head was posted (#1506). Its start is its instant
9804
+ * (`orderMs`): its proof's start, capped as above, or its `started_at` when
9805
+ * it is ordered there. Every other run's post is its `started_at`, rounded
9806
+ * down to the whole second. Only the pass's own generation is left out: its
9807
+ * re-post group, or, for a pass with no typed proof, that run alone. At most
9808
+ * one generation can satisfy this, and that one is the newest, so this
9809
+ * reader selects the newest generation and, when it is a pass
9810
+ * (`isPassingRun`), trusts nothing unless every other run was posted before
9811
+ * its instant (`postedSinceItStarted`). When the newest generation is not a
9812
+ * pass, the caller refuses reuse anyway, so it is returned as before.
9813
+ * - The check exists because a run may be ordered earlier than its post by
9814
+ * its proof. This reader validates proofs, so for it the check only refuses
9815
+ * a valid pass that started before another pass was posted. The hosted gate
9816
+ * cannot validate them: without the check, a successful run whose malformed
9817
+ * proof claimed an early start ranked under an older pass there, whether
9818
+ * that pass was ordered by its proof or at its `started_at`, and the gate
9819
+ * reused the older pass where this reader, which drops that payload and
9820
+ * ranks the run at its `started_at`, refused. Both readers apply the check
9821
+ * so they agree. With it, whether a pass may be reused never consults
9822
+ * another run's proof fields, only when each other run was posted, which
9823
+ * GitHub records. In the hosted gate those fields can only cause a refusal;
9824
+ * they never hide a veto. The pass's own clock is capped at its post, so it
9825
+ * can only count against that pass.
9826
+ * - Two changes on valid input follow, and both only add refusals, each
9827
+ * costing one fresh verification in a rare case. A failure carrying a proof
9828
+ * is ordered when it was posted instead of when it started, so an older
9829
+ * failure that finished after a newer pass began now refuses reuse. Two pass
9830
+ * generations whose verifications overlapped now refuse, as does an older
9831
+ * pass re-posted after a newer pass started. Passes that ran one after the
9832
+ * other, and a pass with its own re-posts, are reused as before.
9833
+ * - The hosted gate's trust boundary is a class. The gate does not
9834
+ * reimplement this reader's payload parser. On payloads the factory's
9835
+ * writer emits, both readers select the same pass or both refuse: the
9836
+ * differential suite, `pr-verify-reuse-differential.test.ts`, asserts it
9837
+ * over 9,653 generated run sets. On App-signed text the writer never emits,
9838
+ * the two can differ in either direction. The known instances where the
9839
+ * hosted gate reuses and this reader refuses are: (a) a reused run whose
9840
+ * typed proof fails `validatePrVerifyProof`; (b) a newest pass that repeats
9841
+ * another run's verdict only once this reader normalizes that run's
9842
+ * payload, say by dropping a key; (c) a payload in `output.summary` beside
9843
+ * a text payload this reader cannot read, which it reads from the summary
9844
+ * and the hosted gate from the text; (d) text the gate's strip-and-`fromjson`
9845
+ * reads and this reader's fence match or `JSON.parse` refuses, such as a
9846
+ * closing fence with no opening one, a doubled opening fence, a fence line
9847
+ * inside the JSON, a payload split across two fenced blocks, or a number
9848
+ * literal such as `NaN` where the gate's jq accepts it (jq 1.8 does).
9849
+ * Mirroring any of it would turn the gate's jq into a parser. Only the
9850
+ * factory App can post the run, and that identity bounds the whole class
9851
+ * (#1511).
9852
+ * - A run not ordered by a typed proof carries no generation identity, so it
9853
+ * is never grouped. Two separate executions with the same command names and
9854
+ * verdict post the same compact payload, and so does a re-post. When the
9855
+ * newest generation is one of these and repeats another run's verdict
9856
+ * (`sameVerdict`), it could be a re-post of an older verdict or a new one,
9857
+ * and nothing is trusted. Proofs that size are rare, so refusing costs one
9858
+ * fresh verification.
9859
+ * - Two different generations at the newest instant refuse, by the shared
9860
+ * sequencing owner's rule.
9861
+ *
9862
+ * Every instant is rounded down to its whole second (`wholeSecondMs`), as the
9863
+ * hosted gate's jq must: it compares epoch seconds. Rounding down is monotonic,
9864
+ * so it can merge two instants into a tie, which refuses, but never invert
9865
+ * them. It also fails closed: a proof's start never moves later, so it never
9866
+ * outranks an unbound run in the same second.
9867
+ *
9868
+ * Factory-ci's hosted proof-reuse gate applies the same rule in jq.
9869
+ */
9870
+ const newestVerifyProofGeneration = (runs, bound) => {
9871
+ const { generations, posts } = verifyProofGenerations(runs, bound);
9872
+ const selected = selectNewestGeneration(generations);
9873
+ if (selected.kind !== "newest") return;
9874
+ const newest = selected.generation;
9875
+ const repeatsAnotherRun = !newest.recorded && newest.payload !== void 0 && generations.some((generation) => generation !== newest && sameVerdict(generation, newest));
9876
+ const postedSinceItStarted = isPassingRun(newest.run, newest.payload) && posts.some((post) => post.generation !== newest && post.postedMs >= newest.orderMs);
9877
+ return repeatsAnotherRun || postedSinceItStarted ? void 0 : newest;
9878
+ };
9635
9879
  /**
9636
9880
  * Read the machine-readable `pr:verify` binding for one commit (#247).
9637
9881
  *
@@ -9659,20 +9903,18 @@ function fetchPrVerifyProofBinding({ expectedAppId, githubApp, owner, repo, sha,
9659
9903
  }
9660
9904
  const runs = completeCheckRunPages(pages);
9661
9905
  if (!runs) return;
9662
- const ordered = runs.filter((run) => run?.name === checkName && checkRunProducedByFactoryApp({
9906
+ const owned = runs.filter((run) => run?.name === checkName && checkRunProducedByFactoryApp({
9663
9907
  app: run.app,
9664
9908
  expectedAppId: resolvedAppId
9665
- })).map((run) => ({
9666
- orderMs: generationOrderMs(run.started_at),
9667
- run
9668
9909
  }));
9669
- if (ordered.some(({ orderMs }) => !Number.isFinite(orderMs))) return;
9670
- const selected = selectNewestGeneration(ordered);
9671
- if (selected.kind !== "newest") return;
9672
- const { run } = selected.generation;
9673
- if (run.status !== "completed" || run.head_sha !== sha) return;
9674
- const payload = parsePrVerifyCheckPayload(run.output?.text) ?? parsePrVerifyCheckPayload(run.output?.summary);
9675
- if (!payload || payload.headSha !== sha || payload.repository !== `${owner}/${repo}`) return;
9910
+ if (owned.some((run) => !Number.isFinite(generationOrderMs(run.started_at)))) return;
9911
+ const newest = newestVerifyProofGeneration(owned, {
9912
+ repository: `${owner}/${repo}`,
9913
+ sha
9914
+ });
9915
+ if (newest === void 0) return;
9916
+ const { payload, run } = newest;
9917
+ if (run.status !== "completed" || !payload) return;
9676
9918
  const producer = checkRunProducedByFactoryApp({
9677
9919
  app: run.app,
9678
9920
  expectedAppId: resolvedAppId
@@ -10947,8 +11189,21 @@ var DemandWaiveRefusalError = class extends Error {
10947
11189
  this.refusals = refusals;
10948
11190
  }
10949
11191
  };
10950
- /** Refusals about the request itself: a malformed key, a missing rationale. */
10951
- const requestRefusals = ({ demand, parsedDemand, rationale }) => [...parsedDemand.success ? [] : [`demand:waive refuses demand key "${demand}": a demand key is a lowercase family, optionally qualified by its resolved value (e.g. review-rung:human).`], ...rationale.length === 0 ? ["demand:waive requires a non-empty --rationale."] : []];
11192
+ /**
11193
+ * Refusals about the named key: malformed, or well-formed but outside the one
11194
+ * eligibility contract the readers fold by (#1492). An ineligible key refuses
11195
+ * here, before anything is written, so the store never holds an apparent
11196
+ * operator authorization that no command applies.
11197
+ */
11198
+ const demandKeyRefusals = ({ demand, parsedDemand }) => {
11199
+ if (!parsedDemand.success) return [`demand:waive refuses demand key "${demand}": a demand key is a lowercase family, optionally qualified by its resolved value (e.g. review-rung:human).`];
11200
+ return isWaivableDemand(parsedDemand.data) ? [] : [ineligibleWaiverRefusal(parsedDemand.data)];
11201
+ };
11202
+ /** Refusals about the request itself: the key, a missing rationale. */
11203
+ const requestRefusals = ({ demand, parsedDemand, rationale }) => [...demandKeyRefusals({
11204
+ demand,
11205
+ parsedDemand
11206
+ }), ...rationale.length === 0 ? ["demand:waive requires a non-empty --rationale."] : []];
10952
11207
  /**
10953
11208
  * Refusals about the candidate the waiver would bind to. A waiver names one
10954
11209
  * pushed head. For ordinary demands that head must be the one verification
@@ -12264,10 +12519,14 @@ const defaultResolveRepo = (cwd) => runCapture("gh", [
12264
12519
  "--jq",
12265
12520
  ".nameWithOwner"
12266
12521
  ], cwd).stdout.trim();
12267
- const evaluateProofAgainstDemands = ({ authorsSeen, computed, declaredBy, demandedRung, manifestHash, selected }) => {
12522
+ const parseFailureReason = ({ commentAuthor, commentUrl, parseError }) => `found a boundary-review-proof block from ${commentAuthor}${commentUrl === void 0 ? "" : ` (${commentUrl})`} that failed to parse: ${parseError}; edit that comment so the block matches the boundary-review proof schema — an unparseable proof satisfies nothing`;
12523
+ const evaluateProofAgainstDemands = ({ boundary, comments, computed, declaredBy, demandedRung, manifestHash, selected }) => {
12268
12524
  if (!selected) {
12269
- if (authorsSeen.length === 0) return { blockingReasons: [`no boundary-review proof found on the epic issue; run the boundary review at the ${demandedRung} rung and post its fenced JSON proof comment — absence fails closed`] };
12270
- return { blockingReasons: [`boundary-review proof block(s) found on the epic issue, but none authored by declaredBy "${declaredBy}" (authors seen: ${authorsSeen.join(", ")}); run the boundary review at the ${demandedRung} rung from that account, or correct declaredBy if it names the wrong one`] };
12525
+ const authorsSeen = boundaryReviewProofAuthorsSeen(comments, boundary);
12526
+ const blockingReasons = boundaryReviewProofParseFailures(comments, boundary).map(parseFailureReason);
12527
+ if (authorsSeen.length > 0) blockingReasons.push(`boundary-review proof block(s) found on the epic issue, but none authored by declaredBy "${declaredBy}" (authors seen: ${authorsSeen.join(", ")}); run the boundary review at the ${demandedRung} rung from that account, or correct declaredBy if it names the wrong one`);
12528
+ if (blockingReasons.length === 0) blockingReasons.push(`no boundary-review proof found on the epic issue; run the boundary review at the ${demandedRung} rung and post its fenced JSON proof comment — absence fails closed`);
12529
+ return { blockingReasons };
12271
12530
  }
12272
12531
  const { proof } = selected;
12273
12532
  const blockingReasons = [];
@@ -12294,7 +12553,8 @@ function runBoundaryCheck(args, dependencies = {}) {
12294
12553
  const comments = github.fetchIssueComments(repo, args.epic);
12295
12554
  const selected = selectBoundaryReviewProof(comments, parsed.manifest.boundary, parsed.manifest.declaredBy);
12296
12555
  const { blockingReasons } = evaluateProofAgainstDemands({
12297
- authorsSeen: selected ? [] : boundaryReviewProofAuthorsSeen(comments, parsed.manifest.boundary),
12556
+ boundary: parsed.manifest.boundary,
12557
+ comments,
12298
12558
  computed,
12299
12559
  declaredBy: parsed.manifest.declaredBy,
12300
12560
  demandedRung,
@@ -14606,8 +14866,9 @@ function buildEpicCloseoutReport(input) {
14606
14866
  *
14607
14867
  * `factory:closeout` reads the lane's native at-rest provider logs by its
14608
14868
  * session join keys and records what they say as closeout metric rows. The
14609
- * rows land in the one local closeout markdown, which is the read path the
14610
- * `patronage-software-factory-lookback` skill uses.
14869
+ * rows land in the one local closeout markdown, and the closeout artifact
14870
+ * carries them to HQ as an advisory handoff. The weekly look-back no longer
14871
+ * reads token cost (#1433).
14611
14872
  *
14612
14873
  * Harvest NEVER throws: a missing or unreadable log degrades to an absent
14613
14874
  * family plus a data gap, so telemetry content can never fail the closeout.
@@ -16686,7 +16947,7 @@ const EPIC_STRUCTURE_SCHEMA_VERSION = 1;
16686
16947
  /**
16687
16948
  * DAG-node PLANNING statuses — the planning-plane vocabulary a planner authors
16688
16949
  * on a `dag.yml` node. Carried forward by ADR 0018 and defined by the epic
16689
- * skill (`reference/epic-artifacts.md`) and both `CONTEXT.md` glossaries.
16950
+ * skill (`reference/epic-artifacts.md`) and both `GLOSSARY.md` glossaries.
16690
16951
  *
16691
16952
  * A DAG node is a unit of *plan*; an HQ lane is a unit of *runtime execution*.
16692
16953
  * These are two planes with two vocabularies and must not be conflated — the
@@ -17756,6 +18017,11 @@ function armAutoMerge(input, dependencies = {}) {
17756
18017
  //#endregion
17757
18018
  //#region src/merge-freeze.ts
17758
18019
  const MERGE_FREEZE_CHECK_NAME = "patronage-factory/merge-freeze";
18020
+ /**
18021
+ * The Factory App's slug, as GitHub serves it on the runs the writer posts.
18022
+ * It is not a trust predicate: an App chooses its own slug, so the reader
18023
+ * trusts the pinned numeric App id instead (ADR 0022, #1393).
18024
+ */
17759
18025
  const MERGE_FREEZE_APP_SLUG = "patronage-factory";
17760
18026
  const MERGE_FREEZE_SCHEMA_VERSION = 1;
17761
18027
  const CHECK_RUNS_PER_PAGE$1 = 100;
@@ -17806,7 +18072,10 @@ function validateMergeFreezeState(value) {
17806
18072
  return mergeFreezeStateSchema.parse(value);
17807
18073
  }
17808
18074
  const mergeFreezeCheckRunListItemSchema = z.object({
17809
- app: z.object({ slug: z.literal(MERGE_FREEZE_APP_SLUG) }),
18075
+ app: z.object({
18076
+ id: z.number(),
18077
+ slug: z.string().nullable().optional()
18078
+ }),
17810
18079
  conclusion: z.enum([
17811
18080
  "failure",
17812
18081
  "neutral",
@@ -17855,43 +18124,161 @@ const commitParentsSchema = z.object({
17855
18124
  parents: z.array(z.object({ sha: shaSchema$2 })),
17856
18125
  sha: shaSchema$2
17857
18126
  });
17858
- function selectListedMergeFreezeGeneration(listed, expectedHeadSha) {
17859
- const runs = z.object({ check_runs: z.array(z.unknown()) }).parse(listed).check_runs.map((run) => mergeFreezeCheckRunListItemSchema.parse(run));
17860
- if (runs.length === 0) return { kind: "missing" };
18127
+ /**
18128
+ * The verdict a completed generation carries. A malformed payload throws, and
18129
+ * a payload that disagrees with its own check run is unreadable.
18130
+ */
18131
+ function settleCompletedGeneration(run) {
18132
+ const completed = completedMergeFreezeCheckRunSchema.parse(run);
18133
+ const state = mergeFreezeStateSchema.parse(JSON.parse(completed.output.text));
18134
+ if (state.generationId !== completed.id || state.headSha !== completed.head_sha || state.outcome === "active" && completed.conclusion !== "failure" || state.outcome === "stale" && completed.conclusion !== "neutral" || !state.active && completed.conclusion !== "success") return {
18135
+ kind: "unreadable",
18136
+ reason: `Newest ${MERGE_FREEZE_CHECK_NAME} check run has inconsistent state.`
18137
+ };
18138
+ return {
18139
+ kind: "settled",
18140
+ state
18141
+ };
18142
+ }
18143
+ /**
18144
+ * Whether a generation's recorded verification was cancelled and nothing in it
18145
+ * failed (#1158): at least one `cancelled` job and no `failure`. It reads only
18146
+ * the recorded job facts and never infers why a run was cancelled. A legacy
18147
+ * schema-1 record without `verification` is never cancellation-only, because
18148
+ * the reader cannot tell why it froze.
18149
+ */
18150
+ function isCancellationOnly(state) {
18151
+ const results = Object.values(state.verification?.jobs ?? {});
18152
+ return results.includes("cancelled") && !results.includes("failure");
18153
+ }
18154
+ /**
18155
+ * The verdict behind a newest generation whose verification was only
18156
+ * cancelled (#1158).
18157
+ *
18158
+ * The freeze reads the newest completed generation at the exact tip whose
18159
+ * verification was not cancellation-only. A cancellation-only generation means
18160
+ * verification did not finish, so it says nothing about the code. A duplicate
18161
+ * push or a concurrency group can cancel a later Verify run at a tip an earlier
18162
+ * run already judged, and that later run must not override the earlier verdict.
18163
+ *
18164
+ * So the reader steps back, newest first by the same `started_at` rule, over
18165
+ * cancellation-only generations to the first older one that decides. A failed
18166
+ * or mixed failed-and-cancelled generation decides, and so does a legacy
18167
+ * record without job facts. If no older generation decides, `newest` stands,
18168
+ * which keeps the freeze active and fail-closed. That covers three cases:
18169
+ * every completed generation at the tip is cancellation-only; the next older
18170
+ * generation is still running, since an older green never outranks a newer
18171
+ * running one; and the next older generation is for another tip, is malformed,
18172
+ * or shares its `started_at` with another.
18173
+ *
18174
+ * `steppedPast` carries the cancellation-only runs walked so far, newest first,
18175
+ * starting with the newest run itself. The selection reports them only when an
18176
+ * older verdict decides; when `newest` stands, it read past nothing.
18177
+ */
18178
+ function verdictBehindCancellation(newest, olderRuns, expectedHeadSha, steppedPast) {
18179
+ const standing = {
18180
+ ...newest,
18181
+ readPast: []
18182
+ };
18183
+ const selected = selectNewestGeneration(olderRuns.map((run) => ({
18184
+ orderMs: generationOrderMs(run.started_at),
18185
+ run
18186
+ })));
18187
+ if (selected.kind !== "newest") return standing;
18188
+ const { run } = selected.generation;
18189
+ if (run.head_sha !== expectedHeadSha || run.status !== "completed") return standing;
18190
+ let older;
18191
+ try {
18192
+ older = settleCompletedGeneration(run);
18193
+ } catch {
18194
+ return standing;
18195
+ }
18196
+ if (older.kind !== "settled") return standing;
18197
+ if (!isCancellationOnly(older.state)) return {
18198
+ generation: older,
18199
+ readPast: steppedPast,
18200
+ run
18201
+ };
18202
+ return verdictBehindCancellation(newest, olderRuns.filter((candidate) => candidate !== run), expectedHeadSha, [...steppedPast, run]);
18203
+ }
18204
+ /**
18205
+ * The generation the freeze selects among the runs listed at one tip: the one
18206
+ * rule behind `readGeneration`, `read`, and the PR status HUD's freeze row
18207
+ * (#1501). It is pure. Its caller has already decided which runs to trust; the
18208
+ * reader refuses the whole read on a run the pinned App did not write.
18209
+ *
18210
+ * GitHub documents check-run IDs only as unique, not ordered. The factory
18211
+ * deliberately relies on GitHub-stamped `started_at`, and equal newest instants
18212
+ * stay ambiguous and fail closed. `filter=all` in the API reader is required
18213
+ * because `latest` can hide a live generation (#522, #524).
18214
+ *
18215
+ * A malformed newest payload throws, as `settleCompletedGeneration` does.
18216
+ */
18217
+ function selectMergeFreezeRun(runs, expectedHeadSha) {
18218
+ if (runs.length === 0) return {
18219
+ generation: { kind: "missing" },
18220
+ readPast: []
18221
+ };
17861
18222
  const selected = selectNewestGeneration(runs.map((run) => ({
17862
18223
  orderMs: generationOrderMs(run.started_at),
17863
18224
  run
17864
18225
  })));
17865
18226
  if (selected.kind === "ambiguous") return {
17866
- kind: "unreadable",
17867
- reason: `Newest ${MERGE_FREEZE_CHECK_NAME} generation is ambiguous: ${selected.tied.length} check runs share ${new Date(selected.orderMs).toISOString()}.`
18227
+ generation: {
18228
+ kind: "unreadable",
18229
+ reason: `Newest ${MERGE_FREEZE_CHECK_NAME} generation is ambiguous: ${selected.tied.length} check runs share ${new Date(selected.orderMs).toISOString()}.`
18230
+ },
18231
+ readPast: []
18232
+ };
18233
+ if (selected.kind === "none") return {
18234
+ generation: { kind: "missing" },
18235
+ readPast: []
17868
18236
  };
17869
- if (selected.kind === "none") return { kind: "missing" };
17870
18237
  const newest = selected.generation.run;
17871
18238
  if (newest.head_sha !== expectedHeadSha) return {
17872
- kind: "unreadable",
17873
- reason: `Newest ${MERGE_FREEZE_CHECK_NAME} check run targets ${newest.head_sha}, expected ${expectedHeadSha}.`
18239
+ generation: {
18240
+ kind: "unreadable",
18241
+ reason: `Newest ${MERGE_FREEZE_CHECK_NAME} check run targets ${newest.head_sha}, expected ${expectedHeadSha}.`
18242
+ },
18243
+ readPast: []
17874
18244
  };
17875
18245
  if (newest.status !== "completed") return {
17876
- kind: "settling",
17877
- phase: "running-generation",
17878
- reason: `Authoritative merge freeze generation ${newest.id} is in progress.`,
17879
- witness: {
17880
- headSha: newest.head_sha,
17881
- runId: newest.id
17882
- }
18246
+ generation: {
18247
+ kind: "settling",
18248
+ phase: "running-generation",
18249
+ reason: `Authoritative merge freeze generation ${newest.id} is in progress.`,
18250
+ witness: {
18251
+ headSha: newest.head_sha,
18252
+ runId: newest.id
18253
+ }
18254
+ },
18255
+ readPast: []
17883
18256
  };
17884
- const completed = completedMergeFreezeCheckRunSchema.parse(newest);
17885
- const state = mergeFreezeStateSchema.parse(JSON.parse(completed.output.text));
17886
- if (state.generationId !== completed.id || state.headSha !== completed.head_sha || state.outcome === "active" && completed.conclusion !== "failure" || state.outcome === "stale" && completed.conclusion !== "neutral" || !state.active && completed.conclusion !== "success") return {
17887
- kind: "unreadable",
17888
- reason: `Newest ${MERGE_FREEZE_CHECK_NAME} check run has inconsistent state.`
18257
+ const verdict = settleCompletedGeneration(newest);
18258
+ if (verdict.kind !== "settled") return {
18259
+ generation: verdict,
18260
+ readPast: []
17889
18261
  };
18262
+ if (isCancellationOnly(verdict.state)) return verdictBehindCancellation({
18263
+ generation: verdict,
18264
+ run: newest
18265
+ }, runs.filter((run) => run !== newest), expectedHeadSha, [newest]);
17890
18266
  return {
17891
- kind: "settled",
17892
- state
18267
+ generation: verdict,
18268
+ readPast: [],
18269
+ run: newest
17893
18270
  };
17894
18271
  }
18272
+ function selectListedMergeFreezeGeneration(listed, expectedHeadSha) {
18273
+ return selectMergeFreezeRun(z.object({ check_runs: z.array(z.unknown()) }).parse(listed).check_runs.map((run) => {
18274
+ const listedRun = mergeFreezeCheckRunListItemSchema.parse(run);
18275
+ if (!checkRunProducedByFactoryApp({
18276
+ app: listedRun.app,
18277
+ expectedAppId: FACTORY_PROOF_GATE_APP_ID
18278
+ })) throw new Error(`${MERGE_FREEZE_CHECK_NAME} check run ${listedRun.id} was not written by the pinned Factory App ${FACTORY_PROOF_GATE_APP_ID}.`);
18279
+ return listedRun;
18280
+ }), expectedHeadSha).generation;
18281
+ }
17895
18282
  function unconfiguredMergeFreeze(input, detail) {
17896
18283
  return {
17897
18284
  kind: "unreadable",
@@ -18763,26 +19150,9 @@ const reviewRoundLimitBlockers = (input) => {
18763
19150
  }];
18764
19151
  };
18765
19152
  /**
18766
- * The demands an operator waiver can reach (#354, moved here from
18767
- * `pr:merge-check` by #477). Exactly the set the merge-time site could waive:
18768
- * the merge freeze, the profile's external required checks, and the boundary
18769
- * wave's review rung — the demands the resolver puts *in force* for one
18770
- * candidate — plus the review-round limit (#1280), which exists only to be
18771
- * waived.
18772
- *
18773
- * Everything else stays unwaivable, and relocating the consumption site does
18774
- * not widen the operator's override. Structural refusals — candidate identity,
18775
- * proof binding, an unreadable demand authority, wave membership — were never
18776
- * waivable at merge time and are not waivable here; in `pr:ready` most of them
18777
- * refuse by throwing before any blocker list exists. The evidence gates
18778
- * readiness has always owned (verify, review modes, draft, human blockers) are
18779
- * satisfied or not; a waiver is not a second way to pass them.
18780
- */
18781
- const WAIVABLE_QUALIFIED_DEMAND_FAMILIES = ["required-check", "review-rung"];
18782
- const isWaivableDemand = (demand) => demand === DEMAND_KEYS.mergeFreeze || demand === DEMAND_KEYS.reviewRoundLimit || WAIVABLE_QUALIFIED_DEMAND_FAMILIES.some((family) => demand.startsWith(`${family}:`));
18783
- /**
18784
19153
  * Fold the operator's waivers through the blockers, one call per demand so
18785
- * `demand-waiver.ts` stays the single authority on what a waiver means. A
19154
+ * `demand-waiver.ts` stays the single authority on what a waiver means and
19155
+ * which demands it can reach — the contract `demand:waive` refuses by. A
18786
19156
  * waived demand keeps every refusal it made, verbatim, on the `WaivedDemand`
18787
19157
  * record — it is never rewritten as satisfied.
18788
19158
  */
@@ -19221,6 +19591,8 @@ const renderTable = (rows) => [
19221
19591
  "| --- | --- | --- | --- |",
19222
19592
  ...rows.map((row) => `| ${markdownCell(row.target)} | ${statusLabel(row.status)} | ${row.link} | ${row.timestamp} |`)
19223
19593
  ].join("\n");
19594
+ /** Name the cancellation-only runs a merge-freeze row read past (#1501). */
19595
+ const withReadPastNote = (target, runIds) => runIds && runIds.length > 0 ? `${target} (read past cancellation-only ${runIds.length === 1 ? "run" : "runs"} ${runIds.join(", ")})` : target;
19224
19596
  const checkRow = (target, snapshot, renderedAt) => {
19225
19597
  if (!snapshot) return {
19226
19598
  detail: "No completed Factory App snapshot is available at this head.",
@@ -19233,7 +19605,7 @@ const checkRow = (target, snapshot, renderedAt) => {
19233
19605
  detail: `Completed snapshot: ${snapshot.conclusion}.`,
19234
19606
  link: safeMarkdownLink("Details", snapshot.detailsUrl),
19235
19607
  status: snapshot.conclusion === "success" ? "Ready" : "Failed",
19236
- target,
19608
+ target: withReadPastNote(target, snapshot.readPastRunIds),
19237
19609
  timestamp: utcTimestamp(snapshot.completedAt)
19238
19610
  };
19239
19611
  };
@@ -19345,7 +19717,9 @@ const checkRunSchema = z.object({
19345
19717
  conclusion: z.string().nullable().optional(),
19346
19718
  details_url: z.string().nullable().optional(),
19347
19719
  head_sha: z.string().regex(/^[0-9a-f]{40}$/u),
19720
+ id: z.number().int().positive(),
19348
19721
  name: z.string(),
19722
+ output: z.object({ text: z.string().nullable().optional() }).nullable().optional(),
19349
19723
  started_at: z.iso.datetime().nullable(),
19350
19724
  status: z.enum([
19351
19725
  "completed",
@@ -19416,16 +19790,13 @@ const listFactoryCheckRuns = async (input, name, sha, dependencies) => {
19416
19790
  }
19417
19791
  throw new Error(`${name} pagination limit was exhausted before GitHub returned a final page.`);
19418
19792
  };
19419
- const completedSnapshot = (name, sha, runs) => {
19793
+ /** The Factory App's generations of one check at one SHA, each orderable. */
19794
+ const factoryGenerations = (name, sha, runs) => {
19420
19795
  const candidates = runs.filter((run) => String(run.app?.id ?? "") === FACTORY_PROOF_GATE_APP_ID && run.head_sha === sha && run.name === name);
19421
19796
  if (candidates.some((run) => !Number.isFinite(generationOrderMs(run.started_at)))) throw new Error(`${name} presentation snapshot has a generation with no usable started_at.`);
19422
- const selected = selectNewestGeneration(candidates.map((run) => ({
19423
- orderMs: generationOrderMs(run.started_at),
19424
- run
19425
- })));
19426
- if (selected.kind === "ambiguous") throw new Error(`Newest ${name} presentation snapshot is ambiguous: ${selected.tied.length} check runs share the newest timestamp.`);
19427
- if (selected.kind === "none") return;
19428
- const { run } = selected.generation;
19797
+ return candidates;
19798
+ };
19799
+ const runSnapshot = (name, run) => {
19429
19800
  if (run.status !== "completed" || !run.completed_at || !run.conclusion) return;
19430
19801
  return {
19431
19802
  completedAt: run.completed_at,
@@ -19435,6 +19806,45 @@ const completedSnapshot = (name, sha, runs) => {
19435
19806
  name
19436
19807
  };
19437
19808
  };
19809
+ const newestSnapshot = (name, generations) => {
19810
+ const selected = selectNewestGeneration(generations.map((run) => ({
19811
+ orderMs: generationOrderMs(run.started_at),
19812
+ run
19813
+ })));
19814
+ if (selected.kind === "ambiguous") throw new Error(`Newest ${name} presentation snapshot is ambiguous: ${selected.tied.length} check runs share the newest timestamp.`);
19815
+ if (selected.kind === "none") return;
19816
+ return runSnapshot(name, selected.generation.run);
19817
+ };
19818
+ const completedSnapshot = (name, sha, runs) => newestSnapshot(name, factoryGenerations(name, sha, runs));
19819
+ const MERGE_FREEZE_CHECK = "patronage-factory/merge-freeze";
19820
+ /**
19821
+ * The merge-freeze row shows the generation readiness reads (#1501), chosen by
19822
+ * the freeze's own selection rather than a second copy of it. That selection
19823
+ * reads past a cancellation-only generation to an older verdict at the same
19824
+ * tip (#1158). The newest completed run alone would show that cancellation as
19825
+ * a freeze readiness does not apply, so the row names the runs it read past.
19826
+ *
19827
+ * Otherwise the row is the newest completed snapshot, as for every other check:
19828
+ * running, missing and ambiguous generations present as before, and so does a
19829
+ * newest generation the selection cannot settle. Every case where the freeze
19830
+ * stays fail-closed keeps the newest generation, so the row stays red. The HUD
19831
+ * is advisory and never gates.
19832
+ */
19833
+ const mergeFreezeSnapshot = (sha, runs) => {
19834
+ const generations = factoryGenerations(MERGE_FREEZE_CHECK, sha, runs);
19835
+ const newest = newestSnapshot(MERGE_FREEZE_CHECK, generations);
19836
+ if (!newest) return newest;
19837
+ try {
19838
+ const { readPast, run } = selectMergeFreezeRun(generations, sha);
19839
+ const selected = run && readPast.length > 0 ? runSnapshot(MERGE_FREEZE_CHECK, run) : void 0;
19840
+ return selected ? {
19841
+ ...selected,
19842
+ readPastRunIds: readPast.map((past) => past.id)
19843
+ } : newest;
19844
+ } catch {
19845
+ return newest;
19846
+ }
19847
+ };
19438
19848
  const FAILED_PREVIEW_CONCLUSIONS = new Set([
19439
19849
  "action_required",
19440
19850
  "cancelled",
@@ -19483,7 +19893,8 @@ async function readFactoryPrStatusSources(input, dependencies = {}) {
19483
19893
  ];
19484
19894
  const listedChecks = await Promise.all(checkTargets.map(([name, sha]) => listFactoryCheckRuns(input, name, sha, dependencies)));
19485
19895
  const checks = Object.fromEntries(checkTargets.flatMap(([name, sha], index) => {
19486
- const snapshot = completedSnapshot(name, sha, listedChecks[index] ?? []);
19896
+ const runs = listedChecks[index] ?? [];
19897
+ const snapshot = name === MERGE_FREEZE_CHECK ? mergeFreezeSnapshot(sha, runs) : completedSnapshot(name, sha, runs);
19487
19898
  return snapshot ? [[name, snapshot]] : [];
19488
19899
  }));
19489
19900
  const previewTargets = input.previewTargets ?? [];
@@ -21346,11 +21757,20 @@ const assertEpicPullRequestOpen = ({ args, base }) => {
21346
21757
  * prompt for another patch, with no ladder cycle for that patch, is a review
21347
21758
  * that nobody recorded, and no ladder control can see it. Assembling again at
21348
21759
  * the same patch only rewrites that prompt.
21760
+ *
21761
+ * The prompt also counts as recorded when the checkout's own review proof
21762
+ * holds a cycle at exactly the patch and head the prompt names, whatever the
21763
+ * lineage (#1415). A squash merge of the previous pull request takes that head
21764
+ * off the next branch, so the ladder this candidate continues is empty, yet
21765
+ * the review of that prompt is on record. Only this question reads
21766
+ * `checkoutLadder`; the next review still continues `priorLadder`.
21349
21767
  */
21350
- const assertPriorPromptRecorded = ({ existing, patchId, priorLadder }) => {
21768
+ const assertPriorPromptRecorded = ({ checkoutLadder, existing, patchId, priorLadder }) => {
21351
21769
  if (existing.status !== "ok" || existing.artifact.patchId === patchId) return;
21352
21770
  const prompted = existing.artifact;
21353
- if ((priorLadder?.cycles ?? []).some((cycle) => cycle.identity?.patchId === prompted.patchId)) return;
21771
+ const recordedOnContinuedLadder = (priorLadder?.cycles ?? []).some((cycle) => cycle.identity?.patchId === prompted.patchId);
21772
+ const recordedAtPromptedHead = (checkoutLadder?.cycles ?? []).some((cycle) => cycle.identity?.patchId === prompted.patchId && cycle.identity.headSha === prompted.headSha);
21773
+ if (recordedOnContinuedLadder || recordedAtPromptedHead) return;
21354
21774
  throw new Error(`pr:review --assemble refuses: ${DEFAULT_ASSEMBLED_REVIEW_PROMPT_PATH} holds a prompt for patch ${prompted.patchId} at ${prompted.headSha}, and the review ladder has no cycle for that patch. The candidate is now patch ${patchId}. Record the review of that patch first: patronage-factory pr:review --historical --findings <findings file for patch ${prompted.patchId}>. If no reviewer read that prompt, delete ${DEFAULT_ASSEMBLED_REVIEW_PROMPT_PATH} and assemble again.`);
21355
21775
  };
21356
21776
  /**
@@ -21397,14 +21817,16 @@ const assertAssembleMayPrepareReview = ({ args, base, cwd, git, headSha, output,
21397
21817
  args,
21398
21818
  base
21399
21819
  });
21820
+ const prior = readOptionalProofFor(PR_REVIEW_PROOF_DESCRIPTOR, cwd, output);
21400
21821
  const priorLadder = priorLadderStateFor({
21401
21822
  base: base.ref,
21402
21823
  headSha,
21403
21824
  isAncestor: (ancestor, descendant) => git.isAncestor(cwd, ancestor, descendant),
21404
- prior: readOptionalProofFor(PR_REVIEW_PROOF_DESCRIPTOR, cwd, output)
21825
+ prior
21405
21826
  });
21406
21827
  const existing = readAssembledReviewPrompt(path.resolve(cwd, DEFAULT_ASSEMBLED_REVIEW_PROMPT_PATH));
21407
21828
  assertPriorPromptRecorded({
21829
+ checkoutLadder: prior?.ladder,
21408
21830
  existing,
21409
21831
  patchId,
21410
21832
  priorLadder
@@ -22143,6 +22565,35 @@ const admittedPullRequestBodyEdit = (admission) => async ({ body, owner, repo, p
22143
22565
  repo
22144
22566
  }), process.cwd(), extraEnv === void 0 ? {} : { extraEnv });
22145
22567
  };
22568
+ /**
22569
+ * `gh pr create` for the draft (#1366). With a base, the title and body come
22570
+ * from the branch's commits, built as the Cursor VM's REST create builds them
22571
+ * (#1327). gh's own `--fill` keeps only the commit subjects once a branch has
22572
+ * two commits, which dropped every `Closes #N` line in a commit body, and
22573
+ * membership derives from those lines. With no base there is no range to
22574
+ * read, so gh fills from the repository default branch, every commit body
22575
+ * included (`--fill-verbose`).
22576
+ */
22577
+ const ghArgsForPullRequestCreate = ({ base, commits, cwd }) => {
22578
+ if (base === void 0) return [
22579
+ "pr",
22580
+ "create",
22581
+ "--draft",
22582
+ "--fill-verbose"
22583
+ ];
22584
+ const { body, title } = pullRequestFill(commits(cwd, base));
22585
+ return [
22586
+ "pr",
22587
+ "create",
22588
+ "--draft",
22589
+ "--title",
22590
+ title,
22591
+ "--body",
22592
+ body,
22593
+ "--base",
22594
+ base
22595
+ ];
22596
+ };
22146
22597
  /** The REST create a Cursor VM uses in place of `gh pr create` (#1327). */
22147
22598
  const ghApiArgsForPullRequestCreate = ({ base, body, head, owner, repo, title }) => [
22148
22599
  "api",
@@ -22169,13 +22620,18 @@ const ghApiArgsForPullRequestCreate = ({ base, body, head, owner, repo, title })
22169
22620
  * reads both branch refs, which is why the admission scope carries
22170
22621
  * `contents: read` (#1375). The title and body come from the branch's
22171
22622
  * commits, and the head is the already-pushed branch, so nothing is pushed.
22172
- * Off a Cursor VM the call is `gh pr create`, unchanged.
22623
+ * Off a Cursor VM the call is `gh pr create`, with the same title and body
22624
+ * (#1366).
22173
22625
  */
22174
- const admittedPullRequestCreate = (admission, repository) => async ({ args, base, branch, cwd }) => {
22626
+ const admittedPullRequestCreate = (admission, repository, commits) => async ({ base, branch, cwd }) => {
22175
22627
  const extraEnv = await admission.ghEnv("create-pull-request", repository);
22176
- if (extraEnv === void 0) return runCapture("gh", args, cwd).stdout;
22628
+ if (extraEnv === void 0) return runCapture("gh", ghArgsForPullRequestCreate({
22629
+ base,
22630
+ commits,
22631
+ cwd
22632
+ }), cwd).stdout;
22177
22633
  if (base === void 0) throw new Error("pr:publish on a Cursor VM needs a base branch to create the pull request against, and none was passed or recorded. Run pr:verify, which records its base, or pass --base.");
22178
- const { body, title } = pullRequestFill(commitsSinceBase(cwd, base));
22634
+ const { body, title } = pullRequestFill(commits(cwd, base));
22179
22635
  return runCapture("gh", ghApiArgsForPullRequestCreate({
22180
22636
  base,
22181
22637
  body,
@@ -22194,9 +22650,14 @@ const admittedPullRequestCreate = (admission, repository) => async ({ args, base
22194
22650
  */
22195
22651
  const publishGithubAdapters = (dependencies, repository) => {
22196
22652
  const admission = dependencies.admission ?? processAdmissionCredential;
22653
+ const commits = dependencies.pullRequestCommits ?? commitsSinceBase;
22197
22654
  const injectedGh = dependencies.gh;
22198
22655
  return {
22199
- create: injectedGh ? ({ args, cwd }) => injectedGh(args, cwd) : admittedPullRequestCreate(admission, repository),
22656
+ create: injectedGh ? ({ base, cwd }) => injectedGh(ghArgsForPullRequestCreate({
22657
+ base,
22658
+ commits,
22659
+ cwd
22660
+ }), cwd) : admittedPullRequestCreate(admission, repository, commits),
22200
22661
  github: dependencies.github ?? {
22201
22662
  ...defaultPrReadyGithub(),
22202
22663
  editPullRequestBody: admittedPullRequestBodyEdit(admission)
@@ -22289,15 +22750,7 @@ async function discoverOrCreatePullRequest({ args, branch, create, cwd, gh, revi
22289
22750
  reviewProofPath,
22290
22751
  verifyProofPath
22291
22752
  });
22292
- const createArgs = [
22293
- "pr",
22294
- "create",
22295
- "--draft",
22296
- "--fill"
22297
- ];
22298
- if (creationBase !== void 0) createArgs.push("--base", creationBase);
22299
22753
  await create({
22300
- args: createArgs,
22301
22754
  base: creationBase,
22302
22755
  branch,
22303
22756
  cwd
@@ -22469,6 +22922,18 @@ async function ensureVerifyProof({ args, cwd, dependencies, headSha, pr, verifyP
22469
22922
  };
22470
22923
  }
22471
22924
  const reviewNotRequired = (proof) => proof.reviewRequirement?.status === "not-required";
22925
+ /**
22926
+ * Let the `pr-review-proof` event a cycle recorded from `--findings` reach HQ
22927
+ * or the spool before publish goes on (#1382). The sink's setup budget starts
22928
+ * when `pr:review` enqueues the event, and its first step waits for the event
22929
+ * loop. Standalone `pr:review` exits next, so the loop is free. Publish goes
22930
+ * straight on to synchronous `gh` calls, which held the loop past that budget,
22931
+ * so the sink gave up before spooling anything. Bounded by the sink's own
22932
+ * budgets.
22933
+ */
22934
+ const settleRecordedReviewEvent = async (dependencies) => {
22935
+ await (dependencies.awaitPendingIngest ?? awaitPendingHqIngest)();
22936
+ };
22472
22937
  async function ensureReviewProof({ args, cwd, dependencies, currentReviewIdentity, pr, reviewProofPath }) {
22473
22938
  const proof = readOptionalReviewProof(cwd, reviewProofPath);
22474
22939
  const ladderPolicy = resolveReviewLadderPolicy();
@@ -22497,9 +22962,11 @@ async function ensureReviewProof({ args, cwd, dependencies, currentReviewIdentit
22497
22962
  verifyProof: args.verifyProof
22498
22963
  }, publisher.dependencies);
22499
22964
  } catch (error) {
22965
+ await settleRecordedReviewEvent(dependencies);
22500
22966
  await publisher.post()?.settled;
22501
22967
  throw error;
22502
22968
  }
22969
+ await settleRecordedReviewEvent(dependencies);
22503
22970
  const producerPost = publisher.post();
22504
22971
  return {
22505
22972
  ...producerPost === void 0 ? {} : { producerPost },
package/dist/schemas.d.ts CHANGED
@@ -66,7 +66,7 @@ type EvidenceReviewRung = (typeof EVIDENCE_REVIEW_RUNGS)[number];
66
66
  //#region src/review-ladder-ledger.d.ts
67
67
  type LadderCycleStage = "interior" | "gate";
68
68
  type LadderFindingReport = ReviewFindingAttributes;
69
- /** One finding as recorded in one cycle (CONTEXT.md: finding occurrence). */
69
+ /** One finding as recorded in one cycle (GLOSSARY.md: finding occurrence). */
70
70
  interface ReviewOccurrenceRecord {
71
71
  address: string;
72
72
  /** The ladder-assigned cycle number the occurrence was recorded in. */
@@ -82,7 +82,7 @@ interface ReviewOccurrenceRecord {
82
82
  declare const POLICY_DECLARATION_RULES: readonly ["cycle-2-severity-floor", "identity-repeat"];
83
83
  type PolicyDeclarationRule = (typeof POLICY_DECLARATION_RULES)[number];
84
84
  /**
85
- * A disposition declaration as stored (CONTEXT.md: disposition declaration):
85
+ * A disposition declaration as stored (GLOSSARY.md: disposition declaration):
86
86
  * one occurrence, one disposition, the authority that made it and its
87
87
  * reference (operator, reviewer) or rule (policy).
88
88
  */
package/dist/schemas.js CHANGED
@@ -252,6 +252,7 @@ const waivedDemandSchema = z.object({
252
252
  session: z.string().trim().min(1).optional(),
253
253
  unmetReasons: z.array(z.string().min(1)).min(1)
254
254
  });
255
+ DEMAND_KEYS.mergeFreeze, DEMAND_KEYS.reviewRoundLimit, DEMAND_KEYS.childIssueOpen;
255
256
  //#endregion
256
257
  //#region src/diff-classification.ts
257
258
  const DIFF_CLASSIFICATIONS = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patronage/software-factory",
3
- "version": "1.0.0-alpha.40",
3
+ "version": "1.0.0-alpha.42",
4
4
  "description": "Shared Patronage software factory CLI and project-profile validation tools",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -15,7 +15,7 @@
15
15
  "files": [
16
16
  "dist",
17
17
  "README.md",
18
- "CONTEXT.md"
18
+ "GLOSSARY.md"
19
19
  ],
20
20
  "type": "module",
21
21
  "exports": {
@@ -37,7 +37,7 @@
37
37
  "picomatch": "^4.0.5",
38
38
  "yaml": "^2.9.0",
39
39
  "zod": "4.4.3",
40
- "@patronage/factory-ci": "1.0.0-alpha.40"
40
+ "@patronage/factory-ci": "1.0.0-alpha.42"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "24.13.3",
File without changes