Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
claude/adoring-hopper-5x74bqclaude/affectionate-feynman-ykrf1hclaude/architecture-audit-design-wxprenclaude/build-status-update-3MXsfclaude/charming-meitner-mllb5rclaude/compare-gate-gluecron-s4mFQclaude/confident-faraday-tikcwbclaude/continue-work-XMTlIclaude/crontech-gluecron-deploy-7MIECclaude/crontech-platform-setup-SeKfwclaude/design-2026claude/ecstatic-ptolemy-jMdigclaude/enhance-github-integration-QNHdGclaude/fix-aa-loop-issue-PonMQclaude/fix-actions-and-processclaude/fix-desktop-errors-XqoW8claude/fix-red-workflowsclaude/fix-website-access-6FKJNclaude/gatetest-integration-hardeningclaude/github-audit-improvements-bDFr9claude/gluecron-launch-status-FoMRlclaude/hopeful-lamport-olfCTclaude/issue-to-pr-and-protectionsclaude/jolly-heisenberg-2sg1Qclaude/launch-preparation-QmTb6claude/new-session-xk1l7claude/plan-platform-architecture-kkN4yclaude/platform-analysis-roadmap-1nUGLclaude/platform-launch-assessment-8dWV8claude/polish-platform-release-AeDrUclaude/resume-previous-work-KzyLwclaude/review-crontech-handoff-qYEVqclaude/review-project-completeness-lHhS2claude/review-readme-docs-ulqPKclaude/serene-edison-rj87weclaude/setup-multi-repo-dev-BCwNQclaude/ship-fixes-and-tests-Jvz1cclaude/site-audit-competitive-pctlwgclaude/site-migration-vercel-XstpKclaude/standalone-product-repos-XHFTDcopilot/feat-smart-empty-states-keyboard-first-enhancementcopilot/feat-smart-morning-digest-review-context-restorecopilot/fix-and-process-workflowscopilot/update-ai-powered-code-reviewfeat/debt-mapfeat/push-policy-codeowners-hardeningfeat/smart-digest-contextfeat/stage-impactfeat/t1-secret-migrationfeat/u-polishfeat/w-self-hostfeat/w2-claude-configfix/agent-journey-orphan-sweepgatetest/auto-fix-1776586424172gatetest/auto-fix-1776586534814gatetest/auto-fix-1776590685143gatetest/auto-fix-1776590808199mainops/redeploy-retriggerstyle/dxt-cta-themeworktree-agent-a3377aad30d55da26worktree-agent-a7ef607b7ee1d6c74
automation-settings.ts7.5 KB · 193 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
/**
 * Per-repo automation settings — the single read/write surface for the
 * "Automation" settings page (`/:owner/:repo/settings/automation`) and for
 * every automation dispatch site (AI review, PR/issue triage, auto-merge,
 * CI autofix).
 *
 * Contract:
 *
 *   - One `repo_automation_settings` row per repository (migration 0106).
 *     No row → `AUTOMATION_DEFAULTS`, which match pre-0106 behavior exactly,
 *     so a repo that never touches the page behaves as it always did.
 *   - `getAutomationSettings` FAILS OPEN: any DB error returns the defaults
 *     so a broken lookup can never change platform behavior.
 *   - Env kill-switches stay supreme. A feature disabled at the environment
 *     level (missing ANTHROPIC_API_KEY, AI_LOOP_ENABLED unset, …) is off
 *     regardless of the stored mode — see `resolveEffectiveMode`. Dispatch
 *     sites keep their existing env guards; this module only ever *narrows*
 *     what runs, never widens it.
 *
 * Mode semantics per feature (see drizzle/0106 for the long-form docs):
 *
 *   aiReviewMode     'off' | 'suggest'            (default 'suggest')
 *   prTriageMode     'off' | 'suggest'            (default 'suggest')
 *   issueTriageMode  'off' | 'suggest'            (default 'suggest')
 *   autoMergeMode    'off' | 'suggest' | 'auto'   (default 'off')
 *   ciAutofixMode    'off' | 'suggest' | 'auto'   (default 'suggest')
 *   autoRepairMode   'off' | 'suggest'            (default 'off')
 *   autoIssuesMode   'off' | 'suggest'            (default 'off')
 *   docDriftMode     'off' | 'suggest'            (default 'off')
 *
 * Where only on/off is meaningful, 'auto' is treated the same as 'suggest'
 * (i.e. "on") — `isAutomationOn` encodes that rule.
 */

import { eq } from "drizzle-orm";
import { db } from "../db";
import { repoAutomationSettings } from "../db/schema";

// ---------------------------------------------------------------------------
// Types + defaults
// ---------------------------------------------------------------------------

export type AutomationMode = "off" | "suggest" | "auto";

export interface AutomationSettings {
  aiReviewMode: AutomationMode;
  prTriageMode: AutomationMode;
  issueTriageMode: AutomationMode;
  autoMergeMode: AutomationMode;
  ciAutofixMode: AutomationMode;
  autoRepairMode: AutomationMode;
  autoIssuesMode: AutomationMode;
  docDriftMode: AutomationMode;
}

/** Loader signature — the DI seam dispatch sites accept for tests. */
export type AutomationSettingsLoader = (
  repositoryId: string
) => Promise<AutomationSettings>;

