CedarCopilot wants to merge 0 commits into staging from fix/playbook-xml-persist-manifest-before-verify
Live on prod, no production signal yet
OTEL fallback found no prod spans matching writePlaybookXml patch mode since deploy.
OTEL fallback found no prod spans matching writePlaybookXml since deploy.
OTEL fallback found no prod spans matching writePlaybookXml verification rollback since deploy.
Libra has verdicts on 0 of 3 tracked behaviors on prod; 3 are still being checked. Libra checks hourly for 3 days after each deploy.
Agent / MCP / CLI-driven structural edits to a user PLAYBOOK.md , adding or removing a <trigger> block or a <ref> , were silently reverted with STALE_COMPILED_PLAYBOOK, even though the edit was valid. Prose-only edits (cross-cutting text, stage bodies) and all resource/subagent edits worked fine.
Found while trying to unregister a user's inbound-email-notifier: every attempt to remove its <trigger type="email"> block reverted, while editing subagent bodies succeeded.
writePlaybookXml wrote the new XML but left compiled_playbook metadata to be regenerated by the Yjs-flush compile hook (playbookCompileHook, run inside writeFileAsYjs). That hook is skipped whenever the caller passes skipYjsSync (documents/index.ts , the block is gated on !params.skipYjsSync), and the live write-document tool always sets it because it needs the yjsResult itself.
So the sequence for a tool-driven edit was:
compiled_playbook.manifest stays one edit staleverifyPlaybook → checkCompiledFreshness recompiles the new content and compares to the stale stored manifest → mismatch → STALE_COMPILED_PLAYBOOKOnly structural edits tripped it, because only they change the manifest (eventTypes, fieldWatchers, cronSchedules, beforeMeetingConfigs, hasGlobalBlock). Prose edits leave the manifest identical, so stored == recompile and verify passed , exactly the asymmetry observed. Org playbooks were immune because writeOrgPlaybookXml already compiles + persists the manifest inline.
Verified empirically against the affected playbook: a fresh recompile of the unchanged XML matched the stored manifest exactly (EQUAL? true) , i.e. no data drift; the failure was self-inflicted by the write path on the structural change.
Capture compiled + manifest from the pre-write compile and write them into metadata (compiled_playbook / playbook_manifest) in the same upsert, before verify , mirroring what writeOrgPlaybookXml already does. A patch is resolved into full prospective XML and written as an upsert so the metadata can ride along. Existing metadata is preserved (merge, not replace).
This makes freshness independent of whether the Yjs hook runs, so skipYjsSync no longer causes a false-positive revert. When the hook does run (non-tool callers), it recompiles to the same value , idempotent.
pnpm --filter @zero/server exec vitest run src/services/playbook/__tests__/guarded-write.test.ts → 24 passed (22 existing + 2 new). Server typecheck clean for the changed file.
New regression tests assert the freshly compiled manifest is persisted in the same write for both the skipYjsSync: true upsert path and the structural-patch path.
🤖 Generated with Claude Code
This PR persists freshly compiled playbook metadata in the same write as structural content changes so verification no longer compares new XML with a stale manifest.
The PR should not merge until patch writes preserve concurrent edits and failed verification restores metadata consistent with the reverted content.
Converting patches to unconditional full-document upserts creates a lost-update path, while persisting new compiled metadata without restoring the old metadata during rollback leaves reverted documents stale.
Files Needing Attention: apps/server/src/services/playbook/guarded-write.ts
| Filename | Overview |
|---|---|
| apps/server/src/services/playbook/guarded-write.ts | P |
Libra has not measured any production surfaces for this change yet.