CedarCopilot wants to merge 2 commits into staging from fix/single-load-skill-tool
Live on prod, no production signal yet
Libra has verdicts on 0 of 4 tracked behaviors on prod; 4 are still being checked. Libra checks hourly for 3 days after each deploy.
Replaces #2625, which filtered
load-skillout of the injected catalog. That fixed the instance and left the trap armed. This removes the condition that made it possible.
Two tools shared the id load-skill with different enums:
| tool | accepts |
|---|---|
loadSkillTool (in-app) | chat + brain |
createScopedLoadSkillTool (automation) | chat + daily-agenda |
The automation run injects the chat skill catalog as a toolset; a toolset entry overrides the
agent's tool of the same id; and multi-conversation-operations ships the in-app tool among its
own. So the chat-enum tool replaced the automation one on every run.
The enums disagreed about exactly one name, and it was the only one the daily-agenda run needed.
62 Ć "skill": "daily-agenda"
62 à skill: Invalid enum value. Expected 'drafting' | ⦠| 'analysis-decomposition',
received 'daily-agenda'
Zero successful loads across 1,562 agenda runs in 14 days. tasks was loaded in 1,264 , the
skill the pipeline lived in until it was split out on 2026-08-24. Downstream: no Step 2b status
exclusion (closed-lost deals reached the morning brief), no Step 2c stale cleanup (388 overdue
tasks stranded on closed deals), agenda format improvised.
The collision has existed since 2026-04-02, when both the scoped tool and the toolset injection
landed. It was harmless: the scoped tool originally used .enum(CHAT_SKILL_NAMES) , the same
list it was being overridden by, so the override changed nothing.
4d8fb3437 (2026-08-24) pointed it at AUTOMATION_LOADABLE_SKILL_NAMES and made the two enums
differ for the first time. The commit that armed the bug and the commit that created the override
are four months apart, which is why nobody connected them.
One load-skill.
ALL_LOADABLE_SKILL_NAMES = chat + brain + automation. One schema.workflowType === 'automation', a key buildSurfaceContext
sets in services/aop/automations.ts and nowhere else.CHAT_SKILL_NAMES, so an agent on the wrong surface is never
told the name exists and is refused if it guesses.With one tool holding the id, an override cannot change what is loadable. The failure is structurally impossible rather than filtered against.
Moved from a construction argument to a request-context key rather than deleted with the wrapper.
It was dead either way: every production call site passed [], and the allowedSkills computed
from a subagent's permissions frontmatter never reached it. That wiring gap is unchanged here
and worth its own issue , a scoping feature that has never been enforced, not a regression
introduced by this PR.
tsc -b on @zero/server , cleanpnpm deps:check , no violations (1587 modules, 8259 dependencies)tests/services/organizations/organizations.test.ts fails in shard 2
and is pre-existing , reproduces identically on a clean origin/staging worktreeautomation-agent.test.ts rewritten against the unified tool: 10 tests, same coverage as the
wrapper's, plus the surface gate in both directionsTomorrow's 09:00 UTC batch. load-skill {"skill":"daily-agenda"} should start appearing in
agent_tool_calls, and cancel-task with cancelOlderThan (Tier 1) should fire for the first
time.
š¤ Generated with Claude Code
The PR eliminates competing load-skill definitions by expanding one shared schema and enforcing automation-only access at execution time.
loadSkillTool with the automation agent.daily-agenda on `workflowTypeLibra has not measured any production surfaces for this change yet.