@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,5 +1,5 @@
1
1
  /**
2
- * Memory Installer — populates Claude Code persistent memory during morph-init.
2
+ * Memory Installer — populates Claude Code persistent memory during `morph-spec init`.
3
3
  *
4
4
  * Writes two memory files (project + reference) plus an entry in MEMORY.md
5
5
  * so future Claude Code sessions enter pre-contextualized about the
@@ -67,7 +67,6 @@ metadata:
67
67
  - When user mentions a feature, check \`.morph/features/\` first.
68
68
  - When user types \`/morph-proposal\`, invoke \`Skill(morph-proposal)\` directly.
69
69
  - In \`trust: "auto"\`, gates auto-pass; only AskUserQuestion on risk signals.
70
- - Prefer morph-* skills over superpowers:* equivalents in this project.
71
70
  `;
72
71
  }
73
72
 
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Where the morph-spec projects on this machine are — the list the dashboard's
3
+ * portfolio reads. Nothing else records it: each project only knows its own
4
+ * `.morph/`.
5
+ *
6
+ * Three writers register a project as a side note of what they already do —
7
+ * `init`, `update` (never under `--dry-run`) and `dashboard` — and the
8
+ * dashboard adds and removes on request. Registering is fail-open: a registry
9
+ * that cannot be read or written never fails the command that tried.
10
+ *
11
+ * The file lives in the user's home (`~/.morph-spec/projects.json`, or
12
+ * `MORPH_PROJECTS_REGISTRY`), read through `os.homedir()` at call time so a
13
+ * suite that isolates the home also isolates this file. Paths under the
14
+ * system temp dir are never registered: the test suites run `init`/`update`
15
+ * there by the hundred, including through spawned CLIs that keep the real home.
16
+ */
17
+
18
+ import { existsSync, readFileSync, readdirSync } from 'fs';
19
+ import { homedir, tmpdir } from 'os';
20
+ import { join, resolve, basename, dirname } from 'path';
21
+ import { atomicWriteJson } from '../../core/state/state-manager.js';
22
+ import { canonicalPath, isInside } from '../tasks/test-runner.js';
23
+ import { WORKTREE_DIR_NAMES } from '../worktree/dirs.js';
24
+
25
+ export const REGISTRY_VERSION = 1;
26
+
27
+ export function registryPath() {
28
+ return process.env.MORPH_PROJECTS_REGISTRY || join(homedir(), '.morph-spec', 'projects.json');
29
+ }
30
+
31
+ /** One spelling per directory: canonical, forward slashes (the registry is read back on any OS). */
32
+ export function projectKey(dir) {
33
+ return canonicalPath(resolve(dir)).replace(/\\/g, '/');
34
+ }
35
+
36
+ const sameDir = (a, b) => (process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b);
37
+
38
+ /** A folder is a morph-spec project when morph-spec installed its config there. */
39
+ export function isMorphProject(dir) {
40
+ return existsSync(join(dir, '.morph', 'config', 'config.json'));
41
+ }
42
+
43
+ /** A worktree checkout is part of its repo's project, never a project of its own. */
44
+ function insideWorktreeContainer(dir) {
45
+ const parent = basename(dirname(dir));
46
+ return WORKTREE_DIR_NAMES.includes(parent);
47
+ }
48
+
49
+ function projectName(dir) {
50
+ try {
51
+ const config = JSON.parse(readFileSync(join(dir, '.morph', 'config', 'config.json'), 'utf8'));
52
+ const name = config?.project?.name;
53
+ // Some configs carry the project path as its name; the folder reads better.
54
+ return typeof name === 'string' && name.trim() && !/[\\/]/.test(name) ? name.trim() : basename(dir);
55
+ } catch {
56
+ return basename(dir);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * The registered projects that still exist. A folder that was deleted or lost
62
+ * its `.morph/` drops out on read — the file is rewritten on the next write.
63
+ *
64
+ * @returns {{ version: number, projects: Array<{ dir: string, name: string, addedAt: string, lastSeenAt: string, source: string }> }}
65
+ */
66
+ export function readRegistry() {
67
+ let data = null;
68
+ try {
69
+ const path = registryPath();
70
+ if (existsSync(path)) data = JSON.parse(readFileSync(path, 'utf8'));
71
+ } catch { /* unreadable registry reads as empty */ }
72
+ const projects = Array.isArray(data?.projects) ? data.projects : [];
73
+ return {
74
+ version: REGISTRY_VERSION,
75
+ projects: projects.filter((p) => typeof p?.dir === 'string' && isMorphProject(p.dir)),
76
+ };
77
+ }
78
+
79
+ function write(projects) {
80
+ atomicWriteJson(registryPath(), { version: REGISTRY_VERSION, projects });
81
+ }
82
+
83
+ /**
84
+ * Note that `dir` is a morph-spec project. Idempotent; refreshes `lastSeenAt`.
85
+ * Never throws.
86
+ *
87
+ * @param {string} dir
88
+ * @param {'init'|'update'|'dashboard'|'manual'} source
89
+ * @param {{ tempRoot?: string }} [opts] - test seam: the folder whose
90
+ * projects are never registered (the system temp dir)
91
+ * @returns {boolean} true when the project is in the registry afterwards
92
+ */
93
+ export function registerProject(dir, source, { tempRoot = tmpdir() } = {}) {
94
+ try {
95
+ const key = projectKey(dir);
96
+ if (isInside(canonicalPath(key), canonicalPath(tempRoot))) return false;
97
+ if (!isMorphProject(key) || insideWorktreeContainer(key)) return false;
98
+ const now = new Date().toISOString();
99
+ const { projects } = readRegistry();
100
+ const found = projects.find((p) => sameDir(p.dir, key));
101
+ if (found) {
102
+ found.lastSeenAt = now;
103
+ found.name = projectName(key);
104
+ } else {
105
+ projects.push({ dir: key, name: projectName(key), addedAt: now, lastSeenAt: now, source });
106
+ }
107
+ write(projects);
108
+ return true;
109
+ } catch {
110
+ return false;
111
+ }
112
+ }
113
+
114
+ /** Take a project off the list. Its files are not touched. Never throws. */
115
+ export function unregisterProject(dir) {
116
+ try {
117
+ const key = projectKey(dir);
118
+ const { projects } = readRegistry();
119
+ const kept = projects.filter((p) => !sameDir(p.dir, key));
120
+ if (kept.length !== projects.length) write(kept);
121
+ return kept.length !== projects.length;
122
+ } catch {
123
+ return false;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * morph-spec projects directly under each root that are not registered yet —
129
+ * the portfolio offers them, the person decides. One level deep: a repos
130
+ * folder is the expected root, and a deeper walk would enter node_modules and
131
+ * worktrees for nothing.
132
+ *
133
+ * @param {string[]} roots
134
+ * @returns {Array<{ dir: string, name: string }>}
135
+ */
136
+ export function discoverProjects(roots) {
137
+ const registered = readRegistry().projects.map((p) => p.dir);
138
+ const out = [];
139
+ const seen = new Set();
140
+ for (const root of roots) {
141
+ let entries = [];
142
+ try { entries = readdirSync(root, { withFileTypes: true }); } catch { continue; }
143
+ for (const e of entries) {
144
+ if (!e.isDirectory() || e.name.startsWith('.') || e.name === 'node_modules') continue;
145
+ const dir = join(root, e.name);
146
+ if (!isMorphProject(dir)) continue;
147
+ const key = projectKey(dir);
148
+ if (seen.has(key.toLowerCase()) || registered.some((r) => sameDir(r, key))) continue;
149
+ seen.add(key.toLowerCase());
150
+ out.push({ dir: key, name: projectName(key) });
151
+ }
152
+ }
153
+ return out.sort((a, b) => a.name.localeCompare(b.name));
154
+ }
@@ -5,7 +5,9 @@
5
5
  * (a) linhas de comentário que citam processo (task, UC, ADR, Gate...), que o
6
6
  * standard `architecture-code-comments` manda para o commit;
7
7
  * (b) linhas de teste ÷ linhas de produção, contra o orçamento por task do
8
- * `morph-plan` (um teste por pós-condição + um de regressão por bug).
8
+ * `morph-plan` (um teste por pós-condição + um de regressão por bug);
9
+ * (c) linhas de comentário ÷ linhas de código, porque o padrão do standard é
10
+ * nenhum comentário. Doc-comment (`///`, JSDoc) conta como qualquer outro.
9
11
  *
10
12
  * Sempre `warning`: é medida de volume, e o excesso pode estar justificado na
11
13
  * task. Um aviso por medida, não por linha, porque cada aviso custa 1 ponto no
@@ -28,6 +30,12 @@ export const MAX_TEST_RATIO = 1.5;
28
30
  /** Abaixo disto a razão não é julgada: a regressão de um bug de 2 linhas cabe aqui. */
29
31
  export const MIN_TEST_LINES = 40;
30
32
 
33
+ /** Mais de uma linha de comentário a cada dez de código: os nomes não estão fazendo o trabalho. */
34
+ export const MAX_COMMENT_RATIO = 0.1;
35
+
36
+ /** Abaixo disto a densidade não é julgada: um comentário legítimo num arquivo pequeno já passaria da razão. */
37
+ export const MIN_CODE_LINES = 40;
38
+
31
39
  /** Quantos locais citar no aviso de comentário antes de resumir com "(+N)". */
32
40
  const MAX_CITED = 4;
33
41
 
@@ -42,10 +50,10 @@ const decimal = (n) => n.toFixed(1).replace('.', ',');
42
50
  * @param {Array<{path: string, content: string, added: number[]|null}>} files -
43
51
  * `added` são as linhas adicionadas; `null` é o arquivo inteiro (novo)
44
52
  * @returns {{commentLines: number, processLines: Array<{file: string, line: number, text: string}>,
45
- * testLines: number, productionLines: number}}
53
+ * testLines: number, productionLines: number, commentsByFile: Record<string, number>}}
46
54
  */
47
55
  export function measureCodeBudget(files) {
48
- const measured = { commentLines: 0, processLines: [], testLines: 0, productionLines: 0 };
56
+ const measured = { commentLines: 0, processLines: [], testLines: 0, productionLines: 0, commentsByFile: {} };
49
57
 
50
58
  for (const { path, content, added } of files) {
51
59
  if (!isProjectSource(path) || GENERATED.test(path)) continue;
@@ -60,6 +68,7 @@ export function measureCodeBudget(files) {
60
68
  for (const [line, text] of commentLines(content, path)) {
61
69
  if (!isAdded(line)) continue;
62
70
  measured.commentLines++;
71
+ measured.commentsByFile[path] = (measured.commentsByFile[path] ?? 0) + 1;
63
72
  if (citesProcess(text)) measured.processLines.push({ file: path, line, text });
64
73
  }
65
74
  }
@@ -73,7 +82,7 @@ export function measureCodeBudget(files) {
73
82
  */
74
83
  export function budgetIssues(measured) {
75
84
  const issues = [];
76
- const { commentLines: comments, processLines, testLines, productionLines } = measured;
85
+ const { commentLines: comments, processLines, testLines, productionLines, commentsByFile = {} } = measured;
77
86
 
78
87
  if (processLines.length > 0) {
79
88
  const shown = processLines.slice(0, MAX_CITED).map((p) => `${p.file}:${p.line}`);
@@ -104,6 +113,25 @@ export function budgetIssues(measured) {
104
113
  });
105
114
  }
106
115
 
116
+ const codeLines = testLines + productionLines - comments;
117
+ if (codeLines >= MIN_CODE_LINES && comments > MAX_COMMENT_RATIO * codeLines) {
118
+ const ranked = Object.entries(commentsByFile).sort(([, a], [, b]) => b - a);
119
+ const shown = ranked.slice(0, MAX_CITED).map(([file, n]) => `${file} (${n})`);
120
+ const more = ranked.length - shown.length;
121
+ issues.push({
122
+ level: 'warning',
123
+ message:
124
+ `${comments} linhas de comentário para ${codeLines} de código adicionadas ` +
125
+ `(${decimal((comments / codeLines) * 10)} a cada 10, o teto é ${decimal(MAX_COMMENT_RATIO * 10)}): ` +
126
+ `${shown.join(', ')}${more > 0 ? ` (+${more})` : ''}`,
127
+ file: ranked[0]?.[0],
128
+ solution:
129
+ 'O padrão é nenhum comentário: renomeie, extraia função ou constante, expresse no tipo ou ' +
130
+ 'escreva um teste. Fica só o porquê que o código não mostra, sem doc-comment que repete a ' +
131
+ 'assinatura (standard architecture-code-comments).',
132
+ });
133
+ }
134
+
107
135
  return issues;
108
136
  }
109
137
 
@@ -100,7 +100,7 @@ async function downloadPluginFiles(pluginName, sha, destDir) {
100
100
 
101
101
  /**
102
102
  * Install a Claude Code plugin from GitHub.
103
- * @param {string} pluginName - e.g. "superpowers"
103
+ * @param {string} pluginName - e.g. "context7"
104
104
  * @returns {{ installed: boolean, version: string, alreadyInstalled: boolean }}
105
105
  */
106
106
  export async function installPlugin(pluginName) {
@@ -2,9 +2,8 @@
2
2
  * setup-infra.js
3
3
  *
4
4
  * Headless infrastructure setup for MORPH-SPEC.
5
- * Extracted from init.js (steps 1-10) so it can be called by the
6
- * /morph-init Claude Code skill via `morph-spec setup-infra` without
7
- * interactive prompts or stack detection.
5
+ * Extracted from init.js (steps 1-10) so it can run as
6
+ * `morph-spec setup-infra` without interactive prompts or stack detection.
8
7
  *
9
8
  * Usage:
10
9
  * import { setupInfra } from './setup-infra.js';
@@ -38,7 +37,6 @@ import { realEffects } from '../lib/installers/effects.js';
38
37
  const FRAMEWORK_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'framework');
39
38
 
40
39
  const REQUIRED_PLUGINS = [
41
- 'superpowers@claude-plugins-official',
42
40
  'context7@claude-plugins-official',
43
41
  ];
44
42
 
@@ -196,19 +196,63 @@ function personaError(agent) {
196
196
  }
197
197
 
198
198
  /**
199
- * Load the ids of MCP servers known to the framework registry
200
- * (framework/mcp/registry.json). Used to validate agents.json `mcp` entries:
201
- * only registry-known servers become tool grants (fail-open on any error).
199
+ * Tools every persona gets on top of its scope. A `tools:` line is a strict
200
+ * allowlist: without `Skill` the sub-agent cannot invoke a single skill, and
201
+ * without `ToolSearch` it cannot load an MCP tool the session defers.
202
+ */
203
+ export const PLATFORM_TOOLS = ['Skill', 'ToolSearch'];
204
+
205
+ /**
206
+ * The server names an MCP registry entry is granted under. Claude Code names a
207
+ * server's tools after WHERE it is configured — `mcp__context7__*` from
208
+ * `.mcp.json`, `mcp__plugin_context7_context7__*` from the plugin — and a grant
209
+ * only matches its own spelling (measured 2026-09-24: `mcp__context7` gave a
210
+ * plugin-installed context7 nothing). So every spelling becomes a grant; one
211
+ * the project does not configure is inert.
212
+ *
213
+ * @param {string} id - registry key
214
+ * @param {object} entry - registry entry
215
+ * @returns {string[]}
216
+ */
217
+ export function mcpServerNames(id, entry) {
218
+ // A malformed list falls back to the id instead of breaking the render:
219
+ // installAgents and checkAgentsDrift both go through here (fail-open).
220
+ return serverNamesProblem(id, entry?.serverNames) === null ? entry?.serverNames ?? [id] : [id];
221
+ }
222
+
223
+ /**
224
+ * The ONE rule for a registry entry's `serverNames` (validate-framework Check 8
225
+ * reads it). Absent is valid (`[id]`). Present: non-empty, only the characters
226
+ * Claude Code keeps in a tool name, and it must include the id itself — that is
227
+ * the name the MCP installer writes into `.mcp.json`.
228
+ *
229
+ * @param {string} id - registry key
230
+ * @param {unknown} value - the raw `serverNames`
231
+ * @returns {string|null} what was expected, or null when valid
232
+ */
233
+ export function serverNamesProblem(id, value) {
234
+ if (value === undefined) return null;
235
+ if (!Array.isArray(value) || value.length === 0) return 'esperado array não vazio';
236
+ const bad = value.find(n => typeof n !== 'string' || !/^[A-Za-z0-9_-]+$/.test(n));
237
+ if (bad !== undefined) return `nome inválido ${JSON.stringify(bad)} (só [A-Za-z0-9_-])`;
238
+ return value.includes(id) ? null : `precisa incluir o próprio id "${id}"`;
239
+ }
240
+
241
+ /**
242
+ * Load the MCP servers known to the framework registry
243
+ * (framework/mcp/registry.json) with the server names each is granted under.
244
+ * Only registry-known ids in agents.json `mcp` become tool grants (fail-open
245
+ * on any error).
202
246
  *
203
247
  * @param {string} frameworkDir
204
- * @returns {Set<string>|null} null = no registry found (accept nothing)
248
+ * @returns {Map<string, string[]>|null} id → server names; null = no registry found (accept nothing)
205
249
  */
206
250
  export function loadKnownMcpIds(frameworkDir) {
207
251
  try {
208
252
  const registryPath = join(frameworkDir, 'mcp', 'registry.json');
209
253
  if (!existsSync(registryPath)) return null;
210
254
  const registry = JSON.parse(readFileSync(registryPath, 'utf-8'));
211
- return new Set(Object.keys(registry.mcps ?? {}));
255
+ return new Map(Object.entries(registry.mcps ?? {}).map(([id, entry]) => [id, mcpServerNames(id, entry)]));
212
256
  } catch {
213
257
  return null;
214
258
  }
@@ -321,8 +365,8 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
321
365
  * stale file differs byte-for-byte from this output).
322
366
  *
323
367
  * @param {object} agent - agents.json entry with `id` merged in
324
- * @param {Set<string>|null} [knownMcpIds] - registry-known MCP ids; agents.json
325
- * `mcp` entries outside this set are ignored (null = ignore all)
368
+ * @param {Map<string, string[]>|null} [knownMcpIds] - registry-known MCP ids and
369
+ * their server names; agents.json `mcp` entries outside it are ignored (null = ignore all)
326
370
  * @returns {string} complete file content
327
371
  */
328
372
  export function renderAgentMd(agent, knownMcpIds = null) {
@@ -338,14 +382,15 @@ export function renderAgentMd(agent, knownMcpIds = null) {
338
382
  }
339
383
 
340
384
  function buildToolsLine(agent, knownMcpIds) {
341
- const base = agent.tools ?? DEFAULT_AGENT_TOOLS;
385
+ const base = String(agent.tools ?? DEFAULT_AGENT_TOOLS).split(',').map(t => t.trim()).filter(Boolean);
386
+ const platform = PLATFORM_TOOLS.filter(t => !base.includes(t));
342
387
  // Domain-relevant MCP servers (agents.json `mcp: [ids]`) become server-level
343
- // grants (`mcp__{id}`). Only registry-known ids are emitted; in projects
344
- // without that server configured the grant is inert.
388
+ // grants, one per server name (`mcp__{name}`). Only registry-known ids are
389
+ // emitted; in projects without that server configured the grant is inert.
345
390
  const mcpGrants = (agent.mcp ?? [])
346
391
  .filter(id => knownMcpIds?.has(id))
347
- .map(id => `mcp__${id}`);
348
- return [base, ...mcpGrants].join(', ');
392
+ .flatMap(id => knownMcpIds.get(id).map(name => `mcp__${name}`));
393
+ return [...base, ...platform, ...mcpGrants].join(', ');
349
394
  }
350
395
 
351
396
  function buildFrontmatter(agent, description, knownMcpIds, maxTurns) {
@@ -117,7 +117,7 @@ export function printInstallBanner({ version, nodeVersion, results = [] }) {
117
117
  sep("Next · new project"),
118
118
  ` ${chalk.cyan("1.")} ${DIM("cd")} ${chalk.white("your-project")} ${DIM("(git repo)")}`,
119
119
  ` ${chalk.cyan("2.")} Run ${chalk.bold.white("morph-spec init")}`,
120
- ` ${chalk.cyan("3.")} Open it in Claude Code and run ${chalk.bold.white("/morph-init")}`,
120
+ ` ${chalk.cyan("3.")} Open it in Claude Code and run ${chalk.bold.white("/morph-proposal <feature>")}`,
121
121
  "",
122
122
  sep("Next · project already on morph-spec"),
123
123
  ` ${chalk.cyan("1.")} Run ${chalk.bold.white("morph-spec update")} ${DIM("— after a CLI upgrade or a project.stack change: full stack-aware resync")}`,
@@ -1,13 +0,0 @@
1
- /**
2
- * All Commands - Category Exports
3
- *
4
- * Commands organized by functional category.
5
- */
6
-
7
- export * from './state/index.js';
8
- export * from './validation/index.js';
9
- export * from './templates/index.js';
10
- export * from './project/index.js';
11
- export * from './tasks/index.js';
12
- export * from './agents/index.js';
13
- export * from './utils/index.js';
@@ -1,209 +0,0 @@
1
- /**
2
- * MORPH-SPEC Checkpoint Save/Restore Command
3
- *
4
- * Save and restore full context at phase boundaries.
5
- *
6
- * Usage:
7
- * morph-spec checkpoint save <feature-name>
8
- * morph-spec checkpoint restore <feature-name>
9
- * morph-spec checkpoint list <feature-name>
10
- */
11
-
12
- import fs from 'fs';
13
- import path from 'path';
14
- import chalk from 'chalk';
15
- import { loadState, saveState, getFeature } from '../../core/state/state-manager.js';
16
-
17
- /**
18
- * Get checkpoints directory for a feature
19
- */
20
- function getCheckpointsDir(featureName) {
21
- return path.join(process.cwd(), '.morph/checkpoints', featureName);
22
- }
23
-
24
- /**
25
- * Save checkpoint command
26
- */
27
- export async function checkpointSaveCommand(featureName, options = {}) {
28
- if (!featureName) {
29
- console.error(chalk.red('Error: Feature name required'));
30
- process.exit(1);
31
- }
32
-
33
- const feature = getFeature(featureName);
34
- if (!feature) {
35
- console.error(chalk.red(`Error: Feature '${featureName}' not found`));
36
- process.exit(1);
37
- }
38
-
39
- const outputDir = path.join(process.cwd(), '.morph/features', featureName);
40
- if (!fs.existsSync(outputDir)) {
41
- console.error(chalk.red(`Error: No outputs found for '${featureName}'`));
42
- process.exit(1);
43
- }
44
-
45
- // Create checkpoint directory with timestamp
46
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
47
- const label = options.label || feature.phase;
48
- const checkpointName = `${label}_${timestamp}`;
49
- const checkpointDir = path.join(getCheckpointsDir(featureName), checkpointName);
50
- fs.mkdirSync(checkpointDir, { recursive: true });
51
-
52
- // Copy all output files
53
- const files = fs.readdirSync(outputDir);
54
- let fileCount = 0;
55
- for (const file of files) {
56
- const src = path.join(outputDir, file);
57
- const stat = fs.statSync(src);
58
- if (stat.isFile()) {
59
- fs.copyFileSync(src, path.join(checkpointDir, file));
60
- fileCount++;
61
- }
62
- }
63
-
64
- // Save state snapshot
65
- const stateSnapshot = {
66
- feature: { ...feature },
67
- timestamp: new Date().toISOString(),
68
- phase: feature.phase,
69
- label,
70
- files: files.filter(f => fs.statSync(path.join(outputDir, f)).isFile())
71
- };
72
- fs.writeFileSync(
73
- path.join(checkpointDir, '_checkpoint.json'),
74
- JSON.stringify(stateSnapshot, null, 2)
75
- );
76
-
77
- console.log(chalk.green(`\n✓ Checkpoint saved: ${checkpointName}`));
78
- console.log(chalk.gray(` Phase: ${feature.phase}`));
79
- console.log(chalk.gray(` Files: ${fileCount}`));
80
- console.log(chalk.gray(` Path: ${checkpointDir}\n`));
81
- }
82
-
83
- /**
84
- * Restore checkpoint command
85
- */
86
- export async function checkpointRestoreCommand(featureName, checkpointName, options = {}) {
87
- if (!featureName) {
88
- console.error(chalk.red('Error: Feature name required'));
89
- process.exit(1);
90
- }
91
-
92
- const checkpointsDir = getCheckpointsDir(featureName);
93
- if (!fs.existsSync(checkpointsDir)) {
94
- console.error(chalk.red(`Error: No checkpoints found for '${featureName}'`));
95
- process.exit(1);
96
- }
97
-
98
- // Find checkpoint to restore
99
- let targetDir;
100
- if (checkpointName) {
101
- targetDir = path.join(checkpointsDir, checkpointName);
102
- if (!fs.existsSync(targetDir)) {
103
- console.error(chalk.red(`Error: Checkpoint '${checkpointName}' not found`));
104
- process.exit(1);
105
- }
106
- } else {
107
- // Restore latest
108
- const checkpoints = fs.readdirSync(checkpointsDir)
109
- .filter(d => fs.statSync(path.join(checkpointsDir, d)).isDirectory())
110
- .sort()
111
- .reverse();
112
-
113
- if (checkpoints.length === 0) {
114
- console.error(chalk.red(`Error: No checkpoints found for '${featureName}'`));
115
- process.exit(1);
116
- }
117
-
118
- targetDir = path.join(checkpointsDir, checkpoints[0]);
119
- checkpointName = checkpoints[0];
120
- }
121
-
122
- // Read checkpoint metadata
123
- const metaPath = path.join(targetDir, '_checkpoint.json');
124
- if (!fs.existsSync(metaPath)) {
125
- console.error(chalk.red('Error: Checkpoint metadata not found'));
126
- process.exit(1);
127
- }
128
-
129
- const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
130
-
131
- // Restore output files
132
- const outputDir = path.join(process.cwd(), '.morph/features', featureName);
133
- fs.mkdirSync(outputDir, { recursive: true });
134
-
135
- const files = fs.readdirSync(targetDir).filter(f => f !== '_checkpoint.json');
136
- for (const file of files) {
137
- fs.copyFileSync(path.join(targetDir, file), path.join(outputDir, file));
138
- }
139
-
140
- // Restore state
141
- const state = loadState();
142
- if (state.features[featureName] && meta.feature) {
143
- // Preserve some current fields, restore phase and outputs
144
- state.features[featureName].phase = meta.feature.phase;
145
- state.features[featureName].outputs = meta.feature.outputs;
146
- state.features[featureName].tasks = meta.feature.tasks;
147
- state.features[featureName].approvalGates = meta.feature.approvalGates;
148
- state.features[featureName].updatedAt = new Date().toISOString();
149
- saveState(state);
150
- }
151
-
152
- console.log(chalk.green(`\n✓ Checkpoint restored: ${checkpointName}`));
153
- console.log(chalk.gray(` Phase: ${meta.phase}`));
154
- console.log(chalk.gray(` Files restored: ${files.length}`));
155
- console.log(chalk.gray(` Original date: ${meta.timestamp}\n`));
156
- }
157
-
158
- /**
159
- * List checkpoints command
160
- */
161
- export async function checkpointListCommand(featureName, options = {}) {
162
- if (!featureName) {
163
- console.error(chalk.red('Error: Feature name required'));
164
- process.exit(1);
165
- }
166
-
167
- const checkpointsDir = getCheckpointsDir(featureName);
168
- if (!fs.existsSync(checkpointsDir)) {
169
- console.log(chalk.gray(`No checkpoints found for '${featureName}'.\n`));
170
- return;
171
- }
172
-
173
- const checkpoints = fs.readdirSync(checkpointsDir)
174
- .filter(d => fs.statSync(path.join(checkpointsDir, d)).isDirectory())
175
- .sort()
176
- .reverse();
177
-
178
- if (checkpoints.length === 0) {
179
- console.log(chalk.gray(`No checkpoints found for '${featureName}'.\n`));
180
- return;
181
- }
182
-
183
- // JSON output
184
- if (options.json) {
185
- const list = checkpoints.map(cp => {
186
- const metaPath = path.join(checkpointsDir, cp, '_checkpoint.json');
187
- const meta = fs.existsSync(metaPath) ? JSON.parse(fs.readFileSync(metaPath, 'utf8')) : {};
188
- return { name: cp, phase: meta.phase, timestamp: meta.timestamp, files: meta.files?.length || 0 };
189
- });
190
- console.log(JSON.stringify({ feature: featureName, checkpoints: list }, null, 2));
191
- return;
192
- }
193
-
194
- // Visual output
195
- console.log(chalk.cyan(`\nCheckpoints for '${featureName}':\n`));
196
-
197
- for (const cp of checkpoints) {
198
- const metaPath = path.join(checkpointsDir, cp, '_checkpoint.json');
199
- let meta = {};
200
- if (fs.existsSync(metaPath)) {
201
- meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
202
- }
203
-
204
- const date = meta.timestamp ? new Date(meta.timestamp).toLocaleString() : 'unknown';
205
- console.log(chalk.white(` ${cp}`));
206
- console.log(chalk.gray(` Phase: ${meta.phase || 'unknown'} | Files: ${meta.files?.length || '?'} | ${date}`));
207
- }
208
- console.log('');
209
- }