@skilly-hand/skilly-hand 0.16.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -16,6 +16,44 @@ All notable changes to this project are documented in this file.
16
16
  ### Removed
17
17
  - _None._
18
18
 
19
+ ## [0.18.0] - 2026-04-08
20
+ [View on npm](https://www.npmjs.com/package/@skilly-hand/skilly-hand/v/0.18.0)
21
+
22
+ ### Added
23
+ - Added `sync-catalog` orchestration script to compute catalog README + skill frontmatter updates up front and apply writes atomically with rollback on failure.
24
+ - Added `sync-skill-frontmatter` CLI script with `--check`, `--json`, and `--skill` filtering support.
25
+ - Added regression coverage for catalog sync rollback/idempotency and frontmatter normalization edge cases (`tests/sync-catalog.test.js`, `tests/skill-frontmatter.test.js`).
26
+
27
+ ### Changed
28
+ - Updated root `catalog:sync` script to run `scripts/sync-catalog.mjs` for unified catalog synchronization.
29
+ - Expanded script JSON contract coverage for `sync-catalog` and `sync-skill-frontmatter` in `tests/scripts-output.test.js`.
30
+ - Updated catalog validation flow to verify catalog README drift through dry-run sync checks.
31
+
32
+ ### Fixed
33
+ - Hardened skill frontmatter parsing and verification to avoid false frontmatter detection and preserve markdown content for malformed leading YAML-like blocks.
34
+ - Improved catalog README sync behavior to treat CRLF/LF-equivalent content as in sync.
35
+
36
+ ### Removed
37
+ - _None._
38
+
39
+ ## [0.17.0] - 2026-04-08
40
+ [View on npm](https://www.npmjs.com/package/@skilly-hand/skilly-hand/v/0.17.0)
41
+
42
+ ### Added
43
+ - Added sandbox harness and matrix integration tests (`scripts/test-in-sandbox.mjs`, `tests/sandbox-harness.test.js`, `tests/sandbox-matrix.test.js`) and wired them into the root test pipeline.
44
+ - Added required `review-rangers` final-gate guidance to the spec-driven-development verify flow and validation checklist.
45
+
46
+ ### Changed
47
+ - Updated installer reconciliation logic to remove stale managed targets when agent selection narrows, while preserving/restoring backups for retained targets.
48
+ - Updated backup behavior to skip backup creation for files already marked as managed content.
49
+ - Updated root `npm test` to run sandbox integration verification after the node test suite.
50
+
51
+ ### Fixed
52
+ - Fixed uninstall and re-install behavior for narrowed agent selections by restoring original files and cleaning obsolete managed artifacts.
53
+
54
+ ### Removed
55
+ - _None._
56
+
19
57
  ## [0.16.1] - 2026-04-08
20
58
  [View on npm](https://www.npmjs.com/package/@skilly-hand/skilly-hand/v/0.16.1)
21
59
 
@@ -1,3 +1,24 @@
1
+ ---
2
+ description: "Audit web accessibility against W3C WCAG 2.2 Level AA using framework-agnostic checks, remediation patterns, and portable command-line scanning."
3
+ skillMetadata:
4
+ author: "skilly-hand"
5
+ last-edit: "2026-04-04"
6
+ license: "Apache-2.0"
7
+ version: "1.0.0"
8
+ changelog: "Added portable WCAG 2.2 Level AA accessibility auditing skill with W3C-only references and scanner script; enables consistent web accessibility review across frameworks; affects catalog skill coverage and install plans for stacks recommending accessibility-audit"
9
+ auto-invoke: "Auditing, reviewing, or implementing web accessibility against WCAG 2.2 Level AA"
10
+ allowed-tools:
11
+ - "Read"
12
+ - "Edit"
13
+ - "Write"
14
+ - "Glob"
15
+ - "Grep"
16
+ - "Bash"
17
+ - "WebFetch"
18
+ - "WebSearch"
19
+ - "Task"
20
+ - "SubAgent"
21
+ ---
1
22
  # Accessibility Audit Guide
2
23
 
3
24
  ## When to Use
@@ -1,3 +1,22 @@
1
+ ---
2
+ description: "Author root AGENTS.md as a Where/What/When orchestrator that routes tasks and skill invocation clearly."
3
+ skillMetadata:
4
+ author: "skilly-hand"
5
+ last-edit: "2026-04-03"
6
+ license: "Apache-2.0"
7
+ version: "1.0.0"
8
+ changelog: "Added root AGENTS orchestration guidance around Where/What/When structure; improves AI task routing clarity and trigger recognition; affects root AGENTS authoring workflow"
9
+ auto-invoke: "Creating or updating root AGENTS.md orchestration guidance"
10
+ allowed-tools:
11
+ - "Read"
12
+ - "Edit"
13
+ - "Write"
14
+ - "Glob"
15
+ - "Grep"
16
+ - "Bash"
17
+ - "Task"
18
+ - "SubAgent"
19
+ ---
1
20
  # AGENTS Root Orchestrator Guide
2
21
 
3
22
  ## When to Use
@@ -1,3 +1,24 @@
1
+ ---
2
+ description: "Guide Angular code generation and review using latest stable Angular verification and modern framework best practices."
3
+ skillMetadata:
4
+ author: "skilly-hand"
5
+ last-edit: "2026-04-03"
6
+ license: "Apache-2.0"
7
+ version: "1.1.1"
8
+ changelog: "Added allowed-modes metadata to declare angular-guidelines sub-agent routing targets; improves discoverability of component-creator and angular-tester delegation modes; affects angular-guidelines manifest metadata"
9
+ auto-invoke: "Generating, reviewing, or refactoring Angular code artifacts in Angular projects"
10
+ allowed-tools:
11
+ - "Read"
12
+ - "Edit"
13
+ - "Write"
14
+ - "Glob"
15
+ - "Grep"
16
+ - "Bash"
17
+ - "WebFetch"
18
+ - "WebSearch"
19
+ - "Task"
20
+ - "SubAgent"
21
+ ---
1
22
  # Angular Guidelines
2
23
 
3
24
  ## When to Use
@@ -1,3 +1,24 @@
1
+ ---
2
+ description: "Guide users from Figma MCP installation and authentication through first canvas creation, with function-level tool coverage and operational recovery patterns."
3
+ skillMetadata:
4
+ author: "skilly-hand"
5
+ last-edit: "2026-04-03"
6
+ license: "Apache-2.0"
7
+ version: "1.0.1"
8
+ changelog: "Added allowed-modes metadata to declare figma-mcp-0to1 sub-agent routing targets; improves discoverability of install-auth, tool-function-catalog, canvas-creation-playbook, and troubleshooting-ops delegation modes; affects figma-mcp-0to1 manifest metadata"
9
+ auto-invoke: "Installing, configuring, or using Figma MCP from setup through first canvas creation"
10
+ allowed-tools:
11
+ - "Read"
12
+ - "Edit"
13
+ - "Write"
14
+ - "Glob"
15
+ - "Grep"
16
+ - "Bash"
17
+ - "WebFetch"
18
+ - "WebSearch"
19
+ - "Task"
20
+ - "SubAgent"
21
+ ---
1
22
  # Figma MCP 0-to-1 Guide
2
23
 
3
24
  ## When to Use
@@ -1,3 +1,20 @@
1
+ ---
2
+ description: "Project-aware frontend design skill that detects the existing tech stack, UI libraries, CSS variables, and design tokens before proposing any UI work. Supports greenfield projects via DESIGN.md context setup, and includes post-generation motion and visual refinement phases."
3
+ skillMetadata:
4
+ author: "skilly-hand"
5
+ last-edit: "2026-04-05"
6
+ license: "Apache-2.0"
7
+ version: "1.1.0"
8
+ changelog: "v1.1.0: Added design-context-setter agent for greenfield/DESIGN.md workflow; added visual-refiner agent for post-generation quality evaluation; added motion-designer agent for stack-aware micro-interactions; added aesthetic-archetypes reference asset; expanded SKILL.md routing map with optional motion and refinement phases; upgraded component-designer with interaction states checklist and aesthetic principles"
9
+ auto-invoke: "Designing or generating UI components, pages, or layouts in a web or mobile project; setting up visual direction for a greenfield project; adding motion or micro-interactions to existing UI; refining or polishing generated UI output"
10
+ allowed-tools:
11
+ - "Read"
12
+ - "Grep"
13
+ - "Glob"
14
+ - "Bash"
15
+ - "Edit"
16
+ - "Write"
17
+ ---
1
18
  # Frontend Design Guide
2
19
 
3
20
  ## When to Use
@@ -1,3 +1,21 @@
1
+ ---
2
+ description: "Optimize output token consumption through compact interpreter modes with controlled expansion when complexity, ambiguity, or risk requires more detail. Trigger: minimizing response verbosity while preserving clarity and correctness."
3
+ skillMetadata:
4
+ author: "skilly-hand"
5
+ last-edit: "2026-04-07"
6
+ license: "Apache-2.0"
7
+ version: "1.0.0"
8
+ changelog: "Added a new portable output compression skill with deterministic interpreter modes and guarded detail expansion; reduces response token costs while preserving safety and clarity; affects response shaping workflows and catalog routing"
9
+ auto-invoke: "When minimizing output verbosity or selecting compact communication modes"
10
+ allowed-tools:
11
+ - "Read"
12
+ - "Edit"
13
+ - "Write"
14
+ - "Glob"
15
+ - "Grep"
16
+ - "Bash"
17
+ - "Task"
18
+ ---
1
19
  # Output Optimizer Guide
2
20
 
3
21
  ## When to Use
@@ -1,3 +1,22 @@
1
+ ---
2
+ description: "Scan project configuration and release surfaces for leak and security risks, and enforce security gates on commit, push, and publish workflows across GitHub, GitLab, npm, pnpm, yarn, and generic CI. Trigger: validating repository security posture, preventing secret leaks, or hardening delivery pipelines."
3
+ skillMetadata:
4
+ author: "skilly-hand"
5
+ last-edit: "2026-04-07"
6
+ license: "Apache-2.0"
7
+ version: "1.0.0"
8
+ changelog: "Added portable project-security skill with commit/push/publish gating assets and CI templates; reduces secret leak and misconfiguration risk before delivery; affects catalog security workflow coverage and auto-invoke routing"
9
+ auto-invoke: "Scanning project configuration and delivery workflows for leaks or security issues before commit, push, or publish"
10
+ allowed-tools:
11
+ - "Read"
12
+ - "Edit"
13
+ - "Write"
14
+ - "Glob"
15
+ - "Grep"
16
+ - "Bash"
17
+ - "Task"
18
+ - "SubAgent"
19
+ ---
1
20
  # Project Security Guide
2
21
 
3
22
  ## When to Use
@@ -1,3 +1,20 @@
1
+ ---
2
+ description: "Scan the active project and teach any concept, code path, or decision using verified information, interactive questions, and simple explanations. Trigger: user asks to explain, understand, clarify, or learn about anything in the project or codebase."
3
+ skillMetadata:
4
+ author: "skilly-hand"
5
+ last-edit: "2026-04-04"
6
+ license: "Apache-2.0"
7
+ version: "1.0.0"
8
+ changelog: "Initial release of project-teacher skill; provides interactive, project-grounded teaching for any concept or code path; affects education and clarification workflows across all projects"
9
+ auto-invoke: "User needs to understand, explain, or learn about any aspect of the project or codebase"
10
+ allowed-tools:
11
+ - "Read"
12
+ - "Glob"
13
+ - "Grep"
14
+ - "Bash"
15
+ - "WebFetch"
16
+ - "WebSearch"
17
+ ---
1
18
  # Project Teacher Guide
2
19
 
3
20
  ## When to Use
@@ -1,3 +1,24 @@
1
+ ---
2
+ description: "Guide React code generation and review using latest stable React verification and modern framework best practices."
3
+ skillMetadata:
4
+ author: "skilly-hand"
5
+ last-edit: "2026-04-04"
6
+ license: "Apache-2.0"
7
+ version: "1.0.0"
8
+ changelog: "Added new react-guidelines skill with component and testing sub-agent routing; improves React-specific generation and review consistency with latest-stable preflight checks; affects portable catalog skill discovery and React workflow guidance"
9
+ auto-invoke: "Generating, reviewing, or refactoring React code artifacts in React projects"
10
+ allowed-tools:
11
+ - "Read"
12
+ - "Edit"
13
+ - "Write"
14
+ - "Glob"
15
+ - "Grep"
16
+ - "Bash"
17
+ - "WebFetch"
18
+ - "WebSearch"
19
+ - "Task"
20
+ - "SubAgent"
21
+ ---
1
22
  # React Guidelines
2
23
 
3
24
  ## When to Use
@@ -1,3 +1,20 @@
1
+ ---
2
+ description: "Review code, decisions, and artifacts through a multi-perspective committee and a domain expert safety guard, then synthesize a structured verdict."
3
+ skillMetadata:
4
+ author: "skilly-hand"
5
+ last-edit: "2026-04-04"
6
+ license: "Apache-2.0"
7
+ version: "1.0.0"
8
+ changelog: "Added multi-perspective review skill with committee + safety guard synthesis; enables adversarial evaluation without permanent agent files; affects catalog skill coverage for review and quality workflows"
9
+ auto-invoke: "Reviewing code, decisions, or artifacts where adversarial multi-perspective evaluation adds value"
10
+ allowed-tools:
11
+ - "Read"
12
+ - "Grep"
13
+ - "Glob"
14
+ - "Bash"
15
+ - "Task"
16
+ - "SubAgent"
17
+ ---
1
18
  # Review Rangers Guide
2
19
 
3
20
  ## When to Use
@@ -1,3 +1,24 @@
1
+ ---
2
+ description: "Create and standardize AI skills with reusable structure, metadata rules, and templates."
3
+ skillMetadata:
4
+ author: "skilly-hand"
5
+ last-edit: "2026-03-27"
6
+ license: "Apache-2.0"
7
+ version: "1.2.3"
8
+ changelog: "Metadata updated to ensure compliance with current standards; maintains skill integrity and version tracking; affects metadata section"
9
+ auto-invoke: "Creating a new skill"
10
+ allowed-tools:
11
+ - "Read"
12
+ - "Edit"
13
+ - "Write"
14
+ - "Glob"
15
+ - "Grep"
16
+ - "Bash"
17
+ - "WebFetch"
18
+ - "WebSearch"
19
+ - "Task"
20
+ - "SubAgent"
21
+ ---
1
22
  # Skill Creator Guide
2
23
 
3
24
  ## When to Create a Skill
@@ -87,6 +108,17 @@ Generic skill needs {product-name} info? -> Add references/ pointing to {produ
87
108
  | `skillMetadata.allowed-tools` | Yes | String list | All tools this skill can invoke (e.g., `Read`, `Edit`, `Write`, `SubAgent`) |
88
109
  | `skillMetadata.allowed-modes` | Optional | String list | Use only when skill has an `agents/` folder |
89
110
 
111
+ ### SKILL.md Frontmatter Mirroring
112
+
113
+ Top-level `SKILL.md` files now include managed YAML frontmatter mirrored from `manifest.json`.
114
+
115
+ Rules:
116
+
117
+ - `manifest.json` is the single source of truth.
118
+ - Mirror only `description` and `skillMetadata.{author,last-edit,license,version,changelog,auto-invoke,allowed-tools}`.
119
+ - Do not manually edit mirrored frontmatter in `SKILL.md`; run sync automation instead.
120
+ - Keep instruction body content in `SKILL.md` focused on workflow guidance.
121
+
90
122
  ---
91
123
 
92
124
  ## Metadata Standards
@@ -153,6 +185,7 @@ Do not:
153
185
  - Use web URLs in references.
154
186
  - Leave `changelog` empty or informal.
155
187
  - Use non-ISO date formats.
188
+ - Manually drift `SKILL.md` frontmatter away from `manifest.json`.
156
189
 
157
190
  ---
158
191
 
@@ -167,6 +200,7 @@ Do not:
167
200
  - [ ] `changelog` uses structured format: `what; why; where`.
168
201
  - [ ] `allowed-modes` is present only when `agents/` exists.
169
202
  - [ ] `allowed-tools` matches actual tool usage.
203
+ - [ ] `SKILL.md` frontmatter is synced from `manifest.json`.
170
204
  - [ ] Critical patterns are clear and concise.
171
205
  - [ ] Code examples are minimal and focused.
172
206
  - [ ] Commands section exists with copy-paste commands.
@@ -1,5 +1,10 @@
1
1
  # {Name of the Skill} Guide
2
2
 
3
+ <!--
4
+ Managed frontmatter is mirrored from manifest.json by automation.
5
+ Do not hand-author frontmatter in this template.
6
+ -->
7
+
3
8
  ## When to Use
4
9
 
5
10
  Use this skill when:
@@ -74,3 +79,4 @@ Otherwise -> {Default action}
74
79
 
75
80
  - Template assets: Place reusable templates, schemas, and examples in `assets/`.
76
81
  - Define metadata in `manifest.json` (`id`, `description`, `skillMetadata`, `allowed-tools`, optional `allowed-modes`).
82
+ - Run skill frontmatter sync so top-level `SKILL.md` mirrors manifest metadata.
@@ -1,3 +1,22 @@
1
+ ---
2
+ description: "Plan, execute, and verify multi-step work through versioned specs with small, testable tasks."
3
+ skillMetadata:
4
+ author: "skilly-hand"
5
+ last-edit: "2026-04-03"
6
+ license: "Apache-2.0"
7
+ version: "1.0.3"
8
+ changelog: "Added OpenSpec complementary support routing guidance to spec-driven-development instructions; improves planning continuity and review clarity when local SDD needs reinforcement; affects spec-driven-development SKILL guidance and manifest metadata"
9
+ auto-invoke: "Planning or executing feature work, bug fixes, and multi-phase implementation"
10
+ allowed-tools:
11
+ - "Read"
12
+ - "Edit"
13
+ - "Write"
14
+ - "Glob"
15
+ - "Grep"
16
+ - "Bash"
17
+ - "Task"
18
+ - "SubAgent"
19
+ ---
1
20
  # Spec-Driven Development Guide
2
21
 
3
22
  ## When to Use
@@ -22,7 +41,7 @@ Do not use this skill for:
22
41
  1. Define the spec in `.sdd/active/<feature-name>/spec.md`.
23
42
  2. Review and refine scope, constraints, and tasks.
24
43
  3. Execute one small task at a time.
25
- 4. Verify each task and the end-to-end outcome.
44
+ 4. Verify each task and the end-to-end outcome, ending with a required `review-rangers` final gate.
26
45
  5. Archive to `.sdd/archive/` when complete.
27
46
 
28
47
  Recommended task size:
@@ -14,7 +14,7 @@ Coordinate planning, implementation, and verification through explicit checkpoin
14
14
  1. PLAN: Produce or update the spec.
15
15
  2. REVIEW CHECKPOINT: Confirm the plan is approved.
16
16
  3. APPLY: Execute agreed task batch.
17
- 4. VERIFY CHECKPOINT: Validate outputs against the spec.
17
+ 4. VERIFY CHECKPOINT: Validate outputs against the spec and run the required final `review-rangers` gate.
18
18
  5. REPEAT: Continue by phase or task batch.
19
19
  6. ARCHIVE: Move completed work from `.sdd/active/` to `.sdd/archive/`.
20
20
 
@@ -15,7 +15,15 @@ Validate that implementation matches the approved spec and passes quality checks
15
15
  2. Run task-level verification evidence checks.
16
16
  3. Run feature-level validation commands.
17
17
  4. Confirm constraints (`MUST`, `MUST NOT`) were respected.
18
- 5. Report pass/fail per area with concrete evidence.
18
+ 5. Run a final structured `review-rangers` pass over the full change set.
19
+ 6. Report pass/fail per area with concrete evidence.
20
+
21
+ ### Required Final Gate (`review-rangers`)
22
+
23
+ - Validate selected agent targets vs actual instruction files/symlinks written.
24
+ - Validate stale managed target cleanup after re-install/reselection.
25
+ - Validate backup and restore safety (including uninstall restore behavior).
26
+ - Any unresolved `review-rangers` blocker keeps verification in failed state.
19
27
 
20
28
  ## Quality Bar
21
29
 
@@ -28,5 +28,6 @@ Use this checklist before implementation and again before archive.
28
28
  - [ ] All planned tasks are complete.
29
29
  - [ ] Feature-level validation passes.
30
30
  - [ ] Constraints were respected.
31
+ - [ ] Final `review-rangers` gate completed with no unresolved blockers.
31
32
  - [ ] No unintended scope creep.
32
33
  - [ ] Work is moved from `.sdd/active/` to `.sdd/archive/`.
@@ -1,3 +1,20 @@
1
+ ---
2
+ description: "Guide implementation using the RED → GREEN → REFACTOR TDD cycle: write a failing test first, write the minimum code to pass, then refactor while tests stay green."
3
+ skillMetadata:
4
+ author: "skilly-hand"
5
+ last-edit: "2026-04-04"
6
+ license: "Apache-2.0"
7
+ version: "1.0.0"
8
+ changelog: "Initial TDD skill ported from legacy scannlab-sdd tdd-templates; enables RED→GREEN→REFACTOR workflow across any stack; affects catalog skill coverage for test-first development"
9
+ auto-invoke: "Implementing features, services, or components using test-driven development (TDD) or RED→GREEN→REFACTOR cycles"
10
+ allowed-tools:
11
+ - "Read"
12
+ - "Edit"
13
+ - "Write"
14
+ - "Glob"
15
+ - "Grep"
16
+ - "Bash"
17
+ ---
1
18
  # Test-Driven Development Guide
2
19
 
3
20
  ## When to Use
@@ -1,3 +1,21 @@
1
+ ---
2
+ description: "Classify task complexity and right-size reasoning depth, context gathering, and response detail to reduce wasted tokens."
3
+ skillMetadata:
4
+ author: "skilly-hand"
5
+ last-edit: "2026-04-03"
6
+ license: "Apache-2.0"
7
+ version: "1.0.3"
8
+ changelog: "Migrated token-optimizer into portable catalog format with curated model-agnostic guidance; improves default reasoning and token-efficiency behavior across installs; affects skill discovery, auto-invoke routing, and install baseline"
9
+ auto-invoke: "Classifying task complexity and choosing reasoning depth/token budget"
10
+ allowed-tools:
11
+ - "Read"
12
+ - "Edit"
13
+ - "Write"
14
+ - "Glob"
15
+ - "Grep"
16
+ - "Bash"
17
+ - "Task"
18
+ ---
1
19
  # Token Optimizer Guide
2
20
 
3
21
  ## When to Use
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skilly-hand/skilly-hand",
3
- "version": "0.16.1",
3
+ "version": "0.18.0",
4
4
  "license": "CC-BY-NC-4.0",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -26,9 +26,9 @@
26
26
  "scripts": {
27
27
  "build": "node ./scripts/build-catalog-index.mjs",
28
28
  "catalog:check": "node ./scripts/check-catalog.mjs",
29
- "catalog:sync": "node ./scripts/sync-catalog-readme.mjs",
29
+ "catalog:sync": "node ./scripts/sync-catalog.mjs",
30
30
  "agentic:self:sync": "node ./scripts/sync-self-agentic.mjs",
31
- "test": "node --test tests/*.test.js",
31
+ "test": "node --test tests/*.test.js && node ./scripts/test-in-sandbox.mjs",
32
32
  "security:check": "node ./scripts/security-check.mjs",
33
33
  "verify:packlist": "node ./scripts/verify-packlist.mjs",
34
34
  "verify:versions": "node ./scripts/verify-versions.mjs",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skilly-hand/catalog",
3
- "version": "0.16.1",
3
+ "version": "0.18.0",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -1,4 +1,4 @@
1
- import { cp, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
1
+ import { cp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
 
@@ -21,6 +21,16 @@ const REQUIRED_FIELDS = [
21
21
  "dependencies"
22
22
  ];
23
23
 
24
+ const MIRRORED_SKILL_METADATA_KEYS = [
25
+ "author",
26
+ "last-edit",
27
+ "license",
28
+ "version",
29
+ "changelog",
30
+ "auto-invoke",
31
+ "allowed-tools"
32
+ ];
33
+
24
34
  export function getCatalogRoot() {
25
35
  return catalogDir;
26
36
  }
@@ -70,9 +80,372 @@ export function validateSkillManifest(manifest) {
70
80
  throw new Error(`Skill "${manifest.id}" must declare files`);
71
81
  }
72
82
 
83
+ const hasSkillInstruction = manifest.files.some((file) => file.path === "SKILL.md" && file.kind === "instruction");
84
+ if (!hasSkillInstruction) {
85
+ throw new Error(`Skill "${manifest.id}" must include files entry for SKILL.md as instruction`);
86
+ }
87
+
88
+ assertFrontmatterFields(manifest);
89
+
73
90
  return true;
74
91
  }
75
92
 
93
+ function toLf(text) {
94
+ return text.replaceAll("\r\n", "\n");
95
+ }
96
+
97
+ function splitLinesWithOffsets(text) {
98
+ const lines = [];
99
+ let start = 0;
100
+ for (let index = 0; index < text.length; index += 1) {
101
+ if (text[index] === "\n") {
102
+ lines.push({
103
+ text: text.slice(start, index),
104
+ start,
105
+ end: index + 1
106
+ });
107
+ start = index + 1;
108
+ }
109
+ }
110
+ if (start < text.length) {
111
+ lines.push({
112
+ text: text.slice(start),
113
+ start,
114
+ end: text.length
115
+ });
116
+ } else if (text.length === 0) {
117
+ lines.push({ text: "", start: 0, end: 0 });
118
+ }
119
+ return lines;
120
+ }
121
+
122
+ function yamlQuote(value) {
123
+ return JSON.stringify(String(value));
124
+ }
125
+
126
+ function assertFrontmatterFields(manifest) {
127
+ if (!manifest || typeof manifest !== "object") {
128
+ throw new Error("Invalid manifest while building SKILL.md frontmatter");
129
+ }
130
+
131
+ if (typeof manifest.description !== "string" || manifest.description.length === 0) {
132
+ throw new Error(`Skill "${manifest.id}" is missing required manifest.description for frontmatter mirroring`);
133
+ }
134
+
135
+ if (!manifest.skillMetadata || typeof manifest.skillMetadata !== "object") {
136
+ throw new Error(`Skill "${manifest.id}" is missing required manifest.skillMetadata for frontmatter mirroring`);
137
+ }
138
+
139
+ for (const key of MIRRORED_SKILL_METADATA_KEYS) {
140
+ if (!(key in manifest.skillMetadata)) {
141
+ throw new Error(`Skill "${manifest.id}" is missing required skillMetadata.${key} for frontmatter mirroring`);
142
+ }
143
+ }
144
+
145
+ const scalarKeys = ["author", "last-edit", "license", "version", "changelog", "auto-invoke"];
146
+ for (const key of scalarKeys) {
147
+ if (typeof manifest.skillMetadata[key] !== "string" || manifest.skillMetadata[key].trim().length === 0) {
148
+ throw new Error(`Skill "${manifest.id}" has invalid skillMetadata.${key}; expected a non-empty string`);
149
+ }
150
+ }
151
+
152
+ if (!Array.isArray(manifest.skillMetadata["allowed-tools"])) {
153
+ throw new Error(`Skill "${manifest.id}" must declare skillMetadata.allowed-tools as an array`);
154
+ }
155
+
156
+ for (const tool of manifest.skillMetadata["allowed-tools"]) {
157
+ if (typeof tool !== "string" || tool.trim().length === 0) {
158
+ throw new Error(`Skill "${manifest.id}" has invalid skillMetadata.allowed-tools; expected non-empty strings`);
159
+ }
160
+ }
161
+ }
162
+
163
+ export function buildSkillFrontmatterPayload(manifest) {
164
+ assertFrontmatterFields(manifest);
165
+ return {
166
+ description: manifest.description,
167
+ skillMetadata: {
168
+ author: manifest.skillMetadata.author,
169
+ "last-edit": manifest.skillMetadata["last-edit"],
170
+ license: manifest.skillMetadata.license,
171
+ version: manifest.skillMetadata.version,
172
+ changelog: manifest.skillMetadata.changelog,
173
+ "auto-invoke": manifest.skillMetadata["auto-invoke"],
174
+ "allowed-tools": [...manifest.skillMetadata["allowed-tools"]]
175
+ }
176
+ };
177
+ }
178
+
179
+ function renderSkillFrontmatterInner(payload) {
180
+ const lines = [
181
+ `description: ${yamlQuote(payload.description)}`,
182
+ "skillMetadata:",
183
+ ` author: ${yamlQuote(payload.skillMetadata.author)}`,
184
+ ` last-edit: ${yamlQuote(payload.skillMetadata["last-edit"])}`,
185
+ ` license: ${yamlQuote(payload.skillMetadata.license)}`,
186
+ ` version: ${yamlQuote(payload.skillMetadata.version)}`,
187
+ ` changelog: ${yamlQuote(payload.skillMetadata.changelog)}`,
188
+ ` auto-invoke: ${yamlQuote(payload.skillMetadata["auto-invoke"])}`,
189
+ " allowed-tools:"
190
+ ];
191
+
192
+ for (const tool of payload.skillMetadata["allowed-tools"]) {
193
+ lines.push(` - ${yamlQuote(tool)}`);
194
+ }
195
+
196
+ return lines.join("\n");
197
+ }
198
+
199
+ export function renderSkillFrontmatter(manifest) {
200
+ const payload = buildSkillFrontmatterPayload(manifest);
201
+ return `---\n${renderSkillFrontmatterInner(payload)}\n---\n`;
202
+ }
203
+
204
+ export function splitSkillMarkdown(content) {
205
+ const normalized = toLf(content);
206
+ const source = normalized.startsWith("\uFEFF") ? normalized.slice(1) : normalized;
207
+ const lines = splitLinesWithOffsets(source);
208
+ const mirroredKeys = new Set([
209
+ "description",
210
+ "skillMetadata",
211
+ "author",
212
+ "last-edit",
213
+ "license",
214
+ "version",
215
+ "changelog",
216
+ "auto-invoke",
217
+ "allowed-tools"
218
+ ]);
219
+ const isYamlLike = (line) => (
220
+ line.trim().length === 0 ||
221
+ /^\s*[A-Za-z0-9_-]+:(?:\s.*)?$/.test(line) ||
222
+ /^\s*-\s+.*$/.test(line)
223
+ );
224
+
225
+ let firstNonBlankIndex = 0;
226
+ while (firstNonBlankIndex < lines.length && lines[firstNonBlankIndex].text.trim().length === 0) {
227
+ firstNonBlankIndex += 1;
228
+ }
229
+
230
+ if (firstNonBlankIndex >= lines.length || lines[firstNonBlankIndex].text !== "---") {
231
+ return {
232
+ hasFrontmatter: false,
233
+ malformedFrontmatter: false,
234
+ frontmatter: null,
235
+ body: source
236
+ };
237
+ }
238
+
239
+ const openLine = lines[firstNonBlankIndex];
240
+ let sawKeyValue = false;
241
+ let sawMirroredKey = false;
242
+ const detectedKeys = new Set();
243
+
244
+ for (let index = firstNonBlankIndex + 1; index < lines.length; index += 1) {
245
+ const line = lines[index].text;
246
+ const nextLine = index + 1 < lines.length ? lines[index + 1].text : null;
247
+
248
+ if (
249
+ sawMirroredKey &&
250
+ line.trim().length === 0 &&
251
+ nextLine &&
252
+ /^(?:[-*+]\s+|\d+\.\s+|#{1,6}\s+|>\s+|```)/.test(nextLine)
253
+ ) {
254
+ return {
255
+ hasFrontmatter: true,
256
+ malformedFrontmatter: true,
257
+ frontmatter: null,
258
+ body: source.slice(lines[index + 1].start)
259
+ };
260
+ }
261
+
262
+ if (line === "---") {
263
+ if (!sawKeyValue || !sawMirroredKey) {
264
+ return {
265
+ hasFrontmatter: false,
266
+ malformedFrontmatter: false,
267
+ frontmatter: null,
268
+ body: source
269
+ };
270
+ }
271
+ const end = lines[index].end;
272
+ const frontmatter = source.slice(openLine.start, end);
273
+ return {
274
+ hasFrontmatter: true,
275
+ malformedFrontmatter: false,
276
+ frontmatter: frontmatter.endsWith("\n") ? frontmatter : `${frontmatter}\n`,
277
+ body: source.slice(end),
278
+ detectedKeys: Array.from(detectedKeys)
279
+ };
280
+ }
281
+
282
+ if (/^\s*[A-Za-z0-9_-]+:(?:\s.*)?$/.test(line)) {
283
+ sawKeyValue = true;
284
+ const key = line.split(":", 1)[0].trim();
285
+ detectedKeys.add(key);
286
+ if (mirroredKeys.has(key)) {
287
+ sawMirroredKey = true;
288
+ }
289
+ }
290
+
291
+ if (!isYamlLike(line)) {
292
+ if (!sawKeyValue || !sawMirroredKey) {
293
+ return {
294
+ hasFrontmatter: false,
295
+ malformedFrontmatter: false,
296
+ frontmatter: null,
297
+ body: source
298
+ };
299
+ }
300
+ return {
301
+ hasFrontmatter: true,
302
+ malformedFrontmatter: true,
303
+ frontmatter: null,
304
+ body: source.slice(lines[index].start),
305
+ detectedKeys: Array.from(detectedKeys)
306
+ };
307
+ }
308
+ }
309
+
310
+ if (!sawKeyValue) {
311
+ return {
312
+ hasFrontmatter: false,
313
+ malformedFrontmatter: false,
314
+ frontmatter: null,
315
+ body: source
316
+ };
317
+ }
318
+
319
+ if (!sawMirroredKey) {
320
+ return {
321
+ hasFrontmatter: false,
322
+ malformedFrontmatter: false,
323
+ frontmatter: null,
324
+ body: source
325
+ };
326
+ }
327
+
328
+ return {
329
+ hasFrontmatter: true,
330
+ malformedFrontmatter: true,
331
+ frontmatter: null,
332
+ body: "",
333
+ detectedKeys: Array.from(detectedKeys)
334
+ };
335
+ }
336
+
337
+ export function applyManifestFrontmatterToSkill(content, manifest) {
338
+ const expectedFrontmatter = renderSkillFrontmatter(manifest);
339
+ const parts = splitSkillMarkdown(content);
340
+ return `${expectedFrontmatter}${parts.body}`;
341
+ }
342
+
343
+ export function verifySkillFrontmatterContent(content, manifest) {
344
+ const expectedFrontmatter = renderSkillFrontmatter(manifest);
345
+ const parts = splitSkillMarkdown(content);
346
+ if (!parts.hasFrontmatter) {
347
+ return { ok: false, reason: "missing" };
348
+ }
349
+ if (parts.malformedFrontmatter || !parts.frontmatter) {
350
+ return { ok: false, reason: "malformed" };
351
+ }
352
+ if (parts.frontmatter !== expectedFrontmatter) {
353
+ return { ok: false, reason: "mismatch" };
354
+ }
355
+ const residual = splitSkillMarkdown(parts.body);
356
+ if (residual.hasFrontmatter && !residual.malformedFrontmatter && residual.frontmatter === expectedFrontmatter) {
357
+ return { ok: false, reason: "residual-frontmatter" };
358
+ }
359
+ return { ok: true, reason: null };
360
+ }
361
+
362
+ export async function syncSkillFrontmatter({ skillId, dryRun = false } = {}) {
363
+ const plan = await planSkillFrontmatterSync({ skillId });
364
+ if (!dryRun) {
365
+ await applyTextUpdatesAtomically(plan.updates);
366
+ }
367
+ return {
368
+ skillCount: plan.skillCount,
369
+ updatedSkillIds: plan.updatedSkillIds
370
+ };
371
+ }
372
+
373
+ export async function planSkillFrontmatterSync({ skillId } = {}) {
374
+ const allIds = await listSkillIds();
375
+ if (skillId && !allIds.includes(skillId)) {
376
+ throw new Error(`Unknown skill id: ${skillId}`);
377
+ }
378
+
379
+ const ids = skillId ? [skillId] : allIds;
380
+ const updatedSkillIds = [];
381
+ const updates = [];
382
+
383
+ for (const id of ids) {
384
+ const manifest = await loadSkillManifest(id);
385
+ const skillPath = path.join(skillsDir, id, "SKILL.md");
386
+ const current = await readFile(skillPath, "utf8");
387
+ const next = applyManifestFrontmatterToSkill(current, manifest);
388
+ if (next !== toLf(current)) {
389
+ updatedSkillIds.push(id);
390
+ updates.push({
391
+ skillId: id,
392
+ path: skillPath,
393
+ content: next
394
+ });
395
+ }
396
+ }
397
+
398
+ return {
399
+ skillCount: ids.length,
400
+ updatedSkillIds,
401
+ updates
402
+ };
403
+ }
404
+
405
+ export async function applyTextUpdatesAtomically(updates) {
406
+ const deduped = [];
407
+ const seenPaths = new Set();
408
+ for (let index = updates.length - 1; index >= 0; index -= 1) {
409
+ const update = updates[index];
410
+ if (!seenPaths.has(update.path)) {
411
+ seenPaths.add(update.path);
412
+ deduped.push(update);
413
+ }
414
+ }
415
+ deduped.reverse();
416
+
417
+ const originals = new Map();
418
+ for (const update of deduped) {
419
+ try {
420
+ originals.set(update.path, await readFile(update.path, "utf8"));
421
+ } catch (error) {
422
+ if (error?.code === "ENOENT") {
423
+ originals.set(update.path, null);
424
+ } else {
425
+ throw error;
426
+ }
427
+ }
428
+ }
429
+
430
+ const writtenPaths = [];
431
+ try {
432
+ for (const update of deduped) {
433
+ await writeFile(update.path, update.content, "utf8");
434
+ writtenPaths.push(update.path);
435
+ }
436
+ } catch (error) {
437
+ for (const targetPath of writtenPaths.reverse()) {
438
+ const original = originals.get(targetPath);
439
+ if (original === null) {
440
+ await rm(targetPath, { force: true });
441
+ } else {
442
+ await writeFile(targetPath, original, "utf8");
443
+ }
444
+ }
445
+ throw error;
446
+ }
447
+ }
448
+
76
449
  export async function copySkillTo(targetCatalogDir, skillId) {
77
450
  const source = path.join(skillsDir, skillId);
78
451
  const destination = path.join(targetCatalogDir, skillId);
@@ -88,6 +461,7 @@ export async function readTemplate(templateName) {
88
461
  }
89
462
 
90
463
  export function renderAgentsMarkdown({ skills, detections, generatedAt, projectName }) {
464
+ const escapeTableCell = (value) => String(value).replaceAll("|", "\\|").replaceAll("\n", "<br>");
91
465
  const sortedSkills = [...skills].sort((a, b) => a.id.localeCompare(b.id));
92
466
  const sortedDetections = [...detections].sort((a, b) => a.technology.localeCompare(b.technology));
93
467
  const autoInvokeSkills = sortedSkills.filter((skill) => skill.skillMetadata?.["auto-invoke"]);
@@ -114,7 +488,7 @@ export function renderAgentsMarkdown({ skills, detections, generatedAt, projectN
114
488
  ];
115
489
 
116
490
  for (const skill of sortedSkills) {
117
- lines.push(`| \`${skill.id}\` | ${skill.description} | ${skill.tags.join(", ")} |`);
491
+ lines.push(`| \`${skill.id}\` | ${escapeTableCell(skill.description)} | ${escapeTableCell(skill.tags.join(", "))} |`);
118
492
  }
119
493
 
120
494
  lines.push(
@@ -126,7 +500,7 @@ export function renderAgentsMarkdown({ skills, detections, generatedAt, projectN
126
500
  "1. Always run `token-optimizer` first to classify complexity and set the minimum viable reasoning depth.",
127
501
  "2. Always run `output-optimizer` immediately after `token-optimizer` for response-shape control.",
128
502
  "3. `output-optimizer` mode policy:",
129
- " - Default: select a random canonical mode for each new interaction.",
503
+ " - Default: use `step-brief` when there is no explicit mode or strong phrasing signal.",
130
504
  " - Override: if user explicitly requests a mode (for example `mode: step-brief`), that explicit mode wins.",
131
505
  " - Persistence: keep the explicitly requested mode active until the user asks for a different mode.",
132
506
  "",
@@ -154,7 +528,7 @@ export function renderAgentsMarkdown({ skills, detections, generatedAt, projectN
154
528
  } else {
155
529
  lines.push("| Action | Skill |", "| ------ | ----- |");
156
530
  for (const skill of autoInvokeSkills) {
157
- lines.push(`| ${skill.skillMetadata["auto-invoke"]} | \`${skill.id}\` |`);
531
+ lines.push(`| ${escapeTableCell(skill.skillMetadata["auto-invoke"])} | \`${skill.id}\` |`);
158
532
  }
159
533
  }
160
534
 
@@ -230,6 +604,28 @@ export async function verifyCatalogFiles() {
230
604
  issues.push(`Missing file for ${skillId}: ${file.path}`);
231
605
  }
232
606
  }
607
+
608
+ const skillDocPath = path.join(skillPath, "SKILL.md");
609
+ let skillDocContent;
610
+ try {
611
+ skillDocContent = await readFile(skillDocPath, "utf8");
612
+ } catch (error) {
613
+ if (error?.code === "ENOENT") {
614
+ // Missing file is already surfaced above via manifest file verification.
615
+ continue;
616
+ }
617
+ issues.push(`Cannot read ${skillId}/SKILL.md: ${error.message}`);
618
+ continue;
619
+ }
620
+
621
+ try {
622
+ const status = verifySkillFrontmatterContent(skillDocContent, manifest);
623
+ if (!status.ok) {
624
+ issues.push(`Frontmatter ${status.reason} for ${skillId}: SKILL.md`);
625
+ }
626
+ } catch (error) {
627
+ issues.push(`Frontmatter validation failed for ${skillId}: ${error.message}`);
628
+ }
233
629
  }
234
630
 
235
631
  return issues;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skilly-hand/cli",
3
- "version": "0.16.1",
3
+ "version": "0.18.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skilly-hand/core",
3
- "version": "0.16.1",
3
+ "version": "0.18.0",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -175,7 +175,11 @@ async function ensureManagedTextFile(targetPath, content, backupsDir, lockData)
175
175
  return;
176
176
  }
177
177
 
178
- await backupPathIfNeeded(targetPath, backupsDir, lockData);
178
+ // Do not back up previously managed content; backups are for restoring
179
+ // user-authored files replaced by managed files.
180
+ if (!current.includes(MANAGED_MARKER)) {
181
+ await backupPathIfNeeded(targetPath, backupsDir, lockData);
182
+ }
179
183
  }
180
184
 
181
185
  await mkdir(path.dirname(targetPath), { recursive: true });
@@ -230,6 +234,69 @@ function buildInstallTargets(selectedAgents) {
230
234
  };
231
235
  }
232
236
 
237
+ async function reconcileManagedTargets({
238
+ previousLock,
239
+ selectedInstructionTargets,
240
+ selectedSkillTargets,
241
+ lockData
242
+ }) {
243
+ if (!previousLock) {
244
+ return;
245
+ }
246
+
247
+ const selectedInstructions = new Set(selectedInstructionTargets);
248
+ const selectedSkills = new Set(selectedSkillTargets);
249
+ const selectedTargets = new Set([...selectedInstructions, ...selectedSkills]);
250
+ const previousBackups = previousLock.backups || {};
251
+
252
+ for (const [targetPath, backupPath] of Object.entries(previousBackups)) {
253
+ if (!selectedTargets.has(targetPath)) {
254
+ continue;
255
+ }
256
+ if (await exists(backupPath)) {
257
+ lockData.backups[targetPath] = backupPath;
258
+ }
259
+ }
260
+
261
+ for (const symlinkPath of previousLock.managedSymlinks || []) {
262
+ if (selectedSkills.has(symlinkPath)) {
263
+ continue;
264
+ }
265
+
266
+ if (await exists(symlinkPath)) {
267
+ await rm(symlinkPath, { recursive: true, force: true });
268
+ }
269
+
270
+ const backupPath = previousBackups[symlinkPath];
271
+ if (backupPath && await exists(backupPath)) {
272
+ await mkdir(path.dirname(symlinkPath), { recursive: true });
273
+ await cp(backupPath, symlinkPath, { recursive: true, force: true });
274
+ }
275
+ }
276
+
277
+ for (const filePath of previousLock.managedFiles || []) {
278
+ if (selectedInstructions.has(filePath)) {
279
+ continue;
280
+ }
281
+
282
+ const backupPath = previousBackups[filePath];
283
+ if (backupPath && await exists(backupPath)) {
284
+ await mkdir(path.dirname(filePath), { recursive: true });
285
+ await cp(backupPath, filePath, { recursive: true, force: true });
286
+ continue;
287
+ }
288
+
289
+ if (!(await exists(filePath))) {
290
+ continue;
291
+ }
292
+
293
+ const content = await readFile(filePath, "utf8");
294
+ if (content.includes(MANAGED_MARKER)) {
295
+ await rm(filePath, { force: true });
296
+ }
297
+ }
298
+ }
299
+
233
300
  export async function installProject({
234
301
  cwd,
235
302
  agents,
@@ -259,6 +326,7 @@ export async function installProject({
259
326
  const targetCatalogDir = path.join(installRoot, "catalog");
260
327
  const backupsDir = path.join(installRoot, "backups");
261
328
  const lockPath = path.join(installRoot, "manifest.lock.json");
329
+ const previousLock = await exists(lockPath) ? await readJson(lockPath) : null;
262
330
  const lockData = {
263
331
  version: 1,
264
332
  generatedAt: plan.generatedAt,
@@ -289,15 +357,24 @@ export async function installProject({
289
357
  await writeFile(path.join(installRoot, "AGENTS.md"), agentsMarkdown, "utf8");
290
358
 
291
359
  const { instructionTargets, skillTargets } = buildInstallTargets(selectedAgents);
360
+ const absoluteInstructionTargets = instructionTargets.map((pathParts) => path.join(cwd, ...pathParts));
361
+ const absoluteSkillTargets = skillTargets.map((pathParts) => path.join(cwd, ...pathParts));
362
+
363
+ await reconcileManagedTargets({
364
+ previousLock,
365
+ selectedInstructionTargets: absoluteInstructionTargets,
366
+ selectedSkillTargets: absoluteSkillTargets,
367
+ lockData
368
+ });
292
369
 
293
- for (const pathParts of instructionTargets) {
294
- await ensureManagedTextFile(path.join(cwd, ...pathParts), agentsMarkdown, backupsDir, lockData);
370
+ for (const targetPath of absoluteInstructionTargets) {
371
+ await ensureManagedTextFile(targetPath, agentsMarkdown, backupsDir, lockData);
295
372
  }
296
373
 
297
374
  const skillsSourcePath = path.join(installRoot, "catalog");
298
375
 
299
- for (const pathParts of skillTargets) {
300
- await ensureSymlink(path.join(cwd, ...pathParts), skillsSourcePath, backupsDir, lockData);
376
+ for (const targetPath of absoluteSkillTargets) {
377
+ await ensureSymlink(targetPath, skillsSourcePath, backupsDir, lockData);
301
378
  }
302
379
 
303
380
  await writeJson(lockPath, lockData);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skilly-hand/detectors",
3
- "version": "0.16.1",
3
+ "version": "0.18.0",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }