@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.
- package/CLAUDE.md +19 -8
- package/README.md +2 -2
- package/bin/morph-spec.js +60 -43
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +3 -2
- package/framework/MORPH.md +1 -1
- package/framework/agents.json +36 -25
- package/framework/docs/CLI.md +2 -2
- package/framework/evals/rubrics/code-quality.md +4 -4
- package/framework/evals/rubrics/condensed.md +2 -2
- package/framework/mcp/registry.json +33 -2
- package/framework/rules/infrastructure-standards.md +0 -1
- package/framework/rules/morph-workflow.md +9 -4
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +5 -1
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +6 -1
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +4 -3
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +6 -5
- package/framework/standards/STANDARDS.json +3 -3
- package/framework/standards/ai-agents/context-providers.md +16 -26
- package/framework/standards/ai-agents/durable-workflows-hitl.md +15 -21
- package/framework/standards/ai-agents/modalities-image-gen.md +21 -25
- package/framework/standards/ai-agents/modalities-speech-to-text.md +4 -15
- package/framework/standards/ai-agents/modalities-text-to-speech.md +2 -11
- package/framework/standards/ai-agents/modalities-vision.md +1 -18
- package/framework/standards/ai-agents/prompt-sources.md +0 -4
- package/framework/standards/architecture/code-comments.md +52 -16
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +63 -50
- package/framework/standards/frontend/design-system/ai-image-generation.md +5 -5
- package/framework/standards/integration/mcp/mcp-tools.md +48 -117
- package/framework/templates/code/dotnet/abstractions/result.cs +0 -2
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +26 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +28 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +99 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +2 -44
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +40 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +54 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +53 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +192 -281
- package/framework/templates/code/dotnet/database/migration.cs +49 -83
- package/framework/templates/code/dotnet/database/vector-entity.cs +2 -46
- package/framework/templates/code/dotnet/extensions/result-extensions.cs +0 -2
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +11 -15
- package/framework/templates/code/dotnet/jobs/agent.cs +3 -51
- package/framework/templates/code/dotnet/jobs/job.cs +2 -97
- package/framework/templates/code/dotnet/test.cs +2 -87
- package/framework/templates/code/typescript/contracts.ts +140 -167
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +1 -48
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +3 -79
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +1 -70
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +1 -47
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +2 -99
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +0 -6
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +3 -88
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +1 -41
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +0 -21
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +1 -25
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +2 -63
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +0 -30
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +1 -52
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +3 -78
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +4 -109
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +0 -22
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +1 -36
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +2 -57
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +0 -75
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +2 -59
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +4 -111
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +0 -34
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +1 -57
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +0 -19
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +2 -74
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +2 -33
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +1 -43
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +1 -96
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +0 -13
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +0 -27
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +0 -33
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +0 -31
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +3 -75
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +0 -42
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +0 -39
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +1 -55
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +3 -59
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +0 -46
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +3 -28
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +7 -61
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +2 -37
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +1 -23
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +2 -82
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +1 -68
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +1 -62
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +2 -62
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +1 -25
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +0 -124
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +1 -73
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +1 -66
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +0 -41
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +2 -112
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +2 -43
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +5 -110
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +7 -103
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +1 -42
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +1 -30
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +1 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +0 -48
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +1 -29
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +2 -38
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +1 -14
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +4 -69
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +1 -10
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +1 -57
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +0 -26
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +1 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +0 -32
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +0 -14
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +0 -32
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +0 -37
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +0 -78
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +0 -53
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +0 -44
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +0 -44
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +0 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +6 -48
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +1 -13
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +2 -71
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +5 -100
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +39 -43
- package/framework/templates/frontend/nextjs/env.mjs.hbs +29 -32
- package/framework/templates/frontend/nextjs/page.tsx.hbs +21 -22
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +42 -54
- package/framework/templates/integrations/asaas-client.cs +332 -387
- package/framework/templates/integrations/asaas-webhook.cs +290 -351
- package/framework/templates/integrations/neon-auth-config.cs +9 -105
- package/framework/templates/project/validate.js +97 -120
- package/package.json +2 -2
- package/src/commands/dashboard/dashboard.js +80 -53
- package/src/commands/mcp/mcp-setup.js +37 -9
- package/src/commands/project/doctor.js +3 -5
- package/src/commands/project/init.js +6 -3
- package/src/commands/project/install-plugin-cmd.js +1 -1
- package/src/commands/project/update.js +5 -0
- package/src/dashboard/app/App.jsx +80 -16
- package/src/dashboard/app/components/ActivityFeed.jsx +172 -0
- package/src/dashboard/app/components/CommandPalette.jsx +155 -0
- package/src/dashboard/app/components/DetailPanel.jsx +47 -11
- package/src/dashboard/app/components/EntityLink.jsx +55 -0
- package/src/dashboard/app/components/ErrorBoundary.jsx +54 -0
- package/src/dashboard/app/components/FlowGraph.jsx +19 -17
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +53 -0
- package/src/dashboard/app/components/Markdown.jsx +47 -0
- package/src/dashboard/app/components/PhaseRail.jsx +111 -0
- package/src/dashboard/app/components/SearchFilter.jsx +40 -22
- package/src/dashboard/app/components/Sidebar.jsx +146 -37
- package/src/dashboard/app/hooks/useMorphQuery.js +54 -0
- package/src/dashboard/app/hooks/usePref.js +38 -0
- package/src/dashboard/app/lib/format.js +69 -0
- package/src/dashboard/app/lib/phases.js +56 -0
- package/src/dashboard/app/lib/project.js +50 -0
- package/src/dashboard/app/pages/Activity.jsx +24 -0
- package/src/dashboard/app/pages/AgentMap.jsx +371 -370
- package/src/dashboard/app/pages/FeatureDetail.jsx +604 -0
- package/src/dashboard/app/pages/Features.jsx +400 -0
- package/src/dashboard/app/pages/Guide.jsx +146 -0
- package/src/dashboard/app/pages/HookInspector.jsx +357 -322
- package/src/dashboard/app/pages/PhasePipeline.jsx +277 -408
- package/src/dashboard/app/pages/Portfolio.jsx +156 -0
- package/src/dashboard/app/pages/Rubrics.jsx +101 -0
- package/src/dashboard/app/pages/RulesMap.jsx +300 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +483 -366
- package/src/dashboard/app/pages/Standards.jsx +407 -332
- package/src/dashboard/app/styles/index.css +187 -47
- package/src/dashboard/index.html +14 -12
- package/src/dashboard/package.json +32 -24
- package/src/dashboard/scanner/activity-scanner.js +351 -0
- package/src/dashboard/scanner/agents-scanner.js +59 -1
- package/src/dashboard/scanner/docs-scanner.js +59 -0
- package/src/dashboard/scanner/feature-status.js +10 -0
- package/src/dashboard/scanner/features-scanner.js +360 -0
- package/src/dashboard/scanner/index.js +14 -7
- package/src/dashboard/scanner/phases-scanner.js +14 -15
- package/src/dashboard/scanner/portfolio.js +153 -0
- package/src/dashboard/scanner/project-api.js +195 -0
- package/src/dashboard/scanner/rules-scanner.js +42 -37
- package/src/dashboard/scanner/skills-scanner.js +105 -43
- package/src/dashboard/scanner/standards-scanner.js +2 -0
- package/src/dashboard/vite-plugin-morph-data.js +134 -17
- package/src/lib/detectors/claude-config-detector.js +2 -7
- package/src/lib/detectors/index.js +5 -1
- package/src/lib/evaluator-report.js +22 -0
- package/src/lib/generators/settings-generator.js +208 -209
- package/src/lib/installers/mcp-installer.js +724 -719
- package/src/lib/installers/memory-installer.js +1 -2
- package/src/lib/projects/registry.js +154 -0
- package/src/lib/validators/code-budget/code-budget-validator.js +32 -4
- package/src/scripts/install-plugin.js +1 -1
- package/src/scripts/setup-infra.js +2 -4
- package/src/utils/agents-installer.js +57 -12
- package/src/utils/banner.js +1 -1
- package/src/commands/index.js +0 -13
- package/src/commands/project/checkpoint.js +0 -209
- package/src/core/state/phase-state-machine.js +0 -197
- package/src/dashboard/app/hooks/useDetailPanel.js +0 -15
- package/src/dashboard/app/pages/LiveState.jsx +0 -206
- package/src/dashboard/scanner/squads-scanner.js +0 -13
- package/src/dashboard/scanner/state-scanner.js +0 -85
- package/src/lib/generators/index.js +0 -10
- package/src/lib/hooks/hook-executor.js +0 -167
- package/src/lib/validators/checkpoint-persistence.js +0 -39
- package/src/lib/validators/shared/emit-validator-dispatch.js +0 -14
- package/src/lib/validators/shared/index.js +0 -12
- package/src/lib/validators/shared/issue-counter.js +0 -14
- 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. "
|
|
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
|
|
6
|
-
*
|
|
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
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
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 {
|
|
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
|
|
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 {
|
|
325
|
-
* `mcp` entries outside
|
|
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__{
|
|
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
|
-
.
|
|
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) {
|
package/src/utils/banner.js
CHANGED
|
@@ -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-
|
|
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")}`,
|
package/src/commands/index.js
DELETED
|
@@ -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
|
-
}
|