@patronage/software-factory 0.23.0 → 0.25.0

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
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync, rmSync } from "node:fs";
2
2
  import { Command } from "commander";
3
3
  import { z } from "zod";
4
+ import { readFile } from "node:fs/promises";
4
5
 
5
6
  //#region src/github-issue-comments.d.ts
6
7
  interface GithubIssueCommentApi {
@@ -166,6 +167,195 @@ interface HqIngestDependencies {
166
167
  timeoutMs?: number;
167
168
  transportTimeoutMs?: number;
168
169
  }
170
+ /** The repository whose undelivered evidence a spool holds. */
171
+ interface HqSpoolRepository {
172
+ owner: string;
173
+ repo: string;
174
+ }
175
+ /**
176
+ * What became of one spooled event. `delivered` and `duplicate` both mean HQ
177
+ * holds it (dedup is by content-addressed eventId), so the entry is removed;
178
+ * `rejected` and `unreachable` leave it spooled.
179
+ */
180
+ interface HqSpoolEntryOutcome {
181
+ detail?: string;
182
+ eventId: string;
183
+ kind: string;
184
+ spool: string;
185
+ /**
186
+ * `migrated` belongs to the legacy JSONL journal only: the row was moved
187
+ * into the current spool without being delivered. The spool pass that runs
188
+ * after it in the same drain supersedes that line with a real outcome when
189
+ * it gets to the row; a `migrated` line that survives the run means the row
190
+ * is still waiting.
191
+ *
192
+ * `undeliverable` is the one terminal verdict (#445). Every other status
193
+ * describes a moment: HQ was unreachable, HQ refused this content today, the
194
+ * row moved. Retrying is meaningful for all of them. A wrong-origin entry is
195
+ * different in kind — it is refused here, from the entry's own bytes, with no
196
+ * request made, and the same bytes produce the same verdict on every future
197
+ * run. Leaving it spooled asks the operator to retry something that provably
198
+ * cannot succeed, and the count it inflates is the one doctor goes red on.
199
+ */
200
+ status: "delivered" | "duplicate" | "migrated" | "rejected" | "undeliverable" | "unreachable";
201
+ }
202
+ interface HqSpoolFlushInput {
203
+ clientId: string;
204
+ clientSecret: string;
205
+ /** Repository root whose legacy `.factory-memory` spool is also drained. */
206
+ cwd: string;
207
+ /** The profile's HQ origin; entries recorded against another are refused. */
208
+ endpoint: string;
209
+ /** Operator-named spool directories; replaces the default two locations. */
210
+ explicitDirectories?: string[];
211
+ repository: HqSpoolRepository;
212
+ }
213
+ interface HqSpoolFlushSummary {
214
+ delivered: number;
215
+ duplicate: number;
216
+ /**
217
+ * The drain did not finish: the budget elapsed, or events beyond the
218
+ * rejected ones are still spooled. Never report an incomplete pass as a
219
+ * clean drain — a recovery run reads this to know whether to run again.
220
+ */
221
+ incomplete: boolean;
222
+ outcomes: HqSpoolEntryOutcome[];
223
+ rejected: number;
224
+ /** Events still in the drained spools when the pass ended. */
225
+ remaining: number;
226
+ /** Locations that were read; a missing one is simply absent from the list. */
227
+ spools: string[];
228
+ /**
229
+ * Events dispositioned as permanently undeliverable this pass (#445). They
230
+ * are gone from `remaining` — that is the point — so this is the only place
231
+ * the run says they existed.
232
+ */
233
+ undeliverable: number;
234
+ unreachable: number;
235
+ /** Files retained by a transport failure, counted per file. */
236
+ unreachableFiles: number;
237
+ }
238
+ interface HqSpoolWorkCount {
239
+ /**
240
+ * The earliest moment learned across pending spool files (their own write
241
+ * time) and legacy journal rows (their own `failedAt`). Absent only when
242
+ * `pending` is `0`, or when every timestamp source was unreadable within
243
+ * budget — an estimate for doctor's remediation message (#394), never a
244
+ * precise audit trail.
245
+ */
246
+ oldestQueuedAt?: string;
247
+ /** Spooled events and replayable journals waiting in the locations below. */
248
+ pending: number;
249
+ /** Locations that exist and hold spooled work. */
250
+ spools: string[];
251
+ /**
252
+ * A location existed but could not be listed. The count above saw nothing
253
+ * there, so a caller deciding whether the drain is worth doing must treat a
254
+ * non-zero value as "work may be waiting" — never as an empty spool.
255
+ */
256
+ unlistable: number;
257
+ }
258
+ /**
259
+ * Counts spooled work for a repository without draining it or touching a
260
+ * credential (#414).
261
+ *
262
+ * `hq:flush` used to resolve the HQ Access token before it ever looked at the
263
+ * spool, so a lane with nothing to send still paid a secret-manager round trip
264
+ * — and still failed, opaquely, in a sandbox that has no keychain access. The
265
+ * same locations `flushHqSpool` drains are inspected here, read-only: no
266
+ * directory is created, nothing is secured, and nothing is delivered.
267
+ */
268
+ declare function countHqSpoolWork(input: Pick<HqSpoolFlushInput, "cwd" | "explicitDirectories" | "repository">, dependencies?: {
269
+ budgetMs?: number;
270
+ env?: NodeJS.ProcessEnv;
271
+ }): Promise<HqSpoolWorkCount>;
272
+ /** A spool under an earlier key for this repository, not the current one. */
273
+ interface HqSpoolOrphan {
274
+ /** The `hq-retry-spool` directory itself, ready to pass to `--dir`. */
275
+ directory: string;
276
+ oldestQueuedAt?: string;
277
+ /** Spooled events and journal rows waiting there. */
278
+ pending: number;
279
+ /**
280
+ * The location exists but could not be listed. As everywhere else in this
281
+ * inspection, unknown counts as work: a swept location nobody could read is
282
+ * reported, never quietly dropped as empty.
283
+ */
284
+ unlistable: number;
285
+ }
286
+ interface HqSpoolOrphanSweep {
287
+ /** Only locations holding work; an empty orphan spool is not a finding. */
288
+ orphans: HqSpoolOrphan[];
289
+ root: string;
290
+ }
291
+ /**
292
+ * Reports spooled evidence sitting under an earlier key for *this* repository
293
+ * (#420).
294
+ *
295
+ * The spool is keyed by owner and repo, so a change to the segment encoding
296
+ * itself — which happened during #390's own development — moves the address
297
+ * without moving the evidence. Both readers of the spool resolve exactly one
298
+ * key, so the events under the old one become invisible: the drain reports
299
+ * success, doctor reports empty, and three real proofs sat unread until an
300
+ * attended recovery enumerated the tree by hand.
301
+ *
302
+ * This sweep only reports. Draining another key's events is a decision this
303
+ * does not make — the operator gets the location and the count, and
304
+ * `hq:flush --dir` remains the recovery path.
305
+ *
306
+ * **Why candidate keys and not a walk of the root (#446).** The root is shared
307
+ * by every factory repository on the machine, so enumerating it and calling
308
+ * everything that is not the current key an orphan describes another
309
+ * repository's ordinary, current, correct spool exactly as well as it describes
310
+ * this repository's obsolete one. That made doctor red in one checkout because
311
+ * a different repository had pending work, and told the operator to drain it —
312
+ * confidently prescribing the wrong action. Nothing on disk distinguishes the
313
+ * two cases: an unrecognised key carries no statement about who wrote it.
314
+ *
315
+ * So discovery is scoped to the keys *this* repository could plausibly have
316
+ * produced — the current scheme plus the earlier ones listed in
317
+ * `hqSpoolCandidateKeys` — and a key outside that set is never this
318
+ * repository's business. The #420 incident is inside it: the joined
319
+ * single-segment key is one of the candidates.
320
+ *
321
+ * **And a marked walk beside them (#447).** Derivation's other blind spot is a
322
+ * key whose *encoding* this checkout no longer produces but whose owner and
323
+ * repo are unchanged — the retired non-injective era being the live example.
324
+ * That era cannot be derived safely, because a candidate built from it can
325
+ * equal a different repository's current key, so #446 dropped it rather than
326
+ * risk the cross-repository claim again.
327
+ *
328
+ * `SPOOL_REPOSITORY_MARKER` supplies the proof that derivation could not. Every
329
+ * enqueue stamps its spool with the repository writing it, so the root can be
330
+ * walked again: a directory whose marker names *this* repository is this
331
+ * repository's, whatever key encoding it sits under, and a directory whose
332
+ * marker names another repository is never reported here. That is the
333
+ * discriminator #446 correctly said did not exist — it exists now because
334
+ * something writes it down.
335
+ *
336
+ * The two discoveries are complements, not alternatives. The walk sees only
337
+ * what was stamped; directories written before this shipped have no marker, and
338
+ * candidate keys still find those. An unmarked directory is still never
339
+ * reported, because it still carries no statement about who wrote it.
340
+ *
341
+ * **What this does NOT close, despite being the marker's obvious use: renames
342
+ * and owner changes.** A marker records the identity that was current when the
343
+ * directory was written, so after `patronage/old` becomes `patronage/new` the
344
+ * stranded directory is stamped `patronage/old` — and matching is equality
345
+ * against the checkout's *present* identity, which rejects it. Making that work
346
+ * needs an identifier that survives a rename, which neither the profile nor the
347
+ * marker carries today; accepting a non-matching marker instead would be
348
+ * guessing, which is the #446 defect wearing a new hat. #447 stays open for it.
349
+ *
350
+ * Also outside the sweep, by construction: evidence under a *different state
351
+ * root*, if `XDG_STATE_HOME` moves. No walk of this root can reach another one.
352
+ */
353
+ declare function sweepHqSpoolOrphans(input: {
354
+ repository: HqSpoolRepository;
355
+ }, dependencies?: {
356
+ budgetMs?: number;
357
+ env?: NodeJS.ProcessEnv;
358
+ }): Promise<HqSpoolOrphanSweep>;
169
359
  //#endregion
170
360
  //#region src/demand-waiver.d.ts
171
361
  declare const DEFAULT_DEMAND_WAIVER_PATH = ".factory-memory/demand-waivers.json";
@@ -264,6 +454,13 @@ declare const authorizeDemandWaiver: ({
264
454
  session: string | undefined;
265
455
  }) => AuthorizeDemandWaiverResult;
266
456
  //#endregion
457
+ //#region src/blocked-reasons.d.ts
458
+ declare const blockedReasonSchema: z.ZodObject<{
459
+ code: z.ZodString;
460
+ detail: z.ZodString;
461
+ }, z.core.$strip>;
462
+ type BlockedReason = z.infer<typeof blockedReasonSchema>;
463
+ //#endregion
267
464
  //#region src/checkout-repository.d.ts
268
465
  interface CheckoutRepository {
269
466
  name: string;
@@ -321,8 +518,8 @@ declare const mergeFreezeStateSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
321
518
  generationId: z.ZodNumber;
322
519
  headSha: z.ZodString;
323
520
  outcome: z.ZodEnum<{
324
- stale: "stale";
325
521
  active: "active";
522
+ stale: "stale";
326
523
  }>;
327
524
  reason: z.ZodString;
328
525
  recordedAt: z.ZodISODateTime;
@@ -1399,8 +1596,8 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
1399
1596
  status: z.ZodEnum<{
1400
1597
  blocked: "blocked";
1401
1598
  "not-required": "not-required";
1402
- current: "current";
1403
1599
  stale: "stale";
1600
+ current: "current";
1404
1601
  missing: "missing";
1405
1602
  }>;
1406
1603
  }, z.core.$strip>;
@@ -1412,8 +1609,8 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
1412
1609
  status: z.ZodEnum<{
1413
1610
  blocked: "blocked";
1414
1611
  "not-required": "not-required";
1415
- current: "current";
1416
1612
  stale: "stale";
1613
+ current: "current";
1417
1614
  missing: "missing";
1418
1615
  }>;
1419
1616
  }, z.core.$strip>>;
