From 049feeb7aef7cc0d980624c28c8458300dd3c5ab Mon Sep 17 00:00:00 2001
From: JSONbored <49853598+JSONbored@users.noreply.github.com>
Date: Sat, 4 Jul 2026 00:39:51 -0700
Subject: [PATCH 1/2] fix(selfhost): make hard guardrails authoritative
---
.gittensory.yml | 5 --
apps/gittensory-ui/public/openapi.json | 7 --
.../site/app-panels/maintainer-settings.tsx | 2 +-
.../src/routes/docs.how-reviews-work.tsx | 11 ++--
.../src/routes/docs.privacy-security.tsx | 4 +-
apps/gittensory-ui/src/routes/docs.tuning.tsx | 24 +++----
config/examples/README.md | 4 +-
src/config/gittensory-repo-focus-manifest.ts | 5 --
src/openapi/schemas.ts | 1 -
src/queue/processors.ts | 5 --
src/review/parity-wire.ts | 5 +-
src/rules/advisory.ts | 33 ++--------
src/rules/predicted-gate.ts | 2 +-
src/selfhost/config-lint.ts | 1 -
src/services/contributor-issue-draft.ts | 2 +-
src/services/decision-pack.ts | 6 --
.../miner-dashboard-recommendations.ts | 3 +-
src/services/remediation-plan.ts | 2 +-
src/signals/focus-manifest-loader.ts | 1 -
src/signals/focus-manifest.ts | 48 +++-----------
src/signals/repo-policy-readiness.ts | 13 ----
src/types.ts | 5 +-
.../engine-parity/predicted-gate/README.md | 2 +-
.../predicted-gate/manifest-blocked-path.ts | 11 ++--
test/integration/api.test.ts | 13 ++--
test/unit/contributor-issue-draft.test.ts | 4 +-
test/unit/decision-pack.test.ts | 7 +-
test/unit/focus-manifest-loader.test.ts | 8 +--
test/unit/focus-manifest.test.ts | 31 ++++-----
test/unit/gate-check-policy.test.ts | 66 +++++++------------
test/unit/gittensory-focus-manifest.test.ts | 20 +++---
test/unit/local-branch.test.ts | 8 +--
test/unit/mcp-predict-gate.test.ts | 8 +--
.../miner-dashboard-recommendations.test.ts | 10 +--
test/unit/parity-wire.test.ts | 4 +-
test/unit/policy-sanitizer.test.ts | 5 +-
test/unit/pr-body-draft.test.ts | 1 -
test/unit/predicted-gate.test.ts | 17 +++--
.../unit/public-safe-manifest-finding.test.ts | 19 +-----
test/unit/queue.test.ts | 8 +--
test/unit/repo-policy-readiness.test.ts | 6 +-
test/unit/routes-focus-manifest.test.ts | 7 +-
test/unit/routes-remediation-plan.test.ts | 2 +-
test/unit/selfhost-config-lint.test.ts | 12 +++-
test/unit/unified-comment.test.ts | 4 +-
45 files changed, 146 insertions(+), 316 deletions(-)
diff --git a/.gittensory.yml b/.gittensory.yml
index 296ff074d6..e1825a7c4c 100644
--- a/.gittensory.yml
+++ b/.gittensory.yml
@@ -14,11 +14,6 @@ wantedPaths:
- wrangler.jsonc
- apps/gittensory-ui/
-blockedPaths:
- - site/
- - CNAME
- - "**/lovable/**"
-
preferredLabels:
- bug
- enhancement
diff --git a/apps/gittensory-ui/public/openapi.json b/apps/gittensory-ui/public/openapi.json
index 59d8272122..117103dc38 100644
--- a/apps/gittensory-ui/public/openapi.json
+++ b/apps/gittensory-ui/public/openapi.json
@@ -6266,12 +6266,6 @@
"type": "string"
}
},
- "matchedBlockedPaths": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
"preferredLabelHits": {
"type": "array",
"items": {
@@ -6334,7 +6328,6 @@
"linkedIssuePolicy",
"issueDiscoveryPolicy",
"matchedWantedPaths",
- "matchedBlockedPaths",
"preferredLabelHits",
"findings",
"publicNextSteps",
diff --git a/apps/gittensory-ui/src/components/site/app-panels/maintainer-settings.tsx b/apps/gittensory-ui/src/components/site/app-panels/maintainer-settings.tsx
index 9d108ed1e2..919f9c0b14 100644
--- a/apps/gittensory-ui/src/components/site/app-panels/maintainer-settings.tsx
+++ b/apps/gittensory-ui/src/components/site/app-panels/maintainer-settings.tsx
@@ -766,7 +766,7 @@ function FocusManifestEditor({ base }: { base: string | null }) {
Focus manifest (config-as-code)
- The repo's maintainer focus policy as JSON — wanted/blocked paths, linked-issue policy,
+ The repo's maintainer focus policy as JSON — wanted paths, linked-issue policy,
test expectations, and gate overrides. Mirrors{" "}
.gittensory.yml.
diff --git a/apps/gittensory-ui/src/routes/docs.how-reviews-work.tsx b/apps/gittensory-ui/src/routes/docs.how-reviews-work.tsx
index 6c97de8d88..dc4f01d1ae 100644
--- a/apps/gittensory-ui/src/routes/docs.how-reviews-work.tsx
+++ b/apps/gittensory-ui/src/routes/docs.how-reviews-work.tsx
@@ -115,8 +115,9 @@ function HowReviewsWork() {
Manifest-policy gate (manifestPolicyGateMode, default{" "}
- off) — when block, the repo's declared policy (blocked paths,
- required linked issue, test expectations) becomes enforceable.
+ off) — when block, the repo's declared policy (required linked
+ issue and test expectations) becomes enforceable. Manual-review path holds are controlled
+ separately by settings.hardGuardrailGlobs.
@@ -228,9 +229,9 @@ function HowReviewsWork() {
reviewer reads it.
- manifest_blocked_path — the PR touches a path listed in the repo's{" "}
- blockedPaths. Enforceable when manifestPolicy is{" "}
- block.
+ guardrail_hold — the PR touches a path listed in{" "}
+ settings.hardGuardrailGlobs. This is a manual-review hold, not an
+ auto-close reason.
manifest_missing_tests — code changed but the expected test paths (
diff --git a/apps/gittensory-ui/src/routes/docs.privacy-security.tsx b/apps/gittensory-ui/src/routes/docs.privacy-security.tsx
index fceab499f2..c7c2a34b43 100644
--- a/apps/gittensory-ui/src/routes/docs.privacy-security.tsx
+++ b/apps/gittensory-ui/src/routes/docs.privacy-security.tsx
@@ -63,8 +63,8 @@ function PrivacySecurity() {
Per-repo settings — gate modes, score thresholds, and guardrails, stored
in the operator's database (set through the dashboard/API) or declared as config-as-code
in a repo's .gittensory.yml. Choosing gate.slop.minScore or
- marking a path under blockedPaths tightens the gate without telling a
- contributor how to pass it.
+ setting settings.hardGuardrailGlobs tightens the gate without telling a
+ contributor how to bypass it.
Operator feature flags — the GITTENSORY_REVIEW_* family of
diff --git a/apps/gittensory-ui/src/routes/docs.tuning.tsx b/apps/gittensory-ui/src/routes/docs.tuning.tsx
index 27de1f1853..c372000e90 100644
--- a/apps/gittensory-ui/src/routes/docs.tuning.tsx
+++ b/apps/gittensory-ui/src/routes/docs.tuning.tsx
@@ -248,8 +248,8 @@ function Tuning() {
gate.manifestPolicy — when block, the manifest's declared policy
- (blocked paths, required linked issue, test expectations) becomes an enforceable blocker.
- Default off.
+ (required linked issue and test expectations) becomes an enforceable blocker. Manual-review
+ path holds use settings.hardGuardrailGlobs instead. Default off.
gate.firstTimeContributorGrace — when true, softens a would-be
@@ -294,21 +294,16 @@ function Tuning() {
Guardrails and scope
- Top-level keys in .gittensory.yml declare the repo's focus and guardrails.
- These feed the deterministic findings (such as manifest_blocked_path and{" "}
- manifest_missing_tests) and — when gate.manifestPolicy: block —
- can become enforceable blockers.
+ Top-level keys in .gittensory.yml declare the repo's focus and validation
+ expectations. These feed deterministic findings such as manifest_missing_tests{" "}
+ and — when gate.manifestPolicy: block — can become enforceable blockers. Manual
+ path holds are configured only through settings.hardGuardrailGlobs.
-
wantedPaths — globs for work areas you want; PRs touching these are
preferred. Default [].
- -
-
blockedPaths — globs off-limits to contributors. Touching one yields a{" "}
- manifest_blocked_path finding, enforceable when{" "}
- gate.manifestPolicy: block. Default [].
-
-
preferredLabels — labels you prefer on incoming PRs; a missing one is
surfaced. Default [].
@@ -388,18 +383,15 @@ function Tuning() {
Example .gittensory.yml
- A worked manifest: focus and guardrails up top, a refined gate, BYOK AI review, and a few
+ A worked manifest: focus and validation up top, a refined gate, BYOK AI review, and a few
dashboard-equivalent overrides.
>
> = {
- manifest_blocked_path: {
- detail: "Changed paths match maintainer-blocked areas.",
- action:
- "Move this work out of the maintainer-blocked area or confirm with the maintainer before opening a PR.",
- },
manifest_missing_tests: {
detail: "Maintainer test expectations are not satisfied by this PR.",
action:
diff --git a/src/review/parity-wire.ts b/src/review/parity-wire.ts
index d84779d865..e723df4796 100644
--- a/src/review/parity-wire.ts
+++ b/src/review/parity-wire.ts
@@ -42,7 +42,6 @@ const NEUTRAL_HOLD_REASON_CODES = [
"ai_review_inconclusive",
"oversized_pr",
"guardrail_hold",
- "manifest_blocked_path",
"repo_not_registered",
"repo_not_seen",
"pr_not_cached",
@@ -87,8 +86,8 @@ const PARITY_WINDOW_DAYS = 90;
* keeps the parity SAFETY metric honest: a shadow 'hold' is
* never the dangerous "shadow merges where authoritative
* wouldn't" direction.
- * • 'neutral' → 'hold' — a REAL, deliberate decision (a guardrail/size/manifest-blocked
- * hold, or an AI-inconclusive fail-closed hold): the gate chose
+ * • 'neutral' → 'hold' — a REAL, deliberate decision (a guardrail/size hold, or an
+ * AI-inconclusive fail-closed hold): the gate chose
* to hold this PR for a human rather than pass it automatically.
* This is exactly as terminal, from an observability standpoint,
* as a 'failure' hold (#terminal-outcome-audit) -- recording it
diff --git a/src/rules/advisory.ts b/src/rules/advisory.ts
index b6704e0ba3..f4f3fe32e5 100644
--- a/src/rules/advisory.ts
+++ b/src/rules/advisory.ts
@@ -46,8 +46,8 @@ export type GateCheckPolicy = {
* linked-issue, duplicate, quality/readiness, slop — to its mode, so a maintainer flips ONE switch instead
* of four and the review-agent check stays the single required check. `off` = sub-gates use their own modes. */
mergeReadinessGateMode?: GateRuleMode | undefined;
- /** Focus-manifest policy gate (#555). When `block`, linked-issue/test policy findings become hard blockers;
- * blocked-path findings become manual-review holds because guardrailed paths should be reviewed, not closed.
+ /** Focus-manifest policy gate (#555). When `block`, linked-issue/test policy findings become hard blockers.
+ * Path-based manual-review holds are configured only with `settings.hardGuardrailGlobs`.
* An INDEPENDENT dimension, deliberately NOT folded into the merge-readiness composite so #555 stays focused.
* `off`/`advisory` = the findings stay advisory (never block). Default off. */
manifestPolicyGateMode?: GateRuleMode | undefined;
@@ -475,27 +475,6 @@ function buildGuardrailHoldFinding(matches: GuardrailPathMatch[] = []): Advisory
};
}
-function buildManifestBlockedPathHoldFinding(
- findings: AdvisoryFinding[],
- policy: GateCheckPolicy,
-): AdvisoryFinding | null {
- if (gateMode(policy.manifestPolicyGateMode ?? "off") !== "block")
- return null;
- const blocked = findings.find(
- (finding) => finding.code === "manifest_blocked_path",
- );
- if (!blocked) return null;
- return {
- code: "manifest_blocked_path",
- severity: "warning",
- title: "Touches a maintainer-blocked path — held for manual review",
- detail:
- "This PR changes a maintainer-blocked path, so it is held for a maintainer to review and merge manually.",
- action: "A maintainer must review and merge this change.",
- ...(blocked.publicText !== undefined ? { publicText: blocked.publicText } : {}),
- };
-}
-
/** Dry-run disposition (#gate-dryrun): promote every `advisory` sub-gate mode to `block` so the core eval yields the
* would-be conclusion. `off`/`block`/unset modes are untouched; non-mode policy (size HOLD, guardrail) is
* preserved as-is, so the would-be verdict still honours manual-review holds. PURE. */
@@ -574,11 +553,7 @@ function evaluateGateCheckCore(advisoryResult: Advisory, policy: GateCheckPolicy
// so neutral never blocks the merge (dry-run/advisory friendly) and a contributor PR is never auto-closed for size.
const sizeHold = buildSizeHoldFinding(effective);
const guardrailHold = effective.guardrailHit ? buildGuardrailHoldFinding(effective.guardrailMatches) : null;
- const manifestBlockedPathHold = buildManifestBlockedPathHoldFinding(
- advisoryResult.findings,
- effective,
- );
- const holds = [sizeHold, guardrailHold, manifestBlockedPathHold].filter(
+ const holds = [sizeHold, guardrailHold].filter(
(f): f is AdvisoryFinding => f !== null,
);
if (holds.length > 0) {
@@ -903,7 +878,7 @@ function isConfiguredGateBlocker(finding: AdvisoryFinding, policy: GateCheckPoli
// warning and is never blocked here. No AI judgment is involved, so this can never cause an AI false-close.
if (code === "pre_merge_check_required") return true;
// Focus-manifest policy (#555): linked-issue/test policy findings block ONLY when the maintainer opts into
- // manifestPolicy: block. Blocked paths are guardrails, so they are handled as manual-review holds above.
+ // manifestPolicy: block. Path holds are intentionally separate and configured via hardGuardrailGlobs.
if (code === "manifest_linked_issue_required" || code === "manifest_missing_tests") {
return gateMode(policy.manifestPolicyGateMode ?? "off") === "block";
}
diff --git a/src/rules/predicted-gate.ts b/src/rules/predicted-gate.ts
index aee9d2eda0..da3a87b1ca 100644
--- a/src/rules/predicted-gate.ts
+++ b/src/rules/predicted-gate.ts
@@ -238,7 +238,7 @@ export function buildPredictedGateVerdict(args: {
testFileCount: changedPaths.filter((path) => isTestPath(path)).length,
passedValidationCount: 0,
});
- const policyCodes = new Set(["manifest_blocked_path", "manifest_linked_issue_required", "manifest_missing_tests"]);
+ const policyCodes = new Set(["manifest_linked_issue_required", "manifest_missing_tests"]);
for (const finding of guidance.findings) {
if (!policyCodes.has(finding.code)) continue;
advisory.findings.push({
diff --git a/src/selfhost/config-lint.ts b/src/selfhost/config-lint.ts
index 7c49f7ba44..ac19b360fd 100644
--- a/src/selfhost/config-lint.ts
+++ b/src/selfhost/config-lint.ts
@@ -4,7 +4,6 @@ import { MAX_FOCUS_MANIFEST_BYTES, parseFocusManifestContent } from "../signals/
const TOP_LEVEL_FIELDS = [
"source",
"wantedPaths",
- "blockedPaths",
"preferredLabels",
"linkedIssuePolicy",
"testExpectations",
diff --git a/src/services/contributor-issue-draft.ts b/src/services/contributor-issue-draft.ts
index 8f9f687cfa..450eb89ede 100644
--- a/src/services/contributor-issue-draft.ts
+++ b/src/services/contributor-issue-draft.ts
@@ -486,7 +486,7 @@ function wantedPathCandidate(repoFullName: string, wantedPath: string, openIssue
"Keep maintainerNotes private; use publicNotes only when explicitly opted in.",
],
acceptanceCriteria: [
- `The change materially improves ${wantedPath} without expanding into blocked areas.`,
+ `The change materially improves ${wantedPath} without expanding beyond the requested scope.`,
"Manifest-guided guidance and tests stay aligned.",
],
testingRequirements: buildContributorIssueDraftTestingRequirements(manifest),
diff --git a/src/services/decision-pack.ts b/src/services/decision-pack.ts
index 439ec1b469..27a4adb390 100644
--- a/src/services/decision-pack.ts
+++ b/src/services/decision-pack.ts
@@ -245,7 +245,6 @@ export type RepoDecisionManifestSummary = {
linkedIssuePolicy: FocusManifestLinkedIssuePolicy;
issueDiscoveryPolicy: FocusManifestIssueDiscoveryPolicy;
wantedPathCount: number;
- blockedPathCount: number;
preferredLabels: string[];
publicNotes: string[];
};
@@ -1017,7 +1016,6 @@ function buildRepoDecisionManifestSummary(manifest: FocusManifest): RepoDecision
linkedIssuePolicy: manifest.linkedIssuePolicy,
issueDiscoveryPolicy: manifest.issueDiscoveryPolicy,
wantedPathCount: manifest.wantedPaths.length,
- blockedPathCount: manifest.blockedPaths.length,
preferredLabels: manifest.preferredLabels.slice(0, 8),
publicNotes: manifest.publicNotes.filter(isFocusManifestPublicSafe).slice(0, 4),
};
@@ -1032,10 +1030,6 @@ function buildRepoDecisionManifestReasons(manifest: FocusManifest): { whyThisHel
whyThisHelps.push(`Maintainer focus manifest declares ${manifest.wantedPaths.length} wanted path(s) for this repo.`);
publicNextActions.push("Target the maintainer-wanted areas for this repo when picking a change.");
}
- if (manifest.blockedPaths.length > 0) {
- riskReasons.push(`Maintainer focus manifest blocks ${manifest.blockedPaths.length} path pattern(s) for this repo.`);
- publicNextActions.push("Avoid the maintainer-blocked areas for this repo.");
- }
if (manifest.linkedIssuePolicy === "required") {
nextActions.push("Link a tracked issue on every PR; the maintainer's manifest requires it.");
publicNextActions.push("Link a tracked issue on every PR; the maintainer requires linked issues.");
diff --git a/src/services/miner-dashboard-recommendations.ts b/src/services/miner-dashboard-recommendations.ts
index a1adaccad5..7e47d8f8bc 100644
--- a/src/services/miner-dashboard-recommendations.ts
+++ b/src/services/miner-dashboard-recommendations.ts
@@ -362,8 +362,7 @@ function manifestSummary(decision: DashboardRecord | undefined): string | undefi
const linkedIssuePolicy = stringValue(manifest, "linkedIssuePolicy") ?? "unknown";
const issueDiscoveryPolicy = stringValue(manifest, "issueDiscoveryPolicy") ?? "unknown";
const wantedPathCount = numberValue(manifest, "wantedPathCount") ?? 0;
- const blockedPathCount = numberValue(manifest, "blockedPathCount") ?? 0;
- return `${linkedIssuePolicy}/${issueDiscoveryPolicy}/${wantedPathCount} wanted/${blockedPathCount} blocked`;
+ return `${linkedIssuePolicy}/${issueDiscoveryPolicy}/${wantedPathCount} wanted`;
}
function recordArray(value: unknown): DashboardRecord[] {
diff --git a/src/services/remediation-plan.ts b/src/services/remediation-plan.ts
index f17715c8c8..f5246b403c 100644
--- a/src/services/remediation-plan.ts
+++ b/src/services/remediation-plan.ts
@@ -99,7 +99,7 @@ function stepFromBlocker(source: RemediationPlanSource, blocker: string, finding
function impactFor(source: RemediationPlanSource, blocker: string): "high" | "medium" {
if (source === "account_state") return "high";
- if (/GitHub checks|validation failed|maintainer-blocked|duplicate|ineligible/i.test(blocker)) return "high";
+ if (/GitHub checks|validation failed|guarded path|duplicate|ineligible/i.test(blocker)) return "high";
return source === "branch_quality" ? "high" : "medium";
}
diff --git a/src/signals/focus-manifest-loader.ts b/src/signals/focus-manifest-loader.ts
index 8c8a74a48b..a1499033bb 100644
--- a/src/signals/focus-manifest-loader.ts
+++ b/src/signals/focus-manifest-loader.ts
@@ -272,7 +272,6 @@ function manifestToJson(manifest: FocusManifest): Record {
return {
source: manifest.source,
wantedPaths: manifest.wantedPaths,
- blockedPaths: manifest.blockedPaths,
preferredLabels: manifest.preferredLabels,
linkedIssuePolicy: manifest.linkedIssuePolicy,
testExpectations: manifest.testExpectations,
diff --git a/src/signals/focus-manifest.ts b/src/signals/focus-manifest.ts
index f260280468..4ea43c723b 100644
--- a/src/signals/focus-manifest.ts
+++ b/src/signals/focus-manifest.ts
@@ -314,15 +314,15 @@ const MAX_PATH_INSTRUCTIONS = 50;
/**
* Normalized maintainer focus manifest. Repo owners declare which work areas are wanted,
- * blocked, or preferred so Gittensory guidance can explain why a path is encouraged or
- * discouraged. `maintainerNotes` are private review context and must never reach a public
+ * preferred, and how PRs should present validation. Path-based manual review is intentionally
+ * not part of this manifest anymore; use `settings.hardGuardrailGlobs` for that single
+ * authoritative control. `maintainerNotes` are private review context and must never reach a public
* GitHub surface; `publicNotes` are explicitly opted into public output by the maintainer.
*/
export type FocusManifest = {
present: boolean;
source: FocusManifestSource;
wantedPaths: string[];
- blockedPaths: string[];
preferredLabels: string[];
linkedIssuePolicy: FocusManifestLinkedIssuePolicy;
testExpectations: string[];
@@ -339,7 +339,6 @@ export type FocusManifest = {
export type FocusManifestFinding = {
code:
- | "manifest_blocked_path"
| "manifest_off_focus"
| "manifest_preferred_path"
| "manifest_missing_preferred_label"
@@ -360,7 +359,6 @@ export type FocusManifestGuidance = {
linkedIssuePolicy: FocusManifestLinkedIssuePolicy;
issueDiscoveryPolicy: FocusManifestIssueDiscoveryPolicy;
matchedWantedPaths: string[];
- matchedBlockedPaths: string[];
preferredLabelHits: string[];
findings: FocusManifestFinding[];
publicNextSteps: string[];
@@ -433,7 +431,6 @@ const EMPTY_MANIFEST: FocusManifest = {
present: false,
source: "none",
wantedPaths: [],
- blockedPaths: [],
preferredLabels: [],
linkedIssuePolicy: "optional",
testExpectations: [],
@@ -1718,7 +1715,6 @@ export function parseFocusManifest(raw: unknown, source?: FocusManifestSource):
present: true,
source: normalizeSource(source, record.source, warnings),
wantedPaths: normalizeStringList(record.wantedPaths, "wantedPaths", warnings),
- blockedPaths: normalizeStringList(record.blockedPaths, "blockedPaths", warnings),
preferredLabels: normalizeStringList(record.preferredLabels, "preferredLabels", warnings),
linkedIssuePolicy: normalizeEnum(record.linkedIssuePolicy, "linkedIssuePolicy", ["required", "preferred", "optional"] as const, "optional", warnings),
testExpectations: normalizeStringList(record.testExpectations, "testExpectations", warnings),
@@ -1734,7 +1730,6 @@ export function parseFocusManifest(raw: unknown, source?: FocusManifestSource):
};
if (
manifest.wantedPaths.length === 0 &&
- manifest.blockedPaths.length === 0 &&
manifest.preferredLabels.length === 0 &&
manifest.testExpectations.length === 0 &&
manifest.maintainerNotes.length === 0 &&
@@ -1892,7 +1887,6 @@ export function buildFocusManifestGuidance(args: {
const testFileCount = Math.max(0, args.testFileCount ?? 0);
const passedValidationCount = Math.max(0, args.passedValidationCount ?? 0);
- const matchedBlockedPaths = matchedPatterns(changedPaths, manifest.blockedPaths);
const matchedWantedPaths = matchedPatterns(changedPaths, manifest.wantedPaths);
const preferredLabelHits = manifest.preferredLabels.filter((label) => labels.includes(label.toLowerCase()));
@@ -1909,7 +1903,6 @@ export function buildFocusManifestGuidance(args: {
linkedIssuePolicy: manifest.linkedIssuePolicy,
issueDiscoveryPolicy: manifest.issueDiscoveryPolicy,
matchedWantedPaths: [],
- matchedBlockedPaths: [],
preferredLabelHits: [],
findings,
publicNextSteps: [],
@@ -1918,16 +1911,7 @@ export function buildFocusManifestGuidance(args: {
};
}
- if (matchedBlockedPaths.length > 0) {
- findings.push({
- code: "manifest_blocked_path",
- severity: "critical",
- title: "Change touches a maintainer-blocked area",
- detail: `Changed paths match maintainer-blocked patterns: ${matchedBlockedPaths.slice(0, 5).join(", ")}.`,
- action: "Move this work out of the maintainer-blocked area or confirm with the maintainer before opening a PR.",
- });
- publicNextSteps.push("Avoid the maintainer-blocked areas this branch currently touches; confirm scope with the maintainer first.");
- } else if (manifest.wantedPaths.length > 0 && matchedWantedPaths.length === 0 && changedPaths.length > 0) {
+ if (manifest.wantedPaths.length > 0 && matchedWantedPaths.length === 0 && changedPaths.length > 0) {
findings.push({
code: "manifest_off_focus",
severity: "warning",
@@ -2010,17 +1994,15 @@ export function buildFocusManifestGuidance(args: {
linkedIssuePolicy: manifest.linkedIssuePolicy,
issueDiscoveryPolicy: manifest.issueDiscoveryPolicy,
matchedWantedPaths,
- matchedBlockedPaths,
preferredLabelHits,
findings,
publicNextSteps: safeNextSteps,
warnings: manifest.warnings,
- summary: summarize(manifest, matchedBlockedPaths, matchedWantedPaths),
+ summary: summarize(manifest, matchedWantedPaths),
};
}
-function summarize(manifest: FocusManifest, blocked: string[], wanted: string[]): string {
- if (blocked.length > 0) return "Maintainer focus manifest: change touches a blocked area.";
+function summarize(manifest: FocusManifest, wanted: string[]): string {
if (wanted.length > 0) return "Maintainer focus manifest: change aligns with a wanted area.";
if (manifest.wantedPaths.length > 0) return "Maintainer focus manifest: change is outside the wanted areas.";
return "Maintainer focus manifest applied with no path-specific verdict.";
@@ -2149,9 +2131,6 @@ function buildPolicyEntryGuidance(manifest: FocusManifest): string[] {
if (manifest.wantedPaths.length > 0) {
guidance.push(`Focus changes on maintainer-wanted areas: ${manifest.wantedPaths.slice(0, 5).join(", ")}.`);
}
- if (manifest.blockedPaths.length > 0) {
- guidance.push(`Avoid maintainer-blocked areas: ${manifest.blockedPaths.slice(0, 5).join(", ")}.`);
- }
if (manifest.linkedIssuePolicy === "required") guidance.push("Link a tracked issue before opening a pull request.");
else if (manifest.linkedIssuePolicy === "preferred") guidance.push("Linking a tracked issue is preferred before opening a pull request.");
if (manifest.preferredLabels.length > 0) {
@@ -2175,7 +2154,6 @@ function buildPolicyContributionLanes(manifest: FocusManifest): FocusManifestPol
const lanes: FocusManifestPolicyContributionLane[] = [];
const safeWantedPaths = manifest.wantedPaths.filter(isFocusManifestPublicSafe);
- const safeBlockedPaths = manifest.blockedPaths.filter(isFocusManifestPublicSafe);
const safeTestExpectations = manifest.testExpectations.filter(isFocusManifestPublicSafe);
// Derive the public preference only from public-safe signals: use the SAME filtered list that surfaces in
@@ -2198,7 +2176,7 @@ function buildPolicyContributionLanes(manifest: FocusManifest): FocusManifestPol
? "Contribute changes in maintainer-wanted areas with required validation evidence."
: "Direct pull requests are accepted when they stay inside maintainer-wanted scope.",
preferredPaths: safeWantedPaths,
- discouragedPaths: safeBlockedPaths,
+ discouragedPaths: [],
validationExpectations: safeTestExpectations,
publicNotes: manifest.publicNotes.filter(isFocusManifestPublicSafe),
});
@@ -2219,7 +2197,7 @@ function buildPolicyContributionLanes(manifest: FocusManifest): FocusManifestPol
? "The maintainer has indicated this repo prefers direct fixes over new issue reports."
: "Issue discovery is optional; confirm maintainer scope before filing new issues.",
preferredPaths: [],
- discouragedPaths: safeBlockedPaths,
+ discouragedPaths: [],
validationExpectations: [],
publicNotes: [],
});
@@ -2236,9 +2214,6 @@ function buildPolicyReadinessWarnings(manifest: FocusManifest): string[] {
if (manifest.testExpectations.length === 0) {
warnings.push("Focus manifest does not define validation expectations; contributors may not know what tests to run.");
}
- if (manifest.blockedPaths.length > 0 && manifest.wantedPaths.length === 0) {
- warnings.push("Focus manifest blocks work areas but does not define wanted paths; pair blocked areas with a positive lane.");
- }
return warnings.filter(isFocusManifestPublicSafe);
}
@@ -2287,7 +2262,6 @@ export function deriveContributionLanes(manifest: FocusManifest): ContributionLa
}
const safeWanted = manifest.wantedPaths.filter(isFocusManifestPublicSafe);
- const safeBlocked = manifest.blockedPaths.filter(isFocusManifestPublicSafe);
const safePublicNotes = manifest.publicNotes.filter(isFocusManifestPublicSafe);
const validationExpectations: string[] = [];
@@ -2300,7 +2274,6 @@ export function deriveContributionLanes(manifest: FocusManifest): ContributionLa
const directPrLane: ContributionLanePreference =
manifest.issueDiscoveryPolicy === "encouraged" ? "discouraged"
: safeWanted.length > 0 ? "preferred"
- : safeBlocked.length > 0 ? "discouraged"
: "neutral";
const issueDiscoveryLane: ContributionLanePreference =
@@ -2324,9 +2297,6 @@ export function deriveContributionLanes(manifest: FocusManifest): ContributionLa
if (safeWanted.length > 0) {
prEntryGuidance.push(`Focus changes on maintainer-wanted areas: ${manifest.wantedPaths.slice(0, 5).join(", ")}.`);
}
- if (safeBlocked.length > 0) {
- prEntryGuidance.push(`Avoid maintainer-blocked areas: ${manifest.blockedPaths.slice(0, 5).join(", ")}.`);
- }
if (manifest.preferredLabels.length > 0) {
const safeLabels = manifest.preferredLabels.filter(isFocusManifestPublicSafe);
if (safeLabels.length > 0) {
@@ -2366,7 +2336,7 @@ export function deriveContributionLanes(manifest: FocusManifest): ContributionLa
directPrLane,
issueDiscoveryLane,
preferredEntryPaths: safeWanted,
- discouragedEntryPaths: safeBlocked,
+ discouragedEntryPaths: [],
validationExpectations,
issueEntryGuidance: issueEntryGuidance.filter(isFocusManifestPublicSafe),
prEntryGuidance: safeprEntryGuidance,
diff --git a/src/signals/repo-policy-readiness.ts b/src/signals/repo-policy-readiness.ts
index 0f1448e4a2..a2070cb360 100644
--- a/src/signals/repo-policy-readiness.ts
+++ b/src/signals/repo-policy-readiness.ts
@@ -13,7 +13,6 @@ export type RepoPolicyReadinessWarningCode =
| "focus_policy_missing"
| "focus_policy_needs_review"
| "contribution_scope_unclear"
- | "blocked_work_without_wanted_scope"
| "direct_pr_policy_unclear"
| "linked_issue_policy_mismatch"
| "issue_discovery_policy_mismatch"
@@ -43,7 +42,6 @@ export type RepoPolicyReadinessReport = {
privateNoteCount: number;
manifestWarningCount: number;
wantedPathCount: number;
- blockedPathCount: number;
validationExpectationCount: number;
queueLevel: QueueHealth["level"];
contributorIntakeLevel: ContributorIntakeHealth["level"];
@@ -107,16 +105,6 @@ export function buildRepoPolicyReadiness(input: RepoPolicyReadinessInput): RepoP
});
}
- if (manifest.blockedPaths.length > 0 && manifest.wantedPaths.length === 0) {
- candidates.push({
- code: "blocked_work_without_wanted_scope",
- category: "contribution_flow",
- severity: "warning",
- title: "Blocked work lacks a positive lane",
- detail: "The focus manifest blocks work areas but does not define wanted paths.",
- action: "Pair blocked areas with wanted work areas so contributors know where to focus.",
- });
- }
if (input.lane.lane === "direct_pr" && manifest.linkedIssuePolicy === "optional" && !input.settings.requireLinkedIssue) {
candidates.push({
@@ -224,7 +212,6 @@ export function buildRepoPolicyReadiness(input: RepoPolicyReadinessInput): RepoP
privateNoteCount: manifest?.maintainerNotes.length ?? 0,
manifestWarningCount: manifest?.warnings.length ?? 0,
wantedPathCount: manifest?.wantedPaths.length ?? 0,
- blockedPathCount: manifest?.blockedPaths.length ?? 0,
validationExpectationCount: manifest?.testExpectations.length ?? 0,
queueLevel: input.queueHealth.level,
contributorIntakeLevel: input.contributorIntakeHealth.level,
diff --git a/src/types.ts b/src/types.ts
index 0e9a8ec78f..a4ae118561 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -639,8 +639,9 @@ export type RepositorySettings = {
premergeContentRecheck?: boolean | undefined;
/** Merge-readiness gate (#merge-readiness). `off`/`advisory`/`block`. No min-score. Default `off`. */
mergeReadinessGateMode: GateRuleMode;
- /** Focus-manifest policy gate (#555). When `block`, the focus manifest's declared policy (blocked paths,
- * required-linked-issue, test expectations) becomes an enforceable review-agent blocker. An
+ /** Focus-manifest policy gate (#555). When `block`, the focus manifest's declared policy (required-linked
+ * issue and test expectations) becomes an enforceable review-agent blocker. Path-based manual-review holds
+ * are configured separately through `settings.hardGuardrailGlobs`. An
* INDEPENDENT dimension, deliberately not folded into the merge-readiness composite. Default `off` — opt-in. */
manifestPolicyGateMode: GateRuleMode;
/** Self-authored linked-issue gate. When `block`, the gate closes a PR where the contributor also
diff --git a/test/fixtures/engine-parity/predicted-gate/README.md b/test/fixtures/engine-parity/predicted-gate/README.md
index 63f33be521..674e4627c1 100644
--- a/test/fixtures/engine-parity/predicted-gate/README.md
+++ b/test/fixtures/engine-parity/predicted-gate/README.md
@@ -10,7 +10,7 @@ surface it should produce today. The follow-up parity runner can reuse the same
| `clean-pass-oss-anti-slop.ts` | Clean pass under `oss-anti-slop`, including the public funnel | `success` | none |
| `duplicate-pr-block.ts` | Open sibling PR sharing the linked issue | `failure` | `duplicate_pr_risk` |
| `missing-linked-issue-block.ts` | `linkedIssue:block` with no linked issue in the body or metadata | `failure` | `missing_linked_issue` |
-| `manifest-blocked-path.ts` | `manifestPolicy:block` with a changed path that hits `blockedPaths` | `neutral` | `manifest_blocked_path` |
+| `manifest-blocked-path.ts` | Legacy `blockedPaths` with `manifestPolicy:block` are ignored | `success` | none |
| `readiness-warning.ts` | `gate.readiness.mode=advisory` with a threshold above the public readiness score | `success` | `readiness_score_below_threshold` |
| `path-gated-check-with-paths.ts` | Enforced `review.pre_merge_checks` rule once matching `changedPaths` are supplied | `failure` | `pre_merge_check_required` |
| `path-gated-check-without-paths.ts` | The same path-gated pre-merge rule before `changedPaths` are known | `success` | none |
diff --git a/test/fixtures/engine-parity/predicted-gate/manifest-blocked-path.ts b/test/fixtures/engine-parity/predicted-gate/manifest-blocked-path.ts
index 99f0a3e12a..16cfab8b00 100644
--- a/test/fixtures/engine-parity/predicted-gate/manifest-blocked-path.ts
+++ b/test/fixtures/engine-parity/predicted-gate/manifest-blocked-path.ts
@@ -1,10 +1,11 @@
import { BASE_INPUT, BASE_REPO, definePredictedGateFixture, openIssue, parseManifest } from "./_shared";
-// Focus-manifest policy branch: a changed path hits a blocked glob and manifestPolicy:block downgrades to HOLD.
+// Legacy focus-manifest blockedPaths are inert. Path holds are configured through
+// settings.hardGuardrailGlobs, not manifestPolicy.
export default definePredictedGateFixture({
id: "manifest-blocked-path",
- title: "Blocked manifest path yields a neutral hold",
- branch: "manifest_blocked_path with changedPaths supplied and manifestPolicy:block",
+ title: "Legacy blocked manifest path is ignored",
+ branch: "legacy blockedPaths with changedPaths supplied and manifestPolicy:block",
input: BASE_INPUT,
manifest: parseManifest({ gate: { manifestPolicy: "block" }, blockedPaths: ["dist/**"] }),
repo: BASE_REPO,
@@ -12,10 +13,10 @@ export default definePredictedGateFixture({
pullRequests: [],
changedPaths: ["dist/bundle.js"],
expected: {
- conclusion: "neutral",
+ conclusion: "success",
pack: "gittensor",
blockerCodes: [],
- warningCodes: ["manifest_blocked_path"],
+ warningCodes: [],
funnelPresent: false,
noteExcludes: ["Provide the PR's changed paths"],
},
diff --git a/test/integration/api.test.ts b/test/integration/api.test.ts
index 24d0827782..d614034590 100644
--- a/test/integration/api.test.ts
+++ b/test/integration/api.test.ts
@@ -5749,7 +5749,6 @@ describe("api routes", () => {
});
await upsertRepoFocusManifest(env, "entrius/allways-ui", {
- blockedPaths: ["dist/"],
linkedIssuePolicy: "optional",
issueDiscoveryPolicy: "discouraged",
maintainerNotes: [
@@ -5767,14 +5766,12 @@ describe("api routes", () => {
previewOnly: true,
present: true,
publicWarnings: expect.arrayContaining([
- expect.objectContaining({ code: "blocked_work_without_wanted_scope" }),
+ expect.objectContaining({ code: "contribution_scope_unclear" }),
expect.objectContaining({ code: "linked_issue_policy_mismatch" }),
expect.objectContaining({ code: "validation_expectations_missing" }),
]),
},
- warnings: expect.arrayContaining([
- expect.stringContaining("Blocked work lacks a positive lane"),
- ]),
+ warnings: expect.arrayContaining([expect.stringContaining("Contribution scope is unclear")]),
});
expect(policyPayload.policyReadiness).not.toHaveProperty("ownerContext");
expect(JSON.stringify(policyPayload.policyReadiness.publicWarnings)).not.toMatch(FORBIDDEN_PUBLIC_REPORT_TERMS);
@@ -6292,7 +6289,6 @@ describe("api routes", () => {
headers: internalHeaders(env),
body: JSON.stringify({
wantedPaths: ["src/"],
- blockedPaths: ["dist/"],
preferredLabels: ["bug"],
linkedIssuePolicy: "required",
issueDiscoveryPolicy: "discouraged",
@@ -6315,7 +6311,6 @@ describe("api routes", () => {
present: true,
source: "api_record",
wantedPaths: ["src/"],
- blockedPaths: ["dist/"],
maintainerNotes: [privateNote],
},
policy: {
@@ -6328,7 +6323,7 @@ describe("api routes", () => {
id: "direct-pr",
preference: "preferred",
preferredPaths: ["src/"],
- discouragedPaths: ["dist/"],
+ discouragedPaths: [],
validationExpectations: ["Run npm run test:ci."],
publicNotes: ["Prefer small, focused PRs."],
}),
@@ -6336,7 +6331,7 @@ describe("api routes", () => {
id: "issue-discovery",
preference: "discouraged",
preferredPaths: [],
- discouragedPaths: ["dist/"],
+ discouragedPaths: [],
}),
],
labelPolicy: { preferredLabels: ["bug"], required: true },
diff --git a/test/unit/contributor-issue-draft.test.ts b/test/unit/contributor-issue-draft.test.ts
index fe5387b318..81173d3742 100644
--- a/test/unit/contributor-issue-draft.test.ts
+++ b/test/unit/contributor-issue-draft.test.ts
@@ -25,7 +25,6 @@ const FORBIDDEN = /wallet|hotkey|raw trust score|payout|reward estimate|farming|
const GITTENSORY_MANIFEST = parseFocusManifestContent(
JSON.stringify({
wantedPaths: ["src/", "apps/gittensory-ui/", "packages/gittensory-mcp/"],
- blockedPaths: ["site/", "CNAME", "**/lovable/**"],
testExpectations: ["npm run test:ci"],
publicNotes: ["Stay advisory."],
linkedIssuePolicy: "required",
@@ -167,7 +166,6 @@ describe("contributor issue drafts", () => {
const manifest = {
...GITTENSORY_MANIFEST,
wantedPaths: ["src/unique-path-119/"],
- blockedPaths: [],
testExpectations: ["npm run test:ci"],
};
const repo = { fullName: "JSONbored/gittensory", isRegistered: true } as never;
@@ -425,7 +423,7 @@ describe("contributor issue drafts", () => {
upstreamDriftWarnings: [],
};
const manifest = parseFocusManifestContent(
- '{"wantedPaths":["src/"],"blockedPaths":["dist/"],"testExpectations":["npm run test:ci"],"issueDiscoveryPolicy":"discouraged","linkedIssuePolicy":"optional"}',
+ '{"wantedPaths":["src/"],"testExpectations":["npm run test:ci"],"issueDiscoveryPolicy":"discouraged","linkedIssuePolicy":"optional"}',
"repo_file",
);
const candidates = buildContributorIssueDraftCandidates({ ...base, focusManifest: manifest });
diff --git a/test/unit/decision-pack.test.ts b/test/unit/decision-pack.test.ts
index 9939d654e0..7e37c89543 100644
--- a/test/unit/decision-pack.test.ts
+++ b/test/unit/decision-pack.test.ts
@@ -1681,7 +1681,6 @@ describe("decision-pack service", () => {
const manifest = parseFocusManifest({
source: "repo_file",
wantedPaths: ["src/"],
- blockedPaths: ["migrations/"],
preferredLabels: ["bug"],
linkedIssuePolicy: "required",
issueDiscoveryPolicy: "discouraged",
@@ -1703,11 +1702,10 @@ describe("decision-pack service", () => {
linkedIssuePolicy: "required",
issueDiscoveryPolicy: "discouraged",
wantedPathCount: 1,
- blockedPathCount: 1,
preferredLabels: ["bug"],
publicNotes: ["Prefer small, focused PRs."],
});
- expect(decision.riskReasons.join(" ")).toMatch(/maintainer focus manifest blocks/i);
+ expect(decision.riskReasons.join(" ")).not.toMatch(/blocked path|blocked area/i);
expect(decision.whyThisHelps.join(" ")).toMatch(/wanted path/i);
expect(decision.publicNextActions.join(" ")).toMatch(/maintainer requires linked issues/i);
expect(decision.publicNextActions.join(" ")).toMatch(/Prefer small, focused PRs/);
@@ -1747,7 +1745,7 @@ describe("decision-pack service", () => {
const { parseFocusManifest } = await import("../../src/signals/focus-manifest");
const manifest = parseFocusManifest({
source: "api_record",
- blockedPaths: ["migrations/"],
+ publicNotes: ["Keep PRs small."],
linkedIssuePolicy: "optional",
issueDiscoveryPolicy: "neutral",
});
@@ -1776,7 +1774,6 @@ describe("decision-pack service", () => {
present: true,
source: "repo_file",
wantedPaths: ["src/"],
- blockedPaths: [],
preferredLabels: [],
linkedIssuePolicy: "optional",
testExpectations: [],
diff --git a/test/unit/focus-manifest-loader.test.ts b/test/unit/focus-manifest-loader.test.ts
index 9bc3805851..df57f6f772 100644
--- a/test/unit/focus-manifest-loader.test.ts
+++ b/test/unit/focus-manifest-loader.test.ts
@@ -271,13 +271,13 @@ describe("focus-manifest loader", () => {
return repoFullName === "owner/a"
? JSON.stringify({ wantedPaths: ["src/"] })
: repoFullName === "owner/b"
- ? JSON.stringify({ blockedPaths: ["dist/"] })
+ ? JSON.stringify({ preferredLabels: ["feature"] })
: null;
};
const repos = ["owner/a", "owner/b", "owner/c", "owner/d", "owner/e", "owner/f"];
const map = await loadRepoFocusManifests(env, repos, { fetcher });
expect(map.get("owner/a")?.wantedPaths).toEqual(["src/"]);
- expect(map.get("owner/b")?.blockedPaths).toEqual(["dist/"]);
+ expect(map.get("owner/b")?.preferredLabels).toEqual(["feature"]);
expect(map.get("owner/c")?.present).toBe(false);
expect(maxActive).toBeGreaterThan(1);
expect(maxActive).toBeLessThanOrEqual(REPO_FOCUS_MANIFEST_MAX_CONCURRENT_LOADS);
@@ -351,10 +351,10 @@ describe("focus-manifest loader", () => {
vi.spyOn(globalThis, "fetch").mockImplementation(async () => {
call += 1;
if (call === 1) throw new Error("network down");
- return new Response('{"blockedPaths":["dist/"]}', { status: 200 });
+ return new Response('{"wantedPaths":["src/"]}', { status: 200 });
});
const text = await fetchRepoFocusManifestFile("owner/repo");
- expect(text).toBe('{"blockedPaths":["dist/"]}');
+ expect(text).toBe('{"wantedPaths":["src/"]}');
});
it("exposes a reasonable default max-age", () => {
diff --git a/test/unit/focus-manifest.test.ts b/test/unit/focus-manifest.test.ts
index 5d3a3e5ad8..0d0bc404b7 100644
--- a/test/unit/focus-manifest.test.ts
+++ b/test/unit/focus-manifest.test.ts
@@ -43,7 +43,6 @@ describe("parseFocusManifest", () => {
present: true,
source: "repo_file",
wantedPaths: ["src/", "packages/*/lib"],
- blockedPaths: ["migrations/", "infra/secrets.tf"],
preferredLabels: ["bug", "good first issue"],
linkedIssuePolicy: "required",
issueDiscoveryPolicy: "discouraged",
@@ -77,7 +76,6 @@ describe("parseFocusManifest", () => {
issueDiscoveryPolicy: 7,
});
expect(manifest.wantedPaths).toEqual([]);
- expect(manifest.blockedPaths).toEqual(["ok"]);
expect(manifest.preferredLabels[0]).toHaveLength(300);
expect(manifest.linkedIssuePolicy).toBe("optional");
expect(manifest.issueDiscoveryPolicy).toBe("neutral");
@@ -140,7 +138,6 @@ describe("parseFocusManifestContent", () => {
const manifest = parseFocusManifestContent(JSON.stringify(FULL_MANIFEST));
expect(manifest.present).toBe(true);
expect(manifest.source).toBe("repo_file");
- expect(manifest.blockedPaths).toContain("migrations/");
});
it("warns instead of throwing on malformed JSON", () => {
@@ -161,7 +158,6 @@ describe("parseFocusManifestContent", () => {
const manifest = parseFocusManifestContent("wantedPaths:\n - src/\nblockedPaths:\n - dist/\n", "repo_file");
expect(manifest.present).toBe(true);
expect(manifest.wantedPaths).toEqual(["src/"]);
- expect(manifest.blockedPaths).toEqual(["dist/"]);
});
it("warns instead of throwing on malformed YAML", () => {
@@ -297,13 +293,11 @@ describe("buildFocusManifestGuidance", () => {
expect(guidance.publicNextSteps).toEqual([]);
});
- it("flags a critical blocked-path finding and public next step", () => {
+ it("ignores legacy blockedPaths for review guidance and manual holds", () => {
const guidance = buildFocusManifestGuidance({ manifest: wanted, changedPaths: ["migrations/0099_x.sql"] });
- const blocked = guidance.findings.find((finding) => finding.code === "manifest_blocked_path");
- expect(blocked?.severity).toBe("critical");
- expect(guidance.matchedBlockedPaths).toEqual(["migrations/"]);
- expect(guidance.publicNextSteps.join(" ")).toMatch(/maintainer-blocked/i);
- expect(guidance.summary).toMatch(/blocked area/i);
+ expect(guidance.findings.map((finding) => finding.code)).not.toContain("manifest_malformed");
+ expect(guidance.publicNextSteps.join(" ")).not.toMatch(/blocked|guarded/i);
+ expect(guidance.summary).toMatch(/outside the wanted areas/i);
});
it("recommends preferred paths when the change is in a wanted area", () => {
@@ -461,10 +455,10 @@ describe("compileFocusManifestPolicy", () => {
expect(policy.publicSafe.issueDiscoveryPolicy).toBe("encouraged");
});
- it("handles a manifest with only blockedPaths set", () => {
+ it("treats legacy blockedPaths-only manifests as absent", () => {
const policy = compileFocusManifestPolicy(REPO, parseFocusManifest({ blockedPaths: ["infra/"] }), opts);
- expect(policy.present).toBe(true);
- expect(policy.publicSafe.readinessWarnings.join(" ")).toMatch(/blocked area|pair blocked/i);
+ expect(policy.present).toBe(false);
+ expect(policy.publicSafe.readinessWarnings).toEqual([]);
});
it("emits a readiness warning when no wanted paths or preferred labels are declared", () => {
@@ -472,7 +466,7 @@ describe("compileFocusManifestPolicy", () => {
expect(policy.publicSafe.readinessWarnings.join(" ")).toMatch(/does not define wanted paths|contribution scope may be unclear/i);
});
- it("emits a readiness warning when blocked paths exist but no wanted paths are declared", () => {
+ it("emits a readiness warning when linked issue policy exists but no wanted paths are declared", () => {
const policy = compileFocusManifestPolicy(REPO, parseFocusManifest({ linkedIssuePolicy: "required" }), opts);
expect(policy.publicSafe.readinessWarnings.join(" ")).toMatch(/does not define wanted paths|contribution scope/i);
});
@@ -509,7 +503,6 @@ describe("compileFocusManifestPolicy", () => {
present: true,
source: "api_record",
wantedPaths: ["src/"],
- blockedPaths: [],
preferredLabels: [],
linkedIssuePolicy: "optional",
testExpectations: [],
@@ -652,10 +645,10 @@ describe("deriveContributionLanes", () => {
expect(lanes.issueEntryGuidance).toContain("Issues must be linked to a PR before it is opened.");
});
- it("includes blocked paths in discouragedEntryPaths and PR entry guidance", () => {
+ it("ignores legacy blocked paths in discouragedEntryPaths and PR entry guidance", () => {
const lanes = deriveContributionLanes(parseFocusManifest({ wantedPaths: ["src/"], blockedPaths: ["migrations/", "infra/secrets.tf"] }));
- expect(lanes.discouragedEntryPaths).toEqual(["migrations/", "infra/secrets.tf"]);
- expect(lanes.prEntryGuidance.join(" ")).toMatch(/migrations\/.*infra\/secrets\.tf|infra\/secrets\.tf.*migrations\//);
+ expect(lanes.discouragedEntryPaths).toEqual([]);
+ expect(lanes.prEntryGuidance.join(" ")).not.toMatch(/migrations\/|infra\/secrets\.tf/);
});
it("includes preferred labels in PR entry guidance", () => {
@@ -717,7 +710,7 @@ describe("deriveContributionLanes", () => {
expect(lanes.directPrLane).toBe("preferred");
expect(lanes.issueDiscoveryLane).toBe("discouraged");
expect(lanes.preferredEntryPaths).toContain("src/");
- expect(lanes.discouragedEntryPaths).toContain("migrations/");
+ expect(lanes.discouragedEntryPaths).toEqual([]);
expect(lanes.validationExpectations).toContain("Link a tracked issue before opening a PR.");
expect(lanes.validationExpectations).toContain("unit tests for new branches");
expect(lanes.issueEntryGuidance.join(" ")).toMatch(/discourages/i);
diff --git a/test/unit/gate-check-policy.test.ts b/test/unit/gate-check-policy.test.ts
index afb9728164..6ef3b5f927 100644
--- a/test/unit/gate-check-policy.test.ts
+++ b/test/unit/gate-check-policy.test.ts
@@ -597,9 +597,9 @@ describe("review-thread blocker gate", () => {
});
describe("focus-manifest policy gate (#555)", () => {
- // The three enforceable manifest-policy findings buildFocusManifestGuidance emits.
+ // The manifest-policy findings buildFocusManifestGuidance emits and the gate can enforce.
+ // Path-based manual review lives in settings.hardGuardrailGlobs, not manifest policy.
const POLICY_FINDINGS = {
- manifest_blocked_path: { code: "manifest_blocked_path", title: "Change touches a maintainer-blocked area", severity: "critical" as const, detail: "Changed paths match maintainer-blocked patterns.", action: "Move out of the blocked area." },
manifest_linked_issue_required: { code: "manifest_linked_issue_required", title: "Maintainer requires a linked issue", severity: "warning" as const, detail: "Manifest requires a linked issue.", action: "Link the issue." },
manifest_missing_tests: { code: "manifest_missing_tests", title: "Maintainer test expectations unmet", severity: "warning" as const, detail: "Manifest expects test evidence.", action: "Add tests." },
};
@@ -632,60 +632,38 @@ describe("focus-manifest policy gate (#555)", () => {
});
}
- describe("manifest_blocked_path", () => {
- it("holds for manual review when manifestPolicy: block", () => {
- const result = evaluateGateCheck(manifestAdvisory("manifest_blocked_path"), { manifestPolicyGateMode: "block", confirmedContributor: true });
- expect(result.conclusion).toBe("neutral");
- expect(result.blockers).toEqual([]);
- expect(result.warnings.map((finding) => finding.code)).toContain("manifest_blocked_path");
- expect(result.summary).toMatch(/held for manual review/i);
- });
-
- it("preserves public blocked-path context on the manual-review hold warning", () => {
- const advisory = manifestAdvisory("manifest_blocked_path");
- advisory.findings[0] = {
- ...advisory.findings[0]!,
- publicText: "Matched guarded paths: .github/workflows/**.",
- };
- const result = evaluateGateCheck(advisory, {
- manifestPolicyGateMode: "block",
- confirmedContributor: true,
- });
- const hold = result.warnings.find(
- (finding) => finding.code === "manifest_blocked_path",
- );
- expect(result.conclusion).toBe("neutral");
- expect(hold?.publicText).toBe("Matched guarded paths: .github/workflows/**.");
- });
-
- it("also holds non-confirmed contributors for manual review instead of closing", () => {
- const result = evaluateGateCheck(manifestAdvisory("manifest_blocked_path"), { manifestPolicyGateMode: "block", confirmedContributor: false });
- expect(result.conclusion).toBe("neutral");
- expect(result.blockers).toEqual([]);
- });
-
- it("does not block when manifestPolicy: off/advisory", () => {
- expect(evaluateGateCheck(manifestAdvisory("manifest_blocked_path"), { manifestPolicyGateMode: "off", confirmedContributor: true }).conclusion).toBe("success");
- expect(evaluateGateCheck(manifestAdvisory("manifest_blocked_path"), { manifestPolicyGateMode: "advisory", confirmedContributor: true }).conclusion).toBe("success");
- });
+ it("ignores legacy manifest_blocked_path findings even when manifestPolicy:block is enabled", () => {
+ const advisory: Advisory = {
+ ...missingIssueAdvisory(),
+ findings: [{
+ code: "manifest_blocked_path",
+ title: "Change touches a maintainer-blocked area",
+ severity: "critical",
+ detail: "Changed paths match maintainer-blocked patterns.",
+ action: "Move out of the blocked area.",
+ }],
+ };
+ const result = evaluateGateCheck(advisory, { manifestPolicyGateMode: "block", confirmedContributor: true });
+ expect(result.conclusion).toBe("success");
+ expect(result.blockers).toEqual([]);
+ expect(result.warnings.map((finding) => finding.code)).not.toContain("manifest_blocked_path");
});
it("is an INDEPENDENT dimension: mergeReadiness: block does NOT promote a manifest-policy finding (kept out of the composite)", () => {
const eff = resolveEffectiveSettings(settings({ manifestPolicyGateMode: "off", mergeReadinessGateMode: "block" }), parseFocusManifest(null));
- expect(evaluateGateCheck(manifestAdvisory("manifest_blocked_path"), gateCheckPolicy(eff, null, true)).conclusion).toBe("success");
+ expect(evaluateGateCheck(manifestAdvisory("manifest_missing_tests"), gateCheckPolicy(eff, null, true)).conclusion).toBe("success");
});
it("gateCheckPolicy threads manifestPolicyGateMode into the policy", () => {
expect(gateCheckPolicy(settings({ manifestPolicyGateMode: "block" }), null, true).manifestPolicyGateMode).toBe("block");
});
- it("end-to-end: a manifest gate.manifestPolicy: block sets effective.manifestPolicyGateMode and holds a blockedPath PR", () => {
+ it("end-to-end: a manifest gate.manifestPolicy: block sets effective.manifestPolicyGateMode and blocks enforceable manifest policy", () => {
const eff = resolveEffectiveSettings(settings({ manifestPolicyGateMode: "off" }), parseFocusManifest({ gate: { manifestPolicy: "block" } }));
expect(eff.manifestPolicyGateMode).toBe("block");
- const result = evaluateGateCheck(manifestAdvisory("manifest_blocked_path"), gateCheckPolicy(eff, null, true));
- expect(result.conclusion).toBe("neutral");
- expect(result.blockers).toEqual([]);
- expect(result.warnings.map((finding) => finding.code)).toContain("manifest_blocked_path");
+ const result = evaluateGateCheck(manifestAdvisory("manifest_missing_tests"), gateCheckPolicy(eff, null, true));
+ expect(result.conclusion).toBe("failure");
+ expect(result.blockers.map((finding) => finding.code)).toContain("manifest_missing_tests");
});
});
diff --git a/test/unit/gittensory-focus-manifest.test.ts b/test/unit/gittensory-focus-manifest.test.ts
index 83ba243659..2ceeb1649a 100644
--- a/test/unit/gittensory-focus-manifest.test.ts
+++ b/test/unit/gittensory-focus-manifest.test.ts
@@ -43,8 +43,6 @@ describe("Gittensory repo focus manifest", () => {
expect(manifest.wantedPaths).toContain("src/");
expect(manifest.wantedPaths).toContain("review-enrichment/");
expect(manifest.wantedPaths).toContain("apps/gittensory-ui/");
- expect(manifest.blockedPaths).toContain("site/");
- expect(manifest.blockedPaths).not.toContain("apps/gittensory-ui/");
expect(manifest.issueDiscoveryPolicy).toBe("discouraged");
const policy = compileFocusManifestPolicy(manifest);
@@ -61,14 +59,14 @@ describe("Gittensory repo focus manifest", () => {
expect(manifest.warnings.join(" ")).toMatch(/not valid YAML/i);
});
- it("blocked path fixture flags retired static-site surfaces", () => {
+ it("retired static-site surfaces no longer create manifest path holds", () => {
const manifest = parseFocusManifestContent(GITTENSORY_REPO_FOCUS_MANIFEST_YAML, "repo_file");
const guidance = buildFocusManifestGuidance({
manifest,
changedPaths: ["site/docs/index.html"],
});
- expect(guidance.findings.some((finding) => finding.code === "manifest_blocked_path")).toBe(true);
- expect(guidance.summary).toMatch(/blocked area/i);
+ expect(guidance.findings.map((finding) => finding.code)).not.toContain("manifest_malformed");
+ expect(guidance.summary).toMatch(/outside the wanted areas/i);
});
it("focused control-panel UI changes align with wanted paths", () => {
@@ -78,17 +76,17 @@ describe("Gittensory repo focus manifest", () => {
changedPaths: ["apps/gittensory-ui/src/routes/app.operator.tsx"],
});
expect(guidance.findings.some((finding) => finding.code === "manifest_preferred_path")).toBe(true);
- expect(guidance.findings.some((finding) => finding.code === "manifest_blocked_path")).toBe(false);
});
- it("recommendation influence prefers in-scope backend and UI paths separately from blocked surfaces", () => {
+ it("recommendation influence prefers in-scope backend and UI paths without legacy blocked surfaces", () => {
const manifest = parseFocusManifestContent(GITTENSORY_REPO_FOCUS_MANIFEST_YAML, "repo_file");
const backend = buildFocusManifestGuidance({ manifest, changedPaths: ["src/api/routes.ts"] });
const controlPanel = buildFocusManifestGuidance({ manifest, changedPaths: ["apps/gittensory-ui/src/app.tsx"] });
const retiredSite = buildFocusManifestGuidance({ manifest, changedPaths: ["site/index.html"] });
expect(backend.findings.some((finding) => finding.code === "manifest_preferred_path")).toBe(true);
expect(controlPanel.findings.some((finding) => finding.code === "manifest_preferred_path")).toBe(true);
- expect(retiredSite.findings.some((finding) => finding.code === "manifest_blocked_path")).toBe(true);
+ expect(retiredSite.findings.map((finding) => finding.code)).not.toContain("manifest_malformed");
+ expect(retiredSite.summary).toMatch(/outside the wanted areas/i);
});
it("public/private boundary regression keeps maintainer notes out of public guidance", () => {
@@ -107,14 +105,14 @@ describe("Gittensory repo focus manifest", () => {
expect(manifest.present).toBe(true);
expect(manifest.wantedPaths).toContain("packages/");
expect(manifest.wantedPaths).toContain("apps/gittensory-ui/");
- expect(manifest.blockedPaths).toContain("site/");
});
- it("flags lovable-only and CNAME paths as blocked surfaces", () => {
+ it("does not treat legacy lovable-only and CNAME paths as manifest holds", () => {
const manifest = parseFocusManifestContent(GITTENSORY_REPO_FOCUS_MANIFEST_YAML, "repo_file");
for (const changedPath of ["CNAME", "vendor/lovable/widget.ts"]) {
const guidance = buildFocusManifestGuidance({ manifest, changedPaths: [changedPath] });
- expect(guidance.findings.some((finding) => finding.code === "manifest_blocked_path")).toBe(true);
+ expect(guidance.findings.map((finding) => finding.code)).not.toContain("manifest_malformed");
+ expect(guidance.summary).toMatch(/outside the wanted areas/i);
}
});
diff --git a/test/unit/local-branch.test.ts b/test/unit/local-branch.test.ts
index 854fd5efbe..d28c1b9ad1 100644
--- a/test/unit/local-branch.test.ts
+++ b/test/unit/local-branch.test.ts
@@ -1561,7 +1561,7 @@ describe("local branch analysis", () => {
expect(JSON.stringify(analysis.prPacket)).not.toMatch(/reward|score|wallet|hotkey|farming|payout|ranking|trust score/i);
});
- it("treats a maintainer-blocked path as a branch-quality blocker", () => {
+ it("ignores legacy focus-manifest blockedPaths during branch-quality analysis", () => {
const analysis = buildLocalBranchAnalysis({
input: {
login: "oktofeesh1",
@@ -1580,9 +1580,9 @@ describe("local branch analysis", () => {
scoringProfile,
});
- expect(analysis.manifestGuidance.matchedBlockedPaths).toEqual(["migrations/"]);
- expect(analysis.localFindings).toEqual(expect.arrayContaining([expect.objectContaining({ code: "manifest_blocked_path", severity: "critical" })]));
- expect(analysis.branchQualityBlockers).toEqual(expect.arrayContaining([expect.stringContaining("maintainer-blocked area")]));
+ expect(analysis.manifestGuidance.present).toBe(false);
+ expect(analysis.localFindings.map((finding) => finding.code)).not.toContain("manifest_blocked_path");
+ expect(analysis.branchQualityBlockers.join(" ")).not.toMatch(/maintainer-blocked|blocked path|guarded path/i);
expect(JSON.stringify(analysis.prPacket)).not.toMatch(/reward|score|wallet|hotkey|farming|payout|ranking|trust score/i);
});
diff --git a/test/unit/mcp-predict-gate.test.ts b/test/unit/mcp-predict-gate.test.ts
index bdc2a58731..dcb51f7fc0 100644
--- a/test/unit/mcp-predict-gate.test.ts
+++ b/test/unit/mcp-predict-gate.test.ts
@@ -60,10 +60,10 @@ describe("MCP gittensory_predict_gate", () => {
expect(JSON.stringify(result.content)).toContain("Too big");
});
- it("predicts the focus-manifest path policy when changedPaths are supplied (#11-13/#18)", async () => {
+ it("ignores legacy focus-manifest blockedPaths when changedPaths are supplied (#11-13/#18)", async () => {
const env = createTestEnv();
await upsertRepositoryFromGitHub(env, { name: "widgets", full_name: "acme/widgets" });
- // Public config: oss-anti-slop (no account needed), manifest path policy in block mode, dist/** blocked.
+ // Public config: oss-anti-slop (no account needed), manifest policy in block mode, legacy dist/** blocked.
// Stored as a PUBLIC repo_file manifest — predict_gate reads only public config (#selfhost-app-id / #1405).
await upsertRepoFocusManifest(env, "acme/widgets", { gate: { pack: "oss-anti-slop", manifestPolicy: "block" }, blockedPaths: ["dist/**"] }, "repo_file");
const client = await connect(env);
@@ -74,9 +74,9 @@ describe("MCP gittensory_predict_gate", () => {
});
expect(result.isError).toBeFalsy();
const data = result.structuredContent as { conclusion: string; blockers: Array<{ code: string }>; warnings: Array<{ code: string }>; note: string };
- expect(data.conclusion).toBe("neutral");
+ expect(data.conclusion).toBe("success");
expect(data.blockers.some((b) => b.code === "manifest_blocked_path")).toBe(false);
- expect(data.warnings.some((w) => w.code === "manifest_blocked_path")).toBe(true);
+ expect(data.warnings.some((w) => w.code === "manifest_blocked_path")).toBe(false);
// With paths supplied the note drops the "provide changed paths" disclaimer but still disclaims slop.
expect(data.note).not.toContain("Provide the PR's changed paths");
expect(data.note.toLowerCase()).toContain("slop");
diff --git a/test/unit/miner-dashboard-recommendations.test.ts b/test/unit/miner-dashboard-recommendations.test.ts
index 2efd5544d7..8ba2f2b5d4 100644
--- a/test/unit/miner-dashboard-recommendations.test.ts
+++ b/test/unit/miner-dashboard-recommendations.test.ts
@@ -24,7 +24,6 @@ describe("miner dashboard recommendation metadata", () => {
linkedIssuePolicy: "optional",
issueDiscoveryPolicy: "allowed",
wantedPathCount: 1,
- blockedPathCount: 0,
},
}),
],
@@ -44,7 +43,6 @@ describe("miner dashboard recommendation metadata", () => {
linkedIssuePolicy: "required",
issueDiscoveryPolicy: "restricted",
wantedPathCount: 2,
- blockedPathCount: 1,
},
}),
],
@@ -93,7 +91,7 @@ describe("miner dashboard recommendation metadata", () => {
queue: { openPullRequests: 0, openIssues: 1, mergedPullRequests: 0, closedUnmergedPullRequests: 0 },
outcome: { openPullRequests: 0, mergedPullRequests: 1, closedPullRequests: 0 },
scoreBlockers: [],
- manifestSummary: { linkedIssuePolicy: "optional", issueDiscoveryPolicy: "allowed", wantedPathCount: 1, blockedPathCount: 0 },
+ manifestSummary: { linkedIssuePolicy: "optional", issueDiscoveryPolicy: "allowed", wantedPathCount: 1 },
}),
],
topActions: [action({ actionKind: "file_issue_discovery", lane: "issue-discovery", recommendation: "watch", priorityScore: 35 })],
@@ -108,7 +106,7 @@ describe("miner dashboard recommendation metadata", () => {
queue: { openPullRequests: 3, openIssues: 4, mergedPullRequests: 1, closedUnmergedPullRequests: 0 },
outcome: { openPullRequests: 2, mergedPullRequests: 0, closedPullRequests: 1 },
scoreBlockers: [{ code: "open_pr_pressure" }],
- manifestSummary: { linkedIssuePolicy: "required", issueDiscoveryPolicy: "restricted", wantedPathCount: 2, blockedPathCount: 1 },
+ manifestSummary: { linkedIssuePolicy: "required", issueDiscoveryPolicy: "restricted", wantedPathCount: 2 },
}),
],
topActions: [action({ actionKind: "open_new_direct_pr", lane: "direct-pr", recommendation: "pursue", priorityScore: 82 })],
@@ -227,7 +225,6 @@ describe("miner dashboard recommendation metadata", () => {
linkedIssuePolicy: "required",
issueDiscoveryPolicy: "restricted",
wantedPathCount: 2,
- blockedPathCount: 1,
},
},
],
@@ -246,7 +243,7 @@ describe("miner dashboard recommendation metadata", () => {
expect.objectContaining({ label: "Action changed", before: "file_issue_discovery", after: "open_new_direct_pr" }),
expect.objectContaining({ label: "Recommendation changed", before: "watch", after: "pursue" }),
expect.objectContaining({ label: "Priority bucket changed", before: "medium", after: "none" }),
- expect.objectContaining({ label: "Repo policy changed", before: "unknown/unknown/0 wanted/0 blocked", after: "required/restricted/2 wanted/1 blocked" }),
+ expect.objectContaining({ label: "Repo policy changed", before: "unknown/unknown/0 wanted", after: "required/restricted/2 wanted" }),
]),
);
expect(repoWithoutName?.change.status).toBe("new");
@@ -421,7 +418,6 @@ function repoDecision(overrides: Record = {}): Record {
- for (const code of ["guardrail_hold", "oversized_pr", "manifest_blocked_path", "ai_review_inconclusive", "repo_not_registered", "repo_not_seen", "pr_not_cached", "pre_merge_check_unresolved", "cla_check_unresolved"]) {
+ it("returns the recognized code for each known neutral-hold class (guardrail, size, ai-inconclusive, sync-state)", () => {
+ for (const code of ["guardrail_hold", "oversized_pr", "ai_review_inconclusive", "repo_not_registered", "repo_not_seen", "pr_not_cached", "pre_merge_check_unresolved", "cla_check_unresolved"]) {
expect(neutralHoldReasonCode({ conclusion: "neutral", warnings: [finding(code)] })).toBe(code);
}
});
diff --git a/test/unit/policy-sanitizer.test.ts b/test/unit/policy-sanitizer.test.ts
index 7258cac915..7d2ca7c72f 100644
--- a/test/unit/policy-sanitizer.test.ts
+++ b/test/unit/policy-sanitizer.test.ts
@@ -373,10 +373,11 @@ describe("compileFocusManifestPolicy — public-safe output boundaries", () => {
expect(policy.publicSafe.readinessWarnings).toEqual([]);
});
- it("emits a readiness warning for blocked-only manifests with no wanted scope", () => {
+ it("treats legacy blocked-only manifests as absent", () => {
const manifest = parseFocusManifest({ blockedPaths: ["migrations/"], wantedPaths: [], preferredLabels: [], testExpectations: [] });
const policy = compileFocusManifestPolicy("JSONbored/gittensory", manifest, { generatedAt: FIXED_DATE });
- expect(policy.publicSafe.readinessWarnings.join(" ")).toMatch(/blocks work areas.*does not define wanted|pair blocked areas/i);
+ expect(policy.present).toBe(false);
+ expect(policy.publicSafe.readinessWarnings).toEqual([]);
expect(JSON.stringify(policy.publicSafe)).not.toMatch(FORBIDDEN_POLICY_PATTERN);
});
diff --git a/test/unit/pr-body-draft.test.ts b/test/unit/pr-body-draft.test.ts
index a17432917c..26dee7dd0d 100644
--- a/test/unit/pr-body-draft.test.ts
+++ b/test/unit/pr-body-draft.test.ts
@@ -41,7 +41,6 @@ function source(overrides: Partial = {}): PrBodyDraftSource {
linkedIssuePolicy: "optional",
issueDiscoveryPolicy: "neutral",
matchedWantedPaths: [],
- matchedBlockedPaths: [],
preferredLabelHits: [],
findings: [],
publicNextSteps: [],
diff --git a/test/unit/predicted-gate.test.ts b/test/unit/predicted-gate.test.ts
index 3d66c61d95..00ad5103ad 100644
--- a/test/unit/predicted-gate.test.ts
+++ b/test/unit/predicted-gate.test.ts
@@ -336,23 +336,22 @@ describe("buildPredictedGateVerdict", () => {
expect(result.conclusion).toBe("neutral");
});
- it("predicts a manifest path-policy HOLD when a changed path hits a blocked glob and manifestPolicy:block (#12)", () => {
+ it("ignores legacy blockedPaths even when manifestPolicy:block is enabled (#12)", () => {
const result = verdict({
gate: { manifestPolicy: "block" },
manifestExtra: { blockedPaths: ["dist/**"] },
changedPaths: ["dist/bundle.js"],
});
- expect(result.conclusion).toBe("neutral");
+ expect(result.conclusion).toBe("success");
expect(result.blockers.some((b) => b.code === "manifest_blocked_path")).toBe(false);
- expect(result.warnings.some((w) => w.code === "manifest_blocked_path")).toBe(true);
+ expect(result.warnings.some((w) => w.code === "manifest_blocked_path")).toBe(false);
// The note no longer disclaims path-policy once paths are supplied, but slop stays disclaimed.
expect(result.note).not.toContain("Provide the PR's changed paths");
expect(result.note.toLowerCase()).toContain("slop");
});
- it("manifestPolicy:advisory does NOT block on a blocked path (parity with the live advisory gate) (#12)", () => {
- // The blocked-path finding is critical, so under advisory mode it neither blocks nor surfaces as a warning —
- // exactly how the live gate treats it. The meaningful parity is that advisory never fails the prediction.
+ it("manifestPolicy:advisory does NOT block on legacy blockedPaths (parity with the live advisory gate) (#12)", () => {
+ // Path holds are configured through settings.hardGuardrailGlobs, not manifest blockedPaths.
const result = verdict({
gate: { manifestPolicy: "advisory" },
manifestExtra: { blockedPaths: ["dist/**"] },
@@ -362,7 +361,7 @@ describe("buildPredictedGateVerdict", () => {
expect(result.blockers.some((b) => b.code === "manifest_blocked_path")).toBe(false);
});
- it("manifestPolicy:off (default) emits NO manifest finding even when a blocked path is touched", () => {
+ it("manifestPolicy:off (default) emits NO manifest finding for legacy blockedPaths", () => {
const result = verdict({
gate: { manifestPolicy: "off" },
manifestExtra: { blockedPaths: ["dist/**"] },
@@ -372,9 +371,9 @@ describe("buildPredictedGateVerdict", () => {
expect(result.warnings.some((w) => w.code === "manifest_blocked_path")).toBe(false);
});
- it("ignores non-policy guidance findings (e.g. off-focus) — only the three enforceable policy codes are threaded (#12)", () => {
+ it("ignores non-policy guidance findings (e.g. off-focus) — only enforceable policy codes are threaded (#12)", () => {
// The path isn't blocked but it's outside the wanted areas → guidance emits the NON-policy `manifest_off_focus`.
- // The predictor must skip it (only manifest_blocked_path / _linked_issue_required / _missing_tests are gateable).
+ // The predictor must skip it (only linked-issue-required / missing-tests are gateable).
const result = verdict({
gate: { manifestPolicy: "block" },
manifestExtra: { wantedPaths: ["src/**"] },
diff --git a/test/unit/public-safe-manifest-finding.test.ts b/test/unit/public-safe-manifest-finding.test.ts
index 283a0446cd..d5f1971bf5 100644
--- a/test/unit/public-safe-manifest-finding.test.ts
+++ b/test/unit/public-safe-manifest-finding.test.ts
@@ -3,25 +3,8 @@ import { publicSafeManifestPolicyFinding } from "../../src/queue/processors";
import type { FocusManifestFinding } from "../../src/signals/focus-manifest";
// #1405 / #selfhost-app-id: the focus-manifest policy findings surfaced on the PUBLIC advisory must not echo the
-// maintainer's private blocked-path globs or test expectations (which can come from a container-mounted config).
+// maintainer's private test expectations (which can come from a container-mounted config).
describe("publicSafeManifestPolicyFinding", () => {
- it("redacts the private blocked-path detail to a static phrase, preserving code/severity/title", () => {
- const finding: FocusManifestFinding = {
- code: "manifest_blocked_path",
- severity: "critical",
- title: "Change touches a maintainer-blocked area",
- detail: "Changed paths match maintainer-blocked patterns: secret/private/**, internal/keys/**.",
- action: "Move this work elsewhere — secret/private/** is off-limits.",
- };
- const safe = publicSafeManifestPolicyFinding(finding);
- expect(safe.code).toBe("manifest_blocked_path");
- expect(safe.severity).toBe("critical");
- expect(safe.title).toBe("Change touches a maintainer-blocked area");
- expect(safe.detail).not.toContain("secret/private/**");
- expect(safe.action).not.toContain("secret/private/**");
- expect(safe.detail).toBe("Changed paths match maintainer-blocked areas.");
- });
-
it("redacts the private test-expectation detail to a static phrase", () => {
const finding: FocusManifestFinding = {
code: "manifest_missing_tests",
diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts
index 6462dd3001..1f5e090f48 100644
--- a/test/unit/queue.test.ts
+++ b/test/unit/queue.test.ts
@@ -6443,7 +6443,7 @@ describe("queue processors", () => {
expect(rcAudit).toBeFalsy();
});
- it("auto-maintain (#778): uses the full gate verdict so manifest-policy blockers cannot be merged", async () => {
+ it("auto-maintain (#778): uses hard guardrails so guarded paths cannot be merged", async () => {
const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() });
await persistRegistrySnapshot(
env,
@@ -6472,7 +6472,7 @@ describe("queue processors", () => {
agentDryRun: true,
});
await upsertOfficialMinerDetection(env, "contributor", { status: "confirmed", snapshot: queueMinerSnapshot("contributor") }, 60_000);
- await upsertRepoFocusManifest(env, "JSONbored/gittensory", { gate: { manifestPolicy: "block" }, blockedPaths: ["migrations/**"] });
+ await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { hardGuardrailGlobs: ["migrations/**"] } });
await upsertPullRequestFile(env, {
repoFullName: "JSONbored/gittensory",
pullNumber: 48,
@@ -6495,7 +6495,7 @@ describe("queue processors", () => {
await processJob(env, {
type: "github-webhook",
- deliveryId: "auto-maintain-manifest-block",
+ deliveryId: "auto-maintain-hard-guardrail",
eventName: "pull_request",
payload: {
action: "opened",
@@ -6516,7 +6516,7 @@ describe("queue processors", () => {
});
const mergeCount = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?").bind("agent.action.merge").first<{ n: number }>();
- expect(mergeCount?.n).toBe(0); // the manifest-policy blocker prevents the auto-merge (the key assertion)
+ expect(mergeCount?.n).toBe(0); // the hard guardrail prevents the auto-merge (the key assertion)
// The bot never posts a formal request_changes. With close NOT at an acting level here, the blocked PR is
// simply not merged (no blocking review); with close acting it would be closed.
const rcAudit = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("agent.action.request_changes").first<{ outcome: string }>();
diff --git a/test/unit/repo-policy-readiness.test.ts b/test/unit/repo-policy-readiness.test.ts
index 30567028a9..79482d9dfa 100644
--- a/test/unit/repo-policy-readiness.test.ts
+++ b/test/unit/repo-policy-readiness.test.ts
@@ -184,12 +184,11 @@ describe("buildRepoPolicyReadiness", () => {
expect(report.publicWarnings.flatMap((warning) => [warning.title, warning.detail, warning.action]).every(isFocusManifestPublicSafe)).toBe(true);
});
- it("warns when contribution scope only defines blocked work", () => {
+ it("warns when contribution scope is unclear", () => {
const report = buildRepoPolicyReadiness(
input({
settings: settings({ requireLinkedIssue: true }),
focusManifest: parseFocusManifest({
- blockedPaths: ["docs/"],
linkedIssuePolicy: "optional",
testExpectations: ["Run npm run test:ci."],
}),
@@ -198,13 +197,11 @@ describe("buildRepoPolicyReadiness", () => {
expect(report.ownerContext).toMatchObject({
wantedPathCount: 0,
- blockedPathCount: 1,
issuePolicy: "direct_pr_requires_linked_issue",
});
expect(report.publicWarnings.map((warning) => warning.code)).toEqual(
expect.arrayContaining([
"contribution_scope_unclear",
- "blocked_work_without_wanted_scope",
"linked_issue_policy_mismatch",
]),
);
@@ -412,7 +409,6 @@ describe("buildRepoPolicyReadiness", () => {
privateNoteCount: 0,
manifestWarningCount: 0,
wantedPathCount: 0,
- blockedPathCount: 0,
validationExpectationCount: 0,
issueDiscoveryPolicy: "neutral",
},
diff --git a/test/unit/routes-focus-manifest.test.ts b/test/unit/routes-focus-manifest.test.ts
index 49a6d5d76a..74aed49609 100644
--- a/test/unit/routes-focus-manifest.test.ts
+++ b/test/unit/routes-focus-manifest.test.ts
@@ -82,7 +82,7 @@ describe("focus-manifest route auth", () => {
{
method: "PUT",
headers: { cookie, "content-type": "application/json" },
- body: JSON.stringify({ wantedPaths: ["src/"], blockedPaths: ["dist/"] }),
+ body: JSON.stringify({ wantedPaths: ["src/"], preferredLabels: ["bug"] }),
},
env,
);
@@ -217,7 +217,6 @@ describe("focus-manifest route auth", () => {
manifest: {
present: true,
wantedPaths: expect.arrayContaining(["apps/gittensory-ui/"]),
- blockedPaths: expect.arrayContaining(["site/"]),
},
policy: {
present: true,
@@ -225,7 +224,7 @@ describe("focus-manifest route auth", () => {
contributionLanes: expect.arrayContaining([
expect.objectContaining({
id: "direct-pr",
- discouragedPaths: expect.arrayContaining(["site/"]),
+ discouragedPaths: [],
}),
]),
}),
@@ -305,7 +304,6 @@ describe("focus-manifest route auth", () => {
headers: apiHeaders(env),
body: JSON.stringify({
wantedPaths: ["src/"],
- blockedPaths: ["dist/"],
publicNotes: ["Keep changes focused."],
}),
},
@@ -318,7 +316,6 @@ describe("focus-manifest route auth", () => {
present: true,
source: "api_record",
wantedPaths: ["src/"],
- blockedPaths: ["dist/"],
},
});
});
diff --git a/test/unit/routes-remediation-plan.test.ts b/test/unit/routes-remediation-plan.test.ts
index 8013d8d296..9d6f1a57a7 100644
--- a/test/unit/routes-remediation-plan.test.ts
+++ b/test/unit/routes-remediation-plan.test.ts
@@ -103,7 +103,7 @@ describe("remediation-plan route", () => {
const app = createApp();
const env = createTestEnv();
await seedRepo(env, "miner", "demo", 301);
- await upsertRepoFocusManifest(env, "miner/demo", { wantedPaths: ["src/"], blockedPaths: ["dist/"] });
+ await upsertRepoFocusManifest(env, "miner/demo", { wantedPaths: ["src/"], preferredLabels: ["bug"] });
const response = await app.request(
PATH,
{
diff --git a/test/unit/selfhost-config-lint.test.ts b/test/unit/selfhost-config-lint.test.ts
index e8f0d3f9a7..fa9ce44e6d 100644
--- a/test/unit/selfhost-config-lint.test.ts
+++ b/test/unit/selfhost-config-lint.test.ts
@@ -15,7 +15,6 @@ describe("lintManifestText (#2079)", () => {
it("reports every recognized focus field without echoing values", () => {
const result = lintManifestText(`
wantedPaths: [src/private-policy/]
-blockedPaths: [dist/]
preferredLabels: [operator-only]
linkedIssuePolicy: required
testExpectations: [unit coverage]
@@ -37,10 +36,9 @@ contentLane:
expect(result.ok).toBe(true);
expect(result.warnings).toEqual([]);
- expect(result.summary).toBe("Manifest parsed 13 recognized fields.");
+ expect(result.summary).toBe("Manifest parsed 12 recognized fields.");
expect(result.recognizedFields).toEqual([
"wantedPaths",
- "blockedPaths",
"preferredLabels",
"linkedIssuePolicy",
"testExpectations",
@@ -57,6 +55,14 @@ contentLane:
expect(JSON.stringify(result)).not.toContain("operator-only");
});
+ it("flags legacy blockedPaths as an unknown top-level field", () => {
+ const result = lintManifestText("wantedPaths: [src/]\nblockedPaths: [dist/]\n");
+
+ expect(result.ok).toBe(false);
+ expect(result.recognizedFields).toEqual(["wantedPaths"]);
+ expect(result.warnings).toEqual(["Manifest contains unknown top-level field: blockedPaths."]);
+ });
+
it("flags empty or fieldless manifests as not ok", () => {
expect(lintManifestText(undefined)).toEqual({
ok: false,
diff --git a/test/unit/unified-comment.test.ts b/test/unit/unified-comment.test.ts
index 2a6729caa0..9b555addd4 100644
--- a/test/unit/unified-comment.test.ts
+++ b/test/unit/unified-comment.test.ts
@@ -330,13 +330,13 @@ describe("renderUnifiedReviewComment", () => {
...base,
decision: "manual",
recommendations: ["manual_review"],
- verdictReason: "Touches a guarded path — held for manual review; Touches a maintainer-blocked path — held for manual review",
+ verdictReason: "Touches a guarded path — held for manual review; Needs human review before automation proceeds",
},
{},
);
expect(md).toContain("**⏸️ Suggested Action - Manual Review**");
expect(md).toContain("- Touches a guarded path — held for manual review");
- expect(md).toContain("- Touches a maintainer-blocked path — held for manual review");
+ expect(md).toContain("- Needs human review before automation proceeds");
expect(md).not.toContain("Suggested Action - Manual Review — Touches");
});
From 5d4b7b1c39829014eef92c47c1296d1cf9218990 Mon Sep 17 00:00:00 2001
From: JSONbored <49853598+JSONbored@users.noreply.github.com>
Date: Sat, 4 Jul 2026 01:21:18 -0700
Subject: [PATCH 2/2] fix(selfhost): reformat prose text and improve
blockedPaths migration message
- Apply prettier's canonical formatting to the 3 doc/settings JSX text
nodes this PR edited (the only CI failure: 4 prettier/prettier errors).
- config-lint.ts: give retired blockedPaths a migration-specific warning
pointing at settings.hardGuardrailGlobs, instead of the generic
"unknown top-level field" message.
- Fix a test assertion that checked for the absent-but-unrelated
manifest_malformed code instead of the actual regression this test
guards, manifest_blocked_path no longer being emitted.
---
.../site/app-panels/maintainer-settings.tsx | 6 +++---
.../src/routes/docs.how-reviews-work.tsx | 4 ++--
apps/gittensory-ui/src/routes/docs.tuning.tsx | 5 +++--
src/selfhost/config-lint.ts | 19 +++++++++++++------
test/unit/focus-manifest.test.ts | 5 ++++-
test/unit/selfhost-config-lint.test.ts | 13 +++++++++++--
6 files changed, 36 insertions(+), 16 deletions(-)
diff --git a/apps/gittensory-ui/src/components/site/app-panels/maintainer-settings.tsx b/apps/gittensory-ui/src/components/site/app-panels/maintainer-settings.tsx
index 919f9c0b14..1919327003 100644
--- a/apps/gittensory-ui/src/components/site/app-panels/maintainer-settings.tsx
+++ b/apps/gittensory-ui/src/components/site/app-panels/maintainer-settings.tsx
@@ -766,9 +766,9 @@ function FocusManifestEditor({ base }: { base: string | null }) {
Focus manifest (config-as-code)
- The repo's maintainer focus policy as JSON — wanted paths, linked-issue policy,
- test expectations, and gate overrides. Mirrors{" "}
- .gittensory.yml.
+ The repo's maintainer focus policy as JSON — wanted paths, linked-issue policy, test
+ expectations, and gate overrides. Mirrors .gittensory.yml
+ .