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,356 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Right-sized ceremony: decide in code how much process a piece of work gets.
|
|
4
|
+
*
|
|
5
|
+
* Measured 2026-10-24 (docs/specs/right-sized-ceremony.md): on precisely specified work
|
|
6
|
+
* PAN spent about 70% of its cost and time on planning ceremony (research, a planner and
|
|
7
|
+
* a plan-checker revising each other three or four times), all of it on the launch model,
|
|
8
|
+
* against a bare host that finished the same tasks correctly for about a thirtieth of the
|
|
9
|
+
* cost. A small, well-specified piece of work does not need that: the spec is the plan.
|
|
10
|
+
*
|
|
11
|
+
* lean the executor works from the spec; the engine's deterministic plan checks and
|
|
12
|
+
* test gate check it; an LLM checker or verifier runs only when those cannot decide
|
|
13
|
+
* full today's path: research, planner, plan-checker loop, verifier, reviewers
|
|
14
|
+
*
|
|
15
|
+
* The mode: `full` everywhere when the profile is `quality` (the old behaviour, chosen at
|
|
16
|
+
* new-project or settings), when `workflow.ceremony` is `full`, or when a run passes
|
|
17
|
+
* `--quality`; otherwise `auto`, which sizes each piece of work. Pure apart from reads.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const { loadConfig } = require('./core.cjs');
|
|
22
|
+
const { roadmapRequirementIds } = require('./roadmap.cjs');
|
|
23
|
+
const { ROADMAP_FILE, REQUIREMENTS_FILE } = require('./constants.cjs');
|
|
24
|
+
const { planningPath } = require('./utils.cjs');
|
|
25
|
+
|
|
26
|
+
const DEFAULT_LEAN_MAX_REQUIREMENTS = 5;
|
|
27
|
+
/** A lean phase holds at most this many times lean_max_requirements; above it the planner runs. */
|
|
28
|
+
const LEAN_CEILING_FACTOR = 3;
|
|
29
|
+
const isValidationRule = (c) => require('./verify-plan-set.cjs').isValidationRule(c);
|
|
30
|
+
|
|
31
|
+
// loadConfig flattens the workflow section away; the ceremony keys are read from the raw file.
|
|
32
|
+
function ceremonyConfig(cwd) {
|
|
33
|
+
const workflow = require('./test-gate.cjs').rawConfig(cwd).workflow || {};
|
|
34
|
+
return { ...loadConfig(cwd), workflow };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 'full' or 'auto' for this project and run. `config`: ceremonyConfig's. */
|
|
38
|
+
function ceremonyMode(config, opts = {}) {
|
|
39
|
+
if (opts.quality) return { mode: 'full', why: '--quality was passed for this run' };
|
|
40
|
+
const wf = (config && config.workflow) || {};
|
|
41
|
+
if (wf.ceremony === 'full') return { mode: 'full', why: 'workflow.ceremony is full' };
|
|
42
|
+
if (config && config.model_profile === 'quality') return { mode: 'full', why: 'the quality profile keeps the full ceremony' };
|
|
43
|
+
return { mode: 'auto', why: null };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const words = (t) => String(t || '').trim().split(/\s+/).filter(Boolean).length;
|
|
47
|
+
// A concrete artifact: a backticked token, a file with an extension, or a call `name(`.
|
|
48
|
+
const ARTIFACT = /`[^`\n]+`|\b[\w./-]+\.(?:c?js|mjs|tsx?|jsx|py|json|md|go|rs|java|rb|cs|php|sh|ya?ml|toml|sql|html|css)\b|\b[a-zA-Z_][\w]*\([^)\n]*\)/;
|
|
49
|
+
|
|
50
|
+
/** Whether a requirement says precisely what to build. Pure. */
|
|
51
|
+
function requirementSpecified(req) {
|
|
52
|
+
if (req && Array.isArray(req.criteria) && req.criteria.length) return true;
|
|
53
|
+
const t = String((req && req.text) || '');
|
|
54
|
+
return ARTIFACT.test(t) && words(t) >= 12;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Whether a quick task's description says precisely what to build. Pure. */
|
|
58
|
+
function taskSpecified(text) {
|
|
59
|
+
const t = String(text || '');
|
|
60
|
+
const reasons = [];
|
|
61
|
+
if (!ARTIFACT.test(t)) reasons.push('names no file, function or other concrete artifact');
|
|
62
|
+
if (words(t) < 15) reasons.push('is too short to specify the work (under 15 words)');
|
|
63
|
+
return { specified: reasons.length === 0, reasons };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The ceremony for a quick task. */
|
|
67
|
+
function sizeTask(cwd, text, opts = {}) {
|
|
68
|
+
const config = ceremonyConfig(cwd);
|
|
69
|
+
const m = ceremonyMode(config, opts);
|
|
70
|
+
if (m.mode === 'full') return { path: 'full', mode: m.mode, reasons: [m.why] };
|
|
71
|
+
if (opts.full) return { path: 'full', mode: m.mode, reasons: ['--full was passed'] };
|
|
72
|
+
const s = taskSpecified(text);
|
|
73
|
+
return s.specified
|
|
74
|
+
? { path: 'lean', mode: m.mode, reasons: ['the description names what to build and how to check it'] }
|
|
75
|
+
: { path: 'full', mode: m.mode, reasons: s.reasons.map((r) => `the description ${r}`) };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The ceremony for a roadmap phase. */
|
|
79
|
+
function sizePhase(cwd, phase, opts = {}) {
|
|
80
|
+
const config = ceremonyConfig(cwd);
|
|
81
|
+
const m = ceremonyMode(config, opts);
|
|
82
|
+
const max = Number(((config && config.workflow) || {}).lean_max_requirements) || DEFAULT_LEAN_MAX_REQUIREMENTS;
|
|
83
|
+
let roadmap = '';
|
|
84
|
+
let reqText = '';
|
|
85
|
+
try { roadmap = fs.readFileSync(planningPath(cwd, ROADMAP_FILE), 'utf8'); } catch { /* no roadmap */ }
|
|
86
|
+
try { reqText = fs.readFileSync(planningPath(cwd, REQUIREMENTS_FILE), 'utf8'); } catch { /* no requirements */ }
|
|
87
|
+
const ids = roadmapRequirementIds(roadmap, String(phase));
|
|
88
|
+
const all = require('./requirements-lint.cjs').parseRequirements(reqText);
|
|
89
|
+
const byId = new Map(all.map((r) => [String(r.id).toUpperCase(), r]));
|
|
90
|
+
const reqs = ids.map((id) => byId.get(String(id).toUpperCase()) || { id, text: '', criteria: [] });
|
|
91
|
+
const unspecified = reqs.filter((r) => !requirementSpecified(r)).map((r) => r.id);
|
|
92
|
+
const metrics = { requirements: reqs.length, unspecified, lean_max_requirements: max };
|
|
93
|
+
if (m.mode === 'full') return { phase: String(phase), path: 'full', mode: m.mode, reasons: [m.why], metrics };
|
|
94
|
+
if (opts.full) return { phase: String(phase), path: 'full', mode: m.mode, reasons: ['--full was passed'], metrics };
|
|
95
|
+
const reasons = [];
|
|
96
|
+
if (!reqs.length) reasons.push('the roadmap maps no requirements to this phase, so nothing pins down the work');
|
|
97
|
+
if (unspecified.length) reasons.push(`not precisely specified: ${unspecified.join(', ')} (no acceptance criterion and no named artifact)`);
|
|
98
|
+
// One session holds a lean phase, and one session loses the thread on too much work, which
|
|
99
|
+
// is what PAN exists to prevent. Above the ceiling the planner designs the phase (2026-10-25).
|
|
100
|
+
if (reqs.length > max * LEAN_CEILING_FACTOR) reasons.push(`${reqs.length} requirements is more than one session should hold (over ${max * LEAN_CEILING_FACTOR}, ${LEAN_CEILING_FACTOR} x workflow.lean_max_requirements); the planner designs it`);
|
|
101
|
+
return reasons.length
|
|
102
|
+
? { phase: String(phase), path: 'full', mode: m.mode, reasons, metrics }
|
|
103
|
+
: { phase: String(phase), path: 'lean', mode: m.mode, reasons: [`${reqs.length} requirement(s), each naming what to build${reqs.length > max ? `; split into plans of at most ${max} (workflow.lean_max_requirements)` : ''}`], metrics };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The lean path's plan check, in code: the plan set (requirement coverage, waves,
|
|
108
|
+
* dependencies), every plan's structure, and the plans' own grep gates. When all pass it
|
|
109
|
+
* records a plan-check verdict from the engine, bound to the plans' hash like the
|
|
110
|
+
* plan-checker agent's, so exec-phase's plan-check gate holds. When any fails, nothing is
|
|
111
|
+
* recorded and the workflow falls back to the plan-checker agent.
|
|
112
|
+
*/
|
|
113
|
+
function engineCheckPlans(cwd, phase) {
|
|
114
|
+
const VPS = require('./verify-plan-set.cjs');
|
|
115
|
+
const set = VPS.checkPlanSet(cwd, phase);
|
|
116
|
+
if (set.error) return { ok: false, error: set.error, reason: set.reason };
|
|
117
|
+
const pf = VPS.phaseFiles(cwd, phase);
|
|
118
|
+
const { planStructure } = require('./verify.cjs');
|
|
119
|
+
const path = require('path');
|
|
120
|
+
const structure = pf.planFiles.map((f) => ({ plan: f, ...planStructure(fs.readFileSync(path.join(pf.dir, f), 'utf8')) }));
|
|
121
|
+
const gates = require('./plan-lint.cjs').checkGates(cwd, phase);
|
|
122
|
+
const errors = [
|
|
123
|
+
...(set.errors || []).map((e) => (typeof e === 'string' ? e : e.message || e.code || JSON.stringify(e))),
|
|
124
|
+
...structure.flatMap((s) => s.errors.map((e) => `${s.plan}: ${e}`)),
|
|
125
|
+
...((gates && gates.errors) || []).map((e) => (typeof e === 'string' ? e : `${e.plan || ''} ${e.code || ''} ${e.command || ''}`.trim())),
|
|
126
|
+
];
|
|
127
|
+
return { ok: errors.length === 0, errors, plans: pf.planFiles.length };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Record the engine's passing plan check as the phase's plan-check verdict. */
|
|
131
|
+
function recordEngineCheck(cwd, phase, result) {
|
|
132
|
+
const F = require('./findings.cjs');
|
|
133
|
+
const block = '```pan-verdict\n' + JSON.stringify({ contract: '1.0', agent: 'pan-plan-checker', phase: String(phase), outcome: 'pass', verdict: 'PASSED', findings: [] }) + '\n```\n';
|
|
134
|
+
// The plans' hash makes each checked plan set its own record: an identical text is a
|
|
135
|
+
// duplicate verdict, so a re-check after a revision would never be recorded.
|
|
136
|
+
const sha = require('./verify-plan-set.cjs').plansShaFor(cwd, phase);
|
|
137
|
+
const text = `Plan check by the engine (lean path): plan set, plan structure and grep gates all pass for ${result.plans} plan(s) (plans ${sha}).\n\n${block}`;
|
|
138
|
+
return F.recordVerdict(cwd, { phase: String(phase), agent: 'pan-plan-checker', text, source: 'engine:lean-plan-check' });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The one-task plan for a lean quick task, written by the engine instead of a planner
|
|
143
|
+
* session: the description is the plan. Files are the ones the description names; the
|
|
144
|
+
* verify command is the project's test command. Pure apart from the test-command read.
|
|
145
|
+
*/
|
|
146
|
+
function leanQuickPlan(cwd, description, num) {
|
|
147
|
+
const desc = String(description || '').trim();
|
|
148
|
+
const files = [...new Set((desc.match(/\b[\w./-]+\.(?:c?js|mjs|tsx?|jsx|py|json|md|go|rs|java|rb|cs|php|sh|ya?ml|toml|sql|html|css)\b/g) || []))].filter((f) => !/^\d/.test(f));
|
|
149
|
+
const test = require('./test-gate.cjs').detectTestCommand(cwd);
|
|
150
|
+
const verify = (test && test.command) || 'the project\'s tests';
|
|
151
|
+
const esc = (t) => t.replace(/</g, '<');
|
|
152
|
+
return [
|
|
153
|
+
'---', 'phase: quick', `plan: ${String(num || 1).padStart(2, '0')}`, 'type: execute', 'wave: 1', 'depends_on: []',
|
|
154
|
+
`files_modified: [${files.join(', ')}]`, 'autonomous: true', 'tier: micro', 'must_haves:', ' truths:', ` - ${JSON.stringify(desc.slice(0, 300))}`,
|
|
155
|
+
'---', '', '<objective>', esc(desc), '', 'Written by the engine (lean path): the task description is precise enough to be the plan.', '</objective>', '',
|
|
156
|
+
'<tasks>', '', '<task type="auto">', ` <name>${esc(desc.split(/\.\s|\n/)[0].slice(0, 80))}</name>`, ` <files>${files.join(', ') || 'the files the description names'}</files>`,
|
|
157
|
+
` <action>Do exactly what the objective says, with tests for each rule it states. The objective is the specification: do not widen it.</action>`,
|
|
158
|
+
` <verify>${verify}</verify>`, ' <done>Every rule in the objective holds and is covered by a test; the tests pass.</done>', '</task>', '', '</tasks>', '',
|
|
159
|
+
].join('\n');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const FILE_RE = /\b[\w./-]+\.(?:c?js|mjs|tsx?|jsx|py|json|md|go|rs|java|rb|cs|php|sh|ya?ml|toml|sql|html|css)\b/g;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Write a lean phase's plans from its requirements (autodev step 2): one task per
|
|
166
|
+
* requirement, at most lean_max_requirements tasks a plan, each task's action the requirement's own text. The requirements already say
|
|
167
|
+
* what to build, so a planner session would only restate them. Refuses a phase that is not
|
|
168
|
+
* lean, or that already has plans. Creates the phase directory when it is missing.
|
|
169
|
+
* @returns {{written, path, plan_dir, requirements}|{error, reason}}
|
|
170
|
+
*/
|
|
171
|
+
function writeLeanPhasePlan(cwd, phase, opts = {}) {
|
|
172
|
+
const path = require('path');
|
|
173
|
+
const C = require('./core.cjs');
|
|
174
|
+
const size = sizePhase(cwd, phase, opts);
|
|
175
|
+
if (size.path !== 'lean') return { error: 'not_lean', reason: size.reasons.join('; ') };
|
|
176
|
+
const rp = C.getRoadmapPhaseInternal(cwd, String(phase));
|
|
177
|
+
if (!rp || !rp.found) return { error: 'phase_not_found', reason: `phase ${phase} is not in roadmap.md` };
|
|
178
|
+
const padded = C.normalizePhaseName(String(phase));
|
|
179
|
+
let info = C.findPhaseInternal(cwd, String(phase));
|
|
180
|
+
let dir;
|
|
181
|
+
let createdDir = false;
|
|
182
|
+
if (info && info.found) dir = path.join(cwd, info.directory);
|
|
183
|
+
else {
|
|
184
|
+
createdDir = true;
|
|
185
|
+
dir = path.join(planningPath(cwd, 'phases'), `${padded}-${C.generateSlugInternal(rp.phase_name || 'phase')}`);
|
|
186
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
187
|
+
}
|
|
188
|
+
if (fs.readdirSync(dir).some((f) => /-plan\.md$/i.test(f))) return { error: 'has_plans', reason: `phase ${phase} already has plans` };
|
|
189
|
+
let reqText = '';
|
|
190
|
+
try { reqText = fs.readFileSync(planningPath(cwd, REQUIREMENTS_FILE), 'utf8'); } catch { /* sized lean, so present */ }
|
|
191
|
+
const all = require('./requirements-lint.cjs').parseRequirements(reqText);
|
|
192
|
+
const ids = roadmapRequirementIds(fs.readFileSync(planningPath(cwd, ROADMAP_FILE), 'utf8'), String(phase));
|
|
193
|
+
const reqs = ids.map((id) => all.find((r) => String(r.id).toUpperCase() === String(id).toUpperCase()) || { id, text: '', criteria: [] });
|
|
194
|
+
const one = (t) => String(t || '').replace(/\s+/g, ' ').trim().replace(/^[—–:-]+\s*/, '');
|
|
195
|
+
const filesOf = (t) => [...new Set((String(t).match(FILE_RE) || []).filter((f) => !/^\d/.test(f)))];
|
|
196
|
+
const files = [...new Set(reqs.flatMap((r) => filesOf(r.text)))];
|
|
197
|
+
// A requirement that changes behaviour earlier phases built (a migration, a new format, a
|
|
198
|
+
// rename) supersedes the tests that pin the old behaviour. Declare them, so the executor
|
|
199
|
+
// updates their expectations with a recorded deviation instead of stopping on a "spec
|
|
200
|
+
// conflict" (2026-10-24: autodev --quality halted at a format-change phase on its own
|
|
201
|
+
// earlier tests).
|
|
202
|
+
const changing = reqs.filter((r) => CHANGE_RE.test(String(r.text || '')));
|
|
203
|
+
// A changing requirement often names the function that holds the old behaviour (`openLedger`)
|
|
204
|
+
// and not its file, so its tests are looked for there too (2026-10-25: a live phase 5 stopped
|
|
205
|
+
// on tests/ledger.test.js, which no requirement's named file reached).
|
|
206
|
+
const superseded = changing.length ? testsCovering(cwd, [...new Set([...files, ...definingFiles(cwd, changing.flatMap((r) => identifiersOf(r.text)))])]) : [];
|
|
207
|
+
const declared = [...new Set([...files, ...superseded])];
|
|
208
|
+
const test = require('./test-gate.cjs').detectTestCommand(cwd);
|
|
209
|
+
const verify = (test && test.command) || 'the project\'s tests';
|
|
210
|
+
const head = C.execGit(cwd, ['rev-parse', 'HEAD']);
|
|
211
|
+
const slug = path.basename(dir).replace(/^\d+(?:\.\d+)*-/, '');
|
|
212
|
+
const esc = (t) => t.replace(/</g, '<');
|
|
213
|
+
const tasks = reqs.map((r) => {
|
|
214
|
+
const own = filesOf(r.text);
|
|
215
|
+
const criteria = (r.criteria || []).map((c) => `\n - ${esc(one(c))}`).join('');
|
|
216
|
+
const supersedes = superseded.length && changing.includes(r)
|
|
217
|
+
? ` This requirement changes behaviour earlier phases built: where ${superseded.join(', ')} assert the old behaviour, update those expectations to what ${r.id} requires and record each change as a deviation in the summary. Do not delete, skip or weaken a test.`
|
|
218
|
+
: '';
|
|
219
|
+
const taskFiles = [...new Set([...(own.length ? own : files), ...(supersedes ? superseded : [])])];
|
|
220
|
+
// A requirement that states a validation rule (IF ... THEN, a claim over every input, a
|
|
221
|
+
// "never") needs a property-test task, as the plan check requires (V10): one example
|
|
222
|
+
// test cannot show a rule over many inputs (2026-10-25: the engine's plan failed its own
|
|
223
|
+
// check here and the phase never ran).
|
|
224
|
+
const rule = (r.criteria || []).some(isValidationRule);
|
|
225
|
+
const property = rule ? ' Add a property-based test that checks the rule over generated inputs, with the project\'s own property helper or a seeded loop and no new dependency.' : '';
|
|
226
|
+
const text = [rule ? '<task type="auto" property="true">' : '<task type="auto">', ` <name>${r.id}: ${esc(one(r.text).slice(0, 70))}</name>`, ` <files>${taskFiles.join(', ') || 'the files the requirement names'}</files>`,
|
|
227
|
+
` <action>Implement ${r.id} exactly as requirements.md states it, with a test for each rule in it: ${esc(one(r.text))}${criteria}${supersedes}${property}</action>`,
|
|
228
|
+
` <verify>${verify}</verify>`, ` <done>${r.id} holds and a test covers each rule in it; the tests pass.</done>`, '</task>'].join('\n');
|
|
229
|
+
return { r, text, files: taskFiles, supersedes: Boolean(supersedes) };
|
|
230
|
+
});
|
|
231
|
+
// A large phase of precise requirements is split into plans of at most
|
|
232
|
+
// lean_max_requirements tasks, each in its own wave after the one before, so the one
|
|
233
|
+
// execute session takes them in order and commits a summary between them (2026-10-25:
|
|
234
|
+
// the brief's 9- and 13-requirement phases went to the full ceremony on their size alone,
|
|
235
|
+
// a planner and a plan-checker twice each, about $3 and 13 minutes a phase).
|
|
236
|
+
const per = Math.max(1, Number(size.metrics.lean_max_requirements) || DEFAULT_LEAN_MAX_REQUIREMENTS);
|
|
237
|
+
const chunks = [];
|
|
238
|
+
for (let i = 0; i < tasks.length; i += per) chunks.push(tasks.slice(i, i + per));
|
|
239
|
+
const written = [];
|
|
240
|
+
chunks.forEach((chunk, i) => {
|
|
241
|
+
const nn = String(i + 1).padStart(2, '0');
|
|
242
|
+
const ids = chunk.map((t) => t.r.id);
|
|
243
|
+
const own = [...new Set(chunk.flatMap((t) => t.files))];
|
|
244
|
+
const sup = chunk.some((t) => t.supersedes);
|
|
245
|
+
const text = [
|
|
246
|
+
'---', `phase: ${padded}-${slug}`, `plan: ${nn}`, 'type: execute', `wave: ${i + 1}`, `depends_on: [${i ? `${padded}-${String(i).padStart(2, '0')}` : ''}]`, `files_modified: [${(own.length ? own : declared).join(', ')}]`, 'autonomous: true',
|
|
247
|
+
`requirements: [${ids.join(', ')}]`, ...(head.exitCode === 0 ? [`planned_commit: ${head.stdout.trim()}`] : []),
|
|
248
|
+
...(sup ? [`supersedes_tests: [${superseded.join(', ')}]`, `supersedes_requirements: [${chunk.filter((t) => t.supersedes).map((t) => t.r.id).join(', ')}]`] : []),
|
|
249
|
+
'must_haves:', ' truths:', ...chunk.map((t) => ` - ${JSON.stringify(`${t.r.id}: ${one(t.r.text).slice(0, 240)}`)}`),
|
|
250
|
+
'---', '', '<objective>', esc(one(rp.goal || '')), '', `Written by the engine (lean path): the phase's requirements say precisely what to build, so each is one task${chunks.length > 1 ? `; this is plan ${i + 1} of ${chunks.length} (${ids.join(', ')})` : ''}. The requirements are the specification: do not widen them.`, '</objective>', '',
|
|
251
|
+
'<tasks>', '', chunk.map((t) => t.text).join('\n\n'), '', '</tasks>', '',
|
|
252
|
+
].join('\n');
|
|
253
|
+
const file = path.join(dir, `${padded}-${nn}-plan.md`);
|
|
254
|
+
fs.writeFileSync(file, text);
|
|
255
|
+
written.push(file);
|
|
256
|
+
});
|
|
257
|
+
const rels = written.map((f) => C.toPosix(path.relative(cwd, f)));
|
|
258
|
+
return { written: true, path: rels[0], paths: rels, abs: written[0], abs_all: written, created_dir: createdDir ? dir : null, plan_dir: C.toPosix(path.relative(cwd, dir)), requirements: reqs.map((r) => r.id), ...(superseded.length ? { supersedes_tests: superseded } : {}) };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Wording that marks a requirement as changing behaviour already built.
|
|
262
|
+
const CHANGE_RE = /\b(migrat\w*|format change|new format|version \d+|v\d+ format|replac\w*|instead of|no longer|renam\w*|deprecat\w*|breaking)\b/i;
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* The existing test files that exercise any of `files`: a test file under the project
|
|
266
|
+
* (outside node_modules, .git and the planning tree) whose text names one of the files'
|
|
267
|
+
* module names. Pure apart from reads; sorted.
|
|
268
|
+
*/
|
|
269
|
+
/** The code identifiers a requirement names in backticks (`openLedger`, `importCSV`). */
|
|
270
|
+
function identifiersOf(text) {
|
|
271
|
+
return [...new Set([...String(text || '').matchAll(/`([A-Za-z_$][\w$]{3,})`/g)].map((m) => m[1]))];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** The project's source files (not tests) that define any of `names` as a function, method or binding. */
|
|
275
|
+
function definingFiles(cwd, names) {
|
|
276
|
+
const path = require('path');
|
|
277
|
+
if (!names.length) return [];
|
|
278
|
+
const esc = (n) => n.replace(/[$]/g, '\\$&');
|
|
279
|
+
const def = new RegExp(`(?:function\\s*\\*?\\s*|def\\s+|func\\s+|fn\\s+|(?:const|let|var)\\s+)(?:${names.map(esc).join('|')})\\b|\\b(?:${names.map(esc).join('|')})\\s*(?:[:=]\\s*(?:async\\s*)?(?:function|\\()|\\([^)]*\\)\\s*\\{)`);
|
|
280
|
+
const isTest = (rel) => /(^|\/)(tests?|__tests__|spec)\//.test(rel) || /\.(test|spec)\.[^.]+$/.test(rel);
|
|
281
|
+
const out = [];
|
|
282
|
+
const walk = (dir, depth) => {
|
|
283
|
+
if (depth > 6) return;
|
|
284
|
+
let entries = [];
|
|
285
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
286
|
+
for (const e of entries) {
|
|
287
|
+
if (['node_modules', '.git', '.planning', '.claude', '.codex', '.gemini', '.opencode', '.github', '.agents', 'dist', 'coverage'].includes(e.name)) continue;
|
|
288
|
+
const abs = path.join(dir, e.name);
|
|
289
|
+
if (e.isDirectory()) { walk(abs, depth + 1); continue; }
|
|
290
|
+
const rel = path.relative(cwd, abs).split(path.sep).join('/');
|
|
291
|
+
if (isTest(rel) || !/\.(c?js|mjs|tsx?|jsx|py|go|rs|java|rb|cs)$/.test(rel)) continue;
|
|
292
|
+
let text = '';
|
|
293
|
+
try { text = fs.readFileSync(abs, 'utf8'); } catch { continue; }
|
|
294
|
+
if (def.test(text)) out.push(rel);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
walk(cwd, 0);
|
|
298
|
+
return out.sort();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function testsCovering(cwd, files) {
|
|
302
|
+
const path = require('path');
|
|
303
|
+
const names = [...new Set(files.map((f) => path.basename(f).replace(/\.[^.]+$/, '')).filter((n) => n.length > 2))];
|
|
304
|
+
if (!names.length) return [];
|
|
305
|
+
const isTest = (rel) => /(^|\/)(tests?|__tests__|spec)\//.test(rel) || /\.(test|spec)\.[^.]+$/.test(rel);
|
|
306
|
+
const out = [];
|
|
307
|
+
const walk = (dir, depth) => {
|
|
308
|
+
if (depth > 6) return;
|
|
309
|
+
let entries = [];
|
|
310
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
311
|
+
for (const e of entries) {
|
|
312
|
+
if (['node_modules', '.git', '.planning', '.claude', '.codex', '.gemini', '.opencode', '.github', '.agents', 'dist', 'coverage'].includes(e.name)) continue;
|
|
313
|
+
const abs = path.join(dir, e.name);
|
|
314
|
+
if (e.isDirectory()) { walk(abs, depth + 1); continue; }
|
|
315
|
+
const rel = path.relative(cwd, abs).split(path.sep).join('/');
|
|
316
|
+
if (!isTest(rel) || !/\.(c?js|mjs|tsx?|jsx|py|go|rs|java|rb|cs)$/.test(rel) || files.includes(rel)) continue;
|
|
317
|
+
let text = '';
|
|
318
|
+
try { text = fs.readFileSync(abs, 'utf8'); } catch { continue; }
|
|
319
|
+
if (names.some((n) => new RegExp(`\\b${n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(text))) out.push(rel);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
walk(cwd, 0);
|
|
323
|
+
return out.sort();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* The lean phase's whole planning step, in code: write the plan, check it, record the plan
|
|
328
|
+
* check, and commit the plan when commit_docs is on. No model session.
|
|
329
|
+
*/
|
|
330
|
+
function planLeanPhase(cwd, phase, opts = {}) {
|
|
331
|
+
const w = writeLeanPhasePlan(cwd, phase, opts);
|
|
332
|
+
if (w.error) return w;
|
|
333
|
+
const check = engineCheckPlans(cwd, phase);
|
|
334
|
+
if (!check.ok) {
|
|
335
|
+
// Decline writes nothing: a failed plan left on disk was picked up by the next session.
|
|
336
|
+
for (const f of w.abs_all) { try { fs.unlinkSync(f); } catch { /* already gone */ } }
|
|
337
|
+
if (w.created_dir) { try { fs.rmdirSync(w.created_dir); } catch { /* not empty */ } }
|
|
338
|
+
return { phase: String(phase), ok: false, error: 'check_failed', errors: check.errors };
|
|
339
|
+
}
|
|
340
|
+
const rec = recordEngineCheck(cwd, phase, check);
|
|
341
|
+
if (rec.error) return { ...w, ok: false, errors: [rec.reason || rec.error] };
|
|
342
|
+
const config = loadConfig(cwd);
|
|
343
|
+
let commit = null;
|
|
344
|
+
if (config.commit_docs !== false) {
|
|
345
|
+
const C = require('./core.cjs');
|
|
346
|
+
const add = C.execGit(cwd, ['add', '--', ...w.paths]);
|
|
347
|
+
const msg = `docs(${C.normalizePhaseName(String(phase))}): lean plan written by the engine`;
|
|
348
|
+
const c = add.exitCode === 0 ? C.execGit(cwd, ['commit', '-q', '-m', msg, '--', ...w.paths]) : add;
|
|
349
|
+
commit = c.exitCode === 0 ? (C.execGit(cwd, ['rev-parse', '--short', 'HEAD']).stdout || '').trim() : null;
|
|
350
|
+
// The plan stands without its commit (exec-phase commits as it goes); say why it is missing.
|
|
351
|
+
if (!commit) return { ...w, ok: true, plan_check: 'recorded', commit: null, commit_error: String(c.stderr || c.stdout || 'git commit failed').trim().slice(0, 300) };
|
|
352
|
+
}
|
|
353
|
+
return { ...w, ok: true, plan_check: 'recorded', commit };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
module.exports = { LEAN_CEILING_FACTOR, writeLeanPhasePlan, planLeanPhase, leanQuickPlan, ceremonyMode, requirementSpecified, taskSpecified, sizeTask, sizePhase, engineCheckPlans, recordEngineCheck, DEFAULT_LEAN_MAX_REQUIREMENTS };
|
|
@@ -73,7 +73,7 @@ const SQUADS = Object.freeze({
|
|
|
73
73
|
summary: 'Designs the system before code — contract-first.',
|
|
74
74
|
agents: Object.freeze([
|
|
75
75
|
'pan-roadmapper', 'pan-planner', 'pan-designer', 'pan-plan-checker', 'pan-design-checker',
|
|
76
|
-
'pan-project-researcher', 'pan-phase-researcher',
|
|
76
|
+
'pan-project-researcher', 'pan-phase-researcher',
|
|
77
77
|
]),
|
|
78
78
|
}),
|
|
79
79
|
build: Object.freeze({
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* The verbs that write state.md wholesale or by section (program A7b).
|
|
4
|
+
*
|
|
5
|
+
* Agents used to Write or Edit state.md themselves: the roadmapper and new-project
|
|
6
|
+
* composed it, milestone-new reset it, add-phase and insert-phase appended to it, quick
|
|
7
|
+
* added rows, transition batched five edits into one. Every write now goes through
|
|
8
|
+
* pan-tools and writeStateMd, which keeps the frontmatter in sync and writes atomically
|
|
9
|
+
* under the lock, so the pre-tool guard's `enforce` can deny a direct write.
|
|
10
|
+
*
|
|
11
|
+
* state init a fresh state.md from the template's sections
|
|
12
|
+
* state section append to or set one section, creating it when missing
|
|
13
|
+
* state quick-task one row in the Quick Tasks Completed table
|
|
14
|
+
* state apply bold fields and sections in one write (transition's batch)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { output, escapeRegex } = require('./core.cjs');
|
|
20
|
+
const { planningPath } = require('./utils.cjs');
|
|
21
|
+
const { writeStateMd } = require('./state.cjs');
|
|
22
|
+
|
|
23
|
+
const statePathOf = (cwd) => planningPath(cwd, 'state.md');
|
|
24
|
+
const today = () => new Date().toISOString().slice(0, 10);
|
|
25
|
+
|
|
26
|
+
function readBody(cwd) {
|
|
27
|
+
try { return fs.readFileSync(statePathOf(cwd), 'utf8').replace(/\r\n/g, '\n'); } catch { return null; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The body without its frontmatter (writeStateMd rebuilds the frontmatter). */
|
|
31
|
+
function stripFrontmatter(text) {
|
|
32
|
+
return text.replace(/^---\n[\s\S]*?\n---\n+/, '');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** [start, end) of a section's body: the lines after its heading up to the next heading of its level or higher. */
|
|
36
|
+
function findSection(text, name) {
|
|
37
|
+
const lines = text.split('\n');
|
|
38
|
+
let offset = 0;
|
|
39
|
+
let start = -1;
|
|
40
|
+
let level = 0;
|
|
41
|
+
let bodyStart = 0;
|
|
42
|
+
for (let i = 0; i < lines.length; i++) {
|
|
43
|
+
const m = lines[i].match(/^(#{2,3}) (.*?)[ \t]*$/);
|
|
44
|
+
if (start < 0 && m && m[2] === name) { start = offset; level = m[1].length; bodyStart = offset + lines[i].length + 1; }
|
|
45
|
+
else if (start >= 0) {
|
|
46
|
+
const h = lines[i].match(/^(#+) /);
|
|
47
|
+
if (h && h[1].length <= level) return { level, headingStart: start, bodyStart, bodyEnd: offset };
|
|
48
|
+
}
|
|
49
|
+
offset += lines[i].length + 1;
|
|
50
|
+
}
|
|
51
|
+
return start < 0 ? null : { level, headingStart: start, bodyStart: Math.min(bodyStart, text.length), bodyEnd: text.length };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const PLACEHOLDER_RE = /^(None yet\.|None\.?|\[.*\])\s*$/;
|
|
55
|
+
|
|
56
|
+
function editSection(text, name, op, under) {
|
|
57
|
+
under = under || 'Accumulated Context';
|
|
58
|
+
// The CLI's absent flags arrive as null.
|
|
59
|
+
op = { append: op.append == null ? undefined : op.append, set: op.set == null ? undefined : op.set };
|
|
60
|
+
let s = findSection(text, name);
|
|
61
|
+
if (!s) {
|
|
62
|
+
// Create it as an ### section at the end of `under`, else at the end of the file.
|
|
63
|
+
const parent = findSection(text, under);
|
|
64
|
+
const insertAt = parent ? parent.bodyEnd : text.length;
|
|
65
|
+
const before = text.slice(0, insertAt).replace(/\n*$/, '\n\n');
|
|
66
|
+
text = `${before}### ${name}\n\n${text.slice(insertAt).replace(/^\n*/, text.slice(insertAt) ? '\n' : '')}`;
|
|
67
|
+
s = findSection(text, name);
|
|
68
|
+
}
|
|
69
|
+
const body = text.slice(s.bodyStart, s.bodyEnd);
|
|
70
|
+
let lines = body.replace(/^\n+/, '').replace(/\n+$/, '').split('\n').filter((l, i, all) => !(all.length === 1 && l === ''));
|
|
71
|
+
if (op.set !== undefined) {
|
|
72
|
+
lines = String(op.set).split('\n');
|
|
73
|
+
} else {
|
|
74
|
+
const keep = lines.filter((l) => !PLACEHOLDER_RE.test(l.trim()));
|
|
75
|
+
// Leading prose of the template ("[From .planning/todos/...]") is a placeholder too.
|
|
76
|
+
lines = keep.concat([].concat(op.append || []));
|
|
77
|
+
}
|
|
78
|
+
const tail = s.bodyEnd < text.length ? '\n' : '';
|
|
79
|
+
return `${text.slice(0, s.bodyStart)}\n${lines.join('\n')}\n${tail}${text.slice(s.bodyEnd)}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function setField(text, field, value) {
|
|
83
|
+
const re = new RegExp(`(\\*\\*${escapeRegex(field)}:\\*\\*[ \\t]*)(.*)`, 'i');
|
|
84
|
+
if (!re.test(text)) return null;
|
|
85
|
+
return text.replace(re, (_m, prefix) => `${prefix}${value}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function renderInit(o) {
|
|
89
|
+
const v = (x, d) => (x === undefined || x === null || x === '' ? d : x);
|
|
90
|
+
return `# Project State
|
|
91
|
+
|
|
92
|
+
## Project Reference
|
|
93
|
+
|
|
94
|
+
See: .planning/project.md (updated ${today()})
|
|
95
|
+
|
|
96
|
+
**Core value:** ${v(o.coreValue, '[One-liner from project.md Core Value section]')}
|
|
97
|
+
**Current focus:** ${v(o.focus, v(o.phaseName, 'Not started'))}
|
|
98
|
+
|
|
99
|
+
## Current Position
|
|
100
|
+
|
|
101
|
+
**Current Phase:** ${v(o.phase, '1')}
|
|
102
|
+
**Current Phase Name:** ${v(o.phaseName, v(o.focus, '—'))}
|
|
103
|
+
**Current Plan:** —
|
|
104
|
+
**Total Plans in Phase:** 0
|
|
105
|
+
**Total Phases:** ${v(o.totalPhases, '0')}
|
|
106
|
+
**Status:** ${v(o.status, 'Ready to plan')}
|
|
107
|
+
**Last Activity:** ${today()}
|
|
108
|
+
**Last Activity Description:** ${v(o.activity, 'State initialized')}
|
|
109
|
+
**Progress:** [░░░░░░░░░░] 0%
|
|
110
|
+
|
|
111
|
+
## Performance Metrics
|
|
112
|
+
|
|
113
|
+
**Velocity:**
|
|
114
|
+
- Total plans completed: 0
|
|
115
|
+
- Average duration: —
|
|
116
|
+
- Total execution time: 0.0 hours
|
|
117
|
+
|
|
118
|
+
**By Plan:** (one row per plan, written by \`state record-metric\`)
|
|
119
|
+
|
|
120
|
+
| Plan | Duration | Tasks | Files |
|
|
121
|
+
|------|----------|-------|-------|
|
|
122
|
+
| - | - | - | - |
|
|
123
|
+
|
|
124
|
+
*Updated after each plan completion*
|
|
125
|
+
|
|
126
|
+
## Accumulated Context
|
|
127
|
+
|
|
128
|
+
### Decisions
|
|
129
|
+
|
|
130
|
+
Decisions are logged in project.md Key Decisions table.
|
|
131
|
+
|
|
132
|
+
None yet.
|
|
133
|
+
|
|
134
|
+
### Pending Todos
|
|
135
|
+
|
|
136
|
+
None yet.
|
|
137
|
+
|
|
138
|
+
### Blockers/Concerns
|
|
139
|
+
|
|
140
|
+
None yet.
|
|
141
|
+
|
|
142
|
+
## Session Continuity
|
|
143
|
+
|
|
144
|
+
**Last session:** ${new Date().toISOString().slice(0, 16).replace('T', ' ')}
|
|
145
|
+
**Stopped At:** ${v(o.activity, 'State initialized')}
|
|
146
|
+
**Resume File:** None
|
|
147
|
+
`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function stateInit(cwd, o = {}) {
|
|
151
|
+
const p = statePathOf(cwd);
|
|
152
|
+
const existing = readBody(cwd);
|
|
153
|
+
if (existing !== null && !o.force) return { error: 'state_exists', reason: 'state.md exists; pass --force to replace it (a backup is kept)' };
|
|
154
|
+
let body = renderInit(o);
|
|
155
|
+
let backup = null;
|
|
156
|
+
if (existing !== null) {
|
|
157
|
+
if (o.keepContext) {
|
|
158
|
+
const src = stripFrontmatter(existing);
|
|
159
|
+
const old = findSection(src, 'Accumulated Context');
|
|
160
|
+
const neu = findSection(body, 'Accumulated Context');
|
|
161
|
+
if (old && neu) body = body.slice(0, neu.bodyStart) + src.slice(old.bodyStart, old.bodyEnd).replace(/\n*$/, '\n\n') + body.slice(neu.bodyEnd);
|
|
162
|
+
}
|
|
163
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
164
|
+
backup = `${p}.bak-${stamp}`;
|
|
165
|
+
fs.copyFileSync(p, backup);
|
|
166
|
+
}
|
|
167
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
168
|
+
writeStateMd(p, body, cwd);
|
|
169
|
+
return { written: true, path: path.relative(cwd, p).replace(/\\/g, '/'), backup: backup && path.relative(cwd, backup).replace(/\\/g, '/'), kept_context: !!(o.keepContext && existing) };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function withState(cwd, fn) {
|
|
173
|
+
const text = readBody(cwd);
|
|
174
|
+
if (text === null) return { error: 'state_missing', reason: 'state.md not found; run `pan-tools state init` first' };
|
|
175
|
+
const r = fn(stripFrontmatter(text));
|
|
176
|
+
if (r.error) return r;
|
|
177
|
+
writeStateMd(statePathOf(cwd), r.text, cwd);
|
|
178
|
+
const { text: _t, ...rest } = r;
|
|
179
|
+
return { written: true, ...rest };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function stateSection(cwd, name, op) {
|
|
183
|
+
if (!name) return { error: 'section_required', reason: 'Usage: state section <name> (--append <line> | --set <text>) [--under <section>]' };
|
|
184
|
+
if (op.append == null && op.set == null) return { error: 'op_required', reason: 'pass --append <line> or --set <text>' };
|
|
185
|
+
return withState(cwd, (t) => ({ text: editSection(t, name, op, op.under), section: name }));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function stateQuickTask(cwd, o) {
|
|
189
|
+
if (!o.description) return { error: 'description_required', reason: 'Usage: state quick-task --description D --commit SHA --directory DIR [--status S] [--date YYYY-MM-DD]' };
|
|
190
|
+
return withState(cwd, (t) => {
|
|
191
|
+
const withStatus = o.status != null;
|
|
192
|
+
let s = findSection(t, 'Quick Tasks Completed');
|
|
193
|
+
if (!s) {
|
|
194
|
+
const header = withStatus
|
|
195
|
+
? '| # | Description | Date | Commit | Status | Directory |\n|---|-------------|------|--------|--------|-----------|'
|
|
196
|
+
: '| # | Description | Date | Commit | Directory |\n|---|-------------|------|--------|-----------|';
|
|
197
|
+
const anchor = findSection(t, 'Blockers/Concerns');
|
|
198
|
+
const at = anchor ? anchor.bodyEnd : t.length;
|
|
199
|
+
t = `${t.slice(0, at).replace(/\n*$/, '\n\n')}### Quick Tasks Completed\n\n${header}\n${t.slice(at) ? '\n' + t.slice(at).replace(/^\n*/, '') : ''}`;
|
|
200
|
+
s = findSection(t, 'Quick Tasks Completed');
|
|
201
|
+
}
|
|
202
|
+
const body = t.slice(s.bodyStart, s.bodyEnd);
|
|
203
|
+
const rows = body.split('\n').filter((l) => /^\|\s*\d+\s*\|/.test(l));
|
|
204
|
+
const hasStatusCol = /\|\s*Status\s*\|/.test(body);
|
|
205
|
+
const n = rows.length + 1;
|
|
206
|
+
// Escape a pipe once: init's description_cell arrives already escaped. Line breaks fold.
|
|
207
|
+
const cell = (x) => String(x == null ? '' : x).replace(/\r?\n+/g, ' ').replace(/(^|[^\\])\|/g, '$1\\|').replace(/(^|[^\\])\|/g, '$1\\|');
|
|
208
|
+
const cols = [n, cell(o.description), o.date || today(), cell(o.commit || '')];
|
|
209
|
+
if (hasStatusCol) cols.push(cell(o.status || ''));
|
|
210
|
+
cols.push(cell(o.directory || ''));
|
|
211
|
+
const row = `| ${cols.join(' | ')} |`;
|
|
212
|
+
const lines = body.replace(/\n+$/, '').split('\n');
|
|
213
|
+
const lastTable = lines.map((l, i) => (l.startsWith('|') ? i : -1)).filter((i) => i >= 0).pop();
|
|
214
|
+
lines.splice(lastTable + 1, 0, row);
|
|
215
|
+
const tail = s.bodyEnd < t.length ? '\n\n' : '\n';
|
|
216
|
+
return { text: `${t.slice(0, s.bodyStart)}${lines.join('\n')}${tail}${t.slice(s.bodyEnd).replace(/^\n*/, '')}`, row: n };
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function stateApply(cwd, batch) {
|
|
221
|
+
if (!batch || typeof batch !== 'object') return { error: 'invalid_batch', reason: 'pass --json \'{"fields": {...}, "sections": {"Name": {"append": [...]} | {"set": "..."}}}\'' };
|
|
222
|
+
return withState(cwd, (t) => {
|
|
223
|
+
const updated = [];
|
|
224
|
+
const failed = [];
|
|
225
|
+
for (const [field, value] of Object.entries(batch.fields || {})) {
|
|
226
|
+
const next = setField(t, field, value);
|
|
227
|
+
if (next === null) failed.push(field); else { t = next; updated.push(field); }
|
|
228
|
+
}
|
|
229
|
+
for (const [name, op] of Object.entries(batch.sections || {})) {
|
|
230
|
+
t = editSection(t, name, op || {}, op && op.under);
|
|
231
|
+
updated.push(name);
|
|
232
|
+
}
|
|
233
|
+
return { text: t, updated, failed };
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function cmdStateInit(cwd, o, raw) { const r = stateInit(cwd, o); output(r, raw, r.path || r.error); }
|
|
238
|
+
function cmdStateSection(cwd, name, op, raw) { const r = stateSection(cwd, name, op); output(r, raw, r.section || r.error); }
|
|
239
|
+
function cmdStateQuickTask(cwd, o, raw) { const r = stateQuickTask(cwd, o); output(r, raw, r.row ? String(r.row) : r.error); }
|
|
240
|
+
function cmdStateApply(cwd, json, raw) {
|
|
241
|
+
let batch = null;
|
|
242
|
+
try { batch = JSON.parse(json); } catch { /* reported below */ }
|
|
243
|
+
const r = stateApply(cwd, batch);
|
|
244
|
+
output(r, raw, r.error || String((r.updated || []).length));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
module.exports = { stateInit, stateSection, stateQuickTask, stateApply, editSection, findSection, cmdStateInit, cmdStateSection, cmdStateQuickTask, cmdStateApply };
|