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
5 changes: 0 additions & 5 deletions .gittensory.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@ wantedPaths:
- wrangler.jsonc
- apps/gittensory-ui/

blockedPaths:
- site/
- CNAME
- "**/lovable/**"

preferredLabels:
- bug
- enhancement
Expand Down
7 changes: 0 additions & 7 deletions apps/gittensory-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -6266,12 +6266,6 @@
"type": "string"
}
},
"matchedBlockedPaths": {
"type": "array",
"items": {
"type": "string"
}
},
"preferredLabelHits": {
"type": "array",
"items": {
Expand Down Expand Up @@ -6334,7 +6328,6 @@
"linkedIssuePolicy",
"issueDiscoveryPolicy",
"matchedWantedPaths",
"matchedBlockedPaths",
"preferredLabelHits",
"findings",
"publicNextSteps",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -766,9 +766,9 @@ function FocusManifestEditor({ base }: { base: string | null }) {
<FileCog className="size-4" /> Focus manifest (config-as-code)
</h3>
<p className="mt-1 text-token-2xs text-muted-foreground">
The repo&apos;s maintainer focus policy as JSON — wanted/blocked paths, linked-issue policy,
test expectations, and gate overrides. Mirrors{" "}
<code className="font-mono">.gittensory.yml</code>.
The repo&apos;s maintainer focus policy as JSON — wanted paths, linked-issue policy, test
expectations, and gate overrides. Mirrors <code className="font-mono">.gittensory.yml</code>
.
</p>
<textarea
value={loading ? "Loading…" : text}
Expand Down
11 changes: 6 additions & 5 deletions apps/gittensory-ui/src/routes/docs.how-reviews-work.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,9 @@ function HowReviewsWork() {
</li>
<li>
<strong>Manifest-policy gate</strong> (<code>manifestPolicyGateMode</code>, default{" "}
<code>off</code>) — when <code>block</code>, the repo's declared policy (blocked paths,
required linked issue, test expectations) becomes enforceable.
<code>off</code>) — when <code>block</code>, the repo's declared policy (required linked
issue and test expectations) becomes enforceable. Manual-review path holds are controlled
separately by <code>settings.hardGuardrailGlobs</code>.
</li>
</ul>
<p>
Expand Down Expand Up @@ -228,9 +229,9 @@ function HowReviewsWork() {
reviewer reads it.
</li>
<li>
<code>manifest_blocked_path</code> — the PR touches a path listed in the repo's{" "}
<code>blockedPaths</code>. Enforceable when <code>manifestPolicy</code> is{" "}
<code>block</code>.
<code>guardrail_hold</code> — the PR touches a path listed in{" "}
<code>settings.hardGuardrailGlobs</code>. This is a manual-review hold, not an auto-close
reason.
</li>
<li>
<code>manifest_missing_tests</code> — code changed but the expected test paths (
Expand Down
4 changes: 2 additions & 2 deletions apps/gittensory-ui/src/routes/docs.privacy-security.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ function PrivacySecurity() {
<strong>Per-repo settings</strong> — 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 <code>.gittensory.yml</code>. Choosing <code>gate.slop.minScore</code> or
marking a path under <code>blockedPaths</code> tightens the gate without telling a
contributor how to pass it.
setting <code>settings.hardGuardrailGlobs</code> tightens the gate without telling a
contributor how to bypass it.
</li>
<li>
<strong>Operator feature flags</strong> — the <code>GITTENSORY_REVIEW_*</code> family of
Expand Down
25 changes: 9 additions & 16 deletions apps/gittensory-ui/src/routes/docs.tuning.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,9 @@ function Tuning() {
</li>
<li>
<code>gate.manifestPolicy</code> — when <code>block</code>, the manifest's declared policy
(blocked paths, required linked issue, test expectations) becomes an enforceable blocker.
Default <code>off</code>.
(required linked issue and test expectations) becomes an enforceable blocker.
Manual-review path holds use <code>settings.hardGuardrailGlobs</code> instead. Default{" "}
<code>off</code>.
</li>
<li>
<code>gate.firstTimeContributorGrace</code> — when <code>true</code>, softens a would-be
Expand Down Expand Up @@ -294,21 +295,16 @@ function Tuning() {

<h2>Guardrails and scope</h2>
<p>
Top-level keys in <code>.gittensory.yml</code> declare the repo's focus and guardrails.
These feed the deterministic findings (such as <code>manifest_blocked_path</code> and{" "}
<code>manifest_missing_tests</code>) and — when <code>gate.manifestPolicy: block</code> —
can become enforceable blockers.
Top-level keys in <code>.gittensory.yml</code> declare the repo's focus and validation
expectations. These feed deterministic findings such as <code>manifest_missing_tests</code>{" "}
and — when <code>gate.manifestPolicy: block</code> — can become enforceable blockers. Manual
path holds are configured only through <code>settings.hardGuardrailGlobs</code>.
</p>
<ul>
<li>
<code>wantedPaths</code> — globs for work areas you want; PRs touching these are
preferred. Default <code>[]</code>.
</li>
<li>
<code>blockedPaths</code> — globs off-limits to contributors. Touching one yields a{" "}
<code>manifest_blocked_path</code> finding, enforceable when{" "}
<code>gate.manifestPolicy: block</code>. Default <code>[]</code>.
</li>
<li>
<code>preferredLabels</code> — labels you prefer on incoming PRs; a missing one is
surfaced. Default <code>[]</code>.
Expand Down Expand Up @@ -388,18 +384,15 @@ function Tuning() {

<h2>Example .gittensory.yml</h2>
<p>
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.
</p>
<CodeBlock
filename=".gittensory.yml"
lang="yaml"
code={`# Focus / guardrails
code={`# Focus / validation
wantedPaths:
- "src/**"
blockedPaths:
- "vendor/**"
- ".github/workflows/**"
testExpectations:
- "tests/**"
linkedIssuePolicy: preferred
Expand Down
4 changes: 2 additions & 2 deletions config/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ per-repo file overlaid onto the global default:
- **Nested mappings** (`gate`, `settings`, `review`, `features`, `contentLane`, and their own
nested blocks like `gate.readiness` or `gate.aiReview`) merge **key by key**. A per-repo file
only needs to mention the keys it wants to change; everything else is inherited from global.
- **Arrays** (`wantedPaths`, `blockedPaths`, `preferredLabels`, `testExpectations`,
- **Arrays** (`wantedPaths`, `preferredLabels`, `testExpectations`,
`review.pathInstructions`, `review.excludePaths`, `contentLane.duplicateKeyFields`, etc.)
**replace wholesale** — a per-repo array is never concatenated with the global one.
- An **explicit `null`** at a key in the per-repo file always overrides the global value there.
Expand Down Expand Up @@ -195,7 +195,7 @@ array-replace overlay semantics above) — it does not merge with it.
admin exemption logins, autonomy dials, model/effort overrides, and anything else you don't want
a contributor reading and gaming.
- **Public `.gittensory.yml`** (repo root, contributor-visible): work-area guidance
(`wantedPaths`/`blockedPaths`), test expectations, and review-panel presentation — nothing here
(`wantedPaths`), test expectations, and review-panel presentation — nothing here
should describe your private enforcement strategy.

## Safety
Expand Down
5 changes: 0 additions & 5 deletions src/config/gittensory-repo-focus-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,6 @@ wantedPaths:
- wrangler.jsonc
- apps/gittensory-ui/

blockedPaths:
- site/
- CNAME
- "**/lovable/**"

preferredLabels:
- bug
- enhancement
Expand Down
1 change: 0 additions & 1 deletion src/openapi/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2304,7 +2304,6 @@ export const LocalBranchAnalysisSchema = z
linkedIssuePolicy: z.enum(["required", "preferred", "optional"]),
issueDiscoveryPolicy: z.enum(["encouraged", "neutral", "discouraged"]),
matchedWantedPaths: z.array(z.string()),
matchedBlockedPaths: z.array(z.string()),
preferredLabelHits: z.array(z.string()),
findings: z.array(z.object({ code: z.string(), severity: z.enum(["info", "warning", "critical"]), title: z.string(), detail: z.string(), action: z.string().optional() })),
publicNextSteps: z.array(z.string()),
Expand Down
5 changes: 0 additions & 5 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -727,11 +727,6 @@ const PUBLIC_MANIFEST_POLICY_FINDING_OVERRIDES: Partial<
Pick<AdvisoryFinding, "detail" | "action">
>
> = {
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:
Expand Down
5 changes: 2 additions & 3 deletions src/review/parity-wire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down
33 changes: 4 additions & 29 deletions src/rules/advisory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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";
}
Expand Down
2 changes: 1 addition & 1 deletion src/rules/predicted-gate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
20 changes: 13 additions & 7 deletions src/selfhost/config-lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { MAX_FOCUS_MANIFEST_BYTES, parseFocusManifestContent } from "../signals/
const TOP_LEVEL_FIELDS = [
"source",
"wantedPaths",
"blockedPaths",
"preferredLabels",
"linkedIssuePolicy",
"testExpectations",
Expand Down Expand Up @@ -60,18 +59,25 @@ function recognizedFieldsFor(text: string | null | undefined): string[] {
);
}

// Fields retired from TOP_LEVEL_FIELDS that still warrant a migration-specific warning (rather than the
// generic "unknown field" message) pointing operators at their replacement mechanism.
const RETIRED_FIELD_MIGRATION_WARNINGS: Record<string, string> = {
blockedPaths: "blockedPaths is retired; use settings.hardGuardrailGlobs for path holds.",
};

function unknownTopLevelWarnings(text: string | null | undefined): string[] {
const raw = text ?? "";
const trimmed = raw.trim();
if (!trimmed || isOversize(raw)) return [];
const parsed = parseTopLevelObject(trimmed);
if (parsed === null) return [];
const unknown = Object.keys(parsed)
.filter((key) => !TOP_LEVEL_FIELD_SET.has(key))
.map(formatFieldName);
return unknown.length > 0
? [`Manifest contains unknown top-level field${unknown.length === 1 ? "" : "s"}: ${unknown.join(", ")}.`]
: [];
const keys = Object.keys(parsed).filter((key) => !TOP_LEVEL_FIELD_SET.has(key));
const retiredWarnings = keys.filter((key) => key in RETIRED_FIELD_MIGRATION_WARNINGS).map((key) => RETIRED_FIELD_MIGRATION_WARNINGS[key]!);
const unknown = keys.filter((key) => !(key in RETIRED_FIELD_MIGRATION_WARNINGS)).map(formatFieldName);
return [
...retiredWarnings,
...(unknown.length > 0 ? [`Manifest contains unknown top-level field${unknown.length === 1 ? "" : "s"}: ${unknown.join(", ")}.`] : []),
];
}

function parseCanonicalTopLevelObject(text: string | null | undefined): Record<string, unknown> | null {
Expand Down
2 changes: 1 addition & 1 deletion src/services/contributor-issue-draft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
6 changes: 0 additions & 6 deletions src/services/decision-pack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,6 @@ export type RepoDecisionManifestSummary = {
linkedIssuePolicy: FocusManifestLinkedIssuePolicy;
issueDiscoveryPolicy: FocusManifestIssueDiscoveryPolicy;
wantedPathCount: number;
blockedPathCount: number;
preferredLabels: string[];
publicNotes: string[];
};
Expand Down Expand Up @@ -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),
};
Expand All @@ -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.");
Expand Down
3 changes: 1 addition & 2 deletions src/services/miner-dashboard-recommendations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] {
Expand Down
2 changes: 1 addition & 1 deletion src/services/remediation-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}

Expand Down
1 change: 0 additions & 1 deletion src/signals/focus-manifest-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,6 @@ function manifestToJson(manifest: FocusManifest): Record<string, JsonValue> {
return {
source: manifest.source,
wantedPaths: manifest.wantedPaths,
blockedPaths: manifest.blockedPaths,
preferredLabels: manifest.preferredLabels,
linkedIssuePolicy: manifest.linkedIssuePolicy,
testExpectations: manifest.testExpectations,
Expand Down
Loading
Loading