@patronage/software-factory 0.30.0-beta.1 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-DrSxFLj_.js → chunk-pbuEa-1d.js} +0 -1
- package/dist/index.d.ts +17 -48
- package/dist/index.js +721 -1688
- package/dist/schemas.d.ts +407 -4
- package/dist/schemas.js +68 -15
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as __exportAll } from "./chunk-
|
|
3
|
-
import { createRequire } from "node:module";
|
|
2
|
+
import { t as __exportAll } from "./chunk-pbuEa-1d.js";
|
|
4
3
|
import { appendFileSync, constants, cpSync, createReadStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
5
4
|
import { pathToFileURL } from "node:url";
|
|
6
5
|
import { Command, InvalidArgumentError } from "commander";
|
|
@@ -18,7 +17,7 @@ import picomatch from "picomatch";
|
|
|
18
17
|
import { promisify } from "node:util";
|
|
19
18
|
import { parse } from "yaml";
|
|
20
19
|
//#region package.json
|
|
21
|
-
var version = "0.30.0
|
|
20
|
+
var version = "0.30.0";
|
|
22
21
|
//#endregion
|
|
23
22
|
//#region src/review-rungs.ts
|
|
24
23
|
const EVIDENCE_REVIEW_RUNGS$1 = [
|
|
@@ -931,12 +930,16 @@ const requiredCheckInScope = ({ context, scope }) => {
|
|
|
931
930
|
//#region src/profile.ts
|
|
932
931
|
const DEFAULT_FACTORY_REPOSITORY = "unknown/unknown";
|
|
933
932
|
/**
|
|
934
|
-
* The shapes
|
|
935
|
-
* an encounter with
|
|
933
|
+
* The shapes v5 replaced. Not loadable — {@link legacyProfileMigration} turns
|
|
934
|
+
* an encounter with one into a per-field migration error rather than a
|
|
936
935
|
* schema complaint about a shape the profile was never written in.
|
|
937
936
|
*/
|
|
938
|
-
const LEGACY_PROFILE_SCHEMA_VERSIONS = [
|
|
939
|
-
|
|
937
|
+
const LEGACY_PROFILE_SCHEMA_VERSIONS = [
|
|
938
|
+
2,
|
|
939
|
+
3,
|
|
940
|
+
4
|
|
941
|
+
];
|
|
942
|
+
const PROFILE_JSON_SCHEMA_DESCRIPTION = "Editor validation covers the profile input shape and straightforward cross-field constraints. Zod remains the load-time enforcement authority: verification command names must be distinct and requiredChecks entries must have distinct names; these invariants are enforced at load time, not by this schema.";
|
|
940
943
|
const globSchema = z.string().min(1).refine(isValidGlob, "must be a valid glob");
|
|
941
944
|
const commandSchema = z.object({
|
|
942
945
|
command: z.string().min(1),
|
|
@@ -949,11 +952,7 @@ const commandSchema = z.object({
|
|
|
949
952
|
"full"
|
|
950
953
|
]).default("full")
|
|
951
954
|
});
|
|
952
|
-
const
|
|
953
|
-
const conditionalReviewSchema = z.object({
|
|
954
|
-
modes: z.array(reviewModeSchema).min(1),
|
|
955
|
-
paths: z.array(z.string().min(1).refine(isValidGlob, "must be a valid glob")).min(1)
|
|
956
|
-
}).strict();
|
|
955
|
+
const conditionalReviewSchema = z.object({ paths: z.array(z.string().min(1).refine(isValidGlob, "must be a valid glob")).min(1) }).strict();
|
|
957
956
|
const hqEndpointDeclarationSchema = z.intersection(z.string().url(), z.string().regex(/^https:\/\/[^/@\s]+(?:[/?#]|$)/u, "Must use HTTPS without embedded credentials"));
|
|
958
957
|
const hqIngestConfigSchema = z.object({
|
|
959
958
|
enabled: z.boolean(),
|
|
@@ -967,22 +966,18 @@ const hqIngestConfigSchema = z.object({
|
|
|
967
966
|
}, "Must be an HTTPS origin without a path, query, fragment, or embedded credentials").describe("Repository-declared HQ origin (scheme and host only; the factory composes the ingest path). Credential delivery additionally requires exact-origin authorization in the operator user config hqAllowedOrigins list.")
|
|
968
967
|
}).strict();
|
|
969
968
|
const requiredCheckSchema = z.object({
|
|
970
|
-
checkType: z.enum(EVIDENCE_CHECK_TYPES$1),
|
|
971
969
|
name: z.string().min(1),
|
|
972
970
|
scope: requiredCheckScopeSchema$1.optional()
|
|
973
971
|
}).strict();
|
|
974
972
|
const factoryProjectProfileSchema = z.object({
|
|
975
973
|
$schema: z.string().min(1).optional(),
|
|
976
|
-
env: z.object({ required: z.array(z.string().regex(/^[A-Z_][A-Z0-9_]*$/u)) }).strict().optional(),
|
|
977
974
|
extensions: z.record(z.string(), z.unknown()).optional(),
|
|
978
975
|
hq: hqIngestConfigSchema.optional(),
|
|
979
|
-
project: z.object({ key: z.string().min(1) }).strict(),
|
|
980
976
|
proof: z.object({ classificationPolicy: z.object({
|
|
981
977
|
docsOnly: z.array(globSchema).default([]),
|
|
982
978
|
trivial: z.array(globSchema).default([])
|
|
983
979
|
}).strict() }).strict(),
|
|
984
980
|
repository: z.object({
|
|
985
|
-
defaultBranch: z.string().min(1),
|
|
986
981
|
name: z.string().min(1),
|
|
987
982
|
owner: z.string().min(1)
|
|
988
983
|
}).strict(),
|
|
@@ -993,12 +988,8 @@ const factoryProjectProfileSchema = z.object({
|
|
|
993
988
|
message: "requiredChecks entries must have distinct names."
|
|
994
989
|
});
|
|
995
990
|
}).optional(),
|
|
996
|
-
review: z.object({
|
|
997
|
-
|
|
998
|
-
defaultMaxCycles: z.number().int().positive(),
|
|
999
|
-
modes: z.array(reviewModeSchema).min(1)
|
|
1000
|
-
}).strict(),
|
|
1001
|
-
schemaVersion: z.literal(4),
|
|
991
|
+
review: z.object({ conditional: z.array(conditionalReviewSchema).min(1) }).strict().optional(),
|
|
992
|
+
schemaVersion: z.literal(5),
|
|
1002
993
|
verification: z.object({ commands: z.array(commandSchema).min(1) })
|
|
1003
994
|
}).strict().superRefine((profile, context) => {
|
|
1004
995
|
const commandNames = /* @__PURE__ */ new Set();
|
|
@@ -1019,8 +1010,7 @@ const factoryProjectProfileSchema = z.object({
|
|
|
1019
1010
|
const mappedChecks = /* @__PURE__ */ new Set();
|
|
1020
1011
|
for (const [commandIndex, command] of profile.verification.commands.entries()) {
|
|
1021
1012
|
if (!command.requiredCheck) continue;
|
|
1022
|
-
|
|
1023
|
-
if (!requiredCheck) {
|
|
1013
|
+
if (!requiredChecks.get(command.requiredCheck)) {
|
|
1024
1014
|
context.addIssue({
|
|
1025
1015
|
code: "custom",
|
|
1026
1016
|
message: `Verification command requiredCheck "${command.requiredCheck}" does not name a declared requiredChecks entry.`,
|
|
@@ -1033,16 +1023,6 @@ const factoryProjectProfileSchema = z.object({
|
|
|
1033
1023
|
});
|
|
1034
1024
|
continue;
|
|
1035
1025
|
}
|
|
1036
|
-
if (requiredCheck.checkType !== "verify") context.addIssue({
|
|
1037
|
-
code: "custom",
|
|
1038
|
-
message: `Verification command requiredCheck "${command.requiredCheck}" must target a verify requiredCheck.`,
|
|
1039
|
-
path: [
|
|
1040
|
-
"verification",
|
|
1041
|
-
"commands",
|
|
1042
|
-
commandIndex,
|
|
1043
|
-
"requiredCheck"
|
|
1044
|
-
]
|
|
1045
|
-
});
|
|
1046
1026
|
if (mappedChecks.has(command.requiredCheck)) context.addIssue({
|
|
1047
1027
|
code: "custom",
|
|
1048
1028
|
message: `Required check "${command.requiredCheck}" is mapped by more than one verification command.`,
|
|
@@ -1055,16 +1035,6 @@ const factoryProjectProfileSchema = z.object({
|
|
|
1055
1035
|
});
|
|
1056
1036
|
mappedChecks.add(command.requiredCheck);
|
|
1057
1037
|
}
|
|
1058
|
-
for (const [scopeIndex, conditional] of (profile.review.conditional ?? []).entries()) for (const mode of conditional.modes) if (!profile.review.modes.includes(mode)) context.addIssue({
|
|
1059
|
-
code: "custom",
|
|
1060
|
-
message: `Conditional review mode "${mode}" must also be declared in review.modes.`,
|
|
1061
|
-
path: [
|
|
1062
|
-
"review",
|
|
1063
|
-
"conditional",
|
|
1064
|
-
scopeIndex,
|
|
1065
|
-
"modes"
|
|
1066
|
-
]
|
|
1067
|
-
});
|
|
1068
1038
|
}).meta({ description: PROFILE_JSON_SCHEMA_DESCRIPTION });
|
|
1069
1039
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1070
1040
|
const stringList = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
@@ -1127,11 +1097,35 @@ function v2MigrationLines(input, root) {
|
|
|
1127
1097
|
} catch {}
|
|
1128
1098
|
return lines;
|
|
1129
1099
|
}
|
|
1100
|
+
function hasCorrectnessOnlyReview(review) {
|
|
1101
|
+
const { modes } = review;
|
|
1102
|
+
return Object.keys(review).length > 0 && Object.keys(review).every((field) => field === "conditional" || field === "defaultMaxCycles" || field === "modes") && Array.isArray(modes) && modes.length > 0 && modes.every((mode) => mode === "correctness") && (review.conditional === void 0 || Array.isArray(review.conditional) && review.conditional.length === 0);
|
|
1103
|
+
}
|
|
1104
|
+
const v4MigrationLines = (input) => {
|
|
1105
|
+
const lines = [];
|
|
1106
|
+
if (isRecord(input.project) && "key" in input.project) lines.push("project.key -> removed; current proof writers derive projectKey from repository.name");
|
|
1107
|
+
if (isRecord(input.repository) && "defaultBranch" in input.repository) lines.push("repository.defaultBranch -> removed; commands resolve the live base from Git/GitHub or an explicit --base");
|
|
1108
|
+
if (isRecord(input.env) && "required" in input.env) lines.push("env.required -> removed; the command that needs an environment value owns its fail-closed diagnostic");
|
|
1109
|
+
if (isRecord(input.review)) {
|
|
1110
|
+
const isCorrectnessOnlyReview = hasCorrectnessOnlyReview(input.review);
|
|
1111
|
+
if ("defaultMaxCycles" in input.review) lines.push("review.defaultMaxCycles -> removed; the factory review cap is 5");
|
|
1112
|
+
if ("modes" in input.review) lines.push("review.modes -> removed; correctness is always required");
|
|
1113
|
+
if (isCorrectnessOnlyReview) lines.push("review -> removed; a correctness-only review has no v5 configuration, so delete the object");
|
|
1114
|
+
if (Array.isArray(input.review.conditional)) {
|
|
1115
|
+
for (const [index, conditional] of input.review.conditional.entries()) if (isRecord(conditional) && "modes" in conditional) lines.push(`review.conditional[${index}].modes -> removed; each retained conditional path list demands security review`);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
if (Array.isArray(input.requiredChecks)) {
|
|
1119
|
+
for (const [index, check] of input.requiredChecks.entries()) if (isRecord(check) && "checkType" in check) lines.push(`requiredChecks[${index}].checkType -> removed; external required checks are verify-type`);
|
|
1120
|
+
}
|
|
1121
|
+
return lines;
|
|
1122
|
+
};
|
|
1130
1123
|
function legacyProfileMigration(input, root) {
|
|
1131
1124
|
if (!(isRecord(input) && typeof input.schemaVersion === "number" && LEGACY_PROFILE_SCHEMA_VERSIONS.includes(input.schemaVersion))) return;
|
|
1132
1125
|
const foundVersion = input.schemaVersion;
|
|
1133
|
-
const lines = [`schemaVersion ${foundVersion} ->
|
|
1126
|
+
const lines = [`schemaVersion ${foundVersion} -> 5`];
|
|
1134
1127
|
if (foundVersion === 2) lines.push(...v2MigrationLines(input, root));
|
|
1128
|
+
lines.push(...v4MigrationLines(input));
|
|
1135
1129
|
return {
|
|
1136
1130
|
foundVersion,
|
|
1137
1131
|
lines
|
|
@@ -1198,7 +1192,7 @@ function hqLaneRefBaseUrlFromProfile(profile) {
|
|
|
1198
1192
|
return hqLaneRefBaseUrl(profile.hq.endpoint);
|
|
1199
1193
|
}
|
|
1200
1194
|
function formatLegacyProfileError(profilePath, foundVersion, migration) {
|
|
1201
|
-
return [`Profile is invalid: ${profilePath}: schemaVersion ${foundVersion} is not supported; migrate to schemaVersion
|
|
1195
|
+
return [`Profile is invalid: ${profilePath}: schemaVersion ${foundVersion} is not supported; migrate to schemaVersion 5 (ADR 0023).`, ...migration.map((line) => ` ${line}`)].join("\n");
|
|
1202
1196
|
}
|
|
1203
1197
|
function formatProfileError(profilePath, error) {
|
|
1204
1198
|
return `Profile is invalid: ${profilePath}: ${error.issues.map((issue) => {
|
|
@@ -3798,6 +3792,7 @@ const authorizeDemandWaiver = ({ authenticatedLogin, session }) => {
|
|
|
3798
3792
|
...normalizedSession && normalizedSession !== "unknown" ? { session: normalizedSession } : {}
|
|
3799
3793
|
} };
|
|
3800
3794
|
};
|
|
3795
|
+
const LEGACY_CLOSEOUT_SCHEMA_VERSION = 3;
|
|
3801
3796
|
const MAX_CLOSEOUT_ROWS = 1e3;
|
|
3802
3797
|
const IdentifierSchema = z.string().min(1).max(500);
|
|
3803
3798
|
const NarrativeSchema = z.string().min(1).max(1e4);
|
|
@@ -3809,11 +3804,15 @@ const CloseoutLandingSpotSchema = z.enum([
|
|
|
3809
3804
|
"skill",
|
|
3810
3805
|
"ADR"
|
|
3811
3806
|
]);
|
|
3812
|
-
const
|
|
3807
|
+
const CurrentCloseoutSourceSchema = z.object({
|
|
3808
|
+
detail: NarrativeSchema,
|
|
3809
|
+
kind: z.literal("boundary-thermo-attestation")
|
|
3810
|
+
}).strict();
|
|
3811
|
+
const LegacyCloseoutSourceSchema = z.object({
|
|
3813
3812
|
detail: NarrativeSchema,
|
|
3814
3813
|
kind: z.enum(["interior-telemetry", "boundary-thermo-attestation"])
|
|
3815
3814
|
}).strict();
|
|
3816
|
-
const
|
|
3815
|
+
const LegacyCloseoutScopingSchema = z.object({
|
|
3817
3816
|
epic: IdentifierSchema,
|
|
3818
3817
|
issueFilter: z.number().int().positive().optional(),
|
|
3819
3818
|
prFilter: z.number().int().positive().optional(),
|
|
@@ -3825,9 +3824,13 @@ const CloseoutScopingSchema = z.object({
|
|
|
3825
3824
|
}).strict()
|
|
3826
3825
|
}).strict();
|
|
3827
3826
|
const CloseoutBudgetSchema = z.object({
|
|
3827
|
+
note: NarrativeSchema,
|
|
3828
|
+
tier: z.literal("overseer")
|
|
3829
|
+
}).strict();
|
|
3830
|
+
const LegacyCloseoutBudgetSchema = z.object({
|
|
3828
3831
|
interiorSourcesConsumed: z.number().int().nonnegative(),
|
|
3829
3832
|
note: NarrativeSchema,
|
|
3830
|
-
scoping:
|
|
3833
|
+
scoping: LegacyCloseoutScopingSchema.optional(),
|
|
3831
3834
|
tier: z.literal("overseer")
|
|
3832
3835
|
}).strict();
|
|
3833
3836
|
const BoundaryThermoAttestationSchema = z.object({
|
|
@@ -3839,7 +3842,23 @@ const BoundaryThermoAttestationSchema = z.object({
|
|
|
3839
3842
|
const CloseoutMetricRowSchema = z.object({
|
|
3840
3843
|
label: IdentifierSchema,
|
|
3841
3844
|
metric: IdentifierSchema,
|
|
3842
|
-
source:
|
|
3845
|
+
source: CurrentCloseoutSourceSchema,
|
|
3846
|
+
unit: z.enum([
|
|
3847
|
+
"count",
|
|
3848
|
+
"boolean",
|
|
3849
|
+
"text"
|
|
3850
|
+
]),
|
|
3851
|
+
value: z.union([
|
|
3852
|
+
z.number(),
|
|
3853
|
+
NarrativeSchema,
|
|
3854
|
+
z.boolean(),
|
|
3855
|
+
z.null()
|
|
3856
|
+
])
|
|
3857
|
+
}).strict();
|
|
3858
|
+
const LegacyCloseoutMetricRowSchema = z.object({
|
|
3859
|
+
label: IdentifierSchema,
|
|
3860
|
+
metric: IdentifierSchema,
|
|
3861
|
+
source: LegacyCloseoutSourceSchema,
|
|
3843
3862
|
unit: z.enum([
|
|
3844
3863
|
"tokens",
|
|
3845
3864
|
"usd",
|
|
@@ -3860,14 +3879,21 @@ const CloseoutLessonSchema = z.object({
|
|
|
3860
3879
|
landingSpot: CloseoutLandingSpotSchema,
|
|
3861
3880
|
lesson: NarrativeSchema,
|
|
3862
3881
|
rationale: NarrativeSchema.optional(),
|
|
3863
|
-
source:
|
|
3882
|
+
source: CurrentCloseoutSourceSchema.optional()
|
|
3883
|
+
}).strict();
|
|
3884
|
+
const LegacyCloseoutLessonSchema = z.object({
|
|
3885
|
+
id: IdentifierSchema,
|
|
3886
|
+
landingSpot: CloseoutLandingSpotSchema,
|
|
3887
|
+
lesson: NarrativeSchema,
|
|
3888
|
+
rationale: NarrativeSchema.optional(),
|
|
3889
|
+
source: LegacyCloseoutSourceSchema.optional()
|
|
3864
3890
|
}).strict();
|
|
3865
3891
|
const CloseoutBlindspotSchema = z.object({
|
|
3866
3892
|
id: IdentifierSchema,
|
|
3867
3893
|
note: NarrativeSchema,
|
|
3868
3894
|
reason: NarrativeSchema.optional()
|
|
3869
3895
|
}).strict();
|
|
3870
|
-
const
|
|
3896
|
+
const closeoutLedgerRowSchema = (schemaVersion) => z.object({
|
|
3871
3897
|
detail: NarrativeSchema.optional(),
|
|
3872
3898
|
epic: IdentifierSchema,
|
|
3873
3899
|
generatedAt: z.iso.datetime(),
|
|
@@ -3879,7 +3905,7 @@ const CloseoutLedgerRowSchema = z.object({
|
|
|
3879
3905
|
"lesson",
|
|
3880
3906
|
"blindspot"
|
|
3881
3907
|
]),
|
|
3882
|
-
schemaVersion: z.literal(
|
|
3908
|
+
schemaVersion: z.literal(schemaVersion),
|
|
3883
3909
|
source: NarrativeSchema,
|
|
3884
3910
|
unit: IdentifierSchema.optional(),
|
|
3885
3911
|
value: z.union([
|
|
@@ -3888,19 +3914,37 @@ const CloseoutLedgerRowSchema = z.object({
|
|
|
3888
3914
|
z.null()
|
|
3889
3915
|
])
|
|
3890
3916
|
}).strict();
|
|
3891
|
-
|
|
3892
|
-
const
|
|
3917
|
+
const CloseoutLedgerRowSchema = closeoutLedgerRowSchema(4);
|
|
3918
|
+
const LegacyCloseoutLedgerRowSchema = closeoutLedgerRowSchema(LEGACY_CLOSEOUT_SCHEMA_VERSION);
|
|
3919
|
+
const closeoutArtifactShape = (input) => z.object({
|
|
3893
3920
|
boundaryThermo: BoundaryThermoAttestationSchema,
|
|
3894
|
-
budget:
|
|
3921
|
+
budget: input.budget,
|
|
3895
3922
|
couldNotSee: z.array(CloseoutBlindspotSchema).min(1).max(MAX_CLOSEOUT_ROWS),
|
|
3896
3923
|
epic: IdentifierSchema,
|
|
3897
3924
|
generatedAt: z.iso.datetime(),
|
|
3898
|
-
ledgerRows: z.array(
|
|
3899
|
-
lessons: z.array(
|
|
3900
|
-
metrics: z.array(
|
|
3925
|
+
ledgerRows: z.array(input.ledgerRows).max(MAX_CLOSEOUT_ROWS),
|
|
3926
|
+
lessons: z.array(input.lessons).max(MAX_CLOSEOUT_ROWS),
|
|
3927
|
+
metrics: z.array(input.metrics).max(MAX_CLOSEOUT_ROWS),
|
|
3901
3928
|
repo: RepoSchema,
|
|
3902
|
-
schemaVersion: z.literal(
|
|
3929
|
+
schemaVersion: z.literal(input.schemaVersion)
|
|
3903
3930
|
}).strict();
|
|
3931
|
+
/** Strict, bounded runtime transport contract emitted by factory:closeout. */
|
|
3932
|
+
const closeoutArtifactSchema = closeoutArtifactShape({
|
|
3933
|
+
budget: CloseoutBudgetSchema,
|
|
3934
|
+
ledgerRows: CloseoutLedgerRowSchema,
|
|
3935
|
+
lessons: CloseoutLessonSchema,
|
|
3936
|
+
metrics: CloseoutMetricRowSchema,
|
|
3937
|
+
schemaVersion: 4
|
|
3938
|
+
});
|
|
3939
|
+
/** Reader-only compatibility for closeout artifacts written before #554. */
|
|
3940
|
+
const legacyCloseoutArtifactSchema = closeoutArtifactShape({
|
|
3941
|
+
budget: LegacyCloseoutBudgetSchema,
|
|
3942
|
+
ledgerRows: LegacyCloseoutLedgerRowSchema,
|
|
3943
|
+
lessons: LegacyCloseoutLessonSchema,
|
|
3944
|
+
metrics: LegacyCloseoutMetricRowSchema,
|
|
3945
|
+
schemaVersion: LEGACY_CLOSEOUT_SCHEMA_VERSION
|
|
3946
|
+
});
|
|
3947
|
+
z.union([closeoutArtifactSchema, legacyCloseoutArtifactSchema]);
|
|
3904
3948
|
//#endregion
|
|
3905
3949
|
//#region src/retro-envelope.ts
|
|
3906
3950
|
/**
|
|
@@ -5246,8 +5290,8 @@ const resolveFindingBlocking = (finding, disposition = "open") => {
|
|
|
5246
5290
|
return severity === "critical" || severity === "high" || severity === "unknown";
|
|
5247
5291
|
};
|
|
5248
5292
|
const DEFAULT_INTERIOR_REVIEW_CAP = 0;
|
|
5249
|
-
const resolveReviewLadderPolicy = (
|
|
5250
|
-
gate: { cap:
|
|
5293
|
+
const resolveReviewLadderPolicy = () => ({
|
|
5294
|
+
gate: { cap: 5 },
|
|
5251
5295
|
interior: { cap: DEFAULT_INTERIOR_REVIEW_CAP }
|
|
5252
5296
|
});
|
|
5253
5297
|
//#endregion
|
|
@@ -7458,579 +7502,6 @@ function createFactoryCheckPublisher(output, dependencies = {}) {
|
|
|
7458
7502
|
};
|
|
7459
7503
|
}
|
|
7460
7504
|
//#endregion
|
|
7461
|
-
//#region src/interior-telemetry/codex-usage.ts
|
|
7462
|
-
const walkCodexJson = (value, visit) => {
|
|
7463
|
-
if (!value || typeof value !== "object") return;
|
|
7464
|
-
if (Array.isArray(value)) {
|
|
7465
|
-
for (const entry of value) walkCodexJson(entry, visit);
|
|
7466
|
-
return;
|
|
7467
|
-
}
|
|
7468
|
-
const record = value;
|
|
7469
|
-
visit(record);
|
|
7470
|
-
for (const entry of Object.values(record)) walkCodexJson(entry, visit);
|
|
7471
|
-
};
|
|
7472
|
-
const parseCodexJsonlEvents = (stdout) => stdout.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).flatMap((line) => {
|
|
7473
|
-
try {
|
|
7474
|
-
return [JSON.parse(line)];
|
|
7475
|
-
} catch {
|
|
7476
|
-
return [];
|
|
7477
|
-
}
|
|
7478
|
-
});
|
|
7479
|
-
const integerValue = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
|
|
7480
|
-
const valueForKeys = (record, keys) => {
|
|
7481
|
-
for (const key of keys) {
|
|
7482
|
-
const value = integerValue(record[key]);
|
|
7483
|
-
if (value !== void 0) return value;
|
|
7484
|
-
}
|
|
7485
|
-
};
|
|
7486
|
-
const usageFromRecord = (record) => {
|
|
7487
|
-
const inputTokens = valueForKeys(record, [
|
|
7488
|
-
"input_tokens",
|
|
7489
|
-
"inputTokens",
|
|
7490
|
-
"prompt_tokens",
|
|
7491
|
-
"promptTokens"
|
|
7492
|
-
]);
|
|
7493
|
-
const outputTokens = valueForKeys(record, [
|
|
7494
|
-
"completion_tokens",
|
|
7495
|
-
"completionTokens",
|
|
7496
|
-
"output_tokens",
|
|
7497
|
-
"outputTokens"
|
|
7498
|
-
]);
|
|
7499
|
-
const totalTokens = valueForKeys(record, [
|
|
7500
|
-
"total_token_count",
|
|
7501
|
-
"totalTokenCount",
|
|
7502
|
-
"total_tokens",
|
|
7503
|
-
"totalTokens"
|
|
7504
|
-
]) ?? (inputTokens !== void 0 && outputTokens !== void 0 ? inputTokens + outputTokens : void 0);
|
|
7505
|
-
if (totalTokens === void 0) return;
|
|
7506
|
-
return {
|
|
7507
|
-
cachedInputTokens: valueForKeys(record, [
|
|
7508
|
-
"cached_input_tokens",
|
|
7509
|
-
"cachedInputTokens",
|
|
7510
|
-
"cacheReadTokens"
|
|
7511
|
-
]) ?? null,
|
|
7512
|
-
inputTokens: inputTokens ?? null,
|
|
7513
|
-
outputTokens: outputTokens ?? null,
|
|
7514
|
-
reasoningOutputTokens: valueForKeys(record, ["reasoning_output_tokens", "reasoningOutputTokens"]) ?? null,
|
|
7515
|
-
source: "codex-json",
|
|
7516
|
-
status: "available",
|
|
7517
|
-
totalTokens
|
|
7518
|
-
};
|
|
7519
|
-
};
|
|
7520
|
-
const isInteriorTurn = (record) => {
|
|
7521
|
-
const role = typeof record.role === "string" ? record.role : "";
|
|
7522
|
-
return (typeof record.type === "string" ? record.type : "").includes("agent") || role === "assistant";
|
|
7523
|
-
};
|
|
7524
|
-
const CODEX_TOOL_EVENT_TYPES = new Set([
|
|
7525
|
-
"function_call",
|
|
7526
|
-
"function_call_output",
|
|
7527
|
-
"local_shell_call",
|
|
7528
|
-
"local_shell_call_output",
|
|
7529
|
-
"custom_tool_call",
|
|
7530
|
-
"custom_tool_call_output",
|
|
7531
|
-
"tool_call",
|
|
7532
|
-
"tool_result"
|
|
7533
|
-
]);
|
|
7534
|
-
const isToolCall = (record) => {
|
|
7535
|
-
const role = typeof record.role === "string" ? record.role : "";
|
|
7536
|
-
const type = typeof record.type === "string" ? record.type : "";
|
|
7537
|
-
return role === "tool" || type.includes("tool") || CODEX_TOOL_EVENT_TYPES.has(type);
|
|
7538
|
-
};
|
|
7539
|
-
const processCodexEvent = (event, metrics) => {
|
|
7540
|
-
let turnCounted = false;
|
|
7541
|
-
let toolCallCounted = false;
|
|
7542
|
-
walkCodexJson(event, (record) => {
|
|
7543
|
-
if (!turnCounted && isInteriorTurn(record)) {
|
|
7544
|
-
metrics.interiorTurns += 1;
|
|
7545
|
-
turnCounted = true;
|
|
7546
|
-
}
|
|
7547
|
-
if (!toolCallCounted && isToolCall(record)) {
|
|
7548
|
-
metrics.toolCalls += 1;
|
|
7549
|
-
toolCallCounted = true;
|
|
7550
|
-
}
|
|
7551
|
-
});
|
|
7552
|
-
};
|
|
7553
|
-
const parseCodexExecJsonContent = (output) => {
|
|
7554
|
-
const events = parseCodexJsonlEvents(output);
|
|
7555
|
-
if (events.length === 0) return {
|
|
7556
|
-
interiorTurns: 0,
|
|
7557
|
-
tokensByKind: {
|
|
7558
|
-
missingReason: "codex-json-events-missing",
|
|
7559
|
-
status: "unavailable"
|
|
7560
|
-
},
|
|
7561
|
-
toolCalls: 0
|
|
7562
|
-
};
|
|
7563
|
-
const metrics = {
|
|
7564
|
-
interiorTurns: 0,
|
|
7565
|
-
toolCalls: 0
|
|
7566
|
-
};
|
|
7567
|
-
const usageCandidates = [];
|
|
7568
|
-
for (const event of events) {
|
|
7569
|
-
processCodexEvent(event, metrics);
|
|
7570
|
-
walkCodexJson(event, (record) => {
|
|
7571
|
-
const usage = usageFromRecord(record);
|
|
7572
|
-
if (usage) usageCandidates.push(usage);
|
|
7573
|
-
});
|
|
7574
|
-
}
|
|
7575
|
-
return {
|
|
7576
|
-
interiorTurns: metrics.interiorTurns,
|
|
7577
|
-
tokensByKind: usageCandidates.at(-1) ?? {
|
|
7578
|
-
missingReason: "codex-json-usage-not-emitted",
|
|
7579
|
-
status: "unavailable"
|
|
7580
|
-
},
|
|
7581
|
-
toolCalls: metrics.toolCalls
|
|
7582
|
-
};
|
|
7583
|
-
};
|
|
7584
|
-
const parseCodexExecJsonFile = (logPath) => {
|
|
7585
|
-
try {
|
|
7586
|
-
return parseCodexExecJsonContent(readFileSync(logPath, "utf-8"));
|
|
7587
|
-
} catch {
|
|
7588
|
-
return {
|
|
7589
|
-
interiorTurns: 0,
|
|
7590
|
-
tokensByKind: {
|
|
7591
|
-
missingReason: "codex-log-missing-or-unreadable",
|
|
7592
|
-
status: "unavailable"
|
|
7593
|
-
},
|
|
7594
|
-
toolCalls: 0
|
|
7595
|
-
};
|
|
7596
|
-
}
|
|
7597
|
-
};
|
|
7598
|
-
//#endregion
|
|
7599
|
-
//#region src/interior-telemetry/types.ts
|
|
7600
|
-
const ROLE_CLASSIFICATION_FALLBACK = "coder";
|
|
7601
|
-
const ROLE_CLASSIFICATION_FALLBACK_REASON = "ambiguous-driving-prompt-default-coder";
|
|
7602
|
-
//#endregion
|
|
7603
|
-
//#region src/interior-telemetry/cursor-store.ts
|
|
7604
|
-
const PREAMBLE_CLOSE_TAGS = ["</user_info>", "</git_status>"];
|
|
7605
|
-
const REVIEWER_MARKERS = [
|
|
7606
|
-
"reviewer",
|
|
7607
|
-
"review",
|
|
7608
|
-
"findings",
|
|
7609
|
-
"pr-review-proof"
|
|
7610
|
-
];
|
|
7611
|
-
/** Strong reviewer signals take precedence over incidental coder markers. */
|
|
7612
|
-
const STRONG_REVIEWER_MARKERS = [
|
|
7613
|
-
"reviewer",
|
|
7614
|
-
"findings",
|
|
7615
|
-
"pr-review-proof"
|
|
7616
|
-
];
|
|
7617
|
-
const CODER_MARKERS = [
|
|
7618
|
-
"implementation worker",
|
|
7619
|
-
"coder",
|
|
7620
|
-
"implement ",
|
|
7621
|
-
"write the diff",
|
|
7622
|
-
"write the code",
|
|
7623
|
-
"you write the"
|
|
7624
|
-
];
|
|
7625
|
-
/**
|
|
7626
|
-
* Remove negated "write" phrasing before coder marker matching so prohibitions
|
|
7627
|
-
* like "do not write" / "never write" never count as coder signals.
|
|
7628
|
-
*/
|
|
7629
|
-
const NEGATED_WRITE_PATTERN = /\b(?:do not|does not|don't|never|no)\s+writ\w*/giu;
|
|
7630
|
-
const READ_ONLY_PATTERN = /\bread[\s-]?only\b/giu;
|
|
7631
|
-
const scrubNegatedCoderPhrasing = (text) => text.replace(NEGATED_WRITE_PATTERN, " ").replace(READ_ONLY_PATTERN, " ");
|
|
7632
|
-
const escapeRegex = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
7633
|
-
/** Match markers on token boundaries so substrings like "preview" never hit "review". */
|
|
7634
|
-
const markerMatches = (text, marker) => {
|
|
7635
|
-
const parts = marker.trim().split(/\s+/u).filter((part) => part.length > 0);
|
|
7636
|
-
if (parts.length === 0) return false;
|
|
7637
|
-
const pattern = parts.map(escapeRegex).join("\\s+");
|
|
7638
|
-
return new RegExp(`(?<![a-z0-9])${pattern}(?![a-z0-9])`, "iu").test(text);
|
|
7639
|
-
};
|
|
7640
|
-
const VERDICT_MARKER = "PR-REVIEW-PROOF-END";
|
|
7641
|
-
const asBytes = (data) => {
|
|
7642
|
-
if (Buffer.isBuffer(data)) return data;
|
|
7643
|
-
if (data instanceof Uint8Array) return data;
|
|
7644
|
-
};
|
|
7645
|
-
const isJsonBlob = (data) => {
|
|
7646
|
-
for (const byte of data) {
|
|
7647
|
-
if (byte === 32 || byte === 9 || byte === 10 || byte === 13) continue;
|
|
7648
|
-
return byte === 123;
|
|
7649
|
-
}
|
|
7650
|
-
return false;
|
|
7651
|
-
};
|
|
7652
|
-
const partText = (part) => {
|
|
7653
|
-
if (typeof part === "string") return part;
|
|
7654
|
-
if (!part || typeof part !== "object") return "";
|
|
7655
|
-
const record = part;
|
|
7656
|
-
if (typeof record.text === "string") return record.text;
|
|
7657
|
-
if (typeof record.content === "string") return record.content;
|
|
7658
|
-
return "";
|
|
7659
|
-
};
|
|
7660
|
-
const normalizeMessageContent = (content) => {
|
|
7661
|
-
if (typeof content === "string") return content;
|
|
7662
|
-
if (!Array.isArray(content)) return "";
|
|
7663
|
-
return content.map(partText).join("");
|
|
7664
|
-
};
|
|
7665
|
-
const stripPreamble = (text) => {
|
|
7666
|
-
let remainder = text;
|
|
7667
|
-
for (const closeTag of PREAMBLE_CLOSE_TAGS) {
|
|
7668
|
-
const index = remainder.indexOf(closeTag);
|
|
7669
|
-
if (index !== -1) remainder = remainder.slice(index + closeTag.length);
|
|
7670
|
-
}
|
|
7671
|
-
return remainder.trim();
|
|
7672
|
-
};
|
|
7673
|
-
const classifyRoleFromDrivingPrompt = (drivingPrompt) => {
|
|
7674
|
-
const normalized = drivingPrompt.toLowerCase();
|
|
7675
|
-
const scrubbed = scrubNegatedCoderPhrasing(normalized);
|
|
7676
|
-
const reviewerHits = REVIEWER_MARKERS.filter((marker) => markerMatches(normalized, marker));
|
|
7677
|
-
const coderHits = CODER_MARKERS.filter((marker) => markerMatches(scrubbed, marker));
|
|
7678
|
-
if (reviewerHits.length > 0 && coderHits.length === 0) return {
|
|
7679
|
-
reason: `reviewer-markers:${reviewerHits.join(",")}`,
|
|
7680
|
-
role: "reviewer"
|
|
7681
|
-
};
|
|
7682
|
-
if (coderHits.length > 0 && reviewerHits.length === 0) return {
|
|
7683
|
-
reason: `coder-markers:${coderHits.join(",")}`,
|
|
7684
|
-
role: "coder"
|
|
7685
|
-
};
|
|
7686
|
-
if (reviewerHits.length > 0 && coderHits.length > 0) {
|
|
7687
|
-
if (markerMatches(scrubbed, "implementation worker")) return {
|
|
7688
|
-
reason: "conflicting-markers-implementation-worker-priority",
|
|
7689
|
-
role: "coder"
|
|
7690
|
-
};
|
|
7691
|
-
const strongReviewerHits = STRONG_REVIEWER_MARKERS.filter((marker) => markerMatches(normalized, marker));
|
|
7692
|
-
if (strongReviewerHits.length > 0) return {
|
|
7693
|
-
reason: `conflicting-markers-strong-reviewer-priority:${strongReviewerHits.join(",")}`,
|
|
7694
|
-
role: "reviewer"
|
|
7695
|
-
};
|
|
7696
|
-
}
|
|
7697
|
-
return {
|
|
7698
|
-
reason: ROLE_CLASSIFICATION_FALLBACK_REASON,
|
|
7699
|
-
role: ROLE_CLASSIFICATION_FALLBACK
|
|
7700
|
-
};
|
|
7701
|
-
};
|
|
7702
|
-
const drivingPromptFromMessages = (messages) => {
|
|
7703
|
-
const userMessages = messages.filter((message) => message.role === "user");
|
|
7704
|
-
if (userMessages.length === 0) return "";
|
|
7705
|
-
const afterPreamble = stripPreamble(normalizeMessageContent(userMessages[0]?.content));
|
|
7706
|
-
if (afterPreamble.length > 0) return afterPreamble;
|
|
7707
|
-
for (const message of userMessages.slice(1)) {
|
|
7708
|
-
const text = normalizeMessageContent(message.content).trim();
|
|
7709
|
-
if (text.length > 0) return text;
|
|
7710
|
-
}
|
|
7711
|
-
return "";
|
|
7712
|
-
};
|
|
7713
|
-
const parseStoreMessages = (storePath) => {
|
|
7714
|
-
const require = createRequire(import.meta.url);
|
|
7715
|
-
let DatabaseSync;
|
|
7716
|
-
try {
|
|
7717
|
-
({DatabaseSync} = require("node:sqlite"));
|
|
7718
|
-
} catch {
|
|
7719
|
-
throw new Error("node:sqlite requires Node >= 22.5.0 to read cursor store.db");
|
|
7720
|
-
}
|
|
7721
|
-
const db = new DatabaseSync(storePath, { readOnly: true });
|
|
7722
|
-
try {
|
|
7723
|
-
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='blobs'").get()) throw new Error("cursor store.db has no blobs table");
|
|
7724
|
-
const rows = db.prepare("SELECT data FROM blobs").all();
|
|
7725
|
-
const messages = [];
|
|
7726
|
-
for (const row of rows) {
|
|
7727
|
-
const data = asBytes(row.data);
|
|
7728
|
-
if (!data || !isJsonBlob(data)) continue;
|
|
7729
|
-
try {
|
|
7730
|
-
const parsed = JSON.parse(Buffer.from(data).toString("utf-8"));
|
|
7731
|
-
if (parsed && typeof parsed === "object") messages.push(parsed);
|
|
7732
|
-
} catch {
|
|
7733
|
-
continue;
|
|
7734
|
-
}
|
|
7735
|
-
}
|
|
7736
|
-
return messages;
|
|
7737
|
-
} finally {
|
|
7738
|
-
db.close();
|
|
7739
|
-
}
|
|
7740
|
-
};
|
|
7741
|
-
const metricsFromMessages = (messages) => {
|
|
7742
|
-
let interiorTurns = 0;
|
|
7743
|
-
let toolCalls = 0;
|
|
7744
|
-
let verdictEmitted = false;
|
|
7745
|
-
for (const message of messages) {
|
|
7746
|
-
if (message.role === "assistant") {
|
|
7747
|
-
interiorTurns += 1;
|
|
7748
|
-
if (normalizeMessageContent(message.content).includes(VERDICT_MARKER)) verdictEmitted = true;
|
|
7749
|
-
}
|
|
7750
|
-
if (message.role === "tool") toolCalls += 1;
|
|
7751
|
-
}
|
|
7752
|
-
const classification = classifyRoleFromDrivingPrompt(drivingPromptFromMessages(messages));
|
|
7753
|
-
return {
|
|
7754
|
-
interiorTurns,
|
|
7755
|
-
role: classification.role,
|
|
7756
|
-
roleClassificationReason: classification.reason,
|
|
7757
|
-
toolCalls,
|
|
7758
|
-
verdictEmitted
|
|
7759
|
-
};
|
|
7760
|
-
};
|
|
7761
|
-
const readChatMeta = (storePath) => {
|
|
7762
|
-
const metaPath = path.join(path.dirname(storePath), "meta.json");
|
|
7763
|
-
try {
|
|
7764
|
-
const raw = readFileSync(metaPath, "utf-8");
|
|
7765
|
-
const parsed = JSON.parse(raw);
|
|
7766
|
-
const meta = {};
|
|
7767
|
-
if (typeof parsed.cwd === "string") meta.cwd = parsed.cwd;
|
|
7768
|
-
if (typeof parsed.createdAtMs === "number") meta.createdAtMs = parsed.createdAtMs;
|
|
7769
|
-
return Object.keys(meta).length > 0 ? meta : void 0;
|
|
7770
|
-
} catch {
|
|
7771
|
-
return;
|
|
7772
|
-
}
|
|
7773
|
-
};
|
|
7774
|
-
const chatIdFromStorePath = (storePath) => path.basename(path.dirname(storePath));
|
|
7775
|
-
const parseCursorStore = (storePath) => {
|
|
7776
|
-
const metrics = metricsFromMessages(parseStoreMessages(storePath));
|
|
7777
|
-
return {
|
|
7778
|
-
chatId: chatIdFromStorePath(storePath),
|
|
7779
|
-
meta: readChatMeta(storePath),
|
|
7780
|
-
storePath,
|
|
7781
|
-
...metrics
|
|
7782
|
-
};
|
|
7783
|
-
};
|
|
7784
|
-
//#endregion
|
|
7785
|
-
//#region src/interior-telemetry/usage-csv.ts
|
|
7786
|
-
const COLUMN_ALIASES = {
|
|
7787
|
-
cachedInputTokens: [
|
|
7788
|
-
"Cache Read",
|
|
7789
|
-
"cache read",
|
|
7790
|
-
"cache_read",
|
|
7791
|
-
"cachedInputTokens"
|
|
7792
|
-
],
|
|
7793
|
-
inputTokens: [
|
|
7794
|
-
"Input",
|
|
7795
|
-
"input",
|
|
7796
|
-
"input_tokens",
|
|
7797
|
-
"inputTokens",
|
|
7798
|
-
"Input (w/ Cache Write)",
|
|
7799
|
-
"Input (w/o Cache Write)"
|
|
7800
|
-
],
|
|
7801
|
-
outputTokens: [
|
|
7802
|
-
"Output Tokens",
|
|
7803
|
-
"output tokens",
|
|
7804
|
-
"output_tokens",
|
|
7805
|
-
"outputTokens"
|
|
7806
|
-
],
|
|
7807
|
-
totalTokens: [
|
|
7808
|
-
"Total Tokens",
|
|
7809
|
-
"total tokens",
|
|
7810
|
-
"total_tokens",
|
|
7811
|
-
"totalTokens"
|
|
7812
|
-
]
|
|
7813
|
-
};
|
|
7814
|
-
const parseCsvCell = (value) => {
|
|
7815
|
-
if (value === void 0) return null;
|
|
7816
|
-
const trimmed = value.trim();
|
|
7817
|
-
return trimmed.length === 0 ? null : trimmed;
|
|
7818
|
-
};
|
|
7819
|
-
const parseIntegerCell = (value) => {
|
|
7820
|
-
const cell = parseCsvCell(value);
|
|
7821
|
-
if (cell === null) return null;
|
|
7822
|
-
const parsed = Math.trunc(Number(cell));
|
|
7823
|
-
return Number.isInteger(parsed) && parsed >= 0 ? parsed : null;
|
|
7824
|
-
};
|
|
7825
|
-
const parseCsvLine = (line) => {
|
|
7826
|
-
const fields = [];
|
|
7827
|
-
let current = "";
|
|
7828
|
-
let inQuotes = false;
|
|
7829
|
-
for (let index = 0; index < line.length; index += 1) {
|
|
7830
|
-
const char = line[index];
|
|
7831
|
-
if (char === "\"") {
|
|
7832
|
-
if (inQuotes && line[index + 1] === "\"") {
|
|
7833
|
-
current += "\"";
|
|
7834
|
-
index += 1;
|
|
7835
|
-
} else inQuotes = !inQuotes;
|
|
7836
|
-
continue;
|
|
7837
|
-
}
|
|
7838
|
-
if (char === "," && !inQuotes) {
|
|
7839
|
-
fields.push(current);
|
|
7840
|
-
current = "";
|
|
7841
|
-
continue;
|
|
7842
|
-
}
|
|
7843
|
-
current += char;
|
|
7844
|
-
}
|
|
7845
|
-
fields.push(current);
|
|
7846
|
-
return fields;
|
|
7847
|
-
};
|
|
7848
|
-
const headerIndex = (headers, aliases) => {
|
|
7849
|
-
const normalized = headers.map((header) => header.trim());
|
|
7850
|
-
for (const alias of aliases) {
|
|
7851
|
-
const index = normalized.indexOf(alias);
|
|
7852
|
-
if (index !== -1) return index;
|
|
7853
|
-
}
|
|
7854
|
-
return -1;
|
|
7855
|
-
};
|
|
7856
|
-
const headerIndices = (headers, aliases) => {
|
|
7857
|
-
const normalized = headers.map((header) => header.trim());
|
|
7858
|
-
const indices = [];
|
|
7859
|
-
for (let index = 0; index < normalized.length; index += 1) {
|
|
7860
|
-
const header = normalized[index];
|
|
7861
|
-
if (header !== void 0 && aliases.some((alias) => alias === header)) indices.push(index);
|
|
7862
|
-
}
|
|
7863
|
-
return indices;
|
|
7864
|
-
};
|
|
7865
|
-
const aggregateColumn = (rows, columnIndex) => {
|
|
7866
|
-
if (columnIndex === -1) return null;
|
|
7867
|
-
let sum = 0;
|
|
7868
|
-
let sawValue = false;
|
|
7869
|
-
for (const row of rows) {
|
|
7870
|
-
const value = parseIntegerCell(row[columnIndex]);
|
|
7871
|
-
if (value !== null) {
|
|
7872
|
-
sum += value;
|
|
7873
|
-
sawValue = true;
|
|
7874
|
-
}
|
|
7875
|
-
}
|
|
7876
|
-
return sawValue ? sum : null;
|
|
7877
|
-
};
|
|
7878
|
-
const aggregateColumns = (rows, columnIndices) => {
|
|
7879
|
-
if (columnIndices.length === 0) return null;
|
|
7880
|
-
let sum = 0;
|
|
7881
|
-
let sawValue = false;
|
|
7882
|
-
for (const row of rows) for (const columnIndex of columnIndices) {
|
|
7883
|
-
const value = parseIntegerCell(row[columnIndex]);
|
|
7884
|
-
if (value !== null) {
|
|
7885
|
-
sum += value;
|
|
7886
|
-
sawValue = true;
|
|
7887
|
-
}
|
|
7888
|
-
}
|
|
7889
|
-
return sawValue ? sum : null;
|
|
7890
|
-
};
|
|
7891
|
-
const parseUsageCsvContent = (content) => {
|
|
7892
|
-
const lines = content.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
7893
|
-
if (lines.length < 2) return {
|
|
7894
|
-
missingReason: "cursor-csv-empty-or-header-only",
|
|
7895
|
-
status: "unavailable"
|
|
7896
|
-
};
|
|
7897
|
-
const headers = parseCsvLine(lines[0] ?? "");
|
|
7898
|
-
const dataRows = lines.slice(1).map(parseCsvLine);
|
|
7899
|
-
const inputIndices = headerIndices(headers, COLUMN_ALIASES.inputTokens);
|
|
7900
|
-
const cachedIndex = headerIndex(headers, COLUMN_ALIASES.cachedInputTokens);
|
|
7901
|
-
const outputIndex = headerIndex(headers, COLUMN_ALIASES.outputTokens);
|
|
7902
|
-
const totalIndex = headerIndex(headers, COLUMN_ALIASES.totalTokens);
|
|
7903
|
-
const inputTokens = aggregateColumns(dataRows, inputIndices);
|
|
7904
|
-
const cachedInputTokens = aggregateColumn(dataRows, cachedIndex);
|
|
7905
|
-
const outputTokens = aggregateColumn(dataRows, outputIndex);
|
|
7906
|
-
const totalTokens = aggregateColumn(dataRows, totalIndex) ?? (inputTokens !== null && outputTokens !== null ? inputTokens + outputTokens : null);
|
|
7907
|
-
if (totalTokens === null) return {
|
|
7908
|
-
missingReason: "cursor-csv-no-total-tokens",
|
|
7909
|
-
status: "unavailable"
|
|
7910
|
-
};
|
|
7911
|
-
return {
|
|
7912
|
-
cachedInputTokens,
|
|
7913
|
-
inputTokens,
|
|
7914
|
-
outputTokens,
|
|
7915
|
-
reasoningOutputTokens: null,
|
|
7916
|
-
source: "cursor-csv",
|
|
7917
|
-
status: "available",
|
|
7918
|
-
totalTokens
|
|
7919
|
-
};
|
|
7920
|
-
};
|
|
7921
|
-
const parseUsageCsvFile = (csvPath) => {
|
|
7922
|
-
try {
|
|
7923
|
-
return parseUsageCsvContent(readFileSync(csvPath, "utf-8"));
|
|
7924
|
-
} catch {
|
|
7925
|
-
return {
|
|
7926
|
-
missingReason: "cursor-csv-missing-or-unreadable",
|
|
7927
|
-
status: "unavailable"
|
|
7928
|
-
};
|
|
7929
|
-
}
|
|
7930
|
-
};
|
|
7931
|
-
//#endregion
|
|
7932
|
-
//#region src/interior-telemetry/extract.ts
|
|
7933
|
-
/**
|
|
7934
|
-
* Returns true when the store should be ingested under the given epic scope.
|
|
7935
|
-
* Absent/empty allowlists match everything (back-compat glob-all behavior).
|
|
7936
|
-
*/
|
|
7937
|
-
const chatMatchesEpicScope = (metrics, scope) => {
|
|
7938
|
-
const chatIds = scope.chatIds ?? [];
|
|
7939
|
-
const cwds = scope.cwds ?? [];
|
|
7940
|
-
if (chatIds.length === 0 && cwds.length === 0) return true;
|
|
7941
|
-
if (new Set(chatIds).has(metrics.chatId)) return true;
|
|
7942
|
-
const cwd = metrics.meta?.cwd;
|
|
7943
|
-
if (cwd !== void 0 && new Set(cwds).has(cwd)) return true;
|
|
7944
|
-
return false;
|
|
7945
|
-
};
|
|
7946
|
-
const UNAVAILABLE_CURSOR_TOKENS = {
|
|
7947
|
-
missingReason: "cursor-usage-csv-not-provided",
|
|
7948
|
-
status: "unavailable"
|
|
7949
|
-
};
|
|
7950
|
-
/** Lane CSV has no per-chat breakdown; tokens attach to one cursor row only. */
|
|
7951
|
-
const UNAVAILABLE_LANE_LEVEL_CURSOR_TOKENS = {
|
|
7952
|
-
missingReason: "cursor-csv-is-lane-level-tokens-attributed-once",
|
|
7953
|
-
status: "unavailable"
|
|
7954
|
-
};
|
|
7955
|
-
const discoverStoreDbPaths = (rootDir) => {
|
|
7956
|
-
const storePaths = [];
|
|
7957
|
-
const visit = (currentPath) => {
|
|
7958
|
-
let entries;
|
|
7959
|
-
try {
|
|
7960
|
-
entries = readdirSync(currentPath);
|
|
7961
|
-
} catch {
|
|
7962
|
-
return;
|
|
7963
|
-
}
|
|
7964
|
-
for (const entry of entries) {
|
|
7965
|
-
const fullPath = path.join(currentPath, entry);
|
|
7966
|
-
let stats;
|
|
7967
|
-
try {
|
|
7968
|
-
stats = statSync(fullPath);
|
|
7969
|
-
} catch {
|
|
7970
|
-
continue;
|
|
7971
|
-
}
|
|
7972
|
-
if (stats.isFile() && entry === "store.db") {
|
|
7973
|
-
storePaths.push(fullPath);
|
|
7974
|
-
continue;
|
|
7975
|
-
}
|
|
7976
|
-
if (stats.isDirectory()) visit(fullPath);
|
|
7977
|
-
}
|
|
7978
|
-
};
|
|
7979
|
-
visit(rootDir);
|
|
7980
|
-
return storePaths.toSorted();
|
|
7981
|
-
};
|
|
7982
|
-
const extractInteriorTelemetry = (options) => {
|
|
7983
|
-
const rows = [];
|
|
7984
|
-
const cursorTokens = options.usageCsvPath ? parseUsageCsvFile(options.usageCsvPath) : UNAVAILABLE_CURSOR_TOKENS;
|
|
7985
|
-
if (options.chatsDir) {
|
|
7986
|
-
const storePaths = discoverStoreDbPaths(options.chatsDir);
|
|
7987
|
-
let cursorTokensAttributed = false;
|
|
7988
|
-
for (const storePath of storePaths) {
|
|
7989
|
-
let metrics;
|
|
7990
|
-
try {
|
|
7991
|
-
metrics = parseCursorStore(storePath);
|
|
7992
|
-
} catch {
|
|
7993
|
-
continue;
|
|
7994
|
-
}
|
|
7995
|
-
if (options.epicChatScope && !chatMatchesEpicScope(metrics, options.epicChatScope)) continue;
|
|
7996
|
-
let tokensByKind = cursorTokens;
|
|
7997
|
-
if (cursorTokensAttributed && cursorTokens.status === "available") tokensByKind = UNAVAILABLE_LANE_LEVEL_CURSOR_TOKENS;
|
|
7998
|
-
else if (cursorTokens.status === "available") cursorTokensAttributed = true;
|
|
7999
|
-
rows.push({
|
|
8000
|
-
engine: "cursor",
|
|
8001
|
-
interiorTurns: metrics.interiorTurns,
|
|
8002
|
-
lane: options.lane,
|
|
8003
|
-
role: metrics.role,
|
|
8004
|
-
source: {
|
|
8005
|
-
chatId: metrics.chatId,
|
|
8006
|
-
kind: "cursor-store",
|
|
8007
|
-
path: storePath
|
|
8008
|
-
},
|
|
8009
|
-
tokensByKind,
|
|
8010
|
-
toolCalls: metrics.toolCalls,
|
|
8011
|
-
verdictEmitted: metrics.verdictEmitted
|
|
8012
|
-
});
|
|
8013
|
-
}
|
|
8014
|
-
}
|
|
8015
|
-
if (options.codexLogPath) {
|
|
8016
|
-
const codexMetrics = parseCodexExecJsonFile(options.codexLogPath);
|
|
8017
|
-
rows.push({
|
|
8018
|
-
engine: "codex",
|
|
8019
|
-
interiorTurns: codexMetrics.interiorTurns,
|
|
8020
|
-
lane: options.lane,
|
|
8021
|
-
role: "coder",
|
|
8022
|
-
source: {
|
|
8023
|
-
kind: "codex-exec",
|
|
8024
|
-
path: options.codexLogPath
|
|
8025
|
-
},
|
|
8026
|
-
tokensByKind: codexMetrics.tokensByKind,
|
|
8027
|
-
toolCalls: codexMetrics.toolCalls,
|
|
8028
|
-
verdictEmitted: false
|
|
8029
|
-
});
|
|
8030
|
-
}
|
|
8031
|
-
return rows;
|
|
8032
|
-
};
|
|
8033
|
-
//#endregion
|
|
8034
7505
|
//#region src/interior-telemetry/pricing-snapshot.ts
|
|
8035
7506
|
/** Human-readable provenance tag, embedded in data-gap messages so an unpriced
|
|
8036
7507
|
* block names the exact snapshot that lacked its model. */
|
|
@@ -8760,6 +8231,54 @@ function createBoundaryCheckCommand(output, action) {
|
|
|
8760
8231
|
if (record.status !== "ready") throw new Error(`boundary:check refused: ${record.blockingReasons.join("; ")}`);
|
|
8761
8232
|
}));
|
|
8762
8233
|
}
|
|
8234
|
+
//#endregion
|
|
8235
|
+
//#region src/checkout-repository.ts
|
|
8236
|
+
function normalizeRepositoryIdentity(repository) {
|
|
8237
|
+
return {
|
|
8238
|
+
name: repository.name.replace(/\.git$/iu, "").toLowerCase(),
|
|
8239
|
+
owner: repository.owner.toLowerCase()
|
|
8240
|
+
};
|
|
8241
|
+
}
|
|
8242
|
+
const GITHUB_HOSTNAME = "github.com";
|
|
8243
|
+
const SCP_GITHUB_REMOTE_PATTERN = /^(?:[^@\s]+@)?github\.com:(?<path>[^\s]+)$/iu;
|
|
8244
|
+
function githubRepositoryPath(remoteUrl) {
|
|
8245
|
+
const scpPath = SCP_GITHUB_REMOTE_PATTERN.exec(remoteUrl)?.groups?.path;
|
|
8246
|
+
if (scpPath) return scpPath;
|
|
8247
|
+
try {
|
|
8248
|
+
const url = new URL(remoteUrl);
|
|
8249
|
+
if (url.hostname.toLowerCase() !== GITHUB_HOSTNAME) return;
|
|
8250
|
+
return url.pathname.replace(/^\//u, "");
|
|
8251
|
+
} catch {
|
|
8252
|
+
return;
|
|
8253
|
+
}
|
|
8254
|
+
}
|
|
8255
|
+
function parseCheckoutRepository(remoteUrl) {
|
|
8256
|
+
const segments = githubRepositoryPath(remoteUrl.trim())?.split("/") ?? [];
|
|
8257
|
+
const [owner, rawName] = segments;
|
|
8258
|
+
const name = rawName?.replace(/\.git$/u, "");
|
|
8259
|
+
if (!(owner && name) || segments.length !== 2) throw new Error(`Could not derive a GitHub repository from checkout origin ${remoteUrl}; configure origin as a github.com owner/repository remote.`);
|
|
8260
|
+
return normalizeRepositoryIdentity({
|
|
8261
|
+
name,
|
|
8262
|
+
owner
|
|
8263
|
+
});
|
|
8264
|
+
}
|
|
8265
|
+
function checkoutRepository(cwd) {
|
|
8266
|
+
let remoteUrl;
|
|
8267
|
+
try {
|
|
8268
|
+
remoteUrl = runCapture("git", [
|
|
8269
|
+
"remote",
|
|
8270
|
+
"get-url",
|
|
8271
|
+
"origin"
|
|
8272
|
+
], cwd).stdout;
|
|
8273
|
+
} catch {
|
|
8274
|
+
throw new Error("Could not read checkout origin; repository routing must be rooted in trusted checkout configuration.");
|
|
8275
|
+
}
|
|
8276
|
+
return parseCheckoutRepository(remoteUrl);
|
|
8277
|
+
}
|
|
8278
|
+
const repositorySlug = (repository) => {
|
|
8279
|
+
const normalized = normalizeRepositoryIdentity(repository);
|
|
8280
|
+
return `${normalized.owner}/${normalized.name}`;
|
|
8281
|
+
};
|
|
8763
8282
|
var CloseoutValidationError = class extends Error {
|
|
8764
8283
|
constructor(message) {
|
|
8765
8284
|
super(message);
|
|
@@ -8802,18 +8321,8 @@ const boundaryThermoStatus = (attestation) => {
|
|
|
8802
8321
|
};
|
|
8803
8322
|
//#endregion
|
|
8804
8323
|
//#region src/epic-closeout/gather.ts
|
|
8805
|
-
const BUDGET_NOTE = "bounded
|
|
8806
|
-
const hasInteriorInputs = (opts) => Boolean(opts.chatsDir ?? opts.usageCsvPath ?? opts.codexLogPath);
|
|
8324
|
+
const BUDGET_NOTE = "bounded closeout report: trace diagnostics scan plus boundary-thermo attestation; no per-commit forensic dig";
|
|
8807
8325
|
function gatherCloseoutInputs(opts) {
|
|
8808
|
-
const hasEpicChatAllowlist = (opts.epicChatScope?.chatIds?.length ?? 0) > 0 || (opts.epicChatScope?.cwds?.length ?? 0) > 0;
|
|
8809
|
-
const scopedByEpicIssueSet = Boolean(opts.chatsDir) && hasEpicChatAllowlist;
|
|
8810
|
-
const interiorRows = hasInteriorInputs(opts) ? extractInteriorTelemetry({
|
|
8811
|
-
chatsDir: opts.chatsDir,
|
|
8812
|
-
codexLogPath: opts.codexLogPath,
|
|
8813
|
-
...opts.epicChatScope === void 0 ? {} : { epicChatScope: opts.epicChatScope },
|
|
8814
|
-
lane: opts.lane,
|
|
8815
|
-
usageCsvPath: opts.usageCsvPath
|
|
8816
|
-
}) : [];
|
|
8817
8326
|
const diagnostics = scanFactoryTraceDiagnostics({
|
|
8818
8327
|
filters: {
|
|
8819
8328
|
repo: opts.repo,
|
|
@@ -8826,114 +8335,15 @@ function gatherCloseoutInputs(opts) {
|
|
|
8826
8335
|
});
|
|
8827
8336
|
return {
|
|
8828
8337
|
budget: {
|
|
8829
|
-
interiorSourcesConsumed: interiorRows.length,
|
|
8830
8338
|
note: BUDGET_NOTE,
|
|
8831
|
-
scoping: {
|
|
8832
|
-
epic: opts.epic,
|
|
8833
|
-
issueFilter: opts.issue,
|
|
8834
|
-
prFilter: opts.pr,
|
|
8835
|
-
repo: opts.repo,
|
|
8836
|
-
scopedByEpicIssueSet,
|
|
8837
|
-
window: {
|
|
8838
|
-
from: opts.from,
|
|
8839
|
-
to: opts.to
|
|
8840
|
-
}
|
|
8841
|
-
},
|
|
8842
8339
|
tier: "overseer"
|
|
8843
8340
|
},
|
|
8844
|
-
diagnostics
|
|
8845
|
-
interiorRows
|
|
8341
|
+
diagnostics
|
|
8846
8342
|
};
|
|
8847
8343
|
}
|
|
8848
8344
|
//#endregion
|
|
8849
|
-
//#region src/epic-closeout/row-identity.ts
|
|
8850
|
-
const interiorRowDisambig = (row) => row.source.chatId ?? path.basename(row.source.path);
|
|
8851
|
-
//#endregion
|
|
8852
|
-
//#region src/epic-closeout/metric-rows.ts
|
|
8853
|
-
const INTERIOR_TOTAL_TOOL_CALLS_METRIC = "interior.total-tool-calls";
|
|
8854
|
-
const INTERIOR_TOTAL_TURNS_METRIC = "interior.total-turns";
|
|
8855
|
-
const interiorSourceDetail = (row) => row.source.chatId ?? row.source.path;
|
|
8856
|
-
const interiorPrefix = (row) => {
|
|
8857
|
-
const disambig = interiorRowDisambig(row);
|
|
8858
|
-
return `interior.${row.lane}.${row.role}.${row.engine}.${disambig}`;
|
|
8859
|
-
};
|
|
8860
|
-
function buildMetricRows(input) {
|
|
8861
|
-
const rows = [];
|
|
8862
|
-
let totalInteriorTurns = 0;
|
|
8863
|
-
let totalInteriorToolCalls = 0;
|
|
8864
|
-
for (const interior of input.interiorRows) {
|
|
8865
|
-
const prefix = interiorPrefix(interior);
|
|
8866
|
-
const sourceDetail = interiorSourceDetail(interior);
|
|
8867
|
-
rows.push({
|
|
8868
|
-
label: `${interior.lane} ${interior.role} ${interior.engine} interior turns`,
|
|
8869
|
-
metric: `${prefix}.turns`,
|
|
8870
|
-
source: {
|
|
8871
|
-
detail: sourceDetail,
|
|
8872
|
-
kind: "interior-telemetry"
|
|
8873
|
-
},
|
|
8874
|
-
unit: "count",
|
|
8875
|
-
value: interior.interiorTurns
|
|
8876
|
-
}, {
|
|
8877
|
-
label: `${interior.lane} ${interior.role} ${interior.engine} tool calls`,
|
|
8878
|
-
metric: `${prefix}.tool-calls`,
|
|
8879
|
-
source: {
|
|
8880
|
-
detail: sourceDetail,
|
|
8881
|
-
kind: "interior-telemetry"
|
|
8882
|
-
},
|
|
8883
|
-
unit: "count",
|
|
8884
|
-
value: interior.toolCalls
|
|
8885
|
-
}, {
|
|
8886
|
-
label: `${interior.lane} ${interior.role} ${interior.engine} verdict emitted`,
|
|
8887
|
-
metric: `${prefix}.verdict-emitted`,
|
|
8888
|
-
source: {
|
|
8889
|
-
detail: sourceDetail,
|
|
8890
|
-
kind: "interior-telemetry"
|
|
8891
|
-
},
|
|
8892
|
-
unit: "boolean",
|
|
8893
|
-
value: interior.verdictEmitted
|
|
8894
|
-
});
|
|
8895
|
-
if (interior.tokensByKind.status === "available") rows.push({
|
|
8896
|
-
label: `${interior.lane} ${interior.role} ${interior.engine} total tokens`,
|
|
8897
|
-
metric: `${prefix}.total-tokens`,
|
|
8898
|
-
source: {
|
|
8899
|
-
detail: `${sourceDetail};tokensSource=${interior.tokensByKind.source}`,
|
|
8900
|
-
kind: "interior-telemetry"
|
|
8901
|
-
},
|
|
8902
|
-
unit: "tokens",
|
|
8903
|
-
value: interior.tokensByKind.totalTokens
|
|
8904
|
-
});
|
|
8905
|
-
totalInteriorTurns += interior.interiorTurns;
|
|
8906
|
-
totalInteriorToolCalls += interior.toolCalls;
|
|
8907
|
-
}
|
|
8908
|
-
if (input.interiorRows.length > 0) {
|
|
8909
|
-
const interiorPaths = [...new Set(input.interiorRows.map((row) => interiorSourceDetail(row)))].join(",");
|
|
8910
|
-
rows.push({
|
|
8911
|
-
label: "Interior total turns",
|
|
8912
|
-
metric: INTERIOR_TOTAL_TURNS_METRIC,
|
|
8913
|
-
source: {
|
|
8914
|
-
detail: interiorPaths,
|
|
8915
|
-
kind: "interior-telemetry"
|
|
8916
|
-
},
|
|
8917
|
-
unit: "count",
|
|
8918
|
-
value: totalInteriorTurns
|
|
8919
|
-
}, {
|
|
8920
|
-
label: "Interior total tool calls",
|
|
8921
|
-
metric: INTERIOR_TOTAL_TOOL_CALLS_METRIC,
|
|
8922
|
-
source: {
|
|
8923
|
-
detail: interiorPaths,
|
|
8924
|
-
kind: "interior-telemetry"
|
|
8925
|
-
},
|
|
8926
|
-
unit: "count",
|
|
8927
|
-
value: totalInteriorToolCalls
|
|
8928
|
-
});
|
|
8929
|
-
}
|
|
8930
|
-
return rows.toSorted((left, right) => left.metric.localeCompare(right.metric));
|
|
8931
|
-
}
|
|
8932
|
-
//#endregion
|
|
8933
8345
|
//#region src/epic-closeout/report.ts
|
|
8934
|
-
const INTERIOR_TURN_ATTENTION = 3;
|
|
8935
8346
|
const ROW_LIMIT_BLINDSPOT_ID = "closeout-row-limit";
|
|
8936
|
-
const SUMMARY_METRICS = new Set([INTERIOR_TOTAL_TOOL_CALLS_METRIC, INTERIOR_TOTAL_TURNS_METRIC]);
|
|
8937
8347
|
const boundaryThermoSource = (owner) => ({
|
|
8938
8348
|
detail: `deep thermo owner: ${owner} (#559 boundary-thermo attestation)`,
|
|
8939
8349
|
kind: "boundary-thermo-attestation"
|
|
@@ -8984,76 +8394,31 @@ const assertMetricSources = (metrics) => {
|
|
|
8984
8394
|
for (const metric of metrics) if (!metric.source.detail.trim()) throw new CloseoutValidationError(`metric ${metric.metric} is missing source.detail`);
|
|
8985
8395
|
};
|
|
8986
8396
|
const boundCloseoutRows = (input) => {
|
|
8987
|
-
const
|
|
8988
|
-
const details = input.interiorMetrics.filter((metric) => !SUMMARY_METRICS.has(metric.metric));
|
|
8989
|
-
const retainedAlways = [...summaries, ...input.boundaryMetrics];
|
|
8397
|
+
const retainedAlways = [...input.boundaryMetrics];
|
|
8990
8398
|
let remaining = Math.max(0, MAX_CLOSEOUT_ROWS - retainedAlways.length - 1);
|
|
8991
8399
|
const blindspots = input.blindspots.filter((blindspot) => blindspot.id !== ROW_LIMIT_BLINDSPOT_ID).slice(0, remaining);
|
|
8992
8400
|
remaining -= blindspots.length;
|
|
8993
8401
|
const lessons = input.lessons.slice(0, remaining);
|
|
8994
8402
|
remaining -= lessons.length;
|
|
8995
|
-
const metrics = [...
|
|
8403
|
+
const metrics = [...retainedAlways].toSorted((left, right) => left.metric.localeCompare(right.metric));
|
|
8996
8404
|
const omittedBlindspots = input.blindspots.length - blindspots.length;
|
|
8997
8405
|
const omittedLessons = input.lessons.length - lessons.length;
|
|
8998
|
-
const omittedMetrics = input.
|
|
8406
|
+
const omittedMetrics = input.boundaryMetrics.length - metrics.length;
|
|
8999
8407
|
return {
|
|
9000
8408
|
blindspots: [...blindspots, {
|
|
9001
8409
|
id: ROW_LIMIT_BLINDSPOT_ID,
|
|
9002
|
-
note: `Closeout omitted ${omittedMetrics} metric row(s), ${omittedLessons} lesson row(s), and ${omittedBlindspots} blindspot row(s) to keep the strict artifact and ledger within ${MAX_CLOSEOUT_ROWS} rows;
|
|
8410
|
+
note: `Closeout omitted ${omittedMetrics} metric row(s), ${omittedLessons} lesson row(s), and ${omittedBlindspots} blindspot row(s) to keep the strict artifact and ledger within ${MAX_CLOSEOUT_ROWS} rows; boundary-thermo metrics are retained.`
|
|
9003
8411
|
}],
|
|
9004
8412
|
lessons,
|
|
9005
8413
|
metrics
|
|
9006
8414
|
};
|
|
9007
8415
|
};
|
|
9008
|
-
const epicScopingBlindspotNote = (scoping) => {
|
|
9009
|
-
const filters = [];
|
|
9010
|
-
if (scoping.issueFilter !== void 0) filters.push(`issue=${scoping.issueFilter}`);
|
|
9011
|
-
if (scoping.prFilter !== void 0) filters.push(`pr=${scoping.prFilter}`);
|
|
9012
|
-
const filterSuffix = filters.length > 0 ? ` + ${filters.join(" + ")} filter(s)` : "";
|
|
9013
|
-
const narrowNote = filters.length > 0 ? " Supplied trace filter(s) narrow the scan but do not guarantee epic-exact attribution." : "";
|
|
9014
|
-
const windowRepoScope = `date window (${scoping.window.from}..${scoping.window.to}) + repo ${scoping.repo}${filterSuffix}`;
|
|
9015
|
-
if (scoping.scopedByEpicIssueSet) return `Cursor-store interior telemetry was scoped to this epic's lanes via the chat/cwd allowlist; codex-exec logs are operator-supplied and not chat-filtered. Interior telemetry remains scoped by ${windowRepoScope}, NOT by the epic's child-issue set, so rows in this window may include concurrent non-epic factory work; closeout does not resolve child issues (bounded, no network).${narrowNote}`;
|
|
9016
|
-
return `Telemetry is scoped by ${windowRepoScope}, NOT by the epic's child-issue set. Interior rows in this window may include concurrent non-epic factory work; closeout does not resolve child issues (bounded, no network).${narrowNote}`;
|
|
9017
|
-
};
|
|
9018
8416
|
const DOLLAR_PRICING_DEFERRED_BLINDSPOT = {
|
|
9019
8417
|
id: "dollar-pricing-deferred-to-cost-analysis-skill",
|
|
9020
|
-
note: "
|
|
8418
|
+
note: "Native token families are harvested in the separate retro envelope. Pricing policy remains with the patronage-software-factory-cost-analysis skill (canonical PRICES/TIERS), so this boundary-thermo closeout neither reports token volume nor prices it."
|
|
9021
8419
|
};
|
|
9022
8420
|
const deriveBlindspots = (input, boundaryThermo) => {
|
|
9023
8421
|
const blindspots = [BASELINE_BLINDSPOT, DOLLAR_PRICING_DEFERRED_BLINDSPOT];
|
|
9024
|
-
if (input.budget.scoping) blindspots.push({
|
|
9025
|
-
id: "epic-scoping-is-window-not-child-issue-set",
|
|
9026
|
-
note: epicScopingBlindspotNote(input.budget.scoping)
|
|
9027
|
-
});
|
|
9028
|
-
else blindspots.push({
|
|
9029
|
-
id: "epic-scoping-is-window-not-child-issue-set",
|
|
9030
|
-
note: epicScopingBlindspotNote({
|
|
9031
|
-
epic: String(input.epic),
|
|
9032
|
-
issueFilter: void 0,
|
|
9033
|
-
prFilter: void 0,
|
|
9034
|
-
repo: input.repo,
|
|
9035
|
-
scopedByEpicIssueSet: false,
|
|
9036
|
-
window: {
|
|
9037
|
-
from: "unknown",
|
|
9038
|
-
to: "unknown"
|
|
9039
|
-
}
|
|
9040
|
-
})
|
|
9041
|
-
});
|
|
9042
|
-
if (input.interiorRows.length === 0) blindspots.push({
|
|
9043
|
-
id: "no-interior-telemetry",
|
|
9044
|
-
note: "no interior telemetry collected for this epic"
|
|
9045
|
-
});
|
|
9046
|
-
const unavailableByReason = /* @__PURE__ */ new Map();
|
|
9047
|
-
for (const row of input.interiorRows) if (row.tokensByKind.status === "unavailable") {
|
|
9048
|
-
const reason = row.tokensByKind.missingReason;
|
|
9049
|
-
unavailableByReason.set(reason, (unavailableByReason.get(reason) ?? 0) + 1);
|
|
9050
|
-
}
|
|
9051
|
-
const aggregatedUnavailable = [...unavailableByReason.entries()].map(([missingReason, count]) => ({
|
|
9052
|
-
id: `interior-tokens-unavailable-${missingReason}`,
|
|
9053
|
-
note: `token capture unavailable for ${count} interior row(s): ${missingReason}`,
|
|
9054
|
-
reason: missingReason
|
|
9055
|
-
})).toSorted((left, right) => left.id.localeCompare(right.id));
|
|
9056
|
-
blindspots.push(...aggregatedUnavailable);
|
|
9057
8422
|
if (isBoundaryThermoTripwireTripped(boundaryThermo)) blindspots.push({
|
|
9058
8423
|
id: BOUNDARY_THERMO_NOWHERE_BLINDSPOT_ID,
|
|
9059
8424
|
note: `TRIPWIRE (#559 'thermo nowhere'): this wave/epic closed with zero boundary-thermo runs and no waiver. Deep thermo owner: ${boundaryThermo.owner}. A wave closing with zero boundary-thermo runs is flagged; run the deep Oracle/Fable thermo at the boundary or record an explicit waiver.`,
|
|
@@ -9072,43 +8437,6 @@ const deriveLessons = (input, boundaryThermo) => {
|
|
|
9072
8437
|
rationale: waiverRationale,
|
|
9073
8438
|
source: boundaryThermoSource(boundaryThermo.owner)
|
|
9074
8439
|
});
|
|
9075
|
-
for (const row of input.interiorRows) {
|
|
9076
|
-
const disambig = interiorRowDisambig(row);
|
|
9077
|
-
if (row.interiorTurns >= INTERIOR_TURN_ATTENTION) lessons.push({
|
|
9078
|
-
id: `interior-turn-cap-${row.lane}-${row.role}-${row.engine}-${disambig}`,
|
|
9079
|
-
landingSpot: "profile",
|
|
9080
|
-
lesson: `interior loop hit/near the cap on ${row.lane}/${row.role}; consider tightening the brief or raising review signal`,
|
|
9081
|
-
rationale: `interiorTurns=${row.interiorTurns} >= cap ${INTERIOR_TURN_ATTENTION}`,
|
|
9082
|
-
source: {
|
|
9083
|
-
detail: row.source.chatId ?? row.source.path,
|
|
9084
|
-
kind: "interior-telemetry"
|
|
9085
|
-
}
|
|
9086
|
-
});
|
|
9087
|
-
}
|
|
9088
|
-
const unavailableLessonCounts = /* @__PURE__ */ new Map();
|
|
9089
|
-
for (const row of input.interiorRows) {
|
|
9090
|
-
if (row.tokensByKind.status !== "unavailable") continue;
|
|
9091
|
-
const { missingReason } = row.tokensByKind;
|
|
9092
|
-
const key = `${row.engine}\0${missingReason}`;
|
|
9093
|
-
const existing = unavailableLessonCounts.get(key);
|
|
9094
|
-
if (existing) existing.count += 1;
|
|
9095
|
-
else unavailableLessonCounts.set(key, {
|
|
9096
|
-
count: 1,
|
|
9097
|
-
engine: row.engine,
|
|
9098
|
-
missingReason
|
|
9099
|
-
});
|
|
9100
|
-
}
|
|
9101
|
-
const aggregatedTokenLessons = [...unavailableLessonCounts.values()].map(({ count, engine, missingReason }) => ({
|
|
9102
|
-
id: `wire-token-export-${engine}-${missingReason}`,
|
|
9103
|
-
landingSpot: "CLI",
|
|
9104
|
-
lesson: `token volume capture missing for ${count} ${engine} interior row(s); wire usage export so closeout can report its token volume (dollar pricing stays in the patronage-software-factory-cost-analysis skill)`,
|
|
9105
|
-
rationale: missingReason,
|
|
9106
|
-
source: {
|
|
9107
|
-
detail: `${count} row(s)`,
|
|
9108
|
-
kind: "interior-telemetry"
|
|
9109
|
-
}
|
|
9110
|
-
})).toSorted((left, right) => left.id.localeCompare(right.id));
|
|
9111
|
-
lessons.push(...aggregatedTokenLessons);
|
|
9112
8440
|
if (input.lessons) lessons.push(...input.lessons);
|
|
9113
8441
|
return lessons;
|
|
9114
8442
|
};
|
|
@@ -9120,7 +8448,7 @@ const buildLedgerRows = (input) => {
|
|
|
9120
8448
|
key: metric.metric,
|
|
9121
8449
|
repo: input.repo,
|
|
9122
8450
|
rowType: "metric",
|
|
9123
|
-
schemaVersion:
|
|
8451
|
+
schemaVersion: 4,
|
|
9124
8452
|
source: flattenSource(metric.source),
|
|
9125
8453
|
unit: metric.unit,
|
|
9126
8454
|
value: ledgerValue(metric.value)
|
|
@@ -9133,7 +8461,7 @@ const buildLedgerRows = (input) => {
|
|
|
9133
8461
|
landingSpot: lesson.landingSpot,
|
|
9134
8462
|
repo: input.repo,
|
|
9135
8463
|
rowType: "lesson",
|
|
9136
|
-
schemaVersion:
|
|
8464
|
+
schemaVersion: 4,
|
|
9137
8465
|
source: lesson.source ? flattenSource(lesson.source) : "derived",
|
|
9138
8466
|
value: lesson.lesson
|
|
9139
8467
|
});
|
|
@@ -9144,7 +8472,7 @@ const buildLedgerRows = (input) => {
|
|
|
9144
8472
|
key: blindspot.id,
|
|
9145
8473
|
repo: input.repo,
|
|
9146
8474
|
rowType: "blindspot",
|
|
9147
|
-
schemaVersion:
|
|
8475
|
+
schemaVersion: 4,
|
|
9148
8476
|
source: "derived",
|
|
9149
8477
|
value: blindspot.note
|
|
9150
8478
|
});
|
|
@@ -9156,9 +8484,8 @@ function buildEpicCloseoutReport(input) {
|
|
|
9156
8484
|
const epic = String(input.epic);
|
|
9157
8485
|
const generatedAt = input.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
9158
8486
|
const boundaryThermo = resolveBoundaryThermo(input.boundaryThermo);
|
|
9159
|
-
const interiorMetrics = buildMetricRows({ interiorRows: input.interiorRows });
|
|
9160
8487
|
const boundaryMetrics = boundaryThermoMetrics(boundaryThermo);
|
|
9161
|
-
let metrics = [...
|
|
8488
|
+
let metrics = [...boundaryMetrics];
|
|
9162
8489
|
let couldNotSee = deriveBlindspots(input, boundaryThermo);
|
|
9163
8490
|
if (couldNotSee.length === 0) throw new CloseoutValidationError("closeout must include a non-empty 'what this closeout could not see' section");
|
|
9164
8491
|
let lessons = deriveLessons(input, boundaryThermo);
|
|
@@ -9166,7 +8493,6 @@ function buildEpicCloseoutReport(input) {
|
|
|
9166
8493
|
const { blindspots: boundedBlindspots, lessons: boundedLessons, metrics: boundedMetrics } = boundCloseoutRows({
|
|
9167
8494
|
blindspots: couldNotSee,
|
|
9168
8495
|
boundaryMetrics,
|
|
9169
|
-
interiorMetrics,
|
|
9170
8496
|
lessons
|
|
9171
8497
|
});
|
|
9172
8498
|
couldNotSee = boundedBlindspots;
|
|
@@ -9196,7 +8522,7 @@ function buildEpicCloseoutReport(input) {
|
|
|
9196
8522
|
lessons,
|
|
9197
8523
|
metrics,
|
|
9198
8524
|
repo: input.repo,
|
|
9199
|
-
schemaVersion:
|
|
8525
|
+
schemaVersion: 4
|
|
9200
8526
|
};
|
|
9201
8527
|
return closeoutArtifactSchema.parse(artifact);
|
|
9202
8528
|
}
|
|
@@ -9704,7 +9030,7 @@ function renderCloseoutMarkdown(artifact) {
|
|
|
9704
9030
|
const ledgerLines = [
|
|
9705
9031
|
"## HQ ledger row-set",
|
|
9706
9032
|
"",
|
|
9707
|
-
`${artifact.ledgerRows.length} append-ready rows emitted to the sibling \`${sanitizeEpicFilename(artifact.epic)}.json\` (schemaVersion
|
|
9033
|
+
`${artifact.ledgerRows.length} append-ready rows emitted to the sibling \`${sanitizeEpicFilename(artifact.epic)}.json\` (schemaVersion 4) for the HQ lessons ledger (#433).`,
|
|
9708
9034
|
""
|
|
9709
9035
|
];
|
|
9710
9036
|
return [
|
|
@@ -9748,532 +9074,88 @@ function writeCloseoutArtifact(opts) {
|
|
|
9748
9074
|
};
|
|
9749
9075
|
}
|
|
9750
9076
|
//#endregion
|
|
9751
|
-
//#region src/
|
|
9752
|
-
|
|
9753
|
-
|
|
9754
|
-
|
|
9755
|
-
|
|
9756
|
-
* committed — see epic #132 / ADR 0018), validates the graph fail-closed, and
|
|
9757
|
-
* POSTs an `epic-structure` v1 event to HQ's `/api/ingest` boundary. This
|
|
9758
|
-
* module is the pure core: parse-to-IR validation, the wire mapping, and the
|
|
9759
|
-
* deterministic content-addressed eventId. All I/O (stdin/file read, fetch)
|
|
9760
|
-
* lives in the command wrapper.
|
|
9761
|
-
*
|
|
9762
|
-
* WIRE PARITY (do not fork the schema): the emitted payload is the exact shape
|
|
9763
|
-
* HQ's `EpicGraphSchema` accepts (`software-factory-hq/src/contracts/
|
|
9764
|
-
* epic-schemas.ts`) and is built by hand the same way `seed-dev.ts` builds it
|
|
9765
|
-
* — no re-declared zod twin. HQ's `epic-structure-parity.test.ts` imports
|
|
9766
|
-
* {@link buildEpicStructureEvent} and runs its output through the real
|
|
9767
|
-
* `EpicGraphSchema`, so any drift (field, enum, bound, strictness) fails CI.
|
|
9768
|
-
*
|
|
9769
|
-
* The `dag.yml` schema itself is documented with the epic skill (lane #137);
|
|
9770
|
-
* this module is the executable contract, not a second source of truth.
|
|
9771
|
-
*/
|
|
9772
|
-
const EPIC_STRUCTURE_SCHEMA_VERSION = 1;
|
|
9773
|
-
/**
|
|
9774
|
-
* DAG-node PLANNING statuses — the planning-plane vocabulary a planner authors
|
|
9775
|
-
* on a `dag.yml` node. Carried forward by ADR 0018 §4 and defined by the epic
|
|
9776
|
-
* skill (`reference/epic-artifacts.md`) and both `CONTEXT.md` glossaries.
|
|
9777
|
-
*
|
|
9778
|
-
* A DAG node is a unit of *plan*; an HQ lane is a unit of *runtime execution*.
|
|
9779
|
-
* These are two planes with two vocabularies and must not be conflated — the
|
|
9780
|
-
* producer validates and emits ONLY planning statuses:
|
|
9781
|
-
*
|
|
9782
|
-
* - `open` authored, not yet done
|
|
9783
|
-
* - `closed` done (a PR merged, or otherwise resolved)
|
|
9784
|
-
* - `satisfied-on-main` already true on main without a dedicated PR
|
|
9785
|
-
* - `parked` real node, scope still moving — relabeled off
|
|
9786
|
-
* `ready-for-agent`, never closed
|
|
9787
|
-
*
|
|
9788
|
-
* HQ's `DagNodeSchema` (`software-factory-hq/src/contracts/epic-schemas.ts`)
|
|
9789
|
-
* accepts this planning vocabulary on ingest AND overlays a runtime LANE status
|
|
9790
|
-
* onto a node once a lane is linked (`syncNodesWithLanes` /
|
|
9791
|
-
* `projectEpicMembership`) — the overlay is HQ-internal; the producer never
|
|
9792
|
-
* emits a lane status. HQ's `epic-structure-parity.test.ts` fails CI if the
|
|
9793
|
-
* producer emits any status HQ won't accept.
|
|
9794
|
-
*/
|
|
9795
|
-
const EPIC_STRUCTURE_NODE_STATUSES = [
|
|
9796
|
-
"open",
|
|
9797
|
-
"closed",
|
|
9798
|
-
"satisfied-on-main",
|
|
9799
|
-
"parked"
|
|
9800
|
-
];
|
|
9801
|
-
/**
|
|
9802
|
-
* Fail-closed validation error carrying every offending node/edge diagnostic.
|
|
9803
|
-
*/
|
|
9804
|
-
var EpicStructureValidationError = class extends Error {
|
|
9805
|
-
diagnostics;
|
|
9806
|
-
constructor(diagnostics) {
|
|
9807
|
-
super(`epic:publish-structure refused: dag graph is invalid\n${diagnostics.map((line) => ` - ${line}`).join("\n")}`);
|
|
9808
|
-
this.name = "EpicStructureValidationError";
|
|
9809
|
-
this.diagnostics = diagnostics;
|
|
9810
|
-
}
|
|
9077
|
+
//#region src/commands/closeout.ts
|
|
9078
|
+
const formatUtcDate = (date) => date.toISOString().slice(0, 10);
|
|
9079
|
+
const nonEmptyRepoRoot = (value) => {
|
|
9080
|
+
if (value.trim() === "") throw new InvalidArgumentError("--repo-root requires a non-empty path");
|
|
9081
|
+
return value;
|
|
9811
9082
|
};
|
|
9812
|
-
const
|
|
9813
|
-
const
|
|
9814
|
-
|
|
9815
|
-
|
|
9816
|
-
const
|
|
9817
|
-
|
|
9818
|
-
|
|
9819
|
-
|
|
9820
|
-
|
|
9821
|
-
|
|
9822
|
-
|
|
9823
|
-
|
|
9824
|
-
|
|
9825
|
-
const label = nonEmptyString(raw.slug) ? `"${raw.slug}"` : `node[${index}]`;
|
|
9826
|
-
if (!nonEmptyString(raw.slug)) diagnostics.push(`node[${index}]: missing required \`slug\``);
|
|
9827
|
-
else if (slugs.has(raw.slug)) diagnostics.push(`node ${label}: duplicate slug`);
|
|
9828
|
-
else slugs.add(raw.slug);
|
|
9829
|
-
if (!nonEmptyString(raw.title)) diagnostics.push(`node ${label}: missing required \`title\``);
|
|
9830
|
-
const external = raw.external === true;
|
|
9831
|
-
if (!external && !isPositiveInteger(raw.issue)) diagnostics.push(`node ${label}: \`issue\` number is required — non-external nodes must carry a minted issue (this runs post-ratification); set \`external: true\` to exempt an adoption/upstream node`);
|
|
9832
|
-
let status = "open";
|
|
9833
|
-
if (raw.status === void 0) status = "open";
|
|
9834
|
-
else if (EPIC_STRUCTURE_NODE_STATUSES.includes(raw.status)) status = raw.status;
|
|
9835
|
-
else diagnostics.push(`node ${label}: \`status\` must be one of ${EPIC_STRUCTURE_NODE_STATUSES.join(", ")} (DAG-node planning statuses, not HQ lane statuses)`);
|
|
9836
|
-
if (raw.laneId !== void 0 && !nonEmptyString(raw.laneId)) diagnostics.push(`node ${label}: \`laneId\` must be a non-empty string`);
|
|
9837
|
-
nodes.push({
|
|
9838
|
-
external,
|
|
9839
|
-
...isPositiveInteger(raw.issue) ? { issue: raw.issue } : {},
|
|
9840
|
-
...nonEmptyString(raw.laneId) ? { laneId: raw.laneId } : {},
|
|
9841
|
-
slug: nonEmptyString(raw.slug) ? raw.slug : `node[${index}]`,
|
|
9842
|
-
status,
|
|
9843
|
-
title: nonEmptyString(raw.title) ? raw.title : ""
|
|
9844
|
-
});
|
|
9845
|
-
}
|
|
9083
|
+
const codexThreadIdsForCloseout = (env = process.env) => {
|
|
9084
|
+
const threadId = normalizeSessionId(env.CODEX_THREAD_ID);
|
|
9085
|
+
return threadId === void 0 ? [] : [threadId];
|
|
9086
|
+
};
|
|
9087
|
+
const resolveRetroSubject = (issue, pr) => {
|
|
9088
|
+
if (issue !== void 0) return { issueNumber: issue };
|
|
9089
|
+
if (pr !== void 0) return { prNumber: pr };
|
|
9090
|
+
};
|
|
9091
|
+
const tokenFamilySummary = (tokenFamilies) => {
|
|
9092
|
+
const available = [];
|
|
9093
|
+
const unavailable = [];
|
|
9094
|
+
for (const family of ["claude", "gpt"]) if (tokenFamilies[family] === void 0) unavailable.push(family);
|
|
9095
|
+
else available.push(family);
|
|
9846
9096
|
return {
|
|
9847
|
-
|
|
9848
|
-
|
|
9097
|
+
available,
|
|
9098
|
+
unavailable
|
|
9849
9099
|
};
|
|
9850
9100
|
};
|
|
9851
|
-
const
|
|
9852
|
-
const
|
|
9853
|
-
|
|
9854
|
-
|
|
9855
|
-
|
|
9856
|
-
|
|
9857
|
-
}
|
|
9858
|
-
if (raw.type !== "depends-on") continue;
|
|
9859
|
-
if (!nonEmptyString(raw.from) || !nonEmptyString(raw.to)) {
|
|
9860
|
-
diagnostics.push(`edge[${index}]: depends-on edge requires string \`from\` and \`to\``);
|
|
9861
|
-
continue;
|
|
9862
|
-
}
|
|
9863
|
-
if (!slugs.has(raw.from)) diagnostics.push(`edge ${raw.from} -> ${raw.to}: \`from\` references unknown slug "${raw.from}"`);
|
|
9864
|
-
if (!slugs.has(raw.to)) diagnostics.push(`edge ${raw.from} -> ${raw.to}: \`to\` references unknown slug "${raw.to}"`);
|
|
9865
|
-
if (raw.from === raw.to) diagnostics.push(`edge ${raw.from} -> ${raw.to}: self-dependency (cycle)`);
|
|
9866
|
-
edges.push({
|
|
9867
|
-
from: raw.from,
|
|
9868
|
-
to: raw.to
|
|
9869
|
-
});
|
|
9870
|
-
}
|
|
9871
|
-
return edges;
|
|
9101
|
+
const reportLocalCloseoutOutcome = (output, input) => {
|
|
9102
|
+
const harvest = tokenFamilySummary(input.retro.envelope.tokenFamilies);
|
|
9103
|
+
output.stdout.write(`local artifacts: ${input.paths.markdownPath}, ${input.paths.jsonPath}, and ${input.retro.envelopePath}\n`);
|
|
9104
|
+
output.stdout.write(`boundary thermo input: runs=${input.boundaryThermo.runs}, owner=${input.boundaryThermo.owner}, waived=${input.boundaryThermo.waived}${input.boundaryThermo.waiverRationale === void 0 ? "" : `, waiver rationale=${input.boundaryThermo.waiverRationale}`}\n`);
|
|
9105
|
+
if (harvest.unavailable.length > 0) output.stderr.write(`harvest data unavailable: ${harvest.available.length === 0 ? "no token families were recorded" : `missing ${harvest.unavailable.join(" and ")} token family data`}; recorded gaps: ${input.retro.envelope.dataGaps.join(" | ")}\n`);
|
|
9106
|
+
else if (input.retro.envelope.dataGaps.length > 0) output.stderr.write(`retro-envelope data gaps recorded: ${input.retro.envelope.dataGaps.join(" | ")}\n`);
|
|
9872
9107
|
};
|
|
9873
|
-
|
|
9874
|
-
|
|
9875
|
-
|
|
9876
|
-
|
|
9877
|
-
|
|
9878
|
-
|
|
9879
|
-
const
|
|
9880
|
-
const
|
|
9881
|
-
|
|
9882
|
-
|
|
9883
|
-
|
|
9884
|
-
|
|
9885
|
-
|
|
9886
|
-
|
|
9887
|
-
|
|
9888
|
-
|
|
9889
|
-
|
|
9890
|
-
|
|
9891
|
-
|
|
9892
|
-
state.set(node, IN_STACK);
|
|
9893
|
-
stack.push(node);
|
|
9894
|
-
for (const next of adjacency.get(node) ?? []) {
|
|
9895
|
-
const marker = state.get(next) ?? UNVISITED;
|
|
9896
|
-
if (marker === IN_STACK) {
|
|
9897
|
-
const start = stack.indexOf(next);
|
|
9898
|
-
return [...stack.slice(start), next];
|
|
9899
|
-
}
|
|
9900
|
-
if (marker === UNVISITED) {
|
|
9901
|
-
const found = walk(next);
|
|
9902
|
-
if (found) return found;
|
|
9903
|
-
}
|
|
9904
|
-
}
|
|
9905
|
-
stack.pop();
|
|
9906
|
-
state.set(node, DONE);
|
|
9907
|
-
};
|
|
9908
|
-
for (const slug of slugs) if ((state.get(slug) ?? UNVISITED) === UNVISITED) {
|
|
9909
|
-
const found = walk(slug);
|
|
9910
|
-
if (found) return found;
|
|
9108
|
+
const reportRetroHqDeliveryOutcome = (output, outcome) => {
|
|
9109
|
+
output.stderr.write(`advisory HQ handoff ${outcome} for the retro envelope; local closeout success does not depend on HQ.\n`);
|
|
9110
|
+
};
|
|
9111
|
+
const reportArtifactHqHandoffOutcome = (output, outcome) => {
|
|
9112
|
+
output.stderr.write(`advisory HQ handoff ${outcome} for the closeout artifact; local closeout success does not depend on HQ.\n`);
|
|
9113
|
+
};
|
|
9114
|
+
const handoffCloseoutToHq = async (input) => {
|
|
9115
|
+
const epicIssue = Number(input.artifact.epic);
|
|
9116
|
+
let artifactOutcome = "deferred";
|
|
9117
|
+
try {
|
|
9118
|
+
artifactOutcome = await deferProjectHqIngest({
|
|
9119
|
+
cwd: input.repoRoot,
|
|
9120
|
+
kind: "epic-closeout",
|
|
9121
|
+
payload: input.artifact,
|
|
9122
|
+
payloadPath: input.written.jsonPath,
|
|
9123
|
+
...Number.isSafeInteger(epicIssue) && epicIssue > 0 ? { subject: { issueNumber: epicIssue } } : {}
|
|
9124
|
+
}, input.hq);
|
|
9125
|
+
} catch {
|
|
9126
|
+
artifactOutcome = "held";
|
|
9911
9127
|
}
|
|
9128
|
+
try {
|
|
9129
|
+
reportArtifactHqHandoffOutcome(input.output, artifactOutcome);
|
|
9130
|
+
} catch {}
|
|
9131
|
+
const retroSubject = resolveRetroSubject(input.options.issue, input.options.pr);
|
|
9132
|
+
let outcome = "held";
|
|
9133
|
+
try {
|
|
9134
|
+
outcome = await deferProjectHqIngest({
|
|
9135
|
+
cwd: input.repoRoot,
|
|
9136
|
+
kind: "retro-envelope",
|
|
9137
|
+
payload: input.retro.envelope,
|
|
9138
|
+
payloadPath: input.retro.envelopePath,
|
|
9139
|
+
...retroSubject === void 0 ? {} : { subject: retroSubject }
|
|
9140
|
+
}, input.hq);
|
|
9141
|
+
} catch {}
|
|
9142
|
+
try {
|
|
9143
|
+
reportRetroHqDeliveryOutcome(input.output, outcome);
|
|
9144
|
+
} catch {}
|
|
9912
9145
|
};
|
|
9913
|
-
const
|
|
9914
|
-
|
|
9915
|
-
|
|
9916
|
-
|
|
9917
|
-
|
|
9918
|
-
|
|
9919
|
-
|
|
9920
|
-
|
|
9921
|
-
const validateDagDocument = (input) => {
|
|
9922
|
-
const doc = input.document;
|
|
9923
|
-
const diagnostics = [];
|
|
9924
|
-
if (!isPlainObject(doc)) throw new EpicStructureValidationError(["dag document must be a YAML/JSON mapping with `epic` and `nodes`"]);
|
|
9925
|
-
const epicId = resolveEpicId(doc.epic);
|
|
9926
|
-
if (epicId === void 0) diagnostics.push("dag document requires a top-level `epic` id");
|
|
9927
|
-
const repo = input.repo ?? (nonEmptyString(doc.repo) ? doc.repo.trim() : void 0);
|
|
9928
|
-
if (repo === void 0) diagnostics.push("repo is required — pass `--repo <name>` or set `repo:` in the dag document (needed for the epic record and lane-id derivation)");
|
|
9929
|
-
const rawNodes = Array.isArray(doc.nodes) ? doc.nodes : void 0;
|
|
9930
|
-
if (rawNodes === void 0 || rawNodes.length === 0) diagnostics.push("dag document requires a non-empty `nodes` array");
|
|
9931
|
-
const { nodes, slugs } = validateNodes(rawNodes ?? [], diagnostics);
|
|
9932
|
-
const edges = validateEdges(Array.isArray(doc.edges) ? doc.edges : [], slugs, diagnostics);
|
|
9933
|
-
const cycle = findCycle(slugs, edges);
|
|
9934
|
-
if (cycle) diagnostics.push(`dependency cycle detected: ${cycle.join(" -> ")}`);
|
|
9935
|
-
if (diagnostics.length > 0 || epicId === void 0 || repo === void 0) throw new EpicStructureValidationError(diagnostics);
|
|
9936
|
-
return {
|
|
9937
|
-
boundary: slugify(input.boundary ?? (nonEmptyString(doc.boundary) ? doc.boundary : `epic-${epicId}`)),
|
|
9938
|
-
edges,
|
|
9939
|
-
epicId,
|
|
9940
|
-
name: input.name ?? (nonEmptyString(doc.name) ? doc.name.trim() : `Epic ${epicId}`),
|
|
9941
|
-
nodes,
|
|
9942
|
-
repo
|
|
9943
|
-
};
|
|
9944
|
-
};
|
|
9945
|
-
/**
|
|
9946
|
-
* Builds the wire payload with deterministic ordering (nodes by slug, edges by
|
|
9947
|
-
* from then to) so re-emission of the same graph is byte-stable regardless of
|
|
9948
|
-
* the planner's source ordering — the basis for the content-addressed eventId.
|
|
9949
|
-
*/
|
|
9950
|
-
const buildEpicStructurePayload = (dag) => {
|
|
9951
|
-
const nodes = dag.nodes.map((node) => {
|
|
9952
|
-
const laneId = node.laneId ?? (node.issue === void 0 ? void 0 : laneIdFor(dag.repo, node.issue));
|
|
9953
|
-
return {
|
|
9954
|
-
epicId: dag.epicId,
|
|
9955
|
-
...laneId === void 0 ? {} : { laneId },
|
|
9956
|
-
slug: node.slug,
|
|
9957
|
-
status: node.status,
|
|
9958
|
-
title: node.title
|
|
9959
|
-
};
|
|
9960
|
-
}).toSorted((a, b) => a.slug.localeCompare(b.slug));
|
|
9961
|
-
return {
|
|
9962
|
-
edges: dag.edges.map((edge) => ({
|
|
9963
|
-
epicId: dag.epicId,
|
|
9964
|
-
from: edge.from,
|
|
9965
|
-
to: edge.to,
|
|
9966
|
-
type: "depends-on"
|
|
9967
|
-
})).toSorted((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to)),
|
|
9968
|
-
epics: [{
|
|
9969
|
-
id: dag.epicId,
|
|
9970
|
-
name: dag.name,
|
|
9971
|
-
repo: dag.repo
|
|
9972
|
-
}],
|
|
9973
|
-
nodes
|
|
9974
|
-
};
|
|
9975
|
-
};
|
|
9976
|
-
/** Stable, key-sorted JSON for content hashing (values already ordered). */
|
|
9977
|
-
const stableStringify = (value) => {
|
|
9978
|
-
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
9979
|
-
if (isPlainObject(value)) return `{${Object.keys(value).toSorted().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
|
|
9980
|
-
return JSON.stringify(value ?? null);
|
|
9981
|
-
};
|
|
9982
|
-
/**
|
|
9983
|
-
* Deterministic eventId = `epic-structure-<boundary>-<sha256(payload)[0:16]>`.
|
|
9984
|
-
* Re-emitting identical graph content yields the same eventId (HQ dedups →
|
|
9985
|
-
* 200/duplicate); an amendment changes the content hash → a new eventId that
|
|
9986
|
-
* supersedes the prior structure wholesale (HQ keeps the latest). This mirrors
|
|
9987
|
-
* seed-dev's fixed-eventId idempotency, content-addressed instead of literal.
|
|
9988
|
-
*/
|
|
9989
|
-
const epicStructureEventId = (boundary, payload) => `epic-structure-${slugify(boundary)}-${createHash("sha256").update(stableStringify(payload)).digest("hex").slice(0, 16)}`;
|
|
9990
|
-
/**
|
|
9991
|
-
* Validates and builds the full `epic-structure` v1 ingest event, ready to POST
|
|
9992
|
-
* to `/api/ingest`. Throws {@link EpicStructureValidationError} fail-closed.
|
|
9993
|
-
*/
|
|
9994
|
-
const buildEpicStructureEvent = (input) => {
|
|
9995
|
-
const dag = validateDagDocument(input);
|
|
9996
|
-
const payload = buildEpicStructurePayload(dag);
|
|
9997
|
-
return {
|
|
9998
|
-
eventId: epicStructureEventId(dag.boundary, payload),
|
|
9999
|
-
kind: "epic-structure",
|
|
10000
|
-
observedAt: input.observedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
10001
|
-
payload,
|
|
10002
|
-
schemaVersion: 1
|
|
10003
|
-
};
|
|
10004
|
-
};
|
|
10005
|
-
/** The prod HQ worker is reachable at `hq.patronage.com` and its frozen
|
|
10006
|
-
* `-v1.*.workers.dev` alias. Emitting there without an Access service token is
|
|
10007
|
-
* fail-closed. */
|
|
10008
|
-
const isProductionHqUrl = (url) => url.includes("hq.patronage.com") || /-v1\.[^/]*\.workers\.dev/iu.test(url);
|
|
10009
|
-
/**
|
|
10010
|
-
* POSTs the event to HQ ingest. Fail-closed: a production URL without a
|
|
10011
|
-
* Cloudflare Access service token is refused before any request; a 401/403 is
|
|
10012
|
-
* surfaced with actionable guidance; any non-2xx throws.
|
|
10013
|
-
*/
|
|
10014
|
-
const publishEpicStructure = async (args) => {
|
|
10015
|
-
if (!args.accessServiceToken && isProductionHqUrl(args.url)) throw new Error(`epic:publish-structure refusing to publish to production ${args.url} without a Cloudflare Access service token: set CF-Access-Client-Id and CF-Access-Client-Secret`);
|
|
10016
|
-
const doFetch = args.fetchImpl ?? globalThis.fetch;
|
|
10017
|
-
const requestInit = args.accessServiceToken ? buildCloudflareAccessRequestInit(args.accessServiceToken, {
|
|
10018
|
-
body: JSON.stringify(args.event),
|
|
10019
|
-
headers: { "content-type": "application/json" },
|
|
10020
|
-
method: "POST",
|
|
10021
|
-
...args.signal ? { signal: args.signal } : {}
|
|
10022
|
-
}) : {
|
|
10023
|
-
body: JSON.stringify(args.event),
|
|
10024
|
-
headers: { "content-type": "application/json" },
|
|
10025
|
-
method: "POST",
|
|
10026
|
-
...args.signal ? { signal: args.signal } : {}
|
|
10027
|
-
};
|
|
10028
|
-
let response;
|
|
10029
|
-
try {
|
|
10030
|
-
const normalizedUrl = args.url.replace(/\/+$/u, "");
|
|
10031
|
-
response = await doFetch(normalizedUrl.endsWith("/api/ingest") ? normalizedUrl : `${normalizedUrl}/api/ingest`, requestInit);
|
|
10032
|
-
} catch {
|
|
10033
|
-
throw new Error(`epic:publish-structure could not reach HQ ingest at ${args.url}: request failed. Check the endpoint and network; redirects are refused to protect Cloudflare Access credentials.`);
|
|
10034
|
-
}
|
|
10035
|
-
const text = await response.text();
|
|
10036
|
-
if (response.status === 401 || response.status === 403) throw new Error(`epic:publish-structure rejected by Cloudflare Access or HQ ingest (${response.status}): set valid CF-Access-Client-Id and CF-Access-Client-Secret service-token inputs. Response: ${text}`);
|
|
10037
|
-
if (response.status < 200 || response.status >= 300) throw new Error(`epic:publish-structure failed: HQ ingest returned ${response.status} ${text}`);
|
|
10038
|
-
let duplicate = false;
|
|
10039
|
-
try {
|
|
10040
|
-
duplicate = JSON.parse(text).duplicate === true;
|
|
10041
|
-
} catch {
|
|
10042
|
-
duplicate = response.status === 200;
|
|
10043
|
-
}
|
|
10044
|
-
return {
|
|
10045
|
-
duplicate,
|
|
10046
|
-
eventId: args.event.eventId,
|
|
10047
|
-
status: response.status
|
|
10048
|
-
};
|
|
10049
|
-
};
|
|
10050
|
-
//#endregion
|
|
10051
|
-
//#region src/commands/epic-publish-structure.ts
|
|
10052
|
-
const STDIN = "-";
|
|
10053
|
-
/** Reads a dag.yml source path, or stdin when `source` is `-`. */
|
|
10054
|
-
const readSource = (source) => {
|
|
10055
|
-
const fd = source === STDIN ? 0 : source;
|
|
10056
|
-
try {
|
|
10057
|
-
return readFileSync(fd, "utf-8");
|
|
10058
|
-
} catch (error) {
|
|
10059
|
-
const cause = error instanceof Error ? error.message : String(error);
|
|
10060
|
-
throw new Error(`epic:publish-structure could not read ${source === STDIN ? "stdin" : source} (${cause})`, { cause: error });
|
|
10061
|
-
}
|
|
10062
|
-
};
|
|
10063
|
-
/** Parses a dag.yml/JSON document read from `readSource`. */
|
|
10064
|
-
const parseDocument = (raw, source) => {
|
|
10065
|
-
if (raw.trim() === "") throw new Error(`epic:publish-structure received empty input from ${source === STDIN ? "stdin" : source}`);
|
|
10066
|
-
try {
|
|
10067
|
-
return parse(raw);
|
|
10068
|
-
} catch (error) {
|
|
10069
|
-
const cause = error instanceof Error ? error.message : String(error);
|
|
10070
|
-
throw new Error(`epic:publish-structure could not parse YAML: ${cause}`, { cause: error });
|
|
10071
|
-
}
|
|
10072
|
-
};
|
|
10073
|
-
/**
|
|
10074
|
-
* Reads and validates a dag.yml/JSON source into a full `epic-structure` v1
|
|
10075
|
-
* event, ready to POST or dry-run print. Shared with `factory:closeout`'s
|
|
10076
|
-
* terminal-snapshot re-emission (issue #392) so both commands build the exact
|
|
10077
|
-
* same wire event from the exact same source format — one current contract,
|
|
10078
|
-
* not a forked parser.
|
|
10079
|
-
*/
|
|
10080
|
-
const buildEvent = (source, options = {}) => {
|
|
10081
|
-
return buildEpicStructureEvent({
|
|
10082
|
-
document: parseDocument(readSource(source), source),
|
|
10083
|
-
...options.boundary ? { boundary: options.boundary } : {},
|
|
10084
|
-
...options.name ? { name: options.name } : {},
|
|
10085
|
-
...options.repo ? { repo: options.repo } : {}
|
|
10086
|
-
});
|
|
10087
|
-
};
|
|
10088
|
-
function createEpicPublishStructureCommand(output, deps = {}) {
|
|
10089
|
-
const env = deps.env ?? process.env;
|
|
10090
|
-
return new Command("epic:publish-structure").description("Emit-only: read a planner's ephemeral dag.yml (or - for stdin), validate the graph fail-closed (YAML parses, required fields, edges reference known slugs, acyclic, issue numbers on every non-external node), and POST an epic-structure v1 event to HQ ingest. Idempotent by a content-addressed eventId (re-emission of an amendment supersedes the prior structure). Production auth uses the CF-Access-Client-Id and CF-Access-Client-Secret environment inputs. URL: --url or HQ_INGEST_URL. The dag.yml schema is documented with the epic skill (#137).").argument("[source]", "path to dag.yml, or - for stdin", STDIN).option("--url <url>", "HQ ingest base URL (default: HQ_INGEST_URL env). The command POSTs to <url>/api/ingest").option("--boundary <slug>", "boundary slug for the eventId namespace (default: dag `boundary:`, else `epic-<id>`)").option("--repo <name>", "repository name for the epic record and lane-id derivation (default: dag `repo:`)").option("--name <title>", "epic display name (default: dag `name:`)").option("--dry-run", "validate and build the event, print it, and exit without POSTing").option("--json", "print the built event / publish result as JSON").action(async (source, options) => {
|
|
10091
|
-
const event = buildEvent(source, options);
|
|
10092
|
-
if (options.dryRun) {
|
|
10093
|
-
output.stdout.write(options.json ? `${JSON.stringify(event, null, 2)}\n` : `epic:publish-structure OK (dry-run) — eventId ${event.eventId}, ${event.payload.nodes.length} node(s), ${event.payload.edges.length} edge(s)\n`);
|
|
10094
|
-
return;
|
|
10095
|
-
}
|
|
10096
|
-
const url = options.url ?? env.HQ_INGEST_URL;
|
|
10097
|
-
if (!url) throw new Error("epic:publish-structure requires an HQ ingest URL: pass --url or set HQ_INGEST_URL");
|
|
10098
|
-
const clientId = env[CF_ACCESS_CLIENT_ID_ENV];
|
|
10099
|
-
const clientSecret = env[CF_ACCESS_CLIENT_SECRET_ENV];
|
|
10100
|
-
const result = await publishEpicStructure({
|
|
10101
|
-
...clientId && clientSecret ? { accessServiceToken: {
|
|
10102
|
-
clientId,
|
|
10103
|
-
clientSecret
|
|
10104
|
-
} } : {},
|
|
10105
|
-
event,
|
|
10106
|
-
fetchImpl: deps.fetchImpl,
|
|
10107
|
-
url
|
|
10108
|
-
});
|
|
10109
|
-
output.stdout.write(options.json ? `${JSON.stringify(result, null, 2)}\n` : `epic:publish-structure ${result.duplicate ? "no-op (duplicate)" : "accepted"} — eventId ${result.eventId} [HTTP ${result.status}]\n`);
|
|
10110
|
-
});
|
|
10111
|
-
}
|
|
10112
|
-
//#endregion
|
|
10113
|
-
//#region src/commands/closeout.ts
|
|
10114
|
-
const formatUtcDate = (date) => date.toISOString().slice(0, 10);
|
|
10115
|
-
const nonEmptyRepoRoot = (value) => {
|
|
10116
|
-
if (value.trim() === "") throw new InvalidArgumentError("--repo-root requires a non-empty path");
|
|
10117
|
-
return value;
|
|
10118
|
-
};
|
|
10119
|
-
const codexThreadIdsForCloseout = (env = process.env) => {
|
|
10120
|
-
const threadId = normalizeSessionId(env.CODEX_THREAD_ID);
|
|
10121
|
-
return threadId === void 0 ? [] : [threadId];
|
|
10122
|
-
};
|
|
10123
|
-
const resolveRetroSubject = (issue, pr) => {
|
|
10124
|
-
if (issue !== void 0) return { issueNumber: issue };
|
|
10125
|
-
if (pr !== void 0) return { prNumber: pr };
|
|
10126
|
-
};
|
|
10127
|
-
const tokenFamilySummary = (tokenFamilies) => {
|
|
10128
|
-
const available = [];
|
|
10129
|
-
const unavailable = [];
|
|
10130
|
-
for (const family of ["claude", "gpt"]) if (tokenFamilies[family] === void 0) unavailable.push(family);
|
|
10131
|
-
else available.push(family);
|
|
10132
|
-
return {
|
|
10133
|
-
available,
|
|
10134
|
-
unavailable
|
|
10135
|
-
};
|
|
10136
|
-
};
|
|
10137
|
-
const reportLocalCloseoutOutcome = (output, input) => {
|
|
10138
|
-
const harvest = tokenFamilySummary(input.retro.envelope.tokenFamilies);
|
|
10139
|
-
output.stdout.write(`local artifacts: ${input.paths.markdownPath}, ${input.paths.jsonPath}, and ${input.retro.envelopePath}\n`);
|
|
10140
|
-
output.stdout.write(`boundary thermo input: runs=${input.boundaryThermo.runs}, owner=${input.boundaryThermo.owner}, waived=${input.boundaryThermo.waived}${input.boundaryThermo.waiverRationale === void 0 ? "" : `, waiver rationale=${input.boundaryThermo.waiverRationale}`}\n`);
|
|
10141
|
-
if (harvest.unavailable.length > 0) output.stderr.write(`harvest data unavailable: ${harvest.available.length === 0 ? "no token families were recorded" : `missing ${harvest.unavailable.join(" and ")} token family data`}; recorded gaps: ${input.retro.envelope.dataGaps.join(" | ")}\n`);
|
|
10142
|
-
else if (input.retro.envelope.dataGaps.length > 0) output.stderr.write(`retro-envelope data gaps recorded: ${input.retro.envelope.dataGaps.join(" | ")}\n`);
|
|
10143
|
-
};
|
|
10144
|
-
const reportRetroHqDeliveryOutcome = (output, outcome) => {
|
|
10145
|
-
output.stderr.write(`advisory HQ handoff ${outcome} for the retro envelope; local closeout success does not depend on HQ.\n`);
|
|
10146
|
-
};
|
|
10147
|
-
const reportArtifactHqHandoffOutcome = (output, outcome) => {
|
|
10148
|
-
output.stderr.write(`advisory HQ handoff ${outcome} for the closeout artifact; local closeout success does not depend on HQ.\n`);
|
|
10149
|
-
};
|
|
10150
|
-
const reportStructureReemissionOutcome = (output, outcome, detail) => {
|
|
10151
|
-
output.stderr.write(`advisory HQ epic-structure re-emission ${outcome}${detail ? ` (${detail})` : ""}; local closeout success does not depend on HQ.\n`);
|
|
10152
|
-
};
|
|
10153
|
-
const DEFAULT_STRUCTURE_REEMISSION_TIMEOUT_MS = 5e3;
|
|
10154
|
-
const withTimeout = async (operation, timeoutMs, message) => {
|
|
10155
|
-
const raceAbort = new AbortController();
|
|
10156
|
-
const fetchAbort = new AbortController();
|
|
10157
|
-
const settleTimeout = async () => {
|
|
10158
|
-
try {
|
|
10159
|
-
await setTimeout$1(Math.max(0, timeoutMs), void 0, { signal: raceAbort.signal });
|
|
10160
|
-
} catch {}
|
|
10161
|
-
return { status: "timed-out" };
|
|
10162
|
-
};
|
|
10163
|
-
const settleOperation = async () => ({
|
|
10164
|
-
status: "fulfilled",
|
|
10165
|
-
value: await operation(fetchAbort.signal)
|
|
10166
|
-
});
|
|
10167
|
-
try {
|
|
10168
|
-
const result = await Promise.race([settleOperation(), settleTimeout()]);
|
|
10169
|
-
if (result.status === "timed-out") {
|
|
10170
|
-
fetchAbort.abort();
|
|
10171
|
-
throw new Error(message);
|
|
10172
|
-
}
|
|
10173
|
-
return result.value;
|
|
10174
|
-
} finally {
|
|
10175
|
-
raceAbort.abort();
|
|
10176
|
-
}
|
|
10177
|
-
};
|
|
10178
|
-
const bareRepoName = (ownerRepo) => {
|
|
10179
|
-
const separatorIndex = ownerRepo.lastIndexOf("/");
|
|
10180
|
-
return separatorIndex === -1 ? ownerRepo : ownerRepo.slice(separatorIndex + 1);
|
|
10181
|
-
};
|
|
10182
|
-
const structureIdentityMismatch = (event, expected) => {
|
|
10183
|
-
const expectedRepo = bareRepoName(expected.repo);
|
|
10184
|
-
const { epics } = event.payload;
|
|
10185
|
-
if (epics.length === 0) return;
|
|
10186
|
-
if (epics.every((epic) => epic.id !== expected.epic)) return `dag epic id(s) [${epics.map((epic) => epic.id).join(", ")}] do not include --epic ${expected.epic}`;
|
|
10187
|
-
if (epics.every((epic) => epic.repo !== expectedRepo)) return `dag repo(s) [${epics.map((epic) => epic.repo).join(", ")}] do not include --repo ${expected.repo} (bare "${expectedRepo}")`;
|
|
10188
|
-
};
|
|
10189
|
-
const reemitEpicStructureAtCloseout = async (input) => {
|
|
10190
|
-
if (input.dagSource === void 0) return;
|
|
10191
|
-
const build = input.buildEvent ?? buildEvent;
|
|
10192
|
-
const publish = input.publish ?? publishEpicStructure;
|
|
10193
|
-
const url = input.url ?? input.env.HQ_INGEST_URL;
|
|
10194
|
-
if (!url) {
|
|
10195
|
-
reportStructureReemissionOutcome(input.output, "skipped", "no HQ ingest URL: pass --structure-url or set HQ_INGEST_URL");
|
|
10196
|
-
return;
|
|
10197
|
-
}
|
|
10198
|
-
let event;
|
|
10199
|
-
try {
|
|
10200
|
-
event = build(input.dagSource);
|
|
10201
|
-
} catch (error) {
|
|
10202
|
-
reportStructureReemissionOutcome(input.output, "failed", error instanceof Error ? error.message : String(error));
|
|
10203
|
-
return;
|
|
10204
|
-
}
|
|
10205
|
-
const mismatch = structureIdentityMismatch(event, {
|
|
10206
|
-
epic: input.expectedEpic,
|
|
10207
|
-
repo: input.expectedRepo
|
|
10208
|
-
});
|
|
10209
|
-
if (mismatch !== void 0) {
|
|
10210
|
-
reportStructureReemissionOutcome(input.output, "failed", mismatch);
|
|
10211
|
-
return;
|
|
10212
|
-
}
|
|
10213
|
-
const clientId = input.env[CF_ACCESS_CLIENT_ID_ENV];
|
|
10214
|
-
const clientSecret = input.env[CF_ACCESS_CLIENT_SECRET_ENV];
|
|
10215
|
-
const timeoutMs = input.timeoutMs ?? DEFAULT_STRUCTURE_REEMISSION_TIMEOUT_MS;
|
|
10216
|
-
try {
|
|
10217
|
-
const result = await withTimeout((signal) => publish({
|
|
10218
|
-
...clientId && clientSecret ? { accessServiceToken: {
|
|
10219
|
-
clientId,
|
|
10220
|
-
clientSecret
|
|
10221
|
-
} } : {},
|
|
10222
|
-
event,
|
|
10223
|
-
fetchImpl: input.fetchImpl,
|
|
10224
|
-
signal,
|
|
10225
|
-
url
|
|
10226
|
-
}), timeoutMs, `epic-structure re-emission exceeded its ${timeoutMs}ms advisory deadline`);
|
|
10227
|
-
reportStructureReemissionOutcome(input.output, result.duplicate ? "duplicate" : "accepted", `eventId ${result.eventId}`);
|
|
10228
|
-
} catch (error) {
|
|
10229
|
-
reportStructureReemissionOutcome(input.output, "failed", error instanceof Error ? error.message : String(error));
|
|
10230
|
-
}
|
|
10231
|
-
};
|
|
10232
|
-
const handoffCloseoutToHq = async (input) => {
|
|
10233
|
-
const epicIssue = Number(input.artifact.epic);
|
|
10234
|
-
let artifactOutcome = "deferred";
|
|
10235
|
-
try {
|
|
10236
|
-
artifactOutcome = await deferProjectHqIngest({
|
|
10237
|
-
cwd: input.repoRoot,
|
|
10238
|
-
kind: "epic-closeout",
|
|
10239
|
-
payload: input.artifact,
|
|
10240
|
-
payloadPath: input.written.jsonPath,
|
|
10241
|
-
...Number.isSafeInteger(epicIssue) && epicIssue > 0 ? { subject: { issueNumber: epicIssue } } : {}
|
|
10242
|
-
}, input.hq);
|
|
10243
|
-
} catch {
|
|
10244
|
-
artifactOutcome = "held";
|
|
10245
|
-
}
|
|
10246
|
-
try {
|
|
10247
|
-
reportArtifactHqHandoffOutcome(input.output, artifactOutcome);
|
|
10248
|
-
} catch {}
|
|
10249
|
-
const retroSubject = resolveRetroSubject(input.options.issue, input.options.pr);
|
|
10250
|
-
let outcome = "held";
|
|
10251
|
-
try {
|
|
10252
|
-
outcome = await deferProjectHqIngest({
|
|
10253
|
-
cwd: input.repoRoot,
|
|
10254
|
-
kind: "retro-envelope",
|
|
10255
|
-
payload: input.retro.envelope,
|
|
10256
|
-
payloadPath: input.retro.envelopePath,
|
|
10257
|
-
...retroSubject === void 0 ? {} : { subject: retroSubject }
|
|
10258
|
-
}, input.hq);
|
|
10259
|
-
} catch {}
|
|
10260
|
-
try {
|
|
10261
|
-
reportRetroHqDeliveryOutcome(input.output, outcome);
|
|
10262
|
-
} catch {}
|
|
10263
|
-
};
|
|
10264
|
-
const defaultTraceWindow = () => {
|
|
10265
|
-
const to = /* @__PURE__ */ new Date();
|
|
10266
|
-
const from = new Date(to);
|
|
10267
|
-
from.setUTCDate(from.getUTCDate() - 30);
|
|
10268
|
-
return {
|
|
10269
|
-
from: formatUtcDate(from),
|
|
10270
|
-
to: formatUtcDate(to)
|
|
10271
|
-
};
|
|
9146
|
+
const defaultTraceWindow = () => {
|
|
9147
|
+
const to = /* @__PURE__ */ new Date();
|
|
9148
|
+
const from = new Date(to);
|
|
9149
|
+
from.setUTCDate(from.getUTCDate() - 30);
|
|
9150
|
+
return {
|
|
9151
|
+
from: formatUtcDate(from),
|
|
9152
|
+
to: formatUtcDate(to)
|
|
9153
|
+
};
|
|
10272
9154
|
};
|
|
10273
9155
|
function createCloseoutCommand(output, dependencies = {}) {
|
|
10274
9156
|
const buildRetroGate = dependencies.buildRetroEnvelope ?? buildAndPersistRetroEnvelope;
|
|
10275
9157
|
const persistArtifact = dependencies.writeArtifact ?? writeCloseoutArtifact;
|
|
10276
|
-
return new Command("factory:closeout").description("Emit an epic-close metrics + lessons closeout (markdown + HQ-ledger JSON)").requiredOption("--epic <id>", "epic identifier").option("--repo
|
|
9158
|
+
return new Command("factory:closeout").description("Emit an epic-close metrics + lessons closeout (markdown + HQ-ledger JSON)").requiredOption("--epic <id>", "epic identifier").option("--repo-root <path>", "consumer repo root for trace shards and output", nonEmptyRepoRoot, ".").option("--out-dir <path>", "output directory under repo root", ".factory-memory/closeouts").option("--from <YYYY-MM-DD>", "trace diagnostics scan window start — surfaces malformed trace shards only, no metrics (default: today minus 30 days UTC)").option("--to <YYYY-MM-DD>", "trace diagnostics scan window end (default: today UTC)").option("--issue <n>", "optional issue-number filter for the trace diagnostics scan", Number.parseInt).option("--pr <n>", "optional pull-request-number filter for the trace diagnostics scan", Number.parseInt).option("--json", "print the artifact JSON to stdout").option("--dry-run", "run aggregation without writing files; print intended output paths").option("--boundary-thermo-runs <n>", "deep boundary-thermo run count for this wave/epic (default: 0 → tripwire)", (value) => sanitizeBoundaryThermoRuns(value)).option("--boundary-thermo-owner <name>", "named owner of the boundary-thermo gate (default: epic-closeout orchestrator)").option("--boundary-thermo-waived", "record an explicit waiver for zero boundary-thermo runs").option("--boundary-thermo-waiver <text>", "waiver rationale when --boundary-thermo-waived is set").action(withGateTiming({
|
|
10277
9159
|
gate: "factory:closeout",
|
|
10278
9160
|
resolveLedgerRoot: (options) => resolveCwdOption(options.repoRoot),
|
|
10279
9161
|
shouldRecord: (options) => options.dryRun !== true,
|
|
@@ -10283,25 +9165,15 @@ function createCloseoutCommand(output, dependencies = {}) {
|
|
|
10283
9165
|
const from = options.from ?? defaults.from;
|
|
10284
9166
|
const to = options.to ?? defaults.to;
|
|
10285
9167
|
const repoRoot = resolveCwdOption(options.repoRoot);
|
|
10286
|
-
const
|
|
10287
|
-
const epicChatCwds = options.epicChatCwd ?? [];
|
|
10288
|
-
const epicChatScope = epicChatIds.length > 0 || epicChatCwds.length > 0 ? {
|
|
10289
|
-
...epicChatIds.length > 0 ? { chatIds: epicChatIds } : {},
|
|
10290
|
-
...epicChatCwds.length > 0 ? { cwds: epicChatCwds } : {}
|
|
10291
|
-
} : void 0;
|
|
9168
|
+
const repo = repositorySlug(checkoutRepository(repoRoot));
|
|
10292
9169
|
const gathered = gatherCloseoutInputs({
|
|
10293
|
-
chatsDir: options.chatsDir,
|
|
10294
|
-
codexLogPath: options.codexLog,
|
|
10295
9170
|
epic: options.epic,
|
|
10296
|
-
...epicChatScope === void 0 ? {} : { epicChatScope },
|
|
10297
9171
|
from,
|
|
10298
9172
|
issue: options.issue,
|
|
10299
|
-
lane: options.lane,
|
|
10300
9173
|
pr: options.pr,
|
|
10301
|
-
repo
|
|
9174
|
+
repo,
|
|
10302
9175
|
repoRoot,
|
|
10303
|
-
to
|
|
10304
|
-
usageCsvPath: options.usageCsv
|
|
9176
|
+
to
|
|
10305
9177
|
});
|
|
10306
9178
|
if (gathered.diagnostics.length > 0) output.stderr.write(`factory:closeout trace diagnostics: ${gathered.diagnostics.length}\n`);
|
|
10307
9179
|
const artifact = buildEpicCloseoutReport({
|
|
@@ -10313,8 +9185,7 @@ function createCloseoutCommand(output, dependencies = {}) {
|
|
|
10313
9185
|
},
|
|
10314
9186
|
budget: gathered.budget,
|
|
10315
9187
|
epic: options.epic,
|
|
10316
|
-
|
|
10317
|
-
repo: options.repo
|
|
9188
|
+
repo
|
|
10318
9189
|
});
|
|
10319
9190
|
if (options.json) output.stdout.write(`${JSON.stringify(artifact, null, 2)}\n`);
|
|
10320
9191
|
const paths = closeoutArtifactPaths({
|
|
@@ -10332,11 +9203,11 @@ function createCloseoutCommand(output, dependencies = {}) {
|
|
|
10332
9203
|
repoRoot
|
|
10333
9204
|
});
|
|
10334
9205
|
const retro = buildRetroGate({
|
|
10335
|
-
codexThreadIds: codexThreadIdsForCloseout(),
|
|
9206
|
+
codexThreadIds: codexThreadIdsForCloseout(dependencies.env),
|
|
10336
9207
|
epic: options.epic,
|
|
10337
9208
|
...options.issue === void 0 ? {} : { issue: options.issue },
|
|
10338
9209
|
...options.pr === void 0 ? {} : { pr: options.pr },
|
|
10339
|
-
repo
|
|
9210
|
+
repo,
|
|
10340
9211
|
repoRoot
|
|
10341
9212
|
});
|
|
10342
9213
|
reportLocalCloseoutOutcome(output, {
|
|
@@ -10353,18 +9224,6 @@ function createCloseoutCommand(output, dependencies = {}) {
|
|
|
10353
9224
|
retro,
|
|
10354
9225
|
written
|
|
10355
9226
|
});
|
|
10356
|
-
await reemitEpicStructureAtCloseout({
|
|
10357
|
-
buildEvent: dependencies.buildEpicStructureEvent,
|
|
10358
|
-
dagSource: options.structureDag,
|
|
10359
|
-
env: dependencies.env ?? process.env,
|
|
10360
|
-
expectedEpic: options.epic,
|
|
10361
|
-
expectedRepo: options.repo,
|
|
10362
|
-
fetchImpl: dependencies.structureFetch,
|
|
10363
|
-
output,
|
|
10364
|
-
publish: dependencies.publishEpicStructure,
|
|
10365
|
-
timeoutMs: dependencies.structureTimeoutMs,
|
|
10366
|
-
url: options.structureUrl
|
|
10367
|
-
});
|
|
10368
9227
|
output.stdout.write(`wrote ${written.markdownPath} and ${written.jsonPath}\n`);
|
|
10369
9228
|
}));
|
|
10370
9229
|
}
|
|
@@ -10431,54 +9290,6 @@ function createConfigCommand(output, deps = {}) {
|
|
|
10431
9290
|
return config;
|
|
10432
9291
|
}
|
|
10433
9292
|
//#endregion
|
|
10434
|
-
//#region src/checkout-repository.ts
|
|
10435
|
-
function normalizeRepositoryIdentity(repository) {
|
|
10436
|
-
return {
|
|
10437
|
-
name: repository.name.replace(/\.git$/iu, "").toLowerCase(),
|
|
10438
|
-
owner: repository.owner.toLowerCase()
|
|
10439
|
-
};
|
|
10440
|
-
}
|
|
10441
|
-
const GITHUB_HOSTNAME = "github.com";
|
|
10442
|
-
const SCP_GITHUB_REMOTE_PATTERN = /^(?:[^@\s]+@)?github\.com:(?<path>[^\s]+)$/iu;
|
|
10443
|
-
function githubRepositoryPath(remoteUrl) {
|
|
10444
|
-
const scpPath = SCP_GITHUB_REMOTE_PATTERN.exec(remoteUrl)?.groups?.path;
|
|
10445
|
-
if (scpPath) return scpPath;
|
|
10446
|
-
try {
|
|
10447
|
-
const url = new URL(remoteUrl);
|
|
10448
|
-
if (url.hostname.toLowerCase() !== GITHUB_HOSTNAME) return;
|
|
10449
|
-
return url.pathname.replace(/^\//u, "");
|
|
10450
|
-
} catch {
|
|
10451
|
-
return;
|
|
10452
|
-
}
|
|
10453
|
-
}
|
|
10454
|
-
function parseCheckoutRepository(remoteUrl) {
|
|
10455
|
-
const segments = githubRepositoryPath(remoteUrl.trim())?.split("/") ?? [];
|
|
10456
|
-
const [owner, rawName] = segments;
|
|
10457
|
-
const name = rawName?.replace(/\.git$/u, "");
|
|
10458
|
-
if (!(owner && name) || segments.length !== 2) throw new Error(`Could not derive a GitHub repository from checkout origin ${remoteUrl}; configure origin as a github.com owner/repository remote.`);
|
|
10459
|
-
return normalizeRepositoryIdentity({
|
|
10460
|
-
name,
|
|
10461
|
-
owner
|
|
10462
|
-
});
|
|
10463
|
-
}
|
|
10464
|
-
function checkoutRepository(cwd) {
|
|
10465
|
-
let remoteUrl;
|
|
10466
|
-
try {
|
|
10467
|
-
remoteUrl = runCapture("git", [
|
|
10468
|
-
"remote",
|
|
10469
|
-
"get-url",
|
|
10470
|
-
"origin"
|
|
10471
|
-
], cwd).stdout;
|
|
10472
|
-
} catch {
|
|
10473
|
-
throw new Error("Could not read checkout origin; repository routing must be rooted in trusted checkout configuration.");
|
|
10474
|
-
}
|
|
10475
|
-
return parseCheckoutRepository(remoteUrl);
|
|
10476
|
-
}
|
|
10477
|
-
const repositorySlug = (repository) => {
|
|
10478
|
-
const normalized = normalizeRepositoryIdentity(repository);
|
|
10479
|
-
return `${normalized.owner}/${normalized.name}`;
|
|
10480
|
-
};
|
|
10481
|
-
//#endregion
|
|
10482
9293
|
//#region src/candidate-identity.ts
|
|
10483
9294
|
var CandidateIdentityError = class extends Error {
|
|
10484
9295
|
constructor(message) {
|
|
@@ -10684,23 +9495,18 @@ const bindingRefusalReason = (name) => `Required check "${name}" has no passing,
|
|
|
10684
9495
|
/**
|
|
10685
9496
|
* The `evidence:emit` invocation that would satisfy a specific demanded check.
|
|
10686
9497
|
*
|
|
10687
|
-
* Every flag here is one the
|
|
10688
|
-
* `--outcome`, and `--producer`
|
|
10689
|
-
* envelope additionally MUST carry a rung (ADR 0014 §3), so the rung flag
|
|
10690
|
-
* appears only for review checks and never for verify checks, which must not
|
|
10691
|
-
* carry one. `--cwd` and `--base` are left off deliberately: their defaults
|
|
9498
|
+
* Every flag here is one the current verify-only writer requires: `--check`,
|
|
9499
|
+
* `--outcome`, and `--producer`. `--cwd` and `--base` are left off deliberately: their defaults
|
|
10692
9500
|
* (`.` and `origin/main`) are already what a lane in its own checkout wants,
|
|
10693
9501
|
* and the default output path is {@link EVIDENCE_DIR}, which is where this
|
|
10694
9502
|
* evaluation just looked. Only `--producer` is left as a placeholder, because
|
|
10695
9503
|
* the producing identity is the caller's to name.
|
|
10696
9504
|
*/
|
|
10697
|
-
const evidenceEmitInvocation = (check) => [
|
|
9505
|
+
const evidenceEmitInvocation = (check) => check.checkType === "review" ? "review-type requiredChecks are historical-only; current profiles cannot declare or emit them" : [
|
|
10698
9506
|
"patronage-factory evidence:emit",
|
|
10699
9507
|
`--check "${check.name}"`,
|
|
10700
|
-
`--check-type ${check.checkType}`,
|
|
10701
9508
|
"--outcome pass",
|
|
10702
|
-
"--producer <id>"
|
|
10703
|
-
...check.checkType === "review" ? ["--rung <rung>"] : []
|
|
9509
|
+
"--producer <id>"
|
|
10704
9510
|
].join(" ");
|
|
10705
9511
|
/**
|
|
10706
9512
|
* The refusal when a demanded check has no envelope at all.
|
|
@@ -10901,7 +9707,7 @@ function buildEvidenceEnvelope(args, dependencies = {}) {
|
|
|
10901
9707
|
const sessionId = resolveEmitSessionId(args.sessionId, env);
|
|
10902
9708
|
const parsed = parseEvidenceEnvelope({
|
|
10903
9709
|
check: args.check,
|
|
10904
|
-
checkType:
|
|
9710
|
+
checkType: "verify",
|
|
10905
9711
|
headSha,
|
|
10906
9712
|
mergeBaseSha: merge,
|
|
10907
9713
|
outcome: args.outcome,
|
|
@@ -10912,11 +9718,7 @@ function buildEvidenceEnvelope(args, dependencies = {}) {
|
|
|
10912
9718
|
...args.findingsPointer ? { findingsPointer: args.findingsPointer } : {},
|
|
10913
9719
|
...args.policyVersion ? { policyVersion: args.policyVersion } : {},
|
|
10914
9720
|
...sessionId ? { sessionId } : {},
|
|
10915
|
-
...args.requestId ? { requestId: args.requestId } : {}
|
|
10916
|
-
...args.checkType === "review" ? {
|
|
10917
|
-
...args.rung ? { rung: args.rung } : {},
|
|
10918
|
-
...args.model ? { model: args.model } : {}
|
|
10919
|
-
} : {}
|
|
9721
|
+
...args.requestId ? { requestId: args.requestId } : {}
|
|
10920
9722
|
});
|
|
10921
9723
|
if (!parsed.ok) throw new Error(`Refusing to emit an invalid evidence envelope: ${parsed.error}`);
|
|
10922
9724
|
return parsed.envelope;
|
|
@@ -11657,7 +10459,7 @@ function runPrVerify(args, dependencies = {}) {
|
|
|
11657
10459
|
mode: resolvedMode,
|
|
11658
10460
|
patchId,
|
|
11659
10461
|
profilePath,
|
|
11660
|
-
projectKey: profile.
|
|
10462
|
+
projectKey: profile.repository.name,
|
|
11661
10463
|
repository,
|
|
11662
10464
|
startedAt
|
|
11663
10465
|
};
|
|
@@ -11740,7 +10542,7 @@ function runPrVerify(args, dependencies = {}) {
|
|
|
11740
10542
|
const baselineFullProofs = resolvedMode === "docs-only" ? baselineFullProofsForDocsOnly({
|
|
11741
10543
|
previousProof,
|
|
11742
10544
|
profilePath,
|
|
11743
|
-
projectKey: profile.
|
|
10545
|
+
projectKey: profile.repository.name,
|
|
11744
10546
|
repository
|
|
11745
10547
|
}) : void 0;
|
|
11746
10548
|
const proof = buildPrVerifyProof({
|
|
@@ -11755,7 +10557,6 @@ function runPrVerify(args, dependencies = {}) {
|
|
|
11755
10557
|
runEvidenceEmit({
|
|
11756
10558
|
base: args.base,
|
|
11757
10559
|
check: command.requiredCheck,
|
|
11758
|
-
checkType: "verify",
|
|
11759
10560
|
cwd,
|
|
11760
10561
|
outcome: "pass",
|
|
11761
10562
|
policyVersion: "patronage-factory/pr-verify/v1",
|
|
@@ -11932,23 +10733,24 @@ function createDemandWaiveCommand(_output, action = runDemandWaive) {
|
|
|
11932
10733
|
/**
|
|
11933
10734
|
* Resolve the review modes demanded by a candidate's changed files.
|
|
11934
10735
|
*
|
|
11935
|
-
*
|
|
11936
|
-
*
|
|
11937
|
-
* names it matches any changed file.
|
|
10736
|
+
* Correctness is structural factory policy. Security is additionally demanded
|
|
10737
|
+
* when any repository-declared security path matches the candidate.
|
|
11938
10738
|
*/
|
|
11939
10739
|
const resolveApplicableReviewModes = (profile, changedFiles) => {
|
|
11940
|
-
|
|
11941
|
-
const conditionalModes = new Set(conditional.flatMap((scope) => scope.modes));
|
|
11942
|
-
const seen = /* @__PURE__ */ new Set();
|
|
11943
|
-
return profile.review.modes.filter((mode) => {
|
|
11944
|
-
if (seen.has(mode)) return false;
|
|
11945
|
-
seen.add(mode);
|
|
11946
|
-
return !conditionalModes.has(mode) || conditional.some((scope) => scope.modes.includes(mode) && changedFiles.some((file) => matchesAnyGlob(scope.paths, file)));
|
|
11947
|
-
});
|
|
10740
|
+
return (profile.review?.conditional ?? []).some((scope) => changedFiles.some((file) => matchesAnyGlob(scope.paths, file))) ? ["correctness", "security"] : ["correctness"];
|
|
11948
10741
|
};
|
|
10742
|
+
/**
|
|
10743
|
+
* Convert schema-v5 profile requiredChecks into the executable evidence
|
|
10744
|
+
* demands consumed by readiness and its preflight presentation. Schema-v5
|
|
10745
|
+
* checks are verify-only; no other module should reintroduce that policy.
|
|
10746
|
+
*/
|
|
10747
|
+
const resolveProfileRequiredChecks = (profile) => (profile.requiredChecks ?? []).map((check) => ({
|
|
10748
|
+
...check,
|
|
10749
|
+
checkType: "verify"
|
|
10750
|
+
}));
|
|
11949
10751
|
const resolveCandidateDemands = ({ changedFiles, profile }) => ({
|
|
11950
|
-
ladderPolicy: resolveReviewLadderPolicy(
|
|
11951
|
-
requiredChecks: profile
|
|
10752
|
+
ladderPolicy: resolveReviewLadderPolicy(),
|
|
10753
|
+
requiredChecks: resolveProfileRequiredChecks(profile),
|
|
11952
10754
|
reviewModes: resolveApplicableReviewModes(profile, changedFiles)
|
|
11953
10755
|
});
|
|
11954
10756
|
/**
|
|
@@ -12383,7 +11185,7 @@ const verifyProofCheck = ({ candidate, candidateError, preread, verifyProofPath
|
|
|
12383
11185
|
* modes; currency and acceptance come from the same identity and
|
|
12384
11186
|
* terminal-state helpers `pr:publish` uses.
|
|
12385
11187
|
*/
|
|
12386
|
-
const reviewProofCheck = ({ candidate, candidateError,
|
|
11188
|
+
const reviewProofCheck = ({ candidate, candidateError, proof, reviewModes }) => {
|
|
12387
11189
|
const name = "admission:review-proof";
|
|
12388
11190
|
if (!candidate) return {
|
|
12389
11191
|
message: `Review proof state is unknown: no git candidate is resolvable here (${candidateError ?? "unknown error"}).`,
|
|
@@ -12402,7 +11204,7 @@ const reviewProofCheck = ({ candidate, candidateError, profile, proof, reviewMod
|
|
|
12402
11204
|
reviewedHeadSha: proof?.headSha,
|
|
12403
11205
|
reviewedPatchId: proof?.patchId
|
|
12404
11206
|
});
|
|
12405
|
-
const terminal = proof ? resolveReviewTerminalStateAcrossReviewsForPolicy(proof, resolveReviewLadderPolicy(
|
|
11207
|
+
const terminal = proof ? resolveReviewTerminalStateAcrossReviewsForPolicy(proof, resolveReviewLadderPolicy()) : void 0;
|
|
12406
11208
|
const accepted = status === "current" && (terminal === "clean" || terminal === "accepted-with-findings");
|
|
12407
11209
|
return {
|
|
12408
11210
|
message: accepted ? `pr:review proof is current for this candidate (${terminal}) across ${reviewModes.join(", ")}.` : `pr:review proof is ${status}${terminal ? ` (${terminal})` : ""}; ${reviewModes.join(", ")} review(s) are required. Have an independent clean session write findings and pass them to pr:publish --findings <path>.`,
|
|
@@ -12422,7 +11224,7 @@ const requiredCheckMessage = (outcome) => {
|
|
|
12422
11224
|
* independence stay exactly one implementation.
|
|
12423
11225
|
*/
|
|
12424
11226
|
const requiredCheckChecks = ({ candidate, candidateError, cwd, profile, verifyProof }) => {
|
|
12425
|
-
const requiredChecks = profile
|
|
11227
|
+
const requiredChecks = resolveProfileRequiredChecks(profile);
|
|
12426
11228
|
if (requiredChecks.length === 0) return [{
|
|
12427
11229
|
message: "Profile declares no requiredChecks; no external evidence envelope (including a preview-deploy proof) is demanded.",
|
|
12428
11230
|
name: "admission:required-checks",
|
|
@@ -12479,9 +11281,8 @@ const admissionPreflightChecks = ({ base = "origin/main", cwd, env = process.env
|
|
|
12479
11281
|
reviewProofCheck({
|
|
12480
11282
|
candidate,
|
|
12481
11283
|
candidateError,
|
|
12482
|
-
profile,
|
|
12483
11284
|
proof: reviewProof,
|
|
12484
|
-
reviewModes: candidate ? resolveApplicableReviewModes(profile, candidate.files) :
|
|
11285
|
+
reviewModes: candidate ? resolveApplicableReviewModes(profile, candidate.files) : ["correctness"]
|
|
12485
11286
|
}),
|
|
12486
11287
|
...requiredCheckChecks({
|
|
12487
11288
|
candidate,
|
|
@@ -12751,7 +11552,6 @@ async function doctorProjectProfile(input = {}) {
|
|
|
12751
11552
|
}, input.hqRetroReadbackDependencies)]);
|
|
12752
11553
|
const checks = [
|
|
12753
11554
|
...buildProfileChecks(profile, cwd, {
|
|
12754
|
-
env,
|
|
12755
11555
|
userConfig: userConfig.loaded?.config,
|
|
12756
11556
|
userConfigCheck: userConfig.check
|
|
12757
11557
|
}),
|
|
@@ -12770,9 +11570,8 @@ async function doctorProjectProfile(input = {}) {
|
|
|
12770
11570
|
checks,
|
|
12771
11571
|
ok: checks.every((check) => check.status !== "error"),
|
|
12772
11572
|
profilePath: path,
|
|
12773
|
-
projectKey: profile.
|
|
11573
|
+
projectKey: profile.repository.name,
|
|
12774
11574
|
repository: {
|
|
12775
|
-
defaultBranch: profile.repository.defaultBranch,
|
|
12776
11575
|
name: profile.repository.name,
|
|
12777
11576
|
owner: profile.repository.owner
|
|
12778
11577
|
},
|
|
@@ -12811,12 +11610,12 @@ function resolveDoctorUserConfig(env, provided) {
|
|
|
12811
11610
|
function buildProfileChecks(profile, cwd, userConfigInput) {
|
|
12812
11611
|
return [
|
|
12813
11612
|
{
|
|
12814
|
-
message: `Loaded schema version ${profile.schemaVersion} profile for ${profile.
|
|
11613
|
+
message: `Loaded schema version ${profile.schemaVersion} profile for ${profile.repository.name}.`,
|
|
12815
11614
|
name: "profile",
|
|
12816
11615
|
status: "ok"
|
|
12817
11616
|
},
|
|
12818
11617
|
{
|
|
12819
|
-
message: `${profile.repository.owner}/${profile.repository.name}
|
|
11618
|
+
message: `${profile.repository.owner}/${profile.repository.name} repository identity loaded.`,
|
|
12820
11619
|
name: "repository",
|
|
12821
11620
|
status: "ok"
|
|
12822
11621
|
},
|
|
@@ -12825,31 +11624,16 @@ function buildProfileChecks(profile, cwd, userConfigInput) {
|
|
|
12825
11624
|
name: "verification",
|
|
12826
11625
|
status: "ok"
|
|
12827
11626
|
},
|
|
12828
|
-
requiredEnvironmentCheck(profile, userConfigInput.env),
|
|
12829
11627
|
{
|
|
12830
|
-
message:
|
|
11628
|
+
message: `Correctness review is structural with a 5-cycle cap; ${profile.review?.conditional?.length ?? 0} security path group(s) configured.`,
|
|
12831
11629
|
name: "review-policy",
|
|
12832
11630
|
status: "ok"
|
|
12833
11631
|
},
|
|
12834
|
-
reviewLadderCheck(
|
|
11632
|
+
reviewLadderCheck(),
|
|
12835
11633
|
userConfigInput.userConfigCheck,
|
|
12836
|
-
githubAppCheck(userConfigInput.userConfig),
|
|
12837
|
-
gitRemoteCheck(profile, cwd)
|
|
12838
|
-
];
|
|
12839
|
-
}
|
|
12840
|
-
function requiredEnvironmentCheck(profile, env) {
|
|
12841
|
-
const required = profile.env?.required ?? [];
|
|
12842
|
-
const unset = required.filter((name) => !env[name]);
|
|
12843
|
-
if (unset.length > 0) return {
|
|
12844
|
-
message: `Required environment variables are unset: ${unset.join(", ")}.`,
|
|
12845
|
-
name: "environment",
|
|
12846
|
-
status: "error"
|
|
12847
|
-
};
|
|
12848
|
-
return {
|
|
12849
|
-
message: required.length === 0 ? "No required environment variables declared." : `${required.length} required environment variable name(s) are set.`,
|
|
12850
|
-
name: "environment",
|
|
12851
|
-
status: "ok"
|
|
12852
|
-
};
|
|
11634
|
+
githubAppCheck(userConfigInput.userConfig),
|
|
11635
|
+
gitRemoteCheck(profile, cwd)
|
|
11636
|
+
];
|
|
12853
11637
|
}
|
|
12854
11638
|
function githubAppCheck(userConfig) {
|
|
12855
11639
|
const app = userConfig?.githubApp;
|
|
@@ -12864,85 +11648,440 @@ function githubAppCheck(userConfig) {
|
|
|
12864
11648
|
status: "warning"
|
|
12865
11649
|
};
|
|
12866
11650
|
return {
|
|
12867
|
-
message: `Patronage Factory GitHub App id ${app.appId} is configured${app.installationId ? ` for installation ${app.installationId}` : " with repository installation discovery"}.`,
|
|
12868
|
-
name: "github-app",
|
|
12869
|
-
status: "ok"
|
|
11651
|
+
message: `Patronage Factory GitHub App id ${app.appId} is configured${app.installationId ? ` for installation ${app.installationId}` : " with repository installation discovery"}.`,
|
|
11652
|
+
name: "github-app",
|
|
11653
|
+
status: "ok"
|
|
11654
|
+
};
|
|
11655
|
+
}
|
|
11656
|
+
function reviewLadderCheck() {
|
|
11657
|
+
return {
|
|
11658
|
+
message: `Review ladder: gate cap ${resolveReviewLadderPolicy().gate.cap}.`,
|
|
11659
|
+
name: "review-ladder",
|
|
11660
|
+
status: "ok"
|
|
11661
|
+
};
|
|
11662
|
+
}
|
|
11663
|
+
function gitRemoteCheck(profile, cwd) {
|
|
11664
|
+
const origin = readGitOrigin(cwd);
|
|
11665
|
+
if (!origin) return {
|
|
11666
|
+
message: "No git origin remote was available for this working directory.",
|
|
11667
|
+
name: "git-origin",
|
|
11668
|
+
status: "warning"
|
|
11669
|
+
};
|
|
11670
|
+
if (repositoryUrlMatches(origin, profile)) return {
|
|
11671
|
+
message: `Git origin matches ${profile.repository.owner}/${profile.repository.name}.`,
|
|
11672
|
+
name: "git-origin",
|
|
11673
|
+
status: "ok"
|
|
11674
|
+
};
|
|
11675
|
+
return {
|
|
11676
|
+
message: `Git origin ${origin} does not match ${profile.repository.owner}/${profile.repository.name}.`,
|
|
11677
|
+
name: "git-origin",
|
|
11678
|
+
status: "warning"
|
|
11679
|
+
};
|
|
11680
|
+
}
|
|
11681
|
+
function readGitOrigin(cwd) {
|
|
11682
|
+
try {
|
|
11683
|
+
return execFileSync("git", [
|
|
11684
|
+
"remote",
|
|
11685
|
+
"get-url",
|
|
11686
|
+
"origin"
|
|
11687
|
+
], {
|
|
11688
|
+
cwd,
|
|
11689
|
+
encoding: "utf-8",
|
|
11690
|
+
stdio: [
|
|
11691
|
+
"ignore",
|
|
11692
|
+
"pipe",
|
|
11693
|
+
"ignore"
|
|
11694
|
+
]
|
|
11695
|
+
}).trim();
|
|
11696
|
+
} catch {
|
|
11697
|
+
return null;
|
|
11698
|
+
}
|
|
11699
|
+
}
|
|
11700
|
+
function repositoryUrlMatches(origin, profile) {
|
|
11701
|
+
const normalized = origin.replace(/\.git$/u, "");
|
|
11702
|
+
const repoPath = `${profile.repository.owner}/${profile.repository.name}`;
|
|
11703
|
+
return normalized.endsWith(`github.com:${repoPath}`) || normalized.endsWith(`github.com/${repoPath}`) || normalized.endsWith(repoPath);
|
|
11704
|
+
}
|
|
11705
|
+
//#endregion
|
|
11706
|
+
//#region src/commands/doctor.ts
|
|
11707
|
+
function createDoctorCommand(output) {
|
|
11708
|
+
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", ".").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) => {
|
|
11709
|
+
const report = await doctorProjectProfile({
|
|
11710
|
+
base: options.base,
|
|
11711
|
+
cwd: resolveCwdOption(options.cwd),
|
|
11712
|
+
preflight: options.preflight,
|
|
11713
|
+
profilePath: options.profile
|
|
11714
|
+
});
|
|
11715
|
+
if (options.json) output.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
11716
|
+
else output.stdout.write(formatHumanReport(report));
|
|
11717
|
+
if (!report.ok) process.exitCode = 1;
|
|
11718
|
+
});
|
|
11719
|
+
}
|
|
11720
|
+
function formatHumanReport(report) {
|
|
11721
|
+
return [
|
|
11722
|
+
`Profile: ${report.projectKey} (schema ${report.schemaVersion})`,
|
|
11723
|
+
`Repository: ${report.repository.owner}/${report.repository.name}`,
|
|
11724
|
+
`Path: ${report.profilePath}`,
|
|
11725
|
+
"",
|
|
11726
|
+
...report.checks.map((check) => `[${check.status}] ${check.name}: ${check.message}`),
|
|
11727
|
+
""
|
|
11728
|
+
].join("\n");
|
|
11729
|
+
}
|
|
11730
|
+
//#endregion
|
|
11731
|
+
//#region src/epic-structure.ts
|
|
11732
|
+
/**
|
|
11733
|
+
* Producer-side `epic-structure` v1 emitter (epic #132, lane #135).
|
|
11734
|
+
*
|
|
11735
|
+
* `psf epic:publish-structure` reads a planner's ephemeral `dag.yml` (never
|
|
11736
|
+
* committed — see epic #132 / ADR 0018), validates the graph fail-closed, and
|
|
11737
|
+
* POSTs an `epic-structure` v1 event to HQ's `/api/ingest` boundary. This
|
|
11738
|
+
* module is the pure core: parse-to-IR validation, the wire mapping, and the
|
|
11739
|
+
* deterministic content-addressed eventId. All I/O (stdin/file read, fetch)
|
|
11740
|
+
* lives in the command wrapper.
|
|
11741
|
+
*
|
|
11742
|
+
* WIRE PARITY (do not fork the schema): the emitted payload is the exact shape
|
|
11743
|
+
* HQ's `EpicGraphSchema` accepts (`software-factory-hq/src/contracts/
|
|
11744
|
+
* epic-schemas.ts`) and is built by hand the same way `seed-dev.ts` builds it
|
|
11745
|
+
* — no re-declared zod twin. HQ's `epic-structure-parity.test.ts` imports
|
|
11746
|
+
* {@link buildEpicStructureEvent} and runs its output through the real
|
|
11747
|
+
* `EpicGraphSchema`, so any drift (field, enum, bound, strictness) fails CI.
|
|
11748
|
+
*
|
|
11749
|
+
* The `dag.yml` schema itself is documented with the epic skill (lane #137);
|
|
11750
|
+
* this module is the executable contract, not a second source of truth.
|
|
11751
|
+
*/
|
|
11752
|
+
const EPIC_STRUCTURE_SCHEMA_VERSION = 1;
|
|
11753
|
+
/**
|
|
11754
|
+
* DAG-node PLANNING statuses — the planning-plane vocabulary a planner authors
|
|
11755
|
+
* on a `dag.yml` node. Carried forward by ADR 0018 §4 and defined by the epic
|
|
11756
|
+
* skill (`reference/epic-artifacts.md`) and both `CONTEXT.md` glossaries.
|
|
11757
|
+
*
|
|
11758
|
+
* A DAG node is a unit of *plan*; an HQ lane is a unit of *runtime execution*.
|
|
11759
|
+
* These are two planes with two vocabularies and must not be conflated — the
|
|
11760
|
+
* producer validates and emits ONLY planning statuses:
|
|
11761
|
+
*
|
|
11762
|
+
* - `open` authored, not yet done
|
|
11763
|
+
* - `closed` done (a PR merged, or otherwise resolved)
|
|
11764
|
+
* - `satisfied-on-main` already true on main without a dedicated PR
|
|
11765
|
+
* - `parked` real node, scope still moving — relabeled off
|
|
11766
|
+
* `ready-for-agent`, never closed
|
|
11767
|
+
*
|
|
11768
|
+
* HQ's `DagNodeSchema` (`software-factory-hq/src/contracts/epic-schemas.ts`)
|
|
11769
|
+
* accepts this planning vocabulary on ingest AND overlays a runtime LANE status
|
|
11770
|
+
* onto a node once a lane is linked (`syncNodesWithLanes` /
|
|
11771
|
+
* `projectEpicMembership`) — the overlay is HQ-internal; the producer never
|
|
11772
|
+
* emits a lane status. HQ's `epic-structure-parity.test.ts` fails CI if the
|
|
11773
|
+
* producer emits any status HQ won't accept.
|
|
11774
|
+
*/
|
|
11775
|
+
const EPIC_STRUCTURE_NODE_STATUSES = [
|
|
11776
|
+
"open",
|
|
11777
|
+
"closed",
|
|
11778
|
+
"satisfied-on-main",
|
|
11779
|
+
"parked"
|
|
11780
|
+
];
|
|
11781
|
+
/**
|
|
11782
|
+
* Fail-closed validation error carrying every offending node/edge diagnostic.
|
|
11783
|
+
*/
|
|
11784
|
+
var EpicStructureValidationError = class extends Error {
|
|
11785
|
+
diagnostics;
|
|
11786
|
+
constructor(diagnostics) {
|
|
11787
|
+
super(`epic:publish-structure refused: dag graph is invalid\n${diagnostics.map((line) => ` - ${line}`).join("\n")}`);
|
|
11788
|
+
this.name = "EpicStructureValidationError";
|
|
11789
|
+
this.diagnostics = diagnostics;
|
|
11790
|
+
}
|
|
11791
|
+
};
|
|
11792
|
+
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11793
|
+
const nonEmptyString = (value) => typeof value === "string" && value.trim().length > 0;
|
|
11794
|
+
const isPositiveInteger = (value) => typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
11795
|
+
const slugify = (value) => value.trim().toLowerCase().replaceAll(/[^a-z0-9]+/gu, "-").replaceAll(/^-+|-+$/gu, "") || "epic";
|
|
11796
|
+
const laneIdFor = (repo, issue) => `lane-${repo}-issue-${issue}`;
|
|
11797
|
+
const validateNodes = (rawNodes, diagnostics) => {
|
|
11798
|
+
const nodes = [];
|
|
11799
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
11800
|
+
for (const [index, raw] of rawNodes.entries()) {
|
|
11801
|
+
if (!isPlainObject(raw)) {
|
|
11802
|
+
diagnostics.push(`node[${index}]: expected a mapping`);
|
|
11803
|
+
continue;
|
|
11804
|
+
}
|
|
11805
|
+
const label = nonEmptyString(raw.slug) ? `"${raw.slug}"` : `node[${index}]`;
|
|
11806
|
+
if (!nonEmptyString(raw.slug)) diagnostics.push(`node[${index}]: missing required \`slug\``);
|
|
11807
|
+
else if (slugs.has(raw.slug)) diagnostics.push(`node ${label}: duplicate slug`);
|
|
11808
|
+
else slugs.add(raw.slug);
|
|
11809
|
+
if (!nonEmptyString(raw.title)) diagnostics.push(`node ${label}: missing required \`title\``);
|
|
11810
|
+
const external = raw.external === true;
|
|
11811
|
+
if (!external && !isPositiveInteger(raw.issue)) diagnostics.push(`node ${label}: \`issue\` number is required — non-external nodes must carry a minted issue (this runs post-ratification); set \`external: true\` to exempt an adoption/upstream node`);
|
|
11812
|
+
let status = "open";
|
|
11813
|
+
if (raw.status === void 0) status = "open";
|
|
11814
|
+
else if (EPIC_STRUCTURE_NODE_STATUSES.includes(raw.status)) status = raw.status;
|
|
11815
|
+
else diagnostics.push(`node ${label}: \`status\` must be one of ${EPIC_STRUCTURE_NODE_STATUSES.join(", ")} (DAG-node planning statuses, not HQ lane statuses)`);
|
|
11816
|
+
if (raw.laneId !== void 0 && !nonEmptyString(raw.laneId)) diagnostics.push(`node ${label}: \`laneId\` must be a non-empty string`);
|
|
11817
|
+
nodes.push({
|
|
11818
|
+
external,
|
|
11819
|
+
...isPositiveInteger(raw.issue) ? { issue: raw.issue } : {},
|
|
11820
|
+
...nonEmptyString(raw.laneId) ? { laneId: raw.laneId } : {},
|
|
11821
|
+
slug: nonEmptyString(raw.slug) ? raw.slug : `node[${index}]`,
|
|
11822
|
+
status,
|
|
11823
|
+
title: nonEmptyString(raw.title) ? raw.title : ""
|
|
11824
|
+
});
|
|
11825
|
+
}
|
|
11826
|
+
return {
|
|
11827
|
+
nodes,
|
|
11828
|
+
slugs
|
|
11829
|
+
};
|
|
11830
|
+
};
|
|
11831
|
+
const validateEdges = (rawEdges, slugs, diagnostics) => {
|
|
11832
|
+
const edges = [];
|
|
11833
|
+
for (const [index, raw] of rawEdges.entries()) {
|
|
11834
|
+
if (!isPlainObject(raw)) {
|
|
11835
|
+
diagnostics.push(`edge[${index}]: expected a mapping`);
|
|
11836
|
+
continue;
|
|
11837
|
+
}
|
|
11838
|
+
if (raw.type !== "depends-on") continue;
|
|
11839
|
+
if (!nonEmptyString(raw.from) || !nonEmptyString(raw.to)) {
|
|
11840
|
+
diagnostics.push(`edge[${index}]: depends-on edge requires string \`from\` and \`to\``);
|
|
11841
|
+
continue;
|
|
11842
|
+
}
|
|
11843
|
+
if (!slugs.has(raw.from)) diagnostics.push(`edge ${raw.from} -> ${raw.to}: \`from\` references unknown slug "${raw.from}"`);
|
|
11844
|
+
if (!slugs.has(raw.to)) diagnostics.push(`edge ${raw.from} -> ${raw.to}: \`to\` references unknown slug "${raw.to}"`);
|
|
11845
|
+
if (raw.from === raw.to) diagnostics.push(`edge ${raw.from} -> ${raw.to}: self-dependency (cycle)`);
|
|
11846
|
+
edges.push({
|
|
11847
|
+
from: raw.from,
|
|
11848
|
+
to: raw.to
|
|
11849
|
+
});
|
|
11850
|
+
}
|
|
11851
|
+
return edges;
|
|
11852
|
+
};
|
|
11853
|
+
/**
|
|
11854
|
+
* Detects a dependency cycle over the depends-on edges and returns the offending
|
|
11855
|
+
* cycle path (`a -> b -> a`) if one exists, else undefined. Only edges whose
|
|
11856
|
+
* endpoints both resolve to known slugs are walked, so unknown-slug diagnostics
|
|
11857
|
+
* are reported independently and never masquerade as cycles.
|
|
11858
|
+
*/
|
|
11859
|
+
const findCycle = (slugs, edges) => {
|
|
11860
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
11861
|
+
for (const { from, to } of edges) if (slugs.has(from) && slugs.has(to)) {
|
|
11862
|
+
const list = adjacency.get(from) ?? [];
|
|
11863
|
+
list.push(to);
|
|
11864
|
+
adjacency.set(from, list);
|
|
11865
|
+
}
|
|
11866
|
+
const UNVISITED = 0;
|
|
11867
|
+
const IN_STACK = 1;
|
|
11868
|
+
const DONE = 2;
|
|
11869
|
+
const state = /* @__PURE__ */ new Map();
|
|
11870
|
+
const stack = [];
|
|
11871
|
+
const walk = (node) => {
|
|
11872
|
+
state.set(node, IN_STACK);
|
|
11873
|
+
stack.push(node);
|
|
11874
|
+
for (const next of adjacency.get(node) ?? []) {
|
|
11875
|
+
const marker = state.get(next) ?? UNVISITED;
|
|
11876
|
+
if (marker === IN_STACK) {
|
|
11877
|
+
const start = stack.indexOf(next);
|
|
11878
|
+
return [...stack.slice(start), next];
|
|
11879
|
+
}
|
|
11880
|
+
if (marker === UNVISITED) {
|
|
11881
|
+
const found = walk(next);
|
|
11882
|
+
if (found) return found;
|
|
11883
|
+
}
|
|
11884
|
+
}
|
|
11885
|
+
stack.pop();
|
|
11886
|
+
state.set(node, DONE);
|
|
11887
|
+
};
|
|
11888
|
+
for (const slug of slugs) if ((state.get(slug) ?? UNVISITED) === UNVISITED) {
|
|
11889
|
+
const found = walk(slug);
|
|
11890
|
+
if (found) return found;
|
|
11891
|
+
}
|
|
11892
|
+
};
|
|
11893
|
+
const resolveEpicId = (epic) => {
|
|
11894
|
+
if (typeof epic === "number") return String(epic);
|
|
11895
|
+
if (nonEmptyString(epic)) return epic.trim();
|
|
11896
|
+
};
|
|
11897
|
+
/**
|
|
11898
|
+
* Validates a dag document fail-closed. Throws {@link
|
|
11899
|
+
* EpicStructureValidationError} with a diagnostic per offending node/edge.
|
|
11900
|
+
*/
|
|
11901
|
+
const validateDagDocument = (input) => {
|
|
11902
|
+
const doc = input.document;
|
|
11903
|
+
const diagnostics = [];
|
|
11904
|
+
if (!isPlainObject(doc)) throw new EpicStructureValidationError(["dag document must be a YAML/JSON mapping with `epic` and `nodes`"]);
|
|
11905
|
+
const epicId = resolveEpicId(doc.epic);
|
|
11906
|
+
if (epicId === void 0) diagnostics.push("dag document requires a top-level `epic` id");
|
|
11907
|
+
const repo = nonEmptyString(doc.repo) ? doc.repo.trim() : void 0;
|
|
11908
|
+
if (repo === void 0) diagnostics.push("repo is required in the dag document — set top-level `repo:` (needed for the epic record and lane-id derivation)");
|
|
11909
|
+
const rawNodes = Array.isArray(doc.nodes) ? doc.nodes : void 0;
|
|
11910
|
+
if (rawNodes === void 0 || rawNodes.length === 0) diagnostics.push("dag document requires a non-empty `nodes` array");
|
|
11911
|
+
const { nodes, slugs } = validateNodes(rawNodes ?? [], diagnostics);
|
|
11912
|
+
const edges = validateEdges(Array.isArray(doc.edges) ? doc.edges : [], slugs, diagnostics);
|
|
11913
|
+
const cycle = findCycle(slugs, edges);
|
|
11914
|
+
if (cycle) diagnostics.push(`dependency cycle detected: ${cycle.join(" -> ")}`);
|
|
11915
|
+
if (diagnostics.length > 0 || epicId === void 0 || repo === void 0) throw new EpicStructureValidationError(diagnostics);
|
|
11916
|
+
return {
|
|
11917
|
+
boundary: slugify(nonEmptyString(doc.boundary) ? doc.boundary : `epic-${epicId}`),
|
|
11918
|
+
edges,
|
|
11919
|
+
epicId,
|
|
11920
|
+
name: nonEmptyString(doc.name) ? doc.name.trim() : `Epic ${epicId}`,
|
|
11921
|
+
nodes,
|
|
11922
|
+
repo
|
|
12870
11923
|
};
|
|
12871
|
-
}
|
|
12872
|
-
|
|
11924
|
+
};
|
|
11925
|
+
/**
|
|
11926
|
+
* Builds the wire payload with deterministic ordering (nodes by slug, edges by
|
|
11927
|
+
* from then to) so re-emission of the same graph is byte-stable regardless of
|
|
11928
|
+
* the planner's source ordering — the basis for the content-addressed eventId.
|
|
11929
|
+
*/
|
|
11930
|
+
const buildEpicStructurePayload = (dag) => {
|
|
11931
|
+
const nodes = dag.nodes.map((node) => {
|
|
11932
|
+
const laneId = node.laneId ?? (node.issue === void 0 ? void 0 : laneIdFor(dag.repo, node.issue));
|
|
11933
|
+
return {
|
|
11934
|
+
epicId: dag.epicId,
|
|
11935
|
+
...laneId === void 0 ? {} : { laneId },
|
|
11936
|
+
slug: node.slug,
|
|
11937
|
+
status: node.status,
|
|
11938
|
+
title: node.title
|
|
11939
|
+
};
|
|
11940
|
+
}).toSorted((a, b) => a.slug.localeCompare(b.slug));
|
|
12873
11941
|
return {
|
|
12874
|
-
|
|
12875
|
-
|
|
12876
|
-
|
|
12877
|
-
|
|
12878
|
-
|
|
12879
|
-
|
|
12880
|
-
|
|
12881
|
-
|
|
12882
|
-
|
|
12883
|
-
|
|
12884
|
-
|
|
12885
|
-
|
|
12886
|
-
if (repositoryUrlMatches(origin, profile)) return {
|
|
12887
|
-
message: `Git origin matches ${profile.repository.owner}/${profile.repository.name}.`,
|
|
12888
|
-
name: "git-origin",
|
|
12889
|
-
status: "ok"
|
|
11942
|
+
edges: dag.edges.map((edge) => ({
|
|
11943
|
+
epicId: dag.epicId,
|
|
11944
|
+
from: edge.from,
|
|
11945
|
+
to: edge.to,
|
|
11946
|
+
type: "depends-on"
|
|
11947
|
+
})).toSorted((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to)),
|
|
11948
|
+
epics: [{
|
|
11949
|
+
id: dag.epicId,
|
|
11950
|
+
name: dag.name,
|
|
11951
|
+
repo: dag.repo
|
|
11952
|
+
}],
|
|
11953
|
+
nodes
|
|
12890
11954
|
};
|
|
11955
|
+
};
|
|
11956
|
+
/** Stable, key-sorted JSON for content hashing (values already ordered). */
|
|
11957
|
+
const stableStringify = (value) => {
|
|
11958
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
11959
|
+
if (isPlainObject(value)) return `{${Object.keys(value).toSorted().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
|
|
11960
|
+
return JSON.stringify(value ?? null);
|
|
11961
|
+
};
|
|
11962
|
+
/**
|
|
11963
|
+
* Deterministic eventId = `epic-structure-<boundary>-<sha256(payload)[0:16]>`.
|
|
11964
|
+
* Re-emitting identical graph content yields the same eventId (HQ dedups →
|
|
11965
|
+
* 200/duplicate); an amendment changes the content hash → a new eventId that
|
|
11966
|
+
* supersedes the prior structure wholesale (HQ keeps the latest). This mirrors
|
|
11967
|
+
* seed-dev's fixed-eventId idempotency, content-addressed instead of literal.
|
|
11968
|
+
*/
|
|
11969
|
+
const epicStructureEventId = (boundary, payload) => `epic-structure-${slugify(boundary)}-${createHash("sha256").update(stableStringify(payload)).digest("hex").slice(0, 16)}`;
|
|
11970
|
+
/**
|
|
11971
|
+
* Validates and builds the full `epic-structure` v1 ingest event, ready to POST
|
|
11972
|
+
* to `/api/ingest`. Throws {@link EpicStructureValidationError} fail-closed.
|
|
11973
|
+
*/
|
|
11974
|
+
const buildEpicStructureEvent = (input) => {
|
|
11975
|
+
const dag = validateDagDocument(input);
|
|
11976
|
+
const payload = buildEpicStructurePayload(dag);
|
|
12891
11977
|
return {
|
|
12892
|
-
|
|
12893
|
-
|
|
12894
|
-
|
|
11978
|
+
eventId: epicStructureEventId(dag.boundary, payload),
|
|
11979
|
+
kind: "epic-structure",
|
|
11980
|
+
observedAt: input.observedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
11981
|
+
payload,
|
|
11982
|
+
schemaVersion: 1
|
|
12895
11983
|
};
|
|
12896
|
-
}
|
|
12897
|
-
|
|
11984
|
+
};
|
|
11985
|
+
/** The prod HQ worker is reachable at `hq.patronage.com` and its frozen
|
|
11986
|
+
* `-v1.*.workers.dev` alias. Emitting there without an Access service token is
|
|
11987
|
+
* fail-closed. */
|
|
11988
|
+
const isProductionHqUrl = (url) => url.includes("hq.patronage.com") || /-v1\.[^/]*\.workers\.dev/iu.test(url);
|
|
11989
|
+
/**
|
|
11990
|
+
* POSTs the event to HQ ingest. Fail-closed: a production URL without a
|
|
11991
|
+
* Cloudflare Access service token is refused before any request; a 401/403 is
|
|
11992
|
+
* surfaced with actionable guidance; any non-2xx throws.
|
|
11993
|
+
*/
|
|
11994
|
+
const publishEpicStructure = async (args) => {
|
|
11995
|
+
if (!args.accessServiceToken && isProductionHqUrl(args.url)) throw new Error(`epic:publish-structure refusing to publish to production ${args.url} without a Cloudflare Access service token: set CF-Access-Client-Id and CF-Access-Client-Secret`);
|
|
11996
|
+
const doFetch = args.fetchImpl ?? globalThis.fetch;
|
|
11997
|
+
const requestInit = args.accessServiceToken ? buildCloudflareAccessRequestInit(args.accessServiceToken, {
|
|
11998
|
+
body: JSON.stringify(args.event),
|
|
11999
|
+
headers: { "content-type": "application/json" },
|
|
12000
|
+
method: "POST",
|
|
12001
|
+
...args.signal ? { signal: args.signal } : {}
|
|
12002
|
+
}) : {
|
|
12003
|
+
body: JSON.stringify(args.event),
|
|
12004
|
+
headers: { "content-type": "application/json" },
|
|
12005
|
+
method: "POST",
|
|
12006
|
+
...args.signal ? { signal: args.signal } : {}
|
|
12007
|
+
};
|
|
12008
|
+
let response;
|
|
12898
12009
|
try {
|
|
12899
|
-
|
|
12900
|
-
|
|
12901
|
-
"get-url",
|
|
12902
|
-
"origin"
|
|
12903
|
-
], {
|
|
12904
|
-
cwd,
|
|
12905
|
-
encoding: "utf-8",
|
|
12906
|
-
stdio: [
|
|
12907
|
-
"ignore",
|
|
12908
|
-
"pipe",
|
|
12909
|
-
"ignore"
|
|
12910
|
-
]
|
|
12911
|
-
}).trim();
|
|
12010
|
+
const normalizedUrl = args.url.replace(/\/+$/u, "");
|
|
12011
|
+
response = await doFetch(normalizedUrl.endsWith("/api/ingest") ? normalizedUrl : `${normalizedUrl}/api/ingest`, requestInit);
|
|
12912
12012
|
} catch {
|
|
12913
|
-
|
|
12013
|
+
throw new Error(`epic:publish-structure could not reach HQ ingest at ${args.url}: request failed. Check the endpoint and network; redirects are refused to protect Cloudflare Access credentials.`);
|
|
12914
12014
|
}
|
|
12915
|
-
|
|
12916
|
-
|
|
12917
|
-
|
|
12918
|
-
|
|
12919
|
-
|
|
12920
|
-
|
|
12015
|
+
const text = await response.text();
|
|
12016
|
+
if (response.status === 401 || response.status === 403) throw new Error(`epic:publish-structure rejected by Cloudflare Access or HQ ingest (${response.status}): set valid CF-Access-Client-Id and CF-Access-Client-Secret service-token inputs. Response: ${text}`);
|
|
12017
|
+
if (response.status < 200 || response.status >= 300) throw new Error(`epic:publish-structure failed: HQ ingest returned ${response.status} ${text}`);
|
|
12018
|
+
let duplicate = false;
|
|
12019
|
+
try {
|
|
12020
|
+
duplicate = JSON.parse(text).duplicate === true;
|
|
12021
|
+
} catch {
|
|
12022
|
+
duplicate = response.status === 200;
|
|
12023
|
+
}
|
|
12024
|
+
return {
|
|
12025
|
+
duplicate,
|
|
12026
|
+
eventId: args.event.eventId,
|
|
12027
|
+
status: response.status
|
|
12028
|
+
};
|
|
12029
|
+
};
|
|
12921
12030
|
//#endregion
|
|
12922
|
-
//#region src/commands/
|
|
12923
|
-
|
|
12924
|
-
|
|
12925
|
-
|
|
12926
|
-
|
|
12927
|
-
|
|
12928
|
-
|
|
12929
|
-
|
|
12031
|
+
//#region src/commands/epic-publish-structure.ts
|
|
12032
|
+
const STDIN = "-";
|
|
12033
|
+
/** Reads a dag.yml source path, or stdin when `source` is `-`. */
|
|
12034
|
+
const readSource = (source) => {
|
|
12035
|
+
const fd = source === STDIN ? 0 : source;
|
|
12036
|
+
try {
|
|
12037
|
+
return readFileSync(fd, "utf-8");
|
|
12038
|
+
} catch (error) {
|
|
12039
|
+
const cause = error instanceof Error ? error.message : String(error);
|
|
12040
|
+
throw new Error(`epic:publish-structure could not read ${source === STDIN ? "stdin" : source} (${cause})`, { cause: error });
|
|
12041
|
+
}
|
|
12042
|
+
};
|
|
12043
|
+
/** Parses a dag.yml/JSON document read from `readSource`. */
|
|
12044
|
+
const parseDocument = (raw, source) => {
|
|
12045
|
+
if (raw.trim() === "") throw new Error(`epic:publish-structure received empty input from ${source === STDIN ? "stdin" : source}`);
|
|
12046
|
+
try {
|
|
12047
|
+
return parse(raw);
|
|
12048
|
+
} catch (error) {
|
|
12049
|
+
const cause = error instanceof Error ? error.message : String(error);
|
|
12050
|
+
throw new Error(`epic:publish-structure could not parse YAML: ${cause}`, { cause: error });
|
|
12051
|
+
}
|
|
12052
|
+
};
|
|
12053
|
+
/**
|
|
12054
|
+
* Reads and validates a dag.yml/JSON source into a full `epic-structure` v1
|
|
12055
|
+
* event, ready to POST or dry-run print. Required event identity lives in the
|
|
12056
|
+
* document; the command does not patch authored structure with override flags.
|
|
12057
|
+
*/
|
|
12058
|
+
const buildEvent = (source) => {
|
|
12059
|
+
return buildEpicStructureEvent({ document: parseDocument(readSource(source), source) });
|
|
12060
|
+
};
|
|
12061
|
+
function createEpicPublishStructureCommand(output, deps = {}) {
|
|
12062
|
+
const env = deps.env ?? process.env;
|
|
12063
|
+
return new Command("epic:publish-structure").description("Emit-only: read a planner's ephemeral dag.yml (or - for stdin), validate the graph fail-closed (YAML parses, required fields, edges reference known slugs, acyclic, issue numbers on every non-external node), and POST an epic-structure v1 event to HQ ingest. Idempotent by a content-addressed eventId (re-emission of an amendment supersedes the prior structure). Production auth uses the CF-Access-Client-Id and CF-Access-Client-Secret environment inputs. URL: --url or HQ_INGEST_URL. The dag.yml schema is documented with the epic skill (#137).").argument("[source]", "path to dag.yml, or - for stdin", STDIN).option("--url <url>", "HQ ingest base URL (default: HQ_INGEST_URL env). The command POSTs to <url>/api/ingest").option("--dry-run", "validate and build the event, print it, and exit without POSTing").option("--json", "print the built event / publish result as JSON").action(async (source, options) => {
|
|
12064
|
+
const event = buildEvent(source);
|
|
12065
|
+
if (options.dryRun) {
|
|
12066
|
+
output.stdout.write(options.json ? `${JSON.stringify(event, null, 2)}\n` : `epic:publish-structure OK (dry-run) — eventId ${event.eventId}, ${event.payload.nodes.length} node(s), ${event.payload.edges.length} edge(s)\n`);
|
|
12067
|
+
return;
|
|
12068
|
+
}
|
|
12069
|
+
const url = options.url ?? env.HQ_INGEST_URL;
|
|
12070
|
+
if (!url) throw new Error("epic:publish-structure requires an HQ ingest URL: pass --url or set HQ_INGEST_URL");
|
|
12071
|
+
const clientId = env[CF_ACCESS_CLIENT_ID_ENV];
|
|
12072
|
+
const clientSecret = env[CF_ACCESS_CLIENT_SECRET_ENV];
|
|
12073
|
+
const result = await publishEpicStructure({
|
|
12074
|
+
...clientId && clientSecret ? { accessServiceToken: {
|
|
12075
|
+
clientId,
|
|
12076
|
+
clientSecret
|
|
12077
|
+
} } : {},
|
|
12078
|
+
event,
|
|
12079
|
+
fetchImpl: deps.fetchImpl,
|
|
12080
|
+
url
|
|
12930
12081
|
});
|
|
12931
|
-
|
|
12932
|
-
else output.stdout.write(formatHumanReport(report));
|
|
12933
|
-
if (!report.ok) process.exitCode = 1;
|
|
12082
|
+
output.stdout.write(options.json ? `${JSON.stringify(result, null, 2)}\n` : `epic:publish-structure ${result.duplicate ? "no-op (duplicate)" : "accepted"} — eventId ${result.eventId} [HTTP ${result.status}]\n`);
|
|
12934
12083
|
});
|
|
12935
12084
|
}
|
|
12936
|
-
function formatHumanReport(report) {
|
|
12937
|
-
return [
|
|
12938
|
-
`Profile: ${report.projectKey} (schema ${report.schemaVersion})`,
|
|
12939
|
-
`Repository: ${report.repository.owner}/${report.repository.name} default ${report.repository.defaultBranch}`,
|
|
12940
|
-
`Path: ${report.profilePath}`,
|
|
12941
|
-
"",
|
|
12942
|
-
...report.checks.map((check) => `[${check.status}] ${check.name}: ${check.message}`),
|
|
12943
|
-
""
|
|
12944
|
-
].join("\n");
|
|
12945
|
-
}
|
|
12946
12085
|
//#endregion
|
|
12947
12086
|
//#region src/commands/evidence-emit.ts
|
|
12948
12087
|
const readStdin = () => {
|
|
@@ -12957,6 +12096,15 @@ const readStdin = () => {
|
|
|
12957
12096
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("evidence:emit stdin must be a JSON object of field values");
|
|
12958
12097
|
return parsed;
|
|
12959
12098
|
};
|
|
12099
|
+
const RETIRED_STDIN_FIELDS = [
|
|
12100
|
+
"checkType",
|
|
12101
|
+
"rung",
|
|
12102
|
+
"model"
|
|
12103
|
+
];
|
|
12104
|
+
function rejectRetiredStdinFields(input) {
|
|
12105
|
+
const found = RETIRED_STDIN_FIELDS.filter((field) => field in input);
|
|
12106
|
+
if (found.length > 0) throw new Error(`evidence:emit stdin no longer accepts ${found.join(", ")}; external evidence is verify-only. Remove ${found.join(", ")} from the JSON payload and emit the check with --check, --outcome, and --producer.`);
|
|
12107
|
+
}
|
|
12960
12108
|
const stringField = (value, name) => {
|
|
12961
12109
|
if (value === void 0) return;
|
|
12962
12110
|
if (typeof value !== "string" || value.trim() === "") throw new Error(`evidence:emit ${name} must be a non-empty string`);
|
|
@@ -12966,10 +12114,6 @@ const requireField = (value, name) => {
|
|
|
12966
12114
|
if (value === void 0) throw new Error(`evidence:emit requires ${name} (flag or stdin field)`);
|
|
12967
12115
|
return value;
|
|
12968
12116
|
};
|
|
12969
|
-
const asCheckType = (value) => {
|
|
12970
|
-
if (!EVIDENCE_CHECK_TYPES$1.includes(value)) throw new Error(`evidence:emit --check-type must be one of: ${EVIDENCE_CHECK_TYPES$1.join(", ")}`);
|
|
12971
|
-
return value;
|
|
12972
|
-
};
|
|
12973
12117
|
const asOutcome = (value) => {
|
|
12974
12118
|
if (value !== "pass" && value !== "fail") throw new Error("evidence:emit --outcome must be \"pass\" or \"fail\"");
|
|
12975
12119
|
return value;
|
|
@@ -12978,48 +12122,25 @@ function optionalString(key, flagValue, stdinValue) {
|
|
|
12978
12122
|
const resolved = stringField(flagValue, `--${key}`) ?? stringField(stdinValue, key);
|
|
12979
12123
|
return resolved ? { [key]: resolved } : {};
|
|
12980
12124
|
}
|
|
12981
|
-
|
|
12982
|
-
|
|
12983
|
-
return loadProjectProfile({
|
|
12984
|
-
cwd,
|
|
12985
|
-
profilePath
|
|
12986
|
-
}).profile.requiredChecks?.find((entry) => entry.name === check);
|
|
12987
|
-
};
|
|
12988
|
-
const resolveCheckType = ({ explicitCheckType, requiredCheck }) => {
|
|
12989
|
-
if (explicitCheckType !== void 0 && requiredCheck !== void 0 && explicitCheckType !== requiredCheck.checkType) throw new Error(`evidence:emit --check-type "${explicitCheckType}" conflicts with the profile's requiredChecks declaration for "${requiredCheck.name}" (checkType "${requiredCheck.checkType}")`);
|
|
12990
|
-
return explicitCheckType ?? requiredCheck?.checkType;
|
|
12991
|
-
};
|
|
12992
|
-
const asRung = (value) => {
|
|
12993
|
-
if (!EVIDENCE_REVIEW_RUNGS$1.includes(value)) throw new Error(`evidence:emit --rung must be one of: ${EVIDENCE_REVIEW_RUNGS$1.join(", ")}`);
|
|
12994
|
-
return value;
|
|
12995
|
-
};
|
|
12996
|
-
function createEvidenceEmitCommand(output, action = runEvidenceEmit) {
|
|
12997
|
-
return new Command("evidence:emit").description("Build, validate, and write an external-evidence envelope (ADR 0014)").option("--cwd <path>", "repository working directory", ".").option("--base <ref>", "base branch or ref for the three-way binding", "origin/main").option("--check <name>", "declared requiredChecks name this evidence satisfies").option("--check-type <type>", `one of: ${EVIDENCE_CHECK_TYPES$1.join(", ")}`).option("--outcome <pass|fail>", "the check outcome").option("--producer <id>", "the producing identity (recorded, not gated)").option("--rung <rung>", `review-type only: ${EVIDENCE_REVIEW_RUNGS$1.join(", ")}`).option("--model <model>", "review-type only: the concrete model").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; when the profile declares a requiredChecks entry matching --check, its checkType fills --check-type if omitted)").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) => {
|
|
12125
|
+
function createEvidenceEmitCommand(output, action = runEvidenceEmit, readInput = readStdin) {
|
|
12126
|
+
return new Command("evidence:emit").description("Build, validate, and write an external-evidence envelope (ADR 0014)").option("--cwd <path>", "repository working directory", ".").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) => {
|
|
12998
12127
|
const cwd = resolveCwdOption(options.cwd);
|
|
12999
|
-
const fromStdin = options.stdin ?
|
|
12128
|
+
const fromStdin = options.stdin ? readInput() : {};
|
|
12129
|
+
rejectRetiredStdinFields(fromStdin);
|
|
13000
12130
|
const check = requireField(stringField(options.check, "--check") ?? stringField(fromStdin.check, "check"), "--check");
|
|
13001
|
-
|
|
13002
|
-
check,
|
|
12131
|
+
if (options.profile !== void 0) loadProjectProfile({
|
|
13003
12132
|
cwd,
|
|
13004
12133
|
profilePath: options.profile
|
|
13005
12134
|
});
|
|
13006
|
-
const checkType = asCheckType(requireField(resolveCheckType({
|
|
13007
|
-
explicitCheckType: stringField(options.checkType, "--check-type") ?? stringField(fromStdin.checkType, "checkType"),
|
|
13008
|
-
requiredCheck
|
|
13009
|
-
}), "--check-type"));
|
|
13010
12135
|
const outcome = asOutcome(requireField(stringField(options.outcome, "--outcome") ?? stringField(fromStdin.outcome, "outcome"), "--outcome"));
|
|
13011
12136
|
const producer = requireField(stringField(options.producer, "--producer") ?? stringField(fromStdin.producer, "producer"), "--producer");
|
|
13012
|
-
const rungValue = stringField(options.rung, "--rung") ?? stringField(fromStdin.rung, "rung");
|
|
13013
12137
|
const result = action({
|
|
13014
12138
|
base: options.base,
|
|
13015
12139
|
check,
|
|
13016
|
-
checkType,
|
|
13017
12140
|
cwd,
|
|
13018
12141
|
outcome,
|
|
13019
12142
|
producer,
|
|
13020
12143
|
...options.profile ? { profilePath: options.profile } : {},
|
|
13021
|
-
...rungValue ? { rung: asRung(rungValue) } : {},
|
|
13022
|
-
...optionalString("model", options.model, fromStdin.model),
|
|
13023
12144
|
...optionalString("policyVersion", options.policyVersion, fromStdin.policyVersion),
|
|
13024
12145
|
...optionalString("sessionId", options.sessionId, fromStdin.sessionId),
|
|
13025
12146
|
...optionalString("requestId", options.requestId, fromStdin.requestId),
|
|
@@ -13032,89 +12153,6 @@ function createEvidenceEmitCommand(output, action = runEvidenceEmit) {
|
|
|
13032
12153
|
});
|
|
13033
12154
|
}
|
|
13034
12155
|
//#endregion
|
|
13035
|
-
//#region src/lane-threads.ts
|
|
13036
|
-
const LANE_THREADS_PATH = ".factory-memory/lane-threads";
|
|
13037
|
-
const laneThreadEntrySchema = z.object({
|
|
13038
|
-
agent: z.string().min(1),
|
|
13039
|
-
branch: z.string().min(1).optional(),
|
|
13040
|
-
dispatchedAt: z.iso.datetime(),
|
|
13041
|
-
issue: z.number().int().positive(),
|
|
13042
|
-
model: z.string().min(1),
|
|
13043
|
-
threadId: z.string().min(1)
|
|
13044
|
-
}).strict();
|
|
13045
|
-
const laneThreadFilename = /^(?<issue>\d+)\.json$/u;
|
|
13046
|
-
function issueFromFilename(file) {
|
|
13047
|
-
return Number(laneThreadFilename.exec(file)?.groups?.issue ?? "");
|
|
13048
|
-
}
|
|
13049
|
-
function readLaneThreads(cwd) {
|
|
13050
|
-
const directory = path.join(cwd, LANE_THREADS_PATH);
|
|
13051
|
-
let files;
|
|
13052
|
-
try {
|
|
13053
|
-
files = readdirSync(directory).filter((file) => laneThreadFilename.test(file)).toSorted((left, right) => issueFromFilename(left) - issueFromFilename(right));
|
|
13054
|
-
} catch (error) {
|
|
13055
|
-
if (error.code === "ENOENT") return [];
|
|
13056
|
-
throw error;
|
|
13057
|
-
}
|
|
13058
|
-
return files.map((file) => {
|
|
13059
|
-
const entry = laneThreadEntrySchema.parse(JSON.parse(readFileSync(path.join(directory, file), "utf-8")));
|
|
13060
|
-
const issue = issueFromFilename(file);
|
|
13061
|
-
if (entry.issue !== issue) throw new Error(`Lane thread file ${file} contains issue ${entry.issue}; expected ${issue}.`);
|
|
13062
|
-
return entry;
|
|
13063
|
-
});
|
|
13064
|
-
}
|
|
13065
|
-
function writeLaneThread(cwd, entry) {
|
|
13066
|
-
const parsed = laneThreadEntrySchema.parse(entry);
|
|
13067
|
-
const directory = path.join(cwd, LANE_THREADS_PATH);
|
|
13068
|
-
const file = path.join(directory, `${parsed.issue}.json`);
|
|
13069
|
-
const temporary = path.join(directory, `${parsed.issue}.${process.pid}.${randomUUID()}.tmp`);
|
|
13070
|
-
mkdirSync(directory, { recursive: true });
|
|
13071
|
-
try {
|
|
13072
|
-
writeFileSync(temporary, `${JSON.stringify(parsed, null, 2)}\n`, "utf-8");
|
|
13073
|
-
renameSync(temporary, file);
|
|
13074
|
-
} finally {
|
|
13075
|
-
rmSync(temporary, { force: true });
|
|
13076
|
-
}
|
|
13077
|
-
}
|
|
13078
|
-
//#endregion
|
|
13079
|
-
//#region src/commands/guard.ts
|
|
13080
|
-
function resolveGuardCwd(command) {
|
|
13081
|
-
return resolveCwdOption(command.optsWithGlobals().cwd ?? ".");
|
|
13082
|
-
}
|
|
13083
|
-
function createGuardCommand(output) {
|
|
13084
|
-
const guard = new Command("guard").description("Factory worker safety guards").option("--cwd <path>", "working directory").enablePositionalOptions();
|
|
13085
|
-
guard.command("worker-checkout").description("Refuse branch creation or tracked-file mutation in the control/primary checkout").option("--cwd <path>", "working directory to check").action((_options, command) => {
|
|
13086
|
-
assertWorkerCheckoutAllowed({
|
|
13087
|
-
cwd: resolveGuardCwd(command),
|
|
13088
|
-
operation: "create a branch or mutate tracked files"
|
|
13089
|
-
});
|
|
13090
|
-
output.stdout.write("Worker checkout guard: OK\n");
|
|
13091
|
-
});
|
|
13092
|
-
guard.command("no-stash").description("Refuse `git stash` in a linked/shared worktree (stash reflog is shared across worktrees)").option("--cwd <path>", "working directory to check").action((_options, command) => {
|
|
13093
|
-
assertNoSharedStash({
|
|
13094
|
-
args: ["stash"],
|
|
13095
|
-
command: "git",
|
|
13096
|
-
cwd: resolveGuardCwd(command)
|
|
13097
|
-
});
|
|
13098
|
-
output.stdout.write("Stash guard: OK\n");
|
|
13099
|
-
});
|
|
13100
|
-
const laneThread = guard.command("lane-thread").description("Record or read lane-to-agent-thread identities");
|
|
13101
|
-
laneThread.command("append").requiredOption("--agent <kind>", "agent kind").option("--branch <branch>", "lane branch").option("--cwd <path>", "working directory").option("--dispatched-at <timestamp>", "ISO dispatch timestamp").requiredOption("--issue <number>", "lane issue number", Number).requiredOption("--model <model>", "agent model").requiredOption("--thread-id <id>", "agent thread or session id").action((options, command) => {
|
|
13102
|
-
writeLaneThread(resolveGuardCwd(command), {
|
|
13103
|
-
agent: options.agent,
|
|
13104
|
-
branch: options.branch,
|
|
13105
|
-
dispatchedAt: options.dispatchedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
13106
|
-
issue: options.issue,
|
|
13107
|
-
model: options.model,
|
|
13108
|
-
threadId: options.threadId
|
|
13109
|
-
});
|
|
13110
|
-
output.stdout.write("Lane thread recorded.\n");
|
|
13111
|
-
});
|
|
13112
|
-
laneThread.command("read").option("--cwd <path>", "working directory").action((_options, command) => {
|
|
13113
|
-
output.stdout.write(`${JSON.stringify(readLaneThreads(resolveGuardCwd(command)), null, 2)}\n`);
|
|
13114
|
-
});
|
|
13115
|
-
return guard;
|
|
13116
|
-
}
|
|
13117
|
-
//#endregion
|
|
13118
12156
|
//#region src/hq-flush.ts
|
|
13119
12157
|
/**
|
|
13120
12158
|
* Spools holding this repository's evidence under an earlier key of its own
|
|
@@ -14014,7 +13052,7 @@ const proofInputs = (input, epoch) => {
|
|
|
14014
13052
|
};
|
|
14015
13053
|
const gateCapMismatchBlockers = (input) => {
|
|
14016
13054
|
if (!input.reviewProof || input.ladderPolicy === void 0 || input.reviewProof.maxReviewCycles === input.ladderPolicy.gate.cap) return [];
|
|
14017
|
-
return [{ reason: `Review proof window (${input.reviewProof.maxReviewCycles ?? "unset"}) does not equal review
|
|
13055
|
+
return [{ reason: `Review proof window (${input.reviewProof.maxReviewCycles ?? "unset"}) does not equal the factory review cap (${input.ladderPolicy.gate.cap}); regenerate pr:review.` }];
|
|
14018
13056
|
};
|
|
14019
13057
|
const reviewGateFor = ({ epoch, input, reviewModes }) => {
|
|
14020
13058
|
const correctnessRequired = reviewModes.includes("correctness");
|
|
@@ -15307,7 +14345,7 @@ const assertReviewCycleInWindow = (cycle, maxCycles) => {
|
|
|
15307
14345
|
* alone would not demand it. Real review evidence is never discarded (#339).
|
|
15308
14346
|
*/
|
|
15309
14347
|
function enabledKinds(profile, mode, files = [], suppliedFindingsKinds = []) {
|
|
15310
|
-
const profileModes = new Set(profile.review
|
|
14348
|
+
const profileModes = new Set(["correctness", ...(profile.review?.conditional?.length ?? 0) > 0 ? ["security"] : []]);
|
|
15311
14349
|
const configuredKinds = (mode === "all" ? [...profileModes] : [mode]).filter((kind) => profileModes.has(kind));
|
|
15312
14350
|
if (configuredKinds.length === 0) throw new Error(`No configured review mode matches ${mode}.`);
|
|
15313
14351
|
const applicableKinds = new Set(resolveApplicableReviewModes(profile, files));
|
|
@@ -15414,9 +14452,8 @@ async function runPrReview(args, dependencies = {}) {
|
|
|
15414
14452
|
cwd,
|
|
15415
14453
|
profilePath: args.profilePath
|
|
15416
14454
|
});
|
|
15417
|
-
const ladderPolicy = resolveReviewLadderPolicy(
|
|
15418
|
-
const maxCycles =
|
|
15419
|
-
if (maxCycles !== ladderPolicy.gate.cap) throw new Error(`--max-cycles must equal review.defaultMaxCycles (${ladderPolicy.gate.cap}).`);
|
|
14455
|
+
const ladderPolicy = resolveReviewLadderPolicy();
|
|
14456
|
+
const maxCycles = ladderPolicy.gate.cap;
|
|
15420
14457
|
assertReviewCycleInWindow(args.cycle, maxCycles);
|
|
15421
14458
|
const { headSha, patchId } = createCandidateIdentityResolver({
|
|
15422
14459
|
cwd,
|
|
@@ -15734,9 +14771,9 @@ async function ensureVerifyProof({ args, cwd, dependencies, headSha, verifyProof
|
|
|
15734
14771
|
};
|
|
15735
14772
|
}
|
|
15736
14773
|
const reviewNotRequired = (proof) => proof.reviewRequirement?.status === "not-required";
|
|
15737
|
-
async function ensureReviewProof({ args, cwd, dependencies, currentReviewIdentity,
|
|
14774
|
+
async function ensureReviewProof({ args, cwd, dependencies, currentReviewIdentity, reviewProofPath }) {
|
|
15738
14775
|
const proof = readOptionalReviewProof(cwd, reviewProofPath);
|
|
15739
|
-
const ladderPolicy = resolveReviewLadderPolicy(
|
|
14776
|
+
const ladderPolicy = resolveReviewLadderPolicy();
|
|
15740
14777
|
const terminalFor = (candidate) => resolveReviewTerminalStateAcrossReviewsForPolicy(candidate, ladderPolicy);
|
|
15741
14778
|
const terminal = proof ? terminalFor(proof) : void 0;
|
|
15742
14779
|
const identityStatus = proof ? reviewStatus({
|
|
@@ -16005,7 +15042,6 @@ async function runPrPublish(args, dependencies = {}) {
|
|
|
16005
15042
|
}),
|
|
16006
15043
|
cwd,
|
|
16007
15044
|
dependencies,
|
|
16008
|
-
profile,
|
|
16009
15045
|
reviewProofPath
|
|
16010
15046
|
});
|
|
16011
15047
|
const pr = fetchPr();
|
|
@@ -16177,7 +15213,7 @@ function assertForegroundGate({ gate, env = process.env }) {
|
|
|
16177
15213
|
//#endregion
|
|
16178
15214
|
//#region src/commands/pr-review.ts
|
|
16179
15215
|
function createPrReviewCommand(output, action) {
|
|
16180
|
-
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", ".").option("--cycle <number>", "review cycle number", positiveInteger("--cycle"), 1).option("--
|
|
15216
|
+
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", ".").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({
|
|
16181
15217
|
gate: "pr:review",
|
|
16182
15218
|
resolveCycle: (options) => options.cycle,
|
|
16183
15219
|
resolveLedgerRoot: (options) => resolveCwdOption(options.cwd),
|
|
@@ -16189,7 +15225,6 @@ function createPrReviewCommand(output, action) {
|
|
|
16189
15225
|
"security",
|
|
16190
15226
|
"all"
|
|
16191
15227
|
].includes(options.mode)) throw new Error("--mode requires correctness, security, or all");
|
|
16192
|
-
if (typeof options.maxCycles === "number" && options.cycle > options.maxCycles) throw new Error("--cycle must be less than or equal to --max-cycles");
|
|
16193
15228
|
const args = {
|
|
16194
15229
|
base: options.base,
|
|
16195
15230
|
cwd: resolveCwdOption(options.cwd),
|
|
@@ -16197,7 +15232,6 @@ function createPrReviewCommand(output, action) {
|
|
|
16197
15232
|
dispositions: options.dispositions,
|
|
16198
15233
|
findings: options.findings,
|
|
16199
15234
|
issue: options.issue,
|
|
16200
|
-
maxCycles: options.maxCycles,
|
|
16201
15235
|
mode: options.mode,
|
|
16202
15236
|
output: options.output,
|
|
16203
15237
|
profilePath: options.profile,
|
|
@@ -16471,7 +15505,6 @@ function createProgram(options = {}) {
|
|
|
16471
15505
|
program.name("patronage-factory").description("Shared Patronage software factory CLI").version(version);
|
|
16472
15506
|
program.addCommand(createDoctorCommand(output));
|
|
16473
15507
|
program.addCommand(createConfigCommand(output));
|
|
16474
|
-
program.addCommand(createGuardCommand(output));
|
|
16475
15508
|
program.addCommand(createEvidenceEmitCommand(output));
|
|
16476
15509
|
program.addCommand(createHqFlushCommand(output));
|
|
16477
15510
|
program.addCommand(createBoundaryCheckCommand(output, options.actions?.boundaryCheck));
|