Skip to content

feat: dashboard UI for ceremony policy settings#1038

Merged
Aureliolo merged 14 commits intomainfrom
feat/ceremony-dashboard-dept-overrides
Apr 3, 2026
Merged

feat: dashboard UI for ceremony policy settings#1038
Aureliolo merged 14 commits intomainfrom
feat/ceremony-dashboard-dept-overrides

Conversation

@Aureliolo
Copy link
Copy Markdown
Owner

Summary

Add ceremony policy configuration to the web dashboard at all 3 resolution levels (project, department, per-ceremony), with strategy-specific config and visual feedback.

What changed

Backend

  • Ceremony policy API controller (/ceremony-policy) with 3 endpoints: project policy query, resolved policy with field-level origin tracking (PolicySourceBadge), active sprint strategy (for warning banners)
  • Department ceremony policy endpoints on /departments/{name}/ceremony-policy (GET/PUT/DELETE) with data-loss protection (raises on failed reads instead of silently returning empty state)
  • 7 ceremony settings registered in coordination namespace: strategy, strategy_config, velocity_calculator, auto_transition, transition_threshold, dept_ceremony_policies, ceremony_policy_overrides
  • active_strategy property added to CeremonyScheduler
  • Observability event constants for ceremony policy API operations

Frontend

  • CeremonyPolicyPage at /settings/coordination/ceremony-policy with:
    • Strategy picker (all 8 strategies) with descriptions and velocity unit indicator
    • Strategy-specific config panels for all 8 strategies (task-driven, calendar, hybrid, event-driven, budget-driven, throughput-adaptive, external-trigger, milestone-driven)
    • Auto-transition toggle and threshold controls
    • Strategy change warning banner when pending strategy differs from active sprint
    • PolicySourceBadge showing resolved field origins (project/department/default)
  • DepartmentOverridesPanel with per-department inherit/override toggles and expandable config forms
  • CeremonyListPanel with per-ceremony inherit/override for individual ceremonies (sprint_planning, standup, sprint_review, retrospective)
  • DepartmentCeremonyOverride integrated into DepartmentEditDrawer in org-edit page
  • PolicySourceBadge and InheritToggle shared UI components with Storybook stories
  • Zustand store, API endpoint module, TypeScript types, ceremony constants

Review fixes (pre-reviewed by 10 agents, 30 findings addressed)

  • Fixed boolean-to-integer bug in StrEnum serialization (isinstance instead of hasattr)
  • Fixed data-loss chain in read-modify-write pattern (raises on failed reads)
  • Added deepcopy at system boundary for department policy storage
  • Added error handling for invalid settings values with structured logging
  • Added co-occurrence validator on ActiveCeremonyStrategyResponse
  • Used NotBlankStr for identifier fields, tightened value: Any to union type
  • Added HTTP_204_NO_CONTENT to DELETE endpoint
  • Added aria-label, aria-expanded for accessibility
  • Added .catch() handlers and error state display for all async operations
  • Added JSON parse error feedback in CodeMirror editors
  • Fixed design tokens (space-y-section-gap, p-card)
  • Added readonly to frozen response types

Documentation

  • Updated CLAUDE.md Package Structure (api/ description)
  • Updated web/CLAUDE.md component inventory (PolicySourceBadge, InheritToggle) and stores list
  • Updated docs/design/ceremony-scheduling.md roadmap (moved feat: dashboard UI for ceremony policy settings #979 to shipped)

Test plan

  • uv run python -m pytest tests/ -m unit -n 8 -k ceremony -- 31 tests pass
  • npm --prefix web run type-check -- zero errors
  • npm --prefix web run lint -- zero warnings
  • npm --prefix web run test -- 2342 tests pass
  • Visual: navigate to Settings > Coordination > Ceremony Policy link
  • Visual: strategy picker shows 8 options with descriptions and velocity unit
  • Visual: strategy change warning banner appears when strategy differs from active sprint
  • Visual: department overrides with inherit/override toggles
  • Visual: per-ceremony overrides for individual ceremonies
  • Visual: org-edit department drawer has ceremony policy section

Closes #979

Copilot AI review requested due to automatic review settings April 3, 2026 14:06
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 3, 2026

Walkthrough

Adds ceremony policy support across documentation, backend, and frontend. Backend: new CeremonyPolicyController with endpoints for project policy, resolved per-field policy (with per-field origin tracking) and active strategy; department GET/PUT/DELETE endpoints for per-department overrides; new coordination settings entries; scheduler accessors (active_strategy property and get_active_info); new observability API event constants; and unit tests. Frontend: new ceremony-policy settings page and route, API endpoints and types, a Zustand store, many UI components (including PolicySourceBadge and InheritToggle) with Storybook stories, and related pages/panels.

Suggested labels

autorelease: tagged

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.16% which is insufficient. The required threshold is 40.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly summarizes the main change: adding a dashboard UI for ceremony policy settings. It is specific, concise, and directly reflects the primary objective of this changeset.
Description check ✅ Passed Description is comprehensive and directly related to the changeset. It details backend API controllers, settings, department endpoints, frontend pages, components, store, observability, and documentation updates—all aligned with the actual code changes.
Linked Issues check ✅ Passed The changeset fully addresses all primary objectives from #979: UI for 3-level (project/department/per-ceremony) ceremony policy editing, strategy picker with all 8 types, strategy-specific config, velocity calculator, auto-transition controls, PolicySourceBadge for resolved field origins, strategy-change warning banner, and velocity unit indicator.
Out of Scope Changes check ✅ Passed All changes are in scope for #979 (ceremony policy dashboard UI). Minor additions like animation test options and route constants are standard supporting changes; no unrelated refactoring or scope creep detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 3, 2026

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Snapshot Warnings

⚠️: No snapshots were found for the head SHA c1e6c5c.
Ensure that dependencies are being submitted on PR branches. Re-running this action after a short time may resolve the issue. See the documentation for more information and troubleshooting advice.

Scanned Files

None

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements the Dashboard UI and backend API for ceremony policy management, allowing for project-level configurations and per-department or per-ceremony overrides. Key additions include a new CeremonyPolicyController, extended department endpoints, and a comprehensive React-based settings interface with specialized UI components like InheritToggle and PolicySourceBadge. Feedback identifies critical Python syntax errors in exception handling blocks that will cause runtime failures. Additionally, there are recommendations to improve the UX of JSON editors to prevent state resets during typing and to add validation for numeric inputs to avoid storing NaN values.

"dept_ceremony_policies",
)
return json.loads(entry.value) # type: ignore[no-any-return]
except MemoryError, RecursionError:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

In Python 3, multiple exceptions in an except block must be parenthesized. The current syntax except E1, E2: is invalid and will result in a SyntaxError.

    except (MemoryError, RecursionError):

# Validate policy data via Pydantic
try:
CeremonyPolicyConfig.model_validate(data)
except MemoryError, RecursionError:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Multiple exceptions in an except block must be enclosed in a tuple. Using a comma without parentheses is invalid syntax in Python 3.

        except (MemoryError, RecursionError):

Comment on lines +25 to +32
onChange={(val) => {
try {
onChange({ ...config, sources: JSON.parse(val) })
setJsonError(null)
} catch {
setJsonError('Invalid JSON')
}
}}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation of onChange for the LazyCodeMirrorEditor will cause the editor's content to reset to the last valid state whenever the user types something that makes the JSON invalid (e.g., while in the middle of typing an array or object). This is because the editor is controlled by the sources prop, which only updates when JSON.parse succeeds. It is recommended to maintain a local string state for the editor's content and only attempt to parse and propagate the value to the parent when it is valid, or store the raw string in the parent state.

Comment on lines +25 to +32
onChange={(val) => {
try {
onChange({ ...config, milestones: JSON.parse(val) })
setJsonError(null)
} catch {
setJsonError('Invalid JSON')
}
}}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to ExternalTriggerConfig, this implementation will cause the editor to reset its content on every keystroke that makes the JSON invalid. Consider using a local string state to manage the editor's content to avoid UX issues during typing.

label="Duration (days)"
type="number"
value={String(durationDays)}
onChange={(e) => onChange({ ...config, duration_days: Number(e.target.value) })}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Directly calling Number(e.target.value) without validation can lead to NaN being stored in the configuration state if the input is invalid or empty. It is safer to check Number.isFinite() before updating the state, similar to the pattern used in PolicyFieldsPanel.tsx.

Suggested change
onChange={(e) => onChange({ ...config, duration_days: Number(e.target.value) })}
onChange={(e) => { const val = Number(e.target.value); if (Number.isFinite(val)) onChange({ ...config, duration_days: val }); }}

type="number"
value={String(everyN)}
onChange={(e) => onChange({ ...config, every_n_completions: Number(e.target.value) })}
disabled={disabled}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Validation should be added to ensure that only finite numbers are passed to onChange. If the input is cleared or contains invalid characters, Number() may return NaN, which could cause issues during serialization or backend processing.

Suggested change
disabled={disabled}
onChange={(e) => { const val = Number(e.target.value); if (Number.isFinite(val)) onChange({ ...config, every_n_completions: val }); }}

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 3, 2026

Codecov Report

❌ Patch coverage is 38.24363% with 218 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.68%. Comparing base (e1b14d3) to head (c1e6c5c).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/synthorg/api/controllers/ceremony_policy.py 47.11% 108 Missing and 2 partials ⚠️
src/synthorg/api/controllers/departments.py 16.66% 105 Missing ⚠️
src/synthorg/engine/workflow/ceremony_scheduler.py 50.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1038      +/-   ##
==========================================
- Coverage   91.15%   90.68%   -0.48%     
==========================================
  Files         696      697       +1     
  Lines       39244    39593     +349     
  Branches     3917     3960      +43     
==========================================
+ Hits        35772    35903     +131     
- Misses       2784     3000     +216     
- Partials      688      690       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds end-to-end “Ceremony Policy” configuration support (API + dashboard UI) across project/department/per-ceremony resolution levels, including strategy-specific configuration panels and field-origin badges to explain where effective values come from.

Changes:

  • Introduces new backend ceremony policy controllers/endpoints (project policy, resolved policy with sources, active strategy; plus department override CRUD) and registers the corresponding settings.
  • Adds a new dashboard settings page (/settings/coordination/ceremony-policy) with strategy picker, strategy config panels, auto-transition controls, and department/per-ceremony override UIs.
  • Adds shared UI components (PolicySourceBadge, InheritToggle), new Zustand store + API client module, and supporting constants/types/docs/tests.

Reviewed changes

Copilot reviewed 43 out of 43 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
web/src/utils/constants.ts Adds ceremony strategy/velocity labels, units, and type lists for UI rendering.
web/src/stores/ceremony-policy.ts New Zustand store for resolved policy, active strategy, and department overrides.
web/src/router/routes.ts Adds route constant for the ceremony policy settings page.
web/src/router/index.tsx Registers the new ceremony policy route and lazy-loads the page.
web/src/pages/SettingsPage.tsx Adds Settings page tile/link to the ceremony policy page.
web/src/pages/settings/ceremony-policy/VelocityUnitIndicator.tsx Displays velocity unit label derived from strategy/calculator.
web/src/pages/settings/ceremony-policy/StrategyPicker.tsx Strategy selection UI with description and unit indicator.
web/src/pages/settings/ceremony-policy/StrategyPicker.stories.tsx Storybook coverage for the strategy picker.
web/src/pages/settings/ceremony-policy/StrategyConfigPanel.tsx Switchboard to render the correct strategy config form.
web/src/pages/settings/ceremony-policy/StrategyChangeWarning.tsx Warning banner when pending strategy differs from active sprint.
web/src/pages/settings/ceremony-policy/StrategyChangeWarning.stories.tsx Storybook coverage for the warning banner.
web/src/pages/settings/ceremony-policy/strategies/ThroughputAdaptiveConfig.tsx Strategy-specific form inputs for throughput-adaptive config.
web/src/pages/settings/ceremony-policy/strategies/TaskDrivenConfig.tsx Strategy-specific form inputs for task-driven config.
web/src/pages/settings/ceremony-policy/strategies/MilestoneDrivenConfig.tsx JSON editor + inputs for milestone-driven config.
web/src/pages/settings/ceremony-policy/strategies/HybridConfig.tsx Combines task-driven + calendar config sections.
web/src/pages/settings/ceremony-policy/strategies/ExternalTriggerConfig.tsx JSON editor + inputs for external-trigger config.
web/src/pages/settings/ceremony-policy/strategies/EventDrivenConfig.tsx Inputs for event-driven config (debounce/event name).
web/src/pages/settings/ceremony-policy/strategies/CalendarConfig.tsx Inputs for calendar cadence/duration config.
web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx Inputs for budget thresholds and transition percent.
web/src/pages/settings/ceremony-policy/PolicyFieldsPanel.tsx Shared panel for velocity calculator + auto-transition controls + source badges.
web/src/pages/settings/ceremony-policy/DepartmentOverridesPanel.tsx Department-level inherit/override UI wired to department override API/store.
web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx New settings page integrating project policy editor + dept/per-ceremony panels.
web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx Per-ceremony override UI stored in settings JSON.
web/src/pages/org-edit/DepartmentEditDrawer.tsx Adds department ceremony policy override section to the org-edit drawer.
web/src/pages/org-edit/DepartmentCeremonyOverride.tsx UI to edit a department’s ceremony policy override within org-edit flow.
web/src/pages/org-edit/DepartmentCeremonyOverride.stories.tsx Storybook coverage for department ceremony override UI.
web/src/components/ui/policy-source-badge.tsx New pill component to display field origin (project/department/default).
web/src/components/ui/policy-source-badge.stories.tsx Storybook coverage for PolicySourceBadge.
web/src/components/ui/inherit-toggle.tsx New inherit/override switch component for policy editing UIs.
web/src/components/ui/inherit-toggle.stories.tsx Storybook coverage for InheritToggle.
web/src/api/types.ts Adds TS types for ceremony strategy, policy config, resolved fields, and active strategy.
web/src/api/endpoints/ceremony-policy.ts Adds frontend API module for ceremony-policy and dept ceremony-policy endpoints.
web/CLAUDE.md Updates web package structure docs and component/store inventory.
tests/unit/settings/test_ceremony_settings.py Adds unit tests verifying ceremony-related settings are registered.
tests/unit/api/controllers/test_ceremony_policy.py Adds unit tests for ceremony policy controller helpers and models.
src/synthorg/settings/definitions/coordination.py Registers ceremony policy settings in the coordination namespace.
src/synthorg/observability/events/api.py Adds observability event constants for ceremony policy operations.
src/synthorg/engine/workflow/ceremony_scheduler.py Exposes active_strategy property for active sprint strategy introspection.
src/synthorg/api/controllers/departments.py Adds department ceremony-policy GET/PUT/DELETE with settings-backed persistence.
src/synthorg/api/controllers/ceremony_policy.py Adds ceremony policy controller endpoints for project/resolved/active strategy queries.
src/synthorg/api/controllers/init.py Registers the new CeremonyPolicyController with the API controller list.
docs/design/ceremony-scheduling.md Updates design doc to mark dashboard UI work as shipped in #979.
CLAUDE.md Updates repo package structure docs to mention ceremony policy API capabilities.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +58 to +64
fetchActiveStrategy: async () => {
try {
const active = await ceremonyApi.getActiveStrategy()
set({ activeStrategy: active })
} catch (err) {
set({ error: getErrorMessage(err) })
}
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The store uses a single error field for failures from multiple independent fetches (resolved policy, active strategy, and per-department policy). As written, a transient failure in getActiveStrategy() or a single department policy fetch will set error and can cause CeremonyPolicyPage to render a full-page failure state even though the main policy data may be available. Consider separating errors (e.g., resolvedPolicyError, activeStrategyError, departmentErrorByName) or ensuring only the initial fetchResolvedPolicy() populates the page-blocking error.

Copilot uses AI. Check for mistakes.
Comment on lines +39 to +47
export const useCeremonyPolicyStore = create<CeremonyPolicyState>()((set, get) => ({
resolvedPolicy: null,
activeStrategy: null,
departmentPolicies: new Map(),
loading: false,
error: null,
saving: false,
saveError: null,

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are no unit tests for the new ceremony policy Zustand store, while other stores in this repo have dedicated tests under web/src/__tests__/stores/. Adding tests for the fetch/save error handling and departmentPolicies update behavior would help prevent regressions (especially around Map updates and error state interactions).

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +26
export default function CeremonyPolicyPage() {
const addToast = useToastStore((s) => s.add)
const settingsEntries = useSettingsStore((s) => s.entries)
const updateSetting = useSettingsStore((s) => s.updateSetting)

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This page reads useSettingsStore((s) => s.entries) but never triggers fetchSettingsData() (unlike SettingsPage which uses useSettingsData). If a user deep-links directly to /settings/coordination/ceremony-policy, entries will be empty and the form initializes to defaults, which can lead to overwriting real settings on save. Consider calling useSettingsData() here (or triggering fetchSettingsData() in an effect) and gating rendering until settings are loaded.

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +65
// Local form state for project-level policy (initialized from settings)
const [strategy, setStrategy] = useState<CeremonyStrategyType>(settingsSnapshot.strategy)
const [strategyConfig, setStrategyConfig] = useState<Record<string, unknown>>(settingsSnapshot.strategyConfig)
const [velocityCalculator, setVelocityCalculator] = useState<VelocityCalcType>(settingsSnapshot.velocityCalculator)
const [autoTransition, setAutoTransition] = useState(settingsSnapshot.autoTransition)
const [transitionThreshold, setTransitionThreshold] = useState(settingsSnapshot.transitionThreshold)
const [saving, setSaving] = useState(false)
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Local form state is initialized from settingsSnapshot once, but it is not synchronized if settingsEntries arrive/refresh later. This can leave the UI showing stale defaults even after the settings store updates. Consider updating the local state in an effect when settingsSnapshot changes (ideally only when the form is pristine) and defaulting invalid values (e.g., transitionThreshold) when parsing yields NaN.

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +84
const initialCeremonyOverrides = useMemo(() => {
const raw = settingsEntries.find(
(e) => e.definition.namespace === 'coordination' && e.definition.key === 'ceremony_policy_overrides',
)?.value
if (raw) {
try {
return JSON.parse(raw) as Record<string, CeremonyPolicyConfig | null>
} catch {
console.warn('Failed to parse ceremony_policy_overrides setting')
}
}
return {} as Record<string, CeremonyPolicyConfig | null>
}, [settingsEntries])
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If ceremony_policy_overrides contains invalid JSON, the code only console.warns and falls back to {}. Saving after that will overwrite the existing (possibly user-authored) value with an empty object, causing data loss. Consider surfacing a visible error state/toast and disabling Save until the JSON is repaired, or preserving the raw string and requiring explicit confirmation before overwriting.

Copilot uses AI. Check for mistakes.
Comment on lines 118 to 121
{ path: 'settings', element: <SettingsPage /> },
{ path: 'settings/observability/sinks', element: <SettingsSinksPage /> },
{ path: 'settings/coordination/ceremony-policy', element: <CeremonyPolicyPage /> },
{ path: 'settings/:namespace', element: <SettingsNamespacePage /> },
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new ceremony policy route is registered using a hard-coded path string even though a route constant exists in router/routes.ts. Using ROUTES.SETTINGS_CEREMONY_POLICY (or at least reusing the constant’s value) reduces the chance of router/link mismatches.

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,114 @@
"""Tests for ceremony policy setting definitions.

Verifies that the 6 ceremony-related settings are registered in the
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The module docstring says “6 ceremony-related settings”, but this file (and the registry) now covers 7 settings. Please update the docstring to match the actual count to avoid confusion when maintaining the settings registry.

Suggested change
Verifies that the 6 ceremony-related settings are registered in the
Verifies that the 7 ceremony-related settings are registered in the

Copilot uses AI. Check for mistakes.
Comment on lines +463 to +464
return json.loads(entry.value) # type: ignore[no-any-return]
except MemoryError, RecursionError:
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_load_dept_policies_json() returns json.loads(entry.value) without validating that the parsed value is an object/dict. If the stored JSON is a list/string/etc, later code (policies[department_name], policies.pop) will raise confusing runtime errors, and GET may silently ignore settings-based overrides. Consider validating isinstance(parsed, dict) and either raising a structured API error (for write paths) or returning {} with a warning (for read paths).

Suggested change
return json.loads(entry.value) # type: ignore[no-any-return]
except MemoryError, RecursionError:
parsed = json.loads(entry.value)
if not isinstance(parsed, dict):
msg = "dept_ceremony_policies must be a JSON object"
logger.warning(
API_REQUEST_ERROR,
endpoint="departments.ceremony_policy.load",
error=msg,
parsed_type=type(parsed).__name__,
)
if raise_on_error:
raise ApiValidationError(msg)
return {}
return parsed
except (MemoryError, RecursionError):

Copilot uses AI. Check for mistakes.
Comment on lines +757 to +773
# Validate policy data via Pydantic
try:
CeremonyPolicyConfig.model_validate(data)
except MemoryError, RecursionError:
raise
except Exception as exc:
msg = f"Invalid ceremony policy: {exc}"
logger.warning(
API_REQUEST_ERROR,
endpoint="departments.ceremony_policy.update",
error=str(exc),
)
raise ApiValidationError(msg) from exc

# Merge into the dept_ceremony_policies JSON setting
await _set_dept_ceremony_override(app_state, name, data)

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update_department_ceremony_policy() validates with CeremonyPolicyConfig.model_validate(data) but then persists and returns the unvalidated raw data. This can allow unknown keys or unnormalized values to be stored even if Pydantic would coerce/strip them. Consider using the validated model’s model_dump(mode="json", exclude_none=True) output for both storage and the response payload.

Copilot uses AI. Check for mistakes.
Comment on lines +302 to +331
async def _fetch_department_policy(
app_state: AppState,
department_name: str,
) -> CeremonyPolicyConfig | None:
"""Fetch department-level ceremony policy override.

Args:
app_state: Application state with config resolver.
department_name: Department name to look up.

Returns:
CeremonyPolicyConfig if department has an override, else None.

Raises:
NotFoundError: If the department does not exist.
"""
if not app_state.has_config_resolver:
msg = "Config resolver not available"
logger.warning(API_SERVICE_UNAVAILABLE, service="config_resolver")
raise ServiceUnavailableError(msg)

departments = await app_state.config_resolver.get_departments()
for dept in departments:
if dept.name == department_name:
if dept.ceremony_policy is None:
return None
return CeremonyPolicyConfig.model_validate(
dept.ceremony_policy,
)
msg = f"Department {department_name!r} not found"
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/ceremony-policy/resolved?department=... fetches the department policy via config_resolver (dept.ceremony_policy) only. However, the department override endpoints in departments.py store overrides in the coordination/dept_ceremony_policies setting. As a result, resolved policy queries will not reflect overrides set via the new API/UI. Consider fetching department overrides from the same source as the department endpoints (settings-based dept_ceremony_policies, with config fallback if desired) so the resolution behavior matches what users edit.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 30

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/src/pages/SettingsPage.tsx (1)

385-421: 🧹 Nitpick | 🔵 Trivial

Consider extracting duplicated card structure to a shared component.

The Link-based card structure for "Log Sinks" (lines 386-402) and "Ceremony Policy" (lines 404-420) is nearly identical—both have the same grid layout, title/description structure, and "Open" button styling. This exceeds 8 lines of duplicated JSX.

♻️ Example extraction
// In a shared component or local helper:
interface SettingsActionCardProps {
  to: string
  title: string
  description: string
}

function SettingsActionCard({ to, title, description }: SettingsActionCardProps) {
  return (
    <Link
      to={to}
      className="grid grid-cols-[1fr_auto] items-start gap-grid-gap rounded-md p-card transition-all duration-200 hover:bg-card-hover hover:-translate-y-px"
    >
      <div className="min-w-0 space-y-1">
        <span className="text-sm font-medium text-foreground">{title}</span>
        <p className="text-xs text-text-secondary">{description}</p>
      </div>
      <div className="w-56 shrink-0">
        <span
          className="inline-flex h-9 w-full items-center justify-center rounded-md border border-border bg-card px-4 text-sm font-medium text-foreground"
          aria-hidden
        >
          Open
        </span>
      </div>
    </Link>
  )
}

Then usage becomes:

footerAction={ns === 'observability' ? (
  <SettingsActionCard
    to="/settings/observability/sinks"
    title="Log Sinks"
    description="Configure log outputs, rotation, and routing"
  />
) : ns === 'coordination' ? (
  <SettingsActionCard
    to="/settings/coordination/ceremony-policy"
    title="Ceremony Policy"
    description="Configure scheduling strategies, velocity, and department overrides"
  />
) : undefined}

As per coding guidelines: "Do NOT create complex (>8 line) JSX inside .map() — extract to a shared component."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/SettingsPage.tsx` around lines 385 - 421, The footerAction JSX
for ns === 'observability' and ns === 'coordination' in SettingsPage duplicates
the same Link/card structure; extract that markup into a small shared component
(e.g. SettingsActionCard with props to, title, description) and replace the
inline Link blocks used in footerAction with <SettingsActionCard .../> calls
(keep the existing classes and the "Open" button styling), ensuring you remove
the >8-line JSX from the map/conditional and reuse the new component for both
cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/synthorg/api/controllers/departments.py`:
- Around line 458-475: The loaded setting for "dept_ceremony_policies" must be
validated as a mapping: after calling app_state.settings_service.get(...) and
json.loads(entry.value) in the departments.ceremony_policy.load flow, verify
isinstance(payload, dict); if not, log a warning (reuse the existing
logger.warning context and message) indicating the payload is not an object and
then follow the same error-path behavior (raise if raise_on_error is true,
otherwise return {}); retain the existing except MemoryError, RecursionError
re-raises and general Exception handling.
- Around line 528-536: The DELETE currently removes the settings key but
_get_dept_ceremony_override() then falls back to
app_state.config_resolver.get_departments() and re-exposes dept.ceremony_policy;
change behavior so DELETE persists a null sentinel for that department’s
ceremony_policy (i.e. explicitly store None/null) instead of deleting the key,
and update _get_dept_ceremony_override() to treat a stored null sentinel as
“inherit project policy” by returning None (so the higher-level caller uses the
config fallback) rather than removing the entry; apply the same null-sentinel
persistence logic to the other delete branch mentioned (the second occurrence
corresponding to the alternate delete path).
- Around line 430-475: The code currently masks unreadable or unparsable
dept_ceremony_policies as an empty dict; update _load_dept_policies_json and its
callers so parse errors surface instead of silently falling through: in
_load_dept_policies_json (the function shown) narrow the except clauses so
MemoryError and RecursionError still re-raise, treat json.JSONDecodeError (and
ValueError from json) specially by logging and re-raising regardless of
raise_on_error, and keep the existing fallback only for non-parse transient
errors when raise_on_error is False; then update callers that implement the
GET/read path (notably _get_dept_ceremony_override and the other call sites
mentioned) to call _load_dept_policies_json(..., raise_on_error=True) so
unreadable settings produce a 503 instead of appearing as “no override.”
- Around line 757-779: The code calls CeremonyPolicyConfig.model_validate(data)
but discards the result, so validators and type coercion are lost; fix by
assigning the validated instance (e.g., validated =
CeremonyPolicyConfig.model_validate(data)), use validated.model_dump() when
calling _set_dept_ceremony_override(app_state, name, ...) and when constructing
ApiResponse(data=...), and return the validated.model_dump() so Pydantic
validators (and StrEnum coercion) are applied and their side effects run.

In `@src/synthorg/engine/workflow/ceremony_scheduler.py`:
- Around line 126-129: The property active_strategy exposes lock-protected
mutable state without synchronization, so add a synchronized accessor to avoid
TOCTOU with active_sprint: implement a new method (e.g.,
get_active_strategy_and_sprint()) that acquires self._lock and returns both
self._active_strategy and self._active_sprint atomically; keep the existing
property for backward compatibility but update its docstring to state it is
eventually consistent and callers that need a consistent pair should call
get_active_strategy_and_sprint(); reference methods/fields: active_strategy,
active_sprint, activate_sprint, _deactivate_sprint_unlocked, and self._lock.

In `@src/synthorg/settings/definitions/coordination.py`:
- Around line 164-179: The two SettingDefinition registrations for keys
"dept_ceremony_policies" and "ceremony_policy_overrides" are missing yaml_path
metadata; update both SettingDefinition calls to include a yaml_path string so
these settings can be loaded from YAML like the other ceremony settings (e.g.,
set
dept_ceremony_policies.yaml_path="workflow.sprint.ceremony_policy.dept_overrides"
and
ceremony_policy_overrides.yaml_path="workflow.sprint.ceremony_policy.ceremony_overrides"),
ensuring the yaml_path property is added to the SettingDefinition constructors
for those keys.

In `@web/src/components/ui/inherit-toggle.stories.tsx`:
- Around line 13-23: This story file only defines Inherit, Override and
Disabled; add four additional Story exports named Hover, Loading, Error, and
Empty to cover required states: for Hover export set args to mimic a hover state
(e.g., inherit: true, onChange: () => {}, and a prop or context that triggers
the hovered visual state), for Loading export set args to reflect a loading
state (e.g., loading: true), for Error export set args to reflect an error state
(e.g., error: true or errorMessage set), and for Empty export set args to
represent no value (e.g., inherit: false or value: undefined) so the design
system sees empty rendering; follow the pattern used by the existing Inherit,
Override, Disabled Story objects and ensure the exported names are exactly
Hover, Loading, Error, and Empty so Storybook picks them up.

In `@web/src/components/ui/policy-source-badge.stories.tsx`:
- Around line 13-23: Add the required shared-component state stories by
exporting five new Story objects (e.g., DefaultState, HoverState, LoadingState,
ErrorState, EmptyState) alongside the existing Project, Department and Default
exports; each story should use the same component and set the appropriate story
args or decorators to represent the states (e.g., args: { source: 'default',
state: 'default' } for DefaultState, args: { source: 'default', state: 'loading'
} or isLoading: true for LoadingState, args: { source: 'default', state: 'error'
} for ErrorState, args: { source: 'default', state: 'empty' } for EmptyState,
and use a play or decorator to simulate :hover for HoverState) so the file
covers default, hover, loading, error and empty states as required.

In `@web/src/components/ui/policy-source-badge.tsx`:
- Line 25: The PolicySourceBadge component contains a hardcoded utility
'text-[10px]' in its className array; replace that arbitrary pixel size with a
design-token typography class (for example 'text-xs' or the project's
equivalent) by updating the class string in the PolicySourceBadge JSX (find the
array/string that includes 'inline-flex items-center rounded px-1.5 py-0.5
text-[10px] font-medium uppercase tracking-wider' and swap 'text-[10px]' for the
tokenized class) so the component uses approved design tokens instead of
hardcoded font sizes.

In `@web/src/pages/org-edit/DepartmentCeremonyOverride.tsx`:
- Around line 91-97: The SelectField is using a hardcoded fallback 'task_driven'
for policy?.velocity_calculator which can diverge from the value set by
handleStrategyChange (which uses STRATEGY_DEFAULT_VELOCITY_CALC[s]); update the
SelectField value to use the same strategy-based fallback (e.g.
policy?.velocity_calculator ?? STRATEGY_DEFAULT_VELOCITY_CALC[policy?.strategy])
so the displayed value stays consistent with onChange/handleStrategyChange
behavior.
- Around line 36-37: The local expanded state (expanded, setExpanded) is only
seeded from hasOverride on mount and can become stale if policy changes; add a
useEffect hook that depends on hasOverride and calls setExpanded(hasOverride)
(or at least sets expanded to false when hasOverride is false) to keep UI in
sync, and import useEffect alongside useState at the top of the file.

In `@web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx`:
- Around line 52-55: The handler handleStrategyChange currently wipes out
strategy_config by calling onOverrideChange(name, { ...policy, strategy: s,
strategy_config: {} }), which prevents per-ceremony overrides from editing
strategy-specific settings; update handleStrategyChange to preserve or
initialize strategy_config appropriately and render the StrategyConfigPanel for
the override row so users can edit strategy_config (reuse the existing
StrategyConfigPanel component); apply the same fix to the other similar
override-handling block referenced (the logic around the second override update
at lines 87-103) so both creation and strategy switching keep or expose editable
strategy_config instead of forcing {}.
- Around line 41-49: handleInheritChange currently seeds a new override with
only a hardcoded { strategy: 'task_driven' } which wipes other inherited
settings; instead, create the new override by cloning the current resolved
policy for this ceremony (use the resolvedPolicy for `name`) and then
toggle/modify only the fields the user intends to change (e.g., set strategy if
absent), so unrelated fields remain the inherited values; update the same logic
used at the other occurrence referenced (lines 95-97) to also initialize
overrides from the resolved/inherited policy rather than hardcoded literals, and
call onOverrideChange(name, <clonedResolvedPolicyWithDesiredModifications>).

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx`:
- Around line 113-124: handleSave currently issues six independent updateSetting
calls which can leave the ceremony policy partially updated if one call fails;
change this to perform an atomic update by batching the settings into a single
request or using a dedicated transactional API endpoint. Modify handleSave to
build a single payload (including ceremony_strategy, ceremony_strategy_config,
ceremony_velocity_calculator, ceremony_auto_transition,
ceremony_transition_threshold, ceremony_policy_overrides) and call a single
backend method (e.g., a new updateCeremonyPolicy or updateSettingsBatch) instead
of Promise.all of updateSetting; ensure error handling and addToast remain
intact and only report success after the single atomic call completes. Ensure
references: handleSave, updateSetting, ceremonyOverrides, strategyConfig,
velocityCalculator, transitionThreshold, autoTransition are updated to use the
new batch/transactional API.
- Around line 35-57: Derived state computed in settingsSnapshot is only used as
the initial value for several useState hooks (strategy, strategyConfig,
velocityCalculator, autoTransition, transitionThreshold) so when settingsEntries
updates after mount those useState values never rehydrate; add a useEffect that
depends on settingsSnapshot and inside it call the existing setters
(setStrategy, setStrategyConfig, setVelocityCalculator, setAutoTransition,
setTransitionThreshold) to sync the state with the latest settingsSnapshot
values (keeping current behavior for controlled edits), ensuring you reference
the settingsSnapshot object and the setter functions by name when updating.

In `@web/src/pages/settings/ceremony-policy/DepartmentOverridesPanel.tsx`:
- Around line 31-39: The handler handleInheritChange is immediately persisting a
hardcoded { strategy: 'task_driven' } when turning overrides on, which can
overwrite inherited values; change handleInheritChange (and the analogous block
at 91-93) to not write defaults on enable — either call updatePolicy with the
actual resolved policy values if you want to seed the form, or better: toggle a
local "editing" state and defer calling updatePolicy until the user explicitly
saves the form; keep clearPolicy(dept.name) for disabling, but remove the
immediate updatePolicy(dept.name, { strategy: 'task_driven' }) so enabling
override does not persist wrong defaults.
- Around line 42-45: handleStrategyChange currently wipes out strategy_config
when switching strategies (updatePolicy(dept.name, { ...policy, strategy: s,
strategy_config: {} })), which prevents editing strategy-specific settings for
department overrides; remove the reset to {} so strategy_config is preserved (or
replaced with a proper default only when needed) in both change handlers, and
replace the expanded editor rendering that only shows StrategyPicker and
PolicyFieldsPanel with StrategyConfigPanel as well so strategies like calendar,
budget_driven, and external_trigger can be configured at department scope
(update code around handleStrategyChange and the expanded panel render to
include StrategyConfigPanel, referencing updatePolicy, dept.name,
PolicyFieldsPanel, StrategyPicker, and StrategyConfigPanel).

In `@web/src/pages/settings/ceremony-policy/PolicyFieldsPanel.tsx`:
- Around line 69-81: The Transition Threshold input accepts any finite number
but the UI hint says valid range is 0.01–1.0; update the onChange handler for
the InputField (where transitionThreshold is used) to validate the parsed value
and only call onTransitionThresholdChange if Number.isFinite(val) && val >= 0.01
&& val <= 1.0 (alternatively clamp to the nearest bound before calling if you
prefer auto-correction), and ensure the disabled prop handling remains unchanged
so out-of-range values are not persisted.

In `@web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx`:
- Around line 10-11: The code casts unknown config values directly to types
which can throw at render (e.g., thresholds.join) — validate and coerce before
using: check that config.budget_thresholds is an array with numeric entries (use
Array.isArray and filter/map to Number/isFinite), otherwise fall back to
[25,50,75,100] for the thresholds variable; for transition_threshold validate it
is a finite number (Number(...) + isFinite or parseInt with fallback) and clamp
to a sensible range before assigning transitionPct. Update the logic around the
thresholds and transitionPct variables in BudgetDrivenConfig.tsx to perform
these guards and use the safe fallback values.
- Around line 33-35: The onChange handler in BudgetDrivenConfig.tsx is
persisting raw Number(e.target.value) into config.transition_threshold which can
become NaN or out of 1–100 range; update the onChange logic to parse and
validate the input (e.g., parseInt/Number), clamp the resulting value to the
1–100 bounds, and fall back to a safe default (e.g., 1 or the previous
config.transition_threshold) if parsing yields NaN before calling onChange({
...config, transition_threshold: ... }); ensure you reference the onChange prop,
the config object and the transition_threshold field when making the change.

In `@web/src/pages/settings/ceremony-policy/strategies/CalendarConfig.tsx`:
- Around line 34-41: The Duration input currently uses Number(e.target.value)
which can produce NaN and propagate invalid state; update the onChange handler
for the InputField (the one rendering durationDays and calling onChange with
config.duration_days) to defensively parse and validate the value: read
e.target.value, convert with Number or parseInt then if result is NaN use a
sensible fallback (e.g., previous config.duration_days or 0), and clamp/validate
to the allowed range (1–90) before calling onChange({ ...config, duration_days:
validatedValue }). Ensure the same validation is applied when initializing or
reading durationDays so the component never passes NaN into the store.

In `@web/src/pages/settings/ceremony-policy/strategies/EventDrivenConfig.tsx`:
- Around line 15-22: EventDrivenConfig uses Number(e.target.value) directly
which can produce NaN and doesn’t enforce the 1-10000 range; update the
InputField onChange handler to parse and sanitize the input before calling
onChange: inside the EventDrivenConfig component read e.target.value, convert to
a number (e.g., Number or parseInt), if isNaN then fall back to the current
config.debounce_default (or a safe default like 1), then clamp the numeric value
between 1 and 10000 using Math.max/Math.min, and finally call onChange({
...config, debounce_default: sanitizedValue }); ensure the InputField value
remains String(debounceDefault) so UI reflects the sanitized value.

In `@web/src/pages/settings/ceremony-policy/strategies/ExternalTriggerConfig.tsx`:
- Around line 23-36: The LazyCodeMirrorEditor instance rendering the sources
JSON lacks an accessible name; add an explicit accessible label (e.g.,
aria-label="Sources JSON" or aria-labelledby referencing a visible label
element) to the LazyCodeMirrorEditor component so assistive technologies can
announce the control; update the JSX for LazyCodeMirrorEditor (the prop on the
component in ExternalTriggerConfig.tsx) to include that aria attribute while
preserving existing props like value, onChange, language, readOnly, and
className.

In `@web/src/pages/settings/ceremony-policy/strategies/MilestoneDrivenConfig.tsx`:
- Around line 25-31: The onChange handler in MilestoneDrivenConfig currently
accepts any valid JSON; parse the incoming val, then verify the parsed value is
an array (use Array.isArray) before calling onChange({ ...config, milestones:
parsed }); if it's not an array, do not call onChange and setJsonError to a
descriptive message (e.g., "Invalid JSON: expected array"); ensure
setJsonError(null) is only used when parsing succeeds and the shape check passes
so non-array JSON is rejected locally.

In `@web/src/pages/settings/ceremony-policy/strategies/TaskDrivenConfig.tsx`:
- Around line 40-53: The numeric inputs in TaskDrivenConfig.tsx (the InputField
handlers updating every_n_completions and sprint_percentage via onChange)
currently write raw Number(e.target.value) which allows NaN and out-of-range
values; clamp and validate the parsed value before calling onChange: parse the
input, fallback to a safe default or the existing config value when NaN, then
use Math.max/Math.min to enforce bounds (every_n_completions >= 1,
sprint_percentage between 1 and 100) and pass the bounded number into onChange
so invalid values are never saved to config.

In
`@web/src/pages/settings/ceremony-policy/strategies/ThroughputAdaptiveConfig.tsx`:
- Around line 20-40: The inputs currently call onChange with raw
Number(e.target.value) allowing out-of-range or NaN values; update the onChange
handlers in ThroughputAdaptiveConfig's InputField instances to parse the value,
coerce NaN to a safe default, and clamp it into the allowed ranges before
calling onChange (for velocity_drop_threshold_pct and
velocity_spike_threshold_pct clamp to 1–100, for measurement_window_tasks clamp
to 2–100); reference the existing props/identifiers (onChange, config,
velocity_drop_threshold_pct, velocity_spike_threshold_pct,
measurement_window_tasks, spikePct, window) and perform the validation/clamping
inline in each onChange callback so persisted config always respects the hint
bounds.

In `@web/src/pages/settings/ceremony-policy/StrategyChangeWarning.tsx`:
- Around line 17-30: The strategy-change banner currently rendered in the
StrategyChangeWarning component is dynamic but lacks live-region semantics;
update the outer div (the element rendering the banner that uses
CEREMONY_STRATEGY_LABELS with activeStrategy and currentStrategy) to include
appropriate ARIA attributes such as role="status" (or role="alert" if you want
assertive announcement), aria-live="polite" (or "assertive" for immediate), and
aria-atomic="true" so assistive tech reliably announces the change when the
element appears/disappears.

In `@web/src/pages/settings/ceremony-policy/StrategyPicker.tsx`:
- Line 28: Replace the unchecked cast in the SelectField onChange handler by
narrowing the incoming value `v` with an explicit type guard: check if `v` is
one of the allowed `CeremonyStrategyType` values (use an `includes` check
against the list/array of valid `CeremonyStrategyType` members, following the
pattern used in ArtifactFilters.tsx) and only call the `onChange` prop with the
value when the guard passes; otherwise handle/ignore invalid values. Target
symbols: the SelectField onChange handler, the local `v` parameter,
`CeremonyStrategyType`, and the `onChange` callback.

In `@web/src/stores/ceremony-policy.ts`:
- Around line 67-77: The per-department fetches (fetchDepartmentPolicy and
clearDepartmentPolicy) currently set the shared error state and can overwrite
unrelated errors; change error handling to use a per-department error map (e.g.,
departmentErrors: Map<string, string>) or a dedicated departmentError field so
each department's failures are tracked separately: update fetchDepartmentPolicy
and clearDepartmentPolicy to set/clear departmentErrors.get(name) on
success/failure instead of set({ error: ... }), and adjust the other affected
routines (the functions around lines 92-103) to read from departmentErrors for
department-specific UI messages while leaving the global error untouched.
- Around line 58-65: fetchActiveStrategy does not set loading state like
fetchResolvedPolicy; update fetchActiveStrategy to call set({ loading: true,
error: undefined }) before awaiting ceremonyApi.getActiveStrategy(), then set({
activeStrategy: active, loading: false }) on success and set({ error:
getErrorMessage(err), loading: false }) in the catch block so the UI spinner is
consistent with fetchResolvedPolicy; use the same set function and error helper
(getErrorMessage) as in the existing actions.

---

Outside diff comments:
In `@web/src/pages/SettingsPage.tsx`:
- Around line 385-421: The footerAction JSX for ns === 'observability' and ns
=== 'coordination' in SettingsPage duplicates the same Link/card structure;
extract that markup into a small shared component (e.g. SettingsActionCard with
props to, title, description) and replace the inline Link blocks used in
footerAction with <SettingsActionCard .../> calls (keep the existing classes and
the "Open" button styling), ensuring you remove the >8-line JSX from the
map/conditional and reuse the new component for both cases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a76e905b-f000-49ce-9841-13817dd86ef4

📥 Commits

Reviewing files that changed from the base of the PR and between 760bfbd and 2f10e8a.

📒 Files selected for processing (43)
  • CLAUDE.md
  • docs/design/ceremony-scheduling.md
  • src/synthorg/api/controllers/__init__.py
  • src/synthorg/api/controllers/ceremony_policy.py
  • src/synthorg/api/controllers/departments.py
  • src/synthorg/engine/workflow/ceremony_scheduler.py
  • src/synthorg/observability/events/api.py
  • src/synthorg/settings/definitions/coordination.py
  • tests/unit/api/controllers/test_ceremony_policy.py
  • tests/unit/settings/test_ceremony_settings.py
  • web/CLAUDE.md
  • web/src/api/endpoints/ceremony-policy.ts
  • web/src/api/types.ts
  • web/src/components/ui/inherit-toggle.stories.tsx
  • web/src/components/ui/inherit-toggle.tsx
  • web/src/components/ui/policy-source-badge.stories.tsx
  • web/src/components/ui/policy-source-badge.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/org-edit/DepartmentCeremonyOverride.stories.tsx
  • web/src/pages/org-edit/DepartmentCeremonyOverride.tsx
  • web/src/pages/org-edit/DepartmentEditDrawer.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
  • web/src/pages/settings/ceremony-policy/DepartmentOverridesPanel.tsx
  • web/src/pages/settings/ceremony-policy/PolicyFieldsPanel.tsx
  • web/src/pages/settings/ceremony-policy/StrategyChangeWarning.stories.tsx
  • web/src/pages/settings/ceremony-policy/StrategyChangeWarning.tsx
  • web/src/pages/settings/ceremony-policy/StrategyConfigPanel.tsx
  • web/src/pages/settings/ceremony-policy/StrategyPicker.stories.tsx
  • web/src/pages/settings/ceremony-policy/StrategyPicker.tsx
  • web/src/pages/settings/ceremony-policy/VelocityUnitIndicator.tsx
  • web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/CalendarConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/EventDrivenConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/ExternalTriggerConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/HybridConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/MilestoneDrivenConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/TaskDrivenConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/ThroughputAdaptiveConfig.tsx
  • web/src/router/index.tsx
  • web/src/router/routes.ts
  • web/src/stores/ceremony-policy.ts
  • web/src/utils/constants.ts

Comment on lines +164 to +179
_r.register(
SettingDefinition(
namespace=SettingNamespace.COORDINATION,
key="dept_ceremony_policies",
type=SettingType.JSON,
default="{}",
description=(
"Per-department ceremony policy overrides as JSON. "
"Keys are department names, values are partial "
"CeremonyPolicyConfig objects. Null values inherit "
"the project-level policy."
),
group="Ceremony Policy",
level=SettingLevel.ADVANCED,
)
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check if other JSON override settings in the codebase have yaml_path

# Find SettingDefinition registrations with type=JSON to see the pattern
rg -nA5 'type=SettingType.JSON' --type=py src/synthorg/settings/definitions/

Repository: Aureliolo/synthorg

Length of output: 7552


🏁 Script executed:

# Read the full coordination.py file around the flagged lines to see complete definitions
cat -n src/synthorg/settings/definitions/coordination.py | sed -n '164,196p'

Repository: Aureliolo/synthorg

Length of output: 1330


🏁 Script executed:

# Check if the documentation file exists and read it
fd 'ceremony-scheduling.md' docs/ && cat docs/design/ceremony-scheduling.md | head -100

Repository: Aureliolo/synthorg

Length of output: 4304


🏁 Script executed:

# See all yaml_path usage in coordination.py
rg -nB2 -A2 'yaml_path' src/synthorg/settings/definitions/coordination.py

Repository: Aureliolo/synthorg

Length of output: 1634


🏁 Script executed:

# Look for other override or policy settings in coordination.py to see the pattern
rg -n 'override|Override' src/synthorg/settings/definitions/coordination.py

Repository: Aureliolo/synthorg

Length of output: 233


🏁 Script executed:

# Check if there are any comments explaining DB-only vs YAML settings
rg -n -B3 'DB.only|runtime.only|yaml.path' src/synthorg/settings/ --type=py | head -50

Repository: Aureliolo/synthorg

Length of output: 3253


🏁 Script executed:

# Look at the full ceremony-scheduling.md to find any mention of YAML config paths
grep -i 'yaml\|config.*path\|override' docs/design/ceremony-scheduling.md | head -20

Repository: Aureliolo/synthorg

Length of output: 546


🏁 Script executed:

# Look for any comments or patterns near the override settings
cat -n src/synthorg/settings/definitions/coordination.py | sed -n '150,200p'

Repository: Aureliolo/synthorg

Length of output: 1847


🏁 Script executed:

# Check if there are any tests or usage patterns that reference these override settings
rg -n 'dept_ceremony_policies|ceremony_policy_overrides' --type=py src/synthorg/ | head -20

Repository: Aureliolo/synthorg

Length of output: 930


Add yaml_path for override settings to support YAML-based configuration.

The dept_ceremony_policies and ceremony_policy_overrides settings lack yaml_path, unlike all other ceremony policy settings in the same group (strategy, strategy_config, velocity_calculator, auto_transition, transition_threshold). The documentation explicitly describes a 3-level override hierarchy; these settings should be configurable via YAML files to match the pattern. Without yaml_path, they are DB-only despite being part of the documented configuration resolution chain.

Suggested paths:

  • dept_ceremony_policiesworkflow.sprint.ceremony_policy.dept_overrides or similar
  • ceremony_policy_overridesworkflow.sprint.ceremony_policy.ceremony_overrides or similar
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/settings/definitions/coordination.py` around lines 164 - 179,
The two SettingDefinition registrations for keys "dept_ceremony_policies" and
"ceremony_policy_overrides" are missing yaml_path metadata; update both
SettingDefinition calls to include a yaml_path string so these settings can be
loaded from YAML like the other ceremony settings (e.g., set
dept_ceremony_policies.yaml_path="workflow.sprint.ceremony_policy.dept_overrides"
and
ceremony_policy_overrides.yaml_path="workflow.sprint.ceremony_policy.ceremony_overrides"),
ensuring the yaml_path property is added to the SettingDefinition constructors
for those keys.

Comment on lines +58 to +65
fetchActiveStrategy: async () => {
try {
const active = await ceremonyApi.getActiveStrategy()
set({ activeStrategy: active })
} catch (err) {
set({ error: getErrorMessage(err) })
}
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

fetchActiveStrategy does not manage loading state.

Unlike fetchResolvedPolicy (lines 48-56), this action does not set loading: true before the request or loading: false after. If the UI relies on loading to show a spinner while fetching both resolved policy and active strategy, this inconsistency could cause visual glitches.

🐛 Proposed fix to add loading state management
   fetchActiveStrategy: async () => {
+    set({ loading: true, error: null })
     try {
       const active = await ceremonyApi.getActiveStrategy()
-      set({ activeStrategy: active })
+      set({ activeStrategy: active, loading: false })
     } catch (err) {
-      set({ error: getErrorMessage(err) })
+      set({ error: getErrorMessage(err), loading: false })
     }
   },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/stores/ceremony-policy.ts` around lines 58 - 65, fetchActiveStrategy
does not set loading state like fetchResolvedPolicy; update fetchActiveStrategy
to call set({ loading: true, error: undefined }) before awaiting
ceremonyApi.getActiveStrategy(), then set({ activeStrategy: active, loading:
false }) on success and set({ error: getErrorMessage(err), loading: false }) in
the catch block so the UI spinner is consistent with fetchResolvedPolicy; use
the same set function and error helper (getErrorMessage) as in the existing
actions.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

♻️ Duplicate comments (11)
web/src/pages/settings/ceremony-policy/strategies/TaskDrivenConfig.tsx (1)

40-40: ⚠️ Potential issue | 🟡 Minor

Enforce bounds on numeric inputs per documented constraints.

The isFinite checks are present, but the hints specify bounds ("min: 1" for completions, "1-100" for percentage) that aren't enforced. Users can enter 0 or negative values.

💡 Suggested bounded updates
- onChange={(e) => { const val = Number(e.target.value); if (Number.isFinite(val)) onChange({ ...config, every_n_completions: val }) }}
+ onChange={(e) => {
+   const val = Number(e.target.value)
+   if (!Number.isFinite(val) || val < 1) return
+   onChange({ ...config, every_n_completions: Math.floor(val) })
+ }}
...
- onChange={(e) => { const val = Number(e.target.value); if (Number.isFinite(val)) onChange({ ...config, sprint_percentage: val }) }}
+ onChange={(e) => {
+   const val = Number(e.target.value)
+   if (!Number.isFinite(val)) return
+   const bounded = Math.min(100, Math.max(1, val))
+   onChange({ ...config, sprint_percentage: bounded })
+ }}

Also applies to: 51-51

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/strategies/TaskDrivenConfig.tsx` at
line 40, The numeric inputs currently only check Number.isFinite but don't
enforce documented bounds; update the onChange handlers for the TaskDrivenConfig
inputs (the handler that reads e.target.value and calls onChange with
config.every_n_completions and the handler for config.top_percent) to validate
and clamp the parsed value to the allowed ranges (every_n_completions: minimum
1; top_percent: range 1–100) before invoking onChange, and ignore or coerce
invalid values so the config never receives 0, negative, or out-of-range
percentages.
web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx (2)

10-11: ⚠️ Potential issue | 🟠 Major

Guard unknown config values before array/number usage.

Direct casts from unknown can throw at render time if persisted data is malformed. If config.budget_thresholds is a string or object, thresholds.join(...) on line 17 will throw.

💡 Suggested hardening
-  const thresholds = (config.budget_thresholds as number[]) ?? [25, 50, 75, 100]
-  const transitionPct = (config.transition_threshold as number) ?? 100
+  const rawThresholds = config.budget_thresholds
+  const thresholds =
+    Array.isArray(rawThresholds) &&
+    rawThresholds.every((n): n is number => typeof n === 'number' && Number.isFinite(n))
+      ? rawThresholds
+      : [25, 50, 75, 100]
+
+  const rawTransition = config.transition_threshold
+  const transitionPct =
+    typeof rawTransition === 'number' && Number.isFinite(rawTransition) ? rawTransition : 100
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx`
around lines 10 - 11, The code casts unknown config values directly to
number[]/number (the thresholds and transitionPct variables in
BudgetDrivenConfig.tsx), which can throw at render time; update the logic that
sets thresholds and transitionPct to first validate types: for thresholds use
Array.isArray(config.budget_thresholds) and coerce/map each entry to a number
(filtering out non-numeric values) and fall back to [25,50,75,100] if validation
fails, and for transitionPct check typeof config.transition_threshold ===
"number" (or parse to number safely) and default to 100 on NaN/invalid input;
ensure downstream usage (e.g., thresholds.join(...)) is only called on the
validated array.

33-33: ⚠️ Potential issue | 🟡 Minor

Enforce numeric bounds before persisting transition_threshold.

The hint specifies "(1-100)" but the handler only checks isFinite, allowing out-of-range values like 0 or 500.

💡 Suggested bounded update
-        onChange={(e) => { const val = Number(e.target.value); if (Number.isFinite(val)) onChange({ ...config, transition_threshold: val }) }}
+        onChange={(e) => {
+          const val = Number(e.target.value)
+          if (!Number.isFinite(val)) return
+          const bounded = Math.min(100, Math.max(1, val))
+          onChange({ ...config, transition_threshold: bounded })
+        }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx` at
line 33, The onChange handler for transition_threshold in BudgetDrivenConfig.tsx
currently only checks Number.isFinite and allows out-of-range values; update the
handler referenced (the onChange arrow updating config.transition_threshold) to
parse the input (e.g., const val = Number(e.target.value)), enforce bounds
1..100 (either return early if val < 1 || val > 100 or clamp: val =
Math.min(100, Math.max(1, val))), and then call onChange({ ...config,
transition_threshold: val }) only with the validated/clamped numeric value to
ensure the stored transition_threshold is always within 1–100.
web/src/pages/settings/ceremony-policy/DepartmentOverridesPanel.tsx (1)

38-45: ⚠️ Potential issue | 🟠 Major

Fresh department overrides still start from the wrong baseline.

Line 45 seeds a new draft with { strategy: 'task_driven' }. Because Lines 60-63 later spread effectivePolicy, the first edit to threshold, velocity calculator, or strategy_config will persist task_driven even when the department was inheriting calendar, hybrid, etc. Seed from the resolved policy instead, or keep the draft empty and derive the displayed strategy from the resolved value until the user explicitly changes it.

Also applies to: 60-63, 115-121

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/DepartmentOverridesPanel.tsx` around
lines 38 - 45, The new-draft seeding in handleInheritChange (which calls
setLocalDraft({ strategy: 'task_driven' })) incorrectly fixes the strategy to
task_driven and gets persisted when the component later spreads effectivePolicy
(see the setLocalDraft use around effectivePolicy at lines ~60-63 and the other
draft creation at ~115-121); change this so a fresh draft is seeded from the
resolved effectivePolicy (use effectivePolicy.strategy and
effectivePolicy.strategy_config/thresholds as the initial values) or leave the
draft null/empty and derive displayed values from effectivePolicy until the user
makes an explicit change; update clearPolicy, setLocalDraft calls in
DepartmentOverridesPanel/handleInheritChange and the other draft-creation sites
to follow one of these two approaches so the initial persisted edit doesn't
overwrite an inherited strategy.
web/src/pages/settings/ceremony-policy/strategies/EventDrivenConfig.tsx (1)

10-10: ⚠️ Potential issue | 🟡 Minor

Sanitize debounce_default to the documented range before persisting it.

Line 19 filters out NaN, but it still stores 0, negatives, decimals, and arbitrarily large values (Number('') is 0); Line 10 also lets a persisted NaN bounce back into the control. That breaks the 1-10000 contract for this field.

Proposed fix
-  const debounceDefault = (config.debounce_default as number) ?? 5
+  const rawDebounceDefault = config.debounce_default
+  const debounceDefault =
+    typeof rawDebounceDefault === 'number' && Number.isFinite(rawDebounceDefault)
+      ? Math.min(10000, Math.max(1, Math.trunc(rawDebounceDefault)))
+      : 5
...
-        onChange={(e) => { const val = Number(e.target.value); if (Number.isFinite(val)) onChange({ ...config, debounce_default: val }) }}
+        onChange={(e) => {
+          const val = Number(e.target.value)
+          if (!Number.isFinite(val)) return
+          onChange({
+            ...config,
+            debounce_default: Math.min(10000, Math.max(1, Math.trunc(val))),
+          })
+        }}

Also applies to: 15-19

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/strategies/EventDrivenConfig.tsx` at
line 10, Sanitize config.debounce_default both when reading into the control and
before persisting: coerce it to a finite number, clamp it to the documented
range 1–10000, and convert to an integer (e.g., floor or round) so decimals, 0,
negatives, NaN and huge values are normalized. Update the places using
debounceDefault (the read: const debounceDefault = (config.debounce_default as
number) ?? 5 and the save/filter logic that currently only filters NaN) to apply
this clamp-and-integer coercion so the component never displays or stores values
outside 1..10000.
web/src/pages/settings/ceremony-policy/strategies/CalendarConfig.tsx (1)

20-21: ⚠️ Potential issue | 🟡 Minor

Reject invalid duration_days values before updating shared state.

Line 38 blocks NaN, but it still stores 0, negatives, decimals, and Number('') === 0; Line 21 also lets an already-invalid NaN render back as "NaN". That bypasses the documented 1-90 contract for this field.

Proposed fix
-  const durationDays = (config.duration_days as number) ?? 14
+  const rawDurationDays = config.duration_days
+  const durationDays =
+    typeof rawDurationDays === 'number' && Number.isFinite(rawDurationDays)
+      ? Math.min(90, Math.max(1, Math.trunc(rawDurationDays)))
+      : 14
...
-        onChange={(e) => { const val = Number(e.target.value); if (Number.isFinite(val)) onChange({ ...config, duration_days: val }) }}
+        onChange={(e) => {
+          const val = Number(e.target.value)
+          if (!Number.isFinite(val)) return
+          onChange({
+            ...config,
+            duration_days: Math.min(90, Math.max(1, Math.trunc(val))),
+          })
+        }}

Also applies to: 34-38

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/strategies/CalendarConfig.tsx` around
lines 20 - 21, The duration_days value must be validated/sanitized before
updating shared state: instead of assigning durationDays from
(config.duration_days as number) ?? 14 or allowing
Number('')/NaN/decimals/0/negatives to pass through, validate in the component's
initializer and in the change handler (e.g., where durationDays is read and
where you call the shared state updater such as setConfig/updateConfig). Accept
only integers between 1 and 90 (use Number.isInteger(parseInt(value, 10)) or
equivalent after parsing), reject or ignore updates that produce
NaN/0/negative/decimal/empty-string, and keep the previous valid value when an
invalid input is attempted; also ensure the rendered input never receives "NaN"
by coalescing invalid config.duration_days to a safe default or the last valid
value before rendering.
src/synthorg/api/controllers/departments.py (1)

515-547: ⚠️ Potential issue | 🟠 Major

Settings-backed department reads should be validated before returning 200.

This helper returns any dict stored under dept_ceremony_policies[department_name] before confirming the department still exists, and it never re-validates that dict with CeremonyPolicyConfig.model_validate(). A stale or malformed settings entry can therefore come back as a successful read instead of the 404/503 this endpoint advertises. As per coding guidelines, validate at system boundaries (user input, external APIs, config files).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/api/controllers/departments.py` around lines 515 - 547, The
settings-backed value returned by _get_dept_ceremony_override must be validated
before returning: first confirm the department exists (call the same
department-existence check used elsewhere) before using the policies mapping
from _load_dept_policies_json, and if a non-None dict is found run
CeremonyPolicyConfig.model_validate() on it; if validation raises, convert to
ServiceUnavailableError (or rethrow per existing error semantics), and if the
department does not exist raise NotFoundError instead of returning the settings
value. Ensure you still treat None as the explicit "inherit" sentinel.
src/synthorg/settings/definitions/coordination.py (1)

168-199: ⚠️ Potential issue | 🟠 Major

Wire the override-map settings into YAML too.

dept_ceremony_policies and ceremony_policy_overrides are the only ceremony-policy keys in this block without yaml_path, so those two levels can only exist in DB state. That breaks the feature’s YAML/code parity and the documented precedence chain for settings resolution. Based on learnings, settings use runtime-editable persistence with precedence: DB > env > YAML > code defaults.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/settings/definitions/coordination.py` around lines 168 - 199,
The two SettingDefinition entries for dept_ceremony_policies and
ceremony_policy_overrides are missing yaml_path so they cannot be set via YAML;
update their SettingDefinition instances (the ones with
key="dept_ceremony_policies" and key="ceremony_policy_overrides" under
SettingNamespace.COORDINATION) to include an appropriate yaml_path string
(matching the existing YAML naming convention used by other ceremony policy
settings) so the settings honor YAML precedence (DB > env > YAML > code
defaults) and restore YAML/code parity; keep type=SettingType.JSON and other
fields unchanged.
web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx (1)

153-163: ⚠️ Potential issue | 🟠 Major

Saving one policy across six independent writes is still non-atomic.

web/src/stores/settings.ts:126-165 sends each updateSetting() as a separate request and patches store state per key. If one request fails partway through, the backend is left with a mixed ceremony policy that this page cannot roll back or reconstruct reliably.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx` around lines
153 - 163, The current handleSave performs six independent updateSetting calls
(updateSetting('coordination', 'ceremony_strategy', ...), etc.), which is
non-atomic and can leave the backend in a partially-updated state; change to a
single atomic update by creating and calling a batch update API or store helper
(e.g., add/update a function like updateSettingsBatch or
updateCoordinationSettings) that accepts the entire ceremony policy payload
(strategy, strategy_config, velocity_calculator, auto_transition,
transition_threshold, ceremony_policy_overrides) and sends one request, and only
patch the local settings store after that single request succeeds; update
handleSave to call this new batch method (and remove the Promise.all multi-call
logic) so the six keys are saved atomically and the UI state is only updated on
success.
web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx (1)

41-47: ⚠️ Potential issue | 🟠 Major

Per-ceremony overrides still force a concrete strategy and still cannot edit strategy_config.

Turning override on seeds { strategy: 'task_driven' }, so a row that only wants to override threshold or auto-transition immediately stops inheriting strategy. The row also never renders StrategyConfigPanel, so milestone/calendar/etc. specific config cannot be viewed or changed at this level.

Also applies to: 87-103

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx` around lines 41
- 47, handleInheritChange currently forces a concrete strategy ({ strategy:
'task_driven' }) when turning inheritance off, which prevents per-ceremony rows
from editing strategy_config and from remaining strategy-agnostic; change
handleInheritChange to call onOverrideChange(name, {}) or otherwise create an
override object that preserves existing override fields (merge with existing
override if available) instead of setting strategy, and ensure the codepath that
decides to render StrategyConfigPanel checks for the presence of an override
object (e.g., override !== null) rather than a concrete strategy so
StrategyConfigPanel can render/edit strategy_config; apply the same fix to the
duplicate logic at the other block (the similar handler around the 87-103
range).
src/synthorg/api/controllers/ceremony_policy.py (1)

347-373: ⚠️ Potential issue | 🟠 Major

Resolved-policy lookup still hides broken department override settings.

This helper converts settings/JSON failures—and non-dict department values—into _SETTINGS_NOT_FOUND, then _fetch_department_policy() falls back to config. The resolved-policy endpoint can therefore report YAML/default sources even when the settings-backed override exists but is unreadable or corrupt.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/api/controllers/ceremony_policy.py` around lines 347 - 373, The
current broad except in _fetch_department_policy (around the call to
app_state.settings_service.get and json.loads(entry.value)) swallows JSON/typing
failures and returns _SETTINGS_NOT_FOUND so the caller falls back to defaults;
change the error handling to only treat a missing settings service/entry as
not-found and do not convert JSONDecodeError/TypeError/other parsing errors into
_SETTINGS_NOT_FOUND—log those errors (include exc_info) and re-raise (or return
an explicit error response) so corrupt/unreadable override data is surfaced;
update the try/except around json.loads(entry.value) and the handling of
policies (the policies variable and department_name lookup) accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/design/page-structure.md`:
- Around line 192-198: Update the Ceremony Policy documentation to describe the
full view/edit workflow by adding the missing "velocity-calculator" selection
control and the resolved-policy/source-badge view: explain that the page at
/settings/coordination/ceremony-policy supports both editing (strategy
selection, strategy-specific panels, auto-transition, overrides) and a read-only
resolved view populated from GET /ceremony-policy/resolved with a source badge
that indicates whether values come from department, ceremony, environment, or
system-managed settings; include notes on how the velocity-calculator selection
behaves and is persisted, and show how the resolved view and source badge map to
the PUT/GET department endpoints (`PUT /departments/{name}/ceremony-policy`,
`GET /departments/{name}/ceremony-policy`) and the editor so the doc reflects
the full view/edit workflow referenced by GET /ceremony-policy/resolved.

In `@src/synthorg/api/controllers/ceremony_policy.py`:
- Around line 321-329: The TaskGroup that spawns _fetch tasks lacks exception
handling so any failure in settings.get will propagate as an ExceptionGroup;
wrap the asyncio.TaskGroup block in a try/except* that catches ExceptionGroup
(or except* Exception as eg) and translate child task failures into the
appropriate service exception (e.g., raise ServiceUnavailableError with
contextual info from the ExceptionGroup), and update the _fetch_project_policy
docstring to document that child-task failures are converted to
ServiceUnavailableError (or whatever service-level error you choose). Ensure you
reference the existing helper _fetch and the settings.get call when building the
error message so the translated error clearly points to the failed
coordination/settings retrieval.

In `@tests/unit/settings/test_ceremony_settings.py`:
- Around line 24-35: Add a focused unit test that asserts the full definition
for the coordination/ceremony_policy_overrides setting (the key referenced by
"ceremony_policy_overrides") rather than only its existence: verify its
SettingType matches the intended type (e.g., mapping/list/complex type used for
per-ceremony overrides), confirm the default value is the expected empty
structure, and assert the setting's level/visibility is the correct one
(matching other ceremony settings). Model the new test on the existing
concrete-definition checks used for the other ceremony settings in this file
(see the checks around the block referenced at lines ~92-104) so it validates
type, default, and level for the per-ceremony override flow.

In `@web/src/pages/org-edit/DepartmentCeremonyOverride.tsx`:
- Around line 46-64: The override flow currently forces strategy and wipes
strategy_config: update handleInheritChange and handleStrategyChange so enabling
an override does not clobber existing strategy-specific config and changing
strategy only clears config when the strategy actually changes. Specifically, in
handleInheritChange avoid unconditionally seeding strategy:'task_driven'—create
the new override by preserving any existing policy.strategy and
policy.strategy_config if present (or leave them undefined) and only set
defaults when missing; in handleStrategyChange only reset strategy_config and
set velocity_calculator from STRATEGY_DEFAULT_VELOCITY_CALC[s] when s !==
policy?.strategy (otherwise preserve policy.strategy_config and
velocity_calculator); keep using onChange and policy to locate state.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx`:
- Around line 42-66: The snapshot guard resets form on any settingsEntries
change and reads ref.current during render; replace that pattern by storing the
parsed settingsSnapshot (from useMemo) in component state and use a useEffect to
compare specific ceremony fields (strategy, strategyConfig, velocityCalculator,
autoTransition, transitionThreshold) to decide when to reset local form—do not
rely on prevSnapshotRef identity or read refs during render; update the other
guards mentioned (the blocks around lines 78–85, 92–105, 109–112) to use the
same state+field-wise comparison. Also make handleSave atomic by batching the
six setting updates into a single API call or a server-side batch endpoint
(e.g., saveSettingsBatch or a single saveCeremonyPolicy request) instead of
firing independent requests so partial failures cannot leave the ceremony-policy
in an inconsistent state.

In `@web/src/pages/settings/ceremony-policy/DepartmentOverridesPanel.tsx`:
- Around line 24-25: The code is reading a store-wide saveError and rendering it
inside every department row; instead select the department-scoped save error
using the department key. Replace the top-level saveError selector (const
saveError = useCeremonyPolicyStore((s) => s.saveError)) with a selector that
looks up the error by dept.name (e.g. useCeremonyPolicyStore(s =>
s.saveErrors?.get(dept.name) or s => s.departmentErrors.get(dept.name)) and
update the render logic around the expanded row (lines referenced 89-94) to show
only that department-scoped save error alongside departmentError.

In `@web/src/pages/settings/ceremony-policy/strategies/ExternalTriggerConfig.tsx`:
- Around line 27-30: In ExternalTriggerConfig, don’t assign the JSON-parsed
value (parsed) directly into config.sources without validating its shape;
instead assert and validate the expected type (e.g., ensure
Array.isArray(parsed) or run a lightweight type guard for the expected element
shape) before calling onChange({ ...config, sources: parsed }), and if
validation fails call setJsonError with a clear message and avoid updating
config; also narrow the parsed variable from unknown to the validated type so
downstream code gets the correct shape.

In `@web/src/pages/settings/ceremony-policy/strategies/TaskDrivenConfig.tsx`:
- Around line 19-23: TaskDrivenConfig currently casts unknown config fields
directly (config.trigger, config.every_n_completions, config.sprint_percentage)
which can blow up if persisted data is malformed; update the component to
defensively read these properties by checking types (e.g., typeof config.trigger
=== 'string', typeof config.every_n_completions === 'number', typeof
config.sprint_percentage === 'number') and falling back to the existing defaults
('' / 5 / 50) when checks fail, then use those validated local variables
(trigger, everyN, pct) for rendering and onChange.

In `@web/src/stores/ceremony-policy.ts`:
- Around line 91-98: The updateDepartmentPolicy handler currently writes the
original `data` into the cache after calling
`ceremonyApi.updateDepartmentCeremonyPolicy(name, data)`; instead capture the
API response (the stored `CeremonyPolicyConfig`) and use that response when
updating the `departmentPolicies` Map so the UI reflects
server-normalized/stored values; locate the `updateDepartmentPolicy` function,
assign the result of `await ceremonyApi.updateDepartmentCeremonyPolicy(name,
data)` to a variable (e.g., `saved`), use `saved` when calling
`updated.set(name, ...)`, and ensure you still set `saving: false` and clear
`saveError` in the final state update.

---

Duplicate comments:
In `@src/synthorg/api/controllers/ceremony_policy.py`:
- Around line 347-373: The current broad except in _fetch_department_policy
(around the call to app_state.settings_service.get and json.loads(entry.value))
swallows JSON/typing failures and returns _SETTINGS_NOT_FOUND so the caller
falls back to defaults; change the error handling to only treat a missing
settings service/entry as not-found and do not convert
JSONDecodeError/TypeError/other parsing errors into _SETTINGS_NOT_FOUND—log
those errors (include exc_info) and re-raise (or return an explicit error
response) so corrupt/unreadable override data is surfaced; update the try/except
around json.loads(entry.value) and the handling of policies (the policies
variable and department_name lookup) accordingly.

In `@src/synthorg/api/controllers/departments.py`:
- Around line 515-547: The settings-backed value returned by
_get_dept_ceremony_override must be validated before returning: first confirm
the department exists (call the same department-existence check used elsewhere)
before using the policies mapping from _load_dept_policies_json, and if a
non-None dict is found run CeremonyPolicyConfig.model_validate() on it; if
validation raises, convert to ServiceUnavailableError (or rethrow per existing
error semantics), and if the department does not exist raise NotFoundError
instead of returning the settings value. Ensure you still treat None as the
explicit "inherit" sentinel.

In `@src/synthorg/settings/definitions/coordination.py`:
- Around line 168-199: The two SettingDefinition entries for
dept_ceremony_policies and ceremony_policy_overrides are missing yaml_path so
they cannot be set via YAML; update their SettingDefinition instances (the ones
with key="dept_ceremony_policies" and key="ceremony_policy_overrides" under
SettingNamespace.COORDINATION) to include an appropriate yaml_path string
(matching the existing YAML naming convention used by other ceremony policy
settings) so the settings honor YAML precedence (DB > env > YAML > code
defaults) and restore YAML/code parity; keep type=SettingType.JSON and other
fields unchanged.

In `@web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx`:
- Around line 41-47: handleInheritChange currently forces a concrete strategy ({
strategy: 'task_driven' }) when turning inheritance off, which prevents
per-ceremony rows from editing strategy_config and from remaining
strategy-agnostic; change handleInheritChange to call onOverrideChange(name, {})
or otherwise create an override object that preserves existing override fields
(merge with existing override if available) instead of setting strategy, and
ensure the codepath that decides to render StrategyConfigPanel checks for the
presence of an override object (e.g., override !== null) rather than a concrete
strategy so StrategyConfigPanel can render/edit strategy_config; apply the same
fix to the duplicate logic at the other block (the similar handler around the
87-103 range).

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx`:
- Around line 153-163: The current handleSave performs six independent
updateSetting calls (updateSetting('coordination', 'ceremony_strategy', ...),
etc.), which is non-atomic and can leave the backend in a partially-updated
state; change to a single atomic update by creating and calling a batch update
API or store helper (e.g., add/update a function like updateSettingsBatch or
updateCoordinationSettings) that accepts the entire ceremony policy payload
(strategy, strategy_config, velocity_calculator, auto_transition,
transition_threshold, ceremony_policy_overrides) and sends one request, and only
patch the local settings store after that single request succeeds; update
handleSave to call this new batch method (and remove the Promise.all multi-call
logic) so the six keys are saved atomically and the UI state is only updated on
success.

In `@web/src/pages/settings/ceremony-policy/DepartmentOverridesPanel.tsx`:
- Around line 38-45: The new-draft seeding in handleInheritChange (which calls
setLocalDraft({ strategy: 'task_driven' })) incorrectly fixes the strategy to
task_driven and gets persisted when the component later spreads effectivePolicy
(see the setLocalDraft use around effectivePolicy at lines ~60-63 and the other
draft creation at ~115-121); change this so a fresh draft is seeded from the
resolved effectivePolicy (use effectivePolicy.strategy and
effectivePolicy.strategy_config/thresholds as the initial values) or leave the
draft null/empty and derive displayed values from effectivePolicy until the user
makes an explicit change; update clearPolicy, setLocalDraft calls in
DepartmentOverridesPanel/handleInheritChange and the other draft-creation sites
to follow one of these two approaches so the initial persisted edit doesn't
overwrite an inherited strategy.

In `@web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx`:
- Around line 10-11: The code casts unknown config values directly to
number[]/number (the thresholds and transitionPct variables in
BudgetDrivenConfig.tsx), which can throw at render time; update the logic that
sets thresholds and transitionPct to first validate types: for thresholds use
Array.isArray(config.budget_thresholds) and coerce/map each entry to a number
(filtering out non-numeric values) and fall back to [25,50,75,100] if validation
fails, and for transitionPct check typeof config.transition_threshold ===
"number" (or parse to number safely) and default to 100 on NaN/invalid input;
ensure downstream usage (e.g., thresholds.join(...)) is only called on the
validated array.
- Line 33: The onChange handler for transition_threshold in
BudgetDrivenConfig.tsx currently only checks Number.isFinite and allows
out-of-range values; update the handler referenced (the onChange arrow updating
config.transition_threshold) to parse the input (e.g., const val =
Number(e.target.value)), enforce bounds 1..100 (either return early if val < 1
|| val > 100 or clamp: val = Math.min(100, Math.max(1, val))), and then call
onChange({ ...config, transition_threshold: val }) only with the
validated/clamped numeric value to ensure the stored transition_threshold is
always within 1–100.

In `@web/src/pages/settings/ceremony-policy/strategies/CalendarConfig.tsx`:
- Around line 20-21: The duration_days value must be validated/sanitized before
updating shared state: instead of assigning durationDays from
(config.duration_days as number) ?? 14 or allowing
Number('')/NaN/decimals/0/negatives to pass through, validate in the component's
initializer and in the change handler (e.g., where durationDays is read and
where you call the shared state updater such as setConfig/updateConfig). Accept
only integers between 1 and 90 (use Number.isInteger(parseInt(value, 10)) or
equivalent after parsing), reject or ignore updates that produce
NaN/0/negative/decimal/empty-string, and keep the previous valid value when an
invalid input is attempted; also ensure the rendered input never receives "NaN"
by coalescing invalid config.duration_days to a safe default or the last valid
value before rendering.

In `@web/src/pages/settings/ceremony-policy/strategies/EventDrivenConfig.tsx`:
- Line 10: Sanitize config.debounce_default both when reading into the control
and before persisting: coerce it to a finite number, clamp it to the documented
range 1–10000, and convert to an integer (e.g., floor or round) so decimals, 0,
negatives, NaN and huge values are normalized. Update the places using
debounceDefault (the read: const debounceDefault = (config.debounce_default as
number) ?? 5 and the save/filter logic that currently only filters NaN) to apply
this clamp-and-integer coercion so the component never displays or stores values
outside 1..10000.

In `@web/src/pages/settings/ceremony-policy/strategies/TaskDrivenConfig.tsx`:
- Line 40: The numeric inputs currently only check Number.isFinite but don't
enforce documented bounds; update the onChange handlers for the TaskDrivenConfig
inputs (the handler that reads e.target.value and calls onChange with
config.every_n_completions and the handler for config.top_percent) to validate
and clamp the parsed value to the allowed ranges (every_n_completions: minimum
1; top_percent: range 1–100) before invoking onChange, and ignore or coerce
invalid values so the config never receives 0, negative, or out-of-range
percentages.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b1c97ffd-a6d7-4d9d-a7d1-c541a5749d5e

📥 Commits

Reviewing files that changed from the base of the PR and between 2f10e8a and efb3481.

📒 Files selected for processing (24)
  • docs/design/page-structure.md
  • src/synthorg/api/controllers/ceremony_policy.py
  • src/synthorg/api/controllers/departments.py
  • src/synthorg/engine/workflow/ceremony_scheduler.py
  • src/synthorg/settings/definitions/coordination.py
  • tests/unit/settings/test_ceremony_settings.py
  • web/CLAUDE.md
  • web/src/api/types.ts
  • web/src/components/ui/inherit-toggle.tsx
  • web/src/components/ui/policy-source-badge.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/org-edit/DepartmentCeremonyOverride.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
  • web/src/pages/settings/ceremony-policy/DepartmentOverridesPanel.tsx
  • web/src/pages/settings/ceremony-policy/StrategyConfigPanel.tsx
  • web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/CalendarConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/EventDrivenConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/ExternalTriggerConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/MilestoneDrivenConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/TaskDrivenConfig.tsx
  • web/src/router/index.tsx
  • web/src/stores/ceremony-policy.ts

Comment on lines +24 to +25
const saveError = useCeremonyPolicyStore((s) => s.saveError)
const departmentError = useCeremonyPolicyStore((s) => s.departmentErrors.get(dept.name))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Scope save failures to the department row that actually failed.

Line 24 reads a store-wide saveError, but Line 93 renders it inside every expanded row. A project-level failure or another department's failed save will therefore appear in unrelated departments next to their local departmentError.

Also applies to: 89-94

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/DepartmentOverridesPanel.tsx` around
lines 24 - 25, The code is reading a store-wide saveError and rendering it
inside every department row; instead select the department-scoped save error
using the department key. Replace the top-level saveError selector (const
saveError = useCeremonyPolicyStore((s) => s.saveError)) with a selector that
looks up the error by dept.name (e.g. useCeremonyPolicyStore(s =>
s.saveErrors?.get(dept.name) or s => s.departmentErrors.get(dept.name)) and
update the render logic around the expanded row (lines referenced 89-94) to show
only that department-scoped save error alongside departmentError.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

♻️ Duplicate comments (5)
web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx (1)

13-15: ⚠️ Potential issue | 🟠 Major

Harden transition_threshold normalization on read.

typeof config.transition_threshold === 'number' still accepts NaN/Infinity and out-of-range values. That can render invalid state ("NaN"/unexpected number) before the user edits the field.

Suggested fix
-  const transitionPct = typeof config.transition_threshold === 'number'
-    ? config.transition_threshold
-    : 100
+  const rawTransition = config.transition_threshold
+  const transitionPct =
+    typeof rawTransition === 'number' && Number.isFinite(rawTransition)
+      ? Math.min(100, Math.max(1, Math.round(rawTransition)))
+      : 100
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx`
around lines 13 - 15, The current normalization for transitionPct accepts
NaN/Infinity and out-of-range values; change the read for
config.transition_threshold (used to compute transitionPct in
BudgetDrivenConfig.tsx) to first coerce/parse a numeric value (if needed), then
verify it's finite (Number.isFinite) and clamp it into the 0–100 range, falling
back to 100 when the value is invalid; update the transitionPct assignment to
use this validated/clamped value so the component never receives NaN/Infinity or
values outside 0–100.
src/synthorg/api/controllers/departments.py (1)

542-562: ⚠️ Potential issue | 🟠 Major

Corrupt per-department entries still get masked as inheritance.

Once department_name is present in policies, any non-None value here is persisted state. Returning None for an invalid dict or unexpected type makes the GET path look like “inherit” and lets the dashboard overwrite unreadable data on the next save. This should raise ServiceUnavailableError instead. As per coding guidelines, validate at system boundaries (user input, external APIs, config files).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/api/controllers/departments.py` around lines 542 - 562, The
current branch in departments.ceremony_policy.get masks corrupt persisted
entries as inheritance by returning None; instead detect invalid stored values
(when policies[department_name] is a dict that fails
CeremonyPolicyConfig.model_validate or any unexpected type) and raise
ServiceUnavailableError so callers know the stored state is unreadable. Adjust
the exception handling around CeremonyPolicyConfig.model_validate to use proper
tuple-catching (except (MemoryError, RecursionError): raise) and on other
exceptions log the invalid stored override (using logger.warning as shown) and
then raise ServiceUnavailableError (not return None); also handle non-dict
unexpected types similarly by logging and raising ServiceUnavailableError.
web/src/pages/settings/ceremony-policy/DepartmentOverridesPanel.tsx (1)

34-45: ⚠️ Potential issue | 🟠 Major

Seed new department drafts from the resolved policy, not {}.

When a department currently inherits a non-default policy, policy ?? {} makes the editor open as task_driven with an empty strategy config. The first edit is then made against the wrong strategy/config panel, even though the effective department policy may be something else. Clone the resolved department policy as the initial draft here instead of starting from an empty object.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/DepartmentOverridesPanel.tsx` around
lines 34 - 45, The toggle handler seeds new drafts from the wrong source: change
the else branch in handleInheritChange so it seeds setLocalDraft with the
resolved/effective policy (the one used by effectivePolicy) rather than policy
?? {}; i.e., clone effectivePolicy (not a shared reference) when calling
setLocalDraft to initialize the editor (use structured clone or
{...effectivePolicy} / JSON methods) and keep clearPolicy(dept.name) for the
inherit path.
docs/design/page-structure.md (1)

336-336: ⚠️ Potential issue | 🟡 Minor

Expand this route description to match the page.

This row still reads like an editor-only page. It omits the resolved-policy/source-badge flow and velocity-calculator behavior that are documented above, so the routing table is still incomplete here. Based on learnings, update the relevant docs/design/ page to reflect the new reality.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/design/page-structure.md` at line 336, Update the
`/settings/coordination/ceremony-policy` row description to match the actual
page behavior: replace the editor-only wording with a summary that includes the
resolved-policy/source-badge flow and the velocity-calculator behavior (how
resolved policies are surfaced, source badges indicate origin, and how
velocity-calculator affects metrics/overrides), and ensure the routing table
entry for `/settings/coordination/ceremony-policy` lists strategy selection,
department and per-ceremony overrides plus the resolved-policy/source-badge flow
and velocity-calculator effects so the docs/design/page-structure.md reflects
the real page functionality.
web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx (1)

39-48: ⚠️ Potential issue | 🟠 Major

Per-ceremony override editing is still incomplete.

For inherited ceremonies, this row initializes from {} and falls back to task_driven, so the user never sees the actual effective strategy/config they are overriding. The expanded editor also omits StrategyConfigPanel, which makes strategy_config impossible to view or edit for strategies like event_driven, external_trigger, and milestone_driven. Pass the resolved ceremony policy into this row, seed new overrides from it, and reuse StrategyConfigPanel here.

Also applies to: 88-103

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx` around lines 39
- 48, The per-ceremony override row currently initializes overrides from {} and
uses a fallback 'task_driven', so users can't see the effective policy or edit
strategy_config; update CeremonyListPanel to compute a resolvedPolicy (policy
merged with defaults) and pass that resolvedPolicy as the initial value into the
row and to the expanded editor; change handleInheritChange and the
onOverrideChange calls to seed new overrides from resolvedPolicy (not {} or
policy ?? {}) so inherited rows show effective values, and reuse
StrategyConfigPanel inside the expanded editor so strategy_config for strategies
like event_driven/external_trigger/milestone_driven is viewable and editable;
apply the same fix to the other override-initialization instance that mirrors
this logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/synthorg/api/controllers/ceremony_policy.py`:
- Around line 175-181: The function _parse_auto_transition contains a redundant
check for None followed by a broader falsy check; remove the first `if raw is
None:` branch and keep a single `if not raw: return None` (or equivalently
handle empty/falsy values once) so the function returns None for None/empty
input and otherwise returns raw.lower() == "true".
- Around line 380-388: The JSON parse error for policies
(json.loads(entry.value)) is currently re-raised raw; instead catch
JSONDecodeError/TypeError and raise a sanitized API-level exception (e.g.,
ServiceUnavailableError or a ValidationError) with a non-sensitive message like
"Malformed ceremony policies data" while chaining the original exception (raise
ServiceUnavailableError("Malformed ceremony policies data") from exc) so
internal details are preserved in logs but not returned to consumers; update the
logger.warning call in the same except block (still referencing
endpoint="ceremony_policy.fetch_dept" and entry) to avoid printing the raw
exception to external responses.

In `@src/synthorg/api/controllers/departments.py`:
- Around line 458-475: The current try/except around
app_state.settings_service.get and json.loads swallows errors and can re-raise
raw exceptions; update the error handling in the
departments.ceremony_policy.load block to catch all non-fatal exceptions from
app_state.settings_service.get and json.loads, log a WARNING (using
logger.warning with API_REQUEST_ERROR,
endpoint="departments.ceremony_policy.load" and the error context), and then
raise the controller's ServiceUnavailableError (or the equivalent
service-unavailable exception used by this controller) when raise_on_error is
True, otherwise return {} — preserving the existing MemoryError and
RecursionError re-raises and keeping the same log message fields (error="failed
to load dept_ceremony_policies", exc_info=True).
- Around line 582-635: The in-process _dept_policy_lock in
_set_dept_ceremony_override and _clear_dept_ceremony_override does not prevent
cross-worker races when both functions read/modify/save the shared
dept_ceremony_policies blob; replace the read-modify-write with an atomic
operation: either use the settings service's compare-and-swap/versioned write
API when calling _load_dept_policies_json/_save_dept_policies_json (read the
current version/token and call a save that fails on version mismatch and retry),
or change the storage model to persist one key/record per department and update
only that key atomically; remove reliance on _dept_policy_lock for cross-process
safety and add retry/error handling for version conflicts in
_set_dept_ceremony_override and _clear_dept_ceremony_override.

In `@web/src/pages/org-edit/DepartmentCeremonyOverride.tsx`:
- Around line 119-124: The onChange handler for the Transition Threshold input
currently does onChange({ ...policy, transition_threshold:
Number(e.target.value) }) which persists Number('') => 0 and Number(invalid) =>
NaN; update the handler in the InputField for transition_threshold to read
e.target.valueAsNumber, ignore the change if the value is not finite,
clamp/enforce the value into the 0.01–1.0 range, and only then call onChange
with the updated policy object (preserving policy and setting
transition_threshold to the validated number).
- Around line 46-52: The component DepartmentCeremonyOverride currently replaces
missing keys by calling onChange(policy ?? {}) in handleInheritChange, which
causes sparse overrides to be expanded into concrete defaults and misrepresents
the effective policy; change this to either accept and use the
already-resolved/effective policy passed into the component (i.e., ensure the
parent resolves ceremony_policy and that DepartmentCeremonyOverride reads that
resolved object) or preserve sparsity by passing through a shallow copy of
policy without substituting defaults (so missing keys remain undefined) when
calling onChange; update handleInheritChange and any similar branches (also
referenced in the block around lines 96-124) to avoid injecting default values
for unset fields and to document that the parent must supply the resolved policy
if the UI should show effective values.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx`:
- Around line 169-177: The departments fetch has no loading state so the UI can
render an empty DepartmentOverridesPanel; add a deptLoading state (e.g.,
deptLoading / setDeptLoading) and set it true before calling the dynamically
imported listDepartments, set it false in both success and error paths
(finally), and only render DepartmentOverridesPanel (or show a
skeleton/placeholder) when deptLoading is false; update the useEffect that
imports listDepartments and handles setDepartments/setDeptLoadError/addToast to
toggle deptLoading appropriately so consumers can avoid showing an empty list
while fetching.

In `@web/src/pages/settings/ceremony-policy/strategies/CalendarConfig.tsx`:
- Around line 19-22: The CalendarConfig component currently uses type assertions
for frequency and durationDays which can let invalid types slip through; update
CalendarConfig to extract values defensively by checking typeof on
config.frequency and config.duration_days (e.g., if (typeof config.frequency ===
'string') use it else fallback to '' and if (typeof config.duration_days ===
'number') use it else fallback to 14) so the component mirrors the runtime
guards used in TaskDrivenConfig; reference the CalendarConfig function and the
frequency and durationDays variables when making this change and ensure
onChange/disabled usage remains unchanged.

In `@web/src/pages/settings/ceremony-policy/strategies/ExternalTriggerConfig.tsx`:
- Around line 13-14: rawJson is only initialized once and becomes stale when the
prop config.sources changes; add a useEffect that watches config.sources and
calls setRawJson(JSON.stringify(config.sources ?? [], null, 2)) to keep the
editor in sync (keep jsonError handling as-is), i.e. import useEffect if needed
and update ExternalTriggerConfig to update rawJson whenever config.sources
changes rather than relying solely on the initial useState value.

---

Duplicate comments:
In `@docs/design/page-structure.md`:
- Line 336: Update the `/settings/coordination/ceremony-policy` row description
to match the actual page behavior: replace the editor-only wording with a
summary that includes the resolved-policy/source-badge flow and the
velocity-calculator behavior (how resolved policies are surfaced, source badges
indicate origin, and how velocity-calculator affects metrics/overrides), and
ensure the routing table entry for `/settings/coordination/ceremony-policy`
lists strategy selection, department and per-ceremony overrides plus the
resolved-policy/source-badge flow and velocity-calculator effects so the
docs/design/page-structure.md reflects the real page functionality.

In `@src/synthorg/api/controllers/departments.py`:
- Around line 542-562: The current branch in departments.ceremony_policy.get
masks corrupt persisted entries as inheritance by returning None; instead detect
invalid stored values (when policies[department_name] is a dict that fails
CeremonyPolicyConfig.model_validate or any unexpected type) and raise
ServiceUnavailableError so callers know the stored state is unreadable. Adjust
the exception handling around CeremonyPolicyConfig.model_validate to use proper
tuple-catching (except (MemoryError, RecursionError): raise) and on other
exceptions log the invalid stored override (using logger.warning as shown) and
then raise ServiceUnavailableError (not return None); also handle non-dict
unexpected types similarly by logging and raising ServiceUnavailableError.

In `@web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx`:
- Around line 39-48: The per-ceremony override row currently initializes
overrides from {} and uses a fallback 'task_driven', so users can't see the
effective policy or edit strategy_config; update CeremonyListPanel to compute a
resolvedPolicy (policy merged with defaults) and pass that resolvedPolicy as the
initial value into the row and to the expanded editor; change
handleInheritChange and the onOverrideChange calls to seed new overrides from
resolvedPolicy (not {} or policy ?? {}) so inherited rows show effective values,
and reuse StrategyConfigPanel inside the expanded editor so strategy_config for
strategies like event_driven/external_trigger/milestone_driven is viewable and
editable; apply the same fix to the other override-initialization instance that
mirrors this logic.

In `@web/src/pages/settings/ceremony-policy/DepartmentOverridesPanel.tsx`:
- Around line 34-45: The toggle handler seeds new drafts from the wrong source:
change the else branch in handleInheritChange so it seeds setLocalDraft with the
resolved/effective policy (the one used by effectivePolicy) rather than policy
?? {}; i.e., clone effectivePolicy (not a shared reference) when calling
setLocalDraft to initialize the editor (use structured clone or
{...effectivePolicy} / JSON methods) and keep clearPolicy(dept.name) for the
inherit path.

In `@web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx`:
- Around line 13-15: The current normalization for transitionPct accepts
NaN/Infinity and out-of-range values; change the read for
config.transition_threshold (used to compute transitionPct in
BudgetDrivenConfig.tsx) to first coerce/parse a numeric value (if needed), then
verify it's finite (Number.isFinite) and clamp it into the 0–100 range, falling
back to 100 when the value is invalid; update the transitionPct assignment to
use this validated/clamped value so the component never receives NaN/Infinity or
values outside 0–100.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 197492dd-38a6-4a53-b570-312780f10849

📥 Commits

Reviewing files that changed from the base of the PR and between efb3481 and 51978b1.

📒 Files selected for processing (15)
  • docs/design/page-structure.md
  • src/synthorg/api/controllers/ceremony_policy.py
  • src/synthorg/api/controllers/departments.py
  • src/synthorg/settings/definitions/coordination.py
  • tests/unit/settings/test_ceremony_settings.py
  • web/src/pages/org-edit/DepartmentCeremonyOverride.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
  • web/src/pages/settings/ceremony-policy/DepartmentOverridesPanel.tsx
  • web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/CalendarConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/EventDrivenConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/ExternalTriggerConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/TaskDrivenConfig.tsx
  • web/src/stores/ceremony-policy.ts

Comment on lines +582 to +635
_dept_policy_lock = asyncio.Lock()


async def _set_dept_ceremony_override(
app_state: AppState,
department_name: NotBlankStr,
policy: dict[str, Any],
) -> None:
"""Set the ceremony policy override for a department.

Args:
app_state: Application state.
department_name: Department name.
policy: Validated ceremony policy dict.

Raises:
ServiceUnavailableError: If the settings service or JSON
blob cannot be loaded (prevents data loss from
writing over unreadable state).
"""
async with _dept_policy_lock:
policies = await _load_dept_policies_json(
app_state,
raise_on_error=True,
)
policies[department_name] = copy.deepcopy(policy)
await _save_dept_policies_json(app_state, policies)


async def _clear_dept_ceremony_override(
app_state: AppState,
department_name: NotBlankStr,
) -> None:
"""Clear the ceremony policy override for a department.

Persists a ``None`` sentinel so the department explicitly
inherits the project-level policy, even if the config YAML
defines a ``ceremony_policy`` for the department.

Args:
app_state: Application state.
department_name: Department name.

Raises:
ServiceUnavailableError: If the settings service or JSON
blob cannot be loaded.
"""
async with _dept_policy_lock:
policies = await _load_dept_policies_json(
app_state,
raise_on_error=True,
)
policies[department_name] = None
await _save_dept_policies_json(app_state, policies)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

This lock does not protect cross-worker writes.

_dept_policy_lock only serializes coroutines inside one process, but PUT/DELETE still do read-modify-write on the shared dept_ceremony_policies blob. If the API runs with multiple workers or replicas, two requests can read the same snapshot and last-write-wins each other. This needs compare-and-swap/versioned writes in the settings layer, or a storage model with one persisted record/key per department.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/api/controllers/departments.py` around lines 582 - 635, The
in-process _dept_policy_lock in _set_dept_ceremony_override and
_clear_dept_ceremony_override does not prevent cross-worker races when both
functions read/modify/save the shared dept_ceremony_policies blob; replace the
read-modify-write with an atomic operation: either use the settings service's
compare-and-swap/versioned write API when calling
_load_dept_policies_json/_save_dept_policies_json (read the current
version/token and call a save that fails on version mismatch and retry), or
change the storage model to persist one key/record per department and update
only that key atomically; remove reliance on _dept_policy_lock for cross-process
safety and add retry/error handling for version conflicts in
_set_dept_ceremony_override and _clear_dept_ceremony_override.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

♻️ Duplicate comments (14)
web/src/pages/settings/ceremony-policy/strategies/ThroughputAdaptiveConfig.tsx (1)

16-41: ⚠️ Potential issue | 🟠 Major

Enforce numeric bounds before persisting config values.

Lines 20, 29, and 38 store raw Number(e.target.value) without NaN checks or range validation, despite hints specifying (1-100) and (2-100) constraints. This allows invalid values to propagate to the store.

🐛 Proposed fix to add validation and clamping
       <InputField
         label="Velocity Drop Threshold (%)"
         type="number"
         value={String(dropPct)}
-        onChange={(e) => onChange({ ...config, velocity_drop_threshold_pct: Number(e.target.value) })}
+        onChange={(e) => {
+          const val = Number(e.target.value)
+          if (!Number.isFinite(val)) return
+          onChange({ ...config, velocity_drop_threshold_pct: Math.min(100, Math.max(1, Math.round(val))) })
+        }}
         disabled={disabled}
         hint="Trigger ceremony when velocity drops by this percentage (1-100)"
       />

       <InputField
         label="Velocity Spike Threshold (%)"
         type="number"
         value={String(spikePct)}
-        onChange={(e) => onChange({ ...config, velocity_spike_threshold_pct: Number(e.target.value) })}
+        onChange={(e) => {
+          const val = Number(e.target.value)
+          if (!Number.isFinite(val)) return
+          onChange({ ...config, velocity_spike_threshold_pct: Math.min(100, Math.max(1, Math.round(val))) })
+        }}
         disabled={disabled}
         hint="Trigger ceremony when velocity spikes by this percentage (1-100)"
       />

       <InputField
         label="Measurement Window (tasks)"
         type="number"
         value={String(window)}
-        onChange={(e) => onChange({ ...config, measurement_window_tasks: Number(e.target.value) })}
+        onChange={(e) => {
+          const val = Number(e.target.value)
+          if (!Number.isFinite(val)) return
+          onChange({ ...config, measurement_window_tasks: Math.min(100, Math.max(2, Math.round(val))) })
+        }}
         disabled={disabled}
         hint="Rolling window of task completions for rate calculation (2-100)"
       />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/src/pages/settings/ceremony-policy/strategies/ThroughputAdaptiveConfig.tsx`
around lines 16 - 41, The three InputField onChange handlers in
ThroughputAdaptiveConfig.tsx currently write raw Number(e.target.value) into
config (velocity_drop_threshold_pct, velocity_spike_threshold_pct,
measurement_window_tasks) allowing NaN/out-of-range values; update each handler
to parse the input safely (e.g. Number or parseInt), guard against NaN (fall
back to previous value or a sensible default), and clamp to the allowed ranges:
velocity_drop_threshold_pct and velocity_spike_threshold_pct to 1–100, and
measurement_window_tasks to 2–100, then call onChange with the validated/clamped
value so only valid numbers are persisted.
web/src/components/ui/policy-source-badge.stories.tsx (1)

13-23: ⚠️ Potential issue | 🟠 Major

Add required state stories for this shared UI component.

The file documents source variants, but it still lacks the mandatory state set (hover/loading/error/empty).

Proposed story additions
 export const Default: Story = {
   args: { source: 'default' },
 }
+
+export const Hover: Story = {
+  args: { source: 'project' },
+  parameters: { pseudo: { hover: true } },
+}
+
+export const Loading: Story = {
+  args: { source: 'default' },
+  parameters: {
+    docs: { description: { story: 'Loading state representation in parent container.' } },
+  },
+}
+
+export const Error: Story = {
+  args: { source: 'default' },
+  parameters: {
+    docs: { description: { story: 'Error-state representation in parent container.' } },
+  },
+}
+
+export const Empty: Story = {
+  args: { source: 'default' },
+  parameters: {
+    docs: { description: { story: 'Empty/no-data state representation in parent container.' } },
+  },
+}
As per coding guidelines, “When creating new shared components… create accompanying `.stories.tsx` with all states (default, hover, loading, error, empty).”
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/ui/policy-source-badge.stories.tsx` around lines 13 - 23,
The story file lists Project, Department and Default stories but is missing the
required state stories (hover, loading, error, empty) for the shared
PolicySourceBadge component; add four new Story exports named Hover, Loading,
Error, and Empty that target the same component and set appropriate args/props
(e.g., source values and a loading boolean or error message prop) and any
necessary decorators or interaction/play setups to simulate hover state so the
component renders each required state consistently with the existing
Project/Department/Default stories.
web/src/components/ui/inherit-toggle.stories.tsx (1)

13-23: ⚠️ Potential issue | 🟠 Major

Add the required shared-component state stories (hover/loading/error/empty).

This story currently covers only default variants and still misses the mandatory state set for shared UI components.

Proposed story additions
 export const Disabled: Story = {
   args: { inherit: true, onChange: () => {}, disabled: true },
 }
+
+export const Hover: Story = {
+  args: { inherit: true, onChange: () => {} },
+  parameters: { pseudo: { hover: true } },
+}
+
+export const Loading: Story = {
+  args: { inherit: true, onChange: () => {}, disabled: true },
+  parameters: {
+    docs: { description: { story: 'Loading/locked interaction state.' } },
+  },
+}
+
+export const Error: Story = {
+  args: { inherit: false, onChange: () => {} },
+  parameters: {
+    docs: { description: { story: 'Error-state usage example in parent form context.' } },
+  },
+}
+
+export const Empty: Story = {
+  args: { inherit: true, onChange: () => {} },
+  parameters: {
+    docs: { description: { story: 'Empty/default container state example.' } },
+  },
+}
As per coding guidelines, “When creating new shared components… create accompanying `.stories.tsx` with all states (default, hover, loading, error, empty).”
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/ui/inherit-toggle.stories.tsx` around lines 13 - 23, Add
the missing shared-component state stories (hover, loading, error, empty) to the
existing inherit-toggle.stories.tsx so the shared UI component covers all
required states; create new Story exports named Hover, Loading, Error, and Empty
alongside the existing Inherit, Override, and Disabled exports, each supplying
appropriate args/props (e.g., inherit true/false, disabled where needed) and, if
necessary, a play/parameters/decorator to simulate hover state and to present
loading/error/empty visuals consistent with the component’s props or state
handlers (reference the Story type and the existing Inherit/Override/Disabled
exports to match pattern and naming).
web/src/pages/settings/ceremony-policy/StrategyChangeWarning.tsx (1)

17-29: ⚠️ Potential issue | 🟡 Minor

Add live-region semantics for the dynamic warning banner.

Because this element appears conditionally, Line 17 should expose polite status semantics so assistive tech announces the change consistently.

♿ Suggested fix
-    <div className="flex items-start gap-2 rounded-md border border-warning/30 bg-warning/5 p-card">
+    <div
+      className="flex items-start gap-2 rounded-md border border-warning/30 bg-warning/5 p-card"
+      role="status"
+      aria-live="polite"
+      aria-atomic="true"
+    >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/StrategyChangeWarning.tsx` around
lines 17 - 29, The dynamic warning banner in StrategyChangeWarning should expose
live-region semantics so assistive tech announces it; update the top-level
container div (the element rendering the AlertTriangle and text) to include
live-region attributes (e.g., role="status" and aria-live="polite", and
optionally aria-atomic="true") so changes are announced consistently when the
component renders/changes.
web/src/pages/settings/ceremony-policy/StrategyPicker.tsx (1)

28-28: ⚠️ Potential issue | 🟡 Minor

Guard select values instead of force-casting to CeremonyStrategyType.

Line 28 trusts arbitrary SelectField output via cast. Add a runtime type guard against CEREMONY_STRATEGY_TYPES before invoking onChange.

🔧 Proposed fix
+const isCeremonyStrategyType = (v: string): v is CeremonyStrategyType =>
+  CEREMONY_STRATEGY_TYPES.includes(v as CeremonyStrategyType)
+
 export function StrategyPicker({ value, onChange, disabled }: StrategyPickerProps) {
   return (
@@
-        onChange={(v) => onChange(v as CeremonyStrategyType)}
+        onChange={(v) => {
+          if (isCeremonyStrategyType(v)) onChange(v)
+        }}
         disabled={disabled}
       />
#!/bin/bash
set -euo pipefail

echo "== SelectField onChange contract =="
SELECT_FILE="$(fd -i '^select-field\.tsx$' web/src/components/ui | head -n1)"
rg -n -C3 'onChange' "$SELECT_FILE"

echo
echo "== Current StrategyPicker cast site =="
rg -n -C3 'onChange=\{\(v\) => onChange\(v as CeremonyStrategyType\)\}' web/src/pages/settings/ceremony-policy/StrategyPicker.tsx

echo
echo "== Strategy type domain =="
rg -n -C2 'type CeremonyStrategyType|CEREMONY_STRATEGY_TYPES' web/src/api/types.ts web/src/utils/constants.ts

Based on learnings: “TypeScript strict mode must pass type checking” and “Validate at system boundaries (user input, external APIs, config files)”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/StrategyPicker.tsx` at line 28,
Replace the unsafe cast in StrategyPicker's SelectField onChange handler with a
runtime guard: check that the incoming value exists in the
CEREMONY_STRATEGY_TYPES set/array (or matches a predicate for
CeremonyStrategyType) before calling the onChange prop; if it passes, call
onChange(value as CeremonyStrategyType), otherwise ignore or handle the invalid
input (e.g., log or no-op). Locate the handler where onChange={(v) => onChange(v
as CeremonyStrategyType)} and use CEREMONY_STRATEGY_TYPES and
CeremonyStrategyType to validate the value first.
web/src/pages/org-edit/DepartmentCeremonyOverride.tsx (2)

41-44: ⚠️ Potential issue | 🟠 Major

Move render-time state sync into useEffect.

Line 43 updates state during render; this should be effect-driven to avoid render-phase updates.

♻️ Suggested fix
-import { useCallback, useRef, useState } from 'react'
+import { useCallback, useEffect, useState } from 'react'
@@
-  const prevHasOverrideRef = useRef(hasOverride)
-
-  // Sync expanded state when override status changes externally
-  if (prevHasOverrideRef.current !== hasOverride) {
-    prevHasOverrideRef.current = hasOverride
-    setExpanded(hasOverride)
-  }
+  useEffect(() => {
+    setExpanded(hasOverride)
+  }, [hasOverride])
#!/bin/bash
# Verify render-phase state updates in this component
rg -n "setExpanded\\(|prevHasOverrideRef|useEffect\\(" web/src/pages/org-edit/DepartmentCeremonyOverride.tsx
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/org-edit/DepartmentCeremonyOverride.tsx` around lines 41 - 44,
The render-phase update of state should be moved into an effect: inside the
DepartmentCeremonyOverride component, remove the direct render-time block that
checks prevHasOverrideRef.current !== hasOverride and instead create a useEffect
that depends on hasOverride; within that effect compare
prevHasOverrideRef.current to hasOverride, set prevHasOverrideRef.current =
hasOverride and call setExpanded(hasOverride) so state updates occur in an
effect (use the existing prevHasOverrideRef, hasOverride and setExpanded
identifiers).

58-69: ⚠️ Potential issue | 🟠 Major

Strategy change can erase strategy_config with no editor to recover it.

Changing strategy clears strategy_config, but this component does not expose a strategy-config editor, so data can be lost and cannot be edited here.

Also applies to: 94-134

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/org-edit/DepartmentCeremonyOverride.tsx` around lines 58 - 69,
handleStrategyChange currently clears strategy_config unconditionally which can
silently erase user data because this component does not provide an editor for
strategy_config; instead, preserve existing strategy_config unless you have an
editor here or the strategy truly requires a reset. Update handleStrategyChange
(and the similar block at 94-134) to only update strategy and
velocity_calculator (using STRATEGY_DEFAULT_VELOCITY_CALC[s]) while keeping
strategy_config: policy?.strategy_config, or add a clear only behind an explicit
confirmation flag passed to onChange; ensure you still short-circuit when s ===
policy?.strategy.
web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx (1)

91-107: ⚠️ Potential issue | 🟠 Major

Per-ceremony overrides still cannot edit strategy_config.

The row renders StrategyPicker + PolicyFieldsPanel only, so strategy-specific config is not editable for ceremony-level overrides.

🔧 Suggested direction
 import { StrategyPicker } from './StrategyPicker'
+import { StrategyConfigPanel } from './StrategyConfigPanel'
 import { PolicyFieldsPanel } from './PolicyFieldsPanel'
@@
           {hasOverride && (
             <div className={cn('space-y-3 pl-2 border-l-2 border-accent/20')}>
               <StrategyPicker
                 value={strategy as CeremonyStrategyType}
                 onChange={handleStrategyChange}
                 disabled={saving}
               />
+              <StrategyConfigPanel
+                strategy={strategy as CeremonyStrategyType}
+                config={(policy?.strategy_config ?? {}) as Record<string, unknown>}
+                onChange={(c) => onOverrideChange(name, { ...policy, strategy_config: c })}
+                disabled={saving}
+              />
               <PolicyFieldsPanel
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx` around lines 91
- 107, The ceremony-level override UI only renders StrategyPicker and
PolicyFieldsPanel so the strategy-specific configuration (strategy_config) is
not editable; update the conditional block that renders when hasOverride is true
to also render the component that edits strategy_config (the same one used for
global policy strategy-specific options) and wire its value and change handler
into the override flow by passing policy?.strategy_config (or a sensible
default) and calling onOverrideChange(name, { ...policy, strategy_config:
updated }) from the strategy-config component’s onChange; ensure the
StrategyPicker, PolicyFieldsPanel, and the strategy-config editor use the same
saving/disabled (saving) state and the existing name/onOverrideChange handlers
so ceremony overrides persist correctly.
web/src/pages/settings/ceremony-policy/PolicyFieldsPanel.tsx (1)

73-77: ⚠️ Potential issue | 🟠 Major

Enforce the documented threshold bounds before persisting.

Line 73-Line 77 should clamp/validate to 0.01..1.0; currently any finite number is accepted.

🔧 Suggested fix
               onChange={(e) => {
-                const val = Number(e.target.value)
-                if (Number.isFinite(val)) {
-                  onTransitionThresholdChange(val)
-                }
+                const val = e.target.valueAsNumber
+                if (!Number.isFinite(val)) return
+                onTransitionThresholdChange(Math.min(1.0, Math.max(0.01, val)))
               }}

Based on learnings: Validate at system boundaries (user input, external APIs, config files).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/PolicyFieldsPanel.tsx` around lines 73
- 77, The onChange handler in PolicyFieldsPanel.tsx currently passes any finite
number to onTransitionThresholdChange; change it to validate and clamp the
parsed value into the documented range 0.01..1.0 before calling
onTransitionThresholdChange (e.g., parse Number(e.target.value), ignore NaN,
then clamp with Math.max(0.01, Math.min(1.0, parsed))). Ensure only the
clamped/validated value is passed to onTransitionThresholdChange so out-of-range
inputs cannot be persisted.
web/src/pages/settings/ceremony-policy/strategies/TaskDrivenConfig.tsx (1)

21-22: ⚠️ Potential issue | 🟡 Minor

Harden numeric reads against NaN/Infinity.

Line 21 and Line 22 accept non-finite numbers from malformed persisted config. Guard with Number.isFinite(...) before using values.

💡 Suggested fix
-  const everyN = typeof config.every_n_completions === 'number' ? config.every_n_completions : 5
-  const pct = typeof config.sprint_percentage === 'number' ? config.sprint_percentage : 50
+  const everyN =
+    typeof config.every_n_completions === 'number' &&
+    Number.isFinite(config.every_n_completions)
+      ? config.every_n_completions
+      : 5
+  const pct =
+    typeof config.sprint_percentage === 'number' &&
+    Number.isFinite(config.sprint_percentage)
+      ? config.sprint_percentage
+      : 50

Based on learnings: Validate at system boundaries (user input, external APIs, config files).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/strategies/TaskDrivenConfig.tsx`
around lines 21 - 22, The numeric reads for everyN and pct currently only check
typeof and can accept NaN/Infinity; update the assignments for everyN and pct to
guard with Number.isFinite(config.every_n_completions) and
Number.isFinite(config.sprint_percentage) respectively, falling back to the
defaults 5 and 50 when the value is not a finite number (keep the identifiers
everyN and pct and the referenced properties config.every_n_completions and
config.sprint_percentage).
web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx (1)

188-209: ⚠️ Potential issue | 🟠 Major

Non-atomic save may leave ceremony policy partially updated on failure.

The save handler issues six independent updateSetting calls via Promise.all. If one request fails after others succeed, the backend is left with a mixed ceremony policy that the UI cannot roll back. The comment at lines 186-187 acknowledges the limitation, but this remains a reliability concern for production use.

Consider implementing a batch update endpoint or a transactional settings API to ensure atomicity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx` around lines
188 - 209, The handler handleSave performs six independent updateSetting calls
which can leave partial updates on failure; change the save to a single atomic
operation by creating and using a batch/transactional API (e.g., add a new
backend endpoint like batchUpdateSettings or extend updateSetting to accept an
object) that accepts all coordination keys at once (ceremony_strategy,
ceremony_strategy_config, ceremony_velocity_calculator,
ceremony_auto_transition, ceremony_transition_threshold,
ceremony_policy_overrides) and call that from handleSave; on success call
addToast and fetchResolvedPolicy, on failure show the error toast and avoid
partial commits by relying on the backend transaction semantics.
web/src/stores/ceremony-policy.ts (1)

64-72: ⚠️ Potential issue | 🟡 Minor

fetchActiveStrategy still does not manage loading state.

Unlike fetchResolvedPolicy (lines 54-61), this action does not set loading: true before the request or loading: false after. If the UI relies on loading to show a spinner while fetching both resolved policy and active strategy, users will see inconsistent loading indicators.

🐛 Proposed fix to add loading state management
   fetchActiveStrategy: async () => {
+    set({ loading: true, activeStrategyError: null })
     try {
       const active = await ceremonyApi.getActiveStrategy()
-      set({ activeStrategy: active })
+      set({ activeStrategy: active, loading: false })
     } catch (err) {
-      set({ activeStrategyError: getErrorMessage(err) })
+      set({ activeStrategyError: getErrorMessage(err), loading: false })
     }
   },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/stores/ceremony-policy.ts` around lines 64 - 72, fetchActiveStrategy
omits updating the shared loading flag, causing inconsistent UI; update the
fetchActiveStrategy action to mirror fetchResolvedPolicy by setting loading:
true before initiating ceremonyApi.getActiveStrategy(), preserve existing error
handling, and ensure loading: false is set after completion (use a finally
block) so activeStrategy, activeStrategyError, and loading are all correctly
updated; target the fetchActiveStrategy function in ceremony-policy.ts to
implement this change.
src/synthorg/api/controllers/departments.py (2)

584-591: 🧹 Nitpick | 🔵 Trivial

Cross-worker race condition documented but unresolved.

The comment correctly notes that _dept_policy_lock only serializes within a single process. In multi-worker deployments, concurrent PUT/DELETE requests can still race and cause last-write-wins data loss. This is a known limitation that should be tracked for future resolution.

Consider tracking this as technical debt. Solutions include:

  • Settings service compare-and-swap (CAS) API with version tokens
  • Per-department storage keys instead of a single JSON blob
  • Distributed locking (Redis, etc.) if multi-worker deployment is planned
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/api/controllers/departments.py` around lines 584 - 591, The
comment notes a cross-worker race with the module-level asyncio.Lock
(_dept_policy_lock) protecting the dept_ceremony_policies JSON blob but doesn't
track it as technical debt or provide an actionable plan; add a
TODO/technical-debt marker in the file near _dept_policy_lock that creates a
tracked issue (or links to an existing ticket) for implementing a cross-worker
solution and list accepted remediation options (settings-service CAS with
version tokens, per-department storage keys instead of a single JSON blob, or
distributed locking like Redis), and update any relevant handler names
(PUT/DELETE handlers that modify dept_ceremony_policies) to include a short
inline note pointing to that ticket so future maintainers know this must be
addressed for multi-worker deployments.

491-513: ⚠️ Potential issue | 🟡 Minor

_save_dept_policies_json does not wrap exceptions in ServiceUnavailableError.

While _load_dept_policies_json wraps exceptions in ServiceUnavailableError when raise_on_error=True (lines 473-476), _save_dept_policies_json lets settings_service.set() exceptions propagate raw. This can expose internal error details and produce inconsistent 500 responses instead of the expected 503.

🛡️ Proposed fix to wrap save exceptions
 async def _save_dept_policies_json(
     app_state: AppState,
     policies: dict[str, Any],
 ) -> None:
     ...
     if not app_state.has_settings_service:
         msg = "Settings service not available"
         logger.warning(API_SERVICE_UNAVAILABLE, service="settings")
         raise ServiceUnavailableError(msg)
-    await app_state.settings_service.set(
-        "coordination",
-        "dept_ceremony_policies",
-        json.dumps(policies, separators=(",", ":")),
-    )
+    try:
+        await app_state.settings_service.set(
+            "coordination",
+            "dept_ceremony_policies",
+            json.dumps(policies, separators=(",", ":")),
+        )
+    except MemoryError, RecursionError:
+        raise
+    except Exception as exc:
+        logger.warning(
+            API_REQUEST_ERROR,
+            endpoint="departments.ceremony_policy.save",
+            error="failed to persist dept_ceremony_policies",
+            exc_info=True,
+        )
+        msg = "Failed to persist department ceremony policies"
+        raise ServiceUnavailableError(msg) from exc

As per coding guidelines, all error paths must log at WARNING or ERROR with context before raising.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/api/controllers/departments.py` around lines 491 - 513, The save
path in _save_dept_policies_json currently lets exceptions from
app_state.settings_service.set bubble up; catch any Exception around the await
app_state.settings_service.set(...) call, log a warning using
API_SERVICE_UNAVAILABLE with service="settings" and the error context, and then
raise a ServiceUnavailableError with a clear message (matching the existing
pattern used in _load_dept_policies_json). Ensure you reference
_save_dept_policies_json, app_state.settings_service.set,
API_SERVICE_UNAVAILABLE and ServiceUnavailableError when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/components/ui/inherit-toggle.tsx`:
- Around line 26-33: The switch uses a fixed aria-label ("Override") which is
misleading; update the button in inherit-toggle.tsx (the element using id,
role="switch", aria-checked={!inherit}, onClick={() => onChange(!inherit)},
disabled={disabled}) to expose a state-accurate accessible label—either compute
aria-label dynamically from the inherit prop (e.g., "Inherit from X" vs
"Override") or reference a visible/visually-hidden element via aria-labelledby
that includes the current state; apply the same change to the second switch
block (the similar button around lines 48-56) so screen readers receive the
correct label for both states.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx`:
- Around line 90-105: The effect currently re-syncs form state from
settingsSnapshot on any settings field change which will clobber user edits; add
a "dirty" mechanism (e.g., a local isDirty state or a formTouched flag managed
by your form handlers) and update the useEffect to skip calling setForm when
isDirty is true (or instead trigger a user prompt/confirmation before
overwriting). Locate the useEffect block that reads settingsSnapshot and calls
setForm, add/initialize the isDirty flag near your form state, set isDirty true
from input change handlers (and clear it on save/reset), and conditionally
bypass setForm or show a confirmation when isDirty is set.

In `@web/src/pages/settings/ceremony-policy/DepartmentOverridesPanel.tsx`:
- Line 112: The velocityCalculator prop currently falls back to a hardcoded
string 'task_driven' (velocityCalculator={effectivePolicy?.velocity_calculator
?? 'task_driven'}), which can show the wrong UI when the effective policy's
strategy differs; change the fallback to compute the default from the policy
strategy (e.g., derive the strategy-based default when effectivePolicy is
undefined or its velocity_calculator is missing) so velocityCalculator uses
either effectivePolicy.velocity_calculator or the strategy-derived default;
update the logic where velocityCalculator is set (the prop passed into
DepartmentOverridesPanel / the variable using
effectivePolicy?.velocity_calculator) to call a small helper or inline
conditional that maps policy.strategy -> default velocity calculator instead of
using 'task_driven'.

In `@web/src/pages/settings/ceremony-policy/strategies/EventDrivenConfig.tsx`:
- Around line 10-11: Replace the unsafe type assertions for debounce_default and
transition_event with runtime type guards: check typeof config.debounce_default
=== 'number' before assigning to debounceDefault and typeof
config.transition_event === 'string' before assigning to transitionEvent
(falling back to 5 and '' respectively); update the variables debounceDefault
and transitionEvent in EventDrivenConfig.tsx to mirror the defensive pattern
used in CalendarConfig so unexpected types are rejected at runtime.

In `@web/src/pages/settings/ceremony-policy/strategies/MilestoneDrivenConfig.tsx`:
- Around line 13-14: The editor's rawJson state (initialized via
rawJson/setRawJson from useState) can become stale when the incoming config prop
changes; add a useEffect (similar to ExternalTriggerConfig) that watches config
and/or config.milestones and updates setRawJson(JSON.stringify(config.milestones
?? [], null, 2)) and clears jsonError (setJsonError(null)) to resync the editor
whenever the prop changes so the UI reflects the latest config.

In
`@web/src/pages/settings/ceremony-policy/strategies/ThroughputAdaptiveConfig.tsx`:
- Around line 10-12: Replace the unsafe type assertions for dropPct, spikePct
and window with runtime typeof guards like in sibling components: read values
from config.velocity_drop_threshold_pct, config.velocity_spike_threshold_pct and
config.measurement_window_tasks, check typeof === "number" before using them,
and fall back to the defaults 30, 50 and 10 respectively if the checks fail;
update the variable initialization for dropPct, spikePct and window to perform
these typeof checks on the config properties rather than using "as number".

In `@web/src/pages/settings/ceremony-policy/StrategyConfigPanel.tsx`:
- Around line 24-43: The switch in StrategyConfigPanel (switching on the
strategy variable) currently falls back to `default: return null`, which can
silently ignore new CeremonyStrategyType cases; replace the default null with an
exhaustive check that triggers a compile-time/type error for unhandled union
members—e.g., call an assertNever/assertUnreachable helper (or throw a
descriptive Error using a `never` typed parameter) in the default branch so
missing cases like in TaskDrivenConfig, CalendarConfig, HybridConfig,
EventDrivenConfig, BudgetDrivenConfig, ThroughputAdaptiveConfig,
ExternalTriggerConfig, MilestoneDrivenConfig cause a build-time/type error; add
or reuse the project's `assertNever` helper if needed and update the default to
use it instead of returning null.

---

Duplicate comments:
In `@src/synthorg/api/controllers/departments.py`:
- Around line 584-591: The comment notes a cross-worker race with the
module-level asyncio.Lock (_dept_policy_lock) protecting the
dept_ceremony_policies JSON blob but doesn't track it as technical debt or
provide an actionable plan; add a TODO/technical-debt marker in the file near
_dept_policy_lock that creates a tracked issue (or links to an existing ticket)
for implementing a cross-worker solution and list accepted remediation options
(settings-service CAS with version tokens, per-department storage keys instead
of a single JSON blob, or distributed locking like Redis), and update any
relevant handler names (PUT/DELETE handlers that modify dept_ceremony_policies)
to include a short inline note pointing to that ticket so future maintainers
know this must be addressed for multi-worker deployments.
- Around line 491-513: The save path in _save_dept_policies_json currently lets
exceptions from app_state.settings_service.set bubble up; catch any Exception
around the await app_state.settings_service.set(...) call, log a warning using
API_SERVICE_UNAVAILABLE with service="settings" and the error context, and then
raise a ServiceUnavailableError with a clear message (matching the existing
pattern used in _load_dept_policies_json). Ensure you reference
_save_dept_policies_json, app_state.settings_service.set,
API_SERVICE_UNAVAILABLE and ServiceUnavailableError when making the change.

In `@web/src/components/ui/inherit-toggle.stories.tsx`:
- Around line 13-23: Add the missing shared-component state stories (hover,
loading, error, empty) to the existing inherit-toggle.stories.tsx so the shared
UI component covers all required states; create new Story exports named Hover,
Loading, Error, and Empty alongside the existing Inherit, Override, and Disabled
exports, each supplying appropriate args/props (e.g., inherit true/false,
disabled where needed) and, if necessary, a play/parameters/decorator to
simulate hover state and to present loading/error/empty visuals consistent with
the component’s props or state handlers (reference the Story type and the
existing Inherit/Override/Disabled exports to match pattern and naming).

In `@web/src/components/ui/policy-source-badge.stories.tsx`:
- Around line 13-23: The story file lists Project, Department and Default
stories but is missing the required state stories (hover, loading, error, empty)
for the shared PolicySourceBadge component; add four new Story exports named
Hover, Loading, Error, and Empty that target the same component and set
appropriate args/props (e.g., source values and a loading boolean or error
message prop) and any necessary decorators or interaction/play setups to
simulate hover state so the component renders each required state consistently
with the existing Project/Department/Default stories.

In `@web/src/pages/org-edit/DepartmentCeremonyOverride.tsx`:
- Around line 41-44: The render-phase update of state should be moved into an
effect: inside the DepartmentCeremonyOverride component, remove the direct
render-time block that checks prevHasOverrideRef.current !== hasOverride and
instead create a useEffect that depends on hasOverride; within that effect
compare prevHasOverrideRef.current to hasOverride, set
prevHasOverrideRef.current = hasOverride and call setExpanded(hasOverride) so
state updates occur in an effect (use the existing prevHasOverrideRef,
hasOverride and setExpanded identifiers).
- Around line 58-69: handleStrategyChange currently clears strategy_config
unconditionally which can silently erase user data because this component does
not provide an editor for strategy_config; instead, preserve existing
strategy_config unless you have an editor here or the strategy truly requires a
reset. Update handleStrategyChange (and the similar block at 94-134) to only
update strategy and velocity_calculator (using
STRATEGY_DEFAULT_VELOCITY_CALC[s]) while keeping strategy_config:
policy?.strategy_config, or add a clear only behind an explicit confirmation
flag passed to onChange; ensure you still short-circuit when s ===
policy?.strategy.

In `@web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx`:
- Around line 91-107: The ceremony-level override UI only renders StrategyPicker
and PolicyFieldsPanel so the strategy-specific configuration (strategy_config)
is not editable; update the conditional block that renders when hasOverride is
true to also render the component that edits strategy_config (the same one used
for global policy strategy-specific options) and wire its value and change
handler into the override flow by passing policy?.strategy_config (or a sensible
default) and calling onOverrideChange(name, { ...policy, strategy_config:
updated }) from the strategy-config component’s onChange; ensure the
StrategyPicker, PolicyFieldsPanel, and the strategy-config editor use the same
saving/disabled (saving) state and the existing name/onOverrideChange handlers
so ceremony overrides persist correctly.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx`:
- Around line 188-209: The handler handleSave performs six independent
updateSetting calls which can leave partial updates on failure; change the save
to a single atomic operation by creating and using a batch/transactional API
(e.g., add a new backend endpoint like batchUpdateSettings or extend
updateSetting to accept an object) that accepts all coordination keys at once
(ceremony_strategy, ceremony_strategy_config, ceremony_velocity_calculator,
ceremony_auto_transition, ceremony_transition_threshold,
ceremony_policy_overrides) and call that from handleSave; on success call
addToast and fetchResolvedPolicy, on failure show the error toast and avoid
partial commits by relying on the backend transaction semantics.

In `@web/src/pages/settings/ceremony-policy/PolicyFieldsPanel.tsx`:
- Around line 73-77: The onChange handler in PolicyFieldsPanel.tsx currently
passes any finite number to onTransitionThresholdChange; change it to validate
and clamp the parsed value into the documented range 0.01..1.0 before calling
onTransitionThresholdChange (e.g., parse Number(e.target.value), ignore NaN,
then clamp with Math.max(0.01, Math.min(1.0, parsed))). Ensure only the
clamped/validated value is passed to onTransitionThresholdChange so out-of-range
inputs cannot be persisted.

In `@web/src/pages/settings/ceremony-policy/strategies/TaskDrivenConfig.tsx`:
- Around line 21-22: The numeric reads for everyN and pct currently only check
typeof and can accept NaN/Infinity; update the assignments for everyN and pct to
guard with Number.isFinite(config.every_n_completions) and
Number.isFinite(config.sprint_percentage) respectively, falling back to the
defaults 5 and 50 when the value is not a finite number (keep the identifiers
everyN and pct and the referenced properties config.every_n_completions and
config.sprint_percentage).

In
`@web/src/pages/settings/ceremony-policy/strategies/ThroughputAdaptiveConfig.tsx`:
- Around line 16-41: The three InputField onChange handlers in
ThroughputAdaptiveConfig.tsx currently write raw Number(e.target.value) into
config (velocity_drop_threshold_pct, velocity_spike_threshold_pct,
measurement_window_tasks) allowing NaN/out-of-range values; update each handler
to parse the input safely (e.g. Number or parseInt), guard against NaN (fall
back to previous value or a sensible default), and clamp to the allowed ranges:
velocity_drop_threshold_pct and velocity_spike_threshold_pct to 1–100, and
measurement_window_tasks to 2–100, then call onChange with the validated/clamped
value so only valid numbers are persisted.

In `@web/src/pages/settings/ceremony-policy/StrategyChangeWarning.tsx`:
- Around line 17-29: The dynamic warning banner in StrategyChangeWarning should
expose live-region semantics so assistive tech announces it; update the
top-level container div (the element rendering the AlertTriangle and text) to
include live-region attributes (e.g., role="status" and aria-live="polite", and
optionally aria-atomic="true") so changes are announced consistently when the
component renders/changes.

In `@web/src/pages/settings/ceremony-policy/StrategyPicker.tsx`:
- Line 28: Replace the unsafe cast in StrategyPicker's SelectField onChange
handler with a runtime guard: check that the incoming value exists in the
CEREMONY_STRATEGY_TYPES set/array (or matches a predicate for
CeremonyStrategyType) before calling the onChange prop; if it passes, call
onChange(value as CeremonyStrategyType), otherwise ignore or handle the invalid
input (e.g., log or no-op). Locate the handler where onChange={(v) => onChange(v
as CeremonyStrategyType)} and use CEREMONY_STRATEGY_TYPES and
CeremonyStrategyType to validate the value first.

In `@web/src/stores/ceremony-policy.ts`:
- Around line 64-72: fetchActiveStrategy omits updating the shared loading flag,
causing inconsistent UI; update the fetchActiveStrategy action to mirror
fetchResolvedPolicy by setting loading: true before initiating
ceremonyApi.getActiveStrategy(), preserve existing error handling, and ensure
loading: false is set after completion (use a finally block) so activeStrategy,
activeStrategyError, and loading are all correctly updated; target the
fetchActiveStrategy function in ceremony-policy.ts to implement this change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b350a8b1-2b81-4636-9f8e-68efc6519785

📥 Commits

Reviewing files that changed from the base of the PR and between 51978b1 and 7bcb4c2.

📒 Files selected for processing (44)
  • CLAUDE.md
  • docs/design/ceremony-scheduling.md
  • docs/design/page-structure.md
  • src/synthorg/api/controllers/__init__.py
  • src/synthorg/api/controllers/ceremony_policy.py
  • src/synthorg/api/controllers/departments.py
  • src/synthorg/engine/workflow/ceremony_scheduler.py
  • src/synthorg/observability/events/api.py
  • src/synthorg/settings/definitions/coordination.py
  • tests/unit/api/controllers/test_ceremony_policy.py
  • tests/unit/settings/test_ceremony_settings.py
  • web/CLAUDE.md
  • web/src/api/endpoints/ceremony-policy.ts
  • web/src/api/types.ts
  • web/src/components/ui/inherit-toggle.stories.tsx
  • web/src/components/ui/inherit-toggle.tsx
  • web/src/components/ui/policy-source-badge.stories.tsx
  • web/src/components/ui/policy-source-badge.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/org-edit/DepartmentCeremonyOverride.stories.tsx
  • web/src/pages/org-edit/DepartmentCeremonyOverride.tsx
  • web/src/pages/org-edit/DepartmentEditDrawer.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
  • web/src/pages/settings/ceremony-policy/DepartmentOverridesPanel.tsx
  • web/src/pages/settings/ceremony-policy/PolicyFieldsPanel.tsx
  • web/src/pages/settings/ceremony-policy/StrategyChangeWarning.stories.tsx
  • web/src/pages/settings/ceremony-policy/StrategyChangeWarning.tsx
  • web/src/pages/settings/ceremony-policy/StrategyConfigPanel.tsx
  • web/src/pages/settings/ceremony-policy/StrategyPicker.stories.tsx
  • web/src/pages/settings/ceremony-policy/StrategyPicker.tsx
  • web/src/pages/settings/ceremony-policy/VelocityUnitIndicator.tsx
  • web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/CalendarConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/EventDrivenConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/ExternalTriggerConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/HybridConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/MilestoneDrivenConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/TaskDrivenConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/ThroughputAdaptiveConfig.tsx
  • web/src/router/index.tsx
  • web/src/router/routes.ts
  • web/src/stores/ceremony-policy.ts
  • web/src/utils/constants.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/synthorg/api/controllers/ceremony_policy.py`:
- Around line 382-400: The try/except that parses entry.value uses parenthesized
exception list; change it to PEP 758 comma-separated form (use "except
json.JSONDecodeError, TypeError:" for the json.loads block around policies =
json.loads(entry.value)) and additionally guard the call to
CeremonyPolicyConfig.model_validate(val) with a try/except that catches
pydantic/validation errors (e.g., ValidationError) and logs the error via
logger.warning with the same API_REQUEST_ERROR context and raises
ServiceUnavailableError(msg) from the caught ValidationError so malformed
department policy dicts return a structured ServiceUnavailableError instead of
propagating a 500.
- Around line 437-451: The call to
CeremonyPolicyConfig.model_validate(dept.ceremony_policy) should be wrapped in a
try/except to defensively translate validation failures into a
ServiceUnavailableError (matching the pattern used in departments.py); locate
the loop using get_departments() and replace the direct call to
CeremonyPolicyConfig.model_validate with a try block that catches the validation
exception (e.g., pydantic.ValidationError or the project’s validation error
type), log the exception at warning or error level including the
department_name, and raise ServiceUnavailableError with a concise message like
"Invalid ceremony policy for department" to avoid leaking internal validation
details to API consumers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2946c9d7-9907-4184-bcb1-18e22071946f

📥 Commits

Reviewing files that changed from the base of the PR and between 7bcb4c2 and f60a80c.

📒 Files selected for processing (2)
  • src/synthorg/api/controllers/ceremony_policy.py
  • web/src/__tests__/hooks/useAnimationPreset.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (6)
web/src/**/*.{tsx,ts}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{tsx,ts}: ALWAYS reuse existing components from web/src/components/ui/ before creating new ones (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, TagInput, MetadataGrid, ProjectStatusBadge, ContentTypeBadge)
Use Tailwind semantic classes (text-foreground, bg-card, text-accent, text-success, bg-danger, etc.) or CSS variables (var(--so-accent)) for colors. NEVER hardcode hex values in .tsx/.ts files.
Use font-sans or font-mono for typography (maps to Geist tokens). NEVER set fontFamily directly.
Use density-aware tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing. NEVER hardcode pixel values for layout spacing.
Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows and borders. NEVER hardcode values.
Import cn from @/lib/utils for conditional class merging in new components.
Do NOT recreate status dots inline -- use <StatusBadge> component.
Do NOT build card-with-header layouts from scratch -- use <SectionCard> component.
Do NOT create metric displays with text-metric font-bold -- use <MetricCard> component.
Do NOT render initials circles manually -- use <Avatar> component.
Do NOT create complex (>8 line) JSX inside .map() -- extract to a shared component.
TypeScript strict mode must pass type checking (run npm --prefix web run type-check); all type errors must be resolved before proceeding.
Do NOT hardcode Framer Motion transition durations -- use @/lib/motion presets.

Files:

  • web/src/__tests__/hooks/useAnimationPreset.test.ts
web/src/**/*.{tsx,ts,css}

📄 CodeRabbit inference engine (web/CLAUDE.md)

Do NOT use rgba() with hardcoded values -- use design token variables.

Files:

  • web/src/__tests__/hooks/useAnimationPreset.test.ts
web/src/**/*.{tsx,ts,js,jsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

ESLint must enforce zero warnings (run npm --prefix web run lint); all warnings must be fixed before proceeding.

Files:

  • web/src/__tests__/hooks/useAnimationPreset.test.ts
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: No from __future__ import annotations — Python 3.14 has PEP 649 native lazy annotations.
Use except A, B: (no parentheses) for PEP 758 except syntax on Python 3.14.
Type hints required on all public functions; mypy strict mode enforced.
Docstrings required on public classes and functions using Google style; enforced by ruff D rules.
Create new objects instead of mutating existing ones. For non-Pydantic internal collections (registries, BaseTool), use copy.deepcopy() at construction + MappingProxyType wrapping for read-only enforcement.
For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence).
Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (using model_copy(update=...)) for runtime state that evolves (e.g. agent execution state, task progress). Never mix static config fields with mutable runtime fields in one model.
Use Pydantic v2 (BaseModel, model_validator, computed_field, ConfigDict). In all ConfigDict declarations, use allow_inf_nan=False to reject NaN/Inf in numeric fields at validation time.
Use @computed_field for derived values instead of storing and validating redundant fields (e.g. TokenUsage.total_tokens).
Use NotBlankStr (from core.types) for all identifier/name fields — including optional (NotBlankStr | None) and tuple (tuple[NotBlankStr, ...]) variants — instead of manual whitespace validators.
Prefer asyncio.TaskGroup for fan-out/fan-in parallel operations in new code (e.g. multiple tool invocations, parallel agent calls). Prefer structured concurrency over bare create_task.
Line length maximum is 88 characters, enforced by ruff.
Functions must be < 50 lines; files < 800 lines.
Handle errors explicitly; never silently swallow exceptions.
Validate at system boundaries (use...

Files:

  • src/synthorg/api/controllers/ceremony_policy.py
src/synthorg/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

src/synthorg/**/*.py: Every module with business logic MUST have: from synthorg.observability import get_logger then logger = get_logger(__name__).
Never use import logging / logging.getLogger() / print() in application code. Exceptions: observability/setup.py, observability/sinks.py, observability/syslog_handler.py, and observability/http_handler.py may use stdlib logging and print(..., file=sys.stderr) for handler construction, bootstrap, and error reporting.
Use event name constants from domain-specific modules under synthorg.observability.events (e.g., API_REQUEST_STARTED from events.api, TOOL_INVOKE_START from events.tool). Import directly: from synthorg.observability.events.<domain> import EVENT_CONSTANT.
Always use structured kwargs in logging: logger.info(EVENT, key=value) — never logger.info("msg %s", val).
All error paths must log at WARNING or ERROR with context before raising.
All state transitions must log at INFO level.
DEBUG logging for object creation, internal flow, and entry/exit of key functions.
Retryable errors (is_retryable=True): RateLimitError, ProviderTimeoutError, ProviderConnectionError, ProviderInternalError. Non-retryable errors raise immediately without retry.
Never use real vendor names (Anthropic, OpenAI, Claude, GPT, etc.) in project-owned code, docstrings, comments, tests, or config examples. Use generic names: example-provider, example-large-001, example-medium-001, example-small-001, large/medium/small as aliases.

Files:

  • src/synthorg/api/controllers/ceremony_policy.py
src/**/*.py

⚙️ CodeRabbit configuration file

This project uses Python 3.14+ with PEP 758 except syntax: "except A, B:" (comma-separated, no parentheses) is correct and mandatory -- do NOT flag it as a typo or suggest parenthesized form. The "except builtins.MemoryError, RecursionError: raise" pattern is intentional project convention for system-error propagation. When evaluating the 50-line function limit, count only the function body excluding the signature lines, decorators, and docstring. Functions 1-5 lines over due to docstrings or multi-line signatures should not be flagged. Do not suggest extracting single-use helper functions called exactly once -- this reduces readability without improving maintainability.

Files:

  • src/synthorg/api/controllers/ceremony_policy.py
🧠 Learnings (12)
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/__tests__/**/*.test.{ts,tsx} : Use property-based testing with fast-check in React tests (`fc.assert` + `fc.property`)

Applied to files:

  • web/src/__tests__/hooks/useAnimationPreset.test.ts
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/**/*.test.{ts,tsx} : Web dashboard: Use React Hypothesis (fast-check) for property-based testing with fc.assert + fc.property

Applied to files:

  • web/src/__tests__/hooks/useAnimationPreset.test.ts
📚 Learning: 2026-03-20T08:28:32.845Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T08:28:32.845Z
Learning: Applies to web/src/__tests__/**/*.{ts,js} : Dashboard testing: Vitest unit tests organized by feature under `web/src/__tests__/`. Use fast-check for property-based testing (`fc.assert` + `fc.property`).

Applied to files:

  • web/src/__tests__/hooks/useAnimationPreset.test.ts
📚 Learning: 2026-04-03T10:54:21.472Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T10:54:21.472Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do NOT hardcode Framer Motion transition durations -- use `@/lib/motion` presets.

Applied to files:

  • web/src/__tests__/hooks/useAnimationPreset.test.ts
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/api/**/*.py : API package (api/): Litestar REST + WebSocket with controllers, guards, channels, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint, provider management endpoint (CRUD + test + presets), backup endpoint, RFC 9457 structured errors, AppState hot-reload slots, service auto-wiring (Phase 1 at construction, Phase 2 on startup), lifecycle helpers

Applied to files:

  • src/synthorg/api/controllers/ceremony_policy.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/api/**/*.py : REST API: Litestar framework, controllers with guards, channels for WebSocket, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint. RFC 9457 structured errors (ErrorCategory, ErrorCode, ErrorDetail, ProblemDetail, CATEGORY_TITLES, category_title, category_type_uri, content negotiation).

Applied to files:

  • src/synthorg/api/controllers/ceremony_policy.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/**/*.py : Package structure: src/synthorg/ organized as: api/ (REST+WebSocket, Litestar), auth/ (auth subpackage), backup/ (scheduled/manual backups), budget/ (cost tracking, CFO), cli/ (superseded by Go CLI), communication/ (message bus, meetings), config/ (YAML loading), core/ (domain models, resilience config), engine/ (orchestration, task state, coordination, approval gates, stagnation detection, context budget, compaction), hr/ (hiring, performance, promotion), memory/ (pluggable backend, Mem0, retrieval, consolidation), persistence/ (operational data, SQLite, settings), observability/ (logging, correlation, sinks), providers/ (LLM abstraction, LiteLLM, auth types, presets, runtime CRUD), settings/ (runtime-editable, typed definitions, encryption, config bridge), security/ (SecOps, rule engine, output scanning, progressive trust, autonomy levels), templates/ (company templates, personalities), tools/ (registry, built-in tools, git, sandbox, code_runner, MCP...

Applied to files:

  • src/synthorg/api/controllers/ceremony_policy.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : Prefer `asyncio.TaskGroup` for fan-out/fan-in parallel operations in new code (e.g. multiple tool invocations, parallel agent calls). Prefer structured concurrency over bare `create_task`. Existing code is being migrated incrementally.

Applied to files:

  • src/synthorg/api/controllers/ceremony_policy.py
📚 Learning: 2026-03-17T18:52:05.142Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T18:52:05.142Z
Learning: Applies to **/*.py : Async concurrency: prefer asyncio.TaskGroup for fan-out/fan-in parallel operations in new code (e.g. multiple tool invocations, parallel agent calls). Prefer structured concurrency over bare create_task.

Applied to files:

  • src/synthorg/api/controllers/ceremony_policy.py
📚 Learning: 2026-04-03T16:11:48.610Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-03T16:11:48.610Z
Learning: Applies to **/*.py : Prefer `asyncio.TaskGroup` for fan-out/fan-in parallel operations in new code (e.g. multiple tool invocations, parallel agent calls). Prefer structured concurrency over bare `create_task`.

Applied to files:

  • src/synthorg/api/controllers/ceremony_policy.py
📚 Learning: 2026-04-01T09:39:21.584Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T09:39:21.584Z
Learning: Applies to **/*.py : Prefer `asyncio.TaskGroup` for fan-out/fan-in parallel operations in new code (e.g., multiple tool invocations, parallel agent calls); prefer structured concurrency over bare `create_task`

Applied to files:

  • src/synthorg/api/controllers/ceremony_policy.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : Handle errors explicitly, never silently swallow. Validate at system boundaries (user input, external APIs, config files).

Applied to files:

  • src/synthorg/api/controllers/ceremony_policy.py
🔇 Additional comments (6)
web/src/__tests__/hooks/useAnimationPreset.test.ts (1)

40-43: Good tradeoff: lower property-test iterations for expensive renderHook path.

Keeping fc.assert + fc.property while capping runs at Line 43 is a sensible CI-cost optimization, especially since Line 15-25 already exhaustively covers all presets.

Based on learnings: “Use property-based testing with fast-check in React tests (fc.assert + fc.property).”

src/synthorg/api/controllers/ceremony_policy.py (5)

1-37: LGTM: Module setup follows project conventions.

Imports are well-organized, get_logger is correctly imported from synthorg.observability, logger is named logger, and event constants are imported from the domain-specific events module.


43-125: LGTM: Response models are well-structured.

Frozen Pydantic models with allow_inf_nan=False per coding guidelines. The model_validator on ActiveCeremonyStrategyResponse correctly enforces the contract documented in context snippet 2 (get_active_info() guarantees both values set or both None).


130-195: LGTM: Helper parsing functions with proper error logging.

Each parser logs at WARNING level with structured kwargs before re-raising, correctly following the error-handling guideline. The redundant None check noted in past reviews has been addressed.


328-342: LGTM: TaskGroup exception handling correctly addresses prior feedback.

The except* Exception as eg: block now properly catches child task failures, extracts the first exception, logs it, and raises a ServiceUnavailableError with appropriate context.


457-557: LGTM: Controller implementation is clean and well-documented.

The three endpoint handlers correctly delegate to helper functions, use structured logging with event constants, and properly integrate with AppState. The get_active_strategy handler correctly handles the scheduler's contract that strategy and sprint are both set or both None.

Comment on lines +382 to +400
try:
policies = json.loads(entry.value)
except (json.JSONDecodeError, TypeError) as exc:
logger.warning(
API_REQUEST_ERROR,
endpoint="ceremony_policy.fetch_dept",
error=f"Corrupt dept_ceremony_policies value: {exc}",
)
msg = "Malformed ceremony policies data"
raise ServiceUnavailableError(msg) from exc

if not isinstance(policies, dict) or department_name not in policies:
return _SETTINGS_NOT_FOUND
val = policies[department_name]
if val is None:
return None
if isinstance(val, dict):
return CeremonyPolicyConfig.model_validate(val)
return _SETTINGS_NOT_FOUND
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use PEP 758 except syntax (no parentheses) per coding guidelines.

Line 384 uses parenthesized form except (json.JSONDecodeError, TypeError) instead of the required comma-separated PEP 758 syntax.

Additionally, model_validate at line 399 can raise ValidationError on malformed department policy dicts, which would propagate as a 500 instead of a structured API error. Context snippet 1 shows departments.py wraps this call (lines 548–557) to translate validation failures into ServiceUnavailableError.

🛠️ Proposed fix
-    except (json.JSONDecodeError, TypeError) as exc:
+    except json.JSONDecodeError, TypeError as exc:
         logger.warning(
             API_REQUEST_ERROR,
             endpoint="ceremony_policy.fetch_dept",
             error=f"Corrupt dept_ceremony_policies value: {exc}",
         )
         msg = "Malformed ceremony policies data"
         raise ServiceUnavailableError(msg) from exc

     if not isinstance(policies, dict) or department_name not in policies:
         return _SETTINGS_NOT_FOUND
     val = policies[department_name]
     if val is None:
         return None
     if isinstance(val, dict):
-        return CeremonyPolicyConfig.model_validate(val)
+        try:
+            return CeremonyPolicyConfig.model_validate(val)
+        except MemoryError, RecursionError:
+            raise
+        except Exception as exc:
+            logger.warning(
+                API_REQUEST_ERROR,
+                endpoint="ceremony_policy.fetch_dept",
+                department=department_name,
+                error=f"Invalid override structure: {exc}",
+            )
+            msg = f"Corrupt ceremony policy override for {department_name!r}"
+            raise ServiceUnavailableError(msg) from exc
     return _SETTINGS_NOT_FOUND

As per coding guidelines: "Use except A, B: (no parentheses) for PEP 758 except syntax on Python 3.14."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/api/controllers/ceremony_policy.py` around lines 382 - 400, The
try/except that parses entry.value uses parenthesized exception list; change it
to PEP 758 comma-separated form (use "except json.JSONDecodeError, TypeError:"
for the json.loads block around policies = json.loads(entry.value)) and
additionally guard the call to CeremonyPolicyConfig.model_validate(val) with a
try/except that catches pydantic/validation errors (e.g., ValidationError) and
logs the error via logger.warning with the same API_REQUEST_ERROR context and
raises ServiceUnavailableError(msg) from the caught ValidationError so malformed
department policy dicts return a structured ServiceUnavailableError instead of
propagating a 500.

Comment on lines +437 to +451
departments = await app_state.config_resolver.get_departments()
for dept in departments:
if dept.name == department_name:
if dept.ceremony_policy is None:
return None
return CeremonyPolicyConfig.model_validate(
dept.ceremony_policy,
)
msg = f"Department {department_name!r} not found"
logger.warning(
API_RESOURCE_NOT_FOUND,
resource="department",
name=department_name,
)
raise NotFoundError(msg)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider wrapping model_validate for defensive consistency.

Line 442-444 calls CeremonyPolicyConfig.model_validate(dept.ceremony_policy) without exception handling. While config resolver data is presumably pre-validated, the pattern in departments.py (context snippet 1) wraps similar calls to translate validation errors into ServiceUnavailableError, preventing internal details from reaching API consumers.

♻️ Optional defensive wrapper
     for dept in departments:
         if dept.name == department_name:
             if dept.ceremony_policy is None:
                 return None
-            return CeremonyPolicyConfig.model_validate(
-                dept.ceremony_policy,
-            )
+            try:
+                return CeremonyPolicyConfig.model_validate(
+                    dept.ceremony_policy,
+                )
+            except MemoryError, RecursionError:
+                raise
+            except Exception as exc:
+                logger.warning(
+                    API_REQUEST_ERROR,
+                    endpoint="ceremony_policy.fetch_dept",
+                    department=department_name,
+                    error=f"Invalid config ceremony_policy: {exc}",
+                )
+                msg = f"Invalid ceremony policy config for {department_name!r}"
+                raise ServiceUnavailableError(msg) from exc
     msg = f"Department {department_name!r} not found"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/api/controllers/ceremony_policy.py` around lines 437 - 451, The
call to CeremonyPolicyConfig.model_validate(dept.ceremony_policy) should be
wrapped in a try/except to defensively translate validation failures into a
ServiceUnavailableError (matching the pattern used in departments.py); locate
the loop using get_departments() and replace the direct call to
CeremonyPolicyConfig.model_validate with a try block that catches the validation
exception (e.g., pydantic.ValidationError or the project’s validation error
type), log the exception at warning or error level including the
department_name, and raise ServiceUnavailableError with a concise message like
"Invalid ceremony policy for department" to avoid leaking internal validation
details to API consumers.

@Aureliolo Aureliolo force-pushed the feat/ceremony-dashboard-dept-overrides branch from f60a80c to 83b36d3 Compare April 3, 2026 16:21
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview April 3, 2026 16:22 — with GitHub Actions Inactive
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/src/pages/org-edit/DepartmentEditDrawer.tsx (1)

39-45: ⚠️ Potential issue | 🟠 Major

Move render-phase state initialization into useEffect.

The code at lines 39-49 violates React's render phase purity by calling state setters (setDisplayName, setBudgetPercent, setCeremonyPolicy, setSubmitError, setDeleteOpen, setDeleting) whenever the department prop reference changes. If upstream refetching returns a new object instance, the form state resets before an in-progress save completes, losing user edits.

Replace the render-phase useRef pattern with useEffect([open, department?.name]):

  • open: Resets form when drawer opens (fresh state)
  • department?.name: Uses stable identity instead of object reference, preventing resets on refetch

Update imports from useCallback, useRef, useState to useCallback, useEffect, useState.

Suggested fix
-import { useCallback, useRef, useState } from 'react'
+import { useCallback, useEffect, useState } from 'react'
@@
-  const prevDepartmentRef = useRef<typeof department | undefined>(undefined)
-  if (department !== prevDepartmentRef.current) {
-    prevDepartmentRef.current = department
-    if (department) {
-      setDisplayName(department.display_name ?? department.name)
-      setBudgetPercent(
-        department.budget_percent != null ? String(department.budget_percent) : '0',
-      )
-      setCeremonyPolicy(department.ceremony_policy ?? null)
-      setSubmitError(null)
-    }
-    setDeleteOpen(false)
-    setDeleting(false)
-  }
+  useEffect(() => {
+    if (!open || !department) return
+    setDisplayName(department.display_name ?? department.name)
+    setBudgetPercent(
+      department.budget_percent != null ? String(department.budget_percent) : '0',
+    )
+    setCeremonyPolicy(department.ceremony_policy ?? null)
+    setSubmitError(null)
+    setDeleteOpen(false)
+    setDeleting(false)
+  }, [open, department?.name])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/org-edit/DepartmentEditDrawer.tsx` around lines 39 - 45, The
current render-phase reset in DepartmentEditDrawer uses prevDepartmentRef and
calls setDisplayName, setBudgetPercent, setCeremonyPolicy, setSubmitError,
setDeleteOpen and setDeleting directly during render when department !==
prevDepartmentRef.current; move that logic into a useEffect hook instead:
replace the useRef pattern with useEffect that depends on [open,
department?.name] and perform the state initializations inside that effect
(reset displayName, budgetPercent, ceremonyPolicy, submitError, deleteOpen,
deleting), and update imports to include useEffect (remove useRef) so form
resets only when the drawer opens or the department name truly changes, avoiding
mid-edit resets on object ref updates.
♻️ Duplicate comments (10)
web/src/components/ui/policy-source-badge.stories.tsx (1)

13-23: 🛠️ Refactor suggestion | 🟠 Major

Add required shared-component state stories.

This file covers source variants, but not the required shared-component state set (default, hover, loading, error, empty).

As per coding guidelines, new shared components in web/src/components/ui/ must include .stories.tsx coverage for “default, hover, loading, error, empty”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/ui/policy-source-badge.stories.tsx` around lines 13 - 23,
The story file currently exports Project, Department and Default (args: source)
but lacks the required shared-component state stories; add exports for the five
mandated states named (for example) DefaultState, Hover, Loading, Error, Empty
(or similar) that each set the component props to reproduce those states:
DefaultState should mirror the normal/default UI, Hover should simulate hover
state (use args + a play function or decorator to trigger userEvent.hover on the
component), Loading should set/loading props or a loading flag, Error should set
an error prop or message, and Empty should provide empty-data props; keep the
source variants (Project, Department, Default) intact and ensure the new story
names are exported from the same file so Storybook picks them up.
web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx (3)

59-64: ⚠️ Potential issue | 🟠 Major

Use the strategy default when ceremony_velocity_calculator is unset.

This initializes velocityCalculator to 'task_driven' regardless of the selected strategy. If the stored strategy is calendar and the calculator setting is omitted, the page shows the wrong effective value and the next save will persist that wrong override. Initialize from STRATEGY_DEFAULT_VELOCITY_CALC[strategy] or the resolved policy instead of a hardcoded literal.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx` around lines
59 - 64, The velocityCalculator default is hardcoded to 'task_driven' causing
wrong overrides; change the initialization of velocityCalculator to use the
resolved strategy default (e.g. STRATEGY_DEFAULT_VELOCITY_CALC[strategy]) or the
resolved policy value instead of the literal; locate the return object where
velocityCalculator is set (referenced as velocityCalculator and strategy in the
same returned object, along with strategyConfig/config) and replace the fallback
so when get('ceremony_velocity_calculator') is undefined it uses
STRATEGY_DEFAULT_VELOCITY_CALC[strategy] (or the resolvedPolicy's calculator)
rather than 'task_driven'.

195-205: ⚠️ Potential issue | 🟠 Major

Saving the ceremony policy is still non-atomic.

This one logical update is split across six independent updateSetting() calls. If one request fails after others succeed, the backend is left with a mixed ceremony policy and the UI has no rollback path. This needs a batch or transactional save endpoint before release.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx` around lines
195 - 205, handleSave currently issues six independent updateSetting() calls
causing non-atomic saves; replace these with a single transactional/batch call
from the frontend (e.g., add and call an API like updateSettingsBatch or
updateCeremonyPolicy) that accepts a single payload containing
ceremony_strategy, ceremony_strategy_config, ceremony_velocity_calculator,
ceremony_auto_transition, ceremony_transition_threshold, and
ceremony_policy_overrides, or update the backend to expose a transactional
endpoint and call it from handleSave; ensure the new call serializes
strategyConfig and ceremonyOverrides consistently, returns success/failure for
the whole operation, and that handleSave only marks saving complete or shows an
error based on that single atomic response (refer to handleSave and
updateSetting in the diff).

128-148: ⚠️ Potential issue | 🟠 Major

Unsaved per-ceremony edits are still overwritten by settings refreshes.

ceremonyOverrides is resynced unconditionally from settingsEntries, unlike the guarded project form. Because JSON.parse(raw) returns a fresh object whenever the entries array changes, any WebSocket refresh or unrelated settings update can discard in-progress ceremony edits while isDirty is true. Gate this reset behind the same dirty protection, or compare the raw setting string before resetting local state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx` around lines
128 - 148, The effect that resyncs ceremonyOverrides unconditionally is
overwriting in-progress edits; modify the useEffect that currently calls
setCeremonyOverrides(ceremonyOverridesSnapshot.overrides) to only reset local
state when it is safe: either check the component's dirty flag (e.g., isDirty or
the same form dirty guard used by the project form) and skip the reset while
dirty, or compare the raw settings string from settingsEntries (capture the raw
value used to build ceremonyOverridesSnapshot) against a ref of the last-applied
raw and only call setCeremonyOverrides when the raw has actually changed and the
form is not dirty; update ceremonyOverridesSnapshot construction to expose the
raw string or keep a prevRaw ref and use those in the useEffect guard.
web/src/pages/settings/ceremony-policy/PolicyFieldsPanel.tsx (1)

69-78: ⚠️ Potential issue | 🟠 Major

Don’t clamp a controlled number input on every keystroke.

Number(e.target.value) makes '' become 0, and the immediate clamp rewrites transient edits to 0.01. That makes the threshold field snap while the user is typing. Keep a local string buffer, or parse and clamp on blur instead of on every onChange.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/PolicyFieldsPanel.tsx` around lines 69
- 78, The input currently parses and clamps on every onChange (using
Number(e.target.value)) which turns empty string into 0 and forces the field to
snap; instead keep a local string buffer state in PolicyFieldsPanel for the
Transition Threshold input (use transitionThresholdString or similar), bind
InputField.value to that string, update the string raw on onChange without
Number()/clamping, and only parse, validate and call onTransitionThresholdChange
(with Math.min(1.0, Math.max(0.01, parsed))) in onBlur or on form submit; update
places referencing transitionThreshold to initialize the string state and remove
the immediate Number(...) clamp in the onChange handler.
web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx (1)

40-51: ⚠️ Potential issue | 🟠 Major

Per-ceremony rows still render fake effective values.

An empty or sparse override is displayed as task_driven with task_driven velocity, true, and 1.0, and StrategyConfigPanel is keyed off that fabricated strategy. If the inherited ceremony strategy is something else, the row shows the wrong config editor and the next edit is based on values that are not actually set. This panel needs the resolved ceremony policy, or explicit unset states, instead of hardcoded fallbacks.

Also applies to: 92-113

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx` around lines 40
- 51, The row is fabricating effective values by defaulting strategy to
'task_driven' and passing policy ?? {} to onOverrideChange, causing
StrategyConfigPanel (and rendered controls) to show and edit values that aren't
actually set; change the UI to compute and use the resolved ceremony policy (the
true inherited policy) for display and for initializing StrategyConfigPanel, and
when creating a new per-ceremony override in handleInheritChange call
onOverrideChange(name, {}) or a sparse override object that explicitly leaves
fields unset (e.g., strategy: undefined) instead of policy ?? {}, and stop using
the local fallback const strategy = policy?.strategy ?? 'task_driven' so that
undefined is preserved and the component can render explicit “inherited/unset”
state.
web/src/pages/org-edit/DepartmentCeremonyOverride.tsx (1)

49-55: ⚠️ Potential issue | 🟠 Major

This drawer still fabricates values for sparse overrides.

Turning override on with {} and then rendering policy?.strategy ?? 'task_driven', ?? true, and ?? 1.0 makes an override like { auto_transition: false } look like it also set strategy, velocity, and threshold. The next edit is therefore based on synthetic values, not the effective department policy. Pass the resolved policy alongside the sparse override, or represent unset fields explicitly here.

Also applies to: 96-129

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/org-edit/DepartmentCeremonyOverride.tsx` around lines 49 - 55,
The handler handleInheritChange currently calls onChange(null) or
onChange(policy ?? {}) which fabricates complete values for sparse overrides;
instead, when switching inherit off pass the resolved policy combined with the
sparse override (or an object that explicitly lists unset fields as undefined)
so UI falls back to actual effective department policy rather than synthetic
defaults—update handleInheritChange to compute the resolvedPolicy (merge
departmentPolicy/effectivePolicy with existing policy) and call
onChange(resolvedPolicy) (also apply the same fix to the related logic in the
block around lines 96-129 that creates/updates overrides).
src/synthorg/api/controllers/ceremony_policy.py (2)

382-384: ⚠️ Potential issue | 🟡 Minor

Use the repo's required PEP 758 except A, B: form here.

The parenthesized tuple does not match the Python 3.14 exception style enforced in this repo; use except json.JSONDecodeError, TypeError as exc: instead.

As per coding guidelines, "Use except A, B: (no parentheses) for PEP 758 except syntax on Python 3.14."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/api/controllers/ceremony_policy.py` around lines 382 - 384, The
except clause handling json decoding should use the repo's PEP 758 syntax:
replace the current parenthesized tuple except (json.JSONDecodeError, TypeError)
as exc with the PEP 758 form except json.JSONDecodeError, TypeError as exc in
the try/except block where policies = json.loads(entry.value) is attempted,
keeping the same exception variable name exc and the existing exception handling
body unchanged.

364-400: ⚠️ Potential issue | 🟠 Major

Don't treat a broken department override as “not found”.

A settings read failure, a non-dict stored value, or an invalid override dict all bypass the explicit error path here. /ceremony-policy/resolved?department=... can then either hide a persisted override behind _SETTINGS_NOT_FOUND or 500 with a raw validation error. Only return _SETTINGS_NOT_FOUND when the department key is actually absent; otherwise log and raise ServiceUnavailableError.

Based on learnings, validate at system boundaries (user input, external APIs, config files).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/api/controllers/ceremony_policy.py` around lines 364 - 400, The
code currently treats settings read failures and malformed/invalid override
values as "_SETTINGS_NOT_FOUND"; change fetch_dept so only the true-absent-key
returns _SETTINGS_NOT_FOUND: when awaiting
app_state.settings_service.get("coordination","dept_ceremony_policies") any
exception other than MemoryError/RecursionError should log and raise
ServiceUnavailableError (do not return _SETTINGS_NOT_FOUND), after json.loads
verify the result is a dict and if not raise ServiceUnavailableError (with a
logged warning), then check for department_name presence and return
_SETTINGS_NOT_FOUND only if the key is missing; if the key exists but value is
None return None, if value is a dict call CeremonyPolicyConfig.model_validate
and catch/convert validation errors to ServiceUnavailableError, otherwise log
and raise ServiceUnavailableError.
src/synthorg/api/controllers/departments.py (1)

530-588: ⚠️ Potential issue | 🟠 Major

Don't collapse corrupt department overrides into None.

If dept_ceremony_policies[department_name] contains anything other than null or a valid policy dict, this helper currently reports “inherit”. That hides unreadable state on the GET path and lets the next save overwrite data the dashboard never actually loaded. Even the dict branch validates and then returns the raw payload, so normalization/coercion never reaches the client. Raise ServiceUnavailableError for non-null|dict|valid entries and return the validated JSON dump on both the settings and config fallback paths.

Based on learnings, validate at system boundaries (user input, external APIs, config files).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/api/controllers/departments.py` around lines 530 - 588, In
_get_dept_ceremony_override, treat any stored override that is neither None nor
a valid dict as an error (don’t collapse into “inherit”): when
policies[department_name] exists and is not None and not a dict, raise
ServiceUnavailableError (with a clear message and a warning log) instead of
returning None; for the dict branch, validate via
CeremonyPolicyConfig.model_validate and return the normalized/serialized form
(e.g., model_dump of the validated model) rather than the raw payload; likewise,
when falling back to app_state.config_resolver.get_departments(), locate
dept.ceremony_policy, validate it with CeremonyPolicyConfig.model_validate and
return its normalized/serialized dict or raise ServiceUnavailableError on
validation failure; keep existing logger usage (API_REQUEST_ERROR /
API_SERVICE_UNAVAILABLE) for warning messages and surface errors rather than
hiding corrupt data.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/synthorg/api/controllers/ceremony_policy.py`:
- Around line 197-229: The call to _build_project_policy can raise raw
parse/validation exceptions which bubble out of _fetch_project_policy and
produce 500s; catch exceptions from _build_project_policy inside
_fetch_project_policy (and likewise for the other location handling resolved
settings around the block referenced at 292-342) and wrap/translate them into
the structured ServiceUnavailableError (including the original exception message
for diagnostics) before returning so malformed external settings become a
ServiceUnavailableError rather than an internal 500. Ensure you reference and
handle exceptions thrown by _parse_* helpers invoked by _build_project_policy
and preserve the original exception details when constructing the
ServiceUnavailableError.
- Around line 175-179: The _parse_auto_transition function currently treats any
non-empty string not equal to "true" as False; change it to only accept explicit
"true" or "false" (case-insensitive) and return True/False accordingly, return
None for None/empty input, and raise a clear ValueError for any other string to
surface invalid ceremony_auto_transition config values; update the error message
to include the offending raw value so callers can log or display it.

In `@tests/unit/api/controllers/test_ceremony_policy.py`:
- Around line 25-27: The pytest `unit` marker is applied at the class level
(e.g., TestBuildProjectPolicy) but repo convention requires markers on
individual test functions; remove the `@pytest.mark.unit` decorator from the
class definitions and add `@pytest.mark.unit` immediately above each test
function (each def test_... inside those classes), ensuring every test method in
TestBuildProjectPolicy (and the other test classes called out) is individually
annotated.

In `@web/src/components/ui/policy-source-badge.tsx`:
- Around line 21-31: The PolicySourceBadge currently renders its own <span> with
styling and status presentation; instead refactor it to compose the existing
StatusBadge component: replace the inline <span> render in PolicySourceBadge
with a StatusBadge that receives the mapped variant (from SOURCE_STYLES or a new
SOURCE_VARIANT map) and the label (from SOURCE_LABELS), and forward className as
needed; keep only the mapping logic (SOURCE_STYLES/SOURCE_LABELS) in this file
and remove any inline status dot/rounded/span markup so presentation is
delegated to StatusBadge.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx`:
- Around line 293-323: The page disables the Save button during handleSave() but
leaves the project controls editable, so edits made while a save is in flight
aren’t included and setIsDirty(false) can incorrectly clear them; introduce an
isSaving state (or reuse existing) set true at save start and false on
completion/error, pass that flag as a disable/readOnly prop into StrategyPicker,
StrategyConfigPanel, PolicyFieldsPanel (and any other interactive child used
when saving) so the editor is locked during handleSave(), and ensure
setIsDirty(false) only runs after the save completes and that children respect
the disabled prop (update component props/interfaces if necessary).
- Around line 48-57: The code currently replaces invalid JSON with {} (variables
config/configParseError) and handleSave always writes JSON.stringify(config),
which will overwrite the original raw value; change the flow to preserve the
original raw string and avoid serializing the fallback on save: when reading
get('ceremony_strategy_config') (and the other JSON-backed keys), store both the
parsed object (if parse succeeds) and the raw string (e.g.,
rawCeremonyStrategyConfig), set a parse error flag (configParseError) only for
UI feedback, and in handleSave use the raw string when parse failed (or block
the save by returning an error if configParseError is true) instead of writing
JSON.stringify({})—apply the same approach to the other JSON settings referenced
so invalid stored values are never overwritten.

In `@web/src/pages/settings/ceremony-policy/DepartmentOverridesPanel.tsx`:
- Around line 30-35: The row is currently driven from the sparse stored override
(`policy`) so partial overrides lose inherited values; compute a resolved
department policy by merging the stored `policy` with the inherited baseline
(project/default/resolved dept policy) and use that resolvedPolicy for
display/editing (e.g., for dept.name, StrategyPicker, StrategyConfigPanel,
velocityCalculator and the localDraft baseline) while still persisting only the
override delta; replace uses of `effectivePolicy`/`strategy` that currently
fallback to defaults with this merged `resolvedPolicy` and keep
`localDraft`/persist logic to save only the diff between `resolvedPolicy` and
the stored `policy`.

In `@web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx`:
- Around line 13-17: The transition threshold normalization currently allows 0
(rawPct -> transitionPct uses Math.max(0, rawPct)) which conflicts with the UI
and other logic that expect 1–100; update the normalization for
config.transition_threshold by replacing Math.max(0, rawPct) with Math.max(1,
rawPct) (keep the existing fallback to 100 when config.transition_threshold is
not a finite number) so that the computed transitionPct always falls within
1–100; adjust any related variable names (rawPct, transitionPct) where they are
used to ensure the persisted/displayed value matches the UI contract.

In `@web/src/pages/settings/ceremony-policy/strategies/MilestoneDrivenConfig.tsx`:
- Line 12: The code casts config.transition_milestone to string unsafely; change
the initialization of transitionMilestone in MilestoneDrivenConfig.tsx to
perform a runtime string check on config.transition_milestone (e.g., use typeof
config.transition_milestone === 'string' ? config.transition_milestone : ''), so
malformed persisted values fall back to '' before being fed into the controlled
input; update any related uses of transitionMilestone to rely on this validated
value.

In `@web/src/pages/SettingsPage.tsx`:
- Around line 408-420: The nested ternary used to compute footerAction inside
SettingsPage's .map() (using the ns variable) makes the JSX hard to read;
extract that logic into a small helper or component (e.g., a function
getFooterAction(ns) or a FooterActionSelector component) that returns the
appropriate SettingsActionCard instance for 'observability' and 'coordination'
or undefined otherwise, then replace the inline ternary in the mapped JSX with a
simple call/element (footerAction={getFooterAction(ns)} or
footerAction={<FooterActionSelector ns={ns} />}), keeping the SettingsActionCard
usage and ROUTES constants unchanged.
- Around line 61-66: Replace the hand-rolled span used as a button in
SettingsPage (the inline-flex span rendering "Open") with the shared Button
component from web/src/components/ui/, import Button at the top of the file, and
map the visual/size props to the Button API (e.g., size/variant or className as
needed) so styling is consistent; remove aria-hidden and use Button’s accessible
semantics (onClick/aria-label) instead of the span, and delete the old span
markup (ensure any layout classes like h-9 w-full remain applied via Button
props or className).

---

Outside diff comments:
In `@web/src/pages/org-edit/DepartmentEditDrawer.tsx`:
- Around line 39-45: The current render-phase reset in DepartmentEditDrawer uses
prevDepartmentRef and calls setDisplayName, setBudgetPercent, setCeremonyPolicy,
setSubmitError, setDeleteOpen and setDeleting directly during render when
department !== prevDepartmentRef.current; move that logic into a useEffect hook
instead: replace the useRef pattern with useEffect that depends on [open,
department?.name] and perform the state initializations inside that effect
(reset displayName, budgetPercent, ceremonyPolicy, submitError, deleteOpen,
deleting), and update imports to include useEffect (remove useRef) so form
resets only when the drawer opens or the department name truly changes, avoiding
mid-edit resets on object ref updates.

---

Duplicate comments:
In `@src/synthorg/api/controllers/ceremony_policy.py`:
- Around line 382-384: The except clause handling json decoding should use the
repo's PEP 758 syntax: replace the current parenthesized tuple except
(json.JSONDecodeError, TypeError) as exc with the PEP 758 form except
json.JSONDecodeError, TypeError as exc in the try/except block where policies =
json.loads(entry.value) is attempted, keeping the same exception variable name
exc and the existing exception handling body unchanged.
- Around line 364-400: The code currently treats settings read failures and
malformed/invalid override values as "_SETTINGS_NOT_FOUND"; change fetch_dept so
only the true-absent-key returns _SETTINGS_NOT_FOUND: when awaiting
app_state.settings_service.get("coordination","dept_ceremony_policies") any
exception other than MemoryError/RecursionError should log and raise
ServiceUnavailableError (do not return _SETTINGS_NOT_FOUND), after json.loads
verify the result is a dict and if not raise ServiceUnavailableError (with a
logged warning), then check for department_name presence and return
_SETTINGS_NOT_FOUND only if the key is missing; if the key exists but value is
None return None, if value is a dict call CeremonyPolicyConfig.model_validate
and catch/convert validation errors to ServiceUnavailableError, otherwise log
and raise ServiceUnavailableError.

In `@src/synthorg/api/controllers/departments.py`:
- Around line 530-588: In _get_dept_ceremony_override, treat any stored override
that is neither None nor a valid dict as an error (don’t collapse into
“inherit”): when policies[department_name] exists and is not None and not a
dict, raise ServiceUnavailableError (with a clear message and a warning log)
instead of returning None; for the dict branch, validate via
CeremonyPolicyConfig.model_validate and return the normalized/serialized form
(e.g., model_dump of the validated model) rather than the raw payload; likewise,
when falling back to app_state.config_resolver.get_departments(), locate
dept.ceremony_policy, validate it with CeremonyPolicyConfig.model_validate and
return its normalized/serialized dict or raise ServiceUnavailableError on
validation failure; keep existing logger usage (API_REQUEST_ERROR /
API_SERVICE_UNAVAILABLE) for warning messages and surface errors rather than
hiding corrupt data.

In `@web/src/components/ui/policy-source-badge.stories.tsx`:
- Around line 13-23: The story file currently exports Project, Department and
Default (args: source) but lacks the required shared-component state stories;
add exports for the five mandated states named (for example) DefaultState,
Hover, Loading, Error, Empty (or similar) that each set the component props to
reproduce those states: DefaultState should mirror the normal/default UI, Hover
should simulate hover state (use args + a play function or decorator to trigger
userEvent.hover on the component), Loading should set/loading props or a loading
flag, Error should set an error prop or message, and Empty should provide
empty-data props; keep the source variants (Project, Department, Default) intact
and ensure the new story names are exported from the same file so Storybook
picks them up.

In `@web/src/pages/org-edit/DepartmentCeremonyOverride.tsx`:
- Around line 49-55: The handler handleInheritChange currently calls
onChange(null) or onChange(policy ?? {}) which fabricates complete values for
sparse overrides; instead, when switching inherit off pass the resolved policy
combined with the sparse override (or an object that explicitly lists unset
fields as undefined) so UI falls back to actual effective department policy
rather than synthetic defaults—update handleInheritChange to compute the
resolvedPolicy (merge departmentPolicy/effectivePolicy with existing policy) and
call onChange(resolvedPolicy) (also apply the same fix to the related logic in
the block around lines 96-129 that creates/updates overrides).

In `@web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx`:
- Around line 40-51: The row is fabricating effective values by defaulting
strategy to 'task_driven' and passing policy ?? {} to onOverrideChange, causing
StrategyConfigPanel (and rendered controls) to show and edit values that aren't
actually set; change the UI to compute and use the resolved ceremony policy (the
true inherited policy) for display and for initializing StrategyConfigPanel, and
when creating a new per-ceremony override in handleInheritChange call
onOverrideChange(name, {}) or a sparse override object that explicitly leaves
fields unset (e.g., strategy: undefined) instead of policy ?? {}, and stop using
the local fallback const strategy = policy?.strategy ?? 'task_driven' so that
undefined is preserved and the component can render explicit “inherited/unset”
state.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx`:
- Around line 59-64: The velocityCalculator default is hardcoded to
'task_driven' causing wrong overrides; change the initialization of
velocityCalculator to use the resolved strategy default (e.g.
STRATEGY_DEFAULT_VELOCITY_CALC[strategy]) or the resolved policy value instead
of the literal; locate the return object where velocityCalculator is set
(referenced as velocityCalculator and strategy in the same returned object,
along with strategyConfig/config) and replace the fallback so when
get('ceremony_velocity_calculator') is undefined it uses
STRATEGY_DEFAULT_VELOCITY_CALC[strategy] (or the resolvedPolicy's calculator)
rather than 'task_driven'.
- Around line 195-205: handleSave currently issues six independent
updateSetting() calls causing non-atomic saves; replace these with a single
transactional/batch call from the frontend (e.g., add and call an API like
updateSettingsBatch or updateCeremonyPolicy) that accepts a single payload
containing ceremony_strategy, ceremony_strategy_config,
ceremony_velocity_calculator, ceremony_auto_transition,
ceremony_transition_threshold, and ceremony_policy_overrides, or update the
backend to expose a transactional endpoint and call it from handleSave; ensure
the new call serializes strategyConfig and ceremonyOverrides consistently,
returns success/failure for the whole operation, and that handleSave only marks
saving complete or shows an error based on that single atomic response (refer to
handleSave and updateSetting in the diff).
- Around line 128-148: The effect that resyncs ceremonyOverrides unconditionally
is overwriting in-progress edits; modify the useEffect that currently calls
setCeremonyOverrides(ceremonyOverridesSnapshot.overrides) to only reset local
state when it is safe: either check the component's dirty flag (e.g., isDirty or
the same form dirty guard used by the project form) and skip the reset while
dirty, or compare the raw settings string from settingsEntries (capture the raw
value used to build ceremonyOverridesSnapshot) against a ref of the last-applied
raw and only call setCeremonyOverrides when the raw has actually changed and the
form is not dirty; update ceremonyOverridesSnapshot construction to expose the
raw string or keep a prevRaw ref and use those in the useEffect guard.

In `@web/src/pages/settings/ceremony-policy/PolicyFieldsPanel.tsx`:
- Around line 69-78: The input currently parses and clamps on every onChange
(using Number(e.target.value)) which turns empty string into 0 and forces the
field to snap; instead keep a local string buffer state in PolicyFieldsPanel for
the Transition Threshold input (use transitionThresholdString or similar), bind
InputField.value to that string, update the string raw on onChange without
Number()/clamping, and only parse, validate and call onTransitionThresholdChange
(with Math.min(1.0, Math.max(0.01, parsed))) in onBlur or on form submit; update
places referencing transitionThreshold to initialize the string state and remove
the immediate Number(...) clamp in the onChange handler.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b487edc4-bd9f-44af-ac7c-684270e2da53

📥 Commits

Reviewing files that changed from the base of the PR and between f60a80c and 83b36d3.

📒 Files selected for processing (45)
  • CLAUDE.md
  • docs/design/ceremony-scheduling.md
  • docs/design/page-structure.md
  • src/synthorg/api/controllers/__init__.py
  • src/synthorg/api/controllers/ceremony_policy.py
  • src/synthorg/api/controllers/departments.py
  • src/synthorg/engine/workflow/ceremony_scheduler.py
  • src/synthorg/observability/events/api.py
  • src/synthorg/settings/definitions/coordination.py
  • tests/unit/api/controllers/test_ceremony_policy.py
  • tests/unit/settings/test_ceremony_settings.py
  • web/CLAUDE.md
  • web/src/__tests__/hooks/useAnimationPreset.test.ts
  • web/src/api/endpoints/ceremony-policy.ts
  • web/src/api/types.ts
  • web/src/components/ui/inherit-toggle.stories.tsx
  • web/src/components/ui/inherit-toggle.tsx
  • web/src/components/ui/policy-source-badge.stories.tsx
  • web/src/components/ui/policy-source-badge.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/org-edit/DepartmentCeremonyOverride.stories.tsx
  • web/src/pages/org-edit/DepartmentCeremonyOverride.tsx
  • web/src/pages/org-edit/DepartmentEditDrawer.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
  • web/src/pages/settings/ceremony-policy/DepartmentOverridesPanel.tsx
  • web/src/pages/settings/ceremony-policy/PolicyFieldsPanel.tsx
  • web/src/pages/settings/ceremony-policy/StrategyChangeWarning.stories.tsx
  • web/src/pages/settings/ceremony-policy/StrategyChangeWarning.tsx
  • web/src/pages/settings/ceremony-policy/StrategyConfigPanel.tsx
  • web/src/pages/settings/ceremony-policy/StrategyPicker.stories.tsx
  • web/src/pages/settings/ceremony-policy/StrategyPicker.tsx
  • web/src/pages/settings/ceremony-policy/VelocityUnitIndicator.tsx
  • web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/CalendarConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/EventDrivenConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/ExternalTriggerConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/HybridConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/MilestoneDrivenConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/TaskDrivenConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/ThroughputAdaptiveConfig.tsx
  • web/src/router/index.tsx
  • web/src/router/routes.ts
  • web/src/stores/ceremony-policy.ts
  • web/src/utils/constants.ts

Comment on lines +25 to +27
@pytest.mark.unit
class TestBuildProjectPolicy:
"""Tests for _build_project_policy helper."""
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Move the unit marker onto the test functions.

These tests are currently marked at the class level, but this repo’s test convention requires markers on the individual test functions.
As per coding guidelines, "Use @pytest.mark.unit, @pytest.mark.integration, @pytest.mark.e2e, @pytest.mark.slow markers on test functions."

Also applies to: 63-65, 104-106, 165-167

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/api/controllers/test_ceremony_policy.py` around lines 25 - 27, The
pytest `unit` marker is applied at the class level (e.g.,
TestBuildProjectPolicy) but repo convention requires markers on individual test
functions; remove the `@pytest.mark.unit` decorator from the class definitions
and add `@pytest.mark.unit` immediately above each test function (each def
test_... inside those classes), ensuring every test method in
TestBuildProjectPolicy (and the other test classes called out) is individually
annotated.

Comment on lines +21 to +31
export function PolicySourceBadge({ source, className }: PolicySourceBadgeProps) {
return (
<span
className={cn(
'inline-flex items-center rounded px-1.5 py-0.5 text-xs font-medium uppercase tracking-wider',
SOURCE_STYLES[source],
className,
)}
>
{SOURCE_LABELS[source]}
</span>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Prefer composing existing StatusBadge rather than a bespoke badge.

This shared component reimplements badge/status presentation logic. Please compose the existing UI badge primitive and keep only source→variant/label mapping here.

As per coding guidelines, “ALWAYS reuse existing components from web/src/components/ui/” and “Do NOT recreate status dots inline -- use <StatusBadge> component.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/ui/policy-source-badge.tsx` around lines 21 - 31, The
PolicySourceBadge currently renders its own <span> with styling and status
presentation; instead refactor it to compose the existing StatusBadge component:
replace the inline <span> render in PolicySourceBadge with a StatusBadge that
receives the mapped variant (from SOURCE_STYLES or a new SOURCE_VARIANT map) and
the label (from SOURCE_LABELS), and forward className as needed; keep only the
mapping logic (SOURCE_STYLES/SOURCE_LABELS) in this file and remove any inline
status dot/rounded/span markup so presentation is delegated to StatusBadge.

Comment on lines +30 to +35
const hasOverride = policy != null && Object.keys(policy).length > 0
// Local draft for new overrides (defers API call until explicit save via strategy/field changes)
const [localDraft, setLocalDraft] = useState<CeremonyPolicyConfig | null>(null)
const isEditing = hasOverride || localDraft != null
const effectivePolicy = policy ?? localDraft
const strategy = effectivePolicy?.strategy ?? 'task_driven'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Drive this row from the resolved department policy, not the sparse override.

policy here is only the stored override. If a department overrides just auto_transition or transition_threshold, the summary label, StrategyPicker, StrategyConfigPanel, and velocityCalculator all fall back to 'task_driven'/empty defaults instead of the inherited project values. That makes partial overrides render the wrong strategy/config and can push users into editing the wrong policy. Use the resolved policy for dept.name as the display/edit baseline, then persist only the override delta.

Also applies to: 79-83, 105-114

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/DepartmentOverridesPanel.tsx` around
lines 30 - 35, The row is currently driven from the sparse stored override
(`policy`) so partial overrides lose inherited values; compute a resolved
department policy by merging the stored `policy` with the inherited baseline
(project/default/resolved dept policy) and use that resolvedPolicy for
display/editing (e.g., for dept.name, StrategyPicker, StrategyConfigPanel,
velocityCalculator and the localDraft baseline) while still persisting only the
override delta; replace uses of `effectivePolicy`/`strategy` that currently
fallback to defaults with this merged `resolvedPolicy` and keep
`localDraft`/persist logic to save only the diff between `resolvedPolicy` and
the stored `policy`.

Comment on lines +61 to +66
<span
className="inline-flex h-9 w-full items-center justify-center rounded-md border border-border bg-card px-4 text-sm font-medium text-foreground"
aria-hidden
>
Open
</span>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Use the shared Button component instead of hand-rolled button styling.

This duplicates a core UI primitive and makes consistency harder to maintain.

♻️ Proposed fix
+import { Button } from '@/components/ui/button'
...
-      <div className="w-56 shrink-0">
-        <span
-          className="inline-flex h-9 w-full items-center justify-center rounded-md border border-border bg-card px-4 text-sm font-medium text-foreground"
-          aria-hidden
-        >
-          Open
-        </span>
-      </div>
+      <div className="w-56 shrink-0">
+        <Button asChild variant="outline" className="h-9 w-full pointer-events-none">
+          <span aria-hidden>Open</span>
+        </Button>
+      </div>
As per coding guidelines, “ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones.”
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/SettingsPage.tsx` around lines 61 - 66, Replace the hand-rolled
span used as a button in SettingsPage (the inline-flex span rendering "Open")
with the shared Button component from web/src/components/ui/, import Button at
the top of the file, and map the visual/size props to the Button API (e.g.,
size/variant or className as needed) so styling is consistent; remove
aria-hidden and use Button’s accessible semantics (onClick/aria-label) instead
of the span, and delete the old span markup (ensure any layout classes like h-9
w-full remain applied via Button props or className).

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

♻️ Duplicate comments (4)
web/src/pages/SettingsPage.tsx (1)

50-69: 🛠️ Refactor suggestion | 🟠 Major

Use the shared Button primitive for the “Open” affordance.

The inner span is hand-rolling a core control instead of reusing the design-system button.

♻️ Minimal refactor
+import { Button } from '@/components/ui/button'
...
-      <div className="w-56 shrink-0">
-        <span
-          className="inline-flex h-9 w-full items-center justify-center rounded-md border border-border bg-card px-4 text-sm font-medium text-foreground"
-          aria-hidden
-        >
-          Open
-        </span>
-      </div>
+      <div className="w-56 shrink-0">
+        <Button asChild variant="outline" className="pointer-events-none h-9 w-full">
+          <span aria-hidden>Open</span>
+        </Button>
+      </div>

As per coding guidelines, “ALWAYS reuse existing components from web/src/components/ui/ before creating new ones (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, TagInput, MetadataGrid, ProjectStatusBadge, ContentTypeBadge)”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/SettingsPage.tsx` around lines 50 - 69, Replace the hand-rolled
"Open" span in SettingsActionCard with the shared Button component from
web/src/components/ui/ to reuse the design system; import Button at the top,
swap the inner span in SettingsActionCard for <Button>Open</Button> (passing
through the current sizing and styling via className, e.g. to preserve h-9, full
width, border and text classes) remove aria-hidden so the control remains
accessible, and keep the outer Link wrapper and the "Open" label unchanged.
web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx (1)

195-208: ⚠️ Potential issue | 🟠 Major

This save path can still leave ceremony policy half-updated.

updateSetting persists each key independently and throws on failure. With Promise.all, one rejected request still leaves any already-completed writes committed, so the page can report a save failure after only part of the policy changed. This needs a single transactional ceremony-policy write or a batch endpoint with rollback semantics.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx` around lines
195 - 208, The current handleSave uses multiple independent updateSetting calls
(updateSetting(..., 'ceremony_strategy' ...), updateSetting(...,
'ceremony_strategy_config' ...), etc.), so a single failure can leave the
ceremony policy partially updated; change this to perform one atomic write
instead: assemble a single policy object (including strategy, strategyConfig,
velocityCalculator, autoTransition, transitionThreshold, ceremonyOverrides) and
persist it via one updateSetting call (e.g., updateSetting('coordination',
'ceremony_policy', JSON.stringify(fullPolicy))) or implement/consume a
server-side batch endpoint that accepts the full policy and guarantees
transactional semantics/rollback. Update handleSave to call that single endpoint
and remove the Promise.all multi-write approach so saves are atomic.
src/synthorg/api/controllers/ceremony_policy.py (2)

358-360: ⚠️ Potential issue | 🟡 Minor

Use the repo's PEP 758 multi-exception form in both handlers.

These clauses still use parenthesized except (...). At Line 360, json.JSONDecodeError is already a ValueError, so except ValueError as exc is enough; Line 409 should use the comma-separated form.

Proposed fix
-    except (ValueError, json.JSONDecodeError) as exc:
+    except ValueError as exc:
         logger.warning(
             API_SERVICE_UNAVAILABLE,
             service="settings",
             error=f"Malformed ceremony policy settings: {exc}",
         )
         msg = "Malformed ceremony policy settings"
         raise ServiceUnavailableError(msg) from exc
...
-    except (json.JSONDecodeError, TypeError) as exc:
+    except json.JSONDecodeError, TypeError as exc:
         logger.warning(
             API_REQUEST_ERROR,
             endpoint="ceremony_policy.fetch_dept",
             error=f"Corrupt dept_ceremony_policies value: {exc}",
         )

As per coding guidelines: Use except A, B: (no parentheses) for PEP 758 except syntax on Python 3.14.

In Python 3.14, does PEP 758 allow `except json.JSONDecodeError, TypeError as exc:` without parentheses, and is `json.JSONDecodeError` a subclass of `ValueError`?

Also applies to: 407-409

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/api/controllers/ceremony_policy.py` around lines 358 - 360, The
except clauses use the old parenthesized form and redundantly list
json.JSONDecodeError; update the handler that calls _build_project_policy to
catch only ValueError (replace "except (ValueError, json.JSONDecodeError) as
exc" with "except ValueError as exc"), and change the other multi-exception
handler that currently groups json.JSONDecodeError and TypeError with
parentheses to the PEP 758 comma-separated form (e.g., "except
json.JSONDecodeError, TypeError as exc") so both handlers follow the repo's
Python 3.14 exception syntax.

418-425: ⚠️ Potential issue | 🟠 Major

Reject corrupt dept_ceremony_policies payloads instead of pretending the override is missing.

src/synthorg/settings/definitions/coordination.py defines this setting as {department: object|null}. A non-object root, a scalar/list department value, or a validation failure here currently degrades to _SETTINGS_NOT_FOUND or a raw 500, so /resolved can silently fall back to YAML/defaults while the settings blob is corrupted.

Proposed fix
-    if not isinstance(policies, dict) or department_name not in policies:
-        return _SETTINGS_NOT_FOUND
+    if not isinstance(policies, dict):
+        logger.warning(
+            API_REQUEST_ERROR,
+            endpoint="ceremony_policy.fetch_dept",
+            error="Corrupt dept_ceremony_policies root type",
+        )
+        msg = "Malformed ceremony policies data"
+        raise ServiceUnavailableError(msg)
+    if department_name not in policies:
+        return _SETTINGS_NOT_FOUND
     val = policies[department_name]
     if val is None:
         return None
     if isinstance(val, dict):
-        return CeremonyPolicyConfig.model_validate(val)
-    return _SETTINGS_NOT_FOUND
+        try:
+            return CeremonyPolicyConfig.model_validate(val)
+        except MemoryError, RecursionError:
+            raise
+        except Exception as exc:
+            logger.warning(
+                API_REQUEST_ERROR,
+                endpoint="ceremony_policy.fetch_dept",
+                department=department_name,
+                error=f"Invalid dept_ceremony_policies override: {exc}",
+            )
+            msg = "Malformed ceremony policies data"
+            raise ServiceUnavailableError(msg) from exc
+    logger.warning(
+        API_REQUEST_ERROR,
+        endpoint="ceremony_policy.fetch_dept",
+        department=department_name,
+        error=f"Invalid dept_ceremony_policies value type: {type(val).__name__}",
+    )
+    msg = "Malformed ceremony policies data"
+    raise ServiceUnavailableError(msg)

Based on learnings: Validate at system boundaries (user input, external APIs, config files).

In Pydantic v2.12.5, what exception type does `BaseModel.model_validate(...)` raise for invalid input mappings?
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/synthorg/api/controllers/ceremony_policy.py`:
- Around line 377-380: The department_name parameter in
_lookup_dept_override_from_settings (and the other helper nearby that currently
types department_name as plain str) must keep the NotBlankStr contract: change
their signatures from department_name: str to department_name: NotBlankStr,
import NotBlankStr from core.types, and update any local uses/annotations to
treat department_name as the validated non-blank type (no runtime whitespace
checks needed); ensure type hints propagate to callers if necessary but do not
alter return types.
- Around line 348-356: The current except* Exception block in ceremony_policy.py
(handling the ExceptionGroup bound to eg and using first = eg.exceptions[0]) can
capture system errors like MemoryError or RecursionError; update the handler to
first inspect eg.exceptions and re-raise any system-level exceptions (e.g.,
MemoryError, RecursionError) immediately, and only translate non-system
exceptions into a sanitized ServiceUnavailableError with a generic message (do
not expose first or underlying text to the client). Keep logging via
logger/API_SERVICE_UNAVAILABLE for internal diagnostics but log only
non-sensitive details (avoid sending exception text to the client), and preserve
raising ServiceUnavailableError (raise ServiceUnavailableError("Failed to fetch
ceremony settings") from None or with minimal internal chaining) so the client
sees the generic message; refer to the existing variables/methods eg, first,
logger, API_SERVICE_UNAVAILABLE, and ServiceUnavailableError to locate and
modify the code.

In `@web/src/pages/org-edit/DepartmentEditDrawer.tsx`:
- Around line 39-57: The repeated inline eslint-disable comments in the
useEffect body create noise; replace them with a single block-level directive
above the effect that disables `@eslint-react/set-state-in-effect` for the whole
effect, and keep the same prop-to-local-state sync logic inside useEffect (the
block should wrap the effect that references useEffect, department,
prevDepartmentRef.current, setDisplayName, setBudgetPercent, setCeremonyPolicy,
setSubmitError, setDeleteOpen, and setDeleting) so you remove the six //
eslint-disable-next-line comments while preserving behavior.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx`:
- Around line 59-66: The form seeding currently trusts raw strings for
ceremony_strategy and ceremony_transition_threshold; update the return block to
defensively validate both: for ceremony_strategy, check the raw value from
get('ceremony_strategy') against the known CeremonyStrategyType enum/allowed
keys and if it isn’t one of them fall back to 'task_driven' (also use that
validated value when indexing STRATEGY_DEFAULT_VELOCITY_CALC and when passing to
StrategyConfigPanel to avoid hitting its assertNever); for transitionThreshold,
parse with parseFloat (or Number) then verify isFinite and clamp to a safe range
(e.g., fallback to 1.0 and ensure >= 0) before assigning transitionThreshold in
state so malformed inputs don’t become NaN on save. Ensure you still propagate
configParseError and keep velocityCalculator logic using the validated strategy
value.
- Around line 48-57: The code currently treats any successful JSON.parse as
valid (letting "null" or "" slip through); update both parsing blocks (the one
using get('ceremony_strategy_config') and the similar block around lines 129-141
for ceremony_policy_overrides) to only accept a parsed value when it's a
non-null plain object root: after parsing the string (ensure the source string
is defined and non-empty), check that the result is typeof 'object' && result
!== null && !Array.isArray(result) before assigning to
config/ceremonyOverrides/strategyConfig; if validation fails set the
corresponding parse-error flag (configParseError / overridesParseError) to true
and leave the variable as the default empty object so downstream code (strategy
panels, Object.keys) won't crash.

In `@web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx`:
- Around line 13-17: The strategy config currently reads and persists a
duplicate transition_threshold (see BudgetDrivenConfig and the config variable
usage at the blocks around the shown lines and 34–45); remove any reads, UI
controls, and writes that reference config.transition_threshold inside
BudgetDrivenConfig and instead use the canonical ceremony_transition_threshold
maintained by CeremonyPolicyPage/PolicyFieldsPanel (or accept it via props) for
display and saves; ensure you also stop persisting transition_threshold into
strategyConfig when saving the policy so there is only one source of truth.

In `@web/src/pages/settings/ceremony-policy/strategies/MilestoneDrivenConfig.tsx`:
- Around line 13-24: Normalize the incoming config.milestones at the read
boundary: replace uses of "config.milestones ?? []" in the MilestoneDrivenConfig
initialization and inside the useEffect with a guarded normalization like
Array.isArray(config.milestones) ? config.milestones : [] so non-array values
(object/string/number) are treated as empty and don't get propagated into
rawJson; update the initial rawJson (useState) and the incoming variable in the
useEffect that compares currentParsed vs incomingParsed to use this normalized
array (and optionally set jsonError if you want to surface malformed external
config).

---

Duplicate comments:
In `@src/synthorg/api/controllers/ceremony_policy.py`:
- Around line 358-360: The except clauses use the old parenthesized form and
redundantly list json.JSONDecodeError; update the handler that calls
_build_project_policy to catch only ValueError (replace "except (ValueError,
json.JSONDecodeError) as exc" with "except ValueError as exc"), and change the
other multi-exception handler that currently groups json.JSONDecodeError and
TypeError with parentheses to the PEP 758 comma-separated form (e.g., "except
json.JSONDecodeError, TypeError as exc") so both handlers follow the repo's
Python 3.14 exception syntax.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx`:
- Around line 195-208: The current handleSave uses multiple independent
updateSetting calls (updateSetting(..., 'ceremony_strategy' ...),
updateSetting(..., 'ceremony_strategy_config' ...), etc.), so a single failure
can leave the ceremony policy partially updated; change this to perform one
atomic write instead: assemble a single policy object (including strategy,
strategyConfig, velocityCalculator, autoTransition, transitionThreshold,
ceremonyOverrides) and persist it via one updateSetting call (e.g.,
updateSetting('coordination', 'ceremony_policy', JSON.stringify(fullPolicy))) or
implement/consume a server-side batch endpoint that accepts the full policy and
guarantees transactional semantics/rollback. Update handleSave to call that
single endpoint and remove the Promise.all multi-write approach so saves are
atomic.

In `@web/src/pages/SettingsPage.tsx`:
- Around line 50-69: Replace the hand-rolled "Open" span in SettingsActionCard
with the shared Button component from web/src/components/ui/ to reuse the design
system; import Button at the top, swap the inner span in SettingsActionCard for
<Button>Open</Button> (passing through the current sizing and styling via
className, e.g. to preserve h-9, full width, border and text classes) remove
aria-hidden so the control remains accessible, and keep the outer Link wrapper
and the "Open" label unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 46cce6fa-f069-4eac-80c7-21307952f046

📥 Commits

Reviewing files that changed from the base of the PR and between 83b36d3 and 4329d43.

📒 Files selected for processing (6)
  • src/synthorg/api/controllers/ceremony_policy.py
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/org-edit/DepartmentEditDrawer.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
  • web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/MilestoneDrivenConfig.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Dashboard Test
  • GitHub Check: Test (Python 3.14)
  • GitHub Check: Build Web
  • GitHub Check: Build Backend
🧰 Additional context used
📓 Path-based instructions (6)
web/src/**/*.{tsx,ts}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{tsx,ts}: ALWAYS reuse existing components from web/src/components/ui/ before creating new ones (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, TagInput, MetadataGrid, ProjectStatusBadge, ContentTypeBadge)
Use Tailwind semantic classes (text-foreground, bg-card, text-accent, text-success, bg-danger, etc.) or CSS variables (var(--so-accent)) for colors. NEVER hardcode hex values in .tsx/.ts files.
Use font-sans or font-mono for typography (maps to Geist tokens). NEVER set fontFamily directly.
Use density-aware tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing. NEVER hardcode pixel values for layout spacing.
Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows and borders. NEVER hardcode values.
Import cn from @/lib/utils for conditional class merging in new components.
Do NOT recreate status dots inline -- use <StatusBadge> component.
Do NOT build card-with-header layouts from scratch -- use <SectionCard> component.
Do NOT create metric displays with text-metric font-bold -- use <MetricCard> component.
Do NOT render initials circles manually -- use <Avatar> component.
Do NOT create complex (>8 line) JSX inside .map() -- extract to a shared component.
TypeScript strict mode must pass type checking (run npm --prefix web run type-check); all type errors must be resolved before proceeding.
Do NOT hardcode Framer Motion transition durations -- use @/lib/motion presets.

Files:

  • web/src/pages/org-edit/DepartmentEditDrawer.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
  • web/src/pages/settings/ceremony-policy/strategies/MilestoneDrivenConfig.tsx
web/src/**/*.{tsx,ts,css}

📄 CodeRabbit inference engine (web/CLAUDE.md)

Do NOT use rgba() with hardcoded values -- use design token variables.

Files:

  • web/src/pages/org-edit/DepartmentEditDrawer.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
  • web/src/pages/settings/ceremony-policy/strategies/MilestoneDrivenConfig.tsx
web/src/**/*.{tsx,ts,js,jsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

ESLint must enforce zero warnings (run npm --prefix web run lint); all warnings must be fixed before proceeding.

Files:

  • web/src/pages/org-edit/DepartmentEditDrawer.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
  • web/src/pages/settings/ceremony-policy/strategies/MilestoneDrivenConfig.tsx
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: No from __future__ import annotations — Python 3.14 has PEP 649 native lazy annotations.
Use except A, B: (no parentheses) for PEP 758 except syntax on Python 3.14.
Type hints required on all public functions; mypy strict mode enforced.
Docstrings required on public classes and functions using Google style; enforced by ruff D rules.
Create new objects instead of mutating existing ones. For non-Pydantic internal collections (registries, BaseTool), use copy.deepcopy() at construction + MappingProxyType wrapping for read-only enforcement.
For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence).
Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (using model_copy(update=...)) for runtime state that evolves (e.g. agent execution state, task progress). Never mix static config fields with mutable runtime fields in one model.
Use Pydantic v2 (BaseModel, model_validator, computed_field, ConfigDict). In all ConfigDict declarations, use allow_inf_nan=False to reject NaN/Inf in numeric fields at validation time.
Use @computed_field for derived values instead of storing and validating redundant fields (e.g. TokenUsage.total_tokens).
Use NotBlankStr (from core.types) for all identifier/name fields — including optional (NotBlankStr | None) and tuple (tuple[NotBlankStr, ...]) variants — instead of manual whitespace validators.
Prefer asyncio.TaskGroup for fan-out/fan-in parallel operations in new code (e.g. multiple tool invocations, parallel agent calls). Prefer structured concurrency over bare create_task.
Line length maximum is 88 characters, enforced by ruff.
Functions must be < 50 lines; files < 800 lines.
Handle errors explicitly; never silently swallow exceptions.
Validate at system boundaries (use...

Files:

  • src/synthorg/api/controllers/ceremony_policy.py
src/synthorg/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

src/synthorg/**/*.py: Every module with business logic MUST have: from synthorg.observability import get_logger then logger = get_logger(__name__).
Never use import logging / logging.getLogger() / print() in application code. Exceptions: observability/setup.py, observability/sinks.py, observability/syslog_handler.py, and observability/http_handler.py may use stdlib logging and print(..., file=sys.stderr) for handler construction, bootstrap, and error reporting.
Use event name constants from domain-specific modules under synthorg.observability.events (e.g., API_REQUEST_STARTED from events.api, TOOL_INVOKE_START from events.tool). Import directly: from synthorg.observability.events.<domain> import EVENT_CONSTANT.
Always use structured kwargs in logging: logger.info(EVENT, key=value) — never logger.info("msg %s", val).
All error paths must log at WARNING or ERROR with context before raising.
All state transitions must log at INFO level.
DEBUG logging for object creation, internal flow, and entry/exit of key functions.
Retryable errors (is_retryable=True): RateLimitError, ProviderTimeoutError, ProviderConnectionError, ProviderInternalError. Non-retryable errors raise immediately without retry.
Never use real vendor names (Anthropic, OpenAI, Claude, GPT, etc.) in project-owned code, docstrings, comments, tests, or config examples. Use generic names: example-provider, example-large-001, example-medium-001, example-small-001, large/medium/small as aliases.

Files:

  • src/synthorg/api/controllers/ceremony_policy.py
src/**/*.py

⚙️ CodeRabbit configuration file

This project uses Python 3.14+ with PEP 758 except syntax: "except A, B:" (comma-separated, no parentheses) is correct and mandatory -- do NOT flag it as a typo or suggest parenthesized form. The "except builtins.MemoryError, RecursionError: raise" pattern is intentional project convention for system-error propagation. When evaluating the 50-line function limit, count only the function body excluding the signature lines, decorators, and docstring. Functions 1-5 lines over due to docstrings or multi-line signatures should not be flagged. Do not suggest extracting single-use helper functions called exactly once -- this reduces readability without improving maintainability.

Files:

  • src/synthorg/api/controllers/ceremony_policy.py
🧠 Learnings (47)
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from web/src/components/ui/ (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, form fields, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor) before creating new components

Applied to files:

  • web/src/pages/org-edit/DepartmentEditDrawer.tsx
  • web/src/pages/SettingsPage.tsx
📚 Learning: 2026-04-03T16:11:48.610Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-03T16:11:48.610Z
Learning: See `web/CLAUDE.md` for the full React 19 component inventory, design token rules, and post-training references (TS6, Storybook 10).

Applied to files:

  • web/src/pages/org-edit/DepartmentEditDrawer.tsx
📚 Learning: 2026-04-03T10:54:21.472Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T10:54:21.472Z
Learning: Applies to web/src/**/*.{tsx,ts} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, TagInput, MetadataGrid, ProjectStatusBadge, ContentTypeBadge)

Applied to files:

  • web/src/pages/org-edit/DepartmentEditDrawer.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
  • web/src/pages/settings/ceremony-policy/strategies/MilestoneDrivenConfig.tsx
📚 Learning: 2026-03-27T12:44:29.466Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:44:29.466Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem) before creating new ones

Applied to files:

  • web/src/pages/org-edit/DepartmentEditDrawer.tsx
  • web/src/pages/SettingsPage.tsx
📚 Learning: 2026-04-03T16:11:48.610Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-03T16:11:48.610Z
Learning: Applies to web/src/components/**/*.{ts,tsx} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones in the React web dashboard.

Applied to files:

  • web/src/pages/org-edit/DepartmentEditDrawer.tsx
  • web/src/pages/SettingsPage.tsx
📚 Learning: 2026-03-31T14:31:11.894Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:31:11.894Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use React 19, TypeScript 6.0+, and design system tokens from shadcn/ui + Tailwind CSS 4 + Radix UI in web dashboard

Applied to files:

  • web/src/pages/org-edit/DepartmentEditDrawer.tsx
  • web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/components/ui/**/*.tsx : Export props as a TypeScript interface for new components

Applied to files:

  • web/src/pages/org-edit/DepartmentEditDrawer.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/components/ui/*.{tsx,ts} : For new shared React components: place in web/src/components/ui/ with kebab-case filename, create .stories.tsx with all states, export props as TypeScript interface, use design tokens exclusively

Applied to files:

  • web/src/pages/org-edit/DepartmentEditDrawer.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
  • web/src/pages/settings/ceremony-policy/strategies/MilestoneDrivenConfig.tsx
📚 Learning: 2026-04-03T10:54:21.472Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T10:54:21.472Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do NOT build card-with-header layouts from scratch -- use `<SectionCard>` component.

Applied to files:

  • web/src/pages/SettingsPage.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT build card-with-header layouts from scratch; use `<SectionCard>`

Applied to files:

  • web/src/pages/SettingsPage.tsx
📚 Learning: 2026-04-01T20:43:51.878Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T20:43:51.878Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` before creating new ones. Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions -- use design tokens and `@/lib/motion` presets.

Applied to files:

  • web/src/pages/SettingsPage.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones; refer to design system inventory (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, etc.)

Applied to files:

  • web/src/pages/SettingsPage.tsx
📚 Learning: 2026-04-03T10:54:21.472Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T10:54:21.472Z
Learning: Applies to web/src/components/ui/**/*.{tsx,ts} : When creating new shared components, place them in `web/src/components/ui/` with descriptive kebab-case filename, create accompanying `.stories.tsx` with all states (default, hover, loading, error, empty), export props as TypeScript interface, and use design tokens exclusively with no hardcoded colors, fonts, or spacing.

Applied to files:

  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-04-03T10:54:21.472Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T10:54:21.472Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use token variables (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`) for shadows and borders. NEVER hardcode values.

Applied to files:

  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx
📚 Learning: 2026-04-03T10:54:21.472Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T10:54:21.472Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do NOT create complex (>8 line) JSX inside `.map()` -- extract to a shared component.

Applied to files:

  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
  • web/src/pages/settings/ceremony-policy/strategies/MilestoneDrivenConfig.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT create complex (>8 line) JSX inside .map()—extract to a shared component

Applied to files:

  • web/src/pages/SettingsPage.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT create complex (>8 line) JSX inside `.map()`; extract to a shared component

Applied to files:

  • web/src/pages/SettingsPage.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do NOT create complex (>8 line) JSX inside `.map()` -- extract to a shared component

Applied to files:

  • web/src/pages/SettingsPage.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/components/ui/**/*.tsx : Use design tokens exclusively in new components -- no hardcoded colors, fonts, or spacing

Applied to files:

  • web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx
📚 Learning: 2026-04-03T10:54:21.472Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T10:54:21.472Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do NOT hardcode Framer Motion transition durations -- use `@/lib/motion` presets.

Applied to files:

  • web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/MilestoneDrivenConfig.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Web dashboard shadows/borders: use token variables (var(--so-shadow-card-hover), border-border, border-bright); never hardcode shadow or border values

Applied to files:

  • web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx
📚 Learning: 2026-04-03T10:54:21.472Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T10:54:21.472Z
Learning: Applies to web/src/**/*.{tsx,ts,css} : Do NOT use `rgba()` with hardcoded values -- use design token variables.

Applied to files:

  • web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx
📚 Learning: 2026-03-31T14:31:11.894Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:31:11.894Z
Learning: Applies to web/src/**/*.{ts,tsx} : NEVER hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions — use design tokens and `@/lib/motion` presets

Applied to files:

  • web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do not use rgba() with hardcoded values -- use design token variables

Applied to files:

  • web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{ts,tsx,css} : Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions in web code — use design tokens and `@/lib/motion` presets

Applied to files:

  • web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx
📚 Learning: 2026-04-03T10:54:21.472Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T10:54:21.472Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use density-aware tokens (`p-card`, `gap-section-gap`, `gap-grid-gap`) or standard Tailwind spacing. NEVER hardcode pixel values for layout spacing.

Applied to files:

  • web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx
📚 Learning: 2026-04-03T16:11:48.610Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-03T16:11:48.610Z
Learning: Applies to web/src/components/**/*.{ts,tsx} : NEVER hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions in React components — use design tokens and `@/lib/motion` presets.

Applied to files:

  • web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use density-aware tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; never hardcode pixel values for layout spacing

Applied to files:

  • web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : When creating new shared web components, place in web/src/components/ui/ with kebab-case filename, create .stories.tsx alongside with all states (default, hover, loading, error, empty), export props as TypeScript interface, use design tokens exclusively with no hardcoded colors/fonts/spacing, and import cn from `@/lib/utils` for conditional class merging

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-04-01T14:22:06.315Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T14:22:06.315Z
Learning: Applies to {**/*.py,web/src/**/*.{ts,tsx}} : Validate at system boundaries (user input, external APIs, config files)

Applied to files:

  • web/src/pages/settings/ceremony-policy/strategies/MilestoneDrivenConfig.tsx
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/api/**/*.py : API package (api/): Litestar REST + WebSocket with controllers, guards, channels, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint, provider management endpoint (CRUD + test + presets), backup endpoint, RFC 9457 structured errors, AppState hot-reload slots, service auto-wiring (Phase 1 at construction, Phase 2 on startup), lifecycle helpers

Applied to files:

  • src/synthorg/api/controllers/ceremony_policy.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/api/**/*.py : REST API: Litestar framework, controllers with guards, channels for WebSocket, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint. RFC 9457 structured errors (ErrorCategory, ErrorCode, ErrorDetail, ProblemDetail, CATEGORY_TITLES, category_title, category_type_uri, content negotiation).

Applied to files:

  • src/synthorg/api/controllers/ceremony_policy.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/**/*.py : Package structure: src/synthorg/ organized as: api/ (REST+WebSocket, Litestar), auth/ (auth subpackage), backup/ (scheduled/manual backups), budget/ (cost tracking, CFO), cli/ (superseded by Go CLI), communication/ (message bus, meetings), config/ (YAML loading), core/ (domain models, resilience config), engine/ (orchestration, task state, coordination, approval gates, stagnation detection, context budget, compaction), hr/ (hiring, performance, promotion), memory/ (pluggable backend, Mem0, retrieval, consolidation), persistence/ (operational data, SQLite, settings), observability/ (logging, correlation, sinks), providers/ (LLM abstraction, LiteLLM, auth types, presets, runtime CRUD), settings/ (runtime-editable, typed definitions, encryption, config bridge), security/ (SecOps, rule engine, output scanning, progressive trust, autonomy levels), templates/ (company templates, personalities), tools/ (registry, built-in tools, git, sandbox, code_runner, MCP...

Applied to files:

  • src/synthorg/api/controllers/ceremony_policy.py
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/api/**/*.py : Use Litestar for REST + WebSocket API. Controllers, guards, channels, JWT + API key + WS ticket auth, RFC 9457 structured errors.

Applied to files:

  • src/synthorg/api/controllers/ceremony_policy.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : Prefer `asyncio.TaskGroup` for fan-out/fan-in parallel operations in new code (e.g. multiple tool invocations, parallel agent calls). Prefer structured concurrency over bare `create_task`. Existing code is being migrated incrementally.

Applied to files:

  • src/synthorg/api/controllers/ceremony_policy.py
📚 Learning: 2026-03-17T18:52:05.142Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T18:52:05.142Z
Learning: Applies to **/*.py : Async concurrency: prefer asyncio.TaskGroup for fan-out/fan-in parallel operations in new code (e.g. multiple tool invocations, parallel agent calls). Prefer structured concurrency over bare create_task.

Applied to files:

  • src/synthorg/api/controllers/ceremony_policy.py
📚 Learning: 2026-04-03T16:11:48.610Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-03T16:11:48.610Z
Learning: Applies to **/*.py : Prefer `asyncio.TaskGroup` for fan-out/fan-in parallel operations in new code (e.g. multiple tool invocations, parallel agent calls). Prefer structured concurrency over bare `create_task`.

Applied to files:

  • src/synthorg/api/controllers/ceremony_policy.py
📚 Learning: 2026-04-01T09:39:21.584Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T09:39:21.584Z
Learning: Applies to **/*.py : Prefer `asyncio.TaskGroup` for fan-out/fan-in parallel operations in new code (e.g., multiple tool invocations, parallel agent calls); prefer structured concurrency over bare `create_task`

Applied to files:

  • src/synthorg/api/controllers/ceremony_policy.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : Handle errors explicitly, never silently swallow. Validate at system boundaries (user input, external APIs, config files).

Applied to files:

  • src/synthorg/api/controllers/ceremony_policy.py
📚 Learning: 2026-03-16T20:14:00.937Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T20:14:00.937Z
Learning: Applies to **/*.py : Validate: at system boundaries (user input, external APIs, config files).

Applied to files:

  • src/synthorg/api/controllers/ceremony_policy.py
📚 Learning: 2026-03-19T07:13:44.964Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:13:44.964Z
Learning: Applies to **/*.py : Validate at system boundaries (user input, external APIs, config files)

Applied to files:

  • src/synthorg/api/controllers/ceremony_policy.py
📚 Learning: 2026-04-03T16:11:48.610Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-03T16:11:48.610Z
Learning: Applies to **/*.py : Validate at system boundaries (user input, external APIs, config files).

Applied to files:

  • src/synthorg/api/controllers/ceremony_policy.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 conventions: `BaseModel`, `model_validator`, `computed_field`, `ConfigDict`. For derived values use `computed_field` instead of storing + validating redundant fields. Use `NotBlankStr` (from `core.types`) for all identifier/name fields — including optional (`NotBlankStr | None`) and tuple (`tuple[NotBlankStr, ...]`) variants — instead of manual whitespace validators.

Applied to files:

  • src/synthorg/api/controllers/ceremony_policy.py
📚 Learning: 2026-03-15T18:42:17.990Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:42:17.990Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 conventions: `BaseModel`, `model_validator`, `computed_field`, `ConfigDict`

Applied to files:

  • src/synthorg/api/controllers/ceremony_policy.py
📚 Learning: 2026-03-31T21:07:37.470Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T21:07:37.470Z
Learning: Applies to **/*.py : Use `except A, B:` (no parentheses) per PEP 758 exception syntax on Python 3.14

Applied to files:

  • src/synthorg/api/controllers/ceremony_policy.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 BaseModel, model_validator, computed_field, ConfigDict.

Applied to files:

  • src/synthorg/api/controllers/ceremony_policy.py
📚 Learning: 2026-04-01T09:58:27.410Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T09:58:27.410Z
Learning: Applies to **/*.py : Use `copy.deepcopy()` at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, persistence serialization) for dict/list fields in frozen Pydantic models

Applied to files:

  • src/synthorg/api/controllers/ceremony_policy.py
🔇 Additional comments (5)
web/src/pages/org-edit/DepartmentEditDrawer.tsx (4)

1-10: LGTM — imports are clean and necessary.

The new imports properly support the ceremony policy feature: useEffect for the prop-to-state sync pattern, CeremonyPolicyConfig type, and the new DepartmentCeremonyOverride component.


33-33: LGTM — state declaration is correctly typed.

The ceremonyPolicy state is properly typed as CeremonyPolicyConfig | null to match the optional field in the department model.


68-77: LGTM — ceremony_policy correctly integrated into save flow.

The ceremony_policy field is properly included in the update payload and dependency array. Passing null explicitly allows clearing the department override, which aligns with the API contract in UpdateDepartmentRequest.


118-123: No action needed. DepartmentCeremonyOverride is a form-based component without JSON input, so JSON parse errors cannot occur. The component's inputs are type-safe and include validation (e.g., numeric input guards), preventing invalid data from reaching onChange.

			> Likely an incorrect or invalid review comment.
web/src/pages/settings/ceremony-policy/strategies/MilestoneDrivenConfig.tsx (1)

49-61: Good local validation before propagating config

Line 52-Line 57 correctly enforces both JSON parse validity and array shape before calling onChange, which prevents malformed editor input from leaking upstream.

Comment on lines +39 to +57
useEffect(() => {
if (department !== prevDepartmentRef.current) {
prevDepartmentRef.current = department
if (department) {
// eslint-disable-next-line @eslint-react/set-state-in-effect -- legitimate prop-to-local-state sync
setDisplayName(department.display_name ?? department.name)
// eslint-disable-next-line @eslint-react/set-state-in-effect -- legitimate prop-to-local-state sync
setBudgetPercent(department.budget_percent != null ? String(department.budget_percent) : '0')
// eslint-disable-next-line @eslint-react/set-state-in-effect -- legitimate prop-to-local-state sync
setCeremonyPolicy(department.ceremony_policy ?? null)
// eslint-disable-next-line @eslint-react/set-state-in-effect -- legitimate prop-to-local-state sync
setSubmitError(null)
}
// eslint-disable-next-line @eslint-react/set-state-in-effect -- legitimate prop-to-local-state sync
setDeleteOpen(false)
// eslint-disable-next-line @eslint-react/set-state-in-effect -- legitimate prop-to-local-state sync
setDeleting(false)
}
setDeleteOpen(false)
setDeleting(false)
}
}, [department])
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider consolidating the repeated eslint-disable comments.

The prop-to-state sync pattern is legitimate here, but six separate inline disable comments add visual noise. Consider a single block-level disable above the effect:

♻️ Suggested consolidation
  const prevDepartmentRef = useRef<typeof department | undefined>(undefined)
+ /* eslint-disable `@eslint-react/set-state-in-effect` -- legitimate prop-to-local-state sync on department change */
  useEffect(() => {
    if (department !== prevDepartmentRef.current) {
      prevDepartmentRef.current = department
      if (department) {
-       // eslint-disable-next-line `@eslint-react/set-state-in-effect` -- legitimate prop-to-local-state sync
        setDisplayName(department.display_name ?? department.name)
-       // eslint-disable-next-line `@eslint-react/set-state-in-effect` -- legitimate prop-to-local-state sync
        setBudgetPercent(department.budget_percent != null ? String(department.budget_percent) : '0')
-       // eslint-disable-next-line `@eslint-react/set-state-in-effect` -- legitimate prop-to-local-state sync
        setCeremonyPolicy(department.ceremony_policy ?? null)
-       // eslint-disable-next-line `@eslint-react/set-state-in-effect` -- legitimate prop-to-local-state sync
        setSubmitError(null)
      }
-     // eslint-disable-next-line `@eslint-react/set-state-in-effect` -- legitimate prop-to-local-state sync
      setDeleteOpen(false)
-     // eslint-disable-next-line `@eslint-react/set-state-in-effect` -- legitimate prop-to-local-state sync
      setDeleting(false)
    }
  }, [department])
+ /* eslint-enable `@eslint-react/set-state-in-effect` */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/org-edit/DepartmentEditDrawer.tsx` around lines 39 - 57, The
repeated inline eslint-disable comments in the useEffect body create noise;
replace them with a single block-level directive above the effect that disables
`@eslint-react/set-state-in-effect` for the whole effect, and keep the same
prop-to-local-state sync logic inside useEffect (the block should wrap the
effect that references useEffect, department, prevDepartmentRef.current,
setDisplayName, setBudgetPercent, setCeremonyPolicy, setSubmitError,
setDeleteOpen, and setDeleting) so you remove the six //
eslint-disable-next-line comments while preserving behavior.

Comment on lines +13 to +17
const rawPct = typeof config.transition_threshold === 'number' && Number.isFinite(config.transition_threshold)
? config.transition_threshold
: 100
const transitionPct = Math.min(100, Math.max(1, rawPct))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t keep a second transition_threshold inside budget strategy config.

CeremonyPolicyPage already edits and saves the canonical transition threshold via PolicyFieldsPanel / ceremony_transition_threshold. Writing another threshold under strategyConfig gives budget-driven two unsynchronized controls for the same behavior, so one of them will drift or be ignored.

Also applies to: 34-45

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx`
around lines 13 - 17, The strategy config currently reads and persists a
duplicate transition_threshold (see BudgetDrivenConfig and the config variable
usage at the blocks around the shown lines and 34–45); remove any reads, UI
controls, and writes that reference config.transition_threshold inside
BudgetDrivenConfig and instead use the canonical ceremony_transition_threshold
maintained by CeremonyPolicyPage/PolicyFieldsPanel (or accept it via props) for
display and saves; ensure you also stop persisting transition_threshold into
strategyConfig when saving the policy so there is only one source of truth.

- useAnimationPreset fast-check: reduce iterations to 10 (renderHook
  per iteration is expensive; parameterized test covers all 5 presets)
- Use typed _SettingsNotFound class instead of bare object() sentinel
  for cleaner isinstance narrowing
- inherit-toggle: dynamic aria-label based on inherit/override state
- CeremonyPolicyPage: isDirty guard prevents snapshot clobbering edits
- DepartmentOverridesPanel: strategy-derived velocity_calculator fallback
- EventDrivenConfig: typeof + Number.isFinite defensive reads
- MilestoneDrivenConfig: useEffect to sync rawJson from config.milestones
- ThroughputAdaptiveConfig: defensive reads + clamped onChange handlers
- StrategyConfigPanel: exhaustive switch via assertNever helper
- departments.py: TODO comment for cross-worker CAS on policy lock
- departments.py: wrap _save_dept_policies_json exceptions
- DepartmentCeremonyOverride: move render-phase state update to useEffect
- DepartmentCeremonyOverride: preserve strategy_config on strategy change
- CeremonyListPanel: add StrategyConfigPanel to per-ceremony expanded view
- PolicyFieldsPanel: clamp transition threshold to 0.01-1.0
- TaskDrivenConfig: add Number.isFinite to read guards
- StrategyChangeWarning: add role=status and aria-live=polite
- StrategyPicker: runtime guard validates strategy before onChange
- ceremony_policy.py: strict _parse_auto_transition validation (reject
  non-boolean strings with ValueError instead of silently returning False)
- ceremony_policy.py: wrap _build_project_policy call in try/except for
  ValueError/JSONDecodeError, raising ServiceUnavailableError
- CeremonyPolicyPage.tsx: pass disabled={saving} to StrategyPicker,
  StrategyConfigPanel, PolicyFieldsPanel during save
- CeremonyPolicyPage.tsx: disable Save button and show warning when
  strategy config or overrides JSON has parse errors
- CeremonyPolicyPage.tsx: use STRATEGY_DEFAULT_VELOCITY_CALC[strategy]
  instead of hardcoded task_driven for velocity calculator fallback
- CeremonyPolicyPage.tsx: skip ceremonyOverrides sync when isDirty
- BudgetDrivenConfig.tsx: clamp transitionPct min to 1, not 0
- MilestoneDrivenConfig.tsx: replace as-string cast with typeof guard
- SettingsPage.tsx: extract footerAction ternary to getFooterAction()
- DepartmentEditDrawer.tsx: move render-phase state update to useEffect
- Use NotBlankStr for department_name params in _lookup_dept_override_from_settings and _fetch_department_policy
- Filter MemoryError/RecursionError from except* ExceptionGroup before translating to ServiceUnavailableError
- Sanitize error message in ServiceUnavailableError (keep details in log only)
- Simplify redundant except (ValueError, json.JSONDecodeError) to except ValueError
- Consolidate eslint-disable comments in DepartmentEditDrawer useEffect
- Validate ceremony_strategy against CEREMONY_STRATEGY_TYPES, fall back to task_driven
- Validate transitionThreshold with Number.isFinite and clamp to 0.01-1.0
- Validate JSON.parse results are plain objects before using as config
- Guard config.milestones with Array.isArray in MilestoneDrivenConfig
@Aureliolo Aureliolo force-pushed the feat/ceremony-dashboard-dept-overrides branch from 4329d43 to fca8fdc Compare April 3, 2026 17:22
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview April 3, 2026 17:23 — with GitHub Actions Inactive
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

♻️ Duplicate comments (11)
web/src/pages/SettingsPage.tsx (1)

61-66: 🛠️ Refactor suggestion | 🟠 Major

Reuse the shared Button primitive for the “Open” CTA.

Lines 61-66 still render a hand-rolled button-like span instead of the shared Button, which breaks consistency and duplicates a core UI primitive.

♻️ Proposed fix
+import { Button } from '@/components/ui/button'
...
-      <div className="w-56 shrink-0">
-        <span
-          className="inline-flex h-9 w-full items-center justify-center rounded-md border border-border bg-card px-4 text-sm font-medium text-foreground"
-          aria-hidden
-        >
-          Open
-        </span>
-      </div>
+      <div className="w-56 shrink-0">
+        <Button asChild variant="outline" className="h-9 w-full pointer-events-none">
+          <span aria-hidden>Open</span>
+        </Button>
+      </div>

As per coding guidelines, “ALWAYS reuse existing components from web/src/components/ui/ before creating new ones.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/SettingsPage.tsx` around lines 61 - 66, Replace the hand-rolled
span used to render the “Open” CTA with the shared Button primitive: import
Button (the exported component named Button) and render <Button>Open</Button> in
place of the span, moving any needed className/props (aria-hidden, size/variant
or className used on the span) onto the Button so visual and accessibility
behavior is preserved; ensure you remove the old span markup and use the Button
component from the shared ui primitives to keep consistency.
web/src/components/ui/policy-source-badge.tsx (1)

21-31: 🛠️ Refactor suggestion | 🟠 Major

Compose the existing StatusBadge instead of custom badge markup.

This shared component is re-implementing status badge presentation directly. Please delegate rendering to the existing StatusBadge and keep only source→label/variant mapping here.

As per coding guidelines, “ALWAYS reuse existing components from web/src/components/ui/ before creating new ones” and “Do NOT recreate status dots inline -- use <StatusBadge> component.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/ui/policy-source-badge.tsx` around lines 21 - 31,
PolicySourceBadge currently renders custom badge markup; change it to compose
the existing StatusBadge instead by mapping source → label and variant only.
Inside PolicySourceBadge (and using SOURCE_LABELS and SOURCE_STYLES for the
mapping), return <StatusBadge> with the label set to SOURCE_LABELS[source], the
variant prop set to the corresponding SOURCE_STYLES[source] (or rename mapping
to a variant value if needed), and forward className/other props; remove the
inline span/px/py/text classes so all visual rendering is delegated to
StatusBadge.
web/src/components/ui/inherit-toggle.stories.tsx (1)

13-23: ⚠️ Potential issue | 🟠 Major

Story coverage is incomplete for design-system requirements.

Inherit, Override, and Disabled are present, but required shared-component states (hover, loading, error, empty) are still missing.

As per coding guidelines, “When creating new shared components... create accompanying .stories.tsx with all states (default, hover, loading, error, empty)...”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/ui/inherit-toggle.stories.tsx` around lines 13 - 23, The
story file currently exports Inherit, Override, and Disabled but lacks the
required shared-component states; add new Story exports named Hover, Loading,
Error, and Empty alongside the existing Inherit/Override/Disabled, each
targeting the same component and setting appropriate args/state (e.g., Hover
should simulate hover state via Storybook parameters or a play function, Loading
should set a loading prop or mimic spinner state, Error should pass an error
prop/message, and Empty should pass empty data/props), ensuring the exported
symbol names Hover, Loading, Error, and Empty are added so the design-system
test coverage includes default, hover, loading, error, and empty states.
web/src/components/ui/policy-source-badge.stories.tsx (1)

13-23: ⚠️ Potential issue | 🟠 Major

Add the required state stories for this shared UI component.

This file only covers source variants; it still lacks the mandatory state set (default, hover, loading, error, empty) for shared UI component stories.

As per coding guidelines, “When creating new shared components, place them in web/src/components/ui/ with descriptive kebab-case filename, create accompanying .stories.tsx with all states (default, hover, loading, error, empty)...”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/ui/policy-source-badge.stories.tsx` around lines 13 - 23,
The story file currently only exports the source variants (Project, Department,
Default); add the required state stories for this shared UI component by
creating and exporting stories for the five mandatory states (DefaultState,
HoverState, LoadingState, ErrorState, EmptyState) for the component (ensure each
story is a Story and sets the appropriate args such as source: 'project' |
'department' | 'default' and a state prop or parameter to represent
default/hover/loading/error/empty); repeat or parametrize these state stories
for each source variant (Project, Department, Default) so every source has the
full set of state stories.
src/synthorg/api/controllers/departments.py (1)

598-656: ⚠️ Potential issue | 🟠 Major

The lock still leaves PUT/DELETE lossy across workers.

_dept_policy_lock only serializes coroutines inside one event loop. Two API workers can still read the same dept_ceremony_policies snapshot and last-write-wins each other on update/delete. This needs CAS/versioned writes or per-department storage if the service is ever run with multiple workers/replicas.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/api/controllers/departments.py` around lines 598 - 656, The
current asyncio.Lock (_dept_policy_lock) only serializes within one process so
_set_dept_ceremony_override and _clear_dept_ceremony_override remain racy across
multiple workers; change the read-modify-write to use a CAS/versioned update or
per-department keys via the settings backend instead of relying on the
in-process lock: load the current policies with _load_dept_policies_json
including a version/ETag, apply the change only if the version matches (retry on
mismatch) and persist with _save_dept_policies_json using the backend's
compare-and-swap API, or refactor to store each department under its own key to
avoid snapshot-based races.
src/synthorg/settings/definitions/coordination.py (1)

190-204: ⚠️ Potential issue | 🟡 Minor

Enhance the inline comment to explain both aggregate JSON blob cases.

The comment at lines 168–172 explains why dept_ceremony_policies omits yaml_path, but it should also clarify the same for ceremony_policy_overrides. Both are intentionally service-managed JSON blobs; the comment should explicitly state that per-ceremony overrides are not stored as a separate YAML top-level blob, mirroring the rationale for the department case.

Update the comment to cover both:

# The next two settings are aggregate JSON blobs managed entirely through the
# settings service (keyed by department or ceremony name).  They intentionally
# omit yaml_path because they do not map to a single YAML config path -- the
# YAML company config stores per-department ceremony_policy inline on each
# department object, and per-ceremony overrides inline on each ceremony object,
# not as separate top-level blobs.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/settings/definitions/coordination.py` around lines 190 - 204,
Update the inline comment that explains why dept_ceremony_policies omits
yaml_path to also mention ceremony_policy_overrides: state that both settings
are aggregate JSON blobs managed entirely by the settings service (keyed by
department or ceremony name) and intentionally omit yaml_path because the
company YAML stores per-department ceremony_policy inline on each department and
per-ceremony overrides inline on each ceremony rather than as separate top-level
blobs; reference the SettingDefinition instances for dept_ceremony_policies and
ceremony_policy_overrides to locate where to apply this expanded comment.
web/src/stores/ceremony-policy.ts (1)

64-72: ⚠️ Potential issue | 🟡 Minor

fetchActiveStrategy lacks loading state management.

Unlike fetchResolvedPolicy which sets loading: true/false, this action only manages activeStrategyError. If the UI relies on loading to show a spinner during initial load, the active strategy fetch won't be covered. Consider adding a dedicated activeStrategyLoading state or documenting that this fetch is intentionally fire-and-forget.

🔧 Option 1: Add dedicated loading state
 interface CeremonyPolicyState {
   ...
+  activeStrategyLoading: boolean
   ...
 }

 export const useCeremonyPolicyStore = create<CeremonyPolicyState>()((set, get) => ({
   ...
+  activeStrategyLoading: false,
   ...
   fetchActiveStrategy: async () => {
-    set({ activeStrategyError: null })
+    set({ activeStrategyLoading: true, activeStrategyError: null })
     try {
       const active = await ceremonyApi.getActiveStrategy()
-      set({ activeStrategy: active })
+      set({ activeStrategy: active, activeStrategyLoading: false })
     } catch (err) {
-      set({ activeStrategyError: getErrorMessage(err) })
+      set({ activeStrategyError: getErrorMessage(err), activeStrategyLoading: false })
     }
   },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/stores/ceremony-policy.ts` around lines 64 - 72, fetchActiveStrategy
currently only sets activeStrategyError and doesn't manage a loading flag like
fetchResolvedPolicy; add loading state handling by introducing a dedicated
boolean (e.g., activeStrategyLoading) or reuse the existing loading field if
appropriate, set it true before calling ceremonyApi.getActiveStrategy and false
in both the try and catch branches, ensure you still set activeStrategy on
success and activeStrategyError via getErrorMessage(err) on failure so the UI
spinner is driven correctly by the new loading flag.
src/synthorg/api/controllers/ceremony_policy.py (2)

412-412: ⚠️ Potential issue | 🟡 Minor

Use the repo’s required PEP 758 except A, B form here.

This handler still uses the parenthesized exception list, which violates the Python 3.14 syntax rule enforced for src/**/*.py.

As per coding guidelines: This project uses Python 3.14+ with PEP 758 except syntax: "except A, B:" (comma-separated, no parentheses) is correct and mandatory -- do NOT flag it as a typo or suggest parenthesized form.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/api/controllers/ceremony_policy.py` at line 412, Replace the
parenthesized exception tuple in the except clause with the repository-required
PEP 758 comma-separated form: change the handler that currently reads "except
(json.JSONDecodeError, TypeError) as exc:" to use "except json.JSONDecodeError,
TypeError as exc:" so the exception handler in the function/method around the
ceremony policy parsing uses the correct syntax.

421-428: ⚠️ Potential issue | 🟠 Major

Reject malformed department override payloads instead of hiding or leaking them.

In the settings-backed path, any non-dict/non-None value currently falls through as _SETTINGS_NOT_FOUND, and invalid dicts from either source can still raise raw model_validate() errors. That makes corrupt settings/YAML either disappear silently or surface as 500s. Both branches should validate “None or object” explicitly and translate bad override shapes into ServiceUnavailableError.

Based on learnings, validate at system boundaries (user input, external APIs, config files).

Also applies to: 465-472

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/api/controllers/ceremony_policy.py` around lines 421 - 428, The
code currently lets non-dict/non-None override values fall through to
_SETTINGS_NOT_FOUND and lets raw model_validate errors bubble up; change the
logic to explicitly accept only None or dict for the department override and
translate any other shape into a ServiceUnavailableError, and wrap
CeremonyPolicyConfig.model_validate(...) calls in a try/except (catching
validation errors/exceptions) to raise ServiceUnavailableError with a clear
message instead of allowing a 500 or silent miss; apply the same explicit-type
check and exception-wrapping pattern to the other similar branch that uses
CeremonyPolicyConfig.model_validate so all malformed settings/YAML produce
ServiceUnavailableError rather than _SETTINGS_NOT_FOUND or raw exceptions.
web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx (2)

219-229: ⚠️ Potential issue | 🟠 Major

Make the project save path transactional.

This is still one logical policy update split across six independent writes. If any later request fails after earlier ones succeeded, the persisted ceremony policy is left in a mixed state that the UI cannot roll back. This needs a batch/transactional backend API before the page reports project saves as a single action.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx` around lines
219 - 229, The current handleSave is performing six independent updateSetting
calls (updateSetting('coordination','ceremony_strategy',...),
updateSetting('coordination','ceremony_strategy_config',...),
updateSetting('coordination','ceremony_velocity_calculator',...),
updateSetting('coordination','ceremony_auto_transition',...),
updateSetting('coordination','ceremony_transition_threshold',...),
updateSetting('coordination','ceremony_policy_overrides',...)) which can leave
the policy in a partially-updated state; replace these parallel calls with a
single transactional backend call (e.g., add an API like updateSettingsBatch or
updateProjectCeremonyPolicy that accepts a combined payload { ceremony_strategy,
ceremony_strategy_config, ceremony_velocity_calculator,
ceremony_auto_transition, ceremony_transition_threshold,
ceremony_policy_overrides }) and update handleSave to call that single endpoint,
keep existing error handling and setSaving toggles, and surface any backend
error to the UI so saves are atomic from the frontend's perspective.

50-62: ⚠️ Potential issue | 🟠 Major

Treat empty JSON strings as corrupt settings.

if (sc) and if (raw) still classify '' as “missing”, so invalid raw settings can bypass the parse-error flags and then get silently overwritten on the next save. Parse any defined value here, and mark non-object / empty JSON payloads invalid instead of treating them as absent.

Based on learnings, validate at system boundaries (user input, external APIs, config files).

Also applies to: 151-162

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx` around lines
50 - 62, The code currently treats empty strings as "missing" because it checks
`if (sc)` before parsing; change the check to detect defined values (e.g., `sc
!== undefined && sc !== null`) so you always attempt JSON.parse for any provided
value (including `''`) and on success validate that the parsed value is a
non-empty object (e.g., typeof parsed === 'object' && parsed !== null &&
!Array.isArray(parsed) && Object.keys(parsed).length > 0); if parsing fails or
the parsed value is not a non-empty object set `configParseError = true` and
avoid setting `config`. Apply the same change to the analogous `raw` handling
block (lines referencing `raw`) so empty JSON strings are flagged as corrupt
rather than treated as absent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/synthorg/api/controllers/departments.py`:
- Around line 530-578: The function _get_dept_ceremony_override currently reads
the settings blob via _load_dept_policies_json and treats any non-dict/non-None
value as inheritance and returns None without first confirming the department
exists; update it to first verify the department exists (perform the department
lookup using your app_state's department retrieval method) before applying
settings-backed overrides, and if the stored override is neither dict nor None,
log the unexpected type and raise ServiceUnavailableError (instead of returning
None); keep the existing validation step using
CeremonyPolicyConfig.model_validate and preserve raising on validation errors.

In `@web/src/pages/org-edit/DepartmentCeremonyOverride.tsx`:
- Around line 36-37: The hasOverride check currently uses "policy != null" which
treats an empty object as an override; change it to the stricter check used in
DepartmentOverridesPanel (policy != null && Object.keys(policy).length > 0) so
behavior matches DepartmentOverridesPanel (and decide whether CeremonyListPanel
should be aligned too); update the hasOverride initialization in
DepartmentCeremonyOverride (and anywhere else using the looser check like
CeremonyListPanel if consistency is desired) to use policy != null &&
Object.keys(policy).length > 0.

In `@web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx`:
- Around line 105-113: The velocityCalculator prop is using a hardcoded fallback
'task_driven' which is inconsistent with DepartmentOverridesPanel; change the
fallback to use the strategy-aware default
(STRATEGY_DEFAULT_VELOCITY_CALC[strategy]) instead of 'task_driven' so
PolicyFieldsPanel receives strategy-specific defaults; update the line setting
velocityCalculator in CeremonyListPanel to use
STRATEGY_DEFAULT_VELOCITY_CALC[strategy] (keeping the existing on* handlers and
other fallbacks unchanged).
- Around line 57-62: handleStrategyChange currently updates the policy.strategy
but leaves policy.strategy_config intact, causing stale/incompatible config keys
to accumulate; change the handler (handleStrategyChange) to also reset
strategy_config to an empty object when calling onOverrideChange (i.e., call
onOverrideChange(name, { ...policy, strategy: s, strategy_config: {} })) so new
strategies start with a clean config; update the useCallback dependency if
needed to reflect any renamed symbols.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx`:
- Around line 76-82: The form seeding currently type-asserts
ceremony_velocity_calculator and treats any non-"false" string as true for
ceremony_auto_transition, allowing malformed settings to persist; update the
seeding logic (the velocityCalculator assignment that reads
get('ceremony_velocity_calculator') and the autoTransition assignment that reads
get('ceremony_auto_transition')) to explicitly validate values: parse the
velocityCalculator value and only accept it if it matches one of the keys in
STRATEGY_DEFAULT_VELOCITY_CALC (otherwise set undefined or mark invalid like the
JSON blob handling), and parse autoTransition to boolean only when the value is
the literal "true" or "false" (otherwise treat as invalid and surface it to the
form the same way invalid JSON blobs are surfaced) so malformed YAML/raw
settings aren’t silently accepted and re-written.
- Around line 290-302: The page currently hides the entire editor when
storeError (or resolved-policy fetch) fails, but the editor is driven by
settingsSnapshot and should remain editable; change the conditional rendering so
the editor/form uses settingsSnapshot presence (e.g., render when
settingsSnapshot != null/undefined) instead of gating on !storeError, and keep
resolvedPolicy/storeError rendering limited to the provenance badges/error boxes
(i.e., show the resolvedPolicy badge area or the specific error box when
resolvedPolicy or storeError is present). Apply the same fix for the other
duplicated checks involving storeError at the other locations (the
activeStrategy/resolved-policy badge blocks referenced by activeStrategyError
and resolvedPolicy).

In `@web/src/router/index.tsx`:
- Line 121: The current use of ROUTES.SETTINGS_CEREMONY_POLICY.slice(1) is
brittle because it assumes the constant has a leading "/"—create a small
normalization helper (e.g., normalizeRoute or trimLeadingSlash) or define a
slash-free ROUTE_PATHS mapping and use that instead; update the router entry
that references ROUTES.SETTINGS_CEREMONY_POLICY to call the helper (or use the
new mapping) so the route string is consistently normalized without relying on
slice(1).

In `@web/src/utils/constants.ts`:
- Around line 162-176: VELOCITY_CALC_LABELS and VELOCITY_UNIT_LABELS hardcode
the budget labels to "EUR"; change the budget entries to be currency-aware by
either (A) replacing the static strings for the 'budget' key with placeholders
(e.g. "Per Currency Unit (pts/{currency})" and "pts/{currency}") and then
substitute the active project currency at render time wherever these maps are
used, or (B) convert the maps into functions/getters that accept the active
currency and return the appropriate label for the 'budget' case; update usages
that read VELOCITY_CALC_LABELS and VELOCITY_UNIT_LABELS to pass or substitute
the active currency so the displayed unit matches the project currency.

---

Duplicate comments:
In `@src/synthorg/api/controllers/ceremony_policy.py`:
- Line 412: Replace the parenthesized exception tuple in the except clause with
the repository-required PEP 758 comma-separated form: change the handler that
currently reads "except (json.JSONDecodeError, TypeError) as exc:" to use
"except json.JSONDecodeError, TypeError as exc:" so the exception handler in the
function/method around the ceremony policy parsing uses the correct syntax.
- Around line 421-428: The code currently lets non-dict/non-None override values
fall through to _SETTINGS_NOT_FOUND and lets raw model_validate errors bubble
up; change the logic to explicitly accept only None or dict for the department
override and translate any other shape into a ServiceUnavailableError, and wrap
CeremonyPolicyConfig.model_validate(...) calls in a try/except (catching
validation errors/exceptions) to raise ServiceUnavailableError with a clear
message instead of allowing a 500 or silent miss; apply the same explicit-type
check and exception-wrapping pattern to the other similar branch that uses
CeremonyPolicyConfig.model_validate so all malformed settings/YAML produce
ServiceUnavailableError rather than _SETTINGS_NOT_FOUND or raw exceptions.

In `@src/synthorg/api/controllers/departments.py`:
- Around line 598-656: The current asyncio.Lock (_dept_policy_lock) only
serializes within one process so _set_dept_ceremony_override and
_clear_dept_ceremony_override remain racy across multiple workers; change the
read-modify-write to use a CAS/versioned update or per-department keys via the
settings backend instead of relying on the in-process lock: load the current
policies with _load_dept_policies_json including a version/ETag, apply the
change only if the version matches (retry on mismatch) and persist with
_save_dept_policies_json using the backend's compare-and-swap API, or refactor
to store each department under its own key to avoid snapshot-based races.

In `@src/synthorg/settings/definitions/coordination.py`:
- Around line 190-204: Update the inline comment that explains why
dept_ceremony_policies omits yaml_path to also mention
ceremony_policy_overrides: state that both settings are aggregate JSON blobs
managed entirely by the settings service (keyed by department or ceremony name)
and intentionally omit yaml_path because the company YAML stores per-department
ceremony_policy inline on each department and per-ceremony overrides inline on
each ceremony rather than as separate top-level blobs; reference the
SettingDefinition instances for dept_ceremony_policies and
ceremony_policy_overrides to locate where to apply this expanded comment.

In `@web/src/components/ui/inherit-toggle.stories.tsx`:
- Around line 13-23: The story file currently exports Inherit, Override, and
Disabled but lacks the required shared-component states; add new Story exports
named Hover, Loading, Error, and Empty alongside the existing
Inherit/Override/Disabled, each targeting the same component and setting
appropriate args/state (e.g., Hover should simulate hover state via Storybook
parameters or a play function, Loading should set a loading prop or mimic
spinner state, Error should pass an error prop/message, and Empty should pass
empty data/props), ensuring the exported symbol names Hover, Loading, Error, and
Empty are added so the design-system test coverage includes default, hover,
loading, error, and empty states.

In `@web/src/components/ui/policy-source-badge.stories.tsx`:
- Around line 13-23: The story file currently only exports the source variants
(Project, Department, Default); add the required state stories for this shared
UI component by creating and exporting stories for the five mandatory states
(DefaultState, HoverState, LoadingState, ErrorState, EmptyState) for the
component (ensure each story is a Story and sets the appropriate args such as
source: 'project' | 'department' | 'default' and a state prop or parameter to
represent default/hover/loading/error/empty); repeat or parametrize these state
stories for each source variant (Project, Department, Default) so every source
has the full set of state stories.

In `@web/src/components/ui/policy-source-badge.tsx`:
- Around line 21-31: PolicySourceBadge currently renders custom badge markup;
change it to compose the existing StatusBadge instead by mapping source → label
and variant only. Inside PolicySourceBadge (and using SOURCE_LABELS and
SOURCE_STYLES for the mapping), return <StatusBadge> with the label set to
SOURCE_LABELS[source], the variant prop set to the corresponding
SOURCE_STYLES[source] (or rename mapping to a variant value if needed), and
forward className/other props; remove the inline span/px/py/text classes so all
visual rendering is delegated to StatusBadge.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx`:
- Around line 219-229: The current handleSave is performing six independent
updateSetting calls (updateSetting('coordination','ceremony_strategy',...),
updateSetting('coordination','ceremony_strategy_config',...),
updateSetting('coordination','ceremony_velocity_calculator',...),
updateSetting('coordination','ceremony_auto_transition',...),
updateSetting('coordination','ceremony_transition_threshold',...),
updateSetting('coordination','ceremony_policy_overrides',...)) which can leave
the policy in a partially-updated state; replace these parallel calls with a
single transactional backend call (e.g., add an API like updateSettingsBatch or
updateProjectCeremonyPolicy that accepts a combined payload { ceremony_strategy,
ceremony_strategy_config, ceremony_velocity_calculator,
ceremony_auto_transition, ceremony_transition_threshold,
ceremony_policy_overrides }) and update handleSave to call that single endpoint,
keep existing error handling and setSaving toggles, and surface any backend
error to the UI so saves are atomic from the frontend's perspective.
- Around line 50-62: The code currently treats empty strings as "missing"
because it checks `if (sc)` before parsing; change the check to detect defined
values (e.g., `sc !== undefined && sc !== null`) so you always attempt
JSON.parse for any provided value (including `''`) and on success validate that
the parsed value is a non-empty object (e.g., typeof parsed === 'object' &&
parsed !== null && !Array.isArray(parsed) && Object.keys(parsed).length > 0); if
parsing fails or the parsed value is not a non-empty object set
`configParseError = true` and avoid setting `config`. Apply the same change to
the analogous `raw` handling block (lines referencing `raw`) so empty JSON
strings are flagged as corrupt rather than treated as absent.

In `@web/src/pages/SettingsPage.tsx`:
- Around line 61-66: Replace the hand-rolled span used to render the “Open” CTA
with the shared Button primitive: import Button (the exported component named
Button) and render <Button>Open</Button> in place of the span, moving any needed
className/props (aria-hidden, size/variant or className used on the span) onto
the Button so visual and accessibility behavior is preserved; ensure you remove
the old span markup and use the Button component from the shared ui primitives
to keep consistency.

In `@web/src/stores/ceremony-policy.ts`:
- Around line 64-72: fetchActiveStrategy currently only sets activeStrategyError
and doesn't manage a loading flag like fetchResolvedPolicy; add loading state
handling by introducing a dedicated boolean (e.g., activeStrategyLoading) or
reuse the existing loading field if appropriate, set it true before calling
ceremonyApi.getActiveStrategy and false in both the try and catch branches,
ensure you still set activeStrategy on success and activeStrategyError via
getErrorMessage(err) on failure so the UI spinner is driven correctly by the new
loading flag.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 202f998d-d421-4c1a-9c6e-37542044c9d1

📥 Commits

Reviewing files that changed from the base of the PR and between 4329d43 and fca8fdc.

📒 Files selected for processing (45)
  • CLAUDE.md
  • docs/design/ceremony-scheduling.md
  • docs/design/page-structure.md
  • src/synthorg/api/controllers/__init__.py
  • src/synthorg/api/controllers/ceremony_policy.py
  • src/synthorg/api/controllers/departments.py
  • src/synthorg/engine/workflow/ceremony_scheduler.py
  • src/synthorg/observability/events/api.py
  • src/synthorg/settings/definitions/coordination.py
  • tests/unit/api/controllers/test_ceremony_policy.py
  • tests/unit/settings/test_ceremony_settings.py
  • web/CLAUDE.md
  • web/src/__tests__/hooks/useAnimationPreset.test.ts
  • web/src/api/endpoints/ceremony-policy.ts
  • web/src/api/types.ts
  • web/src/components/ui/inherit-toggle.stories.tsx
  • web/src/components/ui/inherit-toggle.tsx
  • web/src/components/ui/policy-source-badge.stories.tsx
  • web/src/components/ui/policy-source-badge.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/org-edit/DepartmentCeremonyOverride.stories.tsx
  • web/src/pages/org-edit/DepartmentCeremonyOverride.tsx
  • web/src/pages/org-edit/DepartmentEditDrawer.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
  • web/src/pages/settings/ceremony-policy/DepartmentOverridesPanel.tsx
  • web/src/pages/settings/ceremony-policy/PolicyFieldsPanel.tsx
  • web/src/pages/settings/ceremony-policy/StrategyChangeWarning.stories.tsx
  • web/src/pages/settings/ceremony-policy/StrategyChangeWarning.tsx
  • web/src/pages/settings/ceremony-policy/StrategyConfigPanel.tsx
  • web/src/pages/settings/ceremony-policy/StrategyPicker.stories.tsx
  • web/src/pages/settings/ceremony-policy/StrategyPicker.tsx
  • web/src/pages/settings/ceremony-policy/VelocityUnitIndicator.tsx
  • web/src/pages/settings/ceremony-policy/strategies/BudgetDrivenConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/CalendarConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/EventDrivenConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/ExternalTriggerConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/HybridConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/MilestoneDrivenConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/TaskDrivenConfig.tsx
  • web/src/pages/settings/ceremony-policy/strategies/ThroughputAdaptiveConfig.tsx
  • web/src/router/index.tsx
  • web/src/router/routes.ts
  • web/src/stores/ceremony-policy.ts
  • web/src/utils/constants.ts

Comment on lines +530 to +578
async def _get_dept_ceremony_override(
app_state: AppState,
department_name: NotBlankStr,
) -> dict[str, Any] | None:
"""Get the ceremony policy override for a department.

Checks the settings-based overrides first, then falls back to
the department's config ``ceremony_policy`` field.

Args:
app_state: Application state.
department_name: Department name.

Returns:
The override dict, or None if the department inherits.

Raises:
NotFoundError: If the department does not exist.
ServiceUnavailableError: If the settings service is not
available or the JSON blob is unreadable.
"""
# Check settings-based overrides first (raise on error to
# surface service failures instead of silently showing "inherit")
policies = await _load_dept_policies_json(
app_state,
raise_on_error=True,
)
if department_name in policies:
val = policies[department_name]
# None sentinel means "explicitly inheriting"
if val is None:
return None
if isinstance(val, dict):
# Validate structure before returning to catch corrupt data
try:
CeremonyPolicyConfig.model_validate(val)
except MemoryError, RecursionError:
raise
except Exception as exc:
logger.warning(
API_REQUEST_ERROR,
endpoint="departments.ceremony_policy.get",
department=department_name,
error=f"Invalid stored override: {exc}",
)
msg = f"Corrupt ceremony policy override for {department_name!r}"
raise ServiceUnavailableError(msg) from exc
return val
return None
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate settings-backed overrides before returning 200.

_get_dept_ceremony_override() trusts the settings blob before confirming the department still exists, and any non-dict / non-None entry falls through to None. A stale settings entry can therefore make a deleted department look valid, and corrupt stored data is misreported as “inherit”. Check existence first and raise ServiceUnavailableError for unexpected stored types.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/api/controllers/departments.py` around lines 530 - 578, The
function _get_dept_ceremony_override currently reads the settings blob via
_load_dept_policies_json and treats any non-dict/non-None value as inheritance
and returns None without first confirming the department exists; update it to
first verify the department exists (perform the department lookup using your
app_state's department retrieval method) before applying settings-backed
overrides, and if the stored override is neither dict nor None, log the
unexpected type and raise ServiceUnavailableError (instead of returning None);
keep the existing validation step using CeremonyPolicyConfig.model_validate and
preserve raising on validation errors.

Comment on lines +290 to +302
{!loading && storeError && (
<div className="rounded-md border border-danger/30 bg-danger/5 p-card text-sm text-danger">
Failed to load ceremony policy: {storeError}
</div>
)}

{!loading && activeStrategyError && (
<div className="rounded-md border border-warning/30 bg-warning/5 p-card text-sm text-warning">
Failed to load active strategy: {activeStrategyError}
</div>
)}

{!loading && !storeError && (
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t take the whole editor offline when resolved-policy fetch fails.

settingsSnapshot already drives the editable form; resolvedPolicy is only used for provenance badges. Gating the entire page on storeError means a transient /ceremony-policy/resolved failure prevents users from editing or repairing settings that are already loaded locally. Render the editor and degrade the badges/error state separately.

Also applies to: 323-324, 348-349

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx` around lines
290 - 302, The page currently hides the entire editor when storeError (or
resolved-policy fetch) fails, but the editor is driven by settingsSnapshot and
should remain editable; change the conditional rendering so the editor/form uses
settingsSnapshot presence (e.g., render when settingsSnapshot != null/undefined)
instead of gating on !storeError, and keep resolvedPolicy/storeError rendering
limited to the provenance badges/error boxes (i.e., show the resolvedPolicy
badge area or the specific error box when resolvedPolicy or storeError is
present). Apply the same fix for the other duplicated checks involving
storeError at the other locations (the activeStrategy/resolved-policy badge
blocks referenced by activeStrategyError and resolvedPolicy).

{ path: 'workflows/editor', element: <WorkflowEditorPage /> },
{ path: 'settings', element: <SettingsPage /> },
{ path: 'settings/observability/sinks', element: <SettingsSinksPage /> },
{ path: ROUTES.SETTINGS_CEREMONY_POLICY.slice(1), element: <CeremonyPolicyPage /> },
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Avoid brittle slice(1) route normalization.

At Line 121, ROUTES.SETTINGS_CEREMONY_POLICY.slice(1) depends on the constant always starting with /. A small normalization helper/constant is safer for future route changes.

♻️ Proposed refactor
+const SETTINGS_CEREMONY_POLICY_PATH = ROUTES.SETTINGS_CEREMONY_POLICY.startsWith('/')
+  ? ROUTES.SETTINGS_CEREMONY_POLICY.slice(1)
+  : ROUTES.SETTINGS_CEREMONY_POLICY
...
-              { path: ROUTES.SETTINGS_CEREMONY_POLICY.slice(1), element: <CeremonyPolicyPage /> },
+              { path: SETTINGS_CEREMONY_POLICY_PATH, element: <CeremonyPolicyPage /> },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/router/index.tsx` at line 121, The current use of
ROUTES.SETTINGS_CEREMONY_POLICY.slice(1) is brittle because it assumes the
constant has a leading "/"—create a small normalization helper (e.g.,
normalizeRoute or trimLeadingSlash) or define a slash-free ROUTE_PATHS mapping
and use that instead; update the router entry that references
ROUTES.SETTINGS_CEREMONY_POLICY to call the helper (or use the new mapping) so
the route string is consistently normalized without relying on slice(1).

Comment on lines +162 to +176
export const VELOCITY_CALC_LABELS: Readonly<Record<VelocityCalcType, string>> = {
task_driven: 'Per Task (pts/task)',
calendar: 'Per Day (pts/day)',
multi_dimensional: 'Multi-Dimensional (pts/sprint)',
budget: 'Per Currency Unit (pts/EUR)',
points_per_sprint: 'Points per Sprint',
}

export const VELOCITY_UNIT_LABELS: Readonly<Record<VelocityCalcType, string>> = {
task_driven: 'pts/task',
calendar: 'pts/day',
multi_dimensional: 'pts/sprint',
budget: 'pts/EUR',
points_per_sprint: 'pts/sprint',
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Make the budget velocity labels currency-aware.

budget is hardcoded to pts/EUR in both maps, so the velocity indicator is wrong for any project whose budget currency is not EUR. Either inject the active currency at render time or keep these strings currency-neutral.

🛠️ Minimal safe fallback
 export const VELOCITY_CALC_LABELS: Readonly<Record<VelocityCalcType, string>> = {
   task_driven: 'Per Task (pts/task)',
   calendar: 'Per Day (pts/day)',
   multi_dimensional: 'Multi-Dimensional (pts/sprint)',
-  budget: 'Per Currency Unit (pts/EUR)',
+  budget: 'Per Currency Unit (pts/currency unit)',
   points_per_sprint: 'Points per Sprint',
 }

 export const VELOCITY_UNIT_LABELS: Readonly<Record<VelocityCalcType, string>> = {
   task_driven: 'pts/task',
   calendar: 'pts/day',
   multi_dimensional: 'pts/sprint',
-  budget: 'pts/EUR',
+  budget: 'pts/currency',
   points_per_sprint: 'pts/sprint',
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const VELOCITY_CALC_LABELS: Readonly<Record<VelocityCalcType, string>> = {
task_driven: 'Per Task (pts/task)',
calendar: 'Per Day (pts/day)',
multi_dimensional: 'Multi-Dimensional (pts/sprint)',
budget: 'Per Currency Unit (pts/EUR)',
points_per_sprint: 'Points per Sprint',
}
export const VELOCITY_UNIT_LABELS: Readonly<Record<VelocityCalcType, string>> = {
task_driven: 'pts/task',
calendar: 'pts/day',
multi_dimensional: 'pts/sprint',
budget: 'pts/EUR',
points_per_sprint: 'pts/sprint',
}
export const VELOCITY_CALC_LABELS: Readonly<Record<VelocityCalcType, string>> = {
task_driven: 'Per Task (pts/task)',
calendar: 'Per Day (pts/day)',
multi_dimensional: 'Multi-Dimensional (pts/sprint)',
budget: 'Per Currency Unit (pts/currency unit)',
points_per_sprint: 'Points per Sprint',
}
export const VELOCITY_UNIT_LABELS: Readonly<Record<VelocityCalcType, string>> = {
task_driven: 'pts/task',
calendar: 'pts/day',
multi_dimensional: 'pts/sprint',
budget: 'pts/currency',
points_per_sprint: 'pts/sprint',
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/utils/constants.ts` around lines 162 - 176, VELOCITY_CALC_LABELS and
VELOCITY_UNIT_LABELS hardcode the budget labels to "EUR"; change the budget
entries to be currency-aware by either (A) replacing the static strings for the
'budget' key with placeholders (e.g. "Per Currency Unit (pts/{currency})" and
"pts/{currency}") and then substitute the active project currency at render time
wherever these maps are used, or (B) convert the maps into functions/getters
that accept the active currency and return the appropriate label for the
'budget' case; update usages that read VELOCITY_CALC_LABELS and
VELOCITY_UNIT_LABELS to pass or substitute the active currency so the displayed
unit matches the project currency.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (4)
web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx (3)

225-238: ⚠️ Potential issue | 🟠 Major

Persist this policy atomically.

This still breaks one ceremony-policy update into six independent updateSetting calls. If one request fails after others have succeeded, the settings land in a mixed state that the UI cannot roll back or reconstruct reliably.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx` around lines
225 - 238, The current handleSave issues six independent updateSetting calls
which can leave settings partially applied; replace them with a single atomic
update of the ceremony policy (e.g., call a new/exists batch API like
updateSettingsBatch or store the entire policy under one key) so the operation
is transactional from the UI—modify handleSave to build a single payload
(combining strategy, strategyConfig, velocityCalculator, autoTransition,
transitionThreshold, ceremonyOverrides) and call a single API method (e.g.,
updateSettingsBatch or updateSetting with key 'ceremony_policy'); ensure
handleSave checks the single response for success and sets saving/error state
accordingly.

50-62: ⚠️ Potential issue | 🟠 Major

Blank JSON settings still bypass validation.

Both JSON-backed settings use truthy guards, so '' is treated as “missing” instead of invalid. That skips the parse-error state and lets the next save overwrite a corrupt raw setting with {}.

♻️ Minimal fix
-    const sc = get('ceremony_strategy_config')
-    if (sc) {
-      try {
-        const parsed: unknown = JSON.parse(sc)
-        if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
-          config = parsed as Record<string, unknown>
-        } else {
-          configParseError = true
-        }
-      } catch {
-        configParseError = true
-      }
-    }
+    const sc = get('ceremony_strategy_config')
+    if (sc !== undefined) {
+      if (sc.trim() === '') {
+        configParseError = true
+      } else {
+        try {
+          const parsed: unknown = JSON.parse(sc)
+          if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
+            config = parsed as Record<string, unknown>
+          } else {
+            configParseError = true
+          }
+        } catch {
+          configParseError = true
+        }
+      }
+    }
@@
-    if (raw) {
-      try {
-        const parsed: unknown = JSON.parse(raw)
-        if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
-          return { overrides: parsed as Record<string, CeremonyPolicyConfig | null>, overridesParseError }
-        }
-        overridesParseError = true
-      } catch {
-        overridesParseError = true
-      }
-    }
+    if (raw !== undefined) {
+      if (raw.trim() === '') {
+        overridesParseError = true
+      } else {
+        try {
+          const parsed: unknown = JSON.parse(raw)
+          if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
+            return { overrides: parsed as Record<string, CeremonyPolicyConfig | null>, overridesParseError }
+          }
+          overridesParseError = true
+        } catch {
+          overridesParseError = true
+        }
+      }
+    }

Based on learnings, validate at system boundaries (user input, external APIs, config files).

Also applies to: 160-171

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx` around lines
50 - 62, The code treats empty string as "missing" because it checks sc
truthiness; change the guard around get('ceremony_strategy_config') so it
detects empty-string as invalid (e.g., if (sc !== undefined && sc !== null && sc
!== '') ) so the JSON parse branch runs or sets configParseError for blank
input; keep the existing JSON.parse+type check logic and ensure configParseError
is set when sc === '' or parsing fails; apply the same change to the other
JSON-backed setting handling (the other block using get(...) and sc-like
variables).

86-90: ⚠️ Potential issue | 🟠 Major

Parse ceremony_auto_transition as a strict boolean.

raw.toLowerCase() === 'true' coerces every other non-empty string to false. Invalid YAML/raw settings therefore render as a legitimate toggle state and get written back on the next save. Accept only 'true' / 'false' and surface anything else as invalid.

Based on learnings, validate at system boundaries (user input, external APIs, config files).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx` around lines
86 - 90, The autoTransition initializer currently coerces any non-empty string
via raw.toLowerCase() === 'true'; change it to accept only the literal strings
'true' or 'false' (case-insensitive if you prefer) and treat any other value as
invalid: if raw is undefined keep the default (true), else if raw === 'true'
return true, else if raw === 'false' return false, otherwise return undefined
(or null) and surface a validation error in the UI/form state so invalid
external/config values for the key 'ceremony_auto_transition' are not silently
written back; update the logic inside the autoTransition IIFE that calls
get(...) to implement these strict checks and to record a validation message.
web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx (1)

17-25: ⚠️ Potential issue | 🟠 Major

Don't drive sparse ceremony overrides from a fake 'task_driven' strategy.

When an override omits strategy, this row still renders the task-driven picker/config panel and task-driven defaults. For ceremonies inheriting a different strategy, users are editing the wrong config shape and can save a mismatched strategy_config. Pass the resolved/inherited ceremony policy into each row and use that as the display baseline; keep policy as the sparse patch only.

Also applies to: 40-51, 94-106

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx` around lines 17
- 25, Rows are being rendered using a fake 'task_driven' strategy when an
override omits strategy; compute and pass the resolved/inherited full
CeremonyPolicyConfig as the display baseline to each row instead of defaulting
to 'task_driven', and continue to pass the sparse override as the editable
"policy" patch. Concretely: in the component that maps ceremonyNames to rows
(where you currently read overrides[name] and render the picker/panel), derive
resolvedPolicy by merging the inherited/default policy for that ceremony with
the sparse overrides[name] (so resolvedPolicy.strategy and
resolvedPolicy.strategy_config reflect the inherited values), then pass
resolvedPolicy as the baseline/display prop (e.g., resolvedPolicy or
displayPolicy) and keep policy={overrides[name]} as the sparse patch; ensure any
code handling strategy or strategy_config in the row components uses the
resolved/display prop for initial UI and the sparse policy for saves/patching.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/pages/org-edit/DepartmentCeremonyOverride.tsx`:
- Around line 61-71: handleStrategyChange currently spreads the existing
CeremonyPolicyConfig (including strategy_config) into the new policy so
switching strategies can carry stale strategy-specific config; update
handleStrategyChange (and the analogous logic around lines 96-134) to explicitly
clear or reset strategy_config when the strategy actually changes (e.g., set
strategy_config to undefined or to the new strategy's default config) before
calling onChange, and ensure velocity_calculator is still set from
STRATEGY_DEFAULT_VELOCITY_CALC for the new strategy.
- Around line 36-37: hasOverride currently treats only non-empty objects as an
override so toggling on creates {} but the UI immediately treats it as "inherit"
and collapses; change the component to keep the UI expansion state separate from
the persisted-policy shape: initialize expanded from a boolean like (policy !=
null) but do not recompute expanded from hasOverride; in the toggle/handler that
enables an override ensure you call setExpanded(true) when switching from
inherit->override (even if the policy becomes {}), and stop deriving expanded
from Object.keys(policy).length elsewhere (see uses near hasOverride, expanded,
setExpanded and the toggle handlers referenced around lines 49-56 and 90-97) so
empty objects can be edited in the drawer.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx`:
- Around line 215-216: The effect currently imports and calls listDepartments()
and only uses the first page's result to call setDepartments, causing missing
departments when results are paginated; update the logic around listDepartments
(and the effect that sets departments for DepartmentOverridesPanel) to fetch all
pages before calling setDepartments—either by using an API option to request a
large pageSize or by iterating through paginated responses (calling
listDepartments with page tokens/indices until all pages are retrieved) and
concatenating each result.data into one array, then setDepartments with the full
list so DepartmentOverridesPanel can render and edit every department.
- Around line 196-203: The current useMemo for ceremonyNames only combines
ceremonyOverrides keys with a hardcoded four-name fallback, which misses
ceremonies defined only in the sprint configuration; update the ceremonyNames
computation (the useMemo that references ceremonyOverrides and returns
ceremonyNames) to also collect ceremony identifiers from the sprint
configuration (e.g., sprintConfig or the prop that holds sprint data — e.g.,
sprintConfig.ceremonies or sprint?.common_ceremonies) in addition to
ceremonyOverrides, then merge, dedupe, sort and return that list so any ceremony
present only in the sprint config is included in the UI.

---

Duplicate comments:
In `@web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx`:
- Around line 17-25: Rows are being rendered using a fake 'task_driven' strategy
when an override omits strategy; compute and pass the resolved/inherited full
CeremonyPolicyConfig as the display baseline to each row instead of defaulting
to 'task_driven', and continue to pass the sparse override as the editable
"policy" patch. Concretely: in the component that maps ceremonyNames to rows
(where you currently read overrides[name] and render the picker/panel), derive
resolvedPolicy by merging the inherited/default policy for that ceremony with
the sparse overrides[name] (so resolvedPolicy.strategy and
resolvedPolicy.strategy_config reflect the inherited values), then pass
resolvedPolicy as the baseline/display prop (e.g., resolvedPolicy or
displayPolicy) and keep policy={overrides[name]} as the sparse patch; ensure any
code handling strategy or strategy_config in the row components uses the
resolved/display prop for initial UI and the sparse policy for saves/patching.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx`:
- Around line 225-238: The current handleSave issues six independent
updateSetting calls which can leave settings partially applied; replace them
with a single atomic update of the ceremony policy (e.g., call a new/exists
batch API like updateSettingsBatch or store the entire policy under one key) so
the operation is transactional from the UI—modify handleSave to build a single
payload (combining strategy, strategyConfig, velocityCalculator, autoTransition,
transitionThreshold, ceremonyOverrides) and call a single API method (e.g.,
updateSettingsBatch or updateSetting with key 'ceremony_policy'); ensure
handleSave checks the single response for success and sets saving/error state
accordingly.
- Around line 50-62: The code treats empty string as "missing" because it checks
sc truthiness; change the guard around get('ceremony_strategy_config') so it
detects empty-string as invalid (e.g., if (sc !== undefined && sc !== null && sc
!== '') ) so the JSON parse branch runs or sets configParseError for blank
input; keep the existing JSON.parse+type check logic and ensure configParseError
is set when sc === '' or parsing fails; apply the same change to the other
JSON-backed setting handling (the other block using get(...) and sc-like
variables).
- Around line 86-90: The autoTransition initializer currently coerces any
non-empty string via raw.toLowerCase() === 'true'; change it to accept only the
literal strings 'true' or 'false' (case-insensitive if you prefer) and treat any
other value as invalid: if raw is undefined keep the default (true), else if raw
=== 'true' return true, else if raw === 'false' return false, otherwise return
undefined (or null) and surface a validation error in the UI/form state so
invalid external/config values for the key 'ceremony_auto_transition' are not
silently written back; update the logic inside the autoTransition IIFE that
calls get(...) to implement these strict checks and to record a validation
message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ce2af522-b1e3-4059-89a9-8ee9d6e35c2c

📥 Commits

Reviewing files that changed from the base of the PR and between fca8fdc and edc9079.

📒 Files selected for processing (3)
  • web/src/pages/org-edit/DepartmentCeremonyOverride.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Build Sandbox
  • GitHub Check: Build Backend
  • GitHub Check: Build Web
  • GitHub Check: Test (Python 3.14)
  • GitHub Check: Dashboard Test
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (3)
web/src/**/*.{tsx,ts}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{tsx,ts}: ALWAYS reuse existing components from web/src/components/ui/ before creating new ones (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, TagInput, MetadataGrid, ProjectStatusBadge, ContentTypeBadge)
Use Tailwind semantic classes (text-foreground, bg-card, text-accent, text-success, bg-danger, etc.) or CSS variables (var(--so-accent)) for colors. NEVER hardcode hex values in .tsx/.ts files.
Use font-sans or font-mono for typography (maps to Geist tokens). NEVER set fontFamily directly.
Use density-aware tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing. NEVER hardcode pixel values for layout spacing.
Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows and borders. NEVER hardcode values.
Import cn from @/lib/utils for conditional class merging in new components.
Do NOT recreate status dots inline -- use <StatusBadge> component.
Do NOT build card-with-header layouts from scratch -- use <SectionCard> component.
Do NOT create metric displays with text-metric font-bold -- use <MetricCard> component.
Do NOT render initials circles manually -- use <Avatar> component.
Do NOT create complex (>8 line) JSX inside .map() -- extract to a shared component.
TypeScript strict mode must pass type checking (run npm --prefix web run type-check); all type errors must be resolved before proceeding.
Do NOT hardcode Framer Motion transition durations -- use @/lib/motion presets.

Files:

  • web/src/pages/org-edit/DepartmentCeremonyOverride.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
web/src/**/*.{tsx,ts,css}

📄 CodeRabbit inference engine (web/CLAUDE.md)

Do NOT use rgba() with hardcoded values -- use design token variables.

Files:

  • web/src/pages/org-edit/DepartmentCeremonyOverride.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
web/src/**/*.{tsx,ts,js,jsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

ESLint must enforce zero warnings (run npm --prefix web run lint); all warnings must be fixed before proceeding.

Files:

  • web/src/pages/org-edit/DepartmentCeremonyOverride.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
🧠 Learnings (10)
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/components/ui/*.{tsx,ts} : For new shared React components: place in web/src/components/ui/ with kebab-case filename, create .stories.tsx with all states, export props as TypeScript interface, use design tokens exclusively

Applied to files:

  • web/src/pages/org-edit/DepartmentCeremonyOverride.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from web/src/components/ui/ (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, form fields, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor) before creating new components

Applied to files:

  • web/src/pages/org-edit/DepartmentCeremonyOverride.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx
📚 Learning: 2026-04-03T10:54:21.472Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T10:54:21.472Z
Learning: Applies to web/src/**/*.{tsx,ts} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, TagInput, MetadataGrid, ProjectStatusBadge, ContentTypeBadge)

Applied to files:

  • web/src/pages/org-edit/DepartmentCeremonyOverride.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyListPanel.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-04-01T14:22:06.315Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T14:22:06.315Z
Learning: Applies to {**/*.py,web/src/**/*.{ts,tsx}} : Validate at system boundaries (user input, external APIs, config files)

Applied to files:

  • web/src/pages/org-edit/DepartmentCeremonyOverride.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : When creating new shared web components, place in web/src/components/ui/ with kebab-case filename, create .stories.tsx alongside with all states (default, hover, loading, error, empty), export props as TypeScript interface, use design tokens exclusively with no hardcoded colors/fonts/spacing, and import cn from `@/lib/utils` for conditional class merging

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-04-03T10:54:21.472Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T10:54:21.472Z
Learning: Applies to web/src/components/ui/**/*.{tsx,ts} : When creating new shared components, place them in `web/src/components/ui/` with descriptive kebab-case filename, create accompanying `.stories.tsx` with all states (default, hover, loading, error, empty), export props as TypeScript interface, and use design tokens exclusively with no hardcoded colors, fonts, or spacing.

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-03-31T14:31:11.894Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:31:11.894Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use React 19, TypeScript 6.0+, and design system tokens from shadcn/ui + Tailwind CSS 4 + Radix UI in web dashboard

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-04-03T10:54:21.472Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T10:54:21.472Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do NOT create complex (>8 line) JSX inside `.map()` -- extract to a shared component.

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-03-15T21:49:53.264Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T21:49:53.264Z
Learning: Fix everything valid — never skip when review agents find valid issues (including pre-existing issues in surrounding code, suggestions, and findings adjacent to the PR's changes). No deferring, no 'out of scope' skipping.

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-04-01T05:46:17.064Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T05:46:17.064Z
Learning: Fix all valid issues found by review agents (including pre-existing issues in surrounding code, suggestions, and adjacent findings) — never skip or defer

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
🔇 Additional comments (1)
web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx (1)

122-139: Dirty-state re-sync guard looks good.

Skipping the snapshot-to-form sync while isDirty is true avoids clobbering in-progress edits during store refreshes.

Comment on lines +196 to +203
// Derive ceremony names from overrides + common ceremony names
const ceremonyNames = useMemo(() => {
const names = new Set(Object.keys(ceremonyOverrides))
for (const name of ['sprint_planning', 'standup', 'sprint_review', 'retrospective']) {
names.add(name)
}
return [...names].sort()
}, [ceremonyOverrides])
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Stop deriving ceremony names from a hardcoded fallback list.

This memo only emits the four built-ins plus keys that already exist in ceremonyOverrides. Any ceremony present only in sprint config never shows up here, so the UI cannot create an override for it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx` around lines
196 - 203, The current useMemo for ceremonyNames only combines ceremonyOverrides
keys with a hardcoded four-name fallback, which misses ceremonies defined only
in the sprint configuration; update the ceremonyNames computation (the useMemo
that references ceremonyOverrides and returns ceremonyNames) to also collect
ceremony identifiers from the sprint configuration (e.g., sprintConfig or the
prop that holds sprint data — e.g., sprintConfig.ceremonies or
sprint?.common_ceremonies) in addition to ceremonyOverrides, then merge, dedupe,
sort and return that list so any ceremony present only in the sprint config is
included in the UI.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx (2)

237-259: ⚠️ Potential issue | 🟠 Major

Non-atomic save persists as a known limitation.

The handleSave function issues six independent updateSetting calls via Promise.all. As noted in the code comment and past review, a partial failure (e.g., 3 of 6 succeed before one throws) leaves the ceremony policy in an inconsistent state that the UI cannot roll back. While the backend doesn't support batch updates, consider:

🛡️ Mitigation options
  1. Sequential saves with rollback tracking: Save sequentially, track successful keys, and attempt to restore original values on failure.

  2. Backend batch endpoint: Add a dedicated PUT /ceremony-policy endpoint that accepts all fields atomically.

  3. Optimistic UI with conflict detection: After a partial failure, re-fetch settings and show a conflict resolution UI.

Minimal sequential approach:

const handleSave = useCallback(async () => {
  setSaving(true)
  const updates: Array<[string, string]> = [
    ['ceremony_strategy', strategy],
    ['ceremony_strategy_config', JSON.stringify(strategyConfig)],
    // ... etc
  ]
  const saved: Array<[string, string | undefined]> = []
  try {
    for (const [key, value] of updates) {
      const prev = settingsEntries.find(
        e => e.definition.namespace === 'coordination' && e.definition.key === key
      )?.value
      saved.push([key, prev])
      await updateSetting('coordination', key, value)
    }
    // ... success handling
  } catch (err) {
    // Attempt rollback
    for (const [key, prev] of saved) {
      if (prev !== undefined) {
        await updateSetting('coordination', key, prev).catch(() => {})
      }
    }
    // ... error handling
  }
}, [/* deps */])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx` around lines
237 - 259, handleSave currently issues multiple parallel updateSetting calls
which can partially succeed and leave state inconsistent; change it to perform
sequential updates (use an ordered list of keys/values) while recording previous
values from settingsEntries before each update, and on any failure attempt to
roll back only the successfully-updated keys by re-calling updateSetting with
their saved previous values (swallow rollback errors), then surface the original
error via addToast and ensure setSaving(false) and setIsDirty are handled
appropriately; reference the handleSave function, updateSetting, settingsEntries
(to read prev values), addToast, fetchResolvedPolicy, setIsDirty, and setSaving
when implementing this change.

196-203: 🧹 Nitpick | 🔵 Trivial

Ceremony names derivation uses hardcoded fallback list.

This memo combines override keys with four hardcoded ceremony names. As noted in past review, ceremonies defined only in sprint configuration (not in overrides) won't appear here, preventing users from creating overrides for them. This limits the UI's completeness.

♻️ Consider fetching available ceremonies from sprint config

If the sprint configuration contains a list of ceremony identifiers, fetch and merge those:

+  // Fetch ceremony names from sprint config
+  const sprintCeremonies = useMemo(() => {
+    // Extract from resolvedPolicy or a separate endpoint
+    return resolvedPolicy?.available_ceremonies ?? []
+  }, [resolvedPolicy])
+
   const ceremonyNames = useMemo(() => {
     const names = new Set(Object.keys(ceremonyOverrides))
     for (const name of ['sprint_planning', 'standup', 'sprint_review', 'retrospective']) {
       names.add(name)
     }
+    for (const name of sprintCeremonies) {
+      names.add(name)
+    }
     return [...names].sort()
-  }, [ceremonyOverrides])
+  }, [ceremonyOverrides, sprintCeremonies])

This requires the backend to expose available ceremony names, either in the resolved policy response or via a separate endpoint.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx` around lines
196 - 203, The current useMemo that builds ceremonyNames (const ceremonyNames =
useMemo(...)) relies on a hardcoded list; replace that fallback by merging
override keys with the actual ceremony identifiers from the sprint
configuration/resolved policy (e.g., a prop or field like
resolvedPolicy.availableCeremonies or sprintConfig.ceremonies) instead of
['sprint_planning', 'standup', 'sprint_review', 'retrospective']; update the
hook to: collect Object.keys(ceremonyOverrides), union with the sprint-provided
ceremony list when available, and sort the result, and keep the hardcoded list
only as a last-resort fallback if no sprint data is present.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx`:
- Around line 214-232: Refactor the dynamic import handler in the useEffect (the
block using listDepartments, allDepts, setDepartments, setDeptLoadError,
addToast, setDeptLoading) so the async pagination loop runs inside a
try/catch/finally within an inner async function (instead of relying on the
outer promise.catch), and on error persist any partial results by calling
setDepartments(allDepts) before setting setDeptLoadError(true) and
addToast(...); keep the finally branch to call setDeptLoading(false). This
ensures partial pages collected in allDepts are saved on mid-pagination failures
while preserving the existing error/toast/loading behavior.

---

Duplicate comments:
In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx`:
- Around line 237-259: handleSave currently issues multiple parallel
updateSetting calls which can partially succeed and leave state inconsistent;
change it to perform sequential updates (use an ordered list of keys/values)
while recording previous values from settingsEntries before each update, and on
any failure attempt to roll back only the successfully-updated keys by
re-calling updateSetting with their saved previous values (swallow rollback
errors), then surface the original error via addToast and ensure
setSaving(false) and setIsDirty are handled appropriately; reference the
handleSave function, updateSetting, settingsEntries (to read prev values),
addToast, fetchResolvedPolicy, setIsDirty, and setSaving when implementing this
change.
- Around line 196-203: The current useMemo that builds ceremonyNames (const
ceremonyNames = useMemo(...)) relies on a hardcoded list; replace that fallback
by merging override keys with the actual ceremony identifiers from the sprint
configuration/resolved policy (e.g., a prop or field like
resolvedPolicy.availableCeremonies or sprintConfig.ceremonies) instead of
['sprint_planning', 'standup', 'sprint_review', 'retrospective']; update the
hook to: collect Object.keys(ceremonyOverrides), union with the sprint-provided
ceremony list when available, and sort the result, and keep the hardcoded list
only as a last-resort fallback if no sprint data is present.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: dbb7cba4-6c82-4ce8-8b24-0e70b936430b

📥 Commits

Reviewing files that changed from the base of the PR and between edc9079 and 3ca6eb8.

📒 Files selected for processing (2)
  • web/src/pages/org-edit/DepartmentCeremonyOverride.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Dashboard Test
  • GitHub Check: Test (Python 3.14)
  • GitHub Check: Build Backend
  • GitHub Check: Build Sandbox
  • GitHub Check: Build Web
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (3)
web/src/**/*.{tsx,ts}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{tsx,ts}: ALWAYS reuse existing components from web/src/components/ui/ before creating new ones (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, TagInput, MetadataGrid, ProjectStatusBadge, ContentTypeBadge)
Use Tailwind semantic classes (text-foreground, bg-card, text-accent, text-success, bg-danger, etc.) or CSS variables (var(--so-accent)) for colors. NEVER hardcode hex values in .tsx/.ts files.
Use font-sans or font-mono for typography (maps to Geist tokens). NEVER set fontFamily directly.
Use density-aware tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing. NEVER hardcode pixel values for layout spacing.
Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows and borders. NEVER hardcode values.
Import cn from @/lib/utils for conditional class merging in new components.
Do NOT recreate status dots inline -- use <StatusBadge> component.
Do NOT build card-with-header layouts from scratch -- use <SectionCard> component.
Do NOT create metric displays with text-metric font-bold -- use <MetricCard> component.
Do NOT render initials circles manually -- use <Avatar> component.
Do NOT create complex (>8 line) JSX inside .map() -- extract to a shared component.
TypeScript strict mode must pass type checking (run npm --prefix web run type-check); all type errors must be resolved before proceeding.
Do NOT hardcode Framer Motion transition durations -- use @/lib/motion presets.

Files:

  • web/src/pages/org-edit/DepartmentCeremonyOverride.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
web/src/**/*.{tsx,ts,css}

📄 CodeRabbit inference engine (web/CLAUDE.md)

Do NOT use rgba() with hardcoded values -- use design token variables.

Files:

  • web/src/pages/org-edit/DepartmentCeremonyOverride.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
web/src/**/*.{tsx,ts,js,jsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

ESLint must enforce zero warnings (run npm --prefix web run lint); all warnings must be fixed before proceeding.

Files:

  • web/src/pages/org-edit/DepartmentCeremonyOverride.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
🧠 Learnings (12)
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/components/ui/*.{tsx,ts} : For new shared React components: place in web/src/components/ui/ with kebab-case filename, create .stories.tsx with all states, export props as TypeScript interface, use design tokens exclusively

Applied to files:

  • web/src/pages/org-edit/DepartmentCeremonyOverride.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from web/src/components/ui/ (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, form fields, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor) before creating new components

Applied to files:

  • web/src/pages/org-edit/DepartmentCeremonyOverride.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-04-03T10:54:21.472Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T10:54:21.472Z
Learning: Applies to web/src/**/*.{tsx,ts} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, TagInput, MetadataGrid, ProjectStatusBadge, ContentTypeBadge)

Applied to files:

  • web/src/pages/org-edit/DepartmentCeremonyOverride.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-04-01T14:22:06.315Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T14:22:06.315Z
Learning: Applies to {**/*.py,web/src/**/*.{ts,tsx}} : Validate at system boundaries (user input, external APIs, config files)

Applied to files:

  • web/src/pages/org-edit/DepartmentCeremonyOverride.tsx
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : When creating new shared web components, place in web/src/components/ui/ with kebab-case filename, create .stories.tsx alongside with all states (default, hover, loading, error, empty), export props as TypeScript interface, use design tokens exclusively with no hardcoded colors/fonts/spacing, and import cn from `@/lib/utils` for conditional class merging

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-04-03T10:54:21.472Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T10:54:21.472Z
Learning: Applies to web/src/components/ui/**/*.{tsx,ts} : When creating new shared components, place them in `web/src/components/ui/` with descriptive kebab-case filename, create accompanying `.stories.tsx` with all states (default, hover, loading, error, empty), export props as TypeScript interface, and use design tokens exclusively with no hardcoded colors, fonts, or spacing.

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-03-27T12:44:29.466Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:44:29.466Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem) before creating new ones

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use `@/components/ui/animated-presence` (Framer Motion AnimatePresence keyed by route) for page transitions

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-03-31T14:31:11.894Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:31:11.894Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use React 19, TypeScript 6.0+, and design system tokens from shadcn/ui + Tailwind CSS 4 + Radix UI in web dashboard

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-04-03T10:54:21.472Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T10:54:21.472Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do NOT create complex (>8 line) JSX inside `.map()` -- extract to a shared component.

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-03-15T21:49:53.264Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T21:49:53.264Z
Learning: Fix everything valid — never skip when review agents find valid issues (including pre-existing issues in surrounding code, suggestions, and findings adjacent to the PR's changes). No deferring, no 'out of scope' skipping.

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-04-01T05:46:17.064Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T05:46:17.064Z
Learning: Fix all valid issues found by review agents (including pre-existing issues in surrounding code, suggestions, and adjacent findings) — never skip or defer

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
🔇 Additional comments (8)
web/src/pages/org-edit/DepartmentCeremonyOverride.tsx (3)

1-29: LGTM! Imports and type definitions are well-structured.

The component correctly reuses existing UI components (InheritToggle, SelectField, ToggleField, InputField) from @/components/ui/ as required. The props interface is clean and properly typed.


41-73: Handlers correctly implement the override logic.

handleInheritChange preserves existing policy fields when switching to override mode, and handleStrategyChange properly clears strategy_config (since different strategies have different schemas) while resetting velocity_calculator to the strategy default.


75-139: UI rendering follows design guidelines and accessibility standards.

The component uses semantic design tokens (text-text-muted, border-border, border-accent/20) and includes proper accessibility attributes (aria-expanded, aria-label). The threshold input correctly validates with Number.isFinite and clamps to the valid range.

web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx (5)

1-23: Imports are well-organized and reuse existing UI components.

The page correctly imports and uses existing components from @/components/ui/ (Button, ErrorBoundary, SectionCard, SkeletonCard, PolicySourceBadge) as required by coding guidelines.


43-94: Settings snapshot implements proper validation and defensive parsing.

The memo correctly validates:

  • ceremony_strategy against the allowed types with fallback to 'task_driven'
  • ceremony_strategy_config JSON with object-root validation and error tracking
  • ceremony_velocity_calculator against allowed types with strategy-default fallback
  • ceremony_transition_threshold with Number.isFinite check and clamping to [0.01, 1.0]

96-148: Form state management with dirty tracking is well-implemented.

The isDirty flag correctly prevents external settings updates from clobbering in-progress user edits. The consolidated FormState object enables atomic re-sync without multiple setState calls. This addresses the prior concern about form re-sync overwriting user changes.


154-181: Ceremony overrides parsing with validation is correct.

The memo validates that parsed JSON is a non-null, non-array object before using it, and tracks parse errors separately. The re-sync effect respects the isDirty flag.


284-412: UI rendering correctly handles loading, errors, and disabled states.

The page:

  • Shows loading spinner while fetching resolved policy
  • Displays error banners for storeError and activeStrategyError without hiding the editor (addressing past review)
  • Disables all interactive controls (disabled={saving}) during save operations
  • Uses proper design tokens for spacing (space-y-section-gap, p-card) and colors (text-danger, border-danger/30)
  • Shows skeleton while departments load

The strategy-change warning banner correctly appears when the selected strategy differs from the active sprint strategy.

Refactor department fetch to try/catch/finally inside an async
function.  On error, partial results already collected in allDepts
are persisted via setDepartments before setting the error state,
so departments fetched before the failure remain visible.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx (3)

50-62: ⚠️ Potential issue | 🟠 Major

Treat blank JSON settings as corrupt, not as “missing”.

Both parsers use truthy guards, so '' skips JSON.parse entirely and leaves configParseError / overridesParseError false. That bypasses the “stored JSON is corrupt” save guard and lets the next save silently replace a bad raw value with {} or an empty overrides map.

♻️ Suggested fix
-    const sc = get('ceremony_strategy_config')
-    if (sc) {
+    const sc = get('ceremony_strategy_config')
+    if (sc !== undefined) {
+      if (sc.trim() === '') {
+        configParseError = true
+      } else {
         try {
           const parsed: unknown = JSON.parse(sc)
           if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
             config = parsed as Record<string, unknown>
           } else {
             configParseError = true
           }
         } catch {
           configParseError = true
         }
+      }
     }
@@
-    if (raw) {
+    if (raw !== undefined) {
+      if (raw.trim() === '') {
+        overridesParseError = true
+      } else {
         try {
           const parsed: unknown = JSON.parse(raw)
           if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
             return { overrides: parsed as Record<string, CeremonyPolicyConfig | null>, overridesParseError }
           }
           overridesParseError = true
         } catch {
           overridesParseError = true
         }
+      }
     }

Based on learnings, validate at system boundaries (user input, external APIs, config files).

Also applies to: 160-171

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx` around lines
50 - 62, The parsing guard for ceremony_strategy_config currently skips
JSON.parse when the stored string is falsy (e.g., empty string), which treats
blank JSON as "missing" instead of corrupt; update the logic around
get('ceremony_strategy_config') so that an empty string is treated as a parse
error: explicitly check for sc === '' and set configParseError = true before
attempting JSON.parse, and keep the existing try/catch that sets
configParseError on parse failures; apply the same change to the corresponding
overrides parsing block that sets overridesParseError (the one around lines
handling overridesParseError) so blank values are flagged as corrupt as well.

247-257: ⚠️ Potential issue | 🟠 Major

Persist the project policy atomically.

This still fans one logical ceremony-policy change into six unrelated updateSetting calls. If one request fails after others succeed, the backend is left with a mixed policy state that the UI cannot roll back or accurately represent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx` around lines
247 - 257, handleSave currently issues six separate updateSetting calls which
can leave the backend in a partially-updated state if one fails; instead build a
single atomic payload (e.g., compose {strategy, strategyConfig,
velocityCalculator, autoTransition, transitionThreshold, ceremonyOverrides} or a
single "ceremony_policy" object) and call updateSetting only once from
handleSave so the entire ceremony policy is persisted in one request; adjust the
UI call site in handleSave to JSON.stringify that combined policy and replace
the multiple updateSetting calls with a single
updateSetting('coordination','ceremony_policy', ...) invocation (or call a new
backend endpoint that accepts a full policy object) and keep the existing
setSaving/try/catch flow.

79-90: ⚠️ Potential issue | 🟠 Major

Don’t silently coerce malformed scalar settings.

ceremony_auto_transition: "garbage" becomes false, and an invalid ceremony_velocity_calculator is quietly replaced with the strategy default. That means simply opening this page and clicking Save can rewrite malformed YAML/code settings instead of surfacing them as invalid.

Based on learnings, validate at system boundaries (user input, external APIs, config files).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx` around lines
79 - 90, The current initializers silently coerce malformed settings; change the
velocityCalculator and autoTransition blocks to return an explicit invalid/empty
value instead of coercing: for velocityCalculator (the IIFE that reads
get('ceremony_velocity_calculator')), if raw is undefined return undefined, if
raw is in VELOCITY_CALC_TYPES return raw as VelocityCalcType, otherwise return
undefined (do not fall back to STRATEGY_DEFAULT_VELOCITY_CALC[strategy]); for
autoTransition (the IIFE reading get('ceremony_auto_transition')), if raw is
undefined return true, if raw.toLowerCase() === 'true' return true, if
raw.toLowerCase() === 'false' return false, otherwise return undefined; ensure
callers of velocityCalculator and autoTransition treat undefined as
“invalid/missing” so the UI can surface validation errors rather than silently
rewriting config.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx`:
- Around line 38-40: The inline save-error banner uses storeSaveError from
useCeremonyPolicyStore but handleSave() writes via
useSettingsStore.updateSetting, so failures only appear as toasts and the banner
shows stale state; modify the save flow so errors from handleSave() propagate
into the ceremony policy store (e.g., call an updater like
useCeremonyPolicyStore().setSaveError or invoke the store's save method instead
of useSettingsStore.updateSetting) or update storeSaveError when catching errors
in handleSave(), and ensure fetchResolvedPolicy/fetchActiveStrategy usages
remain consistent with the store change so the inline banner reflects the
current save failure.
- Around line 228-236: The fetch logic writes partial results into state via
setDepartments(allDepts) in the finally block but the UI never shows
DepartmentOverridesPanel because rendering is gated by !deptLoadError; change
the render condition in CeremonyPolicyPage so DepartmentOverridesPanel is shown
when departments contains any items (e.g., departments.length > 0) or when
deptLoading is false with partial data, instead of solely relying on
!deptLoadError; update any other identical guard (the similar check around lines
405-409) to the same logic so preserved partial departments are actually
displayed even if pagination failed.

---

Duplicate comments:
In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx`:
- Around line 50-62: The parsing guard for ceremony_strategy_config currently
skips JSON.parse when the stored string is falsy (e.g., empty string), which
treats blank JSON as "missing" instead of corrupt; update the logic around
get('ceremony_strategy_config') so that an empty string is treated as a parse
error: explicitly check for sc === '' and set configParseError = true before
attempting JSON.parse, and keep the existing try/catch that sets
configParseError on parse failures; apply the same change to the corresponding
overrides parsing block that sets overridesParseError (the one around lines
handling overridesParseError) so blank values are flagged as corrupt as well.
- Around line 247-257: handleSave currently issues six separate updateSetting
calls which can leave the backend in a partially-updated state if one fails;
instead build a single atomic payload (e.g., compose {strategy, strategyConfig,
velocityCalculator, autoTransition, transitionThreshold, ceremonyOverrides} or a
single "ceremony_policy" object) and call updateSetting only once from
handleSave so the entire ceremony policy is persisted in one request; adjust the
UI call site in handleSave to JSON.stringify that combined policy and replace
the multiple updateSetting calls with a single
updateSetting('coordination','ceremony_policy', ...) invocation (or call a new
backend endpoint that accepts a full policy object) and keep the existing
setSaving/try/catch flow.
- Around line 79-90: The current initializers silently coerce malformed
settings; change the velocityCalculator and autoTransition blocks to return an
explicit invalid/empty value instead of coercing: for velocityCalculator (the
IIFE that reads get('ceremony_velocity_calculator')), if raw is undefined return
undefined, if raw is in VELOCITY_CALC_TYPES return raw as VelocityCalcType,
otherwise return undefined (do not fall back to
STRATEGY_DEFAULT_VELOCITY_CALC[strategy]); for autoTransition (the IIFE reading
get('ceremony_auto_transition')), if raw is undefined return true, if
raw.toLowerCase() === 'true' return true, if raw.toLowerCase() === 'false'
return false, otherwise return undefined; ensure callers of velocityCalculator
and autoTransition treat undefined as “invalid/missing” so the UI can surface
validation errors rather than silently rewriting config.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4864e6c8-6f4f-4550-9d47-a8a1bb23bf36

📥 Commits

Reviewing files that changed from the base of the PR and between 3ca6eb8 and 71a1d81.

📒 Files selected for processing (1)
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Build Backend
  • GitHub Check: Build Web
  • GitHub Check: Test (Python 3.14)
  • GitHub Check: Dashboard Test
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (3)
web/src/**/*.{tsx,ts}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{tsx,ts}: ALWAYS reuse existing components from web/src/components/ui/ before creating new ones (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, TagInput, MetadataGrid, ProjectStatusBadge, ContentTypeBadge)
Use Tailwind semantic classes (text-foreground, bg-card, text-accent, text-success, bg-danger, etc.) or CSS variables (var(--so-accent)) for colors. NEVER hardcode hex values in .tsx/.ts files.
Use font-sans or font-mono for typography (maps to Geist tokens). NEVER set fontFamily directly.
Use density-aware tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing. NEVER hardcode pixel values for layout spacing.
Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows and borders. NEVER hardcode values.
Import cn from @/lib/utils for conditional class merging in new components.
Do NOT recreate status dots inline -- use <StatusBadge> component.
Do NOT build card-with-header layouts from scratch -- use <SectionCard> component.
Do NOT create metric displays with text-metric font-bold -- use <MetricCard> component.
Do NOT render initials circles manually -- use <Avatar> component.
Do NOT create complex (>8 line) JSX inside .map() -- extract to a shared component.
TypeScript strict mode must pass type checking (run npm --prefix web run type-check); all type errors must be resolved before proceeding.
Do NOT hardcode Framer Motion transition durations -- use @/lib/motion presets.

Files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
web/src/**/*.{tsx,ts,css}

📄 CodeRabbit inference engine (web/CLAUDE.md)

Do NOT use rgba() with hardcoded values -- use design token variables.

Files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
web/src/**/*.{tsx,ts,js,jsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

ESLint must enforce zero warnings (run npm --prefix web run lint); all warnings must be fixed before proceeding.

Files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
🧠 Learnings (13)
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/components/ui/*.{tsx,ts} : For new shared React components: place in web/src/components/ui/ with kebab-case filename, create .stories.tsx with all states, export props as TypeScript interface, use design tokens exclusively

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : When creating new shared web components, place in web/src/components/ui/ with kebab-case filename, create .stories.tsx alongside with all states (default, hover, loading, error, empty), export props as TypeScript interface, use design tokens exclusively with no hardcoded colors/fonts/spacing, and import cn from `@/lib/utils` for conditional class merging

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-04-03T10:54:21.472Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T10:54:21.472Z
Learning: Applies to web/src/components/ui/**/*.{tsx,ts} : When creating new shared components, place them in `web/src/components/ui/` with descriptive kebab-case filename, create accompanying `.stories.tsx` with all states (default, hover, loading, error, empty), export props as TypeScript interface, and use design tokens exclusively with no hardcoded colors, fonts, or spacing.

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from web/src/components/ui/ (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, form fields, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor) before creating new components

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use `@/components/ui/animated-presence` (Framer Motion AnimatePresence keyed by route) for page transitions

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-03-27T12:44:29.466Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:44:29.466Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem) before creating new ones

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : Create new shared components in `web/src/components/ui/` with `.stories.tsx` Storybook file covering all states (default, hover, loading, error, empty)

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-04-03T10:54:21.472Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T10:54:21.472Z
Learning: Applies to web/src/**/*.{tsx,ts} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, TagInput, MetadataGrid, ProjectStatusBadge, ContentTypeBadge)

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-03-31T14:31:11.894Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:31:11.894Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use React 19, TypeScript 6.0+, and design system tokens from shadcn/ui + Tailwind CSS 4 + Radix UI in web dashboard

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-04-03T10:54:21.472Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T10:54:21.472Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do NOT create complex (>8 line) JSX inside `.map()` -- extract to a shared component.

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-04-01T14:22:06.315Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T14:22:06.315Z
Learning: Applies to {**/*.py,web/src/**/*.{ts,tsx}} : Validate at system boundaries (user input, external APIs, config files)

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-04-01T05:46:17.064Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T05:46:17.064Z
Learning: Fix all valid issues found by review agents (including pre-existing issues in surrounding code, suggestions, and adjacent findings) — never skip or defer

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-03-15T21:49:53.264Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T21:49:53.264Z
Learning: Fix everything valid — never skip when review agents find valid issues (including pre-existing issues in surrounding code, suggestions, and findings adjacent to the PR's changes). No deferring, no 'out of scope' skipping.

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx

- Add local saveError state to handleSave (settings save errors now
  display in the inline banner, not just as toasts)
- Show DepartmentOverridesPanel when departments.length > 0 instead
  of gating on !deptLoadError (partial pagination results visible)
- Remove unused deptLoadError state
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx (3)

249-261: ⚠️ Potential issue | 🟠 Major

Project-policy saves are still non-atomic.

This one editor action still fans out into six independent updateSetting() calls. If one request fails after earlier ones succeed, the stored ceremony policy is left partially updated and the page has no rollback path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx` around lines
249 - 261, The current handleSave uses six independent updateSetting() calls
which can leave state partially persisted if one fails; replace this fan-out
with a single atomic save operation (either call a new backend endpoint like
saveCeremonyPolicy or add a batch updateSettings API) that accepts the full
policy payload (strategy, strategyConfig, velocityCalculator, autoTransition,
transitionThreshold, ceremonyOverrides serialized as needed) and performs the
updates transactionally; update handleSave to call that single atomic function,
await its result, only then setIsDirty(false) and clear save state, and
handle/report errors from the single call for proper rollback/consistency.

86-90: ⚠️ Potential issue | 🟠 Major

Malformed booleans currently get coerced to false.

Any non-undefined string other than "true" becomes false here. Values like "0", "no", or typos are silently normalized and then written back as a valid setting on the next save, which hides the underlying config corruption.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx` around lines
86 - 90, The current autoTransition initializer in CeremonyPolicyPage.tsx
coerces any non-undefined string (e.g., "0", "no", typos) to false; change the
logic to only accept explicit "true" or "false" (case-insensitive) and treat any
other value as invalid/undefined so it isn't silently written back.
Specifically, in the autoTransition IIFE that calls
get('ceremony_auto_transition'), check raw === undefined => return true; else if
raw.toLowerCase() === 'true' => return true; else if raw.toLowerCase() ===
'false' => return false; otherwise return undefined (or another sentinel the UI
uses to surface invalid/malformed config) to prevent automatic normalization and
save-back.

50-62: ⚠️ Potential issue | 🟠 Major

Reject empty strings in the JSON-backed settings.

if (sc) / if (raw) still treats '' as “missing” instead of “corrupt”. That bypasses the warning state and the next save silently rewrites the broken value to {}.

♻️ Suggested tightening
-    const sc = get('ceremony_strategy_config')
-    if (sc) {
-      try {
-        const parsed: unknown = JSON.parse(sc)
-        if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
-          config = parsed as Record<string, unknown>
-        } else {
-          configParseError = true
-        }
-      } catch {
-        configParseError = true
-      }
-    }
+    const sc = get('ceremony_strategy_config')
+    if (sc !== undefined) {
+      if (sc.trim() === '') {
+        configParseError = true
+      } else {
+        try {
+          const parsed: unknown = JSON.parse(sc)
+          if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
+            config = parsed as Record<string, unknown>
+          } else {
+            configParseError = true
+          }
+        } catch {
+          configParseError = true
+        }
+      }
+    }
@@
-    if (raw) {
-      try {
-        const parsed: unknown = JSON.parse(raw)
-        if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
-          return { overrides: parsed as Record<string, CeremonyPolicyConfig | null>, overridesParseError }
-        }
-        overridesParseError = true
-      } catch {
-        overridesParseError = true
-      }
-    }
+    if (raw !== undefined) {
+      if (raw.trim() === '') {
+        overridesParseError = true
+      } else {
+        try {
+          const parsed: unknown = JSON.parse(raw)
+          if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
+            return { overrides: parsed as Record<string, CeremonyPolicyConfig | null>, overridesParseError }
+          }
+          overridesParseError = true
+        } catch {
+          overridesParseError = true
+        }
+      }
+    }

Also applies to: 162-173

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx` around lines
50 - 62, The code currently treats empty string sc from
get('ceremony_strategy_config') as "missing" because of the truthy check; change
the check to explicitly reject empty strings and only attempt JSON.parse when sc
is non-null/undefined and not an empty string (e.g. if (sc !== undefined && sc
!== null && sc !== '' && sc.trim() !== '')). On parse failure or when sc is an
empty string set configParseError = true (same branch used in the catch and
non-object case). Apply the identical fix to the other analogous parsing block
that uses raw/parsed/configParseError so empty-string settings are treated as
corrupt rather than missing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx`:
- Around line 323-327: The rendering currently shows PolicySourceBadge and
passes resolvedPolicy into PolicyFieldsPanel even when fetchResolvedPolicy()
failed and storeError is set, causing stale provenance to appear; update the
render conditions so PolicySourceBadge and PolicyFieldsPanel (the components
referenced as PolicySourceBadge and PolicyFieldsPanel) are only rendered when
resolvedPolicy is present AND storeError is falsy (and typically loading is
false), i.e. gate their JSX on something like (!loading && !storeError &&
resolvedPolicy) so a failed refresh won’t display badges or fields from an older
resolvedPolicy.
- Around line 35-40: The page shows StrategyChangeWarning based only on
`loading` (which is tied to `fetchResolvedPolicy`) allowing stale or missing
`/active` data to drive the UI; update the page to wait for fresh
active-strategy data by (1) adding/using an explicit loading flag for
`fetchActiveStrategy` (e.g. `activeStrategyLoading`) from
`useCeremonyPolicyStore` and gate interactive UI and `StrategyChangeWarning` on
both `loading` and `activeStrategyLoading` (or a combined `isLoading`), and (2)
ensure the store clears `activeStrategy` on `fetchActiveStrategy` failure (or
sets an `activeStrategyError`) so `activeStrategy` cannot wrongly render after
an error; reference `StrategyChangeWarning`, `useCeremonyPolicyStore`,
`loading`, `activeStrategyError`, `fetchActiveStrategy`, and `activeStrategy`
when making these changes.

---

Duplicate comments:
In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx`:
- Around line 249-261: The current handleSave uses six independent
updateSetting() calls which can leave state partially persisted if one fails;
replace this fan-out with a single atomic save operation (either call a new
backend endpoint like saveCeremonyPolicy or add a batch updateSettings API) that
accepts the full policy payload (strategy, strategyConfig, velocityCalculator,
autoTransition, transitionThreshold, ceremonyOverrides serialized as needed) and
performs the updates transactionally; update handleSave to call that single
atomic function, await its result, only then setIsDirty(false) and clear save
state, and handle/report errors from the single call for proper
rollback/consistency.
- Around line 86-90: The current autoTransition initializer in
CeremonyPolicyPage.tsx coerces any non-undefined string (e.g., "0", "no", typos)
to false; change the logic to only accept explicit "true" or "false"
(case-insensitive) and treat any other value as invalid/undefined so it isn't
silently written back. Specifically, in the autoTransition IIFE that calls
get('ceremony_auto_transition'), check raw === undefined => return true; else if
raw.toLowerCase() === 'true' => return true; else if raw.toLowerCase() ===
'false' => return false; otherwise return undefined (or another sentinel the UI
uses to surface invalid/malformed config) to prevent automatic normalization and
save-back.
- Around line 50-62: The code currently treats empty string sc from
get('ceremony_strategy_config') as "missing" because of the truthy check; change
the check to explicitly reject empty strings and only attempt JSON.parse when sc
is non-null/undefined and not an empty string (e.g. if (sc !== undefined && sc
!== null && sc !== '' && sc.trim() !== '')). On parse failure or when sc is an
empty string set configParseError = true (same branch used in the catch and
non-object case). Apply the identical fix to the other analogous parsing block
that uses raw/parsed/configParseError so empty-string settings are treated as
corrupt rather than missing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: dcff20a1-4e25-4d7e-bbf4-87fc5a06139e

📥 Commits

Reviewing files that changed from the base of the PR and between 71a1d81 and c1e6c5c.

📒 Files selected for processing (1)
  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Dashboard Test
  • GitHub Check: Test (Python 3.14)
🧰 Additional context used
📓 Path-based instructions (3)
web/src/**/*.{tsx,ts}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{tsx,ts}: ALWAYS reuse existing components from web/src/components/ui/ before creating new ones (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, TagInput, MetadataGrid, ProjectStatusBadge, ContentTypeBadge)
Use Tailwind semantic classes (text-foreground, bg-card, text-accent, text-success, bg-danger, etc.) or CSS variables (var(--so-accent)) for colors. NEVER hardcode hex values in .tsx/.ts files.
Use font-sans or font-mono for typography (maps to Geist tokens). NEVER set fontFamily directly.
Use density-aware tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing. NEVER hardcode pixel values for layout spacing.
Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows and borders. NEVER hardcode values.
Import cn from @/lib/utils for conditional class merging in new components.
Do NOT recreate status dots inline -- use <StatusBadge> component.
Do NOT build card-with-header layouts from scratch -- use <SectionCard> component.
Do NOT create metric displays with text-metric font-bold -- use <MetricCard> component.
Do NOT render initials circles manually -- use <Avatar> component.
Do NOT create complex (>8 line) JSX inside .map() -- extract to a shared component.
TypeScript strict mode must pass type checking (run npm --prefix web run type-check); all type errors must be resolved before proceeding.
Do NOT hardcode Framer Motion transition durations -- use @/lib/motion presets.

Files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
web/src/**/*.{tsx,ts,css}

📄 CodeRabbit inference engine (web/CLAUDE.md)

Do NOT use rgba() with hardcoded values -- use design token variables.

Files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
web/src/**/*.{tsx,ts,js,jsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

ESLint must enforce zero warnings (run npm --prefix web run lint); all warnings must be fixed before proceeding.

Files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
🧠 Learnings (14)
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/components/ui/*.{tsx,ts} : For new shared React components: place in web/src/components/ui/ with kebab-case filename, create .stories.tsx with all states, export props as TypeScript interface, use design tokens exclusively

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : When creating new shared web components, place in web/src/components/ui/ with kebab-case filename, create .stories.tsx alongside with all states (default, hover, loading, error, empty), export props as TypeScript interface, use design tokens exclusively with no hardcoded colors/fonts/spacing, and import cn from `@/lib/utils` for conditional class merging

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-04-03T10:54:21.472Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T10:54:21.472Z
Learning: Applies to web/src/components/ui/**/*.{tsx,ts} : When creating new shared components, place them in `web/src/components/ui/` with descriptive kebab-case filename, create accompanying `.stories.tsx` with all states (default, hover, loading, error, empty), export props as TypeScript interface, and use design tokens exclusively with no hardcoded colors, fonts, or spacing.

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from web/src/components/ui/ (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, form fields, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor) before creating new components

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use `@/components/ui/animated-presence` (Framer Motion AnimatePresence keyed by route) for page transitions

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : Create new shared components in `web/src/components/ui/` with `.stories.tsx` Storybook file covering all states (default, hover, loading, error, empty)

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-03-27T12:44:29.466Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:44:29.466Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem) before creating new ones

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-04-03T10:54:21.472Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T10:54:21.472Z
Learning: Applies to web/src/**/*.{tsx,ts} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, TagInput, MetadataGrid, ProjectStatusBadge, ContentTypeBadge)

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-04-03T10:54:21.472Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T10:54:21.472Z
Learning: Applies to web/src/components/ui/**/*.tsx : Component Storybook stories are mandatory; new components without `.stories.tsx` files violate design system enforcement.

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-03-31T14:31:11.894Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:31:11.894Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use React 19, TypeScript 6.0+, and design system tokens from shadcn/ui + Tailwind CSS 4 + Radix UI in web dashboard

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-04-03T10:54:21.472Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T10:54:21.472Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do NOT create complex (>8 line) JSX inside `.map()` -- extract to a shared component.

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-04-01T14:22:06.315Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T14:22:06.315Z
Learning: Applies to {**/*.py,web/src/**/*.{ts,tsx}} : Validate at system boundaries (user input, external APIs, config files)

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-04-01T05:46:17.064Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T05:46:17.064Z
Learning: Fix all valid issues found by review agents (including pre-existing issues in surrounding code, suggestions, and adjacent findings) — never skip or defer

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
📚 Learning: 2026-03-15T21:49:53.264Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T21:49:53.264Z
Learning: Fix everything valid — never skip when review agents find valid issues (including pre-existing issues in surrounding code, suggestions, and findings adjacent to the PR's changes). No deferring, no 'out of scope' skipping.

Applied to files:

  • web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx
🔇 Additional comments (1)
web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx (1)

216-244: Nice partial-results handling for department pagination.

Preserving allDepts in finally keeps already-fetched pages visible, and the cancelled guard prevents late state updates after unmount.

Comment on lines +35 to +40
const loading = useCeremonyPolicyStore((s) => s.loading)
const storeError = useCeremonyPolicyStore((s) => s.error)
const activeStrategyError = useCeremonyPolicyStore((s) => s.activeStrategyError)
const storeSaveError = useCeremonyPolicyStore((s) => s.saveError)
const fetchResolvedPolicy = useCeremonyPolicyStore((s) => s.fetchResolvedPolicy)
const fetchActiveStrategy = useCeremonyPolicyStore((s) => s.fetchActiveStrategy)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Gate StrategyChangeWarning on fresh active-strategy data.

loading only tracks fetchResolvedPolicy(). In web/src/stores/ceremony-policy.ts, fetchActiveStrategy() neither exposes a loading flag nor clears stale activeStrategy on failure, so this page can become interactive before /active resolves and can still render the warning from old data after an error.

Also applies to: 207-211, 329-343

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx` around lines
35 - 40, The page shows StrategyChangeWarning based only on `loading` (which is
tied to `fetchResolvedPolicy`) allowing stale or missing `/active` data to drive
the UI; update the page to wait for fresh active-strategy data by (1)
adding/using an explicit loading flag for `fetchActiveStrategy` (e.g.
`activeStrategyLoading`) from `useCeremonyPolicyStore` and gate interactive UI
and `StrategyChangeWarning` on both `loading` and `activeStrategyLoading` (or a
combined `isLoading`), and (2) ensure the store clears `activeStrategy` on
`fetchActiveStrategy` failure (or sets an `activeStrategyError`) so
`activeStrategy` cannot wrongly render after an error; reference
`StrategyChangeWarning`, `useCeremonyPolicyStore`, `loading`,
`activeStrategyError`, `fetchActiveStrategy`, and `activeStrategy` when making
these changes.

Comment on lines +323 to +327
{!loading && storeError && (
<div className="rounded-md border border-danger/30 bg-danger/5 p-card text-sm text-danger">
Failed to load resolved policy: {storeError}
</div>
)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid rendering source badges from stale resolvedPolicy.

fetchResolvedPolicy() leaves the previous resolvedPolicy in the store on failure. This path still renders PolicySourceBadge and passes resolvedPolicy into PolicyFieldsPanel while storeError is set, so a failed refresh can show provenance from an older snapshot.

Also applies to: 356-382

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/ceremony-policy/CeremonyPolicyPage.tsx` around lines
323 - 327, The rendering currently shows PolicySourceBadge and passes
resolvedPolicy into PolicyFieldsPanel even when fetchResolvedPolicy() failed and
storeError is set, causing stale provenance to appear; update the render
conditions so PolicySourceBadge and PolicyFieldsPanel (the components referenced
as PolicySourceBadge and PolicyFieldsPanel) are only rendered when
resolvedPolicy is present AND storeError is falsy (and typically loading is
false), i.e. gate their JSX on something like (!loading && !storeError &&
resolvedPolicy) so a failed refresh won’t display badges or fields from an older
resolvedPolicy.

@Aureliolo Aureliolo merged commit 865554c into main Apr 3, 2026
32 of 34 checks passed
@Aureliolo Aureliolo deleted the feat/ceremony-dashboard-dept-overrides branch April 3, 2026 18:35
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview April 3, 2026 18:35 — with GitHub Actions Inactive
Aureliolo added a commit that referenced this pull request Apr 3, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.6.0](v0.5.9...v0.6.0)
(2026-04-03)


### Features

* dashboard UI for ceremony policy settings
([#1038](#1038))
([865554c](865554c)),
closes [#979](#979)
* implement tool-based memory retrieval injection strategy
([#1039](#1039))
([329270e](329270e)),
closes [#207](#207)
* local model management for Ollama and LM Studio
([#1037](#1037))
([e1b14d3](e1b14d3)),
closes [#1030](#1030)
* workflow execution -- instantiate tasks from WorkflowDefinition
([#1040](#1040))
([e9235e3](e9235e3)),
closes [#1004](#1004)


### Maintenance

* shared Hypothesis failure DB + deterministic CI profile
([#1041](#1041))
([901ae92](901ae92))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: dashboard UI for ceremony policy settings

2 participants