@patronage/software-factory 1.0.0-alpha.38 → 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/dist/index.d.ts +97 -0
- package/dist/index.js +446 -9
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -22,6 +22,11 @@ declare const githubAppConfigSchema: z.ZodObject<{
|
|
|
22
22
|
privateKeyPath: z.ZodString;
|
|
23
23
|
}, z.core.$strip>;
|
|
24
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>;
|
|
25
30
|
declare const factoryUserConfigSchema: z.ZodObject<{
|
|
26
31
|
githubApp: z.ZodOptional<z.ZodObject<{
|
|
27
32
|
appId: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
|
|
@@ -2226,6 +2231,8 @@ declare const isSupersededHostedVerifyCheck: (check: StatusCheckRollup, cutoffIs
|
|
|
2226
2231
|
* `gh` is injectable so a test can hand the real read a fake GitHub.
|
|
2227
2232
|
*/
|
|
2228
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>;
|
|
2229
2236
|
//#endregion
|
|
2230
2237
|
//#region src/github-app-client.d.ts
|
|
2231
2238
|
interface FactoryAppRequest {
|
|
@@ -4939,6 +4946,92 @@ interface HqReadinessCheckDependencies {
|
|
|
4939
4946
|
timeoutMs?: number;
|
|
4940
4947
|
}
|
|
4941
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
|
|
4942
5035
|
//#region src/doctor-machine-checks.d.ts
|
|
4943
5036
|
/** Test seams. Production never sets these. */
|
|
4944
5037
|
interface MachineCheckDependencies {
|
|
@@ -4969,10 +5062,14 @@ interface DoctorProjectProfileInput extends LoadProjectProfileInput {
|
|
|
4969
5062
|
/** Diff base for the admission preflight; ignored unless `preflight`. */
|
|
4970
5063
|
base?: string;
|
|
4971
5064
|
env?: NodeJS.ProcessEnv;
|
|
5065
|
+
/** Test seam for the hq-last-stored row (#1342); production never sets this. */
|
|
5066
|
+
hqLastStoredDependencies?: HqLastStoredCheckDependencies;
|
|
4972
5067
|
/** Test seam for the HQ readiness rows (#1315); production never sets this. */
|
|
4973
5068
|
hqReadinessDependencies?: HqReadinessCheckDependencies;
|
|
4974
5069
|
/** Test seam for the local HQ spool check (#394); production never sets this. */
|
|
4975
5070
|
hqSpoolDependencies?: HqSpoolCheckDependencies;
|
|
5071
|
+
/** Test seam for the hq-webhook row (#1341); production never sets this. */
|
|
5072
|
+
hqWebhookDependencies?: HqWebhookCheckDependencies;
|
|
4976
5073
|
/** Test seam for the machine readiness rows (#1315); production never sets this. */
|
|
4977
5074
|
machineDependencies?: MachineCheckDependencies;
|
|
4978
5075
|
/**
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { t as __exportAll } from "./chunk-DrSxFLj_.js";
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { Command, InvalidArgumentError } from "commander";
|
|
5
|
-
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
5
|
+
import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
|
|
6
6
|
import { EventEmitter, once } from "node:events";
|
|
7
7
|
import * as nodeFs from "node:fs";
|
|
8
8
|
import { accessSync, appendFileSync, closeSync, constants, cpSync, existsSync, fsyncSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
@@ -24,7 +24,7 @@ import { pathToFileURL } from "node:url";
|
|
|
24
24
|
import { choice, noul, score } from "@typesafe-ai/sdk";
|
|
25
25
|
//#region package.json
|
|
26
26
|
var name = "@patronage/software-factory";
|
|
27
|
-
var version = "1.0.0-alpha.
|
|
27
|
+
var version = "1.0.0-alpha.39";
|
|
28
28
|
//#endregion
|
|
29
29
|
//#region src/cli-entry.ts
|
|
30
30
|
/**
|
|
@@ -4018,6 +4018,35 @@ const fetchGitHubFileAtRef = (repository, filePath, ref, cwd) => {
|
|
|
4018
4018
|
const response = repositoryFileSchema.parse(runGhJsonAt(["api", `/repos/${repository.owner}/${repository.name}/contents/${encodedPath}?ref=${encodeURIComponent(ref)}`], cwd));
|
|
4019
4019
|
return Buffer$1.from(response.content, "base64").toString("utf-8");
|
|
4020
4020
|
};
|
|
4021
|
+
/** Ceiling for one bounded `gh` read. Expiry kills gh and rejects. */
|
|
4022
|
+
const GITHUB_READ_TIMEOUT_MS = 1e4;
|
|
4023
|
+
/**
|
|
4024
|
+
* `runGhJsonAt` with a deadline, for a diagnostic that must not hang (#1341).
|
|
4025
|
+
* A failure rejects with the `execFile` error: its `stderr` carries gh's
|
|
4026
|
+
* `(HTTP nnn)`, and `killed` marks the deadline.
|
|
4027
|
+
*/
|
|
4028
|
+
const runGhJsonBounded = async (args, cwd) => {
|
|
4029
|
+
const { stdout } = await promisify(execFile)("gh", args, {
|
|
4030
|
+
cwd,
|
|
4031
|
+
encoding: "utf-8",
|
|
4032
|
+
timeout: GITHUB_READ_TIMEOUT_MS
|
|
4033
|
+
});
|
|
4034
|
+
return JSON.parse(stdout);
|
|
4035
|
+
};
|
|
4036
|
+
const repositoryHooksSchema = z.array(z.object({
|
|
4037
|
+
active: z.boolean(),
|
|
4038
|
+
config: z.object({ url: z.string().optional() }),
|
|
4039
|
+
id: z.number()
|
|
4040
|
+
}));
|
|
4041
|
+
/**
|
|
4042
|
+
* A repository's webhooks (`GET /repos/{owner}/{repo}/hooks`). This needs
|
|
4043
|
+
* admin read: a token without it gets 403 or 404, never an empty list. Rejects
|
|
4044
|
+
* on every failure; the caller decides what an unreadable list means.
|
|
4045
|
+
*/
|
|
4046
|
+
const fetchRepositoryHooks = async (repository, cwd, gh = runGhJsonBounded) => repositoryHooksSchema.parse(await gh(["api", `/repos/${repository.owner}/${repository.name}/hooks`], cwd));
|
|
4047
|
+
const hookDeliveriesSchema = z.array(z.object({ status_code: z.number() }));
|
|
4048
|
+
/** A webhook's last 30 deliveries, newest first. Rejects like `fetchRepositoryHooks`. */
|
|
4049
|
+
const fetchHookDeliveries = async (repository, hookId, cwd, gh = runGhJsonBounded) => hookDeliveriesSchema.parse(await gh(["api", `/repos/${repository.owner}/${repository.name}/hooks/${hookId}/deliveries?per_page=30`], cwd));
|
|
4021
4050
|
/**
|
|
4022
4051
|
* Fetch and normalize the issue/PR comment thread for `<owner>/<repo>`.
|
|
4023
4052
|
*
|
|
@@ -5296,6 +5325,15 @@ const loadHqAllowedOrigins = async (env, deadline) => {
|
|
|
5296
5325
|
* No prefix, wildcard, or host-only match.
|
|
5297
5326
|
*/
|
|
5298
5327
|
const isHqOriginAuthorized = (allowedOrigins, endpoint) => allowedOrigins.includes(endpoint.origin);
|
|
5328
|
+
//#endregion
|
|
5329
|
+
//#region src/hq-user-agent.ts
|
|
5330
|
+
/**
|
|
5331
|
+
* The `User-Agent` every HQ ingest sink request carries (#1343). Without it
|
|
5332
|
+
* the sink sends fetch's default, `node`. HQ stores the header in
|
|
5333
|
+
* `ingest_events.sender_agent` (#1337), so a look-back can split events by the
|
|
5334
|
+
* factory version that sent them.
|
|
5335
|
+
*/
|
|
5336
|
+
const HQ_SINK_USER_AGENT = `patronage-software-factory/${version} (${process.platform}; node/${process.versions.node})`;
|
|
5299
5337
|
const DEFAULT_HQ_TRANSPORT_TIMEOUT_MS = 2500;
|
|
5300
5338
|
const HQ_RETRY_SPOOL_DIRNAME = "hq-retry-spool";
|
|
5301
5339
|
/**
|
|
@@ -6048,7 +6086,10 @@ async function attemptTransport(request, endpoint, clientId, clientSecret, body,
|
|
|
6048
6086
|
clientSecret
|
|
6049
6087
|
}, {
|
|
6050
6088
|
body,
|
|
6051
|
-
headers: {
|
|
6089
|
+
headers: {
|
|
6090
|
+
"content-type": "application/json",
|
|
6091
|
+
"user-agent": HQ_SINK_USER_AGENT
|
|
6092
|
+
},
|
|
6052
6093
|
method: "POST",
|
|
6053
6094
|
signal: abort.signal
|
|
6054
6095
|
}))), transportBudgetMs);
|
|
@@ -14923,7 +14964,7 @@ const BASELINE_BLINDSPOT = {
|
|
|
14923
14964
|
};
|
|
14924
14965
|
const NO_CROSS_FAMILY_TOTAL_BLINDSPOT = {
|
|
14925
14966
|
id: "no-cross-family-token-total",
|
|
14926
|
-
note: "Token rows are per model family. Claude and GPT use different tokenizers and accounting conventions, so this closeout reports no combined token total. USD is the only cross-family summable unit, and pricing policy stays with the patronage-software-factory-
|
|
14967
|
+
note: "Token rows are per model family. Claude and GPT use different tokenizers and accounting conventions, so this closeout reports no combined token total. USD is the only cross-family summable unit, and pricing policy stays with the patronage-software-factory-lookback skill."
|
|
14927
14968
|
};
|
|
14928
14969
|
const assertMetricSources = (metrics) => {
|
|
14929
14970
|
for (const metric of metrics) if (!metric.source.detail.trim()) throw new CloseoutValidationError(`metric ${metric.metric} is missing source.detail`);
|
|
@@ -14983,7 +15024,7 @@ function buildEpicCloseoutReport(input) {
|
|
|
14983
15024
|
* `factory:closeout` reads the lane's native at-rest provider logs by its
|
|
14984
15025
|
* session join keys and records what they say as closeout metric rows. The
|
|
14985
15026
|
* rows land in the one local closeout markdown, which is the read path the
|
|
14986
|
-
* `patronage-software-factory-
|
|
15027
|
+
* `patronage-software-factory-lookback` skill uses.
|
|
14987
15028
|
*
|
|
14988
15029
|
* Harvest NEVER throws: a missing or unreadable log degrades to an absent
|
|
14989
15030
|
* family plus a data gap, so telemetry content can never fail the closeout.
|
|
@@ -16150,7 +16191,7 @@ const notProbed = (reason) => ({
|
|
|
16150
16191
|
status: "warning"
|
|
16151
16192
|
});
|
|
16152
16193
|
/** A code or an error name. A transport message is never rendered. */
|
|
16153
|
-
const networkFailure = (error) => {
|
|
16194
|
+
const networkFailure$1 = (error) => {
|
|
16154
16195
|
if (!(error instanceof Error)) return "the request failed";
|
|
16155
16196
|
if (error.name === "TimeoutError" || error.name === "AbortError") return "it did not answer in time";
|
|
16156
16197
|
return `the request failed (${error.cause?.code ?? error.name})`;
|
|
@@ -16167,7 +16208,7 @@ const reachableCheck = async (ingest, credentials, dependencies) => {
|
|
|
16167
16208
|
}));
|
|
16168
16209
|
} catch (error) {
|
|
16169
16210
|
return {
|
|
16170
|
-
message: `HQ at ${ingest.href} is unreachable from this machine: ${networkFailure(error)}. Fix: check this machine's network path to ${ingest.origin}, then rerun doctor.`,
|
|
16211
|
+
message: `HQ at ${ingest.href} is unreachable from this machine: ${networkFailure$1(error)}. Fix: check this machine's network path to ${ingest.origin}, then rerun doctor.`,
|
|
16171
16212
|
name: HQ_REACHABLE_CHECK,
|
|
16172
16213
|
status: "error"
|
|
16173
16214
|
};
|
|
@@ -16227,6 +16268,383 @@ async function hqReadinessDoctorChecks(input, dependencies = {}) {
|
|
|
16227
16268
|
];
|
|
16228
16269
|
}
|
|
16229
16270
|
//#endregion
|
|
16271
|
+
//#region src/hq-last-stored.ts
|
|
16272
|
+
/**
|
|
16273
|
+
* The HQ client for `GET /api/ingest/last-stored` (#1339): when HQ last stored
|
|
16274
|
+
* an event for a repository. Doctor's `hq-webhook` row asks it when GitHub
|
|
16275
|
+
* will not show a repository's deliveries (#1341).
|
|
16276
|
+
*
|
|
16277
|
+
* Whatever HQ cannot answer comes back as an explicit `unknown` with its
|
|
16278
|
+
* reason, never a guess. The commonest today is `no-endpoint`: an HQ deployed
|
|
16279
|
+
* before #1339 answers the route with 404.
|
|
16280
|
+
*
|
|
16281
|
+
* The token goes only to an origin the operator listed in `hqAllowedOrigins`
|
|
16282
|
+
* or `FACTORY_HQ_ALLOWED_ORIGINS` (ADR 0015, `resolveHqAllowedOrigins`, the
|
|
16283
|
+
* rule doctor's `hq-origin` row applies), checked here so no caller can skip
|
|
16284
|
+
* it. Credentials resolve the way `hq:flush` resolves them, environment first
|
|
16285
|
+
* and then the operator's references, and are held only long enough to send
|
|
16286
|
+
* the request.
|
|
16287
|
+
*/
|
|
16288
|
+
/** HQ's route. `software-factory-hq/src/ingest-config.ts` owns it as `INGEST_LAST_STORED_PATH`. */
|
|
16289
|
+
const HQ_LAST_STORED_PATH = "/api/ingest/last-stored";
|
|
16290
|
+
/** Ceiling for the request, reply included. Expiry reads as unreachable. */
|
|
16291
|
+
const HQ_LAST_STORED_TIMEOUT_MS = 1e4;
|
|
16292
|
+
const storedTimeSchema = z.object({ receivedAt: z.string() });
|
|
16293
|
+
const lastFromCallerSchema = z.object({
|
|
16294
|
+
anyRepository: storedTimeSchema.nullable(),
|
|
16295
|
+
inRepository: storedTimeSchema.nullable(),
|
|
16296
|
+
machine: z.string()
|
|
16297
|
+
}).nullable();
|
|
16298
|
+
const lastStoredSchema = z.discriminatedUnion("found", [z.object({
|
|
16299
|
+
found: z.literal(true),
|
|
16300
|
+
lastFromCaller: lastFromCallerSchema,
|
|
16301
|
+
lastProofEvent: storedTimeSchema.extend({
|
|
16302
|
+
kind: z.string(),
|
|
16303
|
+
observedAt: z.string()
|
|
16304
|
+
}).nullable(),
|
|
16305
|
+
lastWebhookEvent: storedTimeSchema.extend({ event: z.string() }).nullable(),
|
|
16306
|
+
repository: z.string()
|
|
16307
|
+
}), z.object({
|
|
16308
|
+
found: z.literal(false),
|
|
16309
|
+
lastFromCaller: lastFromCallerSchema,
|
|
16310
|
+
repository: z.string()
|
|
16311
|
+
})]);
|
|
16312
|
+
const unknown = (reason, detail) => ({
|
|
16313
|
+
detail,
|
|
16314
|
+
reason,
|
|
16315
|
+
status: "unknown"
|
|
16316
|
+
});
|
|
16317
|
+
/** A code or an error name. A transport message is never rendered. */
|
|
16318
|
+
const networkFailure = (error) => {
|
|
16319
|
+
if (!(error instanceof Error)) return "the request failed";
|
|
16320
|
+
if (error.name === "TimeoutError" || error.name === "AbortError") return "it did not answer in time";
|
|
16321
|
+
return `the request failed (${error.cause?.code ?? error.name})`;
|
|
16322
|
+
};
|
|
16323
|
+
/** Asks HQ when it last stored an event for `repository`. Never throws. */
|
|
16324
|
+
async function fetchHqLastStored(input, dependencies = {}) {
|
|
16325
|
+
const origin = validatedHqOrigin(input.endpoint);
|
|
16326
|
+
if (origin === void 0 || !isHqOriginAuthorized(resolveHqAllowedOrigins(input.env, input.hqAllowedOrigins ?? []), origin)) return unknown("origin-not-authorized", `HQ was not asked: ${input.endpoint} is not listed in hqAllowedOrigins in the operator user config or in FACTORY_HQ_ALLOWED_ORIGINS.`);
|
|
16327
|
+
const resolution = resolveHqCredentials({
|
|
16328
|
+
env: input.env,
|
|
16329
|
+
...input.hqIngestCredentials === void 0 ? {} : { references: input.hqIngestCredentials },
|
|
16330
|
+
...dependencies.resolveSecret === void 0 ? {} : { resolve: dependencies.resolveSecret }
|
|
16331
|
+
});
|
|
16332
|
+
if (resolution.status !== "resolved") return unknown("no-credentials", describeHqCredentialFailure(resolution));
|
|
16333
|
+
const url = new URL(HQ_LAST_STORED_PATH, origin.origin);
|
|
16334
|
+
url.searchParams.set("repository", `${input.repository.owner}/${input.repository.repo}`);
|
|
16335
|
+
const request = dependencies.fetch ?? fetch;
|
|
16336
|
+
let response;
|
|
16337
|
+
try {
|
|
16338
|
+
response = await request(url, buildCloudflareAccessRequestInit(resolution.credentials, {
|
|
16339
|
+
method: "GET",
|
|
16340
|
+
signal: AbortSignal.timeout(dependencies.timeoutMs ?? HQ_LAST_STORED_TIMEOUT_MS)
|
|
16341
|
+
}));
|
|
16342
|
+
} catch (error) {
|
|
16343
|
+
return unknown("unreachable", `HQ at ${origin.origin} is unreachable: ${networkFailure(error)}.`);
|
|
16344
|
+
}
|
|
16345
|
+
if (response.status !== 200) {
|
|
16346
|
+
try {
|
|
16347
|
+
await response.body?.cancel();
|
|
16348
|
+
} catch {}
|
|
16349
|
+
return response.status === 404 ? unknown("no-endpoint", `HQ at ${origin.origin} answered 404: it was deployed before GET ${HQ_LAST_STORED_PATH} (#1339).`) : unknown("unexpected-response", `HQ at ${origin.origin} answered ${response.status}.`);
|
|
16350
|
+
}
|
|
16351
|
+
try {
|
|
16352
|
+
return {
|
|
16353
|
+
answer: lastStoredSchema.parse(await response.json()),
|
|
16354
|
+
status: "answered"
|
|
16355
|
+
};
|
|
16356
|
+
} catch {
|
|
16357
|
+
return unknown("unexpected-response", `HQ at ${origin.origin} answered 200 with a reply that is not a last-stored answer.`);
|
|
16358
|
+
}
|
|
16359
|
+
}
|
|
16360
|
+
//#endregion
|
|
16361
|
+
//#region src/doctor-hq-last-stored.ts
|
|
16362
|
+
/**
|
|
16363
|
+
* The `hq-last-stored` doctor row (#1342): the silent-spool alarm. A machine
|
|
16364
|
+
* whose gates stop reaching HQ looks, from HQ, like a machine that stopped
|
|
16365
|
+
* working; this row says so from the machine's side.
|
|
16366
|
+
*
|
|
16367
|
+
* It asks HQ when it last stored an event for this repository (#1339) and
|
|
16368
|
+
* prints one line: from this machine, from anyone in this repository, and the
|
|
16369
|
+
* last webhook. It warns when this checkout's pr:verify proof is more than an
|
|
16370
|
+
* hour newer than HQ's last event from this machine here, or when this
|
|
16371
|
+
* repository's spool holds events or cannot be inspected. The spool is read
|
|
16372
|
+
* with `countHqSpoolWork`, the reader the `hq-spool` row and `hq:flush` use.
|
|
16373
|
+
*
|
|
16374
|
+
* Whatever HQ cannot answer (an HQ older than the endpoint, no credentials, a
|
|
16375
|
+
* token HQ does not map to a machine) reads "unknown", never a warning. A
|
|
16376
|
+
* warning is still never an error: HQ is advisory (ADR 0015, #1228), so
|
|
16377
|
+
* doctor's exit code is unchanged. Doctor never flushes; it points at
|
|
16378
|
+
* `psf hq:flush`.
|
|
16379
|
+
*/
|
|
16380
|
+
const HQ_LAST_STORED_CHECK = "hq-last-stored";
|
|
16381
|
+
/** How much newer a local proof may be than HQ's copy before the row warns. */
|
|
16382
|
+
const BEHIND_TOLERANCE_MS = 36e5;
|
|
16383
|
+
const MINUTE_MS = 6e4;
|
|
16384
|
+
/** Where pr:verify writes its proof, relative to its cwd (`DEFAULT_PR_VERIFY_PROOF_PATH`). */
|
|
16385
|
+
const PR_VERIFY_PROOF_PATH = path.join(".factory-memory", "pr-verify.json");
|
|
16386
|
+
/**
|
|
16387
|
+
* How long after stamping `endedAt` a pr:verify run writes its own proof, at
|
|
16388
|
+
* most, and how far a coarse filesystem clock may put the write before it.
|
|
16389
|
+
*/
|
|
16390
|
+
const PROOF_WRITE_WINDOW_MS = 6e4;
|
|
16391
|
+
const MTIME_GRANULARITY_MS = 2e3;
|
|
16392
|
+
/**
|
|
16393
|
+
* This checkout's pr:verify proof, and whether a pr:verify run here wrote it.
|
|
16394
|
+
* Only such a run handed the proof to the HQ sink, at `endedAt`.
|
|
16395
|
+
*
|
|
16396
|
+
* `pr:verify --reuse-accepted` writes another run's proof to the same path,
|
|
16397
|
+
* byte for byte, with its producer's provenance and time, and sends HQ
|
|
16398
|
+
* nothing. It is the one such writer: a proof never travels between heads or
|
|
16399
|
+
* stores (ADR 0038). So a proof counts only when it names this profile's
|
|
16400
|
+
* path and repository, the identity pr:verify itself uses to call a proof
|
|
16401
|
+
* this profile's, and when the file was written as its run ended. Anything
|
|
16402
|
+
* else is unattributed: whether HQ is behind is then unknown, never a
|
|
16403
|
+
* warning. A missing or unreadable proof is no proof.
|
|
16404
|
+
*/
|
|
16405
|
+
const readLocalProof = (input) => {
|
|
16406
|
+
const proofPath = path.join(input.cwd, PR_VERIFY_PROOF_PATH);
|
|
16407
|
+
let proof;
|
|
16408
|
+
let writtenAtMs;
|
|
16409
|
+
try {
|
|
16410
|
+
proof = validatePrVerifyProof(JSON.parse(readFileSync(proofPath, "utf-8")));
|
|
16411
|
+
writtenAtMs = statSync(proofPath).mtimeMs;
|
|
16412
|
+
} catch {
|
|
16413
|
+
return { status: "none" };
|
|
16414
|
+
}
|
|
16415
|
+
const { name, owner } = input.profile.repository;
|
|
16416
|
+
const writeLag = writtenAtMs - Date.parse(proof.endedAt);
|
|
16417
|
+
return proof.profilePath === input.profilePath && proof.repository === `${owner}/${name}` && writeLag >= -MTIME_GRANULARITY_MS && writeLag <= PROOF_WRITE_WINDOW_MS ? {
|
|
16418
|
+
endedAt: proof.endedAt,
|
|
16419
|
+
status: "local"
|
|
16420
|
+
} : { status: "unattributed" };
|
|
16421
|
+
};
|
|
16422
|
+
const ago = (at, now) => {
|
|
16423
|
+
const minutes = Math.floor((now.getTime() - Date.parse(at)) / MINUTE_MS);
|
|
16424
|
+
if (!Number.isFinite(minutes)) return `at ${at}`;
|
|
16425
|
+
if (minutes < 60) return `${Math.max(minutes, 0)} min ago`;
|
|
16426
|
+
const hours = Math.floor(minutes / 60);
|
|
16427
|
+
return hours < 24 ? `${hours} h ago` : `${Math.floor(hours / 24)} d ago`;
|
|
16428
|
+
};
|
|
16429
|
+
const summary$1 = (answer, now) => {
|
|
16430
|
+
const caller = answer.lastFromCaller;
|
|
16431
|
+
let machine = "unknown sender";
|
|
16432
|
+
if (caller?.inRepository) machine = ago(caller.inRepository.receivedAt, now);
|
|
16433
|
+
else if (caller?.anyRepository) machine = `never here (elsewhere ${ago(caller.anyRepository.receivedAt, now)})`;
|
|
16434
|
+
else if (caller) machine = "never";
|
|
16435
|
+
const proof = answer.found ? answer.lastProofEvent : null;
|
|
16436
|
+
const webhook = answer.found ? answer.lastWebhookEvent : null;
|
|
16437
|
+
return [
|
|
16438
|
+
`HQ last stored: from this machine ${machine}`,
|
|
16439
|
+
`from this repository ${proof ? ago(proof.receivedAt, now) : "never"}`,
|
|
16440
|
+
`webhook ${webhook ? ago(webhook.receivedAt, now) : "never"}.`
|
|
16441
|
+
].join(" · ");
|
|
16442
|
+
};
|
|
16443
|
+
/** What this checkout's proof says about HQ's copy, when it says anything. */
|
|
16444
|
+
const comparison = (answer, local) => {
|
|
16445
|
+
const caller = answer.lastFromCaller;
|
|
16446
|
+
if (caller === null || local.status === "none") return;
|
|
16447
|
+
if (local.status === "unattributed") return {
|
|
16448
|
+
behind: false,
|
|
16449
|
+
note: "This checkout's pr:verify proof was not written here by pr:verify for this repository (a reused proof keeps its producer's time), so whether HQ is behind this machine is unknown."
|
|
16450
|
+
};
|
|
16451
|
+
if (caller.inRepository === null) return {
|
|
16452
|
+
behind: true,
|
|
16453
|
+
note: `This checkout's pr:verify proof ended at ${local.endedAt}, and HQ has stored nothing from this machine in this repository.`
|
|
16454
|
+
};
|
|
16455
|
+
return Date.parse(local.endedAt) - Date.parse(caller.inRepository.receivedAt) > BEHIND_TOLERANCE_MS ? {
|
|
16456
|
+
behind: true,
|
|
16457
|
+
note: `This checkout's pr:verify proof ended at ${local.endedAt}, more than 1 h after HQ last stored an event from this machine here.`
|
|
16458
|
+
} : void 0;
|
|
16459
|
+
};
|
|
16460
|
+
/**
|
|
16461
|
+
* The spool as the reason HQ is behind; the hq-spool row has the detail. The
|
|
16462
|
+
* spool is called empty only when it was fully inspected.
|
|
16463
|
+
*/
|
|
16464
|
+
const spoolNotes = (spool, behind) => {
|
|
16465
|
+
const notes = [...spool.pending > 0 ? [`HQ is behind by the ${spool.pending} event(s) the hq-spool row reports; \`psf hq:flush\` sends them.`] : [], ...spool.unlistable > 0 ? [`${spool.unlistable} spool location(s) could not be inspected, so the events waiting there are unknown; the hq-spool row names them.`] : []];
|
|
16466
|
+
return notes.length === 0 && behind ? ["Nothing is spooled here, so `psf hq:flush` has nothing to send."] : notes;
|
|
16467
|
+
};
|
|
16468
|
+
const unknownLine = (lastStored) => lastStored.reason === "no-credentials" ? `HQ last stored: unknown (no HQ credentials). ${lastStored.detail}` : `HQ last stored: unknown. ${lastStored.detail}`;
|
|
16469
|
+
async function hqLastStoredDoctorCheck(input, dependencies = {}) {
|
|
16470
|
+
const { hq, repository } = input.profile;
|
|
16471
|
+
if (!hq?.enabled) return {
|
|
16472
|
+
message: "Not an HQ repository: the profile does not enable HQ ingest, so HQ is not asked.",
|
|
16473
|
+
name: HQ_LAST_STORED_CHECK,
|
|
16474
|
+
status: "ok"
|
|
16475
|
+
};
|
|
16476
|
+
const slug = {
|
|
16477
|
+
owner: repository.owner,
|
|
16478
|
+
repo: repository.name
|
|
16479
|
+
};
|
|
16480
|
+
const [lastStored, spool] = await Promise.all([(dependencies.lastStored ?? fetchHqLastStored)({
|
|
16481
|
+
endpoint: hq.endpoint,
|
|
16482
|
+
env: input.env,
|
|
16483
|
+
hqAllowedOrigins: input.userConfig?.hqAllowedOrigins,
|
|
16484
|
+
hqIngestCredentials: input.userConfig?.hqIngestCredentials,
|
|
16485
|
+
repository: slug
|
|
16486
|
+
}), (dependencies.countSpool ?? countHqSpoolWork)({ repository: slug }, { env: input.env })]);
|
|
16487
|
+
const now = dependencies.now?.() ?? /* @__PURE__ */ new Date();
|
|
16488
|
+
const compared = lastStored.status === "answered" ? comparison(lastStored.answer, readLocalProof(input)) : void 0;
|
|
16489
|
+
const behind = compared?.behind ?? false;
|
|
16490
|
+
const unidentified = lastStored.status === "answered" && lastStored.answer.lastFromCaller === null ? "HQ does not know which machine this token belongs to, so this machine's proofs are not compared." : void 0;
|
|
16491
|
+
return {
|
|
16492
|
+
message: [
|
|
16493
|
+
lastStored.status === "answered" ? summary$1(lastStored.answer, now) : unknownLine(lastStored),
|
|
16494
|
+
unidentified,
|
|
16495
|
+
compared?.note,
|
|
16496
|
+
...spoolNotes(spool, behind)
|
|
16497
|
+
].filter((note) => note !== void 0).join(" "),
|
|
16498
|
+
name: HQ_LAST_STORED_CHECK,
|
|
16499
|
+
status: behind || spool.pending > 0 || spool.unlistable > 0 ? "warning" : "ok"
|
|
16500
|
+
};
|
|
16501
|
+
}
|
|
16502
|
+
//#endregion
|
|
16503
|
+
//#region src/doctor-hq-webhook.ts
|
|
16504
|
+
/**
|
|
16505
|
+
* The `hq-webhook` doctor row (#1341): whether GitHub's webhook deliveries
|
|
16506
|
+
* from this repository reach HQ. paitronage and firedup answered 401 from
|
|
16507
|
+
* 2026-07-17 on and nothing said so; this row warns on the first one.
|
|
16508
|
+
*
|
|
16509
|
+
* It reads the repository hook whose URL is the profile's HQ origin plus
|
|
16510
|
+
* `/webhooks/github`, then that hook's last 30 deliveries, with the `gh`
|
|
16511
|
+
* credential doctor already uses. It warns on a 401 (the secret does not match
|
|
16512
|
+
* HQ's), any other 4xx, a 5xx, a delivery that got no response, or no hook at
|
|
16513
|
+
* all. HQ answers an out-of-scope delivery 202 (#1338), so any 4xx is a broken
|
|
16514
|
+
* delivery: HQ rejected it and stored nothing.
|
|
16515
|
+
*
|
|
16516
|
+
* Reading hooks needs admin on the repository, which many operators do not
|
|
16517
|
+
* have. When GitHub will not show the deliveries, the row warns and falls back
|
|
16518
|
+
* to HQ's own record, `GET /api/ingest/last-stored`, so a non-admin still gets
|
|
16519
|
+
* an answer, or an explicit "unknown" with its reason.
|
|
16520
|
+
*
|
|
16521
|
+
* A warning, never an error: HQ is advisory (ADR 0015, #1228), so HQ being
|
|
16522
|
+
* deaf never changes doctor's exit code. The row only reads; it never creates,
|
|
16523
|
+
* edits or redelivers a hook.
|
|
16524
|
+
*/
|
|
16525
|
+
const HQ_WEBHOOK_CHECK = "hq-webhook";
|
|
16526
|
+
/** HQ's webhook route. `software-factory-hq/src/webhooks/config.ts` owns it as `GITHUB_WEBHOOK_PATH`. */
|
|
16527
|
+
const HQ_WEBHOOK_PATH = "/webhooks/github";
|
|
16528
|
+
const HTTP_STATUS_PATTERN$1 = /\bHTTP (?<status>\d{3})\b/u;
|
|
16529
|
+
const DAY_MS = 864e5;
|
|
16530
|
+
const sameUrl = (candidate, expected) => {
|
|
16531
|
+
try {
|
|
16532
|
+
return candidate !== void 0 && new URL(candidate).href === expected;
|
|
16533
|
+
} catch {
|
|
16534
|
+
return false;
|
|
16535
|
+
}
|
|
16536
|
+
};
|
|
16537
|
+
/**
|
|
16538
|
+
* Why a GitHub read failed, from gh's own `(HTTP nnn)` and the deadline. The
|
|
16539
|
+
* error text is read here and dropped.
|
|
16540
|
+
*/
|
|
16541
|
+
const githubFailure = (error, slug) => {
|
|
16542
|
+
const failure = error;
|
|
16543
|
+
if (failure.killed === true) return "GitHub did not answer in time";
|
|
16544
|
+
const text = typeof failure.stderr === "string" ? failure.stderr : failure.message ?? "";
|
|
16545
|
+
const status = Number(HTTP_STATUS_PATTERN$1.exec(text)?.groups?.status ?? 0);
|
|
16546
|
+
if (status === 403 || status === 404) return `GitHub answered ${status}, so this gh token has no admin read on ${slug}`;
|
|
16547
|
+
if (status !== 0) return `GitHub answered ${status}`;
|
|
16548
|
+
return "gh could not read GitHub; check `gh auth status`";
|
|
16549
|
+
};
|
|
16550
|
+
const daysAgo = (receivedAt, now) => {
|
|
16551
|
+
const days = Math.floor((now.getTime() - Date.parse(receivedAt)) / DAY_MS);
|
|
16552
|
+
if (!Number.isFinite(days)) return `at ${receivedAt}`;
|
|
16553
|
+
return `${days} day${days === 1 ? "" : "s"} ago (${receivedAt})`;
|
|
16554
|
+
};
|
|
16555
|
+
const describeLastStored = (lastStored, now) => {
|
|
16556
|
+
if (lastStored.status === "unknown") return `HQ's last stored webhook is unknown: ${lastStored.detail}`;
|
|
16557
|
+
const { answer } = lastStored;
|
|
16558
|
+
if (!answer.found) return "HQ has never stored an event from this repository.";
|
|
16559
|
+
if (answer.lastWebhookEvent === null) return "HQ has stored events from this repository, but never a webhook delivery.";
|
|
16560
|
+
return `HQ last stored a webhook from this repository ${daysAgo(answer.lastWebhookEvent.receivedAt, now)}, a ${answer.lastWebhookEvent.event} event.`;
|
|
16561
|
+
};
|
|
16562
|
+
/**
|
|
16563
|
+
* GitHub will not show the deliveries, so the row warns, says why, and reports
|
|
16564
|
+
* what HQ last stored instead.
|
|
16565
|
+
*/
|
|
16566
|
+
const unreadableCheck = async (input, hq, error, dependencies) => {
|
|
16567
|
+
const { repository } = input.profile;
|
|
16568
|
+
const slug = `${repository.owner}/${repository.name}`;
|
|
16569
|
+
const lastStored = await (dependencies.lastStored ?? fetchHqLastStored)({
|
|
16570
|
+
endpoint: hq.endpoint,
|
|
16571
|
+
env: input.env,
|
|
16572
|
+
hqAllowedOrigins: input.userConfig?.hqAllowedOrigins,
|
|
16573
|
+
hqIngestCredentials: input.userConfig?.hqIngestCredentials,
|
|
16574
|
+
repository: {
|
|
16575
|
+
owner: repository.owner,
|
|
16576
|
+
repo: repository.name
|
|
16577
|
+
}
|
|
16578
|
+
});
|
|
16579
|
+
return {
|
|
16580
|
+
message: `Can't read ${slug}'s webhook deliveries: ${githubFailure(error, slug)}. ${describeLastStored(lastStored, dependencies.now?.() ?? /* @__PURE__ */ new Date())}`,
|
|
16581
|
+
name: HQ_WEBHOOK_CHECK,
|
|
16582
|
+
status: "warning"
|
|
16583
|
+
};
|
|
16584
|
+
};
|
|
16585
|
+
/** `n × code` per status, in the order given. Code 0 is no response. */
|
|
16586
|
+
const describeCounts = (counted) => counted.map(([code, n]) => `${n} × ${code === 0 ? "no response" : code}`).join(", ");
|
|
16587
|
+
const deliveriesCheck = (hookId, hookUrl, deliveries) => {
|
|
16588
|
+
if (deliveries.length === 0) return {
|
|
16589
|
+
message: `Hook ${hookId} delivers to ${hookUrl}; GitHub lists no recent deliveries for it.`,
|
|
16590
|
+
name: HQ_WEBHOOK_CHECK,
|
|
16591
|
+
status: "ok"
|
|
16592
|
+
};
|
|
16593
|
+
const tally = /* @__PURE__ */ new Map();
|
|
16594
|
+
for (const { status_code: code } of deliveries) {
|
|
16595
|
+
const key = code < 100 ? 0 : code;
|
|
16596
|
+
tally.set(key, (tally.get(key) ?? 0) + 1);
|
|
16597
|
+
}
|
|
16598
|
+
const counted = [...tally].toSorted(([left], [right]) => left - right);
|
|
16599
|
+
const unauthorized = tally.get(401) ?? 0;
|
|
16600
|
+
const rejected = counted.filter(([code]) => code >= 400 && code < 500 && code !== 401);
|
|
16601
|
+
const rejectedCount = rejected.reduce((total, [, n]) => total + n, 0);
|
|
16602
|
+
const noResponse = tally.get(0) ?? 0;
|
|
16603
|
+
const serverErrors = deliveries.filter(({ status_code: code }) => code >= 500).length;
|
|
16604
|
+
const warnings = [
|
|
16605
|
+
...unauthorized > 0 ? [`${unauthorized} got 401: the webhook secret does not match HQ's; see #1310.`] : [],
|
|
16606
|
+
...rejectedCount > 0 ? [`${rejectedCount} got a 4xx other than 401 (${describeCounts(rejected)}): HQ rejected each delivery and stored nothing.`] : [],
|
|
16607
|
+
...serverErrors > 0 ? [`${serverErrors} got a 5xx from HQ.`] : [],
|
|
16608
|
+
...noResponse > 0 ? [`${noResponse} got no response from HQ (timed out or unreachable).`] : []
|
|
16609
|
+
];
|
|
16610
|
+
return {
|
|
16611
|
+
message: [`Hook ${hookId} delivers to ${hookUrl}; its last ${deliveries.length} deliveries: ${describeCounts(counted)}.`, ...warnings].join(" "),
|
|
16612
|
+
name: HQ_WEBHOOK_CHECK,
|
|
16613
|
+
status: warnings.length > 0 ? "warning" : "ok"
|
|
16614
|
+
};
|
|
16615
|
+
};
|
|
16616
|
+
async function hqWebhookDoctorCheck(input, dependencies = {}) {
|
|
16617
|
+
const { hq, repository } = input.profile;
|
|
16618
|
+
if (!hq?.enabled) return {
|
|
16619
|
+
message: "The profile does not enable HQ ingest, so no webhook delivery is checked.",
|
|
16620
|
+
name: HQ_WEBHOOK_CHECK,
|
|
16621
|
+
status: "ok"
|
|
16622
|
+
};
|
|
16623
|
+
const hookUrl = new URL(HQ_WEBHOOK_PATH, hq.endpoint).href;
|
|
16624
|
+
let hooks;
|
|
16625
|
+
try {
|
|
16626
|
+
hooks = await fetchRepositoryHooks(repository, input.cwd, dependencies.gh);
|
|
16627
|
+
} catch (error) {
|
|
16628
|
+
return unreadableCheck(input, hq, error, dependencies);
|
|
16629
|
+
}
|
|
16630
|
+
const hook = hooks.find((candidate) => sameUrl(candidate.config.url, hookUrl));
|
|
16631
|
+
if (hook === void 0) return {
|
|
16632
|
+
message: `No webhook on ${repository.owner}/${repository.name} delivers to ${hookUrl}, so HQ hears no GitHub event from this repository. Fix: a repository admin adds one with payload URL ${hookUrl}, content type json, the events check_run, issues, pull_request and push, and the secret HQ holds.`,
|
|
16633
|
+
name: HQ_WEBHOOK_CHECK,
|
|
16634
|
+
status: "warning"
|
|
16635
|
+
};
|
|
16636
|
+
if (!hook.active) return {
|
|
16637
|
+
message: `Hook ${hook.id} to ${hookUrl} is disabled in GitHub, so no new event from ${repository.owner}/${repository.name} reaches HQ. Fix: a repository admin re-enables it in Settings → Webhooks.`,
|
|
16638
|
+
name: HQ_WEBHOOK_CHECK,
|
|
16639
|
+
status: "warning"
|
|
16640
|
+
};
|
|
16641
|
+
try {
|
|
16642
|
+
return deliveriesCheck(hook.id, hookUrl, await fetchHookDeliveries(repository, hook.id, input.cwd, dependencies.gh));
|
|
16643
|
+
} catch (error) {
|
|
16644
|
+
return unreadableCheck(input, hq, error, dependencies);
|
|
16645
|
+
}
|
|
16646
|
+
}
|
|
16647
|
+
//#endregion
|
|
16230
16648
|
//#region src/doctor-machine-checks.ts
|
|
16231
16649
|
/**
|
|
16232
16650
|
* Machine readiness rows for `doctor` (#1315): facts about this machine, not
|
|
@@ -16430,6 +16848,19 @@ async function doctorProjectProfile(input = {}) {
|
|
|
16430
16848
|
}),
|
|
16431
16849
|
...hqReadinessChecks,
|
|
16432
16850
|
hqSpoolCheck,
|
|
16851
|
+
await hqWebhookDoctorCheck({
|
|
16852
|
+
cwd,
|
|
16853
|
+
env,
|
|
16854
|
+
profile,
|
|
16855
|
+
userConfig: userConfig.loaded?.config
|
|
16856
|
+
}, input.hqWebhookDependencies),
|
|
16857
|
+
await hqLastStoredDoctorCheck({
|
|
16858
|
+
cwd,
|
|
16859
|
+
env,
|
|
16860
|
+
profile,
|
|
16861
|
+
profilePath: path,
|
|
16862
|
+
userConfig: userConfig.loaded?.config
|
|
16863
|
+
}, input.hqLastStoredDependencies),
|
|
16433
16864
|
...machineDoctorChecks({
|
|
16434
16865
|
env,
|
|
16435
16866
|
profile,
|
|
@@ -16888,12 +17319,18 @@ const publishEpicStructure = async (args) => {
|
|
|
16888
17319
|
const doFetch = args.fetchImpl ?? globalThis.fetch;
|
|
16889
17320
|
const requestInit = args.accessServiceToken ? buildCloudflareAccessRequestInit(args.accessServiceToken, {
|
|
16890
17321
|
body: JSON.stringify(args.event),
|
|
16891
|
-
headers: {
|
|
17322
|
+
headers: {
|
|
17323
|
+
"content-type": "application/json",
|
|
17324
|
+
"user-agent": HQ_SINK_USER_AGENT
|
|
17325
|
+
},
|
|
16892
17326
|
method: "POST",
|
|
16893
17327
|
...args.signal ? { signal: args.signal } : {}
|
|
16894
17328
|
}) : {
|
|
16895
17329
|
body: JSON.stringify(args.event),
|
|
16896
|
-
headers: {
|
|
17330
|
+
headers: {
|
|
17331
|
+
"content-type": "application/json",
|
|
17332
|
+
"user-agent": HQ_SINK_USER_AGENT
|
|
17333
|
+
},
|
|
16897
17334
|
method: "POST",
|
|
16898
17335
|
...args.signal ? { signal: args.signal } : {}
|
|
16899
17336
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patronage/software-factory",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
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.
|
|
45
|
+
"@patronage/factory-ci": "1.0.0-alpha.39"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@oxlint/plugins": "1.76.0",
|