@mrpatronz/nexusflow 2.11.0 → 2.12.1

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 (209) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +154 -13
  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 +1 -1
  50. package/dist/core/generation-lock.d.ts.map +1 -1
  51. package/dist/core/generation-lock.js +18 -11
  52. package/dist/core/generation-lock.js.map +1 -1
  53. package/dist/core/lifecycle.d.ts +28 -0
  54. package/dist/core/lifecycle.d.ts.map +1 -0
  55. package/dist/core/lifecycle.js +423 -0
  56. package/dist/core/lifecycle.js.map +1 -0
  57. package/dist/core/planning-notes.d.ts +11 -0
  58. package/dist/core/planning-notes.d.ts.map +1 -0
  59. package/dist/core/planning-notes.js +108 -0
  60. package/dist/core/planning-notes.js.map +1 -0
  61. package/dist/core/verify.d.ts +39 -0
  62. package/dist/core/verify.d.ts.map +1 -0
  63. package/dist/core/verify.js +397 -0
  64. package/dist/core/verify.js.map +1 -0
  65. package/dist/core/work-guidance.d.ts +202 -0
  66. package/dist/core/work-guidance.d.ts.map +1 -0
  67. package/dist/core/work-guidance.js +231 -0
  68. package/dist/core/work-guidance.js.map +1 -0
  69. package/dist/core/workspace-git.d.ts.map +1 -1
  70. package/dist/core/workspace-git.js +1 -0
  71. package/dist/core/workspace-git.js.map +1 -1
  72. package/dist/core/workspace-state.d.ts +24 -3
  73. package/dist/core/workspace-state.d.ts.map +1 -1
  74. package/dist/core/workspace-state.js +93 -31
  75. package/dist/core/workspace-state.js.map +1 -1
  76. package/dist/core/workspace-tools.d.ts +10 -0
  77. package/dist/core/workspace-tools.d.ts.map +1 -0
  78. package/dist/core/workspace-tools.js +123 -0
  79. package/dist/core/workspace-tools.js.map +1 -0
  80. package/dist/core/workspace.d.ts.map +1 -1
  81. package/dist/core/workspace.js +7 -17
  82. package/dist/core/workspace.js.map +1 -1
  83. package/dist/generators/base.d.ts.map +1 -1
  84. package/dist/generators/base.js +42 -5
  85. package/dist/generators/base.js.map +1 -1
  86. package/dist/generators/index.d.ts.map +1 -1
  87. package/dist/generators/index.js +11 -0
  88. package/dist/generators/index.js.map +1 -1
  89. package/dist/generators/plan-generator.d.ts.map +1 -1
  90. package/dist/generators/plan-generator.js +61 -58
  91. package/dist/generators/plan-generator.js.map +1 -1
  92. package/dist/generators/skills-generator.d.ts.map +1 -1
  93. package/dist/generators/skills-generator.js +36 -3
  94. package/dist/generators/skills-generator.js.map +1 -1
  95. package/dist/gui/assets/AgentsPage-C7Rh_dp7.js +3 -0
  96. package/dist/gui/assets/DashboardPage-YVONGtqS.js +1 -0
  97. package/dist/gui/assets/{GettingStartedPage-1MOTng3B.js → GettingStartedPage-C2Kbvz_N.js} +1 -1
  98. package/dist/gui/assets/ProjectsPage-DOpfV2Oj.js +1 -0
  99. package/dist/gui/assets/{ScaffoldRepoInline-CP0yn5mw.js → ScaffoldRepoInline-D91aitAi.js} +1 -1
  100. package/dist/gui/assets/{SettingsPage-DWmrLV8o.js → SettingsPage-BdaHN6fa.js} +1 -1
  101. package/dist/gui/assets/SkillsPage-DT9lRkCg.js +7 -0
  102. package/dist/gui/assets/StartWorkPage-D932lq9x.js +1 -0
  103. package/dist/gui/assets/{StrategiesPage-CdCbbIu-.js → StrategiesPage-DCfS-wdu.js} +2 -2
  104. package/dist/gui/assets/WorkroomsPage-BQU27nWS.js +4 -0
  105. package/dist/gui/assets/WorkspacesPage-D3TLVmq-.js +13 -0
  106. package/dist/gui/assets/{alert-dialog-u7qgb9Hq.js → alert-dialog-YynbGyj1.js} +1 -1
  107. package/dist/gui/assets/badge-DTBzXOWn.js +1 -0
  108. package/dist/gui/assets/index-er-mkc2S.css +2 -0
  109. package/dist/gui/assets/index-hTa55BQ6.js +57 -0
  110. package/dist/gui/assets/square-check-big-Bj8Rf_dH.js +1 -0
  111. package/dist/gui/assets/tabs-DfW7Akjs.js +1 -0
  112. package/dist/gui/assets/{useAriaLabelledBy-C4zV-1Ra.js → useAriaLabelledBy-BmJErI5a.js} +1 -1
  113. package/dist/gui/assets/vsc-BsVrJLSg.js +1 -0
  114. package/dist/gui/index.html +3 -5
  115. package/dist/http/work-guidance-routes.d.ts +4 -0
  116. package/dist/http/work-guidance-routes.d.ts.map +1 -0
  117. package/dist/http/work-guidance-routes.js +50 -0
  118. package/dist/http/work-guidance-routes.js.map +1 -0
  119. package/dist/index.js +169 -1
  120. package/dist/index.js.map +1 -1
  121. package/dist/mcp/tools.d.ts +0 -7
  122. package/dist/mcp/tools.d.ts.map +1 -1
  123. package/dist/mcp/tools.js +207 -0
  124. package/dist/mcp/tools.js.map +1 -1
  125. package/dist/resources/contracts.d.ts.map +1 -1
  126. package/dist/resources/contracts.js +2 -1
  127. package/dist/resources/contracts.js.map +1 -1
  128. package/dist/resources/fs-safety.d.ts.map +1 -1
  129. package/dist/resources/fs-safety.js +2 -1
  130. package/dist/resources/fs-safety.js.map +1 -1
  131. package/dist/resources/materializer.d.ts +3 -0
  132. package/dist/resources/materializer.d.ts.map +1 -1
  133. package/dist/resources/materializer.js +45 -19
  134. package/dist/resources/materializer.js.map +1 -1
  135. package/dist/resources/service.d.ts +1 -1
  136. package/dist/resources/service.d.ts.map +1 -1
  137. package/dist/resources/service.js +3 -2
  138. package/dist/resources/service.js.map +1 -1
  139. package/dist/server.d.ts.map +1 -1
  140. package/dist/server.js +333 -8
  141. package/dist/server.js.map +1 -1
  142. package/dist/static-resources/skills/contextspace-verify-release/SKILL.md +51 -0
  143. package/dist/static-resources/skills/contextspace-verify-release/references/evidence.md +21 -0
  144. package/dist/static-resources/skills/nexusflow-dev/SKILL.md +49 -0
  145. package/dist/static-resources/skills/nexusflow-dev/references/cli-conventions.md +15 -0
  146. package/dist/static-resources/skills/nexusflow-dev/references/verification-checklist.md +28 -0
  147. package/dist/static-resources/skills/nexusflow-lifecycle/SKILL.md +66 -0
  148. package/dist/static-resources/skills/nexusflow-lifecycle/references/epic-deliverables.md +36 -0
  149. package/dist/static-resources/skills/nexusflow-lifecycle/references/flow-selection-matrix.md +21 -0
  150. package/dist/static-resources/skills/nexusflow-lifecycle/references/stage-outcomes.md +23 -0
  151. package/dist/static-resources/workflows/epic-multi-slice.md +21 -0
  152. package/dist/static-resources/workflows/plan-implement-review.md +17 -6
  153. package/dist/static-resources/workflows/research-verify.md +15 -4
  154. package/dist/static-resources/workflows/solo-developer.md +11 -2
  155. package/dist/test-setup.d.ts +2 -0
  156. package/dist/test-setup.d.ts.map +1 -0
  157. package/dist/test-setup.js +8 -0
  158. package/dist/test-setup.js.map +1 -0
  159. package/dist/types.d.ts +178 -0
  160. package/dist/types.d.ts.map +1 -1
  161. package/dist/utils/resolve-workspace.d.ts.map +1 -1
  162. package/dist/utils/resolve-workspace.js +15 -8
  163. package/dist/utils/resolve-workspace.js.map +1 -1
  164. package/dist/utils/skills-catalog.d.ts +18 -9
  165. package/dist/utils/skills-catalog.d.ts.map +1 -1
  166. package/dist/utils/skills-catalog.js +248 -344
  167. package/dist/utils/skills-catalog.js.map +1 -1
  168. package/dist/utils/workflow-advisor.d.ts +1 -1
  169. package/dist/utils/workflow-advisor.d.ts.map +1 -1
  170. package/dist/utils/workflow-advisor.js +16 -4
  171. package/dist/utils/workflow-advisor.js.map +1 -1
  172. package/dist/utils/workflows.d.ts.map +1 -1
  173. package/dist/utils/workflows.js +5 -2
  174. package/dist/utils/workflows.js.map +1 -1
  175. package/dist/workrooms/contracts.d.ts +3 -3
  176. package/dist/workrooms/local-resources.d.ts +4 -1
  177. package/dist/workrooms/local-resources.d.ts.map +1 -1
  178. package/dist/workrooms/manager.d.ts +5 -2
  179. package/dist/workrooms/manager.d.ts.map +1 -1
  180. package/dist/workrooms/store.d.ts +2 -2
  181. package/package.json +3 -2
  182. package/resources/skills/contextspace-verify-release/SKILL.md +51 -0
  183. package/resources/skills/contextspace-verify-release/references/evidence.md +21 -0
  184. package/resources/skills/nexusflow-dev/SKILL.md +49 -0
  185. package/resources/skills/nexusflow-dev/references/cli-conventions.md +15 -0
  186. package/resources/skills/nexusflow-dev/references/verification-checklist.md +28 -0
  187. package/resources/skills/nexusflow-lifecycle/SKILL.md +66 -0
  188. package/resources/skills/nexusflow-lifecycle/references/epic-deliverables.md +36 -0
  189. package/resources/skills/nexusflow-lifecycle/references/flow-selection-matrix.md +21 -0
  190. package/resources/skills/nexusflow-lifecycle/references/stage-outcomes.md +23 -0
  191. package/resources/workflows/epic-multi-slice.md +21 -0
  192. package/resources/workflows/plan-implement-review.md +17 -6
  193. package/resources/workflows/research-verify.md +15 -4
  194. package/resources/workflows/solo-developer.md +11 -2
  195. package/dist/gui/assets/AgentsPage-DbivhmK0.js +0 -3
  196. package/dist/gui/assets/DashboardPage-D50Qrm3e.js +0 -1
  197. package/dist/gui/assets/NullStore-tdPen1kp.js +0 -1
  198. package/dist/gui/assets/ProjectsPage-srKgf7VN.js +0 -1
  199. package/dist/gui/assets/SkillsPage-CKjTXdrd.js +0 -6
  200. package/dist/gui/assets/StartWorkPage-BCUnJ9k8.js +0 -1
  201. package/dist/gui/assets/WorkroomsPage-BcT-mGtT.js +0 -4
  202. package/dist/gui/assets/WorkspacesPage-CYFCm-7_.js +0 -2
  203. package/dist/gui/assets/badge-DwXvB44H.js +0 -1
  204. package/dist/gui/assets/chunk-BV7QT456-DfUBKG-z.js +0 -3
  205. package/dist/gui/assets/index-3tD_IeyQ.js +0 -55
  206. package/dist/gui/assets/index-CK5YRfPV.css +0 -2
  207. package/dist/gui/assets/square-check-big-BSIiYXPI.js +0 -1
  208. package/dist/gui/assets/tabs-BrHKp5bD.js +0 -1
  209. 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,29 @@ 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
- /**
638
- * Retrieves all available skills (built-in templates + user directory + optional workspace directory).
639
- */
640
- export async function getAllSkills(_workspacePath) {
375
+ export async function getAllSkills(workspacePath, diagnostics) {
641
376
  const skillMap = new Map();
642
377
  // 1. Built-in template skills
643
378
  for (const s of DEFAULT_SKILLS) {
644
- skillMap.set(s.id, { ...s });
379
+ skillMap.set(s.id, { ...s, scope: 'global' });
645
380
  }
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
- ];
381
+ // 2. User directory (~/.nexusflow/skills/ and ~/.contextspace/skills/, or isolated custom home)
382
+ const csHome = process.env.CONTEXTSPACE_HOME?.trim();
383
+ const nfHome = process.env.NEXUSFLOW_HOME?.trim();
384
+ const candidateDirs = (csHome || nfHome)
385
+ ? [getUserSkillsDir()]
386
+ : [
387
+ path.join(os.homedir(), LEGACY_CONFIG_DIR_NAME, 'skills'),
388
+ path.join(os.homedir(), PRIMARY_CONFIG_DIR_NAME, 'skills'),
389
+ path.join(os.homedir(), '.agents', 'skills'),
390
+ ];
651
391
  const activeSkillsDir = getUserSkillsDir();
652
392
  if (!candidateDirs.includes(activeSkillsDir)) {
653
393
  candidateDirs.push(activeSkillsDir);
@@ -669,11 +409,13 @@ export async function getAllSkills(_workspacePath) {
669
409
  if (isBuiltIn) {
670
410
  throw new Error(`A resource named "${loaded.id}" already exists in the built-in catalog.`);
671
411
  }
412
+ loaded.scope = loaded.scope || 'global';
672
413
  skillMap.set(loaded.id, loaded);
673
414
  }
674
415
  }
675
416
  catch (error) {
676
417
  const message = error instanceof Error ? error.message : String(error);
418
+ diagnostics?.push({ id: entryName, scope: 'global', message });
677
419
  console.warn(`Skipping invalid skill "${entryName}": ${message}`);
678
420
  }
679
421
  }
