@mrpatronz/nexusflow 2.11.0 → 2.12.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.
Files changed (194) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +151 -12
  3. package/dist/analyzers/detect-deps.js +1 -1
  4. package/dist/analyzers/detect-deps.js.map +1 -1
  5. package/dist/commands/commit.d.ts.map +1 -1
  6. package/dist/commands/commit.js +15 -0
  7. package/dist/commands/commit.js.map +1 -1
  8. package/dist/commands/create.d.ts +10 -1
  9. package/dist/commands/create.d.ts.map +1 -1
  10. package/dist/commands/create.js +44 -23
  11. package/dist/commands/create.js.map +1 -1
  12. package/dist/commands/finish.d.ts.map +1 -1
  13. package/dist/commands/finish.js +15 -0
  14. package/dist/commands/finish.js.map +1 -1
  15. package/dist/commands/flow.d.ts +13 -0
  16. package/dist/commands/flow.d.ts.map +1 -0
  17. package/dist/commands/flow.js +91 -0
  18. package/dist/commands/flow.js.map +1 -0
  19. package/dist/commands/skill.d.ts +28 -0
  20. package/dist/commands/skill.d.ts.map +1 -0
  21. package/dist/commands/skill.js +250 -0
  22. package/dist/commands/skill.js.map +1 -0
  23. package/dist/commands/status.d.ts.map +1 -1
  24. package/dist/commands/status.js +14 -0
  25. package/dist/commands/status.js.map +1 -1
  26. package/dist/commands/tag.d.ts +13 -0
  27. package/dist/commands/tag.d.ts.map +1 -0
  28. package/dist/commands/tag.js +179 -0
  29. package/dist/commands/tag.js.map +1 -0
  30. package/dist/commands/verify.d.ts +16 -0
  31. package/dist/commands/verify.d.ts.map +1 -0
  32. package/dist/commands/verify.js +91 -0
  33. package/dist/commands/verify.js.map +1 -0
  34. package/dist/core/adapters/local-storage.d.ts.map +1 -1
  35. package/dist/core/adapters/local-storage.js +2 -2
  36. package/dist/core/adapters/local-storage.js.map +1 -1
  37. package/dist/core/commit.d.ts +2 -0
  38. package/dist/core/commit.d.ts.map +1 -1
  39. package/dist/core/commit.js +15 -0
  40. package/dist/core/commit.js.map +1 -1
  41. package/dist/core/domain-catalog.d.ts +9 -0
  42. package/dist/core/domain-catalog.d.ts.map +1 -0
  43. package/dist/core/domain-catalog.js +47 -0
  44. package/dist/core/domain-catalog.js.map +1 -0
  45. package/dist/core/domain-packs.d.ts +81 -0
  46. package/dist/core/domain-packs.d.ts.map +1 -0
  47. package/dist/core/domain-packs.js +443 -0
  48. package/dist/core/domain-packs.js.map +1 -0
  49. package/dist/core/generation-lock.d.ts.map +1 -1
  50. package/dist/core/generation-lock.js +1 -3
  51. package/dist/core/generation-lock.js.map +1 -1
  52. package/dist/core/lifecycle.d.ts +28 -0
  53. package/dist/core/lifecycle.d.ts.map +1 -0
  54. package/dist/core/lifecycle.js +411 -0
  55. package/dist/core/lifecycle.js.map +1 -0
  56. package/dist/core/verify.d.ts +39 -0
  57. package/dist/core/verify.d.ts.map +1 -0
  58. package/dist/core/verify.js +397 -0
  59. package/dist/core/verify.js.map +1 -0
  60. package/dist/core/work-guidance.d.ts +201 -0
  61. package/dist/core/work-guidance.d.ts.map +1 -0
  62. package/dist/core/work-guidance.js +225 -0
  63. package/dist/core/work-guidance.js.map +1 -0
  64. package/dist/core/workspace-state.d.ts +24 -3
  65. package/dist/core/workspace-state.d.ts.map +1 -1
  66. package/dist/core/workspace-state.js +93 -31
  67. package/dist/core/workspace-state.js.map +1 -1
  68. package/dist/core/workspace.d.ts.map +1 -1
  69. package/dist/core/workspace.js +7 -0
  70. package/dist/core/workspace.js.map +1 -1
  71. package/dist/generators/base.d.ts.map +1 -1
  72. package/dist/generators/base.js +38 -4
  73. package/dist/generators/base.js.map +1 -1
  74. package/dist/generators/index.d.ts.map +1 -1
  75. package/dist/generators/index.js +4 -0
  76. package/dist/generators/index.js.map +1 -1
  77. package/dist/generators/plan-generator.d.ts.map +1 -1
  78. package/dist/generators/plan-generator.js +6 -1
  79. package/dist/generators/plan-generator.js.map +1 -1
  80. package/dist/generators/skills-generator.d.ts.map +1 -1
  81. package/dist/generators/skills-generator.js +36 -3
  82. package/dist/generators/skills-generator.js.map +1 -1
  83. package/dist/gui/assets/AgentsPage-GCFiyNSm.js +3 -0
  84. package/dist/gui/assets/DashboardPage-51JHl4Hg.js +1 -0
  85. package/dist/gui/assets/{GettingStartedPage-1MOTng3B.js → GettingStartedPage-DH-zXYgU.js} +1 -1
  86. package/dist/gui/assets/ProjectsPage-DUv2fhI8.js +1 -0
  87. package/dist/gui/assets/ScaffoldRepoInline-Asao9IqG.js +1 -0
  88. package/dist/gui/assets/SettingsPage--pnZKpmm.js +1 -0
  89. package/dist/gui/assets/SkillsPage-BPq8arO5.js +7 -0
  90. package/dist/gui/assets/StartWorkPage-CypGEQZ0.js +1 -0
  91. package/dist/gui/assets/{StrategiesPage-CdCbbIu-.js → StrategiesPage-BHAzXMnc.js} +2 -2
  92. package/dist/gui/assets/WorkroomsPage-BYC8c-rP.js +4 -0
  93. package/dist/gui/assets/WorkspacesPage-DRASo1aL.js +13 -0
  94. package/dist/gui/assets/{alert-dialog-u7qgb9Hq.js → alert-dialog-BD3wT0w3.js} +1 -1
  95. package/dist/gui/assets/badge-DTBzXOWn.js +1 -0
  96. package/dist/gui/assets/index-er-mkc2S.css +2 -0
  97. package/dist/gui/assets/index-jcGHcXQ1.js +57 -0
  98. package/dist/gui/assets/square-check-big-Bj8Rf_dH.js +1 -0
  99. package/dist/gui/assets/tabs-DfbdubLX.js +1 -0
  100. package/dist/gui/assets/{useAriaLabelledBy-C4zV-1Ra.js → useAriaLabelledBy-BmJErI5a.js} +1 -1
  101. package/dist/gui/assets/vsc-Dn-smJni.js +1 -0
  102. package/dist/gui/index.html +3 -5
  103. package/dist/http/work-guidance-routes.d.ts +4 -0
  104. package/dist/http/work-guidance-routes.d.ts.map +1 -0
  105. package/dist/http/work-guidance-routes.js +44 -0
  106. package/dist/http/work-guidance-routes.js.map +1 -0
  107. package/dist/index.js +169 -1
  108. package/dist/index.js.map +1 -1
  109. package/dist/mcp/tools.d.ts.map +1 -1
  110. package/dist/mcp/tools.js +183 -0
  111. package/dist/mcp/tools.js.map +1 -1
  112. package/dist/resources/contracts.d.ts.map +1 -1
  113. package/dist/resources/contracts.js +2 -1
  114. package/dist/resources/contracts.js.map +1 -1
  115. package/dist/resources/fs-safety.d.ts.map +1 -1
  116. package/dist/resources/fs-safety.js +2 -1
  117. package/dist/resources/fs-safety.js.map +1 -1
  118. package/dist/resources/materializer.d.ts +3 -0
  119. package/dist/resources/materializer.d.ts.map +1 -1
  120. package/dist/resources/materializer.js +45 -19
  121. package/dist/resources/materializer.js.map +1 -1
  122. package/dist/server.d.ts.map +1 -1
  123. package/dist/server.js +327 -4
  124. package/dist/server.js.map +1 -1
  125. package/dist/static-resources/skills/contextspace-verify-release/SKILL.md +51 -0
  126. package/dist/static-resources/skills/contextspace-verify-release/references/evidence.md +21 -0
  127. package/dist/static-resources/skills/nexusflow-dev/SKILL.md +49 -0
  128. package/dist/static-resources/skills/nexusflow-dev/references/cli-conventions.md +15 -0
  129. package/dist/static-resources/skills/nexusflow-dev/references/verification-checklist.md +28 -0
  130. package/dist/static-resources/skills/nexusflow-lifecycle/SKILL.md +62 -0
  131. package/dist/static-resources/skills/nexusflow-lifecycle/references/epic-deliverables.md +22 -0
  132. package/dist/static-resources/skills/nexusflow-lifecycle/references/flow-selection-matrix.md +21 -0
  133. package/dist/static-resources/skills/nexusflow-lifecycle/references/stage-outcomes.md +23 -0
  134. package/dist/static-resources/workflows/epic-multi-slice.md +21 -0
  135. package/dist/static-resources/workflows/plan-implement-review.md +17 -6
  136. package/dist/static-resources/workflows/research-verify.md +15 -4
  137. package/dist/static-resources/workflows/solo-developer.md +11 -2
  138. package/dist/test-setup.d.ts +2 -0
  139. package/dist/test-setup.d.ts.map +1 -0
  140. package/dist/test-setup.js +8 -0
  141. package/dist/test-setup.js.map +1 -0
  142. package/dist/types.d.ts +175 -0
  143. package/dist/types.d.ts.map +1 -1
  144. package/dist/utils/resolve-workspace.d.ts.map +1 -1
  145. package/dist/utils/resolve-workspace.js +15 -8
  146. package/dist/utils/resolve-workspace.js.map +1 -1
  147. package/dist/utils/skills-catalog.d.ts +13 -9
  148. package/dist/utils/skills-catalog.d.ts.map +1 -1
  149. package/dist/utils/skills-catalog.js +241 -341
  150. package/dist/utils/skills-catalog.js.map +1 -1
  151. package/dist/utils/workflow-advisor.d.ts +1 -1
  152. package/dist/utils/workflow-advisor.d.ts.map +1 -1
  153. package/dist/utils/workflow-advisor.js +16 -4
  154. package/dist/utils/workflow-advisor.js.map +1 -1
  155. package/dist/utils/workflows.d.ts.map +1 -1
  156. package/dist/utils/workflows.js +5 -2
  157. package/dist/utils/workflows.js.map +1 -1
  158. package/dist/workrooms/contracts.d.ts +3 -3
  159. package/dist/workrooms/local-resources.d.ts +4 -1
  160. package/dist/workrooms/local-resources.d.ts.map +1 -1
  161. package/dist/workrooms/manager.d.ts +5 -2
  162. package/dist/workrooms/manager.d.ts.map +1 -1
  163. package/dist/workrooms/store.d.ts +2 -2
  164. package/package.json +3 -2
  165. package/resources/skills/contextspace-verify-release/SKILL.md +51 -0
  166. package/resources/skills/contextspace-verify-release/references/evidence.md +21 -0
  167. package/resources/skills/nexusflow-dev/SKILL.md +49 -0
  168. package/resources/skills/nexusflow-dev/references/cli-conventions.md +15 -0
  169. package/resources/skills/nexusflow-dev/references/verification-checklist.md +28 -0
  170. package/resources/skills/nexusflow-lifecycle/SKILL.md +62 -0
  171. package/resources/skills/nexusflow-lifecycle/references/epic-deliverables.md +22 -0
  172. package/resources/skills/nexusflow-lifecycle/references/flow-selection-matrix.md +21 -0
  173. package/resources/skills/nexusflow-lifecycle/references/stage-outcomes.md +23 -0
  174. package/resources/workflows/epic-multi-slice.md +21 -0
  175. package/resources/workflows/plan-implement-review.md +17 -6
  176. package/resources/workflows/research-verify.md +15 -4
  177. package/resources/workflows/solo-developer.md +11 -2
  178. package/dist/gui/assets/AgentsPage-DbivhmK0.js +0 -3
  179. package/dist/gui/assets/DashboardPage-D50Qrm3e.js +0 -1
  180. package/dist/gui/assets/NullStore-tdPen1kp.js +0 -1
  181. package/dist/gui/assets/ProjectsPage-srKgf7VN.js +0 -1
  182. package/dist/gui/assets/ScaffoldRepoInline-CP0yn5mw.js +0 -1
  183. package/dist/gui/assets/SettingsPage-DWmrLV8o.js +0 -1
  184. package/dist/gui/assets/SkillsPage-CKjTXdrd.js +0 -6
  185. package/dist/gui/assets/StartWorkPage-BCUnJ9k8.js +0 -1
  186. package/dist/gui/assets/WorkroomsPage-BcT-mGtT.js +0 -4
  187. package/dist/gui/assets/WorkspacesPage-CYFCm-7_.js +0 -2
  188. package/dist/gui/assets/badge-DwXvB44H.js +0 -1
  189. package/dist/gui/assets/chunk-BV7QT456-DfUBKG-z.js +0 -3
  190. package/dist/gui/assets/index-3tD_IeyQ.js +0 -55
  191. package/dist/gui/assets/index-CK5YRfPV.css +0 -2
  192. package/dist/gui/assets/square-check-big-BSIiYXPI.js +0 -1
  193. package/dist/gui/assets/tabs-BrHKp5bD.js +0 -1
  194. package/dist/gui/assets/vsc-D80c4gBT.js +0 -1
