Skip to content

fix: harden agent identity versioning post-review#1128

Merged
Aureliolo merged 9 commits intomainfrom
feat/agent-identity-versioning
Apr 7, 2026
Merged

fix: harden agent identity versioning post-review#1128
Aureliolo merged 9 commits intomainfrom
feat/agent-identity-versioning

Conversation

@Aureliolo
Copy link
Copy Markdown
Owner

Summary

Post-review hardening pass on the agent identity versioning infrastructure introduced in #1076.

Changes

Protocol & Implementation

  • VersionRepository.save_version returns bool -- True if the row was inserted, False if INSERT OR IGNORE silently dropped a concurrent duplicate
  • SQLiteVersionRepository: returns cursor.rowcount > 0; _deserialize_row now raises QueryError with distinct messages for JSON corruption (json_corrupt) vs schema drift (schema_drift) vs unexpected errors

VersioningService

  • Concurrent-write safety: when save_version returns False, re-fetches the actual persisted snapshot via get_latest_version and returns it instead of the locally-constructed (never-persisted) version

Type Design

  • IdentityFieldChange: @model_validator enforces added/removed/modified invariants -- added requires old_value=None, removed requires new_value=None, modified requires both non-None
  • AgentIdentityDiff.summary: converted from a stored str = "" field to @computed_field @property (CLAUDE.md convention)
  • AgentIdentityDiff: @model_validator rejects from_version == to_version

Error Handling

  • review_gate._record_decision: except Exception narrowed to except QueryError; on failure sets metadata = {"charter_version_lookup_failed": True} so callers can distinguish lookup failure from no-version-found; docstring updated
  • hr.registry._snapshot: except Exception narrowed to except PersistenceError (local import to avoid circular dependency); docstring explains the lock-exclusion rationale

Tests

  • test_service.py: mock save_version.return_value = True; new TestConcurrentSave (mock returns False, verifies re-fetch); Hypothesis @given snapshot determinism properties
  • test_diff.py: test_empty_diff_valid summary assertion fixed to "no changes"; TestIdentityFieldChangeModelValidator (6 tests for invalid states); test_same_from_and_to_version_raises
  • test_version_repo.py: TestSaveVersionBoolReturn (True on first insert, False on duplicate); TestTableNameValidationEdgeCases (empty string, leading digit); TestDeserializeRowErrors (corrupt JSON, schema drift)
  • test_identity_versioning.py: test_charter_version_lookup_error_sets_failure_flag integration test
  • test_review_gate.py: _make_mock_persistence now wires identity_versions as AsyncMock (required by narrowed exception handling)
  • test_events.py: test_no_event_constant_contains_vendor_name -- scans all event modules for vendor names

Docs

  • CLAUDE.md: adds VERSION_SAVED and VERSION_SNAPSHOT_FAILED from events.versioning to the Logging event names list
  • docs/reference/claude-reference.md: adds versioning/ package to Package Structure

Closes #1076

Copilot AI review requested due to automatic review settings April 7, 2026 19:52
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 7, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c91e2055-15e3-43d0-a374-6016e694bad5

📥 Commits

Reviewing files that changed from the base of the PR and between f2c4446 and 23654ba.

📒 Files selected for processing (6)
  • src/synthorg/engine/identity/diff.py
  • src/synthorg/engine/review_gate.py
  • tests/unit/engine/identity/test_diff.py
  • tests/unit/persistence/sqlite/test_identity_version_repo.py
  • tests/unit/versioning/test_hashing.py
  • tests/unit/versioning/test_models.py

Walkthrough

Introduces a generic versioning subsystem and integrates identity versioning: new package synthorg.versioning (with VersionSnapshot[T], compute_content_hash, VersioningService[T]), a VersionRepository protocol and SQLiteVersionRepository implementation, an agent_identity_versions SQLite table and backend wiring, and observability event constants for versioning. AgentRegistryService snapshots identities after register/update_identity (best-effort), ReviewGateService performs a best-effort lookup to inject charter_version into DecisionRecord.metadata. Adds identity diff utilities, docs and design updates, tests (unit/integration), and a shell redirect-detection tweak.

Suggested labels

autorelease: tagged

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.96% 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 The title 'fix: harden agent identity versioning post-review' clearly and specifically describes the main objective of the PR—a hardening pass on the agent identity versioning infrastructure introduced in #1076.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, detailing protocol changes, implementation improvements, type design updates, error handling refinements, test coverage, and documentation updates.
Linked Issues check ✅ Passed The PR successfully addresses all key objectives from #1076: agent identity versioning with audit trail integration, design documentation, protocol/repository implementation, error handling, and integration tests demonstrating version pinning through review cycles.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the linked issue #1076 objectives. Modifications to protocol, service, models, error handling, tests, and documentation all contribute to hardening the versioning infrastructure without introducing unrelated functionality.

✏️ 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.

@Aureliolo Aureliolo temporarily deployed to cloudflare-preview April 7, 2026 19:54 — with GitHub Actions Inactive
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 7, 2026

Dependency Review

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

Snapshot Warnings

⚠️: No snapshots were found for the head SHA 23654ba.
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 a generic versioning infrastructure to capture immutable snapshots of Pydantic models, with a primary focus on agent identities. The changes introduce a VersioningService for content-addressable deduplication, a VersionRepository protocol with a SQLite implementation, and a recursive diffing utility for identifying field-level changes between identity versions. The AgentRegistryService now automatically snapshots identities during registration and updates, and the ReviewGateService integrates these versions into DecisionRecord metadata to provide a full audit trail of agent charters. I have no feedback to provide.

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

Hardens the agent identity versioning stack introduced in #1076 by tightening repository semantics (detecting ignored concurrent inserts), improving error classification, and wiring identity-version metadata into review-gate decision records for auditability.

Changes:

  • Introduces generic versioning primitives (VersionSnapshot[T], VersionRepository[T], VersioningService[T]) plus a reusable SQLite implementation and schema support for agent_identity_versions.
  • Makes concurrent INSERT OR IGNORE behavior observable (save_version -> bool) and ensures callers receive the actually-persisted snapshot on concurrent duplicates.
  • Adds identity diffing/validation utilities and expands unit/integration coverage across versioning, persistence, review-gate metadata, and event constant hygiene.

Reviewed changes

Copilot reviewed 31 out of 34 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/unit/versioning/test_service.py Unit tests for VersioningService, including concurrent-insert handling + hash determinism properties.
tests/unit/versioning/test_models.py Unit tests for VersionSnapshot[T] immutability and validation constraints.
tests/unit/versioning/test_hashing.py Unit tests for compute_content_hash determinism/sensitivity/format.
tests/unit/versioning/init.py Establishes unit test package namespace for versioning tests.
tests/unit/persistence/test_protocol.py Updates persistence protocol test scaffolding to include identity_versions.
tests/unit/persistence/sqlite/test_version_repo.py Comprehensive tests for generic SQLiteVersionRepository[T] behavior and error paths.
tests/unit/persistence/sqlite/test_migrations.py Extends migration expectations to include the new identity versions table + indexes.
tests/unit/persistence/sqlite/test_identity_version_repo.py Integration-style unit test ensuring real AgentIdentity round-trips through the generic repo.
tests/unit/observability/test_events.py Includes new versioning event module discovery + vendor-name guard for event constants.
tests/unit/engine/test_review_gate.py Adjusts mocks to provide async identity_versions for narrowed exception handling.
tests/unit/engine/identity/test_diff.py Tests for identity diff computation, computed summary, and model invariants.
tests/unit/engine/identity/init.py Establishes unit test package namespace for identity tests.
tests/unit/api/fakes_backend.py Adds in-memory FakeVersionRepository and wires it into the fake persistence backend.
tests/integration/engine/identity/test_identity_versioning.py End-to-end integration tests for registry snapshotting + decision-record charter_version metadata behavior.
tests/integration/engine/identity/init.py Establishes integration test package namespace for engine identity tests.
src/synthorg/versioning/service.py Adds generic VersioningService[T] with content-hash dedup and concurrent insert hardening.
src/synthorg/versioning/models.py Adds generic immutable VersionSnapshot[T] model with validation constraints.
src/synthorg/versioning/hashing.py Adds canonical JSON-based SHA-256 content hashing for frozen Pydantic models.
src/synthorg/versioning/init.py Public re-exports for versioning package API.
src/synthorg/persistence/version_repo.py Defines VersionRepository[T] protocol with save_version -> bool and full CRUD surface.
src/synthorg/persistence/sqlite/version_repo.py Implements generic SQLite-backed version repository with improved deserialize error classification.
src/synthorg/persistence/sqlite/schema.sql Adds agent_identity_versions table + indexes to SQLite schema.
src/synthorg/persistence/sqlite/backend.py Wires identity_versions repo into SQLite persistence backend.
src/synthorg/persistence/protocol.py Extends PersistenceBackend protocol to expose identity_versions.
src/synthorg/observability/events/versioning.py Introduces versioning-related structured log event constants.
src/synthorg/hr/registry.py Adds optional VersioningService[AgentIdentity] injection and best-effort snapshotting on register/update.
src/synthorg/engine/review_gate.py Injects charter_version metadata into DecisionRecord and narrows exception handling to QueryError.
src/synthorg/engine/identity/diff.py Adds field-level diff models + recursive diff computation for identity snapshots.
src/synthorg/engine/identity/init.py Public re-exports for identity diff utilities.
scripts/check_bash_no_write.sh Reduces false positives by stripping double-quoted segments before redirect detection.
docs/reference/claude-reference.md Documents the new versioning/ package under repository structure.
docs/design/engine.md Updates engine design docs to reflect implemented identity versioning and decision-record linkage.
docs/design/agents.md Adds identity versioning design documentation and review-gate metadata behavior.
CLAUDE.md Updates logging-event-name guidance to include versioning event constants.

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

Comment on lines +234 to +237
This lookup is best-effort. A failure is logged at WARNING and the decision record is
written without the `charter_version` key. No schema migration is required: the
`metadata` field on `DecisionRecord` was designed as a forward-compatible extension
point.
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

The DecisionRecord integration section says that on lookup failure the decision record is written without the charter_version key, but the current implementation sets metadata = {"charter_version_lookup_failed": True} on QueryError. Please update this doc to reflect the failure-flag behavior so operators can distinguish lookup failures from "no version found".

Copilot uses AI. Check for mistakes.
Comment on lines +278 to +280
The versioning call is fire-and-forget; a ``PersistenceError`` is logged
but never re-raised so that registry operations always succeed even when
the versioning back-end is unavailable.
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

The _snapshot docstring says the versioning call is "fire-and-forget", but the code awaits snapshot_if_changed, so it does block on I/O (it’s just best-effort because PersistenceError is swallowed). Please reword to avoid implying this runs in the background (or actually dispatch it in a background task if that’s the intent).

Suggested change
The versioning call is fire-and-forget; a ``PersistenceError`` is logged
but never re-raised so that registry operations always succeed even when
the versioning back-end is unavailable.
The versioning call is awaited here, but failures are best-effort:
a ``PersistenceError`` is logged and never re-raised so that registry
operations always succeed even when the versioning back-end is unavailable.

Copilot uses AI. Check for mistakes.
)
self._select_by_hash_sql = (
f"SELECT {_c} FROM {_t} " # noqa: S608
f"WHERE entity_id = ? AND content_hash = ?"
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

get_by_content_hash can return nondeterministic results if the same content_hash exists in multiple versions for an entity (e.g., an identity changes and later reverts). The SQL currently has no ORDER BY/LIMIT, so fetchone() may pick an arbitrary matching row. Consider adding ORDER BY version DESC LIMIT 1 (or explicitly documenting which version should be returned).

Suggested change
f"WHERE entity_id = ? AND content_hash = ?"
f"WHERE entity_id = ? AND content_hash = ? "
f"ORDER BY version DESC LIMIT 1"

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

codecov bot commented Apr 7, 2026

Codecov Report

❌ Patch coverage is 87.64368% with 43 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.00%. Comparing base (ec57641) to head (f2c4446).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/synthorg/persistence/sqlite/version_repo.py 76.11% 31 Missing and 1 partial ⚠️
src/synthorg/versioning/models.py 77.77% 4 Missing and 2 partials ⚠️
src/synthorg/hr/registry.py 85.71% 2 Missing ⚠️
src/synthorg/versioning/service.py 95.00% 1 Missing and 1 partial ⚠️
src/synthorg/persistence/sqlite/backend.py 88.88% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1128      +/-   ##
==========================================
- Coverage   89.02%   89.00%   -0.02%     
==========================================
  Files         834      843       +9     
  Lines       48719    49053     +334     
  Branches     4908     4930      +22     
==========================================
+ Hits        43370    43662     +292     
- Misses       4432     4471      +39     
- Partials      917      920       +3     

☔ 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

@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

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

Inline comments:
In `@scripts/check_bash_no_write.sh`:
- Line 63: The REDIR extraction uses a GNU-specific sed quantifier (\?) which
fails on macOS/BSD; in the pipeline that computes REDIR (the line assigning
REDIR from COMMAND_FOR_REDIR / grep / sed), replace the GNU-only sed expression
with a POSIX/BSD-compatible one by using extended regex mode (sed -E) and the
pattern s/^>>?["'"'"']*// so it matches both '>' and '>>' across platforms;
update the sed invocation in the REDIR assignment accordingly.

In `@src/synthorg/engine/identity/diff.py`:
- Line 145: The compute_diff function currently types the identifier parameter
agent_id as str; replace it with NotBlankStr from core.types to match the
module's identifier convention (AgentIdentityDiff.agent_id) and ensure
whitespace validation; update the function signature to use NotBlankStr, add the
NotBlankStr import from core.types if missing, and adjust any internal/type
hints or callers of compute_diff to accept NotBlankStr (or NotBlankStr | None if
optional) so the type aligns with existing identifier fields.

In `@src/synthorg/persistence/sqlite/schema.sql`:
- Line 590: Add the same UTC-offset CHECK used elsewhere to the
agent_identity_versions table by enforcing that the saved_at column contains an
ISO8601 UTC offset (e.g., ends with '+00:00' or 'Z'); locate the
agent_identity_versions table definition and update the saved_at column
declaration (or add a table-level CHECK) to include a constraint like
CHECK(saved_at LIKE '%+00:00' OR saved_at LIKE '%Z') so raw SQL callers and
future migrations cannot insert timestamps without the UTC offset.

In `@src/synthorg/persistence/sqlite/version_repo.py`:
- Around line 170-203: The serializer error from
self._serialize(version.snapshot) must be normalized into the same persistence
error path: compute/serialize the snapshot before executing the DB statement (or
wrap the serialization in its own try/except) and on any Exception (e.g.,
TypeError, ValueError) call logger.exception with VERSION_SAVE_FAILED and the
same context (table, entity_id, version, error=str(exc)) and raise
QueryError(...) from the original exception; ensure this references the save
method (save_version / the block using self._insert_sql, VERSION_SAVED,
VERSION_SAVE_FAILED, logger, QueryError, and self._serialize) so serializer
failures are logged and wrapped like sqlite errors.
- Around line 124-160: Add a final broad exception handler to normalize any
other failures from the deserializer into QueryError: after the existing except
blocks in the method that calls self._deserialize (the block handling
JSONDecodeError, ValidationError, ValueError, KeyError), add an except Exception
as exc that builds the same context (using data.get('entity_id') and
data.get('version')), logs VERSION_FETCH_FAILED via logger.exception with
table=self._table, context, error=str(exc) and reason="unexpected", and
re-raises a QueryError with a descriptive msg (e.g., "Failed to deserialize
version snapshot ...") from exc so all callback errors (including TypeError or
others) are wrapped consistently.

In `@src/synthorg/versioning/service.py`:
- Around line 92-97: The current branch that handles a failed save_version (when
save_version returned falsy) must re-fetch the persisted latest row via
self._repo.get_latest_version(entity_id), compare its content_hash to the
content_hash of the version we attempted to insert, and only return immediately
if they match; if they differ (meaning the insert lost to different content),
compute a new version number from latest.version + 1, update the version object
accordingly and retry save by looping this logic until an insert succeeds or the
persisted row matches; use the existing methods self._repo.save_version and
self._repo.get_latest_version and compare on the version.content_hash field to
decide whether to return or retry.
- Around line 71-90: After computing content_hash, make a deep copy of the
snapshot to freeze its state before any await (i.e., immediately after
compute_content_hash and before awaiting self._repo.get_latest_version); use
copy.deepcopy(the_snapshot) (and add "import copy") to produce snapshot_copy and
pass that snapshot_copy into VersionSnapshot (instead of the original snapshot)
so the persisted snapshot matches the precomputed content_hash; references:
compute_content_hash, self._repo.get_latest_version, and VersionSnapshot.

In `@tests/unit/engine/identity/test_diff.py`:
- Around line 164-173: Replace the duplicate per-case tests for
IdentityFieldChange (e.g., test_added_change_new_value_present and the similar
tests around lines 227-295) with a single pytest.mark.parametrize table that
enumerates change_type, old_value, new_value, and expected validity; update the
test function (e.g., test_identity_field_change_matrix) to loop over rows,
instantiate IdentityFieldChange with the provided
field_path/change_type/old_value/new_value, and assert expected outcomes (valid
creation or expected validation error) so the invariant matrix is covered in one
parametrized test rather than many near-duplicate functions.

In `@tests/unit/observability/test_events.py`:
- Around line 749-775: The test test_no_event_constant_contains_vendor_name
currently hardcodes real vendor names in the vendor_names set; remove these
literals from the test and instead load the disallowed vendor identifiers at
runtime from an allowed external resource (e.g., a `.claude/` data file) and use
that loaded list in place of vendor_names when iterating modules via
pkgutil.walk_packages over the events package; ensure the test still references
the same symbols (test_no_event_constant_contains_vendor_name, vendor_names
variable usage, events import, and the pkgutil/importlib loop) but reads the
vendor list from the external file or replaces them with generic placeholders
like "test-provider" loaded at runtime.

In `@tests/unit/persistence/sqlite/test_identity_version_repo.py`:
- Around line 1-5: The module docstring says "Integration test" but the tests
are marked with `@pytest.mark.unit`; make these consistent by converting the
markers and docstring to integration: update the top-of-file docstring in
tests/unit/persistence/sqlite/test_identity_version_repo.py to "Integration
test" (or remove the mismatch wording) and replace all occurrences of
`@pytest.mark.unit` in this file with `@pytest.mark.integration` so the declared
marker matches the in-memory SQLite integration nature of the tests (search for
the module docstring and all `@pytest.mark.unit` annotations to change).

In `@tests/unit/versioning/test_service.py`:
- Around line 176-191: The test test_different_models_different_hash currently
uses an early return when model_a == model_b which silently skips cases and
hinders Hypothesis shrinking; replace that check with Hypothesis's
assume(model_a != model_b) (import assume from hypothesis) so inputs where
_Simple(name=..., value=...) are identical are filtered properly, then assert
compute_content_hash(model_a) != compute_content_hash(model_b) as before.
🪄 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: 2d6e599b-da42-4a70-a1fe-44ad756d75c3

📥 Commits

Reviewing files that changed from the base of the PR and between 59b31f9 and 6c0190c.

📒 Files selected for processing (34)
  • CLAUDE.md
  • docs/design/agents.md
  • docs/design/engine.md
  • docs/reference/claude-reference.md
  • scripts/check_bash_no_write.sh
  • src/synthorg/engine/identity/__init__.py
  • src/synthorg/engine/identity/diff.py
  • src/synthorg/engine/review_gate.py
  • src/synthorg/hr/registry.py
  • src/synthorg/observability/events/versioning.py
  • src/synthorg/persistence/protocol.py
  • src/synthorg/persistence/sqlite/backend.py
  • src/synthorg/persistence/sqlite/schema.sql
  • src/synthorg/persistence/sqlite/version_repo.py
  • src/synthorg/persistence/version_repo.py
  • src/synthorg/versioning/__init__.py
  • src/synthorg/versioning/hashing.py
  • src/synthorg/versioning/models.py
  • src/synthorg/versioning/service.py
  • tests/integration/engine/identity/__init__.py
  • tests/integration/engine/identity/test_identity_versioning.py
  • tests/unit/api/fakes_backend.py
  • tests/unit/engine/identity/__init__.py
  • tests/unit/engine/identity/test_diff.py
  • tests/unit/engine/test_review_gate.py
  • tests/unit/observability/test_events.py
  • tests/unit/persistence/sqlite/test_identity_version_repo.py
  • tests/unit/persistence/sqlite/test_migrations.py
  • tests/unit/persistence/sqlite/test_version_repo.py
  • tests/unit/persistence/test_protocol.py
  • tests/unit/versioning/__init__.py
  • tests/unit/versioning/test_hashing.py
  • tests/unit/versioning/test_models.py
  • tests/unit/versioning/test_service.py
📜 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). (5)
  • GitHub Check: Agent
  • GitHub Check: Build Web
  • GitHub Check: Build Backend
  • GitHub Check: Test (Python 3.14)
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (3)
src/synthorg/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Include from synthorg.observability import get_logger and logger = get_logger(__name__) in every module with business logic

Never use import logging or logging.getLogger() in application code, except in observability setup modules (observability/setup.py, observability/sinks.py, observability/syslog_handler.py, observability/http_handler.py)

Always use logger as the variable name (not _logger or log)

Use event name constants from domain-specific modules under synthorg.observability.events instead of string literals (e.g., API_REQUEST_STARTED from events.api, TOOL_INVOKE_START from events.tool)

Always use structured logging with kwargs: logger.info(EVENT, key=value) instead of string formatting like logger.info('msg %s', val)

Log at WARNING or ERROR with context before raising on all error paths

Log at INFO for all state transitions

Log at DEBUG for object creation, internal flow, and entry/exit of key functions

Do NOT use from __future__ import annotations - Python 3.14 has PEP 649 native lazy annotations

Use except A, B: (no parentheses) syntax as enforced by ruff for Python 3.14

Add type hints to all public functions and enforce mypy strict mode

Use Google-style docstrings on all public classes and functions (enforced by ruff D rules)

Create new objects instead of mutating existing ones; use copy.deepcopy() at construction and MappingProxyType wrapping for read-only enforcement in non-Pydantic internal collections