@@ -685,12 +427,99 @@ export async function getAllSkills(_workspacePath) {
685
427
  }
686
428
  }
687
429
  }
430
+ // 3. Workspace-local skills (<workspace>/.agents/skills/)
431
+ if (workspacePath) {
432
+ try {
433
+ const canonicalWorkspace = await fs.realpath(workspacePath).catch(() => path.resolve(workspacePath));
434
+ const workspaceSkillsDir = path.join(canonicalWorkspace, '.agents', 'skills');
435
+ if (await fse.pathExists(workspaceSkillsDir)) {
436
+ await assertPathIsNotLink(workspaceSkillsDir);
437
+ const materializedGlobalSkillIds = new Set();
438
+ const lockPathPrimary = path.join(canonicalWorkspace, PRIMARY_CONFIG_DIR_NAME, 'resources.lock.json');
439
+ const lockPathLegacy = path.join(canonicalWorkspace, LEGACY_CONFIG_DIR_NAME, 'resources.lock.json');
440
+ const lockPath = (await fse.pathExists(lockPathPrimary))
441
+ ? lockPathPrimary
442
+ : (await fse.pathExists(lockPathLegacy))
443
+ ? lockPathLegacy
444
+ : null;
445
+ if (lockPath) {
446
+ try {
447
+ const lockJson = await fse.readJson(lockPath);
448
+ const outputs = Array.isArray(lockJson?.outputs)
449
+ ? lockJson.outputs
450
+ : Array.isArray(lockJson?.managedFiles)
451
+ ? lockJson.managedFiles
452
+ : [];
453
+ for (const out of outputs) {
454
+ if (out && out.kind === 'skill') {
455
+ const outPath = typeof out.path === 'string' ? out.path.replaceAll('\\', '/') : '';
456
+ if (out.adapter === 'agent-skill-v1' || outPath.startsWith('.agents/skills/')) {
457
+ if (typeof out.resourceId === 'string') {
458
+ materializedGlobalSkillIds.add(out.resourceId);
459
+ }
460
+ }
461
+ }
462
+ }
463
+ }
464
+ catch {
465
+ // Ignore unreadable or corrupt lock file
466
+ }
467
+ }
468
+ const entries = await fs.readdir(workspaceSkillsDir, { withFileTypes: true });
469
+ if (Array.isArray(entries)) {
470
+ for (const entry of entries) {
471
+ const isDir = typeof entry === 'string' ? true : entry.isDirectory ? entry.isDirectory() : true;
472
+ const entryName = typeof entry === 'string' ? entry : entry.name;
473
+ if (isDir) {
474
+ try {
475
+ const skillDir = path.join(workspaceSkillsDir, entryName);
476
+ const loaded = await loadSkillFromDir(skillDir, true, workspaceSkillsDir);
477
+ if (loaded) {
478
+ const isMaterializedGlobal = loaded.scope === 'global' ||
479
+ (loaded.scope !== 'workspace' && materializedGlobalSkillIds.has(loaded.id));
480
+ if (isMaterializedGlobal) {
481
+ // Materialized global skill: do NOT mark its scope as 'workspace'.
482
+ // If its global counterpart was deleted machine-wide, do NOT resurrect it as an active workspace skill.
483
+ if (skillMap.has(loaded.id)) {
484
+ const globalSkill = skillMap.get(loaded.id);
485
+ globalSkill.scope = 'global';
486
+ }
487
+ else {
488
+ diagnostics?.push({ id: loaded.id, scope: 'workspace', message: 'This copy declares global scope or was generated from a global skill, but that catalog source is missing. Restore the source or explicitly convert the package to workspace scope.' });
489
+ }
490
+ }
491
+ else {
492
+ if (skillMap.has(loaded.id))
493
+ diagnostics?.push({ id: loaded.id, scope: 'workspace', message: 'The workspace package overrides a global skill with the same ID. Rename it to keep both independently selectable.' });
494
+ loaded.scope = 'workspace';
495
+ skillMap.set(loaded.id, loaded);
496
+ }
497
+ }
498
+ }
499
+ catch (error) {
500
+ const message = error instanceof Error ? error.message : String(error);
501
+ diagnostics?.push({ id: entryName, scope: 'workspace', message });
502
+ console.warn(`Skipping invalid workspace skill "${entryName}": ${message}`);
503
+ }
504
+ }
505
+ }
506
+ }
507
+ }
508
+ }
509
+ catch (err) {
510
+ console.error('Failed to load workspace skills from', workspacePath, err);
511
+ }
512
+ }
688
513
  return Array.from(skillMap.values());
