feat(skills): Phase 2 skill bundle model#1493
Conversation
Introduce skills as collections of tools + optional instructions. Agents pin bundles via pinned_skills; bootstrap expands members and injects SKILL.md bodies. Retire enable_task_management in favor of the task-management skill. Nested layout for calendar and task-management; skill_registry lifecycle table; per-tool action_risk preserved. Closes #1489 Signed-off-by: Joseph Fung <joseph@josephfung.ca> Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Joseph Fung <josephfung@users.noreply.github.com>
Signed-off-by: Joseph Fung <joseph@josephfung.ca> Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Joseph Fung <josephfung@users.noreply.github.com>
josephfung
left a comment
There was a problem hiding this comment.
Strong Phase 2. The core reframe lands correctly: the tool stays the invocation/authorization atom, the skill (bundle) is layered on top as the pin/install/discover unit, and per-tool action_risk is genuinely preserved through bundle expansion. Registry split (ToolRegistry = Layer-1 runtime catalog, new SkillRegistry = Layer-2 lifecycle) matches design §4. Nice transitional touches: synthetic singletons so old atom-name pins keep resolving, lenient SKILL.md parsing that ignores unknown frontmatter for Phase-3 forward-compat.
Requesting a few changes before merge — one is a prod-boot blocker, two are simplifications that make the PR smaller and more correct.
🔴 Blocking
1. Cross-repo: a live custom agent still sets enable_task_management
This PR removes enable_task_management from the core schema, and schemas/agent-config.schema.json is additionalProperties: false. But curia-deploy/custom/agents/social-media.yaml:25 still declares enable_task_management: true. On the next deploy that agent's YAML fails strict validation at startup (unknown property) — a hard boot failure, or at best silent loss of heartbeat eligibility. curia-deploy is a separate repo this PR can't touch, so this needs a coordinated curia-deploy change (migrate social-media.yaml to pinned_skills: [tasks, documents], drop the flag) landed in lockstep. Please call it out as a merge/deploy prerequisite in the PR body, and confirm whether the startup validator rejects vs ignores the unknown key.
2. Decompose task-management into tasks + documents
The flag was two capabilities welded together. The frontmatter already exposes them as independent flags (heartbeat, document_workspace) and resolvePinnedSkills OR-accumulates across pins, so the split is mostly mechanical:
skills/tasks/SKILL.md→heartbeat: true, toolstask-*, body = the## Task Managementsectionskills/documents/SKILL.md→document_workspace: true, toolsdoc-*, body = the## Document Workspacesection- coordinator, ceo-inbox, contacts each pin
[tasks, documents](no behavior regression) - update
config/registry-defaults.yaml(skills:list) and the CHANGELOG entry
This split exposes a latent wiring coupling that must be fixed as part of it. taskRepo is currently gated only on documentWorkspaceEnabled (src/index.ts:2068), but it's used by both capabilities:
src/agents/runtime.ts:279— documents use it to resolve a doc's project-root task (doc scoping)src/agents/runtime.ts:468— the task-wake scheduler refresh uses it — a tasks/heartbeat concern
Today both flags are always true together so it never bites. Once tasks can be pinned without documents, a heartbeat-eligible agent would get taskRepo: undefined and the scheduler-wake refresh silently no-ops. Re-gate so taskRepo is wired whenever tasks OR documents is present; keep workingDocsRepo/documentWorkspaceEnabled on documents only.
3. Delete the vestigial instruction/tool constants — SKILL.md is the source of truth
parseSkillMd returns the whole markdown body as instructions, resolvePinnedSkills collects it, and appendSkillInstructions injects it into the system prompt at bootstrap. So the pinned skill's SKILL.md body already is the injected block — the old block-insertion path is dead. TASK_MANAGEMENT_BLOCK / DOCUMENT_WORKSPACE_BLOCK now survive only because unit tests assert on them; that's two sources of truth kept married by a test (false confidence + guaranteed drift).
- Delete
TASK_MANAGEMENT_BLOCK,DOCUMENT_WORKSPACE_BLOCK, and the residualapply*helpers. - Rewrite the task/doc unit tests to exercise the real path (pin →
resolvePinnedSkills→ assert the SKILL.md body reaches the effective prompt), reading the actual SKILL.md, not a duplicated constant. - Membership is currently specified three ways (the
tools/dirs, thetools:frontmatter, andTASK_MANAGEMENT_TOOLS). Make the frontmattertools:list authoritative and warn on drift bidirectionally (frontmatter tool with no dir and dir with no frontmatter entry). Drop theTASK_MANAGEMENT_TOOLSTS constant.
Net effect: ~150 lines of dead code removed and one source of truth per fact.
🟡 Should-fix (design coherence)
4. Discovery surfaces skills that can't yet be activated
toolSearch now returns kind: 'skill' bundle results, but runtime activation of a discovered skill is Phase 3 (tiered lookup + task-state persistence, design §6). A allow_discovery: true agent (coordinator) can now discover calendar/tasks as skills it has no mechanism to invoke or activate — risking a hallucinated call, plus a search for "calendar" returns both the bundle and its member atoms (dup/noise). Recommend either deferring kind: 'skill' results until Phase 3 lands activation, or explicitly instructing the LLM that skill results are informational-only for now.
5. The skillRegistry = toolRegistry local alias re-introduces the confusion the epic exists to kill
src/index.ts:1725 keeps a local skillRegistry pointing at the ToolRegistry while the actual SkillRegistry is named bundleRegistry. That inverted naming is exactly the tool-vs-skill ambiguity ADR-031 is removing. Please finish the local rename here (or a fast-follow noted in the PR). Same smell: effectivePinnedSkills now holds tool names.
🟢 Minor / verify
- Expanded members that aren't loaded are dropped silently.
resolvePinnedSkillsvalidates the pin name but not expanded member tools; withcalendarenabled whilecalendar-*stay off-until-credentialed, unloaded atoms vanish with no diagnostic. Re-add a debug/warn on missing expanded tools to preserve the old per-pin observability. - Migration 076 is unique now (075 = Phase 1 rename), but re-run
ls src/db/migrations | sortat merge time per the known collision hazard. - Doc reconciliation: several
enable_task_managementreferences remain in specs (spec 19 §4 is a whole section named after it). Confirm specs 19/02/00/21 andadding-an-agentare fully reconciled to the two-skill model, not just touched.
Follow-up (not this PR)
Only calendar + task-management were bundled; everything else auto-wraps as synthetic singletons, so the §6 payoff ("pin 5 bundles, not 30 atoms") is only fractionally realized. Tracked separately in #1494 to bundle the remaining clusters (contacts, web, memory, scheduler, learning, context-bridge) and settle the synthetic-singleton end-state.
Decompose task-management into independent tasks + documents skills; wire taskRepo for either pin; delete vestigial BLOCK/apply constants; make frontmatter.tools authoritative with bidirectional drift warns; defer kind:skill discovery until Phase 3; rename ToolRegistry alias. Signed-off-by: Joseph Fung <joseph@josephfung.ca> Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Joseph Fung <josephfung@users.noreply.github.com>
|
Addressed review feedback in d516636: Blocking
Should-fix Minor
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
📝 WalkthroughWalkthroughIntroduces first-class skill bundles backed by Possibly related issues
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install timed out. The project may have too many dependencies for the sandbox. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (2)
src/skills/skill-registry.ts (1)
12-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAn if-block that does precisely nothing.
Lines 20-23 test a condition and then... comment about it. No branch, no log, no validation. It compiles, it runs, it achieves the void. Either drop the dead conditional or make it earn its keep (e.g.,
logger?.debugfor the zero-tools case), because right now it just sits there being philosophically empty.♻️ Suggested cleanup
- if (!manifest.tools || manifest.tools.length === 0) { - // Instruction-only skills (Phase 3 imports) may have zero tools; native - // Phase 2 skills should list tools. Allow empty for forward compat. - } + // Instruction-only skills (Phase 3 imports) may have zero tools; native + // Phase 2 skills should list tools. Allow empty for forward compat.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/skills/skill-registry.ts` around lines 12 - 32, Remove the no-op empty-tools conditional from SkillRegistry.register, leaving duplicate-name validation and skill registration unchanged.src/index.ts (1)
2045-2059: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRename
pinnedSkillstopinnedTools
This now carries the expanded tool list, not skill names, so the current name is doing a rather poor job of telling the truth.AgentRuntimeonly forwards it, which makes a clearer field name the sensible option.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/index.ts` around lines 2045 - 2059, Rename the AgentRuntime constructor option from pinnedSkills to pinnedTools at the call site and update the corresponding AgentRuntime property, parameter, and forwarding references consistently. Preserve the existing effectivePinnedTools value and behavior while using pinnedTools throughout.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@skills/calendar/SKILL.md`:
- Line 6: Update the calendar risk summary on line 6 to match the tool
manifests: keep list/find as none, classify ordinary event creation as medium,
distinguish hold placement using its declared risk, and explicitly document any
other calendar tools that are not high risk instead of grouping all creation
under high.
In `@skills/calendar/tools/calendar-check-conflicts/tool.json`:
- Around line 13-16: Add the handler-returned displayTimezone field to the
outputs declaration in skills/calendar/tools/calendar-check-conflicts/tool.json
at lines 13-16. Also add displayTimezone alongside the formatted display value
in skills/calendar/tools/calendar-create-hold/tool.json at lines 17-22, keeping
both manifests aligned with their user-facing timestamp contracts.
In `@skills/documents/tools/doc-search/tool.json`:
- Around line 11-17: Update the outputs schema for the document search tool so
displayTimezone is nullable and matches the sibling doc-write schema, allowing
null when the request context has no timezone. Preserve the existing string
output when a timezone is available.
In `@skills/tasks/tools/task-update/tool.json`:
- Line 16: Update the blocked_by_task_id schema declaration in task-update
tool.json to accept both string values and null, while preserving its
optional/omittable behavior. Ensure the corresponding parsing in the registry
produces a nullable optional field so handler.ts can continue using null to
clear an existing dependency.
In `@src/channels/http/routes/registry.ts`:
- Around line 27-32: Update the action handler’s invalid-kind response to list
skills alongside tools and agents as valid kinds. Keep parseKind’s existing
mapping unchanged and ensure callers receive the complete set of supported URL
segments.
In `@src/registry/registry-repo.ts`:
- Around line 96-99: Update the install query in the registry repository so
conflict handling does not issue an UPDATE or trigger skill_registry_updated_at.
Preserve returning the existing skill_registry row for duplicate names by using
an insert/select or retry-safe DO NOTHING flow, while retaining the current
behavior for new installs.
In `@src/skills/skill-loader.ts`:
- Around line 102-120: Update discoverNestedToolNames to accept an optional
logger and warn when parsing a nested tool.json fails, including the manifest
context and caught error details. Replace the silent catch while preserving
lenient discovery behavior by continuing to the remaining entries after logging.
In `@src/skills/skill-md.ts`:
- Around line 84-93: Update the frontmatter handling for heartbeat and
document_workspace in the skill metadata construction to validate their types
consistently with version and tools. Reject non-boolean values, including
strings such as "true", instead of converting them to undefined; preserve the
existing behavior for omitted fields and valid booleans.
---
Nitpick comments:
In `@src/index.ts`:
- Around line 2045-2059: Rename the AgentRuntime constructor option from
pinnedSkills to pinnedTools at the call site and update the corresponding
AgentRuntime property, parameter, and forwarding references consistently.
Preserve the existing effectivePinnedTools value and behavior while using
pinnedTools throughout.
In `@src/skills/skill-registry.ts`:
- Around line 12-32: Remove the no-op empty-tools conditional from
SkillRegistry.register, leaving duplicate-name validation and skill registration
unchanged.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: e0f9203d-8160-41b0-adcf-ea884b251a14
📒 Files selected for processing (106)
CHANGELOG.mdCLAUDE.mdagents/calendar.yamlagents/ceo-inbox.yamlagents/contacts.yamlagents/coordinator.yamlconfig/registry-defaults.yamldocs/dev/adding-a-tool.mddocs/dev/adding-an-agent.mddocs/specs/00-overview.mddocs/specs/02-agent-system.mddocs/specs/19-tasks-and-backlog.mddocs/specs/21-agent-document-workspace.mdschemas/agent-config.schema.jsonskills/calendar/SKILL.mdskills/calendar/tools/calendar-check-conflicts/handler.test.tsskills/calendar/tools/calendar-check-conflicts/handler.tsskills/calendar/tools/calendar-check-conflicts/tool.jsonskills/calendar/tools/calendar-create-event/handler.test.tsskills/calendar/tools/calendar-create-event/handler.tsskills/calendar/tools/calendar-create-event/tool.jsonskills/calendar/tools/calendar-create-hold/handler.test.tsskills/calendar/tools/calendar-create-hold/handler.tsskills/calendar/tools/calendar-create-hold/tool.jsonskills/calendar/tools/calendar-delete-event/handler.tsskills/calendar/tools/calendar-delete-event/tool.jsonskills/calendar/tools/calendar-find-free-time/handler.test.tsskills/calendar/tools/calendar-find-free-time/handler.tsskills/calendar/tools/calendar-find-free-time/tool.jsonskills/calendar/tools/calendar-holds-sweep/handler.test.tsskills/calendar/tools/calendar-holds-sweep/handler.tsskills/calendar/tools/calendar-holds-sweep/tool.jsonskills/calendar/tools/calendar-list-calendars/handler.tsskills/calendar/tools/calendar-list-calendars/tool.jsonskills/calendar/tools/calendar-list-events/handler.test.tsskills/calendar/tools/calendar-list-events/handler.tsskills/calendar/tools/calendar-list-events/tool.jsonskills/calendar/tools/calendar-register/handler.tsskills/calendar/tools/calendar-register/tool.jsonskills/calendar/tools/calendar-respond-to-invite/handler.test.tsskills/calendar/tools/calendar-respond-to-invite/handler.tsskills/calendar/tools/calendar-respond-to-invite/tool.jsonskills/calendar/tools/calendar-update-event/handler.tsskills/calendar/tools/calendar-update-event/tool.jsonskills/documents/SKILL.mdskills/documents/tools/doc-list/handler.test.tsskills/documents/tools/doc-list/handler.tsskills/documents/tools/doc-list/tool.jsonskills/documents/tools/doc-read/handler.test.tsskills/documents/tools/doc-read/handler.tsskills/documents/tools/doc-read/tool.jsonskills/documents/tools/doc-search/handler.test.tsskills/documents/tools/doc-search/handler.tsskills/documents/tools/doc-search/tool.jsonskills/documents/tools/doc-write/handler.test.tsskills/documents/tools/doc-write/handler.tsskills/documents/tools/doc-write/tool.jsonskills/tasks/SKILL.mdskills/tasks/tools/task-complete/handler.test.tsskills/tasks/tools/task-complete/handler.tsskills/tasks/tools/task-complete/tool.jsonskills/tasks/tools/task-create/handler.test.tsskills/tasks/tools/task-create/handler.tsskills/tasks/tools/task-create/tool.jsonskills/tasks/tools/task-list/handler.test.tsskills/tasks/tools/task-list/handler.tsskills/tasks/tools/task-list/tool.jsonskills/tasks/tools/task-update/handler.test.tsskills/tasks/tools/task-update/handler.tsskills/tasks/tools/task-update/tool.jsonsrc/agents/document-workspace.tssrc/agents/loader.tssrc/agents/task-management.tssrc/channels/calendar/nylas-calendar-client.test.tssrc/channels/http/routes/registry.tssrc/db/migrations/076_create_skill_registry.sqlsrc/db/queries/tasks.tssrc/index.tssrc/registry/reconcile.test.tssrc/registry/reconcile.tssrc/registry/registry-repo.tssrc/registry/registry-service.tssrc/registry/types.tssrc/scheduler/backlog-heartbeat.tssrc/skills/execution.tssrc/skills/loader.tssrc/skills/pin-resolution.tssrc/skills/skill-loader.tssrc/skills/skill-md.tssrc/skills/skill-registry.tssrc/skills/skill-types.tssrc/skills/types.tstests/integration/calendar-holds-collaboration.test.tstests/integration/escalation-ux.test.tstests/unit/agents/calendar-prompt.test.tstests/unit/agents/document-workspace.test.tstests/unit/agents/task-management.test.tstests/unit/skills/calendar-check-conflicts.test.tstests/unit/skills/calendar-create-event.test.tstests/unit/skills/calendar-delete-event.test.tstests/unit/skills/calendar-find-free-time.test.tstests/unit/skills/calendar-list-calendars.test.tstests/unit/skills/calendar-list-events.test.tstests/unit/skills/calendar-register.test.tstests/unit/skills/calendar-update-event.test.tstests/unit/skills/pin-resolution.test.ts
💤 Files with no reviewable changes (6)
- tests/unit/agents/task-management.test.ts
- schemas/agent-config.schema.json
- src/agents/loader.ts
- src/agents/task-management.ts
- tests/unit/agents/document-workspace.test.ts
- src/agents/document-workspace.ts
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
Actionable comments posted: 8
🧹 Nitpick comments (2)
src/skills/skill-registry.ts (1)
12-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAn if-block that does precisely nothing.
Lines 20-23 test a condition and then... comment about it. No branch, no log, no validation. It compiles, it runs, it achieves the void. Either drop the dead conditional or make it earn its keep (e.g.,
logger?.debugfor the zero-tools case), because right now it just sits there being philosophically empty.♻️ Suggested cleanup
- if (!manifest.tools || manifest.tools.length === 0) { - // Instruction-only skills (Phase 3 imports) may have zero tools; native - // Phase 2 skills should list tools. Allow empty for forward compat. - } + // Instruction-only skills (Phase 3 imports) may have zero tools; native + // Phase 2 skills should list tools. Allow empty for forward compat.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/skills/skill-registry.ts` around lines 12 - 32, Remove the no-op empty-tools conditional from SkillRegistry.register, leaving duplicate-name validation and skill registration unchanged.src/index.ts (1)
2045-2059: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRename
pinnedSkillstopinnedTools
This now carries the expanded tool list, not skill names, so the current name is doing a rather poor job of telling the truth.AgentRuntimeonly forwards it, which makes a clearer field name the sensible option.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/index.ts` around lines 2045 - 2059, Rename the AgentRuntime constructor option from pinnedSkills to pinnedTools at the call site and update the corresponding AgentRuntime property, parameter, and forwarding references consistently. Preserve the existing effectivePinnedTools value and behavior while using pinnedTools throughout.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@skills/calendar/SKILL.md`:
- Line 6: Update the calendar risk summary on line 6 to match the tool
manifests: keep list/find as none, classify ordinary event creation as medium,
distinguish hold placement using its declared risk, and explicitly document any
other calendar tools that are not high risk instead of grouping all creation
under high.
In `@skills/calendar/tools/calendar-check-conflicts/tool.json`:
- Around line 13-16: Add the handler-returned displayTimezone field to the
outputs declaration in skills/calendar/tools/calendar-check-conflicts/tool.json
at lines 13-16. Also add displayTimezone alongside the formatted display value
in skills/calendar/tools/calendar-create-hold/tool.json at lines 17-22, keeping
both manifests aligned with their user-facing timestamp contracts.
In `@skills/documents/tools/doc-search/tool.json`:
- Around line 11-17: Update the outputs schema for the document search tool so
displayTimezone is nullable and matches the sibling doc-write schema, allowing
null when the request context has no timezone. Preserve the existing string
output when a timezone is available.
In `@skills/tasks/tools/task-update/tool.json`:
- Line 16: Update the blocked_by_task_id schema declaration in task-update
tool.json to accept both string values and null, while preserving its
optional/omittable behavior. Ensure the corresponding parsing in the registry
produces a nullable optional field so handler.ts can continue using null to
clear an existing dependency.
In `@src/channels/http/routes/registry.ts`:
- Around line 27-32: Update the action handler’s invalid-kind response to list
skills alongside tools and agents as valid kinds. Keep parseKind’s existing
mapping unchanged and ensure callers receive the complete set of supported URL
segments.
In `@src/registry/registry-repo.ts`:
- Around line 96-99: Update the install query in the registry repository so
conflict handling does not issue an UPDATE or trigger skill_registry_updated_at.
Preserve returning the existing skill_registry row for duplicate names by using
an insert/select or retry-safe DO NOTHING flow, while retaining the current
behavior for new installs.
In `@src/skills/skill-loader.ts`:
- Around line 102-120: Update discoverNestedToolNames to accept an optional
logger and warn when parsing a nested tool.json fails, including the manifest
context and caught error details. Replace the silent catch while preserving
lenient discovery behavior by continuing to the remaining entries after logging.
In `@src/skills/skill-md.ts`:
- Around line 84-93: Update the frontmatter handling for heartbeat and
document_workspace in the skill metadata construction to validate their types
consistently with version and tools. Reject non-boolean values, including
strings such as "true", instead of converting them to undefined; preserve the
existing behavior for omitted fields and valid booleans.
---
Nitpick comments:
In `@src/index.ts`:
- Around line 2045-2059: Rename the AgentRuntime constructor option from
pinnedSkills to pinnedTools at the call site and update the corresponding
AgentRuntime property, parameter, and forwarding references consistently.
Preserve the existing effectivePinnedTools value and behavior while using
pinnedTools throughout.
In `@src/skills/skill-registry.ts`:
- Around line 12-32: Remove the no-op empty-tools conditional from
SkillRegistry.register, leaving duplicate-name validation and skill registration
unchanged.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: e0f9203d-8160-41b0-adcf-ea884b251a14
📒 Files selected for processing (106)
CHANGELOG.mdCLAUDE.mdagents/calendar.yamlagents/ceo-inbox.yamlagents/contacts.yamlagents/coordinator.yamlconfig/registry-defaults.yamldocs/dev/adding-a-tool.mddocs/dev/adding-an-agent.mddocs/specs/00-overview.mddocs/specs/02-agent-system.mddocs/specs/19-tasks-and-backlog.mddocs/specs/21-agent-document-workspace.mdschemas/agent-config.schema.jsonskills/calendar/SKILL.mdskills/calendar/tools/calendar-check-conflicts/handler.test.tsskills/calendar/tools/calendar-check-conflicts/handler.tsskills/calendar/tools/calendar-check-conflicts/tool.jsonskills/calendar/tools/calendar-create-event/handler.test.tsskills/calendar/tools/calendar-create-event/handler.tsskills/calendar/tools/calendar-create-event/tool.jsonskills/calendar/tools/calendar-create-hold/handler.test.tsskills/calendar/tools/calendar-create-hold/handler.tsskills/calendar/tools/calendar-create-hold/tool.jsonskills/calendar/tools/calendar-delete-event/handler.tsskills/calendar/tools/calendar-delete-event/tool.jsonskills/calendar/tools/calendar-find-free-time/handler.test.tsskills/calendar/tools/calendar-find-free-time/handler.tsskills/calendar/tools/calendar-find-free-time/tool.jsonskills/calendar/tools/calendar-holds-sweep/handler.test.tsskills/calendar/tools/calendar-holds-sweep/handler.tsskills/calendar/tools/calendar-holds-sweep/tool.jsonskills/calendar/tools/calendar-list-calendars/handler.tsskills/calendar/tools/calendar-list-calendars/tool.jsonskills/calendar/tools/calendar-list-events/handler.test.tsskills/calendar/tools/calendar-list-events/handler.tsskills/calendar/tools/calendar-list-events/tool.jsonskills/calendar/tools/calendar-register/handler.tsskills/calendar/tools/calendar-register/tool.jsonskills/calendar/tools/calendar-respond-to-invite/handler.test.tsskills/calendar/tools/calendar-respond-to-invite/handler.tsskills/calendar/tools/calendar-respond-to-invite/tool.jsonskills/calendar/tools/calendar-update-event/handler.tsskills/calendar/tools/calendar-update-event/tool.jsonskills/documents/SKILL.mdskills/documents/tools/doc-list/handler.test.tsskills/documents/tools/doc-list/handler.tsskills/documents/tools/doc-list/tool.jsonskills/documents/tools/doc-read/handler.test.tsskills/documents/tools/doc-read/handler.tsskills/documents/tools/doc-read/tool.jsonskills/documents/tools/doc-search/handler.test.tsskills/documents/tools/doc-search/handler.tsskills/documents/tools/doc-search/tool.jsonskills/documents/tools/doc-write/handler.test.tsskills/documents/tools/doc-write/handler.tsskills/documents/tools/doc-write/tool.jsonskills/tasks/SKILL.mdskills/tasks/tools/task-complete/handler.test.tsskills/tasks/tools/task-complete/handler.tsskills/tasks/tools/task-complete/tool.jsonskills/tasks/tools/task-create/handler.test.tsskills/tasks/tools/task-create/handler.tsskills/tasks/tools/task-create/tool.jsonskills/tasks/tools/task-list/handler.test.tsskills/tasks/tools/task-list/handler.tsskills/tasks/tools/task-list/tool.jsonskills/tasks/tools/task-update/handler.test.tsskills/tasks/tools/task-update/handler.tsskills/tasks/tools/task-update/tool.jsonsrc/agents/document-workspace.tssrc/agents/loader.tssrc/agents/task-management.tssrc/channels/calendar/nylas-calendar-client.test.tssrc/channels/http/routes/registry.tssrc/db/migrations/076_create_skill_registry.sqlsrc/db/queries/tasks.tssrc/index.tssrc/registry/reconcile.test.tssrc/registry/reconcile.tssrc/registry/registry-repo.tssrc/registry/registry-service.tssrc/registry/types.tssrc/scheduler/backlog-heartbeat.tssrc/skills/execution.tssrc/skills/loader.tssrc/skills/pin-resolution.tssrc/skills/skill-loader.tssrc/skills/skill-md.tssrc/skills/skill-registry.tssrc/skills/skill-types.tssrc/skills/types.tstests/integration/calendar-holds-collaboration.test.tstests/integration/escalation-ux.test.tstests/unit/agents/calendar-prompt.test.tstests/unit/agents/document-workspace.test.tstests/unit/agents/task-management.test.tstests/unit/skills/calendar-check-conflicts.test.tstests/unit/skills/calendar-create-event.test.tstests/unit/skills/calendar-delete-event.test.tstests/unit/skills/calendar-find-free-time.test.tstests/unit/skills/calendar-list-calendars.test.tstests/unit/skills/calendar-list-events.test.tstests/unit/skills/calendar-register.test.tstests/unit/skills/calendar-update-event.test.tstests/unit/skills/pin-resolution.test.ts
💤 Files with no reviewable changes (6)
- tests/unit/agents/task-management.test.ts
- schemas/agent-config.schema.json
- src/agents/loader.ts
- src/agents/task-management.ts
- tests/unit/agents/document-workspace.test.ts
- src/agents/document-workspace.ts
🛑 Comments failed to post (8)
skills/calendar/SKILL.md (1)
6-6: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Make the calendar risk summary match the tool manifests.
Line 6 groups all creation as high risk, but
calendar-create-hold/tool.jsondeclaresaction_risk: "medium". The universe has somehow produced two risk matrices for one calendar; distinguish ordinary event creation from hold placement and document the other non-high-risk tools explicitly.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@skills/calendar/SKILL.md` at line 6, Update the calendar risk summary on line 6 to match the tool manifests: keep list/find as none, classify ordinary event creation as medium, distinguish hold placement using its declared risk, and explicitly document any other calendar tools that are not high risk instead of grouping all creation under high.skills/calendar/tools/calendar-check-conflicts/tool.json (1)
13-16: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Standardise calendar timestamp outputs.
Calendar skills expose user-facing timestamps, but their manifests do not consistently declare the required
displayTimezonefield. Because these manifests define discovery and output contracts, align both locations before this paperwork grows teeth.
skills/calendar/tools/calendar-check-conflicts/tool.json#L13-L16: add thedisplayTimezonefield returned byhandler.ts.skills/calendar/tools/calendar-create-hold/tool.json#L17-L22: adddisplayTimezonealongside the formatteddisplayvalue.📍 Affects 2 files
skills/calendar/tools/calendar-check-conflicts/tool.json#L13-L16(this comment)skills/calendar/tools/calendar-create-hold/tool.json#L17-L22🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@skills/calendar/tools/calendar-check-conflicts/tool.json` around lines 13 - 16, Add the handler-returned displayTimezone field to the outputs declaration in skills/calendar/tools/calendar-check-conflicts/tool.json at lines 13-16. Also add displayTimezone alongside the formatted display value in skills/calendar/tools/calendar-create-hold/tool.json at lines 17-22, keeping both manifests aligned with their user-facing timestamp contracts.Source: Coding guidelines
skills/documents/tools/doc-search/tool.json (1)
11-17: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
displayTimezoneshould probably bestring?, notstring.Marginally thrilling news: this field is documented as always present, but the sibling
doc-write/tool.jsonmarks the same field"string?", and the codebase convention (seecalendar-find-free-time/handler.tsin this very PR) sets it tonullwhenctx.timezoneis undefined. Might as well be consistent — not that consistency has ever cheered anyone up.📝 Proposed fix
- "displayTimezone": "string" + "displayTimezone": "string?"📝 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."outputs": { "query": "string", "path_prefix": "string", "match_count": "number", "matches": "array of {path, line_number, line}", "displayTimezone": "string?" },🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@skills/documents/tools/doc-search/tool.json` around lines 11 - 17, Update the outputs schema for the document search tool so displayTimezone is nullable and matches the sibling doc-write schema, allowing null when the request context has no timezone. Preserve the existing string output when a timezone is available.skills/tasks/tools/task-update/tool.json (1)
16-16: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash set -euo pipefail # Locate the files and inspect the relevant sections. git ls-files 'skills/tasks/tools/task-update/*' 'skills/tasks/tools/*' | sed -n '1,120p' printf '\n--- tool.json ---\n' cat -n skills/tasks/tools/task-update/tool.json | sed -n '1,120p' printf '\n--- handler references ---\n' rg -n "blocked_by_task_id|updateTask|null" skills/tasks/tools/task-update -SRepository: josephfung/curia
Length of output: 5377
🏁 Script executed:
#!/bin/bash set -euo pipefail printf '\n--- comparable tool manifests ---\n' for f in skills/tasks/tools/task-create/tool.json skills/tasks/tools/task-complete/tool.json skills/tasks/tools/task-list/tool.json; do echo "### $f" cat -n "$f" | sed -n '1,120p' echo done printf '\n--- task-update handler ---\n' cat -n skills/tasks/tools/task-update/handler.ts | sed -n '1,220p'Repository: josephfung/curia
Length of output: 10758
🏁 Script executed:
#!/bin/bash set -euo pipefail printf '\n--- schema/docs for tool input types ---\n' rg -n "\"string\\?|\"number\\?|\"boolean\\?|\"timestamp\\?|\\?\"|nullable|optional" src skills README.md docs -S printf '\n--- parser/validator for tool manifests ---\n' rg -n "tool.json|inputs|parse.*schema|schema.*tool|nullable|optional" src skills -SRepository: josephfung/curia
Length of output: 50373
🏁 Script executed:
#!/bin/bash set -euo pipefail printf '\n--- tool schema parser / type syntax ---\n' rg -n "string\\?|number\\?|boolean\\?|timestamp\\?|object\\?|\\[\\]\\?|nullable|optional|undefined|null" src skills -S | sed -n '1,220p' printf '\n--- likely schema/type implementation files ---\n' fd -a ".*(skill|tool|schema|input|manifest).*\\.(ts|js|json)$" src skills | sed -n '1,200p'Repository: josephfung/curia
Length of output: 29007
🏁 Script executed:
#!/bin/bash set -euo pipefail printf '\n--- skill type definitions ---\n' cat -n src/skills/skill-types.ts | sed -n '1,240p' printf '\n--- skill loader / manifest parsing ---\n' cat -n src/skills/skill-loader.ts | sed -n '1,260p' printf '\n--- skill registry ---\n' cat -n src/skills/skill-registry.ts | sed -n '1,260p'Repository: josephfung/curia
Length of output: 12946
🏁 Script executed:
#!/bin/bash set -euo pipefail printf '\n--- parser candidates ---\n' rg -n "parse.*tool|tool manifest|ToolManifest|inputs.*string\\?|endsWith\\('\\?'\\)|endsWith\\(\"\\?\"\\)|nullable|optional" src/skills skills -S printf '\n--- skill / tool related source files ---\n' fd -a ".*(tool|skill|manifest|registry).*\\.(ts|js)$" src | sed -n '1,200p'Repository: josephfung/curia
Length of output: 28480
🏁 Script executed:
#!/bin/bash set -euo pipefail printf '\n--- src/skills/registry.ts relevant section ---\n' cat -n src/skills/registry.ts | sed -n '120,190p' printf '\n--- src/skills/types.ts relevant ToolManifest section ---\n' cat -n src/skills/types.ts | sed -n '31,120p'Repository: josephfung/curia
Length of output: 10112
Allow
blocked_by_task_idto be nullable.handler.tstreatsnullas the way to clear the dependency, butstring?is parsed as optionalstringinsrc/skills/registry.ts, so the generated schema still rejectsnull. Callers can omit the field, but they cannot clear an existing blocker — an irritating little contract mismatch.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@skills/tasks/tools/task-update/tool.json` at line 16, Update the blocked_by_task_id schema declaration in task-update tool.json to accept both string values and null, while preserving its optional/omittable behavior. Ensure the corresponding parsing in the registry produces a nullable optional field so handler.ts can continue using null to clear an existing dependency.src/channels/http/routes/registry.ts (1)
27-32: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Include
skillsin the invalid-kind response.Line 31 accepts
skills, but the action handler still tells callers that only tools or agents are valid. The UI will receive misleading guidance, which is about as useful as a tube map in a power cut.Proposed fix
- error: `Unknown kind '${rawKind}' (expected 'tools' or 'agents')`, + error: `Unknown kind '${rawKind}' (expected 'tools', 'agents', or 'skills')`,🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/channels/http/routes/registry.ts` around lines 27 - 32, Update the action handler’s invalid-kind response to list skills alongside tools and agents as valid kinds. Keep parseKind’s existing mapping unchanged and ensure callers receive the complete set of supported URL segments.src/registry/registry-repo.ts (1)
96-99: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Keep an idempotent install from rewriting
updated_at.The conflict update fires
skill_registry_updated_at, so reinstalling an existing skill changes its audit timestamp despite the comment promising it is left untouched. A rather elaborate way for time to become fiction.Use an insert/select or retry-safe
DO NOTHINGflow that returns the existing row without issuing anUPDATE.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/registry/registry-repo.ts` around lines 96 - 99, Update the install query in the registry repository so conflict handling does not issue an UPDATE or trigger skill_registry_updated_at. Preserve returning the existing skill_registry row for duplicate names by using an insert/select or retry-safe DO NOTHING flow, while retaining the current behavior for new installs.src/skills/skill-loader.ts (1)
102-120: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Empty
catch {}swallows nestedtool.jsonfailures without a peep.Lines 115-117 catch and discard, no log, no propagate. Guidelines here are unambiguous: every catch must log, audit, and propagate. The comment claims it's "reported when tool discovery runs," but that's a different function on a different call path — this one, right here, says nothing to anyone. Truly the sound of one hand clapping.
🔧 Proposed fix — thread an optional logger and warn
-export function discoverNestedToolNames(skillDir: string): string[] { +export function discoverNestedToolNames(skillDir: string, logger?: Logger): string[] { const toolsDir = path.join(skillDir, 'tools'); if (!fs.existsSync(toolsDir)) return []; const names: string[] = []; for (const entry of fs.readdirSync(toolsDir, { withFileTypes: true })) { if (!entry.isDirectory()) continue; const manifestPath = path.join(toolsDir, entry.name, 'tool.json'); if (!fs.existsSync(manifestPath)) continue; try { const raw = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) as { name?: unknown }; if (typeof raw.name === 'string' && raw.name.trim()) { names.push(raw.name.trim()); } - } catch { - // Lenient: broken nested tool is reported when tool discovery runs. + } catch (err) { + logger?.warn( + { manifestPath, error: err instanceof Error ? err.message : String(err) }, + 'skill discovery: failed to read nested tool.json', + ); } } return names; }🧰 Tools
🪛 ast-grep (0.44.1)
[warning] 110-110: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.readFileSync(manifestPath, 'utf-8')
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').(detect-non-literal-fs-filename-typescript)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/skills/skill-loader.ts` around lines 102 - 120, Update discoverNestedToolNames to accept an optional logger and warn when parsing a nested tool.json fails, including the manifest context and caught error details. Replace the silent catch while preserving lenient discovery behavior by continuing to the remaining entries after logging.Source: Coding guidelines
src/skills/skill-md.ts (1)
84-93: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Invalid
heartbeat/document_workspacevanish into the void, silently.Rows 84-85 turn anything other than a literal boolean
trueintoundefined— no throw, no warning, nothing. Meanwhileversionandtoolstwo blocks up correctly throw when given the wrong type. A skill author who writesheartbeat: "true"(a string, easy typo) gets a skill that quietly isn't heartbeat-eligible, and no signal as to why. Riveting way to lose an afternoon debugging.🔧 Proposed fix — validate instead of silently discarding
- const heartbeat = fm.heartbeat === true ? true : undefined; - const document_workspace = fm.document_workspace === true ? true : undefined; + if (fm.heartbeat !== undefined && typeof fm.heartbeat !== 'boolean') { + throw new Error(`${sourceLabel}: frontmatter.heartbeat must be a boolean when set`); + } + if (fm.document_workspace !== undefined && typeof fm.document_workspace !== 'boolean') { + throw new Error(`${sourceLabel}: frontmatter.document_workspace must be a boolean when set`); + } + const heartbeat = fm.heartbeat === true ? true : undefined; + const document_workspace = fm.document_workspace === true ? true : undefined;📝 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.if (fm.heartbeat !== undefined && typeof fm.heartbeat !== 'boolean') { throw new Error(`${sourceLabel}: frontmatter.heartbeat must be a boolean when set`); } if (fm.document_workspace !== undefined && typeof fm.document_workspace !== 'boolean') { throw new Error(`${sourceLabel}: frontmatter.document_workspace must be a boolean when set`); } const heartbeat = fm.heartbeat === true ? true : undefined; const document_workspace = fm.document_workspace === true ? true : undefined; return { name: name.trim(), description: description.trim(), version, tools, heartbeat, document_workspace,🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/skills/skill-md.ts` around lines 84 - 93, Update the frontmatter handling for heartbeat and document_workspace in the skill metadata construction to validate their types consistently with version and tools. Reject non-boolean values, including strings such as "true", instead of converting them to undefined; preserve the existing behavior for omitted fields and valid booleans.
Align calendar risk docs and displayTimezone manifests, make skill registry install conflict-safe, tighten skill-md boolean parsing, and rename AgentRuntime pinnedSkills to pinnedTools. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Joseph Fung <josephfung@users.noreply.github.com>
|
CodeRabbit’s 8 actionable + 2 nitpick findings are addressed in f6b2758. Their inline review comments failed to post on GitHub (platform limit/error), so replies are filed as path/line notes on each site rather than Summary: calendar risk/docs + |
CI failed because task-update's blocked_by_task_id used string|null?, which the manifest parser rejected. Parse T|null unions into JSON Schema type arrays. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Joseph Fung <josephfung@users.noreply.github.com>
|
@CodeRabbit review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/smoke/harness.ts`:
- Around line 203-204: Update the harness configuration assignment around
pinnedTools to use the resolved values from pinResolution: pass
pinResolution.toolNames instead of agentPinnedSkills, and pass its generated
tool definitions instead of agentToolDefs. Mirror the resolution flow used in
src/index.ts so skill bundles expand into member tools and preserve their
associated instructions and capability flags.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 4ca42bd5-dffe-4c1d-ae87-34ade061b47a
📒 Files selected for processing (24)
CHANGELOG.mdskills/calendar/SKILL.mdskills/calendar/tools/calendar-check-conflicts/tool.jsonskills/calendar/tools/calendar-create-hold/tool.jsonskills/documents/tools/doc-search/tool.jsonskills/tasks/tools/task-update/handler.tsskills/tasks/tools/task-update/tool.jsonsrc/agents/runtime.tssrc/channels/http/routes/registry.tssrc/index.tssrc/registry/registry-repo.tssrc/skills/registry.tssrc/skills/skill-loader.tssrc/skills/skill-md.tssrc/skills/skill-registry.tstests/integration/multi-agent-delegation.test.tstests/integration/research-analyst-multi-turn.test.tstests/integration/setup-wizard-delegate.test.tstests/integration/skill-invocation.test.tstests/smoke/harness.tstests/unit/agents/runtime.test.tstests/unit/skills/calendar-check-conflicts.test.tstests/unit/skills/pin-resolution.test.tstests/unit/skills/registry.test.ts
🚧 Files skipped from review as they are similar to previous changes (11)
- skills/tasks/tools/task-update/handler.ts
- skills/calendar/tools/calendar-check-conflicts/tool.json
- tests/unit/skills/calendar-check-conflicts.test.ts
- skills/calendar/SKILL.md
- src/skills/skill-md.ts
- src/skills/skill-loader.ts
- skills/tasks/tools/task-update/tool.json
- tests/unit/skills/pin-resolution.test.ts
- skills/calendar/tools/calendar-create-hold/tool.json
- src/skills/skill-registry.ts
- src/index.ts
Mirror src/index.ts: load SkillRegistry, resolvePinnedSkills, append instruction blocks, and pass expanded tool names/defs to AgentRuntime. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Joseph Fung <josephfung@users.noreply.github.com>
EmailAdapter.poll now guards non-array list results, and the watermark persistence tests keep returning [] on interval ticks after Once mocks. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Joseph Fung <josephfung@users.noreply.github.com>
Summary
Closes #1489 (Phase 2 of epic #1436).
Important
Requires curia-deploy #179 to co-merge and co-deploy. This PR removes the
enable_task_managementagent flag, and the agent-config schema isadditionalProperties: false. Thesocial-mediacustom deploy agent still declaresthe flag, so shipping core alone fails startup validation on deploy. curia-deploy
#179 migrates it to
pinned_skills: [tasks, documents]— land and deploy the two together.Introduces the skill (bundle) as a first-class collection of tools + optional instructions — the install / pin / discover unit — on top of Phase 1’s tool vocabulary (ADR-031).
What changed
SkillRegistry+SKILL.mdparser; nested layoutskills/<skill>/SKILL.md+tools/<tool>/pinned_skillsretarget — pins expand to member tools; instruction bodies inject into the system promptenable_task_managementretired — replaced by pinningtasks+documents(independent skills)calendar,tasks,documents; remaining flat tools auto-wrap as synthetic singleton skillsaction_risk/allowed_callers076_create_skill_registry.sql+RegistryKind: 'skill'taskRepowiring — enabled when eithertasks(heartbeat) ordocumentsis pinnedOpen questions (resolved for this phase)
action_risk/allowed_callersaggregationtoolSearchreturns tools only for nowschemas/agent-config.schema.jsonisadditionalProperties: false. Startup validation (src/startup/validator.ts) rejects unknown agent YAML keys with a hard boot failure (unknown property: …).curia-deploy/custom/agents/social-media.yamlstill setsenable_task_management: true. Before deploying this PR, land a coordinated curia-deploy change that:enable_task_managementtasksanddocumentsinsteadWithout that, the next deploy fails agent YAML validation at startup.
Out of scope
Phases 3–5 (Claude-skill import, sandbox, external docs sync). Remaining cluster bundling tracked in #1494.
Test plan
pnpm run typecheck+ console typecheckpnpm run lint076createsskill_registry; prefix unique vs075)taskRepowired for heartbeat-only pin (tasks without documents)social-media.yamlmigration before prod deploy