Use copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, persistence serialization) for dict/list fields in frozen Pydantic models

Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (with model_copy(update=...)) for runtime state that evolves

Never mix static config fields with mutable runtime fields in one Pydantic model

Use Pydantic v2 with BaseModel, model_validator, computed_field, and ConfigDict

Use ...

Files:

  • src/synthorg/persistence/protocol.py
  • src/synthorg/engine/identity/__init__.py
  • src/synthorg/persistence/sqlite/backend.py
  • src/synthorg/versioning/hashing.py
  • src/synthorg/versioning/__init__.py
  • src/synthorg/engine/review_gate.py
  • src/synthorg/hr/registry.py
  • src/synthorg/versioning/models.py
  • src/synthorg/observability/events/versioning.py
  • src/synthorg/versioning/service.py
  • src/synthorg/engine/identity/diff.py
  • src/synthorg/persistence/version_repo.py
  • src/synthorg/persistence/sqlite/version_repo.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/persistence/protocol.py
  • src/synthorg/engine/identity/__init__.py
  • src/synthorg/persistence/sqlite/backend.py
  • src/synthorg/versioning/hashing.py
  • src/synthorg/versioning/__init__.py
  • src/synthorg/engine/review_gate.py
  • src/synthorg/hr/registry.py
  • src/synthorg/versioning/models.py
  • src/synthorg/observability/events/versioning.py
  • src/synthorg/versioning/service.py
  • src/synthorg/engine/identity/diff.py
  • src/synthorg/persistence/version_repo.py
  • src/synthorg/persistence/sqlite/version_repo.py
tests/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Mark tests with @pytest.mark.unit, @pytest.mark.integration, @pytest.mark.e2e, or @pytest.mark.slow

Maintain 80% minimum test coverage (enforced in CI)

Use @pytest.mark.parametrize for testing similar cases instead of writing multiple test functions

Never skip, dismiss, or ignore flaky tests; always fix them fully and fundamentally by mocking time or using asyncio.Event().wait() for indefinite blocks

Files:

  • tests/unit/persistence/test_protocol.py
  • tests/unit/engine/test_review_gate.py
  • tests/unit/persistence/sqlite/test_migrations.py
  • tests/unit/observability/test_events.py
  • tests/unit/api/fakes_backend.py
  • tests/unit/versioning/test_hashing.py
  • tests/unit/versioning/test_models.py
  • tests/unit/persistence/sqlite/test_identity_version_repo.py
  • tests/unit/versioning/test_service.py
  • tests/integration/engine/identity/test_identity_versioning.py
  • tests/unit/persistence/sqlite/test_version_repo.py
  • tests/unit/engine/identity/test_diff.py

⚙️ CodeRabbit configuration file

Test files do not require Google-style docstrings on classes or functions -- ruff D rules are only enforced on src/. A bare @settings() decorator with no arguments on Hypothesis property tests is a no-op and should not be suggested -- the HYPOTHESIS_PROFILE env var controls example counts via registered profiles, which @given() honors automatically.

Files:

  • tests/unit/persistence/test_protocol.py
  • tests/unit/engine/test_review_gate.py
  • tests/unit/persistence/sqlite/test_migrations.py
  • tests/unit/observability/test_events.py
  • tests/unit/api/fakes_backend.py
  • tests/unit/versioning/test_hashing.py
  • tests/unit/versioning/test_models.py
  • tests/unit/persistence/sqlite/test_identity_version_repo.py
  • tests/unit/versioning/test_service.py
  • tests/integration/engine/identity/test_identity_versioning.py
  • tests/unit/persistence/sqlite/test_version_repo.py
  • tests/unit/engine/identity/test_diff.py
🧠 Learnings (66)
📓 Common learnings
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: Run pytest with `-n 8` for parallel execution locally; never run tests sequentially
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: Use commit format `<type>: <description>` with types: feat, fix, refactor, docs, test, chore, perf, ci (enforced by commitizen)
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: Sign all commits with GPG/SSH on `main` (enforced by branch protection)
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: Use branch naming convention `<type>/<slug>` from main
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: Never use `cd` in Bash commands in this project; use absolute paths or run commands directly. Exception: `bash -c "cd <dir> && <cmd>"` is safe
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: Never use Bash to write or modify files; use the Write or Edit tools instead of `cat >`, `echo >`, `sed -i`, etc.
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: Always read the relevant `docs/design/` page before implementing any feature or planning any issue
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: If implementation deviates from the design spec, alert the user and explain why before proceeding; do NOT silently diverge
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: When a spec topic is referenced, read the relevant `docs/design/` page before coding
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: Update the relevant `docs/design/` page when approved deviations from the spec occur
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: Present every implementation plan to the user for accept/deny before coding starts
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: Actively look for ways to improve the design at every phase; surface improvements as suggestions (not silent changes) for user decision
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: Prioritize issues by dependency order, not priority labels; unblocked dependencies come first
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: See `web/CLAUDE.md` for the full component inventory, design token rules, and post-training references
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: Use `go -C cli` to run CLI commands; never use `cd cli`
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: See `cli/CLAUDE.md` for CLI commands, flags, and reference
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: Never create a PR directly via `gh pr create` (blocked by hookify); always use `/pre-pr-review` or `/pre-pr-review quick`
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: After finishing an issue implementation, create a feature branch (`<type>/<slug>`), commit, and push immediately; do NOT leave work uncommitted on main
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: When review agents find valid issues (including pre-existing issues in surrounding code, suggestions, and findings adjacent to PR changes), fix them all; never skip as 'out of scope'
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: Run `uv sync` to install all deps (dev + test)
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: Use `uv run ruff check src/ tests/` for linting and `uv run ruff check src/ tests/ --fix` for auto-fix
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: Use `uv run ruff format src/ tests/` for code formatting
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: Use `uv run mypy src/ tests/` for strict type checking
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: Run pytest with markers: `uv run python -m pytest tests/ -m unit -n 8` (unit), `-m integration -n 8` (integration), `-m e2e -n 8` (e2e)
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: Run full test suite with coverage: `uv run python -m pytest tests/ -n 8 --cov=synthorg --cov-fail-under=80`
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: Run property tests with dev profile: `HYPOTHESIS_PROFILE=dev uv run python -m pytest tests/ -m unit -n 8 -k properties` (1000 examples)
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: Run deep fuzzing: `HYPOTHESIS_PROFILE=fuzz uv run python -m pytest tests/ -m unit -n 8 --timeout=0` (10,000 examples, no deadline)
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: Run `uv run pre-commit run --all-files` for all pre-commit hooks
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: Run `uv run python scripts/export_openapi.py` before building docs
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: Run `uv run python scripts/generate_comparison.py` before building docs
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: Use `uv run zensical build` to build docs (output: _site/docs/)
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T19:53:57.147Z
Learning: Use `uv run zensical serve` for local docs preview (http://127.0.0.1:8000)
📚 Learning: 2026-03-16T06:24:56.341Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T06:24:56.341Z
Learning: Applies to src/synthorg/**/*.py : Always use event name constants from the domain-specific module under `synthorg.observability.events` in logging calls

Applied to files:

  • CLAUDE.md
  • tests/unit/observability/test_events.py
  • src/synthorg/observability/events/versioning.py
📚 Learning: 2026-03-31T16:09:24.320Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T16:09:24.320Z
Learning: Applies to src/synthorg/**/*.py : Use event name constants from `synthorg.observability.events.<domain>` modules (e.g., `API_REQUEST_STARTED` from `events.api`, `TOOL_INVOKE_START` from `events.tool`); import directly and use in structured logging

Applied to files:

  • CLAUDE.md
  • src/synthorg/observability/events/versioning.py
📚 Learning: 2026-04-02T07:18:02.381Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T07:18:02.381Z
Learning: Applies to src/synthorg/**/*.py : 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 the domain module

Applied to files:

  • CLAUDE.md
  • tests/unit/observability/test_events.py
  • src/synthorg/observability/events/versioning.py
📚 Learning: 2026-03-20T11:18:48.128Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T11:18:48.128Z
Learning: Applies to src/synthorg/**/*.py : 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`.

Applied to files:

  • CLAUDE.md
  • tests/unit/observability/test_events.py
  • src/synthorg/observability/events/versioning.py
📚 Learning: 2026-03-18T21:23:23.586Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-18T21:23:23.586Z
Learning: Applies to src/synthorg/**/*.py : Event names: always use constants from the domain-specific module 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>.

Applied to files:

  • CLAUDE.md
  • tests/unit/observability/test_events.py
  • src/synthorg/observability/events/versioning.py
📚 Learning: 2026-03-15T18:38:44.202Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:38:44.202Z
Learning: Applies to src/synthorg/**/*.py : Always use event name constants from domain-specific modules under `synthorg.observability.events` (e.g., `PROVIDER_CALL_START` from `events.provider`); import directly: `from synthorg.observability.events.<domain> import EVENT_CONSTANT`

Applied to files:

  • CLAUDE.md
  • tests/unit/observability/test_events.py
  • src/synthorg/observability/events/versioning.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : Always 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 rather than using string literals

Applied to files:

  • CLAUDE.md
  • tests/unit/observability/test_events.py
  • src/synthorg/observability/events/versioning.py
📚 Learning: 2026-03-15T18:28:13.207Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:28:13.207Z
Learning: Applies to src/synthorg/**/*.py : Event names: always use constants from domain-specific modules under synthorg.observability.events (e.g., PROVIDER_CALL_START from events.provider, BUDGET_RECORD_ADDED from events.budget, etc.). Import directly: `from synthorg.observability.events.<domain> import EVENT_CONSTANT`.

Applied to files:

  • CLAUDE.md
  • tests/unit/observability/test_events.py
  • src/synthorg/observability/events/versioning.py
📚 Learning: 2026-04-06T16:35:12.934Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-06T16:35:12.934Z
Learning: All project conventions, commands, and standards are defined in CLAUDE.md - refer to it for project structure, package layout, code conventions, quick commands, git workflow, testing standards, design specifications, logging, resilience, and security patterns

Applied to files:

  • CLAUDE.md
📚 Learning: 2026-04-07T16:48:04.262Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T16:48:04.262Z
Learning: Applies to src/**/*.py : Always use event name constants from domain-specific modules under `synthorg.observability.events` (e.g., `API_REQUEST_STARTED` from `events.api`). Import directly: `from synthorg.observability.events.<domain> import EVENT_CONSTANT`

Applied to files:

  • CLAUDE.md
  • tests/unit/observability/test_events.py
  • src/synthorg/observability/events/versioning.py
📚 Learning: 2026-03-17T06:43:14.114Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:43:14.114Z
Learning: Applies to 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. Variable name: always `logger`.

Applied to files:

  • CLAUDE.md
📚 Learning: 2026-03-15T18:28:13.207Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:28:13.207Z
Learning: Applies to 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. Variable name: always `logger` (not `_logger`, not `log`).

Applied to files:

  • CLAUDE.md
📚 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 : 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.

Applied to files:

  • CLAUDE.md
📚 Learning: 2026-03-17T06:43:14.114Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:43:14.114Z
Learning: Applies to src/synthorg/**/*.py : All error paths must log at WARNING or ERROR with context before raising. All state transitions must log at INFO. DEBUG for object creation, internal flow, entry/exit of key functions. Pure data models, enums, and re-exports do NOT need logging.

Applied to files:

  • CLAUDE.md
📚 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 : All error paths must log at WARNING or ERROR with context before raising. All state transitions must log at INFO. DEBUG for object creation, internal flow, entry/exit of key functions.

Applied to files:

  • CLAUDE.md
📚 Learning: 2026-03-20T11:18:48.128Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T11:18:48.128Z
Learning: Applies to src/synthorg/**/*.py : Every module with business logic MUST have `from synthorg.observability import get_logger` followed by `logger = get_logger(__name__)`.

Applied to files:

  • CLAUDE.md
📚 Learning: 2026-04-02T12:07:44.443Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T12:07:44.443Z
Learning: Applies to src/synthorg/**/*.py : Always use structured logging: `logger.info(EVENT, key=value)` — never `logger.info('msg %s', val)`

Applied to files:

  • CLAUDE.md
📚 Learning: 2026-03-16T07:22:28.134Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T07:22:28.134Z
Learning: Applies to src/synthorg/**/*.py : All error paths must log at WARNING or ERROR with context before raising. All state transitions must log at INFO. DEBUG for object creation, internal flow, and key function entry/exit

Applied to files:

  • CLAUDE.md
📚 Learning: 2026-03-31T20:07:03.035Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T20:07:03.035Z
Learning: Applies to src/synthorg/**/*.py : All error paths must log at WARNING or ERROR with context before raising; all state transitions must log at INFO; DEBUG for object creation, internal flow, entry/exit of key functions

Applied to files:

  • CLAUDE.md
📚 Learning: 2026-03-19T11:33:01.580Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T11:33:01.580Z
Learning: Applies to src/synthorg/**/*.py : Use event constants from `synthorg.observability.events.<domain>` (e.g., `API_REQUEST_STARTED` from `events.api`); import directly and log with structured kwargs: `logger.info(EVENT, key=value)`, never interpolated strings

Applied to files:

  • CLAUDE.md
📚 Learning: 2026-03-16T06:24:56.341Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T06:24:56.341Z
Learning: Applies to src/synthorg/memory/**/*.py : Use MemoryBackend protocol with pluggable backends (Mem0 adapter available at backends/mem0/) for persistent agent memory

Applied to files:

  • src/synthorg/persistence/protocol.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/persistence/**/*.py : Persistence uses pluggable PersistenceBackend protocol. SQLite is the initial backend. Settings use SettingsRepository (namespaced settings CRUD).

Applied to files:

  • src/synthorg/persistence/protocol.py
  • src/synthorg/persistence/sqlite/backend.py
  • tests/unit/persistence/sqlite/test_version_repo.py
  • src/synthorg/persistence/version_repo.py
  • src/synthorg/persistence/sqlite/version_repo.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: Persistence backend: pluggable PersistenceBackend protocol in `src/synthorg/persistence/`, SQLite initial, SettingsRepository (namespaced settings CRUD).

Applied to files:

  • src/synthorg/persistence/protocol.py
  • src/synthorg/persistence/sqlite/backend.py
  • src/synthorg/persistence/version_repo.py
  • src/synthorg/persistence/sqlite/version_repo.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 src/synthorg/hr/**/*.py : HR package (hr/): hiring, firing, onboarding, offboarding, agent registry, performance tracking (task metrics, collaboration scoring, LLM calibration, collaboration overrides, trend detection), promotion/demotion (criteria evaluation, approval strategies, model mapping)

Applied to files:

  • src/synthorg/persistence/protocol.py
📚 Learning: 2026-04-02T07:18:02.381Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T07:18:02.381Z
Learning: Applies to {pyproject.toml,src/synthorg/__init__.py} : Update version in `pyproject.toml` (`[tool.commitizen].version`) and `src/synthorg/__init__.py` (`__version__`)

Applied to files:

  • src/synthorg/engine/identity/__init__.py
  • src/synthorg/versioning/hashing.py
  • src/synthorg/versioning/__init__.py
  • src/synthorg/versioning/models.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/engine/identity/__init__.py
  • docs/reference/claude-reference.md
  • src/synthorg/versioning/__init__.py
📚 Learning: 2026-04-07T16:48:04.262Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T16:48:04.262Z
Learning: See [docs/reference/claude-reference.md](docs/reference/claude-reference.md) for Documentation layout, Docker commands, Package Structure, Releasing, CI pipelines, Dependencies.

Applied to files:

  • docs/reference/claude-reference.md
📚 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/memory/**/*.py : Memory package (memory/): pluggable MemoryBackend protocol, backends/ (Mem0 adapter), retrieval pipeline (ranking, RRF fusion, injection, formatting, non-inferable filtering), shared org memory (org/), consolidation/archival (density-aware: DensityClassifier, AbstractiveSummarizer, ExtractivePreserver, DualModeConsolidationStrategy)

Applied to files:

  • docs/reference/claude-reference.md
📚 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: Engine: Agent orchestration, execution loops, parallel execution, task decomposition, routing, task assignment, centralized single-writer task state engine (TaskEngine), task lifecycle, recovery, shutdown, workspace isolation, coordination (multi-agent pipeline: TopologyDispatcher protocol, 4 dispatchers — SAS/centralized/decentralized/context-dependent, wave execution, workspace lifecycle integration, CoordinationSectionConfig company config bridge, build_coordinator factory), coordination error classification, prompt policy validation, checkpoint recovery (checkpoint/, per-turn persistence, heartbeat detection, CheckpointRecoveryStrategy), approval gate (escalation detection, context parking/resume, EscalationInfo/ResumePayload models), stagnation detection (stagnation/, StagnationDetector protocol, ToolRepetitionDetector, dual-signal analysis, corrective prompt injection), agent runtime state (AgentRuntimeState, lightweight per-agent execution status for dashboard queries and recove...

Applied to files:

  • docs/reference/claude-reference.md
  • docs/design/engine.md
📚 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 src/synthorg/providers/**/*.py : Providers: LLM provider abstraction (LiteLLM adapter), auth types (api_key/oauth/custom_header/none), presets (PROVIDER_PRESETS), runtime CRUD (ProviderManagementService with asyncio.Lock serialization), hot-reload via AppState swap.

Applied to files:

  • docs/reference/claude-reference.md
📚 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: Settings: Runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge, ConfigResolver (typed composed reads for controllers), validation, registry, change notifications via message bus. Per-namespace setting definitions in definitions/ submodule (api, company, providers, memory, budget, security, coordination, observability, backup).

Applied to files:

  • docs/reference/claude-reference.md
📚 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: Security: SecOps agent, rule engine (soft-allow/hard-deny, fail-closed), audit log, output scanner, output scan response policies (redact/withhold/log-only/autonomy-tiered), risk classifier, risk tier classifier, action type registry, ToolInvoker security integration, progressive trust (4 strategies: disabled/weighted/per-category/milestone), autonomy levels (presets, resolver, change strategy), timeout policies (park/resume).

Applied to files:

  • docs/reference/claude-reference.md
  • docs/design/engine.md
📚 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: Documentation source in `docs/` (Markdown, built with Zensical). Design spec in `docs/design/` (7 pages: index, agents, organization, communication, engine, memory, operations). Architecture in `docs/architecture/` (overview, tech-stack, decision log). Roadmap in `docs/roadmap/`. Security in `docs/security.md`. Licensing in `docs/licensing.md`. Reference in `docs/reference/`. REST API reference in `docs/rest-api.md`. Library reference in `docs/api/` (auto-generated from docstrings). Custom templates in `docs/overrides/`. Config in `mkdocs.yml`.

Applied to files:

  • docs/reference/claude-reference.md
📚 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/engine/**/*.py : Engine package (engine/): agent orchestration, parallel execution, task decomposition, routing, TaskEngine (centralized single-writer), task lifecycle/recovery/shutdown, workspace isolation, coordination (4 dispatchers: SAS/centralized/decentralized/context-dependent, wave execution), approval gates (escalation detection, context parking/resume), stagnation detection (ToolRepetitionDetector, corrective prompt injection), AgentRuntimeState (execution status), context budget management, conversation compaction (oldest-turns summarizer)

Applied to files:

  • docs/reference/claude-reference.md
📚 Learning: 2026-03-14T15:43:05.601Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T15:43:05.601Z
Learning: Applies to docs/** : Docs source in docs/ (Markdown, built with Zensical); design spec in docs/design/ (7 pages: index, agents, organization, communication, engine, memory, operations)

Applied to files:

  • docs/reference/claude-reference.md
  • docs/design/agents.md
📚 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/observability/**/*.py : Observability package (observability/): structured logging, correlation tracking, log sinks; event constants organized by domain under observability/events/ (e.g., events.api, events.tool, events.git, events.context_budget, events.backup)

Applied to files:

  • docs/reference/claude-reference.md
  • src/synthorg/observability/events/versioning.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 event name constants from synthorg.observability.events domain-specific modules (e.g., PROVIDER_CALL_START from events.provider). Import directly: from synthorg.observability.events.<domain> import EVENT_CONSTANT.

Applied to files:

  • tests/unit/observability/test_events.py
  • src/synthorg/hr/registry.py
  • src/synthorg/observability/events/versioning.py
📚 Learning: 2026-03-14T16:18:57.267Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T16:18:57.267Z
Learning: Applies to src/ai_company/!(observability)/**/*.py : Use event name constants from domain-specific modules under `ai_company.observability.events` (e.g., `PROVIDER_CALL_START` from `events.provider`). Import directly: `from ai_company.observability.events.<domain> import EVENT_CONSTANT`.

Applied to files:

  • tests/unit/observability/test_events.py
  • src/synthorg/observability/events/versioning.py
📚 Learning: 2026-03-15T18:28:13.207Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:28:13.207Z
Learning: Applies to {src,tests,web,cli,site}/**/*.{py,ts,tsx,go,astro} : Vendor-agnostic everywhere: 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. Vendor names may only appear in: (1) Operations design page provider list (docs/design/operations.md), (2) .claude/ skill/agent files, (3) third-party import paths/module names.

Applied to files:

  • tests/unit/observability/test_events.py
📚 Learning: 2026-04-01T15:36:39.993Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T15:36:39.993Z
Learning: Applies to {src,tests}/**/*.py : Vendor-agnostic everywhere: 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

Applied to files:

  • tests/unit/observability/test_events.py
📚 Learning: 2026-04-07T16:48:04.262Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T16:48:04.262Z
Learning: Applies to src/**/*.py : 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.

Applied to files:

  • tests/unit/observability/test_events.py
📚 Learning: 2026-04-01T15:36:39.993Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T15:36:39.993Z
Learning: Applies to {src/synthorg/**/*.py,tests/**/*.py,web/src/**/*.{ts,tsx}} : 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`

Applied to files:

  • tests/unit/observability/test_events.py
📚 Learning: 2026-04-01T15:36:39.993Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T15:36:39.993Z
Learning: Applies to tests/**/*.py : Tests must use `test-provider`, `test-small-001`, etc. instead of real vendor names

Applied to files:

  • tests/unit/observability/test_events.py
📚 Learning: 2026-04-02T11:17:08.039Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T11:17:08.039Z
Learning: Applies to {pyproject.toml,src/synthorg/__init__.py} : Version locations: `pyproject.toml` (`[tool.commitizen].version`), `src/synthorg/__init__.py` (`__version__`)

Applied to files:

  • src/synthorg/versioning/__init__.py
📚 Learning: 2026-03-21T12:54:22.557Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-21T12:54:22.557Z
Learning: Version bumping (pre-1.0): `fix:` = patch, `feat:` = patch, `feat!:` = minor, `BREAKING CHANGE` trailer = minor. Update version in `pyproject.toml` (`[tool.commitizen].version`) and `src/synthorg/__init__.py` (`__version__`)

Applied to files:

  • src/synthorg/versioning/__init__.py
  • src/synthorg/versioning/models.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (via `model_copy(update=...)`) for runtime state that evolves

Applied to files:

  • src/synthorg/versioning/__init__.py
  • src/synthorg/versioning/models.py
  • tests/unit/engine/identity/test_diff.py
  • src/synthorg/engine/identity/diff.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/api/**/*.py : Authentication uses JWT + API key. Approval gate integration for high-risk operations.

Applied to files:

  • src/synthorg/engine/review_gate.py
📚 Learning: 2026-03-16T06:24:56.341Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T06:24:56.341Z
Learning: Applies to docs/design/**/*.md : Design specification pages in `docs/design/` must be consulted before implementing features (7 pages: index, agents, organization, communication, engine, memory, operations)

Applied to files:

  • docs/design/engine.md
  • docs/design/agents.md
📚 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: Always read the relevant `docs/design/` page before implementing any feature or planning any issue — DESIGN_SPEC.md is a pointer file linking to 7 design pages (Agents, Organization, Communication, Engine, Memory, Operations)

Applied to files:

  • docs/design/engine.md
  • docs/design/agents.md
📚 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 docs/design/*.md : Design spec pages: 7 pages in `docs/design/` — index, agents, organization, communication, engine, memory, operations

Applied to files:

  • docs/design/engine.md
  • docs/design/agents.md
📚 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 docs/design/*.md : Update the relevant `docs/design/` page when approved deviations occur to reflect the new reality

Applied to files:

  • docs/design/engine.md
  • docs/design/agents.md
📚 Learning: 2026-03-15T18:38:44.202Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:38:44.202Z
Learning: Always read the relevant `docs/design/` page before implementing any feature or planning any issue. DESIGN_SPEC.md is a pointer file linking to the 7 design pages (index, agents, organization, communication, engine, memory, operations).

Applied to files:

  • docs/design/engine.md
  • docs/design/agents.md
📚 Learning: 2026-04-07T16:48:04.262Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T16:48:04.262Z
Learning: Applies to docs/design/*.md : When approved deviations occur, update the relevant `docs/design/` page to reflect the new reality.

Applied to files:

  • docs/design/engine.md
📚 Learning: 2026-04-07T16:48:04.262Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T16:48:04.262Z
Learning: NEVER use Bash to write or modify files. Use Write or Edit tools instead. Do not use `cat >`, `cat << EOF`, `echo >`, `echo >>`, `sed -i`, `python -c "open(...).write(...)"`, or `tee`. Read-only uses like piping to stdout are fine.

Applied to files:

  • scripts/check_bash_no_write.sh
📚 Learning: 2026-03-15T18:38:44.202Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:38:44.202Z
Learning: Applies to src/synthorg/**/*.py : Use frozen Pydantic models for config/identity; separate mutable-via-copy models (using `model_copy(update=...)`) for runtime state

Applied to files:

  • src/synthorg/versioning/models.py
  • src/synthorg/engine/identity/diff.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 frozen Pydantic models for config/identity; use separate mutable-via-copy models (using model_copy(update=...)) for runtime state that evolves. Never mix static config fields with mutable runtime fields in one model.

Applied to files:

  • src/synthorg/versioning/models.py
  • tests/unit/engine/identity/test_diff.py
  • src/synthorg/engine/identity/diff.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/versioning/models.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/versioning/models.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/versioning/models.py
  • tests/unit/engine/identity/test_diff.py
📚 Learning: 2026-04-07T16:48:04.262Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T16:48:04.262Z
Learning: Applies to src/**/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (using `model_copy(update=...)`) for runtime state that evolves. Never mix static config fields with mutable runtime fields in one model.

Applied to files:

  • src/synthorg/versioning/models.py
  • tests/unit/engine/identity/test_diff.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 : 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).