@@ -15,11 +15,11 @@ import { resolveBrandHomeDir, resolveGlobalDurablePath, resolveWorkspaceConfigDi
15
15
  import { formatValidationError, resourceIdSchema, skillCategorySchema, skillFrontmatterSchema, workspaceResourcesConfigSchema, } from '../resources/contracts.js';
16
16
  import { assertNoLinkedPathComponents, assertPathWithin, assertPathIsNotLink, atomicWriteJson, } from '../resources/fs-safety.js';
17
17
  /**
18
- * Parses YAML frontmatter delimited by `---`, handling CRLF line endings.
18
+ * Parses YAML frontmatter delimited by `---`, handling CRLF line endings and UTF-8 BOM.
19
19
  */
20
20
  export function parseSkillMarkdown(raw) {
21
- // Normalize CRLF to LF
22
- const normalized = raw.replace(/\r\n/g, '\n').trim();
21
+ // Strip UTF-8 BOM and normalize CRLF to LF
22
+ const normalized = raw.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n').trim();
23
23
  if (!normalized.startsWith('---')) {
24
24
  return { metadata: {}, content: raw };
25
25
  }
@@ -79,309 +79,8 @@ async function withCatalogLock(operation) {
79
79
  });
80
80
  }
81
81
  // ─── Default Template Categories & Skills ─────────────────────────────────
