Skip to content

feat: add additionalProperties support for skills#223

Merged
cteyton merged 46 commits intomainfrom
206-skills-properties
Mar 23, 2026
Merged

feat: add additionalProperties support for skills#223
cteyton merged 46 commits intomainfrom
206-skills-properties

Conversation

@cteyton
Copy link
Copy Markdown
Contributor

@cteyton cteyton commented Mar 19, 2026

Explanation

Add full support for skill additionalProperties (e.g., user-invocable, model, context) across the stack — from parsing and storage through CLI diffing, change proposals, and frontend display.

Relates to #206

Type of Change

  • Bug fix
  • New feature
  • Improvement/Enhancement
  • Refactoring
  • Documentation
  • Breaking change

Affected Components

  • Domain packages affected: types, skills, playbook
  • Frontend / Backend / Both: Both
  • Breaking changes (if any): None

Testing

  • Unit tests added/updated
  • Integration tests added/updated
  • Manual testing completed
  • Test coverage maintained or improved

Test Details:

  • Added extractProposalDiffValues.spec.ts covering all proposal type branches (scalar, additional property with key prefix, collection update/add/delete, unknown)
  • Existing test coverage for skill parsing, additional properties flow, and conflict detection

TODO List

  • CHANGELOG Updated
  • Documentation Updated

Reviewer Notes

  • Key fix in this branch: the review changes diff now shows property keys alongside values (e.g., "user-invocable: false""user-invocable: true") instead of just "false""true"
  • The extractProposalDiffValues function has a dedicated branch for updateSkillAdditionalProperty that reads targetId (the property key) from the payload

🤖 Generated with Claude Code

cteyton and others added 10 commits March 18, 2026 15:14
- Add additionalProperties JSONB column to skills and skill_versions tables
- Parse Claude Code-specific frontmatter fields (model, hooks, context, etc.) into additionalProperties
- Thread additionalProperties through upload, diff, and deploy pipelines
- Add updateSkillAdditionalProperty change proposal type with per-key granularity
- Add conflict detection, validation, and applier for the new CP type
- Render additionalProperties in ClaudeDeployer SKILL.md output (not Copilot/Cursor)
- Add frontend display in review original/result tabs

#206

Co-Authored-By: Claude Opus 4.6 <[email protected]>
…lifecycle

Extract Claude Code additional fields (model, user-invocable, hooks, etc.) in
parseSkillDirectory and serialize them back into SKILL.md in the create applier,
so new skills created via `diff add` preserve these properties end-to-end.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Cover additionalProperties in uploadSkill create/update/content-identity
paths, expand parseSkillDirectory to all 7 Claude Code fields, and add
context/agent assertions to parseSkillMd.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Display key-value pairs (e.g. "disableModelInvocation: true") instead of
bare values when rendering additionalProperties changes in the diff command.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…conflict detector

The ConflictDetector type requires 3 parameters but the function was only
accepting 2, causing a TypeScript build error. Also narrow cp2 type after
the runtime type guard to satisfy TypeScript's type checker. Split
multi-expect tests into one-expect-per-test to satisfy the
backend-tests-redaction standard.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Render skill additional properties (argument-hint, hooks, model, etc.)
in the frontmatter UI with deterministic ordering and kebab-case keys,
matching the ClaudeDeployer output format. Complex nested values like
hooks render in YAML-like indented format while simple values stay inline.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Prepend the property key (e.g. "user-invocable") to values displayed in
the review changes diff for updateSkillAdditionalProperty proposals, so
reviewers see "user-invocable: false → true" instead of just "false → true".

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 19, 2026

Greptile Summary

This PR adds full end-to-end support for skill additionalProperties (Claude Code-specific frontmatter fields such as model, user-invocable, argument-hint, hooks, etc.) across the entire stack — from YAML parsing and DB storage (JSONB migration) through CLI diffing, change-proposal creation/validation/application, and frontend display.

Key design decisions are well-executed:

  • A shared CLAUDE_CODE_ADDITIONAL_FIELDS whitelist (kebab → camelCase) and its inverse CAMEL_TO_YAML_KEY live in @packmind/types so every layer references the same mapping.
  • The diff pipeline consistently uses canonicalJsonStringify to serialize raw DB values into JSON-encoded strings for transport, and JSON.parse on the applier side to recover them — the serialization contract is clearly documented.
  • Each deployer (ClaudeDeployer, CopilotDeployer, CursorDeployer) applies its own allow-list filter before emitting YAML, ensuring agent-specific frontmatter boundaries are respected.
  • computeOutdatedProposalIds and SkillChangeProposalValidator both use canonicalJsonStringify for comparison and correctly handle the 'null' sentinel (absent property) vs an actual stored value.