Applied to files:

  • src/synthorg/versioning/models.py
📚 Learning: 2026-03-14T15:43:05.601Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T15:43:05.601Z
Learning: Applies to src/**/*.py : Use event name constants from domain-specific modules under ai_company.observability.events (e.g., PROVIDER_CALL_START from events.provider, BUDGET_RECORD_ADDED from events.budget, etc.) — import directly

Applied to files:

  • src/synthorg/observability/events/versioning.py
📚 Learning: 2026-03-15T18:28:13.207Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:28:13.207Z
Learning: Property-based testing: Python uses Hypothesis (given + settings). Hypothesis profiles: ci (200 examples, default) and dev (1000 examples), controlled via HYPOTHESIS_PROFILE env var. Run dev profile: HYPOTHESIS_PROFILE=dev uv run python -m pytest tests/ -m unit -n auto -k properties.

Applied to files:

  • tests/unit/versioning/test_service.py
📚 Learning: 2026-03-17T11:41:02.964Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T11:41:02.964Z
Learning: Applies to src/**/*.py : Models: Pydantic v2 (`BaseModel`, `model_validator`, `computed_field`, `ConfigDict`). Use `computed_field` for derived values instead of storing + validating redundant fields. Use `NotBlankStr` for all identifier/name fields — including optional (`NotBlankStr | None`) and tuple (`tuple[NotBlankStr, ...]`) variants — instead of manual whitespace validators.

Applied to files:

  • tests/unit/engine/identity/test_diff.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : Use `copy.deepcopy()` at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, persistence serialization) for `dict`/`list` fields

Applied to files:

  • src/synthorg/engine/identity/diff.py
🪛 markdownlint-cli2 (0.22.0)
docs/design/agents.md

[warning] 171-171: No space after hash on atx style heading

(MD018, no-missing-space-atx)


[warning] 224-224: Code block style
Expected: indented; Actual: fenced

(MD046, code-block-style)

Comment on lines +1 to +5
"""Integration test: AgentIdentity round-trip through SQLiteVersionRepository.

Validates that the serialize/deserialize callables used in the backend
correctly preserve an ``AgentIdentity`` through a DB round-trip.
"""
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

Marker/docstring inconsistency: tests marked @pytest.mark.unit but docstring says "Integration test".

The module docstring describes this as an "Integration test" (line 1), but all test methods use @pytest.mark.unit (e.g., line 90). Since these tests use a real in-memory SQLite database with the actual SQLiteVersionRepository implementation, they behave more like integration tests.

Consider either:

  1. Changing the markers to @pytest.mark.integration, or
  2. Updating the docstring to "Unit test" if the in-memory DB is considered acceptable for unit tests in this project
📝 Proposed fix (Option 1: change to integration)
-"""Integration test: AgentIdentity round-trip through SQLiteVersionRepository.
+"""Unit test: AgentIdentity round-trip through SQLiteVersionRepository.

Or change all test markers:

