@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,15 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* state-sync.js — PostToolUse hook
|
|
4
|
-
*
|
|
5
|
-
* Keeps .morph/state.json in sync with the agent's actions, without requiring
|
|
6
|
-
* the agent to think about state. Fail-open: any error returns {} silently.
|
|
7
|
-
*
|
|
8
|
-
* Triggers:
|
|
9
|
-
* - Write/Edit of .morph/features/{feature}/2-plan/plan.md → register feature
|
|
10
|
-
* - Write/Edit of .morph/features/{feature}/{phase}/{output} → mark output created
|
|
11
|
-
* - AskUserQuestion answered with option starting "aprovado" → advance gate
|
|
12
|
-
*/
|
|
13
2
|
|
|
14
3
|
import { existsSync } from 'fs';
|
|
15
4
|
import { join } from 'path';
|
|
@@ -27,7 +16,7 @@ import {
|
|
|
27
16
|
|
|
28
17
|
const PHASE_FOLDERS = {
|
|
29
18
|
'1-design': 'design',
|
|
30
|
-
'1-ui': 'uiux', // legacy alias
|
|
19
|
+
'1-ui': 'uiux', // legacy alias
|
|
31
20
|
'2-plan': 'plan',
|
|
32
21
|
'3-implement': 'implement',
|
|
33
22
|
'4-review': 'review',
|
|
@@ -62,8 +51,6 @@ function registerFeatureIfMissing(state, featureName) {
|
|
|
62
51
|
if (!state.features) state.features = {};
|
|
63
52
|
if (state.features[featureName]) return false;
|
|
64
53
|
|
|
65
|
-
// Canonical object-form gates (incl. optional uiux) + stamped identity
|
|
66
|
-
// (name=slug, workType, createdAt) — no anonymous shell, no boolean drift.
|
|
67
54
|
state.features[featureName] = createDefaultFeature(featureName);
|
|
68
55
|
return true;
|
|
69
56
|
}
|
|
@@ -94,9 +81,7 @@ function isApprovalAnswer(answer) {
|
|
|
94
81
|
);
|
|
95
82
|
}
|
|
96
83
|
|
|
97
|
-
//
|
|
98
|
-
// installed standalone and cannot import from src/). Keep in sync:
|
|
99
|
-
// plan first; uiux and review require plan.
|
|
84
|
+
// Mirror of src/core/state/gate-order.js: hooks are installed standalone and cannot import src/.
|
|
100
85
|
const GATE_PREREQ = { plan: [], uiux: ['plan'], review: ['plan'] };
|
|
101
86
|
function unmetGatePrereqs(gate, gates) {
|
|
102
87
|
const isApproved = (g) => gates?.[g]?.approved === true || gates?.[g] === true;
|
|
@@ -108,9 +93,7 @@ function handleWriteEdit(input, sessionRoot) {
|
|
|
108
93
|
const featureName = extractFeatureName(filePath);
|
|
109
94
|
if (!featureName) return false;
|
|
110
95
|
|
|
111
|
-
// Sync the tree that OWNS the written file
|
|
112
|
-
// instead registered the feature (and its outputs) in the wrong `.morph/`
|
|
113
|
-
// whenever a root-rooted session wrote into a secondary worktree.
|
|
96
|
+
// Sync the tree that OWNS the written file: a root session also writes into worktrees.
|
|
114
97
|
const cwd = resolveMorphRoot(filePath, sessionRoot);
|
|
115
98
|
if (!stateExists(cwd)) return false;
|
|
116
99
|
|
|
@@ -125,8 +108,6 @@ function handleWriteEdit(input, sessionRoot) {
|
|
|
125
108
|
}
|
|
126
109
|
|
|
127
110
|
if (changed) {
|
|
128
|
-
// Persist the touched feature to its authoritative feature.json and refresh
|
|
129
|
-
// the thin root index. The feature's state thus travels in its own folder.
|
|
130
111
|
persistFeature(featureName, state.features[featureName], cwd);
|
|
131
112
|
}
|
|
132
113
|
return true;
|
|
@@ -142,22 +123,11 @@ function handleAskUserQuestion(input, sessionRoot) {
|
|
|
142
123
|
const gate = gateFromQuestion(question);
|
|
143
124
|
if (!gate) continue;
|
|
144
125
|
|
|
145
|
-
//
|
|
146
|
-
// that owns it — never to "the first in_progress feature of whatever tree
|
|
147
|
-
// the session sits in", which is how a Gate 2 for one feature was stamped
|
|
148
|
-
// onto `export-hub`, leaving it with G2 approved and G1 never asked.
|
|
126
|
+
// The feature the QUESTION names, in the tree that owns it — never the session tree's first in-progress feature.
|
|
149
127
|
const located = locateFeatureForQuestion(question, sessionRoot);
|
|
150
128
|
if (!located) {
|
|
151
|
-
// Refuse to guess
|
|
152
|
-
//
|
|
153
|
-
// explicit remediation step.
|
|
154
|
-
//
|
|
155
|
-
// The remediation differs by WHY it refused. When the question names a
|
|
156
|
-
// specific slug that no known tree has registered (a hotfix gate asked
|
|
157
|
-
// before `create` ran — the case that used to fall through to "the
|
|
158
|
-
// tree's one feature"), `morph-spec approve <that-name> <gate>` is a
|
|
159
|
-
// dead end: approve.js does `getFeature()` and hard-refuses "Feature
|
|
160
|
-
// not found". Name the real fix instead of the wrong command.
|
|
129
|
+
// Refuse to guess: a gate on the wrong feature inverts gate order. For an unregistered slug
|
|
130
|
+
// `approve` is a dead end ("Feature not found"), so the remediation names `create`.
|
|
161
131
|
const allNames = allWorkTrees(sessionRoot).flatMap((r) => featureNamesIn(r));
|
|
162
132
|
const candidate = unrecognizedSlugLike(question, allNames);
|
|
163
133
|
const remediation = candidate
|
|
@@ -176,20 +146,14 @@ function handleAskUserQuestion(input, sessionRoot) {
|
|
|
176
146
|
const { name: featureName, root } = located;
|
|
177
147
|
if (!stateExists(root)) continue;
|
|
178
148
|
|
|
179
|
-
//
|
|
180
|
-
// view so a feature carried only by the thin index still reports its real
|
|
181
|
-
// gates (otherwise every predecessor reads as unapproved and the
|
|
182
|
-
// out-of-order warning below fires on a perfectly ordered approval).
|
|
149
|
+
// Fallback to the merged view: a feature only in the thin index would read every predecessor as unapproved.
|
|
183
150
|
const feature = readFeatureFile(featureName, root) || loadState(root)?.features?.[featureName];
|
|
184
151
|
const gates = feature?.approvalGates;
|
|
185
152
|
const existing = gates?.[gate];
|
|
186
153
|
const alreadyApproved = existing === true || existing?.approved === true;
|
|
187
154
|
if (alreadyApproved) continue;
|
|
188
155
|
|
|
189
|
-
//
|
|
190
|
-
// (never block a human in the loop), but if a predecessor gate is still
|
|
191
|
-
// pending we surface a non-blocking warning — the CLI `morph-spec approve`
|
|
192
|
-
// hard-refuses the same shape; here we alert so the operator can reconcile.
|
|
156
|
+
// Out of order still approves (never block a human in the loop); `morph-spec approve` refuses the same shape.
|
|
193
157
|
const unmet = unmetGatePrereqs(gate, gates);
|
|
194
158
|
if (unmet.length) {
|
|
195
159
|
warnings.push(
|
|
@@ -199,15 +163,9 @@ function handleAskUserQuestion(input, sessionRoot) {
|
|
|
199
163
|
);
|
|
200
164
|
}
|
|
201
165
|
|
|
202
|
-
//
|
|
203
|
-
// Route through the shared canonical gate writer so the hook and the CLI
|
|
204
|
-
// (`morph-spec approve`) produce one consistent shape
|
|
205
|
-
// ({ approved, timestamp, approvedBy }) in the authoritative feature.json —
|
|
206
|
-
// and a manual approval stays distinguishable from a policy auto-approval.
|
|
207
|
-
// Same contract as `approve`'s --self-assessed: a human may ship Gate 3 without the
|
|
208
|
-
// independent review reports, but the degradation is stamped, never silent.
|
|
166
|
+
// Same contract as `approve --self-assessed`: approving review without its reports is stamped, never silent.
|
|
209
167
|
const selfAssessed = gate === 'review' && feature?.workType !== 'hotfix'
|
|
210
|
-
&& ['verifier.md', 'reviewer.md'].some((f) => !existsSync(join(root, '.morph', 'features', featureName, '4-review', f)));
|
|
168
|
+
&& (feature?.workType === 'chore' ? ['reviewer.md'] : ['verifier.md', 'reviewer.md']).some((f) => !existsSync(join(root, '.morph', 'features', featureName, '4-review', f)));
|
|
211
169
|
if (selfAssessed) {
|
|
212
170
|
warnings.push(
|
|
213
171
|
`⚠️ [morph-spec] Gate 3 de "${featureName}" aprovado SEM os relatórios de review independentes (verifier.md, reviewer.md) — ` +
|
|
@@ -219,19 +177,9 @@ function handleAskUserQuestion(input, sessionRoot) {
|
|
|
219
177
|
return warnings.length ? warnings.join('\n') : null;
|
|
220
178
|
}
|
|
221
179
|
|
|
222
|
-
/**
|
|
223
|
-
* @param {object} payload - parsed PostToolUse stdin payload (Write|Edit|AskUserQuestion)
|
|
224
|
-
* @returns {Promise<object|null>} a hookSpecificOutput warning on a gate
|
|
225
|
-
* order/attribution violation, or null (pass/silent).
|
|
226
|
-
*/
|
|
227
180
|
export async function run(payload) {
|
|
228
181
|
try {
|
|
229
|
-
//
|
|
230
|
-
// gets read and written is decided per event — by the target path for
|
|
231
|
-
// Write/Edit, by the feature the question names for AskUserQuestion. The
|
|
232
|
-
// `stateExists` gate therefore moved into each handler, against the tree that
|
|
233
|
-
// handler resolved; gating on the session's tree up here would skip work that
|
|
234
|
-
// belongs to a worktree (and, worse, silently pass it to the wrong one).
|
|
182
|
+
// Only a starting point: each handler resolves the tree it reads and writes, so the stateExists gate lives there.
|
|
235
183
|
const sessionRoot = projectDir();
|
|
236
184
|
|
|
237
185
|
const input = payload || {};
|
|
@@ -244,12 +192,8 @@ export async function run(payload) {
|
|
|
244
192
|
} else if (toolName === 'AskUserQuestion') {
|
|
245
193
|
warning = handleAskUserQuestion(input, sessionRoot);
|
|
246
194
|
}
|
|
247
|
-
} catch {
|
|
248
|
-
// fail-open
|
|
249
|
-
}
|
|
195
|
+
} catch {}
|
|
250
196
|
|
|
251
|
-
// A non-blocking order-violation warning is surfaced as injected context
|
|
252
|
-
// (hookSpecificOutput.additionalContext); everything else stays silent.
|
|
253
197
|
return warning ? contextResult(warning, 'PostToolUse') : null;
|
|
254
198
|
} catch {
|
|
255
199
|
return null;
|
|
@@ -1,41 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Telemetry Hook: harness execution-path capture
|
|
5
|
-
*
|
|
6
|
-
* Events: PostToolUse + PostToolUseFailure (all tools), PostToolBatch,
|
|
7
|
-
* SubagentStop, UserPromptSubmit
|
|
8
|
-
*
|
|
9
|
-
* morph-spec is a Natural Language Harness: there is no central runtime to
|
|
10
|
-
* instrument — Claude Code executes the skills and sub-agents. The only
|
|
11
|
-
* vantage points are hooks. This one is the generic capture point: every tool
|
|
12
|
-
* call and every user prompt is appended (one JSON line) to
|
|
13
|
-
* .morph/logs/events.jsonl, from which `morph-spec telemetry` and
|
|
14
|
-
* `morph-spec cost` reconstruct the path and measure it.
|
|
15
|
-
*
|
|
16
|
-
* Correlation is by an EXPLICIT key (feature + phase) written into each event,
|
|
17
|
-
* not by ambient span context — hooks fire as separate node processes with no
|
|
18
|
-
* shared trace context. `sessionId` and `agentId` (present only inside a
|
|
19
|
-
* sub-agent) separate the orchestrator from the sub-agents it dispatched.
|
|
20
|
-
*
|
|
21
|
-
* PostToolUse fires only when a tool SUCCEEDS; a Bash command that exits
|
|
22
|
-
* non-zero fires PostToolUseFailure instead. Only the exit code parsed from
|
|
23
|
-
* its `error` first line is kept — the rest of `error` is the command's
|
|
24
|
-
* output, where a secret value can appear with no name beside it.
|
|
25
|
-
*
|
|
26
|
-
* SINK RESOLUTION: the event is appended to the `.morph/` tree that OWNS the
|
|
27
|
-
* tool's target file — the tree of its `.morph/features/` segment
|
|
28
|
-
* (`resolveMorphRoot`), else the nearest morph tree above the file; tools with
|
|
29
|
-
* no file path sink on the nearest morph tree above the payload's `cwd`, so a
|
|
30
|
-
* sub-agent working in a worktree logs its Bash calls next to its Reads and
|
|
31
|
-
* Writes. The feature is the tree's BINDING (`morph/<f>` branch, then
|
|
32
|
-
* `MORPH_FEATURE` in `.morph/worktree.env`) before its most recent active
|
|
33
|
-
* feature; a bound feature with no `feature.json` in that tree keeps phase null.
|
|
34
|
-
*
|
|
35
|
-
* SILENT: never blocks, never injects context — only appends. Fail-open on any
|
|
36
|
-
* error (exit 0). Only active inside a morph project (stateExists gate).
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
3
|
import { statSync, readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
40
4
|
import { dirname, join } from 'path';
|
|
41
5
|
import { stateExists, getActiveFeature, derivePhaseForFeature } from '../../shared/state-reader.js';
|
|
@@ -51,10 +15,7 @@ function normalize(p) {
|
|
|
51
15
|
return (p || '').replace(/\\/g, '/');
|
|
52
16
|
}
|
|
53
17
|
|
|
54
|
-
//
|
|
55
|
-
// per-line cost. The ORIGINAL length travels in a sibling `<field>Chars` key,
|
|
56
|
-
// which `morph-spec cost` prefers over the truncated string's own length —
|
|
57
|
-
// a contract with the telemetry reader: do not rename either half.
|
|
18
|
+
// The original length travels in a sibling `<field>Chars` key that `morph-spec cost` prefers: do not rename either half.
|
|
58
19
|
const MAX_FIELD_CHARS = 2000;
|
|
59
20
|
const TRUNCATE_SUFFIX = '…[truncado]';
|
|
60
21
|
|
|
@@ -64,13 +25,7 @@ const DEFAULT_READ_LINES = 2000;
|
|
|
64
25
|
const MAX_SLICE_BYTES = 10 * 1024 * 1024;
|
|
65
26
|
const BINARY_READ = /\.(png|jpe?g|gif|webp|bmp|ico|pdf)$/i;
|
|
66
27
|
|
|
67
|
-
|
|
68
|
-
* Redact, then cap a free-text field, reporting its original length. Redacting
|
|
69
|
-
* first keeps a token cut at the cap from surviving as a recognizable prefix.
|
|
70
|
-
*
|
|
71
|
-
* @param {string} value
|
|
72
|
-
* @returns {{ value: string, chars: number }}
|
|
73
|
-
*/
|
|
28
|
+
// Redact before capping: a token cut at the cap would survive as a recognizable prefix.
|
|
74
29
|
function capField(value) {
|
|
75
30
|
const str = typeof value === 'string' ? value : '';
|
|
76
31
|
const clean = redactSecrets(str);
|
|
@@ -78,15 +33,11 @@ function capField(value) {
|
|
|
78
33
|
return { value: clean.slice(0, MAX_FIELD_CHARS) + TRUNCATE_SUFFIX, chars: str.length };
|
|
79
34
|
}
|
|
80
35
|
|
|
81
|
-
|
|
82
|
-
* Classify a Read target into the telemetry `kind` taxonomy — a contract with
|
|
83
|
-
* src/lib/telemetry/telemetry-reader.js, so these values are pinned. Specific
|
|
84
|
-
* categories first, 'code' last.
|
|
85
|
-
*/
|
|
36
|
+
// The `kind` values are a contract with src/lib/telemetry/telemetry-reader.js.
|
|
86
37
|
function classifyReadKind(target) {
|
|
87
38
|
const t = (target || '').toLowerCase();
|
|
88
39
|
|
|
89
|
-
if (t.includes('evals/review-checklist')) return '
|
|
40
|
+
if (t.includes('evals/review-checklist')) return 'checklist';
|
|
90
41
|
if (t.includes('standards/')) return 'standard';
|
|
91
42
|
if (t.endsWith('skill.md') || t.includes('/skills/')) return 'skill';
|
|
92
43
|
if (t.split('/').pop() === 'agents.json') return 'agents';
|
|
@@ -107,7 +58,6 @@ function classifyReadKind(target) {
|
|
|
107
58
|
return 'code';
|
|
108
59
|
}
|
|
109
60
|
|
|
110
|
-
/** Nearest directory at or above `dir` that is a morph project; `fallback` when none. */
|
|
111
61
|
function morphRootAbove(dir, fallback) {
|
|
112
62
|
if (!dir) return fallback;
|
|
113
63
|
let current = normalize(dir);
|
|
@@ -122,12 +72,7 @@ function morphRootAbove(dir, fallback) {
|
|
|
122
72
|
|
|
123
73
|
const FEATURE_NAME = /^[A-Za-z0-9._-]+$/;
|
|
124
74
|
|
|
125
|
-
|
|
126
|
-
* The feature the TREE is bound to, before any guess from its state: the
|
|
127
|
-
* branch `morph/<f>` checked out there, then `MORPH_FEATURE` in its
|
|
128
|
-
* `.morph/worktree.env` (a task worktree sits on `morph-task/<n>`). Without
|
|
129
|
-
* it, a sub-agent's worktree reported the root's most recent active feature.
|
|
130
|
-
*/
|
|
75
|
+
// Branch binding before state: otherwise a sub-agent's worktree reports the root's most recent active feature.
|
|
131
76
|
async function boundFeature(root) {
|
|
132
77
|
const fromBranch = morphBranchFeature(root);
|
|
133
78
|
if (fromBranch) return fromBranch;
|
|
@@ -143,10 +88,6 @@ function featureFromPath(target) {
|
|
|
143
88
|
return m ? m[1] : null;
|
|
144
89
|
}
|
|
145
90
|
|
|
146
|
-
/**
|
|
147
|
-
* What a Read actually put in context: the requested line window, not the
|
|
148
|
-
* whole file. Binary reads (images, PDFs) go in whole.
|
|
149
|
-
*/
|
|
150
91
|
function readSliceBytes(absPath, input, size) {
|
|
151
92
|
if (BINARY_READ.test(absPath)) return size;
|
|
152
93
|
if (size > MAX_SLICE_BYTES) return undefined;
|
|
@@ -157,7 +98,7 @@ function readSliceBytes(absPath, input, size) {
|
|
|
157
98
|
return Buffer.byteLength(lines.slice(offset - 1, offset - 1 + limit).join('\n'), 'utf-8');
|
|
158
99
|
}
|
|
159
100
|
|
|
160
|
-
|
|
101
|
+
// Only the exit code is kept: the rest of `error` is command output, where a secret can appear with no name beside it.
|
|
161
102
|
function outcomeOf(payload, input) {
|
|
162
103
|
if (payload.hook_event_name === 'PostToolUseFailure') {
|
|
163
104
|
const firstLine = String(payload.error ?? '').split('\n', 1)[0];
|
|
@@ -181,11 +122,7 @@ function countByName(calls) {
|
|
|
181
122
|
return out;
|
|
182
123
|
}
|
|
183
124
|
|
|
184
|
-
|
|
185
|
-
* Sub-agent tokens live in their own transcript; each stop emits the delta.
|
|
186
|
-
* One file per agent: two sub-agents stopping together never share a
|
|
187
|
-
* read-modify-write, and one agent never stops twice at once.
|
|
188
|
-
*/
|
|
125
|
+
// One store file per agent, so two sub-agents stopping together never share a read-modify-write.
|
|
189
126
|
function agentUsageEvent(payload, cwd) {
|
|
190
127
|
const usage = readTranscriptUsage(payload.agent_transcript_path);
|
|
191
128
|
if (!usage || !payload.agent_id) return null;
|
|
@@ -204,11 +141,6 @@ function agentUsageEvent(payload, cwd) {
|
|
|
204
141
|
return { event: 'AgentUsage', ...usage, delta };
|
|
205
142
|
}
|
|
206
143
|
|
|
207
|
-
/**
|
|
208
|
-
* @param {object} payload - parsed hook payload (PostToolUse, PostToolUseFailure,
|
|
209
|
-
* PostToolBatch, SubagentStop or UserPromptSubmit)
|
|
210
|
-
* @returns {Promise<null>} always null — pure side effect (append + claim heartbeat)
|
|
211
|
-
*/
|
|
212
144
|
export async function run(payload) {
|
|
213
145
|
try {
|
|
214
146
|
if (!payload) return null;
|
|
@@ -222,8 +154,6 @@ export async function run(payload) {
|
|
|
222
154
|
|
|
223
155
|
if (!stateExists(cwd)) return null;
|
|
224
156
|
|
|
225
|
-
// getActiveFeature → loadState hydrates every feature.json: O(active
|
|
226
|
-
// features) per call, fine while archive keeps that set small.
|
|
227
157
|
let feature = null;
|
|
228
158
|
let phase = null;
|
|
229
159
|
try {
|
|
@@ -237,14 +167,9 @@ export async function run(payload) {
|
|
|
237
167
|
feature = active?.name ?? featureFromPath(targetFilePath);
|
|
238
168
|
if (feature) phase = active?.feature?.phase || derivePhaseForFeature(feature, cwd);
|
|
239
169
|
}
|
|
240
|
-
} catch {
|
|
241
|
-
// enrichment is best-effort — a missing feature never blocks capture
|
|
242
|
-
}
|
|
170
|
+
} catch {}
|
|
243
171
|
|
|
244
|
-
// Claim heartbeat:
|
|
245
|
-
// of life for the advisory feature claim. shouldHeartbeat() is a local,
|
|
246
|
-
// git-free pre-check that says no on ~99% of calls; only then is the git
|
|
247
|
-
// common dir resolved (pure filesystem, no subprocess).
|
|
172
|
+
// Claim heartbeat: shouldHeartbeat is a git-free pre-check that says no on ~99% of calls.
|
|
248
173
|
if (feature && payload.session_id) {
|
|
249
174
|
try {
|
|
250
175
|
const { shouldHeartbeat, heartbeatClaim } = await import('../../shared/claim-registry.js');
|
|
@@ -253,7 +178,7 @@ export async function run(payload) {
|
|
|
253
178
|
const common = resolveGitCommonDir(cwd);
|
|
254
179
|
if (common) heartbeatClaim(feature, { gitCommonDir: common, sessionId: payload.session_id });
|
|
255
180
|
}
|
|
256
|
-
} catch {
|
|
181
|
+
} catch {}
|
|
257
182
|
}
|
|
258
183
|
|
|
259
184
|
const hookEvent = payload.hook_event_name;
|
|
@@ -290,17 +215,14 @@ export async function run(payload) {
|
|
|
290
215
|
event.kind = classifyReadKind(event.target);
|
|
291
216
|
if (Number.isInteger(input.offset)) event.offset = input.offset;
|
|
292
217
|
if (Number.isInteger(input.limit)) event.limit = input.limit;
|
|
293
|
-
// A failed Read ingested nothing
|
|
294
|
-
// between the Read and this hook omits the numbers rather than fail.
|
|
218
|
+
// A failed Read ingested nothing; a file deleted before this hook just omits the numbers.
|
|
295
219
|
if (hookEvent !== 'PostToolUseFailure') {
|
|
296
220
|
try {
|
|
297
221
|
const abs = targetAbs || resolveTargetPath(event.target, projectDir);
|
|
298
222
|
event.bytes = statSync(abs).size;
|
|
299
223
|
const read = readSliceBytes(abs, input, event.bytes);
|
|
300
224
|
if (read !== undefined) event.bytesRead = read;
|
|
301
|
-
} catch {
|
|
302
|
-
// omit bytes
|
|
303
|
-
}
|
|
225
|
+
} catch {}
|
|
304
226
|
}
|
|
305
227
|
} else if (tool === 'Bash' || tool === 'PowerShell') {
|
|
306
228
|
const command = capField(input.command || '');
|
|
@@ -311,8 +233,6 @@ export async function run(payload) {
|
|
|
311
233
|
} else if (tool === 'Write' || tool === 'Edit') {
|
|
312
234
|
event.target = normalize(input.file_path || input.path);
|
|
313
235
|
} else {
|
|
314
|
-
// Every other tool still counts toward "the whole path" — a compact
|
|
315
|
-
// target keeps Grep/Glob/WebFetch bursts distinguishable.
|
|
316
236
|
const target = input.file_path || input.path || input.pattern || input.query || input.url || '';
|
|
317
237
|
if (target) event.target = normalize(String(target));
|
|
318
238
|
}
|
|
@@ -333,7 +253,6 @@ export async function run(payload) {
|
|
|
333
253
|
appendTelemetryEvent(event, cwd);
|
|
334
254
|
return null;
|
|
335
255
|
} catch {
|
|
336
|
-
// Fail-open
|
|
337
256
|
return null;
|
|
338
257
|
}
|
|
339
258
|
}
|
|
@@ -1,14 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Stop Hook: phase-transition alert, slice report audit, session usage.
|
|
5
|
-
*
|
|
6
|
-
* Phase transition tracking: stores last-seen phase per feature in
|
|
7
|
-
* .morph/memory/phase-watch.json so the alert fires exactly once.
|
|
8
|
-
*
|
|
9
|
-
* Fail-open: exits 0 on any error.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
3
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
13
4
|
import { join } from 'path';
|
|
14
5
|
import { createHash } from 'crypto';
|
|
@@ -21,11 +12,7 @@ import { appendTelemetryEvent } from '../../shared/telemetry-logger.js';
|
|
|
21
12
|
import { readTranscriptUsage, usageDelta } from '../../shared/transcript-usage.js';
|
|
22
13
|
import { sliceProgress, sliceReportPath, inspectReport } from '../../shared/slice-progress.js';
|
|
23
14
|
|
|
24
|
-
|
|
25
|
-
* Slices whose report is final but lacks what the report contract asks for:
|
|
26
|
-
* prose, the files touched, every scenario the slice covers. Advisory: the
|
|
27
|
-
* same rule `report-check` applies when the orchestrator resumes control.
|
|
28
|
-
*/
|
|
15
|
+
// Advisory: the same rule `report-check` applies when the orchestrator resumes control.
|
|
29
16
|
function auditSliceReports(featurePath) {
|
|
30
17
|
const progress = sliceProgress(featurePath);
|
|
31
18
|
if (!progress) return [];
|
|
@@ -47,30 +34,16 @@ const MISSING_HINT = {
|
|
|
47
34
|
partial: '1ª linha PARCIAL: a fatia não terminou',
|
|
48
35
|
};
|
|
49
36
|
|
|
50
|
-
// Phases where compaction is strongly recommended before starting
|
|
51
37
|
const COMPACT_TRIGGER_PHASES = new Set(['implement']);
|
|
52
38
|
|
|
53
|
-
// Phases where compaction is suggested when arriving (heavy spec phases)
|
|
54
39
|
const HEAVY_SOURCE_PHASES = new Set(['plan', 'uiux']);
|
|
55
40
|
|
|
56
|
-
/**
|
|
57
|
-
* @param {object} payload - parsed Stop stdin payload. Most of this hook's
|
|
58
|
-
* logic reads its own state from disk rather than the payload, EXCEPT the
|
|
59
|
-
* session-usage capture below, which needs `payload.transcript_path` (and
|
|
60
|
-
* `payload.session_id` for the throttle key) — the one thing only the
|
|
61
|
-
* payload carries.
|
|
62
|
-
* @returns {Promise<object|null>} a hookSpecificOutput advisory (phase
|
|
63
|
-
* transition / build failure / incomplete slice report), or null (pass/silent).
|
|
64
|
-
*/
|
|
65
41
|
export async function run(payload) {
|
|
66
42
|
try {
|
|
67
|
-
// Prevent infinite loop
|
|
68
43
|
if (process.env.MORPH_STOP_HOOK_ACTIVE === '1') return null;
|
|
69
44
|
|
|
70
45
|
if (!stateExists()) return null;
|
|
71
46
|
|
|
72
|
-
// getActiveFeature() only returns in_progress/draft features.
|
|
73
|
-
// Fall back to the most recently updated feature when all features are 'done'.
|
|
74
47
|
let active = getActiveFeature() || getMostRecentFeature();
|
|
75
48
|
if (!active) return null;
|
|
76
49
|
|
|
@@ -79,9 +52,8 @@ export async function run(payload) {
|
|
|
79
52
|
const warnings = [];
|
|
80
53
|
|
|
81
54
|
const memDir = join(cwd, '.morph', 'memory');
|
|
82
|
-
try { mkdirSync(memDir, { recursive: true }); } catch {
|
|
55
|
+
try { mkdirSync(memDir, { recursive: true }); } catch {}
|
|
83
56
|
|
|
84
|
-
// ── Phase transition detection ────────────────────────────────────────────
|
|
85
57
|
const currentPhase = feature.phase || derivePhaseForFeature(name, cwd);
|
|
86
58
|
|
|
87
59
|
const watchPath = join(memDir, 'phase-watch.json');
|
|
@@ -92,13 +64,12 @@ export async function run(payload) {
|
|
|
92
64
|
|
|
93
65
|
const lastPhase = phaseWatch[name];
|
|
94
66
|
|
|
95
|
-
//
|
|
67
|
+
// Always stored, so the alert fires once per transition.
|
|
96
68
|
try {
|
|
97
69
|
phaseWatch[name] = currentPhase;
|
|
98
70
|
writeFileSync(watchPath, JSON.stringify(phaseWatch, null, 2), 'utf-8');
|
|
99
|
-
} catch {
|
|
71
|
+
} catch {}
|
|
100
72
|
|
|
101
|
-
// Alert when entering implement from a heavy spec phase
|
|
102
73
|
const isPhaseTransition = lastPhase && lastPhase !== currentPhase;
|
|
103
74
|
const enteringImplement = COMPACT_TRIGGER_PHASES.has(currentPhase);
|
|
104
75
|
const comingFromHeavyPhase = HEAVY_SOURCE_PHASES.has(lastPhase);
|
|
@@ -116,14 +87,7 @@ export async function run(payload) {
|
|
|
116
87
|
);
|
|
117
88
|
}
|
|
118
89
|
|
|
119
|
-
//
|
|
120
|
-
// Advisory warnings only — Stop hook never blocks.
|
|
121
|
-
//
|
|
122
|
-
// The same audit re-runs every Stop; without dedup it re-injects the same
|
|
123
|
-
// ~500-token block turn after turn even when nothing changed. Hash-gate it:
|
|
124
|
-
// only inject when the warning SET differs from the last one actually
|
|
125
|
-
// surfaced for this feature. Resolved (no more warnings) clears the stored
|
|
126
|
-
// hash, so a later recurrence — even an identical one — is reported fresh.
|
|
90
|
+
// Hash-gated: the audit re-runs every Stop, so only a changed warning set is injected; resolving clears the hash.
|
|
127
91
|
try {
|
|
128
92
|
const featurePath = join(cwd, '.morph', 'features', name);
|
|
129
93
|
const reportWarnings = auditSliceReports(featurePath);
|
|
@@ -140,22 +104,15 @@ export async function run(payload) {
|
|
|
140
104
|
warnings.push('📋 Relatórios de fatia:');
|
|
141
105
|
for (const w of reportWarnings) warnings.push(` • ${w}`);
|
|
142
106
|
warnedHashes[name] = { ...warnedHashes[name], reports: hash };
|
|
143
|
-
try { writeFileSync(warnedHashesPath, JSON.stringify(warnedHashes, null, 2), 'utf-8'); } catch {
|
|
107
|
+
try { writeFileSync(warnedHashesPath, JSON.stringify(warnedHashes, null, 2), 'utf-8'); } catch {}
|
|
144
108
|
}
|
|
145
|
-
// same hash as last time it fired → already surfaced, stay silent
|
|
146
109
|
} else if (warnedHashes[name]?.reports) {
|
|
147
110
|
delete warnedHashes[name].reports;
|
|
148
|
-
try { writeFileSync(warnedHashesPath, JSON.stringify(warnedHashes, null, 2), 'utf-8'); } catch {
|
|
111
|
+
try { writeFileSync(warnedHashesPath, JSON.stringify(warnedHashes, null, 2), 'utf-8'); } catch {}
|
|
149
112
|
}
|
|
150
|
-
} catch {
|
|
151
|
-
// Fail-open — audit must never block Stop
|
|
152
|
-
}
|
|
153
|
-
// ────────────────────────────────────────────────────────────────────────
|
|
113
|
+
} catch {}
|
|
154
114
|
|
|
155
|
-
//
|
|
156
|
-
// Silent side effect. The transcript total is cumulative, so each event
|
|
157
|
-
// also carries the `delta` since the previous sample of the same session:
|
|
158
|
-
// summing deltas per sessionId is the only sum that does not double-count.
|
|
115
|
+
// The transcript total is cumulative: summing the per-session `delta` is the only sum that does not double-count.
|
|
159
116
|
try {
|
|
160
117
|
const usage = readTranscriptUsage(payload?.transcript_path);
|
|
161
118
|
if (usage) {
|
|
@@ -177,13 +134,10 @@ export async function run(payload) {
|
|
|
177
134
|
delta,
|
|
178
135
|
}, cwd);
|
|
179
136
|
stored[sessionKey] = usage;
|
|
180
|
-
try { writeFileSync(usagePath, JSON.stringify(stored, null, 2), 'utf-8'); } catch {
|
|
137
|
+
try { writeFileSync(usagePath, JSON.stringify(stored, null, 2), 'utf-8'); } catch {}
|
|
181
138
|
}
|
|
182
139
|
}
|
|
183
|
-
} catch {
|
|
184
|
-
// Fail-open — usage capture must never block Stop
|
|
185
|
-
}
|
|
186
|
-
// ────────────────────────────────────────────────────────────────────────
|
|
140
|
+
} catch {}
|
|
187
141
|
|
|
188
142
|
if (warnings.length === 0) return null;
|
|
189
143
|
|
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
* warn-bash-source-write — PreToolUse Bash, warn only: a project source file written
|
|
4
|
-
* by the shell (`writeTargets`) skips every Write/Edit guard (phase folders,
|
|
5
|
-
* protected files, comment-guard). The command still runs.
|
|
6
|
-
*/
|
|
2
|
+
// Warn only: a source file written by the shell skips every Write/Edit guard. The command still runs.
|
|
7
3
|
import { contextResult, runStandalone } from '../../shared/hook-response.js';
|
|
8
4
|
import { stateExists } from '../../shared/state-reader.js';
|
|
9
5
|
import { writeTargets } from '../../shared/command-tokens.js';
|
|
10
6
|
import { isProjectSource } from '../../shared/source-files.js';
|
|
11
7
|
|
|
12
|
-
/**
|
|
13
|
-
* @param {object} payload - parsed PreToolUse stdin payload (Bash matcher)
|
|
14
|
-
* @returns {Promise<object|null>} additionalContext naming the files, or null
|
|
15
|
-
*/
|
|
16
8
|
export async function run(payload) {
|
|
17
9
|
try {
|
|
18
10
|
if (!stateExists()) return null;
|