@polymorphism-tech/morph-spec 8.38.0 → 8.40.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 (227) hide show
  1. package/CLAUDE.md +19 -8
  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 +3 -2
  8. package/framework/MORPH.md +1 -1
  9. package/framework/agents.json +36 -25
  10. package/framework/docs/CLI.md +2 -2
  11. package/framework/evals/rubrics/code-quality.md +4 -4
  12. package/framework/evals/rubrics/condensed.md +2 -2
  13. package/framework/mcp/registry.json +33 -2
  14. package/framework/rules/infrastructure-standards.md +0 -1
  15. package/framework/rules/morph-workflow.md +9 -4
  16. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  17. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  18. package/framework/skills/core/morph-apply/SKILL.md +5 -1
  19. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  20. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  21. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  22. package/framework/skills/core/morph-implement/SKILL.md +6 -1
  23. package/framework/skills/core/morph-media/SKILL.md +1 -1
  24. package/framework/skills/core/morph-plan/SKILL.md +4 -3
  25. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  26. package/framework/skills/core/morph-review/SKILL.md +1 -1
  27. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  28. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  29. package/framework/skills/core/morph-uiux/SKILL.md +6 -5
  30. package/framework/standards/STANDARDS.json +3 -3
  31. package/framework/standards/ai-agents/context-providers.md +16 -26
  32. package/framework/standards/ai-agents/durable-workflows-hitl.md +15 -21
  33. package/framework/standards/ai-agents/modalities-image-gen.md +21 -25
  34. package/framework/standards/ai-agents/modalities-speech-to-text.md +4 -15
  35. package/framework/standards/ai-agents/modalities-text-to-speech.md +2 -11
  36. package/framework/standards/ai-agents/modalities-vision.md +1 -18
  37. package/framework/standards/ai-agents/prompt-sources.md +0 -4
  38. package/framework/standards/architecture/code-comments.md +52 -16
  39. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +63 -50
  40. package/framework/standards/frontend/design-system/ai-image-generation.md +5 -5
  41. package/framework/standards/integration/mcp/mcp-tools.md +48 -117
  42. package/framework/templates/code/dotnet/abstractions/result.cs +0 -2
  43. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +26 -39
  44. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +28 -68
  45. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +99 -117
  46. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +2 -44
  47. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +40 -61
  48. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +54 -107
  49. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +53 -58
  50. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +192 -281
  51. package/framework/templates/code/dotnet/database/migration.cs +49 -83
  52. package/framework/templates/code/dotnet/database/vector-entity.cs +2 -46
  53. package/framework/templates/code/dotnet/extensions/result-extensions.cs +0 -2
  54. package/framework/templates/code/dotnet/feature/validator.cs.hbs +11 -15
  55. package/framework/templates/code/dotnet/jobs/agent.cs +3 -51
  56. package/framework/templates/code/dotnet/jobs/job.cs +2 -97
  57. package/framework/templates/code/dotnet/test.cs +2 -87
  58. package/framework/templates/code/typescript/contracts.ts +140 -167
  59. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +1 -48
  60. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +3 -79
  61. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +1 -70
  62. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +1 -47
  63. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +2 -99
  64. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +0 -6
  65. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +3 -88
  66. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +1 -41
  67. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +0 -21
  68. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +1 -25
  69. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +2 -63
  70. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +0 -30
  71. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +1 -52
  72. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +3 -78
  73. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +4 -109
  74. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +0 -22
  75. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +1 -36
  76. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +2 -57
  77. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +0 -75
  78. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +2 -59
  79. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +4 -111
  80. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +0 -34
  81. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +1 -57
  82. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +0 -19
  83. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +2 -74
  84. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +2 -33
  85. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +1 -43
  86. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +1 -96
  87. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +0 -13
  88. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +0 -27
  89. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +0 -33
  90. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +0 -31
  91. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +3 -75
  92. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +0 -42
  93. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +0 -39
  94. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +1 -55
  95. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +3 -59
  96. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +0 -46
  97. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +3 -28
  98. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +7 -61
  99. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +2 -37
  100. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +1 -23
  101. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +2 -82
  102. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +1 -68
  103. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +1 -62
  104. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +2 -62
  105. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +1 -25
  106. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +0 -124
  107. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +1 -73
  108. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +1 -66
  109. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +0 -41
  110. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +2 -112
  111. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +2 -43
  112. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +5 -110
  113. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +7 -103
  114. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +1 -42
  115. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +1 -30
  116. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +1 -19
  117. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +0 -48
  118. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +1 -29
  119. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +2 -38
  120. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +1 -14
  121. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +4 -69
  122. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +1 -10
  123. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +1 -57
  124. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +0 -26
  125. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +1 -19
  126. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +0 -32
  127. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +0 -14
  128. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +0 -24
  129. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +0 -32
  130. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +0 -37
  131. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +0 -24
  132. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +0 -78
  133. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +0 -53
  134. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +0 -44
  135. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +0 -44
  136. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +0 -19
  137. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +0 -24
  138. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +6 -48
  139. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +1 -13
  140. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +2 -71
  141. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +5 -100
  142. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +39 -43
  143. package/framework/templates/frontend/nextjs/env.mjs.hbs +29 -32
  144. package/framework/templates/frontend/nextjs/page.tsx.hbs +21 -22
  145. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +42 -54
  146. package/framework/templates/integrations/asaas-client.cs +332 -387
  147. package/framework/templates/integrations/asaas-webhook.cs +290 -351
  148. package/framework/templates/integrations/neon-auth-config.cs +9 -105
  149. package/framework/templates/project/validate.js +97 -120
  150. package/package.json +2 -2
  151. package/src/commands/dashboard/dashboard.js +80 -53
  152. package/src/commands/mcp/mcp-setup.js +37 -9
  153. package/src/commands/project/doctor.js +3 -5
  154. package/src/commands/project/init.js +6 -3
  155. package/src/commands/project/install-plugin-cmd.js +1 -1
  156. package/src/commands/project/update.js +5 -0
  157. package/src/dashboard/app/App.jsx +80 -16
  158. package/src/dashboard/app/components/ActivityFeed.jsx +172 -0
  159. package/src/dashboard/app/components/CommandPalette.jsx +155 -0
  160. package/src/dashboard/app/components/DetailPanel.jsx +47 -11
  161. package/src/dashboard/app/components/EntityLink.jsx +55 -0
  162. package/src/dashboard/app/components/ErrorBoundary.jsx +54 -0
  163. package/src/dashboard/app/components/FlowGraph.jsx +19 -17
  164. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +53 -0
  165. package/src/dashboard/app/components/Markdown.jsx +47 -0
  166. package/src/dashboard/app/components/PhaseRail.jsx +111 -0
  167. package/src/dashboard/app/components/SearchFilter.jsx +40 -22
  168. package/src/dashboard/app/components/Sidebar.jsx +146 -37
  169. package/src/dashboard/app/hooks/useMorphQuery.js +54 -0
  170. package/src/dashboard/app/hooks/usePref.js +38 -0
  171. package/src/dashboard/app/lib/format.js +69 -0
  172. package/src/dashboard/app/lib/phases.js +56 -0
  173. package/src/dashboard/app/lib/project.js +50 -0
  174. package/src/dashboard/app/pages/Activity.jsx +24 -0
  175. package/src/dashboard/app/pages/AgentMap.jsx +371 -370
  176. package/src/dashboard/app/pages/FeatureDetail.jsx +604 -0
  177. package/src/dashboard/app/pages/Features.jsx +400 -0
  178. package/src/dashboard/app/pages/Guide.jsx +146 -0
  179. package/src/dashboard/app/pages/HookInspector.jsx +357 -322
  180. package/src/dashboard/app/pages/PhasePipeline.jsx +277 -408
  181. package/src/dashboard/app/pages/Portfolio.jsx +156 -0
  182. package/src/dashboard/app/pages/Rubrics.jsx +101 -0
  183. package/src/dashboard/app/pages/RulesMap.jsx +300 -273
  184. package/src/dashboard/app/pages/SkillsLibrary.jsx +483 -366
  185. package/src/dashboard/app/pages/Standards.jsx +407 -332
  186. package/src/dashboard/app/styles/index.css +187 -47
  187. package/src/dashboard/index.html +14 -12
  188. package/src/dashboard/package.json +32 -24
  189. package/src/dashboard/scanner/activity-scanner.js +351 -0
  190. package/src/dashboard/scanner/agents-scanner.js +59 -1
  191. package/src/dashboard/scanner/docs-scanner.js +59 -0
  192. package/src/dashboard/scanner/feature-status.js +10 -0
  193. package/src/dashboard/scanner/features-scanner.js +360 -0
  194. package/src/dashboard/scanner/index.js +14 -7
  195. package/src/dashboard/scanner/phases-scanner.js +14 -15
  196. package/src/dashboard/scanner/portfolio.js +153 -0
  197. package/src/dashboard/scanner/project-api.js +195 -0
  198. package/src/dashboard/scanner/rules-scanner.js +42 -37
  199. package/src/dashboard/scanner/skills-scanner.js +105 -43
  200. package/src/dashboard/scanner/standards-scanner.js +2 -0
  201. package/src/dashboard/vite-plugin-morph-data.js +134 -17
  202. package/src/lib/detectors/claude-config-detector.js +2 -7
  203. package/src/lib/detectors/index.js +5 -1
  204. package/src/lib/evaluator-report.js +22 -0
  205. package/src/lib/generators/settings-generator.js +208 -209
  206. package/src/lib/installers/mcp-installer.js +724 -719
  207. package/src/lib/installers/memory-installer.js +1 -2
  208. package/src/lib/projects/registry.js +154 -0
  209. package/src/lib/validators/code-budget/code-budget-validator.js +32 -4
  210. package/src/scripts/install-plugin.js +1 -1
  211. package/src/scripts/setup-infra.js +2 -4
  212. package/src/utils/agents-installer.js +57 -12
  213. package/src/utils/banner.js +1 -1
  214. package/src/commands/index.js +0 -13
  215. package/src/commands/project/checkpoint.js +0 -209
  216. package/src/core/state/phase-state-machine.js +0 -197
  217. package/src/dashboard/app/hooks/useDetailPanel.js +0 -15
  218. package/src/dashboard/app/pages/LiveState.jsx +0 -206
  219. package/src/dashboard/scanner/squads-scanner.js +0 -13
  220. package/src/dashboard/scanner/state-scanner.js +0 -85
  221. package/src/lib/generators/index.js +0 -10
  222. package/src/lib/hooks/hook-executor.js +0 -167
  223. package/src/lib/validators/checkpoint-persistence.js +0 -39
  224. package/src/lib/validators/shared/emit-validator-dispatch.js +0 -14
  225. package/src/lib/validators/shared/index.js +0 -12
  226. package/src/lib/validators/shared/issue-counter.js +0 -14
  227. package/src/lib/validators/shared/result-formatter.js +0 -124
@@ -1,6 +1,50 @@
1
1
  import { readFileSync, existsSync } from 'fs';
2
2
  import { join } from 'path';
3
- import { personaStandardIds } from '../../utils/agents-installer.js';
3
+ import { personaStandardIds, isImplementer, renderAgentMd, loadKnownMcpIds } from '../../utils/agents-installer.js';
4
+
5
+ /** A line of a phase skill that dispatches a sub-agent. */
6
+ const DISPATCH_LINE = /despach|dispatch|subagent_type/i;
7
+
8
+ /** The generic dispatch token: "the implementer of the task's domain". */
9
+ const GENERIC_DISPATCH = 'morph-{id}';
10
+
11
+ /**
12
+ * Which phases each persona works in, read from the phase skills themselves.
13
+ *
14
+ * v5 has no phase list on the persona: a phase dispatches a persona when its
15
+ * skill (`phases.json → skill`) names `morph-{personaId}` on a dispatch line
16
+ * (morph-uiux → ui-designer, morph-plan → scout, morph-review → evaluator).
17
+ * A skill that dispatches the generic `morph-{id}` runs every implementer —
18
+ * the same `isImplementer` the validator and the installer use.
19
+ *
20
+ * Only dispatch lines count: morph-plan cites `morph-dotnet-senior` as a
21
+ * spelling example of the `agent` field, which dispatches nobody.
22
+ *
23
+ * @param {string} frameworkDir
24
+ * @param {Record<string, object>} agents - agents.json `agents` map
25
+ * @returns {Map<string, string[]>} personaId → phase ids, in phase order
26
+ */
27
+ export function personaPhases(frameworkDir, agents) {
28
+ const result = new Map(Object.keys(agents).map((id) => [id, []]));
29
+ const phasesJsonPath = join(frameworkDir, 'phases.json');
30
+ if (!existsSync(phasesJsonPath)) return result;
31
+
32
+ const phases = Object.values(JSON.parse(readFileSync(phasesJsonPath, 'utf8')).phases || {})
33
+ .sort((a, b) => a.index - b.index);
34
+ for (const phase of phases) {
35
+ const skillPath = join(frameworkDir, 'skills', 'core', phase.skill || '', 'SKILL.md');
36
+ if (!phase.skill || !existsSync(skillPath)) continue;
37
+ const dispatchLines = readFileSync(skillPath, 'utf8').split(/\r?\n/).filter((l) => DISPATCH_LINE.test(l));
38
+ const generic = dispatchLines.some((l) => l.includes(GENERIC_DISPATCH));
39
+ for (const [id, agent] of Object.entries(agents)) {
40
+ const named = new RegExp(`morph-${id}(?![\\w-])`);
41
+ if ((generic && isImplementer(agent)) || dispatchLines.some((l) => named.test(l))) {
42
+ result.get(id).push(phase.id);
43
+ }
44
+ }
45
+ }
46
+ return result;
47
+ }
4
48
 
5
49
  /**
6
50
  * Scan agents.json (v5) — flat domain personas, no tiers.
@@ -12,6 +56,14 @@ export function scanAgents(frameworkDir) {
12
56
  return { version: 'unknown', total: 0, list: [] };
13
57
  }
14
58
  const agentsJson = JSON.parse(readFileSync(agentsJsonPath, 'utf8'));
59
+ const phasesOf = personaPhases(frameworkDir, agentsJson.agents);
60
+ const knownMcpIds = loadKnownMcpIds(frameworkDir);
61
+ // The exact `.claude/agents/morph-{id}.md` the installer writes: the system
62
+ // prompt the sub-agent actually runs with. A persona whose data the renderer
63
+ // refuses shows the refusal instead of hiding the persona.
64
+ const promptOf = (id, agent) => {
65
+ try { return renderAgentMd({ id, ...agent }, knownMcpIds); } catch (err) { return `<!-- não renderizou: ${err.message} -->`; }
66
+ };
15
67
  const list = Object.entries(agentsJson.agents).map(([id, agent]) => ({
16
68
  id,
17
69
  title: agent.title,
@@ -24,7 +76,13 @@ export function scanAgents(frameworkDir) {
24
76
  standards: personaStandardIds(agent),
25
77
  surfaces: agent.surfaces || null,
26
78
  stack: agent.stack || ['*'],
79
+ activePhases: phasesOf.get(id),
27
80
  persona: agent.persona || '',
81
+ model: agent.model || null,
82
+ maxTurns: agent.maxTurns ?? null,
83
+ tools: agent.tools || null,
84
+ mcp: agent.mcp || [],
85
+ prompt: promptOf(id, agent),
28
86
  }));
29
87
  return {
30
88
  version: agentsJson.version,
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The reading material of the framework — the charter, the runtime
3
+ * instructions, the CLI reference and the rubrics the evaluator scores with —
4
+ * for the dashboard's Guide and Rubrics pages.
5
+ */
6
+ import { readFileSync, readdirSync, existsSync } from 'fs';
7
+ import { join } from 'path';
8
+
9
+ function read(path) {
10
+ try { return readFileSync(path, 'utf8'); } catch { return null; }
11
+ }
12
+
13
+ /** `## Heading` lines, the table of contents a long document is read by. */
14
+ export function headingsOf(content) {
15
+ const out = [];
16
+ let inFence = false;
17
+ for (const line of String(content).split(/\r?\n/)) {
18
+ if (/^\s*```/.test(line)) { inFence = !inFence; continue; }
19
+ if (inFence) continue;
20
+ const m = /^(#{1,3})\s+(.+?)\s*#*\s*$/.exec(line);
21
+ if (m) out.push({ depth: m[1].length, text: m[2].replace(/[`*_]/g, '') });
22
+ }
23
+ return out;
24
+ }
25
+
26
+ function titleOf(content, fallback) {
27
+ const h = headingsOf(content || '').find((x) => x.depth === 1);
28
+ return h?.text || fallback;
29
+ }
30
+
31
+ function mdDir(dir) {
32
+ if (!existsSync(dir)) return [];
33
+ return readdirSync(dir)
34
+ .filter((f) => f.endsWith('.md'))
35
+ .sort()
36
+ .map((f) => {
37
+ const content = read(join(dir, f)) || '';
38
+ return { id: f.replace(/\.md$/, ''), file: f, title: titleOf(content, f.replace(/\.md$/, '')), content };
39
+ });
40
+ }
41
+
42
+ const GUIDE = [
43
+ { id: 'morph', path: 'MORPH.md', title: 'Carta do harness (MORPH.md)', summary: 'Como o Claude trabalha: papéis, fases, gates, sub-agents. É a fonte única do comportamento.' },
44
+ { id: 'runtime', path: 'CLAUDE.md', title: 'Instruções de runtime (CLAUDE.md)', summary: 'O que é injetado no CLAUDE.md do projeto: como começar cada sessão, regras críticas, comandos.' },
45
+ { id: 'cli', path: 'docs/CLI.md', title: 'Referência do CLI', summary: 'Todos os comandos morph-spec: gates, verify, worktrees, grafo, telemetria.' },
46
+ ];
47
+
48
+ export function scanDocs(frameworkDir) {
49
+ const guide = GUIDE.map((g) => {
50
+ const content = read(join(frameworkDir, ...g.path.split('/'))) || '';
51
+ return { ...g, content, headings: headingsOf(content).filter((h) => h.depth <= 3) };
52
+ }).filter((g) => g.content);
53
+
54
+ return {
55
+ guide,
56
+ rubrics: mdDir(join(frameworkDir, 'evals', 'rubrics')),
57
+ proofRubrics: mdDir(join(frameworkDir, 'evals', 'proof-rubrics')),
58
+ };
59
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * When a feature counts as finished — the one rule the portfolio (server) and
3
+ * the feature list (client) share. Pure, no node imports: the client bundles it.
4
+ *
5
+ * A feature in `.morph/archive/` is closed whatever its status says: legacy
6
+ * archives keep `draft` or `in_progress` from before `finish` set the status.
7
+ */
8
+ export function isFinished(f) {
9
+ return f?.status === 'completed' || f?.status === 'abandoned' || f?.location === 'archive';
10
+ }
@@ -0,0 +1,360 @@
1
+ /**
2
+ * The dashboard's view of the target project's features — the ONLY scanner
3
+ * that reads the project rather than the framework package.
4
+ *
5
+ * Every rule here is borrowed, never re-encoded:
6
+ * - which tree owns a feature → `resolveFeatureTree` (feature-truth.js)
7
+ * - the current phase → `derivePhaseFrom` (state-reader.js)
8
+ * - gate order → `GATE_PREREQ` (gate-order.js)
9
+ * - the evaluator's verdict → `evaluatorVerdictFromText` (evaluator-report.js)
10
+ * - path confinement → `canonicalPath`/`isInside` (test-runner.js)
11
+ * - tasks.json validity → `validateTasksJson` (tasks-json-parser.js)
12
+ *
13
+ * Three places a feature can live: the root `.morph/features/{f}`, a worktree
14
+ * `{worktrees}/{wt}/.morph/features/{f}`, and `.morph/archive/{f}` once
15
+ * `finish` ran. The root copy of a feature built in a worktree is its
16
+ * birth-instant skeleton and never changes again, so for an active feature the
17
+ * owner tree wins. Only active features pay for that question — it spawns git.
18
+ *
19
+ * Read-only and fail-open: a corrupt file degrades one feature, never the scan.
20
+ */
21
+
22
+ import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
23
+ import { join, basename, isAbsolute } from 'path';
24
+ import { derivePhaseFrom, PHASE_ARTIFACTS } from '../../../framework/hooks/shared/state-reader.js';
25
+ import { GATE_PREREQ } from '../../core/state/gate-order.js';
26
+ import { validateTasksJson } from '../../lib/tasks/tasks-json-parser.js';
27
+ import { evaluatorVerdictFromText } from '../../lib/evaluator-report.js';
28
+ import { resolveFeatureTree } from '../../lib/worktree/feature-truth.js';
29
+ import { WORKTREE_DIR_NAMES } from '../../lib/worktree/dirs.js';
30
+ import { canonicalPath, isInside, readMorphConfig } from '../../lib/tasks/test-runner.js';
31
+ import { isFinished } from './feature-status.js';
32
+
33
+ export const GATES = Object.keys(GATE_PREREQ);
34
+ const FINISHED = new Set(['completed', 'abandoned']);
35
+ const FOLDER_PHASE = Object.fromEntries(PHASE_ARTIFACTS.map(([folder, phase]) => [folder, phase]));
36
+ const EVALUATOR_REPORT = '4-review/evaluator-report.md';
37
+ const MAX_FILE_BYTES = 2 * 1024 * 1024;
38
+ const MAX_DEPTH = 4;
39
+
40
+ function readJson(path) {
41
+ try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
42
+ }
43
+
44
+ function subdirs(dir) {
45
+ try {
46
+ return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
47
+ } catch { return []; }
48
+ }
49
+
50
+ /**
51
+ * Old feature.json files stamped a gate as a bare `true` (MORPH_OS). Lifting it
52
+ * to `{approved: true}` is what lets the shared rules read them unchanged.
53
+ */
54
+ export function normalizeGates(gates) {
55
+ const out = {};
56
+ for (const [name, value] of Object.entries(gates || {})) {
57
+ out[name] = value === true ? { approved: true } : (value && typeof value === 'object' ? value : { approved: false });
58
+ }
59
+ return out;
60
+ }
61
+
62
+ function normalizeFeature(fj) {
63
+ const f = fj && typeof fj === 'object' ? fj : {};
64
+ return { ...f, approvalGates: normalizeGates(f.approvalGates || f.approvals) };
65
+ }
66
+
67
+ /**
68
+ * Every place a feature lives, before deciding which one tells the truth.
69
+ * @returns {Map<string, {root?: string, worktrees: string[], archive?: string}>}
70
+ */
71
+ function collectLocations(projectDir) {
72
+ const found = new Map();
73
+ const slot = (name) => {
74
+ if (!found.has(name)) found.set(name, { worktrees: [] });
75
+ return found.get(name);
76
+ };
77
+
78
+ const rootFeatures = join(projectDir, '.morph', 'features');
79
+ for (const name of subdirs(rootFeatures)) {
80
+ if (existsSync(join(rootFeatures, name, 'feature.json'))) slot(name).root = join(rootFeatures, name);
81
+ }
82
+ for (const wtDirName of WORKTREE_DIR_NAMES) {
83
+ for (const wt of subdirs(join(projectDir, wtDirName))) {
84
+ const wtFeatures = join(projectDir, wtDirName, wt, '.morph', 'features');
85
+ for (const name of subdirs(wtFeatures)) {
86
+ if (existsSync(join(wtFeatures, name, 'feature.json'))) slot(name).worktrees.push(join(wtFeatures, name));
87
+ }
88
+ }
89
+ }
90
+ const archive = join(projectDir, '.morph', 'archive');
91
+ for (const name of subdirs(archive)) slot(name).archive = join(archive, name);
92
+ return found;
93
+ }
94
+
95
+ /**
96
+ * Decide which directory is this feature's truth.
97
+ *
98
+ * @returns {{ location: 'root'|'worktree'|'archive', dir: string, feature: object,
99
+ * ownerTree: string|null, rootStale: boolean }}
100
+ */
101
+ function resolveLocation(projectDir, name, loc, resolveTree) {
102
+ const rootFj = loc.root ? readJson(join(loc.root, 'feature.json')) : null;
103
+
104
+ // Archived: `finish` moved the artifacts. An old root copy may linger (MORPH_OS
105
+ // keeps a feature.json per finished feature) — the archive holds the files.
106
+ const rootFinished = rootFj && (FINISHED.has(rootFj.status) || rootFj.archivedAt);
107
+ if (loc.archive && (!rootFj || rootFinished)) {
108
+ const archFj = readJson(join(loc.archive, 'feature.json'));
109
+ return {
110
+ location: 'archive',
111
+ dir: loc.archive,
112
+ feature: archFj || rootFj || { status: 'completed' },
113
+ ownerTree: null,
114
+ rootStale: false,
115
+ };
116
+ }
117
+
118
+ if (rootFj && !FINISHED.has(rootFj.status)) {
119
+ const truth = resolveTree(name, projectDir);
120
+ const tree = truth?.tree;
121
+ if (tree && canonicalPath(tree) !== canonicalPath(projectDir)) {
122
+ const wtDir = join(tree, '.morph', 'features', name);
123
+ const wtFj = readJson(join(wtDir, 'feature.json'));
124
+ if (wtFj) return { location: 'worktree', dir: wtDir, feature: wtFj, ownerTree: tree, rootStale: false };
125
+ // The owner tree exists but carries no copy of the feature: all we have is
126
+ // the root skeleton, and the UI has to say so.
127
+ return { location: 'root', dir: loc.root, feature: rootFj, ownerTree: tree, rootStale: true };
128
+ }
129
+ }
130
+
131
+ if (rootFj) return { location: 'root', dir: loc.root, feature: rootFj, ownerTree: null, rootStale: false };
132
+
133
+ const wtDir = loc.worktrees[0];
134
+ return {
135
+ location: 'worktree',
136
+ dir: wtDir,
137
+ feature: readJson(join(wtDir, 'feature.json')) || {},
138
+ ownerTree: join(wtDir, '..', '..', '..'),
139
+ rootStale: false,
140
+ };
141
+ }
142
+
143
+ /**
144
+ * tasks.json as the dashboard needs it: the list even when the file breaks the
145
+ * schema (a read-only viewer shows what is there), with the errors alongside.
146
+ */
147
+ export function readTasks(dir) {
148
+ const path = join(dir, '2-plan', 'tasks.json');
149
+ if (!existsSync(path)) return { list: [], errors: [], present: false };
150
+ const data = readJson(path);
151
+ if (!data) return { list: [], errors: ['tasks.json não é JSON válido'], present: true };
152
+ const { valid, errors } = validateTasksJson(data);
153
+ const list = Array.isArray(data.tasks) ? data.tasks.filter((t) => t && typeof t === 'object') : [];
154
+ return { list, errors: valid ? [] : errors, present: true };
155
+ }
156
+
157
+ function taskCounts(list) {
158
+ const counts = { total: list.length, done: 0, inProgress: 0, blocked: 0, pending: 0, skipped: 0 };
159
+ for (const t of list) {
160
+ // `completed` is the pre-schema spelling of `done` (MORPH_OS archives carry both).
161
+ if (t.status === 'done' || t.status === 'completed') counts.done += 1;
162
+ else if (t.status === 'in_progress') counts.inProgress += 1;
163
+ else if (t.status === 'blocked') counts.blocked += 1;
164
+ else if (t.status === 'skipped') counts.skipped += 1;
165
+ else counts.pending += 1;
166
+ }
167
+ return counts;
168
+ }
169
+
170
+ function gatesOf(feature) {
171
+ const skipped = new Set(feature.skippedPhases || []);
172
+ return GATES.map((gate) => {
173
+ const g = feature.approvalGates[gate] || { approved: false };
174
+ const decision = feature.gateDecisions?.[gate] || null;
175
+ const approved = g.approved === true;
176
+ return {
177
+ gate,
178
+ approved,
179
+ approvedBy: g.approvedBy ?? (g.auto ? 'auto' : null),
180
+ at: g.approvedAt || g.timestamp || null,
181
+ paused: !approved && decision?.pause === true,
182
+ skipped: skipped.has(gate),
183
+ reason: decision?.reason ?? null,
184
+ };
185
+ });
186
+ }
187
+
188
+ function evaluatorVerdict(dir) {
189
+ const path = join(dir, ...EVALUATOR_REPORT.split('/'));
190
+ if (!existsSync(path)) return null;
191
+ try { return evaluatorVerdictFromText(readFileSync(path, 'utf8')); } catch { return null; }
192
+ }
193
+
194
+ function summarize(name, resolved) {
195
+ const { location, dir, ownerTree, rootStale } = resolved;
196
+ const feature = normalizeFeature(resolved.feature);
197
+ const has = (rel) => existsSync(join(dir, ...rel.split('/')));
198
+ const gates = gatesOf(feature);
199
+ const reviewApproved = gates.find((g) => g.gate === 'review')?.approved === true;
200
+ const tasks = readTasks(dir);
201
+ const verdict = evaluatorVerdict(dir);
202
+ const status = feature.status || (location === 'archive' ? 'completed' : 'draft');
203
+ const finished = isFinished({ status, location });
204
+ return {
205
+ name,
206
+ description: feature.description || null,
207
+ status,
208
+ workType: feature.workType || null,
209
+ location,
210
+ dir,
211
+ ownerTree,
212
+ rootStale,
213
+ phase: derivePhaseFrom(feature, has),
214
+ inEvaluation: has(EVALUATOR_REPORT) && !reviewApproved,
215
+ gates,
216
+ awaiting: !finished && gates.some((g) => g.paused),
217
+ tasks: taskCounts(tasks.list),
218
+ // Titles of an active feature's tasks, for the global search. Finished
219
+ // features leave them out: 170 archived plans would bloat every list reply.
220
+ taskIndex: finished ? [] : tasks.list.map((t) => ({ id: t.id, title: t.title || '', status: t.status || 'pending' })),
221
+ verification: feature.verification
222
+ ? { status: feature.verification.status ?? null, scope: feature.verification.scope ?? null, ts: feature.verification.ts ?? null }
223
+ : null,
224
+ score: verdict?.score ?? null,
225
+ legacyScore: typeof feature.reviewScore === 'number' ? feature.reviewScore : null,
226
+ createdAt: feature.createdAt || null,
227
+ updatedAt: feature.updatedAt || feature.archivedAt || feature.createdAt || null,
228
+ archivedAt: feature.archivedAt || null,
229
+ };
230
+ }
231
+
232
+ /**
233
+ * Files of a feature, grouped by the phase their top folder belongs to.
234
+ * @returns {Array<{path: string, phase: string|null, size: number, mtime: string}>}
235
+ */
236
+ export function listDocuments(dir) {
237
+ const out = [];
238
+ const walk = (abs, rel, depth) => {
239
+ let entries;
240
+ try { entries = readdirSync(abs, { withFileTypes: true }); } catch { return; }
241
+ for (const e of entries) {
242
+ // A stray `.morph/` under a phase folder is a hook's misplaced log, not a document.
243
+ if (e.name.startsWith('.')) continue;
244
+ const childRel = rel ? `${rel}/${e.name}` : e.name;
245
+ const childAbs = join(abs, e.name);
246
+ if (e.isDirectory()) {
247
+ if (depth < MAX_DEPTH) walk(childAbs, childRel, depth + 1);
248
+ continue;
249
+ }
250
+ let st;
251
+ try { st = statSync(childAbs); } catch { continue; }
252
+ out.push({
253
+ path: childRel,
254
+ phase: FOLDER_PHASE[childRel.split('/')[0]] || null,
255
+ size: st.size,
256
+ mtime: st.mtime.toISOString(),
257
+ });
258
+ }
259
+ };
260
+ walk(dir, '', 0);
261
+ return out.sort((a, b) => a.path.localeCompare(b.path));
262
+ }
263
+
264
+ /**
265
+ * Resolve one feature. `known` is where a previous scan already found it: the
266
+ * list scan paid for the git calls, and opening a feature or one of its files
267
+ * must not pay again. It is only trusted while its feature.json still reads.
268
+ */
269
+ function resolveOne(projectDir, name, resolveTree, known) {
270
+ if (known?.dir) {
271
+ const fj = readJson(join(known.dir, 'feature.json'));
272
+ if (fj) return { location: known.location, dir: known.dir, feature: fj, ownerTree: known.ownerTree ?? null, rootStale: Boolean(known.rootStale) };
273
+ }
274
+ const loc = collectLocations(projectDir).get(name);
275
+ if (!loc) return null;
276
+ return resolveLocation(projectDir, name, loc, resolveTree);
277
+ }
278
+
279
+ /**
280
+ * The feature list of a project.
281
+ *
282
+ * @param {string} projectDir
283
+ * @param {{ resolveTree?: Function }} [opts] - seam for tests; defaults to
284
+ * `resolveFeatureTree`
285
+ */
286
+ export function scanFeatures(projectDir, { resolveTree = resolveFeatureTree } = {}) {
287
+ const config = readMorphConfig(projectDir);
288
+ const features = [];
289
+ for (const [name, loc] of collectLocations(projectDir)) {
290
+ try {
291
+ features.push(summarize(name, resolveLocation(projectDir, name, loc, resolveTree)));
292
+ } catch { /* one unreadable feature never hides the rest */ }
293
+ }
294
+ features.sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || ''));
295
+
296
+ const hasState = existsSync(join(projectDir, '.morph', 'state.json'));
297
+ return {
298
+ project: {
299
+ name: config?.project?.name || basename(projectDir),
300
+ stack: config?.project?.stack || null,
301
+ trust: config?.trust || null,
302
+ dir: projectDir,
303
+ initialized: existsSync(join(projectDir, '.morph')),
304
+ // A project that has state.json but no feature.json anywhere predates the
305
+ // per-feature state model — the dashboard says so instead of showing "empty".
306
+ legacyStateOnly: hasState && features.length === 0,
307
+ },
308
+ features,
309
+ };
310
+ }
311
+
312
+ /**
313
+ * One feature in full: summary + tasks with their verify/score, the gate
314
+ * decisions, the verify nodes and the document tree.
315
+ */
316
+ export function readFeatureDetail(projectDir, name, { resolveTree = resolveFeatureTree, known } = {}) {
317
+ const resolved = resolveOne(projectDir, name, resolveTree, known);
318
+ if (!resolved) return null;
319
+ const summary = summarize(name, resolved);
320
+ const feature = normalizeFeature(resolved.feature);
321
+ const tasks = readTasks(resolved.dir);
322
+ const verdict = evaluatorVerdict(resolved.dir);
323
+ return {
324
+ ...summary,
325
+ tasksList: tasks.list.map((t) => ({
326
+ ...t,
327
+ verification: feature.taskVerifications?.[t.id] || null,
328
+ score: feature.taskScores?.[t.id] || null,
329
+ })),
330
+ tasksErrors: tasks.errors,
331
+ gateDecisions: feature.gateDecisions || {},
332
+ verificationNodes: feature.verification?.nodes || null,
333
+ featureVerification: feature.verifications?.feature || null,
334
+ proofScores: verdict?.proofScores ?? null,
335
+ documents: listDocuments(resolved.dir),
336
+ raw: resolved.feature,
337
+ };
338
+ }
339
+
340
+ /**
341
+ * The content of one file of a feature, confined to the feature's directory.
342
+ * Junctions and win32 case are resolved by `canonicalPath` before the check,
343
+ * so `..`, an absolute path or a link that leaves the folder are all refused.
344
+ *
345
+ * @returns {{ path: string, content: string, mtime: string } | { error: string, status: number }}
346
+ */
347
+ export function readFeatureFile(projectDir, name, relPath, { resolveTree = resolveFeatureTree, known } = {}) {
348
+ if (typeof relPath !== 'string' || relPath === '') return { error: 'path ausente', status: 400 };
349
+ if (isAbsolute(relPath) || /^[a-zA-Z]:/.test(relPath)) return { error: 'path fora da feature', status: 403 };
350
+ const resolved = resolveOne(projectDir, name, resolveTree, known);
351
+ if (!resolved) return { error: `feature "${name}" não encontrada`, status: 404 };
352
+ const base = canonicalPath(resolved.dir);
353
+ const target = canonicalPath(join(resolved.dir, ...relPath.split(/[\\/]/)));
354
+ if (!isInside(target, base) || target === base) return { error: 'path fora da feature', status: 403 };
355
+ let st;
356
+ try { st = statSync(target); } catch { return { error: 'arquivo não encontrado', status: 404 }; }
357
+ if (!st.isFile()) return { error: 'não é um arquivo', status: 400 };
358
+ if (st.size > MAX_FILE_BYTES) return { error: 'arquivo grande demais para exibir', status: 413 };
359
+ return { path: relPath, content: readFileSync(target, 'utf8'), mtime: st.mtime.toISOString() };
360
+ }
@@ -4,17 +4,24 @@ import { scanHooks } from './hooks-scanner.js';
4
4
  import { scanSkills } from './skills-scanner.js';
5
5
  import { scanStandards } from './standards-scanner.js';
6
6
  import { scanRules } from './rules-scanner.js';
7
- import { scanSquads } from './squads-scanner.js';
8
7
  import { scanRubrics } from './rubrics-scanner.js';
9
- import { scanState } from './state-scanner.js';
8
+ import { scanDocs } from './docs-scanner.js';
10
9
 
11
- export function scanAll(frameworkDir, projectDir) {
10
+ /**
11
+ * The framework's knowledge, for the static virtual module. The project's
12
+ * state (features, activity) is served live by project-api.js instead.
13
+ */
14
+ export function scanAll(frameworkDir) {
12
15
  const agents = scanAgents(frameworkDir);
16
+ const phases = scanPhases(frameworkDir);
13
17
  return {
14
- agents, phases: scanPhases(frameworkDir), hooks: scanHooks(frameworkDir),
15
- skills: scanSkills(frameworkDir), standards: scanStandards(frameworkDir, agents.list),
18
+ agents,
19
+ phases,
20
+ hooks: scanHooks(frameworkDir),
21
+ skills: scanSkills(frameworkDir, phases.list),
22
+ standards: scanStandards(frameworkDir, agents.list),
16
23
  rules: scanRules(frameworkDir),
17
- squads: scanSquads(frameworkDir), rubrics: scanRubrics(frameworkDir),
18
- liveState: scanState(projectDir || process.cwd()),
24
+ rubrics: scanRubrics(frameworkDir),
25
+ docs: scanDocs(frameworkDir),
19
26
  };
20
27
  }
@@ -1,15 +1,14 @@
1
- import { readFileSync } from 'fs';
2
- import { join } from 'path';
3
-
4
- export function scanPhases(frameworkDir) {
5
- const phasesJson = JSON.parse(readFileSync(join(frameworkDir, 'phases.json'), 'utf8'));
6
- const list = Object.values(phasesJson.phases).map((p) => ({
7
- id: p.id, index: p.index, folder: p.folder || null, displayName: p.displayName,
8
- optional: p.optional || false, goals: p.goals || [], requiredOutputs: p.requiredOutputs || [],
9
- optionalOutputs: p.optionalOutputs || [], recommendedMCPs: p.recommendedMCPs || [],
10
- pausePoints: p.pausePoints || [], agentTiers: p.agentTiers || [],
11
- requiredSkills: p.requiredSkills || [],
12
- }));
13
- list.sort((a, b) => a.index - b.index);
14
- return { version: phasesJson.version, list };
15
- }
1
+ import { readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ export function scanPhases(frameworkDir) {
5
+ const phasesJson = JSON.parse(readFileSync(join(frameworkDir, 'phases.json'), 'utf8'));
6
+ const list = Object.values(phasesJson.phases).map((p) => ({
7
+ id: p.id, index: p.index, folder: p.folder || null, displayName: p.displayName,
8
+ optional: p.optional || false, goals: p.goals || [], requiredOutputs: p.requiredOutputs || [],
9
+ optionalOutputs: p.optionalOutputs || [], recommendedMCPs: p.recommendedMCPs || [],
10
+ pausePoints: p.pausePoints || [], skill: p.skill || null,
11
+ }));
12
+ list.sort((a, b) => a.index - b.index);
13
+ return { version: phasesJson.version, list };
14
+ }