@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
|
@@ -1,40 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* worktree-context.js — SessionStart hook
|
|
4
|
-
*
|
|
5
|
-
* Event: SessionStart
|
|
6
|
-
*
|
|
7
|
-
* v8 runs each feature in its own git worktree (`worktrees/{feature}/` on
|
|
8
|
-
* branch `morph/{feature}`), each with its own Claude Code instance. When a
|
|
9
|
-
* session starts inside one of those worktrees, Claude has no built-in way to
|
|
10
|
-
* know which feature it belongs to — this hook injects that identity so the
|
|
11
|
-
* agent doesn't have to guess (or worse, wander into another feature's
|
|
12
|
-
* `.morph/`).
|
|
13
|
-
*
|
|
14
|
-
* This also fulfills the SessionStart injection role long documented in
|
|
15
|
-
* `framework/hooks/README.md` as `inject-morph-context.js` — a hook that was
|
|
16
|
-
* described but never registered in MORPH_HOOKS. In the primary root (not a
|
|
17
|
-
* worktree) this hook covers that same ground: a compact active-feature
|
|
18
|
-
* summary so a fresh session doesn't have to re-read state.json by hand.
|
|
19
|
-
*
|
|
20
|
-
* Two branches:
|
|
21
|
-
* 1. Secondary worktree → feature identity (name, branch, phase) +
|
|
22
|
-
* a warning if shared infra junctions (node_modules/.claude) are
|
|
23
|
-
* missing (they're created by `morph-spec worktree link --all`).
|
|
24
|
-
* 2. Primary root → active-feature summary (phase, task stats, pending
|
|
25
|
-
* gates), or silent if there's no feature in progress.
|
|
26
|
-
*
|
|
27
|
-
* Also the real call site for activity-log rotation (R1.2 of the performance
|
|
28
|
-
* plan): SessionStart is the only reliable "between sessions" checkpoint the
|
|
29
|
-
* harness gets, so this is where `.morph/logs/activity.jsonl` gets archived
|
|
30
|
-
* to `.morph/logs/sessions/` once it crosses the size threshold. The same
|
|
31
|
-
* checkpoint now also rotates `.morph/logs/events.jsonl` (telemetry-logger.js)
|
|
32
|
-
* once IT crosses its own threshold — events.jsonl grew unbounded even after
|
|
33
|
-
* R1.2, since that rotation only ever covered activity.jsonl.
|
|
34
|
-
*
|
|
35
|
-
* Fail-open: any error, or nothing to report, → pass() silently. This hook
|
|
36
|
-
* never blocks a session from starting.
|
|
37
|
-
*/
|
|
38
2
|
|
|
39
3
|
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
40
4
|
import { join, resolve } from 'path';
|
|
@@ -80,7 +44,6 @@ function gitRevParse(arg, dir) {
|
|
|
80
44
|
.trim();
|
|
81
45
|
}
|
|
82
46
|
|
|
83
|
-
/** Feature name from the current branch, if it matches `morph/{feature}`. */
|
|
84
47
|
function featureFromBranch(dir) {
|
|
85
48
|
try {
|
|
86
49
|
const branch = gitRevParse('--abbrev-ref HEAD', dir);
|
|
@@ -91,12 +54,6 @@ function featureFromBranch(dir) {
|
|
|
91
54
|
}
|
|
92
55
|
}
|
|
93
56
|
|
|
94
|
-
/**
|
|
95
|
-
* Task-isolation worktree (`morph-task/{name}`): the tree a `WorktreeCreate`
|
|
96
|
-
* hook provisions for a parallel sub-agent batch (morph-apply §3b-w) or for a
|
|
97
|
-
* `claude -w` session with no feature behind it. Not a feature — no
|
|
98
|
-
* feature.json, no gates — but it owns a port block and the same junctions.
|
|
99
|
-
*/
|
|
100
57
|
function taskFromBranch(dir) {
|
|
101
58
|
try {
|
|
102
59
|
const branch = gitRevParse('--abbrev-ref HEAD', dir);
|
|
@@ -107,14 +64,7 @@ function taskFromBranch(dir) {
|
|
|
107
64
|
}
|
|
108
65
|
}
|
|
109
66
|
|
|
110
|
-
|
|
111
|
-
* Is this secondary worktree inside the primary root's `worktrees/` (or the
|
|
112
|
-
* legacy `.worktrees/`) container? A tree kept anywhere else — `.claude/
|
|
113
|
-
* worktrees/` from a native Claude Code run before the hook existed, a
|
|
114
|
-
* sibling `../repo-x`, superpowers' `~/.config/...` — was created without
|
|
115
|
-
* morph: no junctions, no ports, invisible to `fleet`. Fail-open: any error
|
|
116
|
-
* reads as "in container" so a healthy tree is never nagged by mistake.
|
|
117
|
-
*/
|
|
67
|
+
// Any error reads as "in container" so a healthy tree is never nagged.
|
|
118
68
|
function isOutsideContainer(dir) {
|
|
119
69
|
try {
|
|
120
70
|
const common = resolve(dir, gitRevParse('--git-common-dir', dir)).replace(/\\/g, '/');
|
|
@@ -143,7 +93,6 @@ function buildTaskContext(dir, task) {
|
|
|
143
93
|
return text;
|
|
144
94
|
}
|
|
145
95
|
|
|
146
|
-
/** Fallback: if exactly one feature directory exists, assume that's the one. */
|
|
147
96
|
function featureFromSingleDir(dir) {
|
|
148
97
|
try {
|
|
149
98
|
const entries = readdirSync(join(dir, '.morph', 'features'), { withFileTypes: true })
|
|
@@ -154,12 +103,6 @@ function featureFromSingleDir(dir) {
|
|
|
154
103
|
}
|
|
155
104
|
}
|
|
156
105
|
|
|
157
|
-
/**
|
|
158
|
-
* Fleet env line: the worktree's allocated port block, read from the manifest
|
|
159
|
-
* `worktree setup` writes. Fail-open — a missing or malformed manifest just
|
|
160
|
-
* omits the line (the session then uses project defaults, which is exactly
|
|
161
|
-
* the pre-fleet behavior).
|
|
162
|
-
*/
|
|
163
106
|
function buildEnvLine(dir) {
|
|
164
107
|
try {
|
|
165
108
|
const envPath = join(dir, '.morph', 'worktree.env');
|
|
@@ -178,10 +121,6 @@ function buildEnvLine(dir) {
|
|
|
178
121
|
}
|
|
179
122
|
}
|
|
180
123
|
|
|
181
|
-
/**
|
|
182
|
-
* Fleet handoff line: point the fresh session at the compressed handoff doc
|
|
183
|
-
* before it starts re-deriving context by hand. Fail-open.
|
|
184
|
-
*/
|
|
185
124
|
function buildHandoffLine(dir, feature) {
|
|
186
125
|
try {
|
|
187
126
|
const handoffPath = join(dir, '.morph', 'features', feature, 'handoff.md');
|
|
@@ -209,13 +148,7 @@ function buildWorktreeContext(dir, feature) {
|
|
|
209
148
|
return text;
|
|
210
149
|
}
|
|
211
150
|
|
|
212
|
-
|
|
213
|
-
* `.morph/framework` is checked alongside the other two because the hook
|
|
214
|
-
* commands resolve to `$CLAUDE_PROJECT_DIR/.morph/framework/hooks/...`: if
|
|
215
|
-
* that junction is missing, `node` finds no file and EVERY guardrail in this
|
|
216
|
-
* worktree is silently dark — the worst failure mode of the lot, and the one
|
|
217
|
-
* the old two-item check could not see.
|
|
218
|
-
*/
|
|
151
|
+
// The hook commands resolve through the .morph/framework junction: without it every guardrail here is silently off.
|
|
219
152
|
function buildMissingInfraWarning(dir) {
|
|
220
153
|
const missing = ['node_modules', '.claude', join('.morph', 'framework')]
|
|
221
154
|
.filter((rel) => !existsSync(join(dir, rel)));
|
|
@@ -243,20 +176,7 @@ function buildActiveFeatureSummary(dir) {
|
|
|
243
176
|
return text;
|
|
244
177
|
}
|
|
245
178
|
|
|
246
|
-
|
|
247
|
-
* Advisory feature ownership (B1): claim the feature this session is about to
|
|
248
|
-
* work on, and — if another session already holds a LIVE claim — say so loudly
|
|
249
|
-
* before any work starts.
|
|
250
|
-
*
|
|
251
|
-
* Never blocks: the claim is a warning, not a lock (see claim-registry.js).
|
|
252
|
-
* The claim is taken regardless, so the registry reflects reality; the operator
|
|
253
|
-
* decides what to do with the collision.
|
|
254
|
-
*
|
|
255
|
-
* Fail-open per FIELD: no git common dir, or any registry error, just means no
|
|
256
|
-
* claim line — never a failed session start.
|
|
257
|
-
*
|
|
258
|
-
* @returns {string} a block to append to the injected context ('' when quiet)
|
|
259
|
-
*/
|
|
179
|
+
// Advisory: the claim is taken even on collision; it warns, never locks.
|
|
260
180
|
function claimBlock(dir, feature, sessionId) {
|
|
261
181
|
try {
|
|
262
182
|
const gitCommonDir = resolve(dir, gitRevParse('--git-common-dir', dir));
|
|
@@ -274,37 +194,21 @@ function claimBlock(dir, feature, sessionId) {
|
|
|
274
194
|
}
|
|
275
195
|
}
|
|
276
196
|
|
|
277
|
-
/**
|
|
278
|
-
* @param {object} payload - parsed SessionStart stdin payload
|
|
279
|
-
* @returns {Promise<object|null>} a hookSpecificOutput context (worktree
|
|
280
|
-
* identity or active-feature summary), or null (pass/silent).
|
|
281
|
-
*/
|
|
282
197
|
export async function run(payload) {
|
|
283
198
|
try {
|
|
284
199
|
const dir = sessionDir(payload);
|
|
285
|
-
// The SessionStart payload carries session_id — the identity the claim
|
|
286
|
-
// registry uses to tell "me" from "another session".
|
|
287
200
|
const sessionId = payload?.session_id || `pid-${process.pid}`;
|
|
288
201
|
|
|
289
202
|
if (!stateExists(dir)) {
|
|
290
203
|
return null;
|
|
291
204
|
}
|
|
292
205
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
//
|
|
296
|
-
try {
|
|
297
|
-
try {
|
|
298
|
-
|
|
299
|
-
// IDE, a `git pull`) are caught: the detached refresh brings the ROOT's
|
|
300
|
-
// graph to HEAD — never a worktree's (a detached process holding a
|
|
301
|
-
// worktree directory made `finish` fail on win32). A worktree that has no
|
|
302
|
-
// graph yet (created before seeding existed) gets the root's copy here;
|
|
303
|
-
// from then on it is refreshed by hand (`morph-spec graph refresh`).
|
|
304
|
-
// Returns immediately — nothing here waits on graphify.
|
|
305
|
-
try { seedFromPrimary(dir); } catch { /* non-blocking — a no-op on the root */ }
|
|
306
|
-
try { kickGraphRefresh(dir); } catch { /* non-blocking */ }
|
|
307
|
-
try { kickAgentReap(dir); } catch { /* non-blocking */ }
|
|
206
|
+
try { rotateActivityLogIfLarge(dir); } catch {}
|
|
207
|
+
try { rotateEventsLogIfLarge(dir); } catch {}
|
|
208
|
+
// Only the ROOT's graph is refreshed in background: a detached process holding a worktree directory made `finish` fail on win32.
|
|
209
|
+
try { seedFromPrimary(dir); } catch {}
|
|
210
|
+
try { kickGraphRefresh(dir); } catch {}
|
|
211
|
+
try { kickAgentReap(dir); } catch {}
|
|
308
212
|
|
|
309
213
|
if (isSecondaryWorktree(dir)) {
|
|
310
214
|
const task = taskFromBranch(dir);
|
|
@@ -313,16 +217,12 @@ export async function run(payload) {
|
|
|
313
217
|
}
|
|
314
218
|
const feature = featureFromBranch(dir) || featureFromSingleDir(dir);
|
|
315
219
|
if (!feature) {
|
|
316
|
-
//
|
|
317
|
-
// also lives outside the container — that is the signature of a
|
|
318
|
-
// worktree created behind morph's back (native tool, raw git, another
|
|
319
|
-
// skill). Inside the container with no identity → stay silent.
|
|
220
|
+
// No identity AND outside the container is the signature of a worktree created behind morph's back.
|
|
320
221
|
return isOutsideContainer(dir) ? contextResult(buildOutOfContainerWarning(dir), 'SessionStart') : null;
|
|
321
222
|
}
|
|
322
223
|
return contextResult(buildWorktreeContext(dir, feature) + claimBlock(dir, feature, sessionId), 'SessionStart');
|
|
323
224
|
}
|
|
324
225
|
|
|
325
|
-
// Primary root — active-feature summary (or silent if nothing's in progress)
|
|
326
226
|
const summary = buildActiveFeatureSummary(dir);
|
|
327
227
|
if (!summary) {
|
|
328
228
|
return null;
|
|
@@ -1,40 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* worktree-create.js — WorktreeCreate hook
|
|
4
|
-
*
|
|
5
|
-
* Event: WorktreeCreate (fires for `claude -w`, the EnterWorktree tool, and
|
|
6
|
-
* `Agent isolation:"worktree"` — measured, not assumed).
|
|
7
|
-
*
|
|
8
|
-
* Claude Code's native worktree is a bare `git worktree add` somewhere under
|
|
9
|
-
* `.claude/worktrees/`: no infra junctions (so no hooks run in there), no port
|
|
10
|
-
* block, no identity, invisible to the fleet. When this hook is registered it
|
|
11
|
-
* REPLACES that logic entirely: it must print the path of the tree it created
|
|
12
|
-
* as the last non-empty stdout line and exit 0; any non-zero exit fails the
|
|
13
|
-
* creation. It cannot return JSON — the standalone tail below is therefore
|
|
14
|
-
* NOT `runStandalone` (which prints a JSON verdict). Diagnostics go to stderr.
|
|
15
|
-
*
|
|
16
|
-
* What it does:
|
|
17
|
-
* 1. root = the PRIMARY root of the repo `payload.cwd` is in (never a
|
|
18
|
-
* secondary worktree — a tree under a tree is the fleet-invisible shape).
|
|
19
|
-
* 2. name → kebab slug (`Bold_Oak` → `bold-oak`, `agent-a1b2` stays).
|
|
20
|
-
* 3. feature or task? `<root>/.morph/features/<n>/feature.json` exists, or
|
|
21
|
-
* branch `morph/<n>` exists → `morph-spec worktree setup <n>`; otherwise
|
|
22
|
-
* `morph-spec worktree provision <n> [--base <ref>]`.
|
|
23
|
-
* 4. `worktree.baseRef` from the merged settings (`~/.claude/settings.json`
|
|
24
|
-
* < `<root>/.claude/settings.json` < `<root>/.claude/settings.local.json`):
|
|
25
|
-
* `"head"` → the current branch (or sha) of `payload.cwd`; unset and
|
|
26
|
-
* `payload.cwd` on `morph/<f>` → that feature branch; `"fresh"` or
|
|
27
|
-
* anything else → the repo's default branch (the CLI's own default).
|
|
28
|
-
* 5. CLI exit 0 or 2 (already exists) → its `path`. Exit 1/3 → error.
|
|
29
|
-
* CLI unavailable → in-hook git fallback that still lands the tree under
|
|
30
|
-
* `<root>/worktrees/<n>` on `morph-task/<n>` (degraded: no junctions, no
|
|
31
|
-
* ports — but never outside the governed directory).
|
|
32
|
-
*
|
|
33
|
-
* `stateExists(root)` gate: outside a morph project (no `.morph/state.json`
|
|
34
|
-
* and no feature.json) there is nothing to govern — the same in-hook git
|
|
35
|
-
* fallback runs, so a non-morph repo with this hook installed still gets a
|
|
36
|
-
* worktree instead of a failed creation.
|
|
37
|
-
*/
|
|
38
2
|
|
|
39
3
|
import { existsSync, mkdirSync, readFileSync } from 'fs';
|
|
40
4
|
import { execFileSync } from 'child_process';
|
|
@@ -51,21 +15,16 @@ function git(args, cwd) {
|
|
|
51
15
|
return execFileSync('git', args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] }).toString().trim();
|
|
52
16
|
}
|
|
53
17
|
|
|
54
|
-
|
|
55
|
-
* The primary root of the repo containing `startDir`. `git worktree list`'s
|
|
56
|
-
* first entry is the primary checkout, from any worktree; the pure-filesystem
|
|
57
|
-
* common-dir resolver is the no-subprocess fallback. Null outside a repo.
|
|
58
|
-
*/
|
|
18
|
+
// `git worktree list`'s first entry is the primary checkout, from any worktree.
|
|
59
19
|
export function resolvePrimaryRoot(startDir) {
|
|
60
20
|
try {
|
|
61
21
|
const first = git(['worktree', 'list', '--porcelain'], startDir).split('\n').find((l) => l.startsWith('worktree '));
|
|
62
22
|
if (first) return fwd(first.slice('worktree '.length).trim());
|
|
63
|
-
} catch {
|
|
23
|
+
} catch {}
|
|
64
24
|
const common = resolveGitCommonDir(startDir);
|
|
65
25
|
return common ? fwd(dirname(common)) : null;
|
|
66
26
|
}
|
|
67
27
|
|
|
68
|
-
/** `Bold_Oak` → `bold-oak`; empty → a timestamped slug. */
|
|
69
28
|
export function slugify(name) {
|
|
70
29
|
const slug = String(name || '')
|
|
71
30
|
.toLowerCase()
|
|
@@ -79,11 +38,7 @@ function readJsonSafe(path) {
|
|
|
79
38
|
try { return JSON.parse(readFileSync(path, 'utf8').replace(/^/, '')); } catch { return null; }
|
|
80
39
|
}
|
|
81
40
|
|
|
82
|
-
|
|
83
|
-
* `worktree.baseRef` across the three settings layers, last one wins.
|
|
84
|
-
* Fail-open per file. `null` when unset — distinct from an explicit `'fresh'`,
|
|
85
|
-
* which overrides the feature-branch default (see `resolveBase`).
|
|
86
|
-
*/
|
|
41
|
+
// `null` when unset, distinct from an explicit `'fresh'`, which overrides the feature-branch default.
|
|
87
42
|
export function readBaseRefSetting(root, home = homedir()) {
|
|
88
43
|
let value = null;
|
|
89
44
|
for (const p of [
|
|
@@ -98,7 +53,6 @@ export function readBaseRefSetting(root, home = homedir()) {
|
|
|
98
53
|
return value;
|
|
99
54
|
}
|
|
100
55
|
|
|
101
|
-
/** The ref `baseRef:"head"` means: the branch checked out in `cwd`, or its sha. */
|
|
102
56
|
function headRef(cwd) {
|
|
103
57
|
try {
|
|
104
58
|
const name = git(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
|
|
@@ -109,14 +63,7 @@ function headRef(cwd) {
|
|
|
109
63
|
}
|
|
110
64
|
}
|
|
111
65
|
|
|
112
|
-
|
|
113
|
-
* The base of a task tree. `"head"` → whatever `cwd` has checked out;
|
|
114
|
-
* `"fresh"` (or any other explicit value) → the default branch; unset → the
|
|
115
|
-
* `morph/<f>` branch `cwd` is on, when it is one. A sub-agent dispatched by
|
|
116
|
-
* the apply needs the feature's work, and a tree born from the default branch
|
|
117
|
-
* made every dispatch open with `git merge --ff-only morph/<f>`.
|
|
118
|
-
* `morph-task/*` does not match: a tree of a tree tips at the default.
|
|
119
|
-
*/
|
|
66
|
+
// Unset → the `morph/<f>` branch `cwd` is on: a sub-agent tree born from the default branch lacks the feature's work.
|
|
120
67
|
export function resolveBase(setting, cwd) {
|
|
121
68
|
if (setting === 'head') return headRef(cwd);
|
|
122
69
|
if (setting) return null;
|
|
@@ -138,17 +85,13 @@ function defaultBranch(root) {
|
|
|
138
85
|
try {
|
|
139
86
|
const m = git(['symbolic-ref', '--quiet', 'refs/remotes/origin/HEAD'], root).match(/^refs\/remotes\/origin\/(.+)$/);
|
|
140
87
|
if (m) return m[1];
|
|
141
|
-
} catch {
|
|
88
|
+
} catch {}
|
|
142
89
|
for (const c of ['main', 'master']) {
|
|
143
|
-
try { git(['show-ref', '--verify', '--quiet', `refs/heads/${c}`], root); return c; } catch {
|
|
90
|
+
try { git(['show-ref', '--verify', '--quiet', `refs/heads/${c}`], root); return c; } catch {}
|
|
144
91
|
}
|
|
145
92
|
return null;
|
|
146
93
|
}
|
|
147
94
|
|
|
148
|
-
/**
|
|
149
|
-
* Degraded path — the CLI is not reachable, but the tree still has to land
|
|
150
|
-
* in the governed directory on the governed branch namespace.
|
|
151
|
-
*/
|
|
152
95
|
function gitFallback(root, name, base) {
|
|
153
96
|
const branch = `morph-task/${name}`;
|
|
154
97
|
const container = newWorktreesDir(root);
|
|
@@ -164,11 +107,6 @@ function gitFallback(root, name, base) {
|
|
|
164
107
|
return { path };
|
|
165
108
|
}
|
|
166
109
|
|
|
167
|
-
/**
|
|
168
|
-
* @param {object} payload - parsed WorktreeCreate stdin payload
|
|
169
|
-
* ({ session_id, cwd, hook_event_name, name })
|
|
170
|
-
* @returns {Promise<{ path: string }|{ error: string }>}
|
|
171
|
-
*/
|
|
172
110
|
export async function run(payload) {
|
|
173
111
|
const start = payload?.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
174
112
|
const root = resolvePrimaryRoot(start) || (process.env.CLAUDE_PROJECT_DIR ? fwd(resolve(process.env.CLAUDE_PROJECT_DIR)) : null);
|
|
@@ -177,8 +115,6 @@ export async function run(payload) {
|
|
|
177
115
|
const name = slugify(payload?.name);
|
|
178
116
|
const base = resolveBase(readBaseRefSetting(root), start);
|
|
179
117
|
|
|
180
|
-
// Outside a morph project there is nothing to govern beyond WHERE the tree
|
|
181
|
-
// lands — plain git, under <root>/worktrees/.
|
|
182
118
|
if (!stateExists(root)) {
|
|
183
119
|
try { return gitFallback(root, name, base); }
|
|
184
120
|
catch (err) { return { error: `worktree-create: ${err?.message || err}` }; }
|
|
@@ -1,34 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Bash chain.
|
|
5
|
-
*
|
|
6
|
-
* `.morph/worktree.env` used to be a manifest nobody executed: SessionStart
|
|
7
|
-
* printed the port block and the model was expected to pass `PORT` by hand
|
|
8
|
-
* every time it started a dev server. Two worktrees whose sessions forgot
|
|
9
|
-
* collided on 3000 — a runtime clash dressed as a broken app. This hook makes
|
|
10
|
-
* the block reach the process deterministically: when the command is a
|
|
11
|
-
* dev-server / compose / spec launcher and the cwd is a worktree that owns a
|
|
12
|
-
* manifest, the command is rewritten to `export K='V' …; <command>` via
|
|
13
|
-
* `updatedInput`, and the rewrite is named in `additionalContext`.
|
|
14
|
-
*
|
|
15
|
-
* Scope is deliberately narrow: only launcher-shaped commands, only when the
|
|
16
|
-
* command does not already set a layout key itself (`PORT=…`, `--port`,
|
|
17
|
-
* `compose -p`), only for the Bash tool. Everything else passes untouched.
|
|
18
|
-
*
|
|
19
|
-
* Second job, same mechanism — the SESSION ANCHOR. `morph-spec finish` and
|
|
20
|
-
* `morph-spec worktree remove` tear a worktree down, and the session that runs
|
|
21
|
-
* them may be ANCHORED in that very worktree: its CLAUDE_PROJECT_DIR is the
|
|
22
|
-
* worktree, and every project hook is `node "$CLAUDE_PROJECT_DIR/.morph/framework/…"`.
|
|
23
|
-
* The CLI cannot see that anchor (the Bash tool does not export
|
|
24
|
-
* CLAUDE_PROJECT_DIR, and `cd <root> && morph-spec finish` erases the cwd), so
|
|
25
|
-
* it unlinked the junction the session's hooks resolve through — and every hook
|
|
26
|
-
* of every later tool call died with MODULE_NOT_FOUND (48 in one real session).
|
|
27
|
-
* Hooks DO see the anchor, so this one exports it as MORPH_SESSION_PROJECT_DIR
|
|
28
|
-
* on those two commands; the CLI then defers the teardown.
|
|
29
|
-
*
|
|
30
|
-
* Fail-open: no state, no manifest, unreadable manifest → null (pass).
|
|
31
|
-
*/
|
|
2
|
+
// Launchers get the worktree's port block exported; teardowns get the session anchor, because the CLI cannot see
|
|
3
|
+
// CLAUDE_PROJECT_DIR and tearing down the worktree a session is anchored in kills every later hook (MODULE_NOT_FOUND).
|
|
32
4
|
|
|
33
5
|
import { existsSync } from 'fs';
|
|
34
6
|
import { join } from 'path';
|
|
@@ -36,47 +8,30 @@ import { stateExists } from '../../shared/state-reader.js';
|
|
|
36
8
|
import { updatedInputResult, runStandalone } from '../../shared/hook-response.js';
|
|
37
9
|
import { loadEnvFile } from '../../shared/e2e-check.js';
|
|
38
10
|
|
|
39
|
-
/** Commands that start a server or a stack and therefore need the block. */
|
|
40
11
|
const LAUNCHER_RE = /\b(?:(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:dev|start)\b|next\s+(?:dev|start)\b|vite\b|dotnet\s+(?:run|watch)\b|docker(?:\s+|-)compose\b|playwright\s+test\b)/;
|
|
41
12
|
|
|
42
|
-
/** The command already chose its own port/project — respect it. */
|
|
43
13
|
const SELF_CONFIGURED_RE = /(?:^|[\s;&|])(?:PORT|API_PORT|POSTGRES_PORT|COMPOSE_PROJECT_NAME|ASPNETCORE_URLS)=|--port\b|(?:^|\s)-p\s+\d|compose\b[^|;&]*\s-p\s/;
|
|
44
14
|
|
|
45
|
-
|
|
15
|
+
// POSIX single quote, safe for Git Bash on Windows too.
|
|
46
16
|
function shq(v) {
|
|
47
17
|
return `'${String(v).replace(/'/g, "'\\''")}'`;
|
|
48
18
|
}
|
|
49
19
|
|
|
50
|
-
/**
|
|
51
|
-
* Pure decision: the rewritten command, or null when nothing should change.
|
|
52
|
-
* @param {string} command
|
|
53
|
-
* @param {Record<string,string>} env - parsed manifest
|
|
54
|
-
* @returns {{ command: string, injected: string[] }|null}
|
|
55
|
-
*/
|
|
56
20
|
export function rewriteCommand(command, env) {
|
|
57
21
|
if (!command || !LAUNCHER_RE.test(command)) return null;
|
|
58
22
|
if (SELF_CONFIGURED_RE.test(command)) return null;
|
|
59
23
|
const pairs = Object.keys(env).filter((k) => k !== 'MORPH_FEATURE' && env[k]).map((k) => [k, env[k]]);
|
|
60
24
|
if (!pairs.some(([k]) => k === 'PORT')) return null;
|
|
61
|
-
// The web tier's URL is what the spec suite needs; derive it once here.
|
|
62
25
|
if (!env.PLAYWRIGHT_BASE_URL) pairs.push(['PLAYWRIGHT_BASE_URL', `http://localhost:${env.PORT}`]);
|
|
63
26
|
const prefix = `export ${pairs.map(([k, v]) => `${k}=${shq(v)}`).join(' ')}; `;
|
|
64
27
|
return { command: prefix + command, injected: pairs.map(([k, v]) => `${k}=${v}`) };
|
|
65
28
|
}
|
|
66
29
|
|
|
67
|
-
|
|
30
|
+
// Also matches `node …/morph-spec.js` and `npx morph-spec`.
|
|
68
31
|
const TEARDOWN_RE = /\bmorph-spec(?:\.js)?\b[^\n;&|]*?\s(?:finish|worktree\s+remove)\b/;
|
|
69
32
|
|
|
70
33
|
const ANCHOR_KEY = 'MORPH_SESSION_PROJECT_DIR';
|
|
71
34
|
|
|
72
|
-
/**
|
|
73
|
-
* Pure decision: export the session anchor in front of a teardown command, or
|
|
74
|
-
* null when nothing should change (other command, anchor already declared,
|
|
75
|
-
* no anchor to give).
|
|
76
|
-
* @param {string} command
|
|
77
|
-
* @param {string} anchorDir - the session's project dir (CLAUDE_PROJECT_DIR)
|
|
78
|
-
* @returns {{ command: string, anchor: string }|null}
|
|
79
|
-
*/
|
|
80
35
|
export function anchorSessionCommand(command, anchorDir) {
|
|
81
36
|
if (!command || !anchorDir) return null;
|
|
82
37
|
if (!TEARDOWN_RE.test(command)) return null;
|
|
@@ -84,10 +39,6 @@ export function anchorSessionCommand(command, anchorDir) {
|
|
|
84
39
|
return { command: `export ${ANCHOR_KEY}=${shq(anchorDir)}; ${command}`, anchor: anchorDir };
|
|
85
40
|
}
|
|
86
41
|
|
|
87
|
-
/**
|
|
88
|
-
* @param {object} payload - parsed PreToolUse stdin payload (Bash matcher)
|
|
89
|
-
* @returns {Promise<object|null>} an updatedInput result, or null (pass).
|
|
90
|
-
*/
|
|
91
42
|
export async function run(payload) {
|
|
92
43
|
try {
|
|
93
44
|
if (payload?.tool_name && payload.tool_name !== 'Bash') return null;
|
|
@@ -95,9 +46,7 @@ export async function run(payload) {
|
|
|
95
46
|
const command = payload?.tool_input?.command || '';
|
|
96
47
|
if (!command) return null;
|
|
97
48
|
|
|
98
|
-
// CLAUDE_PROJECT_DIR first: it is what the project hooks resolve
|
|
99
|
-
// `.morph/framework` against — the thing a teardown would pull out from
|
|
100
|
-
// under this session. The payload cwd is the fallback.
|
|
49
|
+
// CLAUDE_PROJECT_DIR first: it is what the project hooks resolve `.morph/framework` against.
|
|
101
50
|
const anchored = anchorSessionCommand(command, process.env.CLAUDE_PROJECT_DIR || payload?.cwd || '');
|
|
102
51
|
if (anchored) {
|
|
103
52
|
return updatedInputResult(
|
|
@@ -1,25 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
* Event: WorktreeRemove (Claude Code's own cleanup of a tree it created via
|
|
6
|
-
* `claude -w` / EnterWorktree / Agent isolation). Input carries
|
|
7
|
-
* `worktree_path` (absolute). No decision control, output ignored — this hook
|
|
8
|
-
* ALWAYS exits 0.
|
|
9
|
-
*
|
|
10
|
-
* Why it exists: Claude Code's native removal is a raw `git worktree remove`,
|
|
11
|
-
* which FOLLOWS the infra junctions the WorktreeCreate hook put in that tree
|
|
12
|
-
* and empties the primary root's `.claude/` and `.morph/framework/`. This
|
|
13
|
-
* hook routes the removal through `morph-spec worktree remove --target
|
|
14
|
-
* --if-clean` instead: junctions dropped first, PROVE-or-DECLINE, and a tree
|
|
15
|
-
* holding uncommitted work is left alone (Claude Code's notion of "clean" is
|
|
16
|
-
* not the user's).
|
|
17
|
-
*
|
|
18
|
-
* Secondary net: in headless runs the event was observed NOT to fire; the
|
|
19
|
-
* PreToolUse guard (block-destructive-worktree-removal) stays the primary
|
|
20
|
-
* defense. Fail-open everywhere — diagnostics to stderr, never a throw, never
|
|
21
|
-
* a non-zero exit.
|
|
22
|
-
*/
|
|
2
|
+
// Routes Claude Code's native removal (a raw `git worktree remove`, which follows the infra junctions out) through
|
|
3
|
+
// `morph-spec worktree remove --if-clean`. The event ignores output, so this hook always exits 0.
|
|
23
4
|
|
|
24
5
|
import { existsSync } from 'fs';
|
|
25
6
|
import { execFileSync } from 'child_process';
|
|
@@ -35,15 +16,11 @@ function primaryRoot(startDir) {
|
|
|
35
16
|
const out = execFileSync('git', ['worktree', 'list', '--porcelain'], { cwd: startDir, stdio: ['ignore', 'pipe', 'pipe'] }).toString();
|
|
36
17
|
const first = out.split('\n').find((l) => l.startsWith('worktree '));
|
|
37
18
|
if (first) return fwd(first.slice('worktree '.length).trim());
|
|
38
|
-
} catch {
|
|
19
|
+
} catch {}
|
|
39
20
|
const common = resolveGitCommonDir(startDir);
|
|
40
21
|
return common ? fwd(dirname(common)) : null;
|
|
41
22
|
}
|
|
42
23
|
|
|
43
|
-
/**
|
|
44
|
-
* @param {object} payload - parsed WorktreeRemove stdin payload ({ cwd, worktree_path })
|
|
45
|
-
* @returns {Promise<null>} always null — the event ignores output
|
|
46
|
-
*/
|
|
47
24
|
export async function run(payload) {
|
|
48
25
|
try {
|
|
49
26
|
const target = payload?.worktree_path;
|
|
@@ -52,8 +29,7 @@ export async function run(payload) {
|
|
|
52
29
|
const start = payload?.cwd || process.env.CLAUDE_PROJECT_DIR || dirname(resolve(target));
|
|
53
30
|
const root = primaryRoot(start) || primaryRoot(dirname(resolve(target)));
|
|
54
31
|
if (!root) return null;
|
|
55
|
-
//
|
|
56
|
-
// from here either: the tree is simply left for git's own cleanup.
|
|
32
|
+
// Never a raw removal from here: outside a morph project the tree is left for git's own cleanup.
|
|
57
33
|
if (!stateExists(root)) return null;
|
|
58
34
|
|
|
59
35
|
const r = runMorphSpec(['worktree', 'remove', '--target', fwd(resolve(target)), '--if-clean', '--json'], { cwd: root });
|
|
@@ -72,10 +48,8 @@ export async function run(payload) {
|
|
|
72
48
|
}
|
|
73
49
|
}
|
|
74
50
|
|
|
75
|
-
// Standalone tail: output is ignored by this event, so nothing is printed;
|
|
76
|
-
// the exit code is always 0.
|
|
77
51
|
if (!process.env.MORPH_HOOK_CHAINED) {
|
|
78
52
|
const { readStdin } = await import('../../shared/stdin-reader.js');
|
|
79
|
-
try { await run(await readStdin()); } catch {
|
|
53
|
+
try { await run(await readStdin()); } catch {}
|
|
80
54
|
process.exit(0);
|
|
81
55
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# morph-spec statusline — installed globally to ~/.claude/statusline.sh
|
|
3
|
-
# Claude Code invokes this with JSON via stdin after each response.
|
|
4
|
-
# Requires: Python 3 available as `python3` or `python` on PATH.
|
|
5
|
-
|
|
6
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
-
if command -v python3 &>/dev/null; then
|
|
8
|
-
python3 "${SCRIPT_DIR}/statusline.py" 2>/dev/null || true
|
|
9
|
-
elif command -v python &>/dev/null; then
|
|
10
|
-
python "${SCRIPT_DIR}/statusline.py" 2>/dev/null || true
|
|
11
|
-
fi
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# morph-spec statusline — installed globally to ~/.claude/statusline.sh
|
|
3
|
+
# Claude Code invokes this with JSON via stdin after each response.
|
|
4
|
+
# Requires: Python 3 available as `python3` or `python` on PATH.
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
if command -v python3 &>/dev/null; then
|
|
8
|
+
python3 "${SCRIPT_DIR}/statusline.py" 2>/dev/null || true
|
|
9
|
+
elif command -v python &>/dev/null; then
|
|
10
|
+
python "${SCRIPT_DIR}/statusline.py" 2>/dev/null || true
|
|
11
|
+
fi
|