@polymorphism-tech/morph-spec 8.47.0 → 9.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +51 -43
- package/README.md +39 -38
- package/bin/morph-spec.js +41 -113
- package/bin/validate.js +1 -147
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +41 -101
- package/framework/MORPH.md +95 -252
- package/framework/agents.json +86 -21
- package/framework/commands/morph-apply.md +5 -28
- package/framework/commands/morph-archive.md +1 -1
- package/framework/commands/morph-hotfix.md +8 -8
- package/framework/commands/morph-infra.md +1 -1
- package/framework/commands/morph-preflight.md +2 -3
- package/framework/commands/morph-status.md +4 -4
- package/framework/docs/CLI.md +574 -286
- package/framework/evals/review-checklist.md +82 -41
- package/framework/hooks/README.md +10 -11
- package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
- package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
- package/framework/hooks/claude-code/core/chain.js +4 -65
- package/framework/hooks/claude-code/core/comment-guard.js +3 -17
- package/framework/hooks/claude-code/core/denylist.json +33 -3
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +15 -63
- package/framework/hooks/claude-code/core/gate-guard.js +9 -78
- package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
- package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
- package/framework/hooks/claude-code/core/heavy-release.js +2 -13
- package/framework/hooks/claude-code/core/loop-detect.js +2 -31
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +36 -2
- package/framework/hooks/claude-code/core/protect-spec-files.js +25 -297
- package/framework/hooks/claude-code/core/state-sync.js +15 -85
- package/framework/hooks/claude-code/core/telemetry-log.js +15 -96
- package/framework/hooks/claude-code/core/validate-completion.js +37 -147
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +23 -0
- package/framework/hooks/claude-code/core/worktree-context.js +15 -114
- package/framework/hooks/claude-code/core/worktree-create.js +6 -70
- package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
- package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
- package/framework/hooks/claude-code/statusline.py +55 -41
- package/framework/hooks/package.json +3 -0
- package/framework/hooks/shared/activity-logger.js +9 -89
- package/framework/hooks/shared/agent-reap.js +2 -17
- package/framework/hooks/shared/build-check.js +11 -76
- package/framework/hooks/shared/claim-registry.js +9 -119
- package/framework/hooks/shared/command-tokens.js +168 -44
- package/framework/hooks/shared/comment-rules.js +5 -20
- package/framework/hooks/shared/comment-scanner.js +11 -43
- package/framework/hooks/shared/comment-stripper.js +11 -85
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/errata.js +59 -0
- package/framework/hooks/shared/feature-locator.js +11 -95
- package/framework/hooks/shared/frontend-locations.js +4 -0
- package/framework/hooks/shared/gate-question.js +12 -0
- package/framework/hooks/shared/git-exclude.js +3 -20
- package/framework/hooks/shared/git-paths.js +8 -104
- package/framework/hooks/shared/graph-freshness.js +22 -166
- package/framework/hooks/shared/graph-refresh.js +21 -149
- package/framework/hooks/shared/heavy-command.js +4 -12
- package/framework/hooks/shared/hook-response.js +8 -83
- package/framework/hooks/shared/morph-cli.js +8 -52
- package/framework/hooks/shared/payload-utils.js +10 -132
- package/framework/hooks/shared/phase-utils.js +14 -72
- package/framework/hooks/shared/redact-secrets.js +4 -15
- package/framework/hooks/shared/reparse-scan.js +6 -66
- package/framework/hooks/shared/slice-progress.js +99 -0
- package/framework/hooks/shared/source-files.js +4 -17
- package/framework/hooks/shared/spawn-command.js +23 -0
- package/framework/hooks/shared/state-reader.js +51 -247
- package/framework/hooks/shared/state-writer.js +9 -54
- package/framework/hooks/shared/stdin-reader.js +1 -11
- package/framework/hooks/shared/telemetry-logger.js +7 -67
- package/framework/hooks/shared/transcript-usage.js +7 -29
- package/framework/hooks/shared/worktree-root.js +7 -29
- package/framework/mcp/registry.json +3 -4
- package/framework/phases.json +23 -39
- package/framework/rules/csharp-standards.md +1 -1
- package/framework/rules/frontend-standards.md +1 -0
- package/framework/rules/hangfire-standards.md +1 -1
- package/framework/rules/neon-standards.md +1 -1
- package/framework/schemas/config.schema.json +14 -9
- package/framework/schemas/feature.schema.json +15 -50
- package/framework/skills/README.md +3 -4
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +149 -222
- package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
- package/framework/skills/core/morph-machine/SKILL.md +2 -2
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +137 -299
- package/framework/skills/core/morph-review/SKILL.md +111 -136
- package/framework/skills/core/morph-standards/SKILL.md +27 -39
- package/framework/skills/core/morph-uiux/SKILL.md +94 -152
- package/framework/standards/STANDARDS.json +109 -1219
- package/framework/standards/STANDARDS.meta.json +1319 -0
- package/framework/standards/ai-agents/agent-archetypes.md +2 -2
- package/framework/standards/ai-agents/agent-session.md +1 -1
- package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
- package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
- package/framework/standards/ai-agents/evals-with-cache.md +1 -2
- package/framework/standards/ai-agents/guardrails.md +1 -1
- package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
- package/framework/standards/ai-agents/observability-patterns.md +1 -1
- package/framework/standards/ai-agents/production.md +1 -1
- package/framework/standards/ai-agents/prompt-sources.md +2 -2
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
- package/framework/standards/ai-agents/setup.md +3 -3
- package/framework/standards/ai-agents/sweet-spot.md +2 -2
- package/framework/standards/ai-agents/tool-adapters.md +0 -2
- package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
- package/framework/standards/ai-agents/workflows.md +1 -1
- package/framework/standards/architecture/code-comments.md +7 -7
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
- package/framework/standards/backend/api/validation.md +1 -1
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
- package/framework/standards/data/nosql/cache/redis.md +1 -1
- package/framework/standards/data/nosql/cosmos-db.md +3 -3
- package/framework/standards/frontend/admin-app/forms.md +1 -1
- package/framework/standards/frontend/design-system/art-direction.md +88 -0
- package/framework/standards/frontend/design-system/premium-finish.md +77 -2
- package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
- package/framework/standards/integration/mcp/mcp-tools.md +7 -7
- package/framework/templates/CLAUDE.md.core.template +3 -3
- package/framework/templates/README.md +2 -3
- package/framework/templates/REGISTRY.json +15 -179
- package/framework/templates/docs/plan.md +72 -0
- package/framework/templates/docs/ui-mockups.md +4 -0
- package/framework/templates/dotnet/ai-kit/README.md +1 -9
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
- package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
- package/package.json +1 -1
- package/src/commands/context/context-refresh.js +0 -16
- package/src/commands/dashboard/dashboard.js +2 -8
- package/src/commands/e2e/e2e.js +18 -105
- package/src/commands/graph/affected.js +0 -14
- package/src/commands/graph/build.js +11 -50
- package/src/commands/graph/explain.js +0 -16
- package/src/commands/graph/status.js +1 -12
- package/src/commands/mcp/mcp-setup.js +4 -62
- package/src/commands/observability/cost.js +17 -111
- package/src/commands/observability/telemetry.js +1 -14
- package/src/commands/project/branch.js +4 -45
- package/src/commands/project/claim.js +4 -48
- package/src/commands/project/doctor-procs.js +1 -1
- package/src/commands/project/doctor.js +53 -307
- package/src/commands/project/env.js +1 -27
- package/src/commands/project/fleet.js +6 -35
- package/src/commands/project/heavy.js +3 -3
- package/src/commands/project/init.js +8 -68
- package/src/commands/project/install-plugin-cmd.js +0 -1
- package/src/commands/project/setup-infra-cmd.js +2 -1
- package/src/commands/project/stack-scan.js +58 -0
- package/src/commands/project/standards.js +65 -72
- package/src/commands/project/status.js +28 -121
- package/src/commands/project/update.js +31 -223
- package/src/commands/project/worktree.js +109 -883
- package/src/commands/scaffold/scaffold.js +0 -25
- package/src/commands/state/advance.js +65 -120
- package/src/commands/state/approve.js +87 -229
- package/src/commands/state/archive.js +23 -139
- package/src/commands/state/auto-approval.js +65 -0
- package/src/commands/state/create.js +49 -109
- package/src/commands/state/delete.js +11 -63
- package/src/commands/state/finish.js +105 -494
- package/src/commands/state/gate-check.js +74 -183
- package/src/commands/state/gate-decision.js +8 -42
- package/src/commands/state/handoff.js +4 -31
- package/src/commands/state/list.js +21 -91
- package/src/commands/state/report-check.js +134 -338
- package/src/commands/state/retype.js +12 -28
- package/src/commands/validation/index.js +0 -3
- package/src/commands/validation/mutate.js +79 -19
- package/src/commands/validation/plan-check.js +81 -0
- package/src/commands/validation/validate-feature.js +52 -216
- package/src/commands/validation/verify.js +45 -160
- package/src/core/paths/output-schema.js +9 -155
- package/src/core/state/feature-identity.js +1 -25
- package/src/core/state/gate-order.js +2 -30
- package/src/core/state/lifecycle-audit.js +8 -100
- package/src/core/state/state-manager.js +32 -395
- package/src/core/templates/template-registry.js +2 -97
- package/src/core/templates/template-renderer.js +13 -93
- package/src/core/templates/template-seed.js +21 -0
- package/src/core/work-types.js +10 -45
- package/src/dashboard/app/App.jsx +1 -8
- package/src/dashboard/app/components/ActivityFeed.jsx +2 -8
- package/src/dashboard/app/components/CommandPalette.jsx +4 -11
- package/src/dashboard/app/components/DetailPanel.jsx +1 -5
- package/src/dashboard/app/components/EntityLink.jsx +0 -12
- package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
- package/src/dashboard/app/components/Markdown.jsx +0 -9
- package/src/dashboard/app/components/PhaseRail.jsx +0 -11
- package/src/dashboard/app/components/SearchFilter.jsx +0 -4
- package/src/dashboard/app/components/Sidebar.jsx +0 -5
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
- package/src/dashboard/app/hooks/usePref.js +1 -5
- package/src/dashboard/app/lib/format.js +0 -2
- package/src/dashboard/app/lib/phases.js +3 -9
- package/src/dashboard/app/lib/project.js +0 -12
- package/src/dashboard/app/pages/Activity.jsx +1 -1
- package/src/dashboard/app/pages/AgentMap.jsx +1 -8
- package/src/dashboard/app/pages/FeatureDetail.jsx +139 -133
- package/src/dashboard/app/pages/Features.jsx +9 -16
- package/src/dashboard/app/pages/Guide.jsx +2 -3
- package/src/dashboard/app/pages/HookInspector.jsx +2 -23
- package/src/dashboard/app/pages/PhasePipeline.jsx +3 -7
- package/src/dashboard/app/pages/Portfolio.jsx +0 -1
- package/src/dashboard/app/pages/RulesMap.jsx +1 -16
- package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
- package/src/dashboard/app/pages/Standards.jsx +2 -17
- package/src/dashboard/scanner/activity-scanner.js +14 -83
- package/src/dashboard/scanner/agents-scanner.js +1 -23
- package/src/dashboard/scanner/docs-scanner.js +1 -7
- package/src/dashboard/scanner/feature-status.js +2 -5
- package/src/dashboard/scanner/features-scanner.js +92 -113
- package/src/dashboard/scanner/hooks-scanner.js +5 -28
- package/src/dashboard/scanner/index.js +0 -4
- package/src/dashboard/scanner/portfolio.js +5 -29
- package/src/dashboard/scanner/project-api.js +8 -54
- package/src/dashboard/scanner/rules-scanner.js +1 -2
- package/src/dashboard/scanner/skills-scanner.js +2 -12
- package/src/dashboard/scanner/standards-scanner.js +4 -7
- package/src/dashboard/vite-plugin-morph-data.js +5 -19
- package/src/lib/ai/ai-checks.js +7 -52
- package/src/lib/ai/ai-project-scan.js +14 -108
- package/src/lib/ai/code-audit.js +7 -63
- package/src/lib/ai/pin-drift.js +8 -51
- package/src/lib/ai/registry-audit.js +11 -90
- package/src/lib/ci/ci-verify.js +14 -13
- package/src/lib/ci/github-ci.js +9 -37
- package/src/lib/ci/workflow-scaffold.js +2 -10
- package/src/lib/detectors/claude-config-detector.js +13 -74
- package/src/lib/detectors/index.js +8 -59
- package/src/lib/e2e/compose-env.js +5 -66
- package/src/lib/e2e/stack-teardown.js +6 -40
- package/src/lib/generators/architecture-scanner.js +18 -156
- package/src/lib/generators/handoff-generator.js +37 -112
- package/src/lib/generators/scaffold-generator.js +5 -101
- package/src/lib/generators/settings-generator.js +11 -122
- package/src/lib/git/branch-classifier.js +11 -84
- package/src/lib/git/default-branch.js +14 -169
- package/src/lib/git/evidence-staleness.js +2 -1
- package/src/lib/git/gh.js +2 -17
- package/src/lib/git/pending-prs.js +6 -37
- package/src/lib/git/pr-files.js +3 -28
- package/src/lib/git/pr-index.js +3 -34
- package/src/lib/git/tree-fingerprint.js +17 -17
- package/src/lib/git/tree-state.js +6 -33
- package/src/lib/graph/god-nodes.js +2 -22
- package/src/lib/graph/graph-health.js +3 -24
- package/src/lib/graph/graph-loader.js +5 -56
- package/src/lib/graph/graphify-runtime.js +7 -37
- package/src/lib/graph/graphifyignore.js +8 -74
- package/src/lib/graph/sanitize.js +16 -136
- package/src/lib/heavy/cleanup.js +2 -6
- package/src/lib/installers/effects.js +8 -93
- package/src/lib/installers/mcp-installer.js +9 -169
- package/src/lib/installers/memory-installer.js +8 -33
- package/src/lib/installers/templates-reconcile.js +5 -60
- package/src/lib/mutation/commands.js +3 -2
- package/src/lib/mutation/dotnet-introspection.js +281 -0
- package/src/lib/mutation/plan.js +14 -6
- package/src/lib/mutation/runner.js +1 -1
- package/src/lib/mutation/subset.js +47 -13
- package/src/lib/plan/artifact-vocabulary.js +40 -0
- package/src/lib/plan/checks.js +208 -0
- package/src/lib/plan/plan-parser.js +204 -0
- package/src/lib/plan/plan-template.js +18 -0
- package/src/lib/projects/registry.js +7 -44
- package/src/lib/review-reports.js +204 -0
- package/src/lib/risk-detector.js +131 -274
- package/src/lib/specs/adr-index.js +38 -85
- package/src/lib/specs/design-seed.js +18 -0
- package/src/lib/specs/frontmatter.js +31 -0
- package/src/lib/specs/gfm-table.js +11 -0
- package/src/lib/specs/revocations.js +31 -102
- package/src/lib/specs/screen-checklist.js +56 -132
- package/src/lib/stack/dotnet-build-ignores.js +43 -0
- package/src/lib/stack/stack-profile.js +1 -28
- package/src/lib/stack/stack-scan.js +260 -0
- package/src/lib/stack-filter.js +4 -27
- package/src/lib/standards/registry.js +37 -0
- package/src/lib/standards/sections.js +47 -0
- package/src/lib/standards/verified-against.js +19 -191
- package/src/lib/tasks/eval-node.js +23 -179
- package/src/lib/tasks/linux-runner.js +18 -142
- package/src/lib/tasks/test-nodes.js +43 -313
- package/src/lib/tasks/test-runner.js +107 -447
- package/src/lib/telemetry/metrics.js +13 -103
- package/src/lib/telemetry/telemetry-reader.js +13 -238
- package/src/lib/telemetry/verify-event.js +4 -34
- package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
- package/src/lib/validators/code-budget/index.js +0 -7
- package/src/lib/validators/contracts/contract-compliance-validator.js +4 -26
- package/src/lib/validators/contracts/index.js +0 -6
- package/src/lib/validators/nextjs/index.js +0 -5
- package/src/lib/validators/nextjs/next-component-validator.js +4 -81
- package/src/lib/validators/packages/index.js +0 -6
- package/src/lib/validators/packages/package-validator.js +6 -56
- package/src/lib/validators/reuse/index.js +0 -8
- package/src/lib/validators/reuse/reuse-validator.js +24 -137
- package/src/lib/validators/reuse/symbol-scanner.js +6 -62
- package/src/lib/validators/traceability-check.js +54 -156
- package/src/lib/validators/validation-runner.js +20 -125
- package/src/lib/validators/verify-runner.js +152 -575
- package/src/lib/work-type-detector.js +5 -67
- package/src/lib/worktree/docker-audit.js +17 -137
- package/src/lib/worktree/env-allocator.js +10 -98
- package/src/lib/worktree/feature-truth.js +12 -115
- package/src/lib/worktree/finish-backup.js +32 -242
- package/src/lib/worktree/fleet-scanner.js +31 -126
- package/src/lib/worktree/ignored-triage.js +15 -91
- package/src/lib/worktree/vscode-settings.js +2 -3
- package/src/lib/worktree/worktree-audit.js +21 -213
- package/src/lib/worktree/worktree-include.js +3 -5
- package/src/scripts/global-install.js +3 -9
- package/src/scripts/install-plugin.js +1 -13
- package/src/scripts/setup-infra.js +17 -138
- package/src/utils/agents-installer.js +92 -272
- package/src/utils/banner.js +4 -25
- package/src/utils/claude-md-injector.js +1 -37
- package/src/utils/claude-settings-manager.js +1 -87
- package/src/utils/file-copier.js +32 -90
- package/src/utils/hooks-installer.js +26 -296
- package/src/utils/skills-installer.js +4 -102
- package/src/utils/version-checker.js +4 -83
- package/framework/commands/morph-proposal.md +0 -57
- package/framework/evals/trace-schema.md +0 -67
- package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
- package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
- package/framework/hooks/git/pre-commit/specs.sh +0 -50
- package/framework/hooks/shared/tasks-validator.js +0 -111
- package/framework/rules/morph-task-tracking.md +0 -54
- package/framework/rules/morph-workflow.md +0 -244
- package/framework/schemas/tasks.schema.json +0 -79
- package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
- package/framework/skills/core/morph-implement/SKILL.md +0 -194
- package/framework/skills/core/morph-proposal/SKILL.md +0 -152
- package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
- package/framework/templates/docs/proposal.md +0 -192
- package/framework/templates/docs/spec.md +0 -57
- package/framework/templates/docs/ui-components.md +0 -52
- package/framework/templates/docs/ui-design-system.md +0 -76
- package/framework/templates/docs/ui-flows.md +0 -167
- package/framework/templates/examples/spec-examples.md +0 -91
- package/framework/templates/feature/decisions.md +0 -124
- package/framework/templates/feature/recap.md +0 -145
- package/framework/templates/tasks.template.json +0 -17
- package/src/commands/state/usecase.js +0 -143
- package/src/commands/tasks/dag.js +0 -181
- package/src/commands/utils/index.js +0 -3
- package/src/lib/agents/output-collector.js +0 -76
- package/src/lib/detectors/design-system-detector.js +0 -188
- package/src/lib/evaluator-report.js +0 -104
- package/src/lib/generators/design-system-generator.js +0 -508
- package/src/lib/specs/acervo-format.js +0 -70
- package/src/lib/specs/acervo-lifecycle.js +0 -264
- package/src/lib/specs/done-criteria-check.js +0 -200
- package/src/lib/specs/usecase-check.js +0 -301
- package/src/lib/specs/usecase-promoter.js +0 -301
- package/src/lib/specs/usecase-registry.js +0 -237
- package/src/lib/tasks/dispatch-planner.js +0 -319
- package/src/lib/tasks/dotnet-test-introspection.js +0 -632
- package/src/lib/tasks/mermaid-renderer.js +0 -132
- package/src/lib/tasks/task-classifier.js +0 -30
- package/src/lib/tasks/task-parser.js +0 -209
- package/src/lib/tasks/tasks-json-parser.js +0 -296
- package/src/lib/tasks/test-scope.js +0 -597
- package/src/utils/agent-id-mapper.js +0 -11
|
@@ -1,30 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* Every rule here is borrowed, never re-encoded:
|
|
6
|
-
* - which tree owns a feature → `resolveFeatureTree` (feature-truth.js)
|
|
7
|
-
* - the current phase → `derivePhaseFrom` (state-reader.js)
|
|
8
|
-
* - gate order → `GATE_PREREQ` (gate-order.js)
|
|
9
|
-
* - the evaluator's verdict → `evaluatorVerdictFromText` (evaluator-report.js)
|
|
10
|
-
* - path confinement → `canonicalPath`/`isInside` (test-runner.js)
|
|
11
|
-
* - tasks.json validity → `validateTasksJson` (tasks-json-parser.js)
|
|
12
|
-
*
|
|
13
|
-
* Three places a feature can live: the root `.morph/features/{f}`, a worktree
|
|
14
|
-
* `{worktrees}/{wt}/.morph/features/{f}`, and `.morph/archive/{f}` once
|
|
15
|
-
* `finish` ran. The root copy of a feature built in a worktree is its
|
|
16
|
-
* birth-instant skeleton and never changes again, so for an active feature the
|
|
17
|
-
* owner tree wins. Only active features pay for that question — it spawns git.
|
|
18
|
-
*
|
|
19
|
-
* Read-only and fail-open: a corrupt file degrades one feature, never the scan.
|
|
2
|
+
* The root copy of a feature built in a worktree is its birth skeleton, so for an
|
|
3
|
+
* active feature the owner tree wins. Only active features ask: it spawns git.
|
|
20
4
|
*/
|
|
21
5
|
|
|
22
6
|
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
23
7
|
import { join, basename, isAbsolute } from 'path';
|
|
24
|
-
import { derivePhaseFrom, PHASE_ARTIFACTS } from '../../../framework/hooks/shared/state-reader.js';
|
|
8
|
+
import { derivePhaseFrom, diskArtifactProbe, PHASE_ARTIFACTS } from '../../../framework/hooks/shared/state-reader.js';
|
|
25
9
|
import { GATE_PREREQ } from '../../core/state/gate-order.js';
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
10
|
+
import { sliceProgress, checksPath } from '../../../framework/hooks/shared/slice-progress.js';
|
|
11
|
+
import {
|
|
12
|
+
reviewReportsFromDir, requiredReviewRoles, combinedVerdict, allBlockers, checksCoverage, reviewFileName, REVIEW_ROLES,
|
|
13
|
+
} from '../../lib/review-reports.js';
|
|
28
14
|
import { resolveFeatureTree } from '../../lib/worktree/feature-truth.js';
|
|
29
15
|
import { containerDirs } from '../../../framework/hooks/shared/worktree-root.js';
|
|
30
16
|
import { canonicalPath, isInside, readMorphConfig } from '../../lib/tasks/test-runner.js';
|
|
@@ -33,7 +19,6 @@ import { isFinished } from './feature-status.js';
|
|
|
33
19
|
export const GATES = Object.keys(GATE_PREREQ);
|
|
34
20
|
const FINISHED = new Set(['completed', 'abandoned']);
|
|
35
21
|
const FOLDER_PHASE = Object.fromEntries(PHASE_ARTIFACTS.map(([folder, phase]) => [folder, phase]));
|
|
36
|
-
const EVALUATOR_REPORT = '4-review/evaluator-report.md';
|
|
37
22
|
const MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
38
23
|
const MAX_DEPTH = 4;
|
|
39
24
|
|
|
@@ -47,10 +32,7 @@ function subdirs(dir) {
|
|
|
47
32
|
} catch { return []; }
|
|
48
33
|
}
|
|
49
34
|
|
|
50
|
-
/**
|
|
51
|
-
* Old feature.json files stamped a gate as a bare `true` (MORPH_OS). Lifting it
|
|
52
|
-
* to `{approved: true}` is what lets the shared rules read them unchanged.
|
|
53
|
-
*/
|
|
35
|
+
/** Old feature.json files stamped a gate as a bare `true`. */
|
|
54
36
|
export function normalizeGates(gates) {
|
|
55
37
|
const out = {};
|
|
56
38
|
for (const [name, value] of Object.entries(gates || {})) {
|
|
@@ -64,10 +46,6 @@ function normalizeFeature(fj) {
|
|
|
64
46
|
return { ...f, approvalGates: normalizeGates(f.approvalGates || f.approvals) };
|
|
65
47
|
}
|
|
66
48
|
|
|
67
|
-
/**
|
|
68
|
-
* Every place a feature lives, before deciding which one tells the truth.
|
|
69
|
-
* @returns {Map<string, {root?: string, worktrees: string[], archive?: string}>}
|
|
70
|
-
*/
|
|
71
49
|
function collectLocations(projectDir) {
|
|
72
50
|
const found = new Map();
|
|
73
51
|
const slot = (name) => {
|
|
@@ -92,17 +70,10 @@ function collectLocations(projectDir) {
|
|
|
92
70
|
return found;
|
|
93
71
|
}
|
|
94
72
|
|
|
95
|
-
/**
|
|
96
|
-
* Decide which directory is this feature's truth.
|
|
97
|
-
*
|
|
98
|
-
* @returns {{ location: 'root'|'worktree'|'archive', dir: string, feature: object,
|
|
99
|
-
* ownerTree: string|null, rootStale: boolean }}
|
|
100
|
-
*/
|
|
101
73
|
function resolveLocation(projectDir, name, loc, resolveTree) {
|
|
102
74
|
const rootFj = loc.root ? readJson(join(loc.root, 'feature.json')) : null;
|
|
103
75
|
|
|
104
|
-
//
|
|
105
|
-
// keeps a feature.json per finished feature) — the archive holds the files.
|
|
76
|
+
// An old root copy may linger; the archive holds the files.
|
|
106
77
|
const rootFinished = rootFj && (FINISHED.has(rootFj.status) || rootFj.archivedAt);
|
|
107
78
|
if (loc.archive && (!rootFj || rootFinished)) {
|
|
108
79
|
const archFj = readJson(join(loc.archive, 'feature.json'));
|
|
@@ -122,8 +93,7 @@ function resolveLocation(projectDir, name, loc, resolveTree) {
|
|
|
122
93
|
const wtDir = join(tree, '.morph', 'features', name);
|
|
123
94
|
const wtFj = readJson(join(wtDir, 'feature.json'));
|
|
124
95
|
if (wtFj) return { location: 'worktree', dir: wtDir, feature: wtFj, ownerTree: tree, rootStale: false };
|
|
125
|
-
// The owner tree
|
|
126
|
-
// the root skeleton, and the UI has to say so.
|
|
96
|
+
// The owner tree has no copy of the feature: only the root skeleton is left.
|
|
127
97
|
return { location: 'root', dir: loc.root, feature: rootFj, ownerTree: tree, rootStale: true };
|
|
128
98
|
}
|
|
129
99
|
}
|
|
@@ -140,32 +110,23 @@ function resolveLocation(projectDir, name, loc, resolveTree) {
|
|
|
140
110
|
};
|
|
141
111
|
}
|
|
142
112
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
113
|
+
export function readSlices(dir) {
|
|
114
|
+
const progress = sliceProgress(dir);
|
|
115
|
+
if (!progress) return null;
|
|
116
|
+
const extracted = new Map((readJson(checksPath(dir))?.slices || []).filter((x) => x && x.id).map((x) => [x.id, x]));
|
|
117
|
+
const list = progress.slices.map((sl) => {
|
|
118
|
+
const x = extracted.get(sl.id) || {};
|
|
119
|
+
return {
|
|
120
|
+
...sl,
|
|
121
|
+
persona: x.persona ?? null,
|
|
122
|
+
files: Array.isArray(x.files) ? x.files : [],
|
|
123
|
+
covers: Array.isArray(x.covers) ? x.covers : [],
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
return { counts: { total: progress.total, done: progress.done, partial: progress.partial, pending: progress.pending }, list };
|
|
155
127
|
}
|
|
156
128
|
|
|
157
|
-
|
|
158
|
-
const counts = { total: list.length, done: 0, inProgress: 0, blocked: 0, pending: 0, skipped: 0 };
|
|
159
|
-
for (const t of list) {
|
|
160
|
-
// `completed` is the pre-schema spelling of `done` (MORPH_OS archives carry both).
|
|
161
|
-
if (t.status === 'done' || t.status === 'completed') counts.done += 1;
|
|
162
|
-
else if (t.status === 'in_progress') counts.inProgress += 1;
|
|
163
|
-
else if (t.status === 'blocked') counts.blocked += 1;
|
|
164
|
-
else if (t.status === 'skipped') counts.skipped += 1;
|
|
165
|
-
else counts.pending += 1;
|
|
166
|
-
}
|
|
167
|
-
return counts;
|
|
168
|
-
}
|
|
129
|
+
const NO_SLICES = { total: 0, done: 0, partial: 0, pending: 0 };
|
|
169
130
|
|
|
170
131
|
function gatesOf(feature) {
|
|
171
132
|
const skipped = new Set(feature.skippedPhases || []);
|
|
@@ -185,20 +146,60 @@ function gatesOf(feature) {
|
|
|
185
146
|
});
|
|
186
147
|
}
|
|
187
148
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
149
|
+
// Security counts only when its report exists: deciding it is required takes git.
|
|
150
|
+
function reviewVerdict(dir, workType, reports = reviewReportsFromDir(dir)) {
|
|
151
|
+
const present = Object.values(reports).some((r) => r.status !== 'missing');
|
|
152
|
+
if (!present) return null;
|
|
153
|
+
const required = requiredReviewRoles({ workType, securityRequired: reports.security.status !== 'missing' });
|
|
154
|
+
return { verdict: combinedVerdict(reports, required), blockers: allBlockers(reports) };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function reviewRows(reports, workType) {
|
|
158
|
+
const required = requiredReviewRoles({ workType, securityRequired: reports.security.status !== 'missing' });
|
|
159
|
+
return Object.keys(REVIEW_ROLES)
|
|
160
|
+
.filter((role) => required.includes(role) || reports[role].status !== 'missing')
|
|
161
|
+
.map((role) => {
|
|
162
|
+
const r = reports[role];
|
|
163
|
+
return {
|
|
164
|
+
role,
|
|
165
|
+
path: `4-review/${reviewFileName(role)}`,
|
|
166
|
+
status: r.status,
|
|
167
|
+
verdict: r.verdict ?? null,
|
|
168
|
+
blockers: r.blockers?.length ?? 0,
|
|
169
|
+
reviewedSha: r.reviewedSha ?? null,
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** `coverage` is `null` until a verifier report exists; only PROVADO covers. */
|
|
175
|
+
export function readChecks(dir, verifier) {
|
|
176
|
+
const checks = readJson(checksPath(dir))?.checks;
|
|
177
|
+
if (!Array.isArray(checks)) return null;
|
|
178
|
+
const list = checks.filter((c) => c && c.id);
|
|
179
|
+
const rows = verifier?.checks ?? null;
|
|
180
|
+
const byId = new Map((rows ?? []).map((r) => [r.id, r]));
|
|
181
|
+
const coverage = verifier && verifier.status !== 'missing'
|
|
182
|
+
? { total: list.length, ...checksCoverage(list.map((c) => c.id), rows) }
|
|
183
|
+
: null;
|
|
184
|
+
return {
|
|
185
|
+
coverage,
|
|
186
|
+
list: list.map((c) => ({
|
|
187
|
+
id: c.id,
|
|
188
|
+
text: c.text ?? '',
|
|
189
|
+
proof: c.proof ?? null,
|
|
190
|
+
status: byId.get(c.id)?.status ?? null,
|
|
191
|
+
evidence: byId.get(c.id)?.evidence ?? null,
|
|
192
|
+
})),
|
|
193
|
+
};
|
|
192
194
|
}
|
|
193
195
|
|
|
194
196
|
function summarize(name, resolved) {
|
|
195
197
|
const { location, dir, ownerTree, rootStale } = resolved;
|
|
196
198
|
const feature = normalizeFeature(resolved.feature);
|
|
197
|
-
const has = (rel) => existsSync(join(dir, ...rel.split('/')));
|
|
198
199
|
const gates = gatesOf(feature);
|
|
199
200
|
const reviewApproved = gates.find((g) => g.gate === 'review')?.approved === true;
|
|
200
|
-
const
|
|
201
|
-
const verdict =
|
|
201
|
+
const slices = readSlices(dir);
|
|
202
|
+
const verdict = reviewVerdict(dir, feature.workType);
|
|
202
203
|
const status = feature.status || (location === 'archive' ? 'completed' : 'draft');
|
|
203
204
|
const finished = isFinished({ status, location });
|
|
204
205
|
return {
|
|
@@ -210,29 +211,24 @@ function summarize(name, resolved) {
|
|
|
210
211
|
dir,
|
|
211
212
|
ownerTree,
|
|
212
213
|
rootStale,
|
|
213
|
-
phase: derivePhaseFrom(feature,
|
|
214
|
-
inEvaluation:
|
|
214
|
+
phase: derivePhaseFrom(feature, diskArtifactProbe(dir)),
|
|
215
|
+
inEvaluation: verdict !== null && !reviewApproved,
|
|
215
216
|
gates,
|
|
216
217
|
awaiting: !finished && gates.some((g) => g.paused),
|
|
217
|
-
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
taskIndex: finished ? [] : tasks.list.map((t) => ({ id: t.id, title: t.title || '', status: t.status || 'pending' })),
|
|
218
|
+
slices: slices?.counts ?? NO_SLICES,
|
|
219
|
+
// Finished features leave slice titles out: archived plans would bloat every list reply.
|
|
220
|
+
sliceIndex: finished || !slices ? [] : slices.list.map((sl) => ({ id: sl.id, title: sl.title, status: sl.status })),
|
|
221
221
|
verification: feature.verification
|
|
222
|
-
? { status: feature.verification.status ?? null,
|
|
222
|
+
? { status: feature.verification.status ?? null, ts: feature.verification.ts ?? null }
|
|
223
223
|
: null,
|
|
224
224
|
verdict: verdict?.verdict ?? null,
|
|
225
|
-
blockers: verdict
|
|
225
|
+
blockers: verdict ? verdict.blockers.length : null,
|
|
226
226
|
createdAt: feature.createdAt || null,
|
|
227
227
|
updatedAt: feature.updatedAt || feature.archivedAt || feature.createdAt || null,
|
|
228
228
|
archivedAt: feature.archivedAt || null,
|
|
229
229
|
};
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
/**
|
|
233
|
-
* Files of a feature, grouped by the phase their top folder belongs to.
|
|
234
|
-
* @returns {Array<{path: string, phase: string|null, size: number, mtime: string}>}
|
|
235
|
-
*/
|
|
236
232
|
export function listDocuments(dir) {
|
|
237
233
|
const out = [];
|
|
238
234
|
const walk = (abs, rel, depth) => {
|
|
@@ -262,9 +258,8 @@ export function listDocuments(dir) {
|
|
|
262
258
|
}
|
|
263
259
|
|
|
264
260
|
/**
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
* must not pay again. It is only trusted while its feature.json still reads.
|
|
261
|
+
* `known` is where a previous scan found the feature, so opening it does not pay
|
|
262
|
+
* for git again. It is only trusted while its feature.json still reads.
|
|
268
263
|
*/
|
|
269
264
|
function resolveOne(projectDir, name, resolveTree, known) {
|
|
270
265
|
if (known?.dir) {
|
|
@@ -276,13 +271,6 @@ function resolveOne(projectDir, name, resolveTree, known) {
|
|
|
276
271
|
return resolveLocation(projectDir, name, loc, resolveTree);
|
|
277
272
|
}
|
|
278
273
|
|
|
279
|
-
/**
|
|
280
|
-
* The feature list of a project.
|
|
281
|
-
*
|
|
282
|
-
* @param {string} projectDir
|
|
283
|
-
* @param {{ resolveTree?: Function }} [opts] - seam for tests; defaults to
|
|
284
|
-
* `resolveFeatureTree`
|
|
285
|
-
*/
|
|
286
274
|
export function scanFeatures(projectDir, { resolveTree = resolveFeatureTree } = {}) {
|
|
287
275
|
const config = readMorphConfig(projectDir);
|
|
288
276
|
const features = [];
|
|
@@ -301,32 +289,28 @@ export function scanFeatures(projectDir, { resolveTree = resolveFeatureTree } =
|
|
|
301
289
|
trust: config?.trust || null,
|
|
302
290
|
dir: projectDir,
|
|
303
291
|
initialized: existsSync(join(projectDir, '.morph')),
|
|
304
|
-
//
|
|
305
|
-
// per-feature state model — the dashboard says so instead of showing "empty".
|
|
292
|
+
// state.json with no feature.json anywhere predates the per-feature state model.
|
|
306
293
|
legacyStateOnly: hasState && features.length === 0,
|
|
307
294
|
},
|
|
308
295
|
features,
|
|
309
296
|
};
|
|
310
297
|
}
|
|
311
298
|
|
|
312
|
-
/**
|
|
313
|
-
* One feature in full: summary + tasks with their verify, the gate
|
|
314
|
-
* decisions, the verify nodes and the document tree.
|
|
315
|
-
*/
|
|
316
299
|
export function readFeatureDetail(projectDir, name, { resolveTree = resolveFeatureTree, known } = {}) {
|
|
317
300
|
const resolved = resolveOne(projectDir, name, resolveTree, known);
|
|
318
301
|
if (!resolved) return null;
|
|
319
302
|
const summary = summarize(name, resolved);
|
|
320
303
|
const feature = normalizeFeature(resolved.feature);
|
|
321
|
-
const
|
|
322
|
-
const
|
|
304
|
+
const slices = readSlices(resolved.dir);
|
|
305
|
+
const reports = reviewReportsFromDir(resolved.dir);
|
|
306
|
+
const verdict = reviewVerdict(resolved.dir, feature.workType, reports);
|
|
307
|
+
const checks = readChecks(resolved.dir, reports.verifier);
|
|
323
308
|
return {
|
|
324
309
|
...summary,
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
tasksErrors: tasks.errors,
|
|
310
|
+
slicesList: slices?.list ?? [],
|
|
311
|
+
checksList: checks?.list ?? [],
|
|
312
|
+
checksCoverage: checks?.coverage ?? null,
|
|
313
|
+
reviews: reviewRows(reports, feature.workType),
|
|
330
314
|
gateDecisions: feature.gateDecisions || {},
|
|
331
315
|
verificationNodes: feature.verification?.nodes || null,
|
|
332
316
|
featureVerification: feature.verifications?.feature || null,
|
|
@@ -336,13 +320,8 @@ export function readFeatureDetail(projectDir, name, { resolveTree = resolveFeatu
|
|
|
336
320
|
};
|
|
337
321
|
}
|
|
338
322
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
* Junctions and win32 case are resolved by `canonicalPath` before the check,
|
|
342
|
-
* so `..`, an absolute path or a link that leaves the folder are all refused.
|
|
343
|
-
*
|
|
344
|
-
* @returns {{ path: string, content: string, mtime: string } | { error: string, status: number }}
|
|
345
|
-
*/
|
|
323
|
+
// Junctions and win32 case are resolved by `canonicalPath` before the check,
|
|
324
|
+
// so `..`, an absolute path or a link that leaves the feature folder are refused.
|
|
346
325
|
export function readFeatureFile(projectDir, name, relPath, { resolveTree = resolveFeatureTree, known } = {}) {
|
|
347
326
|
if (typeof relPath !== 'string' || relPath === '') return { error: 'path ausente', status: 400 };
|
|
348
327
|
if (isAbsolute(relPath) || /^[a-zA-Z]:/.test(relPath)) return { error: 'path fora da feature', status: 403 };
|
|
@@ -26,13 +26,7 @@ function extractImports(source) {
|
|
|
26
26
|
return imports;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/**
|
|
30
|
-
* Claude Code event name → the kebab-case key the Hook Inspector groups by.
|
|
31
|
-
* `pre-compact` and `teammate-idle` have no MORPH hooks — those nodes render
|
|
32
|
-
* empty on purpose. `worktree-create`/`worktree-remove` are the worktree
|
|
33
|
-
* lifecycle events (v3.8.0) — outside the tool-call flow, drawn as their own
|
|
34
|
-
* pair in HookInspector.jsx.
|
|
35
|
-
*/
|
|
29
|
+
/** `pre-compact` and `teammate-idle` have no MORPH hooks: those nodes render empty on purpose. */
|
|
36
30
|
const EVENT_KEYS = {
|
|
37
31
|
SessionStart: 'session-start',
|
|
38
32
|
UserPromptSubmit: 'user-prompt',
|
|
@@ -47,20 +41,7 @@ const EVENT_KEYS = {
|
|
|
47
41
|
WorktreeRemove: 'worktree-remove',
|
|
48
42
|
};
|
|
49
43
|
|
|
50
|
-
/**
|
|
51
|
-
* Build filename → [{ event, matcher }] from the installer's MORPH_HOOKS.
|
|
52
|
-
*
|
|
53
|
-
* The event is NOT derivable from the path. Before the v4→v5 migration the
|
|
54
|
-
* hooks sat in per-event folders, so `basename(dirname(file))` produced the
|
|
55
|
-
* event key; the migration flattened everything into `core/`, and from then on
|
|
56
|
-
* every hook reported `event: 'core'`. Nothing crashed — the Inspector groups
|
|
57
|
-
* by event, found no key it knew, and rendered every node empty. Twelve hooks,
|
|
58
|
-
* zero shown.
|
|
59
|
-
*
|
|
60
|
-
* `getHookDefinitions()` is the single source of truth (the same array the
|
|
61
|
-
* installer writes into settings.local.json), so the registration and the
|
|
62
|
-
* drawing can no longer disagree.
|
|
63
|
-
*/
|
|
44
|
+
/** The event is not derivable from the path: every hook sits in `core/`. */
|
|
64
45
|
export function hookRegistrations(definitions = getHookDefinitions()) {
|
|
65
46
|
const byFile = new Map();
|
|
66
47
|
for (const def of definitions) {
|
|
@@ -69,10 +50,8 @@ export function hookRegistrations(definitions = getHookDefinitions()) {
|
|
|
69
50
|
for (const h of def.hooks || []) {
|
|
70
51
|
const file = basename(String(h.command || '').trim());
|
|
71
52
|
if (!file.endsWith('.js')) continue;
|
|
72
|
-
// Grupos consolidados apontam para chain.js com os
|
|
73
|
-
//
|
|
74
|
-
// runner: é o elo que o Inspector precisa mostrar. chain.js em si não é
|
|
75
|
-
// um hook de domínio e ficaria como nó fantasma em todo evento.
|
|
53
|
+
// Grupos consolidados apontam para chain.js com os elos em `args`: registre
|
|
54
|
+
// cada elo, senão chain.js vira nó fantasma em todo evento.
|
|
76
55
|
const files = file === 'chain.js' && Array.isArray(h.args) && h.args.length
|
|
77
56
|
? h.args.map(a => `${a}.js`)
|
|
78
57
|
: [file];
|
|
@@ -80,8 +59,7 @@ export function hookRegistrations(definitions = getHookDefinitions()) {
|
|
|
80
59
|
if (!byFile.has(f)) byFile.set(f, []);
|
|
81
60
|
const entries = byFile.get(f);
|
|
82
61
|
const matcher = def.matcher || null;
|
|
83
|
-
//
|
|
84
|
-
// Failure/Batch siblings): one node per column, not one per event.
|
|
62
|
+
// PostToolUse and its Failure/Batch siblings share one Inspector column.
|
|
85
63
|
if (entries.some((e) => e.event === key && e.matcher === matcher)) continue;
|
|
86
64
|
entries.push({ event: key, rawEvent: def.event, matcher });
|
|
87
65
|
}
|
|
@@ -111,7 +89,6 @@ export function scanHooks(frameworkDir) {
|
|
|
111
89
|
// PostToolUse and UserPromptSubmit, and must show under both.
|
|
112
90
|
const regs = registrations.get(filename);
|
|
113
91
|
if (!regs || regs.length === 0) {
|
|
114
|
-
// On disk but never registered — real dead weight, and worth seeing.
|
|
115
92
|
return [{ ...base, event: 'unregistered', matcher: null }];
|
|
116
93
|
}
|
|
117
94
|
return regs.map(r => ({ ...base, event: r.event, rawEvent: r.rawEvent, matcher: r.matcher }));
|
|
@@ -6,10 +6,6 @@ import { scanStandards } from './standards-scanner.js';
|
|
|
6
6
|
import { scanRules } from './rules-scanner.js';
|
|
7
7
|
import { scanDocs } from './docs-scanner.js';
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
* The framework's knowledge, for the static virtual module. The project's
|
|
11
|
-
* state (features, activity) is served live by project-api.js instead.
|
|
12
|
-
*/
|
|
13
9
|
export function scanAll(frameworkDir) {
|
|
14
10
|
const agents = scanAgents(frameworkDir);
|
|
15
11
|
const phases = scanPhases(frameworkDir);
|
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* module lists the projects, routes `/p/:id/…` to the right one, and answers
|
|
5
|
-
* which project a changed file belongs to.
|
|
6
|
-
*
|
|
7
|
-
* The list is the registry (src/lib/projects/registry.js) plus the project the
|
|
8
|
-
* dashboard was launched from, which is always shown even if it cannot be
|
|
9
|
-
* registered. Projects found next to the registered ones are offered, never
|
|
10
|
-
* added on their own.
|
|
11
|
-
*
|
|
12
|
-
* A cold project can take seconds to read (a 100k-event log). The portfolio
|
|
13
|
-
* never waits for one: `summary` is served only for projects already in
|
|
14
|
-
* memory, and the rest are read in the background (`warmNext`).
|
|
2
|
+
* The portfolio never waits for a cold project (a 100k-event log takes seconds):
|
|
3
|
+
* `summary` is served only from memory, the rest is read by `warmNext`.
|
|
15
4
|
*/
|
|
16
5
|
|
|
17
6
|
import { dirname, basename } from 'path';
|
|
@@ -25,18 +14,12 @@ function slug(text) {
|
|
|
25
14
|
|
|
26
15
|
const lower = (p) => (process.platform === 'win32' ? p.toLowerCase() : p);
|
|
27
16
|
|
|
28
|
-
/**
|
|
29
|
-
* @param {{ launchDir: string, registry?: object, createApi?: Function }} opts
|
|
30
|
-
* `registry` and `createApi` are test seams.
|
|
31
|
-
*/
|
|
32
17
|
export function createPortfolio({ launchDir, registry = registryModule, createApi = createProjectApi }) {
|
|
33
18
|
const launchKey = registry.projectKey(launchDir);
|
|
34
|
-
const apis = new Map();
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
const lastSummary = new Map(); // dir key -> summary
|
|
19
|
+
const apis = new Map();
|
|
20
|
+
// While a changed project is read again, its last summary keeps showing.
|
|
21
|
+
const lastSummary = new Map();
|
|
38
22
|
|
|
39
|
-
/** The projects shown, with stable ids derived from the folder name. */
|
|
40
23
|
function list() {
|
|
41
24
|
const dirs = registry.readRegistry().projects.map((p) => ({ dir: p.dir, name: p.name, source: p.source }));
|
|
42
25
|
if (!dirs.some((p) => lower(p.dir) === lower(launchKey))) {
|
|
@@ -61,7 +44,6 @@ export function createPortfolio({ launchDir, registry = registryModule, createAp
|
|
|
61
44
|
return list().find((p) => p.id === id) || null;
|
|
62
45
|
}
|
|
63
46
|
|
|
64
|
-
/** The project a changed path belongs to — the deepest registered folder containing it. */
|
|
65
47
|
function ownerOf(path) {
|
|
66
48
|
const p = lower(String(path).replace(/\\/g, '/'));
|
|
67
49
|
let best = null;
|
|
@@ -78,7 +60,6 @@ export function createPortfolio({ launchDir, registry = registryModule, createAp
|
|
|
78
60
|
return registry.discoverProjects(roots).filter((s) => !projects.some((p) => lower(p.dir) === lower(s.dir)));
|
|
79
61
|
}
|
|
80
62
|
|
|
81
|
-
/** Read the next project that is not in memory yet. @returns {object|null} the project warmed */
|
|
82
63
|
function warmNext() {
|
|
83
64
|
const next = list().find((p) => !apiFor(p.dir).ready());
|
|
84
65
|
if (!next) return null;
|
|
@@ -106,11 +87,6 @@ export function createPortfolio({ launchDir, registry = registryModule, createAp
|
|
|
106
87
|
|
|
107
88
|
const json = (status, body) => ({ status, body });
|
|
108
89
|
|
|
109
|
-
/**
|
|
110
|
-
* @param {string} method
|
|
111
|
-
* @param {string} url - relative to `/api/morph`
|
|
112
|
-
* @param {object|null} [body] - parsed JSON of a POST
|
|
113
|
-
*/
|
|
114
90
|
function handle(method, url, body = null) {
|
|
115
91
|
const u = new URL(url, 'http://local');
|
|
116
92
|
const parts = u.pathname.split('/').filter(Boolean);
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* changes on disk. The framework knowledge stays in the static virtual module.
|
|
5
|
-
*
|
|
6
|
-
* Two caches with two lifetimes. A running agent appends to `events.jsonl` on
|
|
7
|
-
* every tool call, so a log change drops only the event cache — rescanning the
|
|
8
|
-
* features (git calls for the active ones) on each tool call would be waste.
|
|
2
|
+
* Two caches with two lifetimes: an agent appends to `events.jsonl` on every tool
|
|
3
|
+
* call, so a log change drops only the event cache, never the feature scan.
|
|
9
4
|
*/
|
|
10
5
|
|
|
11
6
|
import { join } from 'path';
|
|
@@ -16,17 +11,9 @@ import { containerDirs } from '../../../framework/hooks/shared/worktree-root.js'
|
|
|
16
11
|
import { isFinished } from './feature-status.js';
|
|
17
12
|
import { resolveFeatureTree } from '../../lib/worktree/feature-truth.js';
|
|
18
13
|
|
|
19
|
-
/** How long a remembered "which tree owns this feature" answer is trusted without a structural change. */
|
|
20
14
|
const TREE_TTL_MS = 60_000;
|
|
21
15
|
|
|
22
|
-
/**
|
|
23
|
-
* Which cache a changed path invalidates, or null when it is none of ours.
|
|
24
|
-
* `.morph/framework/` is the installed framework copy (often a junction), and
|
|
25
|
-
* it never changes what this API returns.
|
|
26
|
-
*
|
|
27
|
-
* @param {string} path
|
|
28
|
-
* @returns {'activity'|'features'|null}
|
|
29
|
-
*/
|
|
16
|
+
/** `.morph/framework/` is the installed framework copy and never changes what this API returns. */
|
|
30
17
|
export function classifyChange(path) {
|
|
31
18
|
const p = String(path).replace(/\\/g, '/');
|
|
32
19
|
if (!p.includes('/.morph/') && !p.endsWith('/.morph')) return null;
|
|
@@ -36,19 +23,12 @@ export function classifyChange(path) {
|
|
|
36
23
|
return null;
|
|
37
24
|
}
|
|
38
25
|
|
|
39
|
-
/**
|
|
40
|
-
* A full-page reload Vite would send for a file under a project's `.morph/`
|
|
41
|
-
* (an agent writing a design .html). The dashboard's own code never lives
|
|
42
|
-
* there, so such a reload is never needed.
|
|
43
|
-
*/
|
|
26
|
+
/** Vite would full-reload for a `.html` under `.morph/`; the dashboard's code never lives there. */
|
|
44
27
|
export function isMorphDataReload(path) {
|
|
45
28
|
return typeof path === 'string' && path.replace(/\\/g, '/').includes('/.morph/');
|
|
46
29
|
}
|
|
47
30
|
|
|
48
|
-
/**
|
|
49
|
-
* The directories worth watching: the root's and each worktree's feature,
|
|
50
|
-
* archive, log and config folders — never a whole worktree (its node_modules).
|
|
51
|
-
*/
|
|
31
|
+
/** Never a whole worktree: its node_modules. */
|
|
52
32
|
export function watchTargets(projectDir) {
|
|
53
33
|
const morphDirs = [join(projectDir, '.morph')];
|
|
54
34
|
for (const container of containerDirs(projectDir)) {
|
|
@@ -72,12 +52,6 @@ function json(status, body) {
|
|
|
72
52
|
return { status, body };
|
|
73
53
|
}
|
|
74
54
|
|
|
75
|
-
/**
|
|
76
|
-
* @param {string} projectDir
|
|
77
|
-
* @param {{ scanFeatures?: Function, readFeatureDetail?: Function,
|
|
78
|
-
* readFeatureFile?: Function, loadEvents?: Function, resolveTree?: Function,
|
|
79
|
-
* now?: () => number }} [deps] - test seams
|
|
80
|
-
*/
|
|
81
55
|
export function createProjectApi(projectDir, deps = {}) {
|
|
82
56
|
const scan = deps.scanFeatures || scanFeatures;
|
|
83
57
|
const detail = deps.readFeatureDetail || readFeatureDetail;
|
|
@@ -87,10 +61,8 @@ export function createProjectApi(projectDir, deps = {}) {
|
|
|
87
61
|
: {});
|
|
88
62
|
const cache = { features: null };
|
|
89
63
|
|
|
90
|
-
// Which tree owns a feature costs
|
|
91
|
-
//
|
|
92
|
-
// an agent rewrites feature.json or tasks.json, which is what happens all
|
|
93
|
-
// the time. Remembered per feature; a structural change or TTL_MS forgets it.
|
|
64
|
+
// Which tree owns a feature costs git calls (~0.3 s each on Windows) and only
|
|
65
|
+
// changes on add/remove, never on a rewrite: remembered until a structural event or TTL_MS.
|
|
94
66
|
const resolveTree = deps.resolveTree || resolveFeatureTree;
|
|
95
67
|
const now = deps.now || Date.now;
|
|
96
68
|
const trees = new Map();
|
|
@@ -104,17 +76,9 @@ export function createProjectApi(projectDir, deps = {}) {
|
|
|
104
76
|
|
|
105
77
|
const features = () => (cache.features ||= scan(projectDir, { resolveTree: rememberedTree }));
|
|
106
78
|
const events = () => store.events();
|
|
107
|
-
// Where the list scan found each feature: opening one reuses it instead of
|
|
108
|
-
// asking git again.
|
|
109
79
|
const known = (name) => features().features?.find((f) => f.name === name) || undefined;
|
|
110
80
|
|
|
111
|
-
/**
|
|
112
|
-
* @param {'features'|'activity'|'all'} scope
|
|
113
|
-
* @param {string} [path] - the file that changed; a live log is then tailed
|
|
114
|
-
* instead of reloading every log
|
|
115
|
-
* @param {string} [event] - the watcher event; anything but 'change' (a file
|
|
116
|
-
* or folder added or removed) may move a feature to another tree
|
|
117
|
-
*/
|
|
81
|
+
/** Any `event` but 'change' may move a feature to another tree. */
|
|
118
82
|
function invalidate(scope, path, event) {
|
|
119
83
|
if (scope === 'features' || scope === 'all') cache.features = null;
|
|
120
84
|
if (scope === 'all' || (scope === 'features' && event !== 'change')) trees.clear();
|
|
@@ -122,10 +86,6 @@ export function createProjectApi(projectDir, deps = {}) {
|
|
|
122
86
|
else if (scope === 'activity') store.changed(path);
|
|
123
87
|
}
|
|
124
88
|
|
|
125
|
-
/**
|
|
126
|
-
* @param {string} url - path + query, relative to `/api/morph`
|
|
127
|
-
* @returns {{ status: number, body: object }}
|
|
128
|
-
*/
|
|
129
89
|
function handle(url) {
|
|
130
90
|
const u = new URL(url, 'http://local');
|
|
131
91
|
const parts = u.pathname.split('/').filter(Boolean).map(decodeURIComponent);
|
|
@@ -159,20 +119,14 @@ export function createProjectApi(projectDir, deps = {}) {
|
|
|
159
119
|
}
|
|
160
120
|
}
|
|
161
121
|
|
|
162
|
-
/** Load everything once, so the first page a person opens does not pay for it. */
|
|
163
122
|
function warm() {
|
|
164
123
|
try { features(); events(); } catch { /* the first request will report it */ }
|
|
165
124
|
}
|
|
166
125
|
|
|
167
|
-
/** Cheap to answer now: the list and the events are both in memory. */
|
|
168
126
|
function ready() {
|
|
169
127
|
return cache.features !== null && store.ready();
|
|
170
128
|
}
|
|
171
129
|
|
|
172
|
-
/**
|
|
173
|
-
* What the portfolio shows of this project: counts, the active features with
|
|
174
|
-
* their last activity, and when anything last happened.
|
|
175
|
-
*/
|
|
176
130
|
function summary() {
|
|
177
131
|
const scan = features();
|
|
178
132
|
const last = lastActivityByFeature(events());
|
|
@@ -20,8 +20,7 @@ function parseFrontmatter(content) {
|
|
|
20
20
|
|
|
21
21
|
const ROUTE_LINE = /^- \*\*([a-z0-9-]+)\*\* \(`([^`]+\.md)`\)/;
|
|
22
22
|
|
|
23
|
-
//
|
|
24
|
-
// import inside .claude/rules resolves relative to the rule file and never loads.
|
|
23
|
+
// An `@path` import inside .claude/rules resolves relative to the rule file and never loads.
|
|
25
24
|
export function parseStandardRoutes(content) {
|
|
26
25
|
const routes = [];
|
|
27
26
|
for (const line of content.split(/\r?\n/)) {
|