@polymorphism-tech/morph-spec 9.0.0 → 9.2.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 +8 -9
- package/README.md +38 -36
- package/bin/morph-spec.js +11 -63
- package/bin/validate.js +1 -39
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +2 -1
- package/framework/MORPH.md +3 -3
- package/framework/agents.json +19 -11
- package/framework/commands/morph-apply.md +1 -1
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +22 -31
- package/framework/evals/review-checklist.md +12 -2
- package/framework/hooks/README.md +8 -44
- 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 +2 -16
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +6 -50
- package/framework/hooks/claude-code/core/gate-guard.js +7 -67
- 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 +0 -4
- package/framework/hooks/claude-code/core/protect-spec-files.js +7 -67
- package/framework/hooks/claude-code/core/state-sync.js +12 -68
- package/framework/hooks/claude-code/core/telemetry-log.js +12 -93
- package/framework/hooks/claude-code/core/validate-completion.js +11 -57
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +1 -9
- package/framework/hooks/claude-code/core/worktree-context.js +10 -110
- 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.sh +11 -11
- 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 +6 -75
- package/framework/hooks/shared/claim-registry.js +9 -119
- package/framework/hooks/shared/command-tokens.js +6 -51
- package/framework/hooks/shared/comment-rules.js +5 -20
- package/framework/hooks/shared/comment-scanner.js +6 -25
- package/framework/hooks/shared/comment-stripper.js +11 -85
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/errata.js +2 -15
- package/framework/hooks/shared/feature-locator.js +11 -95
- package/framework/hooks/shared/frontend-locations.js +1 -3
- package/framework/hooks/shared/gate-question.js +1 -7
- 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 +1 -42
- 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 +4 -23
- package/framework/hooks/shared/source-files.js +4 -17
- package/framework/hooks/shared/state-reader.js +13 -183
- package/framework/hooks/shared/state-writer.js +8 -52
- 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/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/rules/nextjs-standards.md +1 -1
- 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 +4 -4
- package/framework/skills/core/morph-hotfix/SKILL.md +3 -3
- package/framework/skills/core/morph-machine/SKILL.md +1 -1
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +9 -7
- package/framework/skills/core/morph-review/SKILL.md +9 -6
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +29 -12
- package/framework/standards/STANDARDS.json +36 -14
- package/framework/standards/STANDARDS.meta.json +16 -2
- 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/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 +2 -2
- 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 +1 -1
- 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/testing.md +10 -4
- 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/frontend/nextjs/testing.md +10 -6
- package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
- package/framework/templates/.idea/morph-templates.xml +92 -0
- package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/framework/templates/CLAUDE.md.core.template +3 -3
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/REGISTRY.json +9 -9
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/docs/plan.md +3 -1
- package/framework/templates/docs/ui-mockups.md +4 -0
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- 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 +17 -104
- 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 +5 -57
- 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.js +48 -302
- package/src/commands/project/env.js +1 -27
- package/src/commands/project/fleet.js +3 -32
- package/src/commands/project/init.js +4 -68
- package/src/commands/project/install-plugin-cmd.js +0 -1
- package/src/commands/project/standards.js +1 -62
- package/src/commands/project/status.js +0 -53
- package/src/commands/project/update.js +31 -239
- package/src/commands/project/worktree.js +108 -882
- package/src/commands/scaffold/scaffold.js +0 -25
- package/src/commands/state/advance.js +14 -85
- package/src/commands/state/approve.js +15 -97
- package/src/commands/state/archive.js +12 -64
- package/src/commands/state/auto-approval.js +2 -5
- package/src/commands/state/create.js +22 -109
- package/src/commands/state/delete.js +11 -63
- package/src/commands/state/finish.js +98 -475
- package/src/commands/state/gate-check.js +22 -132
- package/src/commands/state/gate-decision.js +3 -39
- package/src/commands/state/handoff.js +3 -30
- package/src/commands/state/list.js +14 -84
- package/src/commands/state/report-check.js +8 -58
- package/src/commands/state/retype.js +4 -26
- package/src/commands/validation/index.js +0 -3
- package/src/commands/validation/mutate.js +11 -11
- package/src/commands/validation/plan-check.js +4 -2
- package/src/commands/validation/tests-audit.js +7 -0
- package/src/commands/validation/validate-feature.js +10 -97
- package/src/commands/validation/verify.js +30 -113
- package/src/core/paths/output-schema.js +2 -101
- package/src/core/state/feature-identity.js +1 -25
- package/src/core/state/gate-order.js +0 -27
- package/src/core/state/lifecycle-audit.js +8 -100
- package/src/core/state/state-manager.js +22 -304
- 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 +0 -1
- package/src/core/work-types.js +1 -36
- package/src/dashboard/app/App.jsx +1 -8
- package/src/dashboard/app/components/ActivityFeed.jsx +0 -5
- package/src/dashboard/app/components/CommandPalette.jsx +0 -7
- 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 +1 -5
- package/src/dashboard/app/lib/project.js +0 -12
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/AgentMap.jsx +1 -8
- package/src/dashboard/app/pages/FeatureDetail.jsx +1 -5
- package/src/dashboard/app/pages/Features.jsx +1 -8
- package/src/dashboard/app/pages/Guide.jsx +0 -1
- package/src/dashboard/app/pages/HookInspector.jsx +2 -23
- package/src/dashboard/app/pages/PhasePipeline.jsx +0 -3
- 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 +10 -69
- package/src/dashboard/scanner/agents-scanner.js +1 -23
- package/src/dashboard/scanner/docs-scanner.js +0 -6
- package/src/dashboard/scanner/feature-status.js +2 -5
- package/src/dashboard/scanner/features-scanner.js +10 -66
- 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 +2 -6
- package/src/dashboard/vite-plugin-morph-data.js +5 -19
- package/src/dashboard/vite.config.js +24 -24
- 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 +4 -19
- package/src/lib/ci/github-ci.js +9 -37
- package/src/lib/ci/workflow-scaffold.js +14 -10
- package/src/lib/detectors/claude-config-detector.js +13 -74
- package/src/lib/detectors/index.js +4 -50
- package/src/lib/e2e/applicability.js +37 -0
- 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 +9 -53
- package/src/lib/generators/scaffold-generator.js +5 -101
- package/src/lib/generators/settings-generator.js +4 -37
- 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 +1 -0
- 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 +3 -15
- 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 +1 -1
- package/src/lib/installers/effects.js +8 -93
- package/src/lib/installers/mcp-installer.js +9 -169
- package/src/lib/installers/memory-installer.js +4 -27
- package/src/lib/installers/templates-reconcile.js +5 -60
- package/src/lib/mutation/adapters.js +6 -17
- package/src/lib/mutation/dotnet-introspection.js +20 -106
- package/src/lib/mutation/runner.js +23 -75
- package/src/lib/mutation/subset.js +1 -4
- package/src/lib/mutation/trx.js +0 -1
- package/src/lib/plan/artifact-vocabulary.js +5 -25
- package/src/lib/plan/checks.js +34 -33
- package/src/lib/plan/plan-parser.js +7 -8
- package/src/lib/projects/registry.js +7 -44
- package/src/lib/review-reports.js +11 -26
- package/src/lib/risk-detector.js +41 -173
- package/src/lib/specs/adr-index.js +6 -34
- package/src/lib/specs/frontmatter.js +2 -15
- package/src/lib/specs/gfm-table.js +1 -7
- package/src/lib/specs/revocations.js +1 -14
- package/src/lib/specs/screen-checklist.js +47 -124
- package/src/lib/stack/dotnet-build-ignores.js +2 -6
- package/src/lib/stack/stack-profile.js +1 -28
- package/src/lib/stack/stack-scan.js +1 -8
- package/src/lib/stack-filter.js +4 -27
- package/src/lib/standards/registry.js +1 -7
- package/src/lib/standards/sections.js +2 -13
- package/src/lib/standards/verified-against.js +19 -191
- package/src/lib/tasks/eval-node.js +21 -169
- package/src/lib/tasks/linux-runner.js +18 -142
- package/src/lib/tasks/test-nodes.js +43 -240
- package/src/lib/tasks/test-runner.js +43 -341
- package/src/lib/telemetry/metrics.js +7 -36
- package/src/lib/telemetry/telemetry-reader.js +13 -125
- package/src/lib/telemetry/verify-event.js +2 -30
- package/src/lib/tests-audit/audit.js +96 -0
- 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 +2 -24
- 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 +11 -101
- package/src/lib/validators/reuse/symbol-scanner.js +6 -62
- package/src/lib/validators/traceability-check.js +5 -63
- package/src/lib/validators/validation-runner.js +21 -158
- package/src/lib/validators/verify-runner.js +85 -376
- package/src/lib/work-type-detector.js +5 -67
- package/src/lib/worktree/docker-audit.js +17 -136
- 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 +13 -95
- 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 -147
- package/src/utils/agents-installer.js +29 -276
- package/src/utils/banner.js +4 -25
- package/src/utils/claude-md-injector.js +1 -37
- package/src/utils/claude-settings-manager.js +1 -52
- package/src/utils/file-copier.js +11 -95
- package/src/utils/hooks-installer.js +21 -298
- package/src/utils/skills-installer.js +4 -101
- package/src/utils/version-checker.js +4 -83
- package/framework/commands/morph-proposal.md +0 -9
- package/framework/hooks/git/commit-msg/conventional-commits.sh +0 -33
- package/framework/hooks/git/pre-commit/agents.sh +0 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +0 -64
- package/framework/hooks/git/pre-push/run-tests.sh +0 -44
- package/src/commands/utils/index.js +0 -3
- package/src/lib/detectors/design-system-detector.js +0 -188
- package/src/lib/generators/design-system-generator.js +0 -508
- package/src/lib/specs/old-format.js +0 -68
- package/src/utils/agent-id-mapper.js +0 -11
|
@@ -9,6 +9,6 @@ allowed-tools: Read, Write, Edit, Bash, Glob, Grep, Agent, AskUserQuestion, Skil
|
|
|
9
9
|
Atalho: invoque `Skill(morph-apply)` com o nome da feature.
|
|
10
10
|
|
|
11
11
|
Pré-requisito: o gate do plano aprovado, com `.morph/features/{feature}/2-plan/plan.md` e
|
|
12
|
-
`2-plan/checks.json` (gravado por `morph-spec plan-check`). Faltando, rode `/morph-
|
|
12
|
+
`2-plan/checks.json` (gravado por `morph-spec plan-check`). Faltando, rode `/morph-plan {feature}`.
|
|
13
13
|
|
|
14
14
|
**Feature:** $ARGUMENTS
|
|
@@ -34,7 +34,7 @@ output completion (X/N outputs), slice progress (reports in `3-implement/reports
|
|
|
34
34
|
After running the CLI, summarize:
|
|
35
35
|
- Current phase and which gate (if any) is open
|
|
36
36
|
- Outstanding outputs for the current phase
|
|
37
|
-
- Recommended next slash command (`/morph-
|
|
37
|
+
- Recommended next slash command (`/morph-plan`, `/morph-apply`, or a gate approval)
|
|
38
38
|
|
|
39
39
|
---
|
|
40
40
|
|
package/framework/docs/CLI.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
**Exceção `hotfix`:** esse tipo nunca despacha (`morph-hotfix` §7); o Gate 3 exige só `4-review/verifier.md`, escrito pelo orquestrador, e a assinatura humana continua obrigatória via `--approver` (o backstop de `--mode auto` acima permanece recusando).
|
|
18
18
|
|
|
19
|
-
**No `uiux`, a assinatura `auto` é recusada (exit 1) onde o `gate-check uiux` pausaria pelo checklist de tela** — o mesmo predicado do `advance` (`uiuxChecklistPause`, em `risk-detector.js`), lido mesmo sem gate-check antes. Destravam: uma decisão gravada com `pause: false` (`gate-decision {f} uiux --pause false --reason …`, ou um gate-check que não pausou) e a assinatura humana (`--approver`); `--force` não. A decisão vale para o mockup que viu: ela grava `mockupsHash` (o `mockups.md` por conteúdo), e um mockup escrito ou mudado depois dela volta a ser conferido.
|
|
19
|
+
**No `uiux`, a assinatura `auto` é recusada (exit 1) onde o `gate-check uiux` pausaria pelo checklist de tela ou pelo protótipo** — o mesmo predicado do `advance` (`uiuxChecklistPause`, em `risk-detector.js`), lido mesmo sem gate-check antes. Destravam: uma decisão gravada com `pause: false` (`gate-decision {f} uiux --pause false --reason …`, ou um gate-check que não pausou) e a assinatura humana (`--approver`); `--force` não. A decisão vale para o mockup que viu: ela grava `mockupsHash` (o `mockups.md` por conteúdo), e um mockup escrito ou mudado depois dela volta a ser conferido.
|
|
20
20
|
|
|
21
21
|
**O hook state-sync.js** cobre o caminho manual: `AskUserQuestion` respondida com frase de aprovação (abaixo) → registra o gate no `feature.json` autoritativo com `approvedBy: "manual"`. **A feature é identificada pelo nome que a pergunta cita** — grava na árvore dona dela, nunca na "feature ativa" da sessão. Pergunta sem nome numa árvore com mais de uma feature: o hook **recusa gravar** e avisa — aprove por `morph-spec approve`.
|
|
22
22
|
|
|
@@ -38,11 +38,11 @@ No **review**, o `gate-check` lê os relatórios de `4-review/` (`src/lib/review
|
|
|
38
38
|
|
|
39
39
|
Um stamp de `verification` com status `fail` (último `morph-spec verify` vermelho) **força pausa** em qualquer gate. Um stamp `pass` mas **stale** (o código mudou depois do commit do stamp — só conta o que está fora de `.morph/`; o `config.json` conta exceto a troca de `expectedCount`, e um commit só de estado, como o do próprio `feature.json` do verify, não invalida) força pausa **só no ship** (`review`); em `plan`/`uiux` a staleness é computada e reportada em `signals`, mas não pausa nada — é regra de ship, não de gate genérico. Stamp **ausente** nunca pausa por isso (features legadas sem stamp).
|
|
40
40
|
|
|
41
|
-
**Dispensa de e2e:** o stamp carrega `nodes.e2e` e a razão literal `e2eReason`, porque um nó e2e que **pula** não reprova o rollup — sem eles o gate lê `pass` tanto quando o e2e rodou e passou quanto quando ele nunca rodou. No **ship** (`review`), um `skip` é classificado pela razão (`classifyE2eSkip`, em `risk-detector.js` — fonte única) e o veredito fica registrado em `gateDecisions.review.signals` (`e2eStatus`, `e2eReason`, `e2eSkipClass`): `project` ("project does not containerize")
|
|
41
|
+
**Dispensa de e2e:** o stamp carrega `nodes.e2e` e a razão literal `e2eReason`, porque um nó e2e que **pula** não reprova o rollup — sem eles o gate lê `pass` tanto quando o e2e rodou e passou quanto quando ele nunca rodou. No **ship** (`review`), um `skip` é classificado pela razão (`classifyE2eSkip`, em `risk-detector.js` — fonte única) e o veredito fica registrado em `gateDecisions.review.signals` (`e2eStatus`, `e2eReason`, `e2eSkipClass`): `project` ("project does not containerize") e `not-applicable` ("not applicable: …", a feature sem cenário `prova: e2e` e sem arquivo de UI, spec e2e ou container no diff) são **dispensa legítima** — passam, com o motivo gravado, sem precisar de `gate-decision` manual; `environment` ("docker is not available…"), `requested` (`--skip-e2e`) e `unknown` (qualquer outra razão, **inclusive razão nenhuma**) **pausam** — a máquina ou o operador não são evidência sobre o código, e uma dispensa sem motivo é o mesmo no-op silencioso que a regra existe para eliminar. `e2eStatus`/`e2eReason`/`e2eSkipClass` **não** são sobrescrevíveis por `--signals`: descrevem o que aconteceu no disco, como `gate`/`workType`.
|
|
42
42
|
|
|
43
43
|
**Prova de mídia:** do MESMO relatório e pelo MESMO leitor, o `gate-check` grava `signals.mediaClaims = {section, featureGeneratesMedia}`. `featureGeneratesMedia` é determinístico do disco — existe ao menos uma fatia do `checks.json` cujo `standards` cita `ai-agents-modalities-image-gen`, `ai-agents-media-video` ou `ai-agents-media-pipeline` — pelo id de STANDARDS.json ou pelo caminho, com ou sem `framework/`/`standards/` na frente, porque uma grafia não prevista desarmaria o guardrail sem uma linha de erro (`featureGeneratesMedia`, função exportada de `gate-check.js`, é onde essa normalização mora e é testada). Nessa condição, relatório **sem** a seção `## Mídia gerada` **pausa o Gate 3**: as quatro afirmações de mídia (custo, revisão humana, marcação, hot path não bloqueado) não foram provadas (`framework/evals/review-checklist.md`, "Feature que gera mídia").
|
|
44
44
|
|
|
45
|
-
**Checklist de tela:** na `uiux` e no `review`, o `gate-check` grava `signals.screenChecklist` lido do `1-design/mockups.md` (`mockups`, `legacy`, `screens`, `problems`). Na **uiux**, checklist incompleto **pausa** nomeando a tela e o item, e um mockup sem nenhuma `## Tela:` é o problema "nenhuma tela declarada". No **review**, o que se cobra é o item **devido** — decidido, nem "não se aplica" nem a superfície (`uncovered`): cada um exige uma linha na seção `## Checklist de tela` do `verifier.md`, e a linha que o verificador marca como não verificável conta. Com item devido, relatório sem a seção ou com linha faltando (`section`, `uncovered`) **pausa** o Gate 3 (`evals/review-checklist.md`, "Contrato de tela"); tela toda "não se aplica" não deve nada e não pausa. **Mockup legado** — telas no formato antigo (`## S001: …`) e nenhuma `## Tela:`, o que o template anterior gerava — é **isento nas duas fases**, com a isenção à vista: o sinal grava `exempt: "checklist dispensado: mockup no formato antigo"` e a mesma razão aparece em "risk signals present". Mockup ausente não pausa. `screenChecklist` **não** é sobrescrevível por `--signals`.
|
|
45
|
+
**Checklist de tela:** na `uiux` e no `review`, o `gate-check` grava `signals.screenChecklist` lido do `1-design/mockups.md` (`mockups`, `legacy`, `screens`, `problems`). Na **uiux**, checklist incompleto **pausa** nomeando a tela e o item, e um mockup sem nenhuma `## Tela:` é o problema "nenhuma tela declarada". Com o checklist completo, cada tela ainda deve o **protótipo** (`prototypes`, só na uiux): a linha `**Protótipo:** 1-design/prototipos/<tela>.html` dentro da `## Tela:`, o HTML e os prints `<tela>-1280.png` e `<tela>-390.png` ao lado dele; faltando um, **pausa** nomeando a tela e o arquivo ("protótipo incompleto"), pelo mesmo predicado. No **review**, o que se cobra é o item **devido** — decidido, nem "não se aplica" nem a superfície (`uncovered`): cada um exige uma linha na seção `## Checklist de tela` do `verifier.md`, e a linha que o verificador marca como não verificável conta. Com item devido, relatório sem a seção ou com linha faltando (`section`, `uncovered`) **pausa** o Gate 3 (`evals/review-checklist.md`, "Contrato de tela"); tela toda "não se aplica" não deve nada e não pausa. **Mockup legado** — telas no formato antigo (`## S001: …`) e nenhuma `## Tela:`, o que o template anterior gerava — é **isento nas duas fases**, com a isenção à vista: o sinal grava `exempt: "checklist dispensado: mockup no formato antigo"` e a mesma razão aparece em "risk signals present". Mockup ausente não pausa. `screenChecklist` **não** é sobrescrevível por `--signals`.
|
|
46
46
|
|
|
47
47
|
**Por quê.** Fora do review a regra não vale (pausar o Gate 1/2 numa máquina sem docker travaria planejamento legítimo). Feature que não pina standard de mídia é **isenta automaticamente** — a regra não pede cerimônia de quem não gera mídia. `mediaClaims` **não** é sobrescrevível por `--signals`, pela mesma razão anti-spoof dos demais sinais de disco.
|
|
48
48
|
|
|
@@ -58,10 +58,10 @@ A tabela é a mesma régua que `shouldPauseGate` aplica e que o `gate-check` imp
|
|
|
58
58
|
| `verificationStatus` | === 'fail' | `verify` vermelho — build, testes, validadores **ou e2e**. Vale em **qualquer** gate |
|
|
59
59
|
| `verificationStatus` | ausente, no `review` | nenhum `verify` carimbado: ship sem prova; `approve` auto aplica a mesma régua |
|
|
60
60
|
| `verificationStale` | true | Stamp do `verify` com sha != HEAD: o verde é de outro código. Só no **ship** |
|
|
61
|
-
| `e2eSkipClass` (derivada) | environment/requested/unknown | e2e pulou sem dispensa legítima (
|
|
61
|
+
| `e2eSkipClass` (derivada) | environment/requested/unknown | e2e pulou sem dispensa legítima (`project` e `not-applicable` passam). Só no **ship** |
|
|
62
62
|
| `promptModel` | prompt/modelo tocado | Sem nó `evals` declarado, ou eval pulado neste verify. Só no **ship** |
|
|
63
63
|
| `mediaClaims` | seção ausente | Feature que gera mídia, relatório sem `## Mídia gerada`. Só no **ship** |
|
|
64
|
-
| `screenChecklist` | item em aberto | **uiux**: tela sem checklist completo,
|
|
64
|
+
| `screenChecklist` | item em aberto | **uiux**: tela sem checklist completo, nenhuma `## Tela:`, ou tela sem protótipo e prints 1280/390. **ship**: item decidido sem linha em `## Checklist de tela` |
|
|
65
65
|
| `planChecks` | ≠ `fresh` | `checks.json` ausente ou de outra versão do `plan.md`: rode `plan-check`. Só no **plano**; `approve`/`advance` automáticos recusam |
|
|
66
66
|
| `sliceCount` | > 12 | Plano grande demais para uma feature |
|
|
67
67
|
| `workType` | === 'hotfix' | Gate 3 pausa SEMPRE (ver "Gates por tipo de trabalho") |
|
|
@@ -268,7 +268,7 @@ Fail-open: uma poda que falha nunca derruba o comando. PROVE-or-DECLINE como o r
|
|
|
268
268
|
|
|
269
269
|
### claim
|
|
270
270
|
|
|
271
|
-
**`morph-spec claim [feature] [--release] [--list] [--all] [--json]`** — posse **advisory** de feature. Cada sessão reclama sua feature ativa automaticamente (hook SessionStart) e renova o heartbeat a cada tool call; o registro vive em `<git-common-dir>/morph-spec/claims.json`, compartilhado por todos os worktrees. **`morph-spec claim <feature>` (sem flags) ADQUIRE** — se esta árvore já tinha claim vivo em outra feature, ele é liberado no mesmo passo (o CLI reporta "movido de X para Y"); é o jeito de trocar de feature no meio da tarefa sem a dança de dois passos `--release` + reclamar. `--list` força a leitura sem adquirir mesmo passando uma feature. **Claims avisam, nunca bloqueiam** — mas se o SessionStart avisar que outra sessão tem claim vivo, **confirme antes de `finish`/`archive`/`delete`**: duas sessões na mesma feature já custaram
|
|
271
|
+
**`morph-spec claim [feature] [--release] [--list] [--all] [--json]`** — posse **advisory** de feature. Cada sessão reclama sua feature ativa automaticamente (hook SessionStart) e renova o heartbeat a cada tool call; o registro vive em `<git-common-dir>/morph-spec/claims.json`, compartilhado por todos os worktrees. **`morph-spec claim <feature>` (sem flags) ADQUIRE** — se esta árvore já tinha claim vivo em outra feature, ele é liberado no mesmo passo (o CLI reporta "movido de X para Y"); é o jeito de trocar de feature no meio da tarefa sem a dança de dois passos `--release` + reclamar. `--list` força a leitura sem adquirir mesmo passando uma feature. **Claims avisam, nunca bloqueiam** — mas se o SessionStart avisar que outra sessão tem claim vivo, **confirme antes de `finish`/`archive`/`delete`**: duas sessões na mesma feature já custaram o trabalho de uma fatia e um sub-agent (o worktree sumiu debaixo dele). Claim órfão de sessão morta: `morph-spec claim <f> --release`. **O board mostra só claims VIVOS** (dentro do TTL de 30 min): um claim morto não avisa mais nada, e deixá-lo listado transformava o quadro em ruído (features encerradas há 20h e 73h ainda aparecendo). `--all` mostra o registro inteiro; a contagem dos omitidos sai no rodapé, nunca em silêncio.
|
|
272
272
|
|
|
273
273
|
## Validação
|
|
274
274
|
|
|
@@ -282,6 +282,8 @@ O nó `e2e` é o único que **executa** a aplicação: sobe o `docker compose` d
|
|
|
282
282
|
|
|
283
283
|
As specs são procuradas na raiz, em `project.frontendPath` e, sem ele, nas pastas de frontend conhecidas (as mesmas do detector, mais `web`). Se o nó cai para smoke enquanto existe um `playwright.config.*` não executado em pasta de primeiro nível, o `verify` avisa nomeando a pasta e `project.frontendPath`. A contagem de specs sai em `nodes.e2e.count` do `verify --json` (`null` quando não rodou ou o sumário é ilegível, nunca 0); ela não é persistida no stamp.
|
|
284
284
|
|
|
285
|
+
**O e2e só sobe quando pode provar algo:** com testes vermelhos ele não roda (`tests failed — the e2e does not run over a red suite`); sem nó de teste `e2e: true` (esses são a prova do backend contra a stack e sempre a sobem), ele também não roda quando nenhum cenário do `checks.json` é `prova: e2e` e o diff da feature contra a branch default (fora de `.morph/`) não toca UI (`.tsx`, `.css`, `.html`…), spec e2e, `playwright.config.*`, Dockerfile/compose nem pasta de frontend conhecida — `skip` com a razão `not applicable: …`, que o gate aceita. Sem git, sem base ou diff vazio: roda (só para diante de evidência).
|
|
286
|
+
|
|
285
287
|
**Dispensa legítima × setup faltando:** projeto sem compose e sem a integração `docker` é `skip` com razão; projeto que **declara** `docker` e não tem compose é `fail` — é verificação que nunca foi montada, não projeto que não containeriza.
|
|
286
288
|
|
|
287
289
|
**Descoberta de testes e relógio:** a varredura por `*Tests.csproj` **não enxerga worktrees** — nem os convencionais (`worktrees/`, `.worktrees/`) nem os registrados em `git worktree list` fora da convenção. O timeout do nó `tests` deixou de ser fixo: `--timeout` (que agora cobre build **e** tests) > `project.testTimeoutMs` no `config.json` > **120 s por projeto de teste encadeado** — um encadeamento nunca nasce condenado pelo tamanho. Quando o relógio estoura, o nó diz isso com nome (`timeout após N ms na execução X de Y — nenhum teste falhou`) em vez de devolver log truncado.
|
|
@@ -316,7 +318,7 @@ Nenhuma regra de gate muda: e2e pulado, escopo, evals e stale valem igual.
|
|
|
316
318
|
|
|
317
319
|
**Sem workflow** que rode `morph-spec verify` publicando `morph-verify.json`, cria e commita `.github/workflows/morph-verify.yml` na branch da feature (gatilho `push` em `morph/**`, `.morph/` fora do gatilho exceto o `config.json`, `cancel-in-progress`, setup de .NET por `global.json`/stack, install por lockfile, browsers do Playwright onde há config, e o `morph-spec` na versão de `.morph/.morphversion`). Um run sobre um commit anterior vale quando só `.morph/` andou desde ele; run `cancelled`/`skipped`/`startup_failure` não é prova.
|
|
318
320
|
|
|
319
|
-
**Sem CI cai para o verify local, com a razão** (`project.ci: false`, origin fora do GitHub, `gh` ausente ou sem login, branch que não é `morph/<feature>`, token HTTPS sem o escopo `workflow` para criar o arquivo, push recusado, workflow que não disparou nem com `gh workflow run`, run sem o artefato). **Antes de
|
|
321
|
+
**Sem CI cai para o verify local, com a razão** (`project.ci: false`, origin fora do GitHub, `gh` ausente ou sem login, branch que não é `morph/<feature>`, token HTTPS sem o escopo `workflow` para criar o arquivo, push recusado, workflow que não disparou nem com `gh workflow run`, run sem o artefato). **Antes de tudo — com ou sem CI — reaproveita o stamp fresco:** último stamp `pass` com `sha`, nada fora de commit e `treeMovedSince(stamp.sha, HEAD)` falso (a mesma régua do `verificationStale`: só `.morph/` andou, ou o `config.json` mudou só `expectedCount`) → não empurra, não roda nem regrava, imprime "stamp verde e fresco — o CI não roda de novo" e o `--json` traz `reason: "stamp-reused"`, `status`, `nodes` e `reused: { sha, ts, source }`, exit 0.
|
|
320
322
|
|
|
321
323
|
Código não commitado é **erro de uso (exit 2)**: o CI só vê commits. CI rodando depois de 50 min é **exit 3** (`status: "pending"`), sem stamp — não é vermelho. Com task, `--filter`, `--skip-*`, `--fresh-e2e`, `--linux`, `--record-baseline`, `--refresh-cache` ou `--no-persist`, erro de uso. Vermelho imprime `gh run view <id> --log-failed`.
|
|
322
324
|
|
|
@@ -328,25 +330,25 @@ Código não commitado é **erro de uso (exit 2)**: o CI só vê commits. CI rod
|
|
|
328
330
|
|
|
329
331
|
**Validador `code-budget` (nó `validators` do `verify`)** — mede o que a árvore **adicionou** contra a branch default: `git diff -U0` a partir do merge-base com `origin/{default}` (ou `{default}` local), arquivo novo não rastreado conta inteiro e arquivo movido sem edição não conta. Lê `.cs`/`.ts`/`.tsx`/`.js`/`.jsx`/`.mjs`/`.cjs`, fora de `.d.ts`, de `node_modules`/`bin`/`obj`/`dist`/`.morph` e de código gerado (`*.Designer.cs`, `*.g.cs`, `*ModelSnapshot.cs`).
|
|
330
332
|
|
|
331
|
-
Três medidas, cada uma **um aviso, nunca erro**: (a) **linhas de comentário adicionadas que citam processo** — `UC-nn`, `ADR-nn`, `FR-nn`, `C{n}`/`F{n}` (cenário/fatia — fora tecla, métrica, célula e o modelo C4), `Gate N`, `plan.md`, `checks.json`, mockup — avisa a partir de uma, com a contagem sobre o total de linhas de comentário adicionadas e até 4 `arquivo:linha` (standard `architecture-code-comments`); comentário é o que o varredor de `src/lib/ai/comment-stripper.js` separa de string, com template e regex literal de TS/JS; (b) **linhas não vazias de teste ÷ de produção** — avisa acima de **1
|
|
333
|
+
Três medidas, cada uma **um aviso, nunca erro**: (a) **linhas de comentário adicionadas que citam processo** — `UC-nn`, `ADR-nn`, `FR-nn`, `C{n}`/`F{n}` (cenário/fatia — fora tecla, métrica, célula e o modelo C4), `Gate N`, `plan.md`, `checks.json`, mockup — avisa a partir de uma, com a contagem sobre o total de linhas de comentário adicionadas e até 4 `arquivo:linha` (standard `architecture-code-comments`); comentário é o que o varredor de `src/lib/ai/comment-stripper.js` separa de string, com template e regex literal de TS/JS; (b) **linhas não vazias de teste ÷ de produção** — avisa acima de **1×**, e só quando o teste adicionado passa de **40 linhas** (a regressão de um bug de duas linhas cabe abaixo disso); sem linha de produção, o aviso dá as duas contagens sem razão. (c) **densidade de comentário** — linhas de comentário adicionadas ÷ linhas de código adicionadas (as não vazias menos as de comentário), doc-comment (`///`, JSDoc) incluído — avisa acima de **1 a cada 10**, e só a partir de **40 linhas** de código, com até 4 arquivos mais comentados e suas contagens (o padrão do standard `architecture-code-comments` é nenhum comentário).
|
|
332
334
|
|
|
333
335
|
Teste é o que o validador `reuse` já trata como teste: arquivo `.test.` ou `.spec.` com extensão `js`/`jsx`/`ts`/`tsx`, e tudo sob `__tests__/`, `test/`, `tests/` ou pasta terminada em `Test`/`Tests` (`Foo.Tests/`, `FooTests/`). O resto conta como produção, inclusive `a.test.mjs`, `a.test.cjs` e `*.e2e.ts` fora dessas pastas.
|
|
334
336
|
|
|
335
|
-
Sem base git, ou sem linha de código adicionada, pula com razão; qualquer falha de medição também vira skip, nunca erro.
|
|
337
|
+
Sem base git, ou sem linha de código adicionada, pula com razão; qualquer falha de medição também vira skip, nunca erro.
|
|
336
338
|
|
|
337
339
|
**Por quê.** Excesso justificado não silencia o aviso: a justificativa mora na task (orçamento do `morph-plan` passo 5), e é lá que o avaliador a lê.
|
|
338
340
|
|
|
339
341
|
### mutate
|
|
340
342
|
|
|
341
|
-
**`morph-spec mutate [feature] [--plan <arquivo>] [--out <arquivo>] [--
|
|
343
|
+
**`morph-spec mutate [feature] [--plan <arquivo>] [--out <arquivo>] [--allow-dirty] [--jobs <n>] [--timeout <ms>] [--restore] [--json]`** — mutação **dirigida** para o verificador do Gate 3: no máximo **3** mutantes, só quando o diff tem uma guarda que um cenário exige e o verificador não achou o teste que a morde.
|
|
342
344
|
|
|
343
345
|
**Manifesto** (`--plan`, default `4-review/mutants.json`): `{"mutants":[{"id":"M1","file":"src/x.cs","line":42,"find":"a && b","replace":"a","expect":"OrdersTests.RecusaSemItens deve falhar"}]}` — `expect` é obrigatório (qual teste deve falhar e por quê, dito antes de rodar), `find` começa na `line` e é único nela; barra invertida do código vai como `{{BS}}` (o transporte do Bash a reduz). Mais de 3 mutantes ou `expect` ausente: exit 2.
|
|
344
346
|
|
|
345
347
|
**O que compila (.NET):** o `build` do nó quando declarado; senão o que o comando nomeia (`.sln`/`.slnx`/`.csproj` — a solution vale pelos projetos que lista, também para o subconjunto); senão a solution única (ou o projeto único) do diretório alvo, como o próprio `dotnet test` escolhe. Só sem nada descobrível recusa (exit 2), antes de mutar, pedindo a solution no comando ou o `build`.
|
|
346
348
|
|
|
347
|
-
**Método:** recusa arquivo não commitado (`--allow-dirty` assume o risco); snapshot dos bytes fora da árvore (`<git-common-dir>/morph-spec/mutation/<run>/`) com journal — rodada que morreu se desfaz por `mutate --restore`; em .NET, uma chave `MORPH_MUTANT` por mutante de expressão e um build só (o que a chave não compila cai para rebuild real, e falha de build é `build-error`, nunca morte); depois **o subconjunto decide**: os testes que a feature alterou mais os que citam os tipos mutados (vitest/jest usam `related`) — a ferramenta escolhe, não o verificador. `killed` = falha **nova** contra a rodada-base do mesmo escopo, com o motivo literal do TRX; `survived` no subconjunto é o veredito, com o `expect` ao lado;
|
|
349
|
+
**Método:** recusa arquivo não commitado (`--allow-dirty` assume o risco); snapshot dos bytes fora da árvore (`<git-common-dir>/morph-spec/mutation/<run>/`) com journal — rodada que morreu se desfaz por `mutate --restore`; em .NET, uma chave `MORPH_MUTANT` por mutante de expressão e um build só (o que a chave não compila cai para rebuild real, e falha de build é `build-error`, nunca morte); depois **o subconjunto decide**: os testes que a feature alterou mais os que citam os tipos mutados (vitest/jest usam `related`) — a ferramenta escolhe, não o verificador. `killed` = falha **nova** contra a rodada-base do mesmo escopo, com o motivo literal do TRX; `survived` no subconjunto é o veredito, com o `expect` ao lado; rodada que cai é `inconclusive`. **Nunca a suíte inteira:** sem subconjunto utilizável (nenhum teste da feature nem citando os tipos mutados) o comando recusa antes de compilar (exit 2) e manda provar o cenário por leitura; subconjunto que some em runtime (base que não seleciona teste, ou cai) deixa os mutantes `inconclusive` com a mesma razão.
|
|
348
350
|
|
|
349
|
-
**Nó `e2e: true`:** arquivo coberto **só** por nó de teste `e2e: true` é recusado (exit 2, nomeando mutante, arquivo e nó): a suíte testa o container do compose e o `mutate` recompila só localmente — todo mutante sobreviveria em falso. Com outro nó dono do arquivo, ele é o escolhido — mas, havendo qualquer nó `e2e: true` no config, só se algum arquivo de teste dele (com atributo de teste) citar um tipo declarado no arquivo mutado: o dono de um `.cs` é a suíte .NET, não a referência de projeto, e a unidade que só referencia a API daria sobrevivente em falso. Sem citação, recusa (exit 2) antes do snapshot. Nas duas recusas a saída prescreve o protocolo manual com o comando pronto do nó e2e, cada passo numa linha só encadeado por `&&` (nunca `;`): base verde antes do 1º mutante (`eval "$(morph-spec env <f>)" && morph-spec e2e up <f> && morph-spec heavy run -- <comando do nó>`), a mesma cadeia depois de cada edição, e restauração conferida por `git diff --quiet` seguida de um novo `e2e up` — registrado em `4-review`. Sem nó `e2e: true`,
|
|
351
|
+
**Nó `e2e: true`:** arquivo coberto **só** por nó de teste `e2e: true` é recusado (exit 2, nomeando mutante, arquivo e nó): a suíte testa o container do compose e o `mutate` recompila só localmente — todo mutante sobreviveria em falso. Com outro nó dono do arquivo, ele é o escolhido — mas, havendo qualquer nó `e2e: true` no config, só se algum arquivo de teste dele (com atributo de teste) citar um tipo declarado no arquivo mutado: o dono de um `.cs` é a suíte .NET, não a referência de projeto, e a unidade que só referencia a API daria sobrevivente em falso. Sem citação, recusa (exit 2) antes do snapshot. Nas duas recusas a saída prescreve o protocolo manual com o comando pronto do nó e2e, cada passo numa linha só encadeado por `&&` (nunca `;`): base verde antes do 1º mutante (`eval "$(morph-spec env <f>)" && morph-spec e2e up <f> && morph-spec heavy run -- <comando do nó>`), a mesma cadeia depois de cada edição, e restauração conferida por `git diff --quiet` seguida de um novo `e2e up` — registrado em `4-review`. Sem nó `e2e: true`, vale a regra do subconjunto acima.
|
|
350
352
|
|
|
351
353
|
**Sem `<feature>`:** vale a única feature em andamento; sem ela (nenhuma, ou mais de uma), exige `--plan`, e a seção só é gravada com `--out` — sai no stdout, `section: null` no `--json`.
|
|
352
354
|
|
|
@@ -496,6 +498,10 @@ Sem fatia na linha de comando, a saída traz `reviews: { verifier, reviewer, sec
|
|
|
496
498
|
|
|
497
499
|
**`morph-spec stack-scan [<feature>] [--json]`** — varredura determinística do código contra `config.project.stack`: só os stacks configurados são julgados (sem `project.stack`, nenhuma regra roda e o resultado é `não auditado: project.stack ausente em .morph/config/config.json`, nunca "Em compliance" — exit 0; stack sem regras de varredura sai `não auditado` pelo mesmo motivo; código de um stack não configurado é só anotado). `dotnet`: TargetFramework abaixo de net10 e ORM/banco diferente (Dapper puro, SQL Server, MySQL) são **Bloqueante**; `Guid.NewGuid()`, repository genérico (`IRepository<T>`/`IUnitOfWork` — código morto: apagar a pasta `Repository/` e as 2 linhas de DI), classe `*Service`/`*Manager` sobre `DbContext` e SQL cru (`FromSqlRaw`, `ExecuteSqlRaw`, `SqlQueryRaw`, `ExecuteQuery`) são **Alto**; classe não-sealed é **Médio**; pacote `Microsoft.EntityFrameworkCore|AspNetCore|Extensions` abaixo da major 10 é **Baixo**. `nextjs`: frontend sem Next.js é Bloqueante; Pages Router sem `app/` e biblioteca de UI diferente (MUI, antd, Chakra, Bootstrap, Mantine) são Médio. Código de teste não conta; `node_modules`, `bin`, `obj`, `.morph`, `.claude` e os contêineres de worktree ficam fora; arquivo ilegível é anotado, nunca derruba. Sem código fonte = GREENFIELD. As contagens são de texto e superestimam consumidores — o relatório aponta `graph affected <símbolo> --uses-only`. Desvio Bloqueante/Alto vira nó da árvore de decisões da `morph-plan`. Com `<feature>`, grava `.morph/features/<f>/2-plan/stack-scan.md` (exit 2 se a feature não existe, 1 fora da árvore dona); sem, imprime o markdown.
|
|
498
500
|
|
|
501
|
+
### tests-audit
|
|
502
|
+
|
|
503
|
+
**`morph-spec tests-audit [--json]`** — auditoria **somente leitura** dos arquivos de teste rastreados (`git ls-files` + `isTestPath`): total de casos (`[Fact]`/`[Theory]`, `it`/`test`), os maiores arquivos e quatro listas de **candidatos** — nunca vereditos, nada é apagado: teste que lê `.md`/`.txt` ou tem 3+ asserts de frase literal (fixa texto, não comportamento); constante comparada ao próprio literal; família de 4+ `[Fact]` com o mesmo prefixo de nome (candidata a `[Theory]`); o mesmo assunto em mais de uma camada (`XHandlerTests` e `XEndpointTests`/`XIntegrationTests` — fique com a mais barata que morde). Fora de git: exit 2 com a razão. É o ponto de partida de uma limpeza: apagar teste e depois rodar `verify --record-baseline` grava a contagem menor.
|
|
504
|
+
|
|
499
505
|
## Suítes de teste (`project.tests[]`) e a divisão loop × suíte
|
|
500
506
|
|
|
501
507
|
O nó `tests` do `verify` é **plural**: um projeto pode declarar quantas suítes tiver em
|
|
@@ -677,7 +683,7 @@ sem `--no-build` já compila sozinho, e pré-compilar ali só dobraria o relógi
|
|
|
677
683
|
**O ciclo da baseline, e o número que só sai à mão.** `verify --record-baseline` é a única via de
|
|
678
684
|
escrita: mede cada nó numa execução real e grava `expectedCount` de volta no `config.json` (escrita
|
|
679
685
|
atômica `tmp.{pid}` + rename, preservando todo o resto do arquivo). Recusa **quatro** vezes — execução
|
|
680
|
-
vermelha, execução **parcial** (sob `--filter`, mesmo com o verify verde: baseline de meia frota é
|
|
686
|
+
vermelha (fora o `count-drop` com exit 0, abaixo), execução **parcial** (sob `--filter`, mesmo com o verify verde: baseline de meia frota é
|
|
681
687
|
afirmação sobre a outra metade que ninguém fez), contagem desconhecida, e **sem `config.json` ou sem
|
|
682
688
|
`project.tests[]`** (erro de uso, exit 2; o arquivo nunca é criado implicitamente — gravar os nós que o
|
|
683
689
|
fallback sintetiza exigiria inventar a declaração que o comando veio manter). Verde com gravação
|
|
@@ -685,11 +691,7 @@ recusada sai **exit 1**, com a razão em **stderr** (sobrevive ao `--json`). A g
|
|
|
685
691
|
pelos **nós de teste**, não pelos quatro nós do verify: um `validators` vermelho não muda quantos
|
|
686
692
|
testes rodaram. A execução que **gravou** a baseline (verde e inteira) persiste o stamp `verification` como qualquer verify: ela mediu a suíte toda, e rodá-la de novo só para ter prova seria pagar duas vezes pelo mesmo
|
|
687
693
|
verde. Sem gravação (vermelha, parcial, contagem desconhecida) não há stamp, e o `reason` do `--json`
|
|
688
|
-
nomeia o nó que impediu. **
|
|
689
|
-
mão **alto demais** (9 onde a suíte roda 7) deixa o verify vermelho por `count-drop`, e então
|
|
690
|
-
`--record-baseline` recusa — preservando o 9. **Não há flag de regravação forçada:** rebaixe ou apague
|
|
691
|
-
o número à mão e então re-meça pelo comando. O caso oposto se conserta sozinho — número baixo demais
|
|
692
|
-
passa com aviso de alta, e o comando grava.
|
|
694
|
+
nomeia o nó que impediu. **Apagar teste é legítimo:** a única cor vermelha que a gravação aceita é o `count-drop` de um nó que saiu com exit 0 — a suíte ficou menor de propósito. `--record-baseline` grava o número menor que mediu, o nó fica verde nessa mesma execução, o stamp persiste e o `feedback` diz `[baseline] rebaixada: <nó> (9 → 7)`; o revisor do Gate 3 vê a queda e julga se foi limpeza. Sem a flag, a queda continua reprovando (regressão silenciosa). Queda ao lado de outro nó vermelho não grava nada.
|
|
693
695
|
|
|
694
696
|
**A divisão loop × suíte.** É a resposta ao "a suíte inteira não cabe numa chamada":
|
|
695
697
|
|
|
@@ -735,7 +737,7 @@ Falhas do `--show` saem 1: id inexistente (`Standard "<id>" não existe.` com at
|
|
|
735
737
|
**`--dry-run` prevê sem tocar em nada.** Previsão e execução são o **mesmo percurso**, com os **mesmos filtros** e o **mesmo coletor de efeitos**: em modo normal cada verbo executa *e* registra, em dry apenas registra.
|
|
736
738
|
|
|
737
739
|
O relatório agrupa por natureza do efeito (apagar, criar diretório, copiar, escrever, mesclar, executar, índice do git) e **nomeia separadamente três grupos que ninguém adivinha da palavra "update"**:
|
|
738
|
-
- **apagados por inteiro antes da reinstalação** — `.claude/{commands,rules,skills,agents}` e `.morph/{framework/standards,framework/hooks,framework/schemas,framework/
|
|
740
|
+
- **apagados por inteiro antes da reinstalação** — `.claude/{commands,rules,skills,agents}` e `.morph/{framework/standards,framework/hooks,framework/schemas,framework/evals,config}`, com a contagem de arquivos que hoje moram em cada um;
|
|
739
741
|
- **fora do projeto** — `~/.claude/statusline.sh`, `~/.claude/statusline.py` e a mesclagem de `~/.claude/settings.json`;
|
|
740
742
|
- **dentro do pacote da CLI** — `generate-refs.js` escrevendo `framework/hooks/shared/phase-utils.js` do pacote, que num `npm link` de desenvolvimento é o próprio repositório do framework. Em dry roda a variante `--check`, que verifica e não escreve.
|
|
741
743
|
|
|
@@ -743,17 +745,6 @@ O `--dry-run` **passa por cima** da saída antecipada de "CLI desatualizada": bl
|
|
|
743
745
|
|
|
744
746
|
**Freio de major.** Um salto de **dois majors ou mais** é recusado sem `--force`, **antes de qualquer mutação** — recusar depois de mutar é meio-estado. A versão instalada vem de `.morph/.morphversion → morphVersion`, o mesmo registro que o `update` já lê; **nunca** de `config.json → frameworkVersion`, que o `update` escreve e jamais lê para decidir (medido: os dois divergem por um major inteiro num projeto real). Quando os dois discordam, a mensagem imprime **os dois** e diz qual decidiu — escolher em silêncio é acertar metade das vezes e nunca saber qual metade. `.morphversion` ausente ou ilegível é tratado como salto grande, com motivo próprio (`versão instalada indeterminável`) distinto do salto medido: "não sei quão velho isto é" é precisamente quando sobrescrever às cegas é mais perigoso. `--force` dispensa o freio e o relatório final **registra a dispensa**. `--dry-run` combinado com uma recusa **imprime o relatório**, com a recusa como primeiro item.
|
|
745
747
|
|
|
746
|
-
**Feature em voo no formato anterior.** Antes de qualquer mutação, o `update` varre `.morph/features/` da raiz e de cada worktree que o `git worktree list` conhece; uma feature não encerrada (`status` diferente de `completed`/`abandoned`) com `2-plan/tasks.json` ou `0-proposal/` e sem `2-plan/plan.md` é do formato que esta versão não conduz, e o `update` recusa (exit 1, nada escrito):
|
|
747
|
-
|
|
748
|
-
```
|
|
749
|
-
Recusado: N feature(s) em voo no formato anterior ao plan.md (proposal/spec/tasks.json), que esta versão não conduz:
|
|
750
|
-
- <feature> (raiz | <caminho da worktree>)
|
|
751
|
-
Termine cada uma na 8.47: `npx @polymorphism-tech/morph-spec@8.47 <comando>` (por exemplo `finish <feature> --pr`),
|
|
752
|
-
ou arquive sem integrar: `morph-spec archive <feature> --force` (o Gate 3 dela não foi aprovado). Depois rode o `update` de novo.
|
|
753
|
-
```
|
|
754
|
-
|
|
755
|
-
`--force` **não** dispensa esta recusa (o freio de major, sim): a CLI nova não sabe conduzir essas features. Feature em `.morph/archive/` não conta. `--dry-run` põe as mesmas linhas no topo da previsão. A mesma regra roda no `init --force` e no `setup-infra` (inclusive `--reinstall`), que sobrescrevem `.morph/` e `.claude/` sem passar pelo `update`: recusam com as mesmas linhas e saem 1. No retorno programático, `{ refused: true, reason: 'old-format-features', features }`.
|
|
756
|
-
|
|
757
748
|
**Por quê.** Não há travessia paralela — o resumo de uma execução real também sai do coletor, de modo que um ponto de emissão esquecido some das duas saídas ao mesmo tempo, em vez de mentir só no `--dry-run`.
|
|
758
749
|
|
|
759
750
|
## Grafo de codebase
|
|
@@ -810,7 +801,7 @@ O servidor MCP (`graphify-mcp`, 10 tools, todas aceitando `project_path`) está
|
|
|
810
801
|
|
|
811
802
|
### cost
|
|
812
803
|
|
|
813
|
-
**`morph-spec cost [feature] [--by phase] [--all-trees] [--json]`** — custo da execução a partir do mesmo `events.jsonl`: bytes de contexto lidos por tipo (`
|
|
804
|
+
**`morph-spec cost [feature] [--by phase] [--all-trees] [--json]`** — custo da execução a partir do mesmo `events.jsonl`: bytes de contexto lidos por tipo (`checklist`/`standard`/`skill`/`agents`/`plan-artifact`/`code` — a janela `offset`/`limit` que o Read trouxe, quando o evento a tem), dispatches + tamanho de prompt e mix de tools. Bytes são **proxy determinística** de custo de token; os tokens reais vêm à parte: `SessionUsage` (sessão principal) e `AgentUsage` (cada sub-agent, lido do transcript dele), somados **só pelo `delta`** de cada amostra — o total de topo é acumulado e se repete a cada Stop, e amostra antiga sem `delta` fica fora da soma e é contada como legada. `--all-trees` agrega **toda árvore git** com log, mais os logs que o `finish` guardou em `.git/morph-spec/finish-backup/` antes de desmontar a worktree, deduplicados por `eventId`.
|
|
814
805
|
|
|
815
806
|
### cost --metrics
|
|
816
807
|
|
|
@@ -41,7 +41,10 @@ diferente") é nota; nota não bloqueia e não pede rodada nova. Na dúvida, sem
|
|
|
41
41
|
## Verificador (`4-review/verifier.md`)
|
|
42
42
|
|
|
43
43
|
Missão: **provar cada cenário** do `2-plan/checks.json`. O default de todo `Cn` é NÃO PROVADO;
|
|
44
|
-
critério que você não verificou nunca vira PROVADO por inferência.
|
|
44
|
+
critério que você não verificou nunca vira PROVADO por inferência. A prova é a que o plano declarou
|
|
45
|
+
(`prova:` do Cn): `leitura` prova-se pelo `arquivo:linha` e pela fiação, **sem teste** — Cn `leitura`
|
|
46
|
+
sem teste não é NÃO PROVADO, e pedir teste para ele não é bloqueador. Só Cn com `risco:` exige teste
|
|
47
|
+
que morde.
|
|
45
48
|
|
|
46
49
|
```markdown
|
|
47
50
|
## Checks
|
|
@@ -82,7 +85,9 @@ morde. Escreva `4-review/mutants.json` e rode `morph-spec mutate {feature}` **um
|
|
|
82
85
|
|
|
83
86
|
`expect` é obrigatório: diga qual teste deve falhar e por quê **antes** de rodar — mutante cujo
|
|
84
87
|
efeito você não sabe prever tende a ser equivalente. Barra invertida do código vira `{{BS}}`.
|
|
85
|
-
Sobreviveu numa linha que um cenário exige → bloqueador `teste`. Sobreviveu em outra → nota.
|
|
88
|
+
Sobreviveu numa linha que um cenário exige → bloqueador `teste`. Sobreviveu em outra → nota.
|
|
89
|
+
Sem subconjunto (exit 2) ou `inconclusive`: a mutação não roda a suíte inteira — prove por leitura
|
|
90
|
+
(`arquivo:linha` + o teste que cobre), nunca em laço. Cole a
|
|
86
91
|
seção `## Mutações` que a ferramenta imprime. A reversão é da ferramenta; rodada morta no meio:
|
|
87
92
|
`morph-spec mutate --restore`.
|
|
88
93
|
|
|
@@ -109,6 +114,11 @@ Read-only, sobre o commit do dispatch: `git diff {base}..{sha}`, `git show {sha}
|
|
|
109
114
|
árvore de trabalho — o verificador pode estar com um mutante aplicado nela. Default SHIP.
|
|
110
115
|
|
|
111
116
|
1. **Escopo**: o diff contra as fatias (`arquivos:`) e o `## Contrato de implementação` do plano.
|
|
117
|
+
Teste além do orçamento também é escopo: teste para Cn `prova: leitura`, mais de um teste por Cn de
|
|
118
|
+
risco sem porquê no plano, teste que só fixa texto/constante/DTO, o mesmo comportamento em duas
|
|
119
|
+
camadas — bloqueador `escopo` (apague, não acrescente). `expectedCount` menor no
|
|
120
|
+
`.morph/config/config.json` do diff sem teste apagado no mesmo diff é bloqueador `regressao`: é
|
|
121
|
+
suíte que parou de rodar, não limpeza.
|
|
112
122
|
2. **Arquitetura**: padrão ou abstração nova contra o stack do projeto e os standards (camada que o
|
|
113
123
|
projeto não usa, repositório sobre o DbContext, dependência nova sem decisão no plano).
|
|
114
124
|
3. **Regressão**: para cada símbolo público alterado, `morph-spec graph affected <símbolo> --prod --json`.
|
|
@@ -6,41 +6,16 @@ Comprehensive hooks system for enforcing spec-driven development at the Claude C
|
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
framework/hooks/
|
|
9
|
-
├── claude-code/
|
|
10
|
-
│ ├── core/
|
|
11
|
-
│
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
│ │ ├── block-destructive-worktree-removal.js # PreToolUse(Bash): raw removal follows junctions out
|
|
15
|
-
│ │ ├── block-raw-worktree-add.js # PreToolUse(Bash): raw `git worktree add` is ungoverned
|
|
16
|
-
│ │ └── warn-bash-source-write.js # PreToolUse(Bash): source written by redirect skips the Write/Edit guards
|
|
17
|
-
│ ├── user-prompt/
|
|
18
|
-
│ │ └── enrich-prompt.js # Context-aware prompt enrichment
|
|
19
|
-
│ ├── pre-tool-use/
|
|
20
|
-
│ │ ├── protect-spec-files.js # Block edits to approved spec artifacts
|
|
21
|
-
│ │ └── enforce-phase-writes.js # Enforce writes to correct phase dir
|
|
22
|
-
│ ├── post-tool-use/
|
|
23
|
-
│ │ └── dispatch.js # Dispatch on CLI commands (auto-checkpoint)
|
|
24
|
-
│ ├── stop/
|
|
25
|
-
│ │ └── validate-completion.js # Advisory: warn about incomplete work
|
|
26
|
-
│ ├── pre-compact/
|
|
27
|
-
│ │ └── save-morph-context.js # Snapshot state before compaction
|
|
28
|
-
├── shared/ # Reusable utilities for all hooks
|
|
29
|
-
│ ├── state-reader.js # Read-only state.json accessor
|
|
30
|
-
│ ├── phase-utils.js # Phase constants and path utilities
|
|
31
|
-
│ ├── hook-response.js # JSON response builders
|
|
32
|
-
│ └── stdin-reader.js # Stdin JSON reader
|
|
33
|
-
├── git/ # Git hooks (Bash)
|
|
34
|
-
│ ├── pre-commit/
|
|
35
|
-
│ │ ├── orchestrator.sh # Master hook dispatcher
|
|
36
|
-
│ │ └── agents.sh # Validates agents.json schema
|
|
37
|
-
│ ├── commit-msg/
|
|
38
|
-
│ │ └── conventional-commits.sh # Enforces conventional commits
|
|
39
|
-
│ └── pre-push/
|
|
40
|
-
│ └── run-tests.sh # Runs test suite before push
|
|
41
|
-
└── README.md # This file
|
|
9
|
+
├── claude-code/
|
|
10
|
+
│ ├── core/ # every hook, plus chain.js (one process per multi-hook group)
|
|
11
|
+
│ └── statusline.py # global statusline
|
|
12
|
+
├── shared/ # code hooks share (and src/ imports): state reader, guards, e2e, heavy, telemetry
|
|
13
|
+
└── README.md
|
|
42
14
|
```
|
|
43
15
|
|
|
16
|
+
The registered set is `MORPH_HOOKS` in `src/utils/hooks-installer.js` — the table below lists it. No hook builds,
|
|
17
|
+
typechecks or runs tests: that is `morph-spec verify`.
|
|
18
|
+
|
|
44
19
|
## Hook Events
|
|
45
20
|
|
|
46
21
|
| Event | Hook | Type | Purpose |
|
|
@@ -104,17 +79,6 @@ The installer writes to `.claude/settings.local.json`:
|
|
|
104
79
|
}
|
|
105
80
|
```
|
|
106
81
|
|
|
107
|
-
### Git Hooks
|
|
108
|
-
|
|
109
|
-
```bash
|
|
110
|
-
# In your project root
|
|
111
|
-
cd .git/hooks
|
|
112
|
-
ln -sf ../../framework/hooks/git/pre-commit/orchestrator.sh pre-commit
|
|
113
|
-
ln -sf ../../framework/hooks/git/commit-msg/conventional-commits.sh commit-msg
|
|
114
|
-
ln -sf ../../framework/hooks/git/pre-push/run-tests.sh pre-push
|
|
115
|
-
chmod +x pre-commit commit-msg pre-push
|
|
116
|
-
```
|
|
117
|
-
|
|
118
82
|
## Shared Utilities
|
|
119
83
|
|
|
120
84
|
All Claude Code hooks import from `framework/hooks/shared/`:
|
|
@@ -6,70 +6,31 @@ import { stateExists } from '../../shared/state-reader.js';
|
|
|
6
6
|
import { scanForBlockers } from '../../shared/reparse-scan.js';
|
|
7
7
|
import { tokenize } from '../../shared/command-tokens.js';
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* someone reading documentation.
|
|
12
|
-
*
|
|
13
|
-
* A morph worktree carries JUNCTIONS to the primary root's shared infra
|
|
14
|
-
* (`node_modules`, `.claude`, `.morph/framework`). Every generic recursive
|
|
15
|
-
* removal — `git worktree remove` (plain AND `--force`), `rm -rf`,
|
|
16
|
-
* `Remove-Item -Recurse`, `rmdir /s` — FOLLOWS those links out of the worktree
|
|
17
|
-
* and deletes what they point at, leaving the primary root's `.claude/` and
|
|
18
|
-
* `.morph/framework/` as empty directories. That is the incident this hook
|
|
19
|
-
* exists for, and it is not hypothetical: it happened, and the command was
|
|
20
|
-
* copied from a message the CLI itself printed.
|
|
21
|
-
*
|
|
22
|
-
* `morph-spec worktree remove` is safe because it drops the junctions BEFORE
|
|
23
|
-
* letting git near the directory. This hook is what stops the unsafe path from
|
|
24
|
-
* being reachable by accident.
|
|
25
|
-
*
|
|
26
|
-
* DELIBERATELY NARROW — an inert or over-eager guardrail is worse than none:
|
|
27
|
-
* - only recursive-removal shapes, only when a target path can be extracted;
|
|
28
|
-
* - only when that path exists AND the scan finds reparse points under it
|
|
29
|
-
* (an already-unlinked worktree, or one that never had junctions, passes);
|
|
30
|
-
* - anything unparseable passes.
|
|
31
|
-
* The scan is the authority, not the path shape: a worktree kept outside
|
|
32
|
-
* `worktrees/` is protected just the same, and a `worktrees/` directory with
|
|
33
|
-
* nothing to traverse is not blocked for looking dangerous.
|
|
34
|
-
*/
|
|
9
|
+
// Generic recursive removals FOLLOW a worktree's junctions and empty the root's `.claude/` and `.morph/framework/`.
|
|
10
|
+
// The reparse scan is the authority, not the path shape; anything unparseable passes.
|
|
35
11
|
|
|
36
|
-
//
|
|
37
|
-
// reads `git worktree add` off the same tokens, and a hook must never import a
|
|
38
|
-
// sibling hook (its standalone tail would fire in the importer's process).
|
|
12
|
+
// A hook never imports a sibling hook (its standalone tail would fire), so the tokenizer lives in shared/.
|
|
39
13
|
export { tokenize };
|
|
40
14
|
|
|
41
|
-
/**
|
|
42
|
-
* The path a recursive-removal command would destroy, or null when this is not
|
|
43
|
-
* such a command (or the target cannot be read off it).
|
|
44
|
-
*
|
|
45
|
-
* @param {string} command
|
|
46
|
-
* @returns {string|null}
|
|
47
|
-
*/
|
|
48
15
|
export function extractRemovalTarget(command) {
|
|
49
16
|
const tokens = tokenize(command);
|
|
50
17
|
if (!tokens.length) return null;
|
|
51
18
|
const lower = tokens.map((t) => t.toLowerCase());
|
|
52
19
|
|
|
53
|
-
//
|
|
54
|
-
// non-flag token; git has no form of this command without one.
|
|
20
|
+
// The path is always the last non-flag token.
|
|
55
21
|
const gi = lower.indexOf('git');
|
|
56
22
|
if (gi !== -1 && lower[gi + 1] === 'worktree' && lower[gi + 2] === 'remove') {
|
|
57
23
|
const rest = tokens.slice(gi + 3).filter((t) => !t.startsWith('-'));
|
|
58
24
|
return rest.length ? rest[rest.length - 1] : null;
|
|
59
25
|
}
|
|
60
26
|
|
|
61
|
-
//
|
|
62
|
-
// flag — a plain `rm file` or `Remove-Item file` cannot traverse a junction's
|
|
63
|
-
// contents and is none of this hook's business.
|
|
27
|
+
// Needs both the command word and a recursive flag: a plain `rm file` cannot traverse a junction.
|
|
64
28
|
const recursive = tokens.some((t) => /^(-[a-z]*r[a-z]*|--recursive|\/s)$/i.test(t));
|
|
65
29
|
const removalCmd = lower.find((t) => /^(rm|remove-item|ri|rd|rmdir|del)$/.test(t));
|
|
66
30
|
if (!removalCmd || !recursive) return null;
|
|
67
31
|
|
|
68
32
|
const idx = lower.indexOf(removalCmd);
|
|
69
|
-
// A cmd.exe switch is a slash plus
|
|
70
|
-
// longer that starts with `/` is a POSIX ABSOLUTE PATH — filtering on the
|
|
71
|
-
// bare `/` made every Linux target look like a flag, so `rm -rf /tmp/wt`
|
|
72
|
-
// extracted nothing and the hook passed exactly where it had to block.
|
|
33
|
+
// A cmd.exe switch is a slash plus one or two letters; anything longer is a POSIX absolute path.
|
|
73
34
|
const isCmdSwitch = (t) => /^\/[a-z]{1,2}$/i.test(t);
|
|
74
35
|
const operands = tokens.slice(idx + 1).filter((t) => !t.startsWith('-') && !isCmdSwitch(t));
|
|
75
36
|
// Named-parameter form: `Remove-Item -Path <p> -Recurse`.
|
|
@@ -78,10 +39,6 @@ export function extractRemovalTarget(command) {
|
|
|
78
39
|
return operands.length ? operands[0] : null;
|
|
79
40
|
}
|
|
80
41
|
|
|
81
|
-
/**
|
|
82
|
-
* @param {object} payload - parsed PreToolUse stdin payload (Bash matcher)
|
|
83
|
-
* @returns {Promise<object|null>} `{decision:'block',reason}` or null (pass).
|
|
84
|
-
*/
|
|
85
42
|
export async function run(payload) {
|
|
86
43
|
try {
|
|
87
44
|
if (!stateExists()) return null;
|
|
@@ -5,56 +5,23 @@ import { stateExists } from '../../shared/state-reader.js';
|
|
|
5
5
|
// standalone tail (stdin read + exit) inside THIS process.
|
|
6
6
|
import { tokenize } from '../../shared/command-tokens.js';
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
* block-raw-worktree-add — PreToolUse (Bash) guard against `git worktree add`.
|
|
10
|
-
*
|
|
11
|
-
* A worktree created by raw git is a worktree morph cannot see: no infra
|
|
12
|
-
* junctions (`.claude`, `node_modules`, `.morph/framework` — so NO hooks run
|
|
13
|
-
* in there), no port block (its dev server collides with the root's), no
|
|
14
|
-
* feature/task identity (invisible to `fleet`, `doctor --worktrees`,
|
|
15
|
-
* `worktree list`), and a location git chose, not `<root>/worktrees/`. The
|
|
16
|
-
* sibling guard (block-destructive-worktree-removal) protects the way OUT;
|
|
17
|
-
* this one protects the way IN, by naming the two commands that build a
|
|
18
|
-
* governed tree:
|
|
19
|
-
*
|
|
20
|
-
* morph-spec worktree setup <feature> — a feature's tree
|
|
21
|
-
* morph-spec worktree provision <name> [--base <ref>] — a task-isolation tree
|
|
22
|
-
*
|
|
23
|
-
* DELIBERATELY NARROW: only the `add` subcommand. `list`, `prune`, `remove`
|
|
24
|
-
* (the sibling's business), `lock`, `unlock`, `move`, `repair` all pass. The
|
|
25
|
-
* `git` token may be followed by global options (`-C <dir>`, `--git-dir=<d>`,
|
|
26
|
-
* `-c k=v`) before `worktree`, and may sit anywhere in a chained command
|
|
27
|
-
* (`cd a && git worktree add b`) — every `git` occurrence is checked.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
/** Global `git` options that take a separate value: `git -C <dir> worktree add`. */
|
|
31
8
|
const GIT_GLOBAL_WITH_VALUE = new Set(['-c', '-C', '--git-dir', '--work-tree', '--namespace', '--exec-path', '--config-env']);
|
|
32
9
|
|
|
33
|
-
/**
|
|
34
|
-
* Does this command line invoke `git worktree add` anywhere?
|
|
35
|
-
* @param {string} command
|
|
36
|
-
* @returns {boolean}
|
|
37
|
-
*/
|
|
38
10
|
export function isRawWorktreeAdd(command) {
|
|
39
11
|
const tokens = tokenize(command);
|
|
40
12
|
const lower = tokens.map((t) => t.toLowerCase());
|
|
41
13
|
for (let i = 0; i < lower.length; i++) {
|
|
42
14
|
if (lower[i] !== 'git' && !/(^|[\\/])git(\.exe)?$/.test(lower[i])) continue;
|
|
43
15
|
let j = i + 1;
|
|
44
|
-
// Skip global options between `git` and the subcommand.
|
|
45
16
|
while (j < lower.length && lower[j].startsWith('-')) {
|
|
46
|
-
if (GIT_GLOBAL_WITH_VALUE.has(tokens[j])) j += 2;
|
|
47
|
-
else j += 1;
|
|
17
|
+
if (GIT_GLOBAL_WITH_VALUE.has(tokens[j])) j += 2;
|
|
18
|
+
else j += 1;
|
|
48
19
|
}
|
|
49
20
|
if (lower[j] === 'worktree' && lower[j + 1] === 'add') return true;
|
|
50
21
|
}
|
|
51
22
|
return false;
|
|
52
23
|
}
|
|
53
24
|
|
|
54
|
-
/**
|
|
55
|
-
* @param {object} payload - parsed PreToolUse stdin payload (Bash matcher)
|
|
56
|
-
* @returns {Promise<object|null>} `{decision:'block',reason}` or null (pass).
|
|
57
|
-
*/
|
|
58
25
|
export async function run(payload) {
|
|
59
26
|
try {
|
|
60
27
|
if (!stateExists()) return null;
|
|
@@ -1,54 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
* Motivação (onda de performance R2): cada hook registrado no Claude Code é
|
|
6
|
-
* hoje um processo node próprio (~60ms de cold start). Um único Write/Edit
|
|
7
|
-
* disparava até 9 desses processos — 2 PreToolUse + 3+2+2 PostToolUse. Este
|
|
8
|
-
* script agrupa os elos de um mesmo event+matcher (a mesma entrada do
|
|
9
|
-
* registry em src/utils/hooks-installer.js) num só spawn: lê stdin UMA vez,
|
|
10
|
-
* importa e chama `run(payload)` de cada hook em sequência, e combina as
|
|
11
|
-
* respostas — reduzindo esses 9 spawns para 4 por Write/Edit (1 PreToolUse +
|
|
12
|
-
* 3 PostToolUse: Write|Edit / Write|Edit|AskUserQuestion / matcher-`*`).
|
|
13
|
-
*
|
|
14
|
-
* Uso: `node chain.js <hook1> <hook2> ...` — nomes de arquivo SEM `.js`,
|
|
15
|
-
* resolvidos no mesmo diretório deste script (core/), na ORDEM em que devem
|
|
16
|
-
* rodar. A ordem é a mesma que o registry antigo (duas entries separadas)
|
|
17
|
-
* já implicava — ver o comentário de cada grupo em hooks-installer.js.
|
|
18
|
-
*
|
|
19
|
-
* Cada hook core exporta `run(payload)` (ver framework/hooks/shared/hook-response.js
|
|
20
|
-
* `runStandalone`) — o arquivo continua executável standalone (spawnSync
|
|
21
|
-
* direto, como os testes já fazem) porque a própria cauda do arquivo só lê
|
|
22
|
-
* stdin/imprime/sai quando NÃO está sendo importado por esta cadeia.
|
|
23
|
-
*
|
|
24
|
-
* Regras de combinação:
|
|
25
|
-
* - PreToolUse (decision:'block'): o PRIMEIRO elo que retornar um bloqueio
|
|
26
|
-
* VENCE e encerra a cadeia — os elos seguintes nem chegam a rodar. Isto é
|
|
27
|
-
* uma otimização NOVA desta consolidação (antes, Claude Code podia
|
|
28
|
-
* disparar os dois processos de qualquer forma); o resultado observável
|
|
29
|
-
* pro Claude Code é idêntico (o tool call é bloqueado do mesmo jeito).
|
|
30
|
-
* - PostToolUse / outros eventos de contexto: os textos de TODOS os elos que
|
|
31
|
-
* retornarem `hookSpecificOutput.additionalContext` (ou `decision:'approve'`
|
|
32
|
-
* com additionalContext) são concatenados, na ORDEM dos argv, separados por
|
|
33
|
-
* uma linha em branco — nenhum elo é pulado por um vizinho ter respondido.
|
|
34
|
-
* - Nome do evento (hookSpecificOutput.hookEventName) no resultado final:
|
|
35
|
-
* o valor que o PRIMEIRO elo a declarar um venceu (mesma regra de
|
|
36
|
-
* "primeiro vence" do bloqueio acima — na prática todos os elos de um
|
|
37
|
-
* mesmo grupo observam o MESMO evento real do Claude Code, então isto só
|
|
38
|
-
* importa como desempate determinístico); na ausência de qualquer elo
|
|
39
|
-
* declarando, cai para `payload.hook_event_name`; por último, `'PostToolUse'`.
|
|
40
|
-
* - updatedInput (PreToolUse): um elo pode devolver
|
|
41
|
-
* `hookSpecificOutput.updatedInput` (ver `updatedInputResult` em
|
|
42
|
-
* hook-response.js) para reescrever o tool_input. O ÚLTIMO elo a devolver um
|
|
43
|
-
* vence (só um elo do grupo reescreve, por construção). `permissionDecision:
|
|
44
|
-
* 'allow'` + `updatedInput` só entram no resultado final quando algum elo
|
|
45
|
-
* reescreveu — um `allow` incondicional pularia o prompt de permissão de
|
|
46
|
-
* TODO Bash, que é exatamente o contrário de um guardrail.
|
|
47
|
-
* - Fail-open POR ELO: uma exceção não tratada por um hook individual (import
|
|
48
|
-
* falhou, run() não existe, etc.) não derruba a cadeia — os elos seguintes
|
|
49
|
-
* ainda rodam. Cada hook já fail-opens sozinho dentro do seu próprio
|
|
50
|
-
* try/catch; isto é a segunda rede de segurança, para o import em si.
|
|
51
|
-
*/
|
|
2
|
+
// Usage: node chain.js <hook> ... (names without .js, run in argv order). The first block wins and ends the chain,
|
|
3
|
+
// the contexts of every link concatenate, and an exception in one link never stops the next.
|
|
52
4
|
|
|
53
5
|
import { dirname, join } from 'path';
|
|
54
6
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
@@ -56,20 +8,12 @@ import { readStdin } from '../../shared/stdin-reader.js';
|
|
|
56
8
|
|
|
57
9
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
58
10
|
|
|
59
|
-
//
|
|
60
|
-
// cauda de execução standalone de cada hook (runStandalone() em
|
|
61
|
-
// hook-response.js) só dispara quando esta flag NÃO está setada. Setada
|
|
62
|
-
// ANTES do primeiro import dinâmico — cada hook importado herda o mesmo
|
|
63
|
-
// process.env deste processo.
|
|
11
|
+
// Set before the first import: each hook's standalone tail runs only when this is unset.
|
|
64
12
|
process.env.MORPH_HOOK_CHAINED = '1';
|
|
65
13
|
|
|
66
|
-
//
|
|
67
|
-
// de core/, sem precisar ser hooks reais nem satisfazer as convenções que
|
|
68
|
-
// hook-wiring.test.js exige de todo arquivo em core/). Produção nunca seta
|
|
69
|
-
// isto — resolve sempre no próprio diretório deste script.
|
|
14
|
+
// Test-only override; production always resolves next to this script.
|
|
70
15
|
const coreDir = process.env.MORPH_HOOK_CORE_DIR || __dirname;
|
|
71
16
|
|
|
72
|
-
/** Extrai o texto de contexto de um resultado de hook, se houver. */
|
|
73
17
|
function contextOf(result) {
|
|
74
18
|
if (!result) return null;
|
|
75
19
|
if (result.decision === 'approve' && typeof result.additionalContext === 'string') {
|
|
@@ -93,13 +37,11 @@ async function main() {
|
|
|
93
37
|
const mod = await import(pathToFileURL(join(coreDir, `${name}.js`)).href);
|
|
94
38
|
result = await mod.run(payload);
|
|
95
39
|
} catch {
|
|
96
|
-
// Fail-open POR ELO — ver doc do módulo acima.
|
|
97
40
|
result = null;
|
|
98
41
|
}
|
|
99
42
|
|
|
100
43
|
if (!result) continue;
|
|
101
44
|
|
|
102
|
-
// PreToolUse: o primeiro bloqueio vence e encerra a cadeia.
|
|
103
45
|
if (result.decision === 'block') {
|
|
104
46
|
// A later link blocking must not swallow what earlier links reported (a tsc error on the same edit).
|
|
105
47
|
if (contexts.length) result = { ...result, reason: [...contexts, result.reason].join('\n\n') };
|
|
@@ -116,9 +58,6 @@ async function main() {
|
|
|
116
58
|
const ctx = contextOf(result);
|
|
117
59
|
if (ctx) {
|
|
118
60
|
contexts.push(ctx);
|
|
119
|
-
// First link to declare a name wins (mirrors block-first-wins above) —
|
|
120
|
-
// in practice every link in a group observes the SAME real Claude Code
|
|
121
|
-
// event, so this only matters as a deterministic tie-break.
|
|
122
61
|
eventName = eventName || result?.hookSpecificOutput?.hookEventName;
|
|
123
62
|
}
|
|
124
63
|
}
|
|
@@ -1,17 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* `architecture-code-comments` standard never allows.
|
|
6
|
-
*
|
|
7
|
-
* Event: PostToolUse | Matcher: Write|Edit
|
|
8
|
-
*
|
|
9
|
-
* `decision: block` makes the agent fix it in the same turn; the edit itself
|
|
10
|
-
* already landed. Only comments absent from `tool_response.originalFile`
|
|
11
|
-
* count, so editing a legacy file never reports its old comments.
|
|
12
|
-
*
|
|
13
|
-
* Fail-open: any error passes silently.
|
|
14
|
-
*/
|
|
3
|
+
// `decision: block` makes the agent fix it in the same turn (the edit already landed). Only comments absent from
|
|
4
|
+
// `tool_response.originalFile` count, so editing a legacy file never reports its old comments.
|
|
15
5
|
|
|
16
6
|
import { readFileSync } from 'fs';
|
|
17
7
|
import { isAbsolute, relative, resolve } from 'path';
|
|
@@ -78,10 +68,6 @@ function describe(file, findings) {
|
|
|
78
68
|
].join('\n');
|
|
79
69
|
}
|
|
80
70
|
|
|
81
|
-
/**
|
|
82
|
-
* @param {object} payload - PostToolUse stdin payload
|
|
83
|
-
* @returns {Promise<object|null>}
|
|
84
|
-
*/
|
|
85
71
|
export async function run(payload) {
|
|
86
72
|
try {
|
|
87
73
|
if (!['Write', 'Edit'].includes(payload?.tool_name)) return null;
|