-    `@pytest.mark.unit`
+    `@pytest.mark.integration`
     async def test_roundtrip_preserves_name(

Also applies to: 90-90

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

In `@tests/unit/persistence/sqlite/test_identity_version_repo.py` around lines 1 -
5, The module docstring says "Integration test" but the tests are marked with
`@pytest.mark.unit`; make these consistent by converting the markers and docstring
to integration: update the top-of-file docstring in
tests/unit/persistence/sqlite/test_identity_version_repo.py to "Integration
test" (or remove the mismatch wording) and replace all occurrences of
`@pytest.mark.unit` in this file with `@pytest.mark.integration` so the declared
marker matches the in-memory SQLite integration nature of the tests (search for
the module docstring and all `@pytest.mark.unit` annotations to change).

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 (1)
tests/unit/engine/identity/test_diff.py (1)

163-173: 🧹 Nitpick | 🔵 Trivial

Duplicate test case: test_added_change_new_value_present is covered by parametrized matrix.

This test duplicates the ("added", None, '"value"', None) case at line 232 in test_change_invariant_matrix. The parametrized approach consolidates all invariant checks, making this standalone test redundant.

🗑️ Remove duplicate test
 class TestIdentityFieldChangeModel:
     """IdentityFieldChange validation."""

-    `@pytest.mark.unit`
-    def test_added_change_new_value_present(self) -> None:
-        c = IdentityFieldChange(
-            field_path="settings.timeout",
-            change_type="added",
-            old_value=None,
-            new_value='"30"',
-        )
-        assert c.change_type == "added"
-        assert c.old_value is None
-
     `@pytest.mark.unit`
     def test_frozen(self) -> None:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/engine/identity/test_diff.py` around lines 163 - 173, The test
test_added_change_new_value_present duplicates a case already covered in the
parametrized test_change_invariant_matrix (the ("added", None, '"value"', None)
scenario); remove the redundant standalone test function
test_added_change_new_value_present from tests/unit/engine/identity/test_diff.py
so the invariant matrix (test_change_invariant_matrix) remains the single source
of truth for IdentityFieldChange change_type/old_value/new_value assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/check_bash_no_write.sh`:
- Around line 48-53: Revert the quote-stripping bypass: stop using the temporary
COMMAND_FOR_REDIR and run the redirect detection against the original COMMAND so
quoted payloads are not hidden; update places that build REDIR and run the grep
deny-rules to read COMMAND directly (instead of COMMAND_FOR_REDIR). Also ensure
REDIR is normalized/truncated of leading whitespace before comparing to
"/dev/null" (trim leading spaces) so exemptions like "> /dev/null" match
correctly; update the REDIR extraction logic and the subsequent deny-rule checks
to use the normalized REDIR and COMMAND variables.

In `@src/synthorg/persistence/sqlite/schema.sql`:
- Line 600: The idx_sv_hostname index declaration for ssrf_violations is located
after the agent_identity_versions block; move the CREATE INDEX IF NOT EXISTS
idx_sv_hostname ON ssrf_violations(hostname, port); statement so it sits
immediately after the ssrf_violations table definition (i.e., directly after the
ssrf_violations block and before the blank line that precedes
agent_identity_versions) to keep indexes adjacent to their table.

In `@tests/unit/persistence/sqlite/test_identity_version_repo.py`:
- Around line 20-30: The test schema defined in _SCHEMA for table
agent_identity_versions is missing the UTC CHECK constraint on the saved_at
column; update the saved_at definition inside _SCHEMA to match production (add
the CHECK that saved_at LIKE '%+00:00' OR saved_at LIKE '%Z') so tests will
reject naive/non-UTC timestamps and exercise the same validation as schema.sql.

In `@tests/unit/persistence/sqlite/test_version_repo.py`:
- Around line 21-31: The test table definition in _CREATE_TABLE for
test_versions is missing the UTC format CHECK on the saved_at column; update the
_CREATE_TABLE string (for table test_versions) to include the same UTC saved_at
CHECK constraint used in production/schema.sql and in
test_identity_version_repo.py so the test schema matches production (apply the
identical CHECK expression for saved_at to validate UTC timestamps).

---

Duplicate comments:
In `@tests/unit/engine/identity/test_diff.py`:
- Around line 163-173: The test test_added_change_new_value_present duplicates a
case already covered in the parametrized test_change_invariant_matrix (the
("added", None, '"value"', None) scenario); remove the redundant standalone test
function test_added_change_new_value_present from
tests/unit/engine/identity/test_diff.py so the invariant matrix
(test_change_invariant_matrix) remains the single source of truth for
IdentityFieldChange change_type/old_value/new_value assertions.
🪄 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: 6142a68d-3b51-4522-84e0-b9b232fad3aa

📥 Commits

Reviewing files that changed from the base of the PR and between 6c0190c and b9f779c.

📒 Files selected for processing (15)
  • docs/design/agents.md
  • docs/reference/claude-reference.md
  • scripts/check_bash_no_write.sh
  • src/synthorg/engine/identity/diff.py
  • src/synthorg/engine/review_gate.py
  • src/synthorg/hr/registry.py
  • src/synthorg/persistence/sqlite/schema.sql
  • src/synthorg/persistence/sqlite/version_repo.py
  • src/synthorg/versioning/service.py
  • tests/conftest.py
  • tests/unit/engine/identity/test_diff.py
  • tests/unit/observability/test_events.py
  • tests/unit/persistence/sqlite/test_identity_version_repo.py
  • tests/unit/persistence/sqlite/test_version_repo.py
  • tests/unit/versioning/test_service.py
📜 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: Build Backend
  • GitHub Check: Build Web
  • GitHub Check: Test (Python 3.14)
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Do NOT use from __future__ import annotations -- Python 3.14 has PEP 649 native lazy annotations

Use except A, B: syntax without parentheses (PEP 758) -- ruff enforces this on Python 3.14

Files:

  • tests/conftest.py
  • tests/unit/observability/test_events.py
  • src/synthorg/hr/registry.py
  • tests/unit/versioning/test_service.py
  • tests/unit/persistence/sqlite/test_identity_version_repo.py
  • src/synthorg/versioning/service.py
  • src/synthorg/engine/identity/diff.py
  • tests/unit/persistence/sqlite/test_version_repo.py
  • tests/unit/engine/identity/test_diff.py
  • src/synthorg/persistence/sqlite/version_repo.py
  • src/synthorg/engine/review_gate.py
tests/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Use pytest markers: @pytest.mark.unit, @pytest.mark.integration, @pytest.mark.e2e, @pytest.mark.slow to categorize tests

Minimum test coverage is 80% (enforced in CI)

Use asyncio_mode = "auto" in pytest config -- no manual @pytest.mark.asyncio needed on test functions

Global test timeout is 30 seconds per test (do not add per-file pytest.mark.timeout(30) markers; only use non-default overrides like timeout(60) when necessary)

ALWAYS run tests with parallelism: use -n 8 flag with pytest (never run tests sequentially). CI uses -n auto

Prefer @pytest.mark.parametrize for testing similar cases instead of writing separate test functions

Use Hypothesis for property-based testing with @given + @settings decorators. Profiles: ci (10 examples, deterministic, derandomize=True), dev (1000 examples), fuzz (10,000 examples, no deadline), extreme (500,000 examples). Controlled via HYPOTHESIS_PROFILE env var

When Hypothesis finds a failing example, fix the underlying bug and add an @example(...) decorator to permanently cover the case in CI -- never just rerun and move on

NEVER skip, dismiss, or ignore flaky tests -- always fix them fully and fundamentally. Mock time.monotonic() and asyncio.sleep() to make timing-sensitive tests deterministic. Use asyncio.Event().wait() for tasks that must block indefinitely instead of asyncio.sleep(large_number)

Files:

  • tests/conftest.py
  • tests/unit/observability/test_events.py
  • tests/unit/versioning/test_service.py
  • tests/unit/persistence/sqlite/test_identity_version_repo.py
  • tests/unit/persistence/sqlite/test_version_repo.py
  • tests/unit/engine/identity/test_diff.py

⚙️ CodeRabbit configuration file

Test files do not require Google-style docstrings on classes or functions -- ruff D rules are only enforced on src/. A bare @settings() decorator with no arguments on Hypothesis property tests is a no-op and should not be suggested -- the HYPOTHESIS_PROFILE env var controls example counts via registered profiles, which @given() honors automatically.

Files:

  • tests/conftest.py
  • tests/unit/observability/test_events.py
  • tests/unit/versioning/test_service.py
  • tests/unit/persistence/sqlite/test_identity_version_repo.py
  • tests/unit/persistence/sqlite/test_version_repo.py
  • tests/unit/engine/identity/test_diff.py
src/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

All public functions and classes must have type hints and comply with mypy strict mode

Use Google-style docstrings on all public classes and functions -- enforced by ruff D rules

Immutability: create new objects, never mutate existing ones. For non-Pydantic internal collections, use copy.deepcopy() at construction + MappingProxyType wrapping for read-only enforcement

Use frozen Pydantic models for config/identity; use separate mutable-via-copy models with model_copy(update=...) for runtime state that evolves. Never mix static config fields with mutable runtime fields in one model

Use Pydantic v2 with model_validator, computed_field, and ConfigDict. Always use allow_inf_nan=False in ConfigDict to reject NaN/Inf in numeric fields at validation time

Use @computed_field for derived values instead of storing + validating redundant fields (e.g., TokenUsage.total_tokens)

Use NotBlankStr from core.types for all identifier/name fields (including optional and tuple variants) instead of manual whitespace validators

Prefer asyncio.TaskGroup for fan-out/fan-in parallel operations in new code (multiple tool invocations, parallel agent calls). Prefer structured concurrency over bare create_task

Line length must be 88 characters (enforced by ruff)

Functions must be < 50 lines; files < 800 lines

Handle errors explicitly, never silently swallow exceptions

Validate at system boundaries (user input, external APIs, config files) -- never assume input is valid

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

All logger calls must use structured kwargs: logger.info(EVENT, key=value) -- never use logger.info("msg %s", val) string formatting

All error paths must log at WARNING or ERROR with context before raising

All state transitions must be ...

Files:

  • src/synthorg/hr/registry.py
  • src/synthorg/versioning/service.py
  • src/synthorg/engine/identity/diff.py
  • src/synthorg/persistence/sqlite/version_repo.py
  • src/synthorg/engine/review_gate.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/hr/registry.py
  • src/synthorg/versioning/service.py
  • src/synthorg/engine/identity/diff.py
  • src/synthorg/persistence/sqlite/version_repo.py
  • src/synthorg/engine/review_gate.py
src/synthorg/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Every module with business logic must have from synthorg.observability import get_logger and logger = get_logger(__name__) -- never use import logging or print() in application code

Files:

  • src/synthorg/hr/registry.py
  • src/synthorg/versioning/service.py
  • src/synthorg/engine/identity/diff.py
  • src/synthorg/persistence/sqlite/version_repo.py
  • src/synthorg/engine/review_gate.py
**/*.sh

📄 CodeRabbit inference engine (CLAUDE.md)

NEVER use cd in Bash commands -- working directory is already set to project root. Use absolute paths or run commands directly. Exception: bash -c "cd <dir> && <cmd>" is safe (child process). Use this for tools without a -C flag

NEVER use Bash to write or modify files -- use the Write or Edit tools. Do not use cat>, cat <<EOF, echo>, echo >>, sed -i, python -c, or tee to create/modify files (read-only/inspection uses are fine)

Use go -C cli instead of cd cli when running Go CLI commands

Files:

  • scripts/check_bash_no_write.sh
🧠 Learnings (70)
📓 Common learnings
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T20:45:51.572Z
Learning: Commit message format: `<type>: <description>` where type is one of: feat, fix, refactor, docs, test, chore, perf, ci. Enforced by commitizen commit-msg hook
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T20:45:51.572Z
Learning: Signed commits are required on main via branch protection -- all commits must be GPG/SSH signed
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T20:45:51.572Z
Learning: Use branch naming convention: `<type>/<slug>` from main (e.g., feat/add-agent-pool, fix/retry-logic)
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T20:45:51.572Z
Learning: ALWAYS read the relevant `docs/design/` page before implementing any feature or planning any issue. DESIGN_SPEC.md is a pointer file linking to 12 design pages
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T20:45:51.572Z
Learning: If implementation deviates from the design spec, alert the user and explain why -- user decides whether to proceed or update the spec. Never silently diverge from the spec
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T20:45:51.572Z
Learning: When spec topics are referenced (e.g., 'the Agents page' or 'the Engine page's Crash Recovery section'), read the relevant docs/design/ page before coding
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T20:45:51.572Z
Learning: When approved deviations occur, update the relevant docs/design/ page to reflect the new reality
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T20:45:51.572Z
Learning: Every implementation plan must be presented to the user for accept/deny before coding starts
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T20:45:51.572Z
Learning: At every phase of planning and implementation, be critical and look for ways to improve the design in spirit of robustness, correctness, simplicity, future-proofing. Surface improvements as suggestions, not silent changes -- user decides
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T20:45:51.572Z
Learning: Prioritize issues by dependency order, not priority labels -- unblocked dependencies come first
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T20:45:51.572Z
Learning: Use `uv sync` to install all dependencies (dev + test), `uv sync --group docs` for docs toolchain. Use `uv run` to execute commands (e.g., `uv run pytest`, `uv run ruff check`)
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T20:45:51.572Z
Learning: Run linting with: `uv run ruff check src/ tests/` for check, `uv run ruff check src/ tests/ --fix` for auto-fix, `uv run ruff format src/ tests/` for formatting
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T20:45:51.572Z
Learning: Run type-checking with: `uv run mypy src/ tests/` (strict mode)
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T20:45:51.572Z
Learning: Run pytest with markers for test categorization: `uv run python -m pytest tests/ -m unit -n 8` (unit), `uv run python -m pytest tests/ -m integration -n 8` (integration), `uv run python -m pytest tests/ -m e2e -n 8` (e2e). Full suite: `uv run python -m pytest tests/ -n 8 --cov=synthorg --cov-fail-under=80`
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T20:45:51.572Z
Learning: Run property tests with: `HYPOTHESIS_PROFILE=dev uv run python -m pytest tests/ -m unit -n 8 -k properties` (quick) or `HYPOTHESIS_PROFILE=fuzz uv run python -m pytest tests/ -m unit -n 8 --timeout=0` (deep fuzzing, 10k examples)
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T20:45:51.572Z
Learning: Run pre-commit hooks with: `uv run pre-commit run --all-files`
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T20:45:51.572Z
Learning: Build and serve documentation with: `uv run zensical build` (output: _site/docs/) or `uv run zensical serve` (local preview). Export OpenAPI schema before docs build: `uv run python scripts/export_openapi.py`
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T20:45:51.572Z
Learning: After finishing an issue implementation, create a feature branch (`<type>/<slug>`), commit, and push -- do NOT create a PR automatically
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T20:45:51.572Z
Learning: Do NOT create a PR directly with `gh pr create` (blocked by hookify). ALWAYS use `/pre-pr-review` to create PRs -- it runs automated checks + review agents + fixes before creating the PR. For trivial/docs-only changes: `/pre-pr-review quick` skips agents but runs automated checks
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T20:45:51.572Z
Learning: After the PR exists, use `/aurelio-review-pr` to handle external reviewer feedback
Learnt from: CR
URL: 
File: CLAUDE.md:undefined-undefined
Timestamp: 2026-04-07T20:45:51.572Z
Learning: Fix all valid issues found by review agents (including pre-existing issues in surrounding code, suggestions, and findings adjacent to the PR's changes). Never skip or defer -- all fixes must be applied
📚 Learning: 2026-04-01T15:36:39.993Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T15:36:39.993Z
Learning: Applies to {src,tests}/**/*.py : Vendor-agnostic everywhere: 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

Applied to files:

  • tests/conftest.py
  • tests/unit/observability/test_events.py
📚 Learning: 2026-03-15T18:28:13.207Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:28:13.207Z
Learning: Applies to {src,tests,web,cli,site}/**/*.{py,ts,tsx,go,astro} : Vendor-agnostic everywhere: 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. Vendor names may only appear in: (1) Operations design page provider list (docs/design/operations.md), (2) .claude/ skill/agent files, (3) third-party import paths/module names.

Applied to files:

  • tests/conftest.py
  • tests/unit/observability/test_events.py
📚 Learning: 2026-04-01T15:36:39.993Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T15:36:39.993Z
Learning: Applies to {src/synthorg/**/*.py,tests/**/*.py,web/src/**/*.{ts,tsx}} : 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`

Applied to files:

  • tests/conftest.py
  • tests/unit/observability/test_events.py
📚 Learning: 2026-04-07T16:48:04.262Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T16:48:04.262Z
Learning: Applies to src/**/*.py : 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.

Applied to files:

  • tests/conftest.py
  • tests/unit/observability/test_events.py
📚 Learning: 2026-04-07T16:48:04.262Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T16:48:04.262Z
Learning: Applies to tests/**/*.py : In tests, use `test-provider`, `test-small-001`, etc. instead of real vendor names.

Applied to files:

  • tests/conftest.py
  • tests/unit/observability/test_events.py
📚 Learning: 2026-04-01T15:36:39.993Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T15:36:39.993Z
Learning: Applies to tests/**/*.py : Tests must use `test-provider`, `test-small-001`, etc. instead of real vendor names

Applied to files:

  • tests/conftest.py
  • tests/unit/observability/test_events.py
📚 Learning: 2026-03-15T18:28:13.207Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:28:13.207Z
Learning: Applies to tests/**/*.py : Tests must use test-provider, test-small-001, etc. for vendor-agnostic test data.

Applied to files:

  • tests/conftest.py
  • tests/unit/observability/test_events.py
📚 Learning: 2026-04-07T16:48:04.262Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T16:48:04.262Z
Learning: Applies to tests/**/*.py : Use Hypothesis for property-based testing: `given` + `settings`. Hypothesis profiles configured in `tests/conftest.py`: `ci` (deterministic, 10 examples), `dev` (1000 examples), `fuzz` (10,000 examples, no deadline), `extreme` (500,000 examples, no deadline). Controlled via `HYPOTHESIS_PROFILE` env var.

Applied to files:

  • tests/conftest.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 tests/**/*.py : Use Hypothesis for property-based testing in Python with `given` + `settings`. Control via `HYPOTHESIS_PROFILE` env var (dev: 1000 examples, ci: 200 examples).

Applied to files:

  • tests/conftest.py
📚 Learning: 2026-03-31T16:58:29.857Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T16:58:29.857Z
Learning: Applies to tests/**/*.py : Use Hypothesis for property-based testing: `given` + `settings`; dev profile has 1000 examples (via `HYPOTHESIS_PROFILE=dev env var`), ci profile (default) has 50

Applied to files:

  • tests/conftest.py
📚 Learning: 2026-03-15T18:28:13.207Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:28:13.207Z
Learning: Property-based testing: Python uses Hypothesis (given + settings). Hypothesis profiles: ci (200 examples, default) and dev (1000 examples), controlled via HYPOTHESIS_PROFILE env var. Run dev profile: HYPOTHESIS_PROFILE=dev uv run python -m pytest tests/ -m unit -n auto -k properties.

Applied to files:

  • tests/conftest.py
  • tests/unit/versioning/test_service.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 tests/**/*.py : Use Hypothesis for property-based testing with `given` + `settings` decorators; control profiles via `HYPOTHESIS_PROFILE` env var (`ci` for 200 examples, `dev` for 1000 examples)

Applied to files:

  • tests/conftest.py
📚 Learning: 2026-04-02T20:17:33.300Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T20:17:33.300Z
Learning: Applies to tests/**/*.py : 30-second global timeout per test (set in `pyproject.toml`); non-default overrides like `timeout(60)` are allowed

Applied to files:

  • tests/conftest.py
📚 Learning: 2026-04-07T16:48:04.262Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T16:48:04.262Z
Learning: CI runs 10 deterministic examples per property test with `derandomize=True` (same inputs every run). Local fuzzing: `HYPOTHESIS_PROFILE=dev` (1000 examples) or `HYPOTHESIS_PROFILE=fuzz` (10,000 examples, no deadline, `--timeout=0`).

Applied to files:

  • tests/conftest.py
📚 Learning: 2026-03-31T16:58:29.857Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T16:58:29.857Z
Learning: Run property tests with `HYPOTHESIS_PROFILE=dev uv run python -m pytest tests/ -m unit -n auto -k properties`

Applied to files:

  • tests/conftest.py
📚 Learning: 2026-04-07T16:48:04.262Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T16:48:04.262Z
Learning: Applies to tests/**/*.py : Global test timeout is 30 seconds per test (configured in `pyproject.toml`). Do not add per-file `pytest.mark.timeout(30)` markers; non-default overrides like `timeout(60)` are allowed.

Applied to files:

  • tests/conftest.py
📚 Learning: 2026-04-02T07:18:02.381Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T07:18:02.381Z
Learning: Applies to src/synthorg/**/*.py : 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 the domain module

Applied to files:

  • tests/unit/observability/test_events.py
📚 Learning: 2026-03-20T11:18:48.128Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T11:18:48.128Z
Learning: Applies to src/synthorg/**/*.py : 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`.

Applied to files:

  • tests/unit/observability/test_events.py
📚 Learning: 2026-03-18T21:23:23.586Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-18T21:23:23.586Z
Learning: Applies to src/synthorg/**/*.py : Event names: always use constants from the domain-specific module 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>.

Applied to files:

  • tests/unit/observability/test_events.py
📚 Learning: 2026-03-15T18:38:44.202Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:38:44.202Z
Learning: Applies to src/synthorg/**/*.py : Always use event name constants from domain-specific modules under `synthorg.observability.events` (e.g., `PROVIDER_CALL_START` from `events.provider`); import directly: `from synthorg.observability.events.<domain> import EVENT_CONSTANT`

Applied to files:

  • tests/unit/observability/test_events.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 event name constants from synthorg.observability.events domain-specific modules (e.g., PROVIDER_CALL_START from events.provider). Import directly: from synthorg.observability.events.<domain> import EVENT_CONSTANT.

Applied to files:

  • tests/unit/observability/test_events.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : Always 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 rather than using string literals

Applied to files:

  • tests/unit/observability/test_events.py
📚 Learning: 2026-03-15T18:28:13.207Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:28:13.207Z
Learning: Applies to src/synthorg/**/*.py : Event names: always use constants from domain-specific modules under synthorg.observability.events (e.g., PROVIDER_CALL_START from events.provider, BUDGET_RECORD_ADDED from events.budget, etc.). Import directly: `from synthorg.observability.events.<domain> import EVENT_CONSTANT`.

Applied to files:

  • tests/unit/observability/test_events.py
📚 Learning: 2026-04-07T16:48:04.262Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T16:48:04.262Z
Learning: Applies to src/**/*.py : Always use event name constants from domain-specific modules under `synthorg.observability.events` (e.g., `API_REQUEST_STARTED` from `events.api`). Import directly: `from synthorg.observability.events.<domain> import EVENT_CONSTANT`

Applied to files:

  • tests/unit/observability/test_events.py
📚 Learning: 2026-03-16T06:24:56.341Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T06:24:56.341Z
Learning: Applies to src/synthorg/**/*.py : Always use event name constants from the domain-specific module under `synthorg.observability.events` in logging calls

Applied to files:

  • tests/unit/observability/test_events.py
📚 Learning: 2026-03-31T16:09:24.320Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T16:09:24.320Z
Learning: Applies to src/synthorg/**/*.py : Use event name constants from `synthorg.observability.events.<domain>` modules (e.g., `API_REQUEST_STARTED` from `events.api`, `TOOL_INVOKE_START` from `events.tool`); import directly and use in structured logging

Applied to files:

  • tests/unit/observability/test_events.py
📚 Learning: 2026-03-19T11:33:01.580Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T11:33:01.580Z
Learning: Applies to web/**/*.vue : Never use real vendor names (Anthropic, OpenAI, Claude, GPT, etc.) in dashboard code — use generic names: `example-provider`, `example-large-001`, etc.

Applied to files:

  • tests/unit/observability/test_events.py
📚 Learning: 2026-03-31T20:07:03.035Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T20:07:03.035Z
Learning: Applies to {docs/design/operations.md,src/synthorg/providers/presets.py,.claude/**/*.{md,yml,yaml}} : Vendor names may appear only in: (1) Operations design page, (2) `.claude/` skill/agent files, (3) third-party import paths, (4) provider presets (`src/synthorg/providers/presets.py`), (5) tests using `test-provider`, `test-small-001`, etc.

Applied to files:

  • tests/unit/observability/test_events.py
📚 Learning: 2026-04-07T16:48:04.262Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T16:48:04.262Z
Learning: Applies to docs/design/operations.md : Vendor names may appear in: (1) Operations design page (`docs/design/operations.md`), (2) `.claude/` skill/agent files, (3) third-party import paths, (4) provider presets (`src/synthorg/providers/presets.py`).

Applied to files:

  • tests/unit/observability/test_events.py
📚 Learning: 2026-03-14T16:18:57.267Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T16:18:57.267Z
Learning: Applies to src/ai_company/!(observability)/**/*.py : Use event name constants from domain-specific modules under `ai_company.observability.events` (e.g., `PROVIDER_CALL_START` from `events.provider`). Import directly: `from ai_company.observability.events.<domain> import EVENT_CONSTANT`.

Applied to files:

  • tests/unit/observability/test_events.py
📚 Learning: 2026-04-07T16:48:04.262Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T16:48:04.262Z
Learning: NEVER use Bash to write or modify files. Use Write or Edit tools instead. Do not use `cat >`, `cat << EOF`, `echo >`, `echo >>`, `sed -i`, `python -c "open(...).write(...)"`, or `tee`. Read-only uses like piping to stdout are fine.

Applied to files:

  • scripts/check_bash_no_write.sh
📚 Learning: 2026-04-07T16:48:04.262Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T16:48:04.262Z
Learning: See [docs/reference/claude-reference.md](docs/reference/claude-reference.md) for Documentation layout, Docker commands, Package Structure, Releasing, CI pipelines, Dependencies.

Applied to files:

  • docs/reference/claude-reference.md
📚 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:

  • docs/reference/claude-reference.md
📚 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: Engine: Agent orchestration, execution loops, parallel execution, task decomposition, routing, task assignment, centralized single-writer task state engine (TaskEngine), task lifecycle, recovery, shutdown, workspace isolation, coordination (multi-agent pipeline: TopologyDispatcher protocol, 4 dispatchers — SAS/centralized/decentralized/context-dependent, wave execution, workspace lifecycle integration, CoordinationSectionConfig company config bridge, build_coordinator factory), coordination error classification, prompt policy validation, checkpoint recovery (checkpoint/, per-turn persistence, heartbeat detection, CheckpointRecoveryStrategy), approval gate (escalation detection, context parking/resume, EscalationInfo/ResumePayload models), stagnation detection (stagnation/, StagnationDetector protocol, ToolRepetitionDetector, dual-signal analysis, corrective prompt injection), agent runtime state (AgentRuntimeState, lightweight per-agent execution status for dashboard queries and recove...

Applied to files:

  • docs/reference/claude-reference.md
📚 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 src/synthorg/hr/**/*.py : HR package (hr/): hiring, firing, onboarding, offboarding, agent registry, performance tracking (task metrics, collaboration scoring, LLM calibration, collaboration overrides, trend detection), promotion/demotion (criteria evaluation, approval strategies, model mapping)

Applied to files:

  • docs/reference/claude-reference.md
📚 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/memory/**/*.py : Memory package (memory/): pluggable MemoryBackend protocol, backends/ (Mem0 adapter), retrieval pipeline (ranking, RRF fusion, injection, formatting, non-inferable filtering), shared org memory (org/), consolidation/archival (density-aware: DensityClassifier, AbstractiveSummarizer, ExtractivePreserver, DualModeConsolidationStrategy)

Applied to files:

  • docs/reference/claude-reference.md
📚 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 src/synthorg/providers/**/*.py : Providers: LLM provider abstraction (LiteLLM adapter), auth types (api_key/oauth/custom_header/none), presets (PROVIDER_PRESETS), runtime CRUD (ProviderManagementService with asyncio.Lock serialization), hot-reload via AppState swap.

Applied to files:

  • docs/reference/claude-reference.md
📚 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: Security: SecOps agent, rule engine (soft-allow/hard-deny, fail-closed), audit log, output scanner, output scan response policies (redact/withhold/log-only/autonomy-tiered), risk classifier, risk tier classifier, action type registry, ToolInvoker security integration, progressive trust (4 strategies: disabled/weighted/per-category/milestone), autonomy levels (presets, resolver, change strategy), timeout policies (park/resume).

Applied to files:

  • docs/reference/claude-reference.md
📚 Learning: 2026-03-16T06:24:56.341Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T06:24:56.341Z
Learning: Applies to src/synthorg/hr/**/*.py : HR engine must provide: hiring, firing, onboarding, offboarding, agent registry, performance tracking (task metrics, collaboration scoring, trend detection), promotion/demotion

Applied to files:

  • docs/reference/claude-reference.md
📚 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/engine/**/*.py : Engine package (engine/): agent orchestration, parallel execution, task decomposition, routing, TaskEngine (centralized single-writer), task lifecycle/recovery/shutdown, workspace isolation, coordination (4 dispatchers: SAS/centralized/decentralized/context-dependent, wave execution), approval gates (escalation detection, context parking/resume), stagnation detection (ToolRepetitionDetector, corrective prompt injection), AgentRuntimeState (execution status), context budget management, conversation compaction (oldest-turns summarizer)

Applied to files:

  • docs/reference/claude-reference.md
📚 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: Documentation source in `docs/` (Markdown, built with Zensical). Design spec in `docs/design/` (7 pages: index, agents, organization, communication, engine, memory, operations). Architecture in `docs/architecture/` (overview, tech-stack, decision log). Roadmap in `docs/roadmap/`. Security in `docs/security.md`. Licensing in `docs/licensing.md`. Reference in `docs/reference/`. REST API reference in `docs/rest-api.md`. Library reference in `docs/api/` (auto-generated from docstrings). Custom templates in `docs/overrides/`. Config in `mkdocs.yml`.

Applied to files:

  • docs/reference/claude-reference.md
📚 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: Settings: Runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge, ConfigResolver (typed composed reads for controllers), validation, registry, change notifications via message bus. Per-namespace setting definitions in definitions/ submodule (api, company, providers, memory, budget, security, coordination, observability, backup).

Applied to files:

  • docs/reference/claude-reference.md
📚 Learning: 2026-04-07T16:48:04.262Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T16:48:04.262Z
Learning: When Hypothesis finds a failure, it is a real bug. Read the shrunk example, fix the underlying bug, and add an explicit `example(...)` decorator to permanently cover the case in CI.

Applied to files:

  • tests/unit/versioning/test_service.py
📚 Learning: 2026-04-02T09:04:54.709Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T09:04:54.709Z
Learning: Applies to tests/**/*.py : Use pytest markers: `pytest.mark.unit`, `pytest.mark.integration`, `pytest.mark.e2e`, `pytest.mark.slow`

Applied to files:

  • tests/unit/persistence/sqlite/test_identity_version_repo.py
📚 Learning: 2026-03-15T18:28:13.207Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:28:13.207Z
Learning: Applies to tests/**/*.py : Test markers: pytest.mark.unit, pytest.mark.integration, pytest.mark.e2e, pytest.mark.slow. Coverage: 80% minimum (enforced in CI).

Applied to files:

  • tests/unit/persistence/sqlite/test_identity_version_repo.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 tests/**/*.py : Test markers: `pytest.mark.unit`, `pytest.mark.integration`, `pytest.mark.e2e`, `pytest.mark.slow`. Coverage: 80% minimum. Async: `asyncio_mode = 'auto'` — no manual `pytest.mark.asyncio` needed. Timeout: 30 seconds per test. Parallelism: `pytest-xdist` via `-n auto` — ALWAYS include `-n auto` when running pytest, never run tests sequentially.

Applied to files:

  • tests/unit/persistence/sqlite/test_identity_version_repo.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : Use `copy.deepcopy()` at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, persistence serialization) for `dict`/`list` fields

Applied to files:

  • src/synthorg/versioning/service.py
  • src/synthorg/engine/identity/diff.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/versioning/service.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 frozen Pydantic models for config/identity; use separate mutable-via-copy models (using model_copy(update=...)) for runtime state that evolves. Never mix static config fields with mutable runtime fields in one model.

Applied to files:

  • src/synthorg/versioning/service.py
  • src/synthorg/engine/identity/diff.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (via `model_copy(update=...)`) for runtime state that evolves

Applied to files:

  • src/synthorg/versioning/service.py
  • src/synthorg/engine/identity/diff.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 : 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).

Applied to files:

  • src/synthorg/versioning/service.py
  • src/synthorg/engine/identity/diff.py
📚 Learning: 2026-03-15T18:38:44.202Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:38:44.202Z
Learning: Applies to src/synthorg/**/*.py : Use frozen Pydantic models for config/identity; separate mutable-via-copy models (using `model_copy(update=...)`) for runtime state

Applied to files:

  • src/synthorg/engine/identity/diff.py
📚 Learning: 2026-04-07T16:48:04.262Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T16:48:04.262Z
Learning: Applies to src/**/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (using `model_copy(update=...)`) for runtime state that evolves. Never mix static config fields with mutable runtime fields in one model.

Applied to files:

  • src/synthorg/engine/identity/diff.py
📚 Learning: 2026-04-01T09:37:49.451Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T09:37:49.451Z
Learning: Applies to **/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models with `model_copy(update=...)` for runtime state that evolves

Applied to files:

  • src/synthorg/engine/identity/diff.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 with adopted conventions: use computed_field for derived values instead of storing + validating redundant fields; use NotBlankStr from core.types for all identifier/name fields (including optional and tuple variants) instead of manual whitespace validators.

Applied to files:

  • src/synthorg/engine/identity/diff.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : Use `NotBlankStr` from `core.types` for all identifier/name fields (including optional and tuple variants) instead of manual whitespace validators

Applied to files:

  • src/synthorg/engine/identity/diff.py
📚 Learning: 2026-04-07T16:48:04.262Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T16:48:04.262Z
Learning: Applies to src/**/*.py : 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/engine/identity/diff.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/persistence/**/*.py : Persistence uses pluggable PersistenceBackend protocol. SQLite is the initial backend. Settings use SettingsRepository (namespaced settings CRUD).

Applied to files:

  • tests/unit/persistence/sqlite/test_version_repo.py
  • src/synthorg/persistence/sqlite/version_repo.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 tests/**/*.py : Prefer `pytest.mark.parametrize` for testing similar cases.

Applied to files:

  • tests/unit/engine/identity/test_diff.py
📚 Learning: 2026-03-15T18:28:13.207Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:28:13.207Z
Learning: Applies to tests/**/*.py : Parametrize: Prefer pytest.mark.parametrize for testing similar cases.

Applied to files:

  • tests/unit/engine/identity/test_diff.py
📚 Learning: 2026-03-31T20:29:10.177Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T20:29:10.177Z
Learning: Applies to tests/**/*.py : Prefer `pytest.mark.parametrize` for testing similar cases

Applied to files:

  • tests/unit/engine/identity/test_diff.py
📚 Learning: 2026-03-16T06:24:56.341Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T06:24:56.341Z
Learning: Applies to docs/design/**/*.md : Design specification pages in `docs/design/` must be consulted before implementing features (7 pages: index, agents, organization, communication, engine, memory, operations)

Applied to files:

  • docs/design/agents.md
📚 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: Always read the relevant `docs/design/` page before implementing any feature or planning any issue — DESIGN_SPEC.md is a pointer file linking to 7 design pages (Agents, Organization, Communication, Engine, Memory, Operations)

Applied to files:

  • docs/design/agents.md
📚 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 docs/design/*.md : Design spec pages: 7 pages in `docs/design/` — index, agents, organization, communication, engine, memory, operations

Applied to files:

  • docs/design/agents.md
📚 Learning: 2026-03-15T18:38:44.202Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:38:44.202Z
Learning: Always read the relevant `docs/design/` page before implementing any feature or planning any issue. DESIGN_SPEC.md is a pointer file linking to the 7 design pages (index, agents, organization, communication, engine, memory, operations).

Applied to files:

  • docs/design/agents.md
📚 Learning: 2026-03-14T15:43:05.601Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T15:43:05.601Z
Learning: Applies to docs/** : Docs source in docs/ (Markdown, built with Zensical); design spec in docs/design/ (7 pages: index, agents, organization, communication, engine, memory, operations)

Applied to files:

  • docs/design/agents.md
📚 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: Persistence backend: pluggable PersistenceBackend protocol in `src/synthorg/persistence/`, SQLite initial, SettingsRepository (namespaced settings CRUD).

Applied to files:

  • src/synthorg/persistence/sqlite/version_repo.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/persistence/sqlite/version_repo.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/api/**/*.py : Authentication uses JWT + API key. Approval gate integration for high-risk operations.

Applied to files:

  • src/synthorg/engine/review_gate.py
