CedarCopilot wants to merge 4 commits into staging from feat/create-aop-and-update-selection-procedure
Live on prod, watching, 2 days left
Libra has verdicts on 0 of 9 tracked behaviors on prod; 9 are still being checked. Libra checks hourly for 3 days after each deploy.
Two new MCP write capabilities, codifying a real, previously hand-done operation: giving a customer a second playbook cloned from an existing one, then routing future conversations to it by a new rule.
create-aop , clone an existing user AOP into a brand-new AOP: conversationFieldDefinitions, customFieldDefinitions, displayConfig, scope/icon/color, plus the entire user/playbooks/{aopId}/ document tree (PLAYBOOK.md, resources/, subagents/). Always clones from the SOURCE's user-level row (never its linked org_aops row , the two can diverge, and the user-level row is what's actually live). cloneFromAopId is required; there is no from-scratch seeding path (that's a bigger, separate feature account-setup already owns differently).
update-selection-procedure , a plain full-replace write to an AOP's selection_procedure column (the routing text an LLM classifier reads to decide which AOP a new conversation belongs to), at scope: 'user' | 'org'. Already readable via config-read's read-playbook (mode: "list") but had no write path anywhere.
The source PLAYBOOK.md and subagent docs reference each other by document id via <ref id="..."> tags. Those ids don't exist until the clone writes mint them, so cloneAopDocuments (apps/server/src/services/aop/create-aop.ts) runs in two passes:
user/playbooks/{newAopId}/... via writeDocument (never raw SQL, so content_yjs never goes stale for a doc the live editor can open), minting new document ids and building an old→new id map keyed by relative path.<ref id="OLD"> to the new id.findDanglingOldIds is the automated form of the manual "grep the whole tree for the old ids, expect zero matches" check this was verified against by hand in the real case , it runs at the end of every clone and throws (triggering a best-effort rollback of the two AOP rows + the whole new document subtree) rather than leaving a half-remapped playbook live. Covered by a full PGlite-backed integration test (create-aop.test.ts) with a real PLAYBOOK.md referencing two subagents and a subagent doc that refs another subagent , plus direct unit tests for remapRefIds/findDanglingOldIds in isolation.
Two identity bugs a byte-for-byte clone would have reintroduced, closed along the way (not explicitly called out in the original manual clone, but real correctness bugs against the current schema):
crm_conversation_field_values.field_id is a global column with no aopId of its own; countFieldValues/deleteFieldValues query it by fieldId alone. Reusing the source's field ids would mean deleting a field on either AOP cascade-deletes values written under the other. Every cloned field gets a freshly minted id.agent_id , a subagent's identity is metadata.agent_id (cron dispatch, findSubagentByAgentId, field-ownership resolution). Cloning a subagent doc's frontmatter verbatim with no override would make reconcileSubagentWrite adopt the SOURCE's live agent_id on the new doc , two rows resolving to one identity. Every cloned subagent doc mints a fresh agent_id, and any ownerAgentId claim in the cloned field/display config pointing at the old subagent is repointed at its new one (remapOwnerAgentIds).Ran the actual budget test (mcp/external/__tests__/tool-inputschema-size.test.ts, ceiling 18,000 bytes):
config-write today (its existing 10 actions, unmodified): 14,445 bytesSummed that's 17,392 bytes , technically under the ceiling, but leaving only ~600 bytes of headroom on what would become a 12-action tool, the exact fragility that forced connection-write off config-write previously (past the ceiling, the connector silen
Libra has not measured any production surfaces for this change yet.