/**
 * Defaults = pre-0106 platform behavior. Auto-merge defaults to 'auto'
 * because the K2/K3 path already merges automatically (still default-deny
 * per branch via branch_protection.enable_auto_merge); everything else
 * defaults to 'suggest' because those features only ever posted advisory
 * comments.
 */
export const AUTOMATION_DEFAULTS: Readonly<AutomationSettings> = Object.freeze({
  aiReviewMode: "suggest",
  prTriageMode: "suggest",
  issueTriageMode: "suggest",
  autoMergeMode: "off",
  ciAutofixMode: "suggest",
  autoRepairMode: "off",
  autoIssuesMode: "off",
  docDriftMode: "off",
});

// ---------------------------------------------------------------------------
// Pure mode-resolution helpers (unit-testable, no DB)
// ---------------------------------------------------------------------------

/** Parse an untrusted value (form input, stale DB row) into a mode. */
export function normalizeMode(
  value: unknown,
  fallback: AutomationMode
): AutomationMode {
  return value === "off" || value === "suggest" || value === "auto"
    ? value
    : fallback;
}

/**
 * Env-supremacy rule: when the environment kill-switch for a feature is
 * off, the effective mode is 'off' no matter what the repo row says. The
 * repo setting can only narrow further (e.g. env on + repo 'off' → 'off').
 */
export function resolveEffectiveMode(
  repoMode: AutomationMode,
  envEnabled: boolean
): AutomationMode {
  if (!envEnabled) return "off";
  return repoMode;
}

/** "Is this feature on at all?" — 'suggest' and 'auto' both count as on. */
export function isAutomationOn(mode: AutomationMode): boolean {
  return mode !== "off";
}

/**
 * Coerce a raw row (or anything row-shaped) into a fully-valid settings
 * object, falling back per-field to the defaults. Exported for tests.
 */
export function settingsFromRow(
  row: Partial<Record<keyof AutomationSettings, unknown>> | null | undefined
): AutomationSettings {
  if (!row) return { ...AUTOMATION_DEFAULTS };
  return {
    aiReviewMode: normalizeMode(row.aiReviewMode, AUTOMATION_DEFAULTS.aiReviewMode),
    prTriageMode: normalizeMode(row.prTriageMode, AUTOMATION_DEFAULTS.prTriageMode),
    issueTriageMode: normalizeMode(
      row.issueTriageMode,
      AUTOMATION_DEFAULTS.issueTriageMode
    ),
    autoMergeMode: normalizeMode(row.autoMergeMode, AUTOMATION_DEFAULTS.autoMergeMode),
    ciAutofixMode: normalizeMode(row.ciAutofixMode, AUTOMATION_DEFAULTS.ciAutofixMode),
    autoRepairMode: normalizeMode(row.autoRepairMode, AUTOMATION_DEFAULTS.autoRepairMode),
    autoIssuesMode: normalizeMode(row.autoIssuesMode, AUTOMATION_DEFAULTS.autoIssuesMode),
    docDriftMode: normalizeMode(row.docDriftMode, AUTOMATION_DEFAULTS.docDriftMode),
  };
}

// ---------------------------------------------------------------------------
// DB-backed loader + upsert
// ---------------------------------------------------------------------------

/**
 * Load the automation settings for a repository. No row → defaults.
 * FAILS OPEN: any DB error also returns the defaults (logged once at warn)
 * so a broken settings lookup never alters current platform behavior.
 */
export async function getAutomationSettings(
  repositoryId: string
): Promise<AutomationSettings> {
  try {
    const [row] = await db
      .select()
      .from(repoAutomationSettings)
      .where(eq(repoAutomationSettings.repositoryId, repositoryId))
      .limit(1);
    return settingsFromRow(row ?? null);
  } catch (err) {
    console.warn(
      "[automation-settings] lookup failed (falling back to defaults):",
      err instanceof Error ? err.message : err
    );
    return { ...AUTOMATION_DEFAULTS };
  }
}

/**
 * Create or update the settings row for a repository. Partial patches are
 * merged over the current effective settings so a form that only posts one
 * field can't reset the others. Throws on DB failure — callers (the
 * settings route) surface the error to the user.
 */
export async function upsertAutomationSettings(
  repositoryId: string,
  patch: Partial<AutomationSettings>
): Promise<AutomationSettings> {
  const current = await getAutomationSettings(repositoryId);
  const next: AutomationSettings = {
    aiReviewMode: normalizeMode(patch.aiReviewMode, current.aiReviewMode),
    prTriageMode: normalizeMode(patch.prTriageMode, current.prTriageMode),
    issueTriageMode: normalizeMode(patch.issueTriageMode, current.issueTriageMode),
    autoMergeMode: normalizeMode(patch.autoMergeMode, current.autoMergeMode),
    ciAutofixMode: normalizeMode(patch.ciAutofixMode, current.ciAutofixMode),
    autoRepairMode: normalizeMode(patch.autoRepairMode, current.autoRepairMode),
    autoIssuesMode: normalizeMode(patch.autoIssuesMode, current.autoIssuesMode),
    docDriftMode: normalizeMode(patch.docDriftMode, current.docDriftMode),
  };

  await db
    .insert(repoAutomationSettings)
    .values({ repositoryId, ...next })
    .onConflictDoUpdate({
      target: repoAutomationSettings.repositoryId,
      set: { ...next, updatedAt: new Date() },
    });

  return next;
}