🪛 markdownlint-cli2 (0.22.0)
docs/design/agents.md

[warning] 171-171: No space after hash on atx style heading

(MD018, no-missing-space-atx)


[warning] 224-224: Code block style
Expected: indented; Actual: fenced

(MD046, code-block-style)

Comment on lines +48 to +53
# Strip double-quoted string contents before redirect checks so that >
# inside -m "..." commit messages, script excerpts, or other string
# arguments does not trigger false positives. Intentionally simple:
# does not handle escaped quotes inside strings, which is fine for the
# shell commands we need to guard against.
COMMAND_FOR_REDIR=$(printf '%s\n' "$COMMAND" | sed 's/"[^"]*"/""/g')
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

check() {
    local cmd="$1"
    local command_for_redir
    local verdict

    command_for_redir=$(printf '%s\n' "$cmd" | sed 's/"[^"]*"/""/g')

    if printf '%s\n' "$command_for_redir" | grep -qE '(^|[^|&;])\s*>>?\s*"?[^-]'; then
        verdict="blocked by redirect rule"
    elif printf '%s\n' "$command_for_redir" | grep -qE '\btee\s+[^|]'; then
        verdict="blocked by tee rule"
    else
        verdict="allowed"
    fi

    printf 'cmd: %s\nsanitized: %s\nverdict: %s\n\n' "$cmd" "$command_for_redir" "$verdict"
}

check 'bash -lc "echo hi > out.txt"'
check 'printf "%s\n" "$(tee out.txt </dev/null)"'

Repository: Aureliolo/synthorg

Length of output: 225


🏁 Script executed:

cat -n scripts/check_bash_no_write.sh | sed -n '45,80p'

Repository: Aureliolo/synthorg

Length of output: 2775


Quote stripping creates a critical bypass of all file-write protections.

Line 53 blanks the contents of every double-quoted argument, causing shell commands like bash -lc "echo hi > out.txt" and printf "%s\n" "$(tee out.txt </dev/null)" to bypass all deny rules because their destructive payloads become invisible to the grep checks on lines 58, 74, and 79. This is a security regression: the hook can no longer detect file writes hidden in quoted strings, which is exactly where they are likely to appear in real exploits.

Additionally, line 63's sed pattern leaves leading whitespace in REDIR, breaking the /dev/null exemption on line 68—redirects like > /dev/null fail the string comparison and are incorrectly blocked.

Revert to scanning the original command to close the bypass; the trade-off is tolerating more false positives from quoted strings in commit messages, which is safer than allowing hidden writes:

Suggested fix
-# Strip double-quoted string contents before redirect checks so that >
-# inside -m "..." commit messages, script excerpts, or other string
-# arguments does not trigger false positives.  Intentionally simple:
-# does not handle escaped quotes inside strings, which is fine for the
-# shell commands we need to guard against.
-COMMAND_FOR_REDIR=$(printf '%s\n' "$COMMAND" | sed 's/"[^"]*"/""/g')
+# Any quote-aware suppression must preserve executable payloads such as
+# `bash -lc "..."` and command substitutions `$()`. Until we have that,
+# keep scanning the original command to avoid write bypasses.
+COMMAND_FOR_REDIR=$COMMAND
📝 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
# Strip double-quoted string contents before redirect checks so that >
# inside -m "..." commit messages, script excerpts, or other string
# arguments does not trigger false positives. Intentionally simple:
# does not handle escaped quotes inside strings, which is fine for the
# shell commands we need to guard against.
COMMAND_FOR_REDIR=$(printf '%s\n' "$COMMAND" | sed 's/"[^"]*"/""/g')
# Any quote-aware suppression must preserve executable payloads such as
# `bash -lc "..."` and command substitutions `$()`. Until we have that,
# keep scanning the original command to avoid write bypasses.
COMMAND_FOR_REDIR=$COMMAND
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/check_bash_no_write.sh` around lines 48 - 53, Revert the
quote-stripping bypass: stop using the temporary COMMAND_FOR_REDIR and run the
redirect detection against the original COMMAND so quoted payloads are not
hidden; update places that build REDIR and run the grep deny-rules to read
COMMAND directly (instead of COMMAND_FOR_REDIR). Also ensure REDIR is
normalized/truncated of leading whitespace before comparing to "/dev/null" (trim
leading spaces) so exemptions like "> /dev/null" match correctly; update the
REDIR extraction logic and the subsequent deny-rule checks to use the normalized
REDIR and COMMAND variables.

@Aureliolo Aureliolo force-pushed the feat/agent-identity-versioning branch from b9f779c to a14722e Compare April 7, 2026 21:08
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview April 7, 2026 21:11 — 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: 12

Caution

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

⚠️ Outside diff range comments (1)
src/synthorg/engine/review_gate.py (1)

376-384: ⚠️ Potential issue | 🟠 Major

Switch this to the tupleless multi-except form per PEP 758.

Line 376 uses parenthesized syntax except (QueryError, DuplicateRecordError) which violates the repo's Python 3.14 requirement. Change to except QueryError, DuplicateRecordError: (no parentheses)—ruff enforces this.

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

In `@src/synthorg/engine/review_gate.py` around lines 376 - 384, Replace the
parenthesized multi-except with the tupleless PEP 758 form so the handler still
catches both errors and binds the exception to exc; i.e., change the current
"except (QueryError, DuplicateRecordError) as exc:" to "except QueryError,
DuplicateRecordError as exc:" in the block that logs
APPROVAL_GATE_DECISION_RECORD_FAILED (the logger.exception call that passes
task_id, decided_by, approved, error_type and error).
♻️ Duplicate comments (2)
scripts/check_bash_no_write.sh (1)

48-53: ⚠️ Potential issue | 🔴 Critical

Quote stripping still creates write bypasses.

COMMAND_FOR_REDIR=$(... | sed 's/"[^"]*"/""/g') hides shell payloads before Lines 58, 74, and 79 run. That makes bash -lc "echo hi > out.txt", echo hi > "out.txt", and printf "%s\n" "$(tee out.txt </dev/null)" look non-writing, so the hook no longer enforces the write ban. Until this is shell-aware, scanning the raw COMMAND is safer.

Suggested fix
-# Strip double-quoted string contents before redirect checks so that >
-# inside -m "..." commit messages, script excerpts, or other string
-# arguments does not trigger false positives.  Intentionally simple:
-# does not handle escaped quotes inside strings, which is fine for the
-# shell commands we need to guard against.
-COMMAND_FOR_REDIR=$(printf '%s\n' "$COMMAND" | sed 's/"[^"]*"/""/g')
+# Any quote-aware suppression must preserve executable payloads such as
+# `bash -lc "..."`, command substitutions, and quoted redirect targets.
+# Until we have a shell-aware parser, scan the original command.
+COMMAND_FOR_REDIR=$COMMAND
#!/bin/bash
set -euo pipefail

check() {
    local cmd="$1"
    local command_for_redir redir="" redirect_hit=no tee_hit=no blocked=no

    command_for_redir=$(printf '%s\n' "$cmd" | sed 's/"[^"]*"/""/g')

    if printf '%s\n' "$command_for_redir" | grep -qE '(^|[^|&;])\s*>>?\s*"?[^-]'; then
        redirect_hit=yes
        redir=$(printf '%s\n' "$command_for_redir" \
            | grep -oE '>>?\s*"?[^|&;<>]+' \
            | head -1 \
            | sed -E 's/^>>?\s*["'"'"']*//' || true)
        if [[ -n "$redir" && "$redir" != "/dev/null" && ! "$redir" =~ ^&[0-9]+$ ]]; then
            blocked=yes
        fi
    fi

    if printf '%s\n' "$command_for_redir" | grep -qE '\btee\s+[^|]'; then
        tee_hit=yes
    fi

    printf 'cmd: %s\nsanitized: %s\nredirect_hit=%s redir=%q blocked=%s tee_hit=%s\n\n' \
        "$cmd" "$command_for_redir" "$redirect_hit" "$redir" "$blocked" "$tee_hit"
}

check 'bash -lc "echo hi > out.txt"'
check 'echo hi > "out.txt"'
check 'printf "%s\n" "$(tee out.txt </dev/null)"'

Based on learnings: Never use Bash to write or modify files; use Write or Edit tools instead. Avoid cat >, cat << EOF, echo >, echo >>, sed -i, etc.

Also applies to: 58-79

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

In `@scripts/check_bash_no_write.sh` around lines 48 - 53, The current
quote-stripping into COMMAND_FOR_REDIR masks real redirections and allows write
bypasses; in the check() function stop using COMMAND_FOR_REDIR for detection and
instead run the redirection and tee greps against the raw cmd (variable cmd /
original COMMAND), preserving sanitization only for logging/display. Concretely:
keep the sed-based COMMAND_FOR_REDIR only for printing, but change the grep/grep
-oE redirection and the tee detection calls to use "$cmd" (or the original
COMMAND) so patterns like > out.txt, > "out.txt", and nested $(tee ...) are
caught; ensure the redir extraction still strips surrounding quotes when
capturing the target (use the existing sed -E 's/^>>?\s*["'"'"']*//' logic
against the raw-match).
src/synthorg/versioning/service.py (1)

142-163: ⚠️ Potential issue | 🟠 Major

Keep retrying until this distinct snapshot is actually persisted.

If two other writers win vN and then vN+1 before our retry completes, Line 163 returns someone else’s snapshot and drops this content change entirely. That still loses version history under a 3-way race. Please keep looping until get_latest_version() comes back with our content_hash or an insert succeeds, and add a regression for False -> different content -> False.

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

In `@src/synthorg/versioning/service.py` around lines 142 - 163, The current logic
after constructing retry (via _build_snapshot) only attempts one save and then
returns whatever get_latest_version(entity_id) returns, which can drop our
change under a 3-way race; change the flow in the method so that after building
retry you loop: call _repo.save_version(retry) and if it returns True return
retry and log VERSION_SAVED; otherwise call _repo.get_latest_version(entity_id)
and if that persisted has content_hash == content_hash return persisted; else
sleep/yield briefly (or continue immediately) and retry the save/get loop until
either save_version succeeds or get_latest_version returns our content_hash.
Reference symbols: _repo.get_latest_version, _build_snapshot,
_repo.save_version, persisted, retry, content_hash. Also add a regression test
that simulates save_version returning False, get_latest_version returning a
different content, then False again (False -> different content -> False) and
asserts the method eventually persists or returns the snapshot with the expected
content_hash.
🤖 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/agents.md`:
- Around line 162-165: The docs claim "Every mutation to an `AgentIdentity`" is
versioned but the current implementation only snapshots charter/config edits;
update `docs/design/agents.md` to state that `AgentRegistryService` creates
`VersionSnapshot[AgentIdentity]` records for `register()` and
`update_identity()` (charter/config changes) while `update_status()` (status
changes) is not versioned, and explicitly note this deviation from the original
design so `DecisionRecord` citations and audit expectations are accurate.

In `@scripts/check_bash_no_write.sh`:
- Around line 58-68: The current extraction uses head -1 on COMMAND_FOR_REDIR so
only the first redirect target is checked (variable REDIR), letting later unsafe
redirects slip through; change the logic to iterate over every match returned by
grep -oE '>>?\s*"?[^|&;<>]+' (do not use head -1), strip the leading >>? and
quotes as you do now, and for each extracted REDIR check the same conditions
(non-empty, not "/dev/null", and not matching FD_RE) and fail if any single
REDIR is unsafe; update the block that sets REDIR and the if that uses FD_RE so
it processes every match rather than only the first.

In `@src/synthorg/engine/identity/diff.py`:
- Around line 79-88: The validator _validate_version_order on AgentIdentityDiff
currently only rejects equality; change its check to enforce from_version <
to_version by replacing the equality test with a >= check (i.e., if
self.from_version >= self.to_version) and update the ValueError message to state
"from_version must be less than to_version" including the from_version and
to_version values; this aligns the validator with the docstring and the
compute_diff parameter ordering and tests.

In `@src/synthorg/hr/registry.py`:
- Line 76: The snapshot call currently hardcodes saved_by="system", losing the
real actor; change the APIs so the initiating actor is threaded from register()
and update_identity() into _snapshot() (or accept an optional saved_by parameter
on _snapshot), update both call sites to pass the caller/actor value instead of
"system", and ensure persisting uses that actor value so version history records
who made the change; reference functions: register(), update_identity(), and
_snapshot().
- Around line 272-300: The out-of-lock call to _snapshot (which awaits
self._versioning.snapshot_if_changed) allows concurrent update_identity/register
operations to race and reorder/persist stale versions; fix by serializing
snapshot writes per-agent or performing the snapshot while holding the
registry's critical section: either (A) introduce a per-agent mutex (keyed by
identity.id) and acquire it around calls to _snapshot from update_identity and
register so snapshot calls for the same agent run sequentially, or (B) move the
await self._versioning.snapshot_if_changed(...) into the existing registry
lock/critical section used by register/update_identity so the registry mutation
and snapshot persist as an atomic operation; ensure to reference _snapshot,
update_identity, register, get_latest_version and
_versioning.snapshot_if_changed when applying the change.
- Around line 41-47: The registry's VersioningService is left unset in normal
app wiring which causes _snapshot() to be a no-op; update the
AgentRegistryService wiring so __init__ receives the app's default
VersioningService when none is passed and adjust the creator in the app startup
to pass that instance. Concretely, change AgentRegistryService.__init__ to use
the application's shared VersioningService (e.g., obtain from the app/service
container or a VersioningService.get_default() accessor) when versioning is
None, and update the place that constructs AgentRegistryService (the
app-state/service bootstrap that currently calls AgentRegistryService() with no
args) to supply the app's VersioningService so _snapshot() actually persists
identity versions. Ensure you reference AgentRegistryService.__init__ and the
_snapshot method when making the changes.

