@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/scheduler.ts
ADDED
|
@@ -0,0 +1,1370 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GRD Scheduler -- Backend subprocess spawning, account rotation, budget pressure tracking,
|
|
5
|
+
* and idle watchdog. Dispatches agents to claude/codex/gemini/opencode CLI backends with
|
|
6
|
+
* per-account token budget monitoring and adaptive model-tier downgrade under pressure.
|
|
7
|
+
*
|
|
8
|
+
* @module scheduler
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
BackendId,
|
|
13
|
+
AdapterBackendId,
|
|
14
|
+
BackendAdapter,
|
|
15
|
+
BackendUsageState,
|
|
16
|
+
UsageSample,
|
|
17
|
+
SpawnOpts,
|
|
18
|
+
AccountResolution,
|
|
19
|
+
SuperpowersConfig,
|
|
20
|
+
SchedulerConfig,
|
|
21
|
+
SchedulerSpawnResult,
|
|
22
|
+
BudgetPressureLevel,
|
|
23
|
+
BudgetPressureThresholds,
|
|
24
|
+
} from './types';
|
|
25
|
+
import type * as childProcess from 'child_process';
|
|
26
|
+
|
|
27
|
+
const { waitUntilOrAbort } = require('./scheduler-wait') as {
|
|
28
|
+
waitUntilOrAbort: (targetMs: number) => Promise<'waited' | 'aborted'>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const { incrementCounter } = require('./metrics') as {
|
|
32
|
+
incrementCounter: (name: string, delta?: number) => void;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// ─── Per-backend CLI Adapters ─────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Map of backend adapters for all supported CLI backends.
|
|
39
|
+
* Each adapter encapsulates binary name, argument building, token parsing,
|
|
40
|
+
* and rate-limit detection for a specific backend CLI.
|
|
41
|
+
*
|
|
42
|
+
* Meta-backends (superpowers, grd) are not included — they are scheduling
|
|
43
|
+
* strategies that resolve to one of these real adapters at spawn time.
|
|
44
|
+
*/
|
|
45
|
+
const _claudeAdapter: BackendAdapter = {
|
|
46
|
+
binary: 'claude',
|
|
47
|
+
buildArgs(prompt: string, opts: SpawnOpts): string[] {
|
|
48
|
+
const args = ['-p', prompt, '--verbose', '--dangerously-skip-permissions'];
|
|
49
|
+
if (opts.maxTurns) {
|
|
50
|
+
args.push('--max-turns', String(opts.maxTurns));
|
|
51
|
+
}
|
|
52
|
+
if (opts.model) {
|
|
53
|
+
args.push('--model', opts.model);
|
|
54
|
+
}
|
|
55
|
+
args.push('--output-format', 'json');
|
|
56
|
+
return args;
|
|
57
|
+
},
|
|
58
|
+
parseTokenUsage(stderr: string): number | null {
|
|
59
|
+
const totalMatch = stderr.match(/[Tt]otal.tokens:\s*(\d+)/);
|
|
60
|
+
if (totalMatch) return parseInt(totalMatch[1], 10);
|
|
61
|
+
const inputMatch = stderr.match(/input_tokens:\s*(\d+)/);
|
|
62
|
+
const outputMatch = stderr.match(/output_tokens:\s*(\d+)/);
|
|
63
|
+
if (inputMatch && outputMatch) {
|
|
64
|
+
return parseInt(inputMatch[1], 10) + parseInt(outputMatch[1], 10);
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
},
|
|
68
|
+
isRateLimited(exitCode: number, stderr: string): boolean {
|
|
69
|
+
if (exitCode === 0) return false;
|
|
70
|
+
return /rate.limit|429|overloaded_error|too many requests/i.test(stderr);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const ADAPTERS: Record<AdapterBackendId, BackendAdapter> = {
|
|
75
|
+
claude: _claudeAdapter,
|
|
76
|
+
|
|
77
|
+
codex: {
|
|
78
|
+
binary: 'codex',
|
|
79
|
+
buildArgs(prompt: string, opts: SpawnOpts): string[] {
|
|
80
|
+
const args = ['--prompt', prompt, '--approval-mode', 'full-auto'];
|
|
81
|
+
if (opts.model) {
|
|
82
|
+
args.push('--model', opts.model);
|
|
83
|
+
}
|
|
84
|
+
return args;
|
|
85
|
+
},
|
|
86
|
+
parseTokenUsage(stderr: string): number | null {
|
|
87
|
+
const match = stderr.match(/"total_tokens":\s*(\d+)/);
|
|
88
|
+
return match ? parseInt(match[1], 10) : null;
|
|
89
|
+
},
|
|
90
|
+
isRateLimited(_exitCode: number, stderr: string): boolean {
|
|
91
|
+
return /rate.limit|429|rate_limit_exceeded/i.test(stderr);
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
gemini: {
|
|
96
|
+
binary: 'gemini',
|
|
97
|
+
buildArgs(prompt: string, opts: SpawnOpts): string[] {
|
|
98
|
+
const args = ['-p', prompt, '--sandbox', 'off'];
|
|
99
|
+
if (opts.model) {
|
|
100
|
+
args.push('--model', opts.model);
|
|
101
|
+
}
|
|
102
|
+
return args;
|
|
103
|
+
},
|
|
104
|
+
parseTokenUsage(stderr: string): number | null {
|
|
105
|
+
const match = stderr.match(/tokenCount["\s:]*(\d+)/);
|
|
106
|
+
return match ? parseInt(match[1], 10) : null;
|
|
107
|
+
},
|
|
108
|
+
isRateLimited(_exitCode: number, stderr: string): boolean {
|
|
109
|
+
return /rate.limit|429|RESOURCE_EXHAUSTED|quota/i.test(stderr);
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
opencode: {
|
|
114
|
+
binary: 'opencode',
|
|
115
|
+
buildArgs(prompt: string, opts: SpawnOpts): string[] {
|
|
116
|
+
const args = ['--non-interactive', '--prompt', prompt];
|
|
117
|
+
if (opts.model) {
|
|
118
|
+
args.push('--model', opts.model);
|
|
119
|
+
}
|
|
120
|
+
return args;
|
|
121
|
+
},
|
|
122
|
+
parseTokenUsage(stderr: string): number | null {
|
|
123
|
+
const match = stderr.match(/(?:total_tokens|tokens?.used)[\s:"]*(\d+)/i);
|
|
124
|
+
return match ? parseInt(match[1], 10) : null;
|
|
125
|
+
},
|
|
126
|
+
isRateLimited(_exitCode: number, stderr: string): boolean {
|
|
127
|
+
return /rate.limit|429|too many requests|quota/i.test(stderr);
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
overstory: {
|
|
132
|
+
binary: 'ov',
|
|
133
|
+
buildArgs(prompt: string, opts: SpawnOpts): string[] {
|
|
134
|
+
const args = ['run', '--prompt', prompt];
|
|
135
|
+
if (opts.model) {
|
|
136
|
+
args.push('--model', opts.model);
|
|
137
|
+
}
|
|
138
|
+
return args;
|
|
139
|
+
},
|
|
140
|
+
parseTokenUsage(stderr: string): number | null {
|
|
141
|
+
const match = stderr.match(/tokens?:\s*(\d+)/i);
|
|
142
|
+
return match ? parseInt(match[1], 10) : null;
|
|
143
|
+
},
|
|
144
|
+
isRateLimited(_exitCode: number, stderr: string): boolean {
|
|
145
|
+
return /rate.limit|429|quota/i.test(stderr);
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Maps each adapter backend to its config-directory environment variable.
|
|
152
|
+
* Used by account rotation to override which account a CLI binary uses.
|
|
153
|
+
*/
|
|
154
|
+
export const ENV_VAR_MAP: Record<AdapterBackendId, string> = {
|
|
155
|
+
claude: 'CLAUDE_CONFIG_DIR',
|
|
156
|
+
codex: 'CODEX_HOME',
|
|
157
|
+
gemini: 'GEMINI_CLI_HOME',
|
|
158
|
+
opencode: 'OPENCODE_CONFIG_DIR',
|
|
159
|
+
overstory: 'OVERSTORY_HOME',
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// ─── EWMA and Rolling Window ──────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
/** Default token-per-minute budget for backends with no explicit limit configured. */
|
|
165
|
+
const DEFAULT_BUDGET_TPM = 40000;
|
|
166
|
+
|
|
167
|
+
/** Token-per-minute budget for the free-fallback backend (effectively unlimited). */
|
|
168
|
+
export const FREE_FALLBACK_BUDGET = 1000000;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Creates a fresh BackendUsageState with the given token budget.
|
|
172
|
+
*
|
|
173
|
+
* @param tokenBudget - tokens-per-minute budget for this backend
|
|
174
|
+
* @returns initialized state with zeroed counters
|
|
175
|
+
*/
|
|
176
|
+
export function createBackendState(tokenBudget: number): BackendUsageState {
|
|
177
|
+
return {
|
|
178
|
+
samples: [],
|
|
179
|
+
ewma_tokens_per_task: 0,
|
|
180
|
+
tokens_consumed_in_window: 0,
|
|
181
|
+
tokens_reserved: 0,
|
|
182
|
+
in_flight_count: 0,
|
|
183
|
+
token_budget: tokenBudget,
|
|
184
|
+
budget_learned: false,
|
|
185
|
+
budget_confidence: 0,
|
|
186
|
+
cooldown_until: undefined,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Updates the EWMA estimate in-place with a new token observation.
|
|
192
|
+
* On first observation (ewma === 0), sets directly to the observed value.
|
|
193
|
+
*
|
|
194
|
+
* @param state - backend usage state to update
|
|
195
|
+
* @param tokens - observed token count for the latest task
|
|
196
|
+
* @param alpha - EWMA smoothing factor (0 < alpha < 1)
|
|
197
|
+
*/
|
|
198
|
+
export function updateEWMA(state: BackendUsageState, tokens: number, alpha: number): void {
|
|
199
|
+
if (state.ewma_tokens_per_task === 0) {
|
|
200
|
+
state.ewma_tokens_per_task = tokens;
|
|
201
|
+
} else {
|
|
202
|
+
state.ewma_tokens_per_task = alpha * tokens + (1 - alpha) * state.ewma_tokens_per_task;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Removes samples older than windowMinutes from state and recalculates
|
|
208
|
+
* tokens_consumed_in_window from the remaining samples.
|
|
209
|
+
*
|
|
210
|
+
* @param state - backend usage state to mutate
|
|
211
|
+
* @param windowMinutes - rolling window duration in minutes
|
|
212
|
+
*/
|
|
213
|
+
export function evictExpiredSamples(state: BackendUsageState, windowMinutes: number): void {
|
|
214
|
+
if (windowMinutes <= 0) return;
|
|
215
|
+
const cutoff = Date.now() - windowMinutes * 60 * 1000;
|
|
216
|
+
state.samples = state.samples.filter((s) => s.timestamp >= cutoff);
|
|
217
|
+
state.tokens_consumed_in_window = state.samples.reduce((sum, s) => sum + s.tokenEstimate, 0);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Records a completed usage sample, evicts stale samples from the window,
|
|
222
|
+
* updates EWMA, and recalculates budget_confidence.
|
|
223
|
+
*
|
|
224
|
+
* @param state - backend usage state to update
|
|
225
|
+
* @param sample - new usage sample to record
|
|
226
|
+
* @param windowMinutes - rolling window duration in minutes
|
|
227
|
+
* @param alpha - EWMA smoothing factor
|
|
228
|
+
*/
|
|
229
|
+
export function recordSample(
|
|
230
|
+
state: BackendUsageState,
|
|
231
|
+
sample: UsageSample,
|
|
232
|
+
windowMinutes: number,
|
|
233
|
+
alpha: number
|
|
234
|
+
): void {
|
|
235
|
+
state.samples.push(sample);
|
|
236
|
+
evictExpiredSamples(state, windowMinutes);
|
|
237
|
+
updateEWMA(state, sample.tokenEstimate, alpha);
|
|
238
|
+
state.budget_confidence = 1 - 1 / (1 + state.samples.length * 0.2);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ─── Backend Picker with Concurrency Accounting ───────────────────────────────
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Selects the highest-priority backend that has sufficient token headroom.
|
|
245
|
+
* Skips backends in cooldown or without enough remaining capacity (accounting
|
|
246
|
+
* for in-flight reservations). Falls back to freeFallback if none qualify.
|
|
247
|
+
*
|
|
248
|
+
* @param priority - ordered list of backend IDs to try
|
|
249
|
+
* @param states - map of backend ID to usage state
|
|
250
|
+
* @param safetyMargin - minimum remaining tasks before a backend is considered full
|
|
251
|
+
* @param freeFallback - fallback backend used when all priority backends are exhausted
|
|
252
|
+
* @returns selected BackendId
|
|
253
|
+
*/
|
|
254
|
+
export function pickBackend(
|
|
255
|
+
priority: BackendId[],
|
|
256
|
+
states: Map<string, BackendUsageState>,
|
|
257
|
+
safetyMargin: number,
|
|
258
|
+
freeFallback: { backend: BackendId }
|
|
259
|
+
): BackendId {
|
|
260
|
+
const now = Date.now();
|
|
261
|
+
for (const backend of priority) {
|
|
262
|
+
const state = states.get(backend);
|
|
263
|
+
if (!state) continue;
|
|
264
|
+
if (state.cooldown_until && state.cooldown_until > now) continue;
|
|
265
|
+
if (state.ewma_tokens_per_task === 0) return backend;
|
|
266
|
+
const effective = state.tokens_consumed_in_window + state.tokens_reserved;
|
|
267
|
+
const remaining = state.token_budget - effective;
|
|
268
|
+
const tasksRemaining = remaining / state.ewma_tokens_per_task;
|
|
269
|
+
if (tasksRemaining >= safetyMargin) return backend;
|
|
270
|
+
}
|
|
271
|
+
return freeFallback.backend;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ─── Account Resolution Waterfall ─────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Checks whether a single account state key has sufficient headroom for
|
|
278
|
+
* scheduling, considering EWMA prediction, in-flight reservations, and cooldown.
|
|
279
|
+
*
|
|
280
|
+
* @param state - the account's usage state
|
|
281
|
+
* @param safetyMargin - minimum remaining tasks before considered full
|
|
282
|
+
* @returns true if the account has capacity or no EWMA data yet
|
|
283
|
+
*/
|
|
284
|
+
function _hasHeadroom(state: BackendUsageState, safetyMargin: number): boolean {
|
|
285
|
+
const now = Date.now();
|
|
286
|
+
if (state.cooldown_until && state.cooldown_until > now) return false;
|
|
287
|
+
if (state.ewma_tokens_per_task === 0) return true;
|
|
288
|
+
const effective = state.tokens_consumed_in_window + state.tokens_reserved;
|
|
289
|
+
const remaining = state.token_budget - effective;
|
|
290
|
+
const tasksRemaining = remaining / state.ewma_tokens_per_task;
|
|
291
|
+
return tasksRemaining >= safetyMargin;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Sends `signal` to the process group of `child` on POSIX platforms, or to
|
|
296
|
+
* the direct child on Windows. Using a negative PID with process.kill ensures
|
|
297
|
+
* grandchildren (e.g., tool-invocation forks spawned by the backend CLI) are
|
|
298
|
+
* also terminated.
|
|
299
|
+
*
|
|
300
|
+
* Requires the child to have been spawned with `detached: true` so that it
|
|
301
|
+
* gets its own process group (pgid === pid).
|
|
302
|
+
*
|
|
303
|
+
* @param child - the spawned ChildProcess whose process group to signal
|
|
304
|
+
* @param signal - signal to send (e.g. 'SIGTERM', 'SIGKILL')
|
|
305
|
+
*/
|
|
306
|
+
export function _killProcessTree(
|
|
307
|
+
child: Pick<childProcess.ChildProcess, 'pid' | 'kill'>,
|
|
308
|
+
signal: NodeJS.Signals
|
|
309
|
+
): void {
|
|
310
|
+
if (child.pid === undefined) return;
|
|
311
|
+
if (process.platform === 'win32') {
|
|
312
|
+
// Windows: just kill the direct child (no POSIX process groups)
|
|
313
|
+
try {
|
|
314
|
+
child.kill(signal);
|
|
315
|
+
} catch {
|
|
316
|
+
/* already dead */
|
|
317
|
+
}
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
// POSIX: signal the whole process group via negative pid
|
|
321
|
+
try {
|
|
322
|
+
process.kill(-child.pid, signal);
|
|
323
|
+
} catch (e) {
|
|
324
|
+
// ESRCH (no such process) is benign — process already exited.
|
|
325
|
+
// Fall back to direct kill in case the group wasn't created (e.g., race).
|
|
326
|
+
if ((e as NodeJS.ErrnoException).code !== 'ESRCH') {
|
|
327
|
+
try {
|
|
328
|
+
child.kill(signal);
|
|
329
|
+
} catch {
|
|
330
|
+
/* already dead */
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Resolves the idle timeout in seconds for the given backend, applying the
|
|
338
|
+
* lookup order: per-backend override → global idle_timeout_seconds → default 900.
|
|
339
|
+
*
|
|
340
|
+
* @param backend - backend ID (e.g. 'claude', 'gemini')
|
|
341
|
+
* @param config - subset of SchedulerConfig with timeout fields
|
|
342
|
+
* @returns resolved idle timeout in seconds
|
|
343
|
+
*/
|
|
344
|
+
export function _resolveIdleTimeoutSeconds(
|
|
345
|
+
backend: string,
|
|
346
|
+
config: {
|
|
347
|
+
idle_timeout_seconds_by_backend?: Record<string, number>;
|
|
348
|
+
idle_timeout_seconds?: number;
|
|
349
|
+
}
|
|
350
|
+
): number {
|
|
351
|
+
return config.idle_timeout_seconds_by_backend?.[backend] ?? config.idle_timeout_seconds ?? 900;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Starts an idle watchdog that invokes `onIdle` when no markActivity
|
|
356
|
+
* has been called for longer than `idleTimeoutMs`. Returns markActivity
|
|
357
|
+
* and stop functions.
|
|
358
|
+
*
|
|
359
|
+
* Polls every 1000ms. Fires at most once — subsequent ticks are no-ops.
|
|
360
|
+
*/
|
|
361
|
+
export function _startIdleWatchdog(
|
|
362
|
+
idleTimeoutMs: number,
|
|
363
|
+
onIdle: () => void
|
|
364
|
+
): { markActivity: () => void; stop: () => void } {
|
|
365
|
+
const POLL_INTERVAL_MS = 1000;
|
|
366
|
+
let lastActivityAt = Date.now();
|
|
367
|
+
let stopped = false;
|
|
368
|
+
|
|
369
|
+
const timer = setInterval(() => {
|
|
370
|
+
if (stopped) return;
|
|
371
|
+
if (Date.now() - lastActivityAt >= idleTimeoutMs) {
|
|
372
|
+
stopped = true;
|
|
373
|
+
clearInterval(timer);
|
|
374
|
+
onIdle();
|
|
375
|
+
}
|
|
376
|
+
}, POLL_INTERVAL_MS);
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
markActivity: () => {
|
|
380
|
+
lastActivityAt = Date.now();
|
|
381
|
+
},
|
|
382
|
+
stop: () => {
|
|
383
|
+
if (stopped) return;
|
|
384
|
+
stopped = true;
|
|
385
|
+
clearInterval(timer);
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Returns true iff at least one account in the priority list has headroom.
|
|
392
|
+
* Small helper used by the _spawnWithRetry wait-branch decision.
|
|
393
|
+
*/
|
|
394
|
+
export function _anyPriorityHasHeadroom(
|
|
395
|
+
priority: BackendId[],
|
|
396
|
+
accounts: SuperpowersConfig['accounts'],
|
|
397
|
+
states: Map<string, BackendUsageState>,
|
|
398
|
+
safetyMargin: number
|
|
399
|
+
): boolean {
|
|
400
|
+
for (const backend of priority) {
|
|
401
|
+
const backendAccounts = accounts[backend as AdapterBackendId] || [];
|
|
402
|
+
for (const account of backendAccounts) {
|
|
403
|
+
const stateKey = `${backend}/${account.config_dir}`;
|
|
404
|
+
const state = states.get(stateKey);
|
|
405
|
+
if (!state) continue;
|
|
406
|
+
if (_hasHeadroom(state, safetyMargin)) return true;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Computes the earliest timestamp (ms since epoch) at which ANY priority
|
|
414
|
+
* account will regain headroom based on sample aging out of the rolling
|
|
415
|
+
* window. Used by the wait-loop in _spawnWithRetry when all priority
|
|
416
|
+
* accounts are currently exhausted.
|
|
417
|
+
*
|
|
418
|
+
* For each priority account, walks its samples oldest-first, hypothetically
|
|
419
|
+
* dropping each one and recomputing projected headroom. The latest-dropped
|
|
420
|
+
* sample's timestamp + windowMinutes is the moment that account will have
|
|
421
|
+
* enough headroom for one more EWMA-sized task.
|
|
422
|
+
*
|
|
423
|
+
* Returns null if:
|
|
424
|
+
* - No priority account has samples (nothing to wait for)
|
|
425
|
+
* - Soonest recovery across all accounts is beyond Date.now() + maxWaitMs
|
|
426
|
+
* - All considered accounts have zero ewma_tokens_per_task (no prediction data)
|
|
427
|
+
*
|
|
428
|
+
* Pattern adopted from gsd-2 v2.67 auto-timeout-recovery.ts — but
|
|
429
|
+
* sample-based rather than attempt-based.
|
|
430
|
+
*
|
|
431
|
+
* Note: tokens_reserved (in-flight EWMA cost) is held constant during the
|
|
432
|
+
* simulation because in-flight tasks are expected to complete independently
|
|
433
|
+
* of sample aging. This makes the estimate slightly pessimistic — actual
|
|
434
|
+
* headroom may return sooner.
|
|
435
|
+
*/
|
|
436
|
+
export function computeSoonestRecovery(
|
|
437
|
+
states: Map<string, BackendUsageState>,
|
|
438
|
+
priority: BackendId[],
|
|
439
|
+
accounts: SuperpowersConfig['accounts'],
|
|
440
|
+
windowMinutes: number,
|
|
441
|
+
maxWaitMs: number
|
|
442
|
+
): number | null {
|
|
443
|
+
const now = Date.now();
|
|
444
|
+
let soonest = Infinity;
|
|
445
|
+
|
|
446
|
+
for (const backend of priority) {
|
|
447
|
+
const backendAccounts = accounts[backend as AdapterBackendId] || [];
|
|
448
|
+
for (const account of backendAccounts) {
|
|
449
|
+
const stateKey = `${backend}/${account.config_dir}`;
|
|
450
|
+
const state = states.get(stateKey);
|
|
451
|
+
if (!state || state.samples.length === 0) continue;
|
|
452
|
+
if (state.ewma_tokens_per_task === 0) continue;
|
|
453
|
+
|
|
454
|
+
const sortedSamples = [...state.samples].sort((a, b) => a.timestamp - b.timestamp);
|
|
455
|
+
|
|
456
|
+
const ewmaCost = state.ewma_tokens_per_task;
|
|
457
|
+
let consumed = state.tokens_consumed_in_window;
|
|
458
|
+
const reserved = state.tokens_reserved;
|
|
459
|
+
let latestDroppedTs: number | null = null;
|
|
460
|
+
|
|
461
|
+
for (const sample of sortedSamples) {
|
|
462
|
+
const projectedRemaining = state.token_budget - consumed - reserved;
|
|
463
|
+
if (projectedRemaining >= ewmaCost) break;
|
|
464
|
+
consumed -= sample.tokenEstimate;
|
|
465
|
+
latestDroppedTs = sample.timestamp;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (latestDroppedTs === null) continue;
|
|
469
|
+
const recoveryTime = latestDroppedTs + windowMinutes * 60 * 1000;
|
|
470
|
+
if (recoveryTime < soonest) soonest = recoveryTime;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (soonest === Infinity) return null;
|
|
475
|
+
if (soonest > now + maxWaitMs) return null;
|
|
476
|
+
return soonest;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ─── Spec 4: budget pressure detection ────────────────────────────────────
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Default thresholds for budget pressure classification. Overridable
|
|
483
|
+
* via SchedulerConfig.budget_pressure_thresholds.
|
|
484
|
+
*/
|
|
485
|
+
const DEFAULT_PRESSURE_THRESHOLDS: BudgetPressureThresholds = {
|
|
486
|
+
warning: 0.6,
|
|
487
|
+
high: 0.8,
|
|
488
|
+
critical: 0.95,
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Returns true if any priority account has consumed more than the warning
|
|
493
|
+
* threshold (default 60%) of its rolling-window budget. Pure function.
|
|
494
|
+
*/
|
|
495
|
+
export function isBudgetPressured(
|
|
496
|
+
states: Map<string, BackendUsageState>,
|
|
497
|
+
priority: BackendId[],
|
|
498
|
+
accounts: SuperpowersConfig['accounts'],
|
|
499
|
+
thresholds?: BudgetPressureThresholds
|
|
500
|
+
): boolean {
|
|
501
|
+
return computeBudgetPressureLevel(states, priority, accounts, thresholds) !== 'none';
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Module-level state for transition-based logging
|
|
505
|
+
const _lastLoggedPressure: Map<string, BudgetPressureLevel> = new Map();
|
|
506
|
+
|
|
507
|
+
// Monotonic counter for unique per-scheduler session keys. Each
|
|
508
|
+
// createScheduler call gets its own ID so _lastLoggedPressure
|
|
509
|
+
// transitions are tracked independently (O3).
|
|
510
|
+
let _nextSchedulerSessionId = 0;
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Logs a single stderr line when the pressure level has changed since
|
|
514
|
+
* the last call with the same sessionKey. Safe to call per spawn —
|
|
515
|
+
* only emits on transitions. Noop when current == previous.
|
|
516
|
+
*
|
|
517
|
+
* The sessionKey lets multiple sessions in the same process have
|
|
518
|
+
* independent transition state. Autopilot/evolve/autoresearch
|
|
519
|
+
* typically pass process.pid.toString().
|
|
520
|
+
*/
|
|
521
|
+
export function logPressureTransition(
|
|
522
|
+
sessionKey: string,
|
|
523
|
+
current: BudgetPressureLevel,
|
|
524
|
+
agentType: string,
|
|
525
|
+
baseTier: string,
|
|
526
|
+
effectiveTier: string
|
|
527
|
+
): void {
|
|
528
|
+
const previous = _lastLoggedPressure.get(sessionKey) || 'none';
|
|
529
|
+
if (previous === current) return;
|
|
530
|
+
_lastLoggedPressure.set(sessionKey, current);
|
|
531
|
+
|
|
532
|
+
incrementCounter(`scheduler.pressure_transitions.${current}`);
|
|
533
|
+
|
|
534
|
+
if (current === 'none') return;
|
|
535
|
+
const tierNote =
|
|
536
|
+
baseTier === effectiveTier
|
|
537
|
+
? ''
|
|
538
|
+
: ` — downgrading ${agentType} from ${baseTier} to ${effectiveTier}`;
|
|
539
|
+
process.stderr.write(`[scheduler] budget pressure detected — level=${current}${tierNote}\n`);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Classifies the worst pressure level across all priority accounts.
|
|
544
|
+
* Returns 'none' | 'warning' | 'high' | 'critical'. Pure function.
|
|
545
|
+
*
|
|
546
|
+
* For each priority account, computes (consumed + reserved) / budget
|
|
547
|
+
* and picks the worst ratio across all accounts (i.e., the one closest
|
|
548
|
+
* to exhaustion determines the level for the whole session).
|
|
549
|
+
*/
|
|
550
|
+
export function computeBudgetPressureLevel(
|
|
551
|
+
states: Map<string, BackendUsageState>,
|
|
552
|
+
priority: BackendId[],
|
|
553
|
+
accounts: SuperpowersConfig['accounts'],
|
|
554
|
+
thresholds?: BudgetPressureThresholds
|
|
555
|
+
): BudgetPressureLevel {
|
|
556
|
+
const t = thresholds || DEFAULT_PRESSURE_THRESHOLDS;
|
|
557
|
+
let worstRatio = 0;
|
|
558
|
+
|
|
559
|
+
for (const backend of priority) {
|
|
560
|
+
const backendAccounts = accounts[backend as AdapterBackendId] || [];
|
|
561
|
+
for (const account of backendAccounts) {
|
|
562
|
+
const stateKey = `${backend}/${account.config_dir}`;
|
|
563
|
+
const state = states.get(stateKey);
|
|
564
|
+
if (!state) continue;
|
|
565
|
+
if (state.token_budget <= 0) continue;
|
|
566
|
+
const ratio = (state.tokens_consumed_in_window + state.tokens_reserved) / state.token_budget;
|
|
567
|
+
if (ratio > worstRatio) worstRatio = ratio;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (worstRatio >= t.critical) return 'critical';
|
|
572
|
+
if (worstRatio >= t.high) return 'high';
|
|
573
|
+
if (worstRatio >= t.warning) return 'warning';
|
|
574
|
+
return 'none';
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Resolves which backend and account to use for the next scheduled task.
|
|
579
|
+
* Walks the backend_priority list, and within each backend tries every
|
|
580
|
+
* configured account in order. Falls back to the free_fallback backend
|
|
581
|
+
* when all priority accounts are exhausted.
|
|
582
|
+
*
|
|
583
|
+
* Edge cases:
|
|
584
|
+
* - Empty accounts ({}) — returns default_backend with no config dir override
|
|
585
|
+
* - Empty account array ([]) — skips that backend
|
|
586
|
+
* - Backend in priority but missing from accounts — skipped
|
|
587
|
+
*
|
|
588
|
+
* @param superpowersConfig - superpowers configuration with accounts
|
|
589
|
+
* @param schedulerConfig - scheduler configuration with priority and fallback
|
|
590
|
+
* @param states - map of compound state keys to usage state
|
|
591
|
+
* @param safetyMargin - minimum remaining tasks before an account is considered full
|
|
592
|
+
* @returns resolved backend, account, and state key
|
|
593
|
+
*/
|
|
594
|
+
export function resolveAccount(
|
|
595
|
+
superpowersConfig: SuperpowersConfig,
|
|
596
|
+
schedulerConfig: SchedulerConfig,
|
|
597
|
+
states: Map<string, BackendUsageState>,
|
|
598
|
+
safetyMargin: number
|
|
599
|
+
): AccountResolution {
|
|
600
|
+
const accounts = superpowersConfig.accounts;
|
|
601
|
+
|
|
602
|
+
// Edge case: accounts is empty — use default_backend with no config dir
|
|
603
|
+
const hasAnyAccounts = Object.keys(accounts).some(
|
|
604
|
+
(k) => (accounts[k as AdapterBackendId] || []).length > 0
|
|
605
|
+
);
|
|
606
|
+
if (!hasAnyAccounts) {
|
|
607
|
+
return {
|
|
608
|
+
backend: superpowersConfig.default_backend as AdapterBackendId,
|
|
609
|
+
account: { config_dir: '' },
|
|
610
|
+
stateKey: superpowersConfig.default_backend,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Walk priority list, try each account within each backend
|
|
615
|
+
for (const backend of schedulerConfig.backend_priority) {
|
|
616
|
+
const backendAccounts = accounts[backend];
|
|
617
|
+
if (!backendAccounts || backendAccounts.length === 0) continue;
|
|
618
|
+
|
|
619
|
+
for (const account of backendAccounts) {
|
|
620
|
+
const stateKey = `${backend}/${account.config_dir}`;
|
|
621
|
+
const state = states.get(stateKey);
|
|
622
|
+
if (!state) continue;
|
|
623
|
+
if (_hasHeadroom(state, safetyMargin)) {
|
|
624
|
+
return { backend, account, stateKey };
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Exhaustion fallback: use free_fallback backend
|
|
630
|
+
const fallbackBackend = schedulerConfig.free_fallback.backend;
|
|
631
|
+
const fallbackAccounts = accounts[fallbackBackend];
|
|
632
|
+
if (fallbackAccounts && fallbackAccounts.length > 0) {
|
|
633
|
+
return {
|
|
634
|
+
backend: fallbackBackend,
|
|
635
|
+
account: fallbackAccounts[0],
|
|
636
|
+
stateKey: `${fallbackBackend}/${fallbackAccounts[0].config_dir}`,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// No accounts configured for fallback — use default account (empty config_dir)
|
|
641
|
+
return {
|
|
642
|
+
backend: fallbackBackend,
|
|
643
|
+
account: { config_dir: '' },
|
|
644
|
+
stateKey: fallbackBackend,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Marks one task as in-flight, incrementing the in-flight counter and
|
|
650
|
+
* reserving the EWMA-predicted token cost.
|
|
651
|
+
*
|
|
652
|
+
* @param state - backend usage state to mutate
|
|
653
|
+
*/
|
|
654
|
+
export function markInFlight(state: BackendUsageState): void {
|
|
655
|
+
state.in_flight_count += 1;
|
|
656
|
+
state.tokens_reserved += state.ewma_tokens_per_task;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Marks one in-flight task as complete, decrementing the counter and
|
|
661
|
+
* recalculating tokens_reserved from the updated in-flight count.
|
|
662
|
+
*
|
|
663
|
+
* @param state - backend usage state to mutate
|
|
664
|
+
*/
|
|
665
|
+
export function markComplete(state: BackendUsageState): void {
|
|
666
|
+
state.in_flight_count = Math.max(0, state.in_flight_count - 1);
|
|
667
|
+
state.tokens_reserved = state.ewma_tokens_per_task * state.in_flight_count;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// ─── Shared Helpers ──────────────────────────────────────────────────────────
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Checks whether a CLI binary is available on the system PATH.
|
|
674
|
+
* Uses 'where' on Windows and 'which' on POSIX (I8 fix).
|
|
675
|
+
*/
|
|
676
|
+
export function checkBinary(binary: string): boolean {
|
|
677
|
+
try {
|
|
678
|
+
const { execFileSync } = require('child_process') as typeof import('child_process');
|
|
679
|
+
const cmd = process.platform === 'win32' ? 'where' : 'which';
|
|
680
|
+
execFileSync(cmd, [binary], { stdio: 'ignore' });
|
|
681
|
+
return true;
|
|
682
|
+
} catch {
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// ─── Scheduler Interface and Factory ─────────────────────────────────────────
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* High-level scheduler that selects backends, spawns CLI processes,
|
|
691
|
+
* records usage samples, and persists learned state across sessions.
|
|
692
|
+
*/
|
|
693
|
+
export interface Scheduler {
|
|
694
|
+
/**
|
|
695
|
+
* Unique per-createScheduler session key used to namespace pressure
|
|
696
|
+
* transition logging. Format: 'pid-<pid>-session-<counter>'. Read-only.
|
|
697
|
+
* (O3 fix — multiple createScheduler calls in the same process no
|
|
698
|
+
* longer share _lastLoggedPressure state.)
|
|
699
|
+
*/
|
|
700
|
+
readonly sessionKey: string;
|
|
701
|
+
spawn(prompt: string, opts: SpawnOpts): Promise<SchedulerSpawnResult>;
|
|
702
|
+
getState(stateKey: string): BackendUsageState | undefined;
|
|
703
|
+
/**
|
|
704
|
+
* Returns a snapshot of the current per-account states map. Used by
|
|
705
|
+
* the Spec 4 budget pressure detection and complexity estimation
|
|
706
|
+
* wire-ups. Do NOT mutate the returned map — it is shared with the
|
|
707
|
+
* scheduler's internal state.
|
|
708
|
+
*/
|
|
709
|
+
getStates(): Map<string, BackendUsageState>;
|
|
710
|
+
recordExternalSample(stateKey: string, sample: UsageSample): void;
|
|
711
|
+
persistState(planningDir: string): void;
|
|
712
|
+
loadPersistedState(planningDir: string): void;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Initializes per-account states when account rotation is enabled.
|
|
717
|
+
* Creates a BackendUsageState for each account across all priority backends
|
|
718
|
+
* and the fallback backend, using compound keys like "claude/~/.claude-personal".
|
|
719
|
+
*
|
|
720
|
+
* @param states - state map to populate
|
|
721
|
+
* @param schedulerConfig - scheduler configuration with priority and fallback
|
|
722
|
+
* @param superpowersConfig - superpowers configuration with accounts
|
|
723
|
+
*/
|
|
724
|
+
function _initAccountStates(
|
|
725
|
+
states: Map<string, BackendUsageState>,
|
|
726
|
+
schedulerConfig: SchedulerConfig,
|
|
727
|
+
superpowersConfig: SuperpowersConfig
|
|
728
|
+
): void {
|
|
729
|
+
const accounts = superpowersConfig.accounts;
|
|
730
|
+
const allBackends = new Set([
|
|
731
|
+
...schedulerConfig.backend_priority,
|
|
732
|
+
schedulerConfig.free_fallback.backend,
|
|
733
|
+
]);
|
|
734
|
+
|
|
735
|
+
for (const backend of allBackends) {
|
|
736
|
+
const backendAccounts = accounts[backend];
|
|
737
|
+
if (!backendAccounts || backendAccounts.length === 0) continue;
|
|
738
|
+
|
|
739
|
+
const limit = schedulerConfig.backend_limits?.[backend]?.tpm;
|
|
740
|
+
const isFallback = backend === schedulerConfig.free_fallback.backend;
|
|
741
|
+
const budget = limit ?? (isFallback ? FREE_FALLBACK_BUDGET : DEFAULT_BUDGET_TPM);
|
|
742
|
+
|
|
743
|
+
for (const account of backendAccounts) {
|
|
744
|
+
const stateKey = `${backend}/${account.config_dir}`;
|
|
745
|
+
states.set(stateKey, createBackendState(budget));
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Computes the maximum number of 429 retries allowed for account rotation.
|
|
752
|
+
* Equals total number of accounts across all priority backends.
|
|
753
|
+
*
|
|
754
|
+
* @param schedulerConfig - scheduler configuration with priority
|
|
755
|
+
* @param superpowersConfig - superpowers configuration with accounts
|
|
756
|
+
* @returns maximum retry count
|
|
757
|
+
*/
|
|
758
|
+
function _computeMaxRetries(
|
|
759
|
+
schedulerConfig: SchedulerConfig,
|
|
760
|
+
superpowersConfig: SuperpowersConfig
|
|
761
|
+
): number {
|
|
762
|
+
let maxAccountsPerBackend = 0;
|
|
763
|
+
for (const backend of schedulerConfig.backend_priority) {
|
|
764
|
+
const backendAccounts = superpowersConfig.accounts[backend];
|
|
765
|
+
if (backendAccounts) {
|
|
766
|
+
maxAccountsPerBackend = Math.max(maxAccountsPerBackend, backendAccounts.length);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return schedulerConfig.backend_priority.length * Math.max(maxAccountsPerBackend, 1);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Creates a Scheduler instance from the given config, or returns null
|
|
774
|
+
* when no config is provided (pass-through / disabled mode).
|
|
775
|
+
*
|
|
776
|
+
* When superpowersConfig is provided with account_rotation enabled, the
|
|
777
|
+
* scheduler tracks per-account state and uses resolveAccount() for backend
|
|
778
|
+
* selection. Otherwise, it uses the simple pickBackend() flow.
|
|
779
|
+
*
|
|
780
|
+
* @param config - scheduler configuration, or undefined to disable
|
|
781
|
+
* @param superpowersConfig - optional superpowers configuration for account rotation
|
|
782
|
+
* @returns Scheduler instance, or null if config is absent
|
|
783
|
+
*/
|
|
784
|
+
export function createScheduler(
|
|
785
|
+
config: SchedulerConfig | undefined,
|
|
786
|
+
superpowersConfig?: SuperpowersConfig
|
|
787
|
+
): Scheduler | null {
|
|
788
|
+
if (!config) return null;
|
|
789
|
+
|
|
790
|
+
// Unique key for this scheduler instance, used to namespace
|
|
791
|
+
// _lastLoggedPressure so multiple schedulers in the same process do not
|
|
792
|
+
// share transition state (O3).
|
|
793
|
+
const sessionKey = `pid-${process.pid}-session-${_nextSchedulerSessionId++}`;
|
|
794
|
+
|
|
795
|
+
// Apply Spec 2A defaults here so the rest of the scheduler can rely on
|
|
796
|
+
// a fully-populated config. Spread-merge avoids mutating caller input.
|
|
797
|
+
const schedulerConfig: SchedulerConfig = {
|
|
798
|
+
...config,
|
|
799
|
+
max_wait_minutes: config.max_wait_minutes ?? 90,
|
|
800
|
+
};
|
|
801
|
+
const states = new Map<string, BackendUsageState>();
|
|
802
|
+
const prediction = schedulerConfig.prediction;
|
|
803
|
+
const accountRotation = !!superpowersConfig?.account_rotation;
|
|
804
|
+
|
|
805
|
+
if (accountRotation && superpowersConfig) {
|
|
806
|
+
// Per-account state initialization
|
|
807
|
+
_initAccountStates(states, schedulerConfig, superpowersConfig);
|
|
808
|
+
|
|
809
|
+
// Also initialize fallback backend with no config_dir for the exhaustion case
|
|
810
|
+
const fallbackBackend = schedulerConfig.free_fallback.backend;
|
|
811
|
+
if (!states.has(fallbackBackend)) {
|
|
812
|
+
const limit = schedulerConfig.backend_limits?.[fallbackBackend]?.tpm;
|
|
813
|
+
const budget = limit ?? FREE_FALLBACK_BUDGET;
|
|
814
|
+
states.set(fallbackBackend, createBackendState(budget));
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// If no accounts at all, initialize default_backend with no config_dir
|
|
818
|
+
const hasAnyAccounts = Object.keys(superpowersConfig.accounts).some(
|
|
819
|
+
(k) => (superpowersConfig.accounts[k as AdapterBackendId] || []).length > 0
|
|
820
|
+
);
|
|
821
|
+
if (!hasAnyAccounts) {
|
|
822
|
+
const defaultBackend = superpowersConfig.default_backend as AdapterBackendId;
|
|
823
|
+
if (!states.has(defaultBackend)) {
|
|
824
|
+
const limit = schedulerConfig.backend_limits?.[defaultBackend]?.tpm;
|
|
825
|
+
const budget = limit ?? DEFAULT_BUDGET_TPM;
|
|
826
|
+
states.set(defaultBackend, createBackendState(budget));
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
} else {
|
|
830
|
+
// Simple per-backend state initialization (existing behavior)
|
|
831
|
+
const allBackends = [
|
|
832
|
+
...schedulerConfig.backend_priority,
|
|
833
|
+
schedulerConfig.free_fallback.backend,
|
|
834
|
+
];
|
|
835
|
+
for (const backend of new Set(allBackends)) {
|
|
836
|
+
const limit = schedulerConfig.backend_limits?.[backend]?.tpm;
|
|
837
|
+
const isFallback = backend === schedulerConfig.free_fallback.backend;
|
|
838
|
+
const budget = limit ?? (isFallback ? FREE_FALLBACK_BUDGET : DEFAULT_BUDGET_TPM);
|
|
839
|
+
states.set(backend, createBackendState(budget));
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Check which backend binaries are available
|
|
844
|
+
const availableBackends = new Set<string>();
|
|
845
|
+
const allBackendIds = new Set([
|
|
846
|
+
...schedulerConfig.backend_priority,
|
|
847
|
+
schedulerConfig.free_fallback.backend,
|
|
848
|
+
]);
|
|
849
|
+
for (const backend of allBackendIds) {
|
|
850
|
+
const adapter = ADAPTERS[backend];
|
|
851
|
+
if (adapter && checkBinary(adapter.binary)) availableBackends.add(backend);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const maxRetries =
|
|
855
|
+
accountRotation && superpowersConfig
|
|
856
|
+
? _computeMaxRetries(schedulerConfig, superpowersConfig)
|
|
857
|
+
: schedulerConfig.backend_priority.length;
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* Internal spawn implementation with retry counter for 429 rate-limit retries.
|
|
861
|
+
* Capped at maxRetries to prevent infinite loops when all accounts are exhausted.
|
|
862
|
+
*/
|
|
863
|
+
async function _spawnWithRetry(
|
|
864
|
+
prompt: string,
|
|
865
|
+
opts: SpawnOpts,
|
|
866
|
+
retryCount: number,
|
|
867
|
+
lastRecoveryTime: number | null = null
|
|
868
|
+
): Promise<SchedulerSpawnResult> {
|
|
869
|
+
let backend: AdapterBackendId;
|
|
870
|
+
let stateKey: string;
|
|
871
|
+
const envOverrides: Record<string, string> = {};
|
|
872
|
+
|
|
873
|
+
if (accountRotation && superpowersConfig) {
|
|
874
|
+
// Account-rotation path: resolve backend + account
|
|
875
|
+
const resolution = resolveAccount(
|
|
876
|
+
superpowersConfig,
|
|
877
|
+
schedulerConfig,
|
|
878
|
+
states,
|
|
879
|
+
prediction.safety_margin_tasks
|
|
880
|
+
);
|
|
881
|
+
backend = resolution.backend;
|
|
882
|
+
stateKey = resolution.stateKey;
|
|
883
|
+
|
|
884
|
+
// Set env var for the account's config directory
|
|
885
|
+
if (resolution.account.config_dir) {
|
|
886
|
+
envOverrides[ENV_VAR_MAP[backend]] = resolution.account.config_dir;
|
|
887
|
+
}
|
|
888
|
+
} else {
|
|
889
|
+
// Simple backend picker path (existing behavior)
|
|
890
|
+
const filteredPriority = schedulerConfig.backend_priority.filter((b) =>
|
|
891
|
+
availableBackends.has(b)
|
|
892
|
+
);
|
|
893
|
+
backend = pickBackend(
|
|
894
|
+
filteredPriority,
|
|
895
|
+
states,
|
|
896
|
+
prediction.safety_margin_tasks,
|
|
897
|
+
schedulerConfig.free_fallback
|
|
898
|
+
) as AdapterBackendId;
|
|
899
|
+
stateKey = backend;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Spec 2A: bounded wait for soonest recovery when all priority accounts
|
|
903
|
+
// are exhausted and resolveAccount fell through to free_fallback.
|
|
904
|
+
if (
|
|
905
|
+
accountRotation &&
|
|
906
|
+
superpowersConfig &&
|
|
907
|
+
backend === schedulerConfig.free_fallback.backend &&
|
|
908
|
+
schedulerConfig.backend_priority.length > 0 &&
|
|
909
|
+
!_anyPriorityHasHeadroom(
|
|
910
|
+
schedulerConfig.backend_priority,
|
|
911
|
+
superpowersConfig.accounts,
|
|
912
|
+
states,
|
|
913
|
+
prediction.safety_margin_tasks
|
|
914
|
+
)
|
|
915
|
+
) {
|
|
916
|
+
// Defensive: createScheduler applies 90 default, but TS can't narrow through
|
|
917
|
+
// the spread-merge. The ?? 90 keeps TypeScript happy and guards against
|
|
918
|
+
// direct construction of the SchedulerConfig bypassing createScheduler.
|
|
919
|
+
const maxWaitMinutes = schedulerConfig.max_wait_minutes ?? 90;
|
|
920
|
+
if (maxWaitMinutes > 0) {
|
|
921
|
+
const maxWaitMs = maxWaitMinutes * 60 * 1000;
|
|
922
|
+
const recoveryTime = computeSoonestRecovery(
|
|
923
|
+
states,
|
|
924
|
+
schedulerConfig.backend_priority,
|
|
925
|
+
superpowersConfig.accounts,
|
|
926
|
+
prediction.window_minutes,
|
|
927
|
+
maxWaitMs
|
|
928
|
+
);
|
|
929
|
+
if (recoveryTime !== null && recoveryTime === lastRecoveryTime) {
|
|
930
|
+
// Infinite-loop guard: if this is the same timestamp we already
|
|
931
|
+
// waited for, sample state didn't change. Fall through to
|
|
932
|
+
// free_fallback instead of waiting again (pre-Spec 2A behavior).
|
|
933
|
+
} else if (recoveryTime !== null) {
|
|
934
|
+
const waitMs = recoveryTime - Date.now();
|
|
935
|
+
if (waitMs <= 0) {
|
|
936
|
+
// Recovery target already elapsed — waiting would be a no-op and
|
|
937
|
+
// recursing may not progress. Fall through to free_fallback (I9).
|
|
938
|
+
process.stderr.write(
|
|
939
|
+
`[scheduler] recovery target already elapsed, falling through to free_fallback\n`
|
|
940
|
+
);
|
|
941
|
+
// Fall through to normal spawn with the fallback backend
|
|
942
|
+
} else {
|
|
943
|
+
const displayMinutes = Math.max(0, Math.ceil(waitMs / 60_000));
|
|
944
|
+
process.stderr.write(
|
|
945
|
+
`[scheduler] all priority accounts exhausted, waiting ${displayMinutes}m for soonest recovery (target=${new Date(recoveryTime).toISOString()})\n`
|
|
946
|
+
);
|
|
947
|
+
const waitResult = await waitUntilOrAbort(recoveryTime);
|
|
948
|
+
if (waitResult === 'aborted') {
|
|
949
|
+
throw new Error('scheduler: wait for account recovery interrupted by SIGINT');
|
|
950
|
+
}
|
|
951
|
+
return _spawnWithRetry(prompt, opts, retryCount, recoveryTime);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const adapter = ADAPTERS[backend] || ADAPTERS.claude;
|
|
958
|
+
let state = states.get(stateKey);
|
|
959
|
+
if (!state) {
|
|
960
|
+
// Register the new state in the shared map so markInFlight/markComplete
|
|
961
|
+
// mutations are visible to subsequent dispatches (previously a throw-away
|
|
962
|
+
// orphan object silently lost budget accounting — I1).
|
|
963
|
+
state = createBackendState(DEFAULT_BUDGET_TPM);
|
|
964
|
+
states.set(stateKey, state);
|
|
965
|
+
}
|
|
966
|
+
const args = adapter.buildArgs(prompt, opts);
|
|
967
|
+
const workItemId = opts.workItemId || `task-${Date.now()}`;
|
|
968
|
+
|
|
969
|
+
markInFlight(state);
|
|
970
|
+
const startTime = Date.now();
|
|
971
|
+
|
|
972
|
+
try {
|
|
973
|
+
const { spawn } = require('child_process') as typeof import('child_process');
|
|
974
|
+
// Codex r7 P2: callers can request "unlimited" total timeout by
|
|
975
|
+
// passing 0 explicitly (autoresearch --time-budget 0 path).
|
|
976
|
+
// Distinguish missing (undefined → 2hr default) from explicit 0
|
|
977
|
+
// (no total timeout — only idle watchdog applies).
|
|
978
|
+
const totalTimeoutMs =
|
|
979
|
+
opts.timeout === 0
|
|
980
|
+
? null
|
|
981
|
+
: (typeof opts.timeout === 'number' ? opts.timeout : 120 * 60 * 1000);
|
|
982
|
+
const idleTimeoutMs = _resolveIdleTimeoutSeconds(backend, schedulerConfig) * 1000;
|
|
983
|
+
const MAX_BUFFER_BYTES = 50 * 1024 * 1024;
|
|
984
|
+
|
|
985
|
+
const result = await new Promise<SchedulerSpawnResult>((resolve) => {
|
|
986
|
+
const isWindows = process.platform === 'win32';
|
|
987
|
+
const child = spawn(adapter.binary, args, {
|
|
988
|
+
cwd: opts.cwd || process.cwd(),
|
|
989
|
+
env: { ...process.env, ...envOverrides },
|
|
990
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
991
|
+
// Create a new process group on POSIX so we can signal children + grandchildren.
|
|
992
|
+
// Windows doesn't support process groups — fall back to default.
|
|
993
|
+
detached: !isWindows,
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
let stdoutBuf = '';
|
|
997
|
+
let stderrBuf = '';
|
|
998
|
+
let stdoutOverflowed = false;
|
|
999
|
+
let idleTimedOut = false;
|
|
1000
|
+
let totalTimedOut = false;
|
|
1001
|
+
let resolved = false;
|
|
1002
|
+
// Track SIGKILL escalation timers so they can be cleared when the
|
|
1003
|
+
// child exits, preventing stale kill signals to recycled PIDs (I2).
|
|
1004
|
+
let idleKillTimer: ReturnType<typeof setTimeout> | undefined;
|
|
1005
|
+
let totalKillTimer: ReturnType<typeof setTimeout> | undefined;
|
|
1006
|
+
|
|
1007
|
+
const safeResolve = (r: SchedulerSpawnResult): void => {
|
|
1008
|
+
if (resolved) return;
|
|
1009
|
+
resolved = true;
|
|
1010
|
+
resolve(r);
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1013
|
+
const watchdog = _startIdleWatchdog(idleTimeoutMs, () => {
|
|
1014
|
+
idleTimedOut = true;
|
|
1015
|
+
incrementCounter('scheduler.idle_kills_total');
|
|
1016
|
+
process.stderr.write(
|
|
1017
|
+
`[scheduler] spawn idle ${Math.round(idleTimeoutMs / 1000)}s, killing ${adapter.binary} (stateKey=${stateKey}, workItemId=${workItemId})\n`
|
|
1018
|
+
);
|
|
1019
|
+
_killProcessTree(child, 'SIGTERM');
|
|
1020
|
+
idleKillTimer = setTimeout(() => {
|
|
1021
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
1022
|
+
_killProcessTree(child, 'SIGKILL');
|
|
1023
|
+
}
|
|
1024
|
+
}, 5000);
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
const totalTimer =
|
|
1028
|
+
totalTimeoutMs === null
|
|
1029
|
+
? null
|
|
1030
|
+
: setTimeout(() => {
|
|
1031
|
+
totalTimedOut = true;
|
|
1032
|
+
_killProcessTree(child, 'SIGTERM');
|
|
1033
|
+
totalKillTimer = setTimeout(() => {
|
|
1034
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
1035
|
+
_killProcessTree(child, 'SIGKILL');
|
|
1036
|
+
}
|
|
1037
|
+
}, 5000);
|
|
1038
|
+
}, totalTimeoutMs);
|
|
1039
|
+
|
|
1040
|
+
// Codex r44 P1 #6: detect live spinning subprocesses. The
|
|
1041
|
+
// prior implementation only ran detectSpin on the captured
|
|
1042
|
+
// stdout buffer in the `close` handler, so an actively-
|
|
1043
|
+
// looping subprocess that keeps printing would never trigger
|
|
1044
|
+
// until total timeout. Keep a rolling window of recent stdout
|
|
1045
|
+
// chunks (each chunk = ~one streamed Claude output token block),
|
|
1046
|
+
// and run detectSpin every SPIN_CHECK_EVERY chunks. If detected
|
|
1047
|
+
// and not yet acted on, kill the subprocess so the parent sees
|
|
1048
|
+
// the spinEvent and can react.
|
|
1049
|
+
const SPIN_CHECK_WINDOW = 5;
|
|
1050
|
+
const SPIN_CHECK_EVERY = 5;
|
|
1051
|
+
const recentChunks: string[] = [];
|
|
1052
|
+
let liveSpinEvent: import('./scheduler').SpinDetectedEvent | null = null;
|
|
1053
|
+
let chunksSinceCheck = 0;
|
|
1054
|
+
|
|
1055
|
+
child.stdout?.on('data', (chunk: Buffer) => {
|
|
1056
|
+
watchdog.markActivity();
|
|
1057
|
+
if (stdoutBuf.length + chunk.length > MAX_BUFFER_BYTES) {
|
|
1058
|
+
stdoutOverflowed = true;
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
const text = chunk.toString('utf-8');
|
|
1062
|
+
stdoutBuf += text;
|
|
1063
|
+
recentChunks.push(text);
|
|
1064
|
+
if (recentChunks.length > SPIN_CHECK_WINDOW) recentChunks.shift();
|
|
1065
|
+
chunksSinceCheck++;
|
|
1066
|
+
if (
|
|
1067
|
+
!liveSpinEvent &&
|
|
1068
|
+
recentChunks.length >= SPIN_CHECK_WINDOW &&
|
|
1069
|
+
chunksSinceCheck >= SPIN_CHECK_EVERY
|
|
1070
|
+
) {
|
|
1071
|
+
chunksSinceCheck = 0;
|
|
1072
|
+
const evt = detectSpin(recentChunks);
|
|
1073
|
+
if (evt.detected) {
|
|
1074
|
+
liveSpinEvent = evt;
|
|
1075
|
+
_killProcessTree(child, 'SIGTERM');
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
child.stderr?.on('data', (chunk: Buffer) => {
|
|
1081
|
+
watchdog.markActivity();
|
|
1082
|
+
if (stderrBuf.length + chunk.length > MAX_BUFFER_BYTES) return;
|
|
1083
|
+
stderrBuf += chunk.toString('utf-8');
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
child.on('error', (err) => {
|
|
1087
|
+
watchdog.stop();
|
|
1088
|
+
if (totalTimer) clearTimeout(totalTimer);
|
|
1089
|
+
if (idleKillTimer) clearTimeout(idleKillTimer);
|
|
1090
|
+
if (totalKillTimer) clearTimeout(totalKillTimer);
|
|
1091
|
+
markComplete(state);
|
|
1092
|
+
safeResolve({
|
|
1093
|
+
exitCode: 1,
|
|
1094
|
+
stdout: undefined,
|
|
1095
|
+
stderr: err.message,
|
|
1096
|
+
timedOut: false,
|
|
1097
|
+
idleTimedOut: false,
|
|
1098
|
+
backend: backend as BackendId,
|
|
1099
|
+
tokensUsed: 0,
|
|
1100
|
+
workItemId,
|
|
1101
|
+
});
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
child.on('close', (code) => {
|
|
1105
|
+
watchdog.stop();
|
|
1106
|
+
if (totalTimer) clearTimeout(totalTimer);
|
|
1107
|
+
if (idleKillTimer) clearTimeout(idleKillTimer);
|
|
1108
|
+
if (totalKillTimer) clearTimeout(totalKillTimer);
|
|
1109
|
+
const duration = Date.now() - startTime;
|
|
1110
|
+
// Codex r45 P1 #6 followup: when we killed the subprocess
|
|
1111
|
+
// because of a live spin detection, the process exits with
|
|
1112
|
+
// code=null (SIGTERM). The prior `code ?? 0` fallback then
|
|
1113
|
+
// reported success, so callers wrote SPIN-REPORT.md AND
|
|
1114
|
+
// marked the phase completed. Treat a spin-kill as a
|
|
1115
|
+
// failure exit so autopilot does not advance on a killed
|
|
1116
|
+
// step.
|
|
1117
|
+
const exitCode =
|
|
1118
|
+
code ?? (idleTimedOut || totalTimedOut || liveSpinEvent ? 1 : 0);
|
|
1119
|
+
const tokens = adapter.parseTokenUsage(stderrBuf) ?? Math.round(duration * 10);
|
|
1120
|
+
|
|
1121
|
+
const sample: UsageSample = {
|
|
1122
|
+
backend: backend as BackendId,
|
|
1123
|
+
stateKey,
|
|
1124
|
+
agentType: opts.agentType, // M2: record per-agent type for complexity routing
|
|
1125
|
+
timestamp: Date.now(),
|
|
1126
|
+
duration,
|
|
1127
|
+
tokenEstimate: tokens,
|
|
1128
|
+
exitCode,
|
|
1129
|
+
workItemId,
|
|
1130
|
+
};
|
|
1131
|
+
|
|
1132
|
+
markComplete(state);
|
|
1133
|
+
recordSample(state, sample, prediction.window_minutes, prediction.ewma_alpha);
|
|
1134
|
+
|
|
1135
|
+
// Periodic persistence: every 10 samples across all backends
|
|
1136
|
+
const totalSamples = Array.from(states.values()).reduce(
|
|
1137
|
+
(sum, s) => sum + s.samples.length,
|
|
1138
|
+
0
|
|
1139
|
+
);
|
|
1140
|
+
if (totalSamples % 10 === 0 && opts.cwd) {
|
|
1141
|
+
const { join } = require('path') as typeof import('path');
|
|
1142
|
+
scheduler.persistState(join(opts.cwd, '.planning'));
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// Codex r15 P2 / r44 P1 #6: prefer the live spin event if
|
|
1146
|
+
// detected during streaming (the process was killed for it);
|
|
1147
|
+
// otherwise scan the final buffer for post-hoc detection.
|
|
1148
|
+
let spinEvent: import('./scheduler').SpinDetectedEvent | undefined =
|
|
1149
|
+
liveSpinEvent ?? undefined;
|
|
1150
|
+
if (!spinEvent && stdoutBuf.length > 500) {
|
|
1151
|
+
const chunkSize = Math.max(200, Math.floor(stdoutBuf.length / 12));
|
|
1152
|
+
const chunks: string[] = [];
|
|
1153
|
+
for (let i = 0; i < stdoutBuf.length; i += chunkSize) {
|
|
1154
|
+
chunks.push(stdoutBuf.slice(i, i + chunkSize));
|
|
1155
|
+
}
|
|
1156
|
+
const evt = detectSpin(chunks);
|
|
1157
|
+
if (evt.detected) spinEvent = evt;
|
|
1158
|
+
}
|
|
1159
|
+
safeResolve({
|
|
1160
|
+
exitCode,
|
|
1161
|
+
stdout: opts.captureOutput && !stdoutOverflowed ? stdoutBuf : undefined,
|
|
1162
|
+
stderr: stderrBuf || undefined,
|
|
1163
|
+
timedOut: totalTimedOut,
|
|
1164
|
+
idleTimedOut,
|
|
1165
|
+
backend: backend as BackendId,
|
|
1166
|
+
tokensUsed: tokens,
|
|
1167
|
+
workItemId,
|
|
1168
|
+
spinEvent,
|
|
1169
|
+
});
|
|
1170
|
+
});
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
// Rate limit retry: if rate-limited despite prediction, cooldown and retry
|
|
1174
|
+
if (adapter.isRateLimited(result.exitCode, result.stderr || '')) {
|
|
1175
|
+
// Enforce a minimum 5-minute cooldown so a zero/missing window_minutes never skips the guard
|
|
1176
|
+
state.cooldown_until = Date.now() + Math.max(prediction.window_minutes || 60, 5) * 60 * 1000;
|
|
1177
|
+
|
|
1178
|
+
// Max retry guard: cap recursive retries
|
|
1179
|
+
if (retryCount >= maxRetries) {
|
|
1180
|
+
return result; // Exhausted all retries, return last result
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
return _spawnWithRetry(prompt, opts, retryCount + 1);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
return result;
|
|
1187
|
+
} catch (_err) {
|
|
1188
|
+
markComplete(state);
|
|
1189
|
+
return {
|
|
1190
|
+
exitCode: 1,
|
|
1191
|
+
timedOut: false,
|
|
1192
|
+
backend: backend as BackendId,
|
|
1193
|
+
tokensUsed: 0,
|
|
1194
|
+
workItemId,
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
const scheduler: Scheduler = {
|
|
1200
|
+
sessionKey,
|
|
1201
|
+
|
|
1202
|
+
getState(stateKey: string): BackendUsageState | undefined {
|
|
1203
|
+
return states.get(stateKey);
|
|
1204
|
+
},
|
|
1205
|
+
|
|
1206
|
+
getStates(): Map<string, BackendUsageState> {
|
|
1207
|
+
return states;
|
|
1208
|
+
},
|
|
1209
|
+
|
|
1210
|
+
recordExternalSample(stateKey: string, sample: UsageSample): void {
|
|
1211
|
+
let state = states.get(stateKey);
|
|
1212
|
+
if (!state) {
|
|
1213
|
+
state = createBackendState(DEFAULT_BUDGET_TPM);
|
|
1214
|
+
states.set(stateKey, state);
|
|
1215
|
+
}
|
|
1216
|
+
recordSample(state, sample, prediction.window_minutes, prediction.ewma_alpha);
|
|
1217
|
+
},
|
|
1218
|
+
|
|
1219
|
+
async spawn(prompt: string, opts: SpawnOpts): Promise<SchedulerSpawnResult> {
|
|
1220
|
+
return _spawnWithRetry(prompt, opts, 0);
|
|
1221
|
+
},
|
|
1222
|
+
|
|
1223
|
+
persistState(planningDir: string): void {
|
|
1224
|
+
const { writeFileSync } = require('fs') as typeof import('fs');
|
|
1225
|
+
const { join } = require('path') as typeof import('path');
|
|
1226
|
+
const data: Record<string, unknown> = { version: 1, backends: {} };
|
|
1227
|
+
const backends = data.backends as Record<string, unknown>;
|
|
1228
|
+
for (const [key, state] of states) {
|
|
1229
|
+
backends[key] = {
|
|
1230
|
+
token_budget: state.token_budget,
|
|
1231
|
+
ewma_tokens_per_task: state.ewma_tokens_per_task,
|
|
1232
|
+
budget_learned: state.budget_learned,
|
|
1233
|
+
budget_confidence: state.budget_confidence,
|
|
1234
|
+
last_updated: Date.now(),
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
writeFileSync(
|
|
1238
|
+
join(planningDir, 'scheduler-state.json'),
|
|
1239
|
+
JSON.stringify(data, null, 2) + '\n'
|
|
1240
|
+
);
|
|
1241
|
+
},
|
|
1242
|
+
|
|
1243
|
+
loadPersistedState(planningDir: string): void {
|
|
1244
|
+
const {
|
|
1245
|
+
safeReadJSON,
|
|
1246
|
+
}: { safeReadJSON: (p: string, d?: unknown) => unknown } = require('./utils');
|
|
1247
|
+
const { join } = require('path') as typeof import('path');
|
|
1248
|
+
const raw = safeReadJSON(join(planningDir, 'scheduler-state.json')) as {
|
|
1249
|
+
version?: number;
|
|
1250
|
+
backends?: Record<
|
|
1251
|
+
string,
|
|
1252
|
+
{
|
|
1253
|
+
token_budget: number;
|
|
1254
|
+
ewma_tokens_per_task: number;
|
|
1255
|
+
budget_learned: boolean;
|
|
1256
|
+
budget_confidence: number;
|
|
1257
|
+
}
|
|
1258
|
+
>;
|
|
1259
|
+
} | null;
|
|
1260
|
+
if (!raw || raw.version !== 1 || !raw.backends) return;
|
|
1261
|
+
for (const [key, saved] of Object.entries(raw.backends)) {
|
|
1262
|
+
const state = states.get(key);
|
|
1263
|
+
if (!state) continue;
|
|
1264
|
+
if (saved.budget_learned) state.token_budget = saved.token_budget;
|
|
1265
|
+
state.ewma_tokens_per_task = saved.ewma_tokens_per_task;
|
|
1266
|
+
state.budget_learned = saved.budget_learned;
|
|
1267
|
+
state.budget_confidence = saved.budget_confidence;
|
|
1268
|
+
}
|
|
1269
|
+
},
|
|
1270
|
+
};
|
|
1271
|
+
|
|
1272
|
+
return scheduler;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// ─── Spin Detection ──────────────────────────────────────────────────────────
|
|
1276
|
+
|
|
1277
|
+
/** Result from detectSpin analysis. */
|
|
1278
|
+
export interface SpinDetectedEvent {
|
|
1279
|
+
detected: boolean;
|
|
1280
|
+
/** The repeated pattern excerpt (first 200 chars of the repeated chunk). */
|
|
1281
|
+
repeated_pattern: string;
|
|
1282
|
+
/** Number of consecutive similar chunks detected. */
|
|
1283
|
+
consecutive_count: number;
|
|
1284
|
+
/** Similarity score of the most repeated pair (0-1). */
|
|
1285
|
+
max_similarity: number;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/**
|
|
1289
|
+
* Analyze the last N stdout chunks from an agent run to detect spin (looping on
|
|
1290
|
+
* the same error without progress). Uses bigram overlap (Jaccard similarity) to
|
|
1291
|
+
* compare consecutive chunks.
|
|
1292
|
+
*
|
|
1293
|
+
* Returns SpinDetectedEvent with detected=true if 3+ consecutive pairs have
|
|
1294
|
+
* similarity >= threshold (default 0.80).
|
|
1295
|
+
*
|
|
1296
|
+
* @param chunks - Array of stdout text chunks (ordered chronologically)
|
|
1297
|
+
* @param threshold - Similarity threshold for spin detection (default 0.80)
|
|
1298
|
+
* @param windowSize - Number of recent chunks to analyze (default 5)
|
|
1299
|
+
*/
|
|
1300
|
+
export function detectSpin(chunks: string[], threshold = 0.80, windowSize = 5): SpinDetectedEvent {
|
|
1301
|
+
const NO_SPIN: SpinDetectedEvent = { detected: false, repeated_pattern: '', consecutive_count: 0, max_similarity: 0 };
|
|
1302
|
+
const recent = chunks.slice(-windowSize);
|
|
1303
|
+
if (recent.length < 3) return NO_SPIN;
|
|
1304
|
+
|
|
1305
|
+
/** Compute bigram set from a normalized text chunk. */
|
|
1306
|
+
function bigrams(text: string): Set<string> {
|
|
1307
|
+
const normalized = text.toLowerCase().replace(/[^a-z0-9\n]/g, ' ').replace(/\s+/g, ' ').trim();
|
|
1308
|
+
const set = new Set<string>();
|
|
1309
|
+
for (let i = 0; i + 1 < normalized.length; i++) {
|
|
1310
|
+
set.add(normalized.slice(i, i + 2));
|
|
1311
|
+
}
|
|
1312
|
+
return set;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
function jaccard(a: Set<string>, b: Set<string>): number {
|
|
1316
|
+
if (a.size === 0 && b.size === 0) return 1;
|
|
1317
|
+
let intersection = 0;
|
|
1318
|
+
for (const t of a) { if (b.has(t)) intersection++; }
|
|
1319
|
+
const union = a.size + b.size - intersection;
|
|
1320
|
+
return union === 0 ? 0 : intersection / union;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
let consecutiveCount = 0;
|
|
1324
|
+
let maxSimilarity = 0;
|
|
1325
|
+
let repeatedPattern = '';
|
|
1326
|
+
|
|
1327
|
+
for (let i = 1; i < recent.length; i++) {
|
|
1328
|
+
const sim = jaccard(bigrams(recent[i - 1]), bigrams(recent[i]));
|
|
1329
|
+
if (sim >= threshold) {
|
|
1330
|
+
consecutiveCount++;
|
|
1331
|
+
if (sim > maxSimilarity) {
|
|
1332
|
+
maxSimilarity = sim;
|
|
1333
|
+
repeatedPattern = recent[i].slice(0, 200);
|
|
1334
|
+
}
|
|
1335
|
+
} else {
|
|
1336
|
+
consecutiveCount = 0;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
if (consecutiveCount >= 2) {
|
|
1341
|
+
return { detected: true, repeated_pattern: repeatedPattern, consecutive_count: consecutiveCount + 1, max_similarity: Math.round(maxSimilarity * 100) / 100 };
|
|
1342
|
+
}
|
|
1343
|
+
return NO_SPIN;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
module.exports = {
|
|
1347
|
+
ADAPTERS,
|
|
1348
|
+
ENV_VAR_MAP,
|
|
1349
|
+
FREE_FALLBACK_BUDGET,
|
|
1350
|
+
checkBinary,
|
|
1351
|
+
_checkBinary: checkBinary,
|
|
1352
|
+
createBackendState,
|
|
1353
|
+
updateEWMA,
|
|
1354
|
+
evictExpiredSamples,
|
|
1355
|
+
recordSample,
|
|
1356
|
+
pickBackend,
|
|
1357
|
+
resolveAccount,
|
|
1358
|
+
markInFlight,
|
|
1359
|
+
markComplete,
|
|
1360
|
+
createScheduler,
|
|
1361
|
+
computeSoonestRecovery,
|
|
1362
|
+
_anyPriorityHasHeadroom,
|
|
1363
|
+
_startIdleWatchdog,
|
|
1364
|
+
_resolveIdleTimeoutSeconds,
|
|
1365
|
+
_killProcessTree,
|
|
1366
|
+
isBudgetPressured,
|
|
1367
|
+
computeBudgetPressureLevel,
|
|
1368
|
+
logPressureTransition,
|
|
1369
|
+
detectSpin,
|
|
1370
|
+
};
|