689
514
  }
690
515
  /**
691
516
  * Saves or updates a portable skill package.
692
517
  */
693
518
  export async function saveSkill(skill, options = {}) {
519
+ const scope = options.scope ?? 'global';
520
+ if (scope === 'workspace' && !options.workspacePath) {
521
+ throw new Error('workspacePath is required when saving a workspace-scoped skill.');
522
+ }
694
523
  const rawId = skill.id || skill.name;
695
524
  const idResult = resourceIdSchema.safeParse(rawId);
696
525
  if (!idResult.success) {
@@ -711,20 +540,19 @@ export async function saveSkill(skill, options = {}) {
711
540
  throw new Error('Skill description is required.');
712
541
  if (!skill.content.trim())
713
542
  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()}`);
543
+ const executeSave = async (skillsBaseDir) => {
544
+ await fse.ensureDir(skillsBaseDir);
545
+ await assertPathIsNotLink(skillsBaseDir);
546
+ const targetDir = assertPathWithin(skillsBaseDir, path.join(skillsBaseDir, id));
547
+ await assertNoLinkedPathComponents(skillsBaseDir, targetDir);
548
+ const stagingDir = await fs.mkdtemp(path.join(skillsBaseDir, `.staging-${id}-`));
549
+ const backupDir = path.join(skillsBaseDir, `.backup-${id}-${randomUUID()}`);
722
550
  let movedExisting = false;
723
551
  let installedStaging = false;
724
552
  try {
725
553
  let existingFrontmatter;
726
554
  if (await fse.pathExists(targetDir)) {
727
- await assertNoLinkedPathComponents(userSkillsDir, targetDir);
555
+ await assertNoLinkedPathComponents(skillsBaseDir, targetDir);
728
556
  for (const entry of await fs.readdir(targetDir, { withFileTypes: true })) {
729
557
  if (entry.isSymbolicLink()) {
730
558
  throw new Error(`Linked skill package entries are not allowed: ${entry.name}`);
@@ -742,6 +570,9 @@ export async function saveSkill(skill, options = {}) {
742
570
  existingFrontmatter = parsedCurrentFrontmatter.data;
743
571
  }
744
572
  const existingMetaObj = existingFrontmatter?.metadata;
573
+ const callerMetadata = skill.metadata && typeof skill.metadata === 'object' && !Array.isArray(skill.metadata)
574
+ ? skill.metadata
575
+ : undefined;
745
576
  const existingBrandMetadata = existingMetaObj &&
746
577
  typeof existingMetaObj[RESOURCE_METADATA_KEY] === 'object' &&
747
578
  existingMetaObj[RESOURCE_METADATA_KEY] !== null
@@ -751,19 +582,59 @@ export async function saveSkill(skill, options = {}) {
751
582
  existingMetaObj[LEGACY_RESOURCE_METADATA_KEY] !== null
752
583
  ? existingMetaObj[LEGACY_RESOURCE_METADATA_KEY]
753
584
  : {};
585
+ const callerBrandMetadata = callerMetadata &&
586
+ typeof callerMetadata[RESOURCE_METADATA_KEY] === 'object' &&
587
+ callerMetadata[RESOURCE_METADATA_KEY] !== null
588
+ ? callerMetadata[RESOURCE_METADATA_KEY]
589
+ : callerMetadata &&
590
+ typeof callerMetadata[LEGACY_RESOURCE_METADATA_KEY] === 'object' &&
591
+ callerMetadata[LEGACY_RESOURCE_METADATA_KEY] !== null
592
+ ? callerMetadata[LEGACY_RESOURCE_METADATA_KEY]
593
+ : {};
594
+ const inferredTitle = skill.title ||
595
+ (callerMetadata && typeof callerMetadata.title === 'string' ? callerMetadata.title : undefined) ||
596
+ (typeof existingBrandMetadata.title === 'string' ? existingBrandMetadata.title : undefined) ||
597
+ id;
598
+ const inferredCategory = options.category ||
599
+ skill.category ||
600
+ (callerMetadata && typeof callerMetadata.category === 'string' ? callerMetadata.category : undefined) ||
601
+ (typeof existingBrandMetadata.category === 'string' ? existingBrandMetadata.category : undefined) ||
602
+ 'general';
603
+ const callerMetaTags = callerMetadata && Array.isArray(callerMetadata.tags)
604
+ ? callerMetadata.tags.filter((t) => typeof t === 'string')
605
+ : undefined;
606
+ const existingBrandTags = Array.isArray(existingBrandMetadata.tags)
607
+ ? existingBrandMetadata.tags.filter((t) => typeof t === 'string')
608
+ : undefined;
609
+ const inferredTags = skill.tags || callerMetaTags || existingBrandTags || [];
754
610
  const metadataPayload = {
755
611
  ...existingBrandMetadata,
756
- title: skill.title || id,
757
- category: skill.category || 'general',
758
- tags: skill.tags || [],
612
+ ...callerBrandMetadata,
613
+ title: inferredTitle,
614
+ category: inferredCategory,
615
+ tags: inferredTags,
616
+ scope,
759
617
  };
618
+ const mergedCustomMetadata = {
619
+ ...(existingFrontmatter?.metadata ?? {}),
620
+ ...(callerMetadata ?? {}),
621
+ };
622
+ delete mergedCustomMetadata[RESOURCE_METADATA_KEY];
623
+ delete mergedCustomMetadata[LEGACY_RESOURCE_METADATA_KEY];
624
+ delete mergedCustomMetadata['__proto__'];
625
+ delete mergedCustomMetadata['constructor'];
626
+ delete mergedCustomMetadata['prototype'];
760
627
  const metadata = {
761
628
  name: id,
629
+ title: inferredTitle,
630
+ category: inferredCategory,
762
631
  description,
763
- license: existingFrontmatter?.license,
764
- compatibility: existingFrontmatter?.compatibility,
632
+ tags: inferredTags,
633
+ license: skill.license ?? existingFrontmatter?.license,
634
+ compatibility: skill.compatibility ?? existingFrontmatter?.compatibility,
635
+ scope,
765
636
  metadata: {
766
- ...(existingFrontmatter?.metadata ?? {}),
637
+ ...mergedCustomMetadata,
767
638
  [RESOURCE_METADATA_KEY]: metadataPayload,
768
639
  [LEGACY_RESOURCE_METADATA_KEY]: metadataPayload,
769
640
  },
@@ -777,6 +648,7 @@ export async function saveSkill(skill, options = {}) {
777
648
  for (const [directory, files] of [
778
649
  ['references', skill.references],
779
650
  ['scripts', skill.scripts],
651
+ ['assets', skill.assets],
780
652
  ]) {
781
653
  if (files === undefined)
782
654
  continue;
@@ -806,18 +678,24 @@ export async function saveSkill(skill, options = {}) {
806
678
  }
807
679
  await options.beforeCommit?.();
808
680
  if (await fse.pathExists(targetDir)) {
809
- await assertNoLinkedPathComponents(userSkillsDir, targetDir);
681
+ await assertNoLinkedPathComponents(skillsBaseDir, targetDir);
810
682
  await fs.rename(targetDir, backupDir);
811
683
  movedExisting = true;
812
684
  }
813
685
  await fs.rename(stagingDir, targetDir);
814
686
  installedStaging = true;
815
- const loaded = await loadSkillFromDir(targetDir, true, userSkillsDir);
687
+ const loaded = await loadSkillFromDir(targetDir, true, skillsBaseDir);
816
688
  if (!loaded)
817
689
  throw new Error('Saved skill could not be loaded.');
690
+ loaded.scope = scope;
818
691
  if (movedExisting)
819
692
  await fse.remove(backupDir).catch(() => { });
820
- return loaded;
693
+ const skillFilePath = path.join(targetDir, 'SKILL.md');
694
+ return {
695
+ ...loaded,
696
+ path: skillFilePath,
697
+ skill: loaded,
698
+ };
821
699
  }
822
700
  catch (error) {
823
701
  await fse.remove(stagingDir).catch(() => { });
@@ -828,16 +706,42 @@ export async function saveSkill(skill, options = {}) {
828
706
  }
829
707
  throw error;
830
708
  }
831
- });
709
+ };
710
+ if (scope === 'workspace') {
711
+ const canonicalWorkspace = await fs.realpath(options.workspacePath).catch(() => path.resolve(options.workspacePath));
712
+ const workspaceSkillsDir = path.join(canonicalWorkspace, '.agents', 'skills');
713
+ return runWorkspaceConfigMutation(async () => {
714
+ await assertNoLinkedPathComponents(canonicalWorkspace, workspaceSkillsDir);
715
+ return executeSave(workspaceSkillsDir);
716
+ });
717
+ }
718
+ return withCatalogLock(async () => executeSave(path.resolve(getUserSkillsDir())));
832
719
  }
833
720
  /**
834
- * Deletes a user custom skill safely.
721
+ * Deletes a skill safely from the global or workspace catalog.
835
722
  */
836
- export async function deleteSkill(id) {
723
+ export async function deleteSkill(id, options) {
837
724
  const idResult = resourceIdSchema.safeParse(id);
838
725
  if (!idResult.success || idResult.data !== id) {
839
726
  throw new Error('Invalid skill ID format.');
840
727
  }
728
+ const scope = options?.scope ?? 'global';
729
+ if (scope === 'workspace') {
730
+ if (!options?.workspacePath) {
731
+ throw new Error('workspacePath is required when deleting a workspace-scoped skill.');
732
+ }
733
+ const wsPath = options.workspacePath;
734
+ const canonicalWorkspace = await fs.realpath(wsPath).catch(() => path.resolve(wsPath));
735
+ const workspaceSkillsDir = path.join(canonicalWorkspace, '.agents', 'skills');
736
+ await runWorkspaceConfigMutation(async () => {
737
+ const targetDir = assertPathWithin(canonicalWorkspace, path.join(workspaceSkillsDir, id));
738
+ await assertNoLinkedPathComponents(canonicalWorkspace, targetDir);
739
+ if (!(await fse.pathExists(targetDir)))
740
+ throw new Error('Skill not found.');
741
+ await fse.remove(targetDir);
742
+ });
743
+ return;
744
+ }
841
745
  await withCatalogLock(async () => {
842
746
  const userSkillsDir = path.resolve(getUserSkillsDir());
843
747
  await fse.ensureDir(userSkillsDir);