@polymorphism-tech/morph-spec 8.38.0 → 8.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/CLAUDE.md +20 -9
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +60 -43
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +4 -3
  8. package/framework/MORPH.md +10 -9
  9. package/framework/agents.json +43 -26
  10. package/framework/docs/CLI.md +7 -5
  11. package/framework/evals/rubrics/code-quality.md +4 -4
  12. package/framework/evals/rubrics/condensed.md +2 -2
  13. package/framework/hooks/README.md +1 -1
  14. package/framework/hooks/claude-code/core/telemetry-log.js +39 -8
  15. package/framework/hooks/claude-code/core/worktree-create.js +22 -6
  16. package/framework/hooks/shared/git-paths.js +38 -5
  17. package/framework/mcp/registry.json +58 -66
  18. package/framework/rules/infrastructure-standards.md +0 -1
  19. package/framework/rules/morph-workflow.md +9 -4
  20. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  21. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  22. package/framework/skills/core/morph-apply/SKILL.md +59 -25
  23. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  24. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  25. package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
  26. package/framework/skills/core/morph-implement/SKILL.md +48 -24
  27. package/framework/skills/core/morph-media/SKILL.md +2 -2
  28. package/framework/skills/core/morph-plan/SKILL.md +10 -3
  29. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  30. package/framework/skills/core/morph-review/SKILL.md +1 -1
  31. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  32. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  33. package/framework/skills/core/morph-uiux/SKILL.md +7 -6
  34. package/framework/standards/PROVENANCE.json +4 -4
  35. package/framework/standards/STANDARDS.json +9 -9
  36. package/framework/standards/ai-agents/context-providers.md +16 -26
  37. package/framework/standards/ai-agents/durable-workflows-hitl.md +15 -21
  38. package/framework/standards/ai-agents/modalities-image-gen.md +21 -25
  39. package/framework/standards/ai-agents/modalities-speech-to-text.md +4 -15
  40. package/framework/standards/ai-agents/modalities-text-to-speech.md +2 -11
  41. package/framework/standards/ai-agents/modalities-vision.md +1 -18
  42. package/framework/standards/ai-agents/prompt-sources.md +0 -4
  43. package/framework/standards/architecture/code-comments.md +52 -16
  44. package/framework/standards/backend/dotnet/testing.md +4 -2
  45. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +63 -50
  46. package/framework/standards/frontend/design-system/ai-image-generation.md +42 -28
  47. package/framework/standards/frontend/design-system/ai-video-generation.md +24 -9
  48. package/framework/standards/frontend/nextjs/testing.md +4 -2
  49. package/framework/standards/integration/mcp/mcp-tools.md +64 -126
  50. package/framework/templates/code/dotnet/abstractions/result.cs +0 -2
  51. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +26 -39
  52. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +28 -68
  53. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +99 -117
  54. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +2 -44
  55. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +40 -61
  56. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +54 -107
  57. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +53 -58
  58. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +192 -281
  59. package/framework/templates/code/dotnet/database/migration.cs +49 -83
  60. package/framework/templates/code/dotnet/database/vector-entity.cs +2 -46
  61. package/framework/templates/code/dotnet/extensions/result-extensions.cs +0 -2
  62. package/framework/templates/code/dotnet/feature/validator.cs.hbs +11 -15
  63. package/framework/templates/code/dotnet/jobs/agent.cs +3 -51
  64. package/framework/templates/code/dotnet/jobs/job.cs +2 -97
  65. package/framework/templates/code/dotnet/test.cs +2 -87
  66. package/framework/templates/code/typescript/contracts.ts +140 -167
  67. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +1 -48
  68. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +3 -79
  69. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +1 -70
  70. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +1 -47
  71. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +2 -99
  72. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +0 -6
  73. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +3 -88
  74. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +1 -41
  75. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +0 -21
  76. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +1 -25
  77. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +2 -63
  78. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +0 -30
  79. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +1 -52
  80. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +3 -78
  81. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +4 -109
  82. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +0 -22
  83. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +1 -36
  84. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +2 -57
  85. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +0 -75
  86. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +2 -59
  87. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +4 -111
  88. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +0 -34
  89. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +1 -57
  90. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +0 -19
  91. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +2 -74
  92. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +2 -33
  93. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +1 -43
  94. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +1 -96
  95. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +0 -13
  96. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +0 -27
  97. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +0 -33
  98. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +0 -31
  99. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +3 -75
  100. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +0 -42
  101. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +0 -39
  102. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +1 -55
  103. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +3 -59
  104. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +0 -46
  105. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +3 -28
  106. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +7 -61
  107. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +2 -37
  108. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +1 -23
  109. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +2 -82
  110. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +1 -68
  111. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +1 -62
  112. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +2 -62
  113. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +1 -25
  114. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +0 -124
  115. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +1 -73
  116. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +1 -66
  117. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +0 -41
  118. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +2 -112
  119. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +2 -43
  120. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +5 -110
  121. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +7 -103
  122. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +1 -42
  123. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +1 -30
  124. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +1 -19
  125. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +0 -48
  126. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +1 -29
  127. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +2 -38
  128. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +1 -14
  129. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +4 -69
  130. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +1 -10
  131. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +1 -57
  132. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +0 -26
  133. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +1 -19
  134. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +0 -32
  135. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +0 -14
  136. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +0 -24
  137. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +0 -32
  138. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +0 -37
  139. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +0 -24
  140. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +0 -78
  141. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +0 -53
  142. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +0 -44
  143. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +0 -44
  144. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +0 -19
  145. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +0 -24
  146. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +6 -48
  147. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +1 -13
  148. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +2 -71
  149. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +5 -100
  150. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +39 -43
  151. package/framework/templates/frontend/nextjs/env.mjs.hbs +29 -32
  152. package/framework/templates/frontend/nextjs/page.tsx.hbs +21 -22
  153. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +42 -54
  154. package/framework/templates/integrations/asaas-client.cs +332 -387
  155. package/framework/templates/integrations/asaas-webhook.cs +290 -351
  156. package/framework/templates/integrations/neon-auth-config.cs +9 -105
  157. package/framework/templates/project/validate.js +97 -120
  158. package/package.json +2 -2
  159. package/src/commands/dashboard/dashboard.js +80 -53
  160. package/src/commands/mcp/mcp-setup.js +37 -9
  161. package/src/commands/project/doctor.js +12 -6
  162. package/src/commands/project/init.js +6 -3
  163. package/src/commands/project/install-plugin-cmd.js +1 -1
  164. package/src/commands/project/update.js +5 -0
  165. package/src/commands/project/worktree.js +60 -4
  166. package/src/commands/state/gate-check.js +5 -0
  167. package/src/commands/tasks/dag.js +27 -4
  168. package/src/commands/validation/verify.js +22 -38
  169. package/src/dashboard/app/App.jsx +80 -16
  170. package/src/dashboard/app/components/ActivityFeed.jsx +172 -0
  171. package/src/dashboard/app/components/CommandPalette.jsx +155 -0
  172. package/src/dashboard/app/components/DetailPanel.jsx +47 -11
  173. package/src/dashboard/app/components/EntityLink.jsx +55 -0
  174. package/src/dashboard/app/components/ErrorBoundary.jsx +54 -0
  175. package/src/dashboard/app/components/FlowGraph.jsx +19 -17
  176. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +53 -0
  177. package/src/dashboard/app/components/Markdown.jsx +47 -0
  178. package/src/dashboard/app/components/PhaseRail.jsx +111 -0
  179. package/src/dashboard/app/components/SearchFilter.jsx +40 -22
  180. package/src/dashboard/app/components/Sidebar.jsx +146 -37
  181. package/src/dashboard/app/hooks/useMorphQuery.js +54 -0
  182. package/src/dashboard/app/hooks/usePref.js +38 -0
  183. package/src/dashboard/app/lib/format.js +69 -0
  184. package/src/dashboard/app/lib/phases.js +56 -0
  185. package/src/dashboard/app/lib/project.js +50 -0
  186. package/src/dashboard/app/pages/Activity.jsx +24 -0
  187. package/src/dashboard/app/pages/AgentMap.jsx +371 -370
  188. package/src/dashboard/app/pages/FeatureDetail.jsx +604 -0
  189. package/src/dashboard/app/pages/Features.jsx +400 -0
  190. package/src/dashboard/app/pages/Guide.jsx +146 -0
  191. package/src/dashboard/app/pages/HookInspector.jsx +357 -322
  192. package/src/dashboard/app/pages/PhasePipeline.jsx +277 -408
  193. package/src/dashboard/app/pages/Portfolio.jsx +156 -0
  194. package/src/dashboard/app/pages/Rubrics.jsx +101 -0
  195. package/src/dashboard/app/pages/RulesMap.jsx +300 -273
  196. package/src/dashboard/app/pages/SkillsLibrary.jsx +483 -366
  197. package/src/dashboard/app/pages/Standards.jsx +407 -332
  198. package/src/dashboard/app/styles/index.css +187 -47
  199. package/src/dashboard/index.html +14 -12
  200. package/src/dashboard/package.json +32 -24
  201. package/src/dashboard/scanner/activity-scanner.js +351 -0
  202. package/src/dashboard/scanner/agents-scanner.js +59 -1
  203. package/src/dashboard/scanner/docs-scanner.js +59 -0
  204. package/src/dashboard/scanner/feature-status.js +10 -0
  205. package/src/dashboard/scanner/features-scanner.js +360 -0
  206. package/src/dashboard/scanner/index.js +14 -7
  207. package/src/dashboard/scanner/phases-scanner.js +14 -15
  208. package/src/dashboard/scanner/portfolio.js +153 -0
  209. package/src/dashboard/scanner/project-api.js +195 -0
  210. package/src/dashboard/scanner/rules-scanner.js +42 -37
  211. package/src/dashboard/scanner/skills-scanner.js +105 -43
  212. package/src/dashboard/scanner/standards-scanner.js +2 -0
  213. package/src/dashboard/vite-plugin-morph-data.js +134 -17
  214. package/src/lib/detectors/claude-config-detector.js +2 -7
  215. package/src/lib/detectors/index.js +5 -1
  216. package/src/lib/evaluator-report.js +22 -0
  217. package/src/lib/generators/settings-generator.js +208 -209
  218. package/src/lib/installers/mcp-installer.js +724 -719
  219. package/src/lib/installers/memory-installer.js +1 -2
  220. package/src/lib/projects/registry.js +154 -0
  221. package/src/lib/risk-detector.js +13 -0
  222. package/src/lib/tasks/tasks-json-parser.js +21 -0
  223. package/src/lib/validators/code-budget/code-budget-validator.js +32 -4
  224. package/src/lib/worktree/env-allocator.js +9 -3
  225. package/src/scripts/install-plugin.js +1 -1
  226. package/src/scripts/setup-infra.js +2 -4
  227. package/src/utils/agents-installer.js +108 -13
  228. package/src/utils/banner.js +1 -1
  229. package/src/commands/index.js +0 -13
  230. package/src/commands/project/checkpoint.js +0 -209
  231. package/src/core/state/phase-state-machine.js +0 -197
  232. package/src/dashboard/app/hooks/useDetailPanel.js +0 -15
  233. package/src/dashboard/app/pages/LiveState.jsx +0 -206
  234. package/src/dashboard/scanner/squads-scanner.js +0 -13
  235. package/src/dashboard/scanner/state-scanner.js +0 -85
  236. package/src/lib/generators/index.js +0 -10
  237. package/src/lib/hooks/hook-executor.js +0 -167
  238. package/src/lib/validators/checkpoint-persistence.js +0 -39
  239. package/src/lib/validators/shared/emit-validator-dispatch.js +0 -14
  240. package/src/lib/validators/shared/index.js +0 -12
  241. package/src/lib/validators/shared/issue-counter.js +0 -14
  242. package/src/lib/validators/shared/result-formatter.js +0 -124
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Many projects in one dashboard. Each project keeps its own live API
3
+ * (project-api.js: features, activity, files), created on first use; this
4
+ * module lists the projects, routes `/p/:id/…` to the right one, and answers
5
+ * which project a changed file belongs to.
6
+ *
7
+ * The list is the registry (src/lib/projects/registry.js) plus the project the
8
+ * dashboard was launched from, which is always shown even if it cannot be
9
+ * registered. Projects found next to the registered ones are offered, never
10
+ * added on their own.
11
+ *
12
+ * A cold project can take seconds to read (a 100k-event log). The portfolio
13
+ * never waits for one: `summary` is served only for projects already in
14
+ * memory, and the rest are read in the background (`warmNext`).
15
+ */
16
+
17
+ import { dirname, basename } from 'path';
18
+ import { createProjectApi } from './project-api.js';
19
+ import * as registryModule from '../../lib/projects/registry.js';
20
+
21
+ function slug(text) {
22
+ return String(text).toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '')
23
+ .replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'projeto';
24
+ }
25
+
26
+ const lower = (p) => (process.platform === 'win32' ? p.toLowerCase() : p);
27
+
28
+ /**
29
+ * @param {{ launchDir: string, registry?: object, createApi?: Function }} opts
30
+ * `registry` and `createApi` are test seams.
31
+ */
32
+ export function createPortfolio({ launchDir, registry = registryModule, createApi = createProjectApi }) {
33
+ const launchKey = registry.projectKey(launchDir);
34
+ const apis = new Map(); // dir key -> project api
35
+ // The last summary served per project: while a changed project is read
36
+ // again, the portfolio keeps showing it instead of going back to "reading".
37
+ const lastSummary = new Map(); // dir key -> summary
38
+
39
+ /** The projects shown, with stable ids derived from the folder name. */
40
+ function list() {
41
+ const dirs = registry.readRegistry().projects.map((p) => ({ dir: p.dir, name: p.name, source: p.source }));
42
+ if (!dirs.some((p) => lower(p.dir) === lower(launchKey))) {
43
+ dirs.unshift({ dir: launchKey, name: basename(launchKey), source: 'launch' });
44
+ }
45
+ const used = new Map();
46
+ return dirs.map((p) => {
47
+ const base = slug(basename(p.dir));
48
+ const n = (used.get(base) || 0) + 1;
49
+ used.set(base, n);
50
+ return { ...p, id: n === 1 ? base : `${base}-${n}`, launch: lower(p.dir) === lower(launchKey) };
51
+ });
52
+ }
53
+
54
+ function apiFor(dir) {
55
+ const key = lower(dir);
56
+ if (!apis.has(key)) apis.set(key, createApi(dir));
57
+ return apis.get(key);
58
+ }
59
+
60
+ function byId(id) {
61
+ return list().find((p) => p.id === id) || null;
62
+ }
63
+
64
+ /** The project a changed path belongs to — the deepest registered folder containing it. */
65
+ function ownerOf(path) {
66
+ const p = lower(String(path).replace(/\\/g, '/'));
67
+ let best = null;
68
+ for (const proj of list()) {
69
+ const d = lower(proj.dir);
70
+ if ((p === d || p.startsWith(`${d}/`)) && (!best || d.length > best.dir.length)) best = { ...proj, dir: d };
71
+ }
72
+ return best ? list().find((x) => x.id === best.id) : null;
73
+ }
74
+
75
+ function suggestions() {
76
+ const projects = list();
77
+ const roots = [...new Set(projects.map((p) => dirname(p.dir)))];
78
+ return registry.discoverProjects(roots).filter((s) => !projects.some((p) => lower(p.dir) === lower(s.dir)));
79
+ }
80
+
81
+ /** Read the next project that is not in memory yet. @returns {object|null} the project warmed */
82
+ function warmNext() {
83
+ const next = list().find((p) => !apiFor(p.dir).ready());
84
+ if (!next) return null;
85
+ apiFor(next.dir).warm();
86
+ return next;
87
+ }
88
+
89
+ function projectsBody() {
90
+ return {
91
+ launch: list().find((p) => p.launch)?.id || null,
92
+ projects: list().map((p) => {
93
+ const api = apiFor(p.dir);
94
+ let summary = lastSummary.get(lower(p.dir)) || null;
95
+ if (api.ready()) {
96
+ try {
97
+ summary = api.summary();
98
+ lastSummary.set(lower(p.dir), summary);
99
+ } catch { /* keep the last one */ }
100
+ }
101
+ return { id: p.id, dir: p.dir, name: summary?.name || p.name, launch: p.launch, source: p.source, summary };
102
+ }),
103
+ suggestions: suggestions(),
104
+ };
105
+ }
106
+
107
+ const json = (status, body) => ({ status, body });
108
+
109
+ /**
110
+ * @param {string} method
111
+ * @param {string} url - relative to `/api/morph`
112
+ * @param {object|null} [body] - parsed JSON of a POST
113
+ */
114
+ function handle(method, url, body = null) {
115
+ const u = new URL(url, 'http://local');
116
+ const parts = u.pathname.split('/').filter(Boolean);
117
+ try {
118
+ if (parts[0] === 'projects' && parts.length === 1) {
119
+ if (method === 'GET') return json(200, projectsBody());
120
+ if (method === 'POST') {
121
+ const dir = body?.dir;
122
+ if (typeof dir !== 'string' || !dir) return json(400, { error: 'informe o caminho do projeto' });
123
+ if (!registry.isMorphProject(dir)) return json(400, { error: 'esta pasta não tem .morph/config/config.json: rode morph-spec init nela primeiro' });
124
+ return registry.registerProject(dir, 'manual')
125
+ ? json(200, projectsBody())
126
+ : json(400, { error: 'não foi possível registrar esta pasta' });
127
+ }
128
+ if (method === 'DELETE') {
129
+ const dir = u.searchParams.get('dir') || '';
130
+ registry.unregisterProject(dir);
131
+ lastSummary.delete(lower(registry.projectKey(dir)));
132
+ return json(200, projectsBody());
133
+ }
134
+ return json(405, { error: 'método não suportado' });
135
+ }
136
+ if (parts[0] === 'p' && parts.length >= 2) {
137
+ const project = byId(decodeURIComponent(parts[1]));
138
+ if (!project) return json(404, { error: `projeto "${parts[1]}" não está no portfólio` });
139
+ const rest = `/${parts.slice(2).join('/')}${u.search}`;
140
+ return apiFor(project.dir).handle(rest);
141
+ }
142
+ return json(404, { error: 'rota desconhecida' });
143
+ } catch (err) {
144
+ return json(500, { error: err?.message || String(err) });
145
+ }
146
+ }
147
+
148
+ function invalidate(project, scope, path, event) {
149
+ apiFor(project.dir).invalidate(scope, path, event);
150
+ }
151
+
152
+ return { list, byId, ownerOf, handle, invalidate, warmNext, suggestions };
153
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * The live half of the dashboard: the project's features and activity, served
3
+ * as JSON by the Vite dev server (`/api/morph/*`) and refreshed when `.morph/`
4
+ * changes on disk. The framework knowledge stays in the static virtual module.
5
+ *
6
+ * Two caches with two lifetimes. A running agent appends to `events.jsonl` on
7
+ * every tool call, so a log change drops only the event cache — rescanning the
8
+ * features (git calls for the active ones) on each tool call would be waste.
9
+ */
10
+
11
+ import { join } from 'path';
12
+ import { existsSync, readdirSync } from 'fs';
13
+ import { scanFeatures, readFeatureDetail, readFeatureFile } from './features-scanner.js';
14
+ import { createEventStore, buildFeed, lastActivityByFeature, ALL_KINDS, DEFAULT_KINDS } from './activity-scanner.js';
15
+ import { WORKTREE_DIR_NAMES } from '../../lib/worktree/dirs.js';
16
+ import { isFinished } from './feature-status.js';
17
+ import { resolveFeatureTree } from '../../lib/worktree/feature-truth.js';
18
+
19
+ /** How long a remembered "which tree owns this feature" answer is trusted without a structural change. */
20
+ const TREE_TTL_MS = 60_000;
21
+
22
+ /**
23
+ * Which cache a changed path invalidates, or null when it is none of ours.
24
+ * `.morph/framework/` is the installed framework copy (often a junction), and
25
+ * it never changes what this API returns.
26
+ *
27
+ * @param {string} path
28
+ * @returns {'activity'|'features'|null}
29
+ */
30
+ export function classifyChange(path) {
31
+ const p = String(path).replace(/\\/g, '/');
32
+ if (!p.includes('/.morph/') && !p.endsWith('/.morph')) return null;
33
+ if (p.includes('/.morph/framework/') || p.endsWith('/.morph/framework')) return null;
34
+ if (p.includes('/.morph/logs/')) return 'activity';
35
+ if (/\/\.morph\/(features|archive|config)(\/|$)/.test(p) || p.endsWith('/.morph/state.json')) return 'features';
36
+ return null;
37
+ }
38
+
39
+ /**
40
+ * A full-page reload Vite would send for a file under a project's `.morph/`
41
+ * (an agent writing a design .html). The dashboard's own code never lives
42
+ * there, so such a reload is never needed.
43
+ */
44
+ export function isMorphDataReload(path) {
45
+ return typeof path === 'string' && path.replace(/\\/g, '/').includes('/.morph/');
46
+ }
47
+
48
+ /**
49
+ * The directories worth watching: the root's and each worktree's feature,
50
+ * archive, log and config folders — never a whole worktree (its node_modules).
51
+ */
52
+ export function watchTargets(projectDir) {
53
+ const morphDirs = [join(projectDir, '.morph')];
54
+ for (const name of WORKTREE_DIR_NAMES) {
55
+ let entries = [];
56
+ try { entries = readdirSync(join(projectDir, name), { withFileTypes: true }); } catch { continue; }
57
+ for (const e of entries) {
58
+ if (e.isDirectory()) morphDirs.push(join(projectDir, name, e.name, '.morph'));
59
+ }
60
+ }
61
+ const targets = [];
62
+ for (const dir of morphDirs) {
63
+ for (const sub of ['features', 'archive', 'logs', 'config', 'state.json']) {
64
+ const p = join(dir, sub);
65
+ if (existsSync(p)) targets.push(p);
66
+ }
67
+ }
68
+ return targets;
69
+ }
70
+
71
+ function json(status, body) {
72
+ return { status, body };
73
+ }
74
+
75
+ /**
76
+ * @param {string} projectDir
77
+ * @param {{ scanFeatures?: Function, readFeatureDetail?: Function,
78
+ * readFeatureFile?: Function, loadEvents?: Function, resolveTree?: Function,
79
+ * now?: () => number }} [deps] - test seams
80
+ */
81
+ export function createProjectApi(projectDir, deps = {}) {
82
+ const scan = deps.scanFeatures || scanFeatures;
83
+ const detail = deps.readFeatureDetail || readFeatureDetail;
84
+ const file = deps.readFeatureFile || readFeatureFile;
85
+ const store = createEventStore(projectDir, deps.loadEvents
86
+ ? { loadTrees: () => [{ tree: 'root', path: projectDir, events: deps.loadEvents(projectDir) }] }
87
+ : {});
88
+ const cache = { features: null };
89
+
90
+ // Which tree owns a feature costs a few git calls (~0.3 s each on Windows)
91
+ // and only changes when a file or folder appears or disappears — never when
92
+ // an agent rewrites feature.json or tasks.json, which is what happens all
93
+ // the time. Remembered per feature; a structural change or TTL_MS forgets it.
94
+ const resolveTree = deps.resolveTree || resolveFeatureTree;
95
+ const now = deps.now || Date.now;
96
+ const trees = new Map();
97
+ const rememberedTree = (name, dir) => {
98
+ const hit = trees.get(name);
99
+ if (hit && now() - hit.at < TREE_TTL_MS) return hit.value;
100
+ const value = resolveTree(name, dir);
101
+ trees.set(name, { value, at: now() });
102
+ return value;
103
+ };
104
+
105
+ const features = () => (cache.features ||= scan(projectDir, { resolveTree: rememberedTree }));
106
+ const events = () => store.events();
107
+ // Where the list scan found each feature: opening one reuses it instead of
108
+ // asking git again.
109
+ const known = (name) => features().features?.find((f) => f.name === name) || undefined;
110
+
111
+ /**
112
+ * @param {'features'|'activity'|'all'} scope
113
+ * @param {string} [path] - the file that changed; a live log is then tailed
114
+ * instead of reloading every log
115
+ * @param {string} [event] - the watcher event; anything but 'change' (a file
116
+ * or folder added or removed) may move a feature to another tree
117
+ */
118
+ function invalidate(scope, path, event) {
119
+ if (scope === 'features' || scope === 'all') cache.features = null;
120
+ if (scope === 'all' || (scope === 'features' && event !== 'change')) trees.clear();
121
+ if (scope === 'all') store.reset();
122
+ else if (scope === 'activity') store.changed(path);
123
+ }
124
+
125
+ /**
126
+ * @param {string} url - path + query, relative to `/api/morph`
127
+ * @returns {{ status: number, body: object }}
128
+ */
129
+ function handle(url) {
130
+ const u = new URL(url, 'http://local');
131
+ const parts = u.pathname.split('/').filter(Boolean).map(decodeURIComponent);
132
+ try {
133
+ if (parts[0] === 'features' && parts.length === 1) {
134
+ const scan = features();
135
+ const last = lastActivityByFeature(events());
136
+ return json(200, { ...scan, features: scan.features.map((f) => ({ ...f, lastActivity: last[f.name] || null })) });
137
+ }
138
+ if (parts[0] === 'features' && parts.length === 2) {
139
+ const d = detail(projectDir, parts[1], { known: known(parts[1]), resolveTree: rememberedTree });
140
+ return d ? json(200, d) : json(404, { error: `feature "${parts[1]}" não encontrada` });
141
+ }
142
+ if (parts[0] === 'file' && parts.length === 1) {
143
+ const name = u.searchParams.get('feature') || '';
144
+ const r = file(projectDir, name, u.searchParams.get('path') || '', { known: known(name), resolveTree: rememberedTree });
145
+ return r.error ? json(r.status, { error: r.error }) : json(200, r);
146
+ }
147
+ if (parts[0] === 'activity' && parts.length === 1) {
148
+ const feature = u.searchParams.get('feature') || undefined;
149
+ const kindsParam = u.searchParams.get('kinds');
150
+ const kinds = kindsParam ? kindsParam.split(',').filter((k) => ALL_KINDS.includes(k)) : DEFAULT_KINDS;
151
+ const limit = Math.min(Number(u.searchParams.get('limit')) || 100, 500);
152
+ const all = events();
153
+ const feed = buildFeed(all, { feature, kinds, before: u.searchParams.get('before') || undefined, limit });
154
+ return json(200, { ...feed, kinds, cost: feature ? store.cost(feature) : null });
155
+ }
156
+ return json(404, { error: 'rota desconhecida' });
157
+ } catch (err) {
158
+ return json(500, { error: err?.message || String(err) });
159
+ }
160
+ }
161
+
162
+ /** Load everything once, so the first page a person opens does not pay for it. */
163
+ function warm() {
164
+ try { features(); events(); } catch { /* the first request will report it */ }
165
+ }
166
+
167
+ /** Cheap to answer now: the list and the events are both in memory. */
168
+ function ready() {
169
+ return cache.features !== null && store.ready();
170
+ }
171
+
172
+ /**
173
+ * What the portfolio shows of this project: counts, the active features with
174
+ * their last activity, and when anything last happened.
175
+ */
176
+ function summary() {
177
+ const scan = features();
178
+ const last = lastActivityByFeature(events());
179
+ const active = scan.features.filter((f) => !isFinished(f)).map((f) => ({ ...f, lastActivity: last[f.name] || null }));
180
+ const lastTs = Object.values(last).reduce((m, a) => (a.ts > m ? a.ts : m), '');
181
+ return {
182
+ name: scan.project.name,
183
+ stack: scan.project.stack,
184
+ trust: scan.project.trust,
185
+ legacyStateOnly: scan.project.legacyStateOnly,
186
+ total: scan.features.length,
187
+ active: active.length,
188
+ awaiting: active.filter((f) => f.awaiting).length,
189
+ activeFeatures: active,
190
+ lastActivityTs: lastTs || null,
191
+ };
192
+ }
193
+
194
+ return { handle, invalidate, warm, ready, summary };
195
+ }
@@ -1,37 +1,42 @@
1
- import { readFileSync, readdirSync, existsSync } from 'fs';
2
- import { join, basename } from 'path';
3
-
4
- function parseFrontmatter(content) {
5
- const match = content.match(/^---\n([\s\S]*?)\n---/);
6
- if (!match) return { paths: [], stacks: [] };
7
- const yaml = match[1];
8
- const paths = []; const stacks = []; let currentList = null;
9
- for (const line of yaml.split('\n')) {
10
- if (line.startsWith('paths:')) { currentList = paths; continue; }
11
- if (line.startsWith('stacks:')) { currentList = stacks; continue; }
12
- if (line.match(/^\w/)) { currentList = null; continue; }
13
- if (currentList !== null) {
14
- const m = line.match(/^\s+-\s+"?(.+?)"?\s*$/);
15
- if (m) currentList.push(m[1]);
16
- }
17
- }
18
- return { paths, stacks };
19
- }
20
-
21
- function extractStandardRefs(content) {
22
- const refs = [];
23
- const regex = /@\.morph\/framework\/standards\/(.+\.md)/g;
24
- let m;
25
- while ((m = regex.exec(content)) !== null) refs.push(m[1]);
26
- return refs;
27
- }
28
-
29
- export function scanRules(frameworkDir) {
30
- const dir = join(frameworkDir, 'rules');
31
- if (!existsSync(dir)) return [];
32
- return readdirSync(dir).filter(f => f.endsWith('.md')).map(f => {
33
- const content = readFileSync(join(dir, f), 'utf8');
34
- const { paths, stacks } = parseFrontmatter(content);
35
- return { name: basename(f, '.md'), filename: f, paths, stacks, content, standardRefs: extractStandardRefs(content) };
36
- });
37
- }
1
+ import { readFileSync, readdirSync, existsSync } from 'fs';
2
+ import { join, basename } from 'path';
3
+
4
+ function parseFrontmatter(content) {
5
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
6
+ if (!match) return { paths: [], stacks: [] };
7
+ const yaml = match[1];
8
+ const paths = []; const stacks = []; let currentList = null;
9
+ for (const line of yaml.split('\n')) {
10
+ if (line.startsWith('paths:')) { currentList = paths; continue; }
11
+ if (line.startsWith('stacks:')) { currentList = stacks; continue; }
12
+ if (line.match(/^\w/)) { currentList = null; continue; }
13
+ if (currentList !== null) {
14
+ const m = line.match(/^\s+-\s+"?(.+?)"?\s*$/);
15
+ if (m) currentList.push(m[1]);
16
+ }
17
+ }
18
+ return { paths, stacks };
19
+ }
20
+
21
+ /**
22
+ * Standards a rule imports, each once. RulesMap keys an edge per ref, so a
23
+ * rule that imports the same standard twice (infrastructure-standards.md did,
24
+ * up to 8.38.0) drew two edges under one React key.
25
+ */
26
+ export function extractStandardRefs(content) {
27
+ const refs = new Set();
28
+ const regex = /@\.morph\/framework\/standards\/(.+\.md)/g;
29
+ let m;
30
+ while ((m = regex.exec(content)) !== null) refs.add(m[1]);
31
+ return [...refs];
32
+ }
33
+
34
+ export function scanRules(frameworkDir) {
35
+ const dir = join(frameworkDir, 'rules');
36
+ if (!existsSync(dir)) return [];
37
+ return readdirSync(dir).filter(f => f.endsWith('.md')).map(f => {
38
+ const content = readFileSync(join(dir, f), 'utf8');
39
+ const { paths, stacks } = parseFrontmatter(content);
40
+ return { name: basename(f, '.md'), filename: f, paths, stacks, content, standardRefs: extractStandardRefs(content) };
41
+ });
42
+ }
@@ -1,43 +1,105 @@
1
- import { readFileSync, readdirSync, existsSync } from 'fs';
2
- import { join } from 'path';
3
-
4
- function parseYamlFrontmatter(content) {
5
- const match = content.match(/^---\n([\s\S]*?)\n---/);
6
- if (!match) return {};
7
- const yaml = {};
8
- for (const line of match[1].split('\n')) {
9
- const colonIdx = line.indexOf(':');
10
- if (colonIdx === -1) continue;
11
- const key = line.slice(0, colonIdx).trim();
12
- let value = line.slice(colonIdx + 1).trim();
13
- if (value === 'true') value = true;
14
- else if (value === 'false') value = false;
15
- yaml[key] = value;
16
- }
17
- return yaml;
18
- }
19
-
20
- export function scanSkills(frameworkDir) {
21
- const skillsDir = join(frameworkDir, 'skills');
22
- if (!existsSync(skillsDir)) return [];
23
- const skills = [];
24
- for (const levelDir of readdirSync(skillsDir, { withFileTypes: true })) {
25
- if (!levelDir.isDirectory()) continue;
26
- const levelPath = join(skillsDir, levelDir.name);
27
- for (const skillDir of readdirSync(levelPath, { withFileTypes: true })) {
28
- if (!skillDir.isDirectory()) continue;
29
- const skillPath = join(levelPath, skillDir.name, 'SKILL.md');
30
- if (!existsSync(skillPath)) continue;
31
- const content = readFileSync(skillPath, 'utf8');
32
- const fm = parseYamlFrontmatter(content);
33
- skills.push({
34
- id: skillDir.name, name: fm.name || skillDir.name,
35
- description: fm.description || '', level: levelDir.name,
36
- userInvocable: fm['user-invocable'] === true || fm['user-invocable'] === 'true',
37
- allowedTools: typeof fm['allowed-tools'] === 'string' ? fm['allowed-tools'].split(',').map(t => t.trim()) : [],
38
- content,
39
- });
40
- }
41
- }
42
- return skills;
43
- }
1
+ import { readFileSync, readdirSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ /** One-line `key: value` frontmatter. CRLF-safe: a checkout with autocrlf is still read. */
5
+ export function parseFrontmatter(content) {
6
+ const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
7
+ if (!match) return {};
8
+ const out = {};
9
+ for (const line of match[1].split(/\r?\n/)) {
10
+ const idx = line.indexOf(':');
11
+ if (idx === -1) continue;
12
+ const key = line.slice(0, idx).trim();
13
+ let value = line.slice(idx + 1).trim().replace(/^["']|["']$/g, '');
14
+ if (value === 'true') value = true;
15
+ else if (value === 'false') value = false;
16
+ out[key] = value;
17
+ }
18
+ return out;
19
+ }
20
+
21
+ const FENCE = /^(\s*)```/;
22
+ const DISPATCH_HINT = /dispatch|despach|subagent_type|Template do prompt/i;
23
+ const PERSONA_REF = /morph-(\{id\}|\{task\.agent\}|[a-z][a-z-]*[a-z])/g;
24
+ /** Words after `morph-` that are skills or CLI verbs, never personas. */
25
+ /** Claude Code's own sub-agents: dispatched by skills, but not personas of agents.json. */
26
+ const BUILTIN_AGENT = /`(Explore|general-purpose|Plan)`/g;
27
+ const NOT_PERSONA = /^(spec|proposal|brainstorming|stack-scan|uiux|plan|implement|apply|review|eval|hotfix|standards|media|task)$/;
28
+
29
+ /**
30
+ * The prompts a skill tells the orchestrator to send to a sub-agent: fenced
31
+ * blocks that are either written as a dispatch (`dispatch Agent (…)`) or sit
32
+ * right under a line about dispatching. Each carries the personas it names —
33
+ * `generic` when the template is `morph-{id}` (any implementer).
34
+ *
35
+ * @returns {Array<{ index: number, personas: string[], generic: boolean, builtin: string[], context: string, text: string }>}
36
+ */
37
+ export function extractDispatchPrompts(content) {
38
+ const lines = content.split(/\r?\n/);
39
+ const out = [];
40
+ for (let i = 0; i < lines.length; i++) {
41
+ const open = FENCE.exec(lines[i]);
42
+ if (!open) continue;
43
+ let j = i + 1;
44
+ while (j < lines.length && !FENCE.test(lines[j])) j++;
45
+ const body = lines.slice(i + 1, j);
46
+ const text = body.map((l) => l.slice(Math.min(open[1].length, l.length - l.trimStart().length))).join('\n');
47
+ // Only the sentence that introduces the block counts: a paragraph further up
48
+ // that mentions a dispatch (morph-apply's DAG example) is not this block's.
49
+ let k = i - 1;
50
+ while (k >= 0 && lines[k].trim() === '') k--;
51
+ const before = k >= 0 && i - k <= 2 ? lines[k] : '';
52
+ const builtin = [...before.matchAll(BUILTIN_AGENT)].map((m) => m[1]);
53
+ const isDispatch = /^\s*dispatch\s+Agent/i.test(body[0] || '')
54
+ || (DISPATCH_HINT.test(before) && (/morph-/.test(before + text) || builtin.length > 0));
55
+ if (isDispatch) {
56
+ const refsIn = (str) => [...str.matchAll(PERSONA_REF)].map((m) => m[1]);
57
+ const personasOf = (refs) => [...new Set(refs.filter((r) => !r.startsWith('{') && !NOT_PERSONA.test(r)))];
58
+ const refs = refsIn(`${before}\n${text}`);
59
+ const generic = refs.some((r) => r.startsWith('{'));
60
+ let personas = personasOf(refs);
61
+ // "Template do prompt:" names no one; the step that introduces it does.
62
+ if (!generic && personas.length === 0 && builtin.length === 0) {
63
+ personas = personasOf(refsIn(lines.slice(Math.max(0, k - 8), k).join('\n')));
64
+ }
65
+ const others = !generic && personas.length === 0 ? [...new Set(builtin)] : [];
66
+ out.push({ index: out.length, personas, generic, builtin: others, context: before.trim().slice(0, 240), text });
67
+ }
68
+ i = j;
69
+ }
70
+ return out;
71
+ }
72
+
73
+ export function scanSkills(frameworkDir, phases = []) {
74
+ const skillsDir = join(frameworkDir, 'skills');
75
+ if (!existsSync(skillsDir)) return [];
76
+ const phaseOf = new Map(phases.filter((p) => p.skill).map((p) => [p.skill, p.id]));
77
+ const skills = [];
78
+ for (const levelDir of readdirSync(skillsDir, { withFileTypes: true })) {
79
+ if (!levelDir.isDirectory()) continue;
80
+ const levelPath = join(skillsDir, levelDir.name);
81
+ for (const skillDir of readdirSync(levelPath, { withFileTypes: true })) {
82
+ if (!skillDir.isDirectory()) continue;
83
+ const skillPath = join(levelPath, skillDir.name, 'SKILL.md');
84
+ if (!existsSync(skillPath)) continue;
85
+ const content = readFileSync(skillPath, 'utf8');
86
+ const fm = parseFrontmatter(content);
87
+ // The framework spells it `user-invokable`; the Claude Code docs say `user-invocable`.
88
+ const invocable = fm['user-invokable'] ?? fm['user-invocable'];
89
+ skills.push({
90
+ id: skillDir.name,
91
+ name: fm.name || skillDir.name,
92
+ description: fm.description || '',
93
+ whenToInvoke: fm['when-to-invoke'] || '',
94
+ level: levelDir.name,
95
+ userInvocable: invocable === true || invocable === 'true',
96
+ stacks: typeof fm.stacks === 'string' ? fm.stacks.replace(/[[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean) : [],
97
+ allowedTools: typeof fm['allowed-tools'] === 'string' ? fm['allowed-tools'].split(',').map((t) => t.trim()) : [],
98
+ phase: phaseOf.get(skillDir.name) || null,
99
+ dispatches: extractDispatchPrompts(content),
100
+ content,
101
+ });
102
+ }
103
+ }
104
+ return skills.sort((a, b) => a.id.localeCompare(b.id));
105
+ }
@@ -31,6 +31,8 @@ export function scanStandards(frameworkDir, agents) {
31
31
  id: std.id, name: std.name, path: std.path, category: std.category,
32
32
  subcategory: std.subcategory || null, tags: std.tags || [],
33
33
  digest: std.digest || '', stacks: std.stacks || [],
34
+ aliases: std.aliases || [], anchors: std.anchors || {},
35
+ provedBy: std.provedBy || [], verifiedAgainst: std.verifiedAgainst || null,
34
36
  content: existsSync(mdPath) ? readFileSync(mdPath, 'utf8') : '',
35
37
  referencedBy: refMap[std.id] || [],
36
38
  };