@therocketcode/gsd-core 1.4.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-plugin/plugin.json +23 -0
- package/GEMINI.md +53 -0
- package/LICENSE +21 -0
- package/README.ja-JP.md +125 -0
- package/README.ko-KR.md +125 -0
- package/README.md +144 -0
- package/README.pt-BR.md +125 -0
- package/README.zh-CN.md +125 -0
- package/agents/gsd-advisor-researcher.md +108 -0
- package/agents/gsd-ai-researcher.md +114 -0
- package/agents/gsd-assumptions-analyzer.md +105 -0
- package/agents/gsd-code-fixer.md +668 -0
- package/agents/gsd-code-reviewer.md +387 -0
- package/agents/gsd-codebase-mapper.md +853 -0
- package/agents/gsd-debug-session-manager.md +314 -0
- package/agents/gsd-debugger.md +1452 -0
- package/agents/gsd-doc-classifier.md +168 -0
- package/agents/gsd-doc-synthesizer.md +204 -0
- package/agents/gsd-doc-verifier.md +217 -0
- package/agents/gsd-doc-writer.md +616 -0
- package/agents/gsd-domain-researcher.md +147 -0
- package/agents/gsd-eval-auditor.md +191 -0
- package/agents/gsd-eval-planner.md +154 -0
- package/agents/gsd-executor.md +785 -0
- package/agents/gsd-framework-selector.md +160 -0
- package/agents/gsd-integration-checker.md +470 -0
- package/agents/gsd-intel-updater.md +342 -0
- package/agents/gsd-nyquist-auditor.md +203 -0
- package/agents/gsd-pattern-mapper.md +335 -0
- package/agents/gsd-phase-researcher.md +867 -0
- package/agents/gsd-plan-checker.md +978 -0
- package/agents/gsd-planner.md +1204 -0
- package/agents/gsd-project-researcher.md +611 -0
- package/agents/gsd-research-synthesizer.md +259 -0
- package/agents/gsd-roadmapper.md +688 -0
- package/agents/gsd-security-auditor.md +155 -0
- package/agents/gsd-ui-auditor.md +495 -0
- package/agents/gsd-ui-checker.md +309 -0
- package/agents/gsd-ui-researcher.md +374 -0
- package/agents/gsd-user-profiler.md +171 -0
- package/agents/gsd-verifier.md +923 -0
- package/assets/gsd-logo-2000-transparent.png +0 -0
- package/assets/gsd-logo-2000-transparent.svg +17 -0
- package/assets/gsd-logo-2000.png +0 -0
- package/assets/gsd-logo-2000.svg +21 -0
- package/assets/terminal.svg +68 -0
- package/bin/install.js +12726 -0
- package/bin/lib/ui-safety-gate.cjs +107 -0
- package/commands/gsd/add-tests.md +42 -0
- package/commands/gsd/ai-integration-phase.md +37 -0
- package/commands/gsd/audit-fix.md +34 -0
- package/commands/gsd/audit-milestone.md +37 -0
- package/commands/gsd/audit-uat.md +24 -0
- package/commands/gsd/autonomous.md +48 -0
- package/commands/gsd/capture.md +62 -0
- package/commands/gsd/cleanup.md +24 -0
- package/commands/gsd/code-review.md +59 -0
- package/commands/gsd/complete-milestone.md +143 -0
- package/commands/gsd/config.md +56 -0
- package/commands/gsd/debug.md +52 -0
- package/commands/gsd/discover-product.md +65 -0
- package/commands/gsd/discuss-phase.md +77 -0
- package/commands/gsd/docs-update.md +49 -0
- package/commands/gsd/eval-review.md +33 -0
- package/commands/gsd/execute-phase.md +66 -0
- package/commands/gsd/explore.md +27 -0
- package/commands/gsd/extract-learnings.md +23 -0
- package/commands/gsd/fast.md +31 -0
- package/commands/gsd/forensics.md +57 -0
- package/commands/gsd/graphify.md +204 -0
- package/commands/gsd/health.md +31 -0
- package/commands/gsd/help.md +28 -0
- package/commands/gsd/import.md +45 -0
- package/commands/gsd/inbox.md +39 -0
- package/commands/gsd/ingest-docs.md +42 -0
- package/commands/gsd/manager.md +45 -0
- package/commands/gsd/map-codebase.md +83 -0
- package/commands/gsd/milestone-summary.md +51 -0
- package/commands/gsd/model-domain.md +65 -0
- package/commands/gsd/mvp-phase.md +45 -0
- package/commands/gsd/new-milestone.md +45 -0
- package/commands/gsd/new-project.md +47 -0
- package/commands/gsd/ns-context.md +23 -0
- package/commands/gsd/ns-ideate.md +24 -0
- package/commands/gsd/ns-manage.md +29 -0
- package/commands/gsd/ns-project.md +22 -0
- package/commands/gsd/ns-review.md +26 -0
- package/commands/gsd/ns-workflow.md +28 -0
- package/commands/gsd/pause-work.md +43 -0
- package/commands/gsd/phase.md +56 -0
- package/commands/gsd/plan-phase.md +64 -0
- package/commands/gsd/plan-review-convergence.md +59 -0
- package/commands/gsd/pr-branch.md +26 -0
- package/commands/gsd/profile-user.md +46 -0
- package/commands/gsd/progress.md +48 -0
- package/commands/gsd/quick.md +174 -0
- package/commands/gsd/recommend-architecture.md +64 -0
- package/commands/gsd/resume-work.md +30 -0
- package/commands/gsd/review-backlog.md +63 -0
- package/commands/gsd/review.md +42 -0
- package/commands/gsd/secure-phase.md +36 -0
- package/commands/gsd/settings.md +29 -0
- package/commands/gsd/ship.md +24 -0
- package/commands/gsd/sketch.md +60 -0
- package/commands/gsd/spec-phase.md +63 -0
- package/commands/gsd/spike.md +57 -0
- package/commands/gsd/stats.md +20 -0
- package/commands/gsd/surface.md +155 -0
- package/commands/gsd/testing-strategy.md +65 -0
- package/commands/gsd/thread.md +24 -0
- package/commands/gsd/ui-phase.md +35 -0
- package/commands/gsd/ui-review.md +33 -0
- package/commands/gsd/ultraplan-phase.md +34 -0
- package/commands/gsd/undo.md +35 -0
- package/commands/gsd/update.md +49 -0
- package/commands/gsd/validate-phase.md +36 -0
- package/commands/gsd/verify-work.md +39 -0
- package/commands/gsd/workspace.md +52 -0
- package/commands/gsd/workstreams.md +70 -0
- package/gemini-extension.json +6 -0
- package/gsd-core/bin/check-latest-version.cjs +161 -0
- package/gsd-core/bin/gsd-tools.cjs +1928 -0
- package/gsd-core/bin/lib/active-workstream-store.cjs +291 -0
- package/gsd-core/bin/lib/adr-parser.cjs +399 -0
- package/gsd-core/bin/lib/agent-command-router.cjs +68 -0
- package/gsd-core/bin/lib/artifacts.cjs +51 -0
- package/gsd-core/bin/lib/audit.cjs +743 -0
- package/gsd-core/bin/lib/check-command-router.cjs +343 -0
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +81 -0
- package/gsd-core/bin/lib/cli-exit.cjs +42 -0
- package/gsd-core/bin/lib/clock.cjs +95 -0
- package/gsd-core/bin/lib/clusters.cjs +132 -0
- package/gsd-core/bin/lib/code-review-flags.cjs +59 -0
- package/gsd-core/bin/lib/command-aliases.cjs +809 -0
- package/gsd-core/bin/lib/command-arg-projection.cjs +55 -0
- package/gsd-core/bin/lib/command-routing-hub.cjs +300 -0
- package/gsd-core/bin/lib/commands.cjs +1203 -0
- package/gsd-core/bin/lib/config-schema.cjs +29 -0
- package/gsd-core/bin/lib/config-types.cjs +19 -0
- package/gsd-core/bin/lib/config.cjs +738 -0
- package/gsd-core/bin/lib/configuration.cjs +239 -0
- package/gsd-core/bin/lib/context-utilization.cjs +48 -0
- package/gsd-core/bin/lib/core.cjs +2051 -0
- package/gsd-core/bin/lib/decisions.cjs +118 -0
- package/gsd-core/bin/lib/docs.cjs +252 -0
- package/gsd-core/bin/lib/drift.cjs +364 -0
- package/gsd-core/bin/lib/fallow-runner.cjs +115 -0
- package/gsd-core/bin/lib/frontmatter.cjs +442 -0
- package/gsd-core/bin/lib/gap-checker.cjs +257 -0
- package/gsd-core/bin/lib/graphify.cjs +496 -0
- package/gsd-core/bin/lib/gsd2-import.cjs +456 -0
- package/gsd-core/bin/lib/init-command-router.cjs +62 -0
- package/gsd-core/bin/lib/init.cjs +1815 -0
- package/gsd-core/bin/lib/install-profiles.cjs +584 -0
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +122 -0
- package/gsd-core/bin/lib/installer-migration-report.cjs +350 -0
- package/gsd-core/bin/lib/installer-migrations/000-first-time-baseline.cjs +218 -0
- package/gsd-core/bin/lib/installer-migrations/001-legacy-orphan-files.cjs +48 -0
- package/gsd-core/bin/lib/installer-migrations/002-codex-legacy-hooks-json.cjs +94 -0
- package/gsd-core/bin/lib/installer-migrations/003-rename-get-shit-done-to-gsd-core.cjs +108 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +823 -0
- package/gsd-core/bin/lib/intel.cjs +590 -0
- package/gsd-core/bin/lib/learnings.cjs +270 -0
- package/gsd-core/bin/lib/legacy-cleanup.cjs +253 -0
- package/gsd-core/bin/lib/milestone.cjs +373 -0
- package/gsd-core/bin/lib/model-catalog.cjs +154 -0
- package/gsd-core/bin/lib/model-profiles.cjs +24 -0
- package/gsd-core/bin/lib/observability/event.cjs +51 -0
- package/gsd-core/bin/lib/observability/logger.cjs +146 -0
- package/gsd-core/bin/lib/observability/redaction.cjs +48 -0
- package/gsd-core/bin/lib/package-identity.cjs +35 -0
- package/gsd-core/bin/lib/package-legitimacy.cjs +368 -0
- package/gsd-core/bin/lib/phase-command-router.cjs +189 -0
- package/gsd-core/bin/lib/phase-lifecycle.cjs +74 -0
- package/gsd-core/bin/lib/phase.cjs +1307 -0
- package/gsd-core/bin/lib/phases-command-router.cjs +43 -0
- package/gsd-core/bin/lib/plan-scan.cjs +91 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +245 -0
- package/gsd-core/bin/lib/profile-output.cjs +1120 -0
- package/gsd-core/bin/lib/profile-pipeline.cjs +517 -0
- package/gsd-core/bin/lib/project-root.cjs +119 -0
- package/gsd-core/bin/lib/prompt-budget.cjs +305 -0
- package/gsd-core/bin/lib/research-provider.cjs +137 -0
- package/gsd-core/bin/lib/research-store.cjs +167 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +121 -0
- package/gsd-core/bin/lib/roadmap-command-router.cjs +166 -0
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +476 -0
- package/gsd-core/bin/lib/roadmap.cjs +600 -0
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +312 -0
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +56 -0
- package/gsd-core/bin/lib/runtime-homes.cjs +190 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +96 -0
- package/gsd-core/bin/lib/runtime-slash.cjs +119 -0
- package/gsd-core/bin/lib/schema-detect.cjs +159 -0
- package/gsd-core/bin/lib/secrets.cjs +34 -0
- package/gsd-core/bin/lib/security.cjs +480 -0
- package/gsd-core/bin/lib/semver-compare.cjs +42 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +533 -0
- package/gsd-core/bin/lib/state-command-router.cjs +160 -0
- package/gsd-core/bin/lib/state-document.cjs +259 -0
- package/gsd-core/bin/lib/state.cjs +2010 -0
- package/gsd-core/bin/lib/surface.cjs +449 -0
- package/gsd-core/bin/lib/task-command-router.cjs +85 -0
- package/gsd-core/bin/lib/template.cjs +237 -0
- package/gsd-core/bin/lib/uat.cjs +297 -0
- package/gsd-core/bin/lib/ui-safety-gate.cjs +98 -0
- package/gsd-core/bin/lib/update-context.cjs +218 -0
- package/gsd-core/bin/lib/validate-command-router.cjs +91 -0
- package/gsd-core/bin/lib/validate.cjs +112 -0
- package/gsd-core/bin/lib/verification-command-router.cjs +31 -0
- package/gsd-core/bin/lib/verification.cjs +193 -0
- package/gsd-core/bin/lib/verify-command-router.cjs +44 -0
- package/gsd-core/bin/lib/verify.cjs +1451 -0
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +81 -0
- package/gsd-core/bin/lib/workstream-inventory.cjs +147 -0
- package/gsd-core/bin/lib/workstream-name-policy.cjs +91 -0
- package/gsd-core/bin/lib/workstream.cjs +380 -0
- package/gsd-core/bin/lib/worktree-base-ref.cjs +325 -0
- package/gsd-core/bin/lib/worktree-safety.cjs +943 -0
- package/gsd-core/bin/shared/config-defaults.manifest.json +98 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +192 -0
- package/gsd-core/bin/shared/model-catalog.json +149 -0
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +75 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +349 -0
- package/gsd-core/contexts/dev.md +21 -0
- package/gsd-core/contexts/research.md +22 -0
- package/gsd-core/contexts/review.md +23 -0
- package/gsd-core/references/agent-contracts.md +79 -0
- package/gsd-core/references/ai-evals.md +156 -0
- package/gsd-core/references/ai-frameworks.md +186 -0
- package/gsd-core/references/architecture-decision.md +74 -0
- package/gsd-core/references/artifact-types.md +131 -0
- package/gsd-core/references/auth-in-tests.md +91 -0
- package/gsd-core/references/autonomous-smart-discuss.md +277 -0
- package/gsd-core/references/checkpoints.md +814 -0
- package/gsd-core/references/common-bug-patterns.md +114 -0
- package/gsd-core/references/context-budget.md +85 -0
- package/gsd-core/references/continuation-format.md +253 -0
- package/gsd-core/references/db-test-isolation.md +54 -0
- package/gsd-core/references/debugger-philosophy.md +76 -0
- package/gsd-core/references/decimal-phase-calculation.md +64 -0
- package/gsd-core/references/doc-conflict-engine.md +91 -0
- package/gsd-core/references/domain-modeling.md +80 -0
- package/gsd-core/references/domain-probes.md +125 -0
- package/gsd-core/references/e2e-tiering.md +35 -0
- package/gsd-core/references/execute-mvp-tdd.md +81 -0
- package/gsd-core/references/executor-examples.md +110 -0
- package/gsd-core/references/few-shot-examples/plan-checker.md +73 -0
- package/gsd-core/references/few-shot-examples/verifier.md +109 -0
- package/gsd-core/references/flaky-test-checklist.md +22 -0
- package/gsd-core/references/gate-prompts.md +100 -0
- package/gsd-core/references/gates.md +70 -0
- package/gsd-core/references/git-integration.md +298 -0
- package/gsd-core/references/git-planning-commit.md +40 -0
- package/gsd-core/references/ios-scaffold.md +123 -0
- package/gsd-core/references/mandatory-initial-read.md +2 -0
- package/gsd-core/references/model-profile-resolution.md +38 -0
- package/gsd-core/references/model-profiles.md +245 -0
- package/gsd-core/references/mvp-concepts.md +49 -0
- package/gsd-core/references/phase-argument-parsing.md +61 -0
- package/gsd-core/references/planner-antipatterns.md +89 -0
- package/gsd-core/references/planner-chunked.md +49 -0
- package/gsd-core/references/planner-gap-closure.md +62 -0
- package/gsd-core/references/planner-graphify-auto-update.md +67 -0
- package/gsd-core/references/planner-human-verify-mode.md +57 -0
- package/gsd-core/references/planner-interface-context.md +62 -0
- package/gsd-core/references/planner-load-graph-context.md +36 -0
- package/gsd-core/references/planner-mvp-mode.md +53 -0
- package/gsd-core/references/planner-reviews.md +39 -0
- package/gsd-core/references/planner-revision.md +87 -0
- package/gsd-core/references/planner-source-audit.md +73 -0
- package/gsd-core/references/planning-config.md +473 -0
- package/gsd-core/references/product-discovery.md +49 -0
- package/gsd-core/references/project-skills-discovery.md +19 -0
- package/gsd-core/references/questioning.md +162 -0
- package/gsd-core/references/realistic-test-data.md +44 -0
- package/gsd-core/references/research-documentation-lookup.md +29 -0
- package/gsd-core/references/research-philosophy.md +29 -0
- package/gsd-core/references/research-verification-protocol.md +27 -0
- package/gsd-core/references/revision-loop.md +97 -0
- package/gsd-core/references/scout-codebase.md +51 -0
- package/gsd-core/references/skeleton-template.md +48 -0
- package/gsd-core/references/sketch-interactivity.md +41 -0
- package/gsd-core/references/sketch-theme-system.md +94 -0
- package/gsd-core/references/sketch-tooling.md +45 -0
- package/gsd-core/references/sketch-variant-patterns.md +81 -0
- package/gsd-core/references/spidr-splitting.md +69 -0
- package/gsd-core/references/tdd.md +330 -0
- package/gsd-core/references/test-containers.md +55 -0
- package/gsd-core/references/test-strategy.md +75 -0
- package/gsd-core/references/thinking-models-debug.md +44 -0
- package/gsd-core/references/thinking-models-execution.md +50 -0
- package/gsd-core/references/thinking-models-planning.md +62 -0
- package/gsd-core/references/thinking-models-research.md +50 -0
- package/gsd-core/references/thinking-models-verification.md +55 -0
- package/gsd-core/references/thinking-partner.md +96 -0
- package/gsd-core/references/ui-brand.md +162 -0
- package/gsd-core/references/universal-anti-patterns.md +63 -0
- package/gsd-core/references/user-profiling.md +681 -0
- package/gsd-core/references/user-story-template.md +58 -0
- package/gsd-core/references/verification-overrides.md +227 -0
- package/gsd-core/references/verification-patterns.md +612 -0
- package/gsd-core/references/verify-mvp-mode.md +85 -0
- package/gsd-core/references/workstream-flag.md +111 -0
- package/gsd-core/references/worktree-branch-check.md +38 -0
- package/gsd-core/references/worktree-path-safety.md +67 -0
- package/gsd-core/templates/AI-SPEC.md +246 -0
- package/gsd-core/templates/DEBUG.md +169 -0
- package/gsd-core/templates/README.md +77 -0
- package/gsd-core/templates/SECURITY.md +61 -0
- package/gsd-core/templates/UAT.md +265 -0
- package/gsd-core/templates/UI-SPEC.md +100 -0
- package/gsd-core/templates/VALIDATION.md +76 -0
- package/gsd-core/templates/adr.md +58 -0
- package/gsd-core/templates/claude-md.md +145 -0
- package/gsd-core/templates/codebase/architecture.md +255 -0
- package/gsd-core/templates/codebase/concerns.md +310 -0
- package/gsd-core/templates/codebase/conventions.md +307 -0
- package/gsd-core/templates/codebase/integrations.md +280 -0
- package/gsd-core/templates/codebase/stack.md +186 -0
- package/gsd-core/templates/codebase/structure.md +285 -0
- package/gsd-core/templates/codebase/testing.md +480 -0
- package/gsd-core/templates/config.json +62 -0
- package/gsd-core/templates/context.md +352 -0
- package/gsd-core/templates/continue-here.md +78 -0
- package/gsd-core/templates/copilot-instructions.md +7 -0
- package/gsd-core/templates/debug-subagent-prompt.md +91 -0
- package/gsd-core/templates/dev-preferences.md +21 -0
- package/gsd-core/templates/discovery.md +146 -0
- package/gsd-core/templates/discussion-log.md +63 -0
- package/gsd-core/templates/domain-model.md +54 -0
- package/gsd-core/templates/milestone-archive.md +123 -0
- package/gsd-core/templates/milestone.md +115 -0
- package/gsd-core/templates/phase-prompt.md +610 -0
- package/gsd-core/templates/planner-subagent-prompt.md +117 -0
- package/gsd-core/templates/product-brief.md +55 -0
- package/gsd-core/templates/project.md +186 -0
- package/gsd-core/templates/requirements.md +231 -0
- package/gsd-core/templates/research-project/ARCHITECTURE.md +204 -0
- package/gsd-core/templates/research-project/FEATURES.md +147 -0
- package/gsd-core/templates/research-project/PITFALLS.md +200 -0
- package/gsd-core/templates/research-project/STACK.md +120 -0
- package/gsd-core/templates/research-project/SUMMARY.md +170 -0
- package/gsd-core/templates/research.md +592 -0
- package/gsd-core/templates/retrospective.md +54 -0
- package/gsd-core/templates/roadmap.md +202 -0
- package/gsd-core/templates/spec.md +307 -0
- package/gsd-core/templates/state.md +195 -0
- package/gsd-core/templates/summary-complex.md +59 -0
- package/gsd-core/templates/summary-minimal.md +41 -0
- package/gsd-core/templates/summary-standard.md +48 -0
- package/gsd-core/templates/summary.md +248 -0
- package/gsd-core/templates/test-strategy.md +50 -0
- package/gsd-core/templates/user-profile.md +146 -0
- package/gsd-core/templates/user-setup.md +311 -0
- package/gsd-core/templates/verification-report.md +322 -0
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -0
- package/gsd-core/workflows/add-backlog.md +91 -0
- package/gsd-core/workflows/add-phase.md +113 -0
- package/gsd-core/workflows/add-tests.md +355 -0
- package/gsd-core/workflows/add-todo.md +161 -0
- package/gsd-core/workflows/ai-integration-phase.md +295 -0
- package/gsd-core/workflows/analyze-dependencies.md +96 -0
- package/gsd-core/workflows/audit-fix.md +178 -0
- package/gsd-core/workflows/audit-milestone.md +360 -0
- package/gsd-core/workflows/audit-uat.md +110 -0
- package/gsd-core/workflows/autonomous.md +797 -0
- package/gsd-core/workflows/check-todos.md +180 -0
- package/gsd-core/workflows/cleanup.md +195 -0
- package/gsd-core/workflows/code-review-fix.md +502 -0
- package/gsd-core/workflows/code-review.md +658 -0
- package/gsd-core/workflows/complete-milestone.md +855 -0
- package/gsd-core/workflows/debug.md +237 -0
- package/gsd-core/workflows/diagnose-issues.md +245 -0
- package/gsd-core/workflows/discover-product.md +112 -0
- package/gsd-core/workflows/discovery-phase.md +291 -0
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +176 -0
- package/gsd-core/workflows/discuss-phase/modes/all.md +28 -0
- package/gsd-core/workflows/discuss-phase/modes/analyze.md +44 -0
- package/gsd-core/workflows/discuss-phase/modes/auto.md +57 -0
- package/gsd-core/workflows/discuss-phase/modes/batch.md +52 -0
- package/gsd-core/workflows/discuss-phase/modes/chain.md +98 -0
- package/gsd-core/workflows/discuss-phase/modes/default.md +141 -0
- package/gsd-core/workflows/discuss-phase/modes/power.md +44 -0
- package/gsd-core/workflows/discuss-phase/modes/text.md +55 -0
- package/gsd-core/workflows/discuss-phase/templates/checkpoint.json +18 -0
- package/gsd-core/workflows/discuss-phase/templates/context.md +136 -0
- package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +50 -0
- package/gsd-core/workflows/discuss-phase-assumptions.md +675 -0
- package/gsd-core/workflows/discuss-phase-power.md +291 -0
- package/gsd-core/workflows/discuss-phase.md +499 -0
- package/gsd-core/workflows/do.md +111 -0
- package/gsd-core/workflows/docs-update.md +1176 -0
- package/gsd-core/workflows/edit-phase.md +295 -0
- package/gsd-core/workflows/eval-review.md +156 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +95 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +94 -0
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +117 -0
- package/gsd-core/workflows/execute-phase.md +1752 -0
- package/gsd-core/workflows/execute-plan.md +526 -0
- package/gsd-core/workflows/explore.md +146 -0
- package/gsd-core/workflows/extract-learnings.md +243 -0
- package/gsd-core/workflows/fast.md +124 -0
- package/gsd-core/workflows/forensics.md +279 -0
- package/gsd-core/workflows/graduation.md +196 -0
- package/gsd-core/workflows/health.md +224 -0
- package/gsd-core/workflows/help/modes/brief.md +22 -0
- package/gsd-core/workflows/help/modes/default.md +50 -0
- package/gsd-core/workflows/help/modes/full.md +789 -0
- package/gsd-core/workflows/help/modes/topic.md +74 -0
- package/gsd-core/workflows/help.md +24 -0
- package/gsd-core/workflows/import.md +256 -0
- package/gsd-core/workflows/inbox.md +387 -0
- package/gsd-core/workflows/ingest-docs.md +340 -0
- package/gsd-core/workflows/insert-phase.md +152 -0
- package/gsd-core/workflows/list-phase-assumptions.md +178 -0
- package/gsd-core/workflows/list-workspaces.md +57 -0
- package/gsd-core/workflows/manager.md +393 -0
- package/gsd-core/workflows/map-codebase.md +446 -0
- package/gsd-core/workflows/milestone-summary.md +224 -0
- package/gsd-core/workflows/model-domain.md +162 -0
- package/gsd-core/workflows/mvp-phase.md +222 -0
- package/gsd-core/workflows/new-milestone.md +635 -0
- package/gsd-core/workflows/new-project.md +1555 -0
- package/gsd-core/workflows/new-workspace.md +240 -0
- package/gsd-core/workflows/next.md +299 -0
- package/gsd-core/workflows/node-repair.md +92 -0
- package/gsd-core/workflows/note.md +158 -0
- package/gsd-core/workflows/pause-work.md +244 -0
- package/gsd-core/workflows/plan-milestone-gaps.md +281 -0
- package/gsd-core/workflows/plan-phase.md +1814 -0
- package/gsd-core/workflows/plan-review-convergence.md +346 -0
- package/gsd-core/workflows/plant-seed.md +230 -0
- package/gsd-core/workflows/pr-branch.md +157 -0
- package/gsd-core/workflows/profile-user.md +453 -0
- package/gsd-core/workflows/progress.md +699 -0
- package/gsd-core/workflows/quick.md +1017 -0
- package/gsd-core/workflows/reapply-patches.md +426 -0
- package/gsd-core/workflows/recommend-architecture.md +135 -0
- package/gsd-core/workflows/remove-phase.md +156 -0
- package/gsd-core/workflows/remove-workspace.md +108 -0
- package/gsd-core/workflows/resume-project.md +332 -0
- package/gsd-core/workflows/review.md +748 -0
- package/gsd-core/workflows/scan.md +107 -0
- package/gsd-core/workflows/secure-phase.md +182 -0
- package/gsd-core/workflows/session-report.md +146 -0
- package/gsd-core/workflows/settings-advanced.md +810 -0
- package/gsd-core/workflows/settings-integrations.md +312 -0
- package/gsd-core/workflows/settings.md +566 -0
- package/gsd-core/workflows/ship.md +405 -0
- package/gsd-core/workflows/sketch-wrap-up.md +286 -0
- package/gsd-core/workflows/sketch.md +361 -0
- package/gsd-core/workflows/spec-phase.md +263 -0
- package/gsd-core/workflows/spike-wrap-up.md +307 -0
- package/gsd-core/workflows/spike.md +453 -0
- package/gsd-core/workflows/stats.md +80 -0
- package/gsd-core/workflows/sync-skills.md +182 -0
- package/gsd-core/workflows/testing-strategy.md +122 -0
- package/gsd-core/workflows/thread.md +222 -0
- package/gsd-core/workflows/transition.md +694 -0
- package/gsd-core/workflows/ui-phase.md +328 -0
- package/gsd-core/workflows/ui-review.md +193 -0
- package/gsd-core/workflows/ultraplan-phase.md +199 -0
- package/gsd-core/workflows/undo.md +314 -0
- package/gsd-core/workflows/update.md +496 -0
- package/gsd-core/workflows/validate-phase.md +181 -0
- package/gsd-core/workflows/verify-phase.md +544 -0
- package/gsd-core/workflows/verify-work.md +781 -0
- package/hooks/dist/gsd-check-update-worker.js +108 -0
- package/hooks/dist/gsd-check-update.js +66 -0
- package/hooks/dist/gsd-config-reload.js +133 -0
- package/hooks/dist/gsd-context-monitor.js +195 -0
- package/hooks/dist/gsd-cursor-post-tool.js +75 -0
- package/hooks/dist/gsd-cursor-session-start.js +52 -0
- package/hooks/dist/gsd-graphify-update.sh +158 -0
- package/hooks/dist/gsd-phase-boundary.sh +47 -0
- package/hooks/dist/gsd-prompt-guard.js +97 -0
- package/hooks/dist/gsd-read-guard.js +101 -0
- package/hooks/dist/gsd-read-injection-scanner.js +203 -0
- package/hooks/dist/gsd-session-state.sh +59 -0
- package/hooks/dist/gsd-statusline.js +566 -0
- package/hooks/dist/gsd-update-banner.js +138 -0
- package/hooks/dist/gsd-validate-commit.sh +57 -0
- package/hooks/dist/gsd-workflow-guard.js +167 -0
- package/hooks/dist/gsd-worktree-path-guard.js +169 -0
- package/hooks/dist/lib/git-cmd.js +150 -0
- package/hooks/dist/lib/gsd-graphify-rebuild.sh +65 -0
- package/hooks/dist/managed-hooks-registry.cjs +38 -0
- package/hooks/gsd-check-update-worker.js +108 -0
- package/hooks/gsd-check-update.js +66 -0
- package/hooks/gsd-config-reload.js +133 -0
- package/hooks/gsd-context-monitor.js +195 -0
- package/hooks/gsd-cursor-post-tool.js +75 -0
- package/hooks/gsd-cursor-session-start.js +52 -0
- package/hooks/gsd-graphify-update.sh +158 -0
- package/hooks/gsd-phase-boundary.sh +47 -0
- package/hooks/gsd-prompt-guard.js +97 -0
- package/hooks/gsd-read-guard.js +101 -0
- package/hooks/gsd-read-injection-scanner.js +203 -0
- package/hooks/gsd-session-state.sh +59 -0
- package/hooks/gsd-statusline.js +566 -0
- package/hooks/gsd-update-banner.js +138 -0
- package/hooks/gsd-validate-commit.sh +57 -0
- package/hooks/gsd-workflow-guard.js +167 -0
- package/hooks/gsd-worktree-path-guard.js +169 -0
- package/hooks/hooks.json +69 -0
- package/hooks/lib/git-cmd.js +150 -0
- package/hooks/lib/gsd-graphify-rebuild.sh +65 -0
- package/hooks/managed-hooks-registry.cjs +38 -0
- package/package.json +115 -0
- package/scripts/affected-tests-lib.cjs +542 -0
- package/scripts/audit-workflow-script-paths.cjs +73 -0
- package/scripts/base64-scan.sh +351 -0
- package/scripts/build-hooks.js +247 -0
- package/scripts/changeset/README.md +129 -0
- package/scripts/changeset/cli.cjs +590 -0
- package/scripts/changeset/github-release-notes.cjs +199 -0
- package/scripts/changeset/lint.cjs +111 -0
- package/scripts/changeset/new.cjs +137 -0
- package/scripts/changeset/parse.cjs +114 -0
- package/scripts/changeset/render.cjs +34 -0
- package/scripts/changeset/serialize.cjs +130 -0
- package/scripts/check-alias-drift.cjs +114 -0
- package/scripts/check-env.cjs +312 -0
- package/scripts/check-npm-integrity.cjs +215 -0
- package/scripts/ci-guard-runner.cjs +22 -0
- package/scripts/ci-prepare-test-scope.cjs +51 -0
- package/scripts/ci-rebase-check.cjs +86 -0
- package/scripts/ci-test-scope.cjs +431 -0
- package/scripts/command-contract-helpers.cjs +64 -0
- package/scripts/diff-touches-shipped-paths.cjs +155 -0
- package/scripts/fix-slash-commands.cjs +147 -0
- package/scripts/gen-inventory-manifest.cjs +115 -0
- package/scripts/gen-research-agents.cjs +276 -0
- package/scripts/generate-package-identity.cjs +125 -0
- package/scripts/issue-dedupe.cjs +278 -0
- package/scripts/lib/allowlist-ratchet.cjs +136 -0
- package/scripts/lib/cli-exit.cjs +56 -0
- package/scripts/lint-command-contract.cjs +114 -0
- package/scripts/lint-descriptions.cjs +87 -0
- package/scripts/lint-docs-required.cjs +222 -0
- package/scripts/lint-legacy-dir-name.cjs +160 -0
- package/scripts/lint-package-identity-drift.cjs +141 -0
- package/scripts/lint-pr-check-project-dir.cjs +99 -0
- package/scripts/lint-shell-command-projection-drift.cjs +62 -0
- package/scripts/lint-skill-deps.cjs +185 -0
- package/scripts/lint-test-file-count.allowlist.json +135 -0
- package/scripts/lint-test-file-count.cjs +246 -0
- package/scripts/mutation-matrix.cjs +222 -0
- package/scripts/pr-template-policy.cjs +268 -0
- package/scripts/prompt-injection-scan.sh +207 -0
- package/scripts/release-notes/discord-release-summary.cjs +373 -0
- package/scripts/release-notes/format-github-release-notes.cjs +261 -0
- package/scripts/release-tarball-smoke.cjs +629 -0
- package/scripts/research-profiles.cjs +149 -0
- package/scripts/run-affected-tests.cjs +7 -0
- package/scripts/run-cross-platform-tests.cjs +67 -0
- package/scripts/run-tests.cjs +315 -0
- package/scripts/secret-scan-lint.sh +231 -0
- package/scripts/secret-scan.sh +358 -0
- package/scripts/setup-branch-protection.sh +236 -0
- package/scripts/strip-prose-atrefs.cjs +106 -0
- package/scripts/sync-manifest-versions.cjs +119 -0
- package/scripts/sync-rulesets.sh +34 -0
- package/scripts/sync-runtime-launcher.cjs +399 -0
- package/scripts/test-failure-reasons.cjs +34 -0
- package/scripts/verify-npm-publish.cjs +240 -0
- package/scripts/workflow-policy.cjs +450 -0
|
@@ -0,0 +1,1307 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Phase — Phase CRUD, query, and lifecycle operations
|
|
4
|
+
*
|
|
5
|
+
* ADR-457 build-at-publish: the hand-written bin/lib/phase.cjs collapsed to
|
|
6
|
+
* a TypeScript source of truth, compiled by tsc to a gitignored .cjs at the
|
|
7
|
+
* same require() path. Behaviour preserved byte-for-behaviour; only types are added.
|
|
8
|
+
*
|
|
9
|
+
* Re-export shim note (issue #4 / ADR-3524):
|
|
10
|
+
* The phase lifecycle pure-computation helpers live in phase-lifecycle.cjs.
|
|
11
|
+
* cmdPhaseComplete uses
|
|
12
|
+
* deriveProgressFromRoadmap + clampPercent from that module to fix the
|
|
13
|
+
* non-idempotent Completed Phases blind-increment bug.
|
|
14
|
+
*
|
|
15
|
+
* The async mutation handlers (phaseAdd, phaseInsert, phaseRemove, phaseComplete)
|
|
16
|
+
* in phase-lifecycle.ts are I/O-bound and remain per-side per ADR-3524 Section 4.
|
|
17
|
+
* This file provides the CJS (sync) implementations of those handlers.
|
|
18
|
+
*/
|
|
19
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
20
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
21
|
+
};
|
|
22
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
23
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- core.cjs is an export= CommonJS module
|
|
25
|
+
const core = require("./core.cjs");
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module
|
|
27
|
+
const planningWorkspace = require("./planning-workspace.cjs");
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- frontmatter.cjs is an export= CommonJS module
|
|
29
|
+
const frontmatterMod = require("./frontmatter.cjs");
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- state.cjs is an export= CommonJS module
|
|
31
|
+
const stateMod = require("./state.cjs");
|
|
32
|
+
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
33
|
+
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
34
|
+
const phase_lifecycle_cjs_1 = require("./phase-lifecycle.cjs");
|
|
35
|
+
const { escapeRegex, loadConfig, normalizePhaseName, phaseMarkdownRegexSource, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone, toPosixPath, output, error, readSubdirectories, phaseTokenMatches, ERROR_REASON, } = core;
|
|
36
|
+
const { planningDir, withPlanningLock } = planningWorkspace;
|
|
37
|
+
const { extractFrontmatter } = frontmatterMod;
|
|
38
|
+
const { readModifyWriteStateMd, stateExtractField, stateReplaceField, stateReplaceFieldWithFallback, syncStateFrontmatter, withStateLock, updatePerformanceMetricsSection, } = stateMod;
|
|
39
|
+
// Unused import silences TS — keep for structural parity with .cjs (stripShippedMilestones,
|
|
40
|
+
// replaceInCurrentMilestone are exported from core but only used in phase.cjs as-is).
|
|
41
|
+
void stripShippedMilestones;
|
|
42
|
+
void replaceInCurrentMilestone;
|
|
43
|
+
// #2893 — strict canonical filter: `{padded_phase}-{NN}-PLAN.md` or `PLAN.md`.
|
|
44
|
+
const isCanonicalPlanFile = (f) => f.endsWith('-PLAN.md') || f === 'PLAN.md';
|
|
45
|
+
// Any .md file with PLAN anywhere in the basename — diagnostic net
|
|
46
|
+
const PLAN_OUTLINE_RE = /-PLAN-OUTLINE\.md$/i;
|
|
47
|
+
const PLAN_PRE_BOUNCE_RE = /-PLAN.*\.pre-bounce\.md$/i;
|
|
48
|
+
const looksLikePlanFile = (f) => /\.md$/i.test(f) &&
|
|
49
|
+
/PLAN/i.test(f) &&
|
|
50
|
+
!PLAN_OUTLINE_RE.test(f) &&
|
|
51
|
+
!PLAN_PRE_BOUNCE_RE.test(f);
|
|
52
|
+
function describeNonCanonicalPlans(dirFiles, matchedFiles) {
|
|
53
|
+
const matched = new Set(matchedFiles);
|
|
54
|
+
const offenders = dirFiles.filter((f) => looksLikePlanFile(f) && !matched.has(f));
|
|
55
|
+
if (offenders.length === 0)
|
|
56
|
+
return null;
|
|
57
|
+
return (`Found ${offenders.length} plan-shaped file(s) in this phase that don't match the canonical ` +
|
|
58
|
+
`naming convention "{padded_phase}-{NN}-PLAN.md" (or bare "PLAN.md") and were skipped: ` +
|
|
59
|
+
offenders.map((f) => `"${f}"`).join(', ') +
|
|
60
|
+
`. Rename to the canonical form (e.g. "01-01-PLAN.md") so the executor can detect them. ` +
|
|
61
|
+
`See agents/gsd-planner.md write_phase_prompt step for the full contract.`);
|
|
62
|
+
}
|
|
63
|
+
function extractCanonicalPlanId(filename) {
|
|
64
|
+
const base = filename
|
|
65
|
+
.replace(/-PLAN\.md$/i, '')
|
|
66
|
+
.replace(/-SUMMARY\.md$/i, '')
|
|
67
|
+
.replace(/\.md$/i, '');
|
|
68
|
+
const parts = base.split('-').filter(Boolean);
|
|
69
|
+
const tokenRe = /^\d+[A-Z]?(?:\.\d+)*$/i;
|
|
70
|
+
const phaseIdx = parts.findIndex((p) => tokenRe.test(p));
|
|
71
|
+
if (phaseIdx >= 0 && phaseIdx + 1 < parts.length && tokenRe.test(parts[phaseIdx + 1])) {
|
|
72
|
+
return `${parts[phaseIdx]}-${parts[phaseIdx + 1]}`;
|
|
73
|
+
}
|
|
74
|
+
return base;
|
|
75
|
+
}
|
|
76
|
+
function cmdPhasesList(cwd, options, raw) {
|
|
77
|
+
const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
|
|
78
|
+
const { type, phase, includeArchived } = options;
|
|
79
|
+
if (!node_fs_1.default.existsSync(phasesDir)) {
|
|
80
|
+
if (type) {
|
|
81
|
+
output({ files: [], count: 0 }, raw, '');
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
output({ directories: [], count: 0 }, raw, '');
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
|
|
90
|
+
let dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
91
|
+
if (includeArchived) {
|
|
92
|
+
const archived = getArchivedPhaseDirs(cwd);
|
|
93
|
+
for (const a of archived) {
|
|
94
|
+
dirs.push(`${a.name} [${a.milestone}]`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
dirs.sort((a, b) => comparePhaseNum(a, b));
|
|
98
|
+
if (phase) {
|
|
99
|
+
const normalized = normalizePhaseName(phase);
|
|
100
|
+
const match = dirs.find((d) => phaseTokenMatches(d, normalized));
|
|
101
|
+
if (!match) {
|
|
102
|
+
output({ files: [], count: 0, phase_dir: null, error: 'Phase not found' }, raw, '');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
dirs = [match];
|
|
106
|
+
}
|
|
107
|
+
if (type) {
|
|
108
|
+
const files = [];
|
|
109
|
+
const warnings = [];
|
|
110
|
+
for (const dir of dirs) {
|
|
111
|
+
const dirPath = node_path_1.default.join(phasesDir, dir);
|
|
112
|
+
const dirFiles = node_fs_1.default.readdirSync(dirPath);
|
|
113
|
+
let filtered;
|
|
114
|
+
if (type === 'plans') {
|
|
115
|
+
filtered = dirFiles.filter(isCanonicalPlanFile);
|
|
116
|
+
const w = describeNonCanonicalPlans(dirFiles, filtered);
|
|
117
|
+
if (w)
|
|
118
|
+
warnings.push(`${dir}: ${w}`);
|
|
119
|
+
}
|
|
120
|
+
else if (type === 'summaries') {
|
|
121
|
+
filtered = dirFiles.filter((f) => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
filtered = dirFiles;
|
|
125
|
+
}
|
|
126
|
+
files.push(...filtered.sort());
|
|
127
|
+
}
|
|
128
|
+
const result = {
|
|
129
|
+
files,
|
|
130
|
+
count: files.length,
|
|
131
|
+
phase_dir: phase ? dirs[0].replace(/^\d+(?:\.\d+)*-?/, '') : null,
|
|
132
|
+
};
|
|
133
|
+
if (warnings.length)
|
|
134
|
+
result['warning'] = warnings.join(' | ');
|
|
135
|
+
output(result, raw, files.join('\n'));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
output({ directories: dirs, count: dirs.length }, raw, dirs.join('\n'));
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
142
|
+
error('Failed to list phases: ' + msg);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function cmdPhaseNextDecimal(cwd, basePhase, raw) {
|
|
146
|
+
const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
|
|
147
|
+
const normalized = normalizePhaseName(basePhase);
|
|
148
|
+
try {
|
|
149
|
+
let baseExists = false;
|
|
150
|
+
const decimalSet = new Set();
|
|
151
|
+
if (node_fs_1.default.existsSync(phasesDir)) {
|
|
152
|
+
const entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
|
|
153
|
+
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
154
|
+
baseExists = dirs.some((d) => phaseTokenMatches(d, normalized));
|
|
155
|
+
const dirPattern = new RegExp(`^(?:[A-Z]{1,6}-)?${escapeRegex(normalized)}\\.(\\d+)`);
|
|
156
|
+
for (const dir of dirs) {
|
|
157
|
+
const match = dir.match(dirPattern);
|
|
158
|
+
if (match)
|
|
159
|
+
decimalSet.add(parseInt(match[1], 10));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
163
|
+
if (node_fs_1.default.existsSync(roadmapPath)) {
|
|
164
|
+
try {
|
|
165
|
+
const roadmapContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
166
|
+
const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${phaseMarkdownRegexSource(normalized)}\\.(\\d+)\\s*:`, 'gi');
|
|
167
|
+
let pm;
|
|
168
|
+
while ((pm = phasePattern.exec(roadmapContent)) !== null) {
|
|
169
|
+
decimalSet.add(parseInt(pm[1], 10));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
/* ROADMAP.md read failure is non-fatal */
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const existingDecimals = Array.from(decimalSet)
|
|
177
|
+
.sort((a, b) => a - b)
|
|
178
|
+
.map((n) => `${normalized}.${n}`);
|
|
179
|
+
let nextDecimal;
|
|
180
|
+
if (decimalSet.size === 0) {
|
|
181
|
+
nextDecimal = `${normalized}.1`;
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
nextDecimal = `${normalized}.${Math.max(...decimalSet) + 1}`;
|
|
185
|
+
}
|
|
186
|
+
output({
|
|
187
|
+
found: baseExists,
|
|
188
|
+
base_phase: normalized,
|
|
189
|
+
next: nextDecimal,
|
|
190
|
+
existing: existingDecimals,
|
|
191
|
+
}, raw, nextDecimal);
|
|
192
|
+
}
|
|
193
|
+
catch (e) {
|
|
194
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
195
|
+
error('Failed to calculate next decimal phase: ' + msg);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function getRoadmapModeForPhase(cwd, phaseNum) {
|
|
199
|
+
const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
200
|
+
if (!node_fs_1.default.existsSync(roadmapPath))
|
|
201
|
+
return null;
|
|
202
|
+
const rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
203
|
+
const milestoneContent = extractCurrentMilestone(rawContent, cwd);
|
|
204
|
+
const fullContent = stripShippedMilestones(rawContent);
|
|
205
|
+
const escapedPhase = phaseMarkdownRegexSource(phaseNum);
|
|
206
|
+
const phaseHeader = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}\\s*:`, 'i');
|
|
207
|
+
for (const content of [milestoneContent, fullContent]) {
|
|
208
|
+
const headerMatch = content.match(phaseHeader);
|
|
209
|
+
if (!headerMatch || headerMatch.index === undefined)
|
|
210
|
+
continue;
|
|
211
|
+
const sectionStart = headerMatch.index;
|
|
212
|
+
const rest = content.slice(sectionStart);
|
|
213
|
+
const nextHeader = rest.slice(headerMatch[0].length).match(/\n#{2,4}\s+Phase\s+\S/i);
|
|
214
|
+
const sectionEnd = nextHeader
|
|
215
|
+
? sectionStart + headerMatch[0].length + nextHeader.index
|
|
216
|
+
: content.length;
|
|
217
|
+
const section = content.slice(sectionStart, sectionEnd);
|
|
218
|
+
const modeMatch = section.match(/\*\*Mode(?::\*\*|\*\*:)\s*([^\n]+)/i);
|
|
219
|
+
if (modeMatch)
|
|
220
|
+
return modeMatch[1].trim().toLowerCase();
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
function cmdPhaseMvpMode(cwd, args, raw) {
|
|
225
|
+
const phaseNum = args[0];
|
|
226
|
+
if (!phaseNum) {
|
|
227
|
+
error('Usage: phase.mvp-mode <phase-number> [--cli-flag]', ERROR_REASON.USAGE);
|
|
228
|
+
}
|
|
229
|
+
const cliFlagPresent = args.includes('--cli-flag');
|
|
230
|
+
const roadmapMode = getRoadmapModeForPhase(cwd, phaseNum);
|
|
231
|
+
const config = loadConfig(cwd);
|
|
232
|
+
const configMvpMode = Boolean(config.mvp_mode);
|
|
233
|
+
let active = false;
|
|
234
|
+
let source = 'none';
|
|
235
|
+
if (cliFlagPresent) {
|
|
236
|
+
active = true;
|
|
237
|
+
source = 'cli_flag';
|
|
238
|
+
}
|
|
239
|
+
else if (roadmapMode === 'mvp') {
|
|
240
|
+
active = true;
|
|
241
|
+
source = 'roadmap';
|
|
242
|
+
}
|
|
243
|
+
else if (configMvpMode) {
|
|
244
|
+
active = true;
|
|
245
|
+
source = 'config';
|
|
246
|
+
}
|
|
247
|
+
output({
|
|
248
|
+
active,
|
|
249
|
+
source,
|
|
250
|
+
roadmap_mode: roadmapMode,
|
|
251
|
+
config_mvp_mode: configMvpMode,
|
|
252
|
+
cli_flag_present: cliFlagPresent,
|
|
253
|
+
}, raw);
|
|
254
|
+
}
|
|
255
|
+
function cmdFindPhase(cwd, phase, raw) {
|
|
256
|
+
if (!phase) {
|
|
257
|
+
error('phase identifier required');
|
|
258
|
+
}
|
|
259
|
+
const planBase = planningDir(cwd);
|
|
260
|
+
const normalized = normalizePhaseName(phase);
|
|
261
|
+
const notFound = {
|
|
262
|
+
found: false,
|
|
263
|
+
directory: null,
|
|
264
|
+
phase_number: null,
|
|
265
|
+
phase_name: null,
|
|
266
|
+
plans: [],
|
|
267
|
+
summaries: [],
|
|
268
|
+
searched_directories: [],
|
|
269
|
+
};
|
|
270
|
+
const searchDirs = [];
|
|
271
|
+
const flatPhasesDir = node_path_1.default.join(planBase, 'phases');
|
|
272
|
+
if (node_fs_1.default.existsSync(flatPhasesDir))
|
|
273
|
+
searchDirs.push(flatPhasesDir);
|
|
274
|
+
try {
|
|
275
|
+
const milestonesDir = node_path_1.default.join(planBase, 'milestones');
|
|
276
|
+
const entries = node_fs_1.default
|
|
277
|
+
.readdirSync(milestonesDir, { withFileTypes: true })
|
|
278
|
+
.filter((e) => e.isDirectory() && /^v\d+.*-phases$/.test(e.name))
|
|
279
|
+
.sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }));
|
|
280
|
+
for (const e of entries) {
|
|
281
|
+
searchDirs.push(node_path_1.default.join(milestonesDir, e.name));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
/* no milestones dir */
|
|
286
|
+
}
|
|
287
|
+
notFound.searched_directories = searchDirs.map((searchDir) => toPosixPath(node_path_1.default.join(node_path_1.default.relative(cwd, planBase), node_path_1.default.relative(planBase, searchDir))));
|
|
288
|
+
for (const searchDir of searchDirs) {
|
|
289
|
+
try {
|
|
290
|
+
const entries = node_fs_1.default.readdirSync(searchDir, { withFileTypes: true });
|
|
291
|
+
const dirs = entries
|
|
292
|
+
.filter((e) => e.isDirectory())
|
|
293
|
+
.map((e) => e.name)
|
|
294
|
+
.sort((a, b) => comparePhaseNum(a, b));
|
|
295
|
+
const match = dirs.find((d) => phaseTokenMatches(d, normalized));
|
|
296
|
+
if (!match)
|
|
297
|
+
continue;
|
|
298
|
+
const dirMatch = match.match(/^(?:[A-Z]{1,6}-)(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i) ||
|
|
299
|
+
match.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
|
|
300
|
+
const phaseNumber = dirMatch ? dirMatch[1] : normalized;
|
|
301
|
+
const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
|
|
302
|
+
const phaseDir = node_path_1.default.join(searchDir, match);
|
|
303
|
+
const phaseFiles = node_fs_1.default.readdirSync(phaseDir);
|
|
304
|
+
const plans = phaseFiles.filter(isCanonicalPlanFile).sort();
|
|
305
|
+
const summaries = phaseFiles.filter((f) => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').sort();
|
|
306
|
+
const planNamingWarning = describeNonCanonicalPlans(phaseFiles, plans);
|
|
307
|
+
const result = {
|
|
308
|
+
found: true,
|
|
309
|
+
directory: toPosixPath(node_path_1.default.join(node_path_1.default.relative(cwd, planBase), node_path_1.default.relative(planBase, searchDir), match)),
|
|
310
|
+
phase_number: phaseNumber,
|
|
311
|
+
phase_name: phaseName,
|
|
312
|
+
plans,
|
|
313
|
+
summaries,
|
|
314
|
+
};
|
|
315
|
+
if (planNamingWarning)
|
|
316
|
+
result['warning'] = planNamingWarning;
|
|
317
|
+
output(result, raw, result['directory']);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
output(notFound, raw, '');
|
|
325
|
+
}
|
|
326
|
+
function extractObjective(content) {
|
|
327
|
+
const m = content.match(/<objective>\s*\n?\s*(.+)/);
|
|
328
|
+
return m ? m[1].trim() : null;
|
|
329
|
+
}
|
|
330
|
+
// O(V + E). Assigns each in-phase plan its longest-path topological level over the
|
|
331
|
+
// in-phase dependsOn DAG (Kahn's algorithm). Returns { level: Map<id,number>, visited: number }.
|
|
332
|
+
// visited < rawPlans.length signals a dependency cycle.
|
|
333
|
+
function computeDependencyLevels(rawPlans, planMap, canonicalToId) {
|
|
334
|
+
const level = new Map();
|
|
335
|
+
const inDeg = new Map();
|
|
336
|
+
const adj = new Map();
|
|
337
|
+
for (const p of rawPlans) {
|
|
338
|
+
if (!inDeg.has(p.id))
|
|
339
|
+
inDeg.set(p.id, 0);
|
|
340
|
+
if (!adj.has(p.id))
|
|
341
|
+
adj.set(p.id, []);
|
|
342
|
+
for (const dep of p.dependsOn) {
|
|
343
|
+
const depLower = dep.toLowerCase();
|
|
344
|
+
const resolvedDep = planMap.has(depLower)
|
|
345
|
+
? planMap.get(depLower).id
|
|
346
|
+
: canonicalToId.get(depLower);
|
|
347
|
+
if (!resolvedDep)
|
|
348
|
+
continue;
|
|
349
|
+
if (!adj.has(resolvedDep))
|
|
350
|
+
adj.set(resolvedDep, []);
|
|
351
|
+
adj.get(resolvedDep).push(p.id);
|
|
352
|
+
inDeg.set(p.id, (inDeg.get(p.id) ?? 0) + 1);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const queue = [];
|
|
356
|
+
for (const p of rawPlans) {
|
|
357
|
+
if ((inDeg.get(p.id) ?? 0) === 0) {
|
|
358
|
+
queue.push(p.id);
|
|
359
|
+
level.set(p.id, 0);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// Dequeue by head index (queue[head++]), NOT Array.shift(): shift() is O(n) per
|
|
363
|
+
// call in V8. Head-index dequeue is O(1) amortized -> O(V+E) overall. (#307)
|
|
364
|
+
let head = 0;
|
|
365
|
+
let visited = 0;
|
|
366
|
+
while (head < queue.length) {
|
|
367
|
+
const cur = queue[head++];
|
|
368
|
+
visited++;
|
|
369
|
+
const curLevel = level.get(cur);
|
|
370
|
+
for (const dep of adj.get(cur) ?? []) {
|
|
371
|
+
const newLevel = curLevel + 1;
|
|
372
|
+
if (newLevel > (level.get(dep) ?? -1)) {
|
|
373
|
+
level.set(dep, newLevel);
|
|
374
|
+
}
|
|
375
|
+
inDeg.set(dep, inDeg.get(dep) - 1);
|
|
376
|
+
if (inDeg.get(dep) === 0) {
|
|
377
|
+
queue.push(dep);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return { level, visited };
|
|
382
|
+
}
|
|
383
|
+
function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
384
|
+
if (!phase) {
|
|
385
|
+
error('phase required for phase-plan-index');
|
|
386
|
+
}
|
|
387
|
+
const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
|
|
388
|
+
const normalized = normalizePhaseName(phase);
|
|
389
|
+
let phaseDir = null;
|
|
390
|
+
let phaseDirName = null;
|
|
391
|
+
try {
|
|
392
|
+
const entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
|
|
393
|
+
const dirs = entries
|
|
394
|
+
.filter((e) => e.isDirectory())
|
|
395
|
+
.map((e) => e.name)
|
|
396
|
+
.sort((a, b) => comparePhaseNum(a, b));
|
|
397
|
+
const match = dirs.find((d) => phaseTokenMatches(d, normalized));
|
|
398
|
+
if (match) {
|
|
399
|
+
phaseDir = node_path_1.default.join(phasesDir, match);
|
|
400
|
+
phaseDirName = match;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
// phases dir doesn't exist
|
|
405
|
+
}
|
|
406
|
+
if (!phaseDir) {
|
|
407
|
+
output({ phase: normalized, error: 'Phase not found', plans: [], waves: {}, incomplete: [], has_checkpoints: false }, raw);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
void phaseDirName; // used only to set phaseDir above
|
|
411
|
+
const phaseFiles = node_fs_1.default.readdirSync(phaseDir);
|
|
412
|
+
const planFiles = phaseFiles.filter(isCanonicalPlanFile).sort();
|
|
413
|
+
const summaryFiles = phaseFiles.filter((f) => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
|
414
|
+
const planNamingWarning = describeNonCanonicalPlans(phaseFiles, planFiles);
|
|
415
|
+
const completedPlanIds = new Set(summaryFiles.flatMap((s) => {
|
|
416
|
+
const exact = s.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
|
|
417
|
+
const canonical = extractCanonicalPlanId(s);
|
|
418
|
+
return canonical === exact ? [exact] : [exact, canonical];
|
|
419
|
+
}));
|
|
420
|
+
// ── Pass 1: parse each plan file ─────────────────────────────────────────
|
|
421
|
+
const rawPlans = [];
|
|
422
|
+
for (const planFile of planFiles) {
|
|
423
|
+
const planId = planFile.replace('-PLAN.md', '').replace('PLAN.md', '');
|
|
424
|
+
const planPath = node_path_1.default.join(phaseDir, planFile);
|
|
425
|
+
const content = node_fs_1.default.readFileSync(planPath, 'utf-8');
|
|
426
|
+
const fm = extractFrontmatter(content);
|
|
427
|
+
const xmlTasks = content.match(/<task[\s>]/gi) || [];
|
|
428
|
+
const mdTasks = content.match(/##\s*Task\s*\d+/gi) || [];
|
|
429
|
+
const taskCount = xmlTasks.length || mdTasks.length;
|
|
430
|
+
const parsedWave = parseInt(fm['wave'], 10);
|
|
431
|
+
const declaredWave = Number.isNaN(parsedWave) ? null : parsedWave;
|
|
432
|
+
let dependsOn = [];
|
|
433
|
+
const fmDeps = fm['depends_on'];
|
|
434
|
+
if (Array.isArray(fmDeps)) {
|
|
435
|
+
dependsOn = fmDeps.map(String);
|
|
436
|
+
}
|
|
437
|
+
else if (typeof fmDeps === 'string' && fmDeps.trim() !== '') {
|
|
438
|
+
dependsOn = [fmDeps];
|
|
439
|
+
}
|
|
440
|
+
let autonomous = true;
|
|
441
|
+
if (fm['autonomous'] !== undefined) {
|
|
442
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string -- FrontmatterValue comparison
|
|
443
|
+
autonomous = fm['autonomous'] === 'true' || String(fm['autonomous']) === 'true';
|
|
444
|
+
}
|
|
445
|
+
let filesModified = [];
|
|
446
|
+
const fmFiles = fm['files_modified'] || fm['files-modified'];
|
|
447
|
+
if (fmFiles) {
|
|
448
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string -- FrontmatterValue scalar-to-string
|
|
449
|
+
filesModified = Array.isArray(fmFiles) ? fmFiles.map(String) : [String(fmFiles)];
|
|
450
|
+
}
|
|
451
|
+
const hasSummary = completedPlanIds.has(planId) || completedPlanIds.has(extractCanonicalPlanId(planFile));
|
|
452
|
+
rawPlans.push({
|
|
453
|
+
id: planId,
|
|
454
|
+
declaredWave,
|
|
455
|
+
dependsOn,
|
|
456
|
+
autonomous,
|
|
457
|
+
objective: extractObjective(content) || fm['objective'] || null,
|
|
458
|
+
filesModified,
|
|
459
|
+
taskCount,
|
|
460
|
+
hasSummary,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
// ── Pass 2: topological level assignment via depends_on DAG ──────────────
|
|
464
|
+
const seenLower = new Map();
|
|
465
|
+
for (const p of rawPlans) {
|
|
466
|
+
const lower = p.id.toLowerCase();
|
|
467
|
+
const existing = seenLower.get(lower);
|
|
468
|
+
if (existing !== undefined) {
|
|
469
|
+
error(`depends_on index collision in phase ${normalized}: plan IDs '${existing}' and '${p.id}' are identical when case-folded. Rename one file to avoid ambiguous dependency resolution.`);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
seenLower.set(lower, p.id);
|
|
473
|
+
}
|
|
474
|
+
const planMap = new Map(rawPlans.map((p) => [p.id.toLowerCase(), p]));
|
|
475
|
+
const canonicalToId = new Map(rawPlans.map((p) => [extractCanonicalPlanId(p.id).toLowerCase(), p.id]));
|
|
476
|
+
const { level, visited } = computeDependencyLevels(rawPlans, planMap, canonicalToId);
|
|
477
|
+
if (visited < rawPlans.length) {
|
|
478
|
+
const cycleNodes = rawPlans.filter((p) => !level.has(p.id)).map((p) => p.id);
|
|
479
|
+
error(`depends_on cycle detected in phase ${normalized} — cycle involves: ${cycleNodes.join(', ')}`);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
// ── Pass 3: determine lowest bucket key and build output ─────────────────
|
|
483
|
+
const anyWaveZero = rawPlans.some((p) => p.declaredWave === 0);
|
|
484
|
+
const levelOffset = anyWaveZero ? 0 : 1;
|
|
485
|
+
const plans = [];
|
|
486
|
+
const waves = {};
|
|
487
|
+
const incomplete = [];
|
|
488
|
+
let hasCheckpoints = false;
|
|
489
|
+
const warnings = [];
|
|
490
|
+
for (const rawPlan of rawPlans) {
|
|
491
|
+
if (!rawPlan.autonomous) {
|
|
492
|
+
hasCheckpoints = true;
|
|
493
|
+
}
|
|
494
|
+
if (!rawPlan.hasSummary) {
|
|
495
|
+
incomplete.push(rawPlan.id);
|
|
496
|
+
}
|
|
497
|
+
const computedWave = (level.get(rawPlan.id) ?? 0) + levelOffset;
|
|
498
|
+
const effectiveWave = computedWave;
|
|
499
|
+
if (rawPlan.declaredWave !== null && rawPlan.declaredWave !== computedWave) {
|
|
500
|
+
warnings.push(`Plan ${rawPlan.id}: declared wave: ${rawPlan.declaredWave} but depends_on DAG places it in wave ${computedWave}`);
|
|
501
|
+
}
|
|
502
|
+
const plan = {
|
|
503
|
+
id: rawPlan.id,
|
|
504
|
+
wave: effectiveWave,
|
|
505
|
+
depends_on: rawPlan.dependsOn.map((dep) => {
|
|
506
|
+
const lower = String(dep).toLowerCase();
|
|
507
|
+
return planMap.has(lower) ? planMap.get(lower).id : dep;
|
|
508
|
+
}),
|
|
509
|
+
autonomous: rawPlan.autonomous,
|
|
510
|
+
objective: rawPlan.objective,
|
|
511
|
+
files_modified: rawPlan.filesModified,
|
|
512
|
+
task_count: rawPlan.taskCount,
|
|
513
|
+
has_summary: rawPlan.hasSummary,
|
|
514
|
+
};
|
|
515
|
+
plans.push(plan);
|
|
516
|
+
const waveKey = String(effectiveWave);
|
|
517
|
+
if (!waves[waveKey]) {
|
|
518
|
+
waves[waveKey] = [];
|
|
519
|
+
}
|
|
520
|
+
waves[waveKey].push(rawPlan.id);
|
|
521
|
+
}
|
|
522
|
+
const result = {
|
|
523
|
+
phase: normalized,
|
|
524
|
+
plans,
|
|
525
|
+
waves,
|
|
526
|
+
incomplete,
|
|
527
|
+
has_checkpoints: hasCheckpoints,
|
|
528
|
+
};
|
|
529
|
+
if (planNamingWarning)
|
|
530
|
+
result['warning'] = planNamingWarning;
|
|
531
|
+
if (warnings.length > 0)
|
|
532
|
+
result['warnings'] = warnings;
|
|
533
|
+
output(result, raw);
|
|
534
|
+
}
|
|
535
|
+
function cmdPhaseAdd(cwd, description, raw, customId) {
|
|
536
|
+
if (!description) {
|
|
537
|
+
error('description required for phase add');
|
|
538
|
+
}
|
|
539
|
+
const config = loadConfig(cwd);
|
|
540
|
+
const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
541
|
+
if (!node_fs_1.default.existsSync(roadmapPath)) {
|
|
542
|
+
error('ROADMAP.md not found');
|
|
543
|
+
}
|
|
544
|
+
const slug = generateSlugInternal(description) || '';
|
|
545
|
+
const { newPhaseId, dirName } = withPlanningLock(cwd, () => {
|
|
546
|
+
const rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
547
|
+
const content = extractCurrentMilestone(rawContent, cwd);
|
|
548
|
+
const projectCode = config.project_code || '';
|
|
549
|
+
const prefix = projectCode ? `${projectCode}-` : '';
|
|
550
|
+
let _newPhaseId;
|
|
551
|
+
let _dirName;
|
|
552
|
+
if (customId || config.phase_naming === 'custom') {
|
|
553
|
+
_newPhaseId = customId || slug.toUpperCase();
|
|
554
|
+
if (!_newPhaseId)
|
|
555
|
+
error('--id required when phase_naming is "custom"');
|
|
556
|
+
_dirName = `${prefix}${_newPhaseId}-${slug}`;
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi;
|
|
560
|
+
let maxPhase = 0;
|
|
561
|
+
let m;
|
|
562
|
+
while ((m = phasePattern.exec(content)) !== null) {
|
|
563
|
+
const num = parseInt(m[1], 10);
|
|
564
|
+
if (num === 999)
|
|
565
|
+
continue;
|
|
566
|
+
if (num > maxPhase)
|
|
567
|
+
maxPhase = num;
|
|
568
|
+
}
|
|
569
|
+
const phasesOnDisk = node_path_1.default.join(planningDir(cwd), 'phases');
|
|
570
|
+
if (node_fs_1.default.existsSync(phasesOnDisk)) {
|
|
571
|
+
const dirNumPattern = /^(?:[A-Z][A-Z0-9]*-)?(\d+)-/;
|
|
572
|
+
for (const entry of node_fs_1.default.readdirSync(phasesOnDisk)) {
|
|
573
|
+
const match = entry.match(dirNumPattern);
|
|
574
|
+
if (!match)
|
|
575
|
+
continue;
|
|
576
|
+
const num = parseInt(match[1], 10);
|
|
577
|
+
if (num === 999)
|
|
578
|
+
continue;
|
|
579
|
+
if (num > maxPhase)
|
|
580
|
+
maxPhase = num;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
_newPhaseId = maxPhase + 1;
|
|
584
|
+
const paddedNum = String(_newPhaseId).padStart(2, '0');
|
|
585
|
+
_dirName = `${prefix}${paddedNum}-${slug}`;
|
|
586
|
+
}
|
|
587
|
+
const dirPath = node_path_1.default.join(planningDir(cwd), 'phases', _dirName);
|
|
588
|
+
(0, shell_command_projection_cjs_1.platformEnsureDir)(dirPath);
|
|
589
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(node_path_1.default.join(dirPath, '.gitkeep'), '');
|
|
590
|
+
const dependsOn = config.phase_naming === 'custom'
|
|
591
|
+
? ''
|
|
592
|
+
: `\n**Depends on:** Phase ${typeof _newPhaseId === 'number' ? _newPhaseId - 1 : 'TBD'}`;
|
|
593
|
+
const phaseEntry = `\n### Phase ${_newPhaseId}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD${dependsOn}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run ${(0, runtime_slash_cjs_1.formatGsdSlash)('plan-phase', (0, runtime_slash_cjs_1.resolveRuntime)(cwd))} ${_newPhaseId} to break down)\n`;
|
|
594
|
+
let updatedContent;
|
|
595
|
+
const lastSeparator = rawContent.lastIndexOf('\n---');
|
|
596
|
+
if (lastSeparator > 0) {
|
|
597
|
+
updatedContent = rawContent.slice(0, lastSeparator) + phaseEntry + rawContent.slice(lastSeparator);
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
updatedContent = rawContent + phaseEntry;
|
|
601
|
+
}
|
|
602
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(roadmapPath, updatedContent);
|
|
603
|
+
return { newPhaseId: _newPhaseId, dirName: _dirName };
|
|
604
|
+
});
|
|
605
|
+
const result = {
|
|
606
|
+
phase_number: typeof newPhaseId === 'number' ? newPhaseId : String(newPhaseId),
|
|
607
|
+
padded: typeof newPhaseId === 'number' ? String(newPhaseId).padStart(2, '0') : String(newPhaseId),
|
|
608
|
+
name: description,
|
|
609
|
+
slug,
|
|
610
|
+
directory: toPosixPath(node_path_1.default.join(node_path_1.default.relative(cwd, planningDir(cwd)), 'phases', dirName)),
|
|
611
|
+
naming_mode: config.phase_naming,
|
|
612
|
+
};
|
|
613
|
+
output(result, raw, result.padded);
|
|
614
|
+
}
|
|
615
|
+
function cmdPhaseAddBatch(cwd, descriptions, raw) {
|
|
616
|
+
if (!Array.isArray(descriptions) || descriptions.length === 0) {
|
|
617
|
+
error('descriptions array required for phase add-batch');
|
|
618
|
+
}
|
|
619
|
+
const config = loadConfig(cwd);
|
|
620
|
+
const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
621
|
+
if (!node_fs_1.default.existsSync(roadmapPath)) {
|
|
622
|
+
error('ROADMAP.md not found');
|
|
623
|
+
}
|
|
624
|
+
const projectCode = config.project_code || '';
|
|
625
|
+
const prefix = projectCode ? `${projectCode}-` : '';
|
|
626
|
+
const results = withPlanningLock(cwd, () => {
|
|
627
|
+
let rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
628
|
+
const content = extractCurrentMilestone(rawContent, cwd);
|
|
629
|
+
let maxPhase = 0;
|
|
630
|
+
if (config.phase_naming !== 'custom') {
|
|
631
|
+
const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi;
|
|
632
|
+
let m;
|
|
633
|
+
while ((m = phasePattern.exec(content)) !== null) {
|
|
634
|
+
const num = parseInt(m[1], 10);
|
|
635
|
+
if (num === 999)
|
|
636
|
+
continue;
|
|
637
|
+
if (num > maxPhase)
|
|
638
|
+
maxPhase = num;
|
|
639
|
+
}
|
|
640
|
+
const phasesOnDisk = node_path_1.default.join(planningDir(cwd), 'phases');
|
|
641
|
+
if (node_fs_1.default.existsSync(phasesOnDisk)) {
|
|
642
|
+
const dirNumPattern = /^(?:[A-Z][A-Z0-9]*-)?(\d+)-/;
|
|
643
|
+
for (const entry of node_fs_1.default.readdirSync(phasesOnDisk)) {
|
|
644
|
+
const match = entry.match(dirNumPattern);
|
|
645
|
+
if (!match)
|
|
646
|
+
continue;
|
|
647
|
+
const num = parseInt(match[1], 10);
|
|
648
|
+
if (num === 999)
|
|
649
|
+
continue;
|
|
650
|
+
if (num > maxPhase)
|
|
651
|
+
maxPhase = num;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
const added = [];
|
|
656
|
+
for (const description of descriptions) {
|
|
657
|
+
const slug = generateSlugInternal(description) || '';
|
|
658
|
+
let newPhaseId;
|
|
659
|
+
let dirName;
|
|
660
|
+
if (config.phase_naming === 'custom') {
|
|
661
|
+
newPhaseId = slug.toUpperCase();
|
|
662
|
+
dirName = `${prefix}${newPhaseId}-${slug}`;
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
maxPhase += 1;
|
|
666
|
+
newPhaseId = maxPhase;
|
|
667
|
+
dirName = `${prefix}${String(newPhaseId).padStart(2, '0')}-${slug}`;
|
|
668
|
+
}
|
|
669
|
+
const dirPath = node_path_1.default.join(planningDir(cwd), 'phases', dirName);
|
|
670
|
+
(0, shell_command_projection_cjs_1.platformEnsureDir)(dirPath);
|
|
671
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(node_path_1.default.join(dirPath, '.gitkeep'), '');
|
|
672
|
+
const dependsOn = config.phase_naming === 'custom'
|
|
673
|
+
? ''
|
|
674
|
+
: `\n**Depends on:** Phase ${typeof newPhaseId === 'number' ? newPhaseId - 1 : 'TBD'}`;
|
|
675
|
+
const phaseEntry = `\n### Phase ${newPhaseId}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD${dependsOn}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run ${(0, runtime_slash_cjs_1.formatGsdSlash)('plan-phase', (0, runtime_slash_cjs_1.resolveRuntime)(cwd))} ${newPhaseId} to break down)\n`;
|
|
676
|
+
const lastSeparator = rawContent.lastIndexOf('\n---');
|
|
677
|
+
rawContent =
|
|
678
|
+
lastSeparator > 0
|
|
679
|
+
? rawContent.slice(0, lastSeparator) + phaseEntry + rawContent.slice(lastSeparator)
|
|
680
|
+
: rawContent + phaseEntry;
|
|
681
|
+
added.push({
|
|
682
|
+
phase_number: typeof newPhaseId === 'number' ? newPhaseId : String(newPhaseId),
|
|
683
|
+
padded: typeof newPhaseId === 'number' ? String(newPhaseId).padStart(2, '0') : String(newPhaseId),
|
|
684
|
+
name: description,
|
|
685
|
+
slug,
|
|
686
|
+
directory: toPosixPath(node_path_1.default.join(node_path_1.default.relative(cwd, planningDir(cwd)), 'phases', dirName)),
|
|
687
|
+
naming_mode: config.phase_naming,
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(roadmapPath, rawContent);
|
|
691
|
+
return added;
|
|
692
|
+
});
|
|
693
|
+
output({ phases: results, count: results.length }, raw);
|
|
694
|
+
}
|
|
695
|
+
function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
696
|
+
if (!afterPhase || !description) {
|
|
697
|
+
error('after-phase and description required for phase insert');
|
|
698
|
+
}
|
|
699
|
+
const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
700
|
+
if (!node_fs_1.default.existsSync(roadmapPath)) {
|
|
701
|
+
error('ROADMAP.md not found');
|
|
702
|
+
}
|
|
703
|
+
const slug = generateSlugInternal(description) || '';
|
|
704
|
+
const { decimalPhase, dirName } = withPlanningLock(cwd, () => {
|
|
705
|
+
const rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
706
|
+
const content = extractCurrentMilestone(rawContent, cwd);
|
|
707
|
+
const normalizedAfter = normalizePhaseName(afterPhase);
|
|
708
|
+
const afterPhaseEscaped = phaseMarkdownRegexSource(normalizedAfter);
|
|
709
|
+
const targetPattern = new RegExp(`#{2,4}\\s*Phase\\s+${afterPhaseEscaped}:`, 'i');
|
|
710
|
+
const headingMatch = targetPattern.test(content);
|
|
711
|
+
const bulletPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*(?:\\*\\*)?Phase\\s+${afterPhaseEscaped}[:\\s]`, 'i');
|
|
712
|
+
const anyHeadingPattern = /#{2,4}\s*Phase\s+\d/i;
|
|
713
|
+
const roadmapHasHeadingPhases = anyHeadingPattern.test(content);
|
|
714
|
+
const isBulletStyle = !headingMatch && bulletPattern.test(content) && !roadmapHasHeadingPhases;
|
|
715
|
+
if (!headingMatch && !isBulletStyle) {
|
|
716
|
+
const checklistPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*(?:\\*\\*)?Phase\\s+${afterPhaseEscaped}[:\\s]`, 'i');
|
|
717
|
+
if (checklistPattern.test(content)) {
|
|
718
|
+
error(`Phase ${afterPhase} exists in roadmap summary but is missing a detail section (### Phase ${afterPhase}: ...).`);
|
|
719
|
+
}
|
|
720
|
+
error(`Phase ${afterPhase} not found in ROADMAP.md`);
|
|
721
|
+
}
|
|
722
|
+
const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
|
|
723
|
+
const normalizedBase = normalizePhaseName(afterPhase);
|
|
724
|
+
const decimalSet = new Set();
|
|
725
|
+
try {
|
|
726
|
+
const entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
|
|
727
|
+
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
728
|
+
const decimalPattern = new RegExp(`^(?:[A-Z]{1,6}-)?${escapeRegex(normalizedBase)}\\.(\\d+)`);
|
|
729
|
+
for (const dir of dirs) {
|
|
730
|
+
const dm = dir.match(decimalPattern);
|
|
731
|
+
if (dm)
|
|
732
|
+
decimalSet.add(parseInt(dm[1], 10));
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
catch {
|
|
736
|
+
/* intentionally empty */
|
|
737
|
+
}
|
|
738
|
+
const rmPhasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${phaseMarkdownRegexSource(normalizedBase)}\\.(\\d+)\\s*:`, 'gi');
|
|
739
|
+
let rmMatch;
|
|
740
|
+
while ((rmMatch = rmPhasePattern.exec(rawContent)) !== null) {
|
|
741
|
+
decimalSet.add(parseInt(rmMatch[1], 10));
|
|
742
|
+
}
|
|
743
|
+
const nextDecimal = decimalSet.size === 0 ? 1 : Math.max(...decimalSet) + 1;
|
|
744
|
+
const _decimalPhase = `${normalizedBase}.${nextDecimal}`;
|
|
745
|
+
const insertConfig = loadConfig(cwd);
|
|
746
|
+
const projectCode = insertConfig.project_code || '';
|
|
747
|
+
const pfx = projectCode ? `${projectCode}-` : '';
|
|
748
|
+
const _dirName = `${pfx}${_decimalPhase}-${slug}`;
|
|
749
|
+
const dirPath = node_path_1.default.join(planningDir(cwd), 'phases', _dirName);
|
|
750
|
+
(0, shell_command_projection_cjs_1.platformEnsureDir)(dirPath);
|
|
751
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(node_path_1.default.join(dirPath, '.gitkeep'), '');
|
|
752
|
+
let updatedContent;
|
|
753
|
+
if (isBulletStyle) {
|
|
754
|
+
const boldBulletPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*\\*\\*Phase\\s+${afterPhaseEscaped}:`, 'i');
|
|
755
|
+
const useBold = boldBulletPattern.test(content);
|
|
756
|
+
const phaseLabel = useBold
|
|
757
|
+
? `**Phase ${_decimalPhase}: ${description}**`
|
|
758
|
+
: `Phase ${_decimalPhase}: ${description}`;
|
|
759
|
+
const bulletEntry = `\n- [ ] ${phaseLabel}`;
|
|
760
|
+
const targetBulletPattern = new RegExp(`(-\\s*\\[[ x]\\]\\s*(?:\\*\\*)?Phase\\s+${afterPhaseEscaped}[:\\s][^\\n]*)`, 'i');
|
|
761
|
+
const bulletMatchResult = rawContent.match(targetBulletPattern);
|
|
762
|
+
if (!bulletMatchResult) {
|
|
763
|
+
error(`Could not find Phase ${afterPhase} bullet line`);
|
|
764
|
+
}
|
|
765
|
+
const bulletLineEnd = rawContent.indexOf(bulletMatchResult[0]) + bulletMatchResult[0].length;
|
|
766
|
+
const afterBullet = rawContent.slice(bulletLineEnd);
|
|
767
|
+
const nextBulletMatch = afterBullet.match(/\n-\s*\[[ x]\]\s*(?:\*\*)?Phase\s+\d/i);
|
|
768
|
+
let insertIdx;
|
|
769
|
+
if (nextBulletMatch) {
|
|
770
|
+
insertIdx = bulletLineEnd + nextBulletMatch.index;
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
insertIdx = bulletLineEnd;
|
|
774
|
+
}
|
|
775
|
+
updatedContent =
|
|
776
|
+
rawContent.slice(0, insertIdx) + bulletEntry + rawContent.slice(insertIdx);
|
|
777
|
+
}
|
|
778
|
+
else {
|
|
779
|
+
const phaseEntry = `\n### Phase ${_decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run ${(0, runtime_slash_cjs_1.formatGsdSlash)('plan-phase', (0, runtime_slash_cjs_1.resolveRuntime)(cwd))} ${_decimalPhase} to break down)\n`;
|
|
780
|
+
const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${afterPhaseEscaped}:[^\\n]*\\n)`, 'i');
|
|
781
|
+
const headerMatch = rawContent.match(headerPattern);
|
|
782
|
+
if (!headerMatch) {
|
|
783
|
+
error(`Could not find Phase ${afterPhase} header`);
|
|
784
|
+
}
|
|
785
|
+
const headerIdx = rawContent.indexOf(headerMatch[0]);
|
|
786
|
+
const afterHeader = rawContent.slice(headerIdx + headerMatch[0].length);
|
|
787
|
+
const nextPhaseMatch = afterHeader.match(/\n#{2,4}\s+Phase\s+\d[\d.]*/i);
|
|
788
|
+
let insertIdx;
|
|
789
|
+
if (nextPhaseMatch) {
|
|
790
|
+
insertIdx = headerIdx + headerMatch[0].length + nextPhaseMatch.index;
|
|
791
|
+
}
|
|
792
|
+
else {
|
|
793
|
+
insertIdx = rawContent.length;
|
|
794
|
+
}
|
|
795
|
+
updatedContent =
|
|
796
|
+
rawContent.slice(0, insertIdx) + phaseEntry + rawContent.slice(insertIdx);
|
|
797
|
+
}
|
|
798
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(roadmapPath, updatedContent);
|
|
799
|
+
return { decimalPhase: _decimalPhase, dirName: _dirName };
|
|
800
|
+
});
|
|
801
|
+
const result = {
|
|
802
|
+
phase_number: decimalPhase,
|
|
803
|
+
after_phase: afterPhase,
|
|
804
|
+
name: description,
|
|
805
|
+
slug,
|
|
806
|
+
directory: toPosixPath(node_path_1.default.join(node_path_1.default.relative(cwd, planningDir(cwd)), 'phases', dirName)),
|
|
807
|
+
};
|
|
808
|
+
output(result, raw, decimalPhase);
|
|
809
|
+
}
|
|
810
|
+
function renameDecimalPhases(phasesDir, baseInt, removedDecimal) {
|
|
811
|
+
const renamedDirs = [];
|
|
812
|
+
const renamedFiles = [];
|
|
813
|
+
const decPattern = new RegExp(`^(0*${baseInt})\\.(\\d+)-(.+)$`);
|
|
814
|
+
const dirs = readSubdirectories(phasesDir, true);
|
|
815
|
+
const toRename = dirs
|
|
816
|
+
.map((dir) => {
|
|
817
|
+
const m = dir.match(decPattern);
|
|
818
|
+
return m
|
|
819
|
+
? { dir, prefix: m[1], oldDecimal: parseInt(m[2], 10), slug: m[3] }
|
|
820
|
+
: null;
|
|
821
|
+
})
|
|
822
|
+
.filter((item) => item !== null && item.oldDecimal > removedDecimal)
|
|
823
|
+
.sort((a, b) => b.oldDecimal - a.oldDecimal);
|
|
824
|
+
for (const item of toRename) {
|
|
825
|
+
const newDecimal = item.oldDecimal - 1;
|
|
826
|
+
const oldPhaseId = `${baseInt}.${item.oldDecimal}`;
|
|
827
|
+
const newPhaseId = `${baseInt}.${newDecimal}`;
|
|
828
|
+
const newDirName = `${item.prefix}.${newDecimal}-${item.slug}`;
|
|
829
|
+
node_fs_1.default.renameSync(node_path_1.default.join(phasesDir, item.dir), node_path_1.default.join(phasesDir, newDirName));
|
|
830
|
+
renamedDirs.push({ from: item.dir, to: newDirName });
|
|
831
|
+
for (const f of node_fs_1.default.readdirSync(node_path_1.default.join(phasesDir, newDirName))) {
|
|
832
|
+
if (f.includes(oldPhaseId)) {
|
|
833
|
+
const newFileName = f.replace(oldPhaseId, newPhaseId);
|
|
834
|
+
node_fs_1.default.renameSync(node_path_1.default.join(phasesDir, newDirName, f), node_path_1.default.join(phasesDir, newDirName, newFileName));
|
|
835
|
+
renamedFiles.push({ from: f, to: newFileName });
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
return { renamedDirs, renamedFiles };
|
|
840
|
+
}
|
|
841
|
+
function renameIntegerPhases(phasesDir, removedInt) {
|
|
842
|
+
const renamedDirs = [];
|
|
843
|
+
const renamedFiles = [];
|
|
844
|
+
const dirs = readSubdirectories(phasesDir, true);
|
|
845
|
+
const toRename = dirs
|
|
846
|
+
.map((dir) => {
|
|
847
|
+
const m = dir.match(/^(\d+)([A-Z])?(?:\.(\d+))?-(.+)$/i);
|
|
848
|
+
if (!m)
|
|
849
|
+
return null;
|
|
850
|
+
const dirInt = parseInt(m[1], 10);
|
|
851
|
+
return dirInt > removedInt && dirInt !== 999
|
|
852
|
+
? {
|
|
853
|
+
dir,
|
|
854
|
+
oldInt: dirInt,
|
|
855
|
+
letter: m[2] ? m[2].toUpperCase() : '',
|
|
856
|
+
decimal: m[3] ? parseInt(m[3], 10) : null,
|
|
857
|
+
slug: m[4],
|
|
858
|
+
}
|
|
859
|
+
: null;
|
|
860
|
+
})
|
|
861
|
+
.filter((item) => item !== null)
|
|
862
|
+
.sort((a, b) => a.oldInt !== b.oldInt ? b.oldInt - a.oldInt : (b.decimal || 0) - (a.decimal || 0));
|
|
863
|
+
for (const item of toRename) {
|
|
864
|
+
const newInt = item.oldInt - 1;
|
|
865
|
+
const newPadded = String(newInt).padStart(2, '0');
|
|
866
|
+
const oldPadded = String(item.oldInt).padStart(2, '0');
|
|
867
|
+
const letterSuffix = item.letter || '';
|
|
868
|
+
const decimalSuffix = item.decimal !== null ? `.${item.decimal}` : '';
|
|
869
|
+
const oldPrefix = `${oldPadded}${letterSuffix}${decimalSuffix}`;
|
|
870
|
+
const newPrefix = `${newPadded}${letterSuffix}${decimalSuffix}`;
|
|
871
|
+
const newDirName = `${newPrefix}-${item.slug}`;
|
|
872
|
+
node_fs_1.default.renameSync(node_path_1.default.join(phasesDir, item.dir), node_path_1.default.join(phasesDir, newDirName));
|
|
873
|
+
renamedDirs.push({ from: item.dir, to: newDirName });
|
|
874
|
+
for (const f of node_fs_1.default.readdirSync(node_path_1.default.join(phasesDir, newDirName))) {
|
|
875
|
+
if (f.startsWith(oldPrefix)) {
|
|
876
|
+
const newFileName = newPrefix + f.slice(oldPrefix.length);
|
|
877
|
+
node_fs_1.default.renameSync(node_path_1.default.join(phasesDir, newDirName, f), node_path_1.default.join(phasesDir, newDirName, newFileName));
|
|
878
|
+
renamedFiles.push({ from: f, to: newFileName });
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return { renamedDirs, renamedFiles };
|
|
883
|
+
}
|
|
884
|
+
function decrementRoadmapPhaseNumber(raw, removedInt) {
|
|
885
|
+
const num = parseInt(raw, 10);
|
|
886
|
+
if (!Number.isInteger(num) || num <= removedInt || num === 999)
|
|
887
|
+
return raw;
|
|
888
|
+
return String(num - 1);
|
|
889
|
+
}
|
|
890
|
+
function decrementRoadmapPhaseToken(raw, removedInt) {
|
|
891
|
+
const match = String(raw).match(/^(\d+)(\.\d+)?$/);
|
|
892
|
+
if (!match)
|
|
893
|
+
return raw;
|
|
894
|
+
const num = parseInt(match[1], 10);
|
|
895
|
+
if (!Number.isInteger(num) || num <= removedInt || num === 999)
|
|
896
|
+
return raw;
|
|
897
|
+
return `${num - 1}${match[2] || ''}`;
|
|
898
|
+
}
|
|
899
|
+
function decrementRoadmapPaddedPhaseNumber(raw, removedInt) {
|
|
900
|
+
const num = parseInt(raw, 10);
|
|
901
|
+
if (!Number.isInteger(num) || num <= removedInt || num === 999)
|
|
902
|
+
return raw;
|
|
903
|
+
return String(num - 1).padStart(raw.length, '0');
|
|
904
|
+
}
|
|
905
|
+
function updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, removedInt, cwd) {
|
|
906
|
+
withPlanningLock(cwd, () => {
|
|
907
|
+
let content = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
908
|
+
const escaped = escapeRegex(targetPhase);
|
|
909
|
+
content = content.replace(new RegExp(`\\n?(?<h>#{2,4})\\s*Phase\\s+${escaped}\\s*:[\\s\\S]*?(?=\\n\\k<h>(?!#)\\s+Phase\\s+[^\\n:]+\\s*:|$)`, 'i'), '');
|
|
910
|
+
content = content.replace(new RegExp(`\\n?-\\s*\\[[ x]\\]\\s*.*Phase\\s+${escaped}[:\\s][^\\n]*`, 'gi'), '');
|
|
911
|
+
content = content.replace(new RegExp(`\\n?\\|\\s*${escaped}\\.?\\s[^|]*\\|[^\\n]*`, 'gi'), '');
|
|
912
|
+
if (!isDecimal) {
|
|
913
|
+
content = content.replace(/(#{2,4}\s*Phase\s+)(\d+(?:\.\d+)?)(\s*:)/gi, (_match, prefix, num, suffix) => `${prefix}${decrementRoadmapPhaseToken(num, removedInt)}${suffix}`);
|
|
914
|
+
content = content.replace(/(-\s*\[[ x]\]\s*.*?Phase\s+)(\d+)(\s*:|\s+)/gi, (_match, prefix, num, suffix) => `${prefix}${decrementRoadmapPhaseNumber(num, removedInt)}${suffix}`);
|
|
915
|
+
content = content.replace(/(\|\s*)(\d+)(\.\s)/g, (_match, prefix, num, suffix) => `${prefix}${decrementRoadmapPhaseNumber(num, removedInt)}${suffix}`);
|
|
916
|
+
content = content.replace(/(?<![0-9-])(\d{2})-(\d{2})(?=(?:(?:-[A-Za-z][A-Za-z0-9-]*)?-(?:PLAN|SUMMARY)\.md)|(?![0-9-]))/g, (_match, phaseNum, planNum) => `${decrementRoadmapPaddedPhaseNumber(phaseNum, removedInt)}-${planNum}`);
|
|
917
|
+
content = content.replace(/(\*\*Depends on\*\*\s*:\s*Phase\s+)(\d+(?:\.\d+)?)\b/gi, (_match, prefix, num) => `${prefix}${decrementRoadmapPhaseToken(num, removedInt)}`);
|
|
918
|
+
content = content.replace(/(Depends on:\*\*\s*Phase\s+)(\d+(?:\.\d+)?)\b/gi, (_match, prefix, num) => `${prefix}${decrementRoadmapPhaseToken(num, removedInt)}`);
|
|
919
|
+
}
|
|
920
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(roadmapPath, content);
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
|
924
|
+
if (!targetPhase)
|
|
925
|
+
error('phase number required for phase remove');
|
|
926
|
+
const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
927
|
+
const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
|
|
928
|
+
if (!node_fs_1.default.existsSync(roadmapPath))
|
|
929
|
+
error('ROADMAP.md not found');
|
|
930
|
+
const normalized = normalizePhaseName(targetPhase);
|
|
931
|
+
const isDecimal = targetPhase.includes('.');
|
|
932
|
+
const force = options.force || false;
|
|
933
|
+
const subdirs = readSubdirectories(phasesDir, true);
|
|
934
|
+
const targetDir = subdirs.find((d) => phaseTokenMatches(d, normalized)) || null;
|
|
935
|
+
if (targetDir && !force) {
|
|
936
|
+
const files = node_fs_1.default.readdirSync(node_path_1.default.join(phasesDir, targetDir));
|
|
937
|
+
const summaries = files.filter((f) => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
|
938
|
+
if (summaries.length > 0) {
|
|
939
|
+
error(`Phase ${targetPhase} has ${summaries.length} executed plan(s). Use --force to remove anyway.`);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
if (targetDir)
|
|
943
|
+
node_fs_1.default.rmSync(node_path_1.default.join(phasesDir, targetDir), { recursive: true, force: true });
|
|
944
|
+
let renamedDirs = [];
|
|
945
|
+
let renamedFiles = [];
|
|
946
|
+
try {
|
|
947
|
+
const renamed = isDecimal
|
|
948
|
+
? renameDecimalPhases(phasesDir, parseInt(normalized.split('.')[0], 10), parseInt(normalized.split('.')[1], 10))
|
|
949
|
+
: renameIntegerPhases(phasesDir, parseInt(normalized, 10));
|
|
950
|
+
renamedDirs = renamed.renamedDirs;
|
|
951
|
+
renamedFiles = renamed.renamedFiles;
|
|
952
|
+
}
|
|
953
|
+
catch {
|
|
954
|
+
/* intentionally empty */
|
|
955
|
+
}
|
|
956
|
+
updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, parseInt(normalized, 10), cwd);
|
|
957
|
+
const statePath = node_path_1.default.join(planningDir(cwd), 'STATE.md');
|
|
958
|
+
if (node_fs_1.default.existsSync(statePath)) {
|
|
959
|
+
readModifyWriteStateMd(statePath, (stateContent) => {
|
|
960
|
+
const totalRaw = stateExtractField(stateContent, 'Total Phases');
|
|
961
|
+
if (totalRaw) {
|
|
962
|
+
stateContent =
|
|
963
|
+
stateReplaceField(stateContent, 'Total Phases', String(parseInt(totalRaw, 10) - 1)) ||
|
|
964
|
+
stateContent;
|
|
965
|
+
}
|
|
966
|
+
const ofMatch = stateContent.match(/(\bof\s+)(\d+)(\s*(?:\(|phases?))/i);
|
|
967
|
+
if (ofMatch) {
|
|
968
|
+
stateContent = stateContent.replace(/(\bof\s+)(\d+)(\s*(?:\(|phases?))/i, `$1${parseInt(ofMatch[2], 10) - 1}$3`);
|
|
969
|
+
}
|
|
970
|
+
return stateContent;
|
|
971
|
+
}, cwd);
|
|
972
|
+
}
|
|
973
|
+
output({
|
|
974
|
+
removed: targetPhase,
|
|
975
|
+
directory_deleted: targetDir,
|
|
976
|
+
renamed_directories: renamedDirs,
|
|
977
|
+
renamed_files: renamedFiles,
|
|
978
|
+
roadmap_updated: true,
|
|
979
|
+
state_updated: node_fs_1.default.existsSync(statePath),
|
|
980
|
+
}, raw);
|
|
981
|
+
}
|
|
982
|
+
function writePlanningFileSet(writes) {
|
|
983
|
+
const applied = [];
|
|
984
|
+
try {
|
|
985
|
+
for (const write of writes) {
|
|
986
|
+
if (write.before === write.after)
|
|
987
|
+
continue;
|
|
988
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(write.filePath, write.after);
|
|
989
|
+
applied.push(write);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
catch (err) {
|
|
993
|
+
for (const write of applied.reverse()) {
|
|
994
|
+
try {
|
|
995
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(write.filePath, write.before);
|
|
996
|
+
}
|
|
997
|
+
catch (rollbackErr) {
|
|
998
|
+
const errObj = err;
|
|
999
|
+
errObj.rollbackError = rollbackErr;
|
|
1000
|
+
const rollbackMsg = rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr);
|
|
1001
|
+
errObj.message +=
|
|
1002
|
+
`\nWARNING: rollback failed while restoring ${write.filePath} ` +
|
|
1003
|
+
`(${rollbackMsg}). Planning files under .planning/ may be left in an ` +
|
|
1004
|
+
`inconsistent, partially rolled back state. Inspect ROADMAP.md / REQUIREMENTS.md / ` +
|
|
1005
|
+
`STATE.md before re-running phase complete.`;
|
|
1006
|
+
break;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
throw err;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
1013
|
+
if (!phaseNum) {
|
|
1014
|
+
error('phase number required for phase complete');
|
|
1015
|
+
}
|
|
1016
|
+
const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
1017
|
+
const statePath = node_path_1.default.join(planningDir(cwd), 'STATE.md');
|
|
1018
|
+
const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
|
|
1019
|
+
const today = new Date().toISOString().split('T')[0];
|
|
1020
|
+
const phaseInfoRaw = findPhaseInternal(cwd, phaseNum);
|
|
1021
|
+
if (!phaseInfoRaw) {
|
|
1022
|
+
error(`Phase ${phaseNum} not found`);
|
|
1023
|
+
}
|
|
1024
|
+
const phaseInfo = phaseInfoRaw;
|
|
1025
|
+
const planCount = phaseInfo['plans']
|
|
1026
|
+
? phaseInfo['plans'].length
|
|
1027
|
+
: 0;
|
|
1028
|
+
const summaryCount = phaseInfo['summaries']
|
|
1029
|
+
? phaseInfo['summaries'].length
|
|
1030
|
+
: 0;
|
|
1031
|
+
let requirementsUpdated = false;
|
|
1032
|
+
const warnings = [];
|
|
1033
|
+
try {
|
|
1034
|
+
const phaseFullDir = node_path_1.default.join(cwd, phaseInfo['directory']);
|
|
1035
|
+
const phaseFiles = node_fs_1.default.readdirSync(phaseFullDir);
|
|
1036
|
+
for (const file of phaseFiles.filter((f) => f.includes('-UAT') && f.endsWith('.md'))) {
|
|
1037
|
+
const content = node_fs_1.default.readFileSync(node_path_1.default.join(phaseFullDir, file), 'utf-8');
|
|
1038
|
+
if (/result: pending/.test(content))
|
|
1039
|
+
warnings.push(`${file}: has pending tests`);
|
|
1040
|
+
if (/result: blocked/.test(content))
|
|
1041
|
+
warnings.push(`${file}: has blocked tests`);
|
|
1042
|
+
if (/status: partial/.test(content))
|
|
1043
|
+
warnings.push(`${file}: testing incomplete (partial)`);
|
|
1044
|
+
if (/status: diagnosed/.test(content))
|
|
1045
|
+
warnings.push(`${file}: has diagnosed gaps`);
|
|
1046
|
+
}
|
|
1047
|
+
for (const file of phaseFiles.filter((f) => f.includes('-VERIFICATION') && f.endsWith('.md'))) {
|
|
1048
|
+
const content = node_fs_1.default.readFileSync(node_path_1.default.join(phaseFullDir, file), 'utf-8');
|
|
1049
|
+
if (/status: human_needed/.test(content))
|
|
1050
|
+
warnings.push(`${file}: needs human verification`);
|
|
1051
|
+
if (/status: gaps_found/.test(content))
|
|
1052
|
+
warnings.push(`${file}: has unresolved gaps`);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
catch {
|
|
1056
|
+
/* intentionally empty */
|
|
1057
|
+
}
|
|
1058
|
+
let nextPhaseNum = null;
|
|
1059
|
+
let nextPhaseName = null;
|
|
1060
|
+
let isLastPhase = true;
|
|
1061
|
+
withPlanningLock(cwd, () => {
|
|
1062
|
+
const runPhaseCompleteTransaction = () => {
|
|
1063
|
+
const writes = [];
|
|
1064
|
+
let roadmapContent = null;
|
|
1065
|
+
if (node_fs_1.default.existsSync(roadmapPath)) {
|
|
1066
|
+
const originalRoadmapContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
1067
|
+
roadmapContent = originalRoadmapContent;
|
|
1068
|
+
const phaseEscaped = phaseMarkdownRegexSource(phaseNum);
|
|
1069
|
+
const checkboxPattern = new RegExp(`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseEscaped}[:\\s][^\\n]*)`, 'i');
|
|
1070
|
+
roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`);
|
|
1071
|
+
const tableRowPattern = new RegExp(`^(\\|\\s*${phaseEscaped}\\.?\\s[^|]*(?:\\|[^\\n]*))$`, 'im');
|
|
1072
|
+
roadmapContent = roadmapContent.replace(tableRowPattern, (fullRow) => {
|
|
1073
|
+
const cells = fullRow.split('|').slice(1, -1);
|
|
1074
|
+
if (cells.length === 5) {
|
|
1075
|
+
cells[2] = ` ${summaryCount}/${planCount} `;
|
|
1076
|
+
cells[3] = ' Complete ';
|
|
1077
|
+
cells[4] = ` ${today} `;
|
|
1078
|
+
}
|
|
1079
|
+
else if (cells.length === 4) {
|
|
1080
|
+
cells[1] = ` ${summaryCount}/${planCount} `;
|
|
1081
|
+
cells[2] = ' Complete ';
|
|
1082
|
+
cells[3] = ` ${today} `;
|
|
1083
|
+
}
|
|
1084
|
+
return '|' + cells.join('|') + '|';
|
|
1085
|
+
});
|
|
1086
|
+
const planCountPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`, 'i');
|
|
1087
|
+
roadmapContent = roadmapContent.replace(planCountPattern, `$1${summaryCount}/${planCount} plans complete`);
|
|
1088
|
+
const phaseInfoSummaries = phaseInfo['summaries'];
|
|
1089
|
+
for (const summaryFile of phaseInfoSummaries) {
|
|
1090
|
+
const planId = summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
|
|
1091
|
+
if (!planId)
|
|
1092
|
+
continue;
|
|
1093
|
+
const planEscaped = escapeRegex(planId);
|
|
1094
|
+
const planCheckboxPattern = new RegExp(`(-\\s*\\[) (\\]\\s*(?:\\*\\*)?${planEscaped}(?:\\*\\*)?)`, 'i');
|
|
1095
|
+
roadmapContent = (roadmapContent).replace(planCheckboxPattern, '$1x$2');
|
|
1096
|
+
}
|
|
1097
|
+
writes.push({
|
|
1098
|
+
filePath: roadmapPath,
|
|
1099
|
+
before: originalRoadmapContent,
|
|
1100
|
+
after: roadmapContent,
|
|
1101
|
+
});
|
|
1102
|
+
const reqPath = node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md');
|
|
1103
|
+
if (node_fs_1.default.existsSync(reqPath)) {
|
|
1104
|
+
const phaseEsc = phaseMarkdownRegexSource(phaseNum);
|
|
1105
|
+
const currentMilestoneRoadmap = extractCurrentMilestone(roadmapContent, cwd);
|
|
1106
|
+
const phaseSectionMatch = currentMilestoneRoadmap.match(new RegExp(`(#{2,4}\\s*Phase\\s+${phaseEsc}[:\\s][\\s\\S]*?)(?=#{2,4}\\s*Phase\\s+|$)`, 'i'));
|
|
1107
|
+
const sectionText = phaseSectionMatch ? phaseSectionMatch[1] : '';
|
|
1108
|
+
const reqMatch = sectionText.match(/\*\*Requirements:?\*\*[^\S\n]*:?[^\S\n]*([^\n]+)/i);
|
|
1109
|
+
const originalReqContent = node_fs_1.default.readFileSync(reqPath, 'utf-8');
|
|
1110
|
+
let reqContent = originalReqContent;
|
|
1111
|
+
if (reqMatch) {
|
|
1112
|
+
const reqIds = reqMatch[1]
|
|
1113
|
+
.replace(/[\[\]]/g, '')
|
|
1114
|
+
.split(/[,\s]+/)
|
|
1115
|
+
.map((r) => r.trim())
|
|
1116
|
+
.filter(Boolean);
|
|
1117
|
+
for (const reqId of reqIds) {
|
|
1118
|
+
const reqEscaped = escapeRegex(reqId);
|
|
1119
|
+
reqContent = reqContent.replace(new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi'), '$1x$2');
|
|
1120
|
+
reqContent = reqContent.replace(new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*(?:Pending|In Progress)\\s*(\\|)`, 'gi'), '$1 Complete $2');
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
const bodyReqIds = [];
|
|
1124
|
+
const bodyReqPattern = /\*\*([A-Z][A-Z0-9]*-\d+)\*\*/g;
|
|
1125
|
+
let bodyMatch;
|
|
1126
|
+
while ((bodyMatch = bodyReqPattern.exec(reqContent)) !== null) {
|
|
1127
|
+
const id = bodyMatch[1];
|
|
1128
|
+
if (!bodyReqIds.includes(id))
|
|
1129
|
+
bodyReqIds.push(id);
|
|
1130
|
+
}
|
|
1131
|
+
const traceabilityHeadingMatch = reqContent.match(/^#{1,6}\s+Traceability\b/im);
|
|
1132
|
+
const traceabilitySection = traceabilityHeadingMatch
|
|
1133
|
+
? reqContent.slice(traceabilityHeadingMatch.index)
|
|
1134
|
+
: '';
|
|
1135
|
+
const tableReqIds = new Set();
|
|
1136
|
+
const tableRowPat = /^\|\s*([A-Z][A-Z0-9]*-\d+)\s*\|/gm;
|
|
1137
|
+
let tableMatch;
|
|
1138
|
+
while ((tableMatch = tableRowPat.exec(traceabilitySection)) !== null) {
|
|
1139
|
+
tableReqIds.add(tableMatch[1]);
|
|
1140
|
+
}
|
|
1141
|
+
const unregistered = bodyReqIds.filter((id) => !tableReqIds.has(id));
|
|
1142
|
+
if (unregistered.length > 0) {
|
|
1143
|
+
warnings.push(`REQUIREMENTS.md: ${unregistered.length} REQ-ID(s) found in body but missing from Traceability table: ${unregistered.join(', ')} — add them manually to keep traceability in sync`);
|
|
1144
|
+
}
|
|
1145
|
+
writes.push({ filePath: reqPath, before: originalReqContent, after: reqContent });
|
|
1146
|
+
requirementsUpdated = true;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
try {
|
|
1150
|
+
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
|
1151
|
+
const entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
|
|
1152
|
+
const dirs = entries
|
|
1153
|
+
.filter((e) => e.isDirectory())
|
|
1154
|
+
.map((e) => e.name)
|
|
1155
|
+
.filter(isDirInMilestone)
|
|
1156
|
+
.sort((a, b) => comparePhaseNum(a, b));
|
|
1157
|
+
for (const dir of dirs) {
|
|
1158
|
+
const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
|
|
1159
|
+
if (dm) {
|
|
1160
|
+
if (/^999(?:\.|$)/.test(dm[1]))
|
|
1161
|
+
continue;
|
|
1162
|
+
if (comparePhaseNum(dm[1], phaseNum) > 0) {
|
|
1163
|
+
nextPhaseNum = dm[1];
|
|
1164
|
+
nextPhaseName = dm[2] || null;
|
|
1165
|
+
isLastPhase = false;
|
|
1166
|
+
break;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
catch {
|
|
1172
|
+
/* intentionally empty */
|
|
1173
|
+
}
|
|
1174
|
+
if (isLastPhase && roadmapContent !== null) {
|
|
1175
|
+
try {
|
|
1176
|
+
const roadmapForPhases = extractCurrentMilestone(roadmapContent, cwd);
|
|
1177
|
+
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
|
|
1178
|
+
let pm;
|
|
1179
|
+
while ((pm = phasePattern.exec(roadmapForPhases)) !== null) {
|
|
1180
|
+
if (comparePhaseNum(pm[1], phaseNum) > 0) {
|
|
1181
|
+
nextPhaseNum = pm[1];
|
|
1182
|
+
nextPhaseName = pm[2]
|
|
1183
|
+
.replace(/\(INSERTED\)/i, '')
|
|
1184
|
+
.trim()
|
|
1185
|
+
.toLowerCase()
|
|
1186
|
+
.replace(/\s+/g, '-');
|
|
1187
|
+
isLastPhase = false;
|
|
1188
|
+
break;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
catch {
|
|
1193
|
+
/* intentionally empty */
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
if (node_fs_1.default.existsSync(statePath)) {
|
|
1197
|
+
const originalStateContent = (0, shell_command_projection_cjs_1.platformReadSync)(statePath) || '';
|
|
1198
|
+
let stateContent = originalStateContent;
|
|
1199
|
+
const phaseValue = nextPhaseNum || phaseNum;
|
|
1200
|
+
const existingPhaseField = stateExtractField(stateContent, 'Current Phase') ||
|
|
1201
|
+
stateExtractField(stateContent, 'Phase');
|
|
1202
|
+
let newPhaseValue = String(phaseValue);
|
|
1203
|
+
if (existingPhaseField) {
|
|
1204
|
+
const totalMatch = existingPhaseField.match(/of\s+(\d+)/);
|
|
1205
|
+
const nameMatch = existingPhaseField.match(/\(([^)]+)\)/);
|
|
1206
|
+
if (totalMatch) {
|
|
1207
|
+
const total = totalMatch[1];
|
|
1208
|
+
const nameStr = nextPhaseName
|
|
1209
|
+
? ` (${nextPhaseName.replace(/-/g, ' ')})`
|
|
1210
|
+
: nameMatch
|
|
1211
|
+
? ` (${nameMatch[1]})`
|
|
1212
|
+
: '';
|
|
1213
|
+
newPhaseValue = `${phaseValue} of ${total}${nameStr}`;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Phase', 'Phase', newPhaseValue);
|
|
1217
|
+
if (nextPhaseName) {
|
|
1218
|
+
stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Phase Name', null, nextPhaseName.replace(/-/g, ' '));
|
|
1219
|
+
}
|
|
1220
|
+
stateContent = stateReplaceFieldWithFallback(stateContent, 'Status', null, isLastPhase ? 'Milestone complete' : 'Ready to plan');
|
|
1221
|
+
stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Plan', 'Plan', 'Not started');
|
|
1222
|
+
stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity', 'Last activity', today);
|
|
1223
|
+
stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity Description', null, `Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}`);
|
|
1224
|
+
const completedRaw = stateExtractField(stateContent, 'Completed Phases');
|
|
1225
|
+
if (completedRaw !== null) {
|
|
1226
|
+
let newCompleted = parseInt(completedRaw, 10);
|
|
1227
|
+
let derivedTotalPhases = null;
|
|
1228
|
+
if (roadmapContent !== null) {
|
|
1229
|
+
const derived = (0, phase_lifecycle_cjs_1.deriveProgressFromRoadmap)(roadmapContent);
|
|
1230
|
+
if (derived.completedPhases !== null)
|
|
1231
|
+
newCompleted = derived.completedPhases;
|
|
1232
|
+
if (derived.totalPhases !== null)
|
|
1233
|
+
derivedTotalPhases = derived.totalPhases;
|
|
1234
|
+
}
|
|
1235
|
+
stateContent =
|
|
1236
|
+
stateReplaceField(stateContent, 'Completed Phases', String(newCompleted)) ||
|
|
1237
|
+
stateContent;
|
|
1238
|
+
const totalRaw = stateExtractField(stateContent, 'Total Phases');
|
|
1239
|
+
const totalPhases = derivedTotalPhases || (totalRaw ? parseInt(totalRaw, 10) : null);
|
|
1240
|
+
if (totalPhases && totalPhases > 0) {
|
|
1241
|
+
const newPercent = (0, phase_lifecycle_cjs_1.clampPercent)(newCompleted, totalPhases);
|
|
1242
|
+
stateContent =
|
|
1243
|
+
stateReplaceField(stateContent, 'Progress', `${newPercent}%`) || stateContent;
|
|
1244
|
+
stateContent = stateContent.replace(/(percent:\s*)\d+/, `$1${newPercent}`);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
stateContent = updatePerformanceMetricsSection(stateContent, cwd, phaseNum, planCount, summaryCount);
|
|
1248
|
+
stateContent = syncStateFrontmatter(stateContent, cwd);
|
|
1249
|
+
writes.push({ filePath: statePath, before: originalStateContent, after: stateContent });
|
|
1250
|
+
}
|
|
1251
|
+
writePlanningFileSet(writes);
|
|
1252
|
+
};
|
|
1253
|
+
if (node_fs_1.default.existsSync(statePath)) {
|
|
1254
|
+
withStateLock(statePath, runPhaseCompleteTransaction);
|
|
1255
|
+
}
|
|
1256
|
+
else {
|
|
1257
|
+
runPhaseCompleteTransaction();
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
let autoPruned = false;
|
|
1261
|
+
try {
|
|
1262
|
+
const configPath = node_path_1.default.join(planningDir(cwd), 'config.json');
|
|
1263
|
+
if (node_fs_1.default.existsSync(configPath)) {
|
|
1264
|
+
const rawConfig = JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
1265
|
+
const workflow = rawConfig['workflow'];
|
|
1266
|
+
const autoPruneEnabled = workflow && workflow['auto_prune_state'] === true;
|
|
1267
|
+
if (autoPruneEnabled && node_fs_1.default.existsSync(statePath)) {
|
|
1268
|
+
// Non-hoisted: load-order matters (stateMod must be fully resolved first).
|
|
1269
|
+
const { cmdStatePrune } = stateMod;
|
|
1270
|
+
cmdStatePrune(cwd, { keepRecent: '3', dryRun: false, silent: true }, true);
|
|
1271
|
+
autoPruned = true;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
catch {
|
|
1276
|
+
/* intentionally empty — auto-prune is best-effort */
|
|
1277
|
+
}
|
|
1278
|
+
const result = {
|
|
1279
|
+
completed_phase: phaseNum,
|
|
1280
|
+
phase_name: phaseInfo['phase_name'],
|
|
1281
|
+
plans_executed: `${summaryCount}/${planCount}`,
|
|
1282
|
+
next_phase: nextPhaseNum,
|
|
1283
|
+
next_phase_name: nextPhaseName,
|
|
1284
|
+
is_last_phase: isLastPhase,
|
|
1285
|
+
date: today,
|
|
1286
|
+
roadmap_updated: node_fs_1.default.existsSync(roadmapPath),
|
|
1287
|
+
state_updated: node_fs_1.default.existsSync(statePath),
|
|
1288
|
+
requirements_updated: requirementsUpdated,
|
|
1289
|
+
auto_pruned: autoPruned,
|
|
1290
|
+
warnings,
|
|
1291
|
+
has_warnings: warnings.length > 0,
|
|
1292
|
+
};
|
|
1293
|
+
output(result, raw);
|
|
1294
|
+
}
|
|
1295
|
+
module.exports = {
|
|
1296
|
+
cmdPhasesList,
|
|
1297
|
+
cmdPhaseNextDecimal,
|
|
1298
|
+
cmdFindPhase,
|
|
1299
|
+
cmdPhasePlanIndex,
|
|
1300
|
+
cmdPhaseAdd,
|
|
1301
|
+
cmdPhaseAddBatch,
|
|
1302
|
+
cmdPhaseMvpMode,
|
|
1303
|
+
cmdPhaseInsert,
|
|
1304
|
+
cmdPhaseRemove,
|
|
1305
|
+
cmdPhaseComplete,
|
|
1306
|
+
computeDependencyLevels,
|
|
1307
|
+
};
|