One notable issue found:

  • In toYamlLike.ts, the simple-array branch returns [items] without prepending pad, so when called with indent > 0 from SkillCreateChangeProposalApplier.ts the array appears at column 0 on a new line, which can produce invalid YAML. The same branch also leaves string elements unquoted, risking type coercion for reserved YAML words.

Changes also include:

  • SingleFileDeployer: renames allowedTools: to allowed-tools: in generated frontmatter (spec-compliant fix) and prefers skillVersion.files over the skillFilesMap fallback.
  • Frontend SkillReviewDetail now also invalidates the getSkillBySlugKey query after applying proposals, so slug-keyed caches stay in sync.

Confidence Score: 4/5

  • PR is safe to merge; the core diff/validate/apply pipeline is correct and well-tested. The toYamlLike array bug only surfaces for simple-array additional-property values in the skill-creation path, which is not exercised by current production payloads.
  • The serialization pipeline (canonicalJsonStringify throughout), the 'null' sentinel handling, and the per-deployer allow-list filtering are all consistent and thoroughly unit-tested. The one identified logic issue (toYamlLike missing pad + unquoted strings for simple arrays) is a real correctness gap but confined to an edge-case code path (createSkill proposals with array-valued additional properties), so it does not block the main feature.
  • packages/types/src/skills/toYamlLike.ts — simple array formatting bug affects YAML generation in SkillCreateChangeProposalApplier.

Important Files Changed

Filename Overview
packages/types/src/skills/toYamlLike.ts New utility for display and YAML generation; simple-array branch omits the pad prefix and leaves string values unquoted, which can produce invalid YAML when used with indent > 0 in SkillCreateChangeProposalApplier.
packages/types/src/skills/skillAdditionalProperties.ts New module defining the CLAUDE_CODE_ADDITIONAL_FIELDS map, CAMEL_TO_YAML_KEY inverse, ordering constants, and filter/sort helpers. Well-structured and correctly shared across CLI, backend, and frontend packages.
packages/types/src/playbookChangeManagement/applier/SkillChangeProposalApplier.ts Adds updateSkillAdditionalProperty handler: deletes property when newValue is empty string, otherwise JSON.parses the value back to raw for DB storage. Consistent with the serialization contract used throughout the diff pipeline.
apps/cli/src/application/useCases/diffStrategies/SkillDiffStrategy.ts Correctly iterates the union of server/local additional property keys, using 'null' as the sentinel for absence on the server side and '' for absence on the local side, consistent with the validator and applier contracts.
packages/node-utils/src/skillMd/parseSkillMd.ts Adds additionalProperties extraction using CLAUDE_CODE_ADDITIONAL_FIELDS whitelist and canonicalJsonStringify serialization. SPEC_FIELDS correctly matches normalised camelCase keys from parseSkillMdContent.
apps/frontend/src/domain/change-proposals/utils/computeOutdatedProposalIds.ts Adds updateSkillAdditionalProperty outdated-detection using canonicalJsonStringify, correctly handling missing properties (null sentinel) and removing the duplicated local serializeSkillMetadata in favour of the shared canonicalJsonStringify.
apps/frontend/src/domain/change-proposals/utils/extractProposalDiffValues.ts New branch for updateSkillAdditionalProperty correctly prepends kebab-case key to old/new values for display, treating the 'null' sentinel and empty string as absence (returns empty string).
packages/playbook-change-management/src/application/validators/SkillChangeProposalValidator.ts Adds updateSkillAdditionalProperty validation using latest SkillVersion (preferred over Skill to avoid version drift) and canonicalJsonStringify for comparison. Correctly falls back to Skill data when no SkillVersion exists.
packages/migrations/src/migrations/1774070400000-AddAdditionalPropertiesToSkills.ts Adds nullable JSONB additional_properties column to both skills and skill_versions tables. Rollback drops columns in reverse order. Well-structured migration.
packages/coding-agent/src/infra/repositories/utils/YamlFrontmatterUtils.ts New YAML formatting utility for deployers. Properly quotes string scalars, handles nested objects and arrays recursively, and sorts nested object keys alphabetically for deterministic output.

Sequence Diagram

