@polymorphism-tech/morph-spec 8.33.0 → 8.35.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 +22 -2
- package/README.md +2 -2
- package/bin/morph-spec.js +43 -5
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +5 -4
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +2 -2
- package/framework/MORPH.md +20 -22
- package/framework/agents.json +198 -44
- package/framework/ai-pin.json +22 -0
- package/framework/commands/morph-media.md +45 -0
- package/framework/docs/CLI.md +238 -10
- package/framework/evals/proof-rubrics/README.md +76 -0
- package/framework/evals/proof-rubrics/eval-de-modelo.md +64 -0
- package/framework/evals/proof-rubrics/fiacao.md +54 -0
- package/framework/evals/proof-rubrics/guard-cego.md +52 -0
- package/framework/evals/proof-rubrics/media-claims.md +109 -0
- package/framework/evals/proof-rubrics/mutation-proof.md +77 -0
- package/framework/evals/proof-rubrics/observabilidade-genai.md +119 -0
- package/framework/evals/proof-rubrics/verde-nao-e-coberto.md +48 -0
- package/framework/evals/rubrics/architecture.md +30 -0
- package/framework/evals/rubrics/code-quality.md +26 -0
- package/framework/evals/rubrics/test-coverage.md +20 -0
- package/framework/hooks/README.md +11 -1
- package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +5 -8
- package/framework/hooks/claude-code/core/block-raw-worktree-add.js +79 -0
- package/framework/hooks/claude-code/core/chain.js +27 -7
- package/framework/hooks/claude-code/core/protect-spec-files.js +41 -4
- package/framework/hooks/claude-code/core/worktree-context.js +83 -14
- package/framework/hooks/claude-code/core/worktree-create.js +206 -0
- package/framework/hooks/claude-code/core/worktree-env-inject.js +87 -0
- package/framework/hooks/claude-code/core/worktree-remove.js +81 -0
- package/framework/hooks/shared/command-tokens.js +23 -0
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/git-paths.js +25 -0
- package/framework/hooks/shared/hook-response.js +17 -0
- package/framework/hooks/shared/morph-cli.js +134 -0
- package/framework/rules/ai-agents-standards.md +14 -2
- package/framework/rules/infrastructure-standards.md +3 -1
- package/framework/schemas/ai-pin.schema.json +97 -0
- package/framework/schemas/config.schema.json +107 -0
- package/framework/schemas/feature.schema.json +23 -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 +28 -7
- 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 +2 -2
- package/framework/skills/core/morph-implement/SKILL.md +20 -2
- package/framework/skills/core/morph-media/SKILL.md +215 -0
- package/framework/skills/core/morph-plan/SKILL.md +38 -4
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +166 -9
- 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 +1 -1
- package/framework/standards/PROVENANCE.json +81 -3
- package/framework/standards/STANDARDS.json +1504 -830
- package/framework/standards/ai-agents/agent-archetypes.md +160 -0
- package/framework/standards/ai-agents/agent-session.md +256 -160
- package/framework/standards/ai-agents/agent-spec.md +196 -0
- package/framework/standards/ai-agents/batching.md +217 -193
- package/framework/standards/ai-agents/context-providers.md +261 -188
- package/framework/standards/ai-agents/conversational-agent-with-phases.md +239 -0
- package/framework/standards/ai-agents/cost-and-budget.md +256 -0
- package/framework/standards/ai-agents/durable-workflows-hitl.md +98 -5
- package/framework/standards/ai-agents/evals-with-cache.md +289 -0
- package/framework/standards/ai-agents/guardrails.md +222 -0
- package/framework/standards/ai-agents/llm-runtime-defaults.md +226 -0
- package/framework/standards/ai-agents/mcp-server.md +351 -250
- package/framework/standards/ai-agents/mcp-tools.md +250 -197
- package/framework/standards/ai-agents/media-pipeline.md +352 -0
- package/framework/standards/ai-agents/media-video.md +267 -0
- package/framework/standards/ai-agents/middleware-patterns.md +241 -207
- package/framework/standards/ai-agents/modalities-image-gen.md +422 -173
- package/framework/standards/ai-agents/modalities-speech-to-text.md +177 -171
- package/framework/standards/ai-agents/modalities-text-to-speech.md +189 -181
- package/framework/standards/ai-agents/modalities-vision.md +205 -201
- package/framework/standards/ai-agents/multi-agent-patterns.md +211 -155
- package/framework/standards/ai-agents/observability-patterns.md +227 -17
- package/framework/standards/ai-agents/production.md +260 -231
- package/framework/standards/ai-agents/prompt-sources.md +389 -0
- package/framework/standards/ai-agents/providers/model-registry.md +293 -158
- package/framework/standards/ai-agents/rag-custom-pgvector.md +69 -4
- package/framework/standards/ai-agents/service-tier-flex.md +143 -135
- package/framework/standards/ai-agents/setup.md +166 -50
- package/framework/standards/ai-agents/structured-output.md +280 -249
- package/framework/standards/ai-agents/sweet-spot.md +129 -109
- package/framework/standards/ai-agents/testing-ai.md +236 -0
- package/framework/standards/ai-agents/vector-data-extensions.md +50 -20
- package/framework/standards/ai-agents/workflows.md +27 -7
- package/framework/standards/frontend/design-system/ai-image-generation.md +12 -0
- package/framework/standards/frontend/design-system/ai-video-generation.md +13 -0
- package/framework/standards/infrastructure/docker/local-compose-isolation.md +90 -0
- package/framework/templates/REGISTRY.json +14 -1
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
- package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
- package/framework/templates/dotnet/ai-kit/Directory.Build.props +25 -0
- package/framework/templates/dotnet/ai-kit/Directory.Packages.props +26 -0
- package/framework/templates/dotnet/ai-kit/Morph.AiKit.sln +56 -0
- package/framework/templates/dotnet/ai-kit/NuGet.config +12 -0
- package/framework/templates/dotnet/ai-kit/README.md +128 -0
- package/framework/templates/dotnet/ai-kit/global.json +9 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +65 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +289 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +91 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +91 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +181 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +14 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +174 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +54 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +75 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +71 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +246 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +57 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +78 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +225 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +463 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +32 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +49 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +214 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +168 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +142 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +349 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +42 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +77 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +28 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +107 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +253 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +103 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +259 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +21 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +47 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +102 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +44 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +192 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +94 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +99 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +97 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +133 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +93 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +112 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +288 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +158 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +29 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +208 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +95 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Morph.AiKit.csproj +38 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +119 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +82 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +33 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +267 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +474 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/model-registry.v2.json +87 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +169 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +49 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +495 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +260 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +405 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +379 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +197 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +139 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +157 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +178 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +153 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +194 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +86 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +213 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +95 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +225 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +215 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +33 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +89 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +55 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +60 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +131 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +133 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +127 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +216 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +264 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +148 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +184 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +89 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +98 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +451 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +76 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +33 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +261 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +374 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/.morph-eval-cache.json +12 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/orcamento.acme.a-resposta-estruturada/1/CF70A7CE27BBD66A/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/orcamento.acme.a-resposta-estruturada/1/CF70A7CE27BBD66A/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.a-saudacao/1/19F631228C6E00BA/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.a-saudacao/1/19F631228C6E00BA/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.b-escalonamento/1/A24A7C9BDD8239D8/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.b-escalonamento/1/A24A7C9BDD8239D8/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.globex.a-saudacao/1/E38CD37DF003EDB7/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.globex.a-saudacao/1/E38CD37DF003EDB7/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/orcamento/acme/a-resposta-estruturada.json +24 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/acme/a-saudacao.json +23 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/acme/b-escalonamento.json +24 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/globex/a-saudacao.json +23 -0
- package/framework/templates/infrastructure/docker/docker-compose.template.yml +24 -14
- package/package.json +3 -3
- package/src/commands/e2e/e2e.js +91 -40
- package/src/commands/project/doctor.js +143 -6
- package/src/commands/project/env.js +95 -0
- package/src/commands/project/update.js +469 -153
- package/src/commands/project/worktree.js +274 -53
- package/src/commands/state/advance.js +24 -2
- package/src/commands/state/approve.js +33 -1
- package/src/commands/state/create.js +72 -11
- package/src/commands/state/delete.js +8 -0
- package/src/commands/state/finish.js +360 -53
- package/src/commands/state/gate-check.js +190 -6
- package/src/commands/state/score.js +36 -1
- package/src/commands/validation/verify.js +134 -25
- package/src/core/state/state-manager.js +9 -1
- package/src/dashboard/app/pages/HookInspector.jsx +430 -418
- package/src/dashboard/scanner/hooks-scanner.js +5 -1
- package/src/lib/ai/ai-checks.js +182 -0
- package/src/lib/ai/ai-project-scan.js +347 -0
- package/src/lib/ai/code-audit.js +181 -0
- package/src/lib/ai/comment-stripper.js +184 -0
- package/src/lib/ai/pin-drift.js +138 -0
- package/src/lib/ai/registry-audit.js +285 -0
- package/src/lib/e2e/compose-env.js +55 -14
- package/src/lib/e2e/stack-teardown.js +115 -0
- package/src/lib/evaluator-report.js +269 -15
- package/src/lib/generators/architecture-scanner.js +2 -1
- package/src/lib/git/default-branch.js +92 -4
- package/src/lib/graph/graphifyignore.js +2 -1
- package/src/lib/installers/effects.js +193 -0
- package/src/lib/installers/templates-reconcile.js +3 -3
- package/src/lib/risk-detector.js +284 -0
- package/src/lib/standards/verified-against.js +276 -0
- package/src/lib/tasks/eval-node.js +485 -0
- package/src/lib/tasks/linux-runner.js +297 -0
- package/src/lib/tasks/test-nodes.js +486 -0
- package/src/lib/tasks/test-runner.js +431 -37
- package/src/lib/validators/verify-runner.js +590 -28
- package/src/lib/worktree/dirs.js +32 -0
- package/src/lib/worktree/docker-audit.js +287 -0
- package/src/lib/worktree/env-allocator.js +69 -11
- package/src/lib/worktree/worktree-audit.js +70 -3
- package/src/scripts/setup-infra.js +24 -11
- package/src/utils/agents-installer.js +18 -8
- package/src/utils/claude-md-injector.js +10 -5
- package/src/utils/file-copier.js +28 -16
- package/src/utils/hooks-installer.js +75 -29
- package/src/utils/skills-installer.js +194 -187
- package/src/utils/version-checker.js +68 -3
- package/framework/standards/ai-agents/prompts-in-db.md +0 -214
|
@@ -13,7 +13,11 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Subcommands:
|
|
15
15
|
* morph-spec worktree setup <feature> [--fresh] — create + materialize state
|
|
16
|
+
* morph-spec worktree provision <name> [--base <ref>] — task-isolation tree
|
|
17
|
+
* (branch morph-task/{name}, same infra, NO feature identity) — what the
|
|
18
|
+
* WorktreeCreate hook calls for `claude -w` / EnterWorktree / Agent isolation
|
|
16
19
|
* morph-spec worktree link [--target <path>] [--all] — (re)link shared infra
|
|
20
|
+
* morph-spec worktree remove [<feature>|--target <path>] [--if-clean] — safe teardown
|
|
17
21
|
* morph-spec worktree list [--json] — list active feature worktrees
|
|
18
22
|
*
|
|
19
23
|
* Closing the lifecycle (integrate + teardown) lives in `morph-spec finish`
|
|
@@ -32,7 +36,7 @@
|
|
|
32
36
|
|
|
33
37
|
import { execSync, execFileSync } from 'child_process';
|
|
34
38
|
import { existsSync, mkdirSync, symlinkSync, readFileSync, writeFileSync, appendFileSync, lstatSync, unlinkSync, rmdirSync, cpSync } from 'fs';
|
|
35
|
-
import { join, resolve, dirname } from 'path';
|
|
39
|
+
import { join, resolve, dirname, basename } from 'path';
|
|
36
40
|
import chalk from 'chalk';
|
|
37
41
|
import { detectDefaultBranch } from '../../lib/git/default-branch.js';
|
|
38
42
|
import { triageIgnoredPaths } from '../../lib/worktree/ignored-triage.js';
|
|
@@ -40,29 +44,11 @@ import { triageIgnoredPaths } from '../../lib/worktree/ignored-triage.js';
|
|
|
40
44
|
// the SAME answer this file does, and hooks can never import from `src/`.
|
|
41
45
|
import { findReparsePoints } from '../../../framework/hooks/shared/reparse-scan.js';
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
* stops matching and an entire frontend suite reports **0 tests without an
|
|
49
|
-
* error** — the suite "passes" having tested nothing. The dot bought nothing:
|
|
50
|
-
* the directory is gitignored either way.
|
|
51
|
-
*/
|
|
52
|
-
export const WORKTREES_DIR = 'worktrees';
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* The pre-C1 directory name. New worktrees never land here, but every reader
|
|
56
|
-
* that resolves worktrees BY CONVENTION must still accept it, so a machine
|
|
57
|
-
* that already had `.worktrees/{f}` keeps working with no manual migration.
|
|
58
|
-
*
|
|
59
|
-
* Readers that go through `git worktree list` (fleet, finish, worktree list)
|
|
60
|
-
* were never affected — git records absolute paths, not this convention.
|
|
61
|
-
*/
|
|
62
|
-
export const LEGACY_WORKTREES_DIR = '.worktrees';
|
|
63
|
-
|
|
64
|
-
/** Both names, new first — the read order for convention-based lookups. */
|
|
65
|
-
export const WORKTREE_DIR_NAMES = [WORKTREES_DIR, LEGACY_WORKTREES_DIR];
|
|
47
|
+
// The container name lives in lib/worktree/dirs.js (single source, C1
|
|
48
|
+
// rationale documented there); re-exported here so existing importers and the
|
|
49
|
+
// dependency-matrix row keep pointing at this file.
|
|
50
|
+
import { WORKTREES_DIR, LEGACY_WORKTREES_DIR, WORKTREE_DIR_NAMES } from '../../lib/worktree/dirs.js';
|
|
51
|
+
export { WORKTREES_DIR, LEGACY_WORKTREES_DIR, WORKTREE_DIR_NAMES };
|
|
66
52
|
|
|
67
53
|
// ── Pure helpers (exported for tests) ──────────────────────────────────────
|
|
68
54
|
|
|
@@ -362,6 +348,43 @@ export function resolvePrimaryRoot(cwd = process.cwd()) {
|
|
|
362
348
|
* @param {string} worktreePath - Absolute path to the worktree
|
|
363
349
|
* @returns {Promise<{ materialized: boolean, reason?: string }>}
|
|
364
350
|
*/
|
|
351
|
+
/**
|
|
352
|
+
* Guarantee the worktree owns a local `.morph/state.json`, hydrated from its
|
|
353
|
+
* committed features when there are any, and an EMPTY index otherwise.
|
|
354
|
+
*
|
|
355
|
+
* Why the empty index matters: every core hook fail-opens on `stateExists()`,
|
|
356
|
+
* which is true only with a `state.json` or a feature dir in the tree. A
|
|
357
|
+
* task-isolation worktree of a project with no committed features (measured
|
|
358
|
+
* in the sandbox e2e) had neither — so the env-inject hook, the removal guard
|
|
359
|
+
* and every other guardrail were silently dark inside it. `state.json` is
|
|
360
|
+
* gitignored and derivable, so an empty one costs nothing and can never dirty
|
|
361
|
+
* the tree the finish gates read.
|
|
362
|
+
*
|
|
363
|
+
* @param {string} worktreePath
|
|
364
|
+
* @returns {Promise<{ materialized: boolean, reason?: string, seeded?: boolean }>}
|
|
365
|
+
*/
|
|
366
|
+
export async function ensureLocalState(worktreePath) {
|
|
367
|
+
const result = await materializeState(worktreePath);
|
|
368
|
+
if (result.materialized) return result;
|
|
369
|
+
const statePath = join(worktreePath, '.morph', 'state.json');
|
|
370
|
+
if (existsSync(statePath)) return { ...result, materialized: true };
|
|
371
|
+
try {
|
|
372
|
+
const { atomicWriteJson } = await import('../../core/state/state-manager.js');
|
|
373
|
+
const now = new Date().toISOString();
|
|
374
|
+
mkdirSync(join(worktreePath, '.morph'), { recursive: true });
|
|
375
|
+
atomicWriteJson(statePath, {
|
|
376
|
+
version: '5.0.0',
|
|
377
|
+
project: { name: basename(worktreePath), type: 'unknown', createdAt: now, updatedAt: now },
|
|
378
|
+
features: {},
|
|
379
|
+
threads: {},
|
|
380
|
+
metadata: { totalFeatures: 0, completedFeatures: 0, totalTimeSpent: 0, lastUpdated: now },
|
|
381
|
+
});
|
|
382
|
+
return { materialized: true, seeded: true, reason: result.reason };
|
|
383
|
+
} catch (err) {
|
|
384
|
+
return { materialized: false, reason: (err && err.message) || result.reason };
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
365
388
|
export async function materializeState(worktreePath) {
|
|
366
389
|
const featuresDir = join(worktreePath, '.morph', 'features');
|
|
367
390
|
if (!existsSync(featuresDir)) {
|
|
@@ -490,33 +513,13 @@ export async function runWorktreeSetup(feature, options = {}) {
|
|
|
490
513
|
setupInfraLinks(worktreePath, cwd);
|
|
491
514
|
|
|
492
515
|
// Fleet env: allocate this feature's deterministic port block and write the
|
|
493
|
-
// worktree-local manifest (.morph/worktree.env)
|
|
494
|
-
//
|
|
495
|
-
|
|
496
|
-
// shieldable at finish (backed up, never a removal blocker). Fail-open — a
|
|
497
|
-
// worktree without ports is degraded, never broken.
|
|
498
|
-
let envBlock = null;
|
|
499
|
-
try {
|
|
500
|
-
const { allocateEnvBlock, resolveFleetConfig, buildEnvFileContent } =
|
|
501
|
-
await import('../../lib/worktree/env-allocator.js');
|
|
502
|
-
const common = execSync('git rev-parse --git-common-dir', { cwd, stdio: 'pipe' }).toString().trim();
|
|
503
|
-
if (common) {
|
|
504
|
-
const gitCommonDir = resolve(cwd, common);
|
|
505
|
-
envBlock = allocateEnvBlock(feature, { gitCommonDir, ...resolveFleetConfig(cwd) });
|
|
506
|
-
let stack = '';
|
|
507
|
-
try {
|
|
508
|
-
const cfg = JSON.parse(readFileSync(join(cwd, '.morph', 'config', 'config.json'), 'utf8').replace(/^/, ''));
|
|
509
|
-
stack = cfg?.project?.stack || '';
|
|
510
|
-
} catch { /* fail-open — stackless env still carries PORT */ }
|
|
511
|
-
mkdirSync(join(worktreePath, '.morph'), { recursive: true });
|
|
512
|
-
writeFileSync(join(worktreePath, '.morph', 'worktree.env'), buildEnvFileContent(feature, envBlock, stack), 'utf8');
|
|
513
|
-
excludeWorktreePaths(cwd, [join('.morph', 'worktree.env')]);
|
|
514
|
-
}
|
|
515
|
-
} catch { envBlock = null; /* fail-open */ }
|
|
516
|
+
// worktree-local manifest (.morph/worktree.env). Fail-open — a worktree
|
|
517
|
+
// without ports is degraded, never broken.
|
|
518
|
+
const envBlock = await allocateWorktreeEnv(worktreePath, cwd, feature);
|
|
516
519
|
|
|
517
520
|
// Materialize a local, real state.json in the worktree (self-heal from the
|
|
518
521
|
// committed feature.json files) so hooks work from the very first session.
|
|
519
|
-
const materialized = await
|
|
522
|
+
const materialized = await ensureLocalState(worktreePath);
|
|
520
523
|
|
|
521
524
|
// Fleet handoff: the fresh instance that opens this worktree reads
|
|
522
525
|
// handoff.md before anything else (SessionStart hook points at it). The doc
|
|
@@ -578,6 +581,109 @@ export async function worktreeSetupCommand(feature, options = {}) {
|
|
|
578
581
|
process.exit(exitCode);
|
|
579
582
|
}
|
|
580
583
|
|
|
584
|
+
// ── provision (task isolation — a worktree that is NOT a feature) ──────────
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Branch name for a task-isolation worktree: `morph-task/<name>`. A separate
|
|
588
|
+
* namespace from `morph/<feature>` on purpose — `worktree list`, `fleet`,
|
|
589
|
+
* `finish` and the SessionStart hook all read `morph/<f>` as "this IS feature
|
|
590
|
+
* f", and a scratch tree named after a feature must never impersonate it.
|
|
591
|
+
*
|
|
592
|
+
* @param {string} name
|
|
593
|
+
* @returns {string}
|
|
594
|
+
*/
|
|
595
|
+
export function buildTaskBranchName(name) {
|
|
596
|
+
return `morph-task/${name}`;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Non-exiting core of `worktree provision` — the tree `claude -w`,
|
|
601
|
+
* `EnterWorktree` and `Agent isolation:"worktree"` ask for when the name is
|
|
602
|
+
* not a feature. Everything `setup` gives a feature worktree EXCEPT the
|
|
603
|
+
* feature identity: it lands under `<primary root>/worktrees/<name>` (never
|
|
604
|
+
* nested under a secondary worktree, whatever the cwd), on `morph-task/<name>`
|
|
605
|
+
* off the default branch (or `--base`), with the infra junctions, a git-
|
|
606
|
+
* invisible `.morph/worktree.env` keyed `task/<name>`, and a materialized
|
|
607
|
+
* local state.json. No feature registration, no handoff.
|
|
608
|
+
*
|
|
609
|
+
* Exit codes mirror `setup`: 0 created (or fail-open), 1 refused (bad name),
|
|
610
|
+
* 2 already exists at this exact path.
|
|
611
|
+
*
|
|
612
|
+
* @param {string} name
|
|
613
|
+
* @param {{ base?: string, cwd?: string }} [options]
|
|
614
|
+
* @returns {Promise<{ ok: boolean, exitCode: 0|1|2, created: boolean, alreadyExists?: boolean, path?: string, branch?: string, branchedFrom?: string|null, kind?: 'task-isolation', name: string, env?: object, error?: string }>}
|
|
615
|
+
*/
|
|
616
|
+
export async function runWorktreeProvision(name, options = {}) {
|
|
617
|
+
const cwd = options.cwd || process.cwd();
|
|
618
|
+
|
|
619
|
+
if (!isValidFeatureName(name)) {
|
|
620
|
+
return { ok: false, exitCode: 1, created: false, error: 'invalid-name', name };
|
|
621
|
+
}
|
|
622
|
+
if (!isGitRepo(cwd)) {
|
|
623
|
+
return { ok: true, exitCode: 0, created: false, error: 'not-a-git-repo', name };
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Always the PRIMARY root: a provision issued from inside a feature worktree
|
|
627
|
+
// must not nest a worktree under it — that tree would be invisible to the
|
|
628
|
+
// fleet and to doctor, which enumerate `<root>/worktrees/`.
|
|
629
|
+
const root = resolvePrimaryRoot(cwd);
|
|
630
|
+
const branch = buildTaskBranchName(name);
|
|
631
|
+
const worktreePath = buildWorktreePath(name, root);
|
|
632
|
+
|
|
633
|
+
if (worktreeExists(worktreePath, root)) {
|
|
634
|
+
return { ok: true, exitCode: 2, created: false, alreadyExists: true, path: worktreePath, branch, kind: 'task-isolation', name };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
let branchedFrom = null;
|
|
638
|
+
try {
|
|
639
|
+
if (!branchExists(branch, root)) {
|
|
640
|
+
const base = options.base || detectDefaultBranch(root);
|
|
641
|
+
if (base) {
|
|
642
|
+
execFileSync('git', ['branch', branch, base], { cwd: root, stdio: 'pipe' });
|
|
643
|
+
branchedFrom = base;
|
|
644
|
+
} else {
|
|
645
|
+
execFileSync('git', ['branch', branch], { cwd: root, stdio: 'pipe' });
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const parentDir = join(root, WORKTREES_DIR);
|
|
650
|
+
if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true });
|
|
651
|
+
|
|
652
|
+
execFileSync('git', ['worktree', 'add', worktreePath, branch], { cwd: root, stdio: 'pipe' });
|
|
653
|
+
|
|
654
|
+
setupInfraLinks(worktreePath, root);
|
|
655
|
+
const envBlock = await allocateWorktreeEnv(worktreePath, root, `task/${name}`);
|
|
656
|
+
const materialized = await ensureLocalState(worktreePath);
|
|
657
|
+
|
|
658
|
+
return {
|
|
659
|
+
ok: true,
|
|
660
|
+
exitCode: 0,
|
|
661
|
+
created: true,
|
|
662
|
+
path: worktreePath,
|
|
663
|
+
branch,
|
|
664
|
+
branchedFrom,
|
|
665
|
+
kind: 'task-isolation',
|
|
666
|
+
name,
|
|
667
|
+
materialized: materialized.materialized,
|
|
668
|
+
...(envBlock ? { env: { base: envBlock.base, range: envBlock.range } } : {}),
|
|
669
|
+
};
|
|
670
|
+
} catch (err) {
|
|
671
|
+
return { ok: true, exitCode: 0, created: false, error: (err.message || 'unknown').slice(0, 200), name };
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* `morph-spec worktree provision <name> [--base <ref>]` — thin wrapper around
|
|
677
|
+
* `runWorktreeProvision`, same JSON-and-exit contract as `setup`.
|
|
678
|
+
* @param {string} name
|
|
679
|
+
* @param {{ base?: string, cwd?: string }} [options]
|
|
680
|
+
*/
|
|
681
|
+
export async function worktreeProvisionCommand(name, options = {}) {
|
|
682
|
+
const { ok, exitCode, ...result } = await runWorktreeProvision(name, options);
|
|
683
|
+
console.log(JSON.stringify(result));
|
|
684
|
+
process.exit(exitCode);
|
|
685
|
+
}
|
|
686
|
+
|
|
581
687
|
/**
|
|
582
688
|
* Command handler for `morph-spec worktree link [--target <path>] [--all]`.
|
|
583
689
|
*
|
|
@@ -588,7 +694,7 @@ export async function worktreeSetupCommand(feature, options = {}) {
|
|
|
588
694
|
*
|
|
589
695
|
* @param {{ target?: string, all?: boolean, cwd?: string }} [options]
|
|
590
696
|
*/
|
|
591
|
-
export function worktreeLinkCommand(options = {}) {
|
|
697
|
+
export async function worktreeLinkCommand(options = {}) {
|
|
592
698
|
const cwdArg = options.cwd || process.cwd();
|
|
593
699
|
const root = resolvePrimaryRoot(cwdArg);
|
|
594
700
|
const target = resolve(options.target || options.cwd || process.cwd()).replace(/\\/g, '/');
|
|
@@ -627,10 +733,79 @@ export function worktreeLinkCommand(options = {}) {
|
|
|
627
733
|
}
|
|
628
734
|
}
|
|
629
735
|
|
|
630
|
-
|
|
736
|
+
// `link --all` is the documented repair for a worktree missing its infra —
|
|
737
|
+
// that includes a `.morph/worktree.env` written by an older layout. The
|
|
738
|
+
// allocation is idempotent (same key → same block), so regenerating the
|
|
739
|
+
// manifest never moves a worktree's ports; it only adds the keys it lacks.
|
|
740
|
+
let env = null;
|
|
741
|
+
if (options.all) {
|
|
742
|
+
const key = envKeyForWorktree(target, root);
|
|
743
|
+
const block = key ? await allocateWorktreeEnv(target, root, key) : null;
|
|
744
|
+
if (block) env = { key, base: block.base, range: block.range };
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
console.log(JSON.stringify({ linked, skipped, root, target, ...(env ? { env } : {}) }));
|
|
631
748
|
process.exit(0);
|
|
632
749
|
}
|
|
633
750
|
|
|
751
|
+
/**
|
|
752
|
+
* The env-registry key of a worktree, derived from its branch: `morph/<f>` →
|
|
753
|
+
* `<f>` (feature), `morph-task/<n>` → `task/<n>` (task isolation). `null` for
|
|
754
|
+
* a branch morph does not own — no block is allocated for those.
|
|
755
|
+
*
|
|
756
|
+
* @param {string} worktreePath
|
|
757
|
+
* @param {string} root
|
|
758
|
+
* @returns {string|null}
|
|
759
|
+
*/
|
|
760
|
+
export function envKeyForWorktree(worktreePath, root) {
|
|
761
|
+
const norm = resolve(worktreePath).replace(/\\/g, '/');
|
|
762
|
+
const entry = parseWorktreeList(root).find((e) => e.path === norm);
|
|
763
|
+
const branch = entry?.branch?.replace(/^refs\/heads\//, '') ?? null;
|
|
764
|
+
if (!branch) return null;
|
|
765
|
+
const feature = branch.match(/^morph\/(.+)$/)?.[1];
|
|
766
|
+
if (feature) return feature;
|
|
767
|
+
const task = branch.match(/^morph-task\/(.+)$/)?.[1];
|
|
768
|
+
return task ? `task/${task}` : null;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Allocate (idempotently) the port block for `envKey` and write the worktree's
|
|
773
|
+
* `.morph/worktree.env`, excluded from git via the same common-dir mechanism as
|
|
774
|
+
* the infra links (it must be invisible to the finish clean-checks). Under
|
|
775
|
+
* `.morph/` on purpose: that prefix is already shieldable at finish (backed up,
|
|
776
|
+
* never a removal blocker). Fail-open: returns `null` when no block could be
|
|
777
|
+
* written — a worktree without ports is degraded, never broken.
|
|
778
|
+
*
|
|
779
|
+
* Shared by `setup` (feature key), `provision` (task key) and `link --all`
|
|
780
|
+
* (regeneration), so the manifest has exactly one writer.
|
|
781
|
+
*
|
|
782
|
+
* @param {string} worktreePath
|
|
783
|
+
* @param {string} root - primary root
|
|
784
|
+
* @param {string} envKey - `<feature>` or `task/<name>`
|
|
785
|
+
* @returns {Promise<{ index: number, base: number, range: [number, number] }|null>}
|
|
786
|
+
*/
|
|
787
|
+
export async function allocateWorktreeEnv(worktreePath, root, envKey) {
|
|
788
|
+
try {
|
|
789
|
+
const { allocateEnvBlock, resolveFleetConfig, buildEnvFileContent } =
|
|
790
|
+
await import('../../lib/worktree/env-allocator.js');
|
|
791
|
+
const common = execSync('git rev-parse --git-common-dir', { cwd: root, stdio: 'pipe' }).toString().trim();
|
|
792
|
+
if (!common) return null;
|
|
793
|
+
const gitCommonDir = resolve(root, common);
|
|
794
|
+
const block = allocateEnvBlock(envKey, { gitCommonDir, ...resolveFleetConfig(root) });
|
|
795
|
+
let stack = '';
|
|
796
|
+
try {
|
|
797
|
+
const cfg = JSON.parse(readFileSync(join(root, '.morph', 'config', 'config.json'), 'utf8').replace(/^/, ''));
|
|
798
|
+
stack = cfg?.project?.stack || '';
|
|
799
|
+
} catch { /* fail-open — stackless env still carries PORT */ }
|
|
800
|
+
mkdirSync(join(worktreePath, '.morph'), { recursive: true });
|
|
801
|
+
writeFileSync(join(worktreePath, '.morph', 'worktree.env'), buildEnvFileContent(envKey, block, stack), 'utf8');
|
|
802
|
+
excludeWorktreePaths(root, [join('.morph', 'worktree.env')]);
|
|
803
|
+
return block;
|
|
804
|
+
} catch {
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
634
809
|
// ── finish / list shared helpers ────────────────────────────────────────────
|
|
635
810
|
|
|
636
811
|
/**
|
|
@@ -831,8 +1006,15 @@ function isLiveSiblingWorktreeRegistration(relPath, root, feature) {
|
|
|
831
1006
|
* @param {string} rootPath - the primary root, for the replica comparison
|
|
832
1007
|
* @param {{ discardIgnored?: boolean }} options
|
|
833
1008
|
* @returns {{ shieldable: string[], blocking: string[], discarded: string[], replicable: string[] }}
|
|
1009
|
+
*
|
|
1010
|
+
* EXPORTADA para que o `finish --dry-run` (pré-voo) responda EXATAMENTE a
|
|
1011
|
+
* mesma pergunta que o teardown real vai responder. Uma segunda enumeração de
|
|
1012
|
+
* `git status --ignored` no finish.js — com a sua própria cópia de
|
|
1013
|
+
* IGNORED_SHIELDABLE_PREFIXES — faria o pré-voo prometer uma coisa e o
|
|
1014
|
+
* comando fazer outra, que é a única maneira de um dry-run ser pior que
|
|
1015
|
+
* nenhum. Consumida read-only: o pré-voo não copia, não apaga, não decide.
|
|
834
1016
|
*/
|
|
835
|
-
function classifyIgnoredLeftovers(worktreePath, rootPath, options = {}) {
|
|
1017
|
+
export function classifyIgnoredLeftovers(worktreePath, rootPath, options = {}) {
|
|
836
1018
|
let out;
|
|
837
1019
|
try {
|
|
838
1020
|
// `-z` + execFile: NUL-delimited, no shell, no C-style quoting to undo —
|
|
@@ -1338,7 +1520,7 @@ export function cwdIsInsideWorktree(cwd, worktreePath) {
|
|
|
1338
1520
|
* closing a feature is still `morph-spec finish`.
|
|
1339
1521
|
*
|
|
1340
1522
|
* @param {string|undefined} featureArg
|
|
1341
|
-
* @param {{ target?: string, discardIgnored?: boolean, deleteBranch?: boolean, cwd?: string }} [options]
|
|
1523
|
+
* @param {{ target?: string, discardIgnored?: boolean, deleteBranch?: boolean, ifClean?: boolean, cwd?: string }} [options]
|
|
1342
1524
|
*/
|
|
1343
1525
|
export async function worktreeRemoveCommand(featureArg, options = {}) {
|
|
1344
1526
|
const cwdArg = options.cwd || process.cwd();
|
|
@@ -1398,9 +1580,47 @@ export async function worktreeRemoveCommand(featureArg, options = {}) {
|
|
|
1398
1580
|
hint: `saia do worktree antes de removê-lo — o processo trava o próprio diretório de trabalho. Rode de novo a partir de "${root}"` }, 1);
|
|
1399
1581
|
}
|
|
1400
1582
|
|
|
1583
|
+
// `--if-clean` — the WorktreeRemove hook's contract: Claude Code fires it
|
|
1584
|
+
// when it believes the tree is disposable, but "disposable" is the user's
|
|
1585
|
+
// call, not the harness's. Any uncommitted or untracked file refuses the
|
|
1586
|
+
// removal BEFORE the junctions are touched, so a refusal leaves the tree
|
|
1587
|
+
// exactly as it was (no rollback needed, nothing to relink). The infra
|
|
1588
|
+
// links and the env manifest are excluded from git, so they never count.
|
|
1589
|
+
if (options.ifClean) {
|
|
1590
|
+
let status;
|
|
1591
|
+
try {
|
|
1592
|
+
status = execFileSync('git', ['status', '--porcelain', '--untracked-files=all'], { cwd: worktreePath, stdio: 'pipe' }).toString();
|
|
1593
|
+
} catch {
|
|
1594
|
+
emit({ removed: false, error: 'status-failed', path: worktreePath, root,
|
|
1595
|
+
hint: 'não foi possível provar que o worktree está limpo — nada foi removido' }, 1);
|
|
1596
|
+
}
|
|
1597
|
+
const dirt = status.split(/\r?\n/).filter((l) => l.trim());
|
|
1598
|
+
if (dirt.length) {
|
|
1599
|
+
emit({ removed: false, error: 'worktree-dirty', path: worktreePath, root, dirt: dirt.slice(0, 20),
|
|
1600
|
+
hint: `o worktree tem trabalho não commitado — commit ou descarte antes, ou rode \`morph-spec worktree remove --target "${worktreePath}"\` sem --if-clean para remover com backup` }, 1);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// The env-registry key this tree allocated under: `<f>` for morph/<f>,
|
|
1605
|
+
// `task/<n>` for morph-task/<n>. Read BEFORE the teardown (the entry is gone
|
|
1606
|
+
// from `git worktree list` afterwards). The old `'worktree'` placeholder
|
|
1607
|
+
// meant every --target removal released a slot nobody held and leaked the
|
|
1608
|
+
// one the tree actually used.
|
|
1609
|
+
const envKey = envKeyForWorktree(worktreePath, root);
|
|
1610
|
+
|
|
1611
|
+
// The tree's compose stack goes down BEFORE the tree: a running bind-mounted
|
|
1612
|
+
// container holds the directory (Windows) and turns a clean removal into a
|
|
1613
|
+
// removalFailure. Volumes survive — removing a worktree is not closing the
|
|
1614
|
+
// feature, and a re-`setup` should find its seeded database intact.
|
|
1615
|
+
let e2e = null;
|
|
1616
|
+
try {
|
|
1617
|
+
const { tearDownFeatureStack } = await import('../../lib/e2e/stack-teardown.js');
|
|
1618
|
+
e2e = tearDownFeatureStack(envKey || feature || basename(worktreePath), { root, worktreePath, volumes: false });
|
|
1619
|
+
} catch { e2e = null; /* fail-open */ }
|
|
1620
|
+
|
|
1401
1621
|
const teardown = await teardownWorktree(worktreePath, {
|
|
1402
1622
|
root,
|
|
1403
|
-
feature: feature || 'worktree',
|
|
1623
|
+
feature: feature || envKey || 'worktree',
|
|
1404
1624
|
branch,
|
|
1405
1625
|
options: { discardIgnored: options.discardIgnored === true },
|
|
1406
1626
|
// Removing a worktree is not closing a feature: the branch is the work, and
|
|
@@ -1421,6 +1641,7 @@ export async function worktreeRemoveCommand(featureArg, options = {}) {
|
|
|
1421
1641
|
// real `.claude/`/`.morph/framework/` were unlinked rather than followed; on
|
|
1422
1642
|
// a refusal it is what `relinked` did or did not put back.
|
|
1423
1643
|
if (teardown.unlinked.length) out.unlinked = teardown.unlinked;
|
|
1644
|
+
if (e2e?.attempted) out.e2e = { status: e2e.status, cwd: e2e.cwd, volumes: false };
|
|
1424
1645
|
if (teardown.worktreeRemoved) {
|
|
1425
1646
|
if (teardown.discarded.length) out.discardedIgnored = teardown.discarded;
|
|
1426
1647
|
// Broken out of `discardedIgnored` on purpose: "deleted" and "deleted
|
|
@@ -198,7 +198,23 @@ export async function advanceCommand(featureName, options = {}) {
|
|
|
198
198
|
process.exit(1);
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
|
|
201
|
+
// Gate idempotency (F15): `setApprovalGate` REPLACES the gate object whole
|
|
202
|
+
// (state-manager.js), so re-writing an already-approved gate does not
|
|
203
|
+
// refresh it — it destroys `approvedBy`, `timestamp`, `notes` and
|
|
204
|
+
// `selfAssessed` in one assignment. And `resolveApprovedBy` above answers
|
|
205
|
+
// "auto" in every headless context, so `approve f proposal --approver
|
|
206
|
+
// lucas` followed by `advance f` used to rewrite a human's signature as
|
|
207
|
+
// "auto" — the one stamp the charter forbids on a human-approved gate.
|
|
208
|
+
// The state-sync hook already carries exactly this guard; `advance`, the
|
|
209
|
+
// other gate write-path, simply never got it.
|
|
210
|
+
//
|
|
211
|
+
// It skips ONLY the write. The scaffold and the handoff below still run,
|
|
212
|
+
// so re-running `advance` after a failed scaffold keeps working — the
|
|
213
|
+
// reason this is a guard and not an abort.
|
|
214
|
+
const priorGate = gate ? feature.approvalGates?.[gate] : null;
|
|
215
|
+
const alreadyApproved = priorGate?.approved === true;
|
|
216
|
+
|
|
217
|
+
if (gate && !alreadyApproved) {
|
|
202
218
|
await setApprovalGate(featureName, gate, true, {
|
|
203
219
|
approvedBy,
|
|
204
220
|
approvedAt: new Date().toISOString(),
|
|
@@ -226,7 +242,13 @@ export async function advanceCommand(featureName, options = {}) {
|
|
|
226
242
|
if (missingArtifacts.length) {
|
|
227
243
|
console.log(chalk.yellow(` ⚠️ --force: avançou SEM ${missingArtifacts.join(', ')}.`));
|
|
228
244
|
}
|
|
229
|
-
|
|
245
|
+
// Report what actually happened: claiming "approved (auto)" when nothing
|
|
246
|
+
// was written is how the destroyed signature stayed invisible.
|
|
247
|
+
if (gate && alreadyApproved) {
|
|
248
|
+
console.log(chalk.gray(` Gate "${gate}" was already approved (${priorGate.approvedBy || 'unknown'}) — approval preserved, nothing re-written.`));
|
|
249
|
+
} else if (gate) {
|
|
250
|
+
console.log(chalk.gray(` Gate "${gate}" approved (${approvedBy}).`));
|
|
251
|
+
}
|
|
230
252
|
if (nextDir) console.log(chalk.gray(` Scaffolded ${nextDir}/ — ready for the next phase's first artifact.\n`));
|
|
231
253
|
else console.log('');
|
|
232
254
|
} catch (error) {
|
|
@@ -180,7 +180,13 @@ export async function approveCommand(featureName, gateName, options = {}) {
|
|
|
180
180
|
if (currentStatus?.approved) {
|
|
181
181
|
console.log(chalk.yellow(`\n⚠️ Gate "${gateName}" is already approved`));
|
|
182
182
|
console.log(chalk.gray(`Approved at: ${currentStatus.timestamp}`));
|
|
183
|
-
console.log(chalk.gray(`Approved by: ${currentStatus.approvedBy || 'Unknown'}
|
|
183
|
+
console.log(chalk.gray(`Approved by: ${currentStatus.approvedBy || 'Unknown'}`));
|
|
184
|
+
// This early-out is a no-op that still exits 0 — deliberate, and
|
|
185
|
+
// `--force` does NOT cover it (that flag only covers gate ORDER). A
|
|
186
|
+
// caller who meant to re-approve therefore reads success and is left
|
|
187
|
+
// with nowhere to go. The way out already exists as `unapprove`; the
|
|
188
|
+
// message just never named it. Naming it is the whole fix (no new flag).
|
|
189
|
+
console.log(chalk.gray(`To approve it again, revoke it first: morph-spec unapprove ${featureName} ${gateName}\n`));
|
|
184
190
|
return;
|
|
185
191
|
}
|
|
186
192
|
|
|
@@ -349,6 +355,32 @@ export async function approveCommand(featureName, gateName, options = {}) {
|
|
|
349
355
|
}
|
|
350
356
|
selfAssessed = true;
|
|
351
357
|
}
|
|
358
|
+
|
|
359
|
+
// REAVALIAÇÃO VISÍVEL. O `evaluator-report.md` é append-only, então uma
|
|
360
|
+
// segunda rodada acrescenta um `**Composite-Score:**` ao fim e o arquivo
|
|
361
|
+
// passa a carregar mais de um veredito. O gate decide pelo ÚLTIMO (o
|
|
362
|
+
// vigente) — mas aprovar em silêncio um relatório com notas superadas
|
|
363
|
+
// esconde do humano exatamente o fato que ele precisa ver: a nota MUDOU,
|
|
364
|
+
// e numa das rodadas medidas em campo ela CAIU (8.9 → 8.3, quando uma
|
|
365
|
+
// correção abriu um bloqueante).
|
|
366
|
+
//
|
|
367
|
+
// Aqui é o lugar: `approve` é a precondição humana do Gate 3 e a única
|
|
368
|
+
// superfície onde uma pessoa lê o veredito antes de carimbá-lo. Sem este
|
|
369
|
+
// bloco, `scoreTokens`/`superseded` seriam mitigação inerte — campo que
|
|
370
|
+
// ninguém lê não mitiga nada.
|
|
371
|
+
if (report.status === 'ok' && report.scoreTokens > 1) {
|
|
372
|
+
console.log(chalk.yellow(
|
|
373
|
+
`
|
|
374
|
+
⚠️ O relatório do avaliador tem ${report.scoreTokens} vereditos (o arquivo é append-only).`
|
|
375
|
+
));
|
|
376
|
+
console.log(chalk.gray(` Vigente (o que o gate usa): ${report.score}`));
|
|
377
|
+
console.log(chalk.gray(` Superado(s), na ordem do arquivo: ${report.superseded.join(', ')}`));
|
|
378
|
+
if (report.superseded.some((n) => n > report.score)) {
|
|
379
|
+
console.log(chalk.yellow(' A nota CAIU em relação a uma rodada anterior — confira o que a correção'));
|
|
380
|
+
console.log(chalk.yellow(' abriu antes de aprovar; o relatório mantém a análise das duas rodadas.'));
|
|
381
|
+
}
|
|
382
|
+
console.log('');
|
|
383
|
+
}
|
|
352
384
|
}
|
|
353
385
|
}
|
|
354
386
|
|
|
@@ -15,32 +15,61 @@ import { detectDefaultBranch } from '../../lib/git/default-branch.js';
|
|
|
15
15
|
* `morph-spec finish` can later merge/PR it back. Symmetric with the worktree
|
|
16
16
|
* flow, which gets its branch from `runWorktreeSetup`.
|
|
17
17
|
*
|
|
18
|
+
* That checkout moves the dev's PRIMARY working tree, and `stdio: 'pipe'`
|
|
19
|
+
* swallows even git's own message — so the move is reported by the caller
|
|
20
|
+
* (`branchMoveWarning`), never silent. `checkout: false` (`--no-checkout`)
|
|
21
|
+
* still CREATES the branch — `finish --pr|--merge` depends on it existing in
|
|
22
|
+
* both flows — and only skips moving the tree.
|
|
23
|
+
*
|
|
18
24
|
* Fail-open: outside a git repo, with no resolvable default, or on any git
|
|
19
25
|
* error, the feature is simply left on the current branch — never a failed birth.
|
|
20
26
|
*
|
|
21
27
|
* @param {string} featureName
|
|
22
|
-
* @
|
|
28
|
+
* @param {{ checkout?: boolean }} [opts] - `checkout: false` keeps the tree put
|
|
29
|
+
* @returns {{ branched: boolean, checkedOut: boolean, branch?: string, from?: string,
|
|
30
|
+
* movedFrom?: string, stayedOn?: string, reason?: string }}
|
|
31
|
+
* `movedFrom` is present ONLY when the working tree actually moved (it names
|
|
32
|
+
* the branch left behind); `stayedOn` only when the move was declined.
|
|
23
33
|
*/
|
|
24
|
-
function branchCommonFlow(featureName) {
|
|
34
|
+
function branchCommonFlow(featureName, { checkout = true } = {}) {
|
|
25
35
|
const cwd = process.cwd();
|
|
26
36
|
try {
|
|
27
|
-
if (!isGitRepo(cwd)) return { branched: false, reason: 'not-a-git-repo' };
|
|
37
|
+
if (!isGitRepo(cwd)) return { branched: false, checkedOut: false, reason: 'not-a-git-repo' };
|
|
28
38
|
const branch = buildBranchName(featureName);
|
|
29
39
|
const defaultBranch = detectDefaultBranch(cwd);
|
|
30
|
-
if (!defaultBranch) return { branched: false, reason: 'no-default-branch' };
|
|
40
|
+
if (!defaultBranch) return { branched: false, checkedOut: false, reason: 'no-default-branch' };
|
|
31
41
|
if (!branchExists(branch, cwd)) {
|
|
32
42
|
execFileSync('git', ['branch', branch, defaultBranch], { cwd, stdio: 'pipe' });
|
|
33
43
|
}
|
|
34
44
|
const current = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd, stdio: 'pipe' }).toString().trim();
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
45
|
+
// Already there: nothing moved, so nothing to warn about.
|
|
46
|
+
if (current === branch) return { branched: true, checkedOut: true, branch, from: defaultBranch };
|
|
47
|
+
if (!checkout) return { branched: true, checkedOut: false, branch, from: defaultBranch, stayedOn: current };
|
|
48
|
+
execFileSync('git', ['checkout', branch], { cwd, stdio: 'pipe' });
|
|
49
|
+
return { branched: true, checkedOut: true, branch, from: defaultBranch, movedFrom: current };
|
|
39
50
|
} catch {
|
|
40
|
-
return { branched: false, reason: 'git-error' };
|
|
51
|
+
return { branched: false, checkedOut: false, reason: 'git-error' };
|
|
41
52
|
}
|
|
42
53
|
}
|
|
43
54
|
|
|
55
|
+
/**
|
|
56
|
+
* The alert for a checkout that MOVED. Built once and rendered on both
|
|
57
|
+
* surfaces — stdout (human) and stderr (under `--json`, so it survives a
|
|
58
|
+
* stdout redirect) — so the two can never drift apart.
|
|
59
|
+
*
|
|
60
|
+
* @param {{ branch: string, movedFrom: string }} branchResult
|
|
61
|
+
* @returns {string[]} lines, unstyled and unindented
|
|
62
|
+
*/
|
|
63
|
+
function branchMoveWarning({ branch, movedFrom }) {
|
|
64
|
+
return [
|
|
65
|
+
`⚠️ Working tree MOVED: ${movedFrom} → ${branch}`,
|
|
66
|
+
` This checkout is now ON the feature branch — uncommitted work came along,`,
|
|
67
|
+
` and anything committed here from now on lands on ${branch}, not ${movedFrom}.`,
|
|
68
|
+
` Use --no-checkout to create the branch without moving the tree, or --worktree`,
|
|
69
|
+
` (the recommended route for parallel work) to isolate the feature in its own tree.`,
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
|
|
44
73
|
/** Feature names are kebab-case: lowercase alphanumerics and hyphens. */
|
|
45
74
|
const FEATURE_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
46
75
|
|
|
@@ -130,6 +159,24 @@ export async function createCommand(featureName, options = {}) {
|
|
|
130
159
|
process.exit(1);
|
|
131
160
|
}
|
|
132
161
|
|
|
162
|
+
// `--no-checkout` pede que o checkout PRINCIPAL não se mova; `--worktree`
|
|
163
|
+
// nunca o move — a feature nasce numa árvore separada, e o caminho de
|
|
164
|
+
// worktree sequer chama `branchCommonFlow`, que é o único leitor do flag.
|
|
165
|
+
// Aceitar a combinação em silêncio é dizer que o CLI honrou um pedido que
|
|
166
|
+
// ele ignorou — o mesmo anti-padrão que esta série de fixes tirou do
|
|
167
|
+
// `finish --pr --push`. Recusa ALTA, e AQUI: junto das outras validações
|
|
168
|
+
// de forma, ANTES de `createFeature` — a primeira mutação do `create`.
|
|
169
|
+
// Uma recusa depois dela deixaria a feature nascida por um comando que
|
|
170
|
+
// falhou, pior que o no-op que ela substitui.
|
|
171
|
+
if (options.worktree && options.checkout === false) {
|
|
172
|
+
console.error(chalk.red(`\n❌ no-checkout-with-worktree: --no-checkout não se combina com --worktree`));
|
|
173
|
+
console.error(chalk.gray(` O --worktree já não move o checkout desta árvore: a feature nasce em worktrees/${featureName},`));
|
|
174
|
+
console.error(chalk.gray(` na branch morph/${featureName}, e o caminho de worktree nem chega a ler o --no-checkout.`));
|
|
175
|
+
console.error(chalk.gray(` Para isolar a feature na própria árvore: morph-spec create ${featureName} --worktree`));
|
|
176
|
+
console.error(chalk.gray(` Para criar só a branch morph/${featureName} sem mover esta árvore: morph-spec create ${featureName} --no-checkout\n`));
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
|
|
133
180
|
const now = new Date().toISOString();
|
|
134
181
|
const { workType, workTypeDetection } = resolveWorkType(options, now);
|
|
135
182
|
|
|
@@ -225,7 +272,8 @@ export async function createCommand(featureName, options = {}) {
|
|
|
225
272
|
} else {
|
|
226
273
|
// Common flow: still isolate the feature on its own branch so `finish`
|
|
227
274
|
// can merge/PR it back. Fail-open (see branchCommonFlow).
|
|
228
|
-
|
|
275
|
+
// Commander's `--no-checkout` sets options.checkout === false.
|
|
276
|
+
branchResult = branchCommonFlow(featureName, { checkout: options.checkout !== false });
|
|
229
277
|
}
|
|
230
278
|
}
|
|
231
279
|
|
|
@@ -238,6 +286,11 @@ export async function createCommand(featureName, options = {}) {
|
|
|
238
286
|
const out = { feature: featureName, created, ...finalFeature };
|
|
239
287
|
if (worktreeResult) out.worktree = worktreeResult;
|
|
240
288
|
if (branchResult) out.branch = branchResult;
|
|
289
|
+
// The move of the PRIMARY checkout is never silent — under --json it goes
|
|
290
|
+
// to stderr so it survives `... --json > out.json` instead of vanishing.
|
|
291
|
+
if (branchResult?.movedFrom) {
|
|
292
|
+
console.error(chalk.yellow(branchMoveWarning(branchResult).join('\n')));
|
|
293
|
+
}
|
|
241
294
|
console.log(JSON.stringify(out, null, 2));
|
|
242
295
|
return;
|
|
243
296
|
}
|
|
@@ -262,7 +315,15 @@ export async function createCommand(featureName, options = {}) {
|
|
|
262
315
|
}
|
|
263
316
|
} else if (branchResult?.branched) {
|
|
264
317
|
console.log(chalk.green(` branch: ${branchResult.branch} (from ${branchResult.from})`));
|
|
265
|
-
console.log(chalk.gray(` close it with: morph-spec finish ${featureName} --pr | --merge
|
|
318
|
+
console.log(chalk.gray(` close it with: morph-spec finish ${featureName} --pr | --merge`));
|
|
319
|
+
if (branchResult.movedFrom) {
|
|
320
|
+
// Alert, not one more success line: the tree the dev is standing in moved.
|
|
321
|
+
console.log('');
|
|
322
|
+
for (const line of branchMoveWarning(branchResult)) console.log(chalk.yellow(` ${line}`));
|
|
323
|
+
} else if (branchResult.checkedOut === false) {
|
|
324
|
+
console.log(chalk.gray(` working tree left on ${branchResult.stayedOn} (--no-checkout)`));
|
|
325
|
+
}
|
|
326
|
+
console.log('');
|
|
266
327
|
}
|
|
267
328
|
} else {
|
|
268
329
|
console.log(chalk.yellow(`\n⚠️ Feature "${featureName}" already exists — left untouched`));
|
|
@@ -4,6 +4,7 @@ import { join, resolve, sep } from 'path';
|
|
|
4
4
|
import { execSync } from 'child_process';
|
|
5
5
|
import { isValidFeatureName, cwdIsInsideWorktree, teardownWorktree, resolvePrimaryRoot } from '../project/worktree.js';
|
|
6
6
|
import { resolveFeatureTree } from '../../lib/worktree/feature-truth.js';
|
|
7
|
+
import { tearDownFeatureStack } from '../../lib/e2e/stack-teardown.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* `morph-spec delete <feature>` — remove a feature. Hard by default (folder is
|
|
@@ -144,6 +145,13 @@ export async function deleteCommand(featureName, options = {}) {
|
|
|
144
145
|
// worktree actually came down. A refusal here must leave EVERYTHING
|
|
145
146
|
// untouched, same contract as `worktree remove`.
|
|
146
147
|
const root = resolvePrimaryRoot(cwd);
|
|
148
|
+
// The feature's verification stack (containers, ports, volumes, stamp)
|
|
149
|
+
// comes down BEFORE its worktree: the compose file and the bind mount
|
|
150
|
+
// live there, and a deleted feature has nothing to keep a volume for.
|
|
151
|
+
// Fail-open by contract — a stubborn container never blocks the delete.
|
|
152
|
+
try {
|
|
153
|
+
tearDownFeatureStack(featureName, { root, worktreePath: owner.tree, volumes: true });
|
|
154
|
+
} catch { /* fail-open */ }
|
|
147
155
|
const teardown = await teardownWorktree(owner.tree, {
|
|
148
156
|
root,
|
|
149
157
|
feature: featureName,
|