@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
|
@@ -1,47 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* Claude Code hooks communicate via JSON to stdout.
|
|
5
|
-
* - PreToolUse: { "decision": "block"|"approve", "reason": "..." }
|
|
6
|
-
* - SessionStart/Stop/PostToolUse/etc (non-blocking context injection):
|
|
7
|
-
* { "hookSpecificOutput": { "hookEventName": "...", "additionalContext": "..." } }
|
|
8
|
-
* A top-level `additionalContext` is NOT honored by Claude Code for any
|
|
9
|
-
* event — it must be nested under `hookSpecificOutput` alongside the
|
|
10
|
-
* event name (see https://code.claude.com/docs/en/hooks.md).
|
|
11
|
-
*/
|
|
1
|
+
// A top-level `additionalContext` is NOT honored for any event: it must be nested
|
|
2
|
+
// under `hookSpecificOutput` (https://code.claude.com/docs/en/hooks.md).
|
|
12
3
|
|
|
13
|
-
/**
|
|
14
|
-
* Block a tool use with a reason.
|
|
15
|
-
* @param {string} reason - Human-readable reason for blocking
|
|
16
|
-
*/
|
|
17
4
|
export function block(reason) {
|
|
18
5
|
console.log(JSON.stringify({ decision: 'block', reason }));
|
|
19
6
|
process.exit(0);
|
|
20
7
|
}
|
|
21
8
|
|
|
22
|
-
/**
|
|
23
|
-
* Approve a tool use, optionally injecting context.
|
|
24
|
-
* @param {string} [context] - Additional context to inject
|
|
25
|
-
*/
|
|
26
9
|
export function approve(context) {
|
|
27
10
|
if (context) {
|
|
28
11
|
console.log(JSON.stringify({ decision: 'approve', additionalContext: context }));
|
|
29
12
|
}
|
|
30
|
-
// No output = implicit approve
|
|
31
13
|
process.exit(0);
|
|
32
14
|
}
|
|
33
15
|
|
|
34
|
-
/**
|
|
35
|
-
* Inject additional (non-blocking) context for the current hook event.
|
|
36
|
-
*
|
|
37
|
-
* Must be nested under `hookSpecificOutput` — Claude Code does not honor a
|
|
38
|
-
* top-level `additionalContext` for any event.
|
|
39
|
-
*
|
|
40
|
-
* @param {string} text - Context text to inject
|
|
41
|
-
* @param {string} eventName - The hook event this is responding to (e.g.
|
|
42
|
-
* 'SessionStart', 'Stop', 'PostToolUse') — required, echoed back as
|
|
43
|
-
* `hookSpecificOutput.hookEventName`.
|
|
44
|
-
*/
|
|
45
16
|
export function injectContext(text, eventName) {
|
|
46
17
|
console.log(JSON.stringify({
|
|
47
18
|
hookSpecificOutput: { hookEventName: eventName, additionalContext: text },
|
|
@@ -49,63 +20,26 @@ export function injectContext(text, eventName) {
|
|
|
49
20
|
process.exit(0);
|
|
50
21
|
}
|
|
51
22
|
|
|
52
|
-
/**
|
|
53
|
-
* Silent exit — no output, no blocking.
|
|
54
|
-
*/
|
|
55
23
|
export function pass() {
|
|
56
24
|
process.exit(0);
|
|
57
25
|
}
|
|
58
26
|
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
// Every core hook used to run inline at module scope and call block()/pass()/
|
|
62
|
-
// injectContext() above to print+exit(0) the moment it knew its verdict. That
|
|
63
|
-
// stops being safe once a hook's logic moves into an exported `run(payload)`
|
|
64
|
-
// that can ALSO be called by framework/hooks/claude-code/core/chain.js — which
|
|
65
|
-
// imports several hooks' run() into ONE process and cannot have any of them
|
|
66
|
-
// call process.exit() mid-chain (it would kill the other links). run() must
|
|
67
|
-
// instead RETURN the verdict object; chain.js (or this file's own
|
|
68
|
-
// runStandalone(), below) is what actually prints it and exits.
|
|
69
|
-
//
|
|
70
|
-
// These builders produce the exact same JSON shapes as block()/approve()/
|
|
71
|
-
// injectContext() above — keep them in sync if that shape ever changes.
|
|
72
|
-
|
|
73
|
-
/** Pure equivalent of block(reason) — returns the verdict instead of printing+exiting. */
|
|
27
|
+
// `run()` returns these instead of exiting: chain.js runs several hooks in one
|
|
28
|
+
// process. Same JSON shapes as block()/approve()/injectContext() above.
|
|
74
29
|
export function blockResult(reason) {
|
|
75
30
|
return { decision: 'block', reason };
|
|
76
31
|
}
|
|
77
32
|
|
|
78
|
-
/** Pure equivalent of approve(context) — null means "pass" (nothing to say). */
|
|
79
33
|
export function approveResult(context) {
|
|
80
34
|
return context ? { decision: 'approve', additionalContext: context } : null;
|
|
81
35
|
}
|
|
82
36
|
|
|
83
|
-
/** Pure equivalent of injectContext(text, eventName). */
|
|
84
37
|
export function contextResult(text, eventName) {
|
|
85
38
|
return { hookSpecificOutput: { hookEventName: eventName, additionalContext: text } };
|
|
86
39
|
}
|
|
87
40
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
* `await runStandalone(run);`
|
|
91
|
-
*
|
|
92
|
-
* Reads stdin once, calls the hook's `run(payload)`, prints the JSON verdict
|
|
93
|
-
* (if any) and exits 0 — reproducing exactly what block()/injectContext()/
|
|
94
|
-
* pass() used to do when a hook's logic lived inline at module scope.
|
|
95
|
-
*
|
|
96
|
-
* No-ops when MORPH_HOOK_CHAINED is set: that means core/chain.js already
|
|
97
|
-
* imported this module and will call run() itself with a payload it read
|
|
98
|
-
* once for the whole chain — this file must not ALSO read stdin or exit here
|
|
99
|
-
* (doing so would race chain.js's own stdin read and kill the chain process
|
|
100
|
-
* after the first link). chain.js sets the flag BEFORE its first dynamic
|
|
101
|
-
* import, so every hook it loads sees it already set at call time.
|
|
102
|
-
*
|
|
103
|
-
* Fail-open: any error (including a rejected run()) prints nothing and exits
|
|
104
|
-
* 0 — the same silent-pass behavior every hook's own outer try/catch used to
|
|
105
|
-
* produce on an unexpected error.
|
|
106
|
-
*
|
|
107
|
-
* @param {(payload: object) => Promise<object|null>} run
|
|
108
|
-
*/
|
|
41
|
+
// No-op under MORPH_HOOK_CHAINED: chain.js reads stdin once for the whole chain,
|
|
42
|
+
// and reading or exiting here would kill it after the first link.
|
|
109
43
|
export async function runStandalone(run) {
|
|
110
44
|
if (process.env.MORPH_HOOK_CHAINED) return;
|
|
111
45
|
|
|
@@ -123,17 +57,8 @@ export async function runStandalone(run) {
|
|
|
123
57
|
process.exit(0);
|
|
124
58
|
}
|
|
125
59
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
* `updatedInput` in place of the original `tool_input`, and the decision is
|
|
129
|
-
* an explicit `allow` (a rewrite without a decision is ignored). Use it ONLY
|
|
130
|
-
* when the hook actually changed something — an unconditional `allow` would
|
|
131
|
-
* skip the permission prompt for every call it touches.
|
|
132
|
-
*
|
|
133
|
-
* @param {object} updatedInput - the full replacement tool_input
|
|
134
|
-
* @param {string} eventName - 'PreToolUse'
|
|
135
|
-
* @param {string} [context] - optional additionalContext (what was changed)
|
|
136
|
-
*/
|
|
60
|
+
// A rewrite without a decision is ignored, so it carries `allow`: use it ONLY when
|
|
61
|
+
// the input changed, or every call it touches skips the permission prompt.
|
|
137
62
|
export function updatedInputResult(updatedInput, eventName, context) {
|
|
138
63
|
const out = { hookEventName: eventName, permissionDecision: 'allow', updatedInput };
|
|
139
64
|
if (context) out.additionalContext = context;
|
|
@@ -1,36 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Hooks are installed STANDALONE into `.morph/framework/hooks/` and may never
|
|
5
|
-
* import from `src/`; the only way for a hook to reuse a CLI command (the
|
|
6
|
-
* WorktreeCreate hook needs `worktree setup`/`provision`, WorktreeRemove needs
|
|
7
|
-
* `worktree remove --if-clean`) is to spawn the CLI as a subprocess. This
|
|
8
|
-
* module is the ONE place that knows how to find it.
|
|
9
|
-
*
|
|
10
|
-
* Resolution order:
|
|
11
|
-
* 1. `MORPH_SPEC_BIN` — an explicit command, possibly with leading args and
|
|
12
|
-
* quoted paths (`node "R:/my repo/bin/morph-spec.js"`). What tests and a
|
|
13
|
-
* framework checkout use.
|
|
14
|
-
* 2. `morph-spec` on PATH — the npm-installed bin. On win32 the shim is
|
|
15
|
-
* `morph-spec.cmd`, which Node refuses to spawn without a shell, so it is
|
|
16
|
-
* located by scanning PATH ourselves and then run through the shell with
|
|
17
|
-
* each arg quoted; on POSIX a plain `shell:false` spawn.
|
|
18
|
-
* 3. `npx --no-install morph-spec` — a project-local dependency.
|
|
19
|
-
*
|
|
20
|
-
* ENOENT (or the shell's "not recognized"/"could not determine executable")
|
|
21
|
-
* everywhere → `{ unavailable: true }`, never a throw: the caller decides what
|
|
22
|
-
* degraded behavior looks like.
|
|
23
|
-
*
|
|
24
|
-
* `json` is the LAST line of stdout that parses as a JSON object (the CLI's
|
|
25
|
-
* commands print exactly one JSON line; anything a stray console.log adds
|
|
26
|
-
* before it is skipped), or null.
|
|
27
|
-
*/
|
|
28
|
-
|
|
1
|
+
// A hook cannot import `src/`, so it reuses a CLI command by spawning the CLI; this is the one
|
|
2
|
+
// place that finds it. On win32 `morph-spec.cmd` is located on PATH by hand and run through the
|
|
3
|
+
// shell, since Node refuses to spawn a `.cmd` without one.
|
|
29
4
|
import { spawnSync } from 'child_process';
|
|
30
5
|
import { existsSync } from 'fs';
|
|
31
6
|
import { delimiter, join } from 'path';
|
|
32
7
|
|
|
33
|
-
/** Split a command string into argv, honoring single and double quotes. */
|
|
34
8
|
function splitCommand(command) {
|
|
35
9
|
const tokens = [];
|
|
36
10
|
const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
@@ -39,22 +13,13 @@ function splitCommand(command) {
|
|
|
39
13
|
return tokens;
|
|
40
14
|
}
|
|
41
15
|
|
|
42
|
-
/** Quote one argument for cmd.exe (the only shell path this module uses). */
|
|
43
16
|
export function quoteForCmd(arg) {
|
|
44
17
|
if (arg === '') return '""';
|
|
45
18
|
if (!/[\s"&|<>^()]/.test(arg)) return arg;
|
|
46
19
|
return `"${arg.replace(/"/g, '""')}"`;
|
|
47
20
|
}
|
|
48
21
|
|
|
49
|
-
/**
|
|
50
|
-
* First PATH entry holding any of `files`, or null. The entries are walked in
|
|
51
|
-
* PATH order and every name is tried inside each one, the way cmd.exe applies
|
|
52
|
-
* PATHEXT: a `graphify.cmd` early in PATH wins over a `graphify.exe` later.
|
|
53
|
-
*
|
|
54
|
-
* @param {string[]} files
|
|
55
|
-
* @param {NodeJS.ProcessEnv} env
|
|
56
|
-
* @returns {string|null}
|
|
57
|
-
*/
|
|
22
|
+
/** Walks PATH in order and tries every name in each entry, the way cmd.exe applies PATHEXT. */
|
|
58
23
|
export function findFirstOnPath(files, env) {
|
|
59
24
|
const raw = env.PATH || env.Path || env.path || '';
|
|
60
25
|
for (const dir of raw.split(delimiter)) {
|
|
@@ -69,7 +34,6 @@ export function findFirstOnPath(files, env) {
|
|
|
69
34
|
|
|
70
35
|
const findOnPath = (file, env) => findFirstOnPath([file], env);
|
|
71
36
|
|
|
72
|
-
/** Last stdout line that parses as a JSON object, or null. */
|
|
73
37
|
export function lastJsonLine(stdout) {
|
|
74
38
|
const lines = String(stdout || '').split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
75
39
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
@@ -82,10 +46,6 @@ export function lastJsonLine(stdout) {
|
|
|
82
46
|
return null;
|
|
83
47
|
}
|
|
84
48
|
|
|
85
|
-
/**
|
|
86
|
-
* The candidates to try, in order. Each is `{ file, args, shell }`.
|
|
87
|
-
* @param {NodeJS.ProcessEnv} env
|
|
88
|
-
*/
|
|
89
49
|
function candidates(env) {
|
|
90
50
|
const list = [];
|
|
91
51
|
const explicit = env.MORPH_SPEC_BIN && splitCommand(env.MORPH_SPEC_BIN);
|
|
@@ -107,10 +67,8 @@ function candidates(env) {
|
|
|
107
67
|
const NOT_FOUND_RE = /not recognized as an internal or external command|could not determine executable to run|command not found|ENOENT/i;
|
|
108
68
|
|
|
109
69
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* @param {string[]} args - CLI argv after the program name
|
|
113
|
-
* @param {{ cwd?: string, timeoutMs?: number, env?: NodeJS.ProcessEnv }} [opts]
|
|
70
|
+
* A missing CLI is `{ unavailable: true }`, never a throw. `json` is the last stdout line that
|
|
71
|
+
* parses as an object.
|
|
114
72
|
* @returns {{ ok: boolean, code: number|null, stdout: string, stderr: string, json: object|null, unavailable: boolean }}
|
|
115
73
|
*/
|
|
116
74
|
export function runMorphSpec(args, { cwd, timeoutMs = 120000, env } = {}) {
|
|
@@ -134,10 +92,8 @@ export function runMorphSpec(args, { cwd, timeoutMs = 120000, env } = {}) {
|
|
|
134
92
|
}
|
|
135
93
|
const stdout = r.stdout || '';
|
|
136
94
|
const stderr = r.stderr || '';
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
// kinds are read that way — a real morph-spec failure is never mistaken
|
|
140
|
-
// for absence.
|
|
95
|
+
// Shell and npx report "not found" as a non-zero exit, never ENOENT; the explicit
|
|
96
|
+
// MORPH_SPEC_BIN is excluded so a real morph-spec failure is never read as absence.
|
|
141
97
|
if (cand.kind !== 'env' && r.status !== 0 && !stdout.trim() && NOT_FOUND_RE.test(stderr)) continue;
|
|
142
98
|
return { ok: r.status === 0, code: r.status, stdout, stderr, json: lastJsonLine(stdout), unavailable: false };
|
|
143
99
|
}
|
|
@@ -1,32 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared payload utilities for Claude Code Write/Edit hooks.
|
|
3
|
-
*
|
|
4
|
-
* Extracts file path and content from tool_input payloads,
|
|
5
|
-
* normalizing between Write (content, file_path) and Edit (new_string, old_string, file_path).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
import { createHash } from 'crypto';
|
|
9
2
|
import { existsSync } from 'fs';
|
|
10
3
|
import { dirname, join, isAbsolute } from 'path';
|
|
11
4
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* directory to the nearest ancestor containing one of `markers` (default
|
|
15
|
-
* `package.json` — the tsc/build root). Falls back to `fallbackCwd` when
|
|
16
|
-
* nothing matches or on any error.
|
|
17
|
-
*
|
|
18
|
-
* This exists so a PostToolUse hook typechecks/builds in the CORRECT root when
|
|
19
|
-
* a subagent edits a file inside its own git worktree (`isolation: "worktree"`,
|
|
20
|
-
* morph-apply §3b-w) instead of always assuming the main working tree. In the
|
|
21
|
-
* normal (non-worktree) case it resolves to the same root as `process.cwd()`.
|
|
22
|
-
*
|
|
23
|
-
* Fail-open: never throws — returns `fallbackCwd` on any error.
|
|
24
|
-
*
|
|
25
|
-
* @param {string} filePath - edited file path (absolute or relative)
|
|
26
|
-
* @param {string} fallbackCwd - root to use if resolution fails (usually process.cwd())
|
|
27
|
-
* @param {string[]} [markers] - ancestor marker filenames to look for
|
|
28
|
-
* @returns {string} the resolved root directory
|
|
29
|
-
*/
|
|
5
|
+
// A sub-agent may edit inside its own worktree: the build root comes from the
|
|
6
|
+
// file, not from the session's cwd.
|
|
30
7
|
export function resolveRootForFile(filePath, fallbackCwd, markers = ['package.json']) {
|
|
31
8
|
try {
|
|
32
9
|
if (!filePath) return fallbackCwd;
|
|
@@ -48,33 +25,9 @@ export function resolveRootForFile(filePath, fallbackCwd, markers = ['package.js
|
|
|
48
25
|
}
|
|
49
26
|
}
|
|
50
27
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
*
|
|
55
|
-
* A hook process knows only where the SESSION lives (`process.cwd()` /
|
|
56
|
-
* `CLAUDE_PROJECT_DIR`). That is not the same thing as where the FILE lives: a
|
|
57
|
-
* session rooted in the primary repo can legitimately write to a feature that
|
|
58
|
-
* lives in a secondary git worktree. Resolving state from the process instead
|
|
59
|
-
* of the path is what made `enforce-phase-writes` block a legal `2-plan/` write
|
|
60
|
-
* (the root still had the feature at `proposal`) while `protect-spec-files`
|
|
61
|
-
* simultaneously went DARK there (the root had no approved gate to enforce) —
|
|
62
|
-
* one defect, opposite directions.
|
|
63
|
-
*
|
|
64
|
-
* The anchor is exact, so no marker walk is needed: everything before the
|
|
65
|
-
* file's own `.morph/features/` segment IS the owning root. `lastIndexOf` picks
|
|
66
|
-
* the CLOSEST enclosing `.morph/` — the worktree's, not the root it nests under.
|
|
67
|
-
*
|
|
68
|
-
* Note this cannot be solved by locating the hook file itself: `worktree setup`
|
|
69
|
-
* junctions `.morph/framework/` back to the primary root, so every worktree
|
|
70
|
-
* executes the very same hook file. Only the target path identifies the tree.
|
|
71
|
-
*
|
|
72
|
-
* Fail-open: never throws — returns `fallbackCwd` for any non-feature path.
|
|
73
|
-
*
|
|
74
|
-
* @param {string} filePath - target file path (absolute or relative)
|
|
75
|
-
* @param {string} fallbackCwd - root to use when the path owns no `.morph/`
|
|
76
|
-
* @returns {string} the resolved project root
|
|
77
|
-
*/
|
|
28
|
+
// State is resolved from the target PATH, never the session: a session in the
|
|
29
|
+
// primary root may write a feature that lives in a worktree. `lastIndexOf` picks
|
|
30
|
+
// the closest enclosing `.morph/`.
|
|
78
31
|
export function resolveMorphRoot(filePath, fallbackCwd) {
|
|
79
32
|
try {
|
|
80
33
|
if (!filePath) return fallbackCwd;
|
|
@@ -88,111 +41,41 @@ export function resolveMorphRoot(filePath, fallbackCwd) {
|
|
|
88
41
|
}
|
|
89
42
|
}
|
|
90
43
|
|
|
91
|
-
|
|
92
|
-
* Is this an already-absolute path, on EITHER convention?
|
|
93
|
-
*
|
|
94
|
-
* `path.isAbsolute` is platform-bound: `C:/x` is absolute on win32 and relative
|
|
95
|
-
* on posix. That asymmetry is a real defect here, not a theoretical one — a
|
|
96
|
-
* Windows-shaped path reaching a posix `join()` produces `/cwd/C:/x`, silently
|
|
97
|
-
* resolving state against a directory that cannot exist. Recognizing the drive
|
|
98
|
-
* letter explicitly makes the answer the same everywhere (and is a no-op on
|
|
99
|
-
* win32, where `isAbsolute` already accepts it).
|
|
100
|
-
*/
|
|
44
|
+
// `path.isAbsolute` is platform-bound: on posix `C:/x` would join into `/cwd/C:/x`.
|
|
101
45
|
const isAbsolutePath = (p) => isAbsolute(p) || /^[a-zA-Z]:\//.test(p);
|
|
102
46
|
|
|
103
|
-
/** Normalize to forward slashes and make absolute against `fallbackCwd`. */
|
|
104
47
|
function toAbsolutePosix(filePath, fallbackCwd) {
|
|
105
48
|
const p = String(filePath).replace(/\\/g, '/');
|
|
106
49
|
return isAbsolutePath(p) ? p : join(fallbackCwd, p).replace(/\\/g, '/');
|
|
107
50
|
}
|
|
108
51
|
|
|
109
|
-
/**
|
|
110
|
-
* Absolute path of a hook's target file, resolved against the root that owns
|
|
111
|
-
* it. Pairs with `resolveMorphRoot` so a hook never mixes "the tree I read
|
|
112
|
-
* state from" with "the file I read content from".
|
|
113
|
-
*
|
|
114
|
-
* @param {string} filePath
|
|
115
|
-
* @param {string} fallbackCwd
|
|
116
|
-
* @returns {string}
|
|
117
|
-
*/
|
|
118
52
|
export function resolveTargetPath(filePath, fallbackCwd) {
|
|
119
53
|
return toAbsolutePosix(filePath || '', fallbackCwd);
|
|
120
54
|
}
|
|
121
55
|
|
|
122
|
-
/**
|
|
123
|
-
* Get the file path from a hook payload, normalizing backslashes.
|
|
124
|
-
* @param {Object} payload - Hook payload from stdin
|
|
125
|
-
* @returns {string} Normalized file path or empty string
|
|
126
|
-
*/
|
|
127
56
|
export function getFilePath(payload) {
|
|
128
57
|
const raw = payload?.tool_input?.file_path || payload?.tool_input?.path || '';
|
|
129
58
|
return raw.replace(/\\/g, '/');
|
|
130
59
|
}
|
|
131
60
|
|
|
132
|
-
/**
|
|
133
|
-
* Get the content being written or edited.
|
|
134
|
-
* For Write: returns `content` (full file content).
|
|
135
|
-
* For Edit: returns `new_string` (replacement text).
|
|
136
|
-
* @param {Object} payload - Hook payload from stdin
|
|
137
|
-
* @returns {string} Content string or empty string
|
|
138
|
-
*/
|
|
139
61
|
export function getContentToValidate(payload) {
|
|
140
62
|
const input = payload?.tool_input;
|
|
141
63
|
if (!input) return '';
|
|
142
|
-
// Write tool uses 'content', Edit tool uses 'new_string'
|
|
143
64
|
return input.content || input.new_string || '';
|
|
144
65
|
}
|
|
145
66
|
|
|
146
|
-
/**
|
|
147
|
-
* Check if this is a Write operation (full file content available).
|
|
148
|
-
* @param {Object} payload - Hook payload from stdin
|
|
149
|
-
* @returns {boolean} True if Write (content exists), false if Edit or unknown
|
|
150
|
-
*/
|
|
151
67
|
export function isWriteOperation(payload) {
|
|
152
68
|
return typeof payload?.tool_input?.content === 'string';
|
|
153
69
|
}
|
|
154
70
|
|
|
155
|
-
/**
|
|
156
|
-
* Get the tool name from a hook payload.
|
|
157
|
-
* @param {Object} payload - Hook payload from stdin
|
|
158
|
-
* @returns {string} Tool name or 'unknown'
|
|
159
|
-
*/
|
|
160
71
|
export function getToolName(payload) {
|
|
161
72
|
return payload?.tool_name || 'unknown';
|
|
162
73
|
}
|
|
163
74
|
|
|
164
|
-
/** Assinatura de Bash: comando normalizado, truncado. Ver getCommandSignature. */
|
|
165
75
|
const BASH_SIGNATURE_MAX = 120;
|
|
166
76
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
* - Edit/Write: "Edit:normalized/file/path#<sha1[0:8] do conteúdo>" ("Edit:path" sem conteúdo)
|
|
170
|
-
* - Bash: "Bash:<normalized command, truncated>"
|
|
171
|
-
* - Every other tool WITH input: "Tool:<truncated JSON of tool_input>"
|
|
172
|
-
* - No tool_input at all: just the tool name
|
|
173
|
-
*
|
|
174
|
-
* The general (non-Edit/Write/Bash) case matters: a burst of `Read`/`Skill`/
|
|
175
|
-
* `Grep`/`Agent` calls against DIFFERENT targets (phase-context loading,
|
|
176
|
-
* post-task evaluation reads, parallel subagent dispatch) is normal work, not
|
|
177
|
-
* a loop. Collapsing every one of them to the bare tool name is what made
|
|
178
|
-
* loop-detect fire on exactly that legitimate traffic — a real, reported
|
|
179
|
-
* false positive. Folding in the actual input (rather than hand-listing a
|
|
180
|
-
* target field per tool) fixes it uniformly for every tool at once.
|
|
181
|
-
*
|
|
182
|
-
* **Bash ficou de fora daquele fix (2026-07-22) e continuou colapsando em DUAS
|
|
183
|
-
* PALAVRAS**, o que é quase sempre o nome da ferramenta e nada mais:
|
|
184
|
-
* `npm test -- a`, `npm test -- b` e `npm run build` viravam `Bash:npm test` /
|
|
185
|
-
* `Bash:npm run`. Um ciclo legítimo de mutação — rodar o mesmo runner sobre
|
|
186
|
-
* alvos diferentes, que é exatamente como se testa se um teste morde — disparava
|
|
187
|
-
* o aviso de loop três vezes seguidas. Aviso que toca à toa é aviso que se
|
|
188
|
-
* aprende a ignorar, e aí ele não serve nem quando está certo.
|
|
189
|
-
*
|
|
190
|
-
* O discriminador agora é o comando inteiro (normalizado e truncado), mesma
|
|
191
|
-
* forma dos demais tools. Repetir o comando IDÊNTICO continua sendo loop.
|
|
192
|
-
*
|
|
193
|
-
* @param {Object} payload - Hook payload from stdin
|
|
194
|
-
* @returns {string} Signature string
|
|
195
|
-
*/
|
|
77
|
+
// The input is part of the signature: the same tool on DIFFERENT targets is
|
|
78
|
+
// work, not a loop. Only an identical call repeats the signature.
|
|
196
79
|
export function getCommandSignature(payload) {
|
|
197
80
|
const tool = getToolName(payload);
|
|
198
81
|
if (tool === 'unknown') return 'unknown';
|
|
@@ -203,11 +86,8 @@ export function getCommandSignature(payload) {
|
|
|
203
86
|
if (tool === 'Edit' || tool === 'Write') {
|
|
204
87
|
const path = (input.file_path || input.path || '').replace(/\\/g, '/');
|
|
205
88
|
if (!path) return tool;
|
|
206
|
-
// O caminho sozinho confundia
|
|
207
|
-
//
|
|
208
|
-
// fonte) dava só duas assinaturas e disparava o aviso. O conteúdo da
|
|
209
|
-
// edição entra na assinatura; repetir a MESMA edição continua casando, e o
|
|
210
|
-
// ping-pong A→B / B→A continua dando duas assinaturas — loop de verdade.
|
|
89
|
+
// O caminho sozinho confundia editar regiões diferentes com loop; o
|
|
90
|
+
// ping-pong A→B / B→A continua dando duas assinaturas.
|
|
211
91
|
const body = tool === 'Write'
|
|
212
92
|
? input.content
|
|
213
93
|
: (input.old_string !== undefined || input.new_string !== undefined)
|
|
@@ -218,8 +98,6 @@ export function getCommandSignature(payload) {
|
|
|
218
98
|
}
|
|
219
99
|
|
|
220
100
|
if (tool === 'Bash') {
|
|
221
|
-
// Espaços colapsados para que a mesma invocação escrita com espaçamento
|
|
222
|
-
// diferente continue casando — o alvo é o comando, não a digitação.
|
|
223
101
|
const cmd = (input.command || '').replace(/\s+/g, ' ').trim();
|
|
224
102
|
return cmd ? `Bash:${cmd.slice(0, BASH_SIGNATURE_MAX)}` : 'Bash';
|
|
225
103
|
}
|
|
@@ -1,136 +1,78 @@
|
|
|
1
1
|
// GENERATED by scripts/generate-refs.js — DO NOT EDIT manually
|
|
2
2
|
// Source of truth: src/core/paths/output-schema.js
|
|
3
3
|
// Regenerate with: node scripts/generate-refs.js
|
|
4
|
-
/**
|
|
5
|
-
* Shared phase utilities for Claude Code hooks.
|
|
6
|
-
*
|
|
7
|
-
* Maps phases to directories and output types.
|
|
8
|
-
*/
|
|
9
4
|
|
|
10
|
-
|
|
11
|
-
export const PHASE_ORDER = ["proposal","uiux","plan","implement","review","sync"];
|
|
5
|
+
export const PHASE_ORDER = ["plan","uiux","implement","review"];
|
|
12
6
|
|
|
13
|
-
/** Map phase → allowed output subdirectory */
|
|
14
7
|
export const PHASE_DIRS = {
|
|
15
|
-
proposal: '0-proposal',
|
|
16
|
-
uiux: '1-design',
|
|
17
8
|
plan: '2-plan',
|
|
9
|
+
uiux: '1-design',
|
|
18
10
|
implement: '3-implement',
|
|
19
11
|
review: '4-review',
|
|
20
|
-
sync: '3-implement',
|
|
21
12
|
};
|
|
22
13
|
|
|
23
|
-
/** Map output type (camelCase) → phase that produces it */
|
|
24
14
|
export const OUTPUT_PHASE_MAP = {
|
|
25
|
-
|
|
26
|
-
|
|
15
|
+
plan: 'plan',
|
|
16
|
+
checks: 'plan',
|
|
27
17
|
uiDesignSystem: 'uiux',
|
|
28
18
|
uiMockups: 'uiux',
|
|
29
19
|
uiComponents: 'uiux',
|
|
30
20
|
uiFlows: 'uiux',
|
|
31
|
-
spec: 'plan',
|
|
32
|
-
tasks: 'plan',
|
|
33
|
-
decisions: 'plan',
|
|
34
|
-
usecases: 'plan',
|
|
35
|
-
recap: 'implement',
|
|
36
21
|
evidencias: 'implement',
|
|
37
|
-
|
|
38
|
-
|
|
22
|
+
verifierReport: 'review',
|
|
23
|
+
reviewerReport: 'review',
|
|
24
|
+
securityReport: 'review',
|
|
39
25
|
};
|
|
40
26
|
|
|
41
|
-
/** Map filename → output type (camelCase) */
|
|
42
27
|
export const FILENAME_TO_OUTPUT = {
|
|
43
|
-
'
|
|
44
|
-
'
|
|
28
|
+
'plan.md': 'plan',
|
|
29
|
+
'checks.json': 'checks',
|
|
45
30
|
'design-system.md': 'uiDesignSystem',
|
|
46
31
|
'mockups.md': 'uiMockups',
|
|
47
32
|
'components.md': 'uiComponents',
|
|
48
33
|
'flows.md': 'uiFlows',
|
|
49
|
-
'
|
|
50
|
-
'
|
|
51
|
-
'
|
|
52
|
-
'recap.md': 'recap',
|
|
53
|
-
'review-report.md': 'reviewReport',
|
|
54
|
-
'evaluator-report.md': 'evaluatorReport',
|
|
34
|
+
'verifier.md': 'verifierReport',
|
|
35
|
+
'reviewer.md': 'reviewerReport',
|
|
36
|
+
'security.md': 'securityReport',
|
|
55
37
|
};
|
|
56
38
|
|
|
57
|
-
/** Protected spec files and the approval gate that locks them */
|
|
58
39
|
export const PROTECTED_SPEC_FILES = {
|
|
40
|
+
'plan.md': 'plan',
|
|
41
|
+
'checks.json': 'plan',
|
|
59
42
|
'design-system.md': 'uiux',
|
|
60
43
|
'mockups.md': 'uiux',
|
|
61
44
|
'components.md': 'uiux',
|
|
62
45
|
'flows.md': 'uiux',
|
|
63
|
-
'spec.md': 'plan',
|
|
64
|
-
'tasks.json': 'plan',
|
|
65
46
|
};
|
|
66
47
|
|
|
67
|
-
/**
|
|
68
|
-
* For protected files that carry mutable progress fields: filename → the
|
|
69
|
-
* top-level array-item fields that may change after the gate is approved.
|
|
70
|
-
* When a protected file is listed here, the protect-spec-files hook does a
|
|
71
|
-
* semantic diff (allowing only these fields to change) instead of a blanket block.
|
|
72
|
-
*/
|
|
73
|
-
export const MUTABLE_SPEC_FIELDS = {
|
|
74
|
-
'tasks.json': ['status', 'outputs', 'notes'],
|
|
75
|
-
};
|
|
76
48
|
|
|
77
|
-
/**
|
|
78
|
-
* Extract feature name from a .morph/features/{feature}/... path
|
|
79
|
-
* @param {string} filePath - File path to analyze
|
|
80
|
-
* @returns {string|null} Feature name or null
|
|
81
|
-
*/
|
|
82
49
|
export function extractFeatureName(filePath) {
|
|
83
50
|
const normalized = filePath.replace(/\\/g, '/');
|
|
84
51
|
const match = normalized.match(/\.morph\/features\/([^/]+)\//);
|
|
85
52
|
return match ? match[1] : null;
|
|
86
53
|
}
|
|
87
54
|
|
|
88
|
-
/**
|
|
89
|
-
* Extract phase subdirectory from a .morph/features/{feature}/{phaseDir}/... path
|
|
90
|
-
* @param {string} filePath - File path to analyze
|
|
91
|
-
* @returns {string|null} Phase directory (e.g., '0-proposal', '2-design') or null
|
|
92
|
-
*/
|
|
93
55
|
export function extractPhaseDir(filePath) {
|
|
94
56
|
const normalized = filePath.replace(/\\/g, '/');
|
|
95
57
|
const match = normalized.match(/\.morph\/features\/[^/]+\/(\d+-[^/]+)\//);
|
|
96
58
|
return match ? match[1] : null;
|
|
97
59
|
}
|
|
98
60
|
|
|
99
|
-
/**
|
|
100
|
-
* Check if a file path is inside .morph/features/
|
|
101
|
-
* @param {string} filePath
|
|
102
|
-
* @returns {boolean}
|
|
103
|
-
*/
|
|
104
61
|
export function isFeaturePath(filePath) {
|
|
105
62
|
const normalized = filePath.replace(/\\/g, '/');
|
|
106
63
|
return normalized.includes('.morph/features/');
|
|
107
64
|
}
|
|
108
65
|
|
|
109
|
-
/**
|
|
110
|
-
* Check if a file path is inside .morph/framework/ (readonly)
|
|
111
|
-
* @param {string} filePath
|
|
112
|
-
* @returns {boolean}
|
|
113
|
-
*/
|
|
114
66
|
export function isFrameworkPath(filePath) {
|
|
115
67
|
const normalized = filePath.replace(/\\/g, '/');
|
|
116
68
|
return normalized.includes('.morph/framework/');
|
|
117
69
|
}
|
|
118
70
|
|
|
119
|
-
/**
|
|
120
|
-
* Check if a file path is state.json
|
|
121
|
-
* @param {string} filePath
|
|
122
|
-
* @returns {boolean}
|
|
123
|
-
*/
|
|
124
71
|
export function isStatePath(filePath) {
|
|
125
72
|
const normalized = filePath.replace(/\\/g, '/');
|
|
126
73
|
return normalized.endsWith('.morph/state.json');
|
|
127
74
|
}
|
|
128
75
|
|
|
129
|
-
/**
|
|
130
|
-
* Get the basename of a file path
|
|
131
|
-
* @param {string} filePath
|
|
132
|
-
* @returns {string}
|
|
133
|
-
*/
|
|
134
76
|
export function getBasename(filePath) {
|
|
135
77
|
const normalized = filePath.replace(/\\/g, '/');
|
|
136
78
|
return normalized.split('/').pop();
|
|
@@ -1,14 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* under `.git/`, so whatever lands there outlives the session.
|
|
5
|
-
*
|
|
6
|
-
* Pattern-based by design: it catches a secret written NEXT TO its name
|
|
7
|
-
* (`KEY=…`, `Authorization: …`, `user:pass@`) or in a well-known token shape.
|
|
8
|
-
* A bare value with no name beside it is invisible to any pattern — which is
|
|
9
|
-
* why tool OUTPUT (stdout/stderr, a failure's `error` body) is never persisted
|
|
10
|
-
* at all instead of being redacted.
|
|
11
|
-
*/
|
|
1
|
+
// Catches a secret written next to its name or in a known token shape; a bare
|
|
2
|
+
// value is invisible to any pattern, which is why tool OUTPUT is never
|
|
3
|
+
// persisted at all instead of being redacted.
|
|
12
4
|
|
|
13
5
|
export const REDACTED = '[REDACTED]';
|
|
14
6
|
|
|
@@ -43,10 +35,7 @@ const RULES = [
|
|
|
43
35
|
[/\beyJ[A-Za-z0-9_-]{5,}\.eyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]+/g, REDACTED],
|
|
44
36
|
];
|
|
45
37
|
|
|
46
|
-
/**
|
|
47
|
-
* @param {*} text
|
|
48
|
-
* @returns {*} the text with secrets replaced by `[REDACTED]`; non-strings unchanged
|
|
49
|
-
*/
|
|
38
|
+
/** Non-strings pass through unchanged. */
|
|
50
39
|
export function redactSecrets(text) {
|
|
51
40
|
if (typeof text !== 'string' || text === '') return text;
|
|
52
41
|
let out = text;
|