Skip to content

fix(skills): preserve unknown frontmatter keys on write, project to known keys on read - #658

Open
rubenvdlinde wants to merge 2 commits into
nextcloud:mainfrom
rubenvdlinde:fix/skills-frontmatter-preserve-and-project
Open

rubenvdlinde wants to merge 2 commits into
nextcloud:mainfrom
rubenvdlinde:fix/skills-frontmatter-preserve-and-project

Conversation

@rubenvdlinde

Copy link
Copy Markdown

Implements #657.

listSkills already answers with name and description alone. loadSkill returns the raw file and storeSkill rewrites the frontmatter from scratch. This makes all three agree: the file keeps every key, the API hands back the ones the agent needs.

Two commits, and the first stands on its own if the second needs more discussion.

1. Keep unknown frontmatter keys when a skill is overwritten

storeSkill built the frontmatter from name and description and wrote it over whatever was there. Any other key was destroyed, and nothing reported it. A store_skill tool call was enough to lose metadata a user or another app had written.

It now reads the existing frontmatter first and keeps everything it does not own. name and description stay first in the block. A file whose frontmatter cannot be parsed is replaced as before, since that is what the caller asked for, and the reason is logged at debug level.

2. Return only the known frontmatter keys from loadSkill

loadSkillFromFolder ended in return $skillFile->getContent();, so every key reached the caller. The caller is context_agent's load_skill, which passes the string straight to a model, so each extra key costs tokens the model cannot spend. nextcloud/context_agent#212 measures the same problem on the list path.

loadSkill now returns the body plus name and description. A document without valid frontmatter is returned unchanged, so a malformed skill still loads.

What changes for callers

The response of GET /ocs/v2.php/apps/assistant/api/v1/skills/{skillName} no longer carries frontmatter keys beyond name and description. The body is byte for byte what it was.

This is the contract listSkills already follows, and context_agent is the only consumer we know of. Say the word if you would rather have this behind a parameter and I will add one.

Testing

Six tests added to AgentSkillsServiceTest, three per commit, covering preservation on overwrite, key ordering, an unparsable existing file, projection on load, a body that contains its own --- rule line, and an unparsable document loading unchanged.

AgentSkillsServiceTest runs green against a Nextcloud 35.0.1 RC1 instance: OK (37 tests, 69 assertions).

extractFrontmatter is refactored onto a shared splitter that also returns the body. Its signature, its two exception messages and its existing tests are untouched.

🤖 AI (if applicable)

  • The content of this PR was partly or fully generated using AI (N/A)

@coderabbitai

coderabbitai Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: d977367a-42d6-460a-a514-a25ac37375f3

📥 Commits

Reviewing files that changed from the base of the PR and between 90a4171 and 5dcf351.

📒 Files selected for processing (2)
  • lib/Service/AgentSkillsService.php
  • tests/unit/Service/AgentSkillsServiceTest.php

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

storeSkill preserves unknown frontmatter keys during overwrites and keeps name and description first. Unparsable frontmatter is replaced. loadSkillFromFolder returns known metadata fields with the original body. Unparsable documents remain unchanged. Tests cover preservation, ordering, replacement, filtering, byte-for-byte body retention, and unchanged invalid documents.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 5dcf3

The metadata overwrite and load behavior has no remaining actionable risk from the reviewed changes. The numeric-key preservation regression is addressed, and empty frontmatter safely follows the existing fallback path.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.11% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 19 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly explains the frontmatter preservation, projection, malformed-document behavior, API impact, and test coverage described by the changeset.
Title check ✅ Passed The title clearly summarizes the main changes: preserving unknown frontmatter keys on write and projecting known keys on read.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: be531ec9-9afe-4fd6-abfe-8baa965a5283

📥 Commits

Reviewing files that changed from the base of the PR and between b4fd52f and 90a4171.

📒 Files selected for processing (2)
  • lib/Service/AgentSkillsService.php
  • tests/unit/Service/AgentSkillsServiceTest.php

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.

Comment thread lib/Service/AgentSkillsService.php Outdated

// name and description are ours to write. Every other key belongs to whoever
// put it there, so an overwrite keeps it instead of dropping it silently.
$frontmatter = Yaml::dump(array_merge(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Preserve numeric YAML keys during overwrite.

Yaml::parse() can produce integer mapping keys. array_merge() renumbers those keys, so an existing key such as 2024: is stored as 0: after an overwrite. Use the array union operator after filtering the owned keys. Add a test with a numeric custom frontmatter key.

Proposed fix
-		$frontmatter = Yaml::dump(array_merge(
-			[
+		$frontmatter = Yaml::dump(
+			[
 				'name' => $skillName,
 				'description' => $description,
-			],
-			$skillFile !== null ? $this->preservedFrontmatterKeys($skillFile) : [],
-		));
+			] + ($skillFile !== null ? $this->preservedFrontmatterKeys($skillFile) : []),
+		);

storeSkill rebuilt the frontmatter from name and description and wrote it
over whatever was there, so any other key was destroyed and nothing
reported it. A store_skill tool call was enough to lose metadata a user
or another app had written.

It now reads the existing frontmatter first and keeps every key it does
not own. name and description stay first in the block. Keys are combined
with the union operator rather than array_merge, because YAML mapping
keys can be integers and array_merge renumbers those. A file whose
frontmatter cannot be parsed is replaced as before, since that is what
the caller asked for, and the reason is logged at debug level.

extractFrontmatter moves onto a shared splitter that also returns the
body. Its signature, its two exception messages and its tests are
unchanged.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Ruben van der Linde <ruben@conduction.nl>
loadSkillFromFolder returned the file as it was, so every frontmatter key
reached the caller. That caller is context_agent's load_skill, which hands
the string straight to a language model, so each extra key costs tokens
the model cannot spend.

loadSkill now returns the body plus name and description, which is the
contract listSkills already follows. A document without valid frontmatter
is returned unchanged, so a malformed skill still loads.

Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Ruben van der Linde <ruben@conduction.nl>
@rubenvdlinde
rubenvdlinde force-pushed the fix/skills-frontmatter-preserve-and-project branch from 90a4171 to 5dcf351 Compare September 22, 2026 16:26
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.

1 participant