feat(team-server): Priority C v0+v1+v1.1 (clean dev-rebased reduplication of #153)#181
Merged
Merged
Conversation
Priority C v0 — Phase 1 of the team-server vertical-slice for multi-dev decision continuity at organizational scale (per the Sales Enablement & Positioning Playbook + research-brief-priority-c-selective-ingest-2026-05-02.md). The team-server is a self-managing, customer-self-hosted Python service. Per CONCEPT.md anti-goals under literal-keyword parsing (SHADOW_GENOME Failure Entry #6 addendum): "no managed backend" forbids vendor SaaS and human-ops-tax architectures, NOT self-managing customer-deployable backends. Sentry self-hosted, Supabase OSS, and the embedded-SurrealDB philosophy already in repo are precedents. Scaffold delivered: - team_server/app.py (47 LOC): FastAPI app factory; lifespan migrates schema on startup, closes DB on teardown; /health endpoint - team_server/schema.py (80 LOC): v0 schema for workspace, channel_allowlist, extraction_cache, team_event tables. Idempotent ensure_schema(); migration dispatch table for future versions. FLEXIBLE TYPE object on canonical_extraction + payload (per #72 lesson + audit Advisory #3). - team_server/db.py (41 LOC): TeamServerDB factory wrapping ledger.client.LedgerClient with team-server's own ns/db pair. - deploy/team-server.docker-compose.yml + Dockerfile.team-server: single-service compose; volume for persistent data; healthcheck on /health; runs as non-root. Tests (6 functionality tests, all green): - tests/test_team_server_app.py: app starts + serves health, schema migrates from empty + idempotent, lifespan teardown closes DB, /health returns well-formed JSON. - tests/test_team_server_deploy.py: docker-compose config validates. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2 of plan-priority-c-team-server-slack-v0.md — adds Slack OAuth v2 flow, workspace persistence with at-rest encrypted tokens, and a schema-validated channel allow-list config loader. Per audit Advisory #2, OAuth routes are factored into team_server/auth/router.py (parallel to Phase 4's events router pattern) rather than left inline in app.py — keeps app.py at 47 lines well under the Section 4 razor cap. Files added: - team_server/auth/slack_oauth.py (58 LOC): pure functions — build_authorize_url, exchange_code; raises SlackOAuthError on ok=false. - team_server/auth/encryption.py: Fernet encrypt/decrypt for OAuth tokens at rest; key loaded from BICAMERAL_TEAM_SERVER_SECRET_KEY env var. - team_server/auth/router.py (73 LOC): /oauth/slack/install + /oauth/slack/callback routes with CSRF state defense; persists workspace row with token encrypted before storage. - team_server/config.py (40 LOC): pydantic-validated YAML loader for channel allow-list; raises ValueError with descriptive message on schema failure. - team_server/app.py (modified): include auth router. Tests (7 functionality tests, all green): - tests/test_team_server_slack_oauth.py: authorize URL contains required params + scopes, exchange_code POSTs correctly, encrypt/decrypt round-trip preserves plaintext while ciphertext differs, callback rejects mismatched state (CSRF defense), callback persists workspace. - tests/test_team_server_channel_allowlist.py: load_channel_allowlist parses valid YAML, raises ValueError on missing team_id. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 3 of plan-priority-c-team-server-slack-v0.md — adds the polling worker, the canonical-extraction cache that closes the multi-dev extraction-divergence gap, and the peer-author event writer. Multi-dev convergence mechanism: any dev's session that touches a Slack message produces the SAME canonical extraction across the team because get_or_compute is keyed on (source_type, source_ref, content_hash) and the cache row is shared via the team-server's append-only event log. This is what closes Playbook Pillar #1 (Decision Continuity) at multi-dev/multi-agent scale that the v1 brief incorrectly framed as "build curation only" — see SHADOW_GENOME Failure Entry #6. The interim canonical extraction uses Anthropic Claude with the model_version='interim-claude-v1' tombstone so a future Phase 5 (CocoIndex #136 integration) can identify and rebuild interim entries when memoized deterministic transforms become available. Files added: - team_server/extraction/canonical_cache.py (45 LOC): get_or_compute() returns cached extraction OR invokes compute_fn + persists. - team_server/extraction/llm_extractor.py: interim Anthropic-backed extractor; production-default until CocoIndex lands. - team_server/sync/peer_writer.py (42 LOC): write_team_event() — appends to team_event with author_email='team-server@<team_id>.bicameral' identity (single-bot per workspace; multi-instance HA is v1). - team_server/workers/slack_worker.py (100 LOC): poll_once() — conversations_history per allowlisted channel, content_hash dedup, cache-keyed extraction, peer event write per new message. Tests (6 functionality tests, all green): - tests/test_team_server_canonical_cache.py: cache hit returns existing without compute_fn, cache miss persists + subsequent call returns cached, content_hash variation produces new rows. - tests/test_team_server_slack_worker.py: worker polls only allowlisted channels, writes one team_event per message with peer-author identity, dedups via message ts (idempotent re-poll). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 4 of plan-priority-c-team-server-slack-v0.md — closes the multi-dev convergence loop by exposing team_event over HTTP and extending EventMaterializer with a failure-isolated team-server pull. The materializer pull is OUTSIDE the deterministic core (per CONCEPT.md literal-keyword parsing of "no network calls in the deterministic core" — SHADOW_GENOME Failure Entry #6 addendum). Failure-isolation contract: team-server outage NEVER cascades into per-dev preflight failures — events/team_server_pull.py swallows transport errors, returns empty events, leaves the watermark unchanged. Files added: - team_server/api/events.py: GET /events?since=N&limit=M endpoint; reads team_event ordered by sequence ascending; pagination via since cursor. - events/team_server_pull.py (57 LOC): pull_team_server_events() queries the team-server's /events endpoint, persists watermark per call, swallows transport errors gracefully. - team_server/app.py (modified): include events router. Tests (6 functionality tests, all green): - tests/test_team_server_events_api.py: /events returns rows in sequence order, paginates via since cursor, returns empty (not error) when no new events. - tests/test_materializer_team_server_pull.py: materializer pulls from team-server URL, watermark advances + persists separately, 503 /transport-error degrades gracefully (no exception, watermark unchanged) — failure-isolation contract verified. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
QorLogic SDLC governance trail for the Priority C v0 implementation that landed in commits 1-4 of this PR. Includes: - docs/research-brief-priority-c-selective-ingest-2026-05-02.md (v3) — research substrate. v1 was rejected for INVARIANT_FROM_IMPLEMENTATION (treating v0 agent-fetches-only code state as product principle); v2 added playbook substrate; v3 narrowed to Slack-first + team-server + CocoIndex-conditional after operator dialogue clarified "no managed backend" = "no human-ops-tax architecture," not "no backend." - plan-priority-c-team-server-slack-v0.md (437 LOC) — the L3 plan with five phases (Phase 5 deferred per "if we can manage it" feasibility caveat). - docs/SHADOW_GENOME.md Failure Entry #6 + addendum — captures the framing-error pattern AND the "anti-goals must be parsed by their load-bearing keyword" lesson; symmetric to v0-code-as-principle. - docs/META_LEDGER.md Entries #27 (IMPLEMENT) + #28 (SEAL). Predecessor: efd0304b (#135-triage seal on dev). Implement chain: 211ffb9e. Substantiation seal: 6f4f8f8f1d63ad82b952a3c6aff270d30584e08b0572077ff685e84ce453f6c2 - docs/SYSTEM_STATE.md — Priority C v0 section appended; documents schema additions, architectural properties achieved, audit advisory disposition, Phase 5 deferred state, and the qor-logic-internal steps skipped for downstream-project rationale. - .agent/staging/AUDIT_REPORT.md — PASS verdict, three non-blocking advisories all addressed at implement-time. Verdict: REALITY = PROMISE for Phases 1-4. Phase 5 (CocoIndex #136) explicitly deferred per plan slip-independence design. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ref + schema_version (Phase 0) Schema v1->v2: extraction_cache.UNIQUE keyed on (source_type, source_ref); content_hash becomes a tracked column. canonical_cache.get_or_compute() replaced by upsert_canonical_extraction(...) -> tuple[dict, bool] returning (extraction, changed). Slack worker adapts to the new contract; gates the team_event write on the returned changed flag (idempotent on no-content change). _MIGRATIONS dispatch upgraded to Callable[[LedgerClient], Awaitable[None]]. New schema_version single-row table records the post- migration version as data, not folklore. _migrate_v1_to_v2 dedups duplicate (source_type, source_ref) rows by max(created_at) before redefining the v2 index. Tests: 12 functionality tests across cache_upsert, schema_migration, and slack_worker adaptation; canonical_cache test rewritten under v2 upsert contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ring (Phase 0.5) Establishes the worker-task lifecycle pattern via worker_loop(name, interval_seconds, work_fn) — single source of truth for the asyncio.create_task / per-iteration error isolation / cancel-on-shutdown shape. slack_runner.run_slack_iteration is the canonical reference implementation: iterates the workspace table, decrypts each Fernet token via load_key_from_env() + decrypt_token(ciphertext, key), reads the channel allowlist, constructs an AsyncWebClient, and delegates one polling pass to slack_worker.poll_once. Per-workspace exceptions are caught for failure isolation. app.py lifespan registers the Slack worker unconditionally (no-op when workspace table is empty); the registered task is cancelled and awaited on shutdown. Closes the v0 dormant-Slack-worker gap: v0 plan claimed an active worker but v0 code shipped poll_once with zero production callers. Tests: 7 functionality tests including the round-trip encryption test (encrypt -> store-as-string -> read-as-bytes -> decrypt -> token reaches slack_client) that closes the audit-round-2 blind spot per SHADOW_GENOME #7 addendum. slack_sdk import in slack_runner is lazy (inside run_slack_iteration) so the team_server package is importable in environments where slack_sdk is declared in requirements.txt but not in dev venv. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
team_server/auth/notion_client.py provides internal-integration-token auth (no OAuth router): load_token resolves NOTION_TOKEN env first, falling back to YAML config's notion.token; raises NotionAuthError if neither is set. Pure async functions over httpx.AsyncClient with Notion-Version pinned to 2022-06-28: list_databases (filtered to object=database), query_database (per-database last_edited_time watermark filter, ascending sort, paginated), fetch_page_blocks (paginated children). team_server/extraction/notion_serializer.py serializes a Notion database row deterministically: title line, then sorted-by-key property lines (title/rich_text/select/multi_select/date/checkbox/number/url/ people branches), then a blank line, then body block plain-text. Byte- stable output is the gating invariant for content_hash stability. team_server/config.py: DEFAULT_CONFIG_PATH constant with BICAMERAL_CONFIG_PATH env-var fallback; Path-typed. Tests: 7 client tests (env-vs-config precedence, MockTransport verification of filter shapes + Notion-Version header pinning + block pagination), 3 serializer tests (ordering, all property-type branches, byte-stability across calls). No new package dependencies — httpx and yaml already in v0 deps. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…se 2)
team_server/workers/notion_worker.py polls allowlist-via-share Notion
databases (the integration sees only databases the operator has shared
with it — derived dynamically from notion_client.list_databases, no
separate allowlist table required). Per-database watermark stored in
the new source_watermark table, advanced monotonically as rows
ingest. Partial-failure recovery: watermark advances only to the last
successfully-ingested row's last_edited_time, so the next poll resumes
correctly. Per-database HTTPError is caught and logged so a single
failing database does not block other databases.
Each row's text input is the deterministic serializer output (title +
sorted properties + body); content_hash is SHA256 over that text.
upsert_canonical_extraction returns (extraction, changed); when
changed=True, a peer-authored team_event is written under
PEER_WORKSPACE_ID="notion" (resulting author_email
"team-server@notion.bicameral" via write_team_event's wrapper).
source_type="notion_database_row"; source_ref="{db_id}/{page_id}".
Tests: 9 functionality tests covering database iteration via
list_databases, first-seen-row event, idempotency on unchanged rows,
new event on edited rows, monotonic watermark advancement, watermark-
to-filter wiring, partial-failure recovery, per-database 404 isolation,
content_hash stability across dict insertion-order changes (the
serializer determinism invariant under the polling layer).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
team_server/workers/notion_runner.py: thin wrapper run_notion_iteration over notion_worker.poll_once for symmetry with slack_runner (both expose a zero-extra-arg work_fn for the lifespan to register via worker_loop). Internal-integration auth means a single token covers a single workspace; v1 ships single-workspace. team_server/app.py lifespan amended: after Slack worker registration (unconditional), attempts notion_client.load_token via DEFAULT_CONFIG_PATH; on success registers a Notion task via the same worker_loop helper. On NotionAuthError logs INFO and continues without Notion ingest. On shutdown, both tasks are cancelled and awaited symmetrically. Tests: 4 functionality tests covering env-gated startup wiring, off-by-default invariant when token unset, cancellation on shutdown, and inner-loop resilience (single-iteration failure does not exit the loop). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three-round audit cycle (VETO -> VETO -> PASS) for Notion ingest + cache contract migration. Plan ships across five phases: - Phase 0 — cache contract migration (schema v1->v2, schema_version table, callable migration dispatch, upsert_canonical_extraction) - Phase 0.5 — worker-task lifecycle pattern + Slack reference wiring (closes the v0 dormant-Slack-worker gap) - Phase 1 — Notion API client + property serializer (internal- integration auth, no OAuth router) - Phase 2 — Notion ingest worker (per-database watermark, peer- authored team_event) - Phase 3 — Notion task registration on lifespan META_LEDGER entries #29-#33 capture: round-1 VETO (4 missing/ undeclared symbols), round-2 VETO (1 wrong-call-shape for decrypt_token), round-3 PASS, IMPLEMENT, and SUBSTANTIATION. SHADOW_GENOME #7 addendum extends the PARALLEL_STRUCTURE_ASSUMED detection heuristic with three new in-sketch checks: signature, type-boundary, helper-symmetry. The two VETOs in this session are the empirical justification. SYSTEM_STATE.md adds the Priority C v1 section: schema state (v2), architectural properties achieved, audit cycle outcomes, implementation deviations from plan. Merkle seal: SHA256(content_hash + previous_hash) = dcb619104e6d88b97a04689093b80b9f03825f9a24bac3c3b9ab3d0107ff24d7 (content_hash 9f003c40..., previous_hash 6f4f8f8f... = Priority C v0 SEAL at Entry #28). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…hase 0) Schema v2->v3: extraction_cache gains classifier_version field (option<string> with DEFAULT 'legacy-pre-v3'). upsert_canonical_extraction now requires classifier_version as keyword-only; cache hit requires BOTH content_hash AND classifier_version match. Either differing triggers re-extraction. The option<string> type accommodates pre-v3 rows whose field reads NONE before the migration's UPDATE backfills them — strict TYPE string would reject those reads (surfaced by the v2-to-v3 backfill integration test added per audit advisory L4-B from the QorLogic Fixer's Layer 4 sweep). _migrate_v2_to_v3 callable: defines the field permissively, then unconditionally UPDATE-backfills rows where classifier_version IS NONE. Idempotent. Workers (slack, notion) pass classifier_version="legacy-pre-v3" until pipeline integration (Phase 4) supplies the real heuristic version. Tests: 14 functionality tests across Phase 0 (cache_upsert/schema adaptations + classifier_version axis verification + v2->v3 backfill integration test). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…(Phase 1) team_server/extraction/heuristic_classifier.py provides Stage 1 of the extraction pipeline: pure-function classify(message, context, rules) returning ClassificationResult(is_positive, matched_triggers, classifier_version). Deterministic by construction (no LLM, no temperature, no time/uuid/random); rule-set hash drives downstream cache invalidation. Inputs: message dict (text + structural fields), context dict (reactions, thread_position, channel/db_id), TriggerRules (operator- configured + corpus-learned terms). The classifier honors: - keyword positives + keyword negatives (negatives short-circuit) - min_word_count length floor - reaction-count boosters (option d — context-aware) - thread-tail position booster (option d) - learned_keywords merge (option c — populated by Phase 5) derive_classifier_version produces a stable SHA256 hash of the sorted rule-set; changes invalidate the upsert cache via the classifier_version axis added in Phase 0. Tests: 9 functionality tests covering keyword match, negative override, length floor, reaction boost, thread-tail booster, determinism, version-changes-on-rule-change, and unicode/emoji robustness. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
team_server/config.py extended with pydantic models for the heuristic trigger rules: HeuristicGlobalRules (workspace-level defaults), HeuristicScopedOverride (per-channel/database additive overrides), SlackHeuristics, NotionHeuristics, NotionConfig, CorpusLearnerConfig. YAML alias 'global:' maps to global_rules field via populate_by_name=True + alias='global' (avoids the Python reserved-word collision). Resolvers resolve_rules_for_slack and resolve_rules_for_notion produce TriggerRules | RulesDisabled, merging global + scoped + learned keywords additively. RulesDisabled is the sentinel for opted-out channels/databases. Backwards compatibility: load_channel_allowlist preserved as an alias for load_rules_from_config so existing v0 OAuth callers continue to work unchanged. Tests: 5 functionality tests covering YAML loading, channel-override merge, database-override merge, disabled-channel sentinel, and ValidationError propagation as ValueError. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
team_server/extraction/llm_extractor.py: full rewrite of the v1.0
paragraph-split placeholder. extract(text, matched_triggers) async
calls the Anthropic Messages API (claude-haiku-4-5 default; selectable
via BICAMERAL_TEAM_SERVER_EXTRACT_MODEL env). Returns structured
{"decisions": [{"summary", "context_snippet"}], "extractor_version",
"matched_triggers"}.
Failure handling:
- ANTHROPIC_API_KEY unset: raises MissingAnthropicKeyError (fail-loud)
- HTTP 429: exponential backoff retry (1s, 2s; max 3 attempts)
- HTTP 5xx / network errors: fail-soft with truncated error string
- Unparseable JSON output: fail-soft with parse-failure message
- Non-text content blocks (ToolUseBlock etc.): fail-soft (closes
Fixer L1-C from the proactive code-quality sweep)
Anthropic SDK imported lazily inside extract() so the module remains
importable when anthropic is in requirements.txt but not in dev venv
(matches the slack_sdk lazy-import pattern from v1.0 Phase 0.5).
extractor_version is a SHA256 prefix of the prompt template + model
name, so changes to either invalidate downstream cache via the
classifier_version cousin axis.
Tests: 7 functionality tests covering structured output parsing,
trigger-grounding in prompt, 429 retry, 500 fail-soft, parse-failure
fail-soft, env-overridden model, and fail-loud-on-missing-key.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ge 2 (Phase 4)
team_server/extraction/pipeline.py provides the single entry point
extract_decision_pipeline(*, text, message, context, rules_or_disabled,
llm_extract_fn). Determines the output shape regardless of source:
{decisions, classifier_version, matched_triggers, extractor_version,
skipped}. extractor_version is None when Stage 2 didn't run (chatter,
rules-disabled).
slack_worker._ingest_message: builds context dict (reactions,
thread_position, thread_ts, subtype), resolves rules per channel via
config, routes through pipeline. classifier_version computed cheaply
from rules; the cache check happens BEFORE the LLM call.
notion_worker._ingest_row: builds context dict (last_edited_by,
edit_count), resolves rules per database, routes through pipeline.
Both workers preserve the legacy `extractor(text)` path when config
is None — preserves v1.0 worker tests + provides a clean cutover path
for callers that haven't adopted the rules schema.
Tests: 5 functionality tests covering pipeline short-circuit on
chatter, LLM invocation on positives, rules-disabled passthrough, and
worker-side context handoff for Slack (thread + reactions) and Notion
(edit metadata).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
team_server/extraction/corpus_learner.py reads the team-server's own team_event log (per OQ-1: not the per-repo decision table that doesn't exist server-side), extracts top n-grams from positive-extraction decisions, persists to learned_heuristic_terms with operator-denylist respected. Schema v3->v4 adds learned_heuristic_terms table (UNIQUE on source_type+term). Persistence is upsert-shaped: re-runs update support_count + learned_at without duplicating rows. resolve_rules_for_slack / resolve_rules_for_notion accept a learned=tuple[str, ...] argument that merges into TriggerRules. learned_keywords. The classifier already consumes this via the same match path as operator-configured keywords. app.py lifespan registers a corpus-learner worker via the existing worker_loop helper when config.corpus_learner.enabled is true (default false). Off-by-default; opt-in via YAML. Tests: 7 functionality tests covering n-gram extraction, denylist honor, persistence, determinism, learned-keyword merge, lifespan- on-when-enabled, lifespan-off-when-disabled. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First-round PASS audit cycle for the real heuristic+LLM extractor. Plan ships across six phases (Phase 0 cache contract evolution; Phase 1 deterministic Stage 1 classifier; Phase 2 trigger rules schema; Phase 3 real Anthropic SDK Stage 2; Phase 4 pipeline integration; Phase 5 corpus learner option-c). META_LEDGER entries #34-#36 capture: round-1 PASS audit, IMPLEMENT, and SUBSTANTIATION. Three audit advisories (extract() boundary, TeamServerRules typo, corpus learner table-source) all addressed inline during implementation. A proactive QorLogic Fixer code-quality sweep before commit produced 2 MED + 2 LOW findings; both MEDs landed (fail-soft on non-text content blocks; v2->v3 backfill integration test) with one surfacing a real defect (the migration's TYPE string was rejecting reads on pre-v3 rows with NONE classifier_version; corrected to TYPE option<string>). SYSTEM_STATE.md adds the Priority C v1.1 section: schema state (v4), architectural properties achieved (heuristic-first determinism + LLM-only-when-needed + rule-version-driven cache invalidation + all four "dynamic" angles wired), audit cycle outcomes. Merkle seal: SHA256(content_hash + previous_hash) = b37003661820e2ef80591b9d0cfdeac3df092d6d9b4b5d87e3036e7ccf37d95b (content_hash e8b1b6b6..., previous_hash dcb61910... = Priority C v1 SEAL at Entry #33). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
) team_server/auth/allowlist_sync.py reconciles channel_allowlist against the workspace table from config.slack.workspaces[]: per-team_id additive + subtractive sync. Idempotent; picks up operator YAML edits on next restart. Workspaces in YAML without a corresponding workspace- table row (no OAuth completed yet) are logged and skipped — they get picked up on the next sync after OAuth completes. team_server/app.py lifespan: calls sync_channel_allowlist after ensure_schema + config load, before worker registration. The Slack runner's _channel_ids query sees populated rows on first poll cycle. Sync failures log+continue so a partial YAML doesn't block startup. Config load is now done once at the top of the lifespan body and passed through to both the allowlist sync and the corpus learner registration (deduplication of _load_config_or_default calls). Implementation note: SurrealDB v2 strict-types `record<workspace>` on channel_allowlist.workspace_id requires `type::thing()` coercion (the SELECT id from workspace returns a 'workspace:<rid>' string; passing that string back into CREATE/DELETE without coercion fails the field type check). Pattern matches the v1.0 schema migration's existing use of type::thing in _migrate_v1_to_v2. Tests: 7 functionality tests across allowlist_sync (5: insert / idempotent / skip-not-in-yaml / skip-not-in-db / removal-on-yaml-edit) and lifespan integration (2: lifespan invokes sync at startup; lifespan continues when sync raises). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ge (closes #160 first half) events/team_server_bridge.py provides two pure functions: - is_team_server_payload(payload) — predicate distinguishing team- server-shaped events ({source_type, source_ref, content_hash, extraction}) from legacy CodeLocatorPayload-shaped events - bridge_team_server_payload(payload) — maps to IngestPayload shape (source='slack'|'notion', empty repo/commit_hash, summary→description, context_snippet→source_excerpt). source_type='notion_database_row' normalizes to source='notion'. Handles both new dict-shape decisions and the legacy interim-claude-v1 paragraph-split string-shape. events/team_server_consumer.py spawns a periodic asyncio task that: 1. Calls pull_team_server_events to fetch new events from the team- server's /events HTTP endpoint 2. Filters team-server-shaped events via is_team_server_payload 3. Bridges via bridge_team_server_payload 4. Invokes inner_adapter.ingest_payload directly (bypasses JSONL — team-server events have their own canonical home in the team- server's SurrealDB; per-author JSONL files would be redundant) Defensive unwrap (audit-round-2 Finding A): get_ledger() returns TeamWriteAdapter in team mode; its ingest_payload emits an 'ingest.completed' event via _writer.write BEFORE delegating. Without the unwrap, consumer-driven ingest would echo team-server events into per-dev JSONL files → git push → other devs replay → O(N²) cross-dev replay amplification per team-server event. The `getattr(adapter, "_inner", adapter)` line in start_team_server_consumer_if_configured is the load-bearing control; it falls through to the bare adapter in solo mode (verified: SurrealDBLedgerAdapter has no _inner attribute). server.py serve_stdio: spawns the consumer task in parallel with the existing dashboard sidecar; cancels and awaits on shutdown via try/finally. Opt-in via BICAMERAL_TEAM_SERVER_URL env; consumer task returns None when unset. Tests: 7 functionality tests including test_consumer_unwraps_team_write_adapter_does_not_echo_to_jsonl which constructs a real TeamWriteAdapter with a recording EventFileWriter stub and asserts _writer.write was NOT called — the load-bearing test that catches the audit-round-2 echo-amplification defect. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…vents (closes #160 second half) events/materializer.py replay loop adds a dispatch branch for event_type in ('ingest', 'ingest.completed') with a team-server-shaped payload: routes through is_team_server_payload + bridge_team_server_payload (from events/team_server_bridge.py landed in Phase 1.5) and invokes inner_adapter.ingest_payload with the bridged IngestPayload. The new branch sits BEFORE the existing 'ingest.completed' dispatch and is gated on the is_team_server_payload predicate. Legacy CodeLocatorPayload-shaped events with event_type='ingest.completed' fall through unchanged; only team-server-shaped payloads route via the bridge. This closes the second half of #160 — Phase 1.5 closed the load- bearing path (per-dev consumer pulling events directly), while this phase covers the secondary path where team-server events end up in git-tracked JSONL files (e.g., if a future flow appends team-server events to per-author JSONL for offline replay). Defensive infrastructure for v1.next; not load-bearing for v0 functionality. Tests: 6 net-new functionality tests in test_materializer_team_server_pull.py: - dispatches team_server 'ingest' event through bridge - bridges slack extraction to IngestPayload (full shape assertion) - bridges notion_database_row to source='notion' (normalization) - skips events with empty extraction.decisions - legacy 'ingest.completed' with non-team-server payload still routes to original dispatch (regression coverage) - malformed payload (missing 'extraction') is shape-checked and skipped without crashing Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three-round audit cycle (VETO → VETO → PASS) for closing v0 release blockers issues #160 (materializer event_type mismatch) and #161 (channel_allowlist not populated). META_LEDGER entries #37-#41 capture: round-1 VETO (infrastructure- mismatch — pull_team_server_events had zero production callers), round-2 VETO (specification-drift — sketch passed wrapped adapter without unwrap; would echo events O(N²) cross-dev), round-3 PASS, IMPLEMENT, SUBSTANTIATION. SHADOW_GENOME #7 heuristic catalog grew 4→6 across this branch: - Heuristic 5 (upstream-consumer) — Entry #37 - Heuristic 6 (wrapper-side-effect) — Entry #38 The catalog is the productive deposit beyond the code; each heuristic is a durable detection pattern reusable in future audits. SYSTEM_STATE.md adds the v0 release-blockers section: end-to-end ingest pipeline now functional (Slack OAuth → workspace row → YAML allowlist sync → channel_allowlist → Slack worker polls → heuristic+ LLM extraction → team_event → /events HTTP → per-dev consumer pulls → bridges to IngestPayload → per-dev local ledger). Merkle seal: SHA256(content_hash + previous_hash) = 7cc405fc8d39f468d502da669982c88321ce3a84bb571d28e0b14be86ab56bdd (content_hash 14e387b1..., previous_hash b3700366... = Priority C v1.1 SEAL at Entry #36). Closes #160, closes #161. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rift cut Brings commits 1-22 of #153's branch into a fresh dev-based branch: team-server scaffold, OAuth, workers, cache evolution, classifier, LLM extractor, channel_allowlist sync (#161), team-server event consumer + bridge (#160). Skips the post-238c0ce commits that drift architecturally from dev's #175 preflight Step 5.6 rework and the events/session_end_bridge.py vs bicameral-capture-corrections split.
Type fixes (3 mypy errors that blocked PR #153): - llm_extractor.py: _one_attempt return type now tuple[str, list[Any] | str | None]; isinstance assert added on the ok branch before _success(decisions=...) - app.py: _interim_extractor as adapter function (not bare alias) so the worker Extractor protocol Callable[[str], Awaitable[dict]] is honored; the underlying llm_extractor.extract takes 2 args (text + triggers); the adapter passes triggers=[] for the legacy fallback path Plus ruff auto-fix + ruff format pass to bring 44 files inline with current ruff config on dev (the cherry-picked commits pre-dated some style updates). Locally: mypy . (131 files), ruff check ., ruff format --check . all pass; 28/28 team-server tests pass.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Closes the e2e flake observed on PR #181 (run 25345393569): Flow 5 (PM history+ratify, MCP-layer) and Flow 3 (commit→sync, agentic) failed because a stale ~/.claude/projects/-tmp-desktop-clone/memory/MEMORY.md from a prior run let the agent answer Flow 5's prompt directly from disk instead of invoking bicameral.history. Flow 3's failure cascaded because its ledger snapshot relies on Flow 5's bicameral call to drain the post-commit JSONL queue via EventMaterializer.replay_new_events (documented at run_e2e_flows.py:342-349). Diagnosis evidence (from /qor-debug Phase 1 four-layer analysis): - PASS run #178 Flow 5.ndjson L5: 'File does not exist' on MEMORY.md → agent runs ToolSearch + bicameral_history. Flow 5 PASS. - FAIL run #181 Flow 5.ndjson L5: full MEMORY.md content shown → agent reads memory + git-log + answers from disk. Zero bicameral calls. - The team-server materializer hunk at events/materializer.py:88-106 is correctly gated (is_team_server_payload requires both source_type AND extraction keys; standard MCP payloads have neither). Dormant in the e2e flow. NOT the regressor. Fix: - New helper clean_claude_memory_for_repo(repo_path) in tests/e2e/_harness_setup.py that purges ~/.claude/projects/<key>/memory/ where <key> is the absolute repo path with / and \ replaced by -. - Wired into tests/e2e/run_e2e_flows.py::main as _clean_claude_memory(), called alongside the existing _clean_ledger / _reset_desktop_repo / _bootstrap_bicameral_dir cleanup chain. - Regression-locking unit tests in tests/test_e2e_harness_memory_purge.py cover: purges-when-present, no-op-when-absent, scoped-to-target-project (does NOT touch other repos' memory on the same runner). 3/3 tests pass; ruff + ruff format + mypy all clean. Refs PR #181, root-cause via /qor-debug session
PR #181 run 25377562963 hit a 600s timeout on Flow 2 with a sequence length that exceeded the post-#175 baseline. Same age-out pattern as the prior 300s → 600s bump after #154's PostToolUse hook landed: every time a new turn lands on Flow 2's hot path (e.g. #175's AskUserQuestion disambiguation gate at preflight Step 5.6.1), the cap eats into its margin until a CI flake materializes. 900s = 50% headroom over Flow 2's observed runtime today. Comment at lines 46-58 documents the full bump history so the next age-out has context. No product code change.
This was referenced May 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fresh PR carrying the same payload as #153 (Priority C v0 + v1 + v1.1 — self-managing team-server, Slack + Notion ingest, heuristic + LLM extractor, channel_allowlist sync, team-server event consumer + materializer dispatch) but cherry-picked onto current
devto avoid the architectural drift in #153 that was blocking merge.Why a fresh PR: PR #153's branch had
events/session_end_bridge.pyand an older Step 5.6 inbicameral-preflight/SKILL.mdthat conflict with what landed ondevsince (#175's Step 5.6.1/5.6.2 rework + the bicameral-capture-corrections approach). #153 also had a stuck CI trigger after #159 (its main-targeting duplicate) was closed mid-push; new SHAs on the branch wouldn't fire workflows. This PR side-steps both problems by branching off latestdevand carrying only the non-drifting team-server commits.Linked issues
Closes #161 (channel_allowlist startup-time YAML sync)
Closes #160 (team-server event consumer + materializer dispatch)
Refs #153 (the original PR being reduplicated; this PR supersedes it)
What was carried over
Commits 1-22 of #153's branch (oldest to newest):
feat(team-server): scaffold + self-managing schema (Phase 1)feat(team-server): Slack OAuth + workspace allow-list (Phase 2)feat(team-server): Slack worker + canonical-extraction cache (Phase 3)feat(team-server): HTTP /events API + materializer extension (Phase 4)refactor(team-server): cache-contract migration to upsert-per-source_ref (Phase 0)feat(team-server): worker-task lifecycle pattern + Slack reference wiring (Phase 0.5)feat(team-server): Notion API client + property serializer (Phase 1)feat(team-server): Notion ingest worker + per-database watermark (Phase 2)feat(team-server): Notion task registration on lifespan (Phase 3)refactor(team-server): cache contract gets classifier_version axis (Phase 0)feat(team-server): heuristic classifier — pure deterministic Stage 1 (Phase 1)feat(team-server): trigger rules schema + per-channel/db merge (Phase 2)feat(team-server): real LLM extractor via Anthropic SDK (Phase 3)feat(team-server): pipeline integration — workers route Stage 1 → Stage 2 (Phase 4)feat(team-server): corpus learner — option-c feedback loop (Phase 5)feat(team-server): channel_allowlist startup-time YAML sync (closes #161)feat(team-server): periodic team-server event consumer + payload bridge (closes #160 first half)feat(team-server): materializer dispatch case for team-server JSONL events (closes #160 second half)Plus a
fix(team-server): mypy + ruff for fresh dev-based branchcommit on top with:_one_attemptreturn-type tightening + isinstance assert inllm_extractor.py_interim_extractoras adapter function inapp.py(matches workerExtractorprotocol)What was deliberately left behind
Commits 23-27 from #153 (architecturally drifted from current
dev):feat(skills): preflight Step 5.6 — capture refinements on contradiction— superseded by feat(skill): user-disambiguation question before Step 5.6 contradiction capture #175 on devfeat(events): SessionEnd transcript bridge — propagate parent transcript_path—events/session_end_bridge.pyconflicts with the bicameral-capture-corrections skill approach landed via feat(skill): session-end auto-capture of uningested decisions — research + observable validation #147; needs PR-author architectural reconciliation, separate from team-server workPlan / Audit / Seal
docs/governance/(carried over from Priority C v0 — self-managing team-server, Slack-first ingest #153)docs/governance/(carried over from Priority C v0 — self-managing team-server, Slack-first ingest #153)Test plan
mypy .— 131 files, no issuesruff check .— All checks passedruff format --check .— 278 files already formattedpytest tests/test_team_server_app.py tests/test_team_server_allowlist_lifespan.py tests/test_team_server_allowlist_sync.py tests/test_team_server_consumer.py tests/test_materializer_team_server_pull.py— 28 passedNotes for reviewer
events/session_end_bridge.pyand the old Step 5.6) should each get their own follow-up if they're still wanted on top of the dev-current Step 5.6 / capture-corrections design. Author judgment — PR author is best positioned to decide whethersession_end_bridge.pycomplements or replaces the capture-corrections approach now that feat(skill): session-end auto-capture of uningested decisions — research + observable validation #147+feat(skill): user-disambiguation question before Step 5.6 contradiction capture #175 are in.