@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,132 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* mermaid-renderer.js — pure renderer: tasks.json DAG -> Mermaid flowchart TD.
|
|
3
|
-
*
|
|
4
|
-
* Backs `morph-spec dag <feature> --mermaid` (S8-T1), so the plan's DAG can be
|
|
5
|
-
* visualized at Gate 2 (IndyDevDan rec. 2: draw the workflow in Mermaid before
|
|
6
|
-
* automating it).
|
|
7
|
-
*
|
|
8
|
-
* STRICT Mermaid rules enforced here (see project CLAUDE.md — violating these
|
|
9
|
-
* breaks rendering in Obsidian):
|
|
10
|
-
* - every node label wrapped in double quotes, no exceptions
|
|
11
|
-
* - labels never start with a numeric-with-dot prefix ("1. ", "2. ...") —
|
|
12
|
-
* Obsidian's markdown-list parser chokes on it even inside quotes
|
|
13
|
-
* - `&` inside labels -> `e`
|
|
14
|
-
* - line breaks inside labels -> `<br/>`, never a literal `\n`
|
|
15
|
-
* - double quotes inside titles are stripped so they can't break out of the
|
|
16
|
-
* surrounding label quotes
|
|
17
|
-
*
|
|
18
|
-
* Pure module: no fs, no Date.now()/Math.random(). Tasks are sorted by
|
|
19
|
-
* numeric id so output is stable regardless of input order.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
const MAX_TITLE_LENGTH = 40;
|
|
23
|
-
|
|
24
|
-
const VALID_STATUS_CLASSES = ['done', 'in_progress', 'blocked', 'skipped', 'pending'];
|
|
25
|
-
|
|
26
|
-
// Web-safe fill/stroke pairs, one per task status.
|
|
27
|
-
const CLASS_DEFS = [
|
|
28
|
-
'classDef done fill:#c6f6d5,stroke:#2f855a,color:#22543d',
|
|
29
|
-
'classDef in_progress fill:#bee3f8,stroke:#2b6cb0,color:#2a4365',
|
|
30
|
-
'classDef blocked fill:#feb2b2,stroke:#c53030,color:#63171b',
|
|
31
|
-
'classDef skipped fill:#e2e8f0,stroke:#718096,color:#2d3748',
|
|
32
|
-
'classDef pending fill:#edf2f7,stroke:#a0aec0,color:#2d3748',
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
function numericId(id) {
|
|
36
|
-
const match = /^T(\d+)$/.exec(id ?? '');
|
|
37
|
-
return match ? parseInt(match[1], 10) : Number.POSITIVE_INFINITY;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Sanitize arbitrary text for embedding inside a double-quoted Mermaid node
|
|
42
|
-
* label, per the project's strict Mermaid rules.
|
|
43
|
-
*
|
|
44
|
-
* - double quotes stripped (they would close the label's own quotes)
|
|
45
|
-
* - `&` -> `e`
|
|
46
|
-
* - truncated to MAX_TITLE_LENGTH chars + `…`
|
|
47
|
-
* - line breaks -> `<br/>`
|
|
48
|
-
*
|
|
49
|
-
* Ordering matters: truncation runs on the raw text BEFORE newlines expand to
|
|
50
|
-
* `<br/>`. Newline->`<br/>` is the only length-changing substitution (1 char ->
|
|
51
|
-
* 5); truncating after it could slice a tag mid-way ("...<b…"), so we truncate
|
|
52
|
-
* first — every emitted `<br/>` is then guaranteed whole. The char-for-char
|
|
53
|
-
* substitutions (`"`->`'`, `&`->`e`) don't move the truncation boundary.
|
|
54
|
-
*/
|
|
55
|
-
export function sanitizeLabel(text) {
|
|
56
|
-
let out = String(text ?? '');
|
|
57
|
-
out = out.replace(/"/g, "'");
|
|
58
|
-
out = out.replace(/&/g, 'e');
|
|
59
|
-
if (out.length > MAX_TITLE_LENGTH) {
|
|
60
|
-
out = `${out.slice(0, MAX_TITLE_LENGTH)}…`;
|
|
61
|
-
}
|
|
62
|
-
out = out.replace(/\r\n|\r|\n/g, '<br/>');
|
|
63
|
-
return out;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Same fallback as groupOf() in dispatch-planner.js / groupTasks() in
|
|
68
|
-
* tasks-json-parser.js — tasks without a `group` field are treated as
|
|
69
|
-
* 'backend'.
|
|
70
|
-
*/
|
|
71
|
-
function groupOf(task) {
|
|
72
|
-
return task.group || 'backend';
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function statusClass(status) {
|
|
76
|
-
return VALID_STATUS_CLASSES.includes(status) ? status : 'pending';
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Render a parsed tasks.json document as a Mermaid `flowchart TD`.
|
|
81
|
-
*
|
|
82
|
-
* @param {{ tasks?: Array<{ id: string, title: string, dependencies?: string[], effort?: string, group?: string, status?: string }> }} tasksData
|
|
83
|
-
* @returns {string}
|
|
84
|
-
*/
|
|
85
|
-
export function renderMermaidFlowchart(tasksData) {
|
|
86
|
-
const tasks = Array.isArray(tasksData?.tasks) ? tasksData.tasks : [];
|
|
87
|
-
|
|
88
|
-
if (tasks.length === 0) {
|
|
89
|
-
return ['flowchart TD', ' empty["sem tasks"]'].join('\n');
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const sorted = [...tasks].sort((a, b) => numericId(a.id) - numericId(b.id));
|
|
93
|
-
const knownIds = new Set(sorted.map(t => t.id));
|
|
94
|
-
|
|
95
|
-
// Group tasks by `group`, preserving the deterministic sorted-by-id order
|
|
96
|
-
// in which each group is first encountered.
|
|
97
|
-
const groups = new Map();
|
|
98
|
-
for (const task of sorted) {
|
|
99
|
-
const g = groupOf(task);
|
|
100
|
-
if (!groups.has(g)) groups.set(g, []);
|
|
101
|
-
groups.get(g).push(task);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const lines = ['flowchart TD'];
|
|
105
|
-
|
|
106
|
-
for (const [group, groupTasks] of groups) {
|
|
107
|
-
lines.push(` subgraph ${group}["${sanitizeLabel(group)}"]`);
|
|
108
|
-
for (const task of groupTasks) {
|
|
109
|
-
const label = `${task.id} · ${sanitizeLabel(task.title)} (${task.effort})`;
|
|
110
|
-
lines.push(` ${task.id}["${label}"]`);
|
|
111
|
-
}
|
|
112
|
-
lines.push(' end');
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Edges — one per dependency. Dependencies pointing at a non-existent id
|
|
116
|
-
// are silently skipped (never throw on a dangling reference).
|
|
117
|
-
for (const task of sorted) {
|
|
118
|
-
const deps = Array.isArray(task.dependencies) ? task.dependencies : [];
|
|
119
|
-
for (const dep of deps) {
|
|
120
|
-
if (!knownIds.has(dep)) continue;
|
|
121
|
-
lines.push(` ${dep} --> ${task.id}`);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
lines.push(...CLASS_DEFS);
|
|
126
|
-
|
|
127
|
-
for (const task of sorted) {
|
|
128
|
-
lines.push(` class ${task.id} ${statusClass(task.status)}`);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return lines.join('\n');
|
|
132
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Task classifier — identifies test/validation and infrastructure tasks by title.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
const TEST_TASK_PATTERN = /\b(tests?|testing|specs?|coverage|e2e|unit|integration|integra[cç][aã]o|valida[cç][aã]o|cobertura|testes?)\b/i;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Returns true if the task title indicates a testing or validation task.
|
|
9
|
-
*
|
|
10
|
-
* @param {string} taskTitle
|
|
11
|
-
* @returns {boolean}
|
|
12
|
-
*/
|
|
13
|
-
export function isTestTask(taskTitle) {
|
|
14
|
-
if (!taskTitle) return false;
|
|
15
|
-
return TEST_TASK_PATTERN.test(taskTitle);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const INFRA_TASK_PATTERN = /\b(infrastructure|infra|cleanup|migration|migra[cç][aã]o|config|setup|ci\/cd|devops|deploy|docker|packages?|dependencies|tags)\b/i;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Returns true if the task title indicates an infrastructure/cleanup/migration task.
|
|
22
|
-
* Used to skip contract validation and suppress breaking change warnings.
|
|
23
|
-
*
|
|
24
|
-
* @param {string} taskTitle
|
|
25
|
-
* @returns {boolean}
|
|
26
|
-
*/
|
|
27
|
-
export function isInfrastructureTask(taskTitle) {
|
|
28
|
-
if (!taskTitle) return false;
|
|
29
|
-
return INFRA_TASK_PATTERN.test(taskTitle);
|
|
30
|
-
}
|
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Task Parser — Shared utilities for reading, parsing, and grouping tasks.
|
|
3
|
-
*
|
|
4
|
-
* Extracted from bin/task-manager.cjs and dispatch-agents.js so all
|
|
5
|
-
* consumers share the same parsing logic without duplication.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { readFile } from 'fs/promises';
|
|
9
|
-
import { readFileSync, existsSync } from 'fs';
|
|
10
|
-
import { join } from 'path';
|
|
11
|
-
|
|
12
|
-
// ============================================================================
|
|
13
|
-
// TASK_CATEGORY_TO_GROUP — maps task categories to parallel groups
|
|
14
|
-
// ============================================================================
|
|
15
|
-
|
|
16
|
-
const TASK_CATEGORY_TO_GROUP = {
|
|
17
|
-
'domain': 'backend',
|
|
18
|
-
'domain-bc': 'backend',
|
|
19
|
-
'infrastructure': 'backend',
|
|
20
|
-
'application': 'backend',
|
|
21
|
-
'presentation': 'frontend',
|
|
22
|
-
'tests': 'tests',
|
|
23
|
-
'infra': 'infra',
|
|
24
|
-
'docs': 'docs',
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
// ============================================================================
|
|
28
|
-
// parseTaskList — structured task parsing with error reporting
|
|
29
|
-
// ============================================================================
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Parse tasks.md content into structured task objects with error reporting.
|
|
33
|
-
*
|
|
34
|
-
* @param {string} content - Raw tasks.md content
|
|
35
|
-
* @returns {{ tasks: ParsedTask[], errors: TaskParseError[], groups: Object<string, string[]> }}
|
|
36
|
-
*/
|
|
37
|
-
export function parseTaskList(content) {
|
|
38
|
-
const tasks = [];
|
|
39
|
-
const errors = [];
|
|
40
|
-
const groups = {};
|
|
41
|
-
|
|
42
|
-
const taskHeadingRe = /^###\s+(T\d{3,})\s*[—–:\-]\s*(.+)$/gm;
|
|
43
|
-
const groupRe = /(?:\*\*Group\*\*\s*:\s*`?([a-z-]+)`?|- \*\*Group:\*\*\s*`?([a-z-]+)`?)/i;
|
|
44
|
-
const categoryRe = /(?:\*\*category\*\*\s*:\s*`?([a-z-]+)`?|category:\s*([a-z-]+))/i;
|
|
45
|
-
|
|
46
|
-
let headingMatch;
|
|
47
|
-
const taskPositions = [];
|
|
48
|
-
|
|
49
|
-
while ((headingMatch = taskHeadingRe.exec(content)) !== null) {
|
|
50
|
-
taskPositions.push({ id: headingMatch[1], title: headingMatch[2].trim(), pos: headingMatch.index });
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
for (let i = 0; i < taskPositions.length; i++) {
|
|
54
|
-
const { id, title, pos } = taskPositions[i];
|
|
55
|
-
const end = i + 1 < taskPositions.length ? taskPositions[i + 1].pos : content.length;
|
|
56
|
-
const block = content.slice(pos, end);
|
|
57
|
-
|
|
58
|
-
// Try group field first (v4), fall back to category field (v3)
|
|
59
|
-
const grpMatch = groupRe.exec(block);
|
|
60
|
-
const catMatch = categoryRe.exec(block);
|
|
61
|
-
let group;
|
|
62
|
-
if (grpMatch) {
|
|
63
|
-
group = (grpMatch[1] || grpMatch[2]).toLowerCase();
|
|
64
|
-
} else if (catMatch) {
|
|
65
|
-
const category = (catMatch[1] || catMatch[2]).toLowerCase();
|
|
66
|
-
group = TASK_CATEGORY_TO_GROUP[category] || 'backend';
|
|
67
|
-
} else {
|
|
68
|
-
group = 'backend';
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
tasks.push({ id, title, group });
|
|
72
|
-
|
|
73
|
-
if (!groups[group]) groups[group] = [];
|
|
74
|
-
groups[group].push(id);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return { tasks, errors, groups };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// ============================================================================
|
|
81
|
-
// parseAndGroupTasks — drop-in replacement for dispatch-agents.js
|
|
82
|
-
// ============================================================================
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Parse tasks.md and return a map of { group -> taskId[] }.
|
|
86
|
-
* Gracefully returns {} if the file doesn't exist or is unparseable.
|
|
87
|
-
*
|
|
88
|
-
* @param {string} tasksPath - Absolute path to tasks.md
|
|
89
|
-
* @returns {Object<string, string[]>}
|
|
90
|
-
*/
|
|
91
|
-
export function parseAndGroupTasks(tasksPath) {
|
|
92
|
-
if (!existsSync(tasksPath)) return {};
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
const content = readFileSync(tasksPath, 'utf8');
|
|
96
|
-
return parseTaskList(content).groups;
|
|
97
|
-
} catch {
|
|
98
|
-
return {};
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// ============================================================================
|
|
103
|
-
// parseTasksMd
|
|
104
|
-
// ============================================================================
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Parse tasks.md to extract task stubs for the v3 state format.
|
|
108
|
-
* Accepts headings in either format:
|
|
109
|
-
* ### T001 — Task title (em-dash / en-dash / hyphen)
|
|
110
|
-
* ### T001: Task title (colon)
|
|
111
|
-
*
|
|
112
|
-
* @param {string} featureName
|
|
113
|
-
* @param {string} [cwd] - Project root (defaults to process.cwd())
|
|
114
|
-
* @returns {Promise<Array<{id, title, status, dependencies, files, checkpoint}>>}
|
|
115
|
-
*/
|
|
116
|
-
export async function parseTasksMd(featureName, cwd = process.cwd()) {
|
|
117
|
-
const tasksPath = join(cwd, `.morph/features/${featureName}/2-plan/tasks.md`);
|
|
118
|
-
let content = '';
|
|
119
|
-
try {
|
|
120
|
-
content = await readFile(tasksPath, 'utf-8');
|
|
121
|
-
} catch {
|
|
122
|
-
return [];
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Phase 1: collect heading positions
|
|
126
|
-
const headingRe = /^###\s+(T\d+)\s*[—–:\-]\s*(.+)$/gm;
|
|
127
|
-
const depRe = /\*\*Dependenc(?:y|ies):\*\*\s*(.+)/i;
|
|
128
|
-
const positions = [];
|
|
129
|
-
let match;
|
|
130
|
-
while ((match = headingRe.exec(content)) !== null) {
|
|
131
|
-
positions.push({ id: match[1], title: match[2].trim(), pos: match.index });
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Phase 2: extract body and parse dependencies
|
|
135
|
-
const tasks = [];
|
|
136
|
-
for (let i = 0; i < positions.length; i++) {
|
|
137
|
-
const { id, title, pos } = positions[i];
|
|
138
|
-
const end = i + 1 < positions.length ? positions[i + 1].pos : content.length;
|
|
139
|
-
const block = content.slice(pos, end);
|
|
140
|
-
|
|
141
|
-
let dependencies = [];
|
|
142
|
-
const depMatch = depRe.exec(block);
|
|
143
|
-
if (depMatch) {
|
|
144
|
-
const val = depMatch[1].trim();
|
|
145
|
-
if (val.toLowerCase() !== 'none') {
|
|
146
|
-
dependencies = val.split(/[,\s]+/).filter(s => /^T\d+$/.test(s));
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
tasks.push({ id, title, status: 'pending', dependencies, files: [], checkpoint: null });
|
|
151
|
-
}
|
|
152
|
-
return tasks;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// ============================================================================
|
|
156
|
-
// ensureTaskList
|
|
157
|
-
// ============================================================================
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Ensure feature.taskList exists (array of individual task objects).
|
|
161
|
-
*
|
|
162
|
-
* In v3 state, feature.tasks is a counter object {total, completed, …}.
|
|
163
|
-
* Individual task objects live in feature.taskList.
|
|
164
|
-
*
|
|
165
|
-
* @param {Object} feature - Feature state object (mutated in place)
|
|
166
|
-
* @param {string} featureName
|
|
167
|
-
* @param {string} [cwd]
|
|
168
|
-
* @returns {Promise<Array>} The task list
|
|
169
|
-
*/
|
|
170
|
-
export async function ensureTaskList(feature, featureName, cwd = process.cwd()) {
|
|
171
|
-
if (Array.isArray(feature.tasks)) {
|
|
172
|
-
// v2 format: tasks IS the array
|
|
173
|
-
return feature.tasks;
|
|
174
|
-
}
|
|
175
|
-
// v3 format: use taskList or build from tasks.md
|
|
176
|
-
if (!feature.taskList || feature.taskList.length === 0) {
|
|
177
|
-
feature.taskList = await parseTasksMd(featureName, cwd);
|
|
178
|
-
}
|
|
179
|
-
return feature.taskList;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// ============================================================================
|
|
183
|
-
// syncCounters
|
|
184
|
-
// ============================================================================
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* After modifying taskList, sync the counter fields back to feature.tasks.
|
|
188
|
-
* tasks.total is derived from the actual taskList length (not trusted from state).
|
|
189
|
-
*
|
|
190
|
-
* @deprecated Prefer reading directly from feature.taskList. Counter fields
|
|
191
|
-
* (feature.tasks) duplicate native Claude Code task tracking. This function
|
|
192
|
-
* is kept for backward compatibility but will be removed in a future version.
|
|
193
|
-
*
|
|
194
|
-
* @param {Object} feature - Feature state object (mutated in place)
|
|
195
|
-
*/
|
|
196
|
-
export function syncCounters(feature) {
|
|
197
|
-
if (Array.isArray(feature.tasks)) return; // v2 format — nothing to sync
|
|
198
|
-
// Safety: ensure feature.tasks is an object (may be undefined after migration or corruption)
|
|
199
|
-
if (!feature.tasks || typeof feature.tasks !== 'object') {
|
|
200
|
-
feature.tasks = { total: 0, completed: 0, inProgress: 0, pending: 0 };
|
|
201
|
-
}
|
|
202
|
-
const list = feature.taskList || [];
|
|
203
|
-
// Exclude expanded tasks — they are replaced by their sub-tasks
|
|
204
|
-
const active = list.filter(t => t.status !== 'expanded');
|
|
205
|
-
feature.tasks.total = active.length;
|
|
206
|
-
feature.tasks.completed = active.filter(t => t.status === 'completed').length;
|
|
207
|
-
feature.tasks.inProgress = active.filter(t => t.status === 'in_progress').length;
|
|
208
|
-
feature.tasks.pending = active.filter(t => t.status === 'pending').length;
|
|
209
|
-
}
|
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* tasks-json-parser.js — read, validate, query, mutate tasks.json
|
|
3
|
-
*
|
|
4
|
-
* Replaces the prior Markdown-based tasks.md parser for new features.
|
|
5
|
-
* Legacy features that still use tasks.md are handled by task-parser.js.
|
|
6
|
-
*
|
|
7
|
-
* Uses a minimal hand-rolled JSON Schema validator (no AJV dep) — sufficient
|
|
8
|
-
* for the controlled schema in framework/schemas/tasks.schema.json.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import fs from 'fs-extra';
|
|
12
|
-
import { readFileSync, existsSync } from 'fs';
|
|
13
|
-
import { join, dirname } from 'path';
|
|
14
|
-
import { fileURLToPath } from 'url';
|
|
15
|
-
import { UC_ID_RE } from '../specs/acervo-format.js';
|
|
16
|
-
|
|
17
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
-
const SCHEMA_PATH = join(__dirname, '..', '..', '..', 'framework', 'schemas', 'tasks.schema.json');
|
|
19
|
-
|
|
20
|
-
let cachedSchema = null;
|
|
21
|
-
export function getSchema() {
|
|
22
|
-
if (cachedSchema) return cachedSchema;
|
|
23
|
-
cachedSchema = JSON.parse(readFileSync(SCHEMA_PATH, 'utf8'));
|
|
24
|
-
return cachedSchema;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const VALID_STATUS = ['pending', 'in_progress', 'done', 'skipped', 'blocked'];
|
|
28
|
-
const VALID_EFFORT = ['S', 'M', 'L', 'XL'];
|
|
29
|
-
const VALID_GROUP = ['backend', 'frontend', 'tests', 'infra', 'docs'];
|
|
30
|
-
/** Veredito de reuso por conceito — ver framework/schemas/tasks.schema.json. */
|
|
31
|
-
const VALID_REUSE_VERDICT = ['REUSAR', 'ESTENDER', 'CRIAR'];
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Validate a tasks.json structure.
|
|
35
|
-
* @returns {{ valid: boolean, errors: string[] }}
|
|
36
|
-
*/
|
|
37
|
-
export function validateTasksJson(data) {
|
|
38
|
-
const errors = [];
|
|
39
|
-
if (!data || typeof data !== 'object') {
|
|
40
|
-
errors.push('tasks.json must be an object');
|
|
41
|
-
return { valid: false, errors };
|
|
42
|
-
}
|
|
43
|
-
if (data.version !== '1.0') errors.push('version must be "1.0"');
|
|
44
|
-
if (typeof data.feature !== 'string' || !data.feature) {
|
|
45
|
-
errors.push('feature must be a non-empty string');
|
|
46
|
-
}
|
|
47
|
-
if (!Array.isArray(data.tasks)) {
|
|
48
|
-
errors.push('tasks must be an array');
|
|
49
|
-
return { valid: false, errors };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const ids = new Set();
|
|
53
|
-
for (const [i, task] of data.tasks.entries()) {
|
|
54
|
-
const ctx = `tasks[${i}]`;
|
|
55
|
-
if (!task.id || !/^T\d+$/.test(task.id)) {
|
|
56
|
-
errors.push(`${ctx}.id must match /^T\\d+$/`);
|
|
57
|
-
} else if (ids.has(task.id)) {
|
|
58
|
-
errors.push(`${ctx}.id duplicate: ${task.id}`);
|
|
59
|
-
}
|
|
60
|
-
ids.add(task.id);
|
|
61
|
-
|
|
62
|
-
if (typeof task.title !== 'string' || !task.title) errors.push(`${ctx}.title required`);
|
|
63
|
-
if (typeof task.description !== 'string' || !task.description) errors.push(`${ctx}.description required`);
|
|
64
|
-
if (!Array.isArray(task.dependencies)) {
|
|
65
|
-
errors.push(`${ctx}.dependencies must be array`);
|
|
66
|
-
} else {
|
|
67
|
-
for (const dep of task.dependencies) {
|
|
68
|
-
if (typeof dep !== 'string' || !/^T\d+$/.test(dep)) {
|
|
69
|
-
errors.push(`${ctx}.dependencies[${dep}] must match /^T\\d+$/`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
if (!VALID_EFFORT.includes(task.effort)) {
|
|
74
|
-
errors.push(`${ctx}.effort must be one of ${VALID_EFFORT.join(', ')}`);
|
|
75
|
-
}
|
|
76
|
-
if (typeof task.doneCriteria !== 'string' || !task.doneCriteria) {
|
|
77
|
-
errors.push(`${ctx}.doneCriteria required`);
|
|
78
|
-
}
|
|
79
|
-
if (!VALID_STATUS.includes(task.status)) {
|
|
80
|
-
errors.push(`${ctx}.status must be one of ${VALID_STATUS.join(', ')}`);
|
|
81
|
-
}
|
|
82
|
-
if (task.group !== undefined && !VALID_GROUP.includes(task.group)) {
|
|
83
|
-
errors.push(`${ctx}.group must be one of ${VALID_GROUP.join(', ')}`);
|
|
84
|
-
}
|
|
85
|
-
// Optional: a task with no observable behaviour references no use case.
|
|
86
|
-
// When present it must be the bare ID — `morph-spec dag` is the gate that
|
|
87
|
-
// catches "UC-07-listar.md" or "UC7" before the plan reaches Gate 2.
|
|
88
|
-
if (task.usecase !== undefined && (typeof task.usecase !== 'string' || !UC_ID_RE.test(task.usecase))) {
|
|
89
|
-
errors.push(`${ctx}.usecase must match /^UC-\\d+$/ (the ID alone, e.g. "UC-07")`);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Optional: morph-plan may stamp a persona + standards per task (R2 token
|
|
93
|
-
// cut — `morph-spec standards --find` replaces reading the whole
|
|
94
|
-
// STANDARDS.json). Both are part of the plan contract and frozen after
|
|
95
|
-
// Gate 2 by protect-spec-files (any key outside MUTABLE_SPEC_FIELDS is
|
|
96
|
-
// blocked by construction — see that hook's top comment). Type-check
|
|
97
|
-
// only here: no cross-check against agents.json, since stack-filtered
|
|
98
|
-
// installs carry fewer personas than the full set and would false-fail.
|
|
99
|
-
if (task.agent !== undefined) {
|
|
100
|
-
if (typeof task.agent !== 'string' || !task.agent.trim()) {
|
|
101
|
-
errors.push(`${ctx}.agent must be a non-empty string (persona id, e.g. "dotnet-senior")`);
|
|
102
|
-
} else if (task.agent.startsWith('morph-')) {
|
|
103
|
-
errors.push(`${ctx}.agent must be the bare persona id — no "morph-" prefix (got "${task.agent}")`);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
// Type-check e enum apenas: quem cobra o veredito contra o código é o
|
|
107
|
-
// reuse-validator, no `morph-spec verify`.
|
|
108
|
-
if (task.reuse !== undefined) {
|
|
109
|
-
if (!Array.isArray(task.reuse)) {
|
|
110
|
-
errors.push(`${ctx}.reuse must be an array of { concept, verdict, target?, why? }`);
|
|
111
|
-
} else {
|
|
112
|
-
task.reuse.forEach((entry, j) => {
|
|
113
|
-
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
114
|
-
errors.push(`${ctx}.reuse[${j}] must be an object { concept, verdict, target?, why? }`);
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
if (typeof entry.concept !== 'string' || !entry.concept.trim()) {
|
|
118
|
-
errors.push(`${ctx}.reuse[${j}].concept must be a non-empty string`);
|
|
119
|
-
}
|
|
120
|
-
if (!VALID_REUSE_VERDICT.includes(entry.verdict)) {
|
|
121
|
-
errors.push(`${ctx}.reuse[${j}].verdict must be one of ${VALID_REUSE_VERDICT.join(', ')}`);
|
|
122
|
-
}
|
|
123
|
-
if (entry.target !== undefined && typeof entry.target !== 'string') {
|
|
124
|
-
errors.push(`${ctx}.reuse[${j}].target must be a string path`);
|
|
125
|
-
}
|
|
126
|
-
if (entry.why !== undefined && typeof entry.why !== 'string') {
|
|
127
|
-
errors.push(`${ctx}.reuse[${j}].why must be a string`);
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
if (task.standards !== undefined) {
|
|
133
|
-
if (!Array.isArray(task.standards)) {
|
|
134
|
-
errors.push(`${ctx}.standards must be an array of paths`);
|
|
135
|
-
} else {
|
|
136
|
-
task.standards.forEach((s, i) => {
|
|
137
|
-
if (typeof s !== 'string' || !s.trim()) {
|
|
138
|
-
errors.push(`${ctx}.standards[${i}] must be a non-empty string path`);
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return { valid: errors.length === 0, errors };
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Read and parse tasks.json from disk. Throws on validation errors.
|
|
150
|
-
* @param {string} filePath - Absolute path to tasks.json
|
|
151
|
-
* @returns {Promise<object>}
|
|
152
|
-
*/
|
|
153
|
-
export async function parseTasksJson(filePath) {
|
|
154
|
-
const data = await fs.readJson(filePath);
|
|
155
|
-
const { valid, errors } = validateTasksJson(data);
|
|
156
|
-
if (!valid) {
|
|
157
|
-
throw new Error(`Invalid tasks.json:\n ${errors.join('\n ')}`);
|
|
158
|
-
}
|
|
159
|
-
return data;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Lenient variant for read-only analysis commands (e.g. `morph-spec dag`):
|
|
164
|
-
* a missing `version` field is defaulted to "1.0" instead of failing
|
|
165
|
-
* validation — legacy tasks.json predating the field is still analyzable.
|
|
166
|
-
* All other schema errors still throw exactly like `parseTasksJson`.
|
|
167
|
-
*
|
|
168
|
-
* @param {string} filePath - Absolute path to tasks.json
|
|
169
|
-
* @returns {Promise<{ data: object, versionDefaulted: boolean }>}
|
|
170
|
-
*/
|
|
171
|
-
export async function parseTasksJsonLenient(filePath) {
|
|
172
|
-
const data = await fs.readJson(filePath);
|
|
173
|
-
const versionDefaulted = data.version === undefined;
|
|
174
|
-
if (versionDefaulted) data.version = '1.0';
|
|
175
|
-
|
|
176
|
-
const { valid, errors } = validateTasksJson(data);
|
|
177
|
-
if (!valid) {
|
|
178
|
-
throw new Error(`Invalid tasks.json:\n ${errors.join('\n ')}`);
|
|
179
|
-
}
|
|
180
|
-
return { data, versionDefaulted };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Synchronous variant — used by hooks.
|
|
185
|
-
*/
|
|
186
|
-
export function parseTasksJsonSync(filePath) {
|
|
187
|
-
if (!existsSync(filePath)) {
|
|
188
|
-
throw new Error(`tasks.json not found: ${filePath}`);
|
|
189
|
-
}
|
|
190
|
-
const data = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
191
|
-
const { valid, errors } = validateTasksJson(data);
|
|
192
|
-
if (!valid) {
|
|
193
|
-
throw new Error(`Invalid tasks.json:\n ${errors.join('\n ')}`);
|
|
194
|
-
}
|
|
195
|
-
return data;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Teto de `outputs` por task: a task inteira tem de caber num dispatch. Medido
|
|
200
|
-
* em campo (chat-demo-simulacao): tasks de 12 a 30 outputs levaram 6 de 10
|
|
201
|
-
* sub-agents ao teto de turnos, e cada retomada custou um contexto frio.
|
|
202
|
-
*/
|
|
203
|
-
export const MAX_TASK_OUTPUTS = 8;
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* A régua única de "task grande demais", lida pelo `dag` e pelo `gate-check`.
|
|
207
|
-
* Fora de `validateTasksJson` de propósito: `outputs` segue mutável depois do
|
|
208
|
-
* Gate 2, e o teto num write de status travaria feature em voo.
|
|
209
|
-
*
|
|
210
|
-
* @returns {{ id: string, outputs: number }[]}
|
|
211
|
-
*/
|
|
212
|
-
export function oversizedTasks(data) {
|
|
213
|
-
const tasks = Array.isArray(data?.tasks) ? data.tasks : [];
|
|
214
|
-
return tasks
|
|
215
|
-
.filter(t => Array.isArray(t?.outputs) && t.outputs.length > MAX_TASK_OUTPUTS)
|
|
216
|
-
.map(t => ({ id: t.id, outputs: t.outputs.length }));
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* @returns {object[]} tasks with status === 'pending'
|
|
221
|
-
*/
|
|
222
|
-
export function getPendingTasks(data) {
|
|
223
|
-
return data.tasks.filter(t => t.status === 'pending');
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Tasks ready to run: pending AND all dependencies are 'done'.
|
|
228
|
-
*/
|
|
229
|
-
export function getReadyTasks(data) {
|
|
230
|
-
const doneIds = new Set(data.tasks.filter(t => t.status === 'done').map(t => t.id));
|
|
231
|
-
return data.tasks.filter(t =>
|
|
232
|
-
t.status === 'pending' && t.dependencies.every(dep => doneIds.has(dep))
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Mutate `data` to set task status. Returns `data` for chaining.
|
|
238
|
-
*/
|
|
239
|
-
export function updateTaskStatus(data, taskId, newStatus) {
|
|
240
|
-
if (!VALID_STATUS.includes(newStatus)) {
|
|
241
|
-
throw new Error(`Invalid status: ${newStatus}`);
|
|
242
|
-
}
|
|
243
|
-
const task = data.tasks.find(t => t.id === taskId);
|
|
244
|
-
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
245
|
-
task.status = newStatus;
|
|
246
|
-
return data;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Write tasks.json to disk. Validates before writing.
|
|
251
|
-
*/
|
|
252
|
-
export async function writeTasksJson(filePath, data) {
|
|
253
|
-
const { valid, errors } = validateTasksJson(data);
|
|
254
|
-
if (!valid) {
|
|
255
|
-
throw new Error(`Cannot write invalid tasks.json:\n ${errors.join('\n ')}`);
|
|
256
|
-
}
|
|
257
|
-
await fs.writeJson(filePath, data, { spaces: 2 });
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Group tasks by `group` field, falling back to 'backend' when absent.
|
|
262
|
-
* Returns `{ [group]: taskId[] }` shape matching the legacy parseAndGroupTasks output.
|
|
263
|
-
*/
|
|
264
|
-
export function groupTasks(data) {
|
|
265
|
-
const groups = {};
|
|
266
|
-
for (const task of data.tasks) {
|
|
267
|
-
const group = task.group || 'backend';
|
|
268
|
-
if (!groups[group]) groups[group] = [];
|
|
269
|
-
groups[group].push(task.id);
|
|
270
|
-
}
|
|
271
|
-
return groups;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Convert tasks.json to the legacy taskList shape used in state.json.
|
|
276
|
-
* Lets the rest of the codebase keep using feature.taskList while migration
|
|
277
|
-
* is in progress.
|
|
278
|
-
*
|
|
279
|
-
* BOUNDARY — this is the ONE place `done` becomes `completed`, and it is a
|
|
280
|
-
* one-way translation OUT of the tasks.json vocabulary. In tasks.json the
|
|
281
|
-
* terminal status is `done` (tasks.schema.json's enum has no `completed`);
|
|
282
|
-
* `completed` belongs to the legacy taskList and to Claude Code's native
|
|
283
|
-
* session tasks. Leaking `completed` back into a tasks.json write is rejected
|
|
284
|
-
* by `protect-spec-files` — see framework/rules/morph-task-tracking.md, which
|
|
285
|
-
* spells the split out for the author.
|
|
286
|
-
*/
|
|
287
|
-
export function toTaskList(data) {
|
|
288
|
-
return data.tasks.map(t => ({
|
|
289
|
-
id: t.id,
|
|
290
|
-
title: t.title,
|
|
291
|
-
status: t.status === 'done' ? 'completed' : t.status,
|
|
292
|
-
dependencies: [...t.dependencies],
|
|
293
|
-
files: t.outputs || [],
|
|
294
|
-
checkpoint: null,
|
|
295
|
-
}));
|
|
296
|
-
}
|