sequenceDiagram
    participant CLI as CLI (SkillDiffStrategy)
    participant API as API (SkillChangeProposalValidator)
    participant Applier as SkillChangeProposalApplier
    participant DB as Database (JSONB)

    CLI->>CLI: parseSkillMd(serverContent)<br/>→ additionalProperties[camelKey] = canonicalJsonStringify(raw)
    CLI->>CLI: parseSkillMd(localContent)<br/>→ additionalProperties[camelKey] = canonicalJsonStringify(raw)
    CLI->>CLI: diff: oldValue='null'|jsonStr, newValue=jsonStr|''
    CLI->>API: submit updateSkillAdditionalProperty<br/>{ targetId, oldValue, newValue }

    API->>DB: getLatestSkillVersion(skillId)
    DB-->>API: skillVersion.additionalProperties[key] = rawValue
    API->>API: currentValue = canonicalJsonStringify(rawValue ?? null)
    API->>API: verify: expectedOld === currentValue

    API->>Applier: applyChangeProposal(source, proposal)
    alt newValue === ''
        Applier->>Applier: delete additionalProperties[key]
    else
        Applier->>Applier: additionalProperties[key] = JSON.parse(newValue)
    end
    Applier->>DB: save updated SkillVersion
Loading

Reviews (21): Last reviewed commit: "Fix formatting issues" | Re-trigger Greptile

cteyton and others added 6 commits March 19, 2026 11:59
…isArray branch

Fix serverValue fallback from ?? '' to ?? 'null' to match validator and DB
expectations, preventing ChangeProposalPayloadMismatchError for missing
server properties. Remove redundant Array.isArray check in formatEntryValue
since typeof value === 'object' already covers arrays.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
buildSkillMdContent was missing additionalProperties when reconstructing the
SKILL.md frontmatter, causing the Raw tab and copy-to-clipboard to omit
fields like argument-hint, user-invocable, model, etc.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…posals

SaveSkillVersionUsecase was not passing additionalProperties to
addSkillVersion or updateSkill, causing all additional properties to be
silently dropped whenever a change proposal was accepted and applied.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…o fix outdated mismatch

The validator was comparing against Skill.additionalProperties while the CLI
diffs against the deployed SKILL.md (rendered from SkillVersion). When these
were out of sync, all additionalProperties submissions failed with
"skill is outdated". Now uses SkillVersion as the source of truth, falling
back to Skill only when no version exists. Also includes mismatch details
in the CLI error message for easier debugging.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…tch in additional properties

JSON.stringify does not guarantee key ordering, causing "oldValue does not match"
errors when comparing nested objects (e.g. hooks) serialized from different sources
(YAML parsing vs PostgreSQL JSONB vs API response). Replace with canonicalJsonStringify
that recursively sorts keys before serialization.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…oposals

The skill detail page fetches data by slug, but only the by-ID cache was
invalidated after accepting change proposals, causing stale frontmatter display.

#206

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
cteyton pushed a commit that referenced this pull request Mar 19, 2026
Identifies 6 issues including a high-severity non-canonical JSON
comparison bug in uploadSkill.usecase.ts that causes false version bumps.

https://claude.ai/code/session_01Vp2EoraDJfuhpR2sv3QWti
cteyton and others added 11 commits March 19, 2026 18:03
Move pure functions (deepSortKeys, canonicalJsonStringify, camelToKebab,
sortAdditionalPropertiesKeys) and constants (CLAUDE_CODE_ADDITIONAL_FIELDS,
CLAUDE_CODE_ADDITIONAL_FIELDS_ORDER, CAMEL_TO_YAML_KEY) into @packmind/types
so both frontend and backend can import from a single source of truth.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Remove local deepSortKeys, canonicalJsonStringify, CLAUDE_CODE_ADDITIONAL_FIELDS,
CLAUDE_CODE_ADDITIONAL_FIELDS_ORDER, and sortAdditionalPropertiesKeys. Import and
re-export from @packmind/types to maintain backward compatibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
When adding a new property, oldValue is 'null' (JSON sentinel). Because
'null' is truthy, the CLI displayed a misleading '- model: null' line.
Filter out the 'null' sentinel to show only meaningful diff lines.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…ops logic

Replace local ADDITIONAL_FIELDS_ORDER, camelToKebab, and
sortAdditionalPropertiesEntries with imports from @packmind/types.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…tants

Replace local camelToKebab, CAMEL_TO_YAML_KEY, and ADDITIONAL_FIELDS_ORDER
with imports from @packmind/types.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…zation