@@ -1430,8 +1627,8 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
1430
1627
  docsOnlyDeltaAccepted: z.ZodOptional<z.ZodBoolean>;
1431
1628
  docsOnlyVerifiedHeadSha: z.ZodOptional<z.ZodString>;
1432
1629
  prVerify: z.ZodEnum<{
1433
- passed: "passed";
1434
1630
  stale: "stale";
1631
+ passed: "passed";
1435
1632
  missing: "missing";
1436
1633
  }>;
1437
1634
  trivialDeltaAccepted: z.ZodOptional<z.ZodBoolean>;
@@ -1516,7 +1713,15 @@ interface GitHubPullRequest {
1516
1713
  url: string;
1517
1714
  }
1518
1715
  interface PrReadyProof {
1519
- schemaVersion: 1;
1716
+ schemaVersion: 2;
1717
+ /**
1718
+ * Why this run blocked, one entry per refusing demand (#391): `code` is the
1719
+ * demand key from the resolver's vocabulary, `detail` the one-sentence
1720
+ * refusal. Same refusals as `blockingReasons`, in the same order — the
1721
+ * analyzable projection of a flat string list, so a wall of blocked proofs
1722
+ * on one PR can be counted by cause. Absent when nothing blocked.
1723
+ */
1724
+ blockedReasons?: BlockedReason[];
1520
1725
  blockingReasons: string[];
1521
1726
  humanBlockingReasons: string[];
1522
1727
  command: "patronage-factory pr:ready";
@@ -1779,8 +1984,8 @@ declare const REVIEW_STATUS_VALUES: readonly ["not-required", "current", "stale"
1779
1984
  declare const reviewStatusSchema: z.ZodEnum<{
1780
1985
  blocked: "blocked";
1781
1986
  "not-required": "not-required";
1782
- current: "current";
1783
1987
  stale: "stale";
1988
+ current: "current";
1784
1989
  missing: "missing";
1785
1990
  }>;
1786
1991
  type ReviewStatus = z.infer<typeof reviewStatusSchema>;
@@ -2369,6 +2574,55 @@ type DemandWaiveAction = (args: DemandWaiveArgs) => DemandWaiver;
2369
2574
  //#region src/commands/pr-merge-check.d.ts
2370
2575
  type PrMergeCheckAction = (args: PrMergeCheckArgs) => PrMergeCheckProof;
2371
2576
  //#endregion
2577
+ //#region src/hq-credentials.d.ts
2578
+ /**
2579
+ * Why a reference did not resolve. Each value names a different remedy, and
2580
+ * none of them can be inferred from a value-or-nothing result:
2581
+ *
2582
+ * - `resolver-missing` — no secret-manager binary on PATH.
2583
+ * - `resolver-blocked` — a binary exists but this session may not execute it
2584
+ * (a sandbox denying exec). The command belongs outside the sandbox.
2585
+ * - `resolver-timeout` — the probe expired: a desktop agent waiting on an
2586
+ * approval nobody can give here.
2587
+ * - `resolver-refused` — the binary ran and produced no value. Its store is
2588
+ * unreachable from this session (a sandbox with no keychain access) or the
2589
+ * reference is not readable. These two stay one status on purpose: telling
2590
+ * them apart would mean reading resolver output.
2591
+ */
2592
+ type SecretResolutionFailure = "resolver-blocked" | "resolver-missing" | "resolver-refused" | "resolver-timeout";
2593
+ /** A structured resolution outcome. The value travels only when resolved. */
2594
+ type SecretResolution = {
2595
+ status: "resolved";
2596
+ value: string;
2597
+ } | {
2598
+ status: SecretResolutionFailure;
2599
+ };
2600
+ /** Resolves a secret reference. Injected so tests stay offline. */
2601
+ type SecretReferenceResolver = (reference: string) => SecretResolution;
2602
+ //#endregion
2603
+ //#region src/hq-flush.d.ts
2604
+ /**
2605
+ * Spool locations this run did not drain because they sit under an earlier
2606
+ * key for this repository. Reported, never drained: draining an older key's
2607
+ * events is an operator decision, made with `--dir`.
2608
+ */
2609
+ interface HqFlushOrphans {
2610
+ orphans: HqSpoolOrphan[];
2611
+ }
2612
+ type HqFlushResult = (HqFlushOrphans & {
2613
+ reason: string;
2614
+ /**
2615
+ * Events still spooled when the command gave up. Non-zero means the
2616
+ * skip retained work: the exit status says so, and no caller may read
2617
+ * the skip as "there was nothing to do".
2618
+ */
2619
+ retained: number;
2620
+ status: "skipped";
2621
+ }) | (HqFlushOrphans & HqSpoolFlushSummary & {
2622
+ endpoint: string;
2623
+ status: "flushed";
2624
+ });
2625
+ //#endregion
2372
2626
  //#region src/pr-review.d.ts
2373
2627
  interface PrReviewArgs extends LoadProjectProfileInput {
2374
2628
  base: string;
@@ -2514,6 +2768,15 @@ interface PrPublishDependencies extends PrReadyDependencies {
2514
2768
  runFollowUp?: FollowUpRunner;
2515
2769
  runPrReady?: typeof runPrReady;
2516
2770
  runPrReview?: (args: PrReviewArgs) => Promise<PrReviewProof>;
2771
+ /** Settles asynchronously scheduled sink work before the handoff drain. */
2772
+ awaitPendingIngest?: () => Promise<void>;
2773
+ /**
2774
+ * The handoff drain (#390). Advisory everywhere: publish reports what it
2775
+ * found and never changes its verdict or exit status on the result.
2776
+ */
2777
+ flushHqSpool?: (args: {
2778
+ cwd: string;
2779
+ }) => Promise<HqFlushResult>;
2517
2780
  runPrVerify?: (args: PrVerifyArgs) => Promise<PrVerifyProof>;
2518
2781
  /**
2519
2782
  * Injectable delay for the bounded hosted-run await (#348; tests only —
@@ -3225,8 +3488,8 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
3225
3488
  interiorCycle: z.ZodOptional<z.ZodNumber>;
3226
3489
  observedAt: z.ZodString;
3227
3490
  purpose: z.ZodEnum<{
3228
- code: "code";
3229
3491
  review: "review";
3492
+ code: "code";
3230
3493
  }>;
3231
3494
  slotResolution: z.ZodObject<{
3232
3495
  effort: z.ZodEnum<{
@@ -3304,8 +3567,8 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
3304
3567
  interiorCycle: z.ZodOptional<z.ZodNumber>;
3305
3568
  observedAt: z.ZodString;
3306
3569
  purpose: z.ZodEnum<{
3307
- code: "code";
3308
3570
  review: "review";
3571
+ code: "code";
3309
3572
  }>;
3310
3573
  slotResolution: z.ZodObject<{
3311
3574
  effort: z.ZodEnum<{
@@ -3374,8 +3637,8 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
3374
3637
  interiorCycle: z.ZodOptional<z.ZodNumber>;
3375
3638
  observedAt: z.ZodString;
3376
3639
  purpose: z.ZodEnum<{
3377
- code: "code";
3378
3640
  review: "review";
3641
+ code: "code";
3379
3642
  }>;
3380
3643
  slotResolution: z.ZodObject<{
3381
3644
  effort: z.ZodEnum<{
@@ -3447,7 +3710,7 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
3447
3710
  exitCode: number | null;
3448
3711
  harness: string;
3449
3712
  observedAt: string;
3450
- purpose: "code" | "review";
3713
+ purpose: "review" | "code";
3451
3714
  slotResolution: {
3452
3715
  effort: "high" | "low" | "medium" | "xhigh";
3453
3716
  engine: string;
@@ -3666,13 +3929,39 @@ declare const isBotLogin: (login?: string | undefined) => boolean;
3666
3929
  declare const SHA_MATCH_MIN_LENGTH = 7;
3667
3930
  declare const sameHeadSha: (left: string, right: string) => boolean;
3668
3931
  //#endregion
3669
- //#region src/doctor.d.ts
3932
+ //#region src/doctor-hq-checks.d.ts
3933
+ /**
3934
+ * Two `doctor` checks that make silent HQ delivery failure loud (#394,
3935
+ * epic #389 wave 2).
3936
+ *
3937
+ * Both consume #414's structured credential resolution rather than inventing
3938
+ * a second classification: a sandboxed session that cannot reach the
3939
+ * keychain is reported as "cannot resolve credentials here", never as
3940
+ * "credentials are wrong" or a silent pass. Both name `psf hq:flush` and
3941
+ * `psf pr:publish` as the commands that need a trusted local session.
3942
+ *
3943
+ * Advisory stays advisory: neither check can block anything but doctor's own
3944
+ * exit status (epic #389 design decision 4). Absent credentials make the
3945
+ * remote check a skip with a printed reason, never a silent pass.
3946
+ */
3670
3947
  type DoctorCheckStatus = "error" | "ok" | "warning";
3671
3948
  interface DoctorCheck {
3672
3949
  message: string;
3673
3950
  name: string;
3674
3951
  status: DoctorCheckStatus;
3675
3952
  }
3953
+ interface HqSpoolCheckDependencies {
3954
+ countSpool?: typeof countHqSpoolWork;
3955
+ sweepOrphans?: typeof sweepHqSpoolOrphans;
3956
+ }
3957
+ interface HqRetroReadbackCheckDependencies {
3958
+ fetch?: typeof fetch;
3959
+ readFile?: typeof readFile;
3960
+ resolve?: SecretReferenceResolver;
3961
+ timeoutMs?: number;
3962
+ }
3963
+ //#endregion
3964
+ //#region src/doctor.d.ts
3676
3965
  interface DoctorReport {
3677
3966
  checks: DoctorCheck[];
3678
3967
  ok: boolean;
@@ -3689,6 +3978,10 @@ interface DoctorProjectProfileInput extends LoadProjectProfileInput {
3689
3978
  /** Diff base for the admission preflight; ignored unless `preflight`. */
3690
3979
  base?: string;
3691
3980
  env?: NodeJS.ProcessEnv;
3981
+ /** Test seam for the local HQ spool check (#394); production never sets this. */
3982
+ hqRetroReadbackDependencies?: HqRetroReadbackCheckDependencies;
3983
+ /** Test seam for the remote HQ retro-envelope read-back check (#394). */
3984
+ hqSpoolDependencies?: HqSpoolCheckDependencies;
3692
3985
  /**
3693
3986
  * Append the read-only admission checklist (#292): every requirement this
3694
3987
  * candidate must satisfy before merge, named in one pass. Preflight checks
@@ -3697,7 +3990,7 @@ interface DoctorProjectProfileInput extends LoadProjectProfileInput {
3697
3990
  preflight?: boolean;
3698
3991
  userConfig?: LoadUserConfigResult;
3699
3992
  }
3700
- declare function doctorProjectProfile(input?: DoctorProjectProfileInput): DoctorReport;
3993
+ declare function doctorProjectProfile(input?: DoctorProjectProfileInput): Promise<DoctorReport>;
3701
3994
  //#endregion
3702
3995
  //#region src/pr-review-gate-trace.d.ts
3703
3996
  interface ReviewGateTraceIdentity {
@@ -4026,6 +4319,10 @@ interface EvaluationInput {
4026
4319
  waveReviewDemand?: Pick<WaveReviewDemand, "review" | "wave">;
4027
4320
  }
4028
4321
  declare const evaluateReadiness: (input: EvaluationInput) => {
4322
+ blockedReasons: {
4323
+ code: string;
4324
+ detail: string;
4325
+ }[];
4029
4326
  blockingReasons: string[];
4030
4327
  humanBlockingReasons: string[];
4031
4328
  ledger: {
@@ -4053,14 +4350,14 @@ declare const evaluateReadiness: (input: EvaluationInput) => {
4053
4350
  reviews: {
4054
4351
  correctness: {
4055
4352
  required: boolean;
4056
- status: "blocked" | "not-required" | "current" | "stale" | "missing";
4353
+ status: "blocked" | "not-required" | "stale" | "current" | "missing";
4057
4354
  docsOnlyDeltaAccepted?: boolean | undefined;
4058
4355
  reviewedHeadSha?: string | undefined;
4059
4356
  reviewedPatchId?: string | undefined;
4060
4357
  };
4061
4358
  security?: {
4062
4359
  required: boolean;
4063
- status: "blocked" | "not-required" | "current" | "stale" | "missing";
4360
+ status: "blocked" | "not-required" | "stale" | "current" | "missing";
4064
4361
  docsOnlyDeltaAccepted?: boolean | undefined;
4065
4362
  reviewedHeadSha?: string | undefined;
4066
4363
  reviewedPatchId?: string | undefined;
@@ -4070,7 +4367,7 @@ declare const evaluateReadiness: (input: EvaluationInput) => {
4070
4367
  stackRole: "slice" | "single" | "rollup" | "merge-gate prerequisite";
4071
4368
  verification: {
4072
4369
  command: "patronage-factory pr:verify";
4073
- prVerify: "passed" | "stale" | "missing";
4370
+ prVerify: "stale" | "passed" | "missing";
4074
4371
  docsOnlyDeltaAccepted?: boolean | undefined;
4075
4372
  docsOnlyVerifiedHeadSha?: string | undefined;
4076
4373
  trivialDeltaAccepted?: boolean | undefined;
@@ -4356,6 +4653,7 @@ type FetchLike = (input: string, init?: {
4356
4653
  headers?: Record<string, string>;
4357
4654
  method?: string;
4358
4655
  redirect?: "error";
4656
+ signal?: AbortSignal;
4359
4657
  }) => Promise<{
4360
4658
  json: () => Promise<unknown>;
4361
4659
  status: number;
@@ -4370,6 +4668,14 @@ interface PublishEpicStructureArgs {
4370
4668
  event: EpicStructureEvent;
4371
4669
  url: string;
4372
4670
  fetchImpl?: FetchLike;
4671
+ /**
4672
+ * Optional caller-owned abort signal. A caller that bounds this call with
4673
+ * its own deadline (e.g. `factory:closeout`'s advisory re-emission, #392)
4674
+ * can abort the in-flight request itself instead of merely abandoning the
4675
+ * `await` — leaving the outbound socket alive past the caller's own
4676
+ * declared timeout.
4677
+ */
4678
+ signal?: AbortSignal;
4373
4679
  }
4374
4680
  interface PublishEpicStructureResult {
4375
4681
  duplicate: boolean;