In `@src/synthorg/persistence/sqlite/version_repo.py`:
- Around line 314-326: In list_versions (async def list_versions) validate the
pagination args before calling self._db.execute: check that limit and offset are
non-negative integers and raise a ValueError (or appropriate exception) with a
clear message if either is negative; do this at the start of the function
(before using self._select_list_sql / self._db.execute) so invalid values fail
fast and never reach the SQLite backend.
- Line 151: Replace the parenthesized multi-exception handlers with PEP 758
syntax in version_repo.py: change occurrences of "except (ValueError, KeyError)
as exc:" to "except ValueError, KeyError as exc:" (and similarly for the other
seven instances) inside the VersionRepo class methods that handle
parsing/lookup/fetch logic so each except clause uses the new comma-separated
form without parentheses while keeping the same exception variable name (exc).
- Around line 162-175: The generic "except Exception as exc" handlers in
version_repo.py (the block that logs VERSION_FETCH_FAILED and raises QueryError
for deserialize_snapshot callbacks and the similar serialize/deserialize
catch-all later) are currently converting system errors into QueryError; add an
explicit pass-through clause before each generic handler that re-raises
MemoryError and RecursionError (e.g., "except (MemoryError, RecursionError):
raise") so these system errors propagate unchanged, leaving the subsequent
except Exception as exc to handle only non-system exceptions and continue to log
VERSION_FETCH_FAILED and raise QueryError as before.

In `@src/synthorg/versioning/models.py`:
- Around line 46-55: The model currently accepts snapshots, content_hashes, and
timestamps that can violate repository invariants; add Pydantic validators to
enforce them at the model boundary: add a field validator for snapshot (named
"snapshot") that asserts isinstance(snapshot, BaseModel) and that
snapshot.model_config.get("frozen", False) is True (raise ValueError otherwise);
add a field validator for content_hash that asserts it's a 64-character
lowercase hex string (match r'^[0-9a-f]{64}$'); add a field validator for
saved_at that asserts saved_at.tzinfo is not None and
saved_at.tzinfo.utcoffset(saved_at) == datetime.timedelta(0) (i.e. UTC); keep
the class model_config as shown to remain frozen and raise clear ValueErrors so
invalid records are rejected before reaching
SQLiteVersionRepository.save_version().

In `@tests/unit/engine/identity/test_diff.py`:
- Around line 163-173: The test test_added_change_new_value_present duplicates
coverage already provided by the parametrized test_change_invariant_matrix;
remove the redundant standalone test (the function named
test_added_change_new_value_present) from
tests/unit/engine/identity/test_diff.py to avoid duplication, or alternatively
keep it but add a comment explaining why the explicit case is needed and/or mark
it with pytest.mark.explicit to indicate intentional overlap; update imports or
test markers only if necessary to keep the test suite green.

In `@tests/unit/versioning/test_service.py`:
- Around line 184-203: The tests test_same_model_same_hash and the subsequent
property test are explicitly setting `@settings`(max_examples=100), which
overrides the project's Hypothesis profiles; remove the
`@settings`(max_examples=100) decorators from those test functions (look for the
decorator above def test_same_model_same_hash and the one above the next
property-based test) so Hypothesis will use the configured profiles
(HYPOTHESIS_PROFILE) defined in tests/conftest.py.

---

Outside diff comments:
In `@src/synthorg/engine/review_gate.py`:
- Around line 376-384: Replace the parenthesized multi-except with the tupleless
PEP 758 form so the handler still catches both errors and binds the exception to
exc; i.e., change the current "except (QueryError, DuplicateRecordError) as
exc:" to "except QueryError, DuplicateRecordError as exc:" in the block that
logs APPROVAL_GATE_DECISION_RECORD_FAILED (the logger.exception call that passes
task_id, decided_by, approved, error_type and error).

---

Duplicate comments:
In `@scripts/check_bash_no_write.sh`:
- Around line 48-53: The current quote-stripping into COMMAND_FOR_REDIR masks
real redirections and allows write bypasses; in the check() function stop using
COMMAND_FOR_REDIR for detection and instead run the redirection and tee greps
against the raw cmd (variable cmd / original COMMAND), preserving sanitization
only for logging/display. Concretely: keep the sed-based COMMAND_FOR_REDIR only
for printing, but change the grep/grep -oE redirection and the tee detection
calls to use "$cmd" (or the original COMMAND) so patterns like > out.txt, >
"out.txt", and nested $(tee ...) are caught; ensure the redir extraction still
strips surrounding quotes when capturing the target (use the existing sed -E
's/^>>?\s*["'"'"']*//' logic against the raw-match).

In `@src/synthorg/versioning/service.py`:
- Around line 142-163: The current logic after constructing retry (via
_build_snapshot) only attempts one save and then returns whatever
get_latest_version(entity_id) returns, which can drop our change under a 3-way
race; change the flow in the method so that after building retry you loop: call
_repo.save_version(retry) and if it returns True return retry and log
VERSION_SAVED; otherwise call _repo.get_latest_version(entity_id) and if that
persisted has content_hash == content_hash return persisted; else sleep/yield
briefly (or continue immediately) and retry the save/get loop until either
save_version succeeds or get_latest_version returns our content_hash. Reference
symbols: _repo.get_latest_version, _build_snapshot, _repo.save_version,
persisted, retry, content_hash. Also add a regression test that simulates
save_version returning False, get_latest_version returning a different content,
then False again (False -> different content -> False) and asserts the method
eventually persists or returns the snapshot with the expected content_hash.
🪄 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: e6aa4449-ae15-420f-a02a-07d95f95e5a6

📥 Commits

Reviewing files that changed from the base of the PR and between b9f779c and a14722e.

📒 Files selected for processing (35)
  • CLAUDE.md
  • docs/design/agents.md
  • docs/design/engine.md
  • docs/reference/claude-reference.md
  • scripts/check_bash_no_write.sh
  • src/synthorg/engine/identity/__init__.py
  • src/synthorg/engine/identity/diff.py
  • src/synthorg/engine/review_gate.py
  • src/synthorg/hr/registry.py
  • src/synthorg/observability/events/versioning.py
  • src/synthorg/persistence/protocol.py
  • src/synthorg/persistence/sqlite/backend.py
  • src/synthorg/persistence/sqlite/schema.sql
  • src/synthorg/persistence/sqlite/version_repo.py
  • src/synthorg/persistence/version_repo.py
  • src/synthorg/versioning/__init__.py
  • src/synthorg/versioning/hashing.py
  • src/synthorg/versioning/models.py
  • src/synthorg/versioning/service.py
  • tests/conftest.py
  • tests/integration/engine/identity/__init__.py
  • tests/integration/engine/identity/test_identity_versioning.py
  • tests/unit/api/fakes_backend.py
  • tests/unit/engine/identity/__init__.py
  • tests/unit/engine/identity/test_diff.py
  • tests/unit/engine/test_review_gate.py
  • tests/unit/observability/test_events.py
  • tests/unit/persistence/sqlite/test_identity_version_repo.py
  • tests/unit/persistence/sqlite/test_migrations.py
  • tests/unit/persistence/sqlite/test_version_repo.py
  • tests/unit/persistence/test_protocol.py
  • tests/unit/versioning/__init__.py
  • tests/unit/versioning/test_hashing.py
  • tests/unit/versioning/test_models.py
  • tests/unit/versioning/test_service.py

Comment on lines +58 to +68
if printf '%s\n' "$COMMAND_FOR_REDIR" | grep -qE '(^|[^|&;])\s*>>?\s*"?[^-]'; then
# Extract redirect target to check if it's a file descriptor
REDIR=$(printf '%s\n' "$COMMAND" | grep -oE '>>?\s*"?[^|&;<>]+' | head -1 | sed 's/^>>\?["'"'"']*//')
# Only allow if it's a file descriptor (>&N or <&N format)
# Extract redirect target. grep returns 1 when there is no match (e.g.
# ">&1" where & is excluded from [^|&;<>]+); || true prevents set -e from
# exiting the script when extraction yields no output.
REDIR=$(printf '%s\n' "$COMMAND_FOR_REDIR" | grep -oE '>>?\s*"?[^|&;<>]+' | head -1 | sed -E 's/^>>?\s*["'"'"']*//' || true)
# Only deny if a non-empty target was extracted and it is not a file
# descriptor. Empty REDIR means the target was &N (fd redirect such as
# 2>&1) whose & could not be captured; those are always safe.
FD_RE='^&[0-9]+$'
if [[ ! "$REDIR" =~ $FD_RE ]]; then
if [[ -n "$REDIR" && "$REDIR" != "/dev/null" && ! "$REDIR" =~ $FD_RE ]]; then
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

check() {
    local cmd="$1"
    local redir="" blocked=no

    if printf '%s\n' "$cmd" | grep -qE '(^|[^|&;])\s*>>?\s*"?[^-]'; then
        redir=$(printf '%s\n' "$cmd" \
            | grep -oE '>>?\s*"?[^|&;<>]+' \
            | head -1 \
            | sed -E 's/^>>?\s*["'"'"']*//' || true)
        if [[ -n "$redir" && "$redir" != "/dev/null" && ! "$redir" =~ ^&[0-9]+$ ]]; then
            blocked=yes
        fi
    fi

    printf 'cmd: %s\nfirst_redir=%q blocked=%s\n\n' "$cmd" "$redir" "$blocked"
}

check 'echo hi > -tmp'
check 'true > /dev/null; cat > out'

Repository: Aureliolo/synthorg

Length of output: 146


🏁 Script executed:

#!/bin/bash
set -euo pipefail

check() {
    local cmd="$1"
    local redir="" blocked=no

    if printf '%s\n' "$cmd" | grep -qE '(^|[^|&;])\s*>>?\s*"?[^-]'; then
        redir=$(printf '%s\n' "$cmd" \
            | grep -oE '>>?\s*"?[^|&;<>]+' \
            | head -1 \
            | sed -E 's/^>>?\s*["'"'"']*//' || true)
        FD_RE='^&[0-9]+$'
        if [[ -n "$redir" && "$redir" != "/dev/null" && ! "$redir" =~ $FD_RE ]]; then
            blocked=yes
        fi
    fi

    printf 'cmd: %s\nfirst_redir=%q blocked=%s\n\n' "$cmd" "$redir" "$blocked"
}

check 'echo hi > -tmp'
check 'true > /dev/null; cat > out'

Repository: Aureliolo/synthorg

Length of output: 178


🏁 Script executed:

cat -n scripts/check_bash_no_write.sh | sed -n '55,70p'

Repository: Aureliolo/synthorg

Length of output: 1413


🏁 Script executed:

# Test what the line 58 grep pattern actually matches
echo "Testing grep pattern from line 58:"
printf '%s\n' 'echo hi > -tmp' | grep -qE '(^|[^|&;])\s*>>?\s*"?[^-]' && echo "MATCHES" || echo "NO MATCH"
printf '%s\n' 'true > /dev/null; cat > out' | grep -qE '(^|[^|&;])\s*>>?\s*"?[^-]' && echo "MATCHES" || echo "NO MATCH"

# Also test some edge cases
printf '%s\n' 'echo > -f' | grep -qE '(^|[^|&;])\s*>>?\s*"?[^-]' && echo "MATCHES: echo > -f" || echo "NO MATCH: echo > -f"
printf '%s\n' 'cat > output' | grep -qE '(^|[^|&;])\s*>>?\s*"?[^-]' && echo "MATCHES: cat > output" || echo "NO MATCH: cat > output"
printf '%s\n' '> /tmp/file' | grep -qE '(^|[^|&;])\s*>>?\s*"?[^-]' && echo "MATCHES: > /tmp/file" || echo "NO MATCH: > /tmp/file"

Repository: Aureliolo/synthorg

Length of output: 174


🏁 Script executed:

# Detailed trace for the -tmp case
echo "=== Testing: echo hi > -tmp ==="
cmd='echo hi > -tmp'
redir=$(printf '%s\n' "$cmd" | grep -oE '>>?\s*"?[^|&;<>]+' | head -1 | sed -E 's/^>>?\s*["'"'"']*//' || true)
echo "Extracted REDIR: '$redir'"
FD_RE='^&[0-9]+$'
if [[ -n "$redir" && "$redir" != "/dev/null" && ! "$redir" =~ $FD_RE ]]; then
    echo "Result: BLOCKED"
else
    echo "Result: NOT blocked"
fi

# Test with just a dash
echo ""
echo "=== Testing: echo > - ==="
cmd='echo > -'
if printf '%s\n' "$cmd" | grep -qE '(^|[^|&;])\s*>>?\s*"?[^-]'; then
    echo "Pattern MATCHES"
    redir=$(printf '%s\n' "$cmd" | grep -oE '>>?\s*"?[^|&;<>]+' | head -1 | sed -E 's/^>>?\s*["'"'"']*//' || true)
    echo "Extracted REDIR: '$redir'"
    if [[ -n "$redir" && "$redir" != "/dev/null" && ! "$redir" =~ $FD_RE ]]; then
        echo "Result: BLOCKED"
    else
        echo "Result: NOT blocked"
    fi
else
    echo "Pattern does NOT match - safe"
fi

# Test the second critical case more carefully
echo ""
echo "=== Testing: true > /dev/null; cat > out ==="
cmd='true > /dev/null; cat > out'
if printf '%s\n' "$cmd" | grep -qE '(^|[^|&;])\s*>>?\s*"?[^-]'; then
    echo "Pattern MATCHES"
    redir=$(printf '%s\n' "$cmd" | grep -oE '>>?\s*"?[^|&;<>]+' | head -1 | sed -E 's/^>>?\s*["'"'"']*//' || true)
    echo "Extracted REDIR (head -1): '$redir'"
    # Show ALL extractions without head -1
    echo "ALL redirects:"
    printf '%s\n' "$cmd" | grep -oE '>>?\s*"?[^|&;<>]+' | sed -E 's/^>>?\s*["'"'"']*//'
    if [[ -n "$redir" && "$redir" != "/dev/null" && ! "$redir" =~ $FD_RE ]]; then
        echo "Result with first only: BLOCKED"
    else
        echo "Result with first only: NOT blocked"
    fi
fi

Repository: Aureliolo/synthorg

Length of output: 379


Fix incomplete redirect enumeration: head -1 bypasses later unsafe redirects.

The extraction at line 63 uses head -1 to check only the first matched redirect, allowing dangerous redirects in later commands to bypass the check. For example, true > /dev/null; cat > out extracts only /dev/null (safe) and misses out (unsafe). Validate every matched redirect target instead of stopping at the first one.

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

In `@scripts/check_bash_no_write.sh` around lines 58 - 68, The current extraction
uses head -1 on COMMAND_FOR_REDIR so only the first redirect target is checked
(variable REDIR), letting later unsafe redirects slip through; change the logic
to iterate over every match returned by grep -oE '>>?\s*"?[^|&;<>]+' (do not use
head -1), strip the leading >>? and quotes as you do now, and for each extracted
REDIR check the same conditions (non-empty, not "/dev/null", and not matching
FD_RE) and fail if any single REDIR is unsafe; update the block that sets REDIR
and the if that uses FD_RE so it processes every match rather than only the
first.

Comment on lines +41 to +47
def __init__(
self,
versioning: VersioningService[AgentIdentity] | None = None,
) -> None:
self._agents: dict[str, AgentIdentity] = {}
self._lock = asyncio.Lock()
self._versioning = versioning
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

Wire the versioning service through the default app path.

versioning is optional here, but src/synthorg/api/state.py:376-395 still constructs AgentRegistryService without it. That makes _snapshot() a silent no-op in the normal app wiring, so no identity versions are persisted outside tests/manual setup.

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

In `@src/synthorg/hr/registry.py` around lines 41 - 47, The registry's
VersioningService is left unset in normal app wiring which causes _snapshot() to
be a no-op; update the AgentRegistryService wiring so __init__ receives the
app's default VersioningService when none is passed and adjust the creator in
the app startup to pass that instance. Concretely, change
AgentRegistryService.__init__ to use the application's shared VersioningService
(e.g., obtain from the app/service container or a
VersioningService.get_default() accessor) when versioning is None, and update
the place that constructs AgentRegistryService (the app-state/service bootstrap
that currently calls AgentRegistryService() with no args) to supply the app's
VersioningService so _snapshot() actually persists identity versions. Ensure you
reference AgentRegistryService.__init__ and the _snapshot method when making the
changes.

Comment on lines +162 to +175
except Exception as exc:
# Catch-all for unconstrained deserialize_snapshot callbacks
# (e.g. TypeError, AttributeError) so all callback errors
# are normalized to QueryError.
context = f"{data.get('entity_id', '?')}@v{data.get('version', '?')}"
msg = f"Failed to deserialize version snapshot {context!r}: {exc}"
logger.exception(
VERSION_FETCH_FAILED,
table=self._table,
context=context,
error=str(exc),
reason="callback_error",
)
raise QueryError(msg) from exc
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n 'except Exception as exc' src/synthorg/persistence/sqlite/version_repo.py

Repository: Aureliolo/synthorg

Length of output: 135


🏁 Script executed:

cat -n src/synthorg/persistence/sqlite/version_repo.py | head -200

Repository: Aureliolo/synthorg

Length of output: 9133


🏁 Script executed:

tail -n +200 src/synthorg/persistence/sqlite/version_repo.py | head -150

Repository: Aureliolo/synthorg

Length of output: 5153


🏁 Script executed:

tail -n +350 src/synthorg/persistence/sqlite/version_repo.py

Repository: Aureliolo/synthorg

Length of output: 1271


🏁 Script executed:

rg -n 'except \(' src/synthorg/persistence/sqlite/version_repo.py

Repository: Aureliolo/synthorg

Length of output: 531


Add explicit pass-through for system errors before generic deserialize/serialize catch-alls.

The except Exception as exc handlers at lines 162–175 and 187–200 swallow MemoryError and RecursionError from custom callbacks, converting them to QueryError. Per project convention, system-error conditions must propagate immediately: add except MemoryError, RecursionError: raise before each generic handler.

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

In `@src/synthorg/persistence/sqlite/version_repo.py` around lines 162 - 175, The
generic "except Exception as exc" handlers in version_repo.py (the block that
logs VERSION_FETCH_FAILED and raises QueryError for deserialize_snapshot
callbacks and the similar serialize/deserialize catch-all later) are currently
converting system errors into QueryError; add an explicit pass-through clause
before each generic handler that re-raises MemoryError and RecursionError (e.g.,
"except (MemoryError, RecursionError): raise") so these system errors propagate
unchanged, leaving the subsequent except Exception as exc to handle only
non-system exceptions and continue to log VERSION_FETCH_FAILED and raise
QueryError as before.

Comment on lines +46 to +55
model_config = ConfigDict(frozen=True, allow_inf_nan=False)

entity_id: NotBlankStr = Field(description="String primary key of the entity")
version: int = Field(ge=1, description="Monotonic version counter (1-indexed)")
content_hash: NotBlankStr = Field(
description="SHA-256 hex digest of canonical JSON"
)
snapshot: T = Field(description="Full frozen entity model at this version")
saved_by: NotBlankStr = Field(description="Actor that triggered the snapshot")
saved_at: AwareDatetime = Field(description="When the snapshot was captured")
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

🧩 Analysis chain

🏁 Script executed:

# First, check the VersionSnapshot model definition
cat -n src/synthorg/versioning/models.py | head -60

Repository: Aureliolo/synthorg

Length of output: 2674


🏁 Script executed:

# Check the SQLite schema for the saved_at column
cat -n src/synthorg/persistence/sqlite/schema.sql | grep -A 5 -B 5 "agent_identity_versions"

Repository: Aureliolo/synthorg

Length of output: 1063


🏁 Script executed:

# Find and check the save_version implementation
fd -t f "*.py" -path "*/persistence/*" | xargs grep -l "save_version" | head -5

Repository: Aureliolo/synthorg

Length of output: 230


🏁 Script executed:

# Fix the fd command - search for persistence files
find . -path "*persistence*" -name "*.py" -type f | xargs grep -l "save_version" 2>/dev/null | head -5

Repository: Aureliolo/synthorg

Length of output: 327


🏁 Script executed:

# Get the full saved_at column definition from schema
sed -n '585,595p' src/synthorg/persistence/sqlite/schema.sql

Repository: Aureliolo/synthorg

Length of output: 533


🏁 Script executed:

# Search for the actual save_version implementation
rg "def save_version" --type py -A 20

Repository: Aureliolo/synthorg

Length of output: 9988


🏁 Script executed:

# Get the full save_version implementation from SQLiteVersionRepository
sed -n '60,130p' src/synthorg/persistence/sqlite/version_repo.py

Repository: Aureliolo/synthorg

Length of output: 2993


🏁 Script executed:

# Check how saved_at is formatted when being saved
rg "saved_at" src/synthorg/persistence/sqlite/version_repo.py -B 2 -A 2

Repository: Aureliolo/synthorg

Length of output: 942


🏁 Script executed:

# Get the complete save_version method implementation
sed -n '100,150p' src/synthorg/persistence/sqlite/version_repo.py

Repository: Aureliolo/synthorg

Length of output: 2246


🏁 Script executed:

# Also check if there's any validation of the schema constraint elsewhere
rg "saved_at" src/synthorg/versioning/models.py -B 3 -A 3

Repository: Aureliolo/synthorg

Length of output: 807


🏁 Script executed:

# Check if content_hash format is validated anywhere
rg "content_hash" src/synthorg/versioning/models.py -B 2 -A 2

Repository: Aureliolo/synthorg

Length of output: 815


🏁 Script executed:

# Check if snapshot frozen validation exists anywhere
rg "frozen" src/synthorg/versioning/ -r

Repository: Aureliolo/synthorg

Length of output: 475


🏁 Script executed:

# Get the complete save_version method
sed -n '145,190p' src/synthorg/persistence/sqlite/version_repo.py

Repository: Aureliolo/synthorg

Length of output: 1991


🏁 Script executed:

# Check what happens if we try to write a non-UTC datetime
rg "def save_version" src/synthorg/persistence/sqlite/version_repo.py -A 40

Repository: Aureliolo/synthorg

Length of output: 1557


🏁 Script executed:

# Check how VersionSnapshot is created in the codebase
rg "VersionSnapshot(" src/ --type py -B 2 -A 2 | head -50

Repository: Aureliolo/synthorg

Length of output: 144


🏁 Script executed:

# Check if there are any places where non-UTC datetimes might be passed
rg "saved_at" src/synthorg/ --type py | grep -v "test" | head -20

Repository: Aureliolo/synthorg

Length of output: 1981


🏁 Script executed:

# Verify the schema constraint is actually enforced - check tests
rg "saved_at.*LIKE" tests/ --type py -B 2 -A 2

Repository: Aureliolo/synthorg

Length of output: 995


🏁 Script executed:

# Check if any tests create VersionSnapshot with non-UTC saved_at
rg "VersionSnapshot" tests/ --type py -A 8 | grep -E "(saved_at|VersionSnapshot)" | head -30

Repository: Aureliolo/synthorg

Length of output: 2627


🏁 Script executed:

# Check if any code creates VersionSnapshot with non-frozen snapshots
rg "VersionSnapshot" src/ --type py -B 2 -A 8 | grep -E "(snapshot.*=|VersionSnapshot)" | head -30

Repository: Aureliolo/synthorg

Length of output: 2699


🏁 Script executed:

# Verify if content_hash values in actual code match SHA256 format
rg "content_hash.*=" src/synthorg/versioning/ --type py -A 1

Repository: Aureliolo/synthorg

Length of output: 1202


🏁 Script executed:

# Check the test file for VersionSnapshot to see what's tested
cat -n tests/unit/versioning/test_models.py

Repository: Aureliolo/synthorg

Length of output: 3789


🏁 Script executed:

# Check compute_content_hash to see what format it produces
rg "def compute_content_hash" src/synthorg/versioning/ -A 15

Repository: Aureliolo/synthorg

Length of output: 1146


🏁 Script executed:

# Check if there are any tests for non-UTC datetime being accepted
rg "saved_at" tests/unit/versioning/test_models.py -B 2 -A 2

Repository: Aureliolo/synthorg

Length of output: 546


🏁 Script executed:

# Verify what happens when non-UTC datetime is passed to isoformat()
python3 << 'EOF'
from datetime import datetime, timezone, timedelta

# Create a non-UTC aware datetime
utc_plus_5 = timezone(timedelta(hours=5))
dt = datetime(2026, 4, 7, 12, 0, tzinfo=utc_plus_5)
print(f"Non-UTC datetime isoformat: {dt.isoformat()}")

# Create a UTC datetime
from datetime import UTC
dt_utc = datetime(2026, 4, 7, 12, 0, tzinfo=UTC)
print(f"UTC datetime isoformat: {dt_utc.isoformat()}")

# Check if they'd match the SQL pattern
import re
pattern = r'%\+00:00|%Z'
non_utc_iso = dt.isoformat()
utc_iso = dt_utc.isoformat()

print(f"Non-UTC matches +00:00? {'+00:00' in non_utc_iso}")
print(f"UTC matches +00:00? {'+00:00' in utc_iso}")
print(f"Non-UTC matches Z? {'Z' in non_utc_iso}")
EOF

Repository: Aureliolo/synthorg

Length of output: 245


🏁 Script executed:

# Check if there's any existing validation in the model that we might have missed
cat -n src/synthorg/versioning/models.py | tail -20

Repository: Aureliolo/synthorg

Length of output: 1291


🏁 Script executed:

# Check if we can access model_config on a BaseModel instance
python3 << 'EOF'
from pydantic import BaseModel, ConfigDict

class FrozenModel(BaseModel):
    model_config = ConfigDict(frozen=True)
    name: str

class MutableModel(BaseModel):
    name: str

frozen_inst = FrozenModel(name="test")
mutable_inst = MutableModel(name="test")

print(f"Frozen instance model_config: {frozen_inst.model_config}")
print(f"Mutable instance model_config: {mutable_inst.model_config}")
print(f"Frozen check: {frozen_inst.model_config.get('frozen') is True}")
print(f"Mutable check: {mutable_inst.model_config.get('frozen') is True}")

# Also check via __class__
print(f"Via class - Frozen: {frozen_inst.__class__.model_config.get('frozen') is True}")
print(f"Via class - Mutable: {mutable_inst.__class__.model_config.get('frozen') is True}")
EOF

Repository: Aureliolo/synthorg

Length of output: 234


🏁 Script executed:

# Check if there are any tests that verify frozen snapshots are required
rg "frozen" tests/unit/versioning/test_models.py -B 2 -A 5

Repository: Aureliolo/synthorg

Length of output: 725


🏁 Script executed:

# Check if there's any documentation about the content_hash format requirement
rg "SHA-256" src/synthorg/versioning/ -B 2 -A 2

Repository: Aureliolo/synthorg

Length of output: 2902


🏁 Script executed:

# Verify the test that checks frozen snapshot doesn't prevent mutable snapshots
cat -n tests/unit/versioning/test_models.py | sed -n '56,61p'

Repository: Aureliolo/synthorg

Length of output: 338


🏁 Script executed:

# Check if there's a test that tries to create VersionSnapshot with mutable snapshot
rg "VersionSnapshot.*snapshot.*=" tests/unit/versioning/test_models.py -A 3

Repository: Aureliolo/synthorg

Length of output: 44


🏁 Script executed:

# Verify compute_content_hash always produces 64-char hex
python3 << 'EOF'
import hashlib
import json

test_data = {"name": "test", "value": 42}
canonical = json.dumps(test_data, sort_keys=True, default=str)
digest = hashlib.sha256(canonical.encode()).hexdigest()
print(f"Hash: {digest}")
print(f"Length: {len(digest)}")
print(f"All lowercase hex: {all(c in '0123456789abcdef' for c in digest)}")
EOF

Repository: Aureliolo/synthorg

Length of output: 167


🏁 Script executed:

# Check if there are any places where VersionSnapshot is constructed with anything but computed content_hash
rg "VersionSnapshot\(" src/synthorg/versioning/ -A 5 | grep -E "(content_hash|VersionSnapshot)"

Repository: Aureliolo/synthorg

Length of output: 368


Validate versioning invariants at the model boundary to prevent downstream failures.

The model currently accepts any BaseModel for snapshot, any non-blank string for content_hash, and any aware timezone for saved_at. This allows callers to construct invalid history records that fail only later during SQLiteVersionRepository.save_version() when the database rejects non-UTC timestamps via the SQL CHECK constraint.

🛡️ Proposed fix
+import re
+from datetime import timedelta
+
-from pydantic import AwareDatetime, BaseModel, ConfigDict, Field
+from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, model_validator
@@
+_SHA256_HEX_RE = re.compile(r"^[0-9a-f]{64}$")
+
+
 class VersionSnapshot[T: BaseModel](BaseModel):
@@
     snapshot: T = Field(description="Full frozen entity model at this version")
     saved_by: NotBlankStr = Field(description="Actor that triggered the snapshot")
     saved_at: AwareDatetime = Field(description="When the snapshot was captured")
+
+    `@model_validator`(mode="after")
+    def _validate_invariants(self) -> "VersionSnapshot[T]":
+        if _SHA256_HEX_RE.fullmatch(str(self.content_hash)) is None:
+            msg = "content_hash must be a 64-character lowercase hex digest"
+            raise ValueError(msg)
+        if self.snapshot.model_config.get("frozen") is not True:
+            msg = "snapshot must be a frozen Pydantic model"
+            raise ValueError(msg)
+        if self.saved_at.utcoffset() != timedelta(0):
+            msg = "saved_at must be UTC"
+            raise ValueError(msg)
+        return self

Enforce these invariants per the coding guidelines: frozen Pydantic models for config/identity, validate data at system boundaries.

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

In `@src/synthorg/versioning/models.py` around lines 46 - 55, The model currently
accepts snapshots, content_hashes, and timestamps that can violate repository
invariants; add Pydantic validators to enforce them at the model boundary: add a
field validator for snapshot (named "snapshot") that asserts
isinstance(snapshot, BaseModel) and that snapshot.model_config.get("frozen",
False) is True (raise ValueError otherwise); add a field validator for
content_hash that asserts it's a 64-character lowercase hex string (match
r'^[0-9a-f]{64}$'); add a field validator for saved_at that asserts
saved_at.tzinfo is not None and saved_at.tzinfo.utcoffset(saved_at) ==
datetime.timedelta(0) (i.e. UTC); keep the class model_config as shown to remain
frozen and raise clear ValueErrors so invalid records are rejected before
reaching SQLiteVersionRepository.save_version().