Replace local deepSortKeys, canonicalJsonStringify, and serializeSkillMetadata
with canonicalJsonStringify from @packmind/types.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…h error handling

Unprotected JSON.parse calls could throw opaque SyntaxErrors on malformed
payloads. Introduce ChangeProposalPayloadParseError for clear diagnostics.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…andler

Replace inline additional property diff logic with the extracted utility
that was created but not yet wired in production code.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
… @packmind/types

Extract toYamlLike utility into a shared module with unit tests so it can
be reused and is properly covered.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Change parsedMetadata from Record<string, unknown> to Record<string, string> to match expected metadata type

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@cteyton
Copy link
Copy Markdown
Contributor Author

cteyton commented Mar 20, 2026

@greptile update your review

cteyton and others added 5 commits March 20, 2026 12:47
…lFrontmatterInfo

Export the existing isDeepValue function from @packmind/types instead of
duplicating it in the frontend component.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…ingify for YAML frontmatter

JSON.stringify produces quoted JSON notation instead of proper YAML values
in the generated SKILL.md frontmatter for additional properties.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…llMdContent

Replace inline sorting logic with the shared sortAdditionalPropertiesKeys
utility to avoid duplicating the ordering implementation.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…alues

Cover the oldValue: 'null' and newValue: 'null' code paths that handle
property addition from null and property removal to null.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@cteyton
Copy link
Copy Markdown
Contributor Author

cteyton commented Mar 20, 2026

@greptile update your review

cteyton and others added 4 commits March 20, 2026 13:37
… targetId to kebab-case in diffs

- Replace duplicate camelToKebab implementations with shared utility from @packmind/types
- Use CAMEL_TO_YAML_KEY fallback map for correct YAML key resolution
- Add null/undefined guard in formatYamlScalar to produce bare YAML null
- Convert camelCase targetId to kebab-case in CLI and frontend diff displays

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Simple arrays (all primitives) like argument-hint now render as
`argument-hint: ['issue-number']` instead of block style with `- ` entries.
Complex arrays containing objects still use block style.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Register effort in CLAUDE_CODE_ADDITIONAL_FIELDS and field order
- Add tests for parsing, rendering, and mapping of effort property

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@jracenet
Copy link
Copy Markdown
Contributor

jracenet commented Mar 23, 2026

  • Error when installing a package containing a skill, when rendering settings contains "Gitlab Duo"
  • compatible custom attributes are not rendered for other agents than Claude. For example, argument-hint is supporter by Copilot as well. (funny thing is that packmind skills add on a Copilot skill with custom attributes works)

@cteyton
Copy link
Copy Markdown
Contributor Author

cteyton commented Mar 23, 2026

  • compatible custom attributes are not rendered for other agents than Claude. For example, argument-hint is supporter by Copilot as well. (funny thing is that packmind skills add on a Copilot skill with custom attributes works)

I also noticed that Cursor supports the disable-model-invocation attribute.

cteyton and others added 3 commits March 23, 2026 13:23
…rsor deployers

- Extract YAML formatting functions to shared YamlFrontmatterUtils
- Add COPILOT_ADDITIONAL_FIELDS (argument-hint, user-invocable, disable-model-invocation)
- Add CURSOR_ADDITIONAL_FIELDS (disable-model-invocation)
- Add filterAdditionalProperties utility function
- Add tests for both deployers and filter utility

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…skillVersion.files support

Fix two rendering inconsistencies in SingleFileDeployer (used by GitlabDuoDeployer):
- Render allowedTools as kebab-case `allowed-tools` in YAML frontmatter
- Check skillVersion.files first (with isBase64, skillFileId, skillFilePermissions),
  falling back to skillFilesMap, matching Claude/Copilot/Cursor deployer behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@cteyton
Copy link
Copy Markdown
Contributor Author

cteyton commented Mar 23, 2026

  • fix also a bug where skill files were not properly rendered for gitlab duo (existing one, not linked to this task)

cteyton and others added 6 commits March 23, 2026 13:42
… skillAdditionalProperties tests

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…ployer

escapeSingleQuotes crashes with "value.replace is not a function" when
metadata contains non-string values (e.g. booleans from additionalProperties).
Wrap the value with String() before calling replace.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@sonarqubecloud
Copy link
Copy Markdown

@cteyton cteyton merged commit 3f52e84 into main Mar 23, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants