@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
@@ -64,21 +64,49 @@ export async function detectStack(targetPath) {
64
64
  }
65
65
  }
66
66
 
67
+ /**
68
+ * Detect the project's integrations (`neon`, `stripe`...), same sources and
69
+ * order as detectStack. The detectors never fold them into the stack string, so
70
+ * without this an MCP scoped to an integration (neon, asaas) is never offered.
71
+ *
72
+ * @param {string} targetPath
73
+ * @returns {Promise<string[]>}
74
+ */
75
+ export async function detectIntegrations(targetPath) {
76
+ try {
77
+ const configPath = join(targetPath, '.morph', 'config', 'config.json');
78
+ if (existsSync(configPath)) {
79
+ const integrations = JSON.parse(readFileSync(configPath, 'utf-8')).project?.integrations;
80
+ if (Array.isArray(integrations)) return integrations;
81
+ }
82
+ } catch {
83
+ // Fall through to filesystem detection
84
+ }
85
+
86
+ try {
87
+ const results = await detectProject(targetPath, { conversation: false, generateStandards: false });
88
+ return results.structure?.integrations ?? [];
89
+ } catch {
90
+ return [];
91
+ }
92
+ }
93
+
67
94
  export async function mcpSetupCommand(name, options) {
68
95
  const targetPath = process.cwd();
69
96
 
70
97
  try {
71
98
  // Detect current environment
72
99
  const stack = await detectStack(targetPath);
100
+ const integrations = await detectIntegrations(targetPath);
73
101
  const claudeConfig = await detectClaudeConfig(targetPath);
74
102
  const existingMcps = claudeConfig.mcpServers || [];
75
103
 
76
104
  if (options.list) {
77
- return showStatusList(targetPath, stack, existingMcps);
105
+ return showStatusList(targetPath, stack, existingMcps, integrations);
78
106
  }
79
107
 
80
108
  if (options.auto) {
81
- return autoInstall(targetPath, stack, existingMcps);
109
+ return autoInstall(targetPath, stack, existingMcps, integrations);
82
110
  }
83
111
 
84
112
  if (name) {
@@ -86,7 +114,7 @@ export async function mcpSetupCommand(name, options) {
86
114
  }
87
115
 
88
116
  // Interactive wizard
89
- return interactiveWizard(targetPath, stack, existingMcps);
117
+ return interactiveWizard(targetPath, stack, existingMcps, integrations);
90
118
  } catch (err) {
91
119
  console.error(chalk.red(`Error: ${err.message}`));
92
120
  process.exit(1);
@@ -96,8 +124,8 @@ export async function mcpSetupCommand(name, options) {
96
124
  /**
97
125
  * Show MCP status list (--list)
98
126
  */
99
- async function showStatusList(targetPath, stack, existingMcps) {
100
- const orchestration = orchestrateMcpSetup(targetPath, stack, existingMcps);
127
+ async function showStatusList(targetPath, stack, existingMcps, integrations) {
128
+ const orchestration = orchestrateMcpSetup(targetPath, stack, existingMcps, { integrations });
101
129
  const statusRows = formatMcpStatusTable(orchestration, {});
102
130
 
103
131
  console.log(chalk.cyan('\n MCP Server Status\n'));
@@ -130,8 +158,8 @@ async function showStatusList(targetPath, stack, existingMcps) {
130
158
  /**
131
159
  * Auto-install credential-free MCPs (--auto)
132
160
  */
133
- async function autoInstall(targetPath, stack, existingMcps) {
134
- const orchestration = orchestrateMcpSetup(targetPath, stack, existingMcps, { autoOnly: true });
161
+ async function autoInstall(targetPath, stack, existingMcps, integrations) {
162
+ const orchestration = orchestrateMcpSetup(targetPath, stack, existingMcps, { autoOnly: true, integrations });
135
163
  const autoNames = Object.keys(orchestration.autoInstallable);
136
164
 
137
165
  if (autoNames.length === 0) {
@@ -391,8 +419,8 @@ async function setupSpecificMcp(targetPath, name, stack, existingMcps) {
391
419
  /**
392
420
  * Interactive wizard showing all MCPs
393
421
  */
394
- async function interactiveWizard(targetPath, stack, existingMcps) {
395
- const orchestration = orchestrateMcpSetup(targetPath, stack, existingMcps);
422
+ async function interactiveWizard(targetPath, stack, existingMcps, integrations) {
423
+ const orchestration = orchestrateMcpSetup(targetPath, stack, existingMcps, { integrations });
396
424
 
397
425
  console.log(chalk.cyan('\n MCP Setup Wizard'));
398
426
  console.log(chalk.gray(` Stack: ${stack}`));
@@ -16,7 +16,7 @@ import {
16
16
  getInstalledCLIVersion
17
17
  } from '../../utils/version-checker.js';
18
18
  import { resetMorphSettings } from '../../utils/claude-settings-manager.js';
19
- import { checkMissingAgents, checkAgentsDrift, maxTurnsError, surfacesError } from '../../utils/agents-installer.js';
19
+ import { checkMissingAgents, checkAgentsDrift, maxTurnsError, surfacesError, effortError } from '../../utils/agents-installer.js';
20
20
  import { getProjectTags } from '../../lib/stack-filter.js';
21
21
  import { auditGraph } from '../../lib/graph/graph-health.js';
22
22
  import { auditFeatureLifecycle } from '../../core/state/lifecycle-audit.js';
@@ -44,10 +44,10 @@ function normCwd(root, cwd) {
44
44
  /**
45
45
  * Check which required Claude Code plugins are installed.
46
46
  * @param {Object} pluginsData - The `plugins` object from installed_plugins.json
47
- * @returns {{ superpowers: boolean, context7: boolean }}
47
+ * @returns {{ context7: boolean }}
48
48
  */
49
49
  export function checkInstalledPlugins(pluginsData) {
50
- const REQUIRED = ['superpowers', 'context7'];
50
+ const REQUIRED = ['context7'];
51
51
  return Object.fromEntries(
52
52
  REQUIRED.map(name => [name, `${name}@claude-plugins-official` in (pluginsData || {})])
53
53
  );
@@ -208,6 +208,14 @@ export function validateAgentsJson(agentsData) {
208
208
  ? { name: 'agents.json surfaces', status: 'error', msg: surfacesErrors.join(' | ') }
209
209
  : { name: 'agents.json surfaces', status: 'ok' });
210
210
 
211
+ // 6: effort — same installer rule, same severity.
212
+ const effortErrors = agentEntries
213
+ .map(([id, data]) => effortError(id, data?.effort))
214
+ .filter(Boolean);
215
+ checks.push(effortErrors.length > 0
216
+ ? { name: 'agents.json effort', status: 'error', msg: effortErrors.join(' | ') }
217
+ : { name: 'agents.json effort', status: 'ok' });
218
+
211
219
  if (missingV5Fields.length > 0 && isV5) {
212
220
  const sample = missingV5Fields.slice(0, 3).join(', ');
213
221
  checks.push({
@@ -1330,15 +1338,13 @@ export async function doctorCommand(options = {}) {
1330
1338
  } catch { /* non-fatal */ }
1331
1339
 
1332
1340
  // ── Claude Code Plugins ──────────────────────────────────────────────────
1333
- console.log(chalk.cyan('\n Claude Code Plugins (required for /morph-init)'));
1341
+ console.log(chalk.cyan('\n Claude Code Plugins (required by morph-spec init)'));
1334
1342
  const pluginsFile = join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
1335
1343
  let pluginData = { plugins: {} };
1336
1344
  try {
1337
1345
  pluginData = JSON.parse(await fs.readFile(pluginsFile, 'utf8'));
1338
1346
  } catch { /* file may not exist */ }
1339
1347
  const pluginStatus = checkInstalledPlugins(pluginData.plugins || {});
1340
- check('superpowers plugin', pluginStatus.superpowers, false,
1341
- 'Run: morph-spec install-plugin superpowers');
1342
1348
  check('context7 plugin', pluginStatus.context7, false,
1343
1349
  'Run: morph-spec install-plugin context7');
1344
1350
  } else {
@@ -34,6 +34,7 @@ import { installAgents, installDomainAgents } from '../../utils/agents-installer
34
34
  import { getProjectTags } from '../../lib/stack-filter.js';
35
35
  import { updateCommand } from './update.js';
36
36
  import { resolvePrimaryRoot } from './worktree.js';
37
+ import { registerProject } from '../../lib/projects/registry.js';
37
38
 
38
39
  /**
39
40
  * Guard against running a full `init` install from inside a secondary
@@ -135,8 +136,7 @@ export async function initCommand(options) {
135
136
  try {
136
137
  claudeConfigDetected = await detectClaudeConfig(targetPath);
137
138
  const hasIntegrations = claudeConfigDetected.plugins.length > 0
138
- || claudeConfigDetected.mcpServers.length > 0
139
- || claudeConfigDetected.superpowers.installed;
139
+ || claudeConfigDetected.mcpServers.length > 0;
140
140
 
141
141
  if (hasIntegrations) {
142
142
  spinner.stop();
@@ -287,7 +287,8 @@ export async function initCommand(options) {
287
287
  try {
288
288
  spinner.stop();
289
289
  const existingMcps = claudeConfigDetected?.mcpServers || [];
290
- const orchestration = orchestrateMcpSetup(targetPath, detectedStack, existingMcps);
290
+ const orchestration = orchestrateMcpSetup(targetPath, detectedStack, existingMcps,
291
+ { integrations: config.project?.integrations ?? [] });
291
292
 
292
293
  const autoNames = Object.keys(orchestration.autoInstallable);
293
294
  const manualNames = Object.keys(orchestration.needsManualSetup);
@@ -467,6 +468,8 @@ export async function initCommand(options) {
467
468
  }
468
469
 
469
470
  spinner.succeed('MORPH-SPEC installed successfully!');
471
+ // The dashboard's portfolio learns about the project here (fail-open).
472
+ registerProject(targetPath, 'init');
470
473
 
471
474
  // Show next steps
472
475
  logger.blank();
@@ -5,7 +5,7 @@ import { logger } from '../../utils/logger.js';
5
5
  export async function installPluginCommand(pluginName, _options) {
6
6
  if (!pluginName) {
7
7
  logger.error('Plugin name required. Usage: morph-spec install-plugin <name>');
8
- logger.dim(' Available: superpowers, context7');
8
+ logger.dim(' Available: context7');
9
9
  process.exit(1);
10
10
  }
11
11
  try {
@@ -33,6 +33,7 @@ import { getProjectTags } from '../../lib/stack-filter.js';
33
33
  import { updateClaudeMdAgentsBlock, installRulesForStack } from '../../scripts/setup-infra.js';
34
34
  import { reconcileLegacyTemplatesCopy } from '../../lib/installers/templates-reconcile.js';
35
35
  import { createEffects } from '../../lib/installers/effects.js';
36
+ import { registerProject } from '../../lib/projects/registry.js';
36
37
  import {
37
38
  writeGraphifyignore, describeOutdatedIgnore, graphifyignoreTracked,
38
39
  } from '../../lib/graph/graphifyignore.js';
@@ -758,6 +759,10 @@ export async function updateCommand(options = {}) {
758
759
  logger.blank();
759
760
 
760
761
  if (!dryRun) {
762
+ // The dashboard's portfolio learns about the project here (fail-open;
763
+ // outside the project, so not an effect of the update itself).
764
+ registerProject(targetPath, 'update');
765
+
761
766
  // Show version change
762
767
  if (projectCheck.current && projectCheck.current !== cliCheck.current) {
763
768
  logger.success(`Updated: ${projectCheck.current} → ${cliCheck.current}`);
@@ -35,7 +35,7 @@
35
35
  */
36
36
 
37
37
  import { execSync, execFileSync } from 'child_process';
38
- import { existsSync, mkdirSync, symlinkSync, readFileSync, writeFileSync, lstatSync, unlinkSync, rmdirSync, cpSync } from 'fs';
38
+ import { existsSync, mkdirSync, symlinkSync, readFileSync, writeFileSync, lstatSync, unlinkSync, rmdirSync, cpSync, copyFileSync, readdirSync } from 'fs';
39
39
  import { join, resolve, dirname, basename } from 'path';
40
40
  import chalk from 'chalk';
41
41
  import { detectDefaultBranch } from '../../lib/git/default-branch.js';
@@ -612,6 +612,43 @@ export function buildTaskBranchName(name) {
612
612
  return `morph-task/${name}`;
613
613
  }
614
614
 
615
+ const ROOT_ENV_FILE_RE = /^\.env(\..+)?$/;
616
+
617
+ /**
618
+ * Copy the root's `.env` / `.env.*` into a new tree: a sub-agent that builds
619
+ * or runs the app needs the same secrets, and they are never committed. Only
620
+ * a REAL file (never a link — nothing in the tree may reach the root through
621
+ * one), only when the tree lacks it (never overwrite), and only when git
622
+ * IGNORES it there: an untracked-but-visible copy would dirty the tree, and
623
+ * `finish`/`worktree remove --if-clean` refuse a dirty tree. At teardown the
624
+ * copy is byte-identical to the root's, which `triageIgnoredPaths` classifies
625
+ * as REPLICABLE — deleted with nothing lost.
626
+ *
627
+ * @param {string} worktreePath
628
+ * @param {string} root
629
+ * @returns {string[]} names copied
630
+ */
631
+ export function copyRootEnvFiles(worktreePath, root) {
632
+ let names;
633
+ try {
634
+ names = readdirSync(root).filter((n) => ROOT_ENV_FILE_RE.test(n));
635
+ } catch {
636
+ return [];
637
+ }
638
+ const copied = [];
639
+ for (const name of names) {
640
+ const dst = join(worktreePath, name);
641
+ try {
642
+ if (!lstatSync(join(root, name)).isFile()) continue;
643
+ try { lstatSync(dst); continue; } catch { /* absent — not even a dangling link */ }
644
+ execFileSync('git', ['check-ignore', '-q', '--', name], { cwd: worktreePath, stdio: 'pipe' });
645
+ copyFileSync(join(root, name), dst);
646
+ copied.push(name);
647
+ } catch { /* not ignored, unreadable or unwritable — leave it out */ }
648
+ }
649
+ return copied;
650
+ }
651
+
615
652
  /**
616
653
  * Non-exiting core of `worktree provision` — the tree `claude -w`,
617
654
  * `EnterWorktree` and `Agent isolation:"worktree"` ask for when the name is
@@ -668,7 +705,11 @@ export async function runWorktreeProvision(name, options = {}) {
668
705
  execFileSync('git', ['worktree', 'add', worktreePath, branch], { cwd: root, stdio: 'pipe' });
669
706
 
670
707
  setupInfraLinks(worktreePath, root);
671
- const envBlock = await allocateWorktreeEnv(worktreePath, root, `task/${name}`);
708
+ const envFiles = copyRootEnvFiles(worktreePath, root);
709
+ // A tree born from `morph/<f>` works FOR that feature: MORPH_FEATURE says
710
+ // so, while the port block stays the task's own (`task/<name>`).
711
+ const forFeature = String(options.base || '').match(/^morph\/(.+)$/)?.[1] ?? null;
712
+ const envBlock = await allocateWorktreeEnv(worktreePath, root, `task/${name}`, { morphFeature: forFeature });
672
713
  const materialized = await ensureLocalState(worktreePath);
673
714
  const graph = seedWorktreeGraph(worktreePath, root);
674
715
 
@@ -681,6 +722,8 @@ export async function runWorktreeProvision(name, options = {}) {
681
722
  branchedFrom,
682
723
  kind: 'task-isolation',
683
724
  name,
725
+ ...(forFeature ? { feature: forFeature } : {}),
726
+ ...(envFiles.length ? { envFiles } : {}),
684
727
  materialized: materialized.materialized,
685
728
  ...(envBlock ? { env: { base: envBlock.base, range: envBlock.range } } : {}),
686
729
  ...(graph.seeded ? { graph: 'seeded' } : {}),
@@ -797,12 +840,25 @@ export function envKeyForWorktree(worktreePath, root) {
797
840
  * Shared by `setup` (feature key), `provision` (task key) and `link --all`
798
841
  * (regeneration), so the manifest has exactly one writer.
799
842
  *
843
+ * `morphFeature` (task trees born from `morph/<f>`) only renames the
844
+ * `MORPH_FEATURE` line. Left undefined — the `link --all` regeneration, which
845
+ * derives the key from the branch and cannot see the base — the value the
846
+ * file already carries is kept, so a repair never forgets the feature.
847
+ *
800
848
  * @param {string} worktreePath
801
849
  * @param {string} root - primary root
802
850
  * @param {string} envKey - `<feature>` or `task/<name>`
851
+ * @param {{ morphFeature?: string|null }} [opts]
803
852
  * @returns {Promise<{ index: number, base: number, range: [number, number] }|null>}
804
853
  */
805
- export async function allocateWorktreeEnv(worktreePath, root, envKey) {
854
+ export async function allocateWorktreeEnv(worktreePath, root, envKey, { morphFeature } = {}) {
855
+ const envPath = join(worktreePath, '.morph', 'worktree.env');
856
+ if (morphFeature === undefined && envKey.startsWith('task/')) {
857
+ try {
858
+ const kept = readFileSync(envPath, 'utf8').match(/^MORPH_FEATURE=(.+)$/m)?.[1]?.trim();
859
+ morphFeature = kept && kept !== envKey ? kept : null;
860
+ } catch { morphFeature = null; }
861
+ }
806
862
  try {
807
863
  const { allocateEnvBlock, resolveFleetConfig, buildEnvFileContent } =
808
864
  await import('../../lib/worktree/env-allocator.js');
@@ -816,7 +872,7 @@ export async function allocateWorktreeEnv(worktreePath, root, envKey) {
816
872
  stack = cfg?.project?.stack || '';
817
873
  } catch { /* fail-open — stackless env still carries PORT */ }
818
874
  mkdirSync(join(worktreePath, '.morph'), { recursive: true });
819
- writeFileSync(join(worktreePath, '.morph', 'worktree.env'), buildEnvFileContent(envKey, block, stack), 'utf8');
875
+ writeFileSync(envPath, buildEnvFileContent(envKey, block, stack, { morphFeature }), 'utf8');
820
876
  excludeWorktreePaths(root, [join('.morph', 'worktree.env')]);
821
877
  return block;
822
878
  } catch {
@@ -9,6 +9,7 @@ import { persistGateDecision, VALID_GATES, readTrustFromConfig } from './gate-de
9
9
  import { minimatch } from 'minimatch';
10
10
  import { detectDefaultBranch, getHeadSha, getDiffStat, changedPathsAgainst } from '../../lib/git/default-branch.js';
11
11
  import { readMorphConfig } from '../../lib/tasks/test-runner.js';
12
+ import { oversizedTasks } from '../../lib/tasks/tasks-json-parser.js';
12
13
  import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
13
14
 
14
15
  /**
@@ -196,6 +197,10 @@ function collectSignals(featureName, feature, cwd, gateName) {
196
197
  designSystemDivergence: false,
197
198
  };
198
199
 
200
+ // Fato de disco, fora de OVERRIDABLE_SIGNAL_KEYS: task que não cabe num
201
+ // dispatch se fatia no plano, não depois de um sub-agent bater no teto.
202
+ if (gateName === 'plan') signals.oversizedTasks = oversizedTasks({ tasks });
203
+
199
204
  // Só onde o checklist decide algo: no fecho do design e no ship. Fato de
200
205
  // disco, fora de OVERRIDABLE_SIGNAL_KEYS como `mutationProof`.
201
206
  if (gateName === 'uiux' || gateName === 'review') {
@@ -1,7 +1,7 @@
1
1
  import chalk from 'chalk';
2
- import { existsSync } from 'fs';
2
+ import { existsSync, readFileSync } from 'fs';
3
3
  import { join } from 'path';
4
- import { parseTasksJsonLenient } from '../../lib/tasks/tasks-json-parser.js';
4
+ import { parseTasksJsonLenient, oversizedTasks, MAX_TASK_OUTPUTS } from '../../lib/tasks/tasks-json-parser.js';
5
5
  import { planDispatch } from '../../lib/tasks/dispatch-planner.js';
6
6
  import { renderMermaidFlowchart } from '../../lib/tasks/mermaid-renderer.js';
7
7
  import { checkUseCases } from '../../lib/specs/usecase-check.js';
@@ -16,6 +16,15 @@ export function printLifecycleWarnings(lifecycle) {
16
16
  for (const w of lifecycle.warnings) console.error(chalk.yellow(` ${w}`));
17
17
  }
18
18
 
19
+ function planGateApproved(featureDir) {
20
+ try {
21
+ const feature = JSON.parse(readFileSync(join(featureDir, 'feature.json'), 'utf8'));
22
+ return feature?.approvalGates?.plan?.approved === true;
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
19
28
  /**
20
29
  * `morph-spec dag <feature>` — analyze the tasks.json DAG and print the
21
30
  * sequential vs parallel dispatch decision, weighted by effort.
@@ -72,11 +81,17 @@ export async function dagCommand(featureName, options = {}) {
72
81
  // revocation rows the finish would fail to apply. Warnings only.
73
82
  const lifecycle = checkLifecycle({ cwd: process.cwd(), featureDir, tasksData: data });
74
83
 
84
+ // Task acima do teto não cabe num dispatch. Antes do Gate 2 é erro (o plano
85
+ // ainda fatia); depois é aviso, para não travar feature em voo.
86
+ const oversized = oversizedTasks(data);
87
+ const sizeBlocks = oversized.length > 0 && !planGateApproved(featureDir);
88
+ const taskSize = { max: MAX_TASK_OUTPUTS, oversized, blocking: sizeBlocks };
89
+
75
90
  const plan = planDispatch(data);
76
91
 
77
92
  if (options.json) {
78
- console.log(JSON.stringify({ ...plan, useCases: ucCheck, artifactCriteria, lifecycle }, null, 2));
79
- if (ucCheck.errors.length) process.exit(1);
93
+ console.log(JSON.stringify({ ...plan, useCases: ucCheck, artifactCriteria, lifecycle, taskSize }, null, 2));
94
+ if (ucCheck.errors.length || sizeBlocks) process.exit(1);
80
95
  return;
81
96
  }
82
97
 
@@ -88,6 +103,14 @@ export async function dagCommand(featureName, options = {}) {
88
103
  console.error(chalk.gray('\n Corrija a referência no tasks.json ou restaure o use case.\n'));
89
104
  process.exit(1);
90
105
  }
106
+ if (oversized.length) {
107
+ const tone = sizeBlocks ? chalk.red : chalk.yellow;
108
+ const head = sizeBlocks ? '❌' : '⚠';
109
+ console.error(tone(`\n${head} Tamanho de task — ${plural(oversized.length, 'task', 'tasks')} acima de ${MAX_TASK_OUTPUTS} outputs`));
110
+ for (const t of oversized) console.error(tone(` ${t.id}: ${t.outputs} outputs`));
111
+ console.error(chalk.gray(' Fatie por pós-condição: uma task cabe num dispatch (morph-plan, "Tamanho da task").\n'));
112
+ if (sizeBlocks) process.exit(1);
113
+ }
91
114
  if (ucCheck.warnings.length) {
92
115
  console.error(chalk.yellow(`\n⚠ Use cases — ${plural(ucCheck.warnings.length, 'aviso', 'avisos')}`));
93
116
  for (const w of ucCheck.warnings) console.error(chalk.yellow(` ${w}`));
@@ -1,9 +1,10 @@
1
1
  import chalk from 'chalk';
2
- import { readFileSync } from 'fs';
3
- import { resolve, join } from 'path';
2
+ import { resolve } from 'path';
4
3
  import { getFeature, updateFeature } from '../../core/state/state-manager.js';
5
4
  import { runVerify, readTask, VerifyUsageError } from '../../lib/validators/verify-runner.js';
6
5
  import { resolvePrimaryRoot } from '../project/worktree.js';
6
+ import { resolveFeatureTree } from '../../lib/worktree/feature-truth.js';
7
+ import { canonicalPath } from '../../lib/tasks/test-runner.js';
7
8
  import { getHeadSha } from '../../lib/git/default-branch.js';
8
9
  import { EVAL_RUNNER } from '../../lib/tasks/eval-node.js';
9
10
  import { buildVerifyEvent, recordVerifyEvent, findRepeatedVerify } from '../../lib/telemetry/verify-event.js';
@@ -93,25 +94,25 @@ function formatVerifyResult(result) {
93
94
  return lines.join('\n');
94
95
  }
95
96
 
97
+ const samePath = (a, b) => canonicalPath(a).replace(/\\/g, '/') === canonicalPath(b).replace(/\\/g, '/');
98
+
96
99
  /**
97
- * Decide whether it's safe to persist a verify result to feature.json.
100
+ * Persist only in the tree that OWNS the feature — the same predicate
101
+ * `list`/`handoff`/the mutator guard read (`resolveFeatureTree`), asked from
102
+ * the PRIMARY ROOT so every tree gets the same answer. Any other tree (a
103
+ * sub-agent's `morph-task/*` tree branched from `morph/{f}`, a scratch
104
+ * checkout) must never write feature.json: its copy is not the one the gates
105
+ * read, and the write would merge-conflict with the owner's.
98
106
  *
99
- * Safe contexts: the primary git root, or the feature's own REGISTERED
100
- * worktree (`worktree.path` — where feature.json is authoritative and
101
- * travels with the branch). Any other worktree (e.g. a harness subagent's
102
- * temporary scratch worktree) must never write feature.json — that would
103
- * risk a merge conflict / violate the single-writer clause.
107
+ * The old rule read only the root's `worktree.path` pointer, which a feature
108
+ * created INSIDE its worktree never has: a `worktrees/homolog-v2` on
109
+ * `morph/chat-demo-simulacao` measured `unregistered-worktree` on 11/11 runs,
110
+ * and `repeatOf` (computed only when persisting) never fired. Git's worktree
111
+ * registry names that tree as the owner.
104
112
  *
105
- * Worktree registration is ROOT-OWNED data: `runWorktreeSetup` (see
106
- * src/commands/project/worktree.js) writes `.worktree` ONLY into the PRIMARY
107
- * ROOT's copy of feature.json (via saveState after chdir'ing back to root) —
108
- * it never touches the worktree's own branch-local copy. So this must read
109
- * the ROOT's feature.json to find the registration, never the cwd-local one
110
- * (`getFeature(featureName)` resolves relative to `process.cwd()`, which
111
- * inside a worktree is the branch-local copy and will never carry
112
- * `.worktree` — reading it here would make this branch permanently dead code
113
- * for the exact case it exists to handle: verify running from inside a
114
- * registered worktree).
113
+ * The root is not special: with a live worktree owning the feature, the
114
+ * root's copy is the birth-instant skeleton and a stamp there is drift —
115
+ * `owner-tree-elsewhere`, the verify side of `assertOwnerTree`.
115
116
  *
116
117
  * @param {{ cwd: string, featureName: string, noPersist: boolean }} params
117
118
  * @returns {{ persist: boolean, reason: string|null }}
@@ -120,26 +121,9 @@ function decidePersistence({ cwd, featureName, noPersist }) {
120
121
  if (noPersist) return { persist: false, reason: 'no-persist-flag' };
121
122
 
122
123
  const root = resolvePrimaryRoot(cwd);
123
- const normalizedCwd = resolve(cwd).replace(/\\/g, '/');
124
-
125
- if (normalizedCwd === root) return { persist: true, reason: null };
126
-
127
- // cwd is (or looks like) a worktree — consult the ROOT's feature.json for
128
- // the registration. Fail-open: an unreadable/missing root feature.json
129
- // means we can't confirm registration, so treat as unregistered.
130
- let registeredWt = null;
131
- try {
132
- const rootFeatureJsonPath = join(root, '.morph', 'features', featureName, 'feature.json');
133
- const rootFeature = JSON.parse(readFileSync(rootFeatureJsonPath, 'utf8'));
134
- registeredWt = rootFeature.worktree?.path
135
- ? String(rootFeature.worktree.path).replace(/\\/g, '/')
136
- : null;
137
- } catch {
138
- registeredWt = null;
139
- }
140
-
141
- if (registeredWt && normalizedCwd === registeredWt) return { persist: true, reason: null };
142
- return { persist: false, reason: 'unregistered-worktree' };
124
+ const owner = resolveFeatureTree(featureName, root);
125
+ if (owner.tree && samePath(owner.tree, cwd)) return { persist: true, reason: null };
126
+ return { persist: false, reason: samePath(root, cwd) ? 'owner-tree-elsewhere' : 'unregistered-worktree' };
143
127
  }
144
128
 
145
129
  /**
@@ -1,28 +1,92 @@
1
- import { Routes, Route, Navigate } from 'react-router-dom';
2
- import Sidebar from './components/Sidebar.jsx';
1
+ import { useEffect, useState } from 'react';
2
+ import { Routes, Route, Navigate, useLocation, useParams } from 'react-router-dom';
3
+ import Sidebar, { MobileNav } from './components/Sidebar.jsx';
4
+ import ErrorBoundary from './components/ErrorBoundary.jsx';
5
+ import CommandPalette from './components/CommandPalette.jsx';
6
+ import Portfolio from './pages/Portfolio.jsx';
7
+ import Features, { isWorking } from './pages/Features.jsx';
8
+ import FeatureDetail from './pages/FeatureDetail.jsx';
9
+ import Activity from './pages/Activity.jsx';
3
10
  import AgentMap from './pages/AgentMap.jsx';
4
11
  import PhasePipeline from './pages/PhasePipeline.jsx';
5
12
  import HookInspector from './pages/HookInspector.jsx';
6
13
  import SkillsLibrary from './pages/SkillsLibrary.jsx';
7
14
  import RulesMap from './pages/RulesMap.jsx';
8
15
  import Standards from './pages/Standards.jsx';
9
- import LiveState from './pages/LiveState.jsx';
16
+ import Guide from './pages/Guide.jsx';
17
+ import Rubrics from './pages/Rubrics.jsx';
18
+ import { useNow } from './hooks/usePref.js';
19
+ import { useProjects, useCurrentProject, projectHref } from './lib/project.js';
20
+
21
+ /**
22
+ * What the sidebar and the tab title say at a glance, from the portfolio:
23
+ * per project, how many features are active, waiting on you and being worked
24
+ * on right now.
25
+ */
26
+ function usePulse(portfolio) {
27
+ const now = useNow();
28
+ const byProject = {};
29
+ for (const p of portfolio?.projects || []) {
30
+ const s = p.summary;
31
+ byProject[p.id] = s
32
+ ? { active: s.active, awaiting: s.awaiting, working: s.activeFeatures.filter((f) => isWorking(f, now)).length }
33
+ : null;
34
+ }
35
+ const awaiting = Object.values(byProject).reduce((n, x) => n + (x?.awaiting || 0), 0);
36
+ useEffect(() => {
37
+ document.title = awaiting ? `(${awaiting}) morph-spec` : 'morph-spec';
38
+ }, [awaiting]);
39
+ return { byProject, awaiting };
40
+ }
41
+
42
+ function ProjectIndex() {
43
+ const { project } = useParams();
44
+ return <Navigate to={projectHref(project)} replace />;
45
+ }
10
46
 
11
47
  export default function App() {
48
+ const { pathname } = useLocation();
49
+ const { data: portfolio } = useProjects();
50
+ const current = useCurrentProject(portfolio);
51
+ const pulse = usePulse(portfolio);
52
+ const [palette, setPalette] = useState(false);
53
+ // Ctrl+K / Cmd+K opens the search from anywhere.
54
+ useEffect(() => {
55
+ const onKey = (e) => {
56
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); setPalette((o) => !o); }
57
+ };
58
+ window.addEventListener('keydown', onKey);
59
+ return () => window.removeEventListener('keydown', onKey);
60
+ }, []);
61
+ const nav = { portfolio, current: current.id, pulse, onSearch: () => setPalette(true) };
12
62
  return (
13
- <div className="flex h-screen bg-gray-950 text-gray-100 overflow-hidden">
14
- <Sidebar />
15
- <main className="flex-1 overflow-auto">
16
- <Routes>
17
- <Route path="/" element={<Navigate to="/agents" replace />} />
18
- <Route path="/agents" element={<AgentMap />} />
19
- <Route path="/phases" element={<PhasePipeline />} />
20
- <Route path="/hooks" element={<HookInspector />} />
21
- <Route path="/skills" element={<SkillsLibrary />} />
22
- <Route path="/rules" element={<RulesMap />} />
23
- <Route path="/standards" element={<Standards />} />
24
- <Route path="/state" element={<LiveState />} />
25
- </Routes>
63
+ <div className="flex h-full overflow-hidden">
64
+ <div className="ambient" aria-hidden="true" />
65
+ <Sidebar {...nav} />
66
+ <CommandPalette open={palette} onClose={() => setPalette(false)} portfolio={portfolio} project={current.id} />
67
+ <main className="relative z-10 flex-1 overflow-auto">
68
+ <MobileNav {...nav} />
69
+ {/* Inside <main>, keyed by route: a page that throws leaves the
70
+ sidebar alive, and navigating away remounts a clean boundary. */}
71
+ <ErrorBoundary key={pathname} page={pathname}>
72
+ <Routes>
73
+ <Route path="/" element={<Portfolio />} />
74
+ <Route path="/p/:project" element={<ProjectIndex />} />
75
+ <Route path="/p/:project/features" element={<Features />} />
76
+ <Route path="/p/:project/features/:name" element={<FeatureDetail />} />
77
+ <Route path="/p/:project/activity" element={<Activity />} />
78
+ <Route path="/guide" element={<Guide />} />
79
+ <Route path="/guide/:doc" element={<Guide />} />
80
+ <Route path="/rubrics" element={<Rubrics />} />
81
+ <Route path="/agents" element={<AgentMap />} />
82
+ <Route path="/phases" element={<PhasePipeline />} />
83
+ <Route path="/hooks" element={<HookInspector />} />
84
+ <Route path="/skills" element={<SkillsLibrary />} />
85
+ <Route path="/rules" element={<RulesMap />} />
86
+ <Route path="/standards" element={<Standards />} />
87
+ <Route path="*" element={<Navigate to="/" replace />} />
88
+ </Routes>
89
+ </ErrorBoundary>
26
90
  </main>
27
91
  </div>
28
92
  );