@selesai/code 0.13.0 → 0.13.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 (461) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/core/agent-session.d.ts +1 -1
  3. package/dist/core/agent-session.js +1 -1
  4. package/dist/core/sdk.d.ts +2 -2
  5. package/dist/core/sdk.js +1 -1
  6. package/dist/core/system-prompt.d.ts +1 -1
  7. package/dist/core/system-prompt.js +1 -1
  8. package/dist/extensions/ascii.txt +9 -0
  9. package/dist/extensions/package.json +1 -2
  10. package/dist/extensions/pi-hermes-memory/CHANGELOG.md +371 -0
  11. package/dist/extensions/pi-hermes-memory/LICENSE +21 -0
  12. package/dist/extensions/pi-hermes-memory/README.md +643 -0
  13. package/dist/extensions/pi-hermes-memory/docs/0.1/TASKS.md +197 -0
  14. package/dist/extensions/pi-hermes-memory/docs/0.2/PLAN.md +290 -0
  15. package/dist/extensions/pi-hermes-memory/docs/0.2/TASKS.md +134 -0
  16. package/dist/extensions/pi-hermes-memory/docs/0.2/TEST-PLAN.md +216 -0
  17. package/dist/extensions/pi-hermes-memory/docs/0.3/PLAN.md +330 -0
  18. package/dist/extensions/pi-hermes-memory/docs/0.3/TASKS.md +125 -0
  19. package/dist/extensions/pi-hermes-memory/docs/0.4/PLAN.md +160 -0
  20. package/dist/extensions/pi-hermes-memory/docs/0.4/TASKS.md +113 -0
  21. package/dist/extensions/pi-hermes-memory/docs/0.6/CHANGELOG.md +57 -0
  22. package/dist/extensions/pi-hermes-memory/docs/0.6/PLAN.md +202 -0
  23. package/dist/extensions/pi-hermes-memory/docs/0.6/TASKS.md +144 -0
  24. package/dist/extensions/pi-hermes-memory/docs/0.7/PLAN.md +349 -0
  25. package/dist/extensions/pi-hermes-memory/docs/0.7/TASKS.md +110 -0
  26. package/dist/extensions/pi-hermes-memory/docs/PUBLISHING.md +149 -0
  27. package/dist/extensions/pi-hermes-memory/docs/ROADMAP.md +468 -0
  28. package/dist/extensions/pi-hermes-memory/docs/images/memory-architecture.svg +1 -0
  29. package/dist/extensions/pi-hermes-memory/docs/images/pi-logo.svg +22 -0
  30. package/dist/extensions/pi-hermes-memory/docs/images/pi_memory.png +0 -0
  31. package/dist/extensions/pi-hermes-memory/docs/images/pi_memory.svg +19 -0
  32. package/dist/extensions/pi-hermes-memory/docs/images/security-flow.svg +1 -0
  33. package/dist/extensions/pi-hermes-memory/docs/images/session-lifecycle.svg +1 -0
  34. package/dist/extensions/pi-hermes-memory/docs/images/source-architecture.svg +1 -0
  35. package/dist/extensions/pi-hermes-memory/docs/mermaid/memory-architecture.mmd +49 -0
  36. package/dist/extensions/pi-hermes-memory/docs/mermaid/security-flow.mmd +40 -0
  37. package/dist/extensions/pi-hermes-memory/docs/mermaid/session-lifecycle.mmd +56 -0
  38. package/dist/extensions/pi-hermes-memory/docs/mermaid/source-architecture.mmd +54 -0
  39. package/dist/extensions/pi-hermes-memory/package-lock.json +3799 -0
  40. package/dist/extensions/pi-hermes-memory/package.json +66 -0
  41. package/dist/extensions/pi-hermes-memory/scripts/check-min-sdk.mjs +99 -0
  42. package/dist/extensions/pi-hermes-memory/scripts/ensure-dev.mjs +57 -0
  43. package/dist/extensions/pi-hermes-memory/src/auto-consolidation-warning.ts +6 -0
  44. package/dist/extensions/pi-hermes-memory/src/config.ts +178 -0
  45. package/dist/extensions/pi-hermes-memory/src/constants.ts +424 -0
  46. package/dist/extensions/pi-hermes-memory/src/extension-root-migration.ts +639 -0
  47. package/dist/extensions/pi-hermes-memory/src/handlers/auto-consolidate.ts +401 -0
  48. package/dist/extensions/pi-hermes-memory/src/handlers/background-review.ts +297 -0
  49. package/dist/extensions/pi-hermes-memory/src/handlers/child-process-watchdog.mjs +90 -0
  50. package/dist/extensions/pi-hermes-memory/src/handlers/correction-detector.ts +300 -0
  51. package/dist/extensions/pi-hermes-memory/src/handlers/index-sessions.ts +86 -0
  52. package/dist/extensions/pi-hermes-memory/src/handlers/insights.ts +78 -0
  53. package/dist/extensions/pi-hermes-memory/src/handlers/interview.ts +37 -0
  54. package/dist/extensions/pi-hermes-memory/src/handlers/learn-memory.ts +186 -0
  55. package/dist/extensions/pi-hermes-memory/src/handlers/message-parts.ts +27 -0
  56. package/dist/extensions/pi-hermes-memory/src/handlers/pi-child-process.ts +485 -0
  57. package/dist/extensions/pi-hermes-memory/src/handlers/preview-context.ts +105 -0
  58. package/dist/extensions/pi-hermes-memory/src/handlers/review-memory-ops.ts +506 -0
  59. package/dist/extensions/pi-hermes-memory/src/handlers/session-backfill.ts +145 -0
  60. package/dist/extensions/pi-hermes-memory/src/handlers/session-flush.ts +151 -0
  61. package/dist/extensions/pi-hermes-memory/src/handlers/session-live-index.ts +94 -0
  62. package/dist/extensions/pi-hermes-memory/src/handlers/skills-command.ts +1334 -0
  63. package/dist/extensions/pi-hermes-memory/src/handlers/standing-pin.ts +111 -0
  64. package/dist/extensions/pi-hermes-memory/src/handlers/switch-project.ts +75 -0
  65. package/dist/extensions/pi-hermes-memory/src/handlers/sync-markdown-memories.ts +266 -0
  66. package/dist/extensions/pi-hermes-memory/src/index.ts +378 -0
  67. package/dist/extensions/pi-hermes-memory/src/lifecycle-timing.ts +54 -0
  68. package/dist/extensions/pi-hermes-memory/src/paths.ts +61 -0
  69. package/dist/extensions/pi-hermes-memory/src/project-context.ts +13 -0
  70. package/dist/extensions/pi-hermes-memory/src/project-memory-migration.ts +93 -0
  71. package/dist/extensions/pi-hermes-memory/src/project.ts +157 -0
  72. package/dist/extensions/pi-hermes-memory/src/prompt-context.ts +55 -0
  73. package/dist/extensions/pi-hermes-memory/src/store/atomic-lock-coordinator.ts +375 -0
  74. package/dist/extensions/pi-hermes-memory/src/store/canonical-storage-path.ts +54 -0
  75. package/dist/extensions/pi-hermes-memory/src/store/content-scanner.ts +101 -0
  76. package/dist/extensions/pi-hermes-memory/src/store/db.ts +1180 -0
  77. package/dist/extensions/pi-hermes-memory/src/store/fts-query.ts +101 -0
  78. package/dist/extensions/pi-hermes-memory/src/store/markdown-mutation-lock.ts +38 -0
  79. package/dist/extensions/pi-hermes-memory/src/store/memory-lookup.ts +24 -0
  80. package/dist/extensions/pi-hermes-memory/src/store/memory-store.ts +1191 -0
  81. package/dist/extensions/pi-hermes-memory/src/store/recovery-maintenance.ts +66 -0
  82. package/dist/extensions/pi-hermes-memory/src/store/schema.ts +118 -0
  83. package/dist/extensions/pi-hermes-memory/src/store/session-anchor-search.ts +472 -0
  84. package/dist/extensions/pi-hermes-memory/src/store/session-indexer.ts +511 -0
  85. package/dist/extensions/pi-hermes-memory/src/store/session-parser.ts +213 -0
  86. package/dist/extensions/pi-hermes-memory/src/store/session-search.ts +234 -0
  87. package/dist/extensions/pi-hermes-memory/src/store/skill-store.ts +950 -0
  88. package/dist/extensions/pi-hermes-memory/src/store/skill-utils.ts +133 -0
  89. package/dist/extensions/pi-hermes-memory/src/store/sqlite-memory-store.ts +1002 -0
  90. package/dist/extensions/pi-hermes-memory/src/store/sqlite-native.ts +241 -0
  91. package/dist/extensions/pi-hermes-memory/src/store/standing-instructions.ts +247 -0
  92. package/dist/extensions/pi-hermes-memory/src/tools/memory-search-tool.ts +95 -0
  93. package/dist/extensions/pi-hermes-memory/src/tools/memory-tool.ts +469 -0
  94. package/dist/extensions/pi-hermes-memory/src/tools/session-search-tool.ts +246 -0
  95. package/dist/extensions/pi-hermes-memory/src/tools/shared-output-view.ts +170 -0
  96. package/dist/extensions/pi-hermes-memory/src/tools/skill-tool.ts +333 -0
  97. package/dist/extensions/pi-hermes-memory/src/tools/tool-result-views.ts +105 -0
  98. package/dist/extensions/pi-hermes-memory/src/types.ts +214 -0
  99. package/dist/extensions/pi-hermes-memory/tests/config.test.ts +434 -0
  100. package/dist/extensions/pi-hermes-memory/tests/extension-root-migration.test.ts +368 -0
  101. package/dist/extensions/pi-hermes-memory/tests/handlers/auto-consolidate.test.ts +984 -0
  102. package/dist/extensions/pi-hermes-memory/tests/handlers/background-review.test.ts +950 -0
  103. package/dist/extensions/pi-hermes-memory/tests/handlers/correction-detector.test.ts +671 -0
  104. package/dist/extensions/pi-hermes-memory/tests/handlers/insights.test.ts +157 -0
  105. package/dist/extensions/pi-hermes-memory/tests/handlers/interview.test.ts +127 -0
  106. package/dist/extensions/pi-hermes-memory/tests/handlers/pi-child-process.test.ts +905 -0
  107. package/dist/extensions/pi-hermes-memory/tests/handlers/preview-context.test.ts +152 -0
  108. package/dist/extensions/pi-hermes-memory/tests/handlers/prompt-context.test.ts +166 -0
  109. package/dist/extensions/pi-hermes-memory/tests/handlers/resources-discover.test.ts +71 -0
  110. package/dist/extensions/pi-hermes-memory/tests/handlers/review-memory-ops.test.ts +614 -0
  111. package/dist/extensions/pi-hermes-memory/tests/handlers/session-backfill.test.ts +192 -0
  112. package/dist/extensions/pi-hermes-memory/tests/handlers/session-flush.test.ts +677 -0
  113. package/dist/extensions/pi-hermes-memory/tests/handlers/session-live-index.test.ts +225 -0
  114. package/dist/extensions/pi-hermes-memory/tests/handlers/skills-command.test.ts +827 -0
  115. package/dist/extensions/pi-hermes-memory/tests/handlers/standing-pin.test.ts +134 -0
  116. package/dist/extensions/pi-hermes-memory/tests/handlers/sync-markdown-memories.test.ts +491 -0
  117. package/dist/extensions/pi-hermes-memory/tests/handlers/system-prompt.test.ts +199 -0
  118. package/dist/extensions/pi-hermes-memory/tests/handlers/thinking-max.test.ts +64 -0
  119. package/dist/extensions/pi-hermes-memory/tests/index.test.ts +30 -0
  120. package/dist/extensions/pi-hermes-memory/tests/integration/flow.test.ts +126 -0
  121. package/dist/extensions/pi-hermes-memory/tests/lifecycle-timing.test.ts +76 -0
  122. package/dist/extensions/pi-hermes-memory/tests/paths.test.ts +23 -0
  123. package/dist/extensions/pi-hermes-memory/tests/project-memory-migration.test.ts +80 -0
  124. package/dist/extensions/pi-hermes-memory/tests/project-rebinding.test.ts +115 -0
  125. package/dist/extensions/pi-hermes-memory/tests/project.test.ts +120 -0
  126. package/dist/extensions/pi-hermes-memory/tests/run-all-timeout.test.ts +107 -0
  127. package/dist/extensions/pi-hermes-memory/tests/run-all.sh +45 -0
  128. package/dist/extensions/pi-hermes-memory/tests/store/atomic-lock-coordinator.test.ts +381 -0
  129. package/dist/extensions/pi-hermes-memory/tests/store/content-scanner.test.ts +388 -0
  130. package/dist/extensions/pi-hermes-memory/tests/store/db.test.ts +1046 -0
  131. package/dist/extensions/pi-hermes-memory/tests/store/markdown-mutation-lock.test.ts +34 -0
  132. package/dist/extensions/pi-hermes-memory/tests/store/memory-store.test.ts +2150 -0
  133. package/dist/extensions/pi-hermes-memory/tests/store/recovery-maintenance.test.ts +170 -0
  134. package/dist/extensions/pi-hermes-memory/tests/store/session-anchor-search.test.ts +210 -0
  135. package/dist/extensions/pi-hermes-memory/tests/store/session-indexer.test.ts +643 -0
  136. package/dist/extensions/pi-hermes-memory/tests/store/session-parser.test.ts +282 -0
  137. package/dist/extensions/pi-hermes-memory/tests/store/session-search.test.ts +287 -0
  138. package/dist/extensions/pi-hermes-memory/tests/store/skill-store.test.ts +678 -0
  139. package/dist/extensions/pi-hermes-memory/tests/store/skill-utils.test.ts +21 -0
  140. package/dist/extensions/pi-hermes-memory/tests/store/sqlite-lazy-load.test.ts +78 -0
  141. package/dist/extensions/pi-hermes-memory/tests/store/sqlite-memory-store.test.ts +543 -0
  142. package/dist/extensions/pi-hermes-memory/tests/store/sqlite-native.test.ts +164 -0
  143. package/dist/extensions/pi-hermes-memory/tests/store/standing-instructions.test.ts +171 -0
  144. package/dist/extensions/pi-hermes-memory/tests/tools/memory-search-tool.test.ts +84 -0
  145. package/dist/extensions/pi-hermes-memory/tests/tools/memory-tool.test.ts +709 -0
  146. package/dist/extensions/pi-hermes-memory/tests/tools/session-search-tool.test.ts +345 -0
  147. package/dist/extensions/pi-hermes-memory/tests/tools/shared-output-view.test.ts +362 -0
  148. package/dist/extensions/pi-hermes-memory/tests/tools/skill-tool.test.ts +478 -0
  149. package/dist/extensions/pi-hermes-memory/tests/tools/tool-result-renderer-wiring.test.ts +40 -0
  150. package/dist/extensions/pi-hermes-memory/tsconfig.json +19 -0
  151. package/dist/extensions/pi-subagents/.oxlintrc.json +43 -0
  152. package/dist/extensions/pi-subagents/AGENTS.md +5 -0
  153. package/dist/extensions/pi-subagents/CHANGELOG.md +157 -0
  154. package/dist/extensions/pi-subagents/README.md +12 -6
  155. package/dist/extensions/pi-subagents/VISION.md +81 -0
  156. package/dist/extensions/pi-subagents/agents/claude-code-writer.md +15 -0
  157. package/dist/extensions/pi-subagents/agents/claude-code.md +15 -0
  158. package/dist/extensions/pi-subagents/agents/codex-exec-writer.md +15 -0
  159. package/dist/extensions/pi-subagents/agents/codex-exec.md +15 -0
  160. package/dist/extensions/pi-subagents/agents/cursor-agent-writer.md +14 -0
  161. package/dist/extensions/pi-subagents/agents/cursor-agent.md +14 -0
  162. package/dist/extensions/pi-subagents/agents/reviewer.md +1 -3
  163. package/dist/extensions/pi-subagents/docs/agents.md +488 -0
  164. package/dist/extensions/pi-subagents/docs/configuration.md +46 -9
  165. package/dist/extensions/pi-subagents/docs/extension-api.md +42 -3
  166. package/dist/extensions/pi-subagents/docs/models.md +27 -4
  167. package/dist/extensions/pi-subagents/docs/observability.md +10 -9
  168. package/dist/extensions/pi-subagents/docs/tool-reference.md +102 -6
  169. package/dist/extensions/pi-subagents/docs/workflows.md +135 -0
  170. package/dist/extensions/pi-subagents/package-lock.json +389 -2
  171. package/dist/extensions/pi-subagents/package.json +3 -1
  172. package/dist/extensions/pi-subagents/prompts/review-loop.md +2 -2
  173. package/dist/extensions/pi-subagents/skills/council-mode/SKILL.md +1 -1
  174. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +3 -1
  175. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +7 -3
  176. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  177. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +26 -5
  178. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +41 -22
  179. package/dist/extensions/pi-subagents/src/agents/agent-refinements.ts +4 -4
  180. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +9 -2
  181. package/dist/extensions/pi-subagents/src/agents/agents.ts +322 -75
  182. package/dist/extensions/pi-subagents/src/agents/builtin-names.ts +6 -0
  183. package/dist/extensions/pi-subagents/src/agents/runtime-agent-events.ts +70 -0
  184. package/dist/extensions/pi-subagents/src/agents/runtime-agent-registry.ts +27 -20
  185. package/dist/extensions/pi-subagents/src/api/agents.ts +10 -5
  186. package/dist/extensions/pi-subagents/src/api/background-work.ts +5 -1
  187. package/dist/extensions/pi-subagents/src/api/delegation.ts +0 -7
  188. package/dist/extensions/pi-subagents/src/api/preflight.ts +34 -12
  189. package/dist/extensions/pi-subagents/src/extension/config.ts +70 -0
  190. package/dist/extensions/pi-subagents/src/extension/doctor.ts +3 -3
  191. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +5 -3
  192. package/dist/extensions/pi-subagents/src/extension/index.ts +83 -30
  193. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +44 -15
  194. package/dist/extensions/pi-subagents/src/extension/rpc.ts +55 -19
  195. package/dist/extensions/pi-subagents/src/extension/schemas.ts +41 -15
  196. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +28 -31
  197. package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +2 -1
  198. package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +2 -10
  199. package/dist/extensions/pi-subagents/src/inspectors/herdr/session-roots-codec.ts +42 -0
  200. package/dist/extensions/pi-subagents/src/integrations/herdr-status.ts +51 -3
  201. package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +2 -0
  202. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +5 -6
  203. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +77 -10
  204. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +176 -53
  205. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +19 -12
  206. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +29 -27
  207. package/dist/extensions/pi-subagents/src/runs/background/async-retention.ts +20 -3
  208. package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +23 -261
  209. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +73 -7
  210. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +8 -3
  211. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +74 -8
  212. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +37 -21
  213. package/dist/extensions/pi-subagents/src/runs/background/inspect-rpc.ts +8 -8
  214. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +184 -9
  215. package/dist/extensions/pi-subagents/src/runs/background/result-delivery-ownership.ts +45 -0
  216. package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +28 -14
  217. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +38 -15
  218. package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +1 -1
  219. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +1 -1
  220. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +50 -13
  221. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +93 -8
  222. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +10 -4
  223. package/dist/extensions/pi-subagents/src/runs/background/steering.ts +4 -14
  224. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +549 -377
  225. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +58 -10
  226. package/dist/extensions/pi-subagents/src/runs/background/terminal-run-index.ts +1 -1
  227. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +26 -1
  228. package/dist/extensions/pi-subagents/src/runs/background/wait-config.ts +23 -9
  229. package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +9 -2
  230. package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +2 -2
  231. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +204 -129
  232. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +9 -0
  233. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +23 -1
  234. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +773 -311
  235. package/dist/extensions/pi-subagents/src/runs/foreground/workflow-detach-reconcile.ts +140 -115
  236. package/dist/extensions/pi-subagents/src/runs/shared/abort-recovery.ts +119 -0
  237. package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +463 -0
  238. package/dist/extensions/pi-subagents/src/runs/shared/child-identity.ts +19 -4
  239. package/dist/extensions/pi-subagents/src/runs/shared/child-launch-plan.ts +151 -0
  240. package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +21 -7
  241. package/dist/extensions/pi-subagents/src/runs/shared/claude-code-adapter.ts +129 -0
  242. package/dist/extensions/pi-subagents/src/runs/shared/codex-exec-adapter.ts +129 -0
  243. package/dist/extensions/pi-subagents/src/runs/shared/completion-evidence.ts +89 -0
  244. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +5 -4
  245. package/dist/extensions/pi-subagents/src/runs/shared/cursor-agent-adapter.ts +114 -0
  246. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +3 -3
  247. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-contract.ts +167 -0
  248. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-preflight.ts +122 -0
  249. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +348 -55
  250. package/dist/extensions/pi-subagents/src/runs/shared/fast-mode-extension.ts +5 -5
  251. package/dist/extensions/pi-subagents/src/runs/shared/host-step-status.ts +230 -0
  252. package/dist/extensions/pi-subagents/src/runs/shared/lane-metadata.ts +105 -0
  253. package/dist/extensions/pi-subagents/src/runs/shared/launch-cwd.ts +16 -0
  254. package/dist/extensions/pi-subagents/src/runs/shared/long-running-guard.ts +2 -1
  255. package/dist/extensions/pi-subagents/src/runs/shared/mcp-config-sources.ts +422 -0
  256. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +221 -158
  257. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-grant.ts +197 -0
  258. package/dist/extensions/pi-subagents/src/runs/shared/model-exclusions.ts +69 -7
  259. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +47 -6
  260. package/dist/extensions/pi-subagents/src/runs/shared/mutation-evidence.ts +7 -2
  261. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +9 -3
  262. package/dist/extensions/pi-subagents/src/runs/shared/nested-render.ts +9 -5
  263. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +419 -7
  264. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +15 -1
  265. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +81 -8
  266. package/dist/extensions/pi-subagents/src/runs/shared/process-signal.ts +13 -0
  267. package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +21 -1
  268. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +44 -4
  269. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +98 -13
  270. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +1 -1
  271. package/dist/extensions/pi-subagents/src/runs/shared/worktree-cleanup-plan.ts +847 -0
  272. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +18 -0
  273. package/dist/extensions/pi-subagents/src/shared/child-session-name.ts +46 -0
  274. package/dist/extensions/pi-subagents/src/shared/extension-context.ts +24 -0
  275. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +21 -0
  276. package/dist/extensions/pi-subagents/src/shared/formatters.ts +18 -3
  277. package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +5 -1
  278. package/dist/extensions/pi-subagents/src/shared/pruned-fork.ts +450 -0
  279. package/dist/extensions/pi-subagents/src/shared/session-file-trust.ts +19 -0
  280. package/dist/extensions/pi-subagents/src/shared/session-tokens.ts +14 -3
  281. package/dist/extensions/pi-subagents/src/shared/settings.ts +19 -105
  282. package/dist/extensions/pi-subagents/src/shared/shortcuts.ts +17 -0
  283. package/dist/extensions/pi-subagents/src/shared/types.ts +359 -40
  284. package/dist/extensions/pi-subagents/src/shared/utils.ts +41 -84
  285. package/dist/extensions/pi-subagents/src/shared/workflow-child-permit.ts +116 -0
  286. package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +1 -9
  287. package/dist/extensions/pi-subagents/src/slash/delegation-request.ts +0 -4
  288. package/dist/extensions/pi-subagents/src/slash/slash-bridge.ts +1 -2
  289. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +378 -88
  290. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +22 -11
  291. package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +3 -0
  292. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +147 -79
  293. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +11 -5
  294. package/dist/extensions/pi-subagents/src/tui/fleet.ts +39 -18
  295. package/dist/extensions/pi-subagents/src/tui/render.ts +369 -46
  296. package/dist/extensions/pi-subagents/src/watchdog/turn-delta.ts +1 -1
  297. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +6 -3
  298. package/dist/extensions/pi-subagents/src/workflows/host-command.ts +230 -0
  299. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +740 -58
  300. package/dist/extensions/pi-subagents/src/workflows/workflow-child-summary.ts +121 -0
  301. package/dist/extensions/pi-subagents/src/workflows/workflow-preflight.ts +270 -0
  302. package/dist/extensions/pi-subagents/src/workflows/workflow-receipt.ts +195 -6
  303. package/dist/extensions/pi-subagents/src/workflows/workflow-settlement.ts +246 -0
  304. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +2 -2
  305. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1114 -260
  306. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +34 -0
  307. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +150 -3
  308. package/dist/extensions/pi-subagents/test/integration/claude-code-smoke.test.ts +52 -0
  309. package/dist/extensions/pi-subagents/test/integration/claude-code-writer-smoke.test.ts +55 -0
  310. package/dist/extensions/pi-subagents/test/integration/codex-exec-smoke.test.ts +53 -0
  311. package/dist/extensions/pi-subagents/test/integration/codex-exec-writer-smoke.test.ts +57 -0
  312. package/dist/extensions/pi-subagents/test/integration/cursor-agent-smoke.test.ts +59 -0
  313. package/dist/extensions/pi-subagents/test/integration/cursor-agent-writer-smoke.test.ts +62 -0
  314. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +57 -1
  315. package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
  316. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +20 -0
  317. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +4 -3
  318. package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +2 -2
  319. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +54 -4
  320. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +482 -4
  321. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +157 -9
  322. package/dist/extensions/pi-subagents/test/integration/session-tokens.test.ts +5 -5
  323. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1508 -291
  324. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +423 -1
  325. package/dist/extensions/pi-subagents/test/integration/template-resolution.test.ts +8 -0
  326. package/dist/extensions/pi-subagents/test/support/cursor-smoke-inputs.ts +51 -0
  327. package/dist/extensions/pi-subagents/test/support/helpers.ts +2 -0
  328. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +5 -5
  329. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +6 -6
  330. package/dist/extensions/pi-subagents/test/unit/abort-recovery.test.ts +152 -0
  331. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +48 -0
  332. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +184 -6
  333. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +66 -6
  334. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +77 -0
  335. package/dist/extensions/pi-subagents/test/unit/agent-refinements.test.ts +18 -0
  336. package/dist/extensions/pi-subagents/test/unit/anti-slop-oxlint.test.ts +86 -0
  337. package/dist/extensions/pi-subagents/test/unit/artifacts.test.ts +1 -1
  338. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +101 -1
  339. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +121 -0
  340. package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +28 -0
  341. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +14 -7
  342. package/dist/extensions/pi-subagents/test/unit/async-retention.test.ts +53 -0
  343. package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +190 -0
  344. package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +42 -0
  345. package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +8 -4
  346. package/dist/extensions/pi-subagents/test/unit/background-work.test.ts +23 -0
  347. package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +78 -1
  348. package/dist/extensions/pi-subagents/test/unit/child-launch-plan.test.ts +94 -0
  349. package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +21 -0
  350. package/dist/extensions/pi-subagents/test/unit/child-session-name.test.ts +50 -0
  351. package/dist/extensions/pi-subagents/test/unit/claude-code-adapter.test.ts +245 -0
  352. package/dist/extensions/pi-subagents/test/unit/codex-exec-adapter.test.ts +192 -0
  353. package/dist/extensions/pi-subagents/test/unit/compaction-resume.test.ts +27 -0
  354. package/dist/extensions/pi-subagents/test/unit/completion-evidence.test.ts +150 -0
  355. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +21 -0
  356. package/dist/extensions/pi-subagents/test/unit/cursor-agent-adapter.test.ts +259 -0
  357. package/dist/extensions/pi-subagents/test/unit/cursor-smoke-inputs.test.ts +77 -0
  358. package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +1 -4
  359. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +1 -1
  360. package/dist/extensions/pi-subagents/test/unit/extension-context.test.ts +27 -0
  361. package/dist/extensions/pi-subagents/test/unit/external-cli-runner.test.ts +223 -0
  362. package/dist/extensions/pi-subagents/test/unit/fast-mode-extension.test.ts +27 -0
  363. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +208 -2
  364. package/dist/extensions/pi-subagents/test/unit/fleet-transcript.test.ts +29 -1
  365. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +110 -1
  366. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +10 -0
  367. package/dist/extensions/pi-subagents/test/unit/formatters.test.ts +14 -0
  368. package/dist/extensions/pi-subagents/test/unit/herdr-inspector.test.ts +18 -9
  369. package/dist/extensions/pi-subagents/test/unit/herdr-session-roots-codec.test.ts +63 -0
  370. package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +2 -2
  371. package/dist/extensions/pi-subagents/test/unit/herdr-status-bridge.test.ts +132 -0
  372. package/dist/extensions/pi-subagents/test/unit/host-command.test.ts +156 -0
  373. package/dist/extensions/pi-subagents/test/unit/host-step-status.test.ts +98 -0
  374. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +178 -26
  375. package/dist/extensions/pi-subagents/test/unit/index-segment.test.ts +2 -1
  376. package/dist/extensions/pi-subagents/test/unit/inspect-rpc.test.ts +21 -0
  377. package/dist/extensions/pi-subagents/test/unit/mcp-direct-tool-grant.test.ts +138 -0
  378. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
  379. package/dist/extensions/pi-subagents/test/unit/model-exclusions.test.ts +91 -0
  380. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +72 -7
  381. package/dist/extensions/pi-subagents/test/unit/mutation-evidence.test.ts +21 -0
  382. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +1 -0
  383. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +20 -2
  384. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +129 -1
  385. package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +2 -2
  386. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +4 -0
  387. package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +358 -0
  388. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +574 -21
  389. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +109 -1
  390. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +71 -1
  391. package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +7 -7
  392. package/dist/extensions/pi-subagents/test/unit/pruned-fork.test.ts +214 -0
  393. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +29 -8
  394. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +53 -0
  395. package/dist/extensions/pi-subagents/test/unit/result-delivery-ownership.test.ts +34 -0
  396. package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +101 -3
  397. package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +1 -0
  398. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +10 -8
  399. package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +71 -1
  400. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +41 -7
  401. package/dist/extensions/pi-subagents/test/unit/runtime-agent-registration.test.ts +136 -2
  402. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +137 -0
  403. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +33 -12
  404. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +504 -1
  405. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +8 -0
  406. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +1 -1
  407. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +12 -1
  408. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +3 -5
  409. package/dist/extensions/pi-subagents/test/unit/steering.test.ts +5 -6
  410. package/dist/extensions/pi-subagents/test/unit/subagent-action-recovery.test.ts +0 -8
  411. package/dist/extensions/pi-subagents/test/unit/subagent-guide.test.ts +5 -0
  412. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +157 -2
  413. package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +1 -1
  414. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +63 -4
  415. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +27 -23
  416. package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +44 -0
  417. package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +1 -0
  418. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +7 -5
  419. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +106 -0
  420. package/dist/extensions/pi-subagents/test/unit/workflow-detach-reconcile.test.ts +484 -9
  421. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +38 -4
  422. package/dist/extensions/pi-subagents/test/unit/workflow-preflight.test.ts +155 -0
  423. package/dist/extensions/pi-subagents/test/unit/workflow-receipt.test.ts +318 -0
  424. package/dist/extensions/pi-subagents/test/unit/workflow-resume-hint.test.ts +365 -0
  425. package/dist/extensions/pi-subagents/test/unit/worktree-cleanup-plan.test.ts +349 -0
  426. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +21 -0
  427. package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +2 -7
  428. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/effect/index.ts +13 -0
  429. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/effect/rules/no-service-constructor-imports.ts +52 -0
  430. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/index.ts +41 -0
  431. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-chained-type-assertions.ts +77 -0
  432. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-conditional-empty-object-spread.ts +49 -0
  433. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-known-value-widening.ts +247 -0
  434. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-module-mocking.ts +91 -0
  435. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-object-parameters.ts +126 -0
  436. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-reflect-apply.ts +28 -0
  437. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-reflect-get.ts +28 -0
  438. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-runtime-typeof.ts +67 -0
  439. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-shape-in-symbol-names.ts +39 -0
  440. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-unknown-parameters.ts +83 -0
  441. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-unknown-returns.ts +115 -0
  442. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-unknown-type-aliases.ts +72 -0
  443. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-unsafe-dictionary-type.ts +134 -0
  444. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-widen-then-assert.ts +366 -0
  445. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/require-safety-comment-for-type-assertion.ts +62 -0
  446. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/shared/dictionary-types.ts +502 -0
  447. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/shared/lexical-type-parameters.ts +60 -0
  448. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/shared/reflect-method.ts +35 -0
  449. package/dist/extensions/pi-tool-display/tests/capabilities.test.ts +1 -1
  450. package/dist/extensions/pi-tool-display/tests/settings-inspector.test.ts +2 -2
  451. package/dist/extensions/pi-tool-display/tests/tool-overrides-mcp.test.ts +1 -1
  452. package/dist/modes/interactive/interactive-mode.js +7 -7
  453. package/dist/skills/workflow/SKILL.md +65 -50
  454. package/package.json +112 -110
  455. package/dist/extensions/enable-readonly-tools.test.ts +0 -63
  456. package/dist/extensions/enable-readonly-tools.ts +0 -24
  457. package/dist/extensions/model-prompt-injector/config.json +0 -16
  458. package/dist/extensions/model-prompt-injector/index.test.ts +0 -102
  459. package/dist/extensions/model-prompt-injector/index.ts +0 -148
  460. package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +0 -98
  461. package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +0 -250
@@ -0,0 +1,950 @@
1
+ /**
2
+ * SkillStore — procedural memory stored as Pi-native skills.
3
+ *
4
+ * Global skills live in ~/.selesai/agent/pi-hermes-memory/skills/<slug>/SKILL.md.
5
+ * Project skills live in ~/.selesai/agent/<projectsMemoryDir>/<project>/skills/<slug>/SKILL.md.
6
+ */
7
+
8
+ import * as fs from "node:fs/promises";
9
+ import * as path from "node:path";
10
+ import { scanContent } from "./content-scanner.js";
11
+ import {
12
+ buildSkillId,
13
+ exists,
14
+ formatFrontmatter,
15
+ jaccardSimilarity,
16
+ parseFrontmatter,
17
+ parseSkillId,
18
+ slugify,
19
+ today,
20
+ tokenizeForSimilarity,
21
+ } from "./skill-utils.js";
22
+ import type { SkillDocument, SkillIndex, SkillResult, SkillScope } from "../types.js";
23
+ import { AGENT_ROOT } from "../paths.js";
24
+
25
+ interface SkillStoreOptions {
26
+ /** Where this extension writes global skills. Never Pi's own root. */
27
+ globalSkillsDir?: string;
28
+ /** Pi's global skills root, consulted read-only to refuse shadowed writes. */
29
+ piGlobalSkillsDir?: string;
30
+ projectSkillsDir?: string | null;
31
+ projectName?: string | null;
32
+ legacySkillsDir?: string;
33
+ migrationSentinelPath?: string;
34
+ }
35
+
36
+ interface SkillLocation {
37
+ skillId: string;
38
+ scope: SkillScope;
39
+ slug: string;
40
+ fileName: string;
41
+ path: string;
42
+ projectName?: string;
43
+ }
44
+
45
+ export interface LegacySkillMigrationResult {
46
+ migrated: number;
47
+ skipped: number;
48
+ warnings: string[];
49
+ }
50
+
51
+ const LIST_SECTIONS = new Set(["procedure", "pitfalls", "verification"]);
52
+
53
+ function normalizeSectionName(section: string): string {
54
+ return section.replace(/^#+\s*/, "").trim();
55
+ }
56
+
57
+ function isExactSectionHeader(line: string, section: string): boolean {
58
+ const heading = line.trim().match(/^##\s+(.+?)\s*$/);
59
+ if (!heading) return false;
60
+ return heading[1].trim().toLowerCase() === section.trim().toLowerCase();
61
+ }
62
+
63
+ function looksLikeJsonArray(content: string): boolean {
64
+ const trimmed = content.trim();
65
+ return trimmed.startsWith("[") && trimmed.endsWith("]");
66
+ }
67
+
68
+ function looksLikeJsonObject(content: string): boolean {
69
+ const trimmed = content.trim();
70
+ return trimmed.startsWith("{") && trimmed.endsWith("}");
71
+ }
72
+
73
+ function formatPatchList(section: string, items: string[]): string {
74
+ const cleaned = items.map((item) => item.trim()).filter(Boolean);
75
+ if (cleaned.length === 0) return "";
76
+ const key = section.trim().toLowerCase();
77
+ if (key === "pitfalls") {
78
+ return cleaned.map((item) => `- ${item.replace(/^[-*]\s+/, "")}`).join("\n");
79
+ }
80
+ // Procedure / Verification default to ordered steps.
81
+ return cleaned
82
+ .map((item, index) => `${index + 1}. ${item.replace(/^\d+\.\s+/, "").replace(/^[-*]\s+/, "")}`)
83
+ .join("\n");
84
+ }
85
+
86
+ /**
87
+ * Normalize/validate free-form patch content so LLM format drift cannot wipe
88
+ * or splice skill sections. Coerces JSON string arrays into Markdown lists for
89
+ * list-shaped sections and rejects empty / object / header-injecting payloads.
90
+ */
91
+ export function normalizeSkillPatchContent(
92
+ section: string,
93
+ rawContent: string,
94
+ ): { content: string } | { error: string } {
95
+ const sectionName = normalizeSectionName(section);
96
+ if (!sectionName) {
97
+ return { error: "section is required for patch." };
98
+ }
99
+
100
+ let content = typeof rawContent === "string" ? rawContent.trim() : "";
101
+ if (!content) {
102
+ return {
103
+ error: "New content is required for patch. Prefer structured fields (procedure_steps, pitfalls, verification_steps, when_to_use) over free-form content.",
104
+ };
105
+ }
106
+
107
+ if (looksLikeJsonObject(content)) {
108
+ return {
109
+ error: "Patch content looks like a JSON object. Provide Markdown section body or a string array via structured fields.",
110
+ };
111
+ }
112
+
113
+ if (looksLikeJsonArray(content)) {
114
+ try {
115
+ const parsed: unknown = JSON.parse(content);
116
+ if (!Array.isArray(parsed)) {
117
+ return { error: "Patch content looks like JSON but is not a string array." };
118
+ }
119
+ const items = parsed
120
+ .filter((item): item is string => typeof item === "string")
121
+ .map((item) => item.trim())
122
+ .filter(Boolean);
123
+ if (items.length === 0) {
124
+ return { error: "Patch content JSON array must contain non-empty strings." };
125
+ }
126
+ const key = sectionName.toLowerCase();
127
+ if (key === "when to use") {
128
+ content = items.join("\n\n");
129
+ } else if (LIST_SECTIONS.has(key)) {
130
+ content = formatPatchList(sectionName, items);
131
+ } else {
132
+ content = items.map((item) => `- ${item}`).join("\n");
133
+ }
134
+ } catch {
135
+ return {
136
+ error: "Patch content looks like a JSON array but could not be parsed. Use Markdown or structured string[] fields.",
137
+ };
138
+ }
139
+ }
140
+
141
+ // Reject payloads that would inject extra ## sections mid-body.
142
+ if (/^#{1,6}\s+\S/m.test(content)) {
143
+ return {
144
+ error: "Patch content must not include Markdown section headers (## ...). Patch only the body of the target section.",
145
+ };
146
+ }
147
+
148
+ if (!content.trim()) {
149
+ return { error: "New content is required for patch." };
150
+ }
151
+
152
+ return { content: content.trim() };
153
+ }
154
+
155
+
156
+ export class SkillStore {
157
+ private globalSkillsDir: string;
158
+ private piGlobalSkillsDir: string;
159
+ private projectSkillsDir: string | null;
160
+ private projectName: string | null;
161
+ private legacySkillsDir: string;
162
+ private migrationSentinelPath: string;
163
+
164
+ constructor(options: SkillStoreOptions = {}) {
165
+ const agentRoot = AGENT_ROOT;
166
+ this.globalSkillsDir = options.globalSkillsDir ?? path.join(agentRoot, "pi-hermes-memory", "skills");
167
+ this.piGlobalSkillsDir = options.piGlobalSkillsDir ?? path.join(agentRoot, "skills");
168
+ this.projectSkillsDir = options.projectSkillsDir ?? null;
169
+ this.projectName = options.projectName ?? null;
170
+ this.legacySkillsDir = options.legacySkillsDir ?? path.join(agentRoot, "memory", "skills");
171
+ this.migrationSentinelPath = options.migrationSentinelPath
172
+ ?? path.join(agentRoot, "pi-hermes-memory", ".skills-migrated-to-extension-storage");
173
+ }
174
+
175
+ getGlobalSkillsDir(): string {
176
+ return this.globalSkillsDir;
177
+ }
178
+
179
+ /**
180
+ * Pi's own global skills root. Read-only from here: we never create, patch,
181
+ * or delete inside it. It is consulted solely to refuse writes that Pi would
182
+ * silently shadow (see `findShadowingPiGlobalSkill`).
183
+ */
184
+ getPiGlobalSkillsDir(): string {
185
+ return this.piGlobalSkillsDir;
186
+ }
187
+
188
+ getProjectSkillsDir(): string | null {
189
+ return this.projectSkillsDir;
190
+ }
191
+
192
+ getProjectName(): string | null {
193
+ return this.projectName;
194
+ }
195
+
196
+ setProjectContext(projectName: string | null, projectSkillsDir: string | null): void {
197
+ this.projectName = projectName;
198
+ this.projectSkillsDir = projectSkillsDir;
199
+ }
200
+
201
+ async ensureDiscoveredRoots(): Promise<void> {
202
+ await fs.mkdir(this.globalSkillsDir, { recursive: true });
203
+ if (this.projectSkillsDir) {
204
+ await fs.mkdir(this.projectSkillsDir, { recursive: true });
205
+ }
206
+ }
207
+
208
+ async migrateLegacySkills(): Promise<LegacySkillMigrationResult> {
209
+ const result: LegacySkillMigrationResult = { migrated: 0, skipped: 0, warnings: [] };
210
+
211
+ // Always normalize flat markdown files under the global skills root,
212
+ // even when a previous migration sentinel already exists.
213
+ await this.migrateFlatMarkdownInGlobalSkillsDir(result);
214
+
215
+ if (await exists(this.migrationSentinelPath)) return result;
216
+
217
+ await fs.mkdir(path.dirname(this.migrationSentinelPath), { recursive: true });
218
+
219
+ // Only this migration's own warnings may hold back its sentinel — a
220
+ // permanently shadowed skill reported above must not make it retry forever.
221
+ const warningsBefore = result.warnings.length;
222
+ try {
223
+ await this.migrateLegacyMarkdownSkills(result);
224
+ } finally {
225
+ if (result.warnings.length === warningsBefore) {
226
+ await fs.writeFile(this.migrationSentinelPath, `${new Date().toISOString()}\n`, "utf-8");
227
+ }
228
+ }
229
+
230
+ return result;
231
+ }
232
+
233
+ private async migrateLegacyMarkdownSkills(result: LegacySkillMigrationResult): Promise<void> {
234
+ if (!await exists(this.legacySkillsDir)) return;
235
+
236
+ const files = (await fs.readdir(this.legacySkillsDir))
237
+ .filter((file) => file.endsWith(".md"))
238
+ .sort();
239
+
240
+ for (const file of files) {
241
+ const legacyPath = path.join(this.legacySkillsDir, file);
242
+ try {
243
+ const raw = await fs.readFile(legacyPath, "utf-8");
244
+ const parsed = parseFrontmatter(raw);
245
+ const fallbackSlug = slugify(path.basename(file, ".md"));
246
+ const slug = slugify(parsed.meta.name || fallbackSlug);
247
+ if (!slug) {
248
+ result.skipped++;
249
+ continue;
250
+ }
251
+
252
+ const targetPath = path.join(this.globalSkillsDir, slug, "SKILL.md");
253
+ if (await exists(targetPath)) {
254
+ result.skipped++;
255
+ continue;
256
+ }
257
+
258
+ const skillDoc = {
259
+ name: slug,
260
+ displayName: parsed.meta.display_name?.trim() || parsed.meta.name?.trim() || undefined,
261
+ description: parsed.meta.description?.trim() || `Migrated legacy skill: ${slug}`,
262
+ version: Number.parseInt(parsed.meta.version || "1", 10) || 1,
263
+ created: parsed.meta.created || today(),
264
+ updated: parsed.meta.updated || today(),
265
+ body: parsed.body || `# ${slug}\n`,
266
+ };
267
+
268
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
269
+ await this.atomicWrite(targetPath, formatFrontmatter(skillDoc));
270
+ result.migrated++;
271
+ } catch (error) {
272
+ result.warnings.push(`${file}: ${error instanceof Error ? error.message : String(error)}`);
273
+ }
274
+ }
275
+ }
276
+
277
+ private async migrateFlatMarkdownInGlobalSkillsDir(result: LegacySkillMigrationResult): Promise<void> {
278
+ if (!await exists(this.globalSkillsDir)) return;
279
+
280
+ const files = (await fs.readdir(this.globalSkillsDir))
281
+ .filter((file) => file.endsWith(".md") && file !== "SKILL.md")
282
+ .sort();
283
+
284
+ for (const file of files) {
285
+ const legacyPath = path.join(this.globalSkillsDir, file);
286
+ try {
287
+ const raw = await fs.readFile(legacyPath, "utf-8");
288
+ const parsed = parseFrontmatter(raw);
289
+ const fallbackSlug = slugify(path.basename(file, ".md"));
290
+ const slug = slugify(parsed.meta.name || fallbackSlug);
291
+ if (!slug) {
292
+ result.skipped++;
293
+ continue;
294
+ }
295
+
296
+ const targetPath = path.join(this.globalSkillsDir, slug, "SKILL.md");
297
+ if (await exists(targetPath)) {
298
+ await fs.rm(legacyPath, { force: true });
299
+ result.skipped++;
300
+ continue;
301
+ }
302
+
303
+ const skillDoc = {
304
+ name: slug,
305
+ displayName: parsed.meta.display_name?.trim() || parsed.meta.name?.trim() || undefined,
306
+ description: parsed.meta.description?.trim() || `Migrated legacy skill: ${slug}`,
307
+ version: Number.parseInt(parsed.meta.version || "1", 10) || 1,
308
+ created: parsed.meta.created || today(),
309
+ updated: parsed.meta.updated || today(),
310
+ body: parsed.body || `# ${slug}\n`,
311
+ };
312
+
313
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
314
+ await this.atomicWrite(targetPath, formatFrontmatter(skillDoc));
315
+ await fs.rm(legacyPath, { force: true });
316
+ result.migrated++;
317
+ } catch (error) {
318
+ result.warnings.push(`${file}: ${error instanceof Error ? error.message : String(error)}`);
319
+ }
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Is `slug` already claimed by a skill in Pi's own global root?
325
+ *
326
+ * Pi keys skills by name, first-loaded wins, and `~/.selesai/agent/skills/` is
327
+ * auto-discovered at higher precedence than anything an extension contributes
328
+ * via `resources_discover`. A global skill we write under a name that also
329
+ * exists there is never the copy Pi loads, so the write succeeds on disk and
330
+ * changes nothing about the agent's behaviour — silent write-loss (#125).
331
+ *
332
+ * Callers refuse the write and name both paths instead, which makes the
333
+ * shadowed state impossible to create rather than merely reported after the
334
+ * fact. Returns the shadowing path, or null when the name is free.
335
+ */
336
+ private async findShadowingPiGlobalSkill(slug: string): Promise<string | null> {
337
+ if (path.resolve(this.piGlobalSkillsDir) === path.resolve(this.globalSkillsDir)) return null;
338
+ const candidate = path.join(this.piGlobalSkillsDir, slug, "SKILL.md");
339
+ return await exists(candidate) ? candidate : null;
340
+ }
341
+
342
+ async loadIndex(scope?: SkillScope): Promise<SkillIndex[]> {
343
+ const locations = await this.collectLocations(scope);
344
+ const skills: SkillIndex[] = [];
345
+
346
+ for (const location of locations) {
347
+ const doc = await this.readLocation(location);
348
+ if (doc) skills.push(this.toIndex(doc));
349
+ }
350
+
351
+ return skills.sort((a, b) => {
352
+ if (a.updated !== b.updated) return b.updated.localeCompare(a.updated);
353
+ if (a.created !== b.created) return b.created.localeCompare(a.created);
354
+ if (a.scope !== b.scope) return a.scope.localeCompare(b.scope);
355
+ return (a.displayName || a.name).localeCompare(b.displayName || b.name);
356
+ });
357
+ }
358
+
359
+ async loadSkill(skillId: string): Promise<SkillDocument | null> {
360
+ const location = await this.findLocationById(skillId);
361
+ if (!location) return null;
362
+ return this.readLocation(location);
363
+ }
364
+
365
+ async create(name: string, description: string, body: string, scope?: SkillScope): Promise<SkillResult> {
366
+ name = name.trim();
367
+ description = description.trim();
368
+ body = body.trim();
369
+
370
+ if (!name) return { success: false, error: "Skill name is required." };
371
+ if (!description) return { success: false, error: "Skill description is required." };
372
+ if (!body) return { success: false, error: "Skill body is required." };
373
+
374
+ const scanError = scanContent(`${name} ${description} ${body}`);
375
+ if (scanError) return { success: false, error: scanError };
376
+
377
+ const slug = slugify(name);
378
+ if (!slug) return { success: false, error: "Skill name produces empty slug." };
379
+
380
+ const resolvedScope = this.resolveScope(scope, name, description, body);
381
+ const root = this.getScopeRoot(resolvedScope);
382
+ if (!root) {
383
+ return { success: false, error: "Project skills require an active project." };
384
+ }
385
+
386
+ const skillId = buildSkillId(resolvedScope, slug, this.projectName);
387
+ const existing = await this.findLocationById(skillId);
388
+ if (existing) {
389
+ return {
390
+ success: false,
391
+ error: `Skill '${slug}' already exists (${skillId}). Use 'patch' or 'update' to update it.`,
392
+ conflictType: "duplicate",
393
+ similarSkillIds: [skillId],
394
+ suggestedAction: "patch",
395
+ };
396
+ }
397
+
398
+ if (resolvedScope === "global") {
399
+ const similarSkillIds = await this.findSimilarGlobalSkillIds(slug, description);
400
+ if (similarSkillIds.length > 0) {
401
+ const targetId = similarSkillIds[0];
402
+ return {
403
+ success: false,
404
+ error: `A similar global skill already exists (${targetId}). Enhance the existing skill with new learnings/failures using 'patch' or 'update' instead of creating a duplicate.`,
405
+ conflictType: "similar",
406
+ similarSkillIds,
407
+ suggestedAction: "patch",
408
+ };
409
+ }
410
+
411
+ const collidingNameSkillIds = await this.findNameCollisionGlobalSkillIds(slug, description);
412
+ if (collidingNameSkillIds.length > 0) {
413
+ const targetId = collidingNameSkillIds[0];
414
+ return {
415
+ success: false,
416
+ error: `A near-name global skill already exists (${targetId}) but with different intent. Use a clearer differentiated name for the new skill, or patch/update the existing skill if the intent is actually the same.`,
417
+ conflictType: "name-collision",
418
+ similarSkillIds: collidingNameSkillIds,
419
+ suggestedAction: "rename",
420
+ };
421
+ }
422
+
423
+ const shadowedBy = await this.findShadowingPiGlobalSkill(slug);
424
+ if (shadowedBy) {
425
+ return {
426
+ success: false,
427
+ error: `Pi already loads a global skill named '${slug}' from ${shadowedBy}. `
428
+ + `Pi keys skills by name and loads its own root first, so a skill written to `
429
+ + `${path.join(this.globalSkillsDir, slug, "SKILL.md")} would never be the copy in effect. `
430
+ + `Choose a different name, or edit ${shadowedBy} directly.`,
431
+ conflictType: "name-collision",
432
+ suggestedAction: "rename",
433
+ };
434
+ }
435
+ }
436
+
437
+ const filePath = path.join(root, slug, "SKILL.md");
438
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
439
+
440
+ const displayName = name;
441
+ const storedName = slug;
442
+ const stamp = today();
443
+ await this.atomicWrite(filePath, formatFrontmatter({
444
+ name: storedName,
445
+ displayName,
446
+ description,
447
+ version: 1,
448
+ created: stamp,
449
+ updated: stamp,
450
+ body,
451
+ }));
452
+
453
+ return {
454
+ success: true,
455
+ message: `Skill '${displayName}' created as a ${resolvedScope} skill.`,
456
+ fileName: path.basename(filePath),
457
+ skillId,
458
+ scope: resolvedScope,
459
+ path: filePath,
460
+ };
461
+ }
462
+
463
+ async patch(skillId: string, section: string, newContent: string): Promise<SkillResult> {
464
+ const sectionName = normalizeSectionName(section);
465
+ if (!sectionName) return { success: false, error: "section is required for patch." };
466
+
467
+ const normalized = normalizeSkillPatchContent(sectionName, newContent);
468
+ if ("error" in normalized) return { success: false, error: normalized.error };
469
+ const content = normalized.content;
470
+
471
+ const scanError = scanContent(content);
472
+ if (scanError) return { success: false, error: scanError };
473
+
474
+ const doc = await this.loadSkill(skillId);
475
+ if (!doc) return { success: false, error: `Skill '${skillId}' not found.` };
476
+
477
+ const sectionHeader = `## ${sectionName}`;
478
+ const lines = doc.body.split("\n");
479
+ let found = false;
480
+ const result: string[] = [];
481
+
482
+ for (let i = 0; i < lines.length; i++) {
483
+ if (isExactSectionHeader(lines[i], sectionName)) {
484
+ result.push(sectionHeader);
485
+ // Preserve multi-line body as discrete lines so later headers stay exact.
486
+ for (const bodyLine of content.split("\n")) {
487
+ result.push(bodyLine);
488
+ }
489
+ found = true;
490
+ i++;
491
+ while (i < lines.length && !lines[i].trim().startsWith("## ")) i++;
492
+ if (i < lines.length) result.push(lines[i]);
493
+ } else {
494
+ result.push(lines[i]);
495
+ }
496
+ }
497
+
498
+ if (!found) {
499
+ if (result.length > 0 && result[result.length - 1] !== "") result.push("");
500
+ result.push(sectionHeader);
501
+ for (const bodyLine of content.split("\n")) {
502
+ result.push(bodyLine);
503
+ }
504
+ }
505
+
506
+ await this.atomicWrite(doc.path, formatFrontmatter({
507
+ name: doc.name,
508
+ displayName: doc.displayName,
509
+ description: doc.description,
510
+ version: doc.version + 1,
511
+ created: doc.created,
512
+ updated: today(),
513
+ body: result.join("\n").trim(),
514
+ }));
515
+
516
+ return {
517
+ success: true,
518
+ message: `Skill '${doc.displayName || doc.name}' section '${sectionName}' updated.`,
519
+ fileName: doc.fileName,
520
+ skillId: doc.skillId,
521
+ scope: doc.scope,
522
+ path: doc.path,
523
+ };
524
+ }
525
+
526
+ async edit(skillId: string, description: string, body: string): Promise<SkillResult> {
527
+ description = description.trim();
528
+ body = body.trim();
529
+
530
+ if (!description && !body) {
531
+ return { success: false, error: "At least one of description or body is required." };
532
+ }
533
+
534
+ const doc = await this.loadSkill(skillId);
535
+ if (!doc) return { success: false, error: `Skill '${skillId}' not found.` };
536
+
537
+ const newDescription = description || doc.description;
538
+ const newBody = body || doc.body;
539
+ const scanError = scanContent(`${newDescription} ${newBody}`);
540
+ if (scanError) return { success: false, error: scanError };
541
+
542
+ await this.atomicWrite(doc.path, formatFrontmatter({
543
+ name: doc.name,
544
+ displayName: doc.displayName,
545
+ description: newDescription,
546
+ version: doc.version + 1,
547
+ created: doc.created,
548
+ updated: today(),
549
+ body: newBody,
550
+ }));
551
+
552
+ return {
553
+ success: true,
554
+ message: `Skill '${doc.displayName || doc.name}' updated.`,
555
+ fileName: doc.fileName,
556
+ skillId: doc.skillId,
557
+ scope: doc.scope,
558
+ path: doc.path,
559
+ };
560
+ }
561
+
562
+ async move(skillId: string, targetScope: SkillScope): Promise<SkillResult> {
563
+ const doc = await this.loadSkill(skillId);
564
+ if (!doc) return { success: false, error: `Skill '${skillId}' not found.` };
565
+
566
+ const parsed = parseSkillId(skillId);
567
+ if (!parsed) return { success: false, error: `Skill '${skillId}' is invalid.` };
568
+
569
+ if (doc.scope === targetScope) {
570
+ return {
571
+ success: true,
572
+ message: `Skill '${doc.displayName || doc.name}' is already ${targetScope}.`,
573
+ fileName: doc.fileName,
574
+ skillId: doc.skillId,
575
+ scope: doc.scope,
576
+ path: doc.path,
577
+ };
578
+ }
579
+
580
+ const targetRoot = this.getScopeRoot(targetScope);
581
+ if (!targetRoot) {
582
+ return { success: false, error: "Project skills require an active project." };
583
+ }
584
+
585
+ const targetSkillId = buildSkillId(targetScope, parsed.slug, this.projectName);
586
+ const targetPath = path.join(targetRoot, parsed.slug, "SKILL.md");
587
+ if (await exists(targetPath)) {
588
+ return {
589
+ success: false,
590
+ error: `Cannot move '${doc.displayName || doc.name}' to ${targetScope}: ${targetSkillId} already exists.`,
591
+ conflictType: "scope-conflict",
592
+ similarSkillIds: [targetSkillId],
593
+ suggestedAction: "rename",
594
+ };
595
+ }
596
+
597
+ if (targetScope === "global") {
598
+ const similarSkillIds = await this.findSimilarGlobalSkillIds(parsed.slug, doc.description);
599
+ if (similarSkillIds.length > 0) {
600
+ const targetId = similarSkillIds[0];
601
+ return {
602
+ success: false,
603
+ error: `Cannot move '${doc.displayName || doc.name}' to global: a similar global skill already exists (${targetId}).`,
604
+ conflictType: "similar",
605
+ similarSkillIds,
606
+ suggestedAction: "patch",
607
+ };
608
+ }
609
+
610
+ const collidingNameSkillIds = await this.findNameCollisionGlobalSkillIds(parsed.slug, doc.description);
611
+ if (collidingNameSkillIds.length > 0) {
612
+ const targetId = collidingNameSkillIds[0];
613
+ return {
614
+ success: false,
615
+ error: `Cannot move '${doc.displayName || doc.name}' to global: a near-name global skill already exists (${targetId}) with different intent.`,
616
+ conflictType: "name-collision",
617
+ similarSkillIds: collidingNameSkillIds,
618
+ suggestedAction: "rename",
619
+ };
620
+ }
621
+ }
622
+
623
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
624
+
625
+ // Same-filesystem move: use rename first for atomicity and to avoid duplicate windows.
626
+ try {
627
+ await fs.rename(doc.path, targetPath);
628
+
629
+ if (path.basename(doc.path) === "SKILL.md") {
630
+ await this.removeEmptyParents(path.dirname(doc.path), this.getScopeRoot(doc.scope));
631
+ }
632
+
633
+ return {
634
+ success: true,
635
+ message: `Skill '${doc.displayName || doc.name}' moved to ${targetScope}.`,
636
+ fileName: path.basename(targetPath),
637
+ skillId: targetSkillId,
638
+ scope: targetScope,
639
+ path: targetPath,
640
+ };
641
+ } catch (renameError) {
642
+ const code = (renameError as NodeJS.ErrnoException)?.code;
643
+ if (code !== "EXDEV") {
644
+ return {
645
+ success: false,
646
+ error: `Move to ${targetScope} failed before copy for skill '${skillId}'. Source path: ${doc.path}. Destination path: ${targetPath}. Error: ${renameError instanceof Error ? renameError.message : String(renameError)}`,
647
+ };
648
+ }
649
+ // Cross-device fallback below.
650
+ }
651
+
652
+ // Cross-device fallback: copy then remove source.
653
+ await this.atomicWrite(targetPath, formatFrontmatter({
654
+ name: parsed.slug,
655
+ displayName: doc.displayName,
656
+ description: doc.description,
657
+ version: doc.version,
658
+ created: doc.created,
659
+ updated: doc.updated,
660
+ body: doc.body,
661
+ }));
662
+
663
+ try {
664
+ await fs.unlink(doc.path);
665
+ if (path.basename(doc.path) === "SKILL.md") {
666
+ await this.removeEmptyParents(path.dirname(doc.path), this.getScopeRoot(doc.scope));
667
+ }
668
+ } catch (error) {
669
+ // Best-effort rollback: remove the destination copy if source removal fails,
670
+ // so we do not silently leave duplicate skills across scopes.
671
+ let rollbackFailed = false;
672
+ try {
673
+ await fs.unlink(targetPath);
674
+ if (path.basename(targetPath) === "SKILL.md") {
675
+ await this.removeEmptyParents(path.dirname(targetPath), this.getScopeRoot(targetScope));
676
+ }
677
+ } catch {
678
+ rollbackFailed = true;
679
+ }
680
+
681
+ return {
682
+ success: false,
683
+ error: rollbackFailed
684
+ ? `Move to ${targetScope} failed while removing source skill '${skillId}', and rollback also failed. Source path: ${doc.path}. Destination path: ${targetPath}. Error: ${error instanceof Error ? error.message : String(error)}`
685
+ : `Move to ${targetScope} failed while removing source skill '${skillId}'. Rolled back destination copy. Source path: ${doc.path}. Destination path: ${targetPath}. Error: ${error instanceof Error ? error.message : String(error)}`,
686
+ };
687
+ }
688
+
689
+ return {
690
+ success: true,
691
+ message: `Skill '${doc.displayName || doc.name}' moved to ${targetScope}.`,
692
+ fileName: path.basename(targetPath),
693
+ skillId: targetSkillId,
694
+ scope: targetScope,
695
+ path: targetPath,
696
+ };
697
+ }
698
+
699
+ async delete(skillId: string): Promise<SkillResult> {
700
+ const doc = await this.loadSkill(skillId);
701
+ if (!doc) return { success: false, error: `Skill '${skillId}' not found.` };
702
+
703
+ await fs.unlink(doc.path);
704
+ if (path.basename(doc.path) === "SKILL.md") {
705
+ await this.removeEmptyParents(path.dirname(doc.path), this.getScopeRoot(doc.scope));
706
+ }
707
+
708
+ return {
709
+ success: true,
710
+ message: `Skill '${doc.displayName || doc.name}' deleted.`,
711
+ fileName: doc.fileName,
712
+ skillId: doc.skillId,
713
+ scope: doc.scope,
714
+ path: doc.path,
715
+ };
716
+ }
717
+
718
+ private resolveScope(scope: SkillScope | undefined, name: string, description: string, body: string): SkillScope {
719
+ if (scope) return scope;
720
+ if (!this.projectSkillsDir || !this.projectName) return "global";
721
+
722
+ const haystack = `${name}\n${description}\n${body}`.toLowerCase();
723
+ const projectLower = this.projectName.toLowerCase();
724
+
725
+ const strongSignals = [
726
+ haystack.includes(projectLower),
727
+ /\bthis repo\b|\bthis repository\b|\bthis project\b|\bour codebase\b|\bour app\b/.test(haystack),
728
+ /\bpackage\.json\b|\bpnpm-lock\.yaml\b|\byarn\.lock\b|\btsconfig\.json\b|\bdocker-compose(\.ya?ml)?\b|\b\.env(\.[a-z0-9._-]+)?\b/.test(haystack),
729
+ /(^|\s)(src|app|apps|packages|services|scripts|tests|docs|infra|migrations|db|api|web|frontend|backend)\/[a-z0-9._/-]+/m.test(haystack),
730
+ /\b(npm|pnpm|yarn|bun)\s+(run|test|build|dev|lint|deploy)\b/.test(haystack),
731
+ ].filter(Boolean).length;
732
+
733
+ const weakerSignals = [
734
+ /\bdeploy\b|\brelease\b|\bmigrate\b|\bmonorepo\b|\bworkspace\b|\bstaging\b|\bproduction\b/.test(haystack),
735
+ /\bteam convention\b|\bcodebase convention\b|\brepo convention\b/.test(haystack),
736
+ ].filter(Boolean).length;
737
+
738
+ return strongSignals >= 2 || (strongSignals >= 1 && weakerSignals >= 1) ? "project" : "global";
739
+ }
740
+
741
+ private getScopeRoot(scope: SkillScope): string | null {
742
+ return scope === "global" ? this.globalSkillsDir : this.projectSkillsDir;
743
+ }
744
+
745
+ private async findSimilarGlobalSkillIds(candidateSlug: string, candidateDescription: string): Promise<string[]> {
746
+ const NAME_SIMILARITY_THRESHOLD = 0.7;
747
+ const DESCRIPTION_SIMILARITY_THRESHOLD = 0.75;
748
+
749
+ const scored = await this.scoreGlobalSimilarity(candidateSlug, candidateDescription);
750
+
751
+ return scored
752
+ .filter((entry) => entry.nameSimilarity > NAME_SIMILARITY_THRESHOLD
753
+ && entry.descriptionSimilarity > DESCRIPTION_SIMILARITY_THRESHOLD)
754
+ .map((entry) => entry.skillId);
755
+ }
756
+
757
+ private async findNameCollisionGlobalSkillIds(candidateSlug: string, candidateDescription: string): Promise<string[]> {
758
+ const NAME_SIMILARITY_THRESHOLD = 0.7;
759
+ const DESCRIPTION_SIMILARITY_THRESHOLD = 0.75;
760
+
761
+ const scored = await this.scoreGlobalSimilarity(candidateSlug, candidateDescription);
762
+
763
+ return scored
764
+ .filter((entry) => entry.nameSimilarity > NAME_SIMILARITY_THRESHOLD
765
+ && entry.descriptionSimilarity <= DESCRIPTION_SIMILARITY_THRESHOLD)
766
+ .map((entry) => entry.skillId);
767
+ }
768
+
769
+ private async scoreGlobalSimilarity(
770
+ candidateSlug: string,
771
+ candidateDescription: string,
772
+ ): Promise<Array<{ skillId: string; nameSimilarity: number; descriptionSimilarity: number }>> {
773
+ const globals = await this.loadIndex("global");
774
+ const candidateNameTokens = tokenizeForSimilarity(candidateSlug.replace(/-/g, " "));
775
+ const candidateDescriptionTokens = tokenizeForSimilarity(candidateDescription);
776
+
777
+ return globals
778
+ .map((skill) => {
779
+ const nameTokens = tokenizeForSimilarity((skill.displayName || skill.name).replace(/-/g, " "));
780
+ const descriptionTokens = tokenizeForSimilarity(skill.description || "");
781
+ const nameSimilarity = jaccardSimilarity(candidateNameTokens, nameTokens);
782
+ const descriptionSimilarity = jaccardSimilarity(candidateDescriptionTokens, descriptionTokens);
783
+
784
+ return {
785
+ skillId: skill.skillId,
786
+ nameSimilarity,
787
+ descriptionSimilarity,
788
+ };
789
+ })
790
+ .sort((a, b) => {
791
+ const byName = b.nameSimilarity - a.nameSimilarity;
792
+ if (Math.abs(byName) > 0.0001) return byName;
793
+ return b.descriptionSimilarity - a.descriptionSimilarity;
794
+ });
795
+ }
796
+
797
+ private async collectLocations(scope?: SkillScope): Promise<SkillLocation[]> {
798
+ const locations: SkillLocation[] = [];
799
+ const seen = new Set<string>();
800
+
801
+ if (!scope || scope === "global") {
802
+ const globalLocations = await this.scanScope(this.globalSkillsDir, "global", true, this.projectName ?? undefined);
803
+ for (const location of globalLocations) {
804
+ if (seen.has(location.skillId)) continue;
805
+ seen.add(location.skillId);
806
+ locations.push(location);
807
+ }
808
+ }
809
+
810
+ if ((!scope || scope === "project") && this.projectSkillsDir && this.projectName) {
811
+ const projectLocations = await this.scanScope(this.projectSkillsDir, "project", false, this.projectName);
812
+ for (const location of projectLocations) {
813
+ if (seen.has(location.skillId)) continue;
814
+ seen.add(location.skillId);
815
+ locations.push(location);
816
+ }
817
+ }
818
+
819
+ return locations;
820
+ }
821
+
822
+ private async scanScope(
823
+ root: string,
824
+ scope: SkillScope,
825
+ allowRootMarkdown: boolean,
826
+ projectName?: string,
827
+ ): Promise<SkillLocation[]> {
828
+ if (!await exists(root)) return [];
829
+ const results: SkillLocation[] = [];
830
+
831
+ const walk = async (dir: string, isRoot: boolean): Promise<void> => {
832
+ const entries = await fs.readdir(dir, { withFileTypes: true });
833
+ const dirs = entries.filter((entry) => entry.isDirectory()).sort((a, b) => a.name.localeCompare(b.name));
834
+ const files = entries.filter((entry) => entry.isFile()).sort((a, b) => a.name.localeCompare(b.name));
835
+
836
+ for (const entry of dirs) {
837
+ if (entry.name.startsWith(".")) continue;
838
+ const childDir = path.join(dir, entry.name);
839
+ const skillFile = path.join(childDir, "SKILL.md");
840
+ if (await exists(skillFile)) {
841
+ results.push({
842
+ skillId: buildSkillId(scope, entry.name, projectName),
843
+ scope,
844
+ slug: entry.name,
845
+ fileName: "SKILL.md",
846
+ path: skillFile,
847
+ projectName,
848
+ });
849
+ }
850
+ await walk(childDir, false);
851
+ }
852
+
853
+ if (!isRoot || !allowRootMarkdown) return;
854
+
855
+ for (const entry of files) {
856
+ if (!entry.name.endsWith(".md") || entry.name === "SKILL.md") continue;
857
+ const slug = slugify(path.basename(entry.name, ".md"));
858
+ if (!slug) continue;
859
+ results.push({
860
+ skillId: buildSkillId(scope, slug, projectName),
861
+ scope,
862
+ slug,
863
+ fileName: entry.name,
864
+ path: path.join(dir, entry.name),
865
+ projectName,
866
+ });
867
+ }
868
+ };
869
+
870
+ await walk(root, true);
871
+ return results;
872
+ }
873
+
874
+ private async findLocationById(skillId: string): Promise<SkillLocation | null> {
875
+ const parsed = parseSkillId(skillId);
876
+ if (!parsed) return null;
877
+
878
+ const locations = await this.collectLocations(parsed.scope);
879
+ return locations.find((location) => location.skillId === skillId) ?? null;
880
+ }
881
+
882
+ private async readLocation(location: SkillLocation): Promise<SkillDocument | null> {
883
+ try {
884
+ const raw = await fs.readFile(location.path, "utf-8");
885
+ const { meta, body } = parseFrontmatter(raw);
886
+ const skillName = meta.name?.trim() || location.slug;
887
+ const displayName = meta.display_name?.trim() || undefined;
888
+ return {
889
+ skillId: location.skillId,
890
+ scope: location.scope,
891
+ fileName: location.fileName,
892
+ path: location.path,
893
+ projectName: location.projectName,
894
+ name: skillName,
895
+ displayName,
896
+ description: meta.description?.trim() || "",
897
+ version: Number.parseInt(meta.version || "1", 10) || 1,
898
+ created: meta.created || today(),
899
+ updated: meta.updated || today(),
900
+ body,
901
+ };
902
+ } catch {
903
+ return null;
904
+ }
905
+ }
906
+
907
+ private toIndex(doc: SkillDocument): SkillIndex {
908
+ return {
909
+ skillId: doc.skillId,
910
+ scope: doc.scope,
911
+ fileName: doc.fileName,
912
+ path: doc.path,
913
+ projectName: doc.projectName,
914
+ name: doc.name,
915
+ displayName: doc.displayName,
916
+ description: doc.description,
917
+ created: doc.created,
918
+ updated: doc.updated,
919
+ };
920
+ }
921
+
922
+ private async atomicWrite(filePath: string, content: string): Promise<void> {
923
+ const dir = path.dirname(filePath);
924
+ await fs.mkdir(dir, { recursive: true });
925
+
926
+ const tempFile = path.join(
927
+ dir,
928
+ `.${path.basename(filePath)}.tmp-${Date.now()}-${Math.random().toString(36).slice(2)}`,
929
+ );
930
+
931
+ await fs.writeFile(tempFile, content, "utf-8");
932
+ await fs.rename(tempFile, filePath);
933
+ }
934
+
935
+ private async removeEmptyParents(startDir: string, stopDir: string | null): Promise<void> {
936
+ if (!stopDir) return;
937
+
938
+ let current = startDir;
939
+ while (current.startsWith(stopDir) && current !== stopDir) {
940
+ try {
941
+ const entries = await fs.readdir(current);
942
+ if (entries.length > 0) return;
943
+ await fs.rmdir(current);
944
+ current = path.dirname(current);
945
+ } catch {
946
+ return;
947
+ }
948
+ }
949
+ }
950
+ }