@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.
@@ -193,8 +193,19 @@ const openJevCache = ({ directory = defaultJevCacheDirectory(process.cwd()), end
193
193
  };
194
194
  //#endregion
195
195
  //#region src/hq-access.ts
196
- const CF_ACCESS_CLIENT_ID_ENV = "CF-Access-Client-Id";
197
- const CF_ACCESS_CLIENT_SECRET_ENV = "CF-Access-Client-Secret";
196
+ /**
197
+ * The environment variables that carry the HQ Cloudflare Access service token.
198
+ * Both names are shell identifiers, so `/bin/sh` passes them through on every
199
+ * platform, dash included (#1286, #1311). These are the only names read.
200
+ */
201
+ const CF_ACCESS_CLIENT_ID_ENV = "CF_ACCESS_CLIENT_ID";
202
+ const CF_ACCESS_CLIENT_SECRET_ENV = "CF_ACCESS_CLIENT_SECRET";
203
+ /**
204
+ * The request headers Cloudflare Access reads the token from. They are the
205
+ * wire contract and do not follow the environment variable names.
206
+ */
207
+ const CF_ACCESS_CLIENT_ID_HEADER = "CF-Access-Client-Id";
208
+ const CF_ACCESS_CLIENT_SECRET_HEADER = "CF-Access-Client-Secret";
198
209
  /**
199
210
  * True in a hosted runner, where secret-manager resolution must not be
200
211
  * tried (#312). Lives here — a dependency-free leaf both
@@ -211,8 +222,8 @@ const buildCloudflareAccessRequestInit = (token, init) => ({
211
222
  ...init,
212
223
  headers: {
213
224
  ...init.headers,
214
- [CF_ACCESS_CLIENT_ID_ENV]: token.clientId,
215
- [CF_ACCESS_CLIENT_SECRET_ENV]: token.clientSecret
225
+ [CF_ACCESS_CLIENT_ID_HEADER]: token.clientId,
226
+ [CF_ACCESS_CLIENT_SECRET_HEADER]: token.clientSecret
216
227
  },
217
228
  redirect: "error"
218
229
  });
@@ -221,9 +232,11 @@ const buildCloudflareAccessRequestInit = (token, init) => ({
221
232
  /**
222
233
  * Resolving the HQ Access credentials, and saying *why* when that fails.
223
234
  *
224
- * A deliberate command (`psf hq:flush`, and doctor's remote checks in #394) is
225
- * the one place allowed to ask the secret manager for the HQ Access token. It
226
- * used to keep a value-or-nothing answer, so a missing binary, a sandbox that
235
+ * A deliberate command (`psf hq:flush`, and doctor's remote checks in #394)
236
+ * asks the secret manager for the HQ Access token, and since #1313 so does an
237
+ * HQ emission on an operator machine, once per process
238
+ * (`resolveHqCredentialsOncePerProcess`). This used to be a
239
+ * value-or-nothing answer, so a missing binary, a sandbox that
227
240
  * cannot reach the keychain, and a mistyped reference all collapsed into one
228
241
  * generic "credentials are unavailable" line. A sandboxed lane read that as
229
242
  * environmental noise while every one of its proofs stayed spooled.
@@ -247,15 +260,16 @@ const spawnFailure = (error) => {
247
260
  if (code === "EACCES" || code === "EPERM") return "resolver-blocked";
248
261
  return "resolver-timeout";
249
262
  };
250
- const probe = (binary, reference, capture) => {
263
+ const probe = (binary, reference, capture, timeoutMs) => {
251
264
  const result = spawnSync(binary, ["read", reference], {
252
265
  encoding: "utf-8",
266
+ killSignal: "SIGKILL",
253
267
  stdio: [
254
268
  "ignore",
255
269
  capture ? "pipe" : "ignore",
256
270
  "ignore"
257
271
  ],
258
- timeout: SECRET_PROBE_TIMEOUT_MS
272
+ timeout: timeoutMs
259
273
  });
260
274
  if (result.error) return { status: spawnFailure(result.error) };
261
275
  if (result.signal) return { status: "resolver-timeout" };
@@ -276,20 +290,26 @@ const probe = (binary, reference, capture) => {
276
290
  * *informative* one: a machine without `op-fast` that has `op` installed and
277
291
  * denied must report the denial, not the missing binary.
278
292
  */
279
- const resolveThrough = (reference, capture) => {
293
+ const resolveThrough = (reference, capture, timeoutMs = SECRET_PROBE_TIMEOUT_MS) => {
280
294
  let failure = "resolver-missing";
281
295
  for (const binary of SECRET_RESOLVER_BINARIES) {
282
- const outcome = probe(binary, reference, capture);
296
+ const outcome = probe(binary, reference, capture, timeoutMs);
283
297
  if (outcome.status === "resolved") return outcome;
284
298
  if (failure === "resolver-missing") failure = outcome.status;
285
299
  }
286
300
  return { status: failure };
287
301
  };
288
- const defaultSecretReferenceResolver = (reference) => resolveThrough(reference, true);
289
302
  /**
290
- * Environment first (the #312 emit-time path), then the operator's configured
291
- * references. The first reference that fails ends the resolution: a second
292
- * probe would add nothing but another chance to hang.
303
+ * The capturing resolver with its own probe ceiling. Production uses
304
+ * `defaultSecretReferenceResolver`; a test passes a short ceiling to prove the
305
+ * probe really ends there.
306
+ */
307
+ const secretReferenceResolverWithin = (timeoutMs) => (reference) => resolveThrough(reference, true, timeoutMs);
308
+ const defaultSecretReferenceResolver = secretReferenceResolverWithin(SECRET_PROBE_TIMEOUT_MS);
309
+ /**
310
+ * Environment first, then the operator's configured references. The first
311
+ * reference that fails ends the resolution: a second probe would add nothing
312
+ * but another chance to hang.
293
313
  */
294
314
  const resolveHqCredentials = ({ env, references, resolve = defaultSecretReferenceResolver }) => {
295
315
  const clientId = env[CF_ACCESS_CLIENT_ID_ENV];
@@ -354,7 +374,7 @@ const hqIngestCredentialReferencesSchema = z.object({
354
374
  const factoryUserConfigSchema = z.object({
355
375
  githubApp: githubAppConfigSchema.optional(),
356
376
  hqAllowedOrigins: z.array(hqAllowedOriginSchema).describe("Operator-controlled exact HTTPS origins authorized to receive HQ credential-bearing requests; an absent or empty list authorizes none.").optional(),
357
- hqIngestCredentials: hqIngestCredentialReferencesSchema.describe("Operator-controlled secret-manager references for the HQ ingest Cloudflare Access service token. References only — the factory never reads, prints, exports, or caches the values.").optional(),
377
+ hqIngestCredentials: hqIngestCredentialReferencesSchema.describe("Operator-controlled secret-manager references for the HQ ingest Cloudflare Access service token. References only: the factory resolves them in memory when it sends HQ evidence, and never prints, exports, or writes the values.").optional(),
358
378
  schemaVersion: z.literal(FACTORY_USER_CONFIG_SCHEMA_VERSION)
359
379
  });
360
380
  function defaultUserConfigPath(env = process.env) {
@@ -368,14 +388,19 @@ function defaultUserConfigPath(env = process.env) {
368
388
  *
369
389
  * A request that carries the token goes only to an HTTPS origin with no
370
390
  * embedded credentials that the operator listed, exactly, in
371
- * `hqAllowedOrigins` in the operator user config. Committed repository data
372
- * and the environment can name an origin; only the operator can authorize one
373
- * (ADR 0015). HQ ingest and the Jev client both call this module, so the two
374
- * cannot drift apart: one protocol rule, one reader, one parser.
391
+ * `hqAllowedOrigins` in the operator user config or in
392
+ * `FACTORY_HQ_ALLOWED_ORIGINS` in the process environment. Those two lists
393
+ * are unioned. Committed repository data can name an origin; it cannot
394
+ * authorize one (ADR 0015). The environment variable is operator
395
+ * configuration, read only from the process environment passed in, never
396
+ * from the profile or any repository file. HQ ingest and the Jev client both
397
+ * call this module, so the two cannot drift apart: one protocol rule, one
398
+ * resolver.
375
399
  *
376
- * The reader is bounded. A config path that is a FIFO, a huge file, or a disk
377
- * that hangs answers "no origin is authorized" within the caller's deadline
378
- * instead of holding a gate or a lint run open.
400
+ * The file reader is bounded. A config path that is a FIFO, a huge file, or a
401
+ * disk that hangs contributes no config origin within the caller's deadline
402
+ * instead of holding a gate or a lint run open. The environment list does not
403
+ * touch the filesystem, so it still applies when the file does not answer.
379
404
  */
380
405
  /** Largest operator config or profile file the HQ readers will open. */
381
406
  const MAX_HQ_CONFIG_BYTES = 1024 * 1024;
@@ -447,20 +472,59 @@ const hqAllowedOriginsFromConfigText = (text) => {
447
472
  return [];
448
473
  }
449
474
  };
475
+ /** Operator allowlist supplied to a machine that has no user config file. */
476
+ const FACTORY_HQ_ALLOWED_ORIGINS_ENV = "FACTORY_HQ_ALLOWED_ORIGINS";
477
+ /**
478
+ * Exact HTTPS origins from `FACTORY_HQ_ALLOWED_ORIGINS` in `env`.
479
+ * Comma-separated. A segment that is not an exact origin is ignored.
480
+ * This reads that one property and nothing else: no file, no profile.
481
+ */
482
+ const hqAllowedOriginsFromEnv = (env) => {
483
+ let raw;
484
+ try {
485
+ raw = env[FACTORY_HQ_ALLOWED_ORIGINS_ENV];
486
+ } catch {
487
+ return [];
488
+ }
489
+ if (typeof raw !== "string" || raw.trim() === "") return [];
490
+ const origins = [];
491
+ for (const segment of raw.split(",")) {
492
+ const candidate = segment.trim();
493
+ if (candidate === "") continue;
494
+ const endpoint = validatedHqOrigin(candidate);
495
+ if (endpoint !== void 0 && candidate === endpoint.origin) origins.push(endpoint.origin);
496
+ }
497
+ return origins;
498
+ };
499
+ /**
500
+ * Operator-authorized HQ origins. `configOrigins` (the user config's
501
+ * `hqAllowedOrigins`) is unioned with `FACTORY_HQ_ALLOWED_ORIGINS` from
502
+ * `env`. The environment value never replaces the config list, and neither
503
+ * list is read from the profile or any repository file.
504
+ */
505
+ const resolveHqAllowedOrigins = (env, configOrigins) => {
506
+ const fromEnv = hqAllowedOriginsFromEnv(env);
507
+ if (fromEnv.length === 0) return [...configOrigins];
508
+ const allowed = [...configOrigins];
509
+ for (const origin of fromEnv) if (!allowed.includes(origin)) allowed.push(origin);
510
+ return allowed;
511
+ };
450
512
  /**
451
513
  * The operator's `hqAllowedOrigins`, read from the operator user config within
452
- * `deadline` (a `performance.now()` time). A missing, unreadable, oversized or
453
- * invalid file, or one that does not answer in time, authorizes nothing.
514
+ * `deadline` (a `performance.now()` time), unioned with
515
+ * `FACTORY_HQ_ALLOWED_ORIGINS`. A missing, unreadable, oversized or invalid
516
+ * file, or one that does not answer in time, contributes no config origin.
517
+ * The environment list still applies.
454
518
  */
455
519
  const loadHqAllowedOrigins = async (env, deadline) => {
456
- let configPath;
520
+ let configOrigins = [];
457
521
  try {
458
- configPath = defaultUserConfigPath(env);
522
+ const contents = await readBoundedTextFile(defaultUserConfigPath(env), deadline);
523
+ if (contents !== void 0) configOrigins = hqAllowedOriginsFromConfigText(contents);
459
524
  } catch {
460
- return [];
525
+ configOrigins = [];
461
526
  }
462
- const contents = await readBoundedTextFile(configPath, deadline);
463
- return contents === void 0 ? [] : hqAllowedOriginsFromConfigText(contents);
527
+ return resolveHqAllowedOrigins(env, configOrigins);
464
528
  };
465
529
  /**
466
530
  * The matching rule: the URL's origin must equal one listed origin exactly.
@@ -481,8 +545,8 @@ const isHqOriginAuthorized = (allowedOrigins, endpoint) => allowedOrigins.includ
481
545
  * anything else happens, by the same code HQ ingest uses
482
546
  * (`hq-origin-policy.ts`, ADR 0015): it must be an `https:` origin with no
483
547
  * embedded credentials, listed exactly in the operator's `hqAllowedOrigins`.
484
- * Only then are credentials read: `resolveHqCredentials` reads the hyphenated
485
- * `CF-Access-Client-Id` / `CF-Access-Client-Secret` pair, and
548
+ * Only then are credentials read: `resolveHqCredentials` reads the
549
+ * `CF_ACCESS_CLIENT_ID` / `CF_ACCESS_CLIENT_SECRET` pair, and
486
550
  * `buildCloudflareAccessRequestInit` attaches them to a request that refuses
487
551
  * redirects so the credentials cannot leave the authorized origin.
488
552
  *
@@ -647,11 +711,8 @@ const exchange = async (input) => {
647
711
  throw new JevRequestError(0, "exchange_failed", "The HQ Jev exchange failed before a response could be read. No upstream text is reported: it can quote the request.");
648
712
  }
649
713
  };
650
- /**
651
- * The whole remedy, in one sentence. The variable names are literally
652
- * hyphenated, so a plain `export` does not set them.
653
- */
654
- const MISSING_CREDENTIALS_REASON = `no HQ credentials: ${CF_ACCESS_CLIENT_ID_ENV} and ${CF_ACCESS_CLIENT_SECRET_ENV} are not both in the environment. The names are literally hyphenated, so a plain \`export\` does not set them — prefix the command: \`env '${CF_ACCESS_CLIENT_ID_ENV}=…' '${CF_ACCESS_CLIENT_SECRET_ENV}=…' <the command> …\`.`;
714
+ /** The whole remedy, in one sentence. */
715
+ const MISSING_CREDENTIALS_REASON = `no HQ credentials: ${CF_ACCESS_CLIENT_ID_ENV} and ${CF_ACCESS_CLIENT_SECRET_ENV} are not both in the environment. Set both, or prefix the command: \`env '${CF_ACCESS_CLIENT_ID_ENV}=…' '${CF_ACCESS_CLIENT_SECRET_ENV}=…' <the command> …\`.`;
655
716
  /** Where the operator config was looked for, for a message that names it. */
656
717
  const operatorConfigLocation = (env) => {
657
718
  try {
@@ -1,4 +1,4 @@
1
- import { i as buildJevRequests, l as noul, n as spawnJevBridge, o as defaultJevCacheDirectory } from "./bridge-B4Sx95ZO.js";
1
+ import { i as buildJevRequests, l as noul, n as spawnJevBridge, o as defaultJevCacheDirectory } from "./bridge-PMzq2gZh.js";
2
2
  import path from "node:path";
3
3
  import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
4
4
  import { createHash } from "node:crypto";
@@ -1,4 +1,4 @@
1
- import { a as resolveJevGateway, r as runJevFiles, s as openJevCache, t as allUnavailable } from "./bridge-B4Sx95ZO.js";
1
+ import { a as resolveJevGateway, r as runJevFiles, s as openJevCache, t as allUnavailable } from "./bridge-PMzq2gZh.js";
2
2
  import { readFileSync } from "node:fs";
3
3
  //#region src/oxlint-jev/worker.ts
4
4
  /**
package/dist/schemas.d.ts CHANGED
@@ -1000,8 +1000,8 @@ declare const closeoutArtifactSchema: z.ZodObject<{
1000
1000
  }, z.core.$strict>;
1001
1001
  unit: z.ZodEnum<{
1002
1002
  boolean: "boolean";
1003
- count: "count";
1004
1003
  text: "text";
1004
+ count: "count";
1005
1005
  tokens: "tokens";
1006
1006
  usd: "usd";
1007
1007
  }>;
@@ -1012,6 +1012,28 @@ declare const closeoutArtifactSchema: z.ZodObject<{
1012
1012
  }, z.core.$strict>;
1013
1013
  type CloseoutArtifact = z.infer<typeof closeoutArtifactSchema>;
1014
1014
  //#endregion
1015
+ //#region src/gate-timing-record.d.ts
1016
+ declare const gateTimingRecordSchema: z.ZodObject<{
1017
+ agentRunId: z.ZodString;
1018
+ cycle: z.ZodNumber;
1019
+ durationMs: z.ZodNumber;
1020
+ gate: z.ZodEnum<{
1021
+ "boundary:check": "boundary:check";
1022
+ "factory:closeout": "factory:closeout";
1023
+ "pr:ready": "pr:ready";
1024
+ "pr:review": "pr:review";
1025
+ "pr:verify": "pr:verify";
1026
+ }>;
1027
+ outcome: z.ZodEnum<{
1028
+ pass: "pass";
1029
+ fail: "fail";
1030
+ }>;
1031
+ schemaVersion: z.ZodLiteral<1>;
1032
+ startOffsetMs: z.ZodNumber;
1033
+ startedAt: z.ZodISODateTime;
1034
+ }, z.core.$strict>;
1035
+ type GateTimingRecord = z.infer<typeof gateTimingRecordSchema>;
1036
+ //#endregion
1015
1037
  //#region src/schemas.d.ts
1016
1038
  declare const EVIDENCE_ENVELOPE_SCHEMA_VERSION = 1;
1017
1039
  declare const evidenceEnvelopeSchema: z.ZodObject<{
@@ -1408,4 +1430,4 @@ declare const prReadyProofSchema: z.ZodObject<{
1408
1430
  waivedDemands: z.ZodOptional<z.ZodArray<z.ZodType<WaivedDemand, unknown, z.core.$ZodTypeInternals<WaivedDemand, unknown>>>>;
1409
1431
  }, z.core.$strip>;
1410
1432
  //#endregion
1411
- export { BLOCKED_REASONS_MAX, BLOCKED_REASON_DETAIL_MAX, BOUNDARY_CHECK_SCHEMA_VERSION, BOUNDARY_REVIEW_PROOF_KIND, type BlockedReason, BoundaryCheckProofRecord, type BoundaryReviewProof, type CloseoutArtifact, EVIDENCE_ENVELOPE_SCHEMA_VERSION, type EvidenceEnvelope, PR_READY_SCHEMA_VERSION, PR_REVIEW_SCHEMA_VERSION, type PrReadyProof, type PrReviewProof, type PrVerifyProof, type ReviewPromptSection, type ReviewPromptSectionProvenance, SUPPORTED_CLOSEOUT_SCHEMA_VERSIONS, SUPPORTED_PR_READY_SCHEMA_VERSIONS, SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS, type WaivedDemand, blockedReasonSchema, blockedReasonsSchema, boundaryCheckProofSchema, boundaryReviewProofSchema, closeoutArtifactSchema, evidenceEnvelopeSchema, prReadyProofSchema, prReviewFindingSchema, prReviewProofSchema, prReviewResultSchema, prVerifyProofSchema, reviewPromptSectionSchema, reviewPromptSectionsSchema, waivedDemandSchema };
1433
+ export { BLOCKED_REASONS_MAX, BLOCKED_REASON_DETAIL_MAX, BOUNDARY_CHECK_SCHEMA_VERSION, BOUNDARY_REVIEW_PROOF_KIND, type BlockedReason, BoundaryCheckProofRecord, type BoundaryReviewProof, type CloseoutArtifact, EVIDENCE_ENVELOPE_SCHEMA_VERSION, type EvidenceEnvelope, type GateTimingRecord, PR_READY_SCHEMA_VERSION, PR_REVIEW_SCHEMA_VERSION, type PrReadyProof, type PrReviewProof, type PrVerifyProof, type ReviewPromptSection, type ReviewPromptSectionProvenance, SUPPORTED_CLOSEOUT_SCHEMA_VERSIONS, SUPPORTED_PR_READY_SCHEMA_VERSIONS, SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS, type WaivedDemand, blockedReasonSchema, blockedReasonsSchema, boundaryCheckProofSchema, boundaryReviewProofSchema, closeoutArtifactSchema, evidenceEnvelopeSchema, gateTimingRecordSchema, prReadyProofSchema, prReviewFindingSchema, prReviewProofSchema, prReviewResultSchema, prVerifyProofSchema, reviewPromptSectionSchema, reviewPromptSectionsSchema, waivedDemandSchema };
package/dist/schemas.js CHANGED
@@ -1284,6 +1284,31 @@ const closeoutArtifactSchema = z.object({
1284
1284
  repo: RepoSchema,
1285
1285
  schemaVersion: z.literal(5)
1286
1286
  }).strict();
1287
+ /** The self-timed gate commands. */
1288
+ const FACTORY_GATE_NAMES = [
1289
+ "boundary:check",
1290
+ "factory:closeout",
1291
+ "pr:ready",
1292
+ "pr:review",
1293
+ "pr:verify"
1294
+ ];
1295
+ const nonNegativeInteger = z.number().int().nonnegative();
1296
+ const gateTimingRecordSchema = z.object({
1297
+ /** The lane run identity the ledger is keyed by. */
1298
+ agentRunId: z.string().min(1),
1299
+ cycle: z.number().int().positive(),
1300
+ durationMs: nonNegativeInteger,
1301
+ gate: z.enum(FACTORY_GATE_NAMES),
1302
+ outcome: z.enum(["fail", "pass"]),
1303
+ schemaVersion: z.literal(1),
1304
+ /**
1305
+ * Milliseconds from the lane run's origin (the first record's start) to
1306
+ * this record's start. The first record is offset 0.
1307
+ */
1308
+ startOffsetMs: nonNegativeInteger,
1309
+ /** Wall-clock start of this gate invocation (ISO 8601). */
1310
+ startedAt: z.iso.datetime()
1311
+ }).strict();
1287
1312
  //#endregion
1288
1313
  //#region src/schemas.ts
1289
1314
  const evidenceCheckTypeSchema = z.enum(EVIDENCE_CHECK_TYPES);
@@ -1626,4 +1651,4 @@ const prReadyProofSchema = z.object({
1626
1651
  for (const issue of blockedReasonIssues({ ...proof })) context.addIssue(issue);
1627
1652
  });
1628
1653
  //#endregion
1629
- export { BLOCKED_REASONS_MAX, BLOCKED_REASON_DETAIL_MAX, BOUNDARY_CHECK_SCHEMA_VERSION, BOUNDARY_REVIEW_PROOF_KIND, EVIDENCE_ENVELOPE_SCHEMA_VERSION, PR_READY_SCHEMA_VERSION, PR_REVIEW_SCHEMA_VERSION, SUPPORTED_CLOSEOUT_SCHEMA_VERSIONS, SUPPORTED_PR_READY_SCHEMA_VERSIONS, SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS, blockedReasonSchema, blockedReasonsSchema, boundaryCheckProofSchema, boundaryReviewProofSchema, closeoutArtifactSchema, evidenceEnvelopeSchema, prReadyProofSchema, prReviewFindingSchema, prReviewProofSchema, prReviewResultSchema, prVerifyProofSchema, reviewPromptSectionSchema, reviewPromptSectionsSchema, waivedDemandSchema };
1654
+ export { BLOCKED_REASONS_MAX, BLOCKED_REASON_DETAIL_MAX, BOUNDARY_CHECK_SCHEMA_VERSION, BOUNDARY_REVIEW_PROOF_KIND, EVIDENCE_ENVELOPE_SCHEMA_VERSION, PR_READY_SCHEMA_VERSION, PR_REVIEW_SCHEMA_VERSION, SUPPORTED_CLOSEOUT_SCHEMA_VERSIONS, SUPPORTED_PR_READY_SCHEMA_VERSIONS, SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS, blockedReasonSchema, blockedReasonsSchema, boundaryCheckProofSchema, boundaryReviewProofSchema, closeoutArtifactSchema, evidenceEnvelopeSchema, gateTimingRecordSchema, prReadyProofSchema, prReviewFindingSchema, prReviewProofSchema, prReviewResultSchema, prVerifyProofSchema, reviewPromptSectionSchema, reviewPromptSectionsSchema, waivedDemandSchema };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patronage/software-factory",
3
- "version": "1.0.0-alpha.37",
3
+ "version": "1.0.0-alpha.39",
4
4
  "description": "Shared Patronage software factory CLI and project-profile validation tools",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -42,7 +42,7 @@
42
42
  "picomatch": "^4.0.5",
43
43
  "yaml": "^2.9.0",
44
44
  "zod": "4.4.3",
45
- "@patronage/factory-ci": "1.0.0-alpha.37"
45
+ "@patronage/factory-ci": "1.0.0-alpha.39"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@oxlint/plugins": "1.76.0",