pan-wizard 3.36.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 +36 -48
- 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 +1 -2
- package/agents/pan-document_code.md +1 -2
- package/agents/pan-hardener.md +2 -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 +79 -488
- package/agents/pan-plan-checker.md +12 -2
- package/agents/pan-planner.md +10 -6
- package/agents/pan-previewer.md +1 -2
- package/agents/pan-project-researcher.md +70 -499
- package/agents/pan-release.md +1 -1
- package/agents/pan-reviewer.md +9 -3
- package/agents/pan-roadmapper.md +12 -7
- package/agents/pan-verifier.md +7 -2
- 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/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/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 +31 -2
- package/pan-wizard-core/bin/lib/constants.cjs +27 -1
- package/pan-wizard-core/bin/lib/core.cjs +22 -10
- package/pan-wizard-core/bin/lib/cost.cjs +14 -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/failure-class.cjs +81 -0
- package/pan-wizard-core/bin/lib/findings.cjs +34 -4
- package/pan-wizard-core/bin/lib/focus.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 +16 -5
- 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.cjs +48 -4
- package/pan-wizard-core/bin/lib/plan-lint.cjs +150 -1
- package/pan-wizard-core/bin/lib/preview.cjs +23 -4
- package/pan-wizard-core/bin/lib/requirements-lint.cjs +130 -0
- package/pan-wizard-core/bin/lib/research-lint.cjs +150 -3
- 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/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/uat.cjs +149 -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 +37 -1
- package/pan-wizard-core/bin/lib/verify-retro.cjs +3 -2
- package/pan-wizard-core/bin/lib/verify.cjs +59 -4
- package/pan-wizard-core/bin/lib/worktree.cjs +70 -0
- package/pan-wizard-core/bin/pan-tools.cjs +168 -27
- package/pan-wizard-core/learnings/universal/autonomous-loop.md +2 -2
- package/pan-wizard-core/references/continuation-format.md +3 -3
- package/pan-wizard-core/references/guardrails.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 +5 -1
- package/pan-wizard-core/templates/debug.md +1 -0
- package/pan-wizard-core/templates/discovery.md +2 -2
- package/pan-wizard-core/templates/requirements.md +9 -0
- 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 +36 -8
- 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 +31 -53
- 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 +86 -43
- 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/research-phase.md +118 -40
- package/pan-wizard-core/workflows/resume-project.md +18 -2
- package/pan-wizard-core/workflows/settings.md +21 -24
- 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 +2 -2
- package/scripts/build-agent-plugin.js +35 -15
- 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/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 +9 -2
- 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 -210
- package/pan-wizard-core/templates/research-project/features.md +0 -153
- package/pan-wizard-core/templates/research-project/pitfalls.md +0 -206
- package/pan-wizard-core/templates/research-project/stack.md +0 -126
- package/pan-wizard-core/templates/research-project/summary.md +0 -170
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* One next-action router (program L7): `pan-tools next-action`.
|
|
4
|
+
*
|
|
5
|
+
* progress, resume and transition each kept their own routing table over the same
|
|
6
|
+
* facts. They now ask this verb, which reads the disk (roadmap.md and the phase
|
|
7
|
+
* directories) into the snapshot the MCP orchestrator's state machine
|
|
8
|
+
* (mcp/orchestrator.cjs `nextAction`) keys on, and maps its decision to the command
|
|
9
|
+
* that carries it out.
|
|
10
|
+
*
|
|
11
|
+
* Run facts on disk: a phase is `verified` when it has a verification file, and
|
|
12
|
+
* finished (`merged` in the state machine's words) when its roadmap checkbox is ticked,
|
|
13
|
+
* which `phase complete` does. Two interactive routes come before the machine: diagnosed
|
|
14
|
+
* UAT gaps are planned first, and a project between milestones (roadmap but no
|
|
15
|
+
* requirements.md) starts the next one.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const { output } = require('./core.cjs');
|
|
21
|
+
const { planningPath } = require('./utils.cjs');
|
|
22
|
+
const { enumerateRoadmapPhases, enrichPhaseWithDiskStatus } = require('./roadmap.cjs');
|
|
23
|
+
const { nextAction } = require('../../mcp/orchestrator.cjs');
|
|
24
|
+
|
|
25
|
+
function escapeRegex(s) {
|
|
26
|
+
return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function phaseDir(cwd, number) {
|
|
30
|
+
const root = path.join(planningPath(cwd), 'phases');
|
|
31
|
+
const num = String(number);
|
|
32
|
+
const padded = num.includes('.') ? num : num.padStart(2, '0');
|
|
33
|
+
try {
|
|
34
|
+
const d = fs.readdirSync(root, { withFileTypes: true })
|
|
35
|
+
.find((e) => e.isDirectory() && (e.name === padded || e.name.startsWith(`${padded}-`) || e.name.startsWith(`${num}-`)));
|
|
36
|
+
return d ? path.join(root, d.name) : null;
|
|
37
|
+
} catch { return null; }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function listDir(dir) {
|
|
41
|
+
try { return fs.readdirSync(dir); } catch { return []; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The state machine's snapshot, read from disk.
|
|
46
|
+
* @returns {{phases: Array<{number, status, verified, merged, uat_gaps}>, roadmap: boolean, requirements: boolean}}
|
|
47
|
+
*/
|
|
48
|
+
function diskSnapshot(cwd) {
|
|
49
|
+
const planning = planningPath(cwd);
|
|
50
|
+
let content = null;
|
|
51
|
+
try { content = fs.readFileSync(path.join(planning, 'roadmap.md'), 'utf8').replace(/\r\n/g, '\n'); } catch {}
|
|
52
|
+
const requirements = fs.existsSync(path.join(planning, 'requirements.md'));
|
|
53
|
+
if (content === null) return { phases: [], roadmap: false, requirements };
|
|
54
|
+
const phases = enumerateRoadmapPhases(content).map((raw) => {
|
|
55
|
+
const p = enrichPhaseWithDiskStatus(cwd, raw);
|
|
56
|
+
const dir = phaseDir(cwd, p.number);
|
|
57
|
+
const files = dir ? listDir(dir) : [];
|
|
58
|
+
const ticked = new RegExp(`-\\s*\\[x\\]\\s*.*Phase\\s+${escapeRegex(p.number)}\\b`, 'i').test(content);
|
|
59
|
+
const uatGaps = files.filter((f) => /uat\.md$/i.test(f)).some((f) => {
|
|
60
|
+
try { return /^status:\s*diagnosed\s*$/m.test(fs.readFileSync(path.join(dir, f), 'utf8')); } catch { return false; }
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
number: p.number,
|
|
64
|
+
status: p.disk_status === 'no_directory' ? 'empty' : p.disk_status,
|
|
65
|
+
verified: files.some((f) => f === 'verification.md' || f.endsWith('-verification.md')),
|
|
66
|
+
merged: ticked,
|
|
67
|
+
uat_gaps: uatGaps,
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
return { phases, roadmap: true, requirements };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const COMMANDS = {
|
|
74
|
+
plan: (n) => `/pan:plan-phase ${n}`,
|
|
75
|
+
execute: (n) => `/pan:exec-phase ${n}`,
|
|
76
|
+
verify: (n) => `/pan:verify-phase ${n}`,
|
|
77
|
+
complete_phase: (n) => `pan-tools phase complete ${n}`,
|
|
78
|
+
plan_gaps: (n) => `/pan:plan-phase ${n} --gaps`,
|
|
79
|
+
audit_milestone: () => '/pan:milestone-audit',
|
|
80
|
+
new_milestone: () => '/pan:milestone-new',
|
|
81
|
+
new_project: () => '/pan:new-project',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Decide the next step for the project at `cwd`.
|
|
86
|
+
* @returns {{action: string, phase: string|null, reason: string, command: string|null, done: boolean}}
|
|
87
|
+
*/
|
|
88
|
+
function routeNextAction(cwd) {
|
|
89
|
+
const snap = diskSnapshot(cwd);
|
|
90
|
+
const result = (action, phase, reason, done = false) => ({
|
|
91
|
+
action, phase: phase == null ? null : String(phase), reason,
|
|
92
|
+
command: COMMANDS[action] ? COMMANDS[action](phase) : null, done,
|
|
93
|
+
});
|
|
94
|
+
if (!snap.roadmap) return result('new_project', null, 'no_roadmap');
|
|
95
|
+
if (!snap.requirements) return result('new_milestone', null, 'between_milestones');
|
|
96
|
+
|
|
97
|
+
const open = snap.phases.find((p) => !(p.verified && p.merged));
|
|
98
|
+
if (open && open.uat_gaps) return result('plan_gaps', open.number, 'uat_gaps_diagnosed');
|
|
99
|
+
|
|
100
|
+
const d = nextAction({ phases: snap.phases });
|
|
101
|
+
if (d.action === 'request_merge') return result('complete_phase', d.args.phase, d.reason);
|
|
102
|
+
if (d.action === 'stop' && d.reason === 'all_complete') return result('audit_milestone', null, 'all_phases_complete');
|
|
103
|
+
if (d.action === 'stop') return result('stop', null, d.reason, d.done);
|
|
104
|
+
return result(d.action, d.args && d.args.phase, d.reason);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function cmdNextAction(cwd, raw) {
|
|
108
|
+
const r = routeNextAction(cwd);
|
|
109
|
+
output(r, raw, r.command || r.action);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = { diskSnapshot, routeNextAction, cmdNextAction };
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// optimize.cjs — Circular optimization loop: trace → learn → apply → repeat.
|
|
3
3
|
//
|
|
4
4
|
// Every agent spawn is logged to a trace session. After a build, /pan:learn
|
|
5
|
-
// invokes pan-optimizer to analyze the trace. /pan:optimize apply writes memory
|
|
5
|
+
// invokes pan-optimizer to analyze the trace. /pan:learn --optimize apply writes memory
|
|
6
6
|
// entries, config notes, and prompt suggestions back into the project, making
|
|
7
7
|
// the next run smarter.
|
|
8
8
|
|
|
@@ -1044,7 +1044,7 @@ function revertApply(cwd, which) {
|
|
|
1044
1044
|
const LESSON_TARGET = "the project's instructions (CLAUDE.md or AGENTS.md, outside PAN's section)";
|
|
1045
1045
|
|
|
1046
1046
|
// Derive suggestion notes from a raw JSON analysis when no optimizer agent
|
|
1047
|
-
// report is available (fallback for /pan:optimize apply on a JSON file).
|
|
1047
|
+
// report is available (fallback for /pan:learn --optimize apply on a JSON file).
|
|
1048
1048
|
function deriveActionsFromAnalysis(analysis) {
|
|
1049
1049
|
const actions = [];
|
|
1050
1050
|
const { memory_miss_patterns, gap_patterns, summary } = analysis;
|
|
@@ -278,9 +278,9 @@ function cmdPhasePlanIndex(cwd, phase, raw, opts = {}) {
|
|
|
278
278
|
const planFiles = filterPlanFiles(phaseFiles);
|
|
279
279
|
const summaryFiles = filterSummaryFiles(phaseFiles);
|
|
280
280
|
|
|
281
|
-
const { plans, waves, incomplete, hasCheckpoints } = buildPlanIndex(phaseDir, planFiles, summaryFiles);
|
|
281
|
+
const { plans, waves, subWaves, incomplete, hasCheckpoints } = buildPlanIndex(phaseDir, planFiles, summaryFiles);
|
|
282
282
|
|
|
283
|
-
const result = { phase: normalized, plans, waves, incomplete, has_checkpoints: hasCheckpoints };
|
|
283
|
+
const result = { phase: normalized, plans, waves, sub_waves: subWaves, incomplete, has_checkpoints: hasCheckpoints };
|
|
284
284
|
// `--failed <ids>` (market-ideas M28): the plans that must not run after those
|
|
285
285
|
// failed, because a dependency, direct or transitive, is among them.
|
|
286
286
|
if (opts.failed) {
|
|
@@ -336,6 +336,35 @@ function blockedPlans(plans, failedIds) {
|
|
|
336
336
|
* @param {string[]} summaryFiles - List of summary filenames
|
|
337
337
|
* @returns {{plans: Array, waves: Object, incomplete: string[], hasCheckpoints: boolean}}
|
|
338
338
|
*/
|
|
339
|
+
/**
|
|
340
|
+
* Split one wave's plans into sub-waves so no two plans in a sub-wave modify the same
|
|
341
|
+
* file (program A3). Greedy, in plan order: each plan joins the first sub-wave it does
|
|
342
|
+
* not overlap. A plan that declares no files may touch anything, so it runs alone.
|
|
343
|
+
* @param {Array<{id: string, files_modified?: string[]}>} plans
|
|
344
|
+
* @returns {string[][]} plan ids per sub-wave
|
|
345
|
+
*/
|
|
346
|
+
function partitionWave(plans) {
|
|
347
|
+
const subs = [];
|
|
348
|
+
const norm = (p) => String(p).replace(/\\/g, '/').replace(/^\.\//, '');
|
|
349
|
+
for (const p of plans) {
|
|
350
|
+
const files = (p.files_modified || []).map(norm);
|
|
351
|
+
const solo = files.length === 0;
|
|
352
|
+
let placed = false;
|
|
353
|
+
if (!solo) {
|
|
354
|
+
for (const sub of subs) {
|
|
355
|
+
if (sub.solo) continue;
|
|
356
|
+
if (files.some((x) => sub.files.has(x))) continue;
|
|
357
|
+
sub.ids.push(p.id);
|
|
358
|
+
for (const x of files) sub.files.add(x);
|
|
359
|
+
placed = true;
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (!placed) subs.push({ ids: [p.id], files: new Set(files), solo });
|
|
364
|
+
}
|
|
365
|
+
return subs.map((x) => x.ids);
|
|
366
|
+
}
|
|
367
|
+
|
|
339
368
|
function buildPlanIndex(phaseDir, planFiles, summaryFiles) {
|
|
340
369
|
const completedPlanIds = new Set(
|
|
341
370
|
summaryFiles.map(summaryFile => summaryFile.replace('-summary.md', '').replace('summary.md', ''))
|
|
@@ -401,7 +430,14 @@ function buildPlanIndex(phaseDir, planFiles, summaryFiles) {
|
|
|
401
430
|
waves[waveKey].push(planId);
|
|
402
431
|
}
|
|
403
432
|
|
|
404
|
-
|
|
433
|
+
// A3: within a wave, plans that share a file run in different sub-waves.
|
|
434
|
+
const subWaves = {};
|
|
435
|
+
for (const [w, ids] of Object.entries(waves)) {
|
|
436
|
+
subWaves[w] = partitionWave(ids.map((id) => plans.find((p) => p.id === id)));
|
|
437
|
+
subWaves[w].forEach((sub, i) => { for (const id of sub) plans.find((p) => p.id === id).sub_wave = i; });
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return { plans, waves, subWaves, incomplete, hasCheckpoints };
|
|
405
441
|
}
|
|
406
442
|
|
|
407
443
|
/**
|
|
@@ -741,8 +777,14 @@ function markRequirementsCompleteForPhase(cwd, phaseNum, roadmapContent) {
|
|
|
741
777
|
const reqIds = roadmapRequirementIds(roadmapContent, phaseNum);
|
|
742
778
|
if (reqIds.length === 0) return;
|
|
743
779
|
let reqContent = reqReadContent;
|
|
780
|
+
// V8: a requirement with EARS criteria is ticked only when its mapped tests pass;
|
|
781
|
+
// a legacy one (no criteria) keeps the old tick. Held ones are reported.
|
|
782
|
+
const decisions = require('./requirements-lint.cjs').tickDecisions(cwd, reqIds);
|
|
783
|
+
const held = [];
|
|
744
784
|
|
|
745
785
|
for (const reqId of reqIds) {
|
|
786
|
+
const d = decisions.get(reqId);
|
|
787
|
+
if (d && !d.allowed) { held.push({ id: reqId, status: d.status }); continue; }
|
|
746
788
|
const reqEscaped = escapeRegex(reqId);
|
|
747
789
|
reqContent = reqContent.replace(
|
|
748
790
|
new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi'),
|
|
@@ -759,7 +801,7 @@ function markRequirementsCompleteForPhase(cwd, phaseNum, roadmapContent) {
|
|
|
759
801
|
} catch (e) {
|
|
760
802
|
return { updated: false, error: 'Failed to write requirements.md: ' + e.message };
|
|
761
803
|
}
|
|
762
|
-
return { updated: true };
|
|
804
|
+
return { updated: true, held };
|
|
763
805
|
} catch {
|
|
764
806
|
// requirements.md not found or unreadable — skip
|
|
765
807
|
return { updated: false };
|
|
@@ -1027,6 +1069,7 @@ function classifyPlanTier(frontmatter, config) {
|
|
|
1027
1069
|
}
|
|
1028
1070
|
|
|
1029
1071
|
module.exports = {
|
|
1072
|
+
partitionWave,
|
|
1030
1073
|
cmdPhasesList,
|
|
1031
1074
|
cmdPhaseNextDecimal,
|
|
1032
1075
|
cmdFindPhase,
|
|
@@ -1044,6 +1087,7 @@ module.exports = {
|
|
|
1044
1087
|
renumberIntegerPhases,
|
|
1045
1088
|
updateRoadmapAfterRemoval,
|
|
1046
1089
|
markPhaseCompleteInRoadmap,
|
|
1090
|
+
markRequirementsCompleteForPhase,
|
|
1047
1091
|
updateStateAfterPhaseComplete,
|
|
1048
1092
|
classifyPlanTier,
|
|
1049
1093
|
};
|
|
@@ -144,6 +144,152 @@ function checkDecisions(cwd, phase) {
|
|
|
144
144
|
|
|
145
145
|
// ─── CLI ─────────────────────────────────────────────────────────────────────
|
|
146
146
|
|
|
147
|
+
// ─── Gate baselines (M48) ─────────────────────────────────────────────────────
|
|
148
|
+
//
|
|
149
|
+
// A plan's `<verify>` often names a grep or count gate. One that already passes
|
|
150
|
+
// before any work proves nothing about the task; one whose pattern cannot compile
|
|
151
|
+
// can never match (gsd-core v1.18.0 "gate baselines"). The simple grep forms are
|
|
152
|
+
// interpreted here, in Node, never through a shell: the same on every platform,
|
|
153
|
+
// and nothing a plan writes is executed. Anything else is listed as not checkable.
|
|
154
|
+
|
|
155
|
+
const GATE_TOOLS = [['git', 'grep'], ['grep'], ['rg']];
|
|
156
|
+
const GATE_FLAGS = new Set(['r', 'R', 'i', 'c', 'E', 'F', 'w', 'q', 'l', 'n', 's', 'H', 'h']);
|
|
157
|
+
const REGRESSION_GUARD = /regression guard|already passes|must still pass/i;
|
|
158
|
+
|
|
159
|
+
/** Split a command line on spaces, honouring quotes. Null on shell syntax. */
|
|
160
|
+
function shellWords(cmd) {
|
|
161
|
+
const words = [];
|
|
162
|
+
let cur = '';
|
|
163
|
+
let quote = null;
|
|
164
|
+
let started = false;
|
|
165
|
+
for (const ch of cmd) {
|
|
166
|
+
if (quote) {
|
|
167
|
+
if (ch === quote) quote = null; else cur += ch;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (ch === '"' || ch === "'") { quote = ch; started = true; continue; }
|
|
171
|
+
if (/[|;&<>`$()\\]/.test(ch)) return null;
|
|
172
|
+
if (/\s/.test(ch)) { if (started) { words.push(cur); cur = ''; started = false; } continue; }
|
|
173
|
+
cur += ch; started = true;
|
|
174
|
+
}
|
|
175
|
+
if (quote) return null;
|
|
176
|
+
if (started) words.push(cur);
|
|
177
|
+
return words;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Parse a simple grep gate: `grep|rg|git grep [flags] PATTERN [paths...]`.
|
|
182
|
+
* @returns {{tool, flags, pattern, paths}|null} null when the command is not one
|
|
183
|
+
*/
|
|
184
|
+
function parseGateCommand(cmd) {
|
|
185
|
+
const words = shellWords(String(cmd || '').trim());
|
|
186
|
+
if (!words || !words.length) return null;
|
|
187
|
+
const tool = GATE_TOOLS.find((t) => t.every((w, i) => words[i] === w));
|
|
188
|
+
if (!tool) return null;
|
|
189
|
+
const flags = {};
|
|
190
|
+
let pattern = null;
|
|
191
|
+
const paths = [];
|
|
192
|
+
let flagsDone = false;
|
|
193
|
+
for (const w of words.slice(tool.length)) {
|
|
194
|
+
if (!flagsDone && w === '--') { flagsDone = true; continue; }
|
|
195
|
+
if (!flagsDone && /^-[A-Za-z]+$/.test(w)) {
|
|
196
|
+
for (const f of w.slice(1)) { if (!GATE_FLAGS.has(f)) return null; flags[f] = true; }
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (!flagsDone && w.startsWith('-')) return null;
|
|
200
|
+
if (pattern === null) pattern = w; else paths.push(w);
|
|
201
|
+
}
|
|
202
|
+
if (pattern === null) return null;
|
|
203
|
+
return { tool: tool.join(' '), flags, pattern, paths };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** A JS RegExp for a grep pattern: -F literal, -E extended, else basic (BRE). Null when it cannot compile. */
|
|
207
|
+
function gateRegex(g) {
|
|
208
|
+
let src = g.pattern;
|
|
209
|
+
if (g.flags.F) src = src.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
210
|
+
else if (!g.flags.E && g.tool !== 'rg') {
|
|
211
|
+
// BRE: \( \) \{ \} \| \+ \? are the operators; their bare forms are literals.
|
|
212
|
+
src = src.replace(/\\([(){}|+?])|([(){}|+?])/g, (m, esc, bare) => (esc ? esc : '\\' + bare));
|
|
213
|
+
}
|
|
214
|
+
if (g.flags.w) src = `\\b(?:${src})\\b`;
|
|
215
|
+
try { return new RegExp(src, g.flags.i ? 'i' : ''); } catch { return null; }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function filesUnder(root, rel, recursive) {
|
|
219
|
+
const abs = path.join(root, rel);
|
|
220
|
+
let st;
|
|
221
|
+
try { st = fs.statSync(abs); } catch { return null; }
|
|
222
|
+
if (st.isFile()) return [abs];
|
|
223
|
+
if (!st.isDirectory() || !recursive) return [];
|
|
224
|
+
const out = [];
|
|
225
|
+
const walk = (d) => {
|
|
226
|
+
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
227
|
+
if (e.name === '.git' || e.name === 'node_modules') continue;
|
|
228
|
+
const p = path.join(d, e.name);
|
|
229
|
+
if (e.isDirectory()) walk(p); else if (e.isFile()) out.push(p);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
walk(abs);
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Run a parsed gate against the tree as it is: passes_now | fails_now | target_missing | invalid. */
|
|
237
|
+
function runGate(cwd, g) {
|
|
238
|
+
const re = gateRegex(g);
|
|
239
|
+
if (!re) return 'invalid';
|
|
240
|
+
const recursive = g.tool !== 'grep' || g.flags.r || g.flags.R;
|
|
241
|
+
const targets = g.paths.length ? g.paths : ['.'];
|
|
242
|
+
let anyTarget = false;
|
|
243
|
+
for (const t of targets) {
|
|
244
|
+
const files = filesUnder(cwd, t, recursive);
|
|
245
|
+
if (files === null) continue;
|
|
246
|
+
anyTarget = true;
|
|
247
|
+
for (const f of files) {
|
|
248
|
+
let text;
|
|
249
|
+
try { text = toLf(fs.readFileSync(f, 'utf-8')); } catch { continue; }
|
|
250
|
+
if (text.split('\n').some((line) => re.test(line))) return 'passes_now';
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return anyTarget ? 'fails_now' : 'target_missing';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** The gate commands in a plan: backticked spans inside each task's <verify>. */
|
|
257
|
+
function gatesOf(text) {
|
|
258
|
+
const out = [];
|
|
259
|
+
const taskRe = /<task\b[^>]*>([\s\S]*?)<\/task>/g;
|
|
260
|
+
let t;
|
|
261
|
+
while ((t = taskRe.exec(text)) !== null) {
|
|
262
|
+
const name = ((t[1].match(/<name>([\s\S]*?)<\/name>/) || [])[1] || 'unnamed').trim();
|
|
263
|
+
const verify = (t[1].match(/<verify>([\s\S]*?)<\/verify>/) || [])[1] || '';
|
|
264
|
+
for (const m of verify.matchAll(/`([^`]+)`/g)) out.push({ task: name, command: m[1].trim(), guard: REGRESSION_GUARD.test(verify) });
|
|
265
|
+
}
|
|
266
|
+
return out;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function checkGates(cwd, phase) {
|
|
270
|
+
const pp = phasePlans(cwd, phase);
|
|
271
|
+
if (pp.error) return pp;
|
|
272
|
+
const gates = [];
|
|
273
|
+
const errors = [];
|
|
274
|
+
for (const p of pp.plans) {
|
|
275
|
+
for (const g of gatesOf(p.text)) {
|
|
276
|
+
const parsed = parseGateCommand(g.command);
|
|
277
|
+
const status = parsed ? runGate(cwd, parsed) : 'not_checkable';
|
|
278
|
+
gates.push({ plan: p.plan, task: g.task, command: g.command, status, ...(g.guard ? { regression_guard: true } : {}) });
|
|
279
|
+
if (status === 'passes_now' && !g.guard) errors.push({ code: 'gate_vacuous', plan: p.plan, task: g.task, command: g.command, detail: 'passes before any work, so it cannot show the task did anything; check for what the task adds, or mark it a regression guard' });
|
|
280
|
+
if (status === 'invalid') errors.push({ code: 'gate_invalid', plan: p.plan, task: g.task, command: g.command, detail: 'the pattern does not compile, so the gate can never match as written' });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return { phase: pp.pf.info.phase_number, ok: errors.length === 0, gates, errors };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function cmdVerifyGates(cwd, phase, raw) {
|
|
287
|
+
if (!phase) error('Usage: verify gates <phase>');
|
|
288
|
+
const r = checkGates(cwd, phase);
|
|
289
|
+
if (r.error) { output(r, raw); return; }
|
|
290
|
+
output(r, raw, r.ok ? 'valid' : 'invalid', r.ok ? 0 : 1);
|
|
291
|
+
}
|
|
292
|
+
|
|
147
293
|
function cmdVerifyReviewFocus(cwd, phase, raw) {
|
|
148
294
|
if (!phase) error('Usage: verify review-focus <phase>');
|
|
149
295
|
const r = checkReviewFocus(cwd, phase);
|
|
@@ -158,4 +304,7 @@ function cmdVerifyDecisions(cwd, phase, raw) {
|
|
|
158
304
|
output(r, raw, r.ok ? 'valid' : 'invalid', r.ok ? 0 : 1);
|
|
159
305
|
}
|
|
160
306
|
|
|
161
|
-
module.exports = {
|
|
307
|
+
module.exports = {
|
|
308
|
+
REVIEW_FOCUS_MAX, checkReviewFocus, checkDecisions, recordedDecisions, cmdVerifyReviewFocus, cmdVerifyDecisions,
|
|
309
|
+
parseGateCommand, checkGates, cmdVerifyGates,
|
|
310
|
+
};
|
|
@@ -371,7 +371,7 @@ function buildMilestoneETA(cwd) {
|
|
|
371
371
|
const remaining = phaseList.filter(p => !p.completed);
|
|
372
372
|
|
|
373
373
|
// Sample phase durations from summary frontmatter or commit metadata.
|
|
374
|
-
const durations = sampleCompletedPhaseDurations(cwd, completed);
|
|
374
|
+
const { durations, sources } = sampleCompletedPhaseDurations(cwd, completed, { withSources: true });
|
|
375
375
|
const avg = durations.length > 0
|
|
376
376
|
? durations.reduce((a, b) => a + b, 0) / durations.length
|
|
377
377
|
: 5.0; // default 5 days per phase when no history
|
|
@@ -408,6 +408,9 @@ function buildMilestoneETA(cwd) {
|
|
|
408
408
|
sample_size: durations.length,
|
|
409
409
|
eta_date: etaDate,
|
|
410
410
|
confidence_pct: confidence,
|
|
411
|
+
// L6b: where the average came from — dated summaries, the phases' plan commits, or
|
|
412
|
+
// the 5-day default when there is no history at all.
|
|
413
|
+
eta_source: durations.length === 0 ? 'default' : (sources.every((s) => s === 'git') ? 'git' : (sources.every((s) => s === 'summary') ? 'summary' : 'mixed')),
|
|
411
414
|
bottleneck,
|
|
412
415
|
};
|
|
413
416
|
}
|
|
@@ -420,8 +423,9 @@ function findPhaseDir(dirs, num) {
|
|
|
420
423
|
return dirs.find(d => d.startsWith(`${num}-`) || d.startsWith(`${padded}-`));
|
|
421
424
|
}
|
|
422
425
|
|
|
423
|
-
function sampleCompletedPhaseDurations(cwd, completedPhases) {
|
|
426
|
+
function sampleCompletedPhaseDurations(cwd, completedPhases, opts = {}) {
|
|
424
427
|
const durations = [];
|
|
428
|
+
const sources = [];
|
|
425
429
|
const phasesRoot = phasesPath(cwd);
|
|
426
430
|
let dirs = [];
|
|
427
431
|
try { dirs = fs.readdirSync(phasesRoot); } catch { return durations; }
|
|
@@ -441,10 +445,25 @@ function sampleCompletedPhaseDurations(cwd, completedPhases) {
|
|
|
441
445
|
const completed_at = fm.completed || fm.finished || fm.done;
|
|
442
446
|
if (started && completed_at) {
|
|
443
447
|
const d = daysBetween(started, completed_at);
|
|
444
|
-
if (d && d > 0 && d < 60) durations.push(d);
|
|
448
|
+
if (d && d > 0 && d < 60) { durations.push(d); sources.push('summary'); continue; }
|
|
445
449
|
}
|
|
450
|
+
// No dated summary (most have none): time the phase by its plan commits, oldest to newest.
|
|
451
|
+
const g = phaseCommitSpanDays(cwd, p.num);
|
|
452
|
+
if (g && g > 0 && g < 60) { durations.push(g); sources.push('git'); }
|
|
446
453
|
}
|
|
447
|
-
return durations;
|
|
454
|
+
return opts.withSources ? { durations, sources } : durations;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** Days between a phase's oldest and newest plan commit ({type}({phase}-{plan}): …), or null. */
|
|
458
|
+
function phaseCommitSpanDays(cwd, phaseNum) {
|
|
459
|
+
const n = String(phaseNum);
|
|
460
|
+
const padded = n.includes('.') ? n.split('.').map((s, i) => (i === 0 ? s.padStart(2, '0') : s)).join('.') : n.padStart(2, '0');
|
|
461
|
+
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
462
|
+
const r = require('./core.cjs').execGit(cwd, ['log', '--format=%ct', '-E', `--grep=^[a-z]+\\((${esc(padded)}|${esc(n)})-[0-9]+\\):`]);
|
|
463
|
+
if (!r || r.exitCode !== 0 || !r.stdout) return null;
|
|
464
|
+
const times = r.stdout.split('\n').map(Number).filter(Number.isFinite);
|
|
465
|
+
if (times.length < 2) return null;
|
|
466
|
+
return Math.round(((Math.max(...times) - Math.min(...times)) / 86400) * 10) / 10;
|
|
448
467
|
}
|
|
449
468
|
|
|
450
469
|
function findBottleneckCandidate(cwd, remainingPhases) {
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Requirements with EARS criteria, a requirements lint and a truth→test trace matrix
|
|
4
|
+
* (program V8).
|
|
5
|
+
*
|
|
6
|
+
* A v1 requirement (a checkbox line `- [ ] **ID**: …`) carries its acceptance criteria
|
|
7
|
+
* as indented sub-bullets in an EARS form, and the tests that check them as `Test:`
|
|
8
|
+
* sub-bullets:
|
|
9
|
+
*
|
|
10
|
+
* - [ ] **AUTH-01**: User can sign in
|
|
11
|
+
* - WHEN a user submits a correct email and password THE SYSTEM SHALL start a session
|
|
12
|
+
* - Test: tests/auth.test.js
|
|
13
|
+
*
|
|
14
|
+
* `verify requirements` fails a v1 requirement with no criterion (`req_no_scenario`).
|
|
15
|
+
* `verify traceability` maps each requirement to its tests and runs them: `passed`, `failing`,
|
|
16
|
+
* or `not_checked` when a truth has no test (an untested truth is never read as met).
|
|
17
|
+
* `phase complete` ticks a requirement that has criteria only when its trace passes; a
|
|
18
|
+
* legacy requirement with no criteria keeps the old tick, so existing projects work.
|
|
19
|
+
*/
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const { output, toLf } = require('./core.cjs');
|
|
23
|
+
const { planningPath } = require('./utils.cjs');
|
|
24
|
+
const { REQUIREMENTS_FILE } = require('./constants.cjs');
|
|
25
|
+
const TG = require('./test-gate.cjs');
|
|
26
|
+
|
|
27
|
+
// EARS forms: ubiquitous (THE <system> SHALL), event (WHEN … SHALL), state (WHILE …),
|
|
28
|
+
// optional feature (WHERE …), unwanted behaviour (IF … THEN … SHALL).
|
|
29
|
+
const EARS_RE = /^(?:(?:WHEN|WHILE|WHERE)\b[\s\S]*\bSHALL\b|IF\b[\s\S]*\bTHEN\b[\s\S]*\bSHALL\b|THE\s+\S[\s\S]*\bSHALL\b)/i;
|
|
30
|
+
const REQ_RE = /^- (\[[ xX]\] )?\*\*([A-Z][A-Z0-9]*-\d+)\*\*:?\s*(.*)$/;
|
|
31
|
+
const DEFAULT_TEST_TIMEOUT_MS = 3 * 60 * 1000;
|
|
32
|
+
|
|
33
|
+
const isEars = (s) => EARS_RE.test(String(s || '').trim());
|
|
34
|
+
|
|
35
|
+
/** The requirements in a requirements.md, with criteria and mapped tests. */
|
|
36
|
+
function parseRequirements(raw) {
|
|
37
|
+
const lines = toLf(String(raw || '')).split('\n');
|
|
38
|
+
const out = [];
|
|
39
|
+
let cur = null;
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
const m = REQ_RE.exec(line);
|
|
42
|
+
if (m) {
|
|
43
|
+
cur = { id: m[2], v1: !!m[1], checked: !!m[1] && /x/i.test(m[1]), text: m[3].trim(), criteria: [], tests: [] };
|
|
44
|
+
out.push(cur);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (!cur) continue;
|
|
48
|
+
const sub = /^\s{2,}- (.*)$/.exec(line);
|
|
49
|
+
if (!sub) { if (line.trim() !== '') cur = null; continue; }
|
|
50
|
+
const body = sub[1].trim();
|
|
51
|
+
const t = /^tests?:\s*(.+)$/i.exec(body);
|
|
52
|
+
if (t) { for (const p of t[1].split(',')) { const file = p.trim().replace(/`/g, '').split('#')[0].trim(); if (file) cur.tests.push(file); } continue; }
|
|
53
|
+
if (isEars(body)) cur.criteria.push(body);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readRequirements(cwd) {
|
|
59
|
+
try { return fs.readFileSync(path.join(planningPath(cwd), REQUIREMENTS_FILE), 'utf-8'); } catch { return null; }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function lintRequirements(cwd) {
|
|
63
|
+
const text = readRequirements(cwd);
|
|
64
|
+
if (text === null) return { error: 'requirements_not_found', reason: '.planning/requirements.md is missing' };
|
|
65
|
+
const reqs = parseRequirements(text).filter((r) => r.v1);
|
|
66
|
+
const errors = reqs.filter((r) => r.criteria.length === 0).map((r) => ({ code: 'req_no_scenario', id: r.id, detail: 'no acceptance criterion in an EARS form (WHEN/WHILE/WHERE/IF … THE SYSTEM SHALL …)' }));
|
|
67
|
+
return { ok: errors.length === 0, requirements: reqs.map((r) => ({ id: r.id, criteria: r.criteria.length, tests: r.tests.length })), errors };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Run a requirement's mapped tests: passed | failing | not_checked | legacy. Pure over the cache. */
|
|
71
|
+
function traceOf(cwd, req, cache, opts = {}) {
|
|
72
|
+
if (req.criteria.length === 0) return { id: req.id, status: 'legacy', tests: req.tests };
|
|
73
|
+
if (req.tests.length === 0) return { id: req.id, status: 'not_checked', tests: [], detail: 'no test is mapped to this requirement' };
|
|
74
|
+
const results = [];
|
|
75
|
+
for (const file of req.tests) {
|
|
76
|
+
if (!cache.has(file)) {
|
|
77
|
+
if (!fs.existsSync(path.join(cwd, file))) cache.set(file, { file, result: 'missing' });
|
|
78
|
+
else {
|
|
79
|
+
const fc = TG.fileCommand(cwd, file);
|
|
80
|
+
if (!fc.command) cache.set(file, { file, result: 'not_checked', detail: fc.reason });
|
|
81
|
+
else cache.set(file, { file, result: TG.runCommand(cwd, fc.command, opts.timeoutMs || DEFAULT_TEST_TIMEOUT_MS).exit === 0 ? 'passed' : 'failing' });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
results.push(cache.get(file));
|
|
85
|
+
}
|
|
86
|
+
const status = results.some((r) => r.result === 'failing' || r.result === 'missing') ? 'failing'
|
|
87
|
+
: results.every((r) => r.result === 'passed') ? 'passed' : 'not_checked';
|
|
88
|
+
return { id: req.id, status, tests: results };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function traceMatrix(cwd, opts = {}) {
|
|
92
|
+
const text = readRequirements(cwd);
|
|
93
|
+
if (text === null) return { error: 'requirements_not_found', reason: '.planning/requirements.md is missing' };
|
|
94
|
+
let reqs = parseRequirements(text).filter((r) => r.v1);
|
|
95
|
+
if (opts.ids && opts.ids.length) reqs = reqs.filter((r) => opts.ids.includes(r.id));
|
|
96
|
+
const cache = new Map();
|
|
97
|
+
const requirements = reqs.map((r) => traceOf(cwd, r, cache, opts));
|
|
98
|
+
const count = (s) => requirements.filter((r) => r.status === s).length;
|
|
99
|
+
return { requirements, passed: count('passed'), failing: count('failing'), not_checked: count('not_checked'), legacy: count('legacy') };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Whether `phase complete` may tick each id. Legacy requirements may; the rest need a passing trace. */
|
|
103
|
+
function tickDecisions(cwd, ids) {
|
|
104
|
+
const text = readRequirements(cwd);
|
|
105
|
+
if (text === null) return new Map(ids.map((id) => [id, { allowed: true, status: 'unknown' }]));
|
|
106
|
+
const byId = new Map(parseRequirements(text).map((r) => [r.id, r]));
|
|
107
|
+
const cache = new Map();
|
|
108
|
+
const out = new Map();
|
|
109
|
+
for (const id of ids) {
|
|
110
|
+
const r = byId.get(id);
|
|
111
|
+
if (!r) { out.set(id, { allowed: true, status: 'unknown' }); continue; }
|
|
112
|
+
const t = traceOf(cwd, r, cache);
|
|
113
|
+
out.set(id, { allowed: t.status === 'legacy' || t.status === 'passed', status: t.status });
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function cmdVerifyRequirements(cwd, raw) {
|
|
119
|
+
const r = lintRequirements(cwd);
|
|
120
|
+
if (r.error) { output(r, raw, 'error', 1); return; }
|
|
121
|
+
output(r, raw, r.ok ? 'valid' : 'invalid', r.ok ? 0 : 1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function cmdVerifyTrace(cwd, ids, raw) {
|
|
125
|
+
const r = traceMatrix(cwd, { ids });
|
|
126
|
+
if (r.error) { output(r, raw, 'error', 1); return; }
|
|
127
|
+
output(r, raw, r.failing ? 'failing' : (r.not_checked ? 'not_checked' : 'passed'), r.failing ? 1 : 0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = { isEars, parseRequirements, lintRequirements, traceMatrix, tickDecisions, cmdVerifyRequirements, cmdVerifyTrace };
|