Comment on lines +163 to +173
@pytest.mark.unit
def test_added_change_new_value_present(self) -> None:
c = IdentityFieldChange(
field_path="settings.timeout",
change_type="added",
old_value=None,
new_value='"30"',
)
assert c.change_type == "added"
assert c.old_value is 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.

🧹 Nitpick | 🔵 Trivial

Minor: test case duplicated in parametrized matrix.

test_added_change_new_value_present (lines 164-172) tests the same valid "added" case that's already covered in the parametrized test_change_invariant_matrix at line 232. Consider removing this standalone test to reduce duplication, or keep it if you prefer the explicit named coverage for readability.

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

In `@tests/unit/engine/identity/test_diff.py` around lines 163 - 173, The test
test_added_change_new_value_present duplicates coverage already provided by the
parametrized test_change_invariant_matrix; remove the redundant standalone test
(the function named test_added_change_new_value_present) from
tests/unit/engine/identity/test_diff.py to avoid duplication, or alternatively
keep it but add a comment explaining why the explicit case is needed and/or mark
it with pytest.mark.explicit to indicate intentional overlap; update imports or
test markers only if necessary to keep the test suite green.

Comment on lines +184 to +203
@pytest.mark.unit
@given(
name=st.text(min_size=1, max_size=50),
value=st.integers(min_value=0, max_value=10_000),
)
@settings(max_examples=100)
def test_same_model_same_hash(self, name: str, value: int) -> None:
model = _Simple(name=name, value=value)
h1 = compute_content_hash(model)
h2 = compute_content_hash(model)
assert h1 == h2

@pytest.mark.unit
@given(
name_a=st.text(min_size=1, max_size=50),
value_a=st.integers(min_value=0, max_value=10_000),
name_b=st.text(min_size=1, max_size=50),
value_b=st.integers(min_value=0, max_value=10_000),
)
@settings(max_examples=100)
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n '@settings\(' tests/unit/versioning/test_service.py
rg -n 'register_profile|HYPOTHESIS_PROFILE' tests/conftest.py

Repository: Aureliolo/synthorg

Length of output: 479


Remove hard-coded max_examples to preserve configured Hypothesis profiles.

The @settings(max_examples=100) decorators at lines 189 and 203 override the repository's ci/dev/fuzz profiles configured in tests/conftest.py, preventing these properties from scaling with HYPOTHESIS_PROFILE. Remove the decorators entirely; the configured profiles (via HYPOTHESIS_PROFILE env var) will control example counts automatically.

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

In `@tests/unit/versioning/test_service.py` around lines 184 - 203, The tests
test_same_model_same_hash and the subsequent property test are explicitly
setting `@settings`(max_examples=100), which overrides the project's Hypothesis
profiles; remove the `@settings`(max_examples=100) decorators from those test
functions (look for the decorator above def test_same_model_same_hash and the
one above the next property-based test) so Hypothesis will use the configured
profiles (HYPOTHESIS_PROFILE) defined in tests/conftest.py.

Generic versioning infrastructure (VersionSnapshot[T], VersionRepository[T],
SQLiteVersionRepository[T], VersioningService[T]) with agent identity as the
first consumer.

- New synthorg.versioning package: VersionSnapshot[T] frozen model, SHA-256
  content-addressable hashing, VersioningService with snapshot deduplication
- Generic VersionRepository[T] protocol + SQLiteVersionRepository[T] impl
  (parameterised by table_name + ser/deser callables, INSERT OR IGNORE)
- Observability events: versioning.py constants for all repo + service ops
- Agent identity diff: IdentityFieldChange, AgentIdentityDiff, compute_diff()
  with recursive dot-notation field paths
- schema.sql: agent_identity_versions table + two indexes
- persistence.protocol + sqlite.backend: identity_versions property wired
- ReviewGateService._record_decision: best-effort charter_version metadata
  injection into DecisionRecord via metadata field (no schema migration)
- AgentRegistryService: snapshot_if_changed called on register/update_identity
- Design spec: agents.md Identity Versioning section, engine.md #1076 note
- 72 tests (unit + integration): models, hashing, service, generic repo CRUD,
  AgentIdentity round-trip, end-to-end versioning + decision metadata

Closes #1076
The grep in REDIR extraction returned exit code 1 on no-match (e.g.
when the only redirect was 2>&1 where & is excluded from the target
character class). With set -euo pipefail this caused the hook to exit
1, which Claude Code shows as 'PreToolUse:Bash hook error' in the UI
while still allowing the command.

Three fixes applied:
1. Append '|| true' to REDIR extraction to suppress grep's no-match
   exit under set -e.
2. Guard deny() with '-n REDIR' -- empty REDIR means the target was
   an fd redirect whose & character could not be captured; those are
   always safe.
3. Explicitly allow /dev/null targets since discarding stderr is not
   a file write.
The redirect-detection regex scanned the entire bash command including
string arguments like -m "...". Any > inside a commit message (arrows
like ->, regex syntax like [^|&;<>]) triggered the check and blocked
the command.

Strip double-quoted string contents into COMMAND_FOR_REDIR before all
redirect checks. Real redirects (echo "" > file.txt) still fire because
the > is outside the quotes; harmless > inside -m "..." args are gone.

Applied to the output-redirection, echo/printf, and tee checks.
- save_version returns bool (True=inserted, False=INSERT OR IGNORE drop)
- VersioningService re-fetches on concurrent duplicate (False return)
- SQLiteVersionRepository: return cursor.rowcount > 0; split
  _deserialize_row errors into json_corrupt / schema_drift / unexpected
- AgentIdentityDiff.summary promoted to @computed_field
- IdentityFieldChange: @model_validator enforces added/removed/modified
  invariants (old_value/new_value nullability)
- AgentIdentityDiff: @model_validator rejects from_version == to_version
- review_gate: narrow except Exception -> except QueryError;
  metadata = {"charter_version_lookup_failed": True} on failure
- hr.registry: narrow except Exception -> except PersistenceError
  (local import to break circular dep); expand _snapshot docstring
- test_review_gate: wire identity_versions AsyncMock in mock persistence
- fakes_backend: FakeVersionRepository.save_version returns bool
- tests: concurrent-save, model_validator, deserialization error, vendor
  name, table name edge cases, QueryError failure integration test,
  Hypothesis snapshot determinism properties
- CLAUDE.md: add versioning events to Logging section
- docs/reference: add versioning/ to Package Structure
…abbit

Critical:
- Race condition retry in VersioningService when concurrent insert
  loses to different content (compare content_hash, retry once)
- copy.deepcopy(snapshot) before await in snapshot_if_changed to
  prevent mutation between hash computation and persistence

Major:
- Catch-all Exception handler in _deserialize_row for unconstrained
  deserializer callbacks (normalized to QueryError)
- Wrap serializer callback errors in save_version (QueryError)
- ORDER BY version DESC LIMIT 1 on get_by_content_hash query
- compute_diff agent_id: str changed to NotBlankStr
- UTC CHECK constraint on agent_identity_versions.saved_at
- agents.md failure-flag doc update (charter_version_lookup_failed)
- registry.py: fix "fire-and-forget" docstring (code awaits)
- registry.py: fix _UPDATABLE_FIELDS comment (list allowed, not blocked)
- claude-reference.md: add "agent identity versions" to persistence list
- check_bash_no_write.sh: sed -E for macOS portability
- test_events.py: vendor names moved to conftest constant
- review_gate.py: decompose _record_decision (130 lines) into 4 methods

Medium/Minor:
- Parametrize IdentityFieldChange invariant tests
- Fix docstring/marker mismatch in test_identity_version_repo.py
- Use hypothesis.assume() instead of early return
- Improve version mismatch error message (show both versions)
- Add Google-style docstring to _diff_dicts
- Add unit tests for callback error paths (deserialize + serialize)
- Move idx_sv_hostname index adjacent to ssrf_violations table
- Add UTC CHECK constraint on saved_at in both test schemas to match
  production schema.sql
- Fix REDIR extraction to strip leading whitespace so /dev/null
  exemption works correctly
- agents.md: clarify only register/update_identity are versioned,
  not update_status (status is transient runtime state)
- diff.py: enforce from_version < to_version (not just inequality)
- registry.py: thread saved_by from register/update_identity callers
  instead of hardcoding "system"
- version_repo.py: validate limit/offset non-negative in list_versions
- version_repo.py: re-raise MemoryError/RecursionError before catch-all
  Exception handlers in deserialize and serialize paths
- models.py: add content_hash (64-char hex) and saved_at (UTC) field
  validators on VersionSnapshot
- test_diff.py: remove duplicate test_added_change_new_value_present
  (covered by parametrized matrix), add test_reversed_versions_raises
- test_service.py: remove @settings(max_examples=100) overrides so
  Hypothesis uses configured profiles from conftest.py
- agents.md: clarify only register/update_identity are versioned,
  not update_status (status is transient runtime state)
- diff.py: enforce from_version < to_version (not just inequality)
- registry.py: thread saved_by from register() callers; use
  "update_identity:<agent_id>" for update_identity() snapshots
- version_repo.py: validate limit/offset non-negative in list_versions
- version_repo.py: re-raise MemoryError/RecursionError before catch-all
  Exception handlers in deserialize and serialize paths
- models.py: add content_hash (64-char hex) and saved_at (UTC) field
  validators on VersionSnapshot
- test_diff.py: remove duplicate test_added_change_new_value_present
  (covered by parametrized matrix), add test_reversed_versions_raises
- test_service.py: remove @settings(max_examples=100) overrides so
  Hypothesis uses configured profiles from conftest.py
@Aureliolo Aureliolo force-pushed the feat/agent-identity-versioning branch from a14722e to f2c4446 Compare April 7, 2026 21:46
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview April 7, 2026 21:47 — 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: 9

Caution

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

⚠️ Outside diff range comments (1)
src/synthorg/engine/review_gate.py (1)

376-384: ⚠️ Potential issue | 🟡 Minor

Use PEP 758 except syntax (comma-separated, no parentheses).

Per coding guidelines, Python 3.14+ code should use except A, B: instead of except (A, B):.

🐛 Fix except syntax
-        except (QueryError, DuplicateRecordError) as exc:
+        except QueryError, DuplicateRecordError as exc:

As per coding guidelines: "Use PEP 758 except syntax: use except A, B: (comma-separated, no parentheses) instead of except (A, B): -- ruff enforces this on Python 3.14."

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

In `@src/synthorg/engine/review_gate.py` around lines 376 - 384, Replace the
tuple-style except block "except (QueryError, DuplicateRecordError) as exc:"
with the PEP 758 comma-separated form "except QueryError, DuplicateRecordError
as exc:" around the logger.exception call that logs
APPROVAL_GATE_DECISION_RECORD_FAILED (which includes task_id, decided_by,
approved, error_type, error) so the same exception handling and logging behavior
is preserved but uses the new syntax.
♻️ Duplicate comments (7)
scripts/check_bash_no_write.sh (2)

58-70: ⚠️ Potential issue | 🔴 Critical

head -1 bypasses detection of multiple redirects.

Line 63 extracts only the first matched redirect target via head -1. A command like true > /dev/null; cat > out passes the check because only /dev/null (safe) is evaluated—the second redirect out (unsafe) is never inspected.

Iterate over all extracted redirect targets and deny if any is unsafe:

Suggested fix
 if printf '%s\n' "$COMMAND_FOR_REDIR" | grep -qE '(^|[^|&;])\s*>>?\s*"?[^-]'; then
-    # Extract redirect target.  grep returns 1 when there is no match (e.g.
-    # ">&1" where & is excluded from [^|&;<>]+); || true prevents set -e from
-    # exiting the script when extraction yields no output.
-    REDIR=$(printf '%s\n' "$COMMAND_FOR_REDIR" | grep -oE '>>?\s*"?[^|&;<>]+' | head -1 | sed -E 's/^>>?\s*["'"'"']*//' || true)
-    # Only deny if a non-empty target was extracted and it is not a file
-    # descriptor.  Empty REDIR means the target was &N (fd redirect such as
-    # 2>&1) whose & could not be captured; those are always safe.
+    # Extract all redirect targets and check each one.
     FD_RE='^&[0-9]+$'
-    if [[ -n "$REDIR" && "$REDIR" != "/dev/null" && ! "$REDIR" =~ $FD_RE ]]; then
-        deny "Do not use shell redirects (> or >>) to write files. Use the Write tool to create new files or the Edit tool to modify existing files. Never use Bash for file creation or modification."
-    fi
+    while IFS= read -r REDIR; do
+        # Strip leading >>? and optional quotes
+        REDIR=$(printf '%s' "$REDIR" | sed -E 's/^>>?\s*["'"'"']*//')
+        # Trim leading/trailing whitespace
+        REDIR=$(printf '%s' "$REDIR" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
+        if [[ -n "$REDIR" && "$REDIR" != "/dev/null" && ! "$REDIR" =~ $FD_RE ]]; then
+            deny "Do not use shell redirects (> or >>) to write files. Use the Write tool to create new files or the Edit tool to modify existing files. Never use Bash for file creation or modification."
+        fi
+    done < <(printf '%s\n' "$COMMAND_FOR_REDIR" | grep -oE '>>?\s*"?[^|&;<>]+' || true)
 fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/check_bash_no_write.sh` around lines 58 - 70, The current REDIR
extraction uses grep -oE piped to head -1, so only the first redirect is checked
and later unsafe redirects (e.g., multiple redirects in COMMAND_FOR_REDIR) are
missed; modify the logic to iterate over all matches from printf '%s\n'
"$COMMAND_FOR_REDIR" | grep -oE '>>?\s*"?[^|&;<>]+' (instead of taking only the
first with head -1), normalize each match the same way you currently do (strip
leading >>? and quotes), and for each extracted REDIR check that it is
non-empty, not "/dev/null", and does not match FD_RE='^&[0-9]+$' — call
deny(...) as soon as any unsafe target is found (using a loop such as a while
read or for loop over the grep output to preserve set -e behavior).

48-53: ⚠️ Potential issue | 🔴 Critical

Quote stripping creates a security bypass for file-write detection.

The pattern sed 's/"[^"]*"/""/g' blanks double-quoted content, causing commands like bash -lc "echo hi > out.txt" and printf "%s\n" "$(tee out.txt)" to evade all redirect/tee checks—their destructive payloads become invisible after sanitization.

This is the exact attack vector the hook must prevent. Scan the original command to close this bypass:

Suggested fix
-# Strip double-quoted string contents before redirect checks so that >
-# inside -m "..." commit messages, script excerpts, or other string
-# arguments does not trigger false positives.  Intentionally simple:
-# does not handle escaped quotes inside strings, which is fine for the
-# shell commands we need to guard against.
-COMMAND_FOR_REDIR=$(printf '%s\n' "$COMMAND" | sed 's/"[^"]*"/""/g')
+# Scan the original command for redirects.  Quote-aware suppression would
+# need to preserve executable payloads (bash -c "...", command substitutions)
+# which is non-trivial; accept false positives on quoted '>' in messages
+# rather than allow write bypasses.
+COMMAND_FOR_REDIR=$COMMAND
tests/conftest.py (1)

110-115: ⚠️ Potential issue | 🟠 Major

Move vendor denylist literals out of test code.

Lines [110]-[115] still embed real vendor names inside tests/, which violates the repo’s vendor-agnostic test policy.

🔧 Proposed fix (load denylist from an allowed location)
-# ── Vendor-agnostic guardrail ───────────────────────────────────
-# Centralized set of disallowed vendor identifiers so tests that
-# scan for vendor names do not embed the literals themselves.
-DISALLOWED_VENDOR_NAMES: frozenset[str] = frozenset(
-    {"anthropic", "openai", "claude", "gpt", "gemini", "mistral"}
-)
+# ── Vendor-agnostic guardrail ───────────────────────────────────
+# Load disallowed vendor identifiers from an allowed location
+# (for example, `.claude/vendor-name-denylist.txt`) so tests
+# remain vendor-agnostic.
+_denylist_path = Path(".claude/vendor-name-denylist.txt")
+DISALLOWED_VENDOR_NAMES: frozenset[str] = frozenset(
+    line.strip().lower()
+    for line in _denylist_path.read_text(encoding="utf-8").splitlines()
+    if line.strip() and not line.lstrip().startswith("#")
+)

As per coding guidelines: "Vendor-agnostic everywhere: NEVER use real vendor names ... in project-owned code ... tests" and "Tests must use test-provider, test-small-001, etc. instead of real vendor names."

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

In `@tests/conftest.py` around lines 110 - 115, DISALLOWED_VENDOR_NAMES currently
hardcodes real vendor strings; replace this by loading the denylist from the
approved shared resource (e.g., a test fixtures loader or config helper) instead
of embedding literals. Update the definition of DISALLOWED_VENDOR_NAMES to call
the central loader (or read the approved denylist constant) and ensure tests
reference that symbol; remove the hardcoded frozenset of
{"anthropic","openai","claude","gpt","gemini","mistral"} and use the external
source so tests remain vendor-agnostic.
src/synthorg/hr/registry.py (2)

83-84: ⚠️ Potential issue | 🔴 Critical

Out-of-lock snapshotting can still leave persisted history behind the live registry.

Two overlapping update_identity() calls can still race here: both mutate _agents under self._lock, then both compute the next snapshot after the lock is released. The loser now re-fetches the winner's row, but _agents[key] keeps the later identity in memory, so get_latest_version() and review-gate charter_version metadata can lag the actual registry state. Serialize snapshotting per agent or move it inside the critical section.

Also applies to: 271-271, 279-300

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

In `@src/synthorg/hr/registry.py` around lines 83 - 84, The race occurs in
update_identity: after mutating self._agents under self._lock, the code releases
the lock and then calls await self._snapshot(identity, ...), allowing
overlapping updates to compute snapshots out of order and leave persisted
history behind the live registry; fix by serializing snapshotting per agent or
moving the snapshot call inside the critical section so the snapshot sees the
same state you just wrote—either call await self._snapshot(...) while still
holding self._lock (in update_identity) or implement a per-agent asyncio.Lock
keyed by identity key and acquire it around both the mutation of self._agents
and the call to _snapshot; ensure get_latest_version and charter_version
metadata observe the updated in-memory _agents and persisted snapshot
atomically.

49-54: ⚠️ Potential issue | 🟠 Major

saved_by is still synthetic on the mutation APIs.

register() still falls back to "system", and update_identity() has no keyword slot for the caller, so it always persists update_identity:{agent_id}. The included src/synthorg/hr/hiring_service.py and src/synthorg/api/bootstrap.py snippets still call register(identity) without saved_by, so the new history cannot answer who actually changed the charter. Make saved_by explicit on both mutators and thread the real actor through.

Also applies to: 218-221, 271-271

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

In `@src/synthorg/hr/registry.py` around lines 49 - 54, The register() method
currently defaults saved_by="system" and update_identity() lacks a saved_by
parameter so history entries always show synthetic actor; change register(self,
identity, *, saved_by: str) to require a caller-provided saved_by (remove the
default), add a saved_by: str keyword argument to update_identity(...) and
propagate that value into any persisted history keys/messages (e.g., where you
write "update_identity:{agent_id}" include the saved_by or record it alongside
the entry), and update all call sites (notably in
src/synthorg/hr/hiring_service.py and src/synthorg/api/bootstrap.py) to pass the
real actor string through to register/update_identity so the real actor is
stored in history.
src/synthorg/versioning/models.py (1)

51-60: ⚠️ Potential issue | 🟠 Major

Enforce that snapshot itself is frozen.

ConfigDict(frozen=True) only freezes rebinding of the snapshot field; it does not prevent callers from passing a mutable BaseModel and changing it later. That breaks the "immutable historical snapshot" contract and can let content_hash diverge from the serialized snapshot across awaits. Add a validator that rejects non-frozen Pydantic models. As per coding guidelines, "Use frozen Pydantic models for config/identity."

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

In `@src/synthorg/versioning/models.py` around lines 51 - 60, Add a Pydantic field
validator on the snapshot field to reject any mutable BaseModel instances:
import BaseModel and field_validator from pydantic, add a
`@field_validator`("snapshot") method (on the same model class that defines
model_config and snapshot) that checks if value is an instance of BaseModel and
if so verifies value.model_config.get("frozen", False) is True, otherwise raise
a ValueError explaining snapshot must be a frozen Pydantic model; keep allowing
non-BaseModel values unchanged. Ensure the validator name references "snapshot"
and does not alter the value.
src/synthorg/persistence/sqlite/version_repo.py (1)

151-151: ⚠️ Potential issue | 🟠 Major

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

These tuple-style handlers are still using the old parenthesized syntax, so this file will keep tripping the Python 3.14/ruff rule.

#!/bin/bash
# Expect no matches once the file is updated to PEP 758 syntax.
rg -n 'except \(' src/synthorg/persistence/sqlite/version_repo.py

As per coding guidelines, "This project uses Python 3.14+ with PEP 758 except syntax: use except A, B: (no parentheses) instead of except (A, B): -- ruff enforces this on Python 3.14."

Also applies to: 226-226, 254-254, 279-279, 304-304, 338-338, 360-360, 377-377

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

In `@src/synthorg/persistence/sqlite/version_repo.py` at line 151, Replace the
parenthesized tuple-style except handlers with PEP 758 form by removing the
surrounding parentheses and separating the exception types with a comma before
the as-clause (e.g., change the except that currently names ValueError and
KeyError to the PEP 758 comma-separated form); apply the same change to all
other except lines flagged (the handlers mentioning ValueError/KeyError and the
other similar except clauses around the VersionRepo methods in this file) and
run the provided ripgrep check to verify no remaining "except (" occurrences.
🤖 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/agents.md`:
- Around line 171-173: The markdown line containing "#1113" is being parsed as a
heading; update the text around the versioning note (the sentence mentioning
src/synthorg/versioning/) so the issue reference doesn't start a new ATX
heading—either place the issue reference at the end of the previous line, escape
the hash (e.g., "\#1113"), or wrap it in inline code/backticks (`#1113`) so the
reference remains part of the sentence instead of becoming a heading.

In `@docs/design/engine.md`:
- Around line 737-744: Update the wording to reflect that persistence is
best-effort: change the sentence about ReviewGateService so it states that
ReviewGateService looks up the executing agent's latest identity version and
injects charter_version ({agent_id, version, content_hash}) into
DecisionRecord.metadata on a best-effort basis; if the lookup fails, log a
WARNING and still attempt an append (but the append is not guaranteed to
succeed), and explicitly state the fallback behavior for metadata (e.g., include
the best-effort charter_version when available, otherwise omit it or set it to a
clearly marked "unavailable" sentinel). Ensure the symbols VersioningService[T],
ReviewGateService, DecisionRecord.metadata, and charter_version are referenced
in the updated phrasing.

In `@src/synthorg/engine/identity/diff.py`:
- Around line 44-57: The validator _validate_change_invariants on
IdentityFieldChange currently allows both old_value and new_value to be None for
change_type "added" or "removed"; update the checks so that for change_type ==
"added" you require old_value is None AND new_value is not None, and for
change_type == "removed" you require old_value is not None AND new_value is
None; keep the "modified" check requiring both non-None and adjust the raised
ValueError messages to reflect the tightened invariants so they state the
required non-None side (use the existing function name
_validate_change_invariants and fields change_type, old_value, new_value to
locate and modify the logic).

In `@src/synthorg/engine/review_gate.py`:
- Around line 298-305: The current loop in review_gate.py builds a tuple on each
iteration (result = (*result, stripped)), causing O(n²) behavior; change result
to a list (e.g., result_list) and use result_list.append(stripped) when stripped
not in seen, then convert to tuple before returning to preserve the return type;
keep the existing seen set, task.acceptance_criteria loop, and stripped logic
unchanged except for using append and final tuple(result_list) as the return.

In `@src/synthorg/persistence/sqlite/version_repo.py`:
- Around line 129-177: The deserialization exception handling (in
version_repo.py around the except blocks for json.JSONDecodeError,
ValidationError, (ValueError, KeyError), and the generic Exception) currently
interpolates the exception object into log fields and raised QueryError
messages, which may leak snapshot contents (e.g. ValidationError.input_value);
change each handler to sanitize outputs by (1) not including exc or exc.args in
the formatted msg or structured logger fields, (2) log only a safe identifier
like type(exc).__name__ or a fixed reason string (e.g. "json_corrupt",
"schema_drift", "unexpected", "callback_error"), and (3) raise QueryError with a
generic sanitized message that omits exception internals while preserving
context and the safe reason; update all logger.exception/raise QueryError usages
referenced by VERSION_FETCH_FAILED and the deserialize_snapshot callback
handlers to follow this pattern.

In `@src/synthorg/versioning/service.py`:
- Around line 145-163: The current flow drops the caller's snapshot if the
second attempt (retry) loses the race because it returns whatever
get_latest_version() returns; instead implement a retry loop around
_build_snapshot + _repo.save_version (use _build_snapshot, the retry variable,
and _repo.save_version) that retries building and saving until save_version
returns inserted=True (or until a reasonable max attempts) and only then return
that snapshot; if you must stop after max attempts, return the last
locally-built snapshot or surface an explicit error rather than returning
get_latest_version() so the caller's snapshot is not silently discarded.

In `@tests/unit/persistence/sqlite/test_identity_version_repo.py`:
- Around line 139-147: Update the test_idempotent_save_with_real_identity test
to assert the new save_version return contract: call await repo.save_version(v)
twice storing results (e.g., first_result = await repo.save_version(v);
second_result = await repo.save_version(v)), assert first_result is truthy
(True) and second_result is False, and keep the existing assert await
repo.count_versions("agt-001") == 1; this ensures save_version's return value
(in SQLiteVersionRepository.save_version) is validated in addition to
count_versions.

In `@tests/unit/versioning/test_hashing.py`:
- Around line 9-18: Update the test Pydantic model configs to match production
validation by adding allow_inf_nan=False to the ConfigDict for both test
classes: modify the model_config in _Simple and _WithEnum (which currently set
model_config = ConfigDict(frozen=True)) to include allow_inf_nan=False (e.g.,
ConfigDict(frozen=True, allow_inf_nan=False)) so the tests exercise the same
validation behavior as frozen production models.

In `@tests/unit/versioning/test_models.py`:
- Around line 82-94: The current tests only exercise the base types
(NotBlankStr, AwareDatetime) and miss enforcing VersionSnapshot's SHA-256 regex
and UTC-only datetime validator; add explicit unit tests (or extend
test_blank_content_hash_rejected and test_naive_datetime_rejected) that call
_make_snapshot with: (1) a non-blank but malformed content_hash (e.g., not
matching the expected SHA-256 hex length) to assert ValidationError for the
SHA-256 regex on VersionSnapshot, and (2) an aware datetime with a non-UTC
tzinfo (or a UTC-offset-aware but not tz=UTC) to assert ValidationError from the
UTC-only validator on VersionSnapshot; reference VersionSnapshot,
_make_snapshot, the SHA-256 regex and the UTC-only validator when locating where
to add these tests.

---

Outside diff comments:
In `@src/synthorg/engine/review_gate.py`:
- Around line 376-384: Replace the tuple-style except block "except (QueryError,
DuplicateRecordError) as exc:" with the PEP 758 comma-separated form "except
QueryError, DuplicateRecordError as exc:" around the logger.exception call that
logs APPROVAL_GATE_DECISION_RECORD_FAILED (which includes task_id, decided_by,
approved, error_type, error) so the same exception handling and logging behavior
is preserved but uses the new syntax.

