pan-wizard 3.35.0 → 3.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3645 -0
- package/README.md +46 -52
- package/agents/pan-conductor.md +2 -3
- package/agents/pan-counterfactual.md +1 -2
- package/agents/pan-debugger.md +14 -3
- package/agents/pan-design-checker.md +1 -2
- package/agents/pan-designer.md +2 -3
- package/agents/pan-document_code.md +1 -2
- package/agents/pan-executor.md +25 -0
- package/agents/pan-hardener.md +4 -3
- package/agents/pan-integration-checker.md +1 -2
- package/agents/pan-knowledge.md +1 -2
- package/agents/pan-meta-reviewer.md +3 -3
- package/agents/pan-optimizer.md +4 -4
- package/agents/pan-phase-researcher.md +80 -475
- package/agents/pan-plan-checker.md +40 -3
- package/agents/pan-planner.md +21 -7
- package/agents/pan-previewer.md +3 -2
- package/agents/pan-project-researcher.md +72 -493
- package/agents/pan-release.md +1 -1
- package/agents/pan-reviewer.md +14 -3
- package/agents/pan-roadmapper.md +12 -7
- package/agents/pan-verifier.md +9 -4
- package/bin/install-lib.cjs +134 -14
- package/bin/install.js +59 -12
- package/commands/pan/army.md +1 -1
- package/commands/pan/autodev.md +76 -0
- package/commands/pan/cost.md +21 -1
- package/commands/pan/exec-phase.md +3 -4
- package/commands/pan/focus-auto.md +14 -7
- package/commands/pan/focus-design.md +4 -5
- package/commands/pan/focus-docs.md +24 -0
- package/commands/pan/learn.md +13 -6
- package/commands/pan/milestone-audit.md +8 -1
- package/commands/pan/milestone-done.md +10 -5
- package/commands/pan/milestone-new.md +2 -2
- package/commands/pan/new-project.md +15 -12
- package/commands/pan/plan-phase.md +15 -9
- package/commands/pan/preview.md +1 -1
- package/commands/pan/quick.md +1 -1
- package/commands/pan/review-deep.md +11 -5
- package/commands/pan/settings.md +6 -0
- package/commands/pan/verify-phase.md +6 -2
- package/docs/AGENTS.md +917 -0
- package/docs/ARCHITECTURE.md +1054 -0
- package/docs/CLI-REFERENCE.md +4582 -0
- package/docs/COMPARISON.md +116 -0
- package/docs/DEVELOPMENT.md +490 -0
- package/docs/ECOSYSTEM-REVIEW-2026-06.md +107 -0
- package/docs/ECOSYSTEM-REVIEW-2026-08.md +349 -0
- package/docs/EXAMPLES.md +584 -0
- package/docs/FAQ.md +254 -0
- package/docs/FIELD-HARVEST-2026-07.md +88 -0
- package/docs/FIELD-REPORT-army-2026-06.md +121 -0
- package/docs/HOOKS.md +638 -0
- package/docs/IMPROVEMENT-TODO.md +246 -0
- package/docs/INTERNALS.md +485 -0
- package/docs/MIGRATION-v2-to-v3.md +258 -0
- package/docs/SECURITY-HARDENING.md +230 -0
- package/docs/SKILLS-ENHANCEMENT-PLAN-V2.md +122 -0
- package/docs/SKILLS-FULL-TEXT.md +9685 -0
- package/docs/SKILLS-REFERENCE.md +1037 -0
- package/docs/TROUBLESHOOTING.md +1084 -0
- package/docs/USER-GUIDE.md +1437 -0
- package/docs/archive/SKILLS-ENHANCEMENT-PLAN.md +230 -0
- package/docs/audits/AUDIT-2026-08-e2e-ROADMAP.md +117 -0
- package/docs/audits/AUDIT-2026-08-e2e-VERIFICATION.md +693 -0
- package/docs/audits/AUDIT-2026-08-e2e.md +1546 -0
- package/docs/audits/OUTSTANDING-2026-08.md +275 -0
- package/docs/audits/STABILITY-TEST-2026-08.md +640 -0
- package/docs/branding/README-brand-draft.md +119 -0
- package/docs/branding/handoff/IMPLEMENTATION.md +205 -0
- package/docs/branding/handoff/README-brand-draft.md +119 -0
- package/docs/branding/handoff/image-prompts.md +46 -0
- package/docs/branding/image-prompts.md +46 -0
- package/docs/branding/nano-banana-prompts.txt +60 -0
- package/docs/context-monitor.md +13 -0
- package/docs/decisions/ADR-0001-strategic-feature-prioritization.md +83 -0
- package/docs/decisions/ADR-0002-copilot-cli-runtime.md +71 -0
- package/docs/decisions/ADR-0003-smart-execution.md +84 -0
- package/docs/decisions/ADR-0004-copilot-cli-interaction.md +89 -0
- package/docs/decisions/ADR-0005-command-naming.md +80 -0
- package/docs/decisions/ADR-0006-focus-commands.md +101 -0
- package/docs/decisions/ADR-0007-ship-v1.md +73 -0
- package/docs/decisions/ADR-0008-map-codebase-mermaid-togaf.md +80 -0
- package/docs/decisions/ADR-0009-production-deployment-checklist.md +68 -0
- package/docs/decisions/ADR-0010-standards-integration.md +79 -0
- package/docs/decisions/ADR-0011-help-commands-rewrite.md +64 -0
- package/docs/decisions/ADR-0012-contributors-attribution-index.md +42 -0
- package/docs/decisions/ADR-0013-strategic-feature-additions.md +53 -0
- package/docs/decisions/ADR-0014-internal-cleanup-code-quality.md +53 -0
- package/docs/decisions/ADR-0015-focus-auto-runner.md +296 -0
- package/docs/decisions/ADR-0016-enhanced-doc-sync.md +54 -0
- package/docs/decisions/ADR-0017-enhanced-focus-design.md +69 -0
- package/docs/decisions/ADR-0018-user-reality-testing.md +84 -0
- package/docs/decisions/ADR-0019-end-to-end-user-acceptance-testing.md +113 -0
- package/docs/decisions/ADR-0020-ai-drift-prevention.md +55 -0
- package/docs/decisions/ADR-0021-codebase-mapper-v2.md +76 -0
- package/docs/decisions/ADR-0022-lifecycle-completeness.md +63 -0
- package/docs/decisions/ADR-0023-opus-4-7-adoption.md +89 -0
- package/docs/decisions/ADR-0024-spec-b-v2-completion.md +137 -0
- package/docs/decisions/ADR-0025-behavioral-guardrails-layer.md +129 -0
- package/docs/decisions/ADR-0026-self-improvement-loop.md +171 -0
- package/docs/decisions/ADR-0027-doc-code-link-graph.md +164 -0
- package/docs/decisions/ADR-0028-unified-agents-skills-target.md +46 -0
- package/docs/decisions/ADR-0029-experiment-evaluator.md +76 -0
- package/docs/decisions/ADR-0030-planning-concurrency.md +37 -0
- package/docs/decisions/ADR-0031-focus-auto-backlog-source.md +47 -0
- package/docs/decisions/ADR-0032-squad-model.md +64 -0
- package/docs/decisions/ADR-0033-army-campaign.md +40 -0
- package/docs/decisions/ADR-0034-scheduled-campaigns.md +57 -0
- package/docs/decisions/ADR-0035-army-hud-dashboard.md +40 -0
- package/docs/decisions/ADR-0036-agentic-memory-architecture.md +106 -0
- package/docs/decisions/ADR-0037-pin-security-agents-to-opus.md +51 -0
- package/docs/decisions/ADR-0038-skill-aligned-decomposition.md +41 -0
- package/docs/decisions/ADR-0039-worktree-as-project-variant.md +37 -0
- package/docs/decisions/ADR-0040-memory-injection-defense.md +55 -0
- package/docs/decisions/ADR-0041-mcp-dual-era-bridge.md +47 -0
- package/docs/decisions/ADR-0042-design-parity-both-flows.md +45 -0
- package/docs/decisions/ADR-0043-addressable-planning-roots.md +56 -0
- package/docs/decisions/ADR-0044-bound-the-cached-context.md +104 -0
- package/docs/decisions/ADR-0045-agent-plugins-bundle.md +76 -0
- package/docs/decisions/ADR-0046-subagent-frontmatter-decisions.md +39 -0
- package/docs/decisions/ADR-0047-pan-harness.md +76 -0
- package/docs/decisions/ADR-0048-one-core-per-project.md +43 -0
- package/docs/decisions/ADR-0049-machine-verdicts-and-findings-ledger.md +116 -0
- package/docs/specs/ai_drift_prevention_featureai.md +286 -0
- package/docs/specs/codebase_mapper_v2_featureai.md +930 -0
- package/docs/specs/command_naming_optimization_featureai.md +930 -0
- package/docs/specs/comp_analysis_markdown_v1.md +149 -0
- package/docs/specs/continuous_autonomous_execution_featureai.md +304 -0
- package/docs/specs/contributors_attribution_index_featureai.md +476 -0
- package/docs/specs/copilot_cli_interaction_featureai.md +868 -0
- package/docs/specs/copilot_cli_runtime_featureai.md +647 -0
- package/docs/specs/custom-skills-assessment.md +461 -0
- package/docs/specs/deployment_audit_findings_featureai.md +243 -0
- package/docs/specs/doc_code_link_graph_featureai.md +253 -0
- package/docs/specs/documentation_alignment_cleanup_featureai.md +69 -0
- package/docs/specs/e2e_visual_testing_featureai.md +615 -0
- package/docs/specs/end_to_end_user_acceptance_testing_featureai.md +810 -0
- package/docs/specs/evidence-loop-plan.md +76 -0
- package/docs/specs/evidence_loop_featureai.md +497 -0
- package/docs/specs/focus-auto-optimize-category.md +449 -0
- package/docs/specs/focus-auto-optimize-superplan.md +188 -0
- package/docs/specs/focus_auto_runner_featureai.md +1102 -0
- package/docs/specs/focus_design_v2_featureai.md +197 -0
- package/docs/specs/focus_sync_v2_featureai.md +202 -0
- package/docs/specs/git_integration_overhaul_featureai.md +326 -0
- package/docs/specs/glm_zai_provider_featureai.md +200 -0
- package/docs/specs/googlecli_adoption_featureai.md +661 -0
- package/docs/specs/help_commands_rewrite_featureai.md +860 -0
- package/docs/specs/industry_analysis_strategic_additions_featureai.md +231 -0
- package/docs/specs/industry_comparison_and_feature_gaps_featureai.md +674 -0
- package/docs/specs/internal_cleanup_code_quality_featureai.md +486 -0
- package/docs/specs/investigations/README.md +31 -0
- package/docs/specs/investigations/digests/2026-09-28.md +47 -0
- package/docs/specs/investigations/digests/2026-10-08.md +62 -0
- package/docs/specs/investigations/digests/2026-10-10.md +61 -0
- package/docs/specs/investigations/features/debugger-red-loop-first.md +55 -0
- package/docs/specs/investigations/features/harness-cross-host-behaviour.md +93 -0
- package/docs/specs/investigations/features/optimize-apply-regression-gate.md +66 -0
- package/docs/specs/investigations/features/reviewer-smell-baseline.md +57 -0
- package/docs/specs/investigations/features/trace-failure-capture.md +69 -0
- package/docs/specs/lifecycle_completeness_featureai.md +754 -0
- package/docs/specs/local-first-ci.md +42 -0
- package/docs/specs/map_codebase_mermaid_togaf_featureai.md +805 -0
- package/docs/specs/market-delta-2026-09-superplan.md +310 -0
- package/docs/specs/market-ideas-2026-09.md +131 -0
- package/docs/specs/market-ideas-2026-10.md +428 -0
- package/docs/specs/market-ideas-ledger.md +177 -0
- package/docs/specs/memory-optimization-2026-10.md +285 -0
- package/docs/specs/multi-track-planning-roots.md +143 -0
- package/docs/specs/multi_model_routing_featureai.md +687 -0
- package/docs/specs/multi_model_routing_superplan.md +197 -0
- package/docs/specs/opus_47_existing_enhancements_featureai.md +247 -0
- package/docs/specs/opus_47_extended_features_featureai.md +363 -0
- package/docs/specs/opus_47_extended_features_v2_featureai.md +397 -0
- package/docs/specs/pan_focus_commands_featureai.md +2336 -0
- package/docs/specs/pan_zcode_mcp_bridge_featureai.md +120 -0
- package/docs/specs/product-excellence-superplan.md +315 -0
- package/docs/specs/product_excellence_program_2026_q4_featureai.md +365 -0
- package/docs/specs/production_deployment_readiness_featureai.md +433 -0
- package/docs/specs/production_readiness_ship_v1_featureai.md +698 -0
- package/docs/specs/real_test_enforcement_featureai.md +213 -0
- package/docs/specs/reality-check-2026-09-superplan.md +362 -0
- package/docs/specs/right-sized-ceremony.md +74 -0
- package/docs/specs/roadmap_and_competition_2026_q4_featureai.md +390 -0
- package/docs/specs/self_improvement_loop_featureai.md +875 -0
- package/docs/specs/skill-aligned-decomposition.md +155 -0
- package/docs/specs/standards_integration_featureai.md +833 -0
- package/docs/specs/testing-system-redesign-2026-09.md +267 -0
- package/docs/specs/user_reality_testing_system_featureai.md +788 -0
- package/docs/specs/watch-triggers.json +22 -0
- package/docs/specs/workflow-integration-redesign.md +742 -0
- package/docs/specs/workflow_state_consistency_featureai.md +183 -0
- package/hooks/dist/pan-context-monitor.js +9 -2
- package/hooks/dist/pan-cost-logger.js +5 -0
- package/hooks/dist/pan-guard.js +28 -5
- package/hooks/dist/pan-stop-guard.js +9 -9
- package/hooks/dist/pan-trace-logger.js +5 -0
- package/package.json +10 -2
- package/pan-wizard-core/bin/lib/agents-md.cjs +57 -8
- package/pan-wizard-core/bin/lib/autodev-report.cjs +180 -0
- package/pan-wizard-core/bin/lib/autodev.cjs +642 -0
- package/pan-wizard-core/bin/lib/ci.cjs +143 -0
- package/pan-wizard-core/bin/lib/commands.cjs +1 -1
- package/pan-wizard-core/bin/lib/config.cjs +41 -5
- package/pan-wizard-core/bin/lib/constants.cjs +27 -1
- package/pan-wizard-core/bin/lib/core.cjs +31 -14
- package/pan-wizard-core/bin/lib/cost.cjs +160 -4
- package/pan-wizard-core/bin/lib/cross-family.cjs +164 -0
- package/pan-wizard-core/bin/lib/debug-cases.cjs +104 -0
- package/pan-wizard-core/bin/lib/doc-lint.cjs +5 -3
- package/pan-wizard-core/bin/lib/failure-class.cjs +81 -0
- package/pan-wizard-core/bin/lib/findings.cjs +148 -9
- package/pan-wizard-core/bin/lib/focus.cjs +1 -1
- package/pan-wizard-core/bin/lib/git.cjs +1 -1
- package/pan-wizard-core/bin/lib/hosts.cjs +88 -0
- package/pan-wizard-core/bin/lib/init.cjs +7 -2
- package/pan-wizard-core/bin/lib/knowledge.cjs +2 -0
- package/pan-wizard-core/bin/lib/merge.cjs +50 -0
- package/pan-wizard-core/bin/lib/mutate.cjs +268 -0
- package/pan-wizard-core/bin/lib/next-action.cjs +112 -0
- package/pan-wizard-core/bin/lib/optimize.cjs +2 -2
- package/pan-wizard-core/bin/lib/phase-gate.cjs +15 -9
- package/pan-wizard-core/bin/lib/phase-remove.cjs +91 -11
- package/pan-wizard-core/bin/lib/phase.cjs +85 -13
- package/pan-wizard-core/bin/lib/plan-lint.cjs +310 -0
- package/pan-wizard-core/bin/lib/preview.cjs +26 -4
- package/pan-wizard-core/bin/lib/requirements-lint.cjs +130 -0
- package/pan-wizard-core/bin/lib/research-lint.cjs +268 -0
- package/pan-wizard-core/bin/lib/review-deep.cjs +14 -22
- package/pan-wizard-core/bin/lib/review-vote.cjs +105 -0
- package/pan-wizard-core/bin/lib/roadmap.cjs +1 -0
- package/pan-wizard-core/bin/lib/runner.cjs +2 -16
- package/pan-wizard-core/bin/lib/sizing.cjs +356 -0
- package/pan-wizard-core/bin/lib/squads.cjs +1 -1
- package/pan-wizard-core/bin/lib/state-write.cjs +247 -0
- package/pan-wizard-core/bin/lib/test-gate.cjs +433 -0
- package/pan-wizard-core/bin/lib/uat.cjs +149 -0
- package/pan-wizard-core/bin/lib/usage.cjs +104 -0
- package/pan-wizard-core/bin/lib/verdict.cjs +26 -3
- package/pan-wizard-core/bin/lib/verify-plan-set.cjs +30 -1
- package/pan-wizard-core/bin/lib/verify-preflight.cjs +2 -2
- package/pan-wizard-core/bin/lib/verify-red.cjs +174 -0
- package/pan-wizard-core/bin/lib/verify-retro.cjs +3 -2
- package/pan-wizard-core/bin/lib/verify-scope.cjs +143 -2
- package/pan-wizard-core/bin/lib/verify.cjs +86 -31
- package/pan-wizard-core/bin/lib/worktree.cjs +70 -0
- package/pan-wizard-core/bin/pan-tools.cjs +257 -28
- package/pan-wizard-core/learnings/universal/autonomous-loop.md +2 -2
- package/pan-wizard-core/mcp/orchestrator.cjs +4 -1
- package/pan-wizard-core/references/continuation-format.md +3 -3
- package/pan-wizard-core/references/design-methodology.md +1 -1
- package/pan-wizard-core/references/guardrails.md +1 -1
- package/pan-wizard-core/references/handoff-decisions.md +1 -1
- package/pan-wizard-core/references/model-profiles.md +2 -3
- package/pan-wizard-core/references/questioning.md +24 -0
- package/pan-wizard-core/references/ui-brand.md +4 -6
- package/pan-wizard-core/references/verdict-contract.md +2 -1
- package/pan-wizard-core/templates/config.json +10 -2
- package/pan-wizard-core/templates/debug.md +1 -0
- package/pan-wizard-core/templates/design.md +5 -4
- package/pan-wizard-core/templates/discovery.md +2 -2
- package/pan-wizard-core/templates/phase-prompt.md +7 -0
- package/pan-wizard-core/templates/requirements.md +9 -0
- package/pan-wizard-core/templates/research.md +10 -0
- package/pan-wizard-core/templates/summary-complex.md +5 -1
- package/pan-wizard-core/templates/summary-minimal.md +4 -0
- package/pan-wizard-core/templates/summary-standard.md +5 -1
- package/pan-wizard-core/templates/summary.md +8 -1
- package/pan-wizard-core/workflows/add-phase.md +4 -6
- package/pan-wizard-core/workflows/assumptions.md +19 -1
- package/pan-wizard-core/workflows/diagnose-issues.md +2 -0
- package/pan-wizard-core/workflows/discuss-phase.md +4 -2
- package/pan-wizard-core/workflows/exec-lean.md +69 -0
- package/pan-wizard-core/workflows/exec-phase.md +79 -15
- package/pan-wizard-core/workflows/execute-plan.md +3 -1
- package/{commands/pan → pan-wizard-core/workflows}/focus-doc-audit.md +13 -29
- package/{commands/pan → pan-wizard-core/workflows}/focus-drift-walking.md +3 -18
- package/{commands/pan → pan-wizard-core/workflows}/focus-exec.md +8 -23
- package/{commands/pan → pan-wizard-core/workflows}/focus-plan.md +6 -20
- package/{commands/pan → pan-wizard-core/workflows}/focus-scan.md +3 -16
- package/{commands/pan → pan-wizard-core/workflows}/focus-sync.md +7 -21
- package/pan-wizard-core/workflows/health.md +6 -0
- package/pan-wizard-core/workflows/help.md +114 -77
- package/pan-wizard-core/workflows/insert-phase.md +4 -6
- package/pan-wizard-core/workflows/learn.md +3 -3
- package/pan-wizard-core/workflows/milestone-audit.md +2 -2
- package/pan-wizard-core/workflows/milestone-new.md +38 -95
- package/pan-wizard-core/workflows/new-project.md +96 -258
- package/pan-wizard-core/workflows/optimize.md +107 -5
- package/pan-wizard-core/workflows/phase-tests.md +10 -0
- package/pan-wizard-core/workflows/plan-phase.md +106 -45
- package/pan-wizard-core/workflows/profile.md +50 -0
- package/pan-wizard-core/workflows/progress.md +10 -0
- package/pan-wizard-core/workflows/quick.md +34 -43
- package/pan-wizard-core/workflows/remove-phase.md +11 -1
- package/pan-wizard-core/workflows/research-phase.md +118 -40
- package/pan-wizard-core/workflows/resume-project.md +18 -2
- package/pan-wizard-core/workflows/settings.md +33 -19
- package/pan-wizard-core/workflows/todo-check.md +6 -2
- package/pan-wizard-core/workflows/transition.md +22 -4
- package/pan-wizard-core/workflows/uat-walkthrough.md +90 -0
- package/pan-wizard-core/workflows/update.md +1 -1
- package/pan-wizard-core/workflows/verify-phase.md +39 -46
- package/scripts/build-agent-plugin.js +35 -15
- package/scripts/build-help.cjs +106 -0
- package/scripts/build-plugin.js +10 -1
- package/scripts/deprecate-old-versions.js +70 -3
- package/scripts/generate-skills-docs.py +1 -1
- package/scripts/git-hooks/pre-push +27 -0
- package/scripts/install-git-hooks.js +7 -5
- package/scripts/install-pre-push.cjs +38 -0
- package/scripts/land.cjs +151 -0
- package/scripts/local-ci.cjs +501 -0
- package/scripts/local-schedule.cjs +277 -0
- package/scripts/mutation-probe.cjs +7 -80
- package/scripts/prompt-lint.cjs +32 -0
- package/scripts/prompt-rules.cjs +156 -0
- package/scripts/run-tests.cjs +7 -1
- package/scripts/test-quality-lint.cjs +3 -3
- package/scripts/test-surface.cjs +175 -6
- package/agents/pan-research-synthesizer.md +0 -246
- package/commands/pan/audit-deployment.md +0 -386
- package/commands/pan/dashboard.md +0 -22
- package/commands/pan/discord.md +0 -19
- package/commands/pan/mcp-bridge.md +0 -143
- package/commands/pan/milestone-cleanup.md +0 -19
- package/commands/pan/milestone-gaps.md +0 -35
- package/commands/pan/optimize.md +0 -115
- package/commands/pan/profile.md +0 -76
- package/commands/pan/research-phase.md +0 -189
- package/pan-wizard-core/bin/lib/bridge.cjs +0 -269
- package/pan-wizard-core/templates/research-project/architecture.md +0 -204
- package/pan-wizard-core/templates/research-project/features.md +0 -147
- package/pan-wizard-core/templates/research-project/pitfalls.md +0 -200
- package/pan-wizard-core/templates/research-project/stack.md +0 -120
- package/pan-wizard-core/templates/research-project/summary.md +0 -170
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* CI as an oracle for the autodev loop (program A2).
|
|
4
|
+
*
|
|
5
|
+
* The loop never takes a session's word for its tests. Locally the oracle is the phase
|
|
6
|
+
* gate and the test-run engine (V1, V2); with `autodev run --ci` it is also the pull
|
|
7
|
+
* request's checks, read through GitHub's `gh` CLI (optional: without it the CI oracle
|
|
8
|
+
* is `unavailable`). A check that already fails on the base branch is not this change's
|
|
9
|
+
* doing and is ignored. The failing log tail is what a fix round gets as feedback, and a
|
|
10
|
+
* breaker stops after N rounds.
|
|
11
|
+
*
|
|
12
|
+
* `autodev run --ci local` needs no pull request: the oracle is the project's own gate,
|
|
13
|
+
* run here — `npm run ci:local` when package.json has that script, else `npm test`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { spawnSync } = require('child_process');
|
|
17
|
+
|
|
18
|
+
const DEFAULT_CI_ROUNDS = 3;
|
|
19
|
+
|
|
20
|
+
function defaultExec(cwd) {
|
|
21
|
+
const refused = require('./autodev.cjs').standinMissing('gh');
|
|
22
|
+
if (refused) return () => ({ status: null, error: new Error(refused), stdout: '', stderr: refused });
|
|
23
|
+
return (args) => spawnSync('gh', args, { cwd, encoding: 'utf8', shell: process.platform === 'win32', timeout: 120000 });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseJson(r) {
|
|
27
|
+
if (!r || r.error || r.status !== 0) return null;
|
|
28
|
+
try { return JSON.parse(r.stdout); } catch { return null; }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const BUCKET = { pass: 'pass', fail: 'fail', pending: 'pending', skipping: 'skip', cancel: 'fail' };
|
|
32
|
+
function bucketOf(c) {
|
|
33
|
+
if (c.bucket && BUCKET[c.bucket]) return BUCKET[c.bucket];
|
|
34
|
+
const s = String(c.state || c.conclusion || '').toUpperCase();
|
|
35
|
+
if (['SUCCESS', 'NEUTRAL'].includes(s)) return 'pass';
|
|
36
|
+
if (['SKIPPED'].includes(s)) return 'skip';
|
|
37
|
+
if (['FAILURE', 'ERROR', 'CANCELLED', 'TIMED_OUT', 'ACTION_REQUIRED', 'STARTUP_FAILURE'].includes(s)) return 'fail';
|
|
38
|
+
return 'pending';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Names (check and workflow) whose latest run on the base branch failed. */
|
|
42
|
+
function baseFailing(exec, base) {
|
|
43
|
+
if (!base) return new Set();
|
|
44
|
+
const runs = parseJson(exec(['run', 'list', '--branch', base, '--limit', '30', '--json', 'name,workflowName,conclusion']));
|
|
45
|
+
const latest = new Map();
|
|
46
|
+
for (const r of Array.isArray(runs) ? runs : []) {
|
|
47
|
+
const key = r.workflowName || r.name;
|
|
48
|
+
if (!latest.has(key)) latest.set(key, r);
|
|
49
|
+
}
|
|
50
|
+
const out = new Set();
|
|
51
|
+
for (const [key, r] of latest) if (String(r.conclusion).toLowerCase() === 'failure') { out.add(key); if (r.name) out.add(r.name); }
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function logTail(exec, checks) {
|
|
56
|
+
for (const c of checks) {
|
|
57
|
+
const m = String(c.link || '').match(/\/actions\/runs\/(\d+)/);
|
|
58
|
+
if (!m) continue;
|
|
59
|
+
const r = exec(['run', 'view', m[1], '--log-failed']);
|
|
60
|
+
if (r && r.status === 0 && r.stdout) return r.stdout.trim().split(/\r?\n/).slice(-40).join('\n');
|
|
61
|
+
}
|
|
62
|
+
return '';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The CI oracle for the current branch's pull request.
|
|
67
|
+
* @param {string} cwd
|
|
68
|
+
* @param {{exec?: Function, ref?: string, base?: string, head?: string}} opts
|
|
69
|
+
* @returns {{status: 'passed'|'failed'|'pending'|'unavailable'|'stale', failed: string[], ignored: string[], tail: string, reason?: string}}
|
|
70
|
+
*/
|
|
71
|
+
function ciOracle(cwd, opts = {}) {
|
|
72
|
+
const exec = opts.exec || defaultExec(cwd);
|
|
73
|
+
const res = { status: 'unavailable', failed: [], ignored: [], tail: '' };
|
|
74
|
+
const checksRaw = exec(['pr', 'checks', ...(opts.ref ? [opts.ref] : []), '--json', 'name,state,bucket,link,workflow']);
|
|
75
|
+
if (!checksRaw || checksRaw.error || checksRaw.status == null) return { ...res, reason: 'gh is not available' };
|
|
76
|
+
const checks = parseJson(checksRaw);
|
|
77
|
+
if (!Array.isArray(checks)) return { ...res, reason: (checksRaw.stderr || 'no pull request checks').trim().slice(0, 200) };
|
|
78
|
+
if (opts.head) {
|
|
79
|
+
const pr = parseJson(exec(['pr', 'view', ...(opts.ref ? [opts.ref] : []), '--json', 'headRefOid']));
|
|
80
|
+
if (pr && pr.headRefOid && pr.headRefOid !== opts.head) return { ...res, status: 'stale', reason: `the checks are for ${pr.headRefOid.slice(0, 9)}, HEAD is ${opts.head.slice(0, 9)}: push the branch` };
|
|
81
|
+
}
|
|
82
|
+
const onBase = baseFailing(exec, opts.base);
|
|
83
|
+
const failing = checks.filter((c) => bucketOf(c) === 'fail');
|
|
84
|
+
const ignored = failing.filter((c) => onBase.has(c.name) || onBase.has(c.workflow));
|
|
85
|
+
const counted = failing.filter((c) => !ignored.includes(c));
|
|
86
|
+
res.ignored = ignored.map((c) => c.name);
|
|
87
|
+
res.failed = counted.map((c) => c.name);
|
|
88
|
+
if (counted.length) return { ...res, status: 'failed', tail: logTail(exec, counted) };
|
|
89
|
+
if (checks.some((c) => bucketOf(c) === 'pending')) return { ...res, status: 'pending' };
|
|
90
|
+
return { ...res, status: 'passed' };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Wait for pending checks, up to `timeoutMs`. */
|
|
94
|
+
function waitForCi(cwd, opts = {}) {
|
|
95
|
+
const deadline = Date.now() + (opts.timeoutMs || 30 * 60 * 1000);
|
|
96
|
+
for (;;) {
|
|
97
|
+
const r = ciOracle(cwd, opts);
|
|
98
|
+
if (r.status !== 'pending' || Date.now() >= deadline) return r;
|
|
99
|
+
spawnSync(process.execPath, ['-e', `setTimeout(() => {}, ${Math.min(opts.pollMs || 30000, Math.max(0, deadline - Date.now()))})`]);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The project's own gate: its `ci:local` script, else its `test` script, else none. */
|
|
104
|
+
function localGate(cwd) {
|
|
105
|
+
let scripts = {};
|
|
106
|
+
try { scripts = JSON.parse(require('fs').readFileSync(require('path').join(cwd, 'package.json'), 'utf8')).scripts || {}; } catch { /* no package.json */ }
|
|
107
|
+
if (scripts['ci:local']) return { name: 'ci:local', args: ['run', 'ci:local'] };
|
|
108
|
+
if (scripts.test) return { name: 'test', args: ['test'] };
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The local CI oracle: run the project's gate in `cwd` and judge it by its exit code.
|
|
114
|
+
* With ci:local, the failing gate ids come from .ci-local/report.json.
|
|
115
|
+
* @param {string} cwd
|
|
116
|
+
* @param {{exec?: Function, timeoutMs?: number}} opts
|
|
117
|
+
* @returns {{status: 'passed'|'failed'|'unavailable', failed: string[], ignored: string[], tail: string, reason?: string}}
|
|
118
|
+
*/
|
|
119
|
+
function localOracle(cwd, opts = {}) {
|
|
120
|
+
const res = { status: 'unavailable', failed: [], ignored: [], tail: '' };
|
|
121
|
+
const gate = localGate(cwd);
|
|
122
|
+
if (!gate) return { ...res, reason: 'package.json has neither a ci:local nor a test script' };
|
|
123
|
+
const exec = opts.exec || ((args) => spawnSync('npm', args, { cwd, encoding: 'utf8', shell: process.platform === 'win32', timeout: opts.timeoutMs || 60 * 60 * 1000, maxBuffer: 256 * 1024 * 1024 }));
|
|
124
|
+
const r = exec(gate.args);
|
|
125
|
+
if (!r || r.error || r.status == null) return { ...res, reason: `npm ${gate.args.join(' ')} did not run${r && r.error ? `: ${r.error.message}` : ''}` };
|
|
126
|
+
const tail = `${r.stdout || ''}${r.stderr || ''}`.trim().split(/\r?\n/).slice(-40).join('\n');
|
|
127
|
+
if (r.status === 0) return { ...res, status: 'passed', tail };
|
|
128
|
+
let failed = [gate.name];
|
|
129
|
+
if (gate.name === 'ci:local') {
|
|
130
|
+
try {
|
|
131
|
+
const report = JSON.parse(require('fs').readFileSync(require('path').join(cwd, '.ci-local', 'report.json'), 'utf8'));
|
|
132
|
+
const ids = (report.gates || []).filter((g) => g.status === 'fail').map((g) => g.id);
|
|
133
|
+
if (ids.length) failed = ids;
|
|
134
|
+
} catch { /* the exit code is the verdict; the gate names are a courtesy */ }
|
|
135
|
+
}
|
|
136
|
+
return { ...res, status: 'failed', failed, tail };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function breakerTripped(rounds, max = DEFAULT_CI_ROUNDS) {
|
|
140
|
+
return rounds >= max;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = { ciOracle, waitForCi, localOracle, localGate, breakerTripped, baseFailing, bucketOf, DEFAULT_CI_ROUNDS };
|
|
@@ -941,7 +941,7 @@ function cmdScaffold(cwd, type, options, raw) {
|
|
|
941
941
|
}
|
|
942
942
|
case 'uat': {
|
|
943
943
|
filePath = path.join(phaseDir, `${padded}${UAT_SUFFIX}`);
|
|
944
|
-
content = `---\nphase: "${padded}"\nname: "${name || phaseInfo?.phase_name || 'Unnamed'}"\ncreated: ${today}\nstatus: pending\n---\n\n# Phase ${phase}: ${name || phaseInfo?.phase_name || 'Unnamed'} -- User Acceptance Testing\n\n##
|
|
944
|
+
content = `---\nphase: "${padded}"\nname: "${name || phaseInfo?.phase_name || 'Unnamed'}"\ncreated: ${today}\nstatus: pending\n---\n\n# Phase ${phase}: ${name || phaseInfo?.phase_name || 'Unnamed'} -- User Acceptance Testing\n\n## Tests\n\n_One \`### N. <name>\` per user-observable truth, with \`expected:\` and \`result: [pending]\` (the walkthrough writes them; \`uat record\` fills the results)_\n\n## Gaps\n\n## Summary\n\n_Pending UAT_\n`;
|
|
945
945
|
break;
|
|
946
946
|
}
|
|
947
947
|
case 'verification': {
|
|
@@ -48,13 +48,36 @@ function buildConfigDefaults(hasBraveSearch, userDefaults) {
|
|
|
48
48
|
phase_branch_template: 'pan/phase-{phase}-{slug}',
|
|
49
49
|
milestone_branch_template: 'pan/{milestone}-{slug}',
|
|
50
50
|
workflow: {
|
|
51
|
-
|
|
51
|
+
// Research is opt-in (2026-10-25): `--research` on new-project or plan-phase, or
|
|
52
|
+
// this key set true. A config that already says true keeps it.
|
|
53
|
+
research: false,
|
|
52
54
|
plan_check: true,
|
|
53
55
|
verifier: true,
|
|
54
56
|
nyquist_validation: false,
|
|
55
57
|
phase_record_compact: false,
|
|
56
|
-
// V1: the phase gate. `
|
|
57
|
-
|
|
58
|
+
// V1: the phase gate. `enforce` (the default since V1E) refuses; `warn` reports
|
|
59
|
+
// refusals and completes.
|
|
60
|
+
phase_gate: 'enforce',
|
|
61
|
+
// V5: the mutation report on a phase's diff. `report` lists surviving mutants and
|
|
62
|
+
// never refuses; `off` skips it; `gate` is reserved for V5G.
|
|
63
|
+
mutation_gate: 'report',
|
|
64
|
+
// V5G: the score `gate` mode needs (killed / sampled, 0..1).
|
|
65
|
+
mutation_floor: 0.6,
|
|
66
|
+
// R2: `single` runs one reviewer pass; `lenses` runs one per lens (review-vote.cjs
|
|
67
|
+
// LENSES) and keeps only the findings a quorum of passes reported. Opt-in until
|
|
68
|
+
// its paired corpus run decides.
|
|
69
|
+
review_mode: 'single',
|
|
70
|
+
// R4: a second review on another host's CLI (another model family), read-only.
|
|
71
|
+
// `off`, or a host: claude | codex | copilot | gemini | opencode.
|
|
72
|
+
cross_family_review: 'off',
|
|
73
|
+
// A3: pan-exec-waves runs each executor in its own git worktree (Claude Code).
|
|
74
|
+
worktree_isolation: false,
|
|
75
|
+
// Right-sized ceremony (sizing.cjs): `auto` sizes each phase and quick task and gives a
|
|
76
|
+
// small, well-specified one the lean path; `full` always runs the whole ceremony. The
|
|
77
|
+
// `quality` profile also means full.
|
|
78
|
+
ceremony: 'auto',
|
|
79
|
+
// The most requirements a phase can map and still be sized lean.
|
|
80
|
+
lean_max_requirements: 5,
|
|
58
81
|
// M2: opt-in per-phase HTML reports at the verify→complete gate.
|
|
59
82
|
phase_reports: { enabled: false, open: false, theme: 'auto', index: true },
|
|
60
83
|
},
|
|
@@ -62,6 +85,8 @@ function buildConfigDefaults(hasBraveSearch, userDefaults) {
|
|
|
62
85
|
// A7: the pre-tool guard. `report` logs to <planning>/guard.jsonl; `enforce` also
|
|
63
86
|
// denies a write that puts a credential in a file; `off` disables it.
|
|
64
87
|
hooks: { guard: 'report' },
|
|
88
|
+
// Spend per phase, in API-equivalent dollars. It only warns (80% and over); 0 = off.
|
|
89
|
+
cost: { phase_budget_usd: 20 },
|
|
65
90
|
brave_search: hasBraveSearch,
|
|
66
91
|
budget: {
|
|
67
92
|
default_points: 50,
|
|
@@ -74,6 +99,10 @@ function buildConfigDefaults(hasBraveSearch, userDefaults) {
|
|
|
74
99
|
// Fraction of the spawn budget held back for re-verification (0..0.5).
|
|
75
100
|
// Surfaced as an indicator always; a hard early stop only under enforce.
|
|
76
101
|
verify_reserve: 0.15,
|
|
102
|
+
// A6: autodev's dollar caps, per run and per day (UTC), across every run's ledger.
|
|
103
|
+
// null: none set. An unattended run needs a run cap, here or as --max-usd.
|
|
104
|
+
max_usd_run: null,
|
|
105
|
+
max_usd_day: null,
|
|
77
106
|
},
|
|
78
107
|
commit: {
|
|
79
108
|
safety_checks: true,
|
|
@@ -102,6 +131,7 @@ function buildConfigDefaults(hasBraveSearch, userDefaults) {
|
|
|
102
131
|
...userDefaults,
|
|
103
132
|
workflow: { ...hardcoded.workflow, ...(userDefaults.workflow || {}) },
|
|
104
133
|
budget: { ...hardcoded.budget, ...(userDefaults.budget || {}) },
|
|
134
|
+
cost: { ...hardcoded.cost, ...(userDefaults.cost || {}) },
|
|
105
135
|
commit: { ...hardcoded.commit, ...(userDefaults.commit || {}) },
|
|
106
136
|
execution: { ...hardcoded.execution, ...(userDefaults.execution || {}) },
|
|
107
137
|
routing: { ...hardcoded.routing, ...(userDefaults.routing || {}) },
|
|
@@ -119,7 +149,7 @@ const CONFIG_KEYS_BEYOND_DEFAULTS = Object.freeze({
|
|
|
119
149
|
top: Object.freeze(['research', 'plan_checker', 'verifier', 'nyquist_validation', 'mode', 'depth', 'planning', 'git',
|
|
120
150
|
'focus', 'model_overrides', 'effort_overrides', 'cost', 'cache', 'build', 'verification', 'concurrency', 'memory']),
|
|
121
151
|
sections: Object.freeze({
|
|
122
|
-
workflow: Object.freeze(['auto_advance', 'stop_guard']),
|
|
152
|
+
workflow: Object.freeze(['auto_advance', 'stop_guard', 'test_command', 'test_file_command']),
|
|
123
153
|
planning: Object.freeze(['commit_docs', 'search_gitignored']),
|
|
124
154
|
git: Object.freeze(['branching_strategy', 'phase_branch_template', 'milestone_branch_template']),
|
|
125
155
|
}),
|
|
@@ -149,8 +179,13 @@ function knownConfigKeys() {
|
|
|
149
179
|
* @param {string} keyPath
|
|
150
180
|
* @returns {boolean}
|
|
151
181
|
*/
|
|
182
|
+
// A lowercase `x-` key is the user's own metadata, never a typo (market-ideas M51,
|
|
183
|
+
// after OpenSpec v1.14.2): PAN ignores it at the top level and inside a section.
|
|
184
|
+
const USER_KEY = /^x-/;
|
|
185
|
+
|
|
152
186
|
function isKnownConfigPath(keyPath) {
|
|
153
187
|
const [head, sub] = String(keyPath).split('.');
|
|
188
|
+
if (USER_KEY.test(head) || (sub !== undefined && USER_KEY.test(sub))) return true;
|
|
154
189
|
const { top, sections } = knownConfigKeys();
|
|
155
190
|
if (!top.has(head)) return false;
|
|
156
191
|
return sub === undefined || !sections.has(head) || sections.get(head).has(sub);
|
|
@@ -167,10 +202,11 @@ function unknownConfigKeys(config) {
|
|
|
167
202
|
const { top, sections } = knownConfigKeys();
|
|
168
203
|
const unknown = [];
|
|
169
204
|
for (const [k, v] of Object.entries(config)) {
|
|
205
|
+
if (USER_KEY.test(k)) continue;
|
|
170
206
|
if (!top.has(k)) { unknown.push(k); continue; }
|
|
171
207
|
const known = sections.get(k);
|
|
172
208
|
if (!known || !v || typeof v !== 'object' || Array.isArray(v)) continue;
|
|
173
|
-
for (const sub of Object.keys(v)) if (!known.has(sub)) unknown.push(`${k}.${sub}`);
|
|
209
|
+
for (const sub of Object.keys(v)) if (!known.has(sub) && !USER_KEY.test(sub)) unknown.push(`${k}.${sub}`);
|
|
174
210
|
}
|
|
175
211
|
return unknown;
|
|
176
212
|
}
|
|
@@ -165,6 +165,29 @@ const CATEGORY_DEFAULTS = {
|
|
|
165
165
|
/** Doc files to scan for staleness (focus sync) */
|
|
166
166
|
const DOC_SYNC_FILES = ['README.md', 'docs/DEVELOPMENT.md', 'docs/CLI-REFERENCE.md', 'docs/USER-GUIDE.md', 'docs/ARCHITECTURE.md'];
|
|
167
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Commands folded into another (program L7). The installer writes an alias command for
|
|
170
|
+
* each old name on every runtime, so `/pan:<old>` keeps working: it runs `to` with
|
|
171
|
+
* `args` in front of whatever the user passed.
|
|
172
|
+
*/
|
|
173
|
+
const COMMAND_ALIASES = {
|
|
174
|
+
dashboard: { to: 'hud', args: '' },
|
|
175
|
+
'milestone-cleanup': { to: 'milestone-done', args: '--cleanup' },
|
|
176
|
+
'milestone-gaps': { to: 'milestone-audit', args: '--gaps' },
|
|
177
|
+
optimize: { to: 'learn', args: '--optimize' },
|
|
178
|
+
'research-phase': { to: 'plan-phase', args: '--research-only' },
|
|
179
|
+
profile: { to: 'settings', args: '--profile' },
|
|
180
|
+
'focus-doc-audit': { to: 'focus-docs', args: '' },
|
|
181
|
+
'focus-sync': { to: 'focus-docs', args: '--sync' },
|
|
182
|
+
'focus-scan': { to: 'focus-auto', args: '--step scan' },
|
|
183
|
+
'focus-plan': { to: 'focus-auto', args: '--step plan' },
|
|
184
|
+
'focus-exec': { to: 'focus-auto', args: '--step exec' },
|
|
185
|
+
'focus-drift-walking': { to: 'focus-auto', args: '--step drift' },
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/** Commands removed with no replacement (program L7). */
|
|
189
|
+
const RETIRED_COMMANDS = ['discord', 'mcp-bridge'];
|
|
190
|
+
|
|
168
191
|
/** Old→new command name mapping for detecting stale docs */
|
|
169
192
|
const COMMAND_RENAME_MAP = {
|
|
170
193
|
'execute-phase': 'exec-phase',
|
|
@@ -178,7 +201,8 @@ const COMMAND_RENAME_MAP = {
|
|
|
178
201
|
'new-milestone': 'milestone-new',
|
|
179
202
|
'complete-milestone': 'milestone-done',
|
|
180
203
|
'audit-milestone': 'milestone-audit',
|
|
181
|
-
'plan-milestone-gaps': 'milestone-gaps',
|
|
204
|
+
'plan-milestone-gaps': 'milestone-audit --gaps',
|
|
205
|
+
...Object.fromEntries(Object.entries(COMMAND_ALIASES).map(([old, a]) => [old, a.args ? `${a.to} ${a.args}` : a.to])),
|
|
182
206
|
};
|
|
183
207
|
|
|
184
208
|
/** Default max cycles for auto-runner */
|
|
@@ -818,6 +842,8 @@ module.exports = {
|
|
|
818
842
|
FOCUS_SOURCES,
|
|
819
843
|
DOC_SYNC_FILES,
|
|
820
844
|
COMMAND_RENAME_MAP,
|
|
845
|
+
COMMAND_ALIASES,
|
|
846
|
+
RETIRED_COMMANDS,
|
|
821
847
|
CATEGORY_PRIORITY_RANGE,
|
|
822
848
|
CATEGORY_DEFAULTS,
|
|
823
849
|
DEFAULT_MAX_CYCLES,
|
|
@@ -132,13 +132,14 @@ const MODEL_PROFILES = {
|
|
|
132
132
|
'pan-designer': { quality: 'reasoning', balanced: 'reasoning', budget: 'mid' },
|
|
133
133
|
'pan-roadmapper': { quality: 'reasoning', balanced: 'reasoning', budget: 'mid' },
|
|
134
134
|
'pan-executor': { quality: 'reasoning', balanced: 'reasoning', budget: 'mid' },
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
'pan-
|
|
135
|
+
// Right-sized ceremony (2026-10-24): balanced runs the researchers and the plan-checker
|
|
136
|
+
// on the mid tier, about half the launch model's price; quality keeps them on reasoning.
|
|
137
|
+
'pan-phase-researcher': { quality: 'reasoning', balanced: 'mid', budget: 'fast' },
|
|
138
|
+
'pan-project-researcher': { quality: 'reasoning', balanced: 'mid', budget: 'fast' },
|
|
138
139
|
'pan-debugger': { quality: 'reasoning', balanced: 'reasoning', budget: 'mid' },
|
|
139
140
|
'pan-document_code': { quality: 'reasoning', balanced: 'reasoning', budget: 'fast' },
|
|
140
141
|
'pan-verifier': { quality: 'reasoning', balanced: 'reasoning', budget: 'fast' },
|
|
141
|
-
'pan-plan-checker': { quality: 'reasoning', balanced: '
|
|
142
|
+
'pan-plan-checker': { quality: 'reasoning', balanced: 'mid', budget: 'fast' },
|
|
142
143
|
'pan-design-checker': { quality: 'reasoning', balanced: 'reasoning', budget: 'fast' },
|
|
143
144
|
'pan-integration-checker': { quality: 'reasoning', balanced: 'reasoning', budget: 'fast' },
|
|
144
145
|
'pan-reviewer': { quality: 'reasoning', balanced: 'reasoning', budget: 'fast' },
|
|
@@ -193,7 +194,6 @@ const AGENT_BASE_EFFORT = {
|
|
|
193
194
|
// Research/synthesis/review — moderate depth
|
|
194
195
|
'pan-phase-researcher': 'medium',
|
|
195
196
|
'pan-project-researcher': 'medium',
|
|
196
|
-
'pan-research-synthesizer': 'medium',
|
|
197
197
|
'pan-knowledge': 'medium',
|
|
198
198
|
'pan-distiller': 'medium',
|
|
199
199
|
'pan-meta-reviewer': 'medium',
|
|
@@ -324,7 +324,7 @@ function reportsFailure(result) {
|
|
|
324
324
|
*/
|
|
325
325
|
function output(result, raw, rawValue, exitCode) {
|
|
326
326
|
if (raw && rawValue !== undefined) {
|
|
327
|
-
|
|
327
|
+
writeStdoutSync(String(rawValue));
|
|
328
328
|
} else {
|
|
329
329
|
const json = JSON.stringify(result, null, 2);
|
|
330
330
|
// Large payloads exceed Claude Code's Bash tool buffer (~50KB).
|
|
@@ -337,19 +337,31 @@ function output(result, raw, rawValue, exitCode) {
|
|
|
337
337
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pan-'));
|
|
338
338
|
const tmpPath = path.join(tmpDir, 'out.json');
|
|
339
339
|
fs.writeFileSync(tmpPath, json, { encoding: 'utf-8', flag: 'wx' });
|
|
340
|
-
|
|
340
|
+
writeStdoutSync('@file:' + tmpPath);
|
|
341
341
|
} catch {
|
|
342
342
|
// Tmpfile write failed (disk full, permissions) — truncate and write to stdout
|
|
343
343
|
const truncated = json.slice(0, MAX_JSON_SIZE);
|
|
344
|
-
|
|
344
|
+
writeStdoutSync(truncated);
|
|
345
345
|
}
|
|
346
346
|
} else {
|
|
347
|
-
|
|
347
|
+
writeStdoutSync(json);
|
|
348
348
|
}
|
|
349
349
|
}
|
|
350
350
|
process.exit(exitCode === undefined ? (reportsFailure(result) ? 1 : 0) : exitCode);
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
+
/**
|
|
354
|
+
* Write all of `text` to stdout before returning. output() exits right after it writes,
|
|
355
|
+
* and on macOS (and Linux) stdout to a pipe is asynchronous: process.exit() dropped
|
|
356
|
+
* whatever had not drained past the pipe buffer, so a reader got JSON cut at about 8 KB.
|
|
357
|
+
* Making the stdout handle blocking first turns the write synchronous on a pipe too.
|
|
358
|
+
*/
|
|
359
|
+
function writeStdoutSync(text) {
|
|
360
|
+
const handle = process.stdout._handle;
|
|
361
|
+
if (handle && typeof handle.setBlocking === 'function') handle.setBlocking(true);
|
|
362
|
+
process.stdout.write(String(text));
|
|
363
|
+
}
|
|
364
|
+
|
|
353
365
|
/**
|
|
354
366
|
* Write error message to stderr and exit with code 1.
|
|
355
367
|
* @param {string} message - Error message
|
|
@@ -434,7 +446,7 @@ function loadConfig(cwd) {
|
|
|
434
446
|
branching_strategy: 'none',
|
|
435
447
|
phase_branch_template: 'pan/phase-{phase}-{slug}',
|
|
436
448
|
milestone_branch_template: 'pan/{milestone}-{slug}',
|
|
437
|
-
research: true
|
|
449
|
+
research: false, // opt-in: a config that says true keeps it
|
|
438
450
|
plan_checker: true,
|
|
439
451
|
verifier: true,
|
|
440
452
|
nyquist_validation: false,
|
|
@@ -559,23 +571,28 @@ function isGitRepo(cwd) {
|
|
|
559
571
|
}
|
|
560
572
|
|
|
561
573
|
/**
|
|
562
|
-
* Execute a git command safely with proper argument escaping.
|
|
574
|
+
* Execute a git command safely with proper argument escaping. stdout is trimmed;
|
|
575
|
+
* `keepLeading` trims only whitespace at its end, for output whose first column means
|
|
576
|
+
* something (`git status --porcelain` starts an unstaged edit with a space, which a
|
|
577
|
+
* full trim cuts off the first line).
|
|
563
578
|
* @param {string} cwd - Working directory for git
|
|
564
579
|
* @param {string[]} args - Git arguments (e.g., ['add', 'file.md'])
|
|
580
|
+
* @param {{keepLeading?: boolean}} [opts]
|
|
565
581
|
* @returns {{exitCode: number, stdout: string, stderr: string}}
|
|
566
582
|
*/
|
|
567
|
-
function execGit(cwd, args) {
|
|
583
|
+
function execGit(cwd, args, opts = {}) {
|
|
584
|
+
const clean = (s) => (opts.keepLeading ? String(s).replace(/\s+$/, '') : String(s).trim());
|
|
568
585
|
try {
|
|
569
586
|
const stdout = execFileSync('git', args, {
|
|
570
587
|
cwd,
|
|
571
588
|
stdio: 'pipe',
|
|
572
589
|
encoding: 'utf-8',
|
|
573
590
|
});
|
|
574
|
-
return { exitCode: 0, stdout: stdout
|
|
591
|
+
return { exitCode: 0, stdout: clean(stdout), stderr: '' };
|
|
575
592
|
} catch (err) {
|
|
576
593
|
return {
|
|
577
594
|
exitCode: err.status ?? 1,
|
|
578
|
-
stdout: (err.stdout ?? '').toString()
|
|
595
|
+
stdout: clean((err.stdout ?? '').toString()),
|
|
579
596
|
stderr: (err.stderr ?? '').toString().trim(),
|
|
580
597
|
};
|
|
581
598
|
}
|
|
@@ -149,7 +149,8 @@ const DEFAULT_RATES = {
|
|
|
149
149
|
|
|
150
150
|
// Google Gemini — ai.google.dev/gemini-api/docs/pricing, paid tier, text input,
|
|
151
151
|
// the ≤200K-token prompt tier where the page splits by length, read 2026-09-23.
|
|
152
|
-
// cache_read is the page's "context caching" price: 0.1× input on every row
|
|
152
|
+
// cache_read is the page's "context caching" price: 0.1× input on every row but
|
|
153
|
+
// Gemini 4 Argon (0.05×), not
|
|
153
154
|
// the 0.25× this block carried until 2026-09-22 (cache reads 2.5× high, R26). The
|
|
154
155
|
// tokens that CREATE a cache bill at the standard input price (Google Cloud's
|
|
155
156
|
// context-cache overview; the Gemini API pages list only cached-token and storage
|
|
@@ -157,7 +158,12 @@ const DEFAULT_RATES = {
|
|
|
157
158
|
// HOUR, which a per-token table cannot carry: PAN's Gemini totals exclude it.
|
|
158
159
|
// The 3.6/3.7/3.8 Flash prices run "through December 31, 2026" and double from
|
|
159
160
|
// January 1, 2027 (the page states both) — re-read them after that date.
|
|
160
|
-
|
|
161
|
+
// Gemini 4 Argon (paid tier only; one price at every prompt length; implicit caching
|
|
162
|
+
// only): these rates run "through November 30, 2026" and double from December 1,
|
|
163
|
+
// 2026 (input $4.00, output $20.00, caching $0.20; read 2026-10-19) — change the row
|
|
164
|
+
// on that date.
|
|
165
|
+
'gemini-4-argon': { input: 2.00, output: 10.0, cache_read: 0.10, cache_write: 2.00 },
|
|
166
|
+
'gemini-3.8-flash': { input: 0.75, output: 3.75, cache_read: 0.075, cache_write: 0.75 },
|
|
161
167
|
'gemini-3.7-flash': { input: 0.75, output: 3.75, cache_read: 0.075, cache_write: 0.75 },
|
|
162
168
|
'gemini-3.6-flash': { input: 0.75, output: 3.75, cache_read: 0.075, cache_write: 0.75 },
|
|
163
169
|
'gemini-3.5-flash': { input: 1.50, output: 9.00, cache_read: 0.15, cache_write: 1.50 },
|
|
@@ -664,6 +670,146 @@ function aggregate(cwd, opts) {
|
|
|
664
670
|
};
|
|
665
671
|
}
|
|
666
672
|
|
|
673
|
+
// ─── Per-phase budget ───────────────────────────────────────────────────────
|
|
674
|
+
// `cost.phase_budget_usd` (default $20, `0`/`false` = off). It only WARNS: `near` from
|
|
675
|
+
// PHASE_BUDGET_WARN_PCT of the budget, `over` once spend reaches it. No run stops on it.
|
|
676
|
+
// On a subscription the dollars are API-equivalent: what the tokens would cost at the
|
|
677
|
+
// configured rates.
|
|
678
|
+
|
|
679
|
+
const DEFAULT_PHASE_BUDGET_USD = 20;
|
|
680
|
+
const PHASE_BUDGET_WARN_PCT = 80;
|
|
681
|
+
|
|
682
|
+
function phaseBudgetUsd(cwd) {
|
|
683
|
+
let v;
|
|
684
|
+
try { v = loadConfig(cwd)?.cost?.phase_budget_usd; } catch { /* defaults */ }
|
|
685
|
+
if (v === false || v === 0 || v === '0') return 0;
|
|
686
|
+
const n = Number(v);
|
|
687
|
+
return v != null && Number.isFinite(n) && n > 0 ? n : DEFAULT_PHASE_BUDGET_USD;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* A phase's spend against its budget.
|
|
692
|
+
* @returns {{phase, budget_usd, spent_usd, pct, status: 'off'|'ok'|'near'|'over', mode: 'warn', warn_at_pct, unpriced}}
|
|
693
|
+
*/
|
|
694
|
+
function phaseBudget(cwd, phase) {
|
|
695
|
+
const budget = phaseBudgetUsd(cwd);
|
|
696
|
+
const rates = effectiveRates(loadConfig(cwd));
|
|
697
|
+
const key = samePhaseKey(phase);
|
|
698
|
+
let spent = 0;
|
|
699
|
+
let unpriced = 0;
|
|
700
|
+
for (const r of readRecords(cwd)) {
|
|
701
|
+
if (samePhaseKey(r.phase) !== key || isSuspectRecord(r) || isEmptyRecord(r)) continue;
|
|
702
|
+
const usd = typeof r.cost_usd === 'number' ? r.cost_usd : computeCost(r, rates);
|
|
703
|
+
if (typeof usd === 'number') spent += usd;
|
|
704
|
+
else unpriced++;
|
|
705
|
+
}
|
|
706
|
+
spent = round4(spent);
|
|
707
|
+
const pct = budget > 0 ? Math.round((spent / budget) * 1000) / 10 : null;
|
|
708
|
+
let status = 'off';
|
|
709
|
+
if (budget > 0) status = pct >= 100 ? 'over' : pct >= PHASE_BUDGET_WARN_PCT ? 'near' : 'ok';
|
|
710
|
+
return { phase: String(phase), budget_usd: budget, spent_usd: spent, pct, status, mode: 'warn', warn_at_pct: PHASE_BUDGET_WARN_PCT, unpriced };
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function cmdCostBudget(cwd, phase, raw) {
|
|
714
|
+
if (!phase) error('Usage: cost budget --phase <N>');
|
|
715
|
+
const r = phaseBudget(cwd, phase);
|
|
716
|
+
output(r, raw, r.status);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// ─── Cost forecast (program P6) ─────────────────────────────────────────────
|
|
720
|
+
// What a plan and a phase will cost, as p10/p50/p90 dollars, from what this project's
|
|
721
|
+
// agents have cost so far. Fewer than FORECAST_MIN_SAMPLES observations is no
|
|
722
|
+
// forecast: the result says `insufficient` rather than extrapolating from two runs.
|
|
723
|
+
|
|
724
|
+
const FORECAST_MIN_SAMPLES = 5;
|
|
725
|
+
const FORECAST_ITERATIONS = 2000;
|
|
726
|
+
const PLAN_AGENT = 'pan-executor';
|
|
727
|
+
|
|
728
|
+
const round4 = (x) => Math.round(x * 10000) / 10000;
|
|
729
|
+
|
|
730
|
+
/** Linear interpolation between the closest ranks (R type 7). */
|
|
731
|
+
function quantile(sorted, q) {
|
|
732
|
+
const h = (sorted.length - 1) * q;
|
|
733
|
+
const lo = Math.floor(h);
|
|
734
|
+
return sorted[lo] + (sorted[Math.min(lo + 1, sorted.length - 1)] - sorted[lo]) * (h - lo);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function quantiles(values) {
|
|
738
|
+
const s = [...values].sort((a, b) => a - b);
|
|
739
|
+
return { p10: round4(quantile(s, 0.1)), p50: round4(quantile(s, 0.5)), p90: round4(quantile(s, 0.9)) };
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/** mulberry32: a seeded PRNG, so a forecast is reproducible. */
|
|
743
|
+
function prng(seed) {
|
|
744
|
+
let a = (Number(seed) >>> 0) || 1;
|
|
745
|
+
return () => {
|
|
746
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
747
|
+
let t = a;
|
|
748
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
749
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
750
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const samePhaseKey = (p) => String(p == null ? '' : p).replace(/^0+(?=\d)/, '');
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Forecast a plan and a phase.
|
|
758
|
+
*
|
|
759
|
+
* - Per plan: the cost of each pan-executor call at one tier (the tier the latest
|
|
760
|
+
* executor call ran at, unless `tier` is given). Tiers differ several-fold in price,
|
|
761
|
+
* so they are never pooled.
|
|
762
|
+
* - Per phase: for each past phase, the cost of its other agents (planner, checkers,
|
|
763
|
+
* verifier, reviewer …); a phase is resampled as one of those plus one executor
|
|
764
|
+
* draw per plan, `FORECAST_ITERATIONS` times, seeded.
|
|
765
|
+
*
|
|
766
|
+
* @param {string} cwd
|
|
767
|
+
* @param {{plans?: number, tier?: string, excludePhase?: string, seed?: number}} [opts]
|
|
768
|
+
*/
|
|
769
|
+
function costForecast(cwd, opts = {}) {
|
|
770
|
+
const rates = effectiveRates(loadConfig(cwd));
|
|
771
|
+
const exclude = opts.excludePhase != null ? samePhaseKey(opts.excludePhase) : null;
|
|
772
|
+
const rows = [];
|
|
773
|
+
for (const r of readRecords(cwd)) {
|
|
774
|
+
if (isSuspectRecord(r) || isEmptyRecord(r)) continue;
|
|
775
|
+
if (exclude != null && samePhaseKey(r.phase) === exclude) continue;
|
|
776
|
+
const usd = typeof r.cost_usd === 'number' ? r.cost_usd : computeCost(r, rates);
|
|
777
|
+
if (typeof usd === 'number') rows.push({ ...r, usd });
|
|
778
|
+
}
|
|
779
|
+
const executor = rows.filter((r) => r.agent === PLAN_AGENT);
|
|
780
|
+
const tier = opts.tier || (executor.length ? executor[executor.length - 1].tier || null : null);
|
|
781
|
+
const planCosts = executor.filter((r) => (r.tier || null) === tier).map((r) => r.usd);
|
|
782
|
+
const parsedPlans = Number.parseInt(opts.plans, 10);
|
|
783
|
+
const plans = Number.isFinite(parsedPlans) && parsedPlans >= 0 ? parsedPlans : 1;
|
|
784
|
+
const insufficient = (n) => ({ status: 'insufficient', n, needed: FORECAST_MIN_SAMPLES });
|
|
785
|
+
|
|
786
|
+
const perPlan = planCosts.length >= FORECAST_MIN_SAMPLES ? { status: 'ok', n: planCosts.length, ...quantiles(planCosts) } : insufficient(planCosts.length);
|
|
787
|
+
|
|
788
|
+
// A past phase is one the executor ran in; its overhead is everything else it spent.
|
|
789
|
+
const ranIn = new Set(executor.filter((r) => r.phase != null).map((r) => samePhaseKey(r.phase)));
|
|
790
|
+
const overhead = new Map([...ranIn].map((p) => [p, 0]));
|
|
791
|
+
for (const r of rows) {
|
|
792
|
+
const p = samePhaseKey(r.phase);
|
|
793
|
+
if (r.agent !== PLAN_AGENT && overhead.has(p)) overhead.set(p, overhead.get(p) + r.usd);
|
|
794
|
+
}
|
|
795
|
+
const overheads = [...overhead.values()];
|
|
796
|
+
let phase;
|
|
797
|
+
if (perPlan.status !== 'ok') phase = { ...insufficient(planCosts.length), n_phases: overheads.length, reason: 'too few executor calls at this tier' };
|
|
798
|
+
else if (overheads.length < FORECAST_MIN_SAMPLES) phase = { ...insufficient(overheads.length), n_phases: overheads.length, reason: 'too few past phases' };
|
|
799
|
+
else {
|
|
800
|
+
const rand = prng(opts.seed == null ? 1 : opts.seed);
|
|
801
|
+
const pick = (arr) => arr[Math.floor(rand() * arr.length)];
|
|
802
|
+
const sims = [];
|
|
803
|
+
for (let i = 0; i < FORECAST_ITERATIONS; i++) {
|
|
804
|
+
let usd = pick(overheads);
|
|
805
|
+
for (let k = 0; k < plans; k++) usd += pick(planCosts);
|
|
806
|
+
sims.push(usd);
|
|
807
|
+
}
|
|
808
|
+
phase = { status: 'ok', n_phases: overheads.length, ...quantiles(sims) };
|
|
809
|
+
}
|
|
810
|
+
return { agent: PLAN_AGENT, tier, plans, min_samples: FORECAST_MIN_SAMPLES, per_plan: perPlan, phase };
|
|
811
|
+
}
|
|
812
|
+
|
|
667
813
|
/**
|
|
668
814
|
* Render aggregation as a human-readable table.
|
|
669
815
|
* @param {Object} agg - from aggregate()
|
|
@@ -759,8 +905,12 @@ function cmdCostAppend(cwd, rec, raw) {
|
|
|
759
905
|
|
|
760
906
|
function cmdCostClear(cwd, raw) {
|
|
761
907
|
try {
|
|
762
|
-
|
|
763
|
-
|
|
908
|
+
// L6a: the ledger is the only record of what was spent; keep a copy before deleting it.
|
|
909
|
+
const file = tokensFile(cwd);
|
|
910
|
+
const backup = `${file}.bak-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}`;
|
|
911
|
+
fs.copyFileSync(file, backup);
|
|
912
|
+
fs.unlinkSync(file);
|
|
913
|
+
output({ cleared: true, file, backup }, raw);
|
|
764
914
|
} catch (e) {
|
|
765
915
|
output({ cleared: false, error: e.message || 'ledger_clear_failed' }, raw);
|
|
766
916
|
}
|
|
@@ -900,6 +1050,12 @@ function cmdModelsCheck(raw) {
|
|
|
900
1050
|
|
|
901
1051
|
module.exports = {
|
|
902
1052
|
computeCost,
|
|
1053
|
+
phaseBudget,
|
|
1054
|
+
cmdCostBudget,
|
|
1055
|
+
DEFAULT_PHASE_BUDGET_USD,
|
|
1056
|
+
costForecast,
|
|
1057
|
+
quantiles,
|
|
1058
|
+
FORECAST_MIN_SAMPLES,
|
|
903
1059
|
appendRecord,
|
|
904
1060
|
readRecords,
|
|
905
1061
|
aggregate,
|