@polymorphism-tech/morph-spec 8.47.0 → 9.1.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 +51 -43
- package/README.md +39 -38
- package/bin/morph-spec.js +41 -113
- package/bin/validate.js +1 -147
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +41 -101
- package/framework/MORPH.md +95 -252
- package/framework/agents.json +86 -21
- package/framework/commands/morph-apply.md +5 -28
- package/framework/commands/morph-archive.md +1 -1
- package/framework/commands/morph-hotfix.md +8 -8
- package/framework/commands/morph-infra.md +1 -1
- package/framework/commands/morph-preflight.md +2 -3
- package/framework/commands/morph-status.md +4 -4
- package/framework/docs/CLI.md +574 -286
- package/framework/evals/review-checklist.md +82 -41
- package/framework/hooks/README.md +10 -11
- package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
- package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
- package/framework/hooks/claude-code/core/chain.js +4 -65
- package/framework/hooks/claude-code/core/comment-guard.js +3 -17
- package/framework/hooks/claude-code/core/denylist.json +33 -3
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +15 -63
- package/framework/hooks/claude-code/core/gate-guard.js +9 -78
- package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
- package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
- package/framework/hooks/claude-code/core/heavy-release.js +2 -13
- package/framework/hooks/claude-code/core/loop-detect.js +2 -31
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +36 -2
- package/framework/hooks/claude-code/core/protect-spec-files.js +25 -297
- package/framework/hooks/claude-code/core/state-sync.js +15 -85
- package/framework/hooks/claude-code/core/telemetry-log.js +15 -96
- package/framework/hooks/claude-code/core/validate-completion.js +37 -147
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +23 -0
- package/framework/hooks/claude-code/core/worktree-context.js +15 -114
- package/framework/hooks/claude-code/core/worktree-create.js +6 -70
- package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
- package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
- package/framework/hooks/claude-code/statusline.py +55 -41
- package/framework/hooks/package.json +3 -0
- package/framework/hooks/shared/activity-logger.js +9 -89
- package/framework/hooks/shared/agent-reap.js +2 -17
- package/framework/hooks/shared/build-check.js +11 -76
- package/framework/hooks/shared/claim-registry.js +9 -119
- package/framework/hooks/shared/command-tokens.js +168 -44
- package/framework/hooks/shared/comment-rules.js +5 -20
- package/framework/hooks/shared/comment-scanner.js +11 -43
- package/framework/hooks/shared/comment-stripper.js +11 -85
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/errata.js +59 -0
- package/framework/hooks/shared/feature-locator.js +11 -95
- package/framework/hooks/shared/frontend-locations.js +4 -0
- package/framework/hooks/shared/gate-question.js +12 -0
- package/framework/hooks/shared/git-exclude.js +3 -20
- package/framework/hooks/shared/git-paths.js +8 -104
- package/framework/hooks/shared/graph-freshness.js +22 -166
- package/framework/hooks/shared/graph-refresh.js +21 -149
- package/framework/hooks/shared/heavy-command.js +4 -12
- package/framework/hooks/shared/hook-response.js +8 -83
- package/framework/hooks/shared/morph-cli.js +8 -52
- package/framework/hooks/shared/payload-utils.js +10 -132
- package/framework/hooks/shared/phase-utils.js +14 -72
- package/framework/hooks/shared/redact-secrets.js +4 -15
- package/framework/hooks/shared/reparse-scan.js +6 -66
- package/framework/hooks/shared/slice-progress.js +99 -0
- package/framework/hooks/shared/source-files.js +4 -17
- package/framework/hooks/shared/spawn-command.js +23 -0
- package/framework/hooks/shared/state-reader.js +51 -247
- package/framework/hooks/shared/state-writer.js +9 -54
- package/framework/hooks/shared/stdin-reader.js +1 -11
- package/framework/hooks/shared/telemetry-logger.js +7 -67
- package/framework/hooks/shared/transcript-usage.js +7 -29
- package/framework/hooks/shared/worktree-root.js +7 -29
- package/framework/mcp/registry.json +3 -4
- package/framework/phases.json +23 -39
- package/framework/rules/csharp-standards.md +1 -1
- package/framework/rules/frontend-standards.md +1 -0
- package/framework/rules/hangfire-standards.md +1 -1
- package/framework/rules/neon-standards.md +1 -1
- package/framework/schemas/config.schema.json +14 -9
- package/framework/schemas/feature.schema.json +15 -50
- package/framework/skills/README.md +3 -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 +149 -222
- package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
- package/framework/skills/core/morph-machine/SKILL.md +2 -2
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +137 -299
- package/framework/skills/core/morph-review/SKILL.md +111 -136
- package/framework/skills/core/morph-standards/SKILL.md +27 -39
- package/framework/skills/core/morph-uiux/SKILL.md +94 -152
- package/framework/standards/STANDARDS.json +109 -1219
- package/framework/standards/STANDARDS.meta.json +1319 -0
- package/framework/standards/ai-agents/agent-archetypes.md +2 -2
- package/framework/standards/ai-agents/agent-session.md +1 -1
- package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
- package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
- package/framework/standards/ai-agents/evals-with-cache.md +1 -2
- package/framework/standards/ai-agents/guardrails.md +1 -1
- package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
- package/framework/standards/ai-agents/observability-patterns.md +1 -1
- package/framework/standards/ai-agents/production.md +1 -1
- package/framework/standards/ai-agents/prompt-sources.md +2 -2
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
- package/framework/standards/ai-agents/setup.md +3 -3
- package/framework/standards/ai-agents/sweet-spot.md +2 -2
- package/framework/standards/ai-agents/tool-adapters.md +0 -2
- package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
- package/framework/standards/ai-agents/workflows.md +1 -1
- package/framework/standards/architecture/code-comments.md +7 -7
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
- package/framework/standards/backend/api/validation.md +1 -1
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
- package/framework/standards/data/nosql/cache/redis.md +1 -1
- package/framework/standards/data/nosql/cosmos-db.md +3 -3
- package/framework/standards/frontend/admin-app/forms.md +1 -1
- package/framework/standards/frontend/design-system/art-direction.md +88 -0
- package/framework/standards/frontend/design-system/premium-finish.md +77 -2
- package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
- package/framework/standards/integration/mcp/mcp-tools.md +7 -7
- package/framework/templates/CLAUDE.md.core.template +3 -3
- package/framework/templates/README.md +2 -3
- package/framework/templates/REGISTRY.json +15 -179
- package/framework/templates/docs/plan.md +72 -0
- package/framework/templates/docs/ui-mockups.md +4 -0
- package/framework/templates/dotnet/ai-kit/README.md +1 -9
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
- package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
- package/package.json +1 -1
- package/src/commands/context/context-refresh.js +0 -16
- package/src/commands/dashboard/dashboard.js +2 -8
- package/src/commands/e2e/e2e.js +18 -105
- package/src/commands/graph/affected.js +0 -14
- package/src/commands/graph/build.js +11 -50
- package/src/commands/graph/explain.js +0 -16
- package/src/commands/graph/status.js +1 -12
- package/src/commands/mcp/mcp-setup.js +4 -62
- package/src/commands/observability/cost.js +17 -111
- package/src/commands/observability/telemetry.js +1 -14
- package/src/commands/project/branch.js +4 -45
- package/src/commands/project/claim.js +4 -48
- package/src/commands/project/doctor-procs.js +1 -1
- package/src/commands/project/doctor.js +53 -307
- package/src/commands/project/env.js +1 -27
- package/src/commands/project/fleet.js +6 -35
- package/src/commands/project/heavy.js +3 -3
- package/src/commands/project/init.js +8 -68
- package/src/commands/project/install-plugin-cmd.js +0 -1
- package/src/commands/project/setup-infra-cmd.js +2 -1
- package/src/commands/project/stack-scan.js +58 -0
- package/src/commands/project/standards.js +65 -72
- package/src/commands/project/status.js +28 -121
- package/src/commands/project/update.js +31 -223
- package/src/commands/project/worktree.js +109 -883
- package/src/commands/scaffold/scaffold.js +0 -25
- package/src/commands/state/advance.js +65 -120
- package/src/commands/state/approve.js +87 -229
- package/src/commands/state/archive.js +23 -139
- package/src/commands/state/auto-approval.js +65 -0
- package/src/commands/state/create.js +49 -109
- package/src/commands/state/delete.js +11 -63
- package/src/commands/state/finish.js +105 -494
- package/src/commands/state/gate-check.js +74 -183
- package/src/commands/state/gate-decision.js +8 -42
- package/src/commands/state/handoff.js +4 -31
- package/src/commands/state/list.js +21 -91
- package/src/commands/state/report-check.js +134 -338
- package/src/commands/state/retype.js +12 -28
- package/src/commands/validation/index.js +0 -3
- package/src/commands/validation/mutate.js +79 -19
- package/src/commands/validation/plan-check.js +81 -0
- package/src/commands/validation/validate-feature.js +52 -216
- package/src/commands/validation/verify.js +45 -160
- package/src/core/paths/output-schema.js +9 -155
- package/src/core/state/feature-identity.js +1 -25
- package/src/core/state/gate-order.js +2 -30
- package/src/core/state/lifecycle-audit.js +8 -100
- package/src/core/state/state-manager.js +32 -395
- package/src/core/templates/template-registry.js +2 -97
- package/src/core/templates/template-renderer.js +13 -93
- package/src/core/templates/template-seed.js +21 -0
- package/src/core/work-types.js +10 -45
- package/src/dashboard/app/App.jsx +1 -8
- package/src/dashboard/app/components/ActivityFeed.jsx +2 -8
- package/src/dashboard/app/components/CommandPalette.jsx +4 -11
- package/src/dashboard/app/components/DetailPanel.jsx +1 -5
- package/src/dashboard/app/components/EntityLink.jsx +0 -12
- package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
- package/src/dashboard/app/components/Markdown.jsx +0 -9
- package/src/dashboard/app/components/PhaseRail.jsx +0 -11
- package/src/dashboard/app/components/SearchFilter.jsx +0 -4
- package/src/dashboard/app/components/Sidebar.jsx +0 -5
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
- package/src/dashboard/app/hooks/usePref.js +1 -5
- package/src/dashboard/app/lib/format.js +0 -2
- package/src/dashboard/app/lib/phases.js +3 -9
- package/src/dashboard/app/lib/project.js +0 -12
- package/src/dashboard/app/pages/Activity.jsx +1 -1
- package/src/dashboard/app/pages/AgentMap.jsx +1 -8
- package/src/dashboard/app/pages/FeatureDetail.jsx +139 -133
- package/src/dashboard/app/pages/Features.jsx +9 -16
- package/src/dashboard/app/pages/Guide.jsx +2 -3
- package/src/dashboard/app/pages/HookInspector.jsx +2 -23
- package/src/dashboard/app/pages/PhasePipeline.jsx +3 -7
- package/src/dashboard/app/pages/Portfolio.jsx +0 -1
- package/src/dashboard/app/pages/RulesMap.jsx +1 -16
- package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
- package/src/dashboard/app/pages/Standards.jsx +2 -17
- package/src/dashboard/scanner/activity-scanner.js +14 -83
- package/src/dashboard/scanner/agents-scanner.js +1 -23
- package/src/dashboard/scanner/docs-scanner.js +1 -7
- package/src/dashboard/scanner/feature-status.js +2 -5
- package/src/dashboard/scanner/features-scanner.js +92 -113
- package/src/dashboard/scanner/hooks-scanner.js +5 -28
- package/src/dashboard/scanner/index.js +0 -4
- package/src/dashboard/scanner/portfolio.js +5 -29
- package/src/dashboard/scanner/project-api.js +8 -54
- package/src/dashboard/scanner/rules-scanner.js +1 -2
- package/src/dashboard/scanner/skills-scanner.js +2 -12
- package/src/dashboard/scanner/standards-scanner.js +4 -7
- package/src/dashboard/vite-plugin-morph-data.js +5 -19
- package/src/lib/ai/ai-checks.js +7 -52
- package/src/lib/ai/ai-project-scan.js +14 -108
- package/src/lib/ai/code-audit.js +7 -63
- package/src/lib/ai/pin-drift.js +8 -51
- package/src/lib/ai/registry-audit.js +11 -90
- package/src/lib/ci/ci-verify.js +14 -13
- package/src/lib/ci/github-ci.js +9 -37
- package/src/lib/ci/workflow-scaffold.js +2 -10
- package/src/lib/detectors/claude-config-detector.js +13 -74
- package/src/lib/detectors/index.js +8 -59
- package/src/lib/e2e/compose-env.js +5 -66
- package/src/lib/e2e/stack-teardown.js +6 -40
- package/src/lib/generators/architecture-scanner.js +18 -156
- package/src/lib/generators/handoff-generator.js +37 -112
- package/src/lib/generators/scaffold-generator.js +5 -101
- package/src/lib/generators/settings-generator.js +11 -122
- package/src/lib/git/branch-classifier.js +11 -84
- package/src/lib/git/default-branch.js +14 -169
- package/src/lib/git/evidence-staleness.js +2 -1
- package/src/lib/git/gh.js +2 -17
- package/src/lib/git/pending-prs.js +6 -37
- package/src/lib/git/pr-files.js +3 -28
- package/src/lib/git/pr-index.js +3 -34
- package/src/lib/git/tree-fingerprint.js +17 -17
- package/src/lib/git/tree-state.js +6 -33
- package/src/lib/graph/god-nodes.js +2 -22
- package/src/lib/graph/graph-health.js +3 -24
- package/src/lib/graph/graph-loader.js +5 -56
- package/src/lib/graph/graphify-runtime.js +7 -37
- package/src/lib/graph/graphifyignore.js +8 -74
- package/src/lib/graph/sanitize.js +16 -136
- package/src/lib/heavy/cleanup.js +2 -6
- package/src/lib/installers/effects.js +8 -93
- package/src/lib/installers/mcp-installer.js +9 -169
- package/src/lib/installers/memory-installer.js +8 -33
- package/src/lib/installers/templates-reconcile.js +5 -60
- package/src/lib/mutation/commands.js +3 -2
- package/src/lib/mutation/dotnet-introspection.js +281 -0
- package/src/lib/mutation/plan.js +14 -6
- package/src/lib/mutation/runner.js +1 -1
- package/src/lib/mutation/subset.js +47 -13
- package/src/lib/plan/artifact-vocabulary.js +40 -0
- package/src/lib/plan/checks.js +208 -0
- package/src/lib/plan/plan-parser.js +204 -0
- package/src/lib/plan/plan-template.js +18 -0
- package/src/lib/projects/registry.js +7 -44
- package/src/lib/review-reports.js +204 -0
- package/src/lib/risk-detector.js +131 -274
- package/src/lib/specs/adr-index.js +38 -85
- package/src/lib/specs/design-seed.js +18 -0
- package/src/lib/specs/frontmatter.js +31 -0
- package/src/lib/specs/gfm-table.js +11 -0
- package/src/lib/specs/revocations.js +31 -102
- package/src/lib/specs/screen-checklist.js +56 -132
- package/src/lib/stack/dotnet-build-ignores.js +43 -0
- package/src/lib/stack/stack-profile.js +1 -28
- package/src/lib/stack/stack-scan.js +260 -0
- package/src/lib/stack-filter.js +4 -27
- package/src/lib/standards/registry.js +37 -0
- package/src/lib/standards/sections.js +47 -0
- package/src/lib/standards/verified-against.js +19 -191
- package/src/lib/tasks/eval-node.js +23 -179
- package/src/lib/tasks/linux-runner.js +18 -142
- package/src/lib/tasks/test-nodes.js +43 -313
- package/src/lib/tasks/test-runner.js +107 -447
- package/src/lib/telemetry/metrics.js +13 -103
- package/src/lib/telemetry/telemetry-reader.js +13 -238
- package/src/lib/telemetry/verify-event.js +4 -34
- package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
- package/src/lib/validators/code-budget/index.js +0 -7
- package/src/lib/validators/contracts/contract-compliance-validator.js +4 -26
- package/src/lib/validators/contracts/index.js +0 -6
- package/src/lib/validators/nextjs/index.js +0 -5
- package/src/lib/validators/nextjs/next-component-validator.js +4 -81
- package/src/lib/validators/packages/index.js +0 -6
- package/src/lib/validators/packages/package-validator.js +6 -56
- package/src/lib/validators/reuse/index.js +0 -8
- package/src/lib/validators/reuse/reuse-validator.js +24 -137
- package/src/lib/validators/reuse/symbol-scanner.js +6 -62
- package/src/lib/validators/traceability-check.js +54 -156
- package/src/lib/validators/validation-runner.js +20 -125
- package/src/lib/validators/verify-runner.js +152 -575
- package/src/lib/work-type-detector.js +5 -67
- package/src/lib/worktree/docker-audit.js +17 -137
- package/src/lib/worktree/env-allocator.js +10 -98
- package/src/lib/worktree/feature-truth.js +12 -115
- package/src/lib/worktree/finish-backup.js +32 -242
- package/src/lib/worktree/fleet-scanner.js +31 -126
- package/src/lib/worktree/ignored-triage.js +15 -91
- package/src/lib/worktree/vscode-settings.js +2 -3
- package/src/lib/worktree/worktree-audit.js +21 -213
- package/src/lib/worktree/worktree-include.js +3 -5
- package/src/scripts/global-install.js +3 -9
- package/src/scripts/install-plugin.js +1 -13
- package/src/scripts/setup-infra.js +17 -138
- package/src/utils/agents-installer.js +92 -272
- package/src/utils/banner.js +4 -25
- package/src/utils/claude-md-injector.js +1 -37
- package/src/utils/claude-settings-manager.js +1 -87
- package/src/utils/file-copier.js +32 -90
- package/src/utils/hooks-installer.js +26 -296
- package/src/utils/skills-installer.js +4 -102
- package/src/utils/version-checker.js +4 -83
- package/framework/commands/morph-proposal.md +0 -57
- package/framework/evals/trace-schema.md +0 -67
- package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
- package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
- package/framework/hooks/git/pre-commit/specs.sh +0 -50
- package/framework/hooks/shared/tasks-validator.js +0 -111
- package/framework/rules/morph-task-tracking.md +0 -54
- package/framework/rules/morph-workflow.md +0 -244
- package/framework/schemas/tasks.schema.json +0 -79
- package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
- package/framework/skills/core/morph-implement/SKILL.md +0 -194
- package/framework/skills/core/morph-proposal/SKILL.md +0 -152
- package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
- package/framework/templates/docs/proposal.md +0 -192
- package/framework/templates/docs/spec.md +0 -57
- package/framework/templates/docs/ui-components.md +0 -52
- package/framework/templates/docs/ui-design-system.md +0 -76
- package/framework/templates/docs/ui-flows.md +0 -167
- package/framework/templates/examples/spec-examples.md +0 -91
- package/framework/templates/feature/decisions.md +0 -124
- package/framework/templates/feature/recap.md +0 -145
- package/framework/templates/tasks.template.json +0 -17
- package/src/commands/state/usecase.js +0 -143
- package/src/commands/tasks/dag.js +0 -181
- package/src/commands/utils/index.js +0 -3
- package/src/lib/agents/output-collector.js +0 -76
- package/src/lib/detectors/design-system-detector.js +0 -188
- package/src/lib/evaluator-report.js +0 -104
- package/src/lib/generators/design-system-generator.js +0 -508
- package/src/lib/specs/acervo-format.js +0 -70
- package/src/lib/specs/acervo-lifecycle.js +0 -264
- package/src/lib/specs/done-criteria-check.js +0 -200
- package/src/lib/specs/usecase-check.js +0 -301
- package/src/lib/specs/usecase-promoter.js +0 -301
- package/src/lib/specs/usecase-registry.js +0 -237
- package/src/lib/tasks/dispatch-planner.js +0 -319
- package/src/lib/tasks/dotnet-test-introspection.js +0 -632
- package/src/lib/tasks/mermaid-renderer.js +0 -132
- package/src/lib/tasks/task-classifier.js +0 -30
- package/src/lib/tasks/task-parser.js +0 -209
- package/src/lib/tasks/tasks-json-parser.js +0 -296
- package/src/lib/tasks/test-scope.js +0 -597
- package/src/utils/agent-id-mapper.js +0 -11
|
@@ -56,28 +56,26 @@ SEP = f" {GRAY}│{R} "
|
|
|
56
56
|
|
|
57
57
|
# ── MORPH framework constants ────────────────────────────────────────────────
|
|
58
58
|
|
|
59
|
-
# Mirrors
|
|
60
|
-
# (scanning backwards) whose CANONICAL artifact exists wins. An
|
|
61
|
-
# is not enough.
|
|
59
|
+
# Mirrors derivePhaseFrom() in framework/hooks/shared/state-reader.js — the
|
|
60
|
+
# first phase (scanning backwards) whose CANONICAL artifact exists wins. An
|
|
61
|
+
# empty folder is not enough; a canonical ending in '/' is any .md under it.
|
|
62
62
|
PHASE_MAP = [
|
|
63
|
-
('4-review', 'review', '
|
|
64
|
-
('3-implement', 'implement', '
|
|
65
|
-
('2-plan', 'plan', 'spec.md'),
|
|
63
|
+
('4-review', 'review', 'verifier.md'),
|
|
64
|
+
('3-implement', 'implement', 'reports/'),
|
|
66
65
|
('1-design', 'uiux', 'design-system.md'),
|
|
67
|
-
('
|
|
66
|
+
('2-plan', 'plan', 'plan.md'),
|
|
68
67
|
]
|
|
69
|
-
PHASE_INDEX = {'
|
|
68
|
+
PHASE_INDEX = {'plan': 0, 'uiux': 1, 'implement': 2, 'review': 3}
|
|
70
69
|
PHASE_LABELS = {
|
|
71
|
-
'proposal': 'proposal',
|
|
72
|
-
'uiux': 'UI/UX',
|
|
73
70
|
'plan': 'planning',
|
|
71
|
+
'uiux': 'UI/UX',
|
|
74
72
|
'implement': 'implement',
|
|
75
73
|
'review': 'review',
|
|
76
74
|
}
|
|
77
|
-
PIPELINE_TOTAL =
|
|
75
|
+
PIPELINE_TOTAL = 4
|
|
78
76
|
|
|
79
77
|
# Gates that can block; `implement` has none. Mirrors feature.schema.json.
|
|
80
|
-
GATE_PHASES = ('
|
|
78
|
+
GATE_PHASES = ('plan', 'uiux', 'review')
|
|
81
79
|
|
|
82
80
|
|
|
83
81
|
# ── Auto-compact threshold ───────────────────────────────────────────────────
|
|
@@ -358,49 +356,65 @@ def get_worktree_label(payload, cwd):
|
|
|
358
356
|
return None
|
|
359
357
|
|
|
360
358
|
|
|
361
|
-
def _gate_implied_phase(gates):
|
|
362
|
-
"""Phase implied purely by approved gates (mirrors state-
|
|
359
|
+
def _gate_implied_phase(gates, design_started):
|
|
360
|
+
"""Phase implied purely by approved gates (mirrors state-reader.js)."""
|
|
363
361
|
def approved(gate):
|
|
364
362
|
return (gates.get(gate) or {}).get('approved') is True
|
|
365
363
|
if approved('review'):
|
|
366
364
|
return 'review'
|
|
367
365
|
if approved('plan'):
|
|
368
|
-
return 'implement'
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
366
|
+
return 'uiux' if design_started and not approved('uiux') else 'implement'
|
|
367
|
+
return 'plan'
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _has_artifact(folder, canonical):
|
|
371
|
+
if not canonical.endswith('/'):
|
|
372
|
+
return (folder / canonical).exists()
|
|
373
|
+
try:
|
|
374
|
+
return any(p.is_file() for p in (folder / canonical).glob('*.md'))
|
|
375
|
+
except OSError:
|
|
376
|
+
return False
|
|
372
377
|
|
|
373
378
|
|
|
374
379
|
def derive_phase(feature_dir, gates):
|
|
375
380
|
"""Phase from the filesystem, bumped by approved gates."""
|
|
376
|
-
artifact_phase = '
|
|
381
|
+
artifact_phase = 'plan'
|
|
377
382
|
for folder, phase, canonical in PHASE_MAP:
|
|
378
|
-
if (feature_dir / folder
|
|
383
|
+
if _has_artifact(feature_dir / folder, canonical):
|
|
379
384
|
artifact_phase = phase
|
|
380
385
|
break
|
|
381
|
-
implied = _gate_implied_phase(gates)
|
|
386
|
+
implied = _gate_implied_phase(gates, (feature_dir / '1-design').exists())
|
|
382
387
|
if PHASE_INDEX[implied] > PHASE_INDEX[artifact_phase]:
|
|
383
388
|
return implied
|
|
384
389
|
return artifact_phase
|
|
385
390
|
|
|
386
391
|
|
|
387
|
-
|
|
388
|
-
"""(done, total) from 2-plan/tasks.json — the declared source of truth.
|
|
392
|
+
PARTIAL_MARKER = re.compile(r'^[\s#>*_]*PARCIAL\b')
|
|
389
393
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
394
|
+
|
|
395
|
+
def count_slices(feature_dir):
|
|
396
|
+
"""(done, total) of the slices in 2-plan/checks.json.
|
|
397
|
+
|
|
398
|
+
Same rule as framework/hooks/shared/slice-progress.js: a slice is done when
|
|
399
|
+
3-implement/reports/{id}.md exists and its first line is not PARCIAL.
|
|
393
400
|
"""
|
|
394
401
|
try:
|
|
395
|
-
|
|
396
|
-
tasks = json.loads(raw).get('tasks')
|
|
397
|
-
if isinstance(tasks, list) and tasks:
|
|
398
|
-
done = sum(1 for task in tasks if task.get('status') in ('done', 'skipped'))
|
|
399
|
-
return done, len(tasks)
|
|
402
|
+
slices = json.loads((feature_dir / '2-plan' / 'checks.json').read_text(encoding='utf-8')).get('slices')
|
|
400
403
|
except Exception:
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
+
return 0, 0
|
|
405
|
+
if not isinstance(slices, list):
|
|
406
|
+
return 0, 0
|
|
407
|
+
ids = [s.get('id') for s in slices if isinstance(s, dict) and isinstance(s.get('id'), str)]
|
|
408
|
+
done = 0
|
|
409
|
+
for slice_id in ids:
|
|
410
|
+
try:
|
|
411
|
+
text = (feature_dir / '3-implement' / 'reports' / f'{slice_id}.md').read_text(encoding='utf-8')
|
|
412
|
+
except Exception:
|
|
413
|
+
continue
|
|
414
|
+
first = next((line for line in text.splitlines() if line.strip()), '')
|
|
415
|
+
if not PARTIAL_MARKER.match(first):
|
|
416
|
+
done += 1
|
|
417
|
+
return done, len(ids)
|
|
404
418
|
|
|
405
419
|
|
|
406
420
|
def get_active_features(cwd):
|
|
@@ -428,15 +442,15 @@ def get_active_features(cwd):
|
|
|
428
442
|
feature_dir = path.parent
|
|
429
443
|
gates = data.get('approvalGates') or {}
|
|
430
444
|
phase = derive_phase(feature_dir, gates)
|
|
431
|
-
done, total =
|
|
445
|
+
done, total = count_slices(feature_dir)
|
|
432
446
|
pending = None
|
|
433
447
|
if phase in GATE_PHASES and (gates.get(phase) or {}).get('approved') is not True:
|
|
434
448
|
pending = phase
|
|
435
449
|
result.append({
|
|
436
450
|
'name': feature_dir.name,
|
|
437
451
|
'phase': phase,
|
|
438
|
-
'
|
|
439
|
-
'
|
|
452
|
+
'slices_done': done,
|
|
453
|
+
'slices_total': total,
|
|
440
454
|
'pending': pending,
|
|
441
455
|
})
|
|
442
456
|
return result
|
|
@@ -484,7 +498,7 @@ def render_morph_line(feature, hook_name, columns):
|
|
|
484
498
|
"""Line 0 — the active MORPH feature.
|
|
485
499
|
|
|
486
500
|
Goes through join_to_width like the status line: a feature name plus phase,
|
|
487
|
-
|
|
501
|
+
slice bar, gate and hook is the longest thing this script prints, and a
|
|
488
502
|
wrapping line 0 would defeat the whole point of the single-row layout.
|
|
489
503
|
"""
|
|
490
504
|
parts = [(0, f"{CYAN}{BOLD}► {feature['name']}{R}")]
|
|
@@ -497,12 +511,12 @@ def render_morph_line(feature, hook_name, columns):
|
|
|
497
511
|
else:
|
|
498
512
|
parts.append((1, f"{CYAN}{label} {position + 1}/{PIPELINE_TOTAL}{R}"))
|
|
499
513
|
|
|
500
|
-
if feature['
|
|
501
|
-
pct = feature['
|
|
514
|
+
if feature['slices_total'] > 0:
|
|
515
|
+
pct = feature['slices_done'] / feature['slices_total'] * 100
|
|
502
516
|
parts.append((
|
|
503
517
|
3,
|
|
504
518
|
f"{GREEN}{progress_bar(pct, 6)} "
|
|
505
|
-
f"{feature['
|
|
519
|
+
f"{feature['slices_done']}/{feature['slices_total']} fatias{R}"
|
|
506
520
|
))
|
|
507
521
|
|
|
508
522
|
if feature['pending']:
|
|
@@ -1,28 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* Writes hook activity to .morph/logs/activity.jsonl for live consumption
|
|
5
|
-
* (statusline, `morph-spec fleet`, etc).
|
|
6
|
-
*
|
|
7
|
-
* Design constraints:
|
|
8
|
-
* - Synchronous only (hooks run in process, no async overhead)
|
|
9
|
-
* - Fail-silent (never throws — hooks must not be blocked by the logger)
|
|
10
|
-
* - ≤5ms per call (no retries, no network)
|
|
11
|
-
* - APPEND-ONLY: one `appendFileSync` line per event, O(1) per call. The
|
|
12
|
-
* previous format did a read-modify-write of the WHOLE JSON doc per call
|
|
13
|
-
* (logHookActivity/logEvent both read, mutated, and rewrote the entire
|
|
14
|
-
* file) — O(n²) across a session once tool calls number in the hundreds,
|
|
15
|
-
* with 6 hooks each calling this 3-5x per Write. Same discipline as
|
|
16
|
-
* telemetry-logger.js's events.jsonl.
|
|
17
|
-
* - Windows-safe paths via path.join
|
|
18
|
-
*
|
|
19
|
-
* Exported names/signatures are unchanged from the previous doc-based
|
|
20
|
-
* implementation on purpose — hooks outside this file's ownership
|
|
21
|
-
* (gate-guard.js, tasks-json-guard.js, enforce-phase-writes.js,
|
|
22
|
-
* loop-detect.js, protect-spec-files.js, validate-completion.js) call
|
|
23
|
-
* logHookActivity/logEvent and must keep working untouched. Only the
|
|
24
|
-
* on-disk format changed.
|
|
25
|
-
*/
|
|
1
|
+
// Append-only on purpose: rewriting a whole JSON doc per event is O(n²) across
|
|
2
|
+
// a session. Fail-silent: never blocks a hook.
|
|
26
3
|
|
|
27
4
|
import {
|
|
28
5
|
appendFileSync, readFileSync, mkdirSync, existsSync, statSync, renameSync, writeFileSync,
|
|
@@ -32,10 +9,7 @@ import { join } from 'path';
|
|
|
32
9
|
const LOGS_DIR = '.morph/logs';
|
|
33
10
|
export const ACTIVITY_FILE = join(LOGS_DIR, 'activity.jsonl');
|
|
34
11
|
|
|
35
|
-
|
|
36
|
-
// size — the archive/rotation call site resetActivity()/archiveSession()
|
|
37
|
-
// were written for but never got until now.
|
|
38
|
-
export const ROTATE_THRESHOLD_BYTES = 2 * 1024 * 1024; // 2 MB
|
|
12
|
+
export const ROTATE_THRESHOLD_BYTES = 2 * 1024 * 1024;
|
|
39
13
|
|
|
40
14
|
function getActivityPath(projectPath) {
|
|
41
15
|
return join(projectPath || process.cwd(), ACTIVITY_FILE);
|
|
@@ -49,20 +23,17 @@ function ensureLogsDir(projectPath) {
|
|
|
49
23
|
}
|
|
50
24
|
|
|
51
25
|
function nowTs() {
|
|
52
|
-
return new Date().toTimeString().slice(0, 8);
|
|
26
|
+
return new Date().toTimeString().slice(0, 8);
|
|
53
27
|
}
|
|
54
28
|
|
|
55
|
-
/** Append one JSON line. Fail-silent, O(1) regardless of file size. */
|
|
56
29
|
function appendLine(projectPath, line) {
|
|
57
30
|
try {
|
|
58
31
|
ensureLogsDir(projectPath);
|
|
59
32
|
appendFileSync(getActivityPath(projectPath), JSON.stringify(line) + '\n', 'utf-8');
|
|
60
33
|
} catch {
|
|
61
|
-
// Fail-silent — never block hooks
|
|
62
34
|
}
|
|
63
35
|
}
|
|
64
36
|
|
|
65
|
-
/** Read every parseable line back. A corrupt/truncated line is skipped, not fatal. */
|
|
66
37
|
function readLines(projectPath) {
|
|
67
38
|
try {
|
|
68
39
|
const activityPath = getActivityPath(projectPath);
|
|
@@ -75,7 +46,7 @@ function readLines(projectPath) {
|
|
|
75
46
|
try {
|
|
76
47
|
lines.push(JSON.parse(trimmed));
|
|
77
48
|
} catch {
|
|
78
|
-
//
|
|
49
|
+
// a process killed mid-write leaves a truncated line
|
|
79
50
|
}
|
|
80
51
|
}
|
|
81
52
|
return lines;
|
|
@@ -84,38 +55,15 @@ function readLines(projectPath) {
|
|
|
84
55
|
}
|
|
85
56
|
}
|
|
86
57
|
|
|
87
|
-
/**
|
|
88
|
-
* Log a hook firing event to the session activity log.
|
|
89
|
-
*
|
|
90
|
-
* @param {string} name - Hook name (e.g. 'dispatch.js', 'enrich-prompt')
|
|
91
|
-
* @param {string} event - Claude Code event (e.g. 'PostToolUse', 'UserPromptSubmit')
|
|
92
|
-
* @param {string} result - Outcome summary (e.g. 'ok', 'blocked', 'checkpoint_saved')
|
|
93
|
-
* @param {string} [projectPath] - Project root (defaults to cwd)
|
|
94
|
-
*/
|
|
95
58
|
export function logHookActivity(name, event, result, projectPath) {
|
|
96
59
|
appendLine(projectPath, { kind: 'hook', name, event, result, ts: nowTs() });
|
|
97
60
|
}
|
|
98
61
|
|
|
99
|
-
/**
|
|
100
|
-
* Log a structured event to the session activity log.
|
|
101
|
-
*
|
|
102
|
-
* @param {string} type - Event type (e.g., 'task_start', 'loop_detected')
|
|
103
|
-
* @param {Object} data - Event data payload
|
|
104
|
-
* @param {string} [projectPath] - Project root (defaults to cwd)
|
|
105
|
-
*/
|
|
106
62
|
export function logEvent(type, data, projectPath) {
|
|
107
63
|
appendLine(projectPath, { kind: 'event', type, data, ts: nowTs() });
|
|
108
64
|
}
|
|
109
65
|
|
|
110
|
-
/**
|
|
111
|
-
* Read the current activity log, reconstructed into the aggregate shape
|
|
112
|
-
* older consumers expect: `{ sessionId, feature, phase, hooks, events }`.
|
|
113
|
-
* `sessionId`/`feature`/`phase` come from the synthetic 'session' header
|
|
114
|
-
* line `resetActivity()` writes, when present.
|
|
115
|
-
*
|
|
116
|
-
* @param {string} [projectPath]
|
|
117
|
-
* @returns {{ sessionId: string, feature: string, phase: string, hooks: Array, events: Array }|null}
|
|
118
|
-
*/
|
|
66
|
+
/** @returns {{ sessionId: string, feature: string, phase: string, hooks: Array, events: Array }|null} */
|
|
119
67
|
export function readActivity(projectPath) {
|
|
120
68
|
const lines = readLines(projectPath);
|
|
121
69
|
if (lines.length === 0) return null;
|
|
@@ -132,17 +80,8 @@ export function readActivity(projectPath) {
|
|
|
132
80
|
};
|
|
133
81
|
}
|
|
134
82
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
* that is NOT append-only (a session boundary, not a log entry). Truncates
|
|
138
|
-
* the file and writes a 'session' header line so `readActivity()` can
|
|
139
|
-
* recover sessionId/feature/phase.
|
|
140
|
-
*
|
|
141
|
-
* @param {string} sessionId - ISO timestamp of new session
|
|
142
|
-
* @param {string} feature - Active feature name (empty string if none)
|
|
143
|
-
* @param {string} phase - Current phase (empty string if none)
|
|
144
|
-
* @param {string} [projectPath]
|
|
145
|
-
*/
|
|
83
|
+
// The one write that truncates: the 'session' header is what `readActivity()`
|
|
84
|
+
// recovers sessionId/feature/phase from.
|
|
146
85
|
export function resetActivity(sessionId, feature, phase, projectPath) {
|
|
147
86
|
try {
|
|
148
87
|
ensureLogsDir(projectPath);
|
|
@@ -154,18 +93,9 @@ export function resetActivity(sessionId, feature, phase, projectPath) {
|
|
|
154
93
|
};
|
|
155
94
|
writeFileSync(getActivityPath(projectPath), JSON.stringify(session) + '\n', 'utf-8');
|
|
156
95
|
} catch {
|
|
157
|
-
// Fail-silent
|
|
158
96
|
}
|
|
159
97
|
}
|
|
160
98
|
|
|
161
|
-
/**
|
|
162
|
-
* Archive the current activity log to sessions/ for historical logging.
|
|
163
|
-
* Only archives when there is at least one hook or event line — a bare (or
|
|
164
|
-
* absent) log is not worth preserving. `renameSync` keeps this O(1) even at
|
|
165
|
-
* the multi-MB sizes rotation is meant to catch (no read+rewrite of content).
|
|
166
|
-
*
|
|
167
|
-
* @param {string} [projectPath] - Project root (defaults to cwd)
|
|
168
|
-
*/
|
|
169
99
|
export function archiveSession(projectPath) {
|
|
170
100
|
try {
|
|
171
101
|
const activityPath = getActivityPath(projectPath);
|
|
@@ -183,20 +113,10 @@ export function archiveSession(projectPath) {
|
|
|
183
113
|
const archivePath = join(sessionsDir, `${stamp}.jsonl`);
|
|
184
114
|
renameSync(activityPath, archivePath);
|
|
185
115
|
} catch {
|
|
186
|
-
// Fail-silent
|
|
187
116
|
}
|
|
188
117
|
}
|
|
189
118
|
|
|
190
|
-
/**
|
|
191
|
-
* Rotate the activity log when it crosses `thresholdBytes` — the real call
|
|
192
|
-
* site for archiveSession() (previously written but never invoked anywhere).
|
|
193
|
-
* Called from worktree-context.js on SessionStart, the only reliable
|
|
194
|
-
* "between sessions" checkpoint the harness gets.
|
|
195
|
-
*
|
|
196
|
-
* @param {string} [projectPath] - Project root (defaults to cwd)
|
|
197
|
-
* @param {number} [thresholdBytes] - Rotate once the log is at least this large
|
|
198
|
-
* @returns {boolean} true if a rotation was performed
|
|
199
|
-
*/
|
|
119
|
+
/** @returns {boolean} true if a rotation was performed */
|
|
200
120
|
export function rotateActivityLogIfLarge(projectPath, thresholdBytes = ROTATE_THRESHOLD_BYTES) {
|
|
201
121
|
try {
|
|
202
122
|
const activityPath = getActivityPath(projectPath);
|
|
@@ -1,15 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* O hook de SessionStart tem 5 s de timeout e o reaper mede git por árvore: por isso o
|
|
5
|
-
* disparo só olha o disco (nenhum subprocesso) e solta este arquivo como worker DESTACADO
|
|
6
|
-
* (`--run <raiz>`), que chama o CLI. Só a raiz: o worker nasce com `cwd` no git common dir
|
|
7
|
-
* e nunca segura o diretório de uma worktree.
|
|
8
|
-
*
|
|
9
|
-
* Fail-open em tudo. `MORPH_WORKTREE_REAP=0` desliga.
|
|
10
|
-
*
|
|
11
|
-
* @module agent-reap
|
|
12
|
-
*/
|
|
1
|
+
// O SessionStart tem 5 s: o disparo só olha o disco e solta um worker DESTACADO
|
|
2
|
+
// com `cwd` no git common dir, que nunca segura o diretório de uma worktree.
|
|
13
3
|
|
|
14
4
|
import { spawn } from 'child_process';
|
|
15
5
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs';
|
|
@@ -29,7 +19,6 @@ function anyEntry(dir, pattern) {
|
|
|
29
19
|
try { return readdirSync(dir).some((name) => pattern.test(name)); } catch { return false; }
|
|
30
20
|
}
|
|
31
21
|
|
|
32
|
-
/** Uma worktree de agent registrada, ou uma branch `morph-task/agent-*` (solta ou empacotada). */
|
|
33
22
|
export function hasAgentLeftovers(common) {
|
|
34
23
|
if (anyEntry(join(common, 'worktrees'), ADMIN_DIR)) return true;
|
|
35
24
|
if (anyEntry(join(common, 'refs', 'heads', 'morph-task'), AGENT_BRANCH_FILE)) return true;
|
|
@@ -49,10 +38,6 @@ function recentlyKicked(marker, now) {
|
|
|
49
38
|
}
|
|
50
39
|
}
|
|
51
40
|
|
|
52
|
-
/**
|
|
53
|
-
* @param {string} dir - diretório do projeto da sessão
|
|
54
|
-
* @returns {{ kicked: boolean, reason?: string, pid?: number }}
|
|
55
|
-
*/
|
|
56
41
|
export function kickAgentReap(dir, { spawnFn = spawn, env = process.env, now = Date.now } = {}) {
|
|
57
42
|
if (env.MORPH_WORKTREE_REAP === '0') return { kicked: false, reason: 'disabled' };
|
|
58
43
|
const common = resolveGitCommonDir(dir);
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Build/Typecheck verification utility.
|
|
3
|
-
*
|
|
4
|
-
* Stack-aware: determines the correct build command(s) based on project stack.
|
|
5
|
-
* Used by the Stop hook and `morph-spec verify` to verify code health.
|
|
6
|
-
*
|
|
7
|
-
* Pure functions — no I/O except the actual build execution.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
1
|
import { execSync } from 'child_process';
|
|
11
2
|
import { existsSync } from 'fs';
|
|
12
3
|
import { join } from 'path';
|
|
@@ -14,7 +5,7 @@ import { join } from 'path';
|
|
|
14
5
|
const DOTNET_BUILD_CMD = 'dotnet build --no-restore --nologo -v q';
|
|
15
6
|
const TSC_BUILD_CMD = 'npx tsc --noEmit';
|
|
16
7
|
|
|
17
|
-
function isDotnetStack(s) {
|
|
8
|
+
export function isDotnetStack(s) {
|
|
18
9
|
return s.includes('dotnet') || s.includes('aspnet');
|
|
19
10
|
}
|
|
20
11
|
|
|
@@ -22,25 +13,8 @@ function isFrontendStack(s) {
|
|
|
22
13
|
return s.includes('nextjs') || s.includes('node') || s.includes('react') || s.includes('vue');
|
|
23
14
|
}
|
|
24
15
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
*
|
|
28
|
-
* Kept for backward compatibility with single-stack projects and existing
|
|
29
|
-
* callers. A monorepo stack that mixes a frontend token with "dotnet" (e.g.
|
|
30
|
-
* the detector's own `${frontendStack}+dotnet` format, or a hyphenated
|
|
31
|
-
* `dotnet-nextjs`) should resolve through `getBuildSteps()` instead: this
|
|
32
|
-
* function alone can only ever return ONE command, and — because it checks
|
|
33
|
-
* `dotnet` before the frontend tokens — a compound stack always resolves to
|
|
34
|
-
* `dotnet build` even when the frontend is what actually needs checking (or
|
|
35
|
-
* when there's no root-level `.sln`/`.csproj`, e.g. in a monorepo where the
|
|
36
|
-
* backend lives in a subfolder). `getBuildSteps()` runs BOTH sides, each
|
|
37
|
-
* from its own working directory, instead of one command winning by
|
|
38
|
-
* substring match order.
|
|
39
|
-
*
|
|
40
|
-
* @param {string} stack - Project stack (e.g., 'dotnet-nextjs', 'nextjs')
|
|
41
|
-
* @param {boolean} [hasTsConfig] - Whether tsconfig.json exists (for TS projects)
|
|
42
|
-
* @returns {string|null} Build command or null if no check available
|
|
43
|
-
*/
|
|
16
|
+
// Single stack only: a compound stack resolves to `dotnet build` alone. Use
|
|
17
|
+
// `getBuildSteps()` for those.
|
|
44
18
|
export function getBuildCommand(stack, hasTsConfig = false) {
|
|
45
19
|
if (!stack) return null;
|
|
46
20
|
const s = stack.toLowerCase();
|
|
@@ -56,38 +30,21 @@ export function getBuildCommand(stack, hasTsConfig = false) {
|
|
|
56
30
|
return null;
|
|
57
31
|
}
|
|
58
32
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
* A monorepo stack mixes a frontend token and "dotnet" (see
|
|
63
|
-
* `detectProject()`'s own `${frontendStack}+dotnet` format) — in that case
|
|
64
|
-
* BOTH sides are checked, each from its own working directory
|
|
65
|
-
* (`frontendPath` / `backendPath`, as stored in
|
|
66
|
-
* `.morph/config/config.json → project`), instead of one command silently
|
|
67
|
-
* winning — or running from the wrong cwd — by substring match order.
|
|
68
|
-
* Single-stack projects (no `frontendPath`/`backendPath`, or a stack that
|
|
69
|
-
* only matches one side) resolve to exactly one step, from `cwd`, matching
|
|
70
|
-
* `getBuildCommand()`'s existing single-stack behavior.
|
|
71
|
-
*
|
|
72
|
-
* @param {string} stack
|
|
73
|
-
* @param {{ cwd: string, frontendPath?: string, backendPath?: string }} opts
|
|
74
|
-
* @returns {Array<{ command: string, cwd: string }>}
|
|
75
|
-
*/
|
|
76
|
-
export function getBuildSteps(stack, { cwd, frontendPath, backendPath } = {}) {
|
|
33
|
+
// A compound stack checks BOTH sides, each from its own directory.
|
|
34
|
+
// `backendTarget` names the solution when the backend directory holds none.
|
|
35
|
+
export function getBuildSteps(stack, { cwd, frontendPath, backendPath, backendTarget } = {}) {
|
|
77
36
|
if (!stack || !cwd) return [];
|
|
78
37
|
const s = stack.toLowerCase();
|
|
79
38
|
const steps = [];
|
|
80
39
|
|
|
81
40
|
if (isDotnetStack(s)) {
|
|
82
|
-
|
|
41
|
+
const command = backendTarget ? DOTNET_BUILD_CMD.replace(/^dotnet build/, `dotnet build "${backendTarget}"`) : DOTNET_BUILD_CMD;
|
|
42
|
+
steps.push({ command, cwd: backendPath ? join(cwd, backendPath) : cwd });
|
|
83
43
|
}
|
|
84
44
|
|
|
85
45
|
if (isFrontendStack(s)) {
|
|
86
46
|
const frontendDir = frontendPath ? join(cwd, frontendPath) : cwd;
|
|
87
|
-
// node_modules
|
|
88
|
-
// `morph-spec worktree link` has tsconfig.json (versioned) but no toolchain — running
|
|
89
|
-
// `npx tsc` there fetches an unrelated "tsc" package and fails noisily. No toolchain →
|
|
90
|
-
// no tsc step; the real typecheck runs wherever node_modules exists (root/CI).
|
|
47
|
+
// Without node_modules, `npx tsc` fetches an unrelated "tsc" package.
|
|
91
48
|
if (
|
|
92
49
|
existsSync(join(frontendDir, 'tsconfig.json')) &&
|
|
93
50
|
existsSync(join(frontendDir, 'node_modules'))
|
|
@@ -99,41 +56,19 @@ export function getBuildSteps(stack, { cwd, frontendPath, backendPath } = {}) {
|
|
|
99
56
|
return steps;
|
|
100
57
|
}
|
|
101
58
|
|
|
102
|
-
/**
|
|
103
|
-
* Parse build command result into a structured object.
|
|
104
|
-
*
|
|
105
|
-
* @param {number} exitCode - Process exit code (0 = success)
|
|
106
|
-
* @param {string} output - stdout+stderr from build
|
|
107
|
-
* @returns {{ status: 'pass'|'fail', output: string }}
|
|
108
|
-
*/
|
|
109
59
|
export function parseBuildResult(exitCode, output) {
|
|
110
60
|
const trimmed = (output || '').trim();
|
|
111
61
|
if (exitCode === 0) {
|
|
112
62
|
return { status: 'pass', output: '' };
|
|
113
63
|
}
|
|
114
|
-
// Truncate to last 20 lines
|
|
115
64
|
const lines = trimmed.split('\n');
|
|
116
65
|
const truncated = lines.slice(-20).join('\n');
|
|
117
66
|
return { status: 'fail', output: truncated };
|
|
118
67
|
}
|
|
119
68
|
|
|
120
|
-
/**
|
|
121
|
-
* Run build check for a project — monorepo-aware (see `getBuildSteps()`).
|
|
122
|
-
*
|
|
123
|
-
* For a compound stack (e.g. "nextjs+dotnet"), runs each resolved step in
|
|
124
|
-
* its own working directory and fails if any step fails; output from all
|
|
125
|
-
* failing steps is concatenated (prefixed by the command that produced it)
|
|
126
|
-
* so a frontend typecheck failure and a backend build failure are never
|
|
127
|
-
* silently masked by one another.
|
|
128
|
-
*
|
|
129
|
-
* @param {string} cwd - Project root directory
|
|
130
|
-
* @param {string} stack - Project stack string
|
|
131
|
-
* @param {number} [timeout=30000] - Timeout in ms
|
|
132
|
-
* @param {{ frontendPath?: string, backendPath?: string }} [paths] - Monorepo sub-paths, from config.json → project
|
|
133
|
-
* @returns {{ status: 'pass'|'fail'|'skip', output: string }}
|
|
134
|
-
*/
|
|
69
|
+
/** @returns {{ status: 'pass'|'fail'|'skip', output: string }} */
|
|
135
70
|
export function runBuildCheck(cwd, stack, timeout = 30000, paths = {}) {
|
|
136
|
-
const steps = getBuildSteps(stack, { cwd, frontendPath: paths.frontendPath, backendPath: paths.backendPath });
|
|
71
|
+
const steps = getBuildSteps(stack, { cwd, frontendPath: paths.frontendPath, backendPath: paths.backendPath, backendTarget: paths.backendTarget });
|
|
137
72
|
if (steps.length === 0) return { status: 'skip', output: '' };
|
|
138
73
|
|
|
139
74
|
const failures = [];
|