Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/gittensory-engine/src/focus-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ export type FocusManifestSettings = Partial<
| "reviewEvasionProtection"
| "reviewEvasionLabel"
| "reviewEvasionComment"
| "mergeTrainMode"
>
> & {
// `typeLabels`/`linkedIssueLabelPropagation`/`linkedIssueHardRules` are declared PARTIAL here (not via the `Pick<RepositorySettings,
Expand Down Expand Up @@ -1867,6 +1868,8 @@ function parseSettingsOverride(value: JsonValue | undefined, warnings: string[])
}
const reviewEvasionComment = normalizeOptionalBoolean(r.reviewEvasionComment, "settings.reviewEvasionComment", warnings);
if (reviewEvasionComment !== null) out.reviewEvasionComment = reviewEvasionComment;
const mergeTrainMode = normalizeOptionalEnum(r.mergeTrainMode, "settings.mergeTrainMode", ["off", "audit", "enforce"] as const, warnings);
if (mergeTrainMode !== null) out.mergeTrainMode = mergeTrainMode;
return out;
}

Expand Down
3 changes: 3 additions & 0 deletions packages/gittensory-engine/src/types/manifest-deps-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,9 @@ export type RepositorySettings = {
/** Review-evasion protection: whether to post the public explanation comment before the enforcement close.
* Default true. */
reviewEvasionComment?: boolean | undefined;
/** Merge-train FIFO gate (#selfhost-merge-train): `"off"` keeps current behavior, `"audit"` logs would-hold
* decisions, and `"enforce"` defers a merge behind a still-viable older sibling. */
mergeTrainMode?: "off" | "audit" | "enforce" | undefined;
/** Config-driven before/after screenshot-table gate (#2006): a DETERMINISTIC check (no AI, zero hallucination
* risk) that a contributor visual/frontend PR's body contains a markdown table with before/after image
* markup, scoped to the repo's configured labels/paths (`whenLabels`/`whenPaths`, OR-matched). Off by
Expand Down
1 change: 1 addition & 0 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,7 @@ const maintainerSettingsSchema = z
manifestPolicyGateMode: z.enum(["off", "advisory", "block"]),
selfAuthoredLinkedIssueGateMode: z.enum(["off", "advisory", "block"]),
linkedIssueSatisfactionGateMode: z.enum(["off", "advisory", "block"]),
mergeTrainMode: z.enum(["off", "audit", "enforce"]),
firstTimeContributorGrace: z.boolean(),
slopGateMode: z.enum(["off", "advisory", "block"]),
slopGateMinScore: z.number().int().min(0).max(100).nullable(),
Expand Down
3 changes: 2 additions & 1 deletion test/integration/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2448,7 +2448,7 @@ describe("api routes", () => {
// #2267: qualityGateMode: "block" is downgraded to "advisory" on write — readiness/quality can never
// hard-block a PR, so the dashboard/API save path can't persist a value implying enforcement it doesn't
// have. slopGateMode: "block" is a DIFFERENT, legitimately-blockable dimension and is left untouched.
body: JSON.stringify({ gateCheckMode: "enabled", slopGateMode: "block", slopGateMinScore: 55, qualityGateMode: "block", autonomy: { merge: "auto_with_approval", deploy: "auto" }, autoMaintain: { requireApprovals: 2, mergeMethod: "rebase" }, agentPaused: true, agentDryRun: true }),
body: JSON.stringify({ gateCheckMode: "enabled", slopGateMode: "block", slopGateMinScore: 55, qualityGateMode: "block", mergeTrainMode: "enforce", autonomy: { merge: "auto_with_approval", deploy: "auto" }, autoMaintain: { requireApprovals: 2, mergeMethod: "rebase" }, agentPaused: true, agentDryRun: true }),
},
ownerEnv,
);
Expand All @@ -2462,6 +2462,7 @@ describe("api routes", () => {
slopGateMode: "block",
slopGateMinScore: 55,
qualityGateMode: "advisory", // #2267: downgraded, not persisted as "block"
mergeTrainMode: "enforce",
autonomy: { merge: "auto_with_approval" }, // unknown action class dropped by the DB normalizer
autoMaintain: { requireApprovals: 2, mergeMethod: "rebase" },
agentPaused: true, // #776 kill-switch
Expand Down
6 changes: 4 additions & 2 deletions test/unit/focus-manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ describe(".gittensory.yml.example field-exhaustiveness (#1670)", () => {
reviewEvasionProtection: "reviewEvasionProtection:",
reviewEvasionLabel: "reviewEvasionLabel:",
reviewEvasionComment: "reviewEvasionComment:",
mergeTrainMode: "mergeTrainMode:",
typeLabels: "typeLabels:",
linkedIssueLabelPropagation: "linkedIssueLabelPropagation:",
linkedIssueHardRules: "linkedIssueHardRules:",
Expand Down Expand Up @@ -1786,13 +1787,14 @@ describe("parseFocusManifest settings override + resolveEffectiveSettings", () =

it("drops invalid settings values with warnings and keeps the valid ones", () => {
const m = parseFocusManifest({
settings: { commentMode: "loud", qualityGateMinScore: "high", autoLabelEnabled: "yes", gittensorLabel: " ", publicSurface: "comment_only" },
settings: { commentMode: "loud", qualityGateMinScore: "high", autoLabelEnabled: "yes", gittensorLabel: " ", mergeTrainMode: "later", publicSurface: "comment_only" },
});
expect(m.settings).toEqual({ publicSurface: "comment_only" });
expect(m.warnings.some((w) => /settings\.commentMode/.test(w))).toBe(true);
expect(m.warnings.some((w) => /settings\.qualityGateMinScore/.test(w))).toBe(true);
expect(m.warnings.some((w) => /settings\.autoLabelEnabled/.test(w))).toBe(true);
expect(m.warnings.some((w) => /settings\.gittensorLabel/.test(w))).toBe(true);
expect(m.warnings.some((w) => /settings\.mergeTrainMode/.test(w))).toBe(true);
});

it("ignores a non-mapping settings block and treats a settings-only manifest as present", () => {
Expand All @@ -1815,7 +1817,7 @@ describe("parseFocusManifest settings override + resolveEffectiveSettings", () =
});

it("round-trips settings through settingsOverrideToJson and serializes empty as null", () => {
const original = parseFocusManifest({ settings: { commentMode: "all_prs", qualityGateMinScore: 40 } });
const original = parseFocusManifest({ settings: { commentMode: "all_prs", qualityGateMinScore: 40, mergeTrainMode: "audit" } });
const reparsed = parseFocusManifest({ settings: settingsOverrideToJson(original.settings) });
expect(reparsed.settings).toEqual(original.settings);
expect(settingsOverrideToJson(parseFocusManifest({}).settings)).toBeNull();
Expand Down