@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,2051 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Core — Shared utilities, constants, and internal helpers
|
|
4
|
+
*
|
|
5
|
+
* ADR-457 build-at-publish: the hand-written bin/lib/core.cjs collapsed
|
|
6
|
+
* to a TypeScript source of truth. Behaviour is preserved byte-for-behaviour
|
|
7
|
+
* from the prior hand-written .cjs; only strict types are added.
|
|
8
|
+
*/
|
|
9
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
10
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
11
|
+
};
|
|
12
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
13
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
14
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
15
|
+
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
17
|
+
const modelProfiles = require("./model-profiles.cjs");
|
|
18
|
+
const { MODEL_PROFILES, AGENT_TO_PHASE_TYPE, VALID_PHASE_TYPES: _VALID_PHASE_TYPES, AGENT_DEFAULT_TIERS, VALID_AGENT_TIERS, nextTier } = modelProfiles;
|
|
19
|
+
const model_catalog_cjs_1 = require("./model-catalog.cjs");
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
21
|
+
const worktreeSafety = require("./worktree-safety.cjs");
|
|
22
|
+
const { resolveWorktreeContext, parseWorktreePorcelain: parseWorktreePorcelainPolicy, planWorktreePrune, executeWorktreePrunePlan, inspectWorktreeHealth, } = worktreeSafety;
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
24
|
+
const planningWorkspace = require("./planning-workspace.cjs");
|
|
25
|
+
// Compatibility shim: new imports should use planning-workspace.cjs directly.
|
|
26
|
+
const { planningDir, planningRoot, planningPaths, withPlanningLock, getActiveWorkstream, setActiveWorkstream, findContextMdIn, } = planningWorkspace;
|
|
27
|
+
const project_root_cjs_1 = require("./project-root.cjs");
|
|
28
|
+
const runtime_homes_cjs_1 = require("./runtime-homes.cjs");
|
|
29
|
+
// ─── Configuration Module (generated CJS mirror) ────────────────────────────
|
|
30
|
+
const configuration_cjs_1 = require("./configuration.cjs");
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
32
|
+
const configSchema = require("./config-schema.cjs");
|
|
33
|
+
const { VALID_CONFIG_KEYS, DYNAMIC_KEY_PATTERNS } = configSchema;
|
|
34
|
+
// ─── Path helpers ────────────────────────────────────────────────────────────
|
|
35
|
+
/** Normalize a relative path to always use forward slashes (cross-platform). */
|
|
36
|
+
function toPosixPath(p) {
|
|
37
|
+
return p.split(node_path_1.default.sep).join('/');
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Scan immediate child directories for separate git repos.
|
|
41
|
+
* Returns a sorted array of directory names that have their own `.git`.
|
|
42
|
+
* Excludes hidden directories and node_modules.
|
|
43
|
+
*/
|
|
44
|
+
function detectSubRepos(cwd) {
|
|
45
|
+
const results = [];
|
|
46
|
+
try {
|
|
47
|
+
const entries = node_fs_1.default.readdirSync(cwd, { withFileTypes: true });
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
if (!entry.isDirectory())
|
|
50
|
+
continue;
|
|
51
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules')
|
|
52
|
+
continue;
|
|
53
|
+
const gitPath = node_path_1.default.join(cwd, entry.name, '.git');
|
|
54
|
+
try {
|
|
55
|
+
if (node_fs_1.default.existsSync(gitPath)) {
|
|
56
|
+
results.push(entry.name);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch { /* ignore */ }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch { /* ignore */ }
|
|
63
|
+
return results.sort();
|
|
64
|
+
}
|
|
65
|
+
// findProjectRoot is now re-exported from the generated CJS module above.
|
|
66
|
+
// ─── Output helpers ───────────────────────────────────────────────────────────
|
|
67
|
+
/**
|
|
68
|
+
* Dedicated GSD temp directory: path.join(os.tmpdir(), 'gsd').
|
|
69
|
+
* Created on first use. Keeps GSD temp files isolated from the system
|
|
70
|
+
* temp directory so reap scans only GSD files (#1975).
|
|
71
|
+
*/
|
|
72
|
+
const GSD_TEMP_DIR = node_path_1.default.join(node_os_1.default.tmpdir(), 'gsd');
|
|
73
|
+
function ensureGsdTempDir() {
|
|
74
|
+
(0, shell_command_projection_cjs_1.platformEnsureDir)(GSD_TEMP_DIR);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Remove stale gsd-* temp files/dirs older than maxAgeMs (default: 5 minutes).
|
|
78
|
+
* Runs opportunistically before each new temp file write to prevent unbounded accumulation.
|
|
79
|
+
* @param prefix - filename prefix to match (e.g., 'gsd-')
|
|
80
|
+
* @param opts
|
|
81
|
+
* @param opts.maxAgeMs - max age in ms before removal (default: 5 min)
|
|
82
|
+
* @param opts.dirsOnly - if true, only remove directories (default: false)
|
|
83
|
+
*/
|
|
84
|
+
function reapStaleTempFiles(prefix = 'gsd-', { maxAgeMs = 5 * 60 * 1000, dirsOnly = false } = {}) {
|
|
85
|
+
try {
|
|
86
|
+
ensureGsdTempDir();
|
|
87
|
+
const now = Date.now();
|
|
88
|
+
const entries = node_fs_1.default.readdirSync(GSD_TEMP_DIR);
|
|
89
|
+
for (const entry of entries) {
|
|
90
|
+
if (!entry.startsWith(prefix))
|
|
91
|
+
continue;
|
|
92
|
+
const fullPath = node_path_1.default.join(GSD_TEMP_DIR, entry);
|
|
93
|
+
try {
|
|
94
|
+
const stat = node_fs_1.default.statSync(fullPath);
|
|
95
|
+
if (now - stat.mtimeMs > maxAgeMs) {
|
|
96
|
+
if (stat.isDirectory()) {
|
|
97
|
+
node_fs_1.default.rmSync(fullPath, { recursive: true, force: true });
|
|
98
|
+
}
|
|
99
|
+
else if (!dirsOnly) {
|
|
100
|
+
node_fs_1.default.unlinkSync(fullPath);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// File may have been removed between readdir and stat — ignore
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// Non-critical — don't let cleanup failures break output
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function output(result, raw, rawValue) {
|
|
114
|
+
let data;
|
|
115
|
+
if (raw && rawValue !== undefined) {
|
|
116
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
117
|
+
data = String(rawValue);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const json = JSON.stringify(result, null, 2);
|
|
121
|
+
// Large payloads exceed Claude Code's Bash tool buffer (~50KB).
|
|
122
|
+
// Write to tmpfile and output the path prefixed with @file: so callers can detect it.
|
|
123
|
+
if (json.length > 50000) {
|
|
124
|
+
reapStaleTempFiles();
|
|
125
|
+
ensureGsdTempDir();
|
|
126
|
+
const tmpPath = node_path_1.default.join(GSD_TEMP_DIR, `gsd-${Date.now()}.json`);
|
|
127
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(tmpPath, json);
|
|
128
|
+
data = '@file:' + tmpPath;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
data = json;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// process.stdout.write() is async when stdout is a pipe — process.exit()
|
|
135
|
+
// can tear down the process before the reader consumes the buffer.
|
|
136
|
+
// fs.writeSync(1, ...) blocks until the kernel accepts the bytes, and
|
|
137
|
+
// skipping process.exit() lets the event loop drain naturally.
|
|
138
|
+
node_fs_1.default.writeSync(1, data);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Frozen enum of typed reason codes used by error() for structured errors.
|
|
142
|
+
* Each subcommand contributes its own codes; the enum exists so tests can
|
|
143
|
+
* assert against typed values instead of grepping stderr (#2974).
|
|
144
|
+
*
|
|
145
|
+
* Adding a new code:
|
|
146
|
+
* - Pick a snake_case lowercase value (the JSON wire form)
|
|
147
|
+
* - Group by subsystem prefix (CONFIG_*, SDK_*, etc)
|
|
148
|
+
* - Pass it to error(msg, ERROR_REASON.NEW_CODE) at the call site
|
|
149
|
+
*/
|
|
150
|
+
const ERROR_REASON = Object.freeze({
|
|
151
|
+
// config-get / config-set
|
|
152
|
+
CONFIG_KEY_NOT_FOUND: 'config_key_not_found',
|
|
153
|
+
CONFIG_NO_FILE: 'config_no_file',
|
|
154
|
+
CONFIG_PARSE_FAILED: 'config_parse_failed',
|
|
155
|
+
CONFIG_INVALID_KEY: 'config_invalid_key',
|
|
156
|
+
// SDK / gsd-tools dispatch
|
|
157
|
+
SDK_FAIL_FAST: 'sdk_fail_fast',
|
|
158
|
+
SDK_UNKNOWN_COMMAND: 'sdk_unknown_command',
|
|
159
|
+
SDK_MISSING_ARG: 'sdk_missing_arg',
|
|
160
|
+
// workflow / phase
|
|
161
|
+
PHASE_NOT_FOUND: 'phase_not_found',
|
|
162
|
+
SUMMARY_NO_PLANNING: 'summary_no_planning',
|
|
163
|
+
// graphify
|
|
164
|
+
GRAPHIFY_NO_GRAPH: 'graphify_no_graph',
|
|
165
|
+
GRAPHIFY_INVALID_QUERY: 'graphify_invalid_query',
|
|
166
|
+
// hooks
|
|
167
|
+
HOOKS_OPT_OUT: 'hooks_opt_out',
|
|
168
|
+
// security-scan
|
|
169
|
+
SECURITY_SCAN_FAILED: 'security_scan_failed',
|
|
170
|
+
// generic
|
|
171
|
+
USAGE: 'usage',
|
|
172
|
+
UNKNOWN: 'unknown',
|
|
173
|
+
});
|
|
174
|
+
/**
|
|
175
|
+
* Process-level flag: when true, error() emits structured JSON to stderr
|
|
176
|
+
* instead of plain "Error: <message>" text. Set by gsd-tools.cjs when the
|
|
177
|
+
* CLI is invoked with `--json-errors`. Tests opt in to typed-IR error
|
|
178
|
+
* assertions by passing that flag and parsing the JSON.
|
|
179
|
+
*
|
|
180
|
+
* Default off so existing callers and human operators keep their plain-text
|
|
181
|
+
* diagnostics. The structured form is opt-in for tooling and tests (#2974).
|
|
182
|
+
*/
|
|
183
|
+
let _jsonErrorMode = false;
|
|
184
|
+
function setJsonErrorMode(v) { _jsonErrorMode = !!v; }
|
|
185
|
+
function getJsonErrorMode() { return _jsonErrorMode; }
|
|
186
|
+
/**
|
|
187
|
+
* Emit an error and exit. When the second argument is provided it must be
|
|
188
|
+
* a value from ERROR_REASON; tests can assert on `result.reason`. When the
|
|
189
|
+
* process is in JSON-error mode, stderr receives `{ ok: false, reason,
|
|
190
|
+
* message }` so callers can parse it; otherwise stderr keeps the plain
|
|
191
|
+
* text form for human operators.
|
|
192
|
+
*/
|
|
193
|
+
function error(message, reason = ERROR_REASON.UNKNOWN) {
|
|
194
|
+
if (_jsonErrorMode) {
|
|
195
|
+
const payload = JSON.stringify({ ok: false, reason, message }) + '\n';
|
|
196
|
+
node_fs_1.default.writeSync(2, payload);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
node_fs_1.default.writeSync(2, 'Error: ' + message + '\n');
|
|
200
|
+
}
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
// ─── File & Config utilities ──────────────────────────────────────────────────
|
|
204
|
+
/**
|
|
205
|
+
* Canonical config defaults — flat-key projection for CJS consumers.
|
|
206
|
+
*
|
|
207
|
+
* Cycle 4: Values are sourced from CANONICAL_CONFIG_DEFAULTS (the nested
|
|
208
|
+
* manifest loaded by configuration.generated.cjs). The flat shape is
|
|
209
|
+
* preserved here so legacy consumers (config.cjs, verify.cjs, tests that
|
|
210
|
+
* regex-parse this source) continue to work without changes. The key names
|
|
211
|
+
* and the `const CONFIG_DEFAULTS = {` pattern are intentionally kept.
|
|
212
|
+
*
|
|
213
|
+
* Mapping notes:
|
|
214
|
+
* - workflow.plan_check → plan_checker (CJS flat name; verify.cjs uses this)
|
|
215
|
+
* - git.* → flat git keys (branching_strategy, templates)
|
|
216
|
+
* - workflow.* → flat names (research, verifier, …)
|
|
217
|
+
* - planning.sub_repos → sub_repos
|
|
218
|
+
* - planning.commit_docs / search_gitignored → top-level flat keys
|
|
219
|
+
*/
|
|
220
|
+
// CANONICAL_CONFIG_DEFAULTS is typed as Record<string, unknown> from configuration.cjs;
|
|
221
|
+
// we use a typed accessor to avoid repeated casts.
|
|
222
|
+
function _getConfigDefault(key) {
|
|
223
|
+
return (configuration_cjs_1.CONFIG_DEFAULTS)[key];
|
|
224
|
+
}
|
|
225
|
+
function _getNestedConfigDefault(section, field) {
|
|
226
|
+
const sec = (configuration_cjs_1.CONFIG_DEFAULTS)[section];
|
|
227
|
+
if (sec && typeof sec === 'object' && !Array.isArray(sec)) {
|
|
228
|
+
return sec[field];
|
|
229
|
+
}
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
const CONFIG_DEFAULTS = {
|
|
233
|
+
model_profile: _getConfigDefault('model_profile'),
|
|
234
|
+
commit_docs: _getConfigDefault('commit_docs'),
|
|
235
|
+
search_gitignored: _getConfigDefault('search_gitignored'),
|
|
236
|
+
branching_strategy: _getNestedConfigDefault('git', 'branching_strategy'),
|
|
237
|
+
phase_branch_template: _getNestedConfigDefault('git', 'phase_branch_template'),
|
|
238
|
+
milestone_branch_template: _getNestedConfigDefault('git', 'milestone_branch_template'),
|
|
239
|
+
quick_branch_template: _getNestedConfigDefault('git', 'quick_branch_template'),
|
|
240
|
+
research: _getNestedConfigDefault('workflow', 'research'),
|
|
241
|
+
plan_checker: _getNestedConfigDefault('workflow', 'plan_check'), // flat CJS name maps to workflow.plan_check
|
|
242
|
+
verifier: _getNestedConfigDefault('workflow', 'verifier'),
|
|
243
|
+
nyquist_validation: _getNestedConfigDefault('workflow', 'nyquist_validation'),
|
|
244
|
+
ai_integration_phase: _getNestedConfigDefault('workflow', 'ai_integration_phase'),
|
|
245
|
+
parallelization: _getConfigDefault('parallelization'),
|
|
246
|
+
brave_search: _getConfigDefault('brave_search'),
|
|
247
|
+
firecrawl: _getConfigDefault('firecrawl'),
|
|
248
|
+
exa_search: _getConfigDefault('exa_search'),
|
|
249
|
+
text_mode: _getNestedConfigDefault('workflow', 'text_mode'),
|
|
250
|
+
sub_repos: _getNestedConfigDefault('planning', 'sub_repos'),
|
|
251
|
+
resolve_model_ids: _getConfigDefault('resolve_model_ids'),
|
|
252
|
+
context_window: _getConfigDefault('context_window'),
|
|
253
|
+
phase_naming: _getConfigDefault('phase_naming'),
|
|
254
|
+
project_code: _getConfigDefault('project_code'),
|
|
255
|
+
subagent_timeout: _getNestedConfigDefault('workflow', 'subagent_timeout'),
|
|
256
|
+
security_enforcement: _getNestedConfigDefault('workflow', 'security_enforcement'),
|
|
257
|
+
security_asvs_level: _getNestedConfigDefault('workflow', 'security_asvs_level'),
|
|
258
|
+
security_block_on: _getNestedConfigDefault('workflow', 'security_block_on'),
|
|
259
|
+
post_planning_gaps: _getNestedConfigDefault('workflow', 'post_planning_gaps'),
|
|
260
|
+
};
|
|
261
|
+
/**
|
|
262
|
+
* Deep-merge two plain config objects. `overlay` wins on key conflict.
|
|
263
|
+
* Explicit `null` in overlay overrides base (null means "unset this key").
|
|
264
|
+
* Arrays are replaced, not merged. Non-object primitives use overlay value.
|
|
265
|
+
*
|
|
266
|
+
* Note: `undefined` in overlay is treated as "no value provided" and falls
|
|
267
|
+
* back to base (preserves inheritance). Explicit `null` overrides base.
|
|
268
|
+
*/
|
|
269
|
+
function _deepMergeConfig(base, overlay) {
|
|
270
|
+
if (overlay === null || overlay === undefined)
|
|
271
|
+
return overlay;
|
|
272
|
+
if (typeof base !== 'object' || typeof overlay !== 'object')
|
|
273
|
+
return overlay;
|
|
274
|
+
const result = { ...base };
|
|
275
|
+
for (const key of Object.keys(overlay)) {
|
|
276
|
+
if (overlay[key] !== null && typeof overlay[key] === 'object' && !Array.isArray(overlay[key])) {
|
|
277
|
+
result[key] = _deepMergeConfig((base[key] ?? {}), overlay[key]);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
result[key] = overlay[key];
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
// Module-level deduplication for unknown-key warnings (#3523).
|
|
286
|
+
// A single `init phase-op N` call invokes loadConfig more than once; this Set
|
|
287
|
+
// prevents the same warning from being echoed on each invocation.
|
|
288
|
+
const _warnedUnknownConfigKeys = new Set();
|
|
289
|
+
function loadConfig(cwd, options = {}) {
|
|
290
|
+
const activeWorkstream = Object.prototype.hasOwnProperty.call(options, 'workstream')
|
|
291
|
+
? options['workstream']
|
|
292
|
+
: (options['workstreamContext'] && Object.prototype.hasOwnProperty.call(options['workstreamContext'], 'ws'))
|
|
293
|
+
? options['workstreamContext']['ws']
|
|
294
|
+
: (process.env['GSD_WORKSTREAM'] || null);
|
|
295
|
+
// When GSD_WORKSTREAM is set, load root config first so workstream config
|
|
296
|
+
// can inherit from it. This prevents users from duplicating model_overrides,
|
|
297
|
+
// workflow.*, etc. across every workstream config (#2714).
|
|
298
|
+
const ws = typeof activeWorkstream === 'string' ? activeWorkstream : (activeWorkstream === null ? null : null);
|
|
299
|
+
// #315 — per-call lazy memo: all three detection sites inside this loadConfig
|
|
300
|
+
// call operate on the same cwd and the subrepo set cannot change mid-call, so
|
|
301
|
+
// a single scan is sufficient. The memo is scoped to THIS call (not module-level)
|
|
302
|
+
// so separate loadConfig invocations each get a fresh scan.
|
|
303
|
+
let cachedSubRepos;
|
|
304
|
+
const getDetectedSubRepos = () => {
|
|
305
|
+
if (cachedSubRepos === undefined)
|
|
306
|
+
cachedSubRepos = detectSubRepos(cwd);
|
|
307
|
+
// Return a copy: original detectSubRepos returned a fresh array per call,
|
|
308
|
+
// so each site must keep an independent array (avoid cross-site aliasing).
|
|
309
|
+
return cachedSubRepos.slice();
|
|
310
|
+
};
|
|
311
|
+
let rootParsed = null;
|
|
312
|
+
if (ws) {
|
|
313
|
+
const rootConfigPath = node_path_1.default.join(planningRoot(cwd), 'config.json');
|
|
314
|
+
try {
|
|
315
|
+
const raw = (0, shell_command_projection_cjs_1.platformReadSync)(rootConfigPath);
|
|
316
|
+
if (raw === null)
|
|
317
|
+
throw new Error('missing');
|
|
318
|
+
rootParsed = JSON.parse(raw);
|
|
319
|
+
// Cycle 4: delegate all legacy-key normalization to the Configuration Module.
|
|
320
|
+
const { parsed: rootNormalized, normalizations: rootNorms } = (0, configuration_cjs_1.normalizeLegacyKeys)(rootParsed);
|
|
321
|
+
if (rootNorms.length > 0) {
|
|
322
|
+
// Resolve filesystem-dependent normalizations (multiRepo → planning.sub_repos)
|
|
323
|
+
for (const norm of rootNorms) {
|
|
324
|
+
if (norm.requiresFilesystem && !rootNormalized.planning?.['sub_repos']) {
|
|
325
|
+
const detected = getDetectedSubRepos();
|
|
326
|
+
if (detected.length > 0) {
|
|
327
|
+
if (!rootNormalized.planning)
|
|
328
|
+
rootNormalized.planning = {};
|
|
329
|
+
rootNormalized.planning['sub_repos'] = detected;
|
|
330
|
+
rootNormalized.planning['commit_docs'] = false;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
rootParsed = rootNormalized;
|
|
335
|
+
try {
|
|
336
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(rootConfigPath, JSON.stringify(rootParsed, null, 2));
|
|
337
|
+
}
|
|
338
|
+
catch { /* ignore */ }
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
rootParsed = rootNormalized;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
// Root config missing or unparseable — workstream config stands alone
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const configPath = node_path_1.default.join(planningDir(cwd, ws), 'config.json');
|
|
349
|
+
const defaults = CONFIG_DEFAULTS;
|
|
350
|
+
try {
|
|
351
|
+
const raw = (0, shell_command_projection_cjs_1.platformReadSync)(configPath);
|
|
352
|
+
if (raw === null)
|
|
353
|
+
throw new Error('missing');
|
|
354
|
+
// `fileData` is the parsed content of the config.json file on disk — used
|
|
355
|
+
// for migrations and writes so we never persist merged values back to disk.
|
|
356
|
+
const fileData = JSON.parse(raw);
|
|
357
|
+
// Cycle 4: Single normalizeLegacyKeys call replaces all four inline migration
|
|
358
|
+
// blocks (depth→granularity, multiRepo→planning.sub_repos, sub_repos→planning.sub_repos,
|
|
359
|
+
// branching_strategy→git.branching_strategy). The Module is pure (no I/O); disk
|
|
360
|
+
// writeback is handled below with the existing platformWriteSync pattern.
|
|
361
|
+
let configDirty = false;
|
|
362
|
+
{
|
|
363
|
+
const { parsed: normalized, normalizations } = (0, configuration_cjs_1.normalizeLegacyKeys)(fileData);
|
|
364
|
+
if (normalizations.length > 0) {
|
|
365
|
+
// Merge normalized values back into fileData (mutation-in-place for legacy code below)
|
|
366
|
+
Object.keys(fileData).forEach(k => delete fileData[k]);
|
|
367
|
+
Object.assign(fileData, normalized);
|
|
368
|
+
configDirty = true;
|
|
369
|
+
// Resolve filesystem-dependent normalizations (multiRepo → planning.sub_repos).
|
|
370
|
+
for (const norm of normalizations) {
|
|
371
|
+
if (norm.requiresFilesystem && !fileData.planning?.['sub_repos']) {
|
|
372
|
+
const detected = getDetectedSubRepos();
|
|
373
|
+
if (detected.length > 0) {
|
|
374
|
+
if (!fileData.planning)
|
|
375
|
+
fileData.planning = {};
|
|
376
|
+
fileData.planning['sub_repos'] = detected;
|
|
377
|
+
fileData.planning['commit_docs'] = false;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// Keep planning.sub_repos in sync with actual filesystem
|
|
384
|
+
const currentSubRepos = fileData.planning?.['sub_repos'] || [];
|
|
385
|
+
if (Array.isArray(currentSubRepos) && currentSubRepos.length > 0) {
|
|
386
|
+
const detected = getDetectedSubRepos();
|
|
387
|
+
if (detected.length > 0) {
|
|
388
|
+
const sorted = [...currentSubRepos].sort();
|
|
389
|
+
if (JSON.stringify(sorted) !== JSON.stringify(detected)) {
|
|
390
|
+
if (!fileData.planning)
|
|
391
|
+
fileData.planning = {};
|
|
392
|
+
fileData.planning['sub_repos'] = detected;
|
|
393
|
+
configDirty = true;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
// Persist sub_repos changes (migration or sync) — write only the on-disk
|
|
398
|
+
// file contents, never the merged result, to avoid polluting workstream configs.
|
|
399
|
+
if (configDirty) {
|
|
400
|
+
try {
|
|
401
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(configPath, JSON.stringify(fileData, null, 2));
|
|
402
|
+
}
|
|
403
|
+
catch { /* ignore */ }
|
|
404
|
+
}
|
|
405
|
+
// Now apply root→workstream inheritance. `parsed` is the effective config
|
|
406
|
+
// used for value extraction below; fileData is kept for disk writes only.
|
|
407
|
+
const parsed = rootParsed
|
|
408
|
+
? (_deepMergeConfig(rootParsed, fileData) ?? fileData)
|
|
409
|
+
: fileData;
|
|
410
|
+
// Warn about unrecognized top-level keys so users don't silently lose config.
|
|
411
|
+
const KNOWN_TOP_LEVEL = new Set([
|
|
412
|
+
// Extract top-level key names from dot-notation paths (e.g., 'workflow.research' → 'workflow')
|
|
413
|
+
...[...VALID_CONFIG_KEYS].map((k) => k.split('.')[0]),
|
|
414
|
+
// Dynamic-pattern top-level containers (e.g. review, model_profile_overrides)
|
|
415
|
+
...DYNAMIC_KEY_PATTERNS.map(p => p.topLevel),
|
|
416
|
+
// Internal keys loadConfig reads but config-set doesn't expose
|
|
417
|
+
'model_overrides', 'context_window', 'resolve_model_ids', 'claude_md_path', 'effort', 'fast_mode',
|
|
418
|
+
// Deprecated keys (still accepted for migration, not in config-set)
|
|
419
|
+
'depth', 'multiRepo', 'branching_strategy',
|
|
420
|
+
]);
|
|
421
|
+
const unknownKeys = Object.keys(parsed).filter(k => !KNOWN_TOP_LEVEL.has(k));
|
|
422
|
+
if (unknownKeys.length > 0) {
|
|
423
|
+
const warnKey = unknownKeys.join(',');
|
|
424
|
+
if (!_warnedUnknownConfigKeys.has(warnKey)) {
|
|
425
|
+
_warnedUnknownConfigKeys.add(warnKey);
|
|
426
|
+
process.stderr.write(`gsd-tools: warning: unknown config key(s) in .planning/config.json: ${unknownKeys.join(', ')} — these will be ignored\n`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// #2517 — Validate runtime/tier values
|
|
430
|
+
_warnUnknownProfileOverrides(parsed, '.planning/config.json');
|
|
431
|
+
const get = (key, nested) => {
|
|
432
|
+
if (parsed[key] !== undefined)
|
|
433
|
+
return parsed[key];
|
|
434
|
+
if (nested && parsed[nested.section] && typeof parsed[nested.section] === 'object' && parsed[nested.section] !== null) {
|
|
435
|
+
const sec = parsed[nested.section];
|
|
436
|
+
if (sec[nested.field] !== undefined) {
|
|
437
|
+
return sec[nested.field];
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return undefined;
|
|
441
|
+
};
|
|
442
|
+
const parallelization = (() => {
|
|
443
|
+
const val = get('parallelization');
|
|
444
|
+
if (typeof val === 'boolean')
|
|
445
|
+
return val;
|
|
446
|
+
if (typeof val === 'object' && val !== null && 'enabled' in (val))
|
|
447
|
+
return val['enabled'];
|
|
448
|
+
return defaults.parallelization;
|
|
449
|
+
})();
|
|
450
|
+
return {
|
|
451
|
+
model_profile: get('model_profile') ?? defaults.model_profile,
|
|
452
|
+
commit_docs: (() => {
|
|
453
|
+
const explicit = get('commit_docs', { section: 'planning', field: 'commit_docs' });
|
|
454
|
+
// If explicitly set in config, respect the user's choice
|
|
455
|
+
if (explicit !== undefined)
|
|
456
|
+
return explicit;
|
|
457
|
+
// Auto-detection: when no explicit value and .planning/ is gitignored,
|
|
458
|
+
// default to false instead of true
|
|
459
|
+
if (isGitIgnored(cwd, '.planning/'))
|
|
460
|
+
return false;
|
|
461
|
+
return defaults.commit_docs;
|
|
462
|
+
})(),
|
|
463
|
+
search_gitignored: get('search_gitignored', { section: 'planning', field: 'search_gitignored' }) ?? defaults.search_gitignored,
|
|
464
|
+
branching_strategy: get('branching_strategy', { section: 'git', field: 'branching_strategy' }) ?? defaults.branching_strategy,
|
|
465
|
+
phase_branch_template: get('phase_branch_template', { section: 'git', field: 'phase_branch_template' }) ?? defaults.phase_branch_template,
|
|
466
|
+
milestone_branch_template: get('milestone_branch_template', { section: 'git', field: 'milestone_branch_template' }) ?? defaults.milestone_branch_template,
|
|
467
|
+
quick_branch_template: get('quick_branch_template', { section: 'git', field: 'quick_branch_template' }) ?? defaults.quick_branch_template,
|
|
468
|
+
research: get('research', { section: 'workflow', field: 'research' }) ?? defaults.research,
|
|
469
|
+
plan_checker: get('plan_checker', { section: 'workflow', field: 'plan_check' }) ?? defaults.plan_checker,
|
|
470
|
+
verifier: get('verifier', { section: 'workflow', field: 'verifier' }) ?? defaults.verifier,
|
|
471
|
+
nyquist_validation: get('nyquist_validation', { section: 'workflow', field: 'nyquist_validation' }) ?? defaults.nyquist_validation,
|
|
472
|
+
post_planning_gaps: get('post_planning_gaps', { section: 'workflow', field: 'post_planning_gaps' }) ?? defaults.post_planning_gaps,
|
|
473
|
+
parallelization,
|
|
474
|
+
brave_search: get('brave_search') ?? defaults.brave_search,
|
|
475
|
+
firecrawl: get('firecrawl') ?? defaults.firecrawl,
|
|
476
|
+
exa_search: get('exa_search') ?? defaults.exa_search,
|
|
477
|
+
tdd_mode: get('tdd_mode', { section: 'workflow', field: 'tdd_mode' }) ?? false,
|
|
478
|
+
mvp_mode: get('mvp_mode', { section: 'workflow', field: 'mvp_mode' }) ?? false,
|
|
479
|
+
text_mode: get('text_mode', { section: 'workflow', field: 'text_mode' }) ?? defaults.text_mode,
|
|
480
|
+
auto_advance: get('auto_advance', { section: 'workflow', field: 'auto_advance' }) ?? false,
|
|
481
|
+
_auto_chain_active: get('_auto_chain_active', { section: 'workflow', field: '_auto_chain_active' }) ?? false,
|
|
482
|
+
mode: get('mode') ?? 'interactive',
|
|
483
|
+
sub_repos: get('sub_repos', { section: 'planning', field: 'sub_repos' }) ?? defaults.sub_repos,
|
|
484
|
+
resolve_model_ids: get('resolve_model_ids') ?? defaults.resolve_model_ids,
|
|
485
|
+
context_window: get('context_window') ?? defaults.context_window,
|
|
486
|
+
phase_naming: get('phase_naming') ?? defaults.phase_naming,
|
|
487
|
+
project_code: get('project_code') ?? defaults.project_code,
|
|
488
|
+
subagent_timeout: get('subagent_timeout', { section: 'workflow', field: 'subagent_timeout' }) ?? defaults.subagent_timeout,
|
|
489
|
+
model_overrides: (parsed['model_overrides']) || null,
|
|
490
|
+
// #3023 — per-phase-type model map.
|
|
491
|
+
models: (parsed['models']) || null,
|
|
492
|
+
// #68 — top-level granularity
|
|
493
|
+
granularity: parsed['granularity'] !== undefined ? parsed['granularity'] : null,
|
|
494
|
+
// #68 — per-phase-type granularity map.
|
|
495
|
+
granularities: (parsed['granularities']) || null,
|
|
496
|
+
// #68 — planning sub-object
|
|
497
|
+
planning: (parsed['planning']) || null,
|
|
498
|
+
// #3024 — dynamic routing block.
|
|
499
|
+
dynamic_routing: (parsed['dynamic_routing']) || null,
|
|
500
|
+
// #2517 — runtime-aware profiles.
|
|
501
|
+
runtime: (parsed['runtime']) || null,
|
|
502
|
+
model_profile_overrides: (parsed['model_profile_overrides']) || null,
|
|
503
|
+
// #49 — provider-neutral model policy presets.
|
|
504
|
+
model_policy: (parsed['model_policy']) || null,
|
|
505
|
+
// #443 — effort/fast_mode
|
|
506
|
+
effort: (parsed['effort']) || null,
|
|
507
|
+
fast_mode: (parsed['fast_mode']) || null,
|
|
508
|
+
agent_skills: (parsed['agent_skills']) || {},
|
|
509
|
+
agent_skills_security: (parsed['agent_skills_security']) || null,
|
|
510
|
+
manager: (parsed['manager']) || {},
|
|
511
|
+
response_language: get('response_language') || null,
|
|
512
|
+
claude_md_path: get('claude_md_path') || null,
|
|
513
|
+
claude_md_assembly: (parsed['claude_md_assembly']) || null,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
catch {
|
|
517
|
+
// Fall back to ~/.gsd/defaults.json only for truly pre-project contexts (#1683)
|
|
518
|
+
if (node_fs_1.default.existsSync(planningDir(cwd, ws))) {
|
|
519
|
+
if (rootParsed) {
|
|
520
|
+
// Workstream has no config.json: re-parse using root config as the sole source.
|
|
521
|
+
return loadConfig(cwd, { workstream: null });
|
|
522
|
+
}
|
|
523
|
+
return defaults;
|
|
524
|
+
}
|
|
525
|
+
try {
|
|
526
|
+
const home = process.env['GSD_HOME'] || node_os_1.default.homedir();
|
|
527
|
+
const globalDefaultsPath = node_path_1.default.join(home, '.gsd', 'defaults.json');
|
|
528
|
+
const raw = (0, shell_command_projection_cjs_1.platformReadSync)(globalDefaultsPath);
|
|
529
|
+
if (raw === null)
|
|
530
|
+
throw new Error('missing');
|
|
531
|
+
const globalDefaults = JSON.parse(raw);
|
|
532
|
+
return {
|
|
533
|
+
...defaults,
|
|
534
|
+
model_profile: (globalDefaults['model_profile']) ?? defaults.model_profile,
|
|
535
|
+
commit_docs: (globalDefaults['commit_docs']) ?? defaults.commit_docs,
|
|
536
|
+
research: (globalDefaults['research']) ?? defaults.research,
|
|
537
|
+
plan_checker: (globalDefaults['plan_checker']) ?? defaults.plan_checker,
|
|
538
|
+
verifier: (globalDefaults['verifier']) ?? defaults.verifier,
|
|
539
|
+
nyquist_validation: (globalDefaults['nyquist_validation']) ?? defaults.nyquist_validation,
|
|
540
|
+
post_planning_gaps: (globalDefaults['post_planning_gaps'])
|
|
541
|
+
?? globalDefaults['workflow']?.['post_planning_gaps']
|
|
542
|
+
?? defaults.post_planning_gaps,
|
|
543
|
+
parallelization: (globalDefaults['parallelization']) ?? defaults.parallelization,
|
|
544
|
+
text_mode: (globalDefaults['text_mode']) ?? defaults.text_mode,
|
|
545
|
+
resolve_model_ids: (globalDefaults['resolve_model_ids']) ?? defaults.resolve_model_ids,
|
|
546
|
+
context_window: (globalDefaults['context_window']) ?? defaults.context_window,
|
|
547
|
+
subagent_timeout: (globalDefaults['subagent_timeout']) ?? defaults.subagent_timeout,
|
|
548
|
+
model_overrides: (globalDefaults['model_overrides']) || null,
|
|
549
|
+
models: (globalDefaults['models']) || null,
|
|
550
|
+
granularity: (globalDefaults['granularity']) !== undefined ? globalDefaults['granularity'] : null,
|
|
551
|
+
granularities: (globalDefaults['granularities']) || null,
|
|
552
|
+
planning: (globalDefaults['planning']) || null,
|
|
553
|
+
dynamic_routing: (globalDefaults['dynamic_routing']) || null,
|
|
554
|
+
effort: (globalDefaults['effort']) || null,
|
|
555
|
+
fast_mode: (globalDefaults['fast_mode']) || null,
|
|
556
|
+
agent_skills: (globalDefaults['agent_skills']) || {},
|
|
557
|
+
response_language: (globalDefaults['response_language']) || null,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
catch {
|
|
561
|
+
return defaults;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
// ─── Git utilities ────────────────────────────────────────────────────────────
|
|
566
|
+
const _gitIgnoredCache = new Map();
|
|
567
|
+
function isGitIgnored(cwd, targetPath) {
|
|
568
|
+
const key = cwd + '::' + targetPath;
|
|
569
|
+
if (_gitIgnoredCache.has(key))
|
|
570
|
+
return _gitIgnoredCache.get(key);
|
|
571
|
+
// --no-index checks .gitignore rules regardless of whether the file is tracked.
|
|
572
|
+
const result = (0, shell_command_projection_cjs_1.execGit)(['check-ignore', '-q', '--no-index', '--', targetPath], { cwd });
|
|
573
|
+
const ignored = result.exitCode === 0;
|
|
574
|
+
_gitIgnoredCache.set(key, ignored);
|
|
575
|
+
return ignored;
|
|
576
|
+
}
|
|
577
|
+
// ─── Common path helpers ──────────────────────────────────────────────────────
|
|
578
|
+
/**
|
|
579
|
+
* Resolve the main worktree root when running inside a git worktree.
|
|
580
|
+
* In a linked worktree, .planning/ lives in the main worktree, not in the linked one.
|
|
581
|
+
* Returns the main worktree path, or cwd if not in a worktree.
|
|
582
|
+
*/
|
|
583
|
+
function resolveWorktreeRoot(cwd) {
|
|
584
|
+
const context = resolveWorktreeContext(cwd, {
|
|
585
|
+
existsSync: node_fs_1.default.existsSync,
|
|
586
|
+
});
|
|
587
|
+
return context.effectiveRoot;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Parse `git worktree list --porcelain` output into an array of
|
|
591
|
+
* { path, branch } objects. Entries with a detached HEAD (no branch line)
|
|
592
|
+
* are skipped because we cannot safely reason about their merge status.
|
|
593
|
+
*
|
|
594
|
+
* @param porcelain - raw output from git worktree list --porcelain
|
|
595
|
+
* @returns {{ path: string, branch: string }[]}
|
|
596
|
+
*/
|
|
597
|
+
function parseWorktreePorcelain(porcelain) {
|
|
598
|
+
return parseWorktreePorcelainPolicy(porcelain);
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Clear stale worktree metadata references via `git worktree prune`.
|
|
602
|
+
*
|
|
603
|
+
* Destructive linked-worktree removal is disabled by default for safety.
|
|
604
|
+
*
|
|
605
|
+
* @param repoRoot - absolute path to the main (or any) worktree of
|
|
606
|
+
* the repository; used as `cwd` for git commands.
|
|
607
|
+
* @returns list of worktree paths that were removed (always empty)
|
|
608
|
+
*/
|
|
609
|
+
function pruneOrphanedWorktrees(repoRoot) {
|
|
610
|
+
try {
|
|
611
|
+
const plan = planWorktreePrune(repoRoot, { allowDestructive: false }, { parseWorktreePorcelain });
|
|
612
|
+
const pruneResult = executeWorktreePrunePlan(plan);
|
|
613
|
+
if (pruneResult && pruneResult.timedOut) {
|
|
614
|
+
process.stderr.write('[gsd-tools] WARNING: worktree health check degraded' +
|
|
615
|
+
' — git worktree prune timed out after 10s.' +
|
|
616
|
+
' Orphaned worktree metadata may remain until the next successful run.\n');
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
catch { /* never crash the caller */ }
|
|
620
|
+
return [];
|
|
621
|
+
}
|
|
622
|
+
// ─── Planning workspace (pathing + active workstream + lock) moved to planning-workspace.cjs ───
|
|
623
|
+
// ─── Phase utilities ──────────────────────────────────────────────────────────
|
|
624
|
+
function escapeRegex(value) {
|
|
625
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
626
|
+
}
|
|
627
|
+
function normalizePhaseName(phase) {
|
|
628
|
+
const str = String(phase);
|
|
629
|
+
// Strip optional project_code prefix (e.g., 'CK-01' → '01')
|
|
630
|
+
const stripped = str.replace(/^[A-Z]{1,6}-(?=\d)/, '');
|
|
631
|
+
// Milestone-prefixed phase IDs: M-NN or M-N-N (deep decomposition).
|
|
632
|
+
const milestoneMatch = stripped.match(/^(\d+)((?:-\d+)+)([A-Z]?(?:\.\d+)*)$/i);
|
|
633
|
+
if (milestoneMatch) {
|
|
634
|
+
const major = milestoneMatch[1].padStart(2, '0');
|
|
635
|
+
const subSegments = milestoneMatch[2].slice(1).split('-').map(s => s.padStart(2, '0'));
|
|
636
|
+
const suffix = milestoneMatch[3] || '';
|
|
637
|
+
return `${major}-${subSegments.join('-')}${suffix}`;
|
|
638
|
+
}
|
|
639
|
+
// Standard numeric phases: 1, 01, 12A, 12.1
|
|
640
|
+
const match = stripped.match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
|
|
641
|
+
if (match) {
|
|
642
|
+
const padded = match[1].padStart(2, '0');
|
|
643
|
+
// Preserve original case of letter suffix (#1962).
|
|
644
|
+
const letter = match[2] || '';
|
|
645
|
+
const decimal = match[3] || '';
|
|
646
|
+
return padded + letter + decimal;
|
|
647
|
+
}
|
|
648
|
+
// Custom phase IDs (e.g. PROJ-42, AUTH-101): return as-is
|
|
649
|
+
return str;
|
|
650
|
+
}
|
|
651
|
+
function getMilestoneFromPhaseId(phaseId) {
|
|
652
|
+
const str = String(phaseId);
|
|
653
|
+
const stripped = str.replace(/^[A-Z]{1,6}-(?=\d)/i, '');
|
|
654
|
+
const m = stripped.match(/^0*(\d+)-\d/);
|
|
655
|
+
if (!m)
|
|
656
|
+
return null;
|
|
657
|
+
const major = parseInt(m[1], 10);
|
|
658
|
+
if (major === 0 || major === 999)
|
|
659
|
+
return null;
|
|
660
|
+
return `v${major}.0`;
|
|
661
|
+
}
|
|
662
|
+
function getPhaseDirFromPhaseId(phaseId, phaseName, projectCode) {
|
|
663
|
+
const str = String(phaseId);
|
|
664
|
+
const stripped = str.replace(/^[A-Z]{1,6}-(?=\d)/i, '');
|
|
665
|
+
const m = stripped.match(/^0*(\d+)-(0*(\d+(?:-\d+)*))$/);
|
|
666
|
+
if (!m)
|
|
667
|
+
return null;
|
|
668
|
+
const milestone = String(parseInt(m[1], 10)).padStart(2, '0');
|
|
669
|
+
const subParts = m[2].split('-').map(p => String(parseInt(p, 10)).padStart(2, '0'));
|
|
670
|
+
const sub = subParts.join('-');
|
|
671
|
+
const slug = phaseName
|
|
672
|
+
? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
|
|
673
|
+
: '';
|
|
674
|
+
const parts = [milestone, sub, slug].filter(Boolean);
|
|
675
|
+
const base = parts.join('-');
|
|
676
|
+
return projectCode ? `${projectCode}-${base}` : base;
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Render a regex source fragment matching a phase number against ROADMAP/STATE
|
|
680
|
+
* prose regardless of zero-padding on either side.
|
|
681
|
+
*/
|
|
682
|
+
function phaseMarkdownRegexSource(phaseNum) {
|
|
683
|
+
const stripped = String(phaseNum).replace(/^[A-Z]{1,6}-(?=\d)/i, '');
|
|
684
|
+
// Milestone-prefixed IDs: M-NN or M-N-N (deep).
|
|
685
|
+
const milestoneSegments = stripped.match(/^(\d+)((?:-\d+)*)([A-Z]?(?:\.\d+)*)$/i);
|
|
686
|
+
if (milestoneSegments && milestoneSegments[2]) {
|
|
687
|
+
const majorUnpadded = milestoneSegments[1].replace(/^0+/, '') || '0';
|
|
688
|
+
const subParts = milestoneSegments[2].slice(1).split('-');
|
|
689
|
+
const subFragments = subParts.map(s => {
|
|
690
|
+
const unpadded = s.replace(/^0+/, '') || '0';
|
|
691
|
+
return `0*${escapeRegex(unpadded)}`;
|
|
692
|
+
});
|
|
693
|
+
const suffix = milestoneSegments[3] || '';
|
|
694
|
+
const suffixFragment = suffix ? escapeRegex(suffix) : '';
|
|
695
|
+
return `0*${escapeRegex(majorUnpadded)}-${subFragments.join('-')}${suffixFragment}`;
|
|
696
|
+
}
|
|
697
|
+
// Plain numeric phase: 1, 01, 12A, 12.1
|
|
698
|
+
const match = stripped.match(/^0*(\d+)([A-Z])?((?:\.\d+)*)$/i);
|
|
699
|
+
if (!match)
|
|
700
|
+
return escapeRegex(phaseNum);
|
|
701
|
+
const integer = match[1].replace(/^0+/, '') || '0';
|
|
702
|
+
const letter = match[2] ? escapeRegex(match[2]) : '';
|
|
703
|
+
const decimal = match[3] ? escapeRegex(match[3]) : '';
|
|
704
|
+
return `0*${escapeRegex(integer)}${letter}${decimal}`;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* #3599: when the caller passed a project-code-prefixed ID like `PROJ-42`,
|
|
708
|
+
* return the exact-escaped form.
|
|
709
|
+
*/
|
|
710
|
+
function phaseMarkdownRegexSourceExact(phaseNum) {
|
|
711
|
+
const raw = String(phaseNum);
|
|
712
|
+
if (!/^[A-Z]{1,6}-(?=\d)/i.test(raw))
|
|
713
|
+
return null;
|
|
714
|
+
return escapeRegex(raw);
|
|
715
|
+
}
|
|
716
|
+
function comparePhaseNum(a, b) {
|
|
717
|
+
// Strip optional project_code prefix before comparing
|
|
718
|
+
const sa = String(a).replace(/^[A-Z]{1,6}-(?=\d)/i, '');
|
|
719
|
+
const sb = String(b).replace(/^[A-Z]{1,6}-(?=\d)/i, '');
|
|
720
|
+
const milestoneA = sa.match(/^(\d+)((?:-\d+)+)([A-Z]?(?:\.\d+)*)$/i);
|
|
721
|
+
const milestoneB = sb.match(/^(\d+)((?:-\d+)+)([A-Z]?(?:\.\d+)*)$/i);
|
|
722
|
+
if (milestoneA && milestoneB) {
|
|
723
|
+
const segsA = [parseInt(milestoneA[1], 10), ...milestoneA[2].slice(1).split('-').map(s => parseInt(s, 10))];
|
|
724
|
+
const segsB = [parseInt(milestoneB[1], 10), ...milestoneB[2].slice(1).split('-').map(s => parseInt(s, 10))];
|
|
725
|
+
const maxSegs = Math.max(segsA.length, segsB.length);
|
|
726
|
+
for (let i = 0; i < maxSegs; i++) {
|
|
727
|
+
const av = segsA[i] !== undefined ? segsA[i] : 0;
|
|
728
|
+
const bv = segsB[i] !== undefined ? segsB[i] : 0;
|
|
729
|
+
if (av !== bv)
|
|
730
|
+
return av - bv;
|
|
731
|
+
}
|
|
732
|
+
const sufA = milestoneA[3] || '';
|
|
733
|
+
const sufB = milestoneB[3] || '';
|
|
734
|
+
if (sufA !== sufB)
|
|
735
|
+
return sufA < sufB ? -1 : 1;
|
|
736
|
+
return 0;
|
|
737
|
+
}
|
|
738
|
+
if (milestoneA || milestoneB)
|
|
739
|
+
return String(a).localeCompare(String(b));
|
|
740
|
+
const pa = sa.match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
|
|
741
|
+
const pb = sb.match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
|
|
742
|
+
if (!pa || !pb)
|
|
743
|
+
return String(a).localeCompare(String(b));
|
|
744
|
+
const intDiff = parseInt(pa[1], 10) - parseInt(pb[1], 10);
|
|
745
|
+
if (intDiff !== 0)
|
|
746
|
+
return intDiff;
|
|
747
|
+
const la = (pa[2] || '').toUpperCase();
|
|
748
|
+
const lb = (pb[2] || '').toUpperCase();
|
|
749
|
+
if (la !== lb) {
|
|
750
|
+
if (!la)
|
|
751
|
+
return -1;
|
|
752
|
+
if (!lb)
|
|
753
|
+
return 1;
|
|
754
|
+
return la < lb ? -1 : 1;
|
|
755
|
+
}
|
|
756
|
+
const aDecParts = pa[3] ? pa[3].slice(1).split('.').map(p => parseInt(p, 10)) : [];
|
|
757
|
+
const bDecParts = pb[3] ? pb[3].slice(1).split('.').map(p => parseInt(p, 10)) : [];
|
|
758
|
+
const maxLen = Math.max(aDecParts.length, bDecParts.length);
|
|
759
|
+
if (aDecParts.length === 0 && bDecParts.length > 0)
|
|
760
|
+
return -1;
|
|
761
|
+
if (bDecParts.length === 0 && aDecParts.length > 0)
|
|
762
|
+
return 1;
|
|
763
|
+
for (let i = 0; i < maxLen; i++) {
|
|
764
|
+
const av = Number.isFinite(aDecParts[i]) ? aDecParts[i] : 0;
|
|
765
|
+
const bv = Number.isFinite(bDecParts[i]) ? bDecParts[i] : 0;
|
|
766
|
+
if (av !== bv)
|
|
767
|
+
return av - bv;
|
|
768
|
+
}
|
|
769
|
+
return 0;
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Extract the phase token from a directory name.
|
|
773
|
+
*/
|
|
774
|
+
function extractPhaseToken(dirName) {
|
|
775
|
+
const codePrefixMatch = dirName.match(/^([A-Z]{1,6})-(\d.*)/i);
|
|
776
|
+
let prefix = '';
|
|
777
|
+
let rest = dirName;
|
|
778
|
+
if (codePrefixMatch) {
|
|
779
|
+
prefix = codePrefixMatch[1] + '-';
|
|
780
|
+
rest = codePrefixMatch[2];
|
|
781
|
+
}
|
|
782
|
+
const segments = rest.split('-');
|
|
783
|
+
const tokenSegments = [];
|
|
784
|
+
for (let i = 0; i < segments.length; i++) {
|
|
785
|
+
const seg = segments[i];
|
|
786
|
+
if (/^\d/.test(seg)) {
|
|
787
|
+
tokenSegments.push(seg);
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
break;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (tokenSegments.length === 0) {
|
|
794
|
+
return dirName;
|
|
795
|
+
}
|
|
796
|
+
return prefix + tokenSegments.join('-');
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Check if a directory name's phase token matches the normalized phase exactly.
|
|
800
|
+
*/
|
|
801
|
+
function phaseTokenMatches(dirName, normalized) {
|
|
802
|
+
const token = extractPhaseToken(dirName);
|
|
803
|
+
if (token.toUpperCase() === normalized.toUpperCase())
|
|
804
|
+
return true;
|
|
805
|
+
const stripped = dirName.replace(/^[A-Z]{1,6}-(?=\d)/i, '');
|
|
806
|
+
if (stripped !== dirName) {
|
|
807
|
+
const strippedToken = extractPhaseToken(stripped);
|
|
808
|
+
if (strippedToken.toUpperCase() === normalized.toUpperCase())
|
|
809
|
+
return true;
|
|
810
|
+
}
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
function extractCanonicalPlanId(filename) {
|
|
814
|
+
const base = filename.replace(/-PLAN\.md$/i, '').replace(/-SUMMARY\.md$/i, '').replace(/\.md$/i, '');
|
|
815
|
+
const parts = base.split('-').filter(Boolean);
|
|
816
|
+
const tokenRe = /^\d+[A-Z]?(?:\.\d+)*$/i;
|
|
817
|
+
const phaseIdx = parts.findIndex(p => tokenRe.test(p));
|
|
818
|
+
if (phaseIdx >= 0 && phaseIdx + 1 < parts.length && tokenRe.test(parts[phaseIdx + 1])) {
|
|
819
|
+
return `${parts[phaseIdx]}-${parts[phaseIdx + 1]}`;
|
|
820
|
+
}
|
|
821
|
+
return base;
|
|
822
|
+
}
|
|
823
|
+
function searchPhaseInDir(baseDir, relBase, normalized) {
|
|
824
|
+
try {
|
|
825
|
+
const dirs = readSubdirectories(baseDir, true);
|
|
826
|
+
const match = dirs.find(d => phaseTokenMatches(d, normalized));
|
|
827
|
+
if (!match)
|
|
828
|
+
return null;
|
|
829
|
+
const phaseToken = extractPhaseToken(match);
|
|
830
|
+
const phaseNumber = phaseToken || normalized;
|
|
831
|
+
const afterToken = match.slice(phaseToken ? phaseToken.length : 0).replace(/^-/, '');
|
|
832
|
+
const phaseName = afterToken || null;
|
|
833
|
+
const phaseDir = node_path_1.default.join(baseDir, match);
|
|
834
|
+
const { plans: unsortedPlans, summaries: unsortedSummaries, hasResearch, hasContext, hasVerification, hasReviews } = getPhaseFileStats(phaseDir);
|
|
835
|
+
const plans = unsortedPlans.sort();
|
|
836
|
+
const summaries = unsortedSummaries.sort();
|
|
837
|
+
const completedPlanIds = new Set(summaries.flatMap(s => {
|
|
838
|
+
const exact = s.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
|
|
839
|
+
const canonical = extractCanonicalPlanId(s);
|
|
840
|
+
return canonical === exact ? [exact] : [exact, canonical];
|
|
841
|
+
}));
|
|
842
|
+
const incompletePlans = plans.filter(p => {
|
|
843
|
+
const planId = p.replace('-PLAN.md', '').replace('PLAN.md', '');
|
|
844
|
+
const canonical = extractCanonicalPlanId(p);
|
|
845
|
+
return !completedPlanIds.has(planId) && !completedPlanIds.has(canonical);
|
|
846
|
+
});
|
|
847
|
+
return {
|
|
848
|
+
found: true,
|
|
849
|
+
directory: toPosixPath(node_path_1.default.join(relBase, match)),
|
|
850
|
+
phase_number: phaseNumber,
|
|
851
|
+
phase_name: phaseName,
|
|
852
|
+
phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null,
|
|
853
|
+
plans,
|
|
854
|
+
summaries,
|
|
855
|
+
incomplete_plans: incompletePlans,
|
|
856
|
+
has_research: hasResearch,
|
|
857
|
+
has_context: hasContext,
|
|
858
|
+
has_verification: hasVerification,
|
|
859
|
+
has_reviews: hasReviews,
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
catch {
|
|
863
|
+
return null;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
function findPhaseInternal(cwd, phase) {
|
|
867
|
+
if (!phase)
|
|
868
|
+
return null;
|
|
869
|
+
const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
|
|
870
|
+
const normalized = normalizePhaseName(phase);
|
|
871
|
+
const relPhasesDir = toPosixPath(node_path_1.default.relative(cwd, phasesDir));
|
|
872
|
+
const current = searchPhaseInDir(phasesDir, relPhasesDir, normalized);
|
|
873
|
+
if (current)
|
|
874
|
+
return current;
|
|
875
|
+
const milestonesDir = node_path_1.default.join(cwd, '.planning', 'milestones');
|
|
876
|
+
if (!node_fs_1.default.existsSync(milestonesDir))
|
|
877
|
+
return null;
|
|
878
|
+
try {
|
|
879
|
+
const milestoneEntries = node_fs_1.default.readdirSync(milestonesDir, { withFileTypes: true });
|
|
880
|
+
const archiveDirs = milestoneEntries
|
|
881
|
+
.filter(e => e.isDirectory() && /^v[\d.]+-phases$/.test(e.name))
|
|
882
|
+
.map(e => e.name)
|
|
883
|
+
.sort()
|
|
884
|
+
.reverse();
|
|
885
|
+
for (const archiveName of archiveDirs) {
|
|
886
|
+
const versionMatch = archiveName.match(/^(v[\d.]+)-phases$/);
|
|
887
|
+
const version = versionMatch[1];
|
|
888
|
+
const archivePath = node_path_1.default.join(milestonesDir, archiveName);
|
|
889
|
+
const relBase = '.planning/milestones/' + archiveName;
|
|
890
|
+
const result = searchPhaseInDir(archivePath, relBase, normalized);
|
|
891
|
+
if (result) {
|
|
892
|
+
result.archived = version;
|
|
893
|
+
return result;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
catch { /* intentionally empty */ }
|
|
898
|
+
return null;
|
|
899
|
+
}
|
|
900
|
+
function getArchivedPhaseDirs(cwd) {
|
|
901
|
+
const milestonesDir = node_path_1.default.join(cwd, '.planning', 'milestones');
|
|
902
|
+
const results = [];
|
|
903
|
+
if (!node_fs_1.default.existsSync(milestonesDir))
|
|
904
|
+
return results;
|
|
905
|
+
try {
|
|
906
|
+
const milestoneEntries = node_fs_1.default.readdirSync(milestonesDir, { withFileTypes: true });
|
|
907
|
+
const phaseDirs = milestoneEntries
|
|
908
|
+
.filter(e => e.isDirectory() && /^v[\d.]+-phases$/.test(e.name))
|
|
909
|
+
.map(e => e.name)
|
|
910
|
+
.sort()
|
|
911
|
+
.reverse();
|
|
912
|
+
for (const archiveName of phaseDirs) {
|
|
913
|
+
const versionMatch = archiveName.match(/^(v[\d.]+)-phases$/);
|
|
914
|
+
const version = versionMatch[1];
|
|
915
|
+
const archivePath = node_path_1.default.join(milestonesDir, archiveName);
|
|
916
|
+
const dirs = readSubdirectories(archivePath, true);
|
|
917
|
+
for (const dir of dirs) {
|
|
918
|
+
results.push({
|
|
919
|
+
name: dir,
|
|
920
|
+
milestone: version,
|
|
921
|
+
basePath: node_path_1.default.join('.planning', 'milestones', archiveName),
|
|
922
|
+
fullPath: node_path_1.default.join(archivePath, dir),
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
catch { /* intentionally empty */ }
|
|
928
|
+
return results;
|
|
929
|
+
}
|
|
930
|
+
// ─── Roadmap milestone scoping ───────────────────────────────────────────────
|
|
931
|
+
/**
|
|
932
|
+
* Strip shipped milestone content wrapped in <details> blocks.
|
|
933
|
+
*/
|
|
934
|
+
function stripShippedMilestones(content) {
|
|
935
|
+
return content.replace(/<details>[\s\S]*?<\/details>/gi, '');
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Extract the current milestone section from ROADMAP.md by positive lookup.
|
|
939
|
+
*/
|
|
940
|
+
function extractCurrentMilestone(content, cwd) {
|
|
941
|
+
if (!cwd)
|
|
942
|
+
return stripShippedMilestones(content);
|
|
943
|
+
let version = null;
|
|
944
|
+
try {
|
|
945
|
+
const statePath = node_path_1.default.join(planningDir(cwd), 'STATE.md');
|
|
946
|
+
const stateRaw = (0, shell_command_projection_cjs_1.platformReadSync)(statePath);
|
|
947
|
+
if (stateRaw !== null) {
|
|
948
|
+
const milestoneMatch = stateRaw.match(/^milestone:\s*(.+)/m);
|
|
949
|
+
if (milestoneMatch) {
|
|
950
|
+
version = milestoneMatch[1].trim();
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
catch { /* ignore */ }
|
|
955
|
+
if (!version) {
|
|
956
|
+
const inProgressMatch = content.match(/(?:🚧|🔄)\s*\*\*v(\d+\.\d+)\s/);
|
|
957
|
+
if (inProgressMatch) {
|
|
958
|
+
version = 'v' + inProgressMatch[1];
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
if (!version)
|
|
962
|
+
return stripShippedMilestones(content);
|
|
963
|
+
const escapedVersion = escapeRegex(version);
|
|
964
|
+
const sectionPattern = new RegExp(`(^#{1,3}\\s+(?!Phase\\s+\\S).*${escapedVersion}\\b[^\\n]*)`, 'gmi');
|
|
965
|
+
const summaryPattern = new RegExp(`<summary[^>]*>([^<]*${escapedVersion}[^<]*)<\\/summary>`, 'i');
|
|
966
|
+
const headingMatches = [...content.matchAll(sectionPattern)];
|
|
967
|
+
if (headingMatches.length === 0) {
|
|
968
|
+
const summaryMatch = content.match(summaryPattern);
|
|
969
|
+
if (summaryMatch) {
|
|
970
|
+
const summaryIdx = content.indexOf(summaryMatch[0]);
|
|
971
|
+
const beforeSummary = content.slice(0, summaryIdx);
|
|
972
|
+
const detailsOpenIdx = beforeSummary.lastIndexOf('<details');
|
|
973
|
+
if (detailsOpenIdx !== -1) {
|
|
974
|
+
const afterDetails = content.slice(detailsOpenIdx);
|
|
975
|
+
const closingMatch = afterDetails.match(/<\/details>/i);
|
|
976
|
+
const detailsEnd = closingMatch
|
|
977
|
+
? detailsOpenIdx + (closingMatch.index ?? 0) + '</details>'.length
|
|
978
|
+
: content.length;
|
|
979
|
+
const anyMilestoneOrDetails = /^#{1,3}\s+(?!Phase\s+\S)(?:.*v\d+\.\d+|✅|📋|🚧|🔄)|<details/im;
|
|
980
|
+
const firstMilestoneMatch = content.match(anyMilestoneOrDetails);
|
|
981
|
+
const preambleCutoff = firstMilestoneMatch ? firstMilestoneMatch.index : detailsOpenIdx;
|
|
982
|
+
const preamble = content.slice(0, preambleCutoff)
|
|
983
|
+
.replace(/<details>[\s\S]*?<\/details>/gi, '')
|
|
984
|
+
.replace(/^#{2,4}\s*Phase\s+[\w][\w.-]*\s*:[^\n]*(?:\n(?!#{1,6}\s)[^\n]*)*\n?/gim, '')
|
|
985
|
+
.replace(/^#{1,4}\s*Phase Details\b[^\n]*\n?/gim, '');
|
|
986
|
+
return preamble + content.slice(detailsOpenIdx, detailsEnd);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
return stripShippedMilestones(content);
|
|
990
|
+
}
|
|
991
|
+
const allMatches = headingMatches;
|
|
992
|
+
const closedMarkerPattern = /\b(?:CLOSED|ARCHIVED|ABANDONED|SHIPPED|FAILED)\b|✅|🗄/i;
|
|
993
|
+
const activeMarkerPattern = /\b(?:STARTED|ACTIVE|WIP)\b|in\s+progress|🚧|🔄/i;
|
|
994
|
+
const isClosed = (h) => closedMarkerPattern.test(h) && !activeMarkerPattern.test(h);
|
|
995
|
+
const firstMatch = allMatches[0];
|
|
996
|
+
const selected = allMatches.find((m) => !isClosed(m[1])) || firstMatch;
|
|
997
|
+
const sectionStart = selected.index;
|
|
998
|
+
const computeSectionEnd = (headingText, headingStart) => {
|
|
999
|
+
const level = (headingText.match(/^(#{1,3})\s/) ?? ['', '#'])[1].length;
|
|
1000
|
+
const rest = content.slice(headingStart + headingText.length);
|
|
1001
|
+
const stopPattern = new RegExp(`^#{1,${level}}\\s+(?!Phase\\s+\\S)(?:.*v\\d+\\.\\d+|✅|📋|🚧)`, 'i');
|
|
1002
|
+
let end = content.length;
|
|
1003
|
+
let fc = null;
|
|
1004
|
+
let fl = 0;
|
|
1005
|
+
let off = 0;
|
|
1006
|
+
for (const line of rest.split('\n')) {
|
|
1007
|
+
const fm = line.match(/^\s{0,3}((?:`{3,}|~{3,}))(.*)/);
|
|
1008
|
+
if (fm) {
|
|
1009
|
+
const ch = fm[1][0];
|
|
1010
|
+
const ln = fm[1].length;
|
|
1011
|
+
const trailing = fm[2] || '';
|
|
1012
|
+
if (!fc) {
|
|
1013
|
+
fc = ch;
|
|
1014
|
+
fl = ln;
|
|
1015
|
+
}
|
|
1016
|
+
else if (ch === fc && ln >= fl && /^\s*$/.test(trailing)) {
|
|
1017
|
+
fc = null;
|
|
1018
|
+
fl = 0;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
else if (!fc && stopPattern.test(line)) {
|
|
1022
|
+
end = headingStart + headingText.length + off;
|
|
1023
|
+
break;
|
|
1024
|
+
}
|
|
1025
|
+
off += line.length + 1;
|
|
1026
|
+
}
|
|
1027
|
+
return end;
|
|
1028
|
+
};
|
|
1029
|
+
const sectionEnd = computeSectionEnd(selected[0], sectionStart);
|
|
1030
|
+
const anyMilestonePattern = /^#{1,3}\s+(?!Phase\s+\S)(?:.*v\d+\.\d+|✅|📋|🚧)/im;
|
|
1031
|
+
const firstMilestoneMatch = content.match(anyMilestonePattern);
|
|
1032
|
+
const preambleCutoff = firstMilestoneMatch
|
|
1033
|
+
? firstMilestoneMatch.index
|
|
1034
|
+
: firstMatch.index;
|
|
1035
|
+
const beforeMilestones = content.slice(0, preambleCutoff);
|
|
1036
|
+
const currentSection = content.slice(sectionStart, sectionEnd);
|
|
1037
|
+
// Multi-milestone roadmaps split each added milestone across two version-bearing
|
|
1038
|
+
// headings: a `## Phases` checklist subsection (early) and a dedicated
|
|
1039
|
+
// `## Milestone … (Phase Details)` section (late) holding the `### Phase N:`
|
|
1040
|
+
// detail headers. The scope window above stops at the next version-bearing
|
|
1041
|
+
// heading — the current milestone's OWN Phase Details heading — leaving those
|
|
1042
|
+
// detail headers outside `currentSection`. Append that section so phase
|
|
1043
|
+
// resolution and counting see the current milestone's phases. Anchor the lookup
|
|
1044
|
+
// to the SELECTED heading's specific version token (boundary-aware, so a
|
|
1045
|
+
// `v3.0` state does not match a `v3.0-A` sub-milestone) so sibling milestones
|
|
1046
|
+
// that share a version prefix do not cross-pollinate. (#730)
|
|
1047
|
+
const selectedVersionToken = selected[1].match(/v\d+(?:\.\d+)+(?:[-.][A-Za-z0-9]+)*/i)?.[0];
|
|
1048
|
+
const detailsVersionBoundary = selectedVersionToken
|
|
1049
|
+
? new RegExp(`${escapeRegex(selectedVersionToken)}(?![\\w.-])`, 'i')
|
|
1050
|
+
: null;
|
|
1051
|
+
let detailsSection = '';
|
|
1052
|
+
const detailsMatch = allMatches.find((m) => /\(Phase\s+Details\)/i.test(m[1]) &&
|
|
1053
|
+
!isClosed(m[1]) &&
|
|
1054
|
+
(!detailsVersionBoundary || detailsVersionBoundary.test(m[1])) &&
|
|
1055
|
+
(m.index ?? 0) >= sectionEnd);
|
|
1056
|
+
if (detailsMatch) {
|
|
1057
|
+
const detailsStart = detailsMatch.index ?? 0;
|
|
1058
|
+
detailsSection = content.slice(detailsStart, computeSectionEnd(detailsMatch[0], detailsStart));
|
|
1059
|
+
}
|
|
1060
|
+
const preamble = beforeMilestones
|
|
1061
|
+
.replace(/<details>[\s\S]*?<\/details>/gi, '')
|
|
1062
|
+
.replace(/^#{2,4}\s*Phase\s+[\w][\w.-]*\s*:[^\n]*(?:\n(?!#{1,6}\s)[^\n]*)*\n?/gim, '')
|
|
1063
|
+
.replace(/^#{1,4}\s*Phase Details\b[^\n]*\n?/gim, '');
|
|
1064
|
+
return detailsSection
|
|
1065
|
+
? preamble + currentSection + '\n' + detailsSection
|
|
1066
|
+
: preamble + currentSection;
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Replace a pattern only in the current milestone section of ROADMAP.md.
|
|
1070
|
+
*/
|
|
1071
|
+
function replaceInCurrentMilestone(content, pattern, replacement) {
|
|
1072
|
+
const lastDetailsClose = content.lastIndexOf('</details>');
|
|
1073
|
+
if (lastDetailsClose === -1) {
|
|
1074
|
+
return content.replace(pattern, replacement);
|
|
1075
|
+
}
|
|
1076
|
+
const offset = lastDetailsClose + '</details>'.length;
|
|
1077
|
+
const before = content.slice(0, offset);
|
|
1078
|
+
const after = content.slice(offset);
|
|
1079
|
+
return before + after.replace(pattern, replacement);
|
|
1080
|
+
}
|
|
1081
|
+
function getRoadmapPhaseInternal(cwd, phaseNum) {
|
|
1082
|
+
if (!phaseNum)
|
|
1083
|
+
return null;
|
|
1084
|
+
const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
1085
|
+
if (!node_fs_1.default.existsSync(roadmapPath))
|
|
1086
|
+
return null;
|
|
1087
|
+
try {
|
|
1088
|
+
const roadmapRaw = (0, shell_command_projection_cjs_1.platformReadSync)(roadmapPath);
|
|
1089
|
+
if (roadmapRaw === null)
|
|
1090
|
+
throw new Error('missing');
|
|
1091
|
+
const content = extractCurrentMilestone(roadmapRaw, cwd);
|
|
1092
|
+
const phasePattern = new RegExp(`#{2,4}\\s*(?:\\[[^\\]]+\\]\\s*)?Phase\\s+${phaseMarkdownRegexSource(phaseNum)}:\\s*([^\\n]+)`, 'i');
|
|
1093
|
+
const headerMatch = content.match(phasePattern);
|
|
1094
|
+
if (!headerMatch)
|
|
1095
|
+
return null;
|
|
1096
|
+
const phaseName = headerMatch[1].trim();
|
|
1097
|
+
const headerIndex = headerMatch.index;
|
|
1098
|
+
const restOfContent = content.slice(headerIndex);
|
|
1099
|
+
const nextHeaderMatch = restOfContent.match(/\n#{2,4}\s+(?:\[[^\]]+\]\s*)?Phase\s+[\w]/i);
|
|
1100
|
+
const sectionEnd = nextHeaderMatch ? headerIndex + nextHeaderMatch.index : content.length;
|
|
1101
|
+
const section = content.slice(headerIndex, sectionEnd).trim();
|
|
1102
|
+
const goalMatch = section.match(/\*\*Goal(?:\*\*:|\*?\*?:\*\*)\s*([^\n]+)/i);
|
|
1103
|
+
const goal = goalMatch ? goalMatch[1].trim() : null;
|
|
1104
|
+
return {
|
|
1105
|
+
found: true,
|
|
1106
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
1107
|
+
phase_number: String(phaseNum),
|
|
1108
|
+
phase_name: phaseName,
|
|
1109
|
+
goal,
|
|
1110
|
+
section,
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
catch {
|
|
1114
|
+
return null;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
// ─── Agent installation validation (#1371) ───────────────────────────────────
|
|
1118
|
+
/**
|
|
1119
|
+
* Resolve the agents directory for the given runtime.
|
|
1120
|
+
*
|
|
1121
|
+
* Priority:
|
|
1122
|
+
* 1. GSD_AGENTS_DIR env var (explicit override, any runtime)
|
|
1123
|
+
* 2. For claude runtime: __dirname-relative path (agents/ sibling of gsd-core/)
|
|
1124
|
+
* This is correct for both repo runs and real installs (the runtime config dir's
|
|
1125
|
+
* agents/ folder) because gsd-tools.cjs lives inside gsd-core/bin/ in both cases.
|
|
1126
|
+
* 3. For non-claude runtimes: getGlobalConfigDir(runtime)/agents
|
|
1127
|
+
*
|
|
1128
|
+
* @param runtime - the active runtime name; defaults to GSD_RUNTIME env, then 'claude'
|
|
1129
|
+
*/
|
|
1130
|
+
function getAgentsDir(runtime) {
|
|
1131
|
+
if (process.env['GSD_AGENTS_DIR']) {
|
|
1132
|
+
return process.env['GSD_AGENTS_DIR'];
|
|
1133
|
+
}
|
|
1134
|
+
const resolved = runtime ?? (process.env['GSD_RUNTIME'] || 'claude');
|
|
1135
|
+
if (resolved === 'claude') {
|
|
1136
|
+
return node_path_1.default.join(__dirname, '..', '..', '..', 'agents');
|
|
1137
|
+
}
|
|
1138
|
+
return node_path_1.default.join((0, runtime_homes_cjs_1.getGlobalConfigDir)(resolved), 'agents');
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Check which GSD agents are installed on disk.
|
|
1142
|
+
*
|
|
1143
|
+
* @param runtime - the active runtime name; defaults to GSD_RUNTIME env, then 'claude'
|
|
1144
|
+
*/
|
|
1145
|
+
function checkAgentsInstalled(runtime) {
|
|
1146
|
+
const resolvedRuntime = runtime ?? (process.env['GSD_RUNTIME'] || 'claude');
|
|
1147
|
+
const agentsDir = getAgentsDir(resolvedRuntime);
|
|
1148
|
+
const expectedAgents = Object.keys(MODEL_PROFILES);
|
|
1149
|
+
const installed = [];
|
|
1150
|
+
const missing = [];
|
|
1151
|
+
if (!node_fs_1.default.existsSync(agentsDir)) {
|
|
1152
|
+
return {
|
|
1153
|
+
agents_installed: false,
|
|
1154
|
+
missing_agents: expectedAgents,
|
|
1155
|
+
installed_agents: [],
|
|
1156
|
+
agents_dir: agentsDir,
|
|
1157
|
+
agent_runtime: resolvedRuntime,
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
for (const agent of expectedAgents) {
|
|
1161
|
+
const agentFile = node_path_1.default.join(agentsDir, `${agent}.md`);
|
|
1162
|
+
const agentFileCopilot = node_path_1.default.join(agentsDir, `${agent}.agent.md`);
|
|
1163
|
+
const agentFileCodex = node_path_1.default.join(agentsDir, `${agent}.toml`);
|
|
1164
|
+
if (node_fs_1.default.existsSync(agentFile) || node_fs_1.default.existsSync(agentFileCopilot) || node_fs_1.default.existsSync(agentFileCodex)) {
|
|
1165
|
+
installed.push(agent);
|
|
1166
|
+
}
|
|
1167
|
+
else {
|
|
1168
|
+
missing.push(agent);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
return {
|
|
1172
|
+
agents_installed: installed.length > 0 && missing.length === 0,
|
|
1173
|
+
missing_agents: missing,
|
|
1174
|
+
installed_agents: installed,
|
|
1175
|
+
agents_dir: agentsDir,
|
|
1176
|
+
agent_runtime: resolvedRuntime,
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
// ─── Model alias resolution ───────────────────────────────────────────────────
|
|
1180
|
+
const RUNTIME_OVERRIDE_TIERS = new Set(['opus', 'sonnet', 'haiku']);
|
|
1181
|
+
const _warnedConfigKeys = new Set();
|
|
1182
|
+
function _warnUnknownProfileOverrides(parsed, configLabel) {
|
|
1183
|
+
if (!parsed || typeof parsed !== 'object')
|
|
1184
|
+
return;
|
|
1185
|
+
const runtime = parsed['runtime'];
|
|
1186
|
+
if (runtime && typeof runtime === 'string' && !(model_catalog_cjs_1.KNOWN_RUNTIMES).has(runtime)) {
|
|
1187
|
+
const key = `${configLabel}::runtime::${runtime}`;
|
|
1188
|
+
if (!_warnedConfigKeys.has(key)) {
|
|
1189
|
+
_warnedConfigKeys.add(key);
|
|
1190
|
+
try {
|
|
1191
|
+
process.stderr.write(`gsd: warning — config key "runtime" has unknown value "${runtime}". ` +
|
|
1192
|
+
`Known runtimes: ${[...(model_catalog_cjs_1.KNOWN_RUNTIMES)].sort().join(', ')}. ` +
|
|
1193
|
+
`Resolution will fall back to safe defaults. (#2517)\n`);
|
|
1194
|
+
}
|
|
1195
|
+
catch { /* stderr might be closed in some test harnesses */ }
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
const overrides = parsed['model_profile_overrides'];
|
|
1199
|
+
if (overrides && typeof overrides === 'object' && !Array.isArray(overrides)) {
|
|
1200
|
+
for (const [overrideRuntime, tierMap] of Object.entries(overrides)) {
|
|
1201
|
+
if (!(model_catalog_cjs_1.KNOWN_RUNTIMES).has(overrideRuntime)) {
|
|
1202
|
+
const key = `${configLabel}::override-runtime::${overrideRuntime}`;
|
|
1203
|
+
if (!_warnedConfigKeys.has(key)) {
|
|
1204
|
+
_warnedConfigKeys.add(key);
|
|
1205
|
+
try {
|
|
1206
|
+
process.stderr.write(`gsd: warning — model_profile_overrides.${overrideRuntime}.* uses ` +
|
|
1207
|
+
`unknown runtime "${overrideRuntime}". Known runtimes: ` +
|
|
1208
|
+
`${[...(model_catalog_cjs_1.KNOWN_RUNTIMES)].sort().join(', ')}. (#2517)\n`);
|
|
1209
|
+
}
|
|
1210
|
+
catch { /* ok */ }
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
if (!tierMap || typeof tierMap !== 'object')
|
|
1214
|
+
continue;
|
|
1215
|
+
for (const tierName of Object.keys(tierMap)) {
|
|
1216
|
+
if (!RUNTIME_OVERRIDE_TIERS.has(tierName)) {
|
|
1217
|
+
const key = `${configLabel}::override-tier::${overrideRuntime}.${tierName}`;
|
|
1218
|
+
if (!_warnedConfigKeys.has(key)) {
|
|
1219
|
+
_warnedConfigKeys.add(key);
|
|
1220
|
+
try {
|
|
1221
|
+
process.stderr.write(`gsd: warning — model_profile_overrides.${overrideRuntime}.${tierName} ` +
|
|
1222
|
+
`uses unknown tier "${tierName}". Allowed tiers: opus, sonnet, haiku. (#2517)\n`);
|
|
1223
|
+
}
|
|
1224
|
+
catch { /* ok */ }
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
const policy = parsed['model_policy'];
|
|
1231
|
+
if (policy && typeof policy === 'object' && !Array.isArray(policy)) {
|
|
1232
|
+
const policyObj = policy;
|
|
1233
|
+
const provider = policyObj['provider'];
|
|
1234
|
+
const _POLICY_SENTINEL_PROVIDERS = new Set(['generic', 'custom']);
|
|
1235
|
+
if (provider && typeof provider === 'string' &&
|
|
1236
|
+
!(model_catalog_cjs_1.KNOWN_PROVIDERS).has(provider) && !_POLICY_SENTINEL_PROVIDERS.has(provider)) {
|
|
1237
|
+
const pkey = `${configLabel}::model_policy::provider::${provider}`;
|
|
1238
|
+
if (!_warnedConfigKeys.has(pkey)) {
|
|
1239
|
+
_warnedConfigKeys.add(pkey);
|
|
1240
|
+
try {
|
|
1241
|
+
process.stderr.write(`gsd: warning — model_policy.provider has unknown value "${provider}". ` +
|
|
1242
|
+
`Known providers: ${[...(model_catalog_cjs_1.KNOWN_PROVIDERS)].sort().join(', ')}. ` +
|
|
1243
|
+
`For manual model IDs use provider="custom". (#49)\n`);
|
|
1244
|
+
}
|
|
1245
|
+
catch { /* ok */ }
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
const rtOverrides = policyObj['runtime_tiers'];
|
|
1249
|
+
if (rtOverrides && typeof rtOverrides === 'object' && !Array.isArray(rtOverrides)) {
|
|
1250
|
+
for (const [pruntime, tierMap] of Object.entries(rtOverrides)) {
|
|
1251
|
+
if (!(model_catalog_cjs_1.KNOWN_RUNTIMES).has(pruntime)) {
|
|
1252
|
+
const key = `${configLabel}::model_policy.runtime_tiers::${pruntime}`;
|
|
1253
|
+
if (!_warnedConfigKeys.has(key)) {
|
|
1254
|
+
_warnedConfigKeys.add(key);
|
|
1255
|
+
try {
|
|
1256
|
+
process.stderr.write(`gsd: warning — model_policy.runtime_tiers.${pruntime}.* uses ` +
|
|
1257
|
+
`unknown runtime "${pruntime}". Known runtimes: ` +
|
|
1258
|
+
`${[...(model_catalog_cjs_1.KNOWN_RUNTIMES)].sort().join(', ')}. (#49)\n`);
|
|
1259
|
+
}
|
|
1260
|
+
catch { /* ok */ }
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
if (!tierMap || typeof tierMap !== 'object')
|
|
1264
|
+
continue;
|
|
1265
|
+
for (const tierName of Object.keys(tierMap)) {
|
|
1266
|
+
if (!RUNTIME_OVERRIDE_TIERS.has(tierName)) {
|
|
1267
|
+
const key = `${configLabel}::model_policy.runtime_tiers::${pruntime}.${tierName}`;
|
|
1268
|
+
if (!_warnedConfigKeys.has(key)) {
|
|
1269
|
+
_warnedConfigKeys.add(key);
|
|
1270
|
+
try {
|
|
1271
|
+
process.stderr.write(`gsd: warning — model_policy.runtime_tiers.${pruntime}.${tierName} ` +
|
|
1272
|
+
`uses unknown tier "${tierName}". Allowed: opus, sonnet, haiku. (#49)\n`);
|
|
1273
|
+
}
|
|
1274
|
+
catch { /* ok */ }
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
// Internal helper exposed for tests so per-process warning state can be reset
|
|
1283
|
+
// between cases that intentionally exercise the warning path repeatedly.
|
|
1284
|
+
function _resetRuntimeWarningCacheForTests() {
|
|
1285
|
+
_warnedConfigKeys.clear();
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* #2517 — Resolve the runtime-aware tier entry for (runtime, tier).
|
|
1289
|
+
*/
|
|
1290
|
+
function resolveTierEntry({ runtime, tier, overrides }) {
|
|
1291
|
+
if (!runtime || !tier)
|
|
1292
|
+
return null;
|
|
1293
|
+
const runtimeMap = model_catalog_cjs_1.RUNTIME_PROFILE_MAP;
|
|
1294
|
+
const builtin = runtimeMap[runtime]?.[tier] || null;
|
|
1295
|
+
const overridesMap = overrides;
|
|
1296
|
+
const userRaw = overridesMap?.[runtime]?.[tier];
|
|
1297
|
+
let userEntry = null;
|
|
1298
|
+
if (userRaw) {
|
|
1299
|
+
userEntry = typeof userRaw === 'string' ? { model: userRaw } : userRaw;
|
|
1300
|
+
}
|
|
1301
|
+
if (!builtin && !userEntry)
|
|
1302
|
+
return null;
|
|
1303
|
+
return { ...(builtin || {}), ...(userEntry || {}) };
|
|
1304
|
+
}
|
|
1305
|
+
/**
|
|
1306
|
+
* Convenience wrapper used by resolveModelInternal.
|
|
1307
|
+
*/
|
|
1308
|
+
function _resolveRuntimeTier(config, tier) {
|
|
1309
|
+
return resolveTierEntry({
|
|
1310
|
+
runtime: config['runtime'],
|
|
1311
|
+
tier,
|
|
1312
|
+
overrides: config['model_profile_overrides'],
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* #49 — Provider-neutral model policy preset resolution.
|
|
1317
|
+
*/
|
|
1318
|
+
function resolveModelPolicy(policy, tier) {
|
|
1319
|
+
if (!policy || typeof policy !== 'object')
|
|
1320
|
+
return null;
|
|
1321
|
+
if (!tier)
|
|
1322
|
+
return null;
|
|
1323
|
+
const runtime = policy['runtime'];
|
|
1324
|
+
const rtOverrides = policy['runtime_tiers'];
|
|
1325
|
+
if (runtime && typeof runtime === 'string' && rtOverrides && typeof rtOverrides === 'object') {
|
|
1326
|
+
const rtOverridesMap = rtOverrides;
|
|
1327
|
+
if (Object.hasOwn(rtOverridesMap, runtime)) {
|
|
1328
|
+
const runtimeEntry = rtOverridesMap[runtime];
|
|
1329
|
+
if (runtimeEntry && typeof runtimeEntry === 'object' && Object.hasOwn(runtimeEntry, tier)) {
|
|
1330
|
+
const raw = runtimeEntry[tier];
|
|
1331
|
+
if (raw != null) {
|
|
1332
|
+
const entry = typeof raw === 'string' ? { model: raw } : raw;
|
|
1333
|
+
if (entry && entry['model'])
|
|
1334
|
+
return entry['model'];
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
const provider = policy['provider'];
|
|
1340
|
+
if (!provider || typeof provider !== 'string')
|
|
1341
|
+
return null;
|
|
1342
|
+
if (provider === 'generic' || provider === 'custom') {
|
|
1343
|
+
const TIER_TO_POLICY_KEY = { opus: 'high', sonnet: 'medium', haiku: 'low' };
|
|
1344
|
+
const policyKey = TIER_TO_POLICY_KEY[tier];
|
|
1345
|
+
if (!policyKey)
|
|
1346
|
+
return null;
|
|
1347
|
+
const v = policy[policyKey];
|
|
1348
|
+
return (v && typeof v === 'string') ? v : null;
|
|
1349
|
+
}
|
|
1350
|
+
const presetsMap = model_catalog_cjs_1.PROVIDER_PRESETS;
|
|
1351
|
+
if (!Object.hasOwn(presetsMap, provider))
|
|
1352
|
+
return null;
|
|
1353
|
+
const presetForProvider = presetsMap[provider];
|
|
1354
|
+
if (!presetForProvider || typeof presetForProvider !== 'object')
|
|
1355
|
+
return null;
|
|
1356
|
+
if (!Object.hasOwn(presetForProvider, tier))
|
|
1357
|
+
return null;
|
|
1358
|
+
const tierPresets = presetForProvider[tier];
|
|
1359
|
+
if (!tierPresets || typeof tierPresets !== 'object')
|
|
1360
|
+
return null;
|
|
1361
|
+
const budget = (policy['budget'] && typeof policy['budget'] === 'string') ? policy['budget'] : 'medium';
|
|
1362
|
+
if (!Object.hasOwn(tierPresets, budget))
|
|
1363
|
+
return null;
|
|
1364
|
+
const budgetEntry = tierPresets[budget];
|
|
1365
|
+
if (!budgetEntry || !budgetEntry.model)
|
|
1366
|
+
return null;
|
|
1367
|
+
return budgetEntry.model;
|
|
1368
|
+
}
|
|
1369
|
+
function resolveModelInternal(cwd, agentType) {
|
|
1370
|
+
const config = loadConfig(cwd);
|
|
1371
|
+
// 1. Per-agent override
|
|
1372
|
+
const modelOverrides = config['model_overrides'];
|
|
1373
|
+
const override = modelOverrides?.[agentType];
|
|
1374
|
+
if (override) {
|
|
1375
|
+
return override;
|
|
1376
|
+
}
|
|
1377
|
+
// 2. Compute the tier
|
|
1378
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
1379
|
+
const profile = String(config['model_profile'] || 'balanced').toLowerCase();
|
|
1380
|
+
const agentModels = MODEL_PROFILES[agentType];
|
|
1381
|
+
const phaseType = (AGENT_TO_PHASE_TYPE)[agentType];
|
|
1382
|
+
const configModels = config['models'];
|
|
1383
|
+
const phaseTypeTier = (phaseType && configModels && typeof configModels === 'object')
|
|
1384
|
+
? configModels[phaseType]
|
|
1385
|
+
: undefined;
|
|
1386
|
+
const VALID_TIERS = new Set(['opus', 'sonnet', 'haiku', 'inherit']);
|
|
1387
|
+
const tier = (phaseTypeTier && VALID_TIERS.has(phaseTypeTier))
|
|
1388
|
+
? phaseTypeTier
|
|
1389
|
+
: (profile === 'inherit'
|
|
1390
|
+
? 'inherit'
|
|
1391
|
+
: (agentModels ? (agentModels[profile] || agentModels['balanced']) : null));
|
|
1392
|
+
// 2.5. model_policy preset (#49)
|
|
1393
|
+
const configRuntime = config['runtime'];
|
|
1394
|
+
if (configRuntime && configRuntime !== 'claude' && tier && tier !== 'inherit') {
|
|
1395
|
+
const mergedPolicy = config['model_policy']
|
|
1396
|
+
? { ...config['model_policy'], runtime: configRuntime }
|
|
1397
|
+
: null;
|
|
1398
|
+
const policyModel = resolveModelPolicy(mergedPolicy, tier);
|
|
1399
|
+
if (policyModel)
|
|
1400
|
+
return policyModel;
|
|
1401
|
+
}
|
|
1402
|
+
// 3. Runtime-aware resolution (#2517)
|
|
1403
|
+
if (configRuntime && configRuntime !== 'claude' && tier && tier !== 'inherit') {
|
|
1404
|
+
const entry = _resolveRuntimeTier(config, tier);
|
|
1405
|
+
if (entry?.model)
|
|
1406
|
+
return entry.model;
|
|
1407
|
+
}
|
|
1408
|
+
// 4. resolve_model_ids: "omit"
|
|
1409
|
+
if (config['resolve_model_ids'] === 'omit') {
|
|
1410
|
+
return '';
|
|
1411
|
+
}
|
|
1412
|
+
// 5. Profile lookup (Claude-native default).
|
|
1413
|
+
if (!agentModels) {
|
|
1414
|
+
return profile === 'quality' ? 'opus'
|
|
1415
|
+
: profile === 'budget' ? 'haiku'
|
|
1416
|
+
: profile === 'inherit' ? 'inherit'
|
|
1417
|
+
: 'sonnet';
|
|
1418
|
+
}
|
|
1419
|
+
if (tier === 'inherit')
|
|
1420
|
+
return 'inherit';
|
|
1421
|
+
const alias = tier;
|
|
1422
|
+
if (config['resolve_model_ids']) {
|
|
1423
|
+
return model_catalog_cjs_1.MODEL_ALIAS_MAP[alias] || alias;
|
|
1424
|
+
}
|
|
1425
|
+
return alias;
|
|
1426
|
+
}
|
|
1427
|
+
const VALID_GRANULARITIES = new Set(['coarse', 'standard', 'fine']);
|
|
1428
|
+
/**
|
|
1429
|
+
* Resolve the planning granularity for a phase type (#68).
|
|
1430
|
+
*/
|
|
1431
|
+
function resolveGranularityInternal(cwd, phaseType, override) {
|
|
1432
|
+
if (override !== undefined && override !== null && override !== '') {
|
|
1433
|
+
if (VALID_GRANULARITIES.has(override)) {
|
|
1434
|
+
return override;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
const config = loadConfig(cwd);
|
|
1438
|
+
const configGranularities = config['granularities'];
|
|
1439
|
+
const perPhase = (phaseType && configGranularities && typeof configGranularities === 'object')
|
|
1440
|
+
? configGranularities[phaseType]
|
|
1441
|
+
: undefined;
|
|
1442
|
+
if (perPhase && VALID_GRANULARITIES.has(perPhase)) {
|
|
1443
|
+
return perPhase;
|
|
1444
|
+
}
|
|
1445
|
+
if (config['granularity'] !== undefined && config['granularity'] !== null && config['granularity'] !== '') {
|
|
1446
|
+
return config['granularity'];
|
|
1447
|
+
}
|
|
1448
|
+
const planning = config['planning'];
|
|
1449
|
+
const planningGran = planning && planning['granularity'];
|
|
1450
|
+
if (planningGran !== undefined && planningGran !== null && planningGran !== '') {
|
|
1451
|
+
return planningGran;
|
|
1452
|
+
}
|
|
1453
|
+
return 'standard';
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Validate a CLI granularity override at the command boundary. Empty/null/undefined
|
|
1457
|
+
* are treated as "no override" (no-op). An invalid non-empty value calls `fail`.
|
|
1458
|
+
*/
|
|
1459
|
+
function assertValidGranularityOverride(override, fail) {
|
|
1460
|
+
if (override !== undefined && override !== null && override !== '' && !VALID_GRANULARITIES.has(override)) {
|
|
1461
|
+
fail(`invalid granularity '${override}' (valid: ${[...VALID_GRANULARITIES].join(', ')})`);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* #3024 — Resolve a model for a specific dynamic-routing attempt.
|
|
1466
|
+
*/
|
|
1467
|
+
function resolveModelForTier(cwd, agentType, attempt) {
|
|
1468
|
+
const config = loadConfig(cwd);
|
|
1469
|
+
const attemptN = Number.isInteger(attempt) && attempt > 0 ? attempt : 0;
|
|
1470
|
+
const modelOverrides = config['model_overrides'];
|
|
1471
|
+
const override = modelOverrides?.[agentType];
|
|
1472
|
+
if (override)
|
|
1473
|
+
return override;
|
|
1474
|
+
if (config['model_policy'] && config['runtime'] && config['runtime'] !== 'claude') {
|
|
1475
|
+
return resolveModelInternal(cwd, agentType);
|
|
1476
|
+
}
|
|
1477
|
+
const dr = config['dynamic_routing'];
|
|
1478
|
+
if (!dr || typeof dr !== 'object' || dr['enabled'] !== true) {
|
|
1479
|
+
return resolveModelInternal(cwd, agentType);
|
|
1480
|
+
}
|
|
1481
|
+
const tierModels = dr['tier_models'];
|
|
1482
|
+
if (!tierModels || typeof tierModels !== 'object') {
|
|
1483
|
+
return resolveModelInternal(cwd, agentType);
|
|
1484
|
+
}
|
|
1485
|
+
const defaultTier = (AGENT_DEFAULT_TIERS)[agentType];
|
|
1486
|
+
if (!defaultTier || !(VALID_AGENT_TIERS).has(defaultTier)) {
|
|
1487
|
+
return resolveModelInternal(cwd, agentType);
|
|
1488
|
+
}
|
|
1489
|
+
const maxEscalations = Number.isInteger(dr['max_escalations']) && dr['max_escalations'] >= 0
|
|
1490
|
+
? dr['max_escalations']
|
|
1491
|
+
: 1;
|
|
1492
|
+
const escalationEnabled = dr['escalate_on_failure'] !== false;
|
|
1493
|
+
const effectiveAttempt = escalationEnabled
|
|
1494
|
+
? Math.min(attemptN, maxEscalations)
|
|
1495
|
+
: 0;
|
|
1496
|
+
let tier = defaultTier;
|
|
1497
|
+
for (let i = 0; i < effectiveAttempt; i += 1) {
|
|
1498
|
+
const next = (nextTier)(tier);
|
|
1499
|
+
if (!next || next === tier)
|
|
1500
|
+
break;
|
|
1501
|
+
tier = next;
|
|
1502
|
+
}
|
|
1503
|
+
const alias = tierModels[tier];
|
|
1504
|
+
if (typeof alias !== 'string' || alias.length === 0) {
|
|
1505
|
+
return resolveModelInternal(cwd, agentType);
|
|
1506
|
+
}
|
|
1507
|
+
return alias;
|
|
1508
|
+
}
|
|
1509
|
+
// ─── #443 — Unified effort + fast_mode resolvers ─────────────────────────────
|
|
1510
|
+
const VALID_EFFORTS = ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
1511
|
+
const EFFORT_SET = new Set(VALID_EFFORTS);
|
|
1512
|
+
/**
|
|
1513
|
+
* Walk one step up the effort ladder from `e`.
|
|
1514
|
+
*/
|
|
1515
|
+
function nextEffort(e) {
|
|
1516
|
+
const i = VALID_EFFORTS.indexOf(e);
|
|
1517
|
+
if (i < 0)
|
|
1518
|
+
return null;
|
|
1519
|
+
return VALID_EFFORTS[Math.min(i + 1, VALID_EFFORTS.length - 1)];
|
|
1520
|
+
}
|
|
1521
|
+
/**
|
|
1522
|
+
* #443 — Resolve a universal effort string for (cwd, agentType).
|
|
1523
|
+
*/
|
|
1524
|
+
function resolveEffortInternal(cwd, agentType, opts) {
|
|
1525
|
+
// Step 1: invocation override
|
|
1526
|
+
if (opts && typeof opts.override === 'string' && EFFORT_SET.has(opts.override)) {
|
|
1527
|
+
return opts.override;
|
|
1528
|
+
}
|
|
1529
|
+
const config = loadConfig(cwd);
|
|
1530
|
+
const effortCfg = (config['effort'] && typeof config['effort'] === 'object' && !Array.isArray(config['effort']))
|
|
1531
|
+
? config['effort']
|
|
1532
|
+
: null;
|
|
1533
|
+
// Step 2: agent_overrides
|
|
1534
|
+
if (effortCfg) {
|
|
1535
|
+
const ao = effortCfg['agent_overrides'];
|
|
1536
|
+
if (ao && typeof ao === 'object' && !Array.isArray(ao)) {
|
|
1537
|
+
const v = ao[agentType];
|
|
1538
|
+
if (typeof v === 'string' && EFFORT_SET.has(v))
|
|
1539
|
+
return v;
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
else {
|
|
1543
|
+
const canonicalEffort = (configuration_cjs_1.CONFIG_DEFAULTS)['effort'];
|
|
1544
|
+
const mao = canonicalEffort && typeof canonicalEffort === 'object'
|
|
1545
|
+
? canonicalEffort['agent_overrides']
|
|
1546
|
+
: undefined;
|
|
1547
|
+
if (mao && typeof mao === 'object' && !Array.isArray(mao)) {
|
|
1548
|
+
const v = mao[agentType];
|
|
1549
|
+
if (typeof v === 'string' && EFFORT_SET.has(v))
|
|
1550
|
+
return v;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
// Step 3: routing_tier_defaults by agent's default tier.
|
|
1554
|
+
const agentTier = (AGENT_DEFAULT_TIERS)[agentType];
|
|
1555
|
+
if (agentTier) {
|
|
1556
|
+
if (effortCfg && effortCfg['routing_tier_defaults'] &&
|
|
1557
|
+
typeof effortCfg['routing_tier_defaults'] === 'object' &&
|
|
1558
|
+
!Array.isArray(effortCfg['routing_tier_defaults'])) {
|
|
1559
|
+
const v = effortCfg['routing_tier_defaults'][agentTier];
|
|
1560
|
+
if (typeof v === 'string' && EFFORT_SET.has(v))
|
|
1561
|
+
return v;
|
|
1562
|
+
}
|
|
1563
|
+
else if (!effortCfg) {
|
|
1564
|
+
const canonicalEffort = (configuration_cjs_1.CONFIG_DEFAULTS)['effort'];
|
|
1565
|
+
const manifestDefaults = canonicalEffort && typeof canonicalEffort === 'object'
|
|
1566
|
+
? canonicalEffort['routing_tier_defaults']
|
|
1567
|
+
: undefined;
|
|
1568
|
+
if (manifestDefaults && typeof manifestDefaults === 'object') {
|
|
1569
|
+
const v = manifestDefaults[agentTier];
|
|
1570
|
+
if (typeof v === 'string' && EFFORT_SET.has(v))
|
|
1571
|
+
return v;
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
// Step 4: effort.default
|
|
1576
|
+
if (effortCfg) {
|
|
1577
|
+
const d = effortCfg['default'];
|
|
1578
|
+
if (typeof d === 'string' && EFFORT_SET.has(d))
|
|
1579
|
+
return d;
|
|
1580
|
+
}
|
|
1581
|
+
else {
|
|
1582
|
+
const canonicalEffort = (configuration_cjs_1.CONFIG_DEFAULTS)['effort'];
|
|
1583
|
+
const d = canonicalEffort && typeof canonicalEffort === 'object'
|
|
1584
|
+
? canonicalEffort['default']
|
|
1585
|
+
: undefined;
|
|
1586
|
+
if (typeof d === 'string' && EFFORT_SET.has(d))
|
|
1587
|
+
return d;
|
|
1588
|
+
}
|
|
1589
|
+
// Step 5: hardcoded default
|
|
1590
|
+
return 'high';
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* #443 — Resolve fast_mode boolean for (cwd, agentType).
|
|
1594
|
+
*/
|
|
1595
|
+
function resolveFastModeInternal(cwd, agentType, opts) {
|
|
1596
|
+
// Step 1: invocation override
|
|
1597
|
+
if (opts && typeof opts.override === 'boolean') {
|
|
1598
|
+
return opts.override;
|
|
1599
|
+
}
|
|
1600
|
+
const config = loadConfig(cwd);
|
|
1601
|
+
const fmCfg = (config['fast_mode'] && typeof config['fast_mode'] === 'object' && !Array.isArray(config['fast_mode']))
|
|
1602
|
+
? config['fast_mode']
|
|
1603
|
+
: null;
|
|
1604
|
+
// Step 2: agent_overrides
|
|
1605
|
+
if (fmCfg) {
|
|
1606
|
+
const ao = fmCfg['agent_overrides'];
|
|
1607
|
+
if (ao && typeof ao === 'object' && !Array.isArray(ao)) {
|
|
1608
|
+
const v = ao[agentType];
|
|
1609
|
+
if (typeof v === 'boolean')
|
|
1610
|
+
return v;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
// Step 3: routing_tier_defaults by agent's default tier.
|
|
1614
|
+
const agentTier = (AGENT_DEFAULT_TIERS)[agentType];
|
|
1615
|
+
if (agentTier) {
|
|
1616
|
+
if (fmCfg && fmCfg['routing_tier_defaults'] &&
|
|
1617
|
+
typeof fmCfg['routing_tier_defaults'] === 'object' &&
|
|
1618
|
+
!Array.isArray(fmCfg['routing_tier_defaults'])) {
|
|
1619
|
+
const v = fmCfg['routing_tier_defaults'][agentTier];
|
|
1620
|
+
if (typeof v === 'boolean')
|
|
1621
|
+
return v;
|
|
1622
|
+
}
|
|
1623
|
+
else if (!fmCfg) {
|
|
1624
|
+
const canonicalFm = (configuration_cjs_1.CONFIG_DEFAULTS)['fast_mode'];
|
|
1625
|
+
const manifestDefaults = canonicalFm && typeof canonicalFm === 'object'
|
|
1626
|
+
? canonicalFm['routing_tier_defaults']
|
|
1627
|
+
: undefined;
|
|
1628
|
+
if (manifestDefaults && typeof manifestDefaults === 'object') {
|
|
1629
|
+
const v = manifestDefaults[agentTier];
|
|
1630
|
+
if (typeof v === 'boolean')
|
|
1631
|
+
return v;
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
// Step 4: fast_mode.enabled
|
|
1636
|
+
if (fmCfg && typeof fmCfg['enabled'] === 'boolean') {
|
|
1637
|
+
return fmCfg['enabled'];
|
|
1638
|
+
}
|
|
1639
|
+
// Step 5: hardcoded default
|
|
1640
|
+
return false;
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* #443 — Resolve effort for a dynamic-routing attempt (with escalation).
|
|
1644
|
+
*/
|
|
1645
|
+
function resolveEffortForTier(cwd, agentType, attempt) {
|
|
1646
|
+
const base = resolveEffortInternal(cwd, agentType);
|
|
1647
|
+
const config = loadConfig(cwd);
|
|
1648
|
+
const dr = config['dynamic_routing'];
|
|
1649
|
+
if (!dr || typeof dr !== 'object' || dr['enabled'] !== true) {
|
|
1650
|
+
return base;
|
|
1651
|
+
}
|
|
1652
|
+
if (dr['escalate_on_failure'] === false) {
|
|
1653
|
+
return base;
|
|
1654
|
+
}
|
|
1655
|
+
const maxEscalations = Number.isInteger(dr['max_escalations']) && dr['max_escalations'] >= 0
|
|
1656
|
+
? dr['max_escalations']
|
|
1657
|
+
: 1;
|
|
1658
|
+
const attemptN = Number.isInteger(attempt) && attempt > 0 ? attempt : 0;
|
|
1659
|
+
const effectiveAttempt = Math.min(attemptN, maxEscalations);
|
|
1660
|
+
let current = base;
|
|
1661
|
+
for (let i = 0; i < effectiveAttempt; i++) {
|
|
1662
|
+
const next = nextEffort(current);
|
|
1663
|
+
if (!next || next === current)
|
|
1664
|
+
break;
|
|
1665
|
+
current = next;
|
|
1666
|
+
}
|
|
1667
|
+
return current;
|
|
1668
|
+
}
|
|
1669
|
+
// ─── Summary body helpers ─────────────────────────────────────────────────
|
|
1670
|
+
/**
|
|
1671
|
+
* Extract a one-liner from the summary body when it's not in frontmatter.
|
|
1672
|
+
*/
|
|
1673
|
+
function extractOneLinerFromBody(content) {
|
|
1674
|
+
if (!content)
|
|
1675
|
+
return null;
|
|
1676
|
+
const normalized = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
1677
|
+
const body = normalized.replace(/^---\n[\s\S]*?\n---\n*/, '');
|
|
1678
|
+
const match = body.match(/^#[^\n]*\n+\*\*([^*\n]+)\*\*([^\n]*)/m);
|
|
1679
|
+
if (!match)
|
|
1680
|
+
return null;
|
|
1681
|
+
const boldInner = match[1].trim();
|
|
1682
|
+
const afterBold = match[2];
|
|
1683
|
+
if (/:\s*$/.test(boldInner)) {
|
|
1684
|
+
const prose = afterBold.trim();
|
|
1685
|
+
return prose.length > 0 ? prose : null;
|
|
1686
|
+
}
|
|
1687
|
+
return boldInner.length > 0 ? boldInner : null;
|
|
1688
|
+
}
|
|
1689
|
+
// ─── Misc utilities ───────────────────────────────────────────────────────────
|
|
1690
|
+
function pathExistsInternal(cwd, targetPath) {
|
|
1691
|
+
const fullPath = node_path_1.default.isAbsolute(targetPath) ? targetPath : node_path_1.default.join(cwd, targetPath);
|
|
1692
|
+
try {
|
|
1693
|
+
node_fs_1.default.statSync(fullPath);
|
|
1694
|
+
return true;
|
|
1695
|
+
}
|
|
1696
|
+
catch {
|
|
1697
|
+
return false;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
/**
|
|
1701
|
+
* Detect whether `cwd` sits inside a git worktree, and if so, return the
|
|
1702
|
+
* absolute path of the worktree root.
|
|
1703
|
+
*/
|
|
1704
|
+
function gitWorktreeInfoInternal(cwd) {
|
|
1705
|
+
try {
|
|
1706
|
+
const insideResult = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '--is-inside-work-tree'], { cwd, timeout: 5000 });
|
|
1707
|
+
if (insideResult.exitCode !== 0) {
|
|
1708
|
+
return { inside: false, worktreeRoot: null };
|
|
1709
|
+
}
|
|
1710
|
+
const insideStdout = String(insideResult.stdout || '').trim();
|
|
1711
|
+
if (insideStdout !== 'true') {
|
|
1712
|
+
return { inside: false, worktreeRoot: null };
|
|
1713
|
+
}
|
|
1714
|
+
const rootResult = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '--show-toplevel'], { cwd, timeout: 5000 });
|
|
1715
|
+
if (rootResult.exitCode !== 0) {
|
|
1716
|
+
return { inside: true, worktreeRoot: null };
|
|
1717
|
+
}
|
|
1718
|
+
const root = String(rootResult.stdout || '').trim();
|
|
1719
|
+
return { inside: true, worktreeRoot: root || null };
|
|
1720
|
+
}
|
|
1721
|
+
catch {
|
|
1722
|
+
return { inside: false, worktreeRoot: null };
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
function generateSlugInternal(text) {
|
|
1726
|
+
if (!text)
|
|
1727
|
+
return null;
|
|
1728
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').substring(0, 60);
|
|
1729
|
+
}
|
|
1730
|
+
function getMilestoneInfo(cwd) {
|
|
1731
|
+
try {
|
|
1732
|
+
const roadmap = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md'));
|
|
1733
|
+
if (roadmap === null)
|
|
1734
|
+
throw new Error('missing');
|
|
1735
|
+
let stateVersion = null;
|
|
1736
|
+
if (cwd) {
|
|
1737
|
+
try {
|
|
1738
|
+
const statePath = node_path_1.default.join(planningDir(cwd), 'STATE.md');
|
|
1739
|
+
const stateRaw = (0, shell_command_projection_cjs_1.platformReadSync)(statePath);
|
|
1740
|
+
if (stateRaw !== null) {
|
|
1741
|
+
const m = stateRaw.match(/^milestone:\s*(.+)/m);
|
|
1742
|
+
if (m)
|
|
1743
|
+
stateVersion = m[1].trim();
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
catch { /* intentionally empty */ }
|
|
1747
|
+
}
|
|
1748
|
+
if (stateVersion) {
|
|
1749
|
+
const escapedVer = escapeRegex(stateVersion);
|
|
1750
|
+
const headingMatch = roadmap.match(new RegExp(`##[^\\n]*${escapedVer}[:\\s]+([^\\n(]+)`, 'i'));
|
|
1751
|
+
if (headingMatch) {
|
|
1752
|
+
if (!headingMatch[0].includes('✅')) {
|
|
1753
|
+
return { version: stateVersion, name: headingMatch[1].trim() };
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
else {
|
|
1757
|
+
const listMatch = roadmap.match(new RegExp(`🚧\\s*\\*?\\*?${escapedVer}\\s+([^*\\n]+)`, 'i'));
|
|
1758
|
+
if (listMatch) {
|
|
1759
|
+
return { version: stateVersion, name: listMatch[1].trim() };
|
|
1760
|
+
}
|
|
1761
|
+
return { version: stateVersion, name: 'milestone' };
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
const inProgressMatch = roadmap.match(/🚧\s*\*\*v(\d+(?:\.\d+)+)\s+([^*]+)\*\*/);
|
|
1765
|
+
if (inProgressMatch) {
|
|
1766
|
+
return {
|
|
1767
|
+
version: 'v' + inProgressMatch[1],
|
|
1768
|
+
name: inProgressMatch[2].trim(),
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
const cleaned = stripShippedMilestones(roadmap);
|
|
1772
|
+
const headingMatch = cleaned.match(/## (?!.*✅).*v(\d+(?:\.\d+)+)[:\s]+([^\n(]+)/);
|
|
1773
|
+
if (headingMatch) {
|
|
1774
|
+
return {
|
|
1775
|
+
version: 'v' + headingMatch[1],
|
|
1776
|
+
name: headingMatch[2].trim(),
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
const versionMatch = cleaned.match(/v(\d+(?:\.\d+)+)/);
|
|
1780
|
+
return {
|
|
1781
|
+
version: versionMatch ? versionMatch[0] : 'v1.0',
|
|
1782
|
+
name: 'milestone',
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
catch {
|
|
1786
|
+
return { version: 'v1.0', name: 'milestone' };
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
/**
|
|
1790
|
+
* Returns a filter function that checks whether a phase directory belongs
|
|
1791
|
+
* to the current milestone based on ROADMAP.md phase headings.
|
|
1792
|
+
*/
|
|
1793
|
+
function getMilestonePhaseFilter(cwd, versionOverride) {
|
|
1794
|
+
const milestonePhaseNums = new Set();
|
|
1795
|
+
let missingExplicitVersion = false;
|
|
1796
|
+
try {
|
|
1797
|
+
const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
1798
|
+
const roadmapContent = (0, shell_command_projection_cjs_1.platformReadSync)(roadmapPath);
|
|
1799
|
+
if (roadmapContent === null)
|
|
1800
|
+
throw new Error('missing');
|
|
1801
|
+
let roadmap = extractCurrentMilestone(roadmapContent, cwd);
|
|
1802
|
+
const hasVersionedMilestonesGlobal = /^#{1,3}\s+.*v\d+\.\d+/mi.test(roadmapContent);
|
|
1803
|
+
const hasPhaseHeadings = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+[\w]/i.test(roadmapContent);
|
|
1804
|
+
if (!hasVersionedMilestonesGlobal && hasPhaseHeadings) {
|
|
1805
|
+
console.warn('[gsd] Deprecated: free-form ROADMAP.md detected (no versioned milestone headings). ' +
|
|
1806
|
+
'Set phase_id_convention in config.json to suppress this warning.');
|
|
1807
|
+
}
|
|
1808
|
+
if (versionOverride) {
|
|
1809
|
+
const escapedVersion = escapeRegex(versionOverride);
|
|
1810
|
+
const sectionPattern = new RegExp(`(^#{1,3}\\s+(?!Phase\\s+\\S).*${escapedVersion}[^\\n]*)`, 'mi');
|
|
1811
|
+
let sectionMatch = roadmapContent.match(sectionPattern);
|
|
1812
|
+
if (!sectionMatch) {
|
|
1813
|
+
const summaryPat = new RegExp(`<summary[^>]*>[^<]*${escapedVersion}[^<]*<\\/summary>`, 'i');
|
|
1814
|
+
const summaryHit = roadmapContent.match(summaryPat);
|
|
1815
|
+
if (summaryHit) {
|
|
1816
|
+
const beforeSummary = roadmapContent.slice(0, summaryHit.index);
|
|
1817
|
+
const detailsIdx = beforeSummary.lastIndexOf('<details');
|
|
1818
|
+
if (detailsIdx !== -1) {
|
|
1819
|
+
sectionMatch = null;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
if (!sectionMatch) {
|
|
1824
|
+
const hasVersionedMilestones = /^#{1,3}\s+(?!Phase\s+\S).*v\d+\.\d+/mi.test(roadmapContent);
|
|
1825
|
+
const versionInSummary = new RegExp(`<summary[^>]*>[^<]*${escapedVersion}[^<]*<\\/summary>`, 'i').test(roadmapContent);
|
|
1826
|
+
if (hasVersionedMilestones && !versionInSummary) {
|
|
1827
|
+
roadmap = '';
|
|
1828
|
+
missingExplicitVersion = true;
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
else {
|
|
1832
|
+
const sectionStart = sectionMatch.index;
|
|
1833
|
+
const headingLevel = (sectionMatch[1].match(/^(#{1,3})\s/) ?? ['', '#'])[1].length;
|
|
1834
|
+
const restContent = roadmapContent.slice(sectionStart + sectionMatch[0].length);
|
|
1835
|
+
const nextMilestonePattern = new RegExp(`^#{1,${headingLevel}}\\s+(?!Phase\\s+\\S)(?:.*v\\d+\\.\\d+|✅|📋|🚧)`, 'i');
|
|
1836
|
+
let sectionEnd = roadmapContent.length;
|
|
1837
|
+
let fenceChar = null;
|
|
1838
|
+
let fenceLen = 0;
|
|
1839
|
+
let charOffset = 0;
|
|
1840
|
+
for (const line of restContent.split('\n')) {
|
|
1841
|
+
const fenceMatch = line.match(/^\s{0,3}((?:`{3,}|~{3,}))(.*)/);
|
|
1842
|
+
if (fenceMatch) {
|
|
1843
|
+
const char = fenceMatch[1][0];
|
|
1844
|
+
const len = fenceMatch[1].length;
|
|
1845
|
+
const trailing = fenceMatch[2] || '';
|
|
1846
|
+
if (!fenceChar) {
|
|
1847
|
+
fenceChar = char;
|
|
1848
|
+
fenceLen = len;
|
|
1849
|
+
}
|
|
1850
|
+
else if (char === fenceChar && len >= fenceLen && /^\s*$/.test(trailing)) {
|
|
1851
|
+
fenceChar = null;
|
|
1852
|
+
fenceLen = 0;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
else if (!fenceChar && nextMilestonePattern.test(line)) {
|
|
1856
|
+
sectionEnd = sectionStart + sectionMatch[0].length + charOffset;
|
|
1857
|
+
break;
|
|
1858
|
+
}
|
|
1859
|
+
charOffset += line.length + 1;
|
|
1860
|
+
}
|
|
1861
|
+
const currentSection = roadmapContent.slice(sectionStart, sectionEnd);
|
|
1862
|
+
roadmap = currentSection;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
const phasePattern = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*)\s*:/gi;
|
|
1866
|
+
let m;
|
|
1867
|
+
while ((m = phasePattern.exec(roadmap)) !== null) {
|
|
1868
|
+
milestonePhaseNums.add(m[1]);
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
catch { /* intentionally empty */ }
|
|
1872
|
+
if (milestonePhaseNums.size === 0) {
|
|
1873
|
+
const passAll = (() => true);
|
|
1874
|
+
passAll.phaseCount = 0;
|
|
1875
|
+
passAll.missingExplicitVersion = missingExplicitVersion;
|
|
1876
|
+
return passAll;
|
|
1877
|
+
}
|
|
1878
|
+
const normalized = new Set([...milestonePhaseNums].map(n => n.split('-').map(seg => (seg.replace(/^0+(?=\d)/, '') || '0')).join('-').toLowerCase()));
|
|
1879
|
+
function normalizePhaseIdSegments(id) {
|
|
1880
|
+
return id.split('-').map(seg => seg.replace(/^0+(?=\d)/, '') || '0').join('-');
|
|
1881
|
+
}
|
|
1882
|
+
const roadmapUsesHyphenedIds = [...normalized].some(n => n.includes('-'));
|
|
1883
|
+
const numericRe = roadmapUsesHyphenedIds
|
|
1884
|
+
? /^0*(\d+(?:-0*\d+)*[A-Za-z]?(?:\.\d+)*)/
|
|
1885
|
+
: /^0*(\d+[A-Za-z]?(?:\.\d+)*)/;
|
|
1886
|
+
function isDirInMilestone(dirName) {
|
|
1887
|
+
const m2 = dirName.match(numericRe);
|
|
1888
|
+
if (m2 && normalized.has(normalizePhaseIdSegments(m2[1]).toLowerCase()))
|
|
1889
|
+
return true;
|
|
1890
|
+
const customMatch = dirName.match(/^([A-Za-z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)*)/);
|
|
1891
|
+
if (customMatch && normalized.has(customMatch[1].toLowerCase()))
|
|
1892
|
+
return true;
|
|
1893
|
+
const stripped = dirName.replace(/^[A-Z]{1,6}-(?=\d)/i, '');
|
|
1894
|
+
if (stripped !== dirName) {
|
|
1895
|
+
const sm = stripped.match(numericRe);
|
|
1896
|
+
if (sm && normalized.has(normalizePhaseIdSegments(sm[1]).toLowerCase()))
|
|
1897
|
+
return true;
|
|
1898
|
+
}
|
|
1899
|
+
return false;
|
|
1900
|
+
}
|
|
1901
|
+
isDirInMilestone.phaseCount = milestonePhaseNums.size;
|
|
1902
|
+
isDirInMilestone.missingExplicitVersion = missingExplicitVersion;
|
|
1903
|
+
return isDirInMilestone;
|
|
1904
|
+
}
|
|
1905
|
+
// ─── Phase file helpers ──────────────────────────────────────────────────────
|
|
1906
|
+
/** Filter a file list to just PLAN.md / *-PLAN.md entries. */
|
|
1907
|
+
function filterPlanFiles(files) {
|
|
1908
|
+
return files.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
|
|
1909
|
+
}
|
|
1910
|
+
/** Filter a file list to just SUMMARY.md / *-SUMMARY.md entries. */
|
|
1911
|
+
function filterSummaryFiles(files) {
|
|
1912
|
+
return files.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
|
1913
|
+
}
|
|
1914
|
+
/**
|
|
1915
|
+
* Read a phase directory and return counts/flags for common file types.
|
|
1916
|
+
*/
|
|
1917
|
+
function getPhaseFileStats(phaseDir) {
|
|
1918
|
+
const files = node_fs_1.default.readdirSync(phaseDir);
|
|
1919
|
+
return {
|
|
1920
|
+
plans: filterPlanFiles(files),
|
|
1921
|
+
summaries: filterSummaryFiles(files),
|
|
1922
|
+
hasResearch: files.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md'),
|
|
1923
|
+
hasContext: findContextMdIn(files) !== null,
|
|
1924
|
+
hasVerification: files.some(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md'),
|
|
1925
|
+
hasReviews: files.some(f => f.endsWith('-REVIEWS.md') || f === 'REVIEWS.md'),
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
/**
|
|
1929
|
+
* Read immediate child directories from a path.
|
|
1930
|
+
* Returns [] if the path doesn't exist or can't be read.
|
|
1931
|
+
* Pass sort=true to apply comparePhaseNum ordering.
|
|
1932
|
+
*/
|
|
1933
|
+
function readSubdirectories(dirPath, sort = false) {
|
|
1934
|
+
try {
|
|
1935
|
+
const entries = node_fs_1.default.readdirSync(dirPath, { withFileTypes: true });
|
|
1936
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
1937
|
+
return sort ? dirs.sort((a, b) => comparePhaseNum(a, b)) : dirs;
|
|
1938
|
+
}
|
|
1939
|
+
catch {
|
|
1940
|
+
return [];
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
/**
|
|
1944
|
+
* Format a Date as a fuzzy relative time string (e.g. "5 minutes ago").
|
|
1945
|
+
*/
|
|
1946
|
+
function timeAgo(date) {
|
|
1947
|
+
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
|
|
1948
|
+
if (seconds < 5)
|
|
1949
|
+
return 'just now';
|
|
1950
|
+
if (seconds < 60)
|
|
1951
|
+
return `${seconds} seconds ago`;
|
|
1952
|
+
const minutes = Math.floor(seconds / 60);
|
|
1953
|
+
if (minutes === 1)
|
|
1954
|
+
return '1 minute ago';
|
|
1955
|
+
if (minutes < 60)
|
|
1956
|
+
return `${minutes} minutes ago`;
|
|
1957
|
+
const hours = Math.floor(minutes / 60);
|
|
1958
|
+
if (hours === 1)
|
|
1959
|
+
return '1 hour ago';
|
|
1960
|
+
if (hours < 24)
|
|
1961
|
+
return `${hours} hours ago`;
|
|
1962
|
+
const days = Math.floor(hours / 24);
|
|
1963
|
+
if (days === 1)
|
|
1964
|
+
return '1 day ago';
|
|
1965
|
+
if (days < 30)
|
|
1966
|
+
return `${days} days ago`;
|
|
1967
|
+
const months = Math.floor(days / 30);
|
|
1968
|
+
if (months === 1)
|
|
1969
|
+
return '1 month ago';
|
|
1970
|
+
if (months < 12)
|
|
1971
|
+
return `${months} months ago`;
|
|
1972
|
+
const years = Math.floor(days / 365);
|
|
1973
|
+
if (years === 1)
|
|
1974
|
+
return '1 year ago';
|
|
1975
|
+
return `${years} years ago`;
|
|
1976
|
+
}
|
|
1977
|
+
module.exports = {
|
|
1978
|
+
output,
|
|
1979
|
+
error,
|
|
1980
|
+
ERROR_REASON,
|
|
1981
|
+
setJsonErrorMode,
|
|
1982
|
+
getJsonErrorMode,
|
|
1983
|
+
loadConfig,
|
|
1984
|
+
isGitIgnored,
|
|
1985
|
+
escapeRegex,
|
|
1986
|
+
normalizePhaseName,
|
|
1987
|
+
getMilestoneFromPhaseId,
|
|
1988
|
+
getPhaseDirFromPhaseId,
|
|
1989
|
+
phaseMarkdownRegexSource,
|
|
1990
|
+
phaseMarkdownRegexSourceExact,
|
|
1991
|
+
comparePhaseNum,
|
|
1992
|
+
searchPhaseInDir,
|
|
1993
|
+
extractPhaseToken,
|
|
1994
|
+
phaseTokenMatches,
|
|
1995
|
+
findPhaseInternal,
|
|
1996
|
+
getArchivedPhaseDirs,
|
|
1997
|
+
getRoadmapPhaseInternal,
|
|
1998
|
+
resolveModelInternal,
|
|
1999
|
+
resolveModelForTier,
|
|
2000
|
+
resolveGranularityInternal,
|
|
2001
|
+
VALID_GRANULARITIES,
|
|
2002
|
+
assertValidGranularityOverride,
|
|
2003
|
+
resolveEffortInternal,
|
|
2004
|
+
resolveFastModeInternal,
|
|
2005
|
+
resolveEffortForTier,
|
|
2006
|
+
VALID_EFFORTS,
|
|
2007
|
+
EFFORT_SET,
|
|
2008
|
+
nextEffort,
|
|
2009
|
+
RUNTIME_PROFILE_MAP: model_catalog_cjs_1.RUNTIME_PROFILE_MAP,
|
|
2010
|
+
RUNTIMES_WITH_REASONING_EFFORT: model_catalog_cjs_1.RUNTIMES_WITH_REASONING_EFFORT,
|
|
2011
|
+
RUNTIMES_WITH_FAST_MODE: model_catalog_cjs_1.RUNTIMES_WITH_FAST_MODE,
|
|
2012
|
+
KNOWN_RUNTIMES: model_catalog_cjs_1.KNOWN_RUNTIMES,
|
|
2013
|
+
RUNTIME_OVERRIDE_TIERS,
|
|
2014
|
+
resolveTierEntry,
|
|
2015
|
+
resolveModelPolicy,
|
|
2016
|
+
KNOWN_PROVIDERS: model_catalog_cjs_1.KNOWN_PROVIDERS,
|
|
2017
|
+
_resetRuntimeWarningCacheForTests,
|
|
2018
|
+
pathExistsInternal,
|
|
2019
|
+
gitWorktreeInfoInternal,
|
|
2020
|
+
generateSlugInternal,
|
|
2021
|
+
getMilestoneInfo,
|
|
2022
|
+
getMilestonePhaseFilter,
|
|
2023
|
+
stripShippedMilestones,
|
|
2024
|
+
extractCurrentMilestone,
|
|
2025
|
+
replaceInCurrentMilestone,
|
|
2026
|
+
toPosixPath,
|
|
2027
|
+
extractOneLinerFromBody,
|
|
2028
|
+
resolveWorktreeRoot,
|
|
2029
|
+
// Deprecated re-exports — prefer direct import from planning-workspace.cjs
|
|
2030
|
+
withPlanningLock,
|
|
2031
|
+
findProjectRoot: project_root_cjs_1.findProjectRoot,
|
|
2032
|
+
detectSubRepos,
|
|
2033
|
+
reapStaleTempFiles,
|
|
2034
|
+
GSD_TEMP_DIR,
|
|
2035
|
+
MODEL_ALIAS_MAP: model_catalog_cjs_1.MODEL_ALIAS_MAP,
|
|
2036
|
+
CONFIG_DEFAULTS,
|
|
2037
|
+
planningDir,
|
|
2038
|
+
planningRoot,
|
|
2039
|
+
planningPaths,
|
|
2040
|
+
getActiveWorkstream,
|
|
2041
|
+
setActiveWorkstream,
|
|
2042
|
+
filterPlanFiles,
|
|
2043
|
+
filterSummaryFiles,
|
|
2044
|
+
getPhaseFileStats,
|
|
2045
|
+
readSubdirectories,
|
|
2046
|
+
getAgentsDir,
|
|
2047
|
+
checkAgentsInstalled,
|
|
2048
|
+
timeAgo,
|
|
2049
|
+
pruneOrphanedWorktrees,
|
|
2050
|
+
inspectWorktreeHealth,
|
|
2051
|
+
};
|