@patronage/software-factory 1.0.0-alpha.5 → 1.0.0-alpha.7
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 +1 -1
- package/dist/index.d.ts +23 -33
- package/dist/index.js +68 -358
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ A stacked release train has one terminal release-owning slice. Interior slices d
|
|
|
37
37
|
A stable cut has two phases. Phase 1 is the verified package candidate: steps 1 through 4 below, ending in the attended npm publish, tag, and GitHub release. Phase 2 is post-publication stable-doc convergence: steps 5 and 6, a normal admitted docs PR followed by the read-only convergence assertion from a clean `main`. Stable docs must never claim an unpublished version, so they cannot land in the phase-1 change. Prerelease behavior is unchanged: prerelease notes may land before publish while stable docs keep naming the prior stable version.
|
|
38
38
|
|
|
39
39
|
1. Bump `package.json` to valid SemVer. The assertion derives its expected stable or prerelease channel from the version; it accepts no channel flag.
|
|
40
|
-
2. Publish to npm per the [repository operations runbook](../docs/repository-operations.md#publishing) (attended, hand-cut, `pnpm publish` only).
|
|
40
|
+
2. Publish to npm per the [repository operations runbook](../docs/repository-operations.md#publishing) (attended, hand-cut, `pnpm publish` only). **Always pass `--tag` explicitly, on every publish, prerelease or stable** — `pnpm --filter @patronage/software-factory publish --tag next` for a prerelease, `--tag latest` for a stable cut. A filtered publish takes pnpm's recursive path, which passes its own `--tag` (defaulting to `latest`) to npm and therefore ignores the `publishConfig.tag: next` pin both factory packages carry; that is how 1.0.0-alpha.6 briefly took `latest` (#658). The pins stay as belt and braces, but they are not the control. Omitting the flag is not a safe failure: on the filtered command it moves `latest`, which the convergence assertion can only report after the fact.
|
|
41
41
|
3. Cut the `software-factory-vX.Y.Z` tag and push it.
|
|
42
42
|
4. Cut the GitHub release for that tag, with notes covering features, deletions, breaking changes, and closed issues. Mark it as a prerelease exactly when its SemVer version has a prerelease component. When the release changes the profile `schemaVersion`, state it in the notes — the profile schema version is a separate axis from the package version.
|
|
43
43
|
5. Converge the stable docs after publication. Update affected `software-factory-docs/` pages through a normal admitted docs PR. Stable docs must state the current stable version. Prerelease docs may record the candidate and its migration notes, but must not represent it as the current stable release.
|
package/dist/index.d.ts
CHANGED
|
@@ -145,8 +145,6 @@ interface HqIngestDependencies {
|
|
|
145
145
|
* `timeoutMs`: see the note there.
|
|
146
146
|
*/
|
|
147
147
|
journalFlushBudgetMs?: number;
|
|
148
|
-
/** @deprecated Legacy sidecars are now streamed directly; retained for API compatibility. */
|
|
149
|
-
sidecarReclaimMs?: number;
|
|
150
148
|
/**
|
|
151
149
|
* Setup budget for callers that own the process lifetime — honored AS GIVEN,
|
|
152
150
|
* above the `DEFAULT_HQ_INGEST_TIMEOUT_MS` ceiling that clamps `timeoutMs`.
|
|
@@ -184,12 +182,6 @@ interface HqSpoolEntryOutcome {
|
|
|
184
182
|
kind: string;
|
|
185
183
|
spool: string;
|
|
186
184
|
/**
|
|
187
|
-
* `migrated` belongs to the legacy JSONL journal only: the row was moved
|
|
188
|
-
* into the current spool without being delivered. The spool pass that runs
|
|
189
|
-
* after it in the same drain supersedes that line with a real outcome when
|
|
190
|
-
* it gets to the row; a `migrated` line that survives the run means the row
|
|
191
|
-
* is still waiting.
|
|
192
|
-
*
|
|
193
185
|
* `undeliverable` is the one terminal verdict (#445). Every other status
|
|
194
186
|
* describes a moment: HQ was unreachable, HQ refused this content today, the
|
|
195
187
|
* row moved. Retrying is meaningful for all of them. A wrong-origin entry is
|
|
@@ -198,13 +190,11 @@ interface HqSpoolEntryOutcome {
|
|
|
198
190
|
* run. Leaving it spooled asks the operator to retry something that provably
|
|
199
191
|
* cannot succeed, and the count it inflates is the one doctor goes red on.
|
|
200
192
|
*/
|
|
201
|
-
status: "delivered" | "duplicate" | "
|
|
193
|
+
status: "delivered" | "duplicate" | "rejected" | "undeliverable" | "unreachable";
|
|
202
194
|
}
|
|
203
195
|
interface HqSpoolFlushInput {
|
|
204
196
|
clientId: string;
|
|
205
197
|
clientSecret: string;
|
|
206
|
-
/** Repository root whose legacy `.factory-memory` spool is also drained. */
|
|
207
|
-
cwd: string;
|
|
208
198
|
/** The profile's HQ origin; entries recorded against another are refused. */
|
|
209
199
|
endpoint: string;
|
|
210
200
|
/** Operator-named spool directories; replaces the default two locations. */
|
|
@@ -239,13 +229,13 @@ interface HqSpoolFlushSummary {
|
|
|
239
229
|
interface HqSpoolWorkCount {
|
|
240
230
|
/**
|
|
241
231
|
* The earliest moment learned across pending spool files (their own write
|
|
242
|
-
* time)
|
|
232
|
+
* time). Absent only when
|
|
243
233
|
* `pending` is `0`, or when every timestamp source was unreadable within
|
|
244
234
|
* budget — an estimate for doctor's remediation message (#394), never a
|
|
245
235
|
* precise audit trail.
|
|
246
236
|
*/
|
|
247
237
|
oldestQueuedAt?: string;
|
|
248
|
-
/** Spooled events
|
|
238
|
+
/** Spooled events waiting in the locations below. */
|
|
249
239
|
pending: number;
|
|
250
240
|
/** Locations that exist and hold spooled work. */
|
|
251
241
|
spools: string[];
|
|
@@ -266,7 +256,7 @@ interface HqSpoolWorkCount {
|
|
|
266
256
|
* same locations `flushHqSpool` drains are inspected here, read-only: no
|
|
267
257
|
* directory is created, nothing is secured, and nothing is delivered.
|
|
268
258
|
*/
|
|
269
|
-
declare function countHqSpoolWork(input: Pick<HqSpoolFlushInput, "
|
|
259
|
+
declare function countHqSpoolWork(input: Pick<HqSpoolFlushInput, "explicitDirectories" | "repository">, dependencies?: {
|
|
270
260
|
budgetMs?: number;
|
|
271
261
|
env?: NodeJS.ProcessEnv;
|
|
272
262
|
}): Promise<HqSpoolWorkCount>;
|
|
@@ -275,7 +265,7 @@ interface HqSpoolOrphan {
|
|
|
275
265
|
/** The `hq-retry-spool` directory itself, ready to pass to `--dir`. */
|
|
276
266
|
directory: string;
|
|
277
267
|
oldestQueuedAt?: string;
|
|
278
|
-
/** Spooled events
|
|
268
|
+
/** Spooled events waiting there. */
|
|
279
269
|
pending: number;
|
|
280
270
|
/**
|
|
281
271
|
* The location exists but could not be listed. As everywhere else in this
|
|
@@ -753,8 +743,8 @@ declare const mergeFreezeStateSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
753
743
|
generationId: z.ZodNumber;
|
|
754
744
|
headSha: z.ZodString;
|
|
755
745
|
outcome: z.ZodEnum<{
|
|
756
|
-
active: "active";
|
|
757
746
|
stale: "stale";
|
|
747
|
+
active: "active";
|
|
758
748
|
}>;
|
|
759
749
|
reason: z.ZodString;
|
|
760
750
|
recordedAt: z.ZodISODateTime;
|
|
@@ -1234,8 +1224,8 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
|
|
|
1234
1224
|
"run-interior-cycle": "run-interior-cycle";
|
|
1235
1225
|
}>;
|
|
1236
1226
|
stage: z.ZodEnum<{
|
|
1237
|
-
gate: "gate";
|
|
1238
1227
|
interior: "interior";
|
|
1228
|
+
gate: "gate";
|
|
1239
1229
|
"interior-complete": "interior-complete";
|
|
1240
1230
|
}>;
|
|
1241
1231
|
}, z.core.$strip>>;
|
|
@@ -1251,10 +1241,10 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
|
|
|
1251
1241
|
reviewedHeadSha: z.ZodOptional<z.ZodString>;
|
|
1252
1242
|
reviewedPatchId: z.ZodOptional<z.ZodString>;
|
|
1253
1243
|
status: z.ZodEnum<{
|
|
1254
|
-
blocked: "blocked";
|
|
1255
1244
|
"not-required": "not-required";
|
|
1256
|
-
|
|
1245
|
+
blocked: "blocked";
|
|
1257
1246
|
current: "current";
|
|
1247
|
+
stale: "stale";
|
|
1258
1248
|
missing: "missing";
|
|
1259
1249
|
}>;
|
|
1260
1250
|
}, z.core.$strip>;
|
|
@@ -1263,10 +1253,10 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
|
|
|
1263
1253
|
reviewedHeadSha: z.ZodOptional<z.ZodString>;
|
|
1264
1254
|
reviewedPatchId: z.ZodOptional<z.ZodString>;
|
|
1265
1255
|
status: z.ZodEnum<{
|
|
1266
|
-
blocked: "blocked";
|
|
1267
1256
|
"not-required": "not-required";
|
|
1268
|
-
|
|
1257
|
+
blocked: "blocked";
|
|
1269
1258
|
current: "current";
|
|
1259
|
+
stale: "stale";
|
|
1270
1260
|
missing: "missing";
|
|
1271
1261
|
}>;
|
|
1272
1262
|
}, z.core.$strip>>;
|
|
@@ -1277,8 +1267,8 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
|
|
|
1277
1267
|
docsOnlyDeltaAccepted: z.ZodOptional<z.ZodBoolean>;
|
|
1278
1268
|
docsOnlyVerifiedHeadSha: z.ZodOptional<z.ZodString>;
|
|
1279
1269
|
prVerify: z.ZodEnum<{
|
|
1280
|
-
stale: "stale";
|
|
1281
1270
|
passed: "passed";
|
|
1271
|
+
stale: "stale";
|
|
1282
1272
|
missing: "missing";
|
|
1283
1273
|
}>;
|
|
1284
1274
|
trivialDeltaAccepted: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -1778,10 +1768,10 @@ declare const loadEvidenceEnvelopes: (cwd: string) => LoadedEvidenceEnvelope[];
|
|
|
1778
1768
|
//#region src/review-proof-applicability.d.ts
|
|
1779
1769
|
declare const REVIEW_STATUS_VALUES: readonly ["not-required", "current", "stale", "missing", "blocked"];
|
|
1780
1770
|
declare const reviewStatusSchema: z.ZodEnum<{
|
|
1781
|
-
blocked: "blocked";
|
|
1782
1771
|
"not-required": "not-required";
|
|
1783
|
-
|
|
1772
|
+
blocked: "blocked";
|
|
1784
1773
|
current: "current";
|
|
1774
|
+
stale: "stale";
|
|
1785
1775
|
missing: "missing";
|
|
1786
1776
|
}>;
|
|
1787
1777
|
type ReviewStatus = z.infer<typeof reviewStatusSchema>;
|
|
@@ -2187,9 +2177,9 @@ declare const retroEnvelopeV1Schema: z.ZodObject<{
|
|
|
2187
2177
|
kind: z.ZodLiteral<"retro-envelope">;
|
|
2188
2178
|
outcome: z.ZodOptional<z.ZodObject<{
|
|
2189
2179
|
status: z.ZodEnum<{
|
|
2190
|
-
blocked: "blocked";
|
|
2191
2180
|
success: "success";
|
|
2192
2181
|
fail: "fail";
|
|
2182
|
+
blocked: "blocked";
|
|
2193
2183
|
"ship-with-followups": "ship-with-followups";
|
|
2194
2184
|
}>;
|
|
2195
2185
|
verdict: z.ZodOptional<z.ZodString>;
|
|
@@ -2949,8 +2939,8 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
2949
2939
|
sessionId: z.ZodOptional<z.ZodString>;
|
|
2950
2940
|
}, z.core.$strict>>;
|
|
2951
2941
|
stage: z.ZodEnum<{
|
|
2952
|
-
gate: "gate";
|
|
2953
2942
|
interior: "interior";
|
|
2943
|
+
gate: "gate";
|
|
2954
2944
|
}>;
|
|
2955
2945
|
usage: z.ZodOptional<z.ZodObject<{
|
|
2956
2946
|
estimatedCostUsd: z.ZodOptional<z.ZodNumber>;
|
|
@@ -3002,8 +2992,8 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
3002
2992
|
sessionId: z.ZodOptional<z.ZodString>;
|
|
3003
2993
|
}, z.core.$strict>>;
|
|
3004
2994
|
stage: z.ZodEnum<{
|
|
3005
|
-
gate: "gate";
|
|
3006
2995
|
interior: "interior";
|
|
2996
|
+
gate: "gate";
|
|
3007
2997
|
}>;
|
|
3008
2998
|
usage: z.ZodOptional<z.ZodObject<{
|
|
3009
2999
|
estimatedCostUsd: z.ZodOptional<z.ZodNumber>;
|
|
@@ -3046,8 +3036,8 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
3046
3036
|
sessionId: z.ZodOptional<z.ZodString>;
|
|
3047
3037
|
}, z.core.$strict>>;
|
|
3048
3038
|
stage: z.ZodEnum<{
|
|
3049
|
-
gate: "gate";
|
|
3050
3039
|
interior: "interior";
|
|
3040
|
+
gate: "gate";
|
|
3051
3041
|
}>;
|
|
3052
3042
|
usage: z.ZodOptional<z.ZodObject<{
|
|
3053
3043
|
estimatedCostUsd: z.ZodOptional<z.ZodNumber>;
|
|
@@ -3077,7 +3067,7 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
3077
3067
|
staleRepeats: number;
|
|
3078
3068
|
};
|
|
3079
3069
|
observedAt: string;
|
|
3080
|
-
stage: "
|
|
3070
|
+
stage: "interior" | "gate";
|
|
3081
3071
|
issue?: number | undefined;
|
|
3082
3072
|
pr?: number | undefined;
|
|
3083
3073
|
threadId?: string | undefined;
|
|
@@ -4076,13 +4066,13 @@ declare const evaluateReadiness: (input: EvaluationInput) => {
|
|
|
4076
4066
|
reviews: {
|
|
4077
4067
|
correctness: {
|
|
4078
4068
|
required: boolean;
|
|
4079
|
-
status: "
|
|
4069
|
+
status: "not-required" | "blocked" | "current" | "stale" | "missing";
|
|
4080
4070
|
reviewedHeadSha?: string | undefined;
|
|
4081
4071
|
reviewedPatchId?: string | undefined;
|
|
4082
4072
|
};
|
|
4083
4073
|
security?: {
|
|
4084
4074
|
required: boolean;
|
|
4085
|
-
status: "
|
|
4075
|
+
status: "not-required" | "blocked" | "current" | "stale" | "missing";
|
|
4086
4076
|
reviewedHeadSha?: string | undefined;
|
|
4087
4077
|
reviewedPatchId?: string | undefined;
|
|
4088
4078
|
} | undefined;
|
|
@@ -4090,7 +4080,7 @@ declare const evaluateReadiness: (input: EvaluationInput) => {
|
|
|
4090
4080
|
schemaVersion: 1;
|
|
4091
4081
|
verification: {
|
|
4092
4082
|
command: "patronage-factory pr:verify";
|
|
4093
|
-
prVerify: "
|
|
4083
|
+
prVerify: "passed" | "stale" | "missing";
|
|
4094
4084
|
docsOnlyDeltaAccepted?: boolean | undefined;
|
|
4095
4085
|
docsOnlyVerifiedHeadSha?: string | undefined;
|
|
4096
4086
|
trivialDeltaAccepted?: boolean | undefined;
|
|
@@ -4133,7 +4123,7 @@ declare const evaluateReadiness: (input: EvaluationInput) => {
|
|
|
4133
4123
|
interior: number;
|
|
4134
4124
|
};
|
|
4135
4125
|
nextAction: "run-gate-cycle" | "accept-nonblocking-findings" | "escalate-to-triage" | "ready-for-human" | "advance-to-gate" | "run-interior-cycle";
|
|
4136
|
-
stage: "
|
|
4126
|
+
stage: "interior" | "gate" | "interior-complete";
|
|
4137
4127
|
forcedTransition?: "gate-cap-exhausted" | "interior-cap-reached" | undefined;
|
|
4138
4128
|
} | undefined;
|
|
4139
4129
|
reviewRuns?: PrReviewResult[] | undefined;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { t as __exportAll } from "./chunk-pbuEa-1d.js";
|
|
3
|
-
import { appendFileSync, constants, cpSync,
|
|
3
|
+
import { appendFileSync, constants, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import { Command, InvalidArgumentError } from "commander";
|
|
6
6
|
import path from "node:path";
|
|
@@ -9,7 +9,6 @@ import { z } from "zod";
|
|
|
9
9
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
10
10
|
import { link, lstat, mkdir, open, readFile, readdir, realpath, rename, stat, unlink } from "node:fs/promises";
|
|
11
11
|
import os, { homedir } from "node:os";
|
|
12
|
-
import { createInterface } from "node:readline";
|
|
13
12
|
import { setImmediate } from "node:timers";
|
|
14
13
|
import { setImmediate as setImmediate$1, setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
15
14
|
import { Worker } from "node:worker_threads";
|
|
@@ -18,7 +17,7 @@ import { parse } from "yaml";
|
|
|
18
17
|
import { promisify } from "node:util";
|
|
19
18
|
import { GitHubApiError, mintInstallationToken } from "@patronage/factory-ci";
|
|
20
19
|
//#region package.json
|
|
21
|
-
var version = "1.0.0-alpha.
|
|
20
|
+
var version = "1.0.0-alpha.7";
|
|
22
21
|
//#endregion
|
|
23
22
|
//#region src/review-rungs.ts
|
|
24
23
|
const EVIDENCE_REVIEW_RUNGS$1 = [
|
|
@@ -1348,7 +1347,6 @@ function formatUserConfigError(configPath, error) {
|
|
|
1348
1347
|
}
|
|
1349
1348
|
const DEFAULT_HQ_TRANSPORT_TIMEOUT_MS = 2500;
|
|
1350
1349
|
const HQ_RETRY_SPOOL_DIRNAME = "hq-retry-spool";
|
|
1351
|
-
const HQ_RETRY_JOURNAL_BASENAME = "hq-retry-journal.jsonl";
|
|
1352
1350
|
/**
|
|
1353
1351
|
* Appended to a spooled event that can never be delivered (#445).
|
|
1354
1352
|
*
|
|
@@ -1389,9 +1387,6 @@ const SPOOL_REPOSITORY_MARKER = ".repository-identity";
|
|
|
1389
1387
|
* and the extra level is slack for a key scheme that nests one deeper.
|
|
1390
1388
|
*/
|
|
1391
1389
|
const HQ_SPOOL_SWEEP_MAX_DEPTH = 4;
|
|
1392
|
-
`${HQ_RETRY_JOURNAL_BASENAME}`;
|
|
1393
|
-
`${HQ_RETRY_SPOOL_DIRNAME}`;
|
|
1394
|
-
const LEGACY_FACTORY_MEMORY_DIRNAME = ".factory-memory";
|
|
1395
1390
|
const HQ_SPOOL_STATE_SEGMENTS = ["patronage-factory", "hq-spool"];
|
|
1396
1391
|
const MAX_HQ_CONFIG_BYTES = 1024 * 1024;
|
|
1397
1392
|
const MAX_HQ_INGEST_PAYLOAD_BYTES = 256 * 1024;
|
|
@@ -1503,12 +1498,6 @@ const repositorySpoolLayout = (repository, env = process.env) => ({
|
|
|
1503
1498
|
HQ_RETRY_SPOOL_DIRNAME
|
|
1504
1499
|
]
|
|
1505
1500
|
});
|
|
1506
|
-
/** Read-only drain source for spools written before the relocation. */
|
|
1507
|
-
const legacySpoolLayout = (cwd) => ({
|
|
1508
|
-
create: false,
|
|
1509
|
-
root: cwd,
|
|
1510
|
-
segments: [LEGACY_FACTORY_MEMORY_DIRNAME, HQ_RETRY_SPOOL_DIRNAME]
|
|
1511
|
-
});
|
|
1512
1501
|
/**
|
|
1513
1502
|
* An operator-named directory (`hq:flush --dir`). Accepts either the spool
|
|
1514
1503
|
* itself or the directory holding it, so a `.factory-memory` path and a
|
|
@@ -1580,30 +1569,10 @@ const secureSpoolLayout = async (layout, deadline) => {
|
|
|
1580
1569
|
const memoryIdentity = identities.at(-2);
|
|
1581
1570
|
if (spool === void 0 || memory === void 0 || spoolIdentity === void 0 || memoryIdentity === void 0 || !await directoryIdentityMatches(memory, memoryIdentity, deadline) || !await directoryIdentityMatches(spool, spoolIdentity, deadline)) return;
|
|
1582
1571
|
return {
|
|
1583
|
-
memory,
|
|
1584
|
-
memoryIdentity,
|
|
1585
1572
|
spool,
|
|
1586
1573
|
spoolIdentity
|
|
1587
1574
|
};
|
|
1588
1575
|
};
|
|
1589
|
-
/**
|
|
1590
|
-
* The legacy JSONL journal lives one level above the spool. Securing it on its
|
|
1591
|
-
* own lets a pre-spool `.factory-memory` (journal but no spool directory)
|
|
1592
|
-
* drain without the read path creating anything inside the worktree.
|
|
1593
|
-
*/
|
|
1594
|
-
const secureJournalDirectory = async (layout, deadline) => {
|
|
1595
|
-
const segments = layout.segments.slice(0, -1);
|
|
1596
|
-
if (segments.length === 0) return;
|
|
1597
|
-
const chain = await secureChain(layout, segments, deadline);
|
|
1598
|
-
if (chain === void 0) return;
|
|
1599
|
-
const directory = chain.directories.at(-1);
|
|
1600
|
-
const identity = chain.identities.at(-1);
|
|
1601
|
-
if (directory === void 0 || identity === void 0 || !await directoryIdentityMatches(directory, identity, deadline)) return;
|
|
1602
|
-
return {
|
|
1603
|
-
directory,
|
|
1604
|
-
identity
|
|
1605
|
-
};
|
|
1606
|
-
};
|
|
1607
1576
|
const readDirectoryIdentity = async (directory, deadline) => {
|
|
1608
1577
|
const opened = await openWithin(directory, constants.O_RDONLY + constants.O_DIRECTORY + constants.O_NOFOLLOW, remainingMs(deadline));
|
|
1609
1578
|
if (opened.status !== "fulfilled") return;
|
|
@@ -2333,225 +2302,6 @@ const replayCloseoutSpool = async (layout, endpoint, clientId, clientSecret, req
|
|
|
2333
2302
|
if (!await mutateBoundDirectory(spool, spoolIdentity, performance.now() + flushBudgetMs, () => outcome.ok ? unlink(claimPath) : rename(claimPath, eventPath))) return;
|
|
2334
2303
|
}
|
|
2335
2304
|
};
|
|
2336
|
-
const writeLegacyCursor = async (directory, directoryIdentity, cursorPath, offset, deadline) => {
|
|
2337
|
-
if (path.dirname(cursorPath) !== directory || !await directoryIdentityMatches(directory, directoryIdentity, deadline)) return false;
|
|
2338
|
-
const temporary = `${cursorPath}.${randomUUID()}.tmp`;
|
|
2339
|
-
const opened = await openWithin(temporary, constants.O_CREAT + constants.O_EXCL + constants.O_WRONLY + constants.O_NOFOLLOW, remainingMs(deadline), 384);
|
|
2340
|
-
if (opened.status !== "fulfilled") return false;
|
|
2341
|
-
const wrote = await settleWithin((async () => {
|
|
2342
|
-
await opened.value.writeFile(String(offset), "utf-8");
|
|
2343
|
-
await opened.value.sync();
|
|
2344
|
-
})(), remainingMs(deadline));
|
|
2345
|
-
const closed = await settleWithin(opened.value.close(), remainingMs(deadline));
|
|
2346
|
-
if (wrote.status !== "fulfilled" || closed.status !== "fulfilled") {
|
|
2347
|
-
await mutateBoundDirectory(directory, directoryIdentity, deadline, async () => unlink(temporary));
|
|
2348
|
-
return false;
|
|
2349
|
-
}
|
|
2350
|
-
if (!await mutateBoundDirectory(directory, directoryIdentity, deadline, async () => rename(temporary, cursorPath))) {
|
|
2351
|
-
await mutateBoundDirectory(directory, directoryIdentity, deadline, async () => unlink(temporary));
|
|
2352
|
-
return false;
|
|
2353
|
-
}
|
|
2354
|
-
return await syncBoundDirectory(directory, directoryIdentity, deadline);
|
|
2355
|
-
};
|
|
2356
|
-
const readLegacyCursor = async (cursorPath, deadline) => {
|
|
2357
|
-
const file = await readBoundedTextFileNoFollow(cursorPath, deadline, 128);
|
|
2358
|
-
if (file === void 0) return 0;
|
|
2359
|
-
const offset = Number(file.contents);
|
|
2360
|
-
return Number.isSafeInteger(offset) && offset >= 0 ? offset : 0;
|
|
2361
|
-
};
|
|
2362
|
-
const claimLegacySource = async (sourcePath, target, directory, directoryIdentity, deadline) => {
|
|
2363
|
-
if (path.dirname(sourcePath) !== directory || !await directoryIdentityMatches(directory, directoryIdentity, deadline)) return;
|
|
2364
|
-
const metadata = await settleWithin(lstat(sourcePath), remainingMs(deadline));
|
|
2365
|
-
if (metadata.status !== "fulfilled" || !metadata.value.isFile() || metadata.value.isSymbolicLink()) return;
|
|
2366
|
-
const claimPath = `${target}.legacy-claim-${randomUUID()}`;
|
|
2367
|
-
if (!await mutateBoundDirectory(directory, directoryIdentity, deadline, async () => rename(sourcePath, claimPath))) return;
|
|
2368
|
-
return claimPath;
|
|
2369
|
-
};
|
|
2370
|
-
const releaseLegacyClaim = async (claimPath, target, directory, directoryIdentity, deadline) => {
|
|
2371
|
-
if (path.dirname(claimPath) !== directory) return false;
|
|
2372
|
-
const readyPath = `${target}.legacy-ready-${randomUUID()}`;
|
|
2373
|
-
return mutateBoundDirectory(directory, directoryIdentity, deadline, async () => rename(claimPath, readyPath));
|
|
2374
|
-
};
|
|
2375
|
-
async function replayRetryJournal(layout, writeLayout, endpoint, clientId, clientSecret, request, dependencies, transportBudgetMs, setupDeadline, replayDeadline, options = {}) {
|
|
2376
|
-
const maxEntries = options.maxEntries ?? MAX_HQ_REPLAY_ENTRIES;
|
|
2377
|
-
const { report } = options;
|
|
2378
|
-
const secured = await settleWithin(secureJournalDirectory(layout, setupDeadline), remainingMs(setupDeadline));
|
|
2379
|
-
if (secured.status !== "fulfilled" || secured.value === void 0) return;
|
|
2380
|
-
const { directory, identity: directoryIdentity } = secured.value;
|
|
2381
|
-
const target = path.join(directory, HQ_RETRY_JOURNAL_BASENAME);
|
|
2382
|
-
const baseName = path.basename(target);
|
|
2383
|
-
const listing = await settleWithin(readdir(directory), remainingMs(setupDeadline));
|
|
2384
|
-
if (listing.status !== "fulfilled") return;
|
|
2385
|
-
let delivered = 0;
|
|
2386
|
-
let corruptDropped = 0;
|
|
2387
|
-
const sources = listing.value.filter((name) => name === baseName || name.startsWith(`${baseName}.legacy-ready-`) || name.startsWith(`${baseName}.draining-`) || name.startsWith(`${baseName}.legacy-claim-`)).filter((name) => !name.includes(".cursor")).toSorted();
|
|
2388
|
-
for (const name of sources) {
|
|
2389
|
-
if (delivered >= maxEntries || remainingMs(replayDeadline) <= 0) break;
|
|
2390
|
-
const sourcePath = path.join(directory, name);
|
|
2391
|
-
if (name.startsWith(`${baseName}.draining-`) || name.startsWith(`${baseName}.legacy-claim-`)) {
|
|
2392
|
-
const metadata = await settleWithin(lstat(sourcePath), remainingMs(setupDeadline));
|
|
2393
|
-
if (metadata.status !== "fulfilled" || !metadata.value.isFile() || metadata.value.isSymbolicLink() || Date.now() - Math.max(metadata.value.mtimeMs, metadata.value.ctimeMs) < STALE_SPOOL_ARTIFACT_MS) continue;
|
|
2394
|
-
}
|
|
2395
|
-
const claimPath = await claimLegacySource(sourcePath, target, directory, directoryIdentity, setupDeadline);
|
|
2396
|
-
if (claimPath === void 0) continue;
|
|
2397
|
-
const opened = await openWithin(claimPath, constants.O_RDONLY + constants.O_NOFOLLOW, remainingMs(setupDeadline));
|
|
2398
|
-
if (opened.status !== "fulfilled") {
|
|
2399
|
-
await releaseLegacyClaim(claimPath, target, directory, directoryIdentity, setupDeadline);
|
|
2400
|
-
continue;
|
|
2401
|
-
}
|
|
2402
|
-
const sourceMetadata = await settleWithin(opened.value.stat(), remainingMs(setupDeadline));
|
|
2403
|
-
if (sourceMetadata.status !== "fulfilled" || !sourceMetadata.value.isFile()) {
|
|
2404
|
-
await settleWithin(opened.value.close(), remainingMs(setupDeadline));
|
|
2405
|
-
await releaseLegacyClaim(claimPath, target, directory, directoryIdentity, setupDeadline);
|
|
2406
|
-
continue;
|
|
2407
|
-
}
|
|
2408
|
-
const cursorPath = `${target}.legacy-cursor-${String(sourceMetadata.value.dev)}-${String(sourceMetadata.value.ino)}`;
|
|
2409
|
-
let offset = await readLegacyCursor(cursorPath, setupDeadline);
|
|
2410
|
-
if (offset >= sourceMetadata.value.size) {
|
|
2411
|
-
await settleWithin(opened.value.close(), remainingMs(setupDeadline));
|
|
2412
|
-
const current = await settleWithin(lstat(claimPath), remainingMs(setupDeadline));
|
|
2413
|
-
if (current.status === "fulfilled" && current.value.isFile() && !current.value.isSymbolicLink() && current.value.dev === sourceMetadata.value.dev && current.value.ino === sourceMetadata.value.ino && current.value.size === sourceMetadata.value.size) {
|
|
2414
|
-
await mutateBoundDirectory(directory, directoryIdentity, setupDeadline, async () => unlink(claimPath));
|
|
2415
|
-
await mutateBoundDirectory(directory, directoryIdentity, setupDeadline, async () => unlink(cursorPath));
|
|
2416
|
-
}
|
|
2417
|
-
continue;
|
|
2418
|
-
}
|
|
2419
|
-
const abort = new AbortController();
|
|
2420
|
-
const streamBudget = Math.min(remainingMs(setupDeadline), remainingMs(replayDeadline));
|
|
2421
|
-
const abortTimer = setTimeout(() => abort.abort(), Math.max(0, streamBudget));
|
|
2422
|
-
abortTimer.unref();
|
|
2423
|
-
const stream = createReadStream(claimPath, {
|
|
2424
|
-
autoClose: false,
|
|
2425
|
-
encoding: "utf-8",
|
|
2426
|
-
fd: opened.value.fd,
|
|
2427
|
-
signal: abort.signal,
|
|
2428
|
-
start: offset
|
|
2429
|
-
});
|
|
2430
|
-
const lines = createInterface({
|
|
2431
|
-
crlfDelay: Infinity,
|
|
2432
|
-
input: stream
|
|
2433
|
-
});
|
|
2434
|
-
let reachedEof = true;
|
|
2435
|
-
try {
|
|
2436
|
-
for await (const line of lines) {
|
|
2437
|
-
if (delivered >= maxEntries || remainingMs(replayDeadline) <= 0 || remainingMs(setupDeadline) <= 0) {
|
|
2438
|
-
reachedEof = false;
|
|
2439
|
-
break;
|
|
2440
|
-
}
|
|
2441
|
-
const nextOffset = Math.min(sourceMetadata.value.size, offset + Buffer.byteLength(line, "utf-8") + 1);
|
|
2442
|
-
if (line.trim().length === 0) {
|
|
2443
|
-
offset = nextOffset;
|
|
2444
|
-
if (!await writeLegacyCursor(directory, directoryIdentity, cursorPath, offset, setupDeadline)) {
|
|
2445
|
-
reachedEof = false;
|
|
2446
|
-
break;
|
|
2447
|
-
}
|
|
2448
|
-
continue;
|
|
2449
|
-
}
|
|
2450
|
-
let parsed;
|
|
2451
|
-
try {
|
|
2452
|
-
parsed = JSON.parse(line);
|
|
2453
|
-
} catch {
|
|
2454
|
-
corruptDropped += 1;
|
|
2455
|
-
offset = nextOffset;
|
|
2456
|
-
if (!await writeLegacyCursor(directory, directoryIdentity, cursorPath, offset, setupDeadline)) {
|
|
2457
|
-
reachedEof = false;
|
|
2458
|
-
break;
|
|
2459
|
-
}
|
|
2460
|
-
continue;
|
|
2461
|
-
}
|
|
2462
|
-
if (!isReplayableEntry(parsed)) {
|
|
2463
|
-
offset = nextOffset;
|
|
2464
|
-
if (!await writeLegacyCursor(directory, directoryIdentity, cursorPath, offset, setupDeadline)) {
|
|
2465
|
-
reachedEof = false;
|
|
2466
|
-
break;
|
|
2467
|
-
}
|
|
2468
|
-
continue;
|
|
2469
|
-
}
|
|
2470
|
-
const entryEndpoint = validatedEndpoint(parsed.endpoint);
|
|
2471
|
-
let handled = false;
|
|
2472
|
-
const row = {
|
|
2473
|
-
eventId: parsed.event.eventId,
|
|
2474
|
-
kind: parsed.event.kind
|
|
2475
|
-
};
|
|
2476
|
-
let rowOutcome = {
|
|
2477
|
-
detail: `recorded endpoint ${entryEndpoint?.origin ?? "(unusable)"} is not this repository's authorized HQ origin ${endpoint.origin}`,
|
|
2478
|
-
...row,
|
|
2479
|
-
spool: directory,
|
|
2480
|
-
status: "migrated"
|
|
2481
|
-
};
|
|
2482
|
-
if (entryEndpoint?.origin === endpoint.origin) {
|
|
2483
|
-
rowOutcome = {
|
|
2484
|
-
...rowOutcome,
|
|
2485
|
-
detail: "event could not be serialized for delivery"
|
|
2486
|
-
};
|
|
2487
|
-
let body;
|
|
2488
|
-
try {
|
|
2489
|
-
body = JSON.stringify(parsed.event);
|
|
2490
|
-
} catch {
|
|
2491
|
-
body = void 0;
|
|
2492
|
-
}
|
|
2493
|
-
if (body !== void 0) {
|
|
2494
|
-
const outcome = await attemptTransport(request, endpoint, clientId, clientSecret, body, Math.min(transportBudgetMs, remainingMs(replayDeadline)), report !== void 0);
|
|
2495
|
-
handled = outcome.ok;
|
|
2496
|
-
if (handled) delivered += 1;
|
|
2497
|
-
rowOutcome = outcome.ok ? {
|
|
2498
|
-
...row,
|
|
2499
|
-
spool: directory,
|
|
2500
|
-
status: outcome.duplicate === true ? "duplicate" : "delivered"
|
|
2501
|
-
} : {
|
|
2502
|
-
...row,
|
|
2503
|
-
detail: outcome.detail === void 0 ? outcome.reason : `${outcome.reason}: ${outcome.detail}`,
|
|
2504
|
-
spool: directory,
|
|
2505
|
-
status: drainFailureStatus(outcome.reason)
|
|
2506
|
-
};
|
|
2507
|
-
}
|
|
2508
|
-
}
|
|
2509
|
-
if (!handled) {
|
|
2510
|
-
const retained = await appendCloseoutSpool(writeLayout, parsed, performance.now() + journalFlushBudgetFor(dependencies));
|
|
2511
|
-
if (retained === void 0) options.onMigrate?.();
|
|
2512
|
-
if (retained !== void 0) {
|
|
2513
|
-
report?.({
|
|
2514
|
-
...rowOutcome,
|
|
2515
|
-
detail: `${rowOutcome.detail ?? rowOutcome.status}; migration into the current spool ${retained}`,
|
|
2516
|
-
status: "unreachable"
|
|
2517
|
-
});
|
|
2518
|
-
reachedEof = false;
|
|
2519
|
-
break;
|
|
2520
|
-
}
|
|
2521
|
-
}
|
|
2522
|
-
report?.(rowOutcome);
|
|
2523
|
-
offset = nextOffset;
|
|
2524
|
-
if (!await writeLegacyCursor(directory, directoryIdentity, cursorPath, offset, setupDeadline)) {
|
|
2525
|
-
reachedEof = false;
|
|
2526
|
-
break;
|
|
2527
|
-
}
|
|
2528
|
-
}
|
|
2529
|
-
} catch {
|
|
2530
|
-
reachedEof = false;
|
|
2531
|
-
} finally {
|
|
2532
|
-
clearTimeout(abortTimer);
|
|
2533
|
-
abort.abort();
|
|
2534
|
-
lines.close();
|
|
2535
|
-
stream.destroy();
|
|
2536
|
-
await settleWithin(opened.value.close(), remainingMs(setupDeadline));
|
|
2537
|
-
}
|
|
2538
|
-
if (reachedEof && offset >= sourceMetadata.value.size) {
|
|
2539
|
-
const current = await settleWithin(lstat(claimPath), remainingMs(setupDeadline));
|
|
2540
|
-
if (current.status === "fulfilled" && current.value.isFile() && !current.value.isSymbolicLink() && current.value.dev === sourceMetadata.value.dev && current.value.ino === sourceMetadata.value.ino && current.value.size === sourceMetadata.value.size && await directoryIdentityMatches(directory, directoryIdentity, setupDeadline)) {
|
|
2541
|
-
await mutateBoundDirectory(directory, directoryIdentity, setupDeadline, async () => unlink(claimPath));
|
|
2542
|
-
await mutateBoundDirectory(directory, directoryIdentity, setupDeadline, async () => unlink(cursorPath));
|
|
2543
|
-
}
|
|
2544
|
-
} else await releaseLegacyClaim(claimPath, target, directory, directoryIdentity, setupDeadline);
|
|
2545
|
-
}
|
|
2546
|
-
await reportReplayOutcome(dependencies, delivered, corruptDropped);
|
|
2547
|
-
}
|
|
2548
|
-
async function reportReplayOutcome(dependencies, delivered, corruptDropped) {
|
|
2549
|
-
if (delivered <= 0 && corruptDropped <= 0) return;
|
|
2550
|
-
const parts = [];
|
|
2551
|
-
if (delivered > 0) parts.push(`${delivered} queued event(s) delivered from the retry journal`);
|
|
2552
|
-
if (corruptDropped > 0) parts.push(`${corruptDropped} unparseable journal line(s) dropped (partial write recovered, #120)`);
|
|
2553
|
-
await reportDiagnostic(dependencies, performance.now() + journalFlushBudgetFor(dependencies), parts.join("; "), delivered > 0 ? "HQ ingest confirmed" : "HQ ingest recovered");
|
|
2554
|
-
}
|
|
2555
2305
|
async function deliverHqIngest(input, dependencies, setupDeadline, timeoutMs, transportBudgetMs) {
|
|
2556
2306
|
let config;
|
|
2557
2307
|
try {
|
|
@@ -2595,7 +2345,6 @@ async function deliverHqIngest(input, dependencies, setupDeadline, timeoutMs, tr
|
|
|
2595
2345
|
owner: input.profile.repository.owner,
|
|
2596
2346
|
repo: input.profile.repository.name
|
|
2597
2347
|
}, env);
|
|
2598
|
-
const readLayouts = [writeLayout, legacySpoolLayout(input.cwd)];
|
|
2599
2348
|
const now = dependencies.now ?? (() => /* @__PURE__ */ new Date());
|
|
2600
2349
|
const makeEventId = dependencies.randomUUID ?? randomUUID;
|
|
2601
2350
|
let event;
|
|
@@ -2666,10 +2415,7 @@ async function deliverHqIngest(input, dependencies, setupDeadline, timeoutMs, tr
|
|
|
2666
2415
|
throw new Error(reason);
|
|
2667
2416
|
}
|
|
2668
2417
|
const replayDeadline = performance.now() + timeoutMs;
|
|
2669
|
-
|
|
2670
|
-
await replayCloseoutSpool(readLayout, endpoint, clientId, clientSecret, request, transportBudgetMs, replayDeadline, replayDeadline, journalFlushBudgetFor(dependencies), persistedEventPath);
|
|
2671
|
-
await replayRetryJournal(readLayout, writeLayout, endpoint, clientId, clientSecret, request, dependencies, transportBudgetMs, replayDeadline, replayDeadline);
|
|
2672
|
-
}
|
|
2418
|
+
await replayCloseoutSpool(writeLayout, endpoint, clientId, clientSecret, request, transportBudgetMs, replayDeadline, replayDeadline, journalFlushBudgetFor(dependencies), persistedEventPath);
|
|
2673
2419
|
const outcome = await attemptTransport(request, endpoint, clientId, clientSecret, body, transportBudgetMs);
|
|
2674
2420
|
if (!outcome.ok) {
|
|
2675
2421
|
({reason, unconfirmed} = outcome);
|
|
@@ -2748,20 +2494,7 @@ async function awaitPendingHqIngest() {
|
|
|
2748
2494
|
const DEFAULT_HQ_FLUSH_BUDGET_MS = 600 * 1e3;
|
|
2749
2495
|
/** ENOENT is the only filesystem answer that means "this location is absent". */
|
|
2750
2496
|
const isMissingEntryError = (error) => typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
2751
|
-
const
|
|
2752
|
-
const journalLineTimestamps = (contents) => {
|
|
2753
|
-
const timestamps = [];
|
|
2754
|
-
for (const line of contents.split("\n")) {
|
|
2755
|
-
const trimmed = line.trim();
|
|
2756
|
-
if (trimmed === "") continue;
|
|
2757
|
-
try {
|
|
2758
|
-
const parsed = JSON.parse(trimmed);
|
|
2759
|
-
if (typeof parsed.failedAt === "string") timestamps.push(parsed.failedAt);
|
|
2760
|
-
} catch {}
|
|
2761
|
-
}
|
|
2762
|
-
return timestamps;
|
|
2763
|
-
};
|
|
2764
|
-
const countRemainingSpoolWork = async (spools, drainLayouts, deadline) => {
|
|
2497
|
+
const countRemainingSpoolWork = async (spools, deadline) => {
|
|
2765
2498
|
let remaining = 0;
|
|
2766
2499
|
let unlistableScans = 0;
|
|
2767
2500
|
let oldestMs;
|
|
@@ -2790,27 +2523,6 @@ const countRemainingSpoolWork = async (spools, drainLayouts, deadline) => {
|
|
|
2790
2523
|
if (probed.status === "fulfilled") noteTimestamp(probed.value.mtime.toISOString());
|
|
2791
2524
|
}
|
|
2792
2525
|
}
|
|
2793
|
-
for (const layout of drainLayouts) {
|
|
2794
|
-
const journalDir = path.dirname(layoutPath(layout));
|
|
2795
|
-
const journalListing = await settleWithin(readdir(journalDir), remainingMs(deadline));
|
|
2796
|
-
if (journalListing.status !== "fulfilled") {
|
|
2797
|
-
await countUnlistable(journalDir);
|
|
2798
|
-
continue;
|
|
2799
|
-
}
|
|
2800
|
-
const journalNames = journalListing.value.filter((name) => name === "hq-retry-journal.jsonl" || name.startsWith(`hq-retry-journal.jsonl.legacy-ready-`) || name.startsWith(`hq-retry-journal.jsonl.legacy-claim-`) || name.startsWith(`hq-retry-journal.jsonl.draining-`));
|
|
2801
|
-
for (const name of journalNames) {
|
|
2802
|
-
const journalFilePath = path.join(journalDir, name);
|
|
2803
|
-
const read = await settleWithin(readFile(journalFilePath, "utf-8"), remainingMs(deadline));
|
|
2804
|
-
if (read.status === "fulfilled") {
|
|
2805
|
-
remaining += journalRowCount(read.value);
|
|
2806
|
-
for (const timestamp of journalLineTimestamps(read.value)) noteTimestamp(timestamp);
|
|
2807
|
-
continue;
|
|
2808
|
-
}
|
|
2809
|
-
remaining += 1;
|
|
2810
|
-
const probed = await settleWithin(lstat(journalFilePath), remainingMs(deadline));
|
|
2811
|
-
if (probed.status === "fulfilled") noteTimestamp(probed.value.mtime.toISOString());
|
|
2812
|
-
}
|
|
2813
|
-
}
|
|
2814
2526
|
return {
|
|
2815
2527
|
...oldestMs === void 0 ? {} : { oldestQueuedAt: new Date(oldestMs).toISOString() },
|
|
2816
2528
|
remaining,
|
|
@@ -2831,10 +2543,10 @@ const DEFAULT_HQ_SPOOL_INSPECT_BUDGET_MS = 5e3;
|
|
|
2831
2543
|
*/
|
|
2832
2544
|
async function countHqSpoolWork(input, dependencies = {}) {
|
|
2833
2545
|
const env = dependencies.env ?? process.env;
|
|
2834
|
-
const layouts = input.explicitDirectories && input.explicitDirectories.length > 0 ? input.explicitDirectories.map(explicitSpoolLayout) : [repositorySpoolLayout(input.repository, env)
|
|
2546
|
+
const layouts = input.explicitDirectories && input.explicitDirectories.length > 0 ? input.explicitDirectories.map(explicitSpoolLayout) : [repositorySpoolLayout(input.repository, env)];
|
|
2835
2547
|
const deadline = performance.now() + (dependencies.budgetMs ?? DEFAULT_HQ_SPOOL_INSPECT_BUDGET_MS);
|
|
2836
2548
|
const paths = layouts.map((layout) => layoutPath(layout));
|
|
2837
|
-
const { oldestQueuedAt, remaining, unlistableScans } = await countRemainingSpoolWork(paths,
|
|
2549
|
+
const { oldestQueuedAt, remaining, unlistableScans } = await countRemainingSpoolWork(paths, deadline);
|
|
2838
2550
|
const existing = await Promise.all(paths.map(async (spool) => {
|
|
2839
2551
|
return (await settleWithin(lstat(spool), remainingMs(deadline))).status === "fulfilled" ? spool : void 0;
|
|
2840
2552
|
}));
|
|
@@ -3002,7 +2714,7 @@ async function sweepHqSpoolOrphans(input, dependencies = {}) {
|
|
|
3002
2714
|
});
|
|
3003
2715
|
continue;
|
|
3004
2716
|
}
|
|
3005
|
-
const counted = await countRemainingSpoolWork([spool],
|
|
2717
|
+
const counted = await countRemainingSpoolWork([spool], deadline);
|
|
3006
2718
|
if (counted.remaining === 0 && counted.unlistableScans === 0) continue;
|
|
3007
2719
|
orphans.push({
|
|
3008
2720
|
directory: spool,
|
|
@@ -3022,7 +2734,7 @@ async function sweepHqSpoolOrphans(input, dependencies = {}) {
|
|
|
3022
2734
|
});
|
|
3023
2735
|
continue;
|
|
3024
2736
|
}
|
|
3025
|
-
const counted = await countRemainingSpoolWork([spool],
|
|
2737
|
+
const counted = await countRemainingSpoolWork([spool], deadline);
|
|
3026
2738
|
if (counted.remaining === 0 && counted.unlistableScans === 0) continue;
|
|
3027
2739
|
orphans.push({
|
|
3028
2740
|
directory: spool,
|
|
@@ -3042,8 +2754,8 @@ async function sweepHqSpoolOrphans(input, dependencies = {}) {
|
|
|
3042
2754
|
* This is the deliberate counterpart to the sink's advisory emit path: the
|
|
3043
2755
|
* caller has already resolved credentials explicitly, so there is no cap, no
|
|
3044
2756
|
* daemon, and no background retry — one pass, bounded, over the repo-keyed
|
|
3045
|
-
* location
|
|
3046
|
-
*
|
|
2757
|
+
* location (or the operator's `--dir`). Delivery itself is the sink's own
|
|
2758
|
+
* replay, so there is exactly one POST path.
|
|
3047
2759
|
*/
|
|
3048
2760
|
async function flushHqSpool(input, dependencies = {}) {
|
|
3049
2761
|
const endpoint = validatedIngestEndpoint(input.endpoint);
|
|
@@ -3056,8 +2768,7 @@ async function flushHqSpool(input, dependencies = {}) {
|
|
|
3056
2768
|
journalFlushBudgetMs: dependencies.journalFlushBudgetMs
|
|
3057
2769
|
});
|
|
3058
2770
|
const deadline = performance.now() + (dependencies.budgetMs ?? DEFAULT_HQ_FLUSH_BUDGET_MS);
|
|
3059
|
-
const
|
|
3060
|
-
const layouts = input.explicitDirectories && input.explicitDirectories.length > 0 ? input.explicitDirectories.map(explicitSpoolLayout) : [writeLayout, legacySpoolLayout(input.cwd)];
|
|
2771
|
+
const layouts = input.explicitDirectories && input.explicitDirectories.length > 0 ? input.explicitDirectories.map(explicitSpoolLayout) : [repositorySpoolLayout(input.repository, env)];
|
|
3061
2772
|
const outcomeById = /* @__PURE__ */ new Map();
|
|
3062
2773
|
let rejectedFiles = 0;
|
|
3063
2774
|
let unreachableFiles = 0;
|
|
@@ -3066,31 +2777,13 @@ async function flushHqSpool(input, dependencies = {}) {
|
|
|
3066
2777
|
outcomeById.set(outcome.eventId, outcome);
|
|
3067
2778
|
dependencies.report?.(outcome);
|
|
3068
2779
|
};
|
|
3069
|
-
const journalReport = (outcome) => {
|
|
3070
|
-
if (outcome.status === "unreachable" && outcome.detail?.includes("migration into the current spool")) unreachableFiles += 1;
|
|
3071
|
-
record(outcome);
|
|
3072
|
-
};
|
|
3073
2780
|
const spoolReport = (outcome) => {
|
|
3074
2781
|
if (outcome.status === "rejected") rejectedFiles += 1;
|
|
3075
2782
|
if (outcome.status === "unreachable") unreachableFiles += 1;
|
|
3076
2783
|
record(outcome);
|
|
3077
2784
|
};
|
|
3078
2785
|
const spools = [];
|
|
3079
|
-
|
|
3080
|
-
const journalDrainOptions = {
|
|
3081
|
-
maxEntries: Number.POSITIVE_INFINITY,
|
|
3082
|
-
onMigrate: () => {
|
|
3083
|
-
migratedIntoWriteLayout = true;
|
|
3084
|
-
},
|
|
3085
|
-
report: journalReport
|
|
3086
|
-
};
|
|
3087
|
-
for (const layout of layouts) await replayRetryJournal(layout, writeLayout, endpoint, input.clientId, input.clientSecret, request, {
|
|
3088
|
-
env,
|
|
3089
|
-
fetch: request,
|
|
3090
|
-
journalFlushBudgetMs: flushBudgetMs
|
|
3091
|
-
}, transportBudgetMs, deadline, deadline, journalDrainOptions);
|
|
3092
|
-
const drainLayouts = migratedIntoWriteLayout && !layouts.some((layout) => layoutPath(layout) === layoutPath(writeLayout)) ? [...layouts, writeLayout] : layouts;
|
|
3093
|
-
for (const layout of drainLayouts) {
|
|
2786
|
+
for (const layout of layouts) {
|
|
3094
2787
|
await replayCloseoutSpool(layout, endpoint, input.clientId, input.clientSecret, request, transportBudgetMs, deadline, deadline, flushBudgetMs, void 0, {
|
|
3095
2788
|
maxEntries: Number.POSITIVE_INFINITY,
|
|
3096
2789
|
report: spoolReport
|
|
@@ -3100,7 +2793,7 @@ async function flushHqSpool(input, dependencies = {}) {
|
|
|
3100
2793
|
if ((await settleWithin(lstat(layoutPath(layout)), remainingMs(deadline))).status === "fulfilled") unsecurableSpools += 1;
|
|
3101
2794
|
} else spools.push(secured.spool);
|
|
3102
2795
|
}
|
|
3103
|
-
const { remaining, unlistableScans } = await countRemainingSpoolWork(spools,
|
|
2796
|
+
const { remaining, unlistableScans } = await countRemainingSpoolWork(spools, deadline);
|
|
3104
2797
|
const outcomes = [...outcomeById.values()];
|
|
3105
2798
|
const count = (status) => outcomes.filter((outcome) => outcome.status === status).length;
|
|
3106
2799
|
const rejected = count("rejected");
|
|
@@ -8860,8 +8553,24 @@ function positiveInteger(name) {
|
|
|
8860
8553
|
};
|
|
8861
8554
|
}
|
|
8862
8555
|
function resolveCwdOption(cwd) {
|
|
8863
|
-
|
|
8864
|
-
|
|
8556
|
+
const value = typeof cwd === "string" ? cwd : ".";
|
|
8557
|
+
if (value.trim() === "") throw new Error("--cwd requires a non-empty path; relative values resolve against the current working directory");
|
|
8558
|
+
return path.resolve(value);
|
|
8559
|
+
}
|
|
8560
|
+
function collectCwdOption(rawValue, previous) {
|
|
8561
|
+
if (previous === void 0) return rawValue;
|
|
8562
|
+
const previousResolved = resolveCwdOption(previous);
|
|
8563
|
+
const currentResolved = resolveCwdOption(rawValue);
|
|
8564
|
+
if (previousResolved !== currentResolved) throw new Error(`--cwd was passed multiple times with conflicting values: "${previous}" (resolves to ${previousResolved}) and "${rawValue}" (resolves to ${currentResolved})`);
|
|
8565
|
+
return rawValue;
|
|
8566
|
+
}
|
|
8567
|
+
function markCwdOptionDefault(command) {
|
|
8568
|
+
const option = command.options.find((candidate) => candidate.attributeName() === "cwd");
|
|
8569
|
+
if (option) {
|
|
8570
|
+
option.defaultValue = ".";
|
|
8571
|
+
option.defaultValueDescription = ".";
|
|
8572
|
+
}
|
|
8573
|
+
return command;
|
|
8865
8574
|
}
|
|
8866
8575
|
function resolveCheckoutPath(cwd, target) {
|
|
8867
8576
|
return path.resolve(cwd, target);
|
|
@@ -8884,7 +8593,7 @@ const renderReport = (record) => {
|
|
|
8884
8593
|
return `${lines.join("\n")}\n`;
|
|
8885
8594
|
};
|
|
8886
8595
|
function createBoundaryCheckCommand(output, action) {
|
|
8887
|
-
return new Command("boundary:check").description("Boundary readiness gate (T8): require a membership-fresh boundary-review proof at the declared closeout rung. Blocks closeout/enablement, never merge.").requiredOption("--epic <number>", "epic issue number carrying the factory-boundary manifest", positiveInteger("--epic")).option("--repo <owner/name>", "GitHub repository (default: resolved from --cwd)").option("--cwd <path>", "repository working directory",
|
|
8596
|
+
return markCwdOptionDefault(new Command("boundary:check").description("Boundary readiness gate (T8): require a membership-fresh boundary-review proof at the declared closeout rung. Blocks closeout/enablement, never merge.").requiredOption("--epic <number>", "epic issue number carrying the factory-boundary manifest", positiveInteger("--epic")).option("--repo <owner/name>", "GitHub repository (default: resolved from --cwd)").option("--cwd <path>", "repository working directory", collectCwdOption).option("--output <path>", "write an additional proof copy to this path").option("--profile <path>", "path to the project profile JSON file").option("--json", "print the full proof record as JSON").action(withGateTiming({
|
|
8888
8597
|
gate: "boundary:check",
|
|
8889
8598
|
resolveLedgerRoot: (options) => resolveCwdOption(options.cwd),
|
|
8890
8599
|
stderr: output.stderr
|
|
@@ -8923,7 +8632,7 @@ function createBoundaryCheckCommand(output, action) {
|
|
|
8923
8632
|
}
|
|
8924
8633
|
output.stdout.write(options.json ? `${JSON.stringify(record, null, 2)}\n` : renderReport(record));
|
|
8925
8634
|
if (record.status !== "ready") throw new Error(`boundary:check refused: ${record.blockingReasons.join("; ")}`);
|
|
8926
|
-
}));
|
|
8635
|
+
})));
|
|
8927
8636
|
}
|
|
8928
8637
|
//#endregion
|
|
8929
8638
|
//#region src/checkout-repository.ts
|
|
@@ -11526,7 +11235,7 @@ const runDemandWaive = (args, dependencies = {}) => {
|
|
|
11526
11235
|
* offers no way to declare a demand met, only to waive it on the record.
|
|
11527
11236
|
*/
|
|
11528
11237
|
function createDemandWaiveCommand(_output, action = runDemandWaive) {
|
|
11529
|
-
return new Command("demand:waive").description("Waive one resolved demand for one candidate, on the operator's identity, with the rationale recorded in the proof").requiredOption("--pr <number>", "pull request number", positiveInteger("--pr")).requiredOption("--demand <key>", "resolved demand key, e.g. review-rung:human, required-check:core, merge-freeze").requiredOption("--rationale <text>", "why this demand is being waived").option("--cwd <path>", "working directory to evaluate",
|
|
11238
|
+
return markCwdOptionDefault(new Command("demand:waive").description("Waive one resolved demand for one candidate, on the operator's identity, with the rationale recorded in the proof").requiredOption("--pr <number>", "pull request number", positiveInteger("--pr")).requiredOption("--demand <key>", "resolved demand key, e.g. review-rung:human, required-check:core, merge-freeze").requiredOption("--rationale <text>", "why this demand is being waived").option("--cwd <path>", "working directory to evaluate", collectCwdOption).option("--json", "print the recorded waiver as JSON").option("--output <path>", "waiver record JSON path").action((options) => {
|
|
11530
11239
|
action({
|
|
11531
11240
|
cwd: resolveCwdOption(options.cwd),
|
|
11532
11241
|
demand: options.demand,
|
|
@@ -11535,7 +11244,7 @@ function createDemandWaiveCommand(_output, action = runDemandWaive) {
|
|
|
11535
11244
|
pr: options.pr,
|
|
11536
11245
|
rationale: options.rationale
|
|
11537
11246
|
});
|
|
11538
|
-
});
|
|
11247
|
+
}));
|
|
11539
11248
|
}
|
|
11540
11249
|
//#endregion
|
|
11541
11250
|
//#region src/demand-resolution.ts
|
|
@@ -12128,10 +11837,10 @@ const describeOrphans = (orphans) => {
|
|
|
12128
11837
|
return ` ${orphans.length} spool location(s) hold this repository's evidence under an earlier key it no longer resolves to — a key-format change strands evidence there where no drain looks: ${described}. Drain each with \`psf hq:flush --dir <path>\`.`;
|
|
12129
11838
|
};
|
|
12130
11839
|
/**
|
|
12131
|
-
*
|
|
12132
|
-
*
|
|
12133
|
-
*
|
|
12134
|
-
*
|
|
11840
|
+
* Warns when HQ evidence is waiting for this repository: its own spool or a
|
|
11841
|
+
* sibling location written under an earlier key of its own that it no longer
|
|
11842
|
+
* resolves to (#420). Another repository's spool under the shared root is that
|
|
11843
|
+
* repository's business, never this check's (#446).
|
|
12135
11844
|
*
|
|
12136
11845
|
* Reuses `countHqSpoolWork` (#414) — the same read-only, credential-free
|
|
12137
11846
|
* inspection `hq:flush` itself consults before ever resolving a secret — so
|
|
@@ -12141,19 +11850,23 @@ const describeOrphans = (orphans) => {
|
|
|
12141
11850
|
* the spool resolve exactly one key, so evidence written under an older one is
|
|
12142
11851
|
* invisible to a drain and, before this, to doctor: the failure epic #389 was
|
|
12143
11852
|
* chartered to end is evidence that is neither delivered nor visibly stranded.
|
|
12144
|
-
* An orphan holding pending events
|
|
12145
|
-
* spool
|
|
11853
|
+
* An orphan holding pending events warns for the same reason the repo-keyed
|
|
11854
|
+
* spool does.
|
|
11855
|
+
*
|
|
11856
|
+
* `warning`, not `error` (#659, epic #663 wave 1): a spooled event is real
|
|
11857
|
+
* HQ-delivery lag, but doctor is a read-only diagnostic that must never gain a
|
|
11858
|
+
* flush side-effect, and a preflight gate that treats doctor's exit code as
|
|
11859
|
+
* the bar must not block admission on delivery lag it cannot fix from here.
|
|
11860
|
+
* `psf hq:flush` — a trusted local session, not doctor — is still how the
|
|
11861
|
+
* spool actually drains.
|
|
12146
11862
|
*/
|
|
12147
11863
|
async function hqSpoolDoctorCheck(input, dependencies = {}) {
|
|
12148
11864
|
const countSpool = dependencies.countSpool ?? countHqSpoolWork;
|
|
12149
11865
|
const sweepOrphans = dependencies.sweepOrphans ?? sweepHqSpoolOrphans;
|
|
12150
|
-
const [counted, swept] = await Promise.all([countSpool({
|
|
12151
|
-
cwd: input.cwd,
|
|
12152
|
-
repository: input.repository
|
|
12153
|
-
}, { env: input.env }), sweepOrphans({ repository: input.repository }, { env: input.env })]);
|
|
11866
|
+
const [counted, swept] = await Promise.all([countSpool({ repository: input.repository }, { env: input.env }), sweepOrphans({ repository: input.repository }, { env: input.env })]);
|
|
12154
11867
|
const orphanNote = swept.orphans.length === 0 ? "" : describeOrphans(swept.orphans);
|
|
12155
11868
|
if (counted.pending === 0 && counted.unlistable === 0 && swept.orphans.length === 0) return {
|
|
12156
|
-
message: "HQ spool
|
|
11869
|
+
message: "HQ spool is empty; no evidence is waiting to be drained.",
|
|
12157
11870
|
name: HQ_SPOOL_CHECK_NAME,
|
|
12158
11871
|
status: "ok"
|
|
12159
11872
|
};
|
|
@@ -12162,17 +11875,17 @@ async function hqSpoolDoctorCheck(input, dependencies = {}) {
|
|
|
12162
11875
|
if (counted.pending > 0) return {
|
|
12163
11876
|
message: `${counted.pending} HQ event(s) are spooled locally.${oldestNote}${locationsNote} ${FLUSH_REMEDY}${orphanNote}`,
|
|
12164
11877
|
name: HQ_SPOOL_CHECK_NAME,
|
|
12165
|
-
status: "
|
|
11878
|
+
status: "warning"
|
|
12166
11879
|
};
|
|
12167
11880
|
if (counted.unlistable > 0) return {
|
|
12168
11881
|
message: `${counted.unlistable} spool location(s) exist but could not be listed within budget, so this cannot be reported as empty.${locationsNote} ${FLUSH_REMEDY}${orphanNote}`,
|
|
12169
11882
|
name: HQ_SPOOL_CHECK_NAME,
|
|
12170
|
-
status: "
|
|
11883
|
+
status: "warning"
|
|
12171
11884
|
};
|
|
12172
11885
|
return {
|
|
12173
11886
|
message: `This repository's HQ spool is empty, but stranded evidence is waiting under the spool root ${swept.root}.${orphanNote}`,
|
|
12174
11887
|
name: HQ_SPOOL_CHECK_NAME,
|
|
12175
|
-
status: "
|
|
11888
|
+
status: "warning"
|
|
12176
11889
|
};
|
|
12177
11890
|
}
|
|
12178
11891
|
const readJsonFiles = async (dir, readFileImpl) => {
|
|
@@ -12357,7 +12070,6 @@ async function doctorProjectProfile(input = {}) {
|
|
|
12357
12070
|
const cwd = input.cwd ?? process.cwd();
|
|
12358
12071
|
const userConfig = resolveDoctorUserConfig(env, input.userConfig);
|
|
12359
12072
|
const [hqSpoolCheck, hqRetroReadbackCheck] = await Promise.all([hqSpoolDoctorCheck({
|
|
12360
|
-
cwd,
|
|
12361
12073
|
env,
|
|
12362
12074
|
repository: {
|
|
12363
12075
|
owner: profile.repository.owner,
|
|
@@ -12524,7 +12236,7 @@ function repositoryUrlMatches(origin, profile) {
|
|
|
12524
12236
|
//#endregion
|
|
12525
12237
|
//#region src/commands/doctor.ts
|
|
12526
12238
|
function createDoctorCommand(output) {
|
|
12527
|
-
return new Command("doctor").description("Validate a project profile without mutating GitHub or local files").option("--base <ref>", "base branch or ref for --preflight", "origin/main").option("--cwd <path>", "working directory to validate",
|
|
12239
|
+
return markCwdOptionDefault(new Command("doctor").description("Validate a project profile without mutating GitHub or local files").option("--base <ref>", "base branch or ref for --preflight", "origin/main").option("--cwd <path>", "working directory to validate", collectCwdOption).option("--json", "print the doctor report as JSON").option("--preflight", "also list the pre-checkable admission requirements for the current candidate (read-only; never blocks); findings-file shape and wave membership have no read-only pre-check").option("--profile <path>", "path to the project profile JSON file").action(async (options) => {
|
|
12528
12240
|
const report = await doctorProjectProfile({
|
|
12529
12241
|
base: options.base,
|
|
12530
12242
|
cwd: resolveCwdOption(options.cwd),
|
|
@@ -12534,7 +12246,7 @@ function createDoctorCommand(output) {
|
|
|
12534
12246
|
if (options.json) output.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
12535
12247
|
else output.stdout.write(formatHumanReport(report));
|
|
12536
12248
|
if (!report.ok) process.exitCode = 1;
|
|
12537
|
-
});
|
|
12249
|
+
}));
|
|
12538
12250
|
}
|
|
12539
12251
|
function formatHumanReport(report) {
|
|
12540
12252
|
return [
|
|
@@ -12942,7 +12654,7 @@ function optionalString(key, flagValue, stdinValue) {
|
|
|
12942
12654
|
return resolved ? { [key]: resolved } : {};
|
|
12943
12655
|
}
|
|
12944
12656
|
function createEvidenceEmitCommand(output, action = runEvidenceEmit, readInput = readStdin) {
|
|
12945
|
-
return new Command("evidence:emit").description("Build, validate, and write an external-evidence envelope (ADR 0014)").option("--cwd <path>", "repository working directory",
|
|
12657
|
+
return markCwdOptionDefault(new Command("evidence:emit").description("Build, validate, and write an external-evidence envelope (ADR 0014)").option("--cwd <path>", "repository working directory", collectCwdOption).option("--base <ref>", "base branch or ref for the three-way binding", "origin/main").option("--check <name>", "declared requiredChecks name this evidence satisfies").option("--outcome <pass|fail>", "the check outcome").option("--producer <id>", "the producing identity (recorded, not gated)").option("--policy-version <version>", "canonical prompt / Warden rules version").option("--session-id <id>", "producer session id (independence join key)").option("--request-id <id>", "producer request id (join key)").option("--findings-pointer <ref>", "pointer to findings (never embedded)").option("--profile <path>", "path to the project profile JSON file (accepted for a uniform consumer-wrapper flag set and loaded strictly when supplied)").option("--output <path>", "write the envelope to a specific path").option("--stdin", "merge JSON field values from stdin (flags win)").option("--json", "print the written envelope as JSON").action((options) => {
|
|
12946
12658
|
const cwd = resolveCwdOption(options.cwd);
|
|
12947
12659
|
const fromStdin = options.stdin ? readInput() : {};
|
|
12948
12660
|
rejectRetiredStdinFields(fromStdin);
|
|
@@ -12969,7 +12681,7 @@ function createEvidenceEmitCommand(output, action = runEvidenceEmit, readInput =
|
|
|
12969
12681
|
});
|
|
12970
12682
|
output.stdout.write(`evidence:emit wrote envelope for "${result.envelope.check}" to ${result.path}\n`);
|
|
12971
12683
|
if (options.json && "envelope" in result) output.stdout.write(`${JSON.stringify(result.envelope, null, 2)}\n`);
|
|
12972
|
-
});
|
|
12684
|
+
}));
|
|
12973
12685
|
}
|
|
12974
12686
|
//#endregion
|
|
12975
12687
|
//#region src/hq-flush.ts
|
|
@@ -13035,7 +12747,6 @@ async function runHqFlush(args, dependencies = {}) {
|
|
|
13035
12747
|
};
|
|
13036
12748
|
const explicitDirectories = args.dir && args.dir.length > 0 ? { explicitDirectories: args.dir } : {};
|
|
13037
12749
|
const pending = await (dependencies.countSpool ?? countHqSpoolWork)({
|
|
13038
|
-
cwd: args.cwd,
|
|
13039
12750
|
...explicitDirectories,
|
|
13040
12751
|
repository
|
|
13041
12752
|
}, { env });
|
|
@@ -13080,7 +12791,6 @@ async function runHqFlush(args, dependencies = {}) {
|
|
|
13080
12791
|
...await (dependencies.flush ?? flushHqSpool)({
|
|
13081
12792
|
clientId: resolution.credentials.clientId,
|
|
13082
12793
|
clientSecret: resolution.credentials.clientSecret,
|
|
13083
|
-
cwd: args.cwd,
|
|
13084
12794
|
endpoint: profile.hq.endpoint,
|
|
13085
12795
|
...explicitDirectories,
|
|
13086
12796
|
repository
|
|
@@ -13107,7 +12817,7 @@ function renderHqFlush(result) {
|
|
|
13107
12817
|
`hq:flush ${result.endpoint}`,
|
|
13108
12818
|
...result.spools.length === 0 ? ["no spool directory found"] : result.spools.map((spool) => `spool: ${spool}`),
|
|
13109
12819
|
...result.outcomes.map(outcomeLine),
|
|
13110
|
-
`delivered ${result.delivered}, duplicate ${result.duplicate}, rejected ${result.rejected}, undeliverable ${result.undeliverable}, unreachable ${result.unreachable}
|
|
12820
|
+
`delivered ${result.delivered}, duplicate ${result.duplicate}, rejected ${result.rejected}, undeliverable ${result.undeliverable}, unreachable ${result.unreachable}`,
|
|
13111
12821
|
...result.undeliverable > 0 ? [`${result.undeliverable} event(s) can never be delivered and were dispositioned in place, renamed with \`.undeliverable\` and left readable; they no longer count as work waiting.`] : [],
|
|
13112
12822
|
result.incomplete ? `INCOMPLETE: ${result.remaining} event(s) still spooled; the drain did not finish. Run hq:flush again.` : `spool drained: ${result.remaining} event(s) remain (rejections stay until HQ accepts them)`,
|
|
13113
12823
|
...orphanLines(result.orphans)
|
|
@@ -13134,7 +12844,7 @@ const hqFlushExitCode = (result) => {
|
|
|
13134
12844
|
//#endregion
|
|
13135
12845
|
//#region src/commands/hq-flush.ts
|
|
13136
12846
|
function createHqFlushCommand(output, action = runHqFlush) {
|
|
13137
|
-
return new Command("hq:flush").description("Drain this repository's spooled HQ evidence and report every event; exits 1 when transport failed and 2 when the drain did not finish").option("--cwd <path>", "working directory to evaluate",
|
|
12847
|
+
return markCwdOptionDefault(new Command("hq:flush").description("Drain this repository's spooled HQ evidence and report every event; exits 1 when transport failed and 2 when the drain did not finish").option("--cwd <path>", "working directory to evaluate", collectCwdOption).option("--dir <path>", "drain an explicit spool directory instead of the default locations; repeatable", (value, previous = []) => [...previous, value]).option("--json", "print the flush result as JSON").option("--profile <path>", "path to the project profile JSON file").action(async (options) => {
|
|
13138
12848
|
const result = await action({
|
|
13139
12849
|
cwd: resolveCwdOption(options.cwd),
|
|
13140
12850
|
...options.dir ? { dir: options.dir } : {},
|
|
@@ -13144,7 +12854,7 @@ function createHqFlushCommand(output, action = runHqFlush) {
|
|
|
13144
12854
|
output.stdout.write(options.json ? `${JSON.stringify(result, null, 2)}\n` : renderHqFlush(result));
|
|
13145
12855
|
const exitCode = hqFlushExitCode(result);
|
|
13146
12856
|
if (exitCode !== 0) process.exitCode = exitCode;
|
|
13147
|
-
});
|
|
12857
|
+
}));
|
|
13148
12858
|
}
|
|
13149
12859
|
//#endregion
|
|
13150
12860
|
//#region src/follow-up.ts
|
|
@@ -16007,7 +15717,7 @@ async function deliverSpooledEvidence({ awaitPending, cwd, flush, profilePath })
|
|
|
16007
15717
|
//#endregion
|
|
16008
15718
|
//#region src/commands/pr-publish.ts
|
|
16009
15719
|
function createPrPublishCommand(_output, action = runPrPublish) {
|
|
16010
|
-
return new Command("pr:publish").description("Compose supplied proof into a ready-for-human handoff; never launches review").option("--pr <number>", "pull request number", positiveInteger("--pr")).option("--base <ref>", "explicit base branch or ref override").option("--authoring-session <id>", "known authoring session identity for verification retries").option("--cwd <path>", "working directory to evaluate",
|
|
15720
|
+
return markCwdOptionDefault(new Command("pr:publish").description("Compose supplied proof into a ready-for-human handoff; never launches review").option("--pr <number>", "pull request number", positiveInteger("--pr")).option("--base <ref>", "explicit base branch or ref override").option("--authoring-session <id>", "known authoring session identity for verification retries").option("--cwd <path>", "working directory to evaluate", collectCwdOption).option("--epic <number>", "epic issue containing the factory-boundary manifest; the composed readiness evaluation then applies the wave-demanded review rung (#351)", positiveInteger("--epic")).option("--findings <path>", "clean-session findings to validate when no current review proof is available").option("--json", "print the publish result as JSON").option("--output <path>", "write readiness proof JSON to a file").option("--profile <path>", "path to the project profile JSON file").option("--review-proof <path>", "pr:review proof JSON path").option("--verify-proof <path>", "pr:verify proof JSON path").action(async (options) => {
|
|
16011
15721
|
await action({
|
|
16012
15722
|
authoringSessionIds: options.authoringSession ? [options.authoringSession] : void 0,
|
|
16013
15723
|
base: options.base ?? "",
|
|
@@ -16021,12 +15731,12 @@ function createPrPublishCommand(_output, action = runPrPublish) {
|
|
|
16021
15731
|
reviewProof: options.reviewProof,
|
|
16022
15732
|
verifyProof: options.verifyProof
|
|
16023
15733
|
});
|
|
16024
|
-
});
|
|
15734
|
+
}));
|
|
16025
15735
|
}
|
|
16026
15736
|
//#endregion
|
|
16027
15737
|
//#region src/commands/pr-ready.ts
|
|
16028
15738
|
function createPrReadyCommand(output, action) {
|
|
16029
|
-
return new Command("pr:ready").description("Evaluate typed proof and GitHub state for final readiness").requiredOption("--pr <number>", "pull request number", positiveInteger("--pr")).option("--base <ref>", "explicit base branch or ref override").option("--authoring-session <id...>", "recorded authoring session id(s) for review-type requiredCheck independence (ADR 0014 §5)").option("--cwd <path>", "working directory to evaluate",
|
|
15739
|
+
return markCwdOptionDefault(new Command("pr:ready").description("Evaluate typed proof and GitHub state for final readiness").requiredOption("--pr <number>", "pull request number", positiveInteger("--pr")).option("--base <ref>", "explicit base branch or ref override").option("--authoring-session <id...>", "recorded authoring session id(s) for review-type requiredCheck independence (ADR 0014 §5)").option("--cwd <path>", "working directory to evaluate", collectCwdOption).option("--epic <number>", "epic issue containing the factory-boundary manifest; readiness then evaluates the wave-demanded review rung (#351)", positiveInteger("--epic")).option("--json", "print the readiness proof as JSON").option("--output <path>", "write proof JSON to a file").option("--profile <path>", "path to the project profile JSON file").option("--review-proof <path>", "pr:review proof JSON path").option("--verify-proof <path>", "pr:verify proof JSON path").action(withGateTiming({
|
|
16030
15740
|
gate: "pr:ready",
|
|
16031
15741
|
resolveLedgerRoot: (options) => resolveCwdOption(options.cwd),
|
|
16032
15742
|
stderr: output.stderr
|
|
@@ -16044,7 +15754,7 @@ function createPrReadyCommand(output, action) {
|
|
|
16044
15754
|
verifyProof: options.verifyProof
|
|
16045
15755
|
};
|
|
16046
15756
|
await (action ? action(args) : runPrReady(args));
|
|
16047
|
-
}));
|
|
15757
|
+
})));
|
|
16048
15758
|
}
|
|
16049
15759
|
//#endregion
|
|
16050
15760
|
//#region src/gate-foreground-guard.ts
|
|
@@ -16073,7 +15783,7 @@ function assertForegroundGate({ gate, env = process.env }) {
|
|
|
16073
15783
|
//#endregion
|
|
16074
15784
|
//#region src/commands/pr-review.ts
|
|
16075
15785
|
function createPrReviewCommand(output, action) {
|
|
16076
|
-
return new Command("pr:review").description("Validate clean-session findings and write typed review proof").option("--base <ref>", "base branch or ref", "origin/main").option("--cwd <path>", "working directory to review",
|
|
15786
|
+
return markCwdOptionDefault(new Command("pr:review").description("Validate clean-session findings and write typed review proof").option("--base <ref>", "base branch or ref", "origin/main").option("--cwd <path>", "working directory to review", collectCwdOption).option("--cycle <number>", "review cycle number", positiveInteger("--cycle"), 1).option("--mode <mode>", "correctness, security, or all", "all").option("--findings <path>", "typed clean-session findings JSON file").option("--dispositions <path>", "JSON file of ladder disposition declarations (waived / follow-up-filed / fixed-in-thread) for previously flagged findings").option("--issue <number>", "issue number recorded on the review ladder trace", positiveInteger("--issue")).option("--output <path>", "write proof JSON to a file").option("--profile <path>", "path to the project profile JSON file").option("--verify-proof <path>", "pr:verify proof JSON path").action(withGateTiming({
|
|
16077
15787
|
gate: "pr:review",
|
|
16078
15788
|
resolveCycle: (options) => options.cycle,
|
|
16079
15789
|
resolveLedgerRoot: (options) => resolveCwdOption(options.cwd),
|
|
@@ -16098,12 +15808,12 @@ function createPrReviewCommand(output, action) {
|
|
|
16098
15808
|
verifyProof: options.verifyProof
|
|
16099
15809
|
};
|
|
16100
15810
|
await (action ? action(args) : runPrReview(args, { publishCheckRun: createFactoryCheckPublisher(output) }));
|
|
16101
|
-
}));
|
|
15811
|
+
})));
|
|
16102
15812
|
}
|
|
16103
15813
|
//#endregion
|
|
16104
15814
|
//#region src/commands/pr-verify.ts
|
|
16105
15815
|
function createPrVerifyCommand(output, action = runPrVerify) {
|
|
16106
|
-
return new Command("pr:verify").description("Run project-profile verification commands and write typed proof").option("--base <ref>", "base branch or ref", "origin/main").option("--cwd <path>", "working directory to verify",
|
|
15816
|
+
return markCwdOptionDefault(new Command("pr:verify").description("Run project-profile verification commands and write typed proof").option("--base <ref>", "base branch or ref", "origin/main").option("--cwd <path>", "working directory to verify", collectCwdOption).option("--docs-only", "run the docs/process verification gate").option("--trivial", "run the trivial verification gate").option("--full", "run the full verification gate").option("--authoring-session <id>", "explicit authoring session identity (required for review evidence)").option("--json", "print the proof as JSON after verification").option("--output <path>", "write proof JSON to a file").option("--profile <path>", "path to the project profile JSON file").option("--no-status", "skip posting the patronage-factory/pr-verify commit status").action(withGateTiming({
|
|
16107
15817
|
gate: "pr:verify",
|
|
16108
15818
|
resolveLedgerRoot: (options) => resolveCwdOption(options.cwd),
|
|
16109
15819
|
stderr: output.stderr
|
|
@@ -16125,7 +15835,7 @@ function createPrVerifyCommand(output, action = runPrVerify) {
|
|
|
16125
15835
|
requireKnownAuthoringSession: true
|
|
16126
15836
|
}, dependencies);
|
|
16127
15837
|
if (options.json) output.stdout.write(`${JSON.stringify(proof, null, 2)}\n`);
|
|
16128
|
-
}));
|
|
15838
|
+
})));
|
|
16129
15839
|
}
|
|
16130
15840
|
function modeFor(options) {
|
|
16131
15841
|
if (options.docsOnly) return "docs-only";
|
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.7",
|
|
4
4
|
"description": "Shared Patronage software factory CLI and project-profile validation tools",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"picomatch": "^4.0.5",
|
|
38
38
|
"yaml": "^2.9.0",
|
|
39
39
|
"zod": "4.4.3",
|
|
40
|
-
"@patronage/factory-ci": "1.0.0-alpha.
|
|
40
|
+
"@patronage/factory-ci": "1.0.0-alpha.7"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "24.13.3",
|
|
@@ -77,6 +77,8 @@
|
|
|
77
77
|
"profile:schema": "tsx scripts/generate-profile-schema.ts",
|
|
78
78
|
"pretest": "bash ../scripts/ensure-worktree-bootstrap.sh",
|
|
79
79
|
"test": "vitest run",
|
|
80
|
+
"pretest:profile": "bash ../scripts/ensure-worktree-bootstrap.sh",
|
|
81
|
+
"test:profile": "tsx scripts/vitest-profile.ts",
|
|
80
82
|
"pretest:stress": "bash ../scripts/ensure-worktree-bootstrap.sh",
|
|
81
83
|
"test:stress": "node scripts/stress-tests.mjs",
|
|
82
84
|
"pretypecheck": "bash ../scripts/ensure-worktree-bootstrap.sh",
|