---

Duplicate comments:
In `@scripts/check_bash_no_write.sh`:
- Around line 58-70: The current REDIR extraction uses grep -oE piped to head
-1, so only the first redirect is checked and later unsafe redirects (e.g.,
multiple redirects in COMMAND_FOR_REDIR) are missed; modify the logic to iterate
over all matches from printf '%s\n' "$COMMAND_FOR_REDIR" | grep -oE
'>>?\s*"?[^|&;<>]+' (instead of taking only the first with head -1), normalize
each match the same way you currently do (strip leading >>? and quotes), and for
each extracted REDIR check that it is non-empty, not "/dev/null", and does not
match FD_RE='^&[0-9]+$' — call deny(...) as soon as any unsafe target is found
(using a loop such as a while read or for loop over the grep output to preserve
set -e behavior).

In `@src/synthorg/hr/registry.py`:
- Around line 83-84: The race occurs in update_identity: after mutating
self._agents under self._lock, the code releases the lock and then calls await
self._snapshot(identity, ...), allowing overlapping updates to compute snapshots
out of order and leave persisted history behind the live registry; fix by
serializing snapshotting per agent or moving the snapshot call inside the
critical section so the snapshot sees the same state you just wrote—either call
await self._snapshot(...) while still holding self._lock (in update_identity) or
implement a per-agent asyncio.Lock keyed by identity key and acquire it around
both the mutation of self._agents and the call to _snapshot; ensure
get_latest_version and charter_version metadata observe the updated in-memory
_agents and persisted snapshot atomically.
- Around line 49-54: The register() method currently defaults saved_by="system"
and update_identity() lacks a saved_by parameter so history entries always show
synthetic actor; change register(self, identity, *, saved_by: str) to require a
caller-provided saved_by (remove the default), add a saved_by: str keyword
argument to update_identity(...) and propagate that value into any persisted
history keys/messages (e.g., where you write "update_identity:{agent_id}"
include the saved_by or record it alongside the entry), and update all call
sites (notably in src/synthorg/hr/hiring_service.py and
src/synthorg/api/bootstrap.py) to pass the real actor string through to
register/update_identity so the real actor is stored in history.

In `@src/synthorg/persistence/sqlite/version_repo.py`:
- Line 151: Replace the parenthesized tuple-style except handlers with PEP 758
form by removing the surrounding parentheses and separating the exception types
with a comma before the as-clause (e.g., change the except that currently names
ValueError and KeyError to the PEP 758 comma-separated form); apply the same
change to all other except lines flagged (the handlers mentioning
ValueError/KeyError and the other similar except clauses around the VersionRepo
methods in this file) and run the provided ripgrep check to verify no remaining
"except (" occurrences.

In `@src/synthorg/versioning/models.py`:
- Around line 51-60: Add a Pydantic field validator on the snapshot field to
reject any mutable BaseModel instances: import BaseModel and field_validator
from pydantic, add a `@field_validator`("snapshot") method (on the same model
class that defines model_config and snapshot) that checks if value is an
instance of BaseModel and if so verifies value.model_config.get("frozen", False)
is True, otherwise raise a ValueError explaining snapshot must be a frozen
Pydantic model; keep allowing non-BaseModel values unchanged. Ensure the
validator name references "snapshot" and does not alter the value.

In `@tests/conftest.py`:
- Around line 110-115: DISALLOWED_VENDOR_NAMES currently hardcodes real vendor
strings; replace this by loading the denylist from the approved shared resource
(e.g., a test fixtures loader or config helper) instead of embedding literals.
Update the definition of DISALLOWED_VENDOR_NAMES to call the central loader (or
read the approved denylist constant) and ensure tests reference that symbol;
remove the hardcoded frozenset of
{"anthropic","openai","claude","gpt","gemini","mistral"} and use the external
source so tests remain vendor-agnostic.
🪄 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: c8161270-fe31-44a4-9420-a53ae53cd231

📥 Commits

Reviewing files that changed from the base of the PR and between a14722e and f2c4446.

📒 Files selected for processing (35)
  • CLAUDE.md
  • docs/design/agents.md
  • docs/design/engine.md
  • docs/reference/claude-reference.md
  • scripts/check_bash_no_write.sh
  • src/synthorg/engine/identity/__init__.py
  • src/synthorg/engine/identity/diff.py
  • src/synthorg/engine/review_gate.py
  • src/synthorg/hr/registry.py
  • src/synthorg/observability/events/versioning.py
  • src/synthorg/persistence/protocol.py
  • src/synthorg/persistence/sqlite/backend.py
  • src/synthorg/persistence/sqlite/schema.sql
  • src/synthorg/persistence/sqlite/version_repo.py
  • src/synthorg/persistence/version_repo.py
  • src/synthorg/versioning/__init__.py
  • src/synthorg/versioning/hashing.py
  • src/synthorg/versioning/models.py
  • src/synthorg/versioning/service.py
  • tests/conftest.py
  • tests/integration/engine/identity/__init__.py
  • tests/integration/engine/identity/test_identity_versioning.py
  • tests/unit/api/fakes_backend.py
  • tests/unit/engine/identity/__init__.py
  • tests/unit/engine/identity/test_diff.py
  • tests/unit/engine/test_review_gate.py
  • tests/unit/observability/test_events.py
  • tests/unit/persistence/sqlite/test_identity_version_repo.py
  • tests/unit/persistence/sqlite/test_migrations.py
  • tests/unit/persistence/sqlite/test_version_repo.py
  • tests/unit/persistence/test_protocol.py
  • tests/unit/versioning/__init__.py
  • tests/unit/versioning/test_hashing.py
  • tests/unit/versioning/test_models.py
  • tests/unit/versioning/test_service.py

Comment on lines +171 to +173
The versioning system lives in `src/synthorg/versioning/` and is intentionally
entity-agnostic so it can be reused for other versioned entity types (tracked in
#1113):
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

Keep #1113 from starting an H1.

Because Line 173 begins with #1113, Markdown parses it as an ATX heading instead of part of the sentence. Keep the issue reference on the previous line or escape the hash.

🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 173-173: No space after hash on atx style heading

(MD018, no-missing-space-atx)

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

In `@docs/design/agents.md` around lines 171 - 173, The markdown line containing
"#1113" is being parsed as a heading; update the text around the versioning note
(the sentence mentioning src/synthorg/versioning/) so the issue reference
doesn't start a new ATX heading—either place the issue reference at the end of
the previous line, escape the hash (e.g., "\#1113"), or wrap it in inline
code/backticks (`#1113`) so the reference remains part of the sentence instead
of becoming a heading.

Comment on lines +737 to +744
**Identity versioning (#1076, implemented):** Agent identities
are versioned as first-class artifacts via the generic
``VersioningService[T]`` infrastructure. ``ReviewGateService``
looks up the executing agent's latest identity version and injects
``charter_version: {agent_id, version, content_hash}`` into the
``DecisionRecord.metadata`` field (best-effort; lookup failure
is logged at WARNING and the decision record is still written).
See ``docs/design/agents.md`` for the full design.
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

Tighten guarantee wording for decision-record persistence.

Lines [742]-[743] currently imply the record is always written; implementation only guarantees best-effort append. Please phrase this as “append is still attempted” and include the explicit fallback metadata behavior.

📝 Suggested wording update
-      ``DecisionRecord.metadata`` field (best-effort; lookup failure
-      is logged at WARNING and the decision record is still written).
+      ``DecisionRecord.metadata`` field (best-effort; lookup failure
+      is logged at WARNING, metadata is set to
+      ``{"charter_version_lookup_failed": True}``, and decision-record
+      append is still attempted).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/design/engine.md` around lines 737 - 744, Update the wording to reflect
that persistence is best-effort: change the sentence about ReviewGateService so
it states that ReviewGateService looks up the executing agent's latest identity
version and injects charter_version ({agent_id, version, content_hash}) into
DecisionRecord.metadata on a best-effort basis; if the lookup fails, log a
WARNING and still attempt an append (but the append is not guaranteed to
succeed), and explicitly state the fallback behavior for metadata (e.g., include
the best-effort charter_version when available, otherwise omit it or set it to a
clearly marked "unavailable" sentinel). Ensure the symbols VersioningService[T],
ReviewGateService, DecisionRecord.metadata, and charter_version are referenced
in the updated phrasing.

Comment on lines +129 to +177
except json.JSONDecodeError as exc:
context = f"{data.get('entity_id', '?')}@v{data.get('version', '?')}"
msg = f"Corrupt JSON in version snapshot {context!r}: {exc}"
logger.exception(
VERSION_FETCH_FAILED,
table=self._table,
context=context,
error=str(exc),
reason="json_corrupt",
)
raise QueryError(msg) from exc
except ValidationError as exc:
context = f"{data.get('entity_id', '?')}@v{data.get('version', '?')}"
msg = f"Schema mismatch in version snapshot {context!r}: {exc}"
logger.exception(
VERSION_FETCH_FAILED,
table=self._table,
context=context,
error=str(exc),
reason="schema_drift",
)
raise QueryError(msg) from exc
except (ValueError, KeyError) as exc:
context = f"{data.get('entity_id', '?')}@v{data.get('version', '?')}"
msg = f"Failed to deserialize version snapshot {context!r}: {exc}"
logger.exception(
VERSION_FETCH_FAILED,
table=self._table,
context=context,
error=str(exc),
reason="unexpected",
)
raise QueryError(msg) from exc
except MemoryError, RecursionError:
raise
except Exception as exc:
# Catch-all for unconstrained deserialize_snapshot callbacks
# (e.g. TypeError, AttributeError) so all callback errors
# are normalized to QueryError.
context = f"{data.get('entity_id', '?')}@v{data.get('version', '?')}"
msg = f"Failed to deserialize version snapshot {context!r}: {exc}"
logger.exception(
VERSION_FETCH_FAILED,
table=self._table,
context=context,
error=str(exc),
reason="callback_error",
)
raise QueryError(msg) from exc
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

Sanitize deserialization failures before logging/raising them.

ValidationError and callback exceptions here can carry parts of the stored snapshot (input_value=...). Interpolating exc into the QueryError message and structured log fields risks leaking versioned snapshot contents on corrupt/schema-drift rows.

🛠️ Example of the safer pattern
-            msg = f"Schema mismatch in version snapshot {context!r}: {exc}"
+            msg = f"Schema mismatch in version snapshot {context!r}"
             logger.exception(
                 VERSION_FETCH_FAILED,
                 table=self._table,
                 context=context,
-                error=str(exc),
+                error_type=type(exc).__name__,
                 reason="schema_drift",
             )
             raise QueryError(msg) from exc

Apply the same treatment to the other deserialization branches.

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

In `@src/synthorg/persistence/sqlite/version_repo.py` around lines 129 - 177, The
deserialization exception handling (in version_repo.py around the except blocks
for json.JSONDecodeError, ValidationError, (ValueError, KeyError), and the
generic Exception) currently interpolates the exception object into log fields
and raised QueryError messages, which may leak snapshot contents (e.g.
ValidationError.input_value); change each handler to sanitize outputs by (1) not
including exc or exc.args in the formatted msg or structured logger fields, (2)
log only a safe identifier like type(exc).__name__ or a fixed reason string
(e.g. "json_corrupt", "schema_drift", "unexpected", "callback_error"), and (3)
raise QueryError with a generic sanitized message that omits exception internals
while preserving context and the safe reason; update all logger.exception/raise
QueryError usages referenced by VERSION_FETCH_FAILED and the
deserialize_snapshot callback handlers to follow this pattern.

Comment on lines +145 to +163
# Different content won the race -- retry with next version.
retry = self._build_snapshot(
entity_id,
persisted,
content_hash,
frozen,
saved_by,
)
inserted = await self._repo.save_version(retry)
if inserted:
logger.info(
VERSION_SAVED,
entity_id=entity_id,
version=retry.version,
saved_by=saved_by,
)
return retry
# Second collision (extremely unlikely). Return persisted state.
return await self._repo.get_latest_version(entity_id)
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 drop the caller's snapshot after a second collision.

If the retry here also loses to a different writer, Line 163 returns whatever row is latest at that moment, so this distinct snapshot is still never versioned. Three concurrent updates from the same base version can hit this path.

🛠️ Suggested direction
-        retry = self._build_snapshot(
-            entity_id,
-            persisted,
-            content_hash,
-            frozen,
-            saved_by,
-        )
-        inserted = await self._repo.save_version(retry)
-        if inserted:
-            logger.info(
-                VERSION_SAVED,
-                entity_id=entity_id,
-                version=retry.version,
-                saved_by=saved_by,
-            )
-            return retry
-        # Second collision (extremely unlikely). Return persisted state.
-        return await self._repo.get_latest_version(entity_id)
+        current = persisted
+        while True:
+            retry = self._build_snapshot(
+                entity_id,
+                current,
+                content_hash,
+                frozen,
+                saved_by,
+            )
+            if await self._repo.save_version(retry):
+                logger.info(
+                    VERSION_SAVED,
+                    entity_id=entity_id,
+                    version=retry.version,
+                    saved_by=saved_by,
+                )
+                return retry
+            current = await self._repo.get_latest_version(entity_id)
+            if current is not None and current.content_hash == content_hash:
+                return current
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/versioning/service.py` around lines 145 - 163, The current flow
drops the caller's snapshot if the second attempt (retry) loses the race because
it returns whatever get_latest_version() returns; instead implement a retry loop
around _build_snapshot + _repo.save_version (use _build_snapshot, the retry
variable, and _repo.save_version) that retries building and saving until
save_version returns inserted=True (or until a reasonable max attempts) and only
then return that snapshot; if you must stop after max attempts, return the last
locally-built snapshot or surface an explicit error rather than returning
get_latest_version() so the caller's snapshot is not silently discarded.

@Aureliolo Aureliolo merged commit 8eb2859 into main Apr 7, 2026
8 of 9 checks passed
@Aureliolo Aureliolo deleted the feat/agent-identity-versioning branch April 7, 2026 22:10
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview April 7, 2026 22:10 — with GitHub Actions Inactive
Aureliolo added a commit that referenced this pull request Apr 7, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.6.4](v0.6.3...v0.6.4)
(2026-04-07)


### Features

* analytics and metrics runtime pipeline
([#226](#226),
[#225](#225),
[#227](#227),
[#224](#224))
([#1127](#1127))
([ec57641](ec57641))
* engine intelligence -- quality signals, health monitoring, trajectory
scoring, coordination metrics
([#1099](#1099))
([aac2029](aac2029)),
closes [#697](#697)
[#707](#707)
[#705](#705)
[#703](#703)
* enterprise-grade auth -- HttpOnly cookie sessions, CSRF, lockout,
session limits
([#1102](#1102))
([d3022c7](d3022c7)),
closes [#1068](#1068)
* implement core tool categories and granular sub-constraints
([#1101](#1101))
([0611b53](0611b53)),
closes [#1034](#1034)
[#220](#220)
* memory evolution -- GraphRAG/consistency research +
SelfEditingMemoryStrategy
([#1036](#1036),
[#208](#208))
([#1129](#1129))
([a9acda3](a9acda3))
* security hardening -- sandbox, risk override, SSRF self-heal, DAST fix
([#1100](#1100))
([31e7273](31e7273)),
closes [#1098](#1098)
[#696](#696)
[#222](#222)
[#671](#671)


### Bug Fixes

* harden agent identity versioning post-review
([#1128](#1128))
([8eb2859](8eb2859)),
closes [#1076](#1076)


### Documentation

* engine architecture research
([#688](#688)
[#690](#690)
[#848](#848)
[#687](#687))
([#1114](#1114))
([59b31f9](59b31f9))


### Maintenance

* add .claudeignore and split CLAUDE.md for token optimization
([#1112](#1112))
([b0fbd18](b0fbd18))
* bump github.com/sigstore/protobuf-specs from 0.5.0 to 0.5.1 in /cli in
the all group
([#1106](#1106))
([73089c9](73089c9))
* bump jsdom from 29.0.1 to 29.0.2 in /site in the all group
([#1107](#1107))
([8e99dce](8e99dce))
* bump jsdom from 29.0.1 to 29.0.2 in /web in the all group
([#1108](#1108))
([ce8c749](ce8c749))
* bump python from `fb83750` to `6869258` in /docker/backend in the all
group ([#1104](#1104))
([4911726](4911726))
* bump python from `fb83750` to `6869258` in /docker/web in the all
group ([#1103](#1103))
([87bdf09](87bdf09))
* bump the all group across 1 directory with 4 updates
([#1111](#1111))
([f702464](f702464))
* bump the all group in /docker/sandbox with 2 updates
([#1105](#1105))
([05a91ca](05a91ca))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview April 7, 2026 22:12 — with GitHub Actions Inactive
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 7, 2026

Preview Deployment

URL: https://pr-1128.synthorg-pr-preview.pages.dev

Built from commit 23654ba

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: version agent identities / charters as first-class artifacts

2 participants