82
- export const DEFAULT_CATEGORIES = [
83
- {
84
- id: 'pull-requests',
85
- name: 'Pull Requests & Review',
86
- description: 'Workflows for authoring, analyzing, and reviewing pull requests and merge readiness.',
87
- icon: 'git-pull-request',
88
- color: '#3b82f6',
89
- custom: false,
90
- isTemplate: true,
91
- skills: ['pr-review-toolkit', 'pr-description-gen', 'merge-conflict-resolver'],
92
- },
93
- {
94
- id: 'testing-qa',
95
- name: 'Testing & Quality Assurance',
96
- description: 'Test automation, coverage verification, and local runtime verifier recipes.',
97
- icon: 'flask-conical',
98
- color: '#10b981',
99
- custom: false,
100
- isTemplate: true,
101
- skills: ['verifier-workspace', 'e2e-runner', 'unit-test-coverage'],
102
- },
103
- {
104
- id: 'cross-repo-release',
105
- name: 'Cross-Repo & Release Ordering',
106
- description: 'Managing multi-repo dependency loops, package packing, and merge ordering.',
107
- icon: 'package',
108
- color: '#8b5cf6',
109
- custom: false,
110
- isTemplate: true,
111
- skills: ['nexusflow-local-package-loop', 'nexusflow-release-ordering'],
112
- },
113
- {
114
- id: 'database-migrations',
115
- name: 'Database & Migrations',
116
- description: 'Safe schema migrations, rollback procedures, and SQL performance checks.',
117
- icon: 'database',
118
- color: '#f59e0b',
119
- custom: false,
120
- isTemplate: true,
121
- skills: ['schema-migration-validator', 'sql-fluff-linter'],
122
- },
123
- {
124
- id: 'security-auditing',
125
- name: 'Security & Auditing',
126
- description: 'OWASP vulnerability scanning, secret leak detection, and compliance auditing.',
127
- icon: 'shield-check',
128
- color: '#ef4444',
129
- custom: false,
130
- isTemplate: true,
131
- skills: ['secret-scanner', 'security-auditor'],
132
- },
133
- ];
134
- export const DEFAULT_SKILLS = [
135
- {
136
- id: 'pr-review-toolkit',
137
- name: 'pr-review-toolkit',
138
- title: 'Pull Request Review Toolkit',
139
- category: 'pull-requests',
140
- description: 'Reviews pull requests for breaking changes, code style, edge cases, and test coverage. Use when user asks to "review PR", "audit changes", or "check diff".',
141
- tags: ['git', 'pr', 'review', 'quality'],
142
- allowedTools: ['run_command', 'view_file', 'grep_search'],
143
- custom: false,
144
- content: `# Pull Request Review Toolkit
145
-
146
- This skill guides the AI assistant through a structured, multi-file pull request review.
147
-
148
- ## Review Steps
149
- 1. **Analyze Diff Scope**:
150
- - Inspect uncommitted changes or branch diff using \`git diff origin/main...HEAD\`.
151
- - Identify affected modules, interfaces, and public API boundaries.
152
- 2. **Contract & Regression Check**:
153
- - Verify that existing function signatures and data contracts remain backwards-compatible.
154
- - Check error handling, boundary conditions, and null safety.
155
- 3. **Test Coverage & Verification**:
156
- - Confirm unit or integration tests exist for newly added logic.
157
- - Run verification commands (\`npm test\`, \`dotnet test\`, or project test runner).
158
- 4. **Structured Feedback**:
159
- - Return findings organized by Severity (Critical, High, Medium, Minor) with exact line references and remediation suggestions.
160
- `,
161
- },
162
- {
163
- id: 'pr-description-gen',
164
- name: 'pr-description-gen',
165
- title: 'PR Description Generator',
166
- category: 'pull-requests',
167
- description: 'Generates conventional, well-structured Pull Request descriptions with summaries, diff breakdowns, and test checklists.',
168
- tags: ['git', 'pr', 'documentation'],
169
- allowedTools: ['run_command'],
170
- custom: false,
171
- content: `# Pull Request Description Generator
172
-
173
- Automatically writes standardized, clear PR descriptions based on git history and repository changes.
174
-
175
- ## Procedure
176
- 1. Extract commit messages on the feature branch via \`git log origin/main..HEAD --oneline\`.
177
- 2. Inspect the file change stats via \`git diff --stat origin/main..HEAD\`.
178
- 3. Format output with:
179
- - **Summary**: 2-3 sentences explaining the "why" and "what".
180
- - **Key Changes**: Bulleted list of architectural and implementation updates.
181
- - **Testing Plan**: Exact verification commands run and outcomes.
182
- - **Breaking Changes**: Explicit flag if public APIs or database schemas were altered.
183
- `,
184
- },
185
- {
186
- id: 'merge-conflict-resolver',
187
- name: 'merge-conflict-resolver',
188
- title: 'Merge Conflict Resolver',
189
- category: 'pull-requests',
190
- description: 'Guides step-by-step resolution of git merge conflicts, ensuring neither upstream updates nor local feature intent are lost.',
191
- tags: ['git', 'merge', 'conflict'],
192
- allowedTools: ['run_command', 'view_file', 'replace_file_content'],
193
- custom: false,
194
- content: `# Merge Conflict Resolver
195
-
196
- Guidelines for resolving complex git merge and rebase conflicts across workspace repositories.
197
-
198
- ## Workflow
199
- 1. Run \`git status\` to locate unmerged paths (\`UU\` status).
200
- 2. For each conflicted file, inspect both the incoming (HEAD) and current versions.
201
- 3. Identify semantic intent of both sides before removing conflict markers (\`<<<<<<<\`, \`=======\`, \`>>>>>>>\`).
202
- 4. Re-run project tests and type-checks to verify resolution.
203
- `,
204
- },
205
- {
206
- id: 'verifier-workspace',
207
- name: 'verifier-workspace',
208
- title: 'Local Runtime Verifier',
209
- category: 'testing-qa',
210
- description: 'Guidelines and recipes to safely launch, mock, and verify services locally across workspace repositories.',
211
- tags: ['testing', 'runtime', 'verifier', 'ports'],
212
- allowedTools: ['run_command'],
213
- custom: false,
214
- content: `# Local Runtime Verifier
215
-
216
- Guidelines to safely launch, mock, and verify services locally in this workspace.
217
-
218
- ## Verification Recipe
219
- 1. **Check local ports**: Ensure target ports do not conflict with running services.
220
- 2. **Run mocks**: Spin up local databases/caches before starting services.
221
- 3. **Watch out for shared staging environment**: Never publish messages or write data to staging infrastructure while testing locally unless explicitly requested.
222
- `,
223
- },
224
- {
225
- id: 'e2e-runner',
226
- name: 'e2e-runner',
227
- title: 'End-to-End Test Runner',
228
- category: 'testing-qa',
229
- description: 'Orchestrates end-to-end testing (Playwright, Cypress) with failure triage and trace analysis.',
230
- tags: ['e2e', 'playwright', 'testing'],
231
- allowedTools: ['run_command', 'view_file'],
232
- custom: false,
233
- content: `# End-to-End Test Runner
234
-
235
- Guides executing and debugging end-to-end browser and API tests.
236
-
237
- ## Instructions
238
- 1. Run headless test suite: \`npx playwright test\` or \`npm run test:e2e\`.
239
- 2. On failure, inspect generated screenshots, videos, or trace logs in the test results directory.
240
- 3. Fix underlying selector mismatches, timing issues, or backend regressions.
241
- `,
242
- },
243
- {
244
- id: 'unit-test-coverage',
245
- name: 'unit-test-coverage',
246
- title: 'Unit Test Coverage & TDD',
247
- category: 'testing-qa',
248
- description: 'Implements comprehensive unit tests for new or modified modules, adhering to test-driven development best practices.',
249
- tags: ['unit-test', 'tdd', 'coverage'],
250
- allowedTools: ['run_command', 'view_file', 'write_to_file', 'replace_file_content'],
251
- custom: false,
252
- content: `# Unit Test Coverage & TDD
253
-
254
- Guides writing high-coverage, maintainable unit tests.
255
-
256
- ## Guidelines
257
- - Test behavior, not implementation details.
258
- - Mock external network calls, database connections, and file system I/O.
259
- - Verify edge cases: empty collections, null inputs, unexpected exceptions, timeouts.
260
- - Target >80% statement and branch coverage on critical business logic.
261
- `,
262
- },
263
- {
264
- id: 'nexusflow-local-package-loop',
265
- name: 'nexusflow-local-package-loop',
266
- title: 'Local Package Development Loop',
267
- category: 'cross-repo-release',
268
- description: 'Guides testing cross-repo package dependencies locally without publishing to external registries.',
269
- tags: ['cross-repo', 'npm', 'nuget', 'packages'],
270
- allowedTools: ['run_command', 'view_file'],
271
- custom: false,
272
- content: `# Local Package Development Loop
273
-
274
- This skill guides the AI assistant through local package testing across repositories in this workspace.
275
-
276
- ## Workflow
277
- When modifying a shared package in one repository, you must test its effect on downstream consumer repositories before pushing.
278
-
279
- ### npm / JS/TS:
280
- 1. Run \`npm pack\` inside the producing package directory.
281
- 2. Copy the generated \`.tgz\` file to \`local-packages/\`.
282
- 3. Reference the local tarball in consumer \`package.json\`.
283
-
284
- ### NuGet / .NET:
285
- 1. Run \`dotnet pack -c Release -o ./local-packages\` in producer.
286
- 2. Reference local package version in consumer \`.csproj\`.
287
- `,
288
- },
289
- {
290
- id: 'nexusflow-release-ordering',
291
- name: 'nexusflow-release-ordering',
292
- title: 'Release & Merge Ordering',
293
- category: 'cross-repo-release',
294
- description: 'Computes and explains the correct topological merge and release order when cross-repo dependencies change.',
295
- tags: ['release', 'dependencies', 'ordering'],
296
- allowedTools: ['view_file'],
297
- custom: false,
298
- content: `# Release and Merge Ordering Guidelines
299
-
300
- Answers what repositories must be merged and released in what order when cross-repo dependencies are modified.
301
-
302
- ## Principles
303
- 1. **Producer First**: Repositories producing shared packages must be merged, tagged, and published first.
304
- 2. **Consumer Bump**: Downstream consumer repositories must update their version reference and be merged next.
305
- 3. **Reversion Check**: Ensure all temporary local package references are reverted before merging consumer branches.
306
- `,
307
- },
308
- {
309
- id: 'schema-migration-validator',
310
- name: 'schema-migration-validator',
311
- title: 'Schema Migration Validator',
312
- category: 'database-migrations',
313
- description: 'Validates database schema migrations for safety, lock contention risks, and backwards compatibility.',
314
- tags: ['database', 'migrations', 'sql', 'prisma'],
315
- allowedTools: ['run_command', 'view_file'],
316
- custom: false,
317
- content: `# Database Schema Migration Validator
318
-
319
- Guidelines for evaluating database migrations before running them against shared environments.
320
-
321
- ## Checklist
322
- - **No Table Locks**: Avoid adding non-null columns without default values to large existing tables.
323
- - **Index Creation**: Use concurrent index creation (e.g. \`CREATE INDEX CONCURRENTLY\`) on production databases.
324
- - **Rollback Safety**: Verify a corresponding down/revert migration script exists and works.
325
- `,
326
- },
327
- {
328
- id: 'sql-fluff-linter',
329
- name: 'sql-fluff-linter',
330
- title: 'SQL Quality & Linter',
331
- category: 'database-migrations',
332
- description: 'Lints SQL queries and migration scripts for formatting, performance traps, and ANSI SQL standards.',
333
- tags: ['sql', 'lint', 'database'],
334
- allowedTools: ['run_command', 'view_file'],
335
- custom: false,
336
- content: `# SQL Quality & Linter
337
-
338
- Enforces clean, performant SQL syntax across migrations and query templates.
339
-
340
- ## Rules
341
- - Use explicit column names in SELECT queries (avoid \`SELECT *\`).
342
- - Ensure all JOIN conditions use indexed foreign keys.
343
- - Uppercase SQL keywords (\`SELECT\`, \`WHERE\`, \`GROUP BY\`, \`ORDER BY\`).
344
- `,
345
- },
346
- {
347
- id: 'secret-scanner',
348
- name: 'secret-scanner',
349
- title: 'Secret & Credential Scanner',
350
- category: 'security-auditing',
351
- description: 'Scans source files, commit history, and configuration files for exposed API keys, private certificates, and secrets.',
352
- tags: ['security', 'secrets', 'credentials'],
353
- allowedTools: ['grep_search', 'view_file'],
354
- custom: false,
355
- content: `# Secret & Credential Scanner
356
-
357
- Proactively prevents committing confidential credentials to version control.
358
-
359
- ## High-Risk Patterns
360
- - AWS access keys (\`AKIA...\`), GitHub PATs (\`ghp_...\`), OpenAI keys (\`sk-...\`).
361
- - RSA / SSH private keys (\`-----BEGIN PRIVATE KEY-----\`).
362
- - Hardcoded connection strings with passwords in source code.
363
- `,
364
- },
365
- {
366
- id: 'security-auditor',
367
- name: 'security-auditor',
368
- title: 'Security Audit Playbook',
369
- category: 'security-auditing',
370
- description: 'Use when auditing code for OWASP Top 10 vulnerabilities, access-control gaps, injection, or cryptographic failures.',
371
- tags: ['security', 'audit', 'owasp'],
372
- allowedTools: ['view_file', 'grep_search'],
373
- custom: false,
374
- content: `# Security Audit Playbook
375
-
376
- Instructions for conducting deep application security audits.
377
-
378
- ## Focus Areas
379
- 1. **Injection Vectors**: SQL, Command, LDAP, and XSS vulnerabilities.
380
- 2. **Broken Access Control**: Missing authorization checks on sensitive REST / RPC routes.
381
- 3. **Cryptographic Failures**: Deprecated hashing algorithms (MD5/SHA1) or unencrypted tokens.
382
- `,
383
- },
384
- ];
82
+ export const DEFAULT_CATEGORIES = [];
83
+ export const DEFAULT_SKILLS = [];
385
84
  // ─── Categories Management ────────────────────────────────────────────────
386
85
  export function getContextSpaceHome() {
387
86
  return resolveBrandHomeDir();
@@ -441,11 +140,15 @@ export async function getSkillCategories() {
441
140
  for (const cat of DEFAULT_CATEGORIES) {
442
141
  categoryMap.set(cat.id, { ...cat });
443
142
  }
444
- // 2. Load user categories (legacy fallback then primary)
445
- const candidateFiles = [
446
- path.join(os.homedir(), LEGACY_CONFIG_DIR_NAME, 'categories.json'),
447
- path.join(os.homedir(), PRIMARY_CONFIG_DIR_NAME, 'categories.json'),
448
- ];
143
+ // 2. Load user categories (legacy fallback then primary, or isolated custom home)
144
+ const csHome = process.env.CONTEXTSPACE_HOME?.trim();
145
+ const nfHome = process.env.NEXUSFLOW_HOME?.trim();
146
+ const candidateFiles = (csHome || nfHome)
147
+ ? [getUserCategoriesPath()]
148
+ : [
149
+ path.join(os.homedir(), LEGACY_CONFIG_DIR_NAME, 'categories.json'),
150
+ path.join(os.homedir(), PRIMARY_CONFIG_DIR_NAME, 'categories.json'),
151
+ ];
449
152
  const userPath = getUserCategoriesPath();
450
153
  if (!candidateFiles.includes(userPath)) {
451
154
  candidateFiles.push(userPath);
@@ -560,31 +263,42 @@ async function loadSkillFromDir(skillDir, custom, catalogRoot = path.dirname(ski
560
263
  metadataObj[LEGACY_RESOURCE_METADATA_KEY] !== null
561
264
  ? metadataObj[LEGACY_RESOURCE_METADATA_KEY]
562
265
  : {};
266
+ const rawMetaTitle = metadataObj && typeof metadataObj.title === 'string' ? metadataObj.title : undefined;
563
267
  const title = parsedMetadata.data.title ||
564
268
  (typeof brandMetadata.title === 'string' ? brandMetadata.title : undefined) ||
269
+ rawMetaTitle ||
565
270
  name
566
271
  .split('-')
567
272
  .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
568
273
  .join(' ');
274
+ const rawMetaCategory = metadataObj && typeof metadataObj.category === 'string' ? metadataObj.category : undefined;
569
275
  const category = parsedMetadata.data.category ||
570
276
  (typeof brandMetadata.category === 'string' ? brandMetadata.category : undefined) ||
277
+ rawMetaCategory ||
571
278
  'general';
572
279
  const description = parsedMetadata.data.description;
280
+ const rawMetaTags = metadataObj && Array.isArray(metadataObj.tags)
281
+ ? metadataObj.tags.filter((t) => typeof t === 'string')
282
+ : metadataObj && typeof metadataObj.tags === 'string'
283
+ ? metadataObj.tags.split(',').map((s) => s.trim()).filter(Boolean)
284
+ : [];
573
285
  const tags = parsedMetadata.data.tags ||
574
286
  (Array.isArray(brandMetadata.tags)
575
287
  ? brandMetadata.tags.filter((tag) => typeof tag === 'string')
576
- : []);
288
+ : rawMetaTags);
577
289
  const rawAllowedTools = parsedMetadata.data['allowed-tools'];
578
290
  const allowedTools = Array.isArray(rawAllowedTools)
579
291
  ? rawAllowedTools
580
292
  : rawAllowedTools
581
293
  ? rawAllowedTools.split(/\s+/).filter(Boolean)
582
294
  : [];
583
- // Inspect references/ and scripts/ if present
295
+ // Inspect references/, scripts/, and assets/ if present (per Agent Skills spec)
584
296
  const referencesDir = path.join(skillDir, 'references');
585
297
  const scriptsDir = path.join(skillDir, 'scripts');
298
+ const assetsDir = path.join(skillDir, 'assets');
586
299
  const references = [];
587
300
  const scripts = [];
301
+ const assets = [];
588
302
  if (await fse.pathExists(referencesDir)) {
589
303
  try {
590
304
  await assertNoLinkedPathComponents(skillDir, referencesDir);
@@ -619,6 +333,25 @@ async function loadSkillFromDir(skillDir, custom, catalogRoot = path.dirname(ski
619
333
  throw error;
620
334
  }
621
335
  }
336
+ if (await fse.pathExists(assetsDir)) {
337
+ try {
338
+ await assertNoLinkedPathComponents(skillDir, assetsDir);
339
+ const files = await fs.readdir(assetsDir, { withFileTypes: true });
340
+ for (const file of files) {
341
+ if (file.isSymbolicLink())
342
+ throw new Error(`Linked skill files are not allowed: ${file.name}`);
343
+ if (file.isFile()) {
344
+ assets.push({ name: file.name, relativePath: path.join('assets', file.name) });
345
+ }
346
+ }
347
+ }
348
+ catch (error) {
349
+ if (error instanceof Error && error.message.includes('Linked skill files'))
350
+ throw error;
351
+ }
352
+ }
353
+ const rawScope = parsedMetadata.data.scope ?? brandMetadata.scope;
354
+ const scope = rawScope === 'workspace' || rawScope === 'global' ? rawScope : undefined;
622
355
  return {
623
356
  id,
624
357
  name,
@@ -632,22 +365,32 @@ async function loadSkillFromDir(skillDir, custom, catalogRoot = path.dirname(ski
632
365
  sourcePath: skillDir,
633
366
  references: references.length > 0 ? references : undefined,
634
367
  scripts: scripts.length > 0 ? scripts : undefined,
368
+ assets: assets.length > 0 ? assets : undefined,
369
+ scope,
370
+ metadata: metadataObj,
371
+ license: parsedMetadata.data.license,
372
+ compatibility: parsedMetadata.data.compatibility,
635
373
  };
636
374
  }
637
375
  /**
638
376
  * Retrieves all available skills (built-in templates + user directory + optional workspace directory).
639
377
  */
640
- export async function getAllSkills(_workspacePath) {
378
+ export async function getAllSkills(workspacePath) {
641
379
  const skillMap = new Map();
642
380
  // 1. Built-in template skills
643
381
  for (const s of DEFAULT_SKILLS) {
644
- skillMap.set(s.id, { ...s });
382
+ skillMap.set(s.id, { ...s, scope: 'global' });
645
383
  }
646
- // 2. User directory (~/.nexusflow/skills/ and ~/.contextspace/skills/)
647
- const candidateDirs = [
648
- path.join(os.homedir(), LEGACY_CONFIG_DIR_NAME, 'skills'),
649
- path.join(os.homedir(), PRIMARY_CONFIG_DIR_NAME, 'skills'),
650
- ];
384
+ // 2. User directory (~/.nexusflow/skills/ and ~/.contextspace/skills/, or isolated custom home)
385
+ const csHome = process.env.CONTEXTSPACE_HOME?.trim();
386
+ const nfHome = process.env.NEXUSFLOW_HOME?.trim();
387
+ const candidateDirs = (csHome || nfHome)
388
+ ? [getUserSkillsDir()]
389
+ : [
390
+ path.join(os.homedir(), LEGACY_CONFIG_DIR_NAME, 'skills'),
391
+ path.join(os.homedir(), PRIMARY_CONFIG_DIR_NAME, 'skills'),
392
+ path.join(os.homedir(), '.agents', 'skills'),
393
+ ];
651
394
  const activeSkillsDir = getUserSkillsDir();
652
395
  if (!candidateDirs.includes(activeSkillsDir)) {
653
396
  candidateDirs.push(activeSkillsDir);
@@ -669,6 +412,7 @@ export async function getAllSkills(_workspacePath) {
669
412
  if (isBuiltIn) {
670
413
  throw new Error(`A resource named "${loaded.id}" already exists in the built-in catalog.`);
671
414
  }
415
+ loaded.scope = loaded.scope || 'global';
672
416
  skillMap.set(loaded.id, loaded);
673
417
  }
674
418
  }
@@ -685,12 +429,93 @@ export async function getAllSkills(_workspacePath) {
685
429
  }
686
430
  }
687
431
  }
432
+ // 3. Workspace-local skills (<workspace>/.agents/skills/)
433
+ if (workspacePath) {
434
+ try {
435
+ const canonicalWorkspace = await fs.realpath(workspacePath).catch(() => path.resolve(workspacePath));
436
+ const workspaceSkillsDir = path.join(canonicalWorkspace, '.agents', 'skills');
437
+ if (await fse.pathExists(workspaceSkillsDir)) {
438
+ await assertPathIsNotLink(workspaceSkillsDir);
439
+ const materializedGlobalSkillIds = new Set();
440
+ const lockPathPrimary = path.join(canonicalWorkspace, PRIMARY_CONFIG_DIR_NAME, 'resources.lock.json');
441
+ const lockPathLegacy = path.join(canonicalWorkspace, LEGACY_CONFIG_DIR_NAME, 'resources.lock.json');
442
+ const lockPath = (await fse.pathExists(lockPathPrimary))
443
+ ? lockPathPrimary
444
+ : (await fse.pathExists(lockPathLegacy))
445
+ ? lockPathLegacy
446
+ : null;
447
+ if (lockPath) {
448
+ try {
449
+ const lockJson = await fse.readJson(lockPath);
450
+ const outputs = Array.isArray(lockJson?.outputs)
451
+ ? lockJson.outputs
452
+ : Array.isArray(lockJson?.managedFiles)
453
+ ? lockJson.managedFiles
454
+ : [];
455
+ for (const out of outputs) {
456
+ if (out && out.kind === 'skill') {
457
+ const outPath = typeof out.path === 'string' ? out.path.replaceAll('\\', '/') : '';
458
+ if (out.adapter === 'agent-skill-v1' || outPath.startsWith('.agents/skills/')) {
459
+ if (typeof out.resourceId === 'string') {
460
+ materializedGlobalSkillIds.add(out.resourceId);
461
+ }
462
+ }
463
+ }
464
+ }
465
+ }
466
+ catch {
467
+ // Ignore unreadable or corrupt lock file
468
+ }
469
+ }
470
+ const entries = await fs.readdir(workspaceSkillsDir, { withFileTypes: true });
471
+ if (Array.isArray(entries)) {
472
+ for (const entry of entries) {
473
+ const isDir = typeof entry === 'string' ? true : entry.isDirectory ? entry.isDirectory() : true;
474
+ const entryName = typeof entry === 'string' ? entry : entry.name;
475
+ if (isDir) {
476
+ try {
477
+ const skillDir = path.join(workspaceSkillsDir, entryName);
478
+ const loaded = await loadSkillFromDir(skillDir, true, workspaceSkillsDir);
479
+ if (loaded) {
480
+ const isMaterializedGlobal = loaded.scope === 'global' ||
481
+ (loaded.scope !== 'workspace' && materializedGlobalSkillIds.has(loaded.id));
482
+ if (isMaterializedGlobal) {
483
+ // Materialized global skill: do NOT mark its scope as 'workspace'.
484
+ // If its global counterpart was deleted machine-wide, do NOT resurrect it as an active workspace skill.
485
+ if (skillMap.has(loaded.id)) {
486
+ const globalSkill = skillMap.get(loaded.id);
487
+ globalSkill.scope = 'global';
488
+ }
489
+ }
490
+ else {
491
+ loaded.scope = 'workspace';
492
+ skillMap.set(loaded.id, loaded);
493
+ }
494
+ }
495
+ }
496
+ catch (error) {
497
+ const message = error instanceof Error ? error.message : String(error);
498
+ console.warn(`Skipping invalid workspace skill "${entryName}": ${message}`);
499
+ }
500
+ }
501
+ }
502
+ }
503
+ }
504
+ }
505
+ catch (err) {
506
+ console.error('Failed to load workspace skills from', workspacePath, err);
507
+ }
508
+ }
688
509
  return Array.from(skillMap.values());
689
510
  }
690
511
  /**
691
512
  * Saves or updates a portable skill package.
692
513
  */
693
514
  export async function saveSkill(skill, options = {}) {
515
+ const scope = options.scope ?? 'global';
516
+ if (scope === 'workspace' && !options.workspacePath) {
517
+ throw new Error('workspacePath is required when saving a workspace-scoped skill.');
518
+ }
694
519
  const rawId = skill.id || skill.name;
695
520
  const idResult = resourceIdSchema.safeParse(rawId);
696
521
  if (!idResult.success) {
@@ -711,20 +536,19 @@ export async function saveSkill(skill, options = {}) {
711
536
  throw new Error('Skill description is required.');
712
537
  if (!skill.content.trim())
713
538
  throw new Error('Skill content is required.');
714
- return withCatalogLock(async () => {
715
- const userSkillsDir = path.resolve(getUserSkillsDir());
716
- await fse.ensureDir(userSkillsDir);
717
- await assertPathIsNotLink(userSkillsDir);
718
- const targetDir = assertPathWithin(userSkillsDir, path.join(userSkillsDir, id));
719
- await assertNoLinkedPathComponents(userSkillsDir, targetDir);
720
- const stagingDir = await fs.mkdtemp(path.join(userSkillsDir, `.staging-${id}-`));
721
- const backupDir = path.join(userSkillsDir, `.backup-${id}-${randomUUID()}`);
539
+ const executeSave = async (skillsBaseDir) => {
540
+ await fse.ensureDir(skillsBaseDir);
541
+ await assertPathIsNotLink(skillsBaseDir);
542
+ const targetDir = assertPathWithin(skillsBaseDir, path.join(skillsBaseDir, id));
543
+ await assertNoLinkedPathComponents(skillsBaseDir, targetDir);
544
+ const stagingDir = await fs.mkdtemp(path.join(skillsBaseDir, `.staging-${id}-`));
545
+ const backupDir = path.join(skillsBaseDir, `.backup-${id}-${randomUUID()}`);
722
546
  let movedExisting = false;
723
547
  let installedStaging = false;
724
548
  try {
725
549
  let existingFrontmatter;
726
550
  if (await fse.pathExists(targetDir)) {
727
- await assertNoLinkedPathComponents(userSkillsDir, targetDir);
551
+ await assertNoLinkedPathComponents(skillsBaseDir, targetDir);
728
552
  for (const entry of await fs.readdir(targetDir, { withFileTypes: true })) {
729
553
  if (entry.isSymbolicLink()) {
730
554
  throw new Error(`Linked skill package entries are not allowed: ${entry.name}`);
@@ -742,6 +566,9 @@ export async function saveSkill(skill, options = {}) {
742
566
  existingFrontmatter = parsedCurrentFrontmatter.data;
743
567
  }
744
568
  const existingMetaObj = existingFrontmatter?.metadata;
569
+ const callerMetadata = skill.metadata && typeof skill.metadata === 'object' && !Array.isArray(skill.metadata)
570
+ ? skill.metadata
571
+ : undefined;
745
572
  const existingBrandMetadata = existingMetaObj &&
746
573
  typeof existingMetaObj[RESOURCE_METADATA_KEY] === 'object' &&
747
574
  existingMetaObj[RESOURCE_METADATA_KEY] !== null
@@ -751,19 +578,59 @@ export async function saveSkill(skill, options = {}) {
751
578
  existingMetaObj[LEGACY_RESOURCE_METADATA_KEY] !== null
752
579
  ? existingMetaObj[LEGACY_RESOURCE_METADATA_KEY]
753
580
  : {};
581
+ const callerBrandMetadata = callerMetadata &&
582
+ typeof callerMetadata[RESOURCE_METADATA_KEY] === 'object' &&
583
+ callerMetadata[RESOURCE_METADATA_KEY] !== null
584
+ ? callerMetadata[RESOURCE_METADATA_KEY]
585
+ : callerMetadata &&
586
+ typeof callerMetadata[LEGACY_RESOURCE_METADATA_KEY] === 'object' &&
587
+ callerMetadata[LEGACY_RESOURCE_METADATA_KEY] !== null
588
+ ? callerMetadata[LEGACY_RESOURCE_METADATA_KEY]
589
+ : {};
590
+ const inferredTitle = skill.title ||
591
+ (callerMetadata && typeof callerMetadata.title === 'string' ? callerMetadata.title : undefined) ||
592
+ (typeof existingBrandMetadata.title === 'string' ? existingBrandMetadata.title : undefined) ||
593
+ id;
594
+ const inferredCategory = options.category ||
595
+ skill.category ||
596
+ (callerMetadata && typeof callerMetadata.category === 'string' ? callerMetadata.category : undefined) ||
597
+ (typeof existingBrandMetadata.category === 'string' ? existingBrandMetadata.category : undefined) ||
598
+ 'general';
599
+ const callerMetaTags = callerMetadata && Array.isArray(callerMetadata.tags)
600
+ ? callerMetadata.tags.filter((t) => typeof t === 'string')
601
+ : undefined;
602
+ const existingBrandTags = Array.isArray(existingBrandMetadata.tags)
603
+ ? existingBrandMetadata.tags.filter((t) => typeof t === 'string')
604
+ : undefined;
605
+ const inferredTags = skill.tags || callerMetaTags || existingBrandTags || [];
754
606
  const metadataPayload = {
755
607
  ...existingBrandMetadata,
756
- title: skill.title || id,
757
- category: skill.category || 'general',
758
- tags: skill.tags || [],
608
+ ...callerBrandMetadata,
609
+ title: inferredTitle,
610
+ category: inferredCategory,
611
+ tags: inferredTags,
612
+ scope,
759
613
  };
614
+ const mergedCustomMetadata = {
615
+ ...(existingFrontmatter?.metadata ?? {}),
616
+ ...(callerMetadata ?? {}),
617
+ };
618
+ delete mergedCustomMetadata[RESOURCE_METADATA_KEY];
619
+ delete mergedCustomMetadata[LEGACY_RESOURCE_METADATA_KEY];
620
+ delete mergedCustomMetadata['__proto__'];
621
+ delete mergedCustomMetadata['constructor'];
622
+ delete mergedCustomMetadata['prototype'];
760
623
  const metadata = {
761
624
  name: id,
625
+ title: inferredTitle,
626
+ category: inferredCategory,
762
627
  description,
763
- license: existingFrontmatter?.license,
764
- compatibility: existingFrontmatter?.compatibility,
628
+ tags: inferredTags,
629
+ license: skill.license ?? existingFrontmatter?.license,
630
+ compatibility: skill.compatibility ?? existingFrontmatter?.compatibility,
631
+ scope,
765
632
  metadata: {
766
- ...(existingFrontmatter?.metadata ?? {}),
633
+ ...mergedCustomMetadata,
767
634
  [RESOURCE_METADATA_KEY]: metadataPayload,
768
635
  [LEGACY_RESOURCE_METADATA_KEY]: metadataPayload,
769
636
  },
@@ -777,6 +644,7 @@ export async function saveSkill(skill, options = {}) {
777
644
  for (const [directory, files] of [
778
645
  ['references', skill.references],
779
646
  ['scripts', skill.scripts],
647
+ ['assets', skill.assets],
780
648
  ]) {
781
649
  if (files === undefined)
782
650
  continue;
@@ -806,18 +674,24 @@ export async function saveSkill(skill, options = {}) {
806
674
  }
807
675
  await options.beforeCommit?.();
808
676
  if (await fse.pathExists(targetDir)) {
809
- await assertNoLinkedPathComponents(userSkillsDir, targetDir);
677
+ await assertNoLinkedPathComponents(skillsBaseDir, targetDir);
810
678
  await fs.rename(targetDir, backupDir);
811
679
  movedExisting = true;
812
680
  }
813
681
  await fs.rename(stagingDir, targetDir);
814
682
  installedStaging = true;
815
- const loaded = await loadSkillFromDir(targetDir, true, userSkillsDir);
683
+ const loaded = await loadSkillFromDir(targetDir, true, skillsBaseDir);
816
684
  if (!loaded)
817
685
  throw new Error('Saved skill could not be loaded.');
686
+ loaded.scope = scope;
818
687
  if (movedExisting)
819
688
  await fse.remove(backupDir).catch(() => { });
820
- return loaded;
689
+ const skillFilePath = path.join(targetDir, 'SKILL.md');
690
+ return {
691
+ ...loaded,
692
+ path: skillFilePath,
693
+ skill: loaded,
694
+ };
821
695
  }
822
696
  catch (error) {
823
697
  await fse.remove(stagingDir).catch(() => { });
@@ -828,16 +702,42 @@ export async function saveSkill(skill, options = {}) {
828
702
  }
829
703
  throw error;
830
704
  }
831
- });
705
+ };
706
+ if (scope === 'workspace') {
707
+ const canonicalWorkspace = await fs.realpath(options.workspacePath).catch(() => path.resolve(options.workspacePath));
708
+ const workspaceSkillsDir = path.join(canonicalWorkspace, '.agents', 'skills');
709
+ return runWorkspaceConfigMutation(async () => {
710
+ await assertNoLinkedPathComponents(canonicalWorkspace, workspaceSkillsDir);
711
+ return executeSave(workspaceSkillsDir);
712
+ });
713
+ }
714
+ return withCatalogLock(async () => executeSave(path.resolve(getUserSkillsDir())));
832
715
  }
833
716
  /**
834
- * Deletes a user custom skill safely.
717
+ * Deletes a skill safely from the global or workspace catalog.
835
718
  */
836
- export async function deleteSkill(id) {
719
+ export async function deleteSkill(id, options) {
837
720
  const idResult = resourceIdSchema.safeParse(id);
838
721
  if (!idResult.success || idResult.data !== id) {
839
722
  throw new Error('Invalid skill ID format.');
840
723
  }
724
+ const scope = options?.scope ?? 'global';
725
+ if (scope === 'workspace') {
726
+ if (!options?.workspacePath) {
727
+ throw new Error('workspacePath is required when deleting a workspace-scoped skill.');
728
+ }
729
+ const wsPath = options.workspacePath;
730
+ const canonicalWorkspace = await fs.realpath(wsPath).catch(() => path.resolve(wsPath));
731
+ const workspaceSkillsDir = path.join(canonicalWorkspace, '.agents', 'skills');
732
+ await runWorkspaceConfigMutation(async () => {
733
+ const targetDir = assertPathWithin(canonicalWorkspace, path.join(workspaceSkillsDir, id));
734
+ await assertNoLinkedPathComponents(canonicalWorkspace, targetDir);
735
+ if (!(await fse.pathExists(targetDir)))
736
+ throw new Error('Skill not found.');
737
+ await fse.remove(targetDir);
738
+ });
739
+ return;
740
+ }
841
741
  await withCatalogLock(async () => {
842
742
  const userSkillsDir = path.resolve(getUserSkillsDir());
843
743
  await fse.ensureDir(userSkillsDir);