@jokerized/getresearchdone 0.4.1
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 +103 -0
- package/README.md +211 -0
- package/agents/grd-baseline-assessor.md +684 -0
- package/agents/grd-code-reviewer.md +300 -0
- package/agents/grd-codebase-mapper.md +355 -0
- package/agents/grd-critique-agent.md +119 -0
- package/agents/grd-debugger.md +519 -0
- package/agents/grd-deep-diver.md +737 -0
- package/agents/grd-eval-planner.md +913 -0
- package/agents/grd-eval-reporter.md +717 -0
- package/agents/grd-executor.md +683 -0
- package/agents/grd-feasibility-analyst.md +624 -0
- package/agents/grd-integration-checker.md +367 -0
- package/agents/grd-knowledge-miner.md +81 -0
- package/agents/grd-migrator.md +88 -0
- package/agents/grd-phase-researcher.md +697 -0
- package/agents/grd-plan-checker.md +443 -0
- package/agents/grd-planner.md +1532 -0
- package/agents/grd-product-owner.md +562 -0
- package/agents/grd-project-researcher.md +513 -0
- package/agents/grd-research-synthesizer.md +273 -0
- package/agents/grd-roadmapper.md +798 -0
- package/agents/grd-surveyor.md +566 -0
- package/agents/grd-verifier.md +893 -0
- package/bin/gd.js +4 -0
- package/bin/gd.ts +227 -0
- package/bin/grd-manifest.js +4 -0
- package/bin/grd-manifest.ts +286 -0
- package/bin/grd-mcp-server.js +4 -0
- package/bin/grd-mcp-server.ts +124 -0
- package/bin/grd-tools.js +4 -0
- package/bin/grd-tools.ts +2471 -0
- package/bin/postinstall.js +4 -0
- package/bin/postinstall.ts +80 -0
- package/commands/add-phase.md +123 -0
- package/commands/add-todo.md +87 -0
- package/commands/assess-baseline.md +289 -0
- package/commands/autopilot.md +100 -0
- package/commands/autoplan.md +55 -0
- package/commands/check-todos.md +87 -0
- package/commands/compare-methods.md +262 -0
- package/commands/complete-milestone.md +225 -0
- package/commands/debug.md +372 -0
- package/commands/deep-dive.md +288 -0
- package/commands/discover.md +281 -0
- package/commands/discuss-phase.md +188 -0
- package/commands/discuss.md +55 -0
- package/commands/eval-report.md +310 -0
- package/commands/evolve.md +79 -0
- package/commands/execute-phase.md +1017 -0
- package/commands/feasibility.md +292 -0
- package/commands/help.md +407 -0
- package/commands/init.md +1508 -0
- package/commands/insert-phase.md +113 -0
- package/commands/iterate.md +327 -0
- package/commands/list-phase-assumptions.md +217 -0
- package/commands/long-term-roadmap.md +202 -0
- package/commands/map-codebase.md +111 -0
- package/commands/migrate.md +159 -0
- package/commands/new-milestone.md +169 -0
- package/commands/pause-work.md +83 -0
- package/commands/plan-milestone-gaps.md +373 -0
- package/commands/plan-phase.md +655 -0
- package/commands/principles.md +328 -0
- package/commands/product-plan.md +319 -0
- package/commands/progress.md +481 -0
- package/commands/quick.md +167 -0
- package/commands/reapply-patches.md +154 -0
- package/commands/remove-phase.md +97 -0
- package/commands/requirement.md +96 -0
- package/commands/resume-project.md +113 -0
- package/commands/settings.md +1144 -0
- package/commands/survey.md +242 -0
- package/commands/sync.md +246 -0
- package/commands/tracker-setup.md +322 -0
- package/commands/update.md +202 -0
- package/commands/verify-phase.md +335 -0
- package/commands/verify-work.md +701 -0
- package/commands/wireup.md +29 -0
- package/dist/bin/gd.d.ts +3 -0
- package/dist/bin/gd.d.ts.map +1 -0
- package/dist/bin/gd.js +178 -0
- package/dist/bin/gd.js.map +1 -0
- package/dist/bin/grd-manifest.d.ts +3 -0
- package/dist/bin/grd-manifest.d.ts.map +1 -0
- package/dist/bin/grd-manifest.js +202 -0
- package/dist/bin/grd-manifest.js.map +1 -0
- package/dist/bin/grd-mcp-server.d.ts +3 -0
- package/dist/bin/grd-mcp-server.d.ts.map +1 -0
- package/dist/bin/grd-mcp-server.js +71 -0
- package/dist/bin/grd-mcp-server.js.map +1 -0
- package/dist/bin/grd-tools.d.ts +3 -0
- package/dist/bin/grd-tools.d.ts.map +1 -0
- package/dist/bin/grd-tools.js +1680 -0
- package/dist/bin/grd-tools.js.map +1 -0
- package/dist/bin/postinstall.d.ts +3 -0
- package/dist/bin/postinstall.d.ts.map +1 -0
- package/dist/bin/postinstall.js +61 -0
- package/dist/bin/postinstall.js.map +1 -0
- package/dist/lib/autopilot-milestone.d.ts +2 -0
- package/dist/lib/autopilot-milestone.d.ts.map +1 -0
- package/dist/lib/autopilot-milestone.js +94 -0
- package/dist/lib/autopilot-milestone.js.map +1 -0
- package/dist/lib/autopilot-pipeline.d.ts +2 -0
- package/dist/lib/autopilot-pipeline.d.ts.map +1 -0
- package/dist/lib/autopilot-pipeline.js +830 -0
- package/dist/lib/autopilot-pipeline.js.map +1 -0
- package/dist/lib/autopilot-waves.d.ts +2 -0
- package/dist/lib/autopilot-waves.d.ts.map +1 -0
- package/dist/lib/autopilot-waves.js +266 -0
- package/dist/lib/autopilot-waves.js.map +1 -0
- package/dist/lib/autopilot.d.ts +2 -0
- package/dist/lib/autopilot.d.ts.map +1 -0
- package/dist/lib/autopilot.js +1314 -0
- package/dist/lib/autopilot.js.map +1 -0
- package/dist/lib/autoplan.d.ts +2 -0
- package/dist/lib/autoplan.d.ts.map +1 -0
- package/dist/lib/autoplan.js +198 -0
- package/dist/lib/autoplan.js.map +1 -0
- package/dist/lib/autoresearch.d.ts +2 -0
- package/dist/lib/autoresearch.d.ts.map +1 -0
- package/dist/lib/autoresearch.js +626 -0
- package/dist/lib/autoresearch.js.map +1 -0
- package/dist/lib/backend.d.ts +2 -0
- package/dist/lib/backend.d.ts.map +1 -0
- package/dist/lib/backend.js +1036 -0
- package/dist/lib/backend.js.map +1 -0
- package/dist/lib/benchmark.d.ts +99 -0
- package/dist/lib/benchmark.d.ts.map +1 -0
- package/dist/lib/benchmark.js +278 -0
- package/dist/lib/benchmark.js.map +1 -0
- package/dist/lib/citations.d.ts +2 -0
- package/dist/lib/citations.d.ts.map +1 -0
- package/dist/lib/citations.js +642 -0
- package/dist/lib/citations.js.map +1 -0
- package/dist/lib/cleanup.d.ts +2 -0
- package/dist/lib/cleanup.d.ts.map +1 -0
- package/dist/lib/cleanup.js +1222 -0
- package/dist/lib/cleanup.js.map +1 -0
- package/dist/lib/cli/adapters.d.ts +10 -0
- package/dist/lib/cli/adapters.d.ts.map +1 -0
- package/dist/lib/cli/adapters.js +27 -0
- package/dist/lib/cli/adapters.js.map +1 -0
- package/dist/lib/cli/agent.d.ts +17 -0
- package/dist/lib/cli/agent.d.ts.map +1 -0
- package/dist/lib/cli/agent.js +53 -0
- package/dist/lib/cli/agent.js.map +1 -0
- package/dist/lib/cli/index.d.ts +21 -0
- package/dist/lib/cli/index.d.ts.map +1 -0
- package/dist/lib/cli/index.js +264 -0
- package/dist/lib/cli/index.js.map +1 -0
- package/dist/lib/cli/output.d.ts +20 -0
- package/dist/lib/cli/output.d.ts.map +1 -0
- package/dist/lib/cli/output.js +22 -0
- package/dist/lib/cli/output.js.map +1 -0
- package/dist/lib/cli/scan-dispatch.d.ts +9 -0
- package/dist/lib/cli/scan-dispatch.d.ts.map +1 -0
- package/dist/lib/cli/scan-dispatch.js +107 -0
- package/dist/lib/cli/scan-dispatch.js.map +1 -0
- package/dist/lib/cli/tools.d.ts +16 -0
- package/dist/lib/cli/tools.d.ts.map +1 -0
- package/dist/lib/cli/tools.js +168 -0
- package/dist/lib/cli/tools.js.map +1 -0
- package/dist/lib/commands/_dashboard-parsers.d.ts +2 -0
- package/dist/lib/commands/_dashboard-parsers.d.ts.map +1 -0
- package/dist/lib/commands/_dashboard-parsers.js +192 -0
- package/dist/lib/commands/_dashboard-parsers.js.map +1 -0
- package/dist/lib/commands/analysis.d.ts +2 -0
- package/dist/lib/commands/analysis.d.ts.map +1 -0
- package/dist/lib/commands/analysis.js +1418 -0
- package/dist/lib/commands/analysis.js.map +1 -0
- package/dist/lib/commands/assumptions.d.ts +2 -0
- package/dist/lib/commands/assumptions.d.ts.map +1 -0
- package/dist/lib/commands/assumptions.js +166 -0
- package/dist/lib/commands/assumptions.js.map +1 -0
- package/dist/lib/commands/blame.d.ts +2 -0
- package/dist/lib/commands/blame.d.ts.map +1 -0
- package/dist/lib/commands/blame.js +133 -0
- package/dist/lib/commands/blame.js.map +1 -0
- package/dist/lib/commands/budget.d.ts +2 -0
- package/dist/lib/commands/budget.d.ts.map +1 -0
- package/dist/lib/commands/budget.js +100 -0
- package/dist/lib/commands/budget.js.map +1 -0
- package/dist/lib/commands/check-plans.d.ts +2 -0
- package/dist/lib/commands/check-plans.d.ts.map +1 -0
- package/dist/lib/commands/check-plans.js +190 -0
- package/dist/lib/commands/check-plans.js.map +1 -0
- package/dist/lib/commands/config.d.ts +2 -0
- package/dist/lib/commands/config.d.ts.map +1 -0
- package/dist/lib/commands/config.js +188 -0
- package/dist/lib/commands/config.js.map +1 -0
- package/dist/lib/commands/dashboard.d.ts +2 -0
- package/dist/lib/commands/dashboard.d.ts.map +1 -0
- package/dist/lib/commands/dashboard.js +466 -0
- package/dist/lib/commands/dashboard.js.map +1 -0
- package/dist/lib/commands/estimate.d.ts +2 -0
- package/dist/lib/commands/estimate.d.ts.map +1 -0
- package/dist/lib/commands/estimate.js +148 -0
- package/dist/lib/commands/estimate.js.map +1 -0
- package/dist/lib/commands/eval-diff.d.ts +2 -0
- package/dist/lib/commands/eval-diff.d.ts.map +1 -0
- package/dist/lib/commands/eval-diff.js +213 -0
- package/dist/lib/commands/eval-diff.js.map +1 -0
- package/dist/lib/commands/freshness.d.ts +2 -0
- package/dist/lib/commands/freshness.d.ts.map +1 -0
- package/dist/lib/commands/freshness.js +163 -0
- package/dist/lib/commands/freshness.js.map +1 -0
- package/dist/lib/commands/health.d.ts +2 -0
- package/dist/lib/commands/health.d.ts.map +1 -0
- package/dist/lib/commands/health.js +435 -0
- package/dist/lib/commands/health.js.map +1 -0
- package/dist/lib/commands/index.d.ts +2 -0
- package/dist/lib/commands/index.d.ts.map +1 -0
- package/dist/lib/commands/index.js +128 -0
- package/dist/lib/commands/index.js.map +1 -0
- package/dist/lib/commands/install.d.ts +56 -0
- package/dist/lib/commands/install.d.ts.map +1 -0
- package/dist/lib/commands/install.js +214 -0
- package/dist/lib/commands/install.js.map +1 -0
- package/dist/lib/commands/knowhow-aggregator.d.ts +2 -0
- package/dist/lib/commands/knowhow-aggregator.d.ts.map +1 -0
- package/dist/lib/commands/knowhow-aggregator.js +279 -0
- package/dist/lib/commands/knowhow-aggregator.js.map +1 -0
- package/dist/lib/commands/knowledge-search.d.ts +2 -0
- package/dist/lib/commands/knowledge-search.d.ts.map +1 -0
- package/dist/lib/commands/knowledge-search.js +113 -0
- package/dist/lib/commands/knowledge-search.js.map +1 -0
- package/dist/lib/commands/long-term-roadmap.d.ts +2 -0
- package/dist/lib/commands/long-term-roadmap.d.ts.map +1 -0
- package/dist/lib/commands/long-term-roadmap.js +272 -0
- package/dist/lib/commands/long-term-roadmap.js.map +1 -0
- package/dist/lib/commands/patterns.d.ts +91 -0
- package/dist/lib/commands/patterns.d.ts.map +1 -0
- package/dist/lib/commands/patterns.js +391 -0
- package/dist/lib/commands/patterns.js.map +1 -0
- package/dist/lib/commands/phase-info.d.ts +2 -0
- package/dist/lib/commands/phase-info.d.ts.map +1 -0
- package/dist/lib/commands/phase-info.js +509 -0
- package/dist/lib/commands/phase-info.js.map +1 -0
- package/dist/lib/commands/plan-lint.d.ts +56 -0
- package/dist/lib/commands/plan-lint.d.ts.map +1 -0
- package/dist/lib/commands/plan-lint.js +481 -0
- package/dist/lib/commands/plan-lint.js.map +1 -0
- package/dist/lib/commands/plan-phase.d.ts +53 -0
- package/dist/lib/commands/plan-phase.d.ts.map +1 -0
- package/dist/lib/commands/plan-phase.js +288 -0
- package/dist/lib/commands/plan-phase.js.map +1 -0
- package/dist/lib/commands/progress.d.ts +2 -0
- package/dist/lib/commands/progress.d.ts.map +1 -0
- package/dist/lib/commands/progress.js +266 -0
- package/dist/lib/commands/progress.js.map +1 -0
- package/dist/lib/commands/quality.d.ts +2 -0
- package/dist/lib/commands/quality.d.ts.map +1 -0
- package/dist/lib/commands/quality.js +80 -0
- package/dist/lib/commands/quality.js.map +1 -0
- package/dist/lib/commands/rollback.d.ts +2 -0
- package/dist/lib/commands/rollback.d.ts.map +1 -0
- package/dist/lib/commands/rollback.js +145 -0
- package/dist/lib/commands/rollback.js.map +1 -0
- package/dist/lib/commands/scan.d.ts +25 -0
- package/dist/lib/commands/scan.d.ts.map +1 -0
- package/dist/lib/commands/scan.js +28 -0
- package/dist/lib/commands/scan.js.map +1 -0
- package/dist/lib/commands/search.d.ts +2 -0
- package/dist/lib/commands/search.d.ts.map +1 -0
- package/dist/lib/commands/search.js +212 -0
- package/dist/lib/commands/search.js.map +1 -0
- package/dist/lib/commands/select-candidate.d.ts +128 -0
- package/dist/lib/commands/select-candidate.d.ts.map +1 -0
- package/dist/lib/commands/select-candidate.js +518 -0
- package/dist/lib/commands/select-candidate.js.map +1 -0
- package/dist/lib/commands/singularity.d.ts +2 -0
- package/dist/lib/commands/singularity.d.ts.map +1 -0
- package/dist/lib/commands/singularity.js +185 -0
- package/dist/lib/commands/singularity.js.map +1 -0
- package/dist/lib/commands/slug-timestamp.d.ts +2 -0
- package/dist/lib/commands/slug-timestamp.d.ts.map +1 -0
- package/dist/lib/commands/slug-timestamp.js +54 -0
- package/dist/lib/commands/slug-timestamp.js.map +1 -0
- package/dist/lib/commands/tail.d.ts +2 -0
- package/dist/lib/commands/tail.d.ts.map +1 -0
- package/dist/lib/commands/tail.js +100 -0
- package/dist/lib/commands/tail.js.map +1 -0
- package/dist/lib/commands/todo.d.ts +2 -0
- package/dist/lib/commands/todo.d.ts.map +1 -0
- package/dist/lib/commands/todo.js +200 -0
- package/dist/lib/commands/todo.js.map +1 -0
- package/dist/lib/commands/watch.d.ts +2 -0
- package/dist/lib/commands/watch.d.ts.map +1 -0
- package/dist/lib/commands/watch.js +72 -0
- package/dist/lib/commands/watch.js.map +1 -0
- package/dist/lib/complexity.d.ts +55 -0
- package/dist/lib/complexity.d.ts.map +1 -0
- package/dist/lib/complexity.js +80 -0
- package/dist/lib/complexity.js.map +1 -0
- package/dist/lib/context/agents.d.ts +2 -0
- package/dist/lib/context/agents.d.ts.map +1 -0
- package/dist/lib/context/agents.js +344 -0
- package/dist/lib/context/agents.js.map +1 -0
- package/dist/lib/context/base.d.ts +2 -0
- package/dist/lib/context/base.d.ts.map +1 -0
- package/dist/lib/context/base.js +81 -0
- package/dist/lib/context/base.js.map +1 -0
- package/dist/lib/context/execute.d.ts +2 -0
- package/dist/lib/context/execute.d.ts.map +1 -0
- package/dist/lib/context/execute.js +753 -0
- package/dist/lib/context/execute.js.map +1 -0
- package/dist/lib/context/index.d.ts +2 -0
- package/dist/lib/context/index.d.ts.map +1 -0
- package/dist/lib/context/index.js +88 -0
- package/dist/lib/context/index.js.map +1 -0
- package/dist/lib/context/progress.d.ts +2 -0
- package/dist/lib/context/progress.d.ts.map +1 -0
- package/dist/lib/context/progress.js +178 -0
- package/dist/lib/context/progress.js.map +1 -0
- package/dist/lib/context/project.d.ts +2 -0
- package/dist/lib/context/project.d.ts.map +1 -0
- package/dist/lib/context/project.js +413 -0
- package/dist/lib/context/project.js.map +1 -0
- package/dist/lib/context/research.d.ts +2 -0
- package/dist/lib/context/research.d.ts.map +1 -0
- package/dist/lib/context/research.js +466 -0
- package/dist/lib/context/research.js.map +1 -0
- package/dist/lib/dead-ends.d.ts +28 -0
- package/dist/lib/dead-ends.d.ts.map +1 -0
- package/dist/lib/dead-ends.js +451 -0
- package/dist/lib/dead-ends.js.map +1 -0
- package/dist/lib/deps.d.ts +2 -0
- package/dist/lib/deps.d.ts.map +1 -0
- package/dist/lib/deps.js +630 -0
- package/dist/lib/deps.js.map +1 -0
- package/dist/lib/discussion.d.ts +2 -0
- package/dist/lib/discussion.d.ts.map +1 -0
- package/dist/lib/discussion.js +1041 -0
- package/dist/lib/discussion.js.map +1 -0
- package/dist/lib/drift.d.ts +36 -0
- package/dist/lib/drift.d.ts.map +1 -0
- package/dist/lib/drift.js +481 -0
- package/dist/lib/drift.js.map +1 -0
- package/dist/lib/evolve/_dimensions-features.d.ts +2 -0
- package/dist/lib/evolve/_dimensions-features.d.ts.map +1 -0
- package/dist/lib/evolve/_dimensions-features.js +369 -0
- package/dist/lib/evolve/_dimensions-features.js.map +1 -0
- package/dist/lib/evolve/_dimensions.d.ts +2 -0
- package/dist/lib/evolve/_dimensions.d.ts.map +1 -0
- package/dist/lib/evolve/_dimensions.js +358 -0
- package/dist/lib/evolve/_dimensions.js.map +1 -0
- package/dist/lib/evolve/_product-ideation.d.ts +2 -0
- package/dist/lib/evolve/_product-ideation.d.ts.map +1 -0
- package/dist/lib/evolve/_product-ideation.js +281 -0
- package/dist/lib/evolve/_product-ideation.js.map +1 -0
- package/dist/lib/evolve/_prompts.d.ts +2 -0
- package/dist/lib/evolve/_prompts.d.ts.map +1 -0
- package/dist/lib/evolve/_prompts.js +153 -0
- package/dist/lib/evolve/_prompts.js.map +1 -0
- package/dist/lib/evolve/cli.d.ts +2 -0
- package/dist/lib/evolve/cli.d.ts.map +1 -0
- package/dist/lib/evolve/cli.js +224 -0
- package/dist/lib/evolve/cli.js.map +1 -0
- package/dist/lib/evolve/discovery.d.ts +2 -0
- package/dist/lib/evolve/discovery.d.ts.map +1 -0
- package/dist/lib/evolve/discovery.js +391 -0
- package/dist/lib/evolve/discovery.js.map +1 -0
- package/dist/lib/evolve/index.d.ts +2 -0
- package/dist/lib/evolve/index.d.ts.map +1 -0
- package/dist/lib/evolve/index.js +88 -0
- package/dist/lib/evolve/index.js.map +1 -0
- package/dist/lib/evolve/orchestrator.d.ts +2 -0
- package/dist/lib/evolve/orchestrator.d.ts.map +1 -0
- package/dist/lib/evolve/orchestrator.js +851 -0
- package/dist/lib/evolve/orchestrator.js.map +1 -0
- package/dist/lib/evolve/scoring.d.ts +2 -0
- package/dist/lib/evolve/scoring.d.ts.map +1 -0
- package/dist/lib/evolve/scoring.js +118 -0
- package/dist/lib/evolve/scoring.js.map +1 -0
- package/dist/lib/evolve/state.d.ts +2 -0
- package/dist/lib/evolve/state.d.ts.map +1 -0
- package/dist/lib/evolve/state.js +264 -0
- package/dist/lib/evolve/state.js.map +1 -0
- package/dist/lib/evolve/types.d.ts +249 -0
- package/dist/lib/evolve/types.d.ts.map +1 -0
- package/dist/lib/evolve/types.js +3 -0
- package/dist/lib/evolve/types.js.map +1 -0
- package/dist/lib/frontmatter.d.ts +2 -0
- package/dist/lib/frontmatter.d.ts.map +1 -0
- package/dist/lib/frontmatter.js +513 -0
- package/dist/lib/frontmatter.js.map +1 -0
- package/dist/lib/gates.d.ts +2 -0
- package/dist/lib/gates.d.ts.map +1 -0
- package/dist/lib/gates.js +578 -0
- package/dist/lib/gates.js.map +1 -0
- package/dist/lib/genome.d.ts +10 -0
- package/dist/lib/genome.d.ts.map +1 -0
- package/dist/lib/genome.js +368 -0
- package/dist/lib/genome.js.map +1 -0
- package/dist/lib/got.d.ts +2 -0
- package/dist/lib/got.d.ts.map +1 -0
- package/dist/lib/got.js +280 -0
- package/dist/lib/got.js.map +1 -0
- package/dist/lib/invariants.d.ts +2 -0
- package/dist/lib/invariants.d.ts.map +1 -0
- package/dist/lib/invariants.js +298 -0
- package/dist/lib/invariants.js.map +1 -0
- package/dist/lib/knowledge.d.ts +2 -0
- package/dist/lib/knowledge.d.ts.map +1 -0
- package/dist/lib/knowledge.js +658 -0
- package/dist/lib/knowledge.js.map +1 -0
- package/dist/lib/long-term-roadmap.d.ts +2 -0
- package/dist/lib/long-term-roadmap.d.ts.map +1 -0
- package/dist/lib/long-term-roadmap.js +602 -0
- package/dist/lib/long-term-roadmap.js.map +1 -0
- package/dist/lib/markdown-split.d.ts +2 -0
- package/dist/lib/markdown-split.d.ts.map +1 -0
- package/dist/lib/markdown-split.js +199 -0
- package/dist/lib/markdown-split.js.map +1 -0
- package/dist/lib/mcp-server.d.ts +2 -0
- package/dist/lib/mcp-server.d.ts.map +1 -0
- package/dist/lib/mcp-server.js +2424 -0
- package/dist/lib/mcp-server.js.map +1 -0
- package/dist/lib/metrics.d.ts +16 -0
- package/dist/lib/metrics.d.ts.map +1 -0
- package/dist/lib/metrics.js +48 -0
- package/dist/lib/metrics.js.map +1 -0
- package/dist/lib/overstory.d.ts +2 -0
- package/dist/lib/overstory.d.ts.map +1 -0
- package/dist/lib/overstory.js +211 -0
- package/dist/lib/overstory.js.map +1 -0
- package/dist/lib/parallel.d.ts +2 -0
- package/dist/lib/parallel.d.ts.map +1 -0
- package/dist/lib/parallel.js +349 -0
- package/dist/lib/parallel.js.map +1 -0
- package/dist/lib/paths.d.ts +2 -0
- package/dist/lib/paths.d.ts.map +1 -0
- package/dist/lib/paths.js +254 -0
- package/dist/lib/paths.js.map +1 -0
- package/dist/lib/phase-complete-llm.d.ts +22 -0
- package/dist/lib/phase-complete-llm.d.ts.map +1 -0
- package/dist/lib/phase-complete-llm.js +331 -0
- package/dist/lib/phase-complete-llm.js.map +1 -0
- package/dist/lib/phase-complete.d.ts +46 -0
- package/dist/lib/phase-complete.d.ts.map +1 -0
- package/dist/lib/phase-complete.js +278 -0
- package/dist/lib/phase-complete.js.map +1 -0
- package/dist/lib/phase-io.d.ts +2 -0
- package/dist/lib/phase-io.d.ts.map +1 -0
- package/dist/lib/phase-io.js +126 -0
- package/dist/lib/phase-io.js.map +1 -0
- package/dist/lib/phase.d.ts +2 -0
- package/dist/lib/phase.d.ts.map +1 -0
- package/dist/lib/phase.js +1344 -0
- package/dist/lib/phase.js.map +1 -0
- package/dist/lib/plan-tournament.d.ts +63 -0
- package/dist/lib/plan-tournament.d.ts.map +1 -0
- package/dist/lib/plan-tournament.js +353 -0
- package/dist/lib/plan-tournament.js.map +1 -0
- package/dist/lib/refinement.d.ts +74 -0
- package/dist/lib/refinement.d.ts.map +1 -0
- package/dist/lib/refinement.js +283 -0
- package/dist/lib/refinement.js.map +1 -0
- package/dist/lib/requirements.d.ts +2 -0
- package/dist/lib/requirements.d.ts.map +1 -0
- package/dist/lib/requirements.js +355 -0
- package/dist/lib/requirements.js.map +1 -0
- package/dist/lib/research-bundle.d.ts +2 -0
- package/dist/lib/research-bundle.d.ts.map +1 -0
- package/dist/lib/research-bundle.js +246 -0
- package/dist/lib/research-bundle.js.map +1 -0
- package/dist/lib/roadmap.d.ts +2 -0
- package/dist/lib/roadmap.d.ts.map +1 -0
- package/dist/lib/roadmap.js +541 -0
- package/dist/lib/roadmap.js.map +1 -0
- package/dist/lib/sample.d.ts +16 -0
- package/dist/lib/sample.d.ts.map +1 -0
- package/dist/lib/sample.js +20 -0
- package/dist/lib/sample.js.map +1 -0
- package/dist/lib/scaffold.d.ts +2 -0
- package/dist/lib/scaffold.d.ts.map +1 -0
- package/dist/lib/scaffold.js +355 -0
- package/dist/lib/scaffold.js.map +1 -0
- package/dist/lib/scan/_utils.d.ts +11 -0
- package/dist/lib/scan/_utils.d.ts.map +1 -0
- package/dist/lib/scan/_utils.js +36 -0
- package/dist/lib/scan/_utils.js.map +1 -0
- package/dist/lib/scan/base64.d.ts +15 -0
- package/dist/lib/scan/base64.d.ts.map +1 -0
- package/dist/lib/scan/base64.js +66 -0
- package/dist/lib/scan/base64.js.map +1 -0
- package/dist/lib/scan/ignorefile.d.ts +30 -0
- package/dist/lib/scan/ignorefile.d.ts.map +1 -0
- package/dist/lib/scan/ignorefile.js +101 -0
- package/dist/lib/scan/ignorefile.js.map +1 -0
- package/dist/lib/scan/injection.d.ts +14 -0
- package/dist/lib/scan/injection.d.ts.map +1 -0
- package/dist/lib/scan/injection.js +39 -0
- package/dist/lib/scan/injection.js.map +1 -0
- package/dist/lib/scan/patterns.d.ts +17 -0
- package/dist/lib/scan/patterns.d.ts.map +1 -0
- package/dist/lib/scan/patterns.js +123 -0
- package/dist/lib/scan/patterns.js.map +1 -0
- package/dist/lib/scan/strip-markdown.d.ts +7 -0
- package/dist/lib/scan/strip-markdown.d.ts.map +1 -0
- package/dist/lib/scan/strip-markdown.js +38 -0
- package/dist/lib/scan/strip-markdown.js.map +1 -0
- package/dist/lib/scan/types.d.ts +23 -0
- package/dist/lib/scan/types.d.ts.map +1 -0
- package/dist/lib/scan/types.js +3 -0
- package/dist/lib/scan/types.js.map +1 -0
- package/dist/lib/scheduler-wait.d.ts +2 -0
- package/dist/lib/scheduler-wait.d.ts.map +1 -0
- package/dist/lib/scheduler-wait.js +59 -0
- package/dist/lib/scheduler-wait.js.map +1 -0
- package/dist/lib/scheduler.d.ts +254 -0
- package/dist/lib/scheduler.d.ts.map +1 -0
- package/dist/lib/scheduler.js +1147 -0
- package/dist/lib/scheduler.js.map +1 -0
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.d.ts.map +1 -0
- package/dist/lib/state.js +744 -0
- package/dist/lib/state.js.map +1 -0
- package/dist/lib/think.d.ts +18 -0
- package/dist/lib/think.d.ts.map +1 -0
- package/dist/lib/think.js +317 -0
- package/dist/lib/think.js.map +1 -0
- package/dist/lib/tracker.d.ts +2 -0
- package/dist/lib/tracker.d.ts.map +1 -0
- package/dist/lib/tracker.js +1121 -0
- package/dist/lib/tracker.js.map +1 -0
- package/dist/lib/types.d.ts +1514 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +4 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +1363 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/lib/verify.d.ts +2 -0
- package/dist/lib/verify.d.ts.map +1 -0
- package/dist/lib/verify.js +1153 -0
- package/dist/lib/verify.js.map +1 -0
- package/dist/lib/wireup/autofix.d.ts +2 -0
- package/dist/lib/wireup/autofix.d.ts.map +1 -0
- package/dist/lib/wireup/autofix.js +188 -0
- package/dist/lib/wireup/autofix.js.map +1 -0
- package/dist/lib/wireup/cli.d.ts +2 -0
- package/dist/lib/wireup/cli.d.ts.map +1 -0
- package/dist/lib/wireup/cli.js +194 -0
- package/dist/lib/wireup/cli.js.map +1 -0
- package/dist/lib/wireup/detection.d.ts +47 -0
- package/dist/lib/wireup/detection.d.ts.map +1 -0
- package/dist/lib/wireup/detection.js +410 -0
- package/dist/lib/wireup/detection.js.map +1 -0
- package/dist/lib/wireup/discovery.d.ts +2 -0
- package/dist/lib/wireup/discovery.d.ts.map +1 -0
- package/dist/lib/wireup/discovery.js +934 -0
- package/dist/lib/wireup/discovery.js.map +1 -0
- package/dist/lib/wireup/execution.d.ts +2 -0
- package/dist/lib/wireup/execution.d.ts.map +1 -0
- package/dist/lib/wireup/execution.js +573 -0
- package/dist/lib/wireup/execution.js.map +1 -0
- package/dist/lib/wireup/index.d.ts +2 -0
- package/dist/lib/wireup/index.d.ts.map +1 -0
- package/dist/lib/wireup/index.js +85 -0
- package/dist/lib/wireup/index.js.map +1 -0
- package/dist/lib/wireup/orchestrator.d.ts +2 -0
- package/dist/lib/wireup/orchestrator.d.ts.map +1 -0
- package/dist/lib/wireup/orchestrator.js +366 -0
- package/dist/lib/wireup/orchestrator.js.map +1 -0
- package/dist/lib/wireup/report.d.ts +47 -0
- package/dist/lib/wireup/report.d.ts.map +1 -0
- package/dist/lib/wireup/report.js +201 -0
- package/dist/lib/wireup/report.js.map +1 -0
- package/dist/lib/wireup/scenarios.d.ts +2 -0
- package/dist/lib/wireup/scenarios.d.ts.map +1 -0
- package/dist/lib/wireup/scenarios.js +516 -0
- package/dist/lib/wireup/scenarios.js.map +1 -0
- package/dist/lib/wireup/state.d.ts +2 -0
- package/dist/lib/wireup/state.d.ts.map +1 -0
- package/dist/lib/wireup/state.js +102 -0
- package/dist/lib/wireup/state.js.map +1 -0
- package/dist/lib/wireup/types.d.ts +376 -0
- package/dist/lib/wireup/types.d.ts.map +1 -0
- package/dist/lib/wireup/types.js +3 -0
- package/dist/lib/wireup/types.js.map +1 -0
- package/dist/lib/worktree.d.ts +2 -0
- package/dist/lib/worktree.d.ts.map +1 -0
- package/dist/lib/worktree.js +999 -0
- package/dist/lib/worktree.js.map +1 -0
- package/lib/autopilot-milestone.ts +136 -0
- package/lib/autopilot-pipeline.ts +1179 -0
- package/lib/autopilot-waves.ts +361 -0
- package/lib/autopilot.ts +1874 -0
- package/lib/autoplan.ts +280 -0
- package/lib/autoresearch.js +4 -0
- package/lib/autoresearch.ts +886 -0
- package/lib/backend.ts +1252 -0
- package/lib/benchmark.ts +341 -0
- package/lib/citations.ts +760 -0
- package/lib/cleanup.ts +1588 -0
- package/lib/cli/adapters.ts +41 -0
- package/lib/cli/agent.ts +83 -0
- package/lib/cli/index.ts +273 -0
- package/lib/cli/output.ts +33 -0
- package/lib/cli/scan-dispatch.ts +130 -0
- package/lib/cli/tools.ts +198 -0
- package/lib/commands/_dashboard-parsers.ts +275 -0
- package/lib/commands/analysis.ts +1851 -0
- package/lib/commands/assumptions.ts +232 -0
- package/lib/commands/blame.ts +174 -0
- package/lib/commands/budget.ts +148 -0
- package/lib/commands/check-plans.ts +233 -0
- package/lib/commands/config.ts +287 -0
- package/lib/commands/dashboard.ts +680 -0
- package/lib/commands/estimate.ts +204 -0
- package/lib/commands/eval-diff.ts +252 -0
- package/lib/commands/freshness.ts +213 -0
- package/lib/commands/health.ts +607 -0
- package/lib/commands/index.ts +266 -0
- package/lib/commands/install.ts +307 -0
- package/lib/commands/knowhow-aggregator.ts +345 -0
- package/lib/commands/knowledge-search.ts +153 -0
- package/lib/commands/long-term-roadmap.ts +390 -0
- package/lib/commands/patterns.ts +465 -0
- package/lib/commands/phase-info.ts +698 -0
- package/lib/commands/plan-lint.ts +546 -0
- package/lib/commands/plan-phase.ts +375 -0
- package/lib/commands/progress.ts +319 -0
- package/lib/commands/quality.ts +138 -0
- package/lib/commands/rollback.ts +195 -0
- package/lib/commands/scan.ts +72 -0
- package/lib/commands/search.ts +300 -0
- package/lib/commands/select-candidate.ts +687 -0
- package/lib/commands/singularity.ts +222 -0
- package/lib/commands/slug-timestamp.ts +74 -0
- package/lib/commands/tail.ts +129 -0
- package/lib/commands/todo.ts +273 -0
- package/lib/commands/watch.ts +80 -0
- package/lib/complexity.ts +117 -0
- package/lib/context/agents.ts +505 -0
- package/lib/context/base.ts +123 -0
- package/lib/context/execute.ts +977 -0
- package/lib/context/index.ts +110 -0
- package/lib/context/progress.ts +278 -0
- package/lib/context/project.ts +531 -0
- package/lib/context/research.ts +646 -0
- package/lib/dead-ends.ts +506 -0
- package/lib/deps.ts +773 -0
- package/lib/discussion.ts +1275 -0
- package/lib/drift.ts +519 -0
- package/lib/evolve/_dimensions-features.ts +525 -0
- package/lib/evolve/_dimensions.ts +511 -0
- package/lib/evolve/_product-ideation.ts +405 -0
- package/lib/evolve/_prompts.ts +178 -0
- package/lib/evolve/cli.ts +330 -0
- package/lib/evolve/discovery.ts +571 -0
- package/lib/evolve/index.ts +105 -0
- package/lib/evolve/orchestrator.ts +1139 -0
- package/lib/evolve/scoring.ts +167 -0
- package/lib/evolve/state.ts +330 -0
- package/lib/evolve/types.ts +290 -0
- package/lib/frontmatter.ts +615 -0
- package/lib/gates.ts +695 -0
- package/lib/genome.ts +402 -0
- package/lib/got.js +4 -0
- package/lib/got.ts +361 -0
- package/lib/invariants.ts +378 -0
- package/lib/knowledge.ts +768 -0
- package/lib/long-term-roadmap.ts +806 -0
- package/lib/markdown-split.ts +273 -0
- package/lib/mcp-server.ts +3292 -0
- package/lib/metrics.ts +49 -0
- package/lib/overstory.ts +270 -0
- package/lib/parallel.ts +570 -0
- package/lib/paths.ts +293 -0
- package/lib/phase-complete-llm.ts +376 -0
- package/lib/phase-complete.ts +366 -0
- package/lib/phase-io.ts +101 -0
- package/lib/phase.ts +1981 -0
- package/lib/plan-tournament.ts +426 -0
- package/lib/refinement.ts +349 -0
- package/lib/requirements.ts +469 -0
- package/lib/research-bundle.ts +300 -0
- package/lib/roadmap.ts +775 -0
- package/lib/scaffold.ts +480 -0
- package/lib/scan/_utils.ts +37 -0
- package/lib/scan/base64.ts +90 -0
- package/lib/scan/ignorefile.ts +109 -0
- package/lib/scan/injection.ts +67 -0
- package/lib/scan/patterns.ts +139 -0
- package/lib/scan/strip-markdown.ts +39 -0
- package/lib/scan/types.ts +28 -0
- package/lib/scheduler-wait.ts +58 -0
- package/lib/scheduler.ts +1370 -0
- package/lib/state.ts +1000 -0
- package/lib/think.ts +365 -0
- package/lib/tracker.ts +1591 -0
- package/lib/types.ts +1663 -0
- package/lib/utils.ts +1479 -0
- package/lib/verify.ts +1434 -0
- package/lib/wireup/autofix.ts +241 -0
- package/lib/wireup/cli.ts +278 -0
- package/lib/wireup/detection.ts +542 -0
- package/lib/wireup/discovery.ts +1063 -0
- package/lib/wireup/execution.ts +686 -0
- package/lib/wireup/index.ts +117 -0
- package/lib/wireup/orchestrator.ts +519 -0
- package/lib/wireup/report.ts +286 -0
- package/lib/wireup/scenarios.ts +616 -0
- package/lib/wireup/state.ts +139 -0
- package/lib/wireup/types.ts +436 -0
- package/lib/worktree.ts +1309 -0
- package/package.json +67 -0
package/lib/utils.ts
ADDED
|
@@ -0,0 +1,1479 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GRD Shared Utilities -- Constants, helpers, and validation functions
|
|
5
|
+
*
|
|
6
|
+
* Extracted from bin/grd-tools.js during Phase 03 modularization.
|
|
7
|
+
* These are zero-dependency foundations used by all other modules.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
GrdConfig,
|
|
12
|
+
GrdTimeouts,
|
|
13
|
+
ExecGitResult,
|
|
14
|
+
PhaseInfo,
|
|
15
|
+
MilestoneInfo,
|
|
16
|
+
ModelTier,
|
|
17
|
+
ModelProfileName,
|
|
18
|
+
RunCache,
|
|
19
|
+
AgentModelProfiles,
|
|
20
|
+
BackendRolesConfig,
|
|
21
|
+
DiscussionConfig,
|
|
22
|
+
EffortAxisLevel,
|
|
23
|
+
EffortKnobName,
|
|
24
|
+
} from './types';
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const os = require('os');
|
|
29
|
+
const { execFileSync } = require('child_process');
|
|
30
|
+
const {
|
|
31
|
+
detectBackend,
|
|
32
|
+
resolveBackendModel,
|
|
33
|
+
resolveEffortLevel,
|
|
34
|
+
getBackendCapabilities,
|
|
35
|
+
VALID_BACKENDS,
|
|
36
|
+
} = require('./backend');
|
|
37
|
+
const { phasesDir: getPhasesDirPath } = require('./paths');
|
|
38
|
+
|
|
39
|
+
// ─── Git Operation Whitelist ────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
const GIT_ALLOWED_COMMANDS: Set<string> = new Set([
|
|
42
|
+
'add',
|
|
43
|
+
'commit',
|
|
44
|
+
'log',
|
|
45
|
+
'status',
|
|
46
|
+
'diff',
|
|
47
|
+
'show',
|
|
48
|
+
'rev-parse',
|
|
49
|
+
'cat-file',
|
|
50
|
+
'check-ignore',
|
|
51
|
+
'ls-files',
|
|
52
|
+
'branch',
|
|
53
|
+
'checkout',
|
|
54
|
+
'merge',
|
|
55
|
+
'rebase',
|
|
56
|
+
'cherry-pick',
|
|
57
|
+
'tag',
|
|
58
|
+
'stash',
|
|
59
|
+
'remote',
|
|
60
|
+
'fetch',
|
|
61
|
+
'pull',
|
|
62
|
+
]);
|
|
63
|
+
const GIT_BLOCKED_COMMANDS: Set<string> = new Set(['config', 'push', 'clean']);
|
|
64
|
+
const GIT_BLOCKED_FLAGS: Set<string> = new Set(['--force', '-f', '--hard', '--delete', '-D']);
|
|
65
|
+
|
|
66
|
+
// ─── Model Profile Table ─────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
const MODEL_PROFILES: AgentModelProfiles = {
|
|
69
|
+
'grd-planner': { quality: 'opus', balanced: 'opus', budget: 'sonnet' },
|
|
70
|
+
'grd-roadmapper': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
|
|
71
|
+
'grd-executor': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
|
|
72
|
+
'grd-phase-researcher': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
|
|
73
|
+
'grd-project-researcher': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
|
|
74
|
+
'grd-research-synthesizer': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
|
|
75
|
+
'grd-debugger': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
|
|
76
|
+
'grd-codebase-mapper': { quality: 'sonnet', balanced: 'haiku', budget: 'haiku' },
|
|
77
|
+
'grd-verifier': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
|
|
78
|
+
// NERFIFY refinement-loop critic (codex r43 P2: was routed through
|
|
79
|
+
// grd-verifier, which never loaded the agent definition).
|
|
80
|
+
'grd-critique-agent': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
|
|
81
|
+
'grd-plan-checker': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
|
|
82
|
+
'grd-integration-checker': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
|
|
83
|
+
// R&D-specific agents
|
|
84
|
+
'grd-surveyor': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
|
|
85
|
+
'grd-deep-diver': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
|
|
86
|
+
'grd-feasibility-analyst': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
|
|
87
|
+
'grd-eval-planner': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
|
|
88
|
+
'grd-eval-reporter': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
|
|
89
|
+
'grd-product-owner': { quality: 'opus', balanced: 'opus', budget: 'sonnet' },
|
|
90
|
+
'grd-baseline-assessor': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
|
|
91
|
+
// Development practice agents
|
|
92
|
+
'grd-code-reviewer': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Parse --include flag from CLI args into a Set of included items.
|
|
99
|
+
* @param args - CLI argument array
|
|
100
|
+
* @returns Set of comma-separated include values, or empty Set if not present
|
|
101
|
+
*/
|
|
102
|
+
function parseIncludeFlag(args: string[]): Set<string> {
|
|
103
|
+
const includeIndex: number = args.indexOf('--include');
|
|
104
|
+
if (includeIndex === -1) return new Set();
|
|
105
|
+
const includeValue: string | undefined = args[includeIndex + 1];
|
|
106
|
+
if (!includeValue) return new Set();
|
|
107
|
+
return new Set(includeValue.split(',').map((s: string) => s.trim()));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Read file returning content or null on error.
|
|
112
|
+
* @param filePath - Absolute path to the file
|
|
113
|
+
* @returns File content as UTF-8 string, or null if read fails
|
|
114
|
+
*/
|
|
115
|
+
function safeReadFile(filePath: string): string | null {
|
|
116
|
+
try {
|
|
117
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Read a markdown file, transparently handling GRD split-format index files.
|
|
125
|
+
* If the file is a GRD index (contains <!-- GRD-INDEX --> marker), partials are
|
|
126
|
+
* automatically reassembled. Otherwise, returns the file content as-is.
|
|
127
|
+
* @param filePath - Absolute path to the markdown file
|
|
128
|
+
* @returns File content (reassembled if split), or null on error
|
|
129
|
+
*/
|
|
130
|
+
function safeReadMarkdown(filePath: string): string | null {
|
|
131
|
+
try {
|
|
132
|
+
// Lazy require to avoid circular dependency (markdown-split.js imports safeReadFile from utils.js)
|
|
133
|
+
const { readMarkdownWithPartials } = require('./markdown-split');
|
|
134
|
+
return readMarkdownWithPartials(filePath) as string;
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Read and parse a JSON file, returning the default value on any error.
|
|
142
|
+
* @param filePath - Absolute path to the JSON file
|
|
143
|
+
* @param defaultValue - Value to return if read or parse fails
|
|
144
|
+
* @returns Parsed JSON object, or defaultValue on error
|
|
145
|
+
*/
|
|
146
|
+
function safeReadJSON(filePath: string, defaultValue: unknown = null): unknown {
|
|
147
|
+
try {
|
|
148
|
+
const raw: string = fs.readFileSync(filePath, 'utf-8');
|
|
149
|
+
return JSON.parse(raw);
|
|
150
|
+
} catch {
|
|
151
|
+
return defaultValue;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Extract content under a markdown heading (## or ### level).
|
|
157
|
+
* @param content - Full markdown content
|
|
158
|
+
* @param heading - Heading text to find (case-insensitive)
|
|
159
|
+
* @param level - Heading level (2 for ##, 3 for ###)
|
|
160
|
+
* @returns Section content (without the heading line), or null if not found
|
|
161
|
+
*/
|
|
162
|
+
function extractMarkdownSection(
|
|
163
|
+
content: string,
|
|
164
|
+
heading: string,
|
|
165
|
+
level: number = 2
|
|
166
|
+
): string | null {
|
|
167
|
+
const prefix: string = '#'.repeat(level);
|
|
168
|
+
const regex: RegExp = new RegExp(
|
|
169
|
+
`${prefix}\\s+${heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\n([\\s\\S]*?)(?=\\n${prefix}\\s|$)`,
|
|
170
|
+
'i'
|
|
171
|
+
);
|
|
172
|
+
const match: RegExpMatchArray | null = content.match(regex);
|
|
173
|
+
return match ? match[1] : null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ─── Levenshtein Distance ────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Compute the Levenshtein edit distance between two strings.
|
|
180
|
+
* @param s1 - First string
|
|
181
|
+
* @param s2 - Second string
|
|
182
|
+
* @returns Edit distance
|
|
183
|
+
*/
|
|
184
|
+
function levenshteinDistance(s1: string, s2: string): number {
|
|
185
|
+
const m: number = s1.length;
|
|
186
|
+
const n: number = s2.length;
|
|
187
|
+
const dp: number[][] = [];
|
|
188
|
+
for (let i = 0; i <= m; i++) {
|
|
189
|
+
dp[i] = [i];
|
|
190
|
+
for (let j = 1; j <= n; j++) {
|
|
191
|
+
if (i === 0) {
|
|
192
|
+
dp[i][j] = j;
|
|
193
|
+
} else {
|
|
194
|
+
dp[i][j] = 0;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
for (let i = 1; i <= m; i++) {
|
|
199
|
+
for (let j = 1; j <= n; j++) {
|
|
200
|
+
if (s1[i - 1] === s2[j - 1]) {
|
|
201
|
+
dp[i][j] = dp[i - 1][j - 1];
|
|
202
|
+
} else {
|
|
203
|
+
dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return dp[m][n];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Find the closest command name from a list of commands using Levenshtein distance.
|
|
212
|
+
* Returns null if no command is close enough (distance > threshold) or if input is invalid.
|
|
213
|
+
* @param input - User input to match
|
|
214
|
+
* @param commands - List of valid command names
|
|
215
|
+
* @returns The closest command name, or null if too different
|
|
216
|
+
*/
|
|
217
|
+
function findClosestCommand(input: string | null, commands: string[]): string | null {
|
|
218
|
+
if (!input || !commands || commands.length === 0) return null;
|
|
219
|
+
const lower: string = input.toLowerCase();
|
|
220
|
+
let best: string | null = null;
|
|
221
|
+
let bestDist: number = Infinity;
|
|
222
|
+
for (const cmd of commands) {
|
|
223
|
+
const dist: number = levenshteinDistance(lower, cmd.toLowerCase());
|
|
224
|
+
if (dist < bestDist) {
|
|
225
|
+
bestDist = dist;
|
|
226
|
+
best = cmd;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// Threshold: only suggest if distance is at most 3 (reasonable typo range)
|
|
230
|
+
const threshold: number = Math.max(3, Math.floor(best ? best.length / 3 : 3));
|
|
231
|
+
if (bestDist > threshold) return null;
|
|
232
|
+
return best;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ─── Phase Cache ─────────────────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
const _phaseCache: Map<string, string> = new Map();
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Clear the internal phase directory cache.
|
|
241
|
+
*/
|
|
242
|
+
function clearPhaseCache(): void {
|
|
243
|
+
_phaseCache.clear();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Known top-level config keys (used for unrecognized key warnings).
|
|
248
|
+
*/
|
|
249
|
+
const KNOWN_CONFIG_KEYS: Set<string> = new Set([
|
|
250
|
+
'model_profile',
|
|
251
|
+
'commit_docs',
|
|
252
|
+
'search_gitignored',
|
|
253
|
+
'branching_strategy',
|
|
254
|
+
'phase_branch_template',
|
|
255
|
+
'milestone_branch_template',
|
|
256
|
+
'base_branch',
|
|
257
|
+
'research',
|
|
258
|
+
'plan_checker',
|
|
259
|
+
'verifier',
|
|
260
|
+
'parallelization',
|
|
261
|
+
'code_review_enabled',
|
|
262
|
+
'code_review_timing',
|
|
263
|
+
'code_review_severity_gate',
|
|
264
|
+
'code_review_auto_fix_warnings',
|
|
265
|
+
'use_teams',
|
|
266
|
+
'team_timeout_minutes',
|
|
267
|
+
'max_concurrent_teammates',
|
|
268
|
+
'backend',
|
|
269
|
+
'backend_models',
|
|
270
|
+
'autonomous_mode',
|
|
271
|
+
// Nested section keys (objects)
|
|
272
|
+
'code_review',
|
|
273
|
+
'execution',
|
|
274
|
+
'git',
|
|
275
|
+
'planning',
|
|
276
|
+
'workflow',
|
|
277
|
+
'tracker',
|
|
278
|
+
'eval_config',
|
|
279
|
+
'ceremony',
|
|
280
|
+
'phase_cleanup',
|
|
281
|
+
'research_gates',
|
|
282
|
+
'confirmation_gates',
|
|
283
|
+
'timeouts',
|
|
284
|
+
'evolve',
|
|
285
|
+
// New-project command keys
|
|
286
|
+
'mode',
|
|
287
|
+
'depth',
|
|
288
|
+
// YOLO saved state keys
|
|
289
|
+
'_saved_research_gates',
|
|
290
|
+
'_saved_confirmation_gates',
|
|
291
|
+
'yolo_decision_log',
|
|
292
|
+
// Backend-specific keys
|
|
293
|
+
'overstory',
|
|
294
|
+
// Scheduler config
|
|
295
|
+
'scheduler',
|
|
296
|
+
// Superpowers config
|
|
297
|
+
'superpowers',
|
|
298
|
+
// Discussion config
|
|
299
|
+
'backend_roles',
|
|
300
|
+
'discussion',
|
|
301
|
+
// Citation gate
|
|
302
|
+
'citation_gate',
|
|
303
|
+
// Transitive citation gate
|
|
304
|
+
'transitive_citation_gate',
|
|
305
|
+
// Refinement loop
|
|
306
|
+
'refinement_loop',
|
|
307
|
+
// LLM fallback for phase completion (Spec 3B)
|
|
308
|
+
'phase_complete_llm_fallback',
|
|
309
|
+
// LLM fallback retry count with exponential backoff
|
|
310
|
+
'phase_complete_llm_fallback_retries',
|
|
311
|
+
// Drift score (Tier-2 #7 of Ouroboros integration)
|
|
312
|
+
'drift',
|
|
313
|
+
// Autopilot termination knobs (Tier-3 #10 of Ouroboros integration)
|
|
314
|
+
'autopilot',
|
|
315
|
+
// Evolve r9 + r14: surface-level keys consumed by Ouroboros r9 CLIs.
|
|
316
|
+
// research_staleness_days drives `gd health`'s STALE_RESEARCH blocker;
|
|
317
|
+
// `survey` carries staleness_days for `gd progress`'s freshness warn.
|
|
318
|
+
'research_staleness_days',
|
|
319
|
+
'survey',
|
|
320
|
+
// Plug-in / context-mode knowledge stats path
|
|
321
|
+
'token_profile',
|
|
322
|
+
// v0.4 Phase 1: orthogonal effort axis
|
|
323
|
+
'effort',
|
|
324
|
+
]);
|
|
325
|
+
|
|
326
|
+
// ─── Effort Axis (v0.4 Phase 1) ─────────────────────────────────────────────
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* v0.4 effort-scaled knobs. Single-knob scope by design (codex r6) — only
|
|
330
|
+
* `candidates_per_plan_phase` is wired in v0.4. The table is structured
|
|
331
|
+
* (object keyed by knob name) so v0.5+ can add knobs without changing the
|
|
332
|
+
* `resolveEffortKnob` signature.
|
|
333
|
+
*/
|
|
334
|
+
const EFFORT_PROFILES: Record<EffortAxisLevel, Record<EffortKnobName, number>> = {
|
|
335
|
+
thrifty: { candidates_per_plan_phase: 1 },
|
|
336
|
+
balanced: { candidates_per_plan_phase: 3 },
|
|
337
|
+
deep: { candidates_per_plan_phase: 7 },
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Return the integer value of an effort-scaled knob for the current
|
|
342
|
+
* `effort` setting in config. Defaults to 'balanced' when unset.
|
|
343
|
+
*
|
|
344
|
+
* @param config - GrdConfig (effort field optional, defaults to 'balanced')
|
|
345
|
+
* @param knob - Name of the effort-scaled knob to resolve
|
|
346
|
+
* @returns Integer value for the knob under the active effort level
|
|
347
|
+
*/
|
|
348
|
+
function resolveEffortKnob(config: GrdConfig, knob: EffortKnobName): number {
|
|
349
|
+
// Codex review P2: loadConfig preserves invalid `effort` values (warns but
|
|
350
|
+
// keeps them), so guard against EFFORT_PROFILES[level] being undefined.
|
|
351
|
+
// An unrecognized level falls back to 'balanced' rather than crashing.
|
|
352
|
+
const raw = config.effort;
|
|
353
|
+
const level: EffortAxisLevel =
|
|
354
|
+
raw !== undefined && Object.prototype.hasOwnProperty.call(EFFORT_PROFILES, raw)
|
|
355
|
+
? raw
|
|
356
|
+
: 'balanced';
|
|
357
|
+
return EFFORT_PROFILES[level][knob];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Load and merge .planning/config.json with default configuration values.
|
|
362
|
+
* @param cwd - Project working directory
|
|
363
|
+
* @returns Merged configuration object with all fields populated
|
|
364
|
+
*/
|
|
365
|
+
function loadConfig(cwd: string): GrdConfig {
|
|
366
|
+
const configPath: string = path.join(cwd, '.planning', 'config.json');
|
|
367
|
+
const defaultTimeouts: GrdTimeouts = {
|
|
368
|
+
jest_ms: 120000,
|
|
369
|
+
lint_ms: 60000,
|
|
370
|
+
format_ms: 60000,
|
|
371
|
+
consistency_ms: 30000,
|
|
372
|
+
tracker_gh_ms: 30000,
|
|
373
|
+
tracker_auth_ms: 10000,
|
|
374
|
+
backend_detect_ms: 10000,
|
|
375
|
+
autopilot_check_ms: 5000,
|
|
376
|
+
autoresearch_test_ms: 120000,
|
|
377
|
+
autoresearch_coverage_ms: 180000,
|
|
378
|
+
autoresearch_lint_ms: 60000,
|
|
379
|
+
backend_probe_ms: 5000,
|
|
380
|
+
discussion_git_ms: 10000,
|
|
381
|
+
overstory_probe_ms: 5000,
|
|
382
|
+
overstory_install_ms: 120000,
|
|
383
|
+
};
|
|
384
|
+
const defaults: GrdConfig = {
|
|
385
|
+
model_profile: 'balanced',
|
|
386
|
+
commit_docs: true,
|
|
387
|
+
search_gitignored: false,
|
|
388
|
+
branching_strategy: 'none',
|
|
389
|
+
phase_branch_template: 'grd/{milestone}/{phase}-{slug}',
|
|
390
|
+
milestone_branch_template: 'grd/{milestone}-{slug}',
|
|
391
|
+
base_branch: 'main',
|
|
392
|
+
research: true,
|
|
393
|
+
plan_checker: true,
|
|
394
|
+
verifier: true,
|
|
395
|
+
parallelization: true,
|
|
396
|
+
// Code review defaults
|
|
397
|
+
code_review_enabled: true,
|
|
398
|
+
code_review_timing: 'per_wave',
|
|
399
|
+
code_review_severity_gate: 'blocker',
|
|
400
|
+
code_review_auto_fix_warnings: false,
|
|
401
|
+
// Execution defaults
|
|
402
|
+
use_teams: false,
|
|
403
|
+
team_timeout_minutes: 30,
|
|
404
|
+
max_concurrent_teammates: 4,
|
|
405
|
+
// Backend config (pass-through, no defaults)
|
|
406
|
+
backend: undefined,
|
|
407
|
+
backend_models: undefined,
|
|
408
|
+
// Autonomous mode
|
|
409
|
+
autonomous_mode: false,
|
|
410
|
+
// Ceremony config
|
|
411
|
+
ceremony: undefined,
|
|
412
|
+
// Timeout defaults (ms)
|
|
413
|
+
timeouts: defaultTimeouts,
|
|
414
|
+
// Evolve config
|
|
415
|
+
evolve: undefined,
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
try {
|
|
419
|
+
const raw: string = fs.readFileSync(configPath, 'utf-8');
|
|
420
|
+
const parsed: Record<string, unknown> = JSON.parse(raw);
|
|
421
|
+
|
|
422
|
+
// Warn about unrecognized top-level config keys
|
|
423
|
+
for (const key of Object.keys(parsed)) {
|
|
424
|
+
if (!KNOWN_CONFIG_KEYS.has(key)) {
|
|
425
|
+
process.stderr.write(
|
|
426
|
+
`Warning: Unrecognized config key "${key}" in .planning/config.json\n`
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Warn about invalid model_profile values
|
|
432
|
+
const validProfiles: string[] = ['quality', 'balanced', 'budget'];
|
|
433
|
+
if (
|
|
434
|
+
parsed.model_profile !== undefined &&
|
|
435
|
+
!validProfiles.includes(parsed.model_profile as string)
|
|
436
|
+
) {
|
|
437
|
+
process.stderr.write(
|
|
438
|
+
`Warning: Invalid model_profile value "${parsed.model_profile}" in .planning/config.json. Valid values: ${validProfiles.join(', ')}\n`
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Warn about invalid effort values (v0.4 Phase 1)
|
|
443
|
+
const validEffortLevels: string[] = ['thrifty', 'balanced', 'deep'];
|
|
444
|
+
if (
|
|
445
|
+
parsed.effort !== undefined &&
|
|
446
|
+
!validEffortLevels.includes(parsed.effort as string)
|
|
447
|
+
) {
|
|
448
|
+
process.stderr.write(
|
|
449
|
+
`Warning: Invalid effort value "${parsed.effort}" in .planning/config.json. Valid values: ${validEffortLevels.join(', ')}\n`
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const get = (key: string, nested?: { section: string; field: string }): unknown => {
|
|
454
|
+
if (parsed[key] !== undefined) return parsed[key];
|
|
455
|
+
if (nested) {
|
|
456
|
+
const section = parsed[nested.section] as Record<string, unknown> | undefined;
|
|
457
|
+
if (section && section[nested.field] !== undefined) {
|
|
458
|
+
return section[nested.field];
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return undefined;
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
const parallelization: boolean = ((): boolean => {
|
|
465
|
+
const val: unknown = get('parallelization');
|
|
466
|
+
if (typeof val === 'boolean') return val;
|
|
467
|
+
if (typeof val === 'object' && val !== null && 'enabled' in (val as Record<string, unknown>))
|
|
468
|
+
return (val as Record<string, unknown>).enabled as boolean;
|
|
469
|
+
return defaults.parallelization;
|
|
470
|
+
})();
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
model_profile: (get('model_profile') ?? defaults.model_profile) as ModelProfileName,
|
|
474
|
+
// v0.4 Phase 1: effort axis. Invalid values pass through as warnings
|
|
475
|
+
// above; we keep raw here so resolveEffortKnob's default ('balanced')
|
|
476
|
+
// takes effect when the field is absent.
|
|
477
|
+
...(parsed.effort !== undefined ? { effort: parsed.effort as EffortAxisLevel } : {}),
|
|
478
|
+
commit_docs: (get('commit_docs', { section: 'planning', field: 'commit_docs' }) ??
|
|
479
|
+
defaults.commit_docs) as boolean,
|
|
480
|
+
search_gitignored: (get('search_gitignored', {
|
|
481
|
+
section: 'planning',
|
|
482
|
+
field: 'search_gitignored',
|
|
483
|
+
}) ?? defaults.search_gitignored) as boolean,
|
|
484
|
+
branching_strategy: (get('branching_strategy', {
|
|
485
|
+
section: 'git',
|
|
486
|
+
field: 'branching_strategy',
|
|
487
|
+
}) ?? defaults.branching_strategy) as string,
|
|
488
|
+
phase_branch_template: (get('phase_branch_template', {
|
|
489
|
+
section: 'git',
|
|
490
|
+
field: 'phase_branch_template',
|
|
491
|
+
}) ?? defaults.phase_branch_template) as string,
|
|
492
|
+
milestone_branch_template: (get('milestone_branch_template', {
|
|
493
|
+
section: 'git',
|
|
494
|
+
field: 'milestone_branch_template',
|
|
495
|
+
}) ?? defaults.milestone_branch_template) as string,
|
|
496
|
+
base_branch: (get('base_branch', { section: 'git', field: 'base_branch' }) ??
|
|
497
|
+
defaults.base_branch) as string,
|
|
498
|
+
research: (get('research', { section: 'workflow', field: 'research' }) ??
|
|
499
|
+
defaults.research) as boolean,
|
|
500
|
+
plan_checker: (get('plan_checker', { section: 'workflow', field: 'plan_check' }) ??
|
|
501
|
+
defaults.plan_checker) as boolean,
|
|
502
|
+
verifier: (get('verifier', { section: 'workflow', field: 'verifier' }) ??
|
|
503
|
+
defaults.verifier) as boolean,
|
|
504
|
+
parallelization,
|
|
505
|
+
// Code review config
|
|
506
|
+
code_review_enabled: (get('code_review_enabled', {
|
|
507
|
+
section: 'code_review',
|
|
508
|
+
field: 'enabled',
|
|
509
|
+
}) ?? defaults.code_review_enabled) as boolean,
|
|
510
|
+
code_review_timing: (get('code_review_timing', { section: 'code_review', field: 'timing' }) ??
|
|
511
|
+
defaults.code_review_timing) as string,
|
|
512
|
+
code_review_severity_gate: (get('code_review_severity_gate', {
|
|
513
|
+
section: 'code_review',
|
|
514
|
+
field: 'severity_gate',
|
|
515
|
+
}) ?? defaults.code_review_severity_gate) as string,
|
|
516
|
+
code_review_auto_fix_warnings: (get('code_review_auto_fix_warnings', {
|
|
517
|
+
section: 'code_review',
|
|
518
|
+
field: 'auto_fix_warnings',
|
|
519
|
+
}) ?? defaults.code_review_auto_fix_warnings) as boolean,
|
|
520
|
+
// Execution config
|
|
521
|
+
use_teams: (get('use_teams', { section: 'execution', field: 'use_teams' }) ??
|
|
522
|
+
defaults.use_teams) as boolean,
|
|
523
|
+
team_timeout_minutes: (get('team_timeout_minutes', {
|
|
524
|
+
section: 'execution',
|
|
525
|
+
field: 'team_timeout_minutes',
|
|
526
|
+
}) ?? defaults.team_timeout_minutes) as number,
|
|
527
|
+
max_concurrent_teammates: (get('max_concurrent_teammates', {
|
|
528
|
+
section: 'execution',
|
|
529
|
+
field: 'max_concurrent_teammates',
|
|
530
|
+
}) ?? defaults.max_concurrent_teammates) as number,
|
|
531
|
+
// Backend config (pass-through, no defaults)
|
|
532
|
+
backend: (parsed.backend || undefined) as string | undefined,
|
|
533
|
+
backend_models: (parsed.backend_models || undefined) as GrdConfig['backend_models'],
|
|
534
|
+
// Autonomous mode
|
|
535
|
+
autonomous_mode: (get('autonomous_mode') ?? false) as boolean,
|
|
536
|
+
// Ceremony config (pass-through)
|
|
537
|
+
ceremony: (parsed.ceremony || undefined) as GrdConfig['ceremony'],
|
|
538
|
+
// Evolve config
|
|
539
|
+
evolve: ((): GrdConfig['evolve'] => {
|
|
540
|
+
const e =
|
|
541
|
+
parsed.evolve && typeof parsed.evolve === 'object'
|
|
542
|
+
? (parsed.evolve as Record<string, unknown>)
|
|
543
|
+
: null;
|
|
544
|
+
if (!e) return undefined;
|
|
545
|
+
return {
|
|
546
|
+
auto_commit: (e.auto_commit ?? true) as boolean,
|
|
547
|
+
create_pr: (e.create_pr ?? true) as boolean,
|
|
548
|
+
// Tier-2 #8 auto-genome follow-up. Default off — opt-in.
|
|
549
|
+
auto_genome_snapshot: (e.auto_genome_snapshot ?? false) as boolean,
|
|
550
|
+
};
|
|
551
|
+
})(),
|
|
552
|
+
// Scheduler config (pass-through)
|
|
553
|
+
scheduler: (parsed.scheduler || undefined) as GrdConfig['scheduler'],
|
|
554
|
+
// Superpowers config (pass-through)
|
|
555
|
+
superpowers: (parsed.superpowers || undefined) as GrdConfig['superpowers'],
|
|
556
|
+
// Drift score config (pass-through; defaults applied in lib/drift.ts)
|
|
557
|
+
drift: (parsed.drift || undefined) as GrdConfig['drift'],
|
|
558
|
+
// Autopilot termination knobs (pass-through; Tier-3 #10)
|
|
559
|
+
autopilot: (parsed.autopilot || undefined) as GrdConfig['autopilot'],
|
|
560
|
+
// Codex r14 P2/P3: Ouroboros r9 staleness knobs need to survive
|
|
561
|
+
// loadConfig so cmdHealth + cmdProgress can read them. Cast the
|
|
562
|
+
// whole return at the bottom to widen the type.
|
|
563
|
+
...(parsed.research_staleness_days !== undefined
|
|
564
|
+
? { research_staleness_days: parsed.research_staleness_days as number }
|
|
565
|
+
: {}),
|
|
566
|
+
...(parsed.survey !== undefined ? { survey: parsed.survey } : {}),
|
|
567
|
+
// Backend roles config: validate each value against VALID_BACKENDS
|
|
568
|
+
backend_roles: ((): BackendRolesConfig | undefined => {
|
|
569
|
+
const raw = parsed.backend_roles;
|
|
570
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined;
|
|
571
|
+
const rawRoles = raw as Record<string, unknown>;
|
|
572
|
+
const validRoles: string[] = ['reviewer', 'brainstormer', 'verifier', 'executor'];
|
|
573
|
+
const result: BackendRolesConfig = {};
|
|
574
|
+
for (const [role, backendVal] of Object.entries(rawRoles)) {
|
|
575
|
+
if (!validRoles.includes(role)) {
|
|
576
|
+
process.stderr.write(
|
|
577
|
+
`Warning: Unrecognized backend_roles role "${role}" in .planning/config.json\n`
|
|
578
|
+
);
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
if (
|
|
582
|
+
typeof backendVal !== 'string' ||
|
|
583
|
+
!(VALID_BACKENDS as readonly string[]).includes(backendVal)
|
|
584
|
+
) {
|
|
585
|
+
process.stderr.write(
|
|
586
|
+
`Warning: Invalid backend "${String(backendVal)}" for role "${role}" in backend_roles — must be a valid BackendId\n`
|
|
587
|
+
);
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
(result as Record<string, string>)[role] = backendVal;
|
|
591
|
+
}
|
|
592
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
593
|
+
})(),
|
|
594
|
+
// Discussion config: validate fields, apply defaults
|
|
595
|
+
discussion: ((): DiscussionConfig | undefined => {
|
|
596
|
+
const raw = parsed.discussion;
|
|
597
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined;
|
|
598
|
+
const d = raw as Record<string, unknown>;
|
|
599
|
+
const enabled = typeof d.enabled === 'boolean' ? d.enabled : true;
|
|
600
|
+
if (!enabled) {
|
|
601
|
+
return {
|
|
602
|
+
enabled: false,
|
|
603
|
+
before_planning: typeof d.before_planning === 'boolean' ? d.before_planning : true,
|
|
604
|
+
before_execution: typeof d.before_execution === 'boolean' ? d.before_execution : false,
|
|
605
|
+
max_rounds: 2,
|
|
606
|
+
timeout_per_round_seconds: 180,
|
|
607
|
+
synthesizer: 'claude',
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
let max_rounds = typeof d.max_rounds === 'number' ? Math.round(d.max_rounds) : 2;
|
|
611
|
+
if (max_rounds < 1) max_rounds = 1;
|
|
612
|
+
if (max_rounds > 3) max_rounds = 3;
|
|
613
|
+
const timeout_per_round_seconds =
|
|
614
|
+
typeof d.timeout_per_round_seconds === 'number' && d.timeout_per_round_seconds > 0
|
|
615
|
+
? d.timeout_per_round_seconds
|
|
616
|
+
: 180;
|
|
617
|
+
const synthRaw = d.synthesizer;
|
|
618
|
+
const synthesizer: string =
|
|
619
|
+
typeof synthRaw === 'string' && (VALID_BACKENDS as readonly string[]).includes(synthRaw)
|
|
620
|
+
? synthRaw
|
|
621
|
+
: 'claude';
|
|
622
|
+
return {
|
|
623
|
+
enabled,
|
|
624
|
+
before_planning: typeof d.before_planning === 'boolean' ? d.before_planning : true,
|
|
625
|
+
before_execution: typeof d.before_execution === 'boolean' ? d.before_execution : false,
|
|
626
|
+
max_rounds,
|
|
627
|
+
timeout_per_round_seconds,
|
|
628
|
+
synthesizer: synthesizer as DiscussionConfig['synthesizer'],
|
|
629
|
+
};
|
|
630
|
+
})(),
|
|
631
|
+
// Citation gate (optional boolean, default: false)
|
|
632
|
+
citation_gate: typeof parsed.citation_gate === 'boolean' ? parsed.citation_gate : false,
|
|
633
|
+
// Transitive citation gate (optional boolean, default: false)
|
|
634
|
+
transitive_citation_gate:
|
|
635
|
+
typeof parsed.transitive_citation_gate === 'boolean'
|
|
636
|
+
? parsed.transitive_citation_gate
|
|
637
|
+
: false,
|
|
638
|
+
// Refinement loop (optional boolean, default: false)
|
|
639
|
+
refinement_loop: typeof parsed.refinement_loop === 'boolean' ? parsed.refinement_loop : false,
|
|
640
|
+
// LLM fallback for phase completion (Spec 3B, optional boolean, default: false)
|
|
641
|
+
phase_complete_llm_fallback:
|
|
642
|
+
typeof parsed.phase_complete_llm_fallback === 'boolean'
|
|
643
|
+
? parsed.phase_complete_llm_fallback
|
|
644
|
+
: undefined,
|
|
645
|
+
// LLM fallback retry count (optional number, default: 0)
|
|
646
|
+
phase_complete_llm_fallback_retries:
|
|
647
|
+
typeof parsed.phase_complete_llm_fallback_retries === 'number'
|
|
648
|
+
? Math.max(0, parsed.phase_complete_llm_fallback_retries)
|
|
649
|
+
: undefined,
|
|
650
|
+
// Timeouts config
|
|
651
|
+
timeouts: ((): GrdTimeouts => {
|
|
652
|
+
const t: Record<string, unknown> =
|
|
653
|
+
parsed.timeouts && typeof parsed.timeouts === 'object'
|
|
654
|
+
? (parsed.timeouts as Record<string, unknown>)
|
|
655
|
+
: {};
|
|
656
|
+
const d: GrdTimeouts = defaultTimeouts;
|
|
657
|
+
return {
|
|
658
|
+
jest_ms: (t.jest_ms ?? d.jest_ms) as number,
|
|
659
|
+
lint_ms: (t.lint_ms ?? d.lint_ms) as number,
|
|
660
|
+
format_ms: (t.format_ms ?? d.format_ms) as number,
|
|
661
|
+
consistency_ms: (t.consistency_ms ?? d.consistency_ms) as number,
|
|
662
|
+
tracker_gh_ms: (t.tracker_gh_ms ?? d.tracker_gh_ms) as number,
|
|
663
|
+
tracker_auth_ms: (t.tracker_auth_ms ?? d.tracker_auth_ms) as number,
|
|
664
|
+
backend_detect_ms: (t.backend_detect_ms ?? d.backend_detect_ms) as number,
|
|
665
|
+
autopilot_check_ms: (t.autopilot_check_ms ?? d.autopilot_check_ms) as number,
|
|
666
|
+
autoresearch_test_ms: (t.autoresearch_test_ms ?? d.autoresearch_test_ms) as number,
|
|
667
|
+
autoresearch_coverage_ms: (t.autoresearch_coverage_ms ?? d.autoresearch_coverage_ms) as number,
|
|
668
|
+
autoresearch_lint_ms: (t.autoresearch_lint_ms ?? d.autoresearch_lint_ms) as number,
|
|
669
|
+
backend_probe_ms: (t.backend_probe_ms ?? d.backend_probe_ms) as number,
|
|
670
|
+
discussion_git_ms: (t.discussion_git_ms ?? d.discussion_git_ms) as number,
|
|
671
|
+
overstory_probe_ms: (t.overstory_probe_ms ?? d.overstory_probe_ms) as number,
|
|
672
|
+
overstory_install_ms: (t.overstory_install_ms ?? d.overstory_install_ms) as number,
|
|
673
|
+
};
|
|
674
|
+
})(),
|
|
675
|
+
};
|
|
676
|
+
} catch {
|
|
677
|
+
return defaults;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Check if a file path is git-ignored via git check-ignore.
|
|
683
|
+
* @param cwd - Project working directory
|
|
684
|
+
* @param targetPath - Path to check against .gitignore rules
|
|
685
|
+
* @returns True if the path is git-ignored, false otherwise
|
|
686
|
+
*/
|
|
687
|
+
function isGitIgnored(cwd: string, targetPath: string): boolean {
|
|
688
|
+
if (targetPath.includes('\0')) return false;
|
|
689
|
+
try {
|
|
690
|
+
execFileSync('git', ['check-ignore', '-q', '--', targetPath], {
|
|
691
|
+
cwd,
|
|
692
|
+
stdio: 'pipe',
|
|
693
|
+
});
|
|
694
|
+
return true;
|
|
695
|
+
} catch {
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Execute a git command with whitelist enforcement for security.
|
|
702
|
+
* @param cwd - Project working directory
|
|
703
|
+
* @param args - Git command arguments (first element is the subcommand)
|
|
704
|
+
* @param opts - Options object
|
|
705
|
+
* @returns Command result with exit code and output
|
|
706
|
+
*/
|
|
707
|
+
function execGit(
|
|
708
|
+
cwd: string,
|
|
709
|
+
args: string[],
|
|
710
|
+
opts: { allowBlocked?: boolean } = {}
|
|
711
|
+
): ExecGitResult {
|
|
712
|
+
// Git operation whitelist enforcement
|
|
713
|
+
const subcommand: string | undefined = args[0];
|
|
714
|
+
if (subcommand && GIT_BLOCKED_COMMANDS.has(subcommand) && !opts.allowBlocked) {
|
|
715
|
+
return {
|
|
716
|
+
exitCode: 1,
|
|
717
|
+
stdout: '',
|
|
718
|
+
stderr: `Blocked: "git ${subcommand}" is not allowed by the GRD security policy. Pass { allowBlocked: true } to override.`,
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
if (!opts.allowBlocked) {
|
|
722
|
+
for (const arg of args) {
|
|
723
|
+
if (GIT_BLOCKED_FLAGS.has(arg)) {
|
|
724
|
+
return {
|
|
725
|
+
exitCode: 1,
|
|
726
|
+
stdout: '',
|
|
727
|
+
stderr: `Blocked: flag "${arg}" is not allowed by the GRD security policy. Pass { allowBlocked: true } to override.`,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
try {
|
|
733
|
+
const stdout: string = execFileSync('git', args, {
|
|
734
|
+
cwd,
|
|
735
|
+
stdio: 'pipe',
|
|
736
|
+
encoding: 'utf-8',
|
|
737
|
+
});
|
|
738
|
+
return { exitCode: 0, stdout: stdout.trim(), stderr: '' };
|
|
739
|
+
} catch (err: unknown) {
|
|
740
|
+
const gitErr = err as { status?: number; stdout?: Buffer | string; stderr?: Buffer | string };
|
|
741
|
+
return {
|
|
742
|
+
exitCode: gitErr.status ?? 1,
|
|
743
|
+
stdout: (gitErr.stdout ?? '').toString().trim(),
|
|
744
|
+
stderr: (gitErr.stderr ?? '').toString().trim(),
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Normalize a phase identifier by zero-padding and validating format.
|
|
751
|
+
* @param phase - Phase identifier (e.g., "7", "07", "07.1")
|
|
752
|
+
* @returns Normalized phase name with zero-padded number (e.g., "07", "07.1")
|
|
753
|
+
* @throws If phase is not a string or contains path traversal/directory separators
|
|
754
|
+
*/
|
|
755
|
+
function normalizePhaseName(phase: string): string {
|
|
756
|
+
if (typeof phase !== 'string') throw new Error('Phase must be a string');
|
|
757
|
+
if (phase.includes('..')) throw new Error('Phase name must not contain path traversal (..)');
|
|
758
|
+
if (phase.includes('/') || phase.includes('\\'))
|
|
759
|
+
throw new Error('Phase name must not contain directory separators');
|
|
760
|
+
const match: RegExpMatchArray | null = phase.match(/^(\d+(?:\.\d+)?)/);
|
|
761
|
+
if (!match) return phase;
|
|
762
|
+
const num: string = match[1];
|
|
763
|
+
const parts: string[] = num.split('.');
|
|
764
|
+
const padded: string = parts[0].padStart(2, '0');
|
|
765
|
+
return parts.length > 1 ? `${padded}.${parts[1]}` : padded;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const CODE_EXTENSIONS: Set<string> = new Set([
|
|
769
|
+
'.ts',
|
|
770
|
+
'.js',
|
|
771
|
+
'.py',
|
|
772
|
+
'.go',
|
|
773
|
+
'.rs',
|
|
774
|
+
'.swift',
|
|
775
|
+
'.java',
|
|
776
|
+
]);
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Recursively find code files up to a maximum depth, capped at 5 results.
|
|
780
|
+
* @param dir - Directory to search in
|
|
781
|
+
* @param maxDepth - Maximum directory depth to recurse into
|
|
782
|
+
* @param found - Accumulator array of found file paths
|
|
783
|
+
* @param depth - Current recursion depth
|
|
784
|
+
* @returns Array of absolute paths to code files found
|
|
785
|
+
*/
|
|
786
|
+
function findCodeFiles(dir: string, maxDepth: number, found: string[], depth: number): string[] {
|
|
787
|
+
if (depth > maxDepth || found.length >= 5) return found;
|
|
788
|
+
let entries: { name: string; isDirectory: () => boolean; isFile: () => boolean }[];
|
|
789
|
+
try {
|
|
790
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
791
|
+
} catch {
|
|
792
|
+
return found;
|
|
793
|
+
}
|
|
794
|
+
for (const entry of entries) {
|
|
795
|
+
if (found.length >= 5) break;
|
|
796
|
+
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
797
|
+
const fullPath: string = path.join(dir, entry.name);
|
|
798
|
+
if (entry.isDirectory()) {
|
|
799
|
+
findCodeFiles(fullPath, maxDepth, found, depth + 1);
|
|
800
|
+
} else if (entry.isFile()) {
|
|
801
|
+
const ext: string = path.extname(entry.name).toLowerCase();
|
|
802
|
+
if (CODE_EXTENSIONS.has(ext)) {
|
|
803
|
+
found.push(fullPath);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return found;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// ─── Input Validation ────────────────────────────────────────────────────────
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Validate phase name format strictly, rejecting path traversal and invalid characters.
|
|
814
|
+
* @param phase - Phase name to validate
|
|
815
|
+
* @returns The validated phase name if valid
|
|
816
|
+
* @throws If phase format is invalid, contains traversal, null bytes, or separators
|
|
817
|
+
*/
|
|
818
|
+
function validatePhaseName(phase: string): string {
|
|
819
|
+
if (typeof phase !== 'string') throw new Error('Phase must be a string');
|
|
820
|
+
if (phase.includes('..')) throw new Error('Phase name must not contain path traversal (..)');
|
|
821
|
+
if (phase.includes('/') || phase.includes('\\'))
|
|
822
|
+
throw new Error('Phase name must not contain directory separators');
|
|
823
|
+
if (phase.includes('\0')) throw new Error('Phase name must not contain null bytes');
|
|
824
|
+
if (!/^\d+(?:\.\d+)?(?:-[a-zA-Z0-9-]+)?$/.test(phase)) {
|
|
825
|
+
throw new Error(
|
|
826
|
+
'Invalid phase name format: must be digits with optional decimal and kebab-case suffix'
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
return phase;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Validate that a file path does not escape the project directory.
|
|
834
|
+
* @param filePath - File path to validate (relative or absolute)
|
|
835
|
+
* @param cwd - Project working directory used as security boundary
|
|
836
|
+
* @returns The validated file path if safe
|
|
837
|
+
* @throws If path is not a string, contains null bytes, or escapes project directory
|
|
838
|
+
*/
|
|
839
|
+
function validateFilePath(filePath: string, cwd: string): string {
|
|
840
|
+
if (typeof filePath !== 'string') throw new Error('File path must be a string');
|
|
841
|
+
if (filePath.includes('\0')) throw new Error('File path must not contain null bytes');
|
|
842
|
+
const resolved: string = path.resolve(cwd, filePath);
|
|
843
|
+
if (!resolved.startsWith(cwd)) throw new Error('File path must not escape project directory');
|
|
844
|
+
return filePath;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Validate git ref format, preventing flag injection and path traversal.
|
|
849
|
+
* @param ref - Git reference to validate (commit hash, branch name, tag)
|
|
850
|
+
* @returns The validated git ref if valid
|
|
851
|
+
* @throws If ref is invalid, starts with dash, contains traversal, or has invalid characters
|
|
852
|
+
*/
|
|
853
|
+
function validateGitRef(ref: string): string {
|
|
854
|
+
if (typeof ref !== 'string') throw new Error('Git ref must be a string');
|
|
855
|
+
if (ref.startsWith('-')) throw new Error('Git ref must not start with a dash (flag injection)');
|
|
856
|
+
if (ref.includes('..')) throw new Error('Git ref must not contain path traversal (..)');
|
|
857
|
+
if (!/^[a-zA-Z0-9._\-/~^]+$/.test(ref)) throw new Error('Git ref contains invalid characters');
|
|
858
|
+
if (ref.length > 256) throw new Error('Git ref too long');
|
|
859
|
+
return ref;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// ─── CLI Argument Validation ──────────────────────────────────────────────────
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Validate CLI phase number argument, ensuring it is present and well-formed.
|
|
866
|
+
* @param phase - Phase number from CLI arguments
|
|
867
|
+
* @returns The validated phase number
|
|
868
|
+
* @throws If phase is missing or not in valid format (digits with optional decimal)
|
|
869
|
+
*/
|
|
870
|
+
function validatePhaseArg(phase: string): string {
|
|
871
|
+
if (phase == null || phase === '') throw new Error('Phase number is required');
|
|
872
|
+
if (typeof phase !== 'string') throw new Error('Phase number is required');
|
|
873
|
+
if (!/^\d+(?:\.\d+)?(?:-[a-zA-Z0-9-]+)?$/.test(phase)) {
|
|
874
|
+
throw new Error('Invalid phase number: must be digits with optional decimal (e.g., 01, 02.1)');
|
|
875
|
+
}
|
|
876
|
+
return phase;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Validate CLI file path argument, ensuring it is present and does not escape project.
|
|
881
|
+
* @param filePath - File path from CLI arguments
|
|
882
|
+
* @param cwd - Project working directory used as security boundary
|
|
883
|
+
* @returns The validated file path
|
|
884
|
+
* @throws If file path is missing or escapes project directory
|
|
885
|
+
*/
|
|
886
|
+
function validateFileArg(filePath: string, cwd: string): string {
|
|
887
|
+
if (filePath == null || filePath === '') throw new Error('File path is required');
|
|
888
|
+
return validateFilePath(filePath, cwd);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Validate CLI subcommand against a list of valid subcommands.
|
|
893
|
+
* @param sub - Subcommand string from CLI arguments
|
|
894
|
+
* @param validSubs - Array of valid subcommand names
|
|
895
|
+
* @param parentCmd - Parent command name for error messages
|
|
896
|
+
* @returns The validated subcommand
|
|
897
|
+
* @throws If subcommand is missing or not in the valid list
|
|
898
|
+
*/
|
|
899
|
+
function validateSubcommand(sub: string, validSubs: string[], parentCmd: string): string {
|
|
900
|
+
if (sub == null || sub === '') {
|
|
901
|
+
throw new Error(`Subcommand required for '${parentCmd}'. Available: ${validSubs.join(', ')}`);
|
|
902
|
+
}
|
|
903
|
+
if (!validSubs.includes(sub)) {
|
|
904
|
+
throw new Error(
|
|
905
|
+
`Unknown ${parentCmd} subcommand: '${sub}'. Available: ${validSubs.join(', ')}`
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
return sub;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Validate that a required CLI argument is present and non-empty.
|
|
913
|
+
* @param value - Argument value to check
|
|
914
|
+
* @param argName - Name of the argument for error messages
|
|
915
|
+
* @returns The validated value
|
|
916
|
+
* @throws If value is null, undefined, or empty string
|
|
917
|
+
*/
|
|
918
|
+
function validateRequiredArg(value: unknown, argName: string): unknown {
|
|
919
|
+
if (value == null || value === '') throw new Error(`${argName} is required`);
|
|
920
|
+
return value;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// ─── Output ──────────────────────────────────────────────────────────────────
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* Write JSON or raw output to stdout and exit with code 0.
|
|
927
|
+
* @param result - Result object to serialize as JSON
|
|
928
|
+
* @param raw - If true, output rawValue as plain text instead of JSON
|
|
929
|
+
* @param rawValue - Plain text value to output when raw is true
|
|
930
|
+
*/
|
|
931
|
+
function output(result: unknown, raw: boolean, rawValue?: unknown): never {
|
|
932
|
+
if (raw && rawValue !== undefined) {
|
|
933
|
+
process.stdout.write(String(rawValue));
|
|
934
|
+
} else {
|
|
935
|
+
process.stdout.write(JSON.stringify(result, null, 2));
|
|
936
|
+
}
|
|
937
|
+
process.exit(0);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Write error message to stderr and exit with code 1.
|
|
942
|
+
* @param message - Error message to display
|
|
943
|
+
*/
|
|
944
|
+
function error(message: string): never {
|
|
945
|
+
process.stderr.write('Error: ' + message + '\n');
|
|
946
|
+
process.exit(1);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* Write debug message to stderr when GRD_DEBUG environment variable is set.
|
|
951
|
+
* No-op in normal operation; enabled by setting GRD_DEBUG=1 (or any truthy value).
|
|
952
|
+
* @param message - Debug message to display
|
|
953
|
+
* @param data - Optional data to JSON-stringify alongside the message
|
|
954
|
+
*/
|
|
955
|
+
function debugLog(message: string, data?: unknown): void {
|
|
956
|
+
if (!process.env.GRD_DEBUG) return;
|
|
957
|
+
const prefix: string = '[grd:debug] ';
|
|
958
|
+
if (data !== undefined) {
|
|
959
|
+
process.stderr.write(prefix + message + ' ' + JSON.stringify(data) + '\n');
|
|
960
|
+
} else {
|
|
961
|
+
process.stderr.write(prefix + message + '\n');
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// ─── Shared Cache Factory ─────────────────────────────────────────────────────
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* Create a run-scoped file content cache.
|
|
969
|
+
* Returns a cache object with `get`, `init`, and `reset` methods.
|
|
970
|
+
* Modules can use this to avoid redundant disk reads within a single CLI invocation.
|
|
971
|
+
*
|
|
972
|
+
* Usage pattern:
|
|
973
|
+
* const cache = createRunCache();
|
|
974
|
+
* cache.init(); // activate caching for this run
|
|
975
|
+
* cache.get(p, reader) // returns cached value or calls reader(p) and stores result
|
|
976
|
+
* cache.reset(); // deactivate and clear
|
|
977
|
+
*
|
|
978
|
+
* @returns Cache object with init, reset, and get methods
|
|
979
|
+
*/
|
|
980
|
+
function createRunCache(): RunCache {
|
|
981
|
+
let _map: Map<string, unknown> | null = null;
|
|
982
|
+
return {
|
|
983
|
+
/** Activate the cache (creates a new Map). */
|
|
984
|
+
init(): void {
|
|
985
|
+
_map = new Map();
|
|
986
|
+
},
|
|
987
|
+
/** Deactivate and clear the cache. */
|
|
988
|
+
reset(): void {
|
|
989
|
+
_map = null;
|
|
990
|
+
},
|
|
991
|
+
/**
|
|
992
|
+
* Get a cached value, or compute it with `reader(key)` and store the result.
|
|
993
|
+
* Falls back to calling `reader(key)` directly if the cache is not active.
|
|
994
|
+
* @param key - Cache key (typically a file path)
|
|
995
|
+
* @param reader - Function to produce the value if not cached
|
|
996
|
+
* @returns The cached or freshly computed value
|
|
997
|
+
*/
|
|
998
|
+
get(key: string, reader: (key: string) => unknown): unknown {
|
|
999
|
+
if (!_map) return reader(key);
|
|
1000
|
+
if (!_map.has(key)) _map.set(key, reader(key));
|
|
1001
|
+
return _map.get(key);
|
|
1002
|
+
},
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// ─── Phase Directory Utilities ────────────────────────────────────────────────
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Find a phase directory name inside `phasesDir` that matches `phaseArg`.
|
|
1010
|
+
* Matches by exact normalized name or by prefix `<normalized>-`.
|
|
1011
|
+
* @param phasesDir - Absolute path to the phases directory
|
|
1012
|
+
* @param phaseArg - Phase identifier (e.g., '1', '01', '1.1')
|
|
1013
|
+
* @returns The matching directory name, or null if not found
|
|
1014
|
+
*/
|
|
1015
|
+
function findPhaseDir(phasesDir: string, phaseArg: string): string | null {
|
|
1016
|
+
const normalized: string = normalizePhaseName(phaseArg);
|
|
1017
|
+
try {
|
|
1018
|
+
const entries: { name: string; isDirectory: () => boolean }[] = fs.readdirSync(phasesDir, {
|
|
1019
|
+
withFileTypes: true,
|
|
1020
|
+
});
|
|
1021
|
+
const dirs: string[] = entries
|
|
1022
|
+
.filter((e: { isDirectory: () => boolean }) => e.isDirectory())
|
|
1023
|
+
.map((e: { name: string }) => e.name)
|
|
1024
|
+
.sort();
|
|
1025
|
+
return dirs.find((d: string) => d.startsWith(normalized + '-') || d === normalized) || null;
|
|
1026
|
+
} catch {
|
|
1027
|
+
return null;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* Parse a phase number from a directory name or plain string.
|
|
1033
|
+
* Handles formats like '01-feature-name', '1.2-sub', '1', etc.
|
|
1034
|
+
* @param str - Directory name or phase string to parse
|
|
1035
|
+
* @returns The numeric phase number as a string (e.g. '1', '1.2'), or null
|
|
1036
|
+
*/
|
|
1037
|
+
function parsePhaseNumber(str: string): string | null {
|
|
1038
|
+
if (!str) return null;
|
|
1039
|
+
const match: RegExpMatchArray | null = str.match(/^(\d+(?:\.\d+)?)/);
|
|
1040
|
+
return match ? match[1] : null;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// ─── Directory Walking ────────────────────────────────────────────────────────
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Recursively collect JavaScript file paths under `rootDir`.
|
|
1047
|
+
* Skips `node_modules`, `.git`, `.planning`, and any paths matching `excludePatterns`.
|
|
1048
|
+
* @param rootDir - Root directory to walk (returned paths are relative to this)
|
|
1049
|
+
* @param excludePatterns - Substrings; any relative path containing one is skipped
|
|
1050
|
+
* @returns Relative paths of all .js files found
|
|
1051
|
+
*/
|
|
1052
|
+
function walkJsFiles(rootDir: string, excludePatterns: string[] = []): string[] {
|
|
1053
|
+
const results: string[] = [];
|
|
1054
|
+
_walkJsDir(rootDir, rootDir, results, excludePatterns);
|
|
1055
|
+
return results;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function _walkJsDir(
|
|
1059
|
+
rootDir: string,
|
|
1060
|
+
currentDir: string,
|
|
1061
|
+
results: string[],
|
|
1062
|
+
excludePatterns: string[]
|
|
1063
|
+
): void {
|
|
1064
|
+
let entries: { name: string; isDirectory: () => boolean; isFile: () => boolean }[];
|
|
1065
|
+
try {
|
|
1066
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
1067
|
+
} catch {
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
for (const entry of entries) {
|
|
1071
|
+
const fullPath: string = path.join(currentDir, entry.name);
|
|
1072
|
+
const relPath: string = path.relative(rootDir, fullPath);
|
|
1073
|
+
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === '.planning') {
|
|
1074
|
+
continue;
|
|
1075
|
+
}
|
|
1076
|
+
if (excludePatterns.some((p: string) => relPath.includes(p))) continue;
|
|
1077
|
+
if (entry.isDirectory()) {
|
|
1078
|
+
_walkJsDir(rootDir, fullPath, results, excludePatterns);
|
|
1079
|
+
} else if (entry.isFile() && entry.name.endsWith('.js')) {
|
|
1080
|
+
results.push(relPath);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// ─── Compound Helpers ────────────────────────────────────────────────────────
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* Resolve the model name for a given agent type from project configuration.
|
|
1089
|
+
* @param cwd - Project working directory
|
|
1090
|
+
* @param agentType - Agent type key (e.g., 'grd-executor', 'grd-planner')
|
|
1091
|
+
* @returns Model name (e.g., 'opus', 'sonnet', 'haiku')
|
|
1092
|
+
*/
|
|
1093
|
+
function resolveModelInternal(cwd: string, agentType: string): string {
|
|
1094
|
+
const config: GrdConfig = loadConfig(cwd);
|
|
1095
|
+
const profile: string = config.model_profile || 'balanced';
|
|
1096
|
+
const agentModels: Record<ModelProfileName, ModelTier> | undefined = MODEL_PROFILES[agentType];
|
|
1097
|
+
if (!agentModels) {
|
|
1098
|
+
// Unknown agent type: resolve 'sonnet' tier through backend
|
|
1099
|
+
const backend: unknown = detectBackend(cwd);
|
|
1100
|
+
return resolveBackendModel(backend, 'sonnet', config, cwd) as string;
|
|
1101
|
+
}
|
|
1102
|
+
const tier: ModelTier =
|
|
1103
|
+
agentModels[profile as ModelProfileName] || agentModels['balanced'] || 'sonnet';
|
|
1104
|
+
const backend: unknown = detectBackend(cwd);
|
|
1105
|
+
return resolveBackendModel(backend, tier, config, cwd) as string;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* Find a phase directory and enumerate its plans, summaries, and metadata.
|
|
1110
|
+
* @param cwd - Project working directory
|
|
1111
|
+
* @param phase - Phase identifier to search for
|
|
1112
|
+
* @returns Phase info object with directory, plans, summaries, and flags, or null if not found
|
|
1113
|
+
*/
|
|
1114
|
+
function findPhaseInternal(cwd: string, phase: string): PhaseInfo | null {
|
|
1115
|
+
if (!phase) return null;
|
|
1116
|
+
|
|
1117
|
+
const phasesDir: string = getPhasesDirPath(cwd) as string;
|
|
1118
|
+
const normalized: string = normalizePhaseName(phase);
|
|
1119
|
+
|
|
1120
|
+
try {
|
|
1121
|
+
const entries: { name: string; isDirectory: () => boolean }[] = fs.readdirSync(phasesDir, {
|
|
1122
|
+
withFileTypes: true,
|
|
1123
|
+
});
|
|
1124
|
+
const dirs: string[] = entries
|
|
1125
|
+
.filter((e: { isDirectory: () => boolean }) => e.isDirectory())
|
|
1126
|
+
.map((e: { name: string }) => e.name)
|
|
1127
|
+
.sort();
|
|
1128
|
+
const match: string | undefined = dirs.find(
|
|
1129
|
+
(d: string) => d.startsWith(normalized + '-') || d === normalized
|
|
1130
|
+
);
|
|
1131
|
+
if (!match) return null;
|
|
1132
|
+
|
|
1133
|
+
const dirMatch: RegExpMatchArray | null = match.match(/^(\d+(?:\.\d+)?)-?(.*)/);
|
|
1134
|
+
const phaseNumber: string = dirMatch ? dirMatch[1] : normalized;
|
|
1135
|
+
const phaseName: string | null = dirMatch && dirMatch[2] ? dirMatch[2] : null;
|
|
1136
|
+
const phaseDir: string = path.join(phasesDir, match);
|
|
1137
|
+
const phaseFiles: string[] = fs.readdirSync(phaseDir);
|
|
1138
|
+
|
|
1139
|
+
const plans: string[] = phaseFiles
|
|
1140
|
+
.filter((f: string) => f.endsWith('-PLAN.md') || f === 'PLAN.md')
|
|
1141
|
+
.sort();
|
|
1142
|
+
const summaries: string[] = phaseFiles
|
|
1143
|
+
.filter((f: string) => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md')
|
|
1144
|
+
.sort();
|
|
1145
|
+
const hasResearch: boolean = phaseFiles.some(
|
|
1146
|
+
(f: string) => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md'
|
|
1147
|
+
);
|
|
1148
|
+
const hasContext: boolean = phaseFiles.some(
|
|
1149
|
+
(f: string) => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md'
|
|
1150
|
+
);
|
|
1151
|
+
const hasVerification: boolean = phaseFiles.some(
|
|
1152
|
+
(f: string) => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md'
|
|
1153
|
+
);
|
|
1154
|
+
|
|
1155
|
+
// Determine incomplete plans (plans without matching summaries)
|
|
1156
|
+
const completedPlanIds: Set<string> = new Set(
|
|
1157
|
+
summaries.map((s: string) => s.replace('-SUMMARY.md', '').replace('SUMMARY.md', ''))
|
|
1158
|
+
);
|
|
1159
|
+
const incompletePlans: string[] = plans.filter((p: string) => {
|
|
1160
|
+
const planId: string = p.replace('-PLAN.md', '').replace('PLAN.md', '');
|
|
1161
|
+
return !completedPlanIds.has(planId);
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
// Check if phase exists in ROADMAP.md (non-blocking consistency check)
|
|
1165
|
+
let consistencyWarning: string | null = null;
|
|
1166
|
+
try {
|
|
1167
|
+
const roadmapPath: string = path.join(cwd, '.planning', 'ROADMAP.md');
|
|
1168
|
+
const roadmapContent: string = fs.readFileSync(roadmapPath, 'utf-8');
|
|
1169
|
+
const unpadded: string = String(parseInt(phaseNumber, 10));
|
|
1170
|
+
const phasePattern: RegExp = new RegExp(
|
|
1171
|
+
`#{2,}\\s*Phase\\s+(?:${phaseNumber}|${unpadded})\\s*:`,
|
|
1172
|
+
'i'
|
|
1173
|
+
);
|
|
1174
|
+
if (!phasePattern.test(roadmapContent)) {
|
|
1175
|
+
consistencyWarning = `Phase ${phaseNumber} found on disk but not in ROADMAP.md — may be from a previous milestone`;
|
|
1176
|
+
}
|
|
1177
|
+
} catch {
|
|
1178
|
+
// ROADMAP.md may not exist yet; skip check
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
return {
|
|
1182
|
+
found: true,
|
|
1183
|
+
directory: path.relative(cwd, path.join(phasesDir, match)),
|
|
1184
|
+
phase_number: phaseNumber,
|
|
1185
|
+
phase_name: phaseName,
|
|
1186
|
+
phase_slug: phaseName
|
|
1187
|
+
? phaseName
|
|
1188
|
+
.toLowerCase()
|
|
1189
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
1190
|
+
.replace(/^-+|-+$/g, '')
|
|
1191
|
+
: null,
|
|
1192
|
+
plans,
|
|
1193
|
+
summaries,
|
|
1194
|
+
incomplete_plans: incompletePlans,
|
|
1195
|
+
has_research: hasResearch,
|
|
1196
|
+
has_context: hasContext,
|
|
1197
|
+
has_verification: hasVerification,
|
|
1198
|
+
consistency_warning: consistencyWarning,
|
|
1199
|
+
};
|
|
1200
|
+
} catch {
|
|
1201
|
+
return null;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* Check if a path exists on the filesystem.
|
|
1207
|
+
* @param cwd - Project working directory (used for resolving relative paths)
|
|
1208
|
+
* @param targetPath - Path to check, relative or absolute
|
|
1209
|
+
* @returns True if the path exists, false otherwise
|
|
1210
|
+
*/
|
|
1211
|
+
function pathExistsInternal(cwd: string, targetPath: string): boolean {
|
|
1212
|
+
const fullPath: string = path.isAbsolute(targetPath) ? targetPath : path.join(cwd, targetPath);
|
|
1213
|
+
try {
|
|
1214
|
+
fs.statSync(fullPath);
|
|
1215
|
+
return true;
|
|
1216
|
+
} catch {
|
|
1217
|
+
return false;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* Generate a kebab-case slug from text, stripping non-alphanumeric characters.
|
|
1223
|
+
* @param text - Input text to slugify
|
|
1224
|
+
* @returns Kebab-case slug, or null if text is falsy
|
|
1225
|
+
*/
|
|
1226
|
+
function generateSlugInternal(text: string): string | null {
|
|
1227
|
+
if (!text) return null;
|
|
1228
|
+
return text
|
|
1229
|
+
.toLowerCase()
|
|
1230
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
1231
|
+
.replace(/^-+|-+$/g, '');
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/**
|
|
1235
|
+
* Strip shipped milestone sections wrapped in <details> blocks.
|
|
1236
|
+
* These contain archived milestone content that should not contaminate
|
|
1237
|
+
* active milestone parsing (phase numbers, version detection, etc.).
|
|
1238
|
+
* @param content - Raw ROADMAP.md content
|
|
1239
|
+
* @returns Content with <details>...</details> blocks removed
|
|
1240
|
+
*/
|
|
1241
|
+
function stripShippedSections(content: string): string {
|
|
1242
|
+
if (!content) return content;
|
|
1243
|
+
return content.replace(/<details>[\s\S]*?<\/details>/gi, '');
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
/**
|
|
1247
|
+
* Extract milestone version and name from ROADMAP.md.
|
|
1248
|
+
* @param cwd - Project working directory
|
|
1249
|
+
* @returns Milestone info with version (e.g., 'v1.0') and name
|
|
1250
|
+
*/
|
|
1251
|
+
function getMilestoneInfo(cwd: string): MilestoneInfo {
|
|
1252
|
+
try {
|
|
1253
|
+
const roadmap: string = fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8');
|
|
1254
|
+
const active: string = stripShippedSections(roadmap);
|
|
1255
|
+
|
|
1256
|
+
// Strategy 1: Find "(in progress)" milestone from bullet list
|
|
1257
|
+
const inProgressMatch: RegExpMatchArray | null = active.match(
|
|
1258
|
+
/-\s+(v[\d.]+)\s+([^\n(]+?)\s*\(in progress\)/im
|
|
1259
|
+
);
|
|
1260
|
+
if (inProgressMatch) {
|
|
1261
|
+
return { version: inProgressMatch[1], name: inProgressMatch[2].trim() };
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// Strategy 2: Last non-shipped milestone bullet
|
|
1265
|
+
const bulletRegex: RegExp = /-\s+(v[\d.]+)\s+([^\n(]+?)(?:\s*\(([^)]*)\))?\s*$/gim;
|
|
1266
|
+
let lastNonShipped: MilestoneInfo | null = null;
|
|
1267
|
+
let bm: RegExpExecArray | null;
|
|
1268
|
+
while ((bm = bulletRegex.exec(active)) !== null) {
|
|
1269
|
+
const status: string = bm[3] || '';
|
|
1270
|
+
if (!/shipped/i.test(status)) {
|
|
1271
|
+
lastNonShipped = { version: bm[1], name: bm[2].trim() };
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
if (lastNonShipped) return lastNonShipped;
|
|
1275
|
+
|
|
1276
|
+
// Strategy 3: Active heading "## ... vX.Y.Z ... (In Progress)"
|
|
1277
|
+
const headingMatch: RegExpMatchArray | null = active.match(
|
|
1278
|
+
/##\s*.*?(v\d+\.\d+(?:\.\d+)?)\s*[:\s]+([^\n(]+)/
|
|
1279
|
+
);
|
|
1280
|
+
if (headingMatch) {
|
|
1281
|
+
return { version: headingMatch[1], name: headingMatch[2].trim() };
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// Strategy 4: Fallback -- first version found (with 3-part support)
|
|
1285
|
+
const versionMatch: RegExpMatchArray | null = active.match(/v(\d+\.\d+(?:\.\d+)?)/);
|
|
1286
|
+
return {
|
|
1287
|
+
version: versionMatch ? versionMatch[0] : 'v1.0',
|
|
1288
|
+
name: 'milestone',
|
|
1289
|
+
};
|
|
1290
|
+
} catch {
|
|
1291
|
+
return { version: 'v1.0', name: 'milestone' };
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/**
|
|
1296
|
+
* Resolve model name from a config object without disk I/O.
|
|
1297
|
+
* When cwd is provided, resolves to backend-specific model name.
|
|
1298
|
+
* When cwd is omitted, returns abstract tier name (backward compatible).
|
|
1299
|
+
* @param config - Configuration object with model_profile field
|
|
1300
|
+
* @param agentType - Agent type key to look up in MODEL_PROFILES
|
|
1301
|
+
* @param cwd - Optional project working directory for backend-specific resolution
|
|
1302
|
+
* @param options - Optional overrides. When options.effectiveTierOverride is set,
|
|
1303
|
+
* it replaces the MODEL_PROFILES lookup entirely. Callers that omit this parameter
|
|
1304
|
+
* get identical behavior to before (backward compatible).
|
|
1305
|
+
* @returns Model name (e.g., 'opus', 'sonnet', 'haiku', or backend-specific name)
|
|
1306
|
+
*/
|
|
1307
|
+
function resolveModelForAgent(
|
|
1308
|
+
config: GrdConfig,
|
|
1309
|
+
agentType: string,
|
|
1310
|
+
cwd?: string,
|
|
1311
|
+
options?: { effectiveTierOverride?: ModelTier }
|
|
1312
|
+
): string {
|
|
1313
|
+
const profile: string = (config.model_profile || 'balanced').toLowerCase();
|
|
1314
|
+
const agentModels: Record<ModelProfileName, ModelTier> | undefined = MODEL_PROFILES[agentType];
|
|
1315
|
+
const baseTier: ModelTier = agentModels
|
|
1316
|
+
? agentModels[profile as ModelProfileName] || agentModels['balanced'] || 'sonnet'
|
|
1317
|
+
: 'sonnet';
|
|
1318
|
+
// Use override when provided; otherwise fall back to MODEL_PROFILES lookup
|
|
1319
|
+
const tier: ModelTier = options?.effectiveTierOverride || baseTier;
|
|
1320
|
+
// If cwd provided, resolve to backend-specific model name
|
|
1321
|
+
if (cwd) {
|
|
1322
|
+
const backend: unknown = detectBackend(cwd);
|
|
1323
|
+
return resolveBackendModel(backend, tier, config, cwd) as string;
|
|
1324
|
+
}
|
|
1325
|
+
// Backward compatible: no cwd means return tier name (existing behavior)
|
|
1326
|
+
return tier;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* Resolve effort level for a given agent type from project configuration.
|
|
1331
|
+
* Returns null if the backend does not support effort levels.
|
|
1332
|
+
* @param config - Project configuration
|
|
1333
|
+
* @param agentType - Agent type key (e.g., 'grd-executor', 'grd-planner')
|
|
1334
|
+
* @param cwd - Optional project working directory for backend detection
|
|
1335
|
+
* @returns Effort level string ('low', 'medium', 'high') or null if unsupported
|
|
1336
|
+
*/
|
|
1337
|
+
function resolveEffortForAgent(config: GrdConfig, agentType: string, cwd?: string): string | null {
|
|
1338
|
+
const backend = cwd ? detectBackend(cwd) : 'claude';
|
|
1339
|
+
const caps = getBackendCapabilities(backend);
|
|
1340
|
+
if (!caps.effort) return null;
|
|
1341
|
+
const profile: ModelProfileName = (config.model_profile || 'balanced') as ModelProfileName;
|
|
1342
|
+
return resolveEffortLevel(agentType, profile);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
// ─── Config Drift Validator ───────────────────────────────────────────────────
|
|
1346
|
+
|
|
1347
|
+
/**
|
|
1348
|
+
* Known config keys with their default values, organized for drift detection.
|
|
1349
|
+
* Each entry has: key (dot-path for nested keys), default value, and the gd settings
|
|
1350
|
+
* command to fix it.
|
|
1351
|
+
*/
|
|
1352
|
+
// Codex r2 P2: `gd settings` tool-mode only accepts token_profile and
|
|
1353
|
+
// phase_complete_llm_fallback. All other keys must use `gd config-set
|
|
1354
|
+
// <dot.path> <value>` (canonical route at lib/cli/index.ts:42), so emit
|
|
1355
|
+
// runnable commands for those instead of broken `gd settings ...` hints.
|
|
1356
|
+
const CONFIG_DRIFT_KEYS: Array<{ key: string; default: unknown; fix: string }> = [
|
|
1357
|
+
{ key: 'token_profile', default: 'balanced', fix: 'gd settings token_profile balanced' },
|
|
1358
|
+
// v0.4 Phase 1: effort axis (tool-mode settings key, alongside the other two).
|
|
1359
|
+
{ key: 'effort', default: 'balanced', fix: 'gd settings effort balanced' },
|
|
1360
|
+
{ key: 'phase_complete_llm_fallback', default: false, fix: 'gd settings phase_complete_llm_fallback false' },
|
|
1361
|
+
{ key: 'autonomous_mode', default: false, fix: 'gd config-set autonomous_mode false' },
|
|
1362
|
+
{ key: 'branching_strategy', default: 'none', fix: 'gd config-set branching_strategy none' },
|
|
1363
|
+
{ key: 'scheduler.idle_timeout_seconds', default: 900, fix: 'gd config-set scheduler.idle_timeout_seconds 900' },
|
|
1364
|
+
{ key: 'scheduler.budget_pressure_thresholds', default: { warning: 0.6, high: 0.8, critical: 0.95 }, fix: 'gd config-set scheduler.budget_pressure_thresholds \'{"warning":0.6,"high":0.8,"critical":0.95}\'' },
|
|
1365
|
+
// Codex r27 P2: cmdHealth uses DEFAULT_WEIGHTS from lib/drift.ts
|
|
1366
|
+
// (0.5/0.3/0.2) when `drift` is missing. The fix command must
|
|
1367
|
+
// materialize the same runtime default so users don't accidentally
|
|
1368
|
+
// change drift-scoring semantics by applying it.
|
|
1369
|
+
{ key: 'drift', default: { weights: { goal: 0.5, constraint: 0.3, ontology: 0.2 }, threshold: 0.3 }, fix: 'gd config-set drift \'{"weights":{"goal":0.5,"constraint":0.3,"ontology":0.2},"threshold":0.3}\'' },
|
|
1370
|
+
{ key: 'autopilot', default: {}, fix: 'gd config-set autopilot \'{}\''},
|
|
1371
|
+
];
|
|
1372
|
+
|
|
1373
|
+
interface DriftReport {
|
|
1374
|
+
missing_keys: Array<{ key: string; default: unknown; fix_command: string }>;
|
|
1375
|
+
deprecated_keys: string[];
|
|
1376
|
+
total_checks: number;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
/**
|
|
1380
|
+
* Validate config.json against the current schema defaults, identifying keys that
|
|
1381
|
+
* were added after initial `gd init` and are missing from the user's config.
|
|
1382
|
+
*
|
|
1383
|
+
* @param cwd - Project working directory
|
|
1384
|
+
* @returns DriftReport with missing keys, fix commands, and deprecated keys
|
|
1385
|
+
*/
|
|
1386
|
+
function validateConfigDrift(cwd: string): DriftReport {
|
|
1387
|
+
const configPath = path.join(cwd, '.planning', 'config.json');
|
|
1388
|
+
let parsed: Record<string, unknown> = {};
|
|
1389
|
+
try {
|
|
1390
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
1391
|
+
parsed = JSON.parse(raw);
|
|
1392
|
+
} catch {
|
|
1393
|
+
// Config doesn't exist or is malformed — all keys are "missing"
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
const missing: DriftReport['missing_keys'] = [];
|
|
1397
|
+
|
|
1398
|
+
for (const entry of CONFIG_DRIFT_KEYS) {
|
|
1399
|
+
// Support dot-path for nested keys (e.g. "scheduler.idle_timeout_seconds")
|
|
1400
|
+
const parts = entry.key.split('.');
|
|
1401
|
+
let cur: unknown = parsed;
|
|
1402
|
+
for (const part of parts) {
|
|
1403
|
+
if (cur === null || typeof cur !== 'object') { cur = undefined; break; }
|
|
1404
|
+
cur = (cur as Record<string, unknown>)[part];
|
|
1405
|
+
}
|
|
1406
|
+
if (cur === undefined) {
|
|
1407
|
+
missing.push({ key: entry.key, default: entry.default, fix_command: entry.fix });
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
return {
|
|
1412
|
+
missing_keys: missing,
|
|
1413
|
+
deprecated_keys: [],
|
|
1414
|
+
total_checks: CONFIG_DRIFT_KEYS.length,
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// ─── Exports ─────────────────────────────────────────────────────────────────
|
|
1419
|
+
|
|
1420
|
+
module.exports = {
|
|
1421
|
+
// Node built-ins (re-exported for convenience)
|
|
1422
|
+
fs,
|
|
1423
|
+
path,
|
|
1424
|
+
os,
|
|
1425
|
+
execFileSync,
|
|
1426
|
+
// Constants
|
|
1427
|
+
GIT_ALLOWED_COMMANDS,
|
|
1428
|
+
GIT_BLOCKED_COMMANDS,
|
|
1429
|
+
GIT_BLOCKED_FLAGS,
|
|
1430
|
+
MODEL_PROFILES,
|
|
1431
|
+
CODE_EXTENSIONS,
|
|
1432
|
+
// Helpers
|
|
1433
|
+
parseIncludeFlag,
|
|
1434
|
+
safeReadFile,
|
|
1435
|
+
safeReadMarkdown,
|
|
1436
|
+
safeReadJSON,
|
|
1437
|
+
extractMarkdownSection,
|
|
1438
|
+
loadConfig,
|
|
1439
|
+
isGitIgnored,
|
|
1440
|
+
execGit,
|
|
1441
|
+
normalizePhaseName,
|
|
1442
|
+
findCodeFiles,
|
|
1443
|
+
// Input validation
|
|
1444
|
+
validatePhaseName,
|
|
1445
|
+
validateFilePath,
|
|
1446
|
+
validateGitRef,
|
|
1447
|
+
// CLI argument validation
|
|
1448
|
+
validatePhaseArg,
|
|
1449
|
+
validateFileArg,
|
|
1450
|
+
validateSubcommand,
|
|
1451
|
+
validateRequiredArg,
|
|
1452
|
+
// Caching
|
|
1453
|
+
createRunCache,
|
|
1454
|
+
// Phase directory utilities
|
|
1455
|
+
findPhaseDir,
|
|
1456
|
+
parsePhaseNumber,
|
|
1457
|
+
// Directory walking
|
|
1458
|
+
walkJsFiles,
|
|
1459
|
+
// Output
|
|
1460
|
+
output,
|
|
1461
|
+
error,
|
|
1462
|
+
debugLog,
|
|
1463
|
+
// Compound helpers
|
|
1464
|
+
resolveModelInternal,
|
|
1465
|
+
findPhaseInternal,
|
|
1466
|
+
pathExistsInternal,
|
|
1467
|
+
generateSlugInternal,
|
|
1468
|
+
getMilestoneInfo,
|
|
1469
|
+
stripShippedSections,
|
|
1470
|
+
resolveModelForAgent,
|
|
1471
|
+
resolveEffortForAgent,
|
|
1472
|
+
levenshteinDistance,
|
|
1473
|
+
findClosestCommand,
|
|
1474
|
+
clearPhaseCache,
|
|
1475
|
+
validateConfigDrift,
|
|
1476
|
+
// v0.4 Phase 1: effort axis
|
|
1477
|
+
EFFORT_PROFILES,
|
|
1478
|
+
resolveEffortKnob,
|
|
1479
|
+
};
|