@patronage/software-factory 1.0.0-alpha.37 → 1.0.0-alpha.39

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/README.md CHANGED
@@ -76,6 +76,6 @@ npx skills add https://github.com/patronage/software-factory -g --all
76
76
 
77
77
  They provide procedure and link back to the documentation site; they are not a second command reference.
78
78
 
79
- The package also exports an Oxlint plugin at `@patronage/software-factory/oxlint-jev`. It asks Jev questions that a project writes about its own code, through HQ, from a dedicated Oxlint config. It records by default and sends only with `PATRONAGE_JEV_MODE=live`. `psf jev:calibrate` scores those questions against the project's labeled cases. Both are session diagnostics, never gates. See [Ask Jev about code](https://factory.patronage.com/how-to/ask-jev-about-code).
79
+ The package also exports an Oxlint plugin at `@patronage/software-factory/oxlint-jev`. It asks Jev questions that a project writes about its own code, through HQ, from a dedicated Oxlint config. It records by default and sends only with `PATRONAGE_JEV_MODE=live`. `psf jev:calibrate` scores those questions against the project's labeled cases. Both are session diagnostics, never gates. See [Ask Jev about your code](https://factory.patronage.com/tutorials/ask-jev-about-code).
80
80
 
81
81
  `assembleReviewPrompt` puts a repository-root `CODING_STANDARDS.md` into the correctness prompt when the file exists and is not blank. It reads the file at the candidate head, not from the working tree, and emits one `coding-standards` section that names the head SHA. A repository without the file gets an unchanged prompt. `pr:review --assemble` writes that factory-owned prompt under `.factory-memory/pr-review-prompt.json`. `pr:review --findings` copies the recorded sections onto the proof when the artifact's patch-id matches.
package/dist/index.d.ts CHANGED
@@ -4,6 +4,114 @@ import { z } from "zod";
4
4
  import { PreviewProofRegistration } from "@patronage/factory-ci";
5
5
  import { JsonValue, Questions, SystemOneRequest, choice, noul, score } from "@typesafe-ai/sdk";
6
6
 
7
+ //#region src/factory-app-broker-client.d.ts
8
+ /**
9
+ * The opt-in broker permission set (#1360): `pull_requests: write`,
10
+ * `issues: read`, and `contents: read` on the requesting repository. The
11
+ * read-only contents grant is there because GitHub's pull request create
12
+ * reads the head and base refs (#1327, #1375). The CLI asks for the scope by
13
+ * name and the broker decides its permissions. A request without a scope is
14
+ * the `checks: write` mint, unchanged.
15
+ */
16
+ declare const FACTORY_APP_BROKER_ADMISSION_SCOPE = "admission";
17
+ //#endregion
18
+ //#region src/user-config.d.ts
19
+ declare const githubAppConfigSchema: z.ZodObject<{
20
+ appId: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
21
+ installationId: z.ZodOptional<z.ZodNumber>;
22
+ privateKeyPath: z.ZodString;
23
+ }, z.core.$strip>;
24
+ type GithubAppConfig = z.infer<typeof githubAppConfigSchema>;
25
+ declare const hqIngestCredentialReferencesSchema: z.ZodObject<{
26
+ clientIdRef: z.ZodString;
27
+ clientSecretRef: z.ZodString;
28
+ }, z.core.$strip>;
29
+ type HqIngestCredentialReferences = z.infer<typeof hqIngestCredentialReferencesSchema>;
30
+ declare const factoryUserConfigSchema: z.ZodObject<{
31
+ githubApp: z.ZodOptional<z.ZodObject<{
32
+ appId: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
33
+ installationId: z.ZodOptional<z.ZodNumber>;
34
+ privateKeyPath: z.ZodString;
35
+ }, z.core.$strip>>;
36
+ hqAllowedOrigins: z.ZodOptional<z.ZodArray<z.ZodString>>;
37
+ hqIngestCredentials: z.ZodOptional<z.ZodObject<{
38
+ clientIdRef: z.ZodString;
39
+ clientSecretRef: z.ZodString;
40
+ }, z.core.$strip>>;
41
+ schemaVersion: z.ZodLiteral<2>;
42
+ }, z.core.$strip>;
43
+ type FactoryUserConfig = z.infer<typeof factoryUserConfigSchema>;
44
+ interface LoadUserConfigResult {
45
+ path: string;
46
+ config: FactoryUserConfig;
47
+ ignoredKeys?: string[];
48
+ }
49
+ //#endregion
50
+ //#region src/github-app-access.d.ts
51
+ interface FactoryAppAccessDependencies {
52
+ env?: NodeJS.ProcessEnv;
53
+ existsSync?: (path: string) => boolean;
54
+ exchangeBroker?: (input: {
55
+ fetch?: typeof fetch;
56
+ oidc: string;
57
+ owner: string;
58
+ repo: string;
59
+ scope?: typeof FACTORY_APP_BROKER_ADMISSION_SCOPE;
60
+ timeoutMs?: number;
61
+ url?: string;
62
+ }) => Promise<string>;
63
+ fetch?: typeof fetch;
64
+ githubApp?: GithubAppConfig;
65
+ mintCursorOidc?: (input: {
66
+ audience?: string;
67
+ signal?: AbortSignal;
68
+ socketPath?: string;
69
+ timeoutMs?: number;
70
+ }) => Promise<string>;
71
+ now?: () => number;
72
+ /**
73
+ * Cancels the Cursor OIDC mint (#1322). Fetch-based calls are cancelled
74
+ * through the `fetch` the caller passes, which joins its own signal.
75
+ */
76
+ signal?: AbortSignal;
77
+ timeoutMs?: number;
78
+ token?: string;
79
+ transportAttempts?: number;
80
+ }
81
+ //#endregion
82
+ //#region src/admission-credential.d.ts
83
+ /**
84
+ * The GitHub calls a Cursor VM's own `gh` credential cannot make (#1327).
85
+ * Cursor mints the VM token without `pull_requests: write` or `issues`, so on
86
+ * a Cursor managed runtime these, and only these, run on the broker's
87
+ * `admission` token (#1360): `pull_requests: write`, `issues: read`, and
88
+ * `contents: read`, which creating a pull request needs because GitHub reads
89
+ * the head and base refs (#1375). Push,
90
+ * marking ready, arming auto-merge, check runs, and every other call keep
91
+ * their own credential.
92
+ */
93
+ type AdmissionOperation = "create-pull-request" | "edit-pull-request-body" | "read-issue" | "read-pull-request-for-status-hud";
94
+ /** The one repository the admission token is minted for. */
95
+ interface AdmissionRepository {
96
+ owner: string;
97
+ repo: string;
98
+ }
99
+ interface AdmissionCredential {
100
+ /**
101
+ * The environment overlay for ONE `gh` subprocess that performs
102
+ * `operation`. `undefined` off a Cursor managed runtime: nothing is minted
103
+ * and the subprocess environment is untouched. On one, `{ GH_TOKEN }` with
104
+ * the admission token. Throws {@link AdmissionCredentialError} when the mint
105
+ * fails.
106
+ */
107
+ ghEnv: (operation: AdmissionOperation, repository: AdmissionRepository) => Promise<Readonly<Record<string, string>> | undefined>;
108
+ /**
109
+ * The same token for one direct REST request, or `undefined` off a Cursor
110
+ * managed runtime.
111
+ */
112
+ token: (operation: AdmissionOperation, repository: AdmissionRepository) => Promise<string | undefined>;
113
+ }
114
+ //#endregion
7
115
  //#region src/review-rungs.d.ts
8
116
  declare const EVIDENCE_REVIEW_RUNGS: readonly ["independent-model", "oracle", "human"];
9
117
  type EvidenceReviewRung = (typeof EVIDENCE_REVIEW_RUNGS)[number];
@@ -332,6 +440,32 @@ declare const authorizeDemandWaiver: ({
332
440
  session: string | undefined;
333
441
  }) => AuthorizeDemandWaiverResult;
334
442
  //#endregion
443
+ //#region src/hq-credentials.d.ts
444
+ /**
445
+ * Why a reference did not resolve. Each value names a different remedy, and
446
+ * none of them can be inferred from a value-or-nothing result:
447
+ *
448
+ * - `resolver-missing` — no secret-manager binary on PATH.
449
+ * - `resolver-blocked` — a binary exists but this session may not execute it
450
+ * (a sandbox denying exec). The command belongs outside the sandbox.
451
+ * - `resolver-timeout` — the probe expired: a desktop agent waiting on an
452
+ * approval nobody can give here.
453
+ * - `resolver-refused` — the binary ran and produced no value. Its store is
454
+ * unreachable from this session (a sandbox with no keychain access) or the
455
+ * reference is not readable. These two stay one status on purpose: telling
456
+ * them apart would mean reading resolver output.
457
+ */
458
+ type SecretResolutionFailure = "resolver-blocked" | "resolver-missing" | "resolver-refused" | "resolver-timeout";
459
+ /** A structured resolution outcome. The value travels only when resolved. */
460
+ type SecretResolution = {
461
+ status: "resolved";
462
+ value: string;
463
+ } | {
464
+ status: SecretResolutionFailure;
465
+ };
466
+ /** Resolves a secret reference. Injected so tests stay offline. */
467
+ type SecretReferenceResolver = (reference: string) => SecretResolution;
468
+ //#endregion
335
469
  //#region src/hq-ingest-sink.d.ts
336
470
  interface HqIngestDependencies {
337
471
  /**
@@ -344,6 +478,12 @@ interface HqIngestDependencies {
344
478
  fetch?: typeof fetch;
345
479
  now?: () => Date;
346
480
  randomUUID?: () => string;
481
+ /**
482
+ * Resolves one `hqIngestCredentials` reference on an operator machine
483
+ * (#1313). Injected so tests stay offline; production asks `op-fast`, then
484
+ * `op`.
485
+ */
486
+ resolveSecret?: SecretReferenceResolver;
347
487
  /**
348
488
  * Ceiling for persisting a failure after the transport leg (default
349
489
  * `HQ_JOURNAL_FLUSH_BUDGET_MS`). Injectable for the same reason as
@@ -458,6 +598,12 @@ interface HqSpoolWorkCount {
458
598
  oldestQueuedAt?: string;
459
599
  /** Spooled events waiting in the locations below. */
460
600
  pending: number;
601
+ /**
602
+ * How many pending events carry each recorded spool reason (#1317). Present
603
+ * only when the caller asked for `reasons`. An event whose file could not be
604
+ * read or parsed within budget is counted in `pending` and absent here.
605
+ */
606
+ reasons?: Record<string, number>;
461
607
  /** Locations that exist and hold spooled work. */
462
608
  spools: string[];
463
609
  /**
@@ -477,7 +623,9 @@ interface HqSpoolWorkCount {
477
623
  * same locations `flushHqSpool` drains are inspected here, read-only: no
478
624
  * directory is created, nothing is secured, and nothing is delivered.
479
625
  */
480
- declare function countHqSpoolWork(input: Pick<HqSpoolFlushInput, "explicitDirectories" | "repository">, dependencies?: {
626
+ declare function countHqSpoolWork(input: Pick<HqSpoolFlushInput, "explicitDirectories" | "repository"> & {
627
+ /** Also read each pending event's recorded reason (#1317). */reasons?: boolean;
628
+ }, dependencies?: {
481
629
  budgetMs?: number;
482
630
  env?: NodeJS.ProcessEnv;
483
631
  }): Promise<HqSpoolWorkCount>;
@@ -1250,58 +1398,6 @@ declare const followUpFromArgv: (argv: string[]) => FollowUpAction;
1250
1398
  /** Canonical zod schema for the optional follow-up field on JSON outputs. */
1251
1399
  declare const FollowUpActionSchema: z.ZodType<FollowUpAction>;
1252
1400
  //#endregion
1253
- //#region src/user-config.d.ts
1254
- declare const githubAppConfigSchema: z.ZodObject<{
1255
- appId: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
1256
- installationId: z.ZodOptional<z.ZodNumber>;
1257
- privateKeyPath: z.ZodString;
1258
- }, z.core.$strip>;
1259
- type GithubAppConfig = z.infer<typeof githubAppConfigSchema>;
1260
- declare const factoryUserConfigSchema: z.ZodObject<{
1261
- githubApp: z.ZodOptional<z.ZodObject<{
1262
- appId: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
1263
- installationId: z.ZodOptional<z.ZodNumber>;
1264
- privateKeyPath: z.ZodString;
1265
- }, z.core.$strip>>;
1266
- hqAllowedOrigins: z.ZodOptional<z.ZodArray<z.ZodString>>;
1267
- hqIngestCredentials: z.ZodOptional<z.ZodObject<{
1268
- clientIdRef: z.ZodString;
1269
- clientSecretRef: z.ZodString;
1270
- }, z.core.$strip>>;
1271
- schemaVersion: z.ZodLiteral<2>;
1272
- }, z.core.$strip>;
1273
- type FactoryUserConfig = z.infer<typeof factoryUserConfigSchema>;
1274
- interface LoadUserConfigResult {
1275
- path: string;
1276
- config: FactoryUserConfig;
1277
- ignoredKeys?: string[];
1278
- }
1279
- //#endregion
1280
- //#region src/github-app-access.d.ts
1281
- interface FactoryAppAccessDependencies {
1282
- env?: NodeJS.ProcessEnv;
1283
- existsSync?: (path: string) => boolean;
1284
- exchangeBroker?: (input: {
1285
- fetch?: typeof fetch;
1286
- oidc: string;
1287
- owner: string;
1288
- repo: string;
1289
- timeoutMs?: number;
1290
- url?: string;
1291
- }) => Promise<string>;
1292
- fetch?: typeof fetch;
1293
- githubApp?: GithubAppConfig;
1294
- mintCursorOidc?: (input: {
1295
- audience?: string;
1296
- socketPath?: string;
1297
- timeoutMs?: number;
1298
- }) => Promise<string>;
1299
- now?: () => number;
1300
- timeoutMs?: number;
1301
- token?: string;
1302
- transportAttempts?: number;
1303
- }
1304
- //#endregion
1305
1401
  //#region src/pr-verify-mode.d.ts
1306
1402
  /**
1307
1403
  * The verification mode `pr:verify` resolved for a run.
@@ -1547,6 +1643,7 @@ type PostCommitStatus = (input: PostCommitStatusInput) => void;
1547
1643
  //#region src/github-check-runs.d.ts
1548
1644
  declare const FACTORY_CHECK_NAMES: {
1549
1645
  readonly "pr-ready": "patronage-factory/pr-ready";
1646
+ readonly "pr-review": "patronage-factory/pr-review";
1550
1647
  readonly "pr-verify": "patronage-factory/pr-verify";
1551
1648
  };
1552
1649
  type FactoryCheckGate = keyof typeof FACTORY_CHECK_NAMES;
@@ -1570,11 +1667,12 @@ interface PublishFactoryCheckInput {
1570
1667
  }
1571
1668
  interface CheckRunDependencies extends FactoryAppAccessDependencies {
1572
1669
  postCommitStatus?: PostCommitStatus;
1573
- resolveDetailsUrl?: (input: PublishFactoryCheckInput) => Promise<string> | string;
1670
+ resolveDetailsUrl?: (input: PublishFactoryCheckInput, signal?: AbortSignal) => Promise<string> | string;
1574
1671
  /**
1575
1672
  * Bounded retry for the check-run POST. GitHub answers 422 for a head SHA it
1576
1673
  * has not seen yet, which is the normal state when `pr:verify` runs before
1577
- * the branch is pushed, and is also briefly true right after a push.
1674
+ * the branch is pushed, and is also briefly true right after a push. Only
1675
+ * that answer is retried (`isRetryableCheckRunFailure`).
1578
1676
  */
1579
1677
  retry?: {
1580
1678
  attempts: number;
@@ -1659,6 +1757,15 @@ declare function fetchPrVerifyProofBinding({
1659
1757
  sha: string;
1660
1758
  runJson?: GhJsonRunner;
1661
1759
  }): PrVerifyProofBinding | undefined;
1760
+ /**
1761
+ * Where rules 2 and 4 read from: the App id to trust and the `gh api` runner.
1762
+ * Both default to production (the user config's App id else the pinned one,
1763
+ * and `runGhJson`); tests state them.
1764
+ */
1765
+ interface ExactHeadRunSource {
1766
+ appId?: () => string;
1767
+ runJson?: GhJsonRunner;
1768
+ }
1662
1769
  //#endregion
1663
1770
  //#region src/merge-freeze.d.ts
1664
1771
  declare const MERGE_FREEZE_CHECK_NAME = "patronage-factory/merge-freeze";
@@ -2124,6 +2231,8 @@ declare const isSupersededHostedVerifyCheck: (check: StatusCheckRollup, cutoffIs
2124
2231
  * `gh` is injectable so a test can hand the real read a fake GitHub.
2125
2232
  */
2126
2233
  declare const fetchGitHubBranchTipSha: (repository: CheckoutRepository, branch: string, cwd: string, gh?: <T>(args: string[], cwd: string) => T) => string;
2234
+ /** An asynchronous `gh` JSON read. Injected so a test can hand the reads a fake GitHub. */
2235
+ type GhBoundedJsonReader = (args: string[], cwd: string) => Promise<unknown>;
2127
2236
  //#endregion
2128
2237
  //#region src/github-app-client.d.ts
2129
2238
  interface FactoryAppRequest {
@@ -2499,6 +2608,12 @@ interface ReadFactoryPrStatusSourcesInput {
2499
2608
  * a completed unsuccessful check run when inventory has no registration.
2500
2609
  */
2501
2610
  readonly previewTargets?: readonly string[];
2611
+ /**
2612
+ * Token for the `/pulls/:n` identity read only. Defaults to `token`. On a
2613
+ * Cursor VM `token` is the broker's checks token, which cannot read pulls,
2614
+ * so this is the broker's admission token (#1327).
2615
+ */
2616
+ readonly pullToken?: string;
2502
2617
  readonly repo: string;
2503
2618
  /** Factory App installation token, normally minted by the presentation job. */
2504
2619
  readonly token: string;
@@ -2555,8 +2670,19 @@ interface RefreshFactoryPrStatusHudInput {
2555
2670
  readonly repo: string;
2556
2671
  readonly token?: string;
2557
2672
  }
2673
+ /**
2674
+ * Where a local HUD refresh gets its credentials. Defaults to the process
2675
+ * environment, the user config, the real broker, and `fetch`.
2676
+ */
2677
+ interface RefreshFactoryPrStatusHudDependencies {
2678
+ /** The broker's admission token on a Cursor VM, for the pull read (#1327). */
2679
+ readonly admission?: AdmissionCredential;
2680
+ /** The checks-scope broker mint on a Cursor VM, unchanged (#949). */
2681
+ readonly factoryApp?: FactoryAppAccessDependencies;
2682
+ readonly fetch?: typeof fetch;
2683
+ }
2558
2684
  /** Plan from the checkout and present the App-owned HUD. */
2559
- declare function refreshFactoryPrStatusHud(input: RefreshFactoryPrStatusHudInput): Promise<UpsertFactoryPrStatusCommentResult>;
2685
+ declare function refreshFactoryPrStatusHud(input: RefreshFactoryPrStatusHudInput, dependencies?: RefreshFactoryPrStatusHudDependencies): Promise<UpsertFactoryPrStatusCommentResult>;
2560
2686
  /**
2561
2687
  * Presentation must not fail an admission gate. Used after pr:ready publishes
2562
2688
  * its check so the table is not stuck at first-write.
@@ -2728,19 +2854,34 @@ interface PullRequestArmingFacts {
2728
2854
  * arm, and its notice says which read failed.
2729
2855
  */
2730
2856
  interface ArmingPolicyReads {
2731
- /** One issue's sub-issue parent, or `null` when it has none. */
2857
+ /**
2858
+ * One issue's sub-issue parent, or `null` when it has none. An issue read,
2859
+ * so on a Cursor VM the real one runs on the admission token (#1327).
2860
+ */
2732
2861
  readIssueParent: (input: {
2733
2862
  issue: number;
2734
2863
  owner: string;
2735
2864
  repo: string;
2736
- }) => IssueParent | null;
2865
+ }) => IssueParent | null | Promise<IssueParent | null>;
2866
+ /**
2867
+ * The repository's auto-merge setting and this pull request's labels and
2868
+ * closing issues. The closing issues are issue data, which the VM's own
2869
+ * `gh` credential reads back as none rather than refusing, so on a Cursor
2870
+ * VM the real one also runs on the admission token (#1327).
2871
+ */
2737
2872
  readPullRequest: (input: {
2738
2873
  owner: string;
2739
2874
  pr: number;
2740
2875
  repo: string;
2741
- }) => PullRequestArmingFacts;
2876
+ }) => PullRequestArmingFacts | Promise<PullRequestArmingFacts>;
2742
2877
  }
2743
2878
  interface PrReadyDependencies {
2879
+ /**
2880
+ * The broker's admission token on a Cursor VM (#1327), for issue reads the
2881
+ * VM's own `gh` credential cannot make. Defaults to the process-wide
2882
+ * credential, which mints nothing off a Cursor managed runtime.
2883
+ */
2884
+ admission?: AdmissionCredential;
2744
2885
  /**
2745
2886
  * Arms GitHub native auto-merge for an admitted candidate (#477, #1299).
2746
2887
  * Defaults to the real `gh pr merge --auto` call plus the read-back that
@@ -2759,6 +2900,16 @@ interface PrReadyDependencies {
2759
2900
  * policy — unreadable authority is never absent authority.
2760
2901
  */
2761
2902
  basePolicy?: BaseReviewPolicyAuthority;
2903
+ /**
2904
+ * #1323: where the exact-head rules read GitHub's App runs for `pr-verify`
2905
+ * and `pr-review` at one head. `pr:ready` refuses when any App run of
2906
+ * either gate at the PR head is not a completed pass (rule 4); `pr:publish`,
2907
+ * which inherits this, posts a gate's proof only when its head has no run
2908
+ * for that gate (rule 2). Defaults to the user config's App id, else the
2909
+ * pinned factory App id, and the real `gh api` read, since readiness is a
2910
+ * control and an absent dependency must not mean an unread veto.
2911
+ */
2912
+ exactHeadRuns?: ExactHeadRunSource;
2762
2913
  github?: {
2763
2914
  fetchClosingPullRequests?: (input: {
2764
2915
  issue: number;
@@ -3188,6 +3339,11 @@ interface BoundaryCheckArgs {
3188
3339
  repo?: string;
3189
3340
  }
3190
3341
  interface BoundaryCheckDependencies {
3342
+ /**
3343
+ * The broker's admission token on a Cursor VM (#1327), for the issue reads.
3344
+ * Read by {@link runBoundaryCheckWithAdmission} only.
3345
+ */
3346
+ admission?: AdmissionCredential;
3191
3347
  github?: BoundaryCheckGithub;
3192
3348
  hq?: HqIngestDependencies;
3193
3349
  readWaivers?: (cwd: string) => DemandWaiver[];
@@ -3355,6 +3511,12 @@ interface PrReviewIssueBodyInput {
3355
3511
  repo: string;
3356
3512
  }
3357
3513
  interface PrReviewDependencies {
3514
+ /**
3515
+ * The broker's admission token on a Cursor VM (#1327), for the issue reads
3516
+ * `--assemble` makes. Defaults to the process-wide credential, which mints
3517
+ * nothing off a Cursor managed runtime.
3518
+ */
3519
+ admission?: AdmissionCredential;
3358
3520
  /**
3359
3521
  * Test-only seam (#1131): runs after the recording has read the proof
3360
3522
  * state it is applied to and immediately before the compare-and-swap write,
@@ -3403,6 +3565,11 @@ interface PrReviewDependencies {
3403
3565
  * measured against (#1131). Defaults to the GitHub lookup.
3404
3566
  */
3405
3567
  lookupPullRequestBase?: (cwd: string) => LivePullRequestBaseLookup;
3568
+ /**
3569
+ * #1323: the detached `patronage-factory/pr-review` publisher, the same one
3570
+ * `pr:verify` uses. Injected by the CLI; absent, no check run is posted.
3571
+ */
3572
+ publishCheckRun?: (input: PublishFactoryCheckInput) => void;
3406
3573
  }
3407
3574
  declare function runPrReview(args: PrReviewArgs, dependencies?: PrReviewDependencies): Promise<PrReviewProof>;
3408
3575
  //#endregion
@@ -3554,6 +3721,12 @@ interface PrPublishResult {
3554
3721
  * it stays an additive change to the published result contract.
3555
3722
  */
3556
3723
  verificationReuse?: VerificationReuse;
3724
+ /**
3725
+ * Whether the conclusion-only pr:review check run landed for this head
3726
+ * (#1323). Optional so adding it stays an additive change to the published
3727
+ * result contract.
3728
+ */
3729
+ reviewProofBinding?: ReviewProofBindingOutcome;
3557
3730
  /**
3558
3731
  * Whether the durable pr:verify check-run binding landed for this head (#247).
3559
3732
  * Always populated from this version on; optional so adding it stays an
@@ -3568,8 +3741,17 @@ declare class PrPublishFollowUpError extends Error {
3568
3741
  interface PrPublishDependencies extends PrReadyDependencies {
3569
3742
  currentBranch?: (cwd: string) => string | undefined;
3570
3743
  /**
3571
- * Awaited republish of the `pr:verify` check run once the head SHA is on
3572
- * GitHub (#247). Returns whether the App-owned check run landed.
3744
+ * Awaited publication of the conclusion-only `pr:review` check run for the
3745
+ * publish head (#1323). Returns whether the App-owned check run landed.
3746
+ * Injected by the CLI like the verify sink. A review cycle publish records
3747
+ * from `--findings` posts through it too, accepted or blocked.
3748
+ */
3749
+ ensureReviewCheckRun?: typeof ensureFactoryCheckRunPublished;
3750
+ /**
3751
+ * Awaited post of the `pr:verify` check run once the head SHA is on GitHub
3752
+ * (#247), when the head has no run for it yet (#1323). Returns whether the
3753
+ * App-owned check run landed. A `pr:verify` publish runs fresh posts through
3754
+ * it too, pass or failure (#1323).
3573
3755
  */
3574
3756
  ensureVerifyCheckRun?: typeof ensureFactoryCheckRunPublished;
3575
3757
  factoryCliInvocation?: FactoryCliInvocation;
@@ -3584,7 +3766,11 @@ interface PrPublishDependencies extends PrReadyDependencies {
3584
3766
  };
3585
3767
  runFollowUp?: FollowUpRunner;
3586
3768
  runPrReady?: typeof runPrReady;
3587
- runPrReview?: (args: PrReviewArgs) => Promise<PrReviewProof>;
3769
+ /**
3770
+ * The review producer publish runs for `--findings`. Its second argument
3771
+ * carries the publisher for the verdict it records (#1323).
3772
+ */
3773
+ runPrReview?: (args: PrReviewArgs, dependencies?: ProducerPublisher) => Promise<PrReviewProof>;
3588
3774
  /** Settles asynchronously scheduled sink work before the handoff drain. */
3589
3775
  awaitPendingIngest?: () => Promise<void>;
3590
3776
  /**
@@ -3594,7 +3780,11 @@ interface PrPublishDependencies extends PrReadyDependencies {
3594
3780
  flushHqSpool?: (args: {
3595
3781
  cwd: string;
3596
3782
  }) => Promise<HqFlushResult>;
3597
- runPrVerify?: (args: PrVerifyArgs) => Promise<PrVerifyProof>;
3783
+ /**
3784
+ * The verify producer publish runs when no proof applies. Its second
3785
+ * argument carries the publisher for the verdict it produces (#1323).
3786
+ */
3787
+ runPrVerify?: (args: PrVerifyArgs, dependencies?: ProducerPublisher) => Promise<PrVerifyProof>;
3598
3788
  /**
3599
3789
  * Injectable clock for the promotion instant the hosted-run await measures
3600
3790
  * `verify` staleness against (#1045; tests only — production reads the real
@@ -3612,13 +3802,19 @@ interface PrPublishDependencies extends PrReadyDependencies {
3612
3802
  * the publish summary: a binding that silently failed to land is
3613
3803
  * indistinguishable from verification that never happened, and the operator is
3614
3804
  * the only one who can fix the cause (usually an unpushed head).
3805
+ *
3806
+ * #1323 (rule 2): `skipped-existing-run` means the head already has an App run
3807
+ * for the gate, so publish posted nothing; `skipped-unreadable` means GitHub's
3808
+ * runs at the head could not be read whole, so publish posted nothing either.
3615
3809
  */
3616
3810
  type VerifyProofBindingOutcome = {
3617
3811
  status: "not-attempted";
3618
3812
  } | {
3619
3813
  headSha: string;
3620
- status: "failed" | "published" | "skipped-relabelled-head";
3814
+ status: ExactHeadBindingStatus | "skipped-relabelled-head";
3621
3815
  };
3816
+ /** What a gate's post at the publish head came to, once rule 2 has read. */
3817
+ type ExactHeadBindingStatus = "failed" | "published" | "skipped-existing-run" | "skipped-unreadable";
3622
3818
  /**
3623
3819
  * Whether publish reused the on-disk `pr:verify` proof, and the one-line reason
3624
3820
  * (#291). Publish decides reuse silently today, which is what made the
@@ -3629,6 +3825,21 @@ interface VerificationReuse {
3629
3825
  reason: string;
3630
3826
  reused: boolean;
3631
3827
  }
3828
+ /** The publisher a producer takes: `pr:verify`'s and `pr:review`'s own seam. */
3829
+ interface ProducerPublisher {
3830
+ publishCheckRun?: (input: PublishFactoryCheckInput) => void;
3831
+ }
3832
+ /**
3833
+ * What became of the conclusion-only `pr:review` check run for this publish
3834
+ * (#1323). A proof that records no review (`not-required`) has no verdict to
3835
+ * post.
3836
+ */
3837
+ type ReviewProofBindingOutcome = {
3838
+ status: "not-attempted";
3839
+ } | {
3840
+ headSha: string;
3841
+ status: ExactHeadBindingStatus | "skipped-no-review" | "skipped-relabelled-head";
3842
+ };
3632
3843
  /**
3633
3844
  * Whether publish runs the follow-up `pr:ready` recommended, plus the one-line
3634
3845
  * reason either way (#291).
@@ -4728,6 +4939,112 @@ interface HqSpoolCheckDependencies {
4728
4939
  countSpool?: typeof countHqSpoolWork;
4729
4940
  sweepOrphans?: typeof sweepHqSpoolOrphans;
4730
4941
  }
4942
+ /** Test seams. Production never sets these. */
4943
+ interface HqReadinessCheckDependencies {
4944
+ fetch?: typeof fetch;
4945
+ resolveSecret?: SecretReferenceResolver;
4946
+ timeoutMs?: number;
4947
+ }
4948
+ //#endregion
4949
+ //#region src/hq-last-stored.d.ts
4950
+ declare const lastStoredSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
4951
+ found: z.ZodLiteral<true>;
4952
+ lastFromCaller: z.ZodNullable<z.ZodObject<{
4953
+ anyRepository: z.ZodNullable<z.ZodObject<{
4954
+ receivedAt: z.ZodString;
4955
+ }, z.core.$strip>>;
4956
+ inRepository: z.ZodNullable<z.ZodObject<{
4957
+ receivedAt: z.ZodString;
4958
+ }, z.core.$strip>>;
4959
+ machine: z.ZodString;
4960
+ }, z.core.$strip>>;
4961
+ lastProofEvent: z.ZodNullable<z.ZodObject<{
4962
+ receivedAt: z.ZodString;
4963
+ kind: z.ZodString;
4964
+ observedAt: z.ZodString;
4965
+ }, z.core.$strip>>;
4966
+ lastWebhookEvent: z.ZodNullable<z.ZodObject<{
4967
+ receivedAt: z.ZodString;
4968
+ event: z.ZodString;
4969
+ }, z.core.$strip>>;
4970
+ repository: z.ZodString;
4971
+ }, z.core.$strip>, z.ZodObject<{
4972
+ found: z.ZodLiteral<false>;
4973
+ lastFromCaller: z.ZodNullable<z.ZodObject<{
4974
+ anyRepository: z.ZodNullable<z.ZodObject<{
4975
+ receivedAt: z.ZodString;
4976
+ }, z.core.$strip>>;
4977
+ inRepository: z.ZodNullable<z.ZodObject<{
4978
+ receivedAt: z.ZodString;
4979
+ }, z.core.$strip>>;
4980
+ machine: z.ZodString;
4981
+ }, z.core.$strip>>;
4982
+ repository: z.ZodString;
4983
+ }, z.core.$strip>], "found">;
4984
+ type HqLastStoredAnswer = z.infer<typeof lastStoredSchema>;
4985
+ /**
4986
+ * Why HQ gave no answer. `detail` is the operator-facing sentence; it can name
4987
+ * a reference or a status, never a credential value.
4988
+ */
4989
+ type HqLastStoredUnknownReason = "no-credentials" | "no-endpoint" | "origin-not-authorized" | "unexpected-response" | "unreachable";
4990
+ type HqLastStored = {
4991
+ answer: HqLastStoredAnswer;
4992
+ status: "answered";
4993
+ } | {
4994
+ detail: string;
4995
+ reason: HqLastStoredUnknownReason;
4996
+ status: "unknown";
4997
+ };
4998
+ interface HqLastStoredInput {
4999
+ /** The profile's `hq.endpoint`, HQ's origin. */
5000
+ endpoint: string;
5001
+ env: NodeJS.ProcessEnv;
5002
+ /** The operator's `hqAllowedOrigins`. */
5003
+ hqAllowedOrigins?: readonly string[];
5004
+ hqIngestCredentials?: HqIngestCredentialReferences;
5005
+ repository: {
5006
+ owner: string;
5007
+ repo: string;
5008
+ };
5009
+ }
5010
+ /** Test seams. Production never sets these. */
5011
+ interface HqLastStoredDependencies {
5012
+ fetch?: typeof fetch;
5013
+ resolveSecret?: SecretReferenceResolver;
5014
+ timeoutMs?: number;
5015
+ }
5016
+ /** Asks HQ when it last stored an event for `repository`. Never throws. */
5017
+ declare function fetchHqLastStored(input: HqLastStoredInput, dependencies?: HqLastStoredDependencies): Promise<HqLastStored>;
5018
+ //#endregion
5019
+ //#region src/doctor-hq-last-stored.d.ts
5020
+ /** Test seams. Production never sets these. */
5021
+ interface HqLastStoredCheckDependencies {
5022
+ countSpool?: typeof countHqSpoolWork;
5023
+ lastStored?: typeof fetchHqLastStored;
5024
+ now?: () => Date;
5025
+ }
5026
+ //#endregion
5027
+ //#region src/doctor-hq-webhook.d.ts
5028
+ /** Test seams. Production never sets these. */
5029
+ interface HqWebhookCheckDependencies {
5030
+ gh?: GhBoundedJsonReader;
5031
+ lastStored?: typeof fetchHqLastStored;
5032
+ now?: () => Date;
5033
+ }
5034
+ //#endregion
5035
+ //#region src/doctor-machine-checks.d.ts
5036
+ /** Test seams. Production never sets these. */
5037
+ interface MachineCheckDependencies {
5038
+ cpuCount?: () => number;
5039
+ /** Whether an executable file with this name is on the PATH in `env`. */
5040
+ isOnPath?: (name: string, env: NodeJS.ProcessEnv) => boolean;
5041
+ nodeVersion?: string;
5042
+ platform?: NodeJS.Platform;
5043
+ /** A small text file's contents, or undefined when it cannot be read. */
5044
+ readText?: (filePath: string) => string | undefined;
5045
+ /** What `/bin/sh` resolves to, or undefined when it cannot be resolved. */
5046
+ resolveShell?: () => string | undefined;
5047
+ }
4731
5048
  //#endregion
4732
5049
  //#region src/doctor.d.ts
4733
5050
  interface DoctorReport {
@@ -4745,8 +5062,16 @@ interface DoctorProjectProfileInput extends LoadProjectProfileInput {
4745
5062
  /** Diff base for the admission preflight; ignored unless `preflight`. */
4746
5063
  base?: string;
4747
5064
  env?: NodeJS.ProcessEnv;
5065
+ /** Test seam for the hq-last-stored row (#1342); production never sets this. */
5066
+ hqLastStoredDependencies?: HqLastStoredCheckDependencies;
5067
+ /** Test seam for the HQ readiness rows (#1315); production never sets this. */
5068
+ hqReadinessDependencies?: HqReadinessCheckDependencies;
4748
5069
  /** Test seam for the local HQ spool check (#394); production never sets this. */
4749
5070
  hqSpoolDependencies?: HqSpoolCheckDependencies;
5071
+ /** Test seam for the hq-webhook row (#1341); production never sets this. */
5072
+ hqWebhookDependencies?: HqWebhookCheckDependencies;
5073
+ /** Test seam for the machine readiness rows (#1315); production never sets this. */
5074
+ machineDependencies?: MachineCheckDependencies;
4750
5075
  /**
4751
5076
  * Append the read-only admission checklist (#292): every requirement this
4752
5077
  * candidate must satisfy before merge, named in one pass. Preflight checks
@@ -4880,6 +5205,10 @@ interface EvaluationInput {
4880
5205
  blockingReasons: string[];
4881
5206
  notices: string[];
4882
5207
  };
5208
+ exactHeadRunRefusals?: {
5209
+ prReview: string[];
5210
+ prVerify: string[];
5211
+ };
4883
5212
  waivers?: DemandWaiver[];
4884
5213
  }
4885
5214
  declare const evaluateReadiness: (input: EvaluationInput) => {
@@ -5802,6 +6131,12 @@ declare function createProgram(options?: CreateProgramOptions): Command;
5802
6131
  * runnable bin module, which follow-up commands spawn; the bin entry passes its
5803
6132
  * own path. This function never runs itself — `./cli.js` is the only caller
5804
6133
  * that does (#386).
6134
+ *
6135
+ * On the way out it waits for the command's own check-run publications
6136
+ * (bounded, #1322) and HQ emissions, then drains
6137
+ * the checkout's HQ spool once (#1313). This is the one place that drains on
6138
+ * exit. `pr:ready` then prints one line if events are still spooled (#1317).
6139
+ * None of these steps can throw or change the command's exit code.
5805
6140
  */
5806
6141
  declare function run(argv?: string[], cliEntry?: string): Promise<void>;
5807
6142
  //#endregion