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,277 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* local-schedule.cjs — the weekly checks GitHub used to run on a schedule, run by this
|
|
4
|
+
* machine's Windows Task Scheduler instead (local-first plan, phase 2).
|
|
5
|
+
*
|
|
6
|
+
* codescan Sunday 03:00 a whole-tree code scan through local-ci.cjs: the CodeQL CLI
|
|
7
|
+
* (security-extended) when installed, else Semgrep in Docker
|
|
8
|
+
* outdated Monday 09:00 `npm outdated`, in place of Dependabot's pull requests
|
|
9
|
+
* market-watch Monday 09:10 harness/autopilot/market-watch.cjs (the WATCH triggers)
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* node scripts/local-schedule.cjs --install [--root <checkout>] [--node <node.exe>]
|
|
13
|
+
* node scripts/local-schedule.cjs --uninstall removes exactly the tasks --install adds
|
|
14
|
+
* node scripts/local-schedule.cjs --list the tasks, their last result, the latest reports
|
|
15
|
+
* node scripts/local-schedule.cjs --run <job> what a task runs; also by hand
|
|
16
|
+
*
|
|
17
|
+
* Tasks are registered for the current user (schtasks, no admin) from an XML definition, so
|
|
18
|
+
* a run missed while the machine was off starts when it is next on. Each task runs this
|
|
19
|
+
* script with `--run <job>` in the checkout given by --root, by default the repository's
|
|
20
|
+
* main worktree (a linked worktree is deleted when its branch lands). Reports go to
|
|
21
|
+
* <checkout>/.ci-local/scheduled/<job>/ (gitignored): a <stamp>.md summary, a <stamp>.log,
|
|
22
|
+
* and last-run.json, which harness/autopilot/state-of-pan.cjs links. Off Windows, --install
|
|
23
|
+
* prints the equivalent crontab lines instead. Requiring this file runs nothing.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
'use strict';
|
|
27
|
+
|
|
28
|
+
const { spawnSync } = require('child_process');
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
const os = require('os');
|
|
31
|
+
const path = require('path');
|
|
32
|
+
|
|
33
|
+
const SCRIPT_ROOT = path.resolve(__dirname, '..');
|
|
34
|
+
const TASK_PREFIX = 'PanWizard-';
|
|
35
|
+
const DAYS = { SUN: 'Sunday', MON: 'Monday', TUE: 'Tuesday', WED: 'Wednesday', THU: 'Thursday', FRI: 'Friday', SAT: 'Saturday' };
|
|
36
|
+
const CRON_DAY = { SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6 };
|
|
37
|
+
|
|
38
|
+
const JOBS = [
|
|
39
|
+
{ id: 'codescan', day: 'SUN', time: '03:00', description: 'PAN Wizard: weekly whole-tree code scan (CodeQL CLI, else Semgrep in Docker)' },
|
|
40
|
+
{ id: 'outdated', day: 'MON', time: '09:00', description: 'PAN Wizard: weekly npm outdated report (in place of Dependabot)' },
|
|
41
|
+
{ id: 'market-watch', day: 'MON', time: '09:10', description: 'PAN Wizard: weekly market watcher (harness/autopilot/market-watch.cjs)' },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const taskName = (job) => `${TASK_PREFIX}${job.id}`;
|
|
45
|
+
|
|
46
|
+
function xmlEscape(s) {
|
|
47
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// The first `day` on or after `from`, at `time`, as a local timestamp without a zone (what
|
|
51
|
+
// Task Scheduler's StartBoundary takes).
|
|
52
|
+
function startBoundary(day, time, from = new Date()) {
|
|
53
|
+
const d = new Date(from.getFullYear(), from.getMonth(), from.getDate());
|
|
54
|
+
while (d.getDay() !== CRON_DAY[day]) d.setDate(d.getDate() + 1);
|
|
55
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
56
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${time}:00`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The Task Scheduler definition for one job. Pure. */
|
|
60
|
+
function taskXml(job, { root, node, from }) {
|
|
61
|
+
// A Task Scheduler definition is a Windows path whichever host builds it.
|
|
62
|
+
const script = path.win32.join(root, 'scripts', 'local-schedule.cjs');
|
|
63
|
+
return `<?xml version="1.0" encoding="UTF-16"?>
|
|
64
|
+
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
65
|
+
<RegistrationInfo>
|
|
66
|
+
<Description>${xmlEscape(job.description)}</Description>
|
|
67
|
+
<Author>PAN Wizard scripts/local-schedule.cjs</Author>
|
|
68
|
+
</RegistrationInfo>
|
|
69
|
+
<Triggers>
|
|
70
|
+
<CalendarTrigger>
|
|
71
|
+
<StartBoundary>${startBoundary(job.day, job.time, from)}</StartBoundary>
|
|
72
|
+
<Enabled>true</Enabled>
|
|
73
|
+
<ScheduleByWeek>
|
|
74
|
+
<DaysOfWeek><${DAYS[job.day]} /></DaysOfWeek>
|
|
75
|
+
<WeeksInterval>1</WeeksInterval>
|
|
76
|
+
</ScheduleByWeek>
|
|
77
|
+
</CalendarTrigger>
|
|
78
|
+
</Triggers>
|
|
79
|
+
<Principals>
|
|
80
|
+
<Principal id="Author">
|
|
81
|
+
<LogonType>InteractiveToken</LogonType>
|
|
82
|
+
<RunLevel>LeastPrivilege</RunLevel>
|
|
83
|
+
</Principal>
|
|
84
|
+
</Principals>
|
|
85
|
+
<Settings>
|
|
86
|
+
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
|
87
|
+
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
|
88
|
+
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
|
89
|
+
<StartWhenAvailable>true</StartWhenAvailable>
|
|
90
|
+
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
|
|
91
|
+
<ExecutionTimeLimit>PT4H</ExecutionTimeLimit>
|
|
92
|
+
<Enabled>true</Enabled>
|
|
93
|
+
</Settings>
|
|
94
|
+
<Actions Context="Author">
|
|
95
|
+
<Exec>
|
|
96
|
+
<Command>${xmlEscape(node)}</Command>
|
|
97
|
+
<Arguments>${xmlEscape(`"${script}" --run ${job.id}`)}</Arguments>
|
|
98
|
+
<WorkingDirectory>${xmlEscape(root)}</WorkingDirectory>
|
|
99
|
+
</Exec>
|
|
100
|
+
</Actions>
|
|
101
|
+
</Task>
|
|
102
|
+
`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The schtasks calls --install makes, one per job, each with the XML it registers. Pure. */
|
|
106
|
+
function installPlan({ root, node, from }) {
|
|
107
|
+
return JOBS.map((job) => ({ name: taskName(job), xml: taskXml(job, { root, node, from }), args: ['/Create', '/TN', taskName(job), '/XML', '<xml file>', '/F'] }));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The schtasks calls --uninstall makes: a delete for every task --install creates. Pure. */
|
|
111
|
+
function uninstallPlan() {
|
|
112
|
+
return JOBS.map((job) => ({ name: taskName(job), args: ['/Delete', '/TN', taskName(job), '/F'] }));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function cronLines({ root, node }) {
|
|
116
|
+
return JOBS.map((job) => {
|
|
117
|
+
const [h, m] = job.time.split(':').map(Number);
|
|
118
|
+
return `${m} ${h} * * ${CRON_DAY[job.day]} cd '${root}' && '${node}' scripts/local-schedule.cjs --run ${job.id}`;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const schtasks = (args) => spawnSync('schtasks', args, { encoding: 'utf8', windowsHide: true });
|
|
123
|
+
|
|
124
|
+
// The repository's main worktree: the first entry `git worktree list` prints.
|
|
125
|
+
function mainWorktree(cwd) {
|
|
126
|
+
const r = spawnSync('git', ['worktree', 'list', '--porcelain'], { cwd, encoding: 'utf8' });
|
|
127
|
+
const m = r.status === 0 && r.stdout.match(/^worktree (.+)$/m);
|
|
128
|
+
return m ? path.resolve(m[1].trim()) : cwd;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function reportDir(root, job) {
|
|
132
|
+
return path.join(root, '.ci-local', 'scheduled', job);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The latest run of each job in `root`, from its last-run.json: { job: {status, at, report} }. */
|
|
136
|
+
function latestReports(root) {
|
|
137
|
+
const out = {};
|
|
138
|
+
for (const job of JOBS) {
|
|
139
|
+
try { out[job.id] = JSON.parse(fs.readFileSync(path.join(reportDir(root, job.id), 'last-run.json'), 'utf8')); } catch { /* never ran */ }
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function stamp(d = new Date()) {
|
|
145
|
+
return d.toISOString().replace(/[-:]/g, '').replace('T', '-').slice(0, 15);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function runCapture(cmd, args, opts) {
|
|
149
|
+
const r = spawnSync(cmd, args, { encoding: 'utf8', windowsHide: true, maxBuffer: 256 * 1024 * 1024, timeout: 4 * 60 * 60 * 1000, ...opts });
|
|
150
|
+
return { status: r.error ? null : r.status, out: `${r.stdout || ''}${r.stderr || ''}${r.error ? `\n${r.error.message}\n` : ''}` };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Each job returns { status: 'pass'|'fail'|'report', summary: markdown, log }.
|
|
154
|
+
const RUNNERS = {
|
|
155
|
+
codescan(root) {
|
|
156
|
+
const out = path.join(reportDir(root, 'codescan'), 'ci-local');
|
|
157
|
+
const r = runCapture(process.execPath, [path.join(root, 'scripts', 'local-ci.cjs'), '--root', root, '--out', out, '--only', 'codescan', '--codescan-all'], { cwd: root });
|
|
158
|
+
let gate = null;
|
|
159
|
+
try { gate = JSON.parse(fs.readFileSync(path.join(out, 'report.json'), 'utf8')).gates.find((g) => g.id === 'codescan'); } catch { /* no report */ }
|
|
160
|
+
const status = r.status === 0 ? (gate && gate.status === 'skip' ? 'skip' : 'pass') : 'fail';
|
|
161
|
+
return { status, log: r.out, summary: `- ${gate ? `${gate.title}: ${gate.status.toUpperCase()}${gate.detail ? ` — ${gate.detail}` : ''}` : `local-ci exited ${r.status}`}\n- gate log: ${gate && gate.log ? path.join(out, 'logs', 'codescan.log') : '—'}\n` };
|
|
162
|
+
},
|
|
163
|
+
outdated(root) {
|
|
164
|
+
// npm outdated exits 1 when anything is outdated: that is the report, not a failure.
|
|
165
|
+
const r = runCapture('npm outdated --json --long', [], { cwd: root, shell: true });
|
|
166
|
+
let data = {};
|
|
167
|
+
try { data = JSON.parse(r.out.slice(r.out.indexOf('{'))); } catch { if (r.status !== 0 && r.status !== 1) return { status: 'fail', log: r.out, summary: `- npm outdated failed (exit ${r.status})\n` }; }
|
|
168
|
+
const names = Object.keys(data).sort();
|
|
169
|
+
const lines = names.length
|
|
170
|
+
? ['| Package | Type | Current | Wanted | Latest |', '|---|---|---|---|---|', ...names.map((n) => { const p = data[n]; return `| ${n} | ${p.type || ''} | ${p.current || '—'} | ${p.wanted} | ${p.latest} |`; })]
|
|
171
|
+
: ['- every dependency is current'];
|
|
172
|
+
return { status: names.length ? 'report' : 'pass', log: r.out, summary: lines.join('\n') + '\n' };
|
|
173
|
+
},
|
|
174
|
+
'market-watch'(root) {
|
|
175
|
+
const r = runCapture(process.execPath, [path.join(root, 'harness', 'autopilot', 'market-watch.cjs')], { cwd: root });
|
|
176
|
+
return { status: r.status === 0 ? 'pass' : 'fail', log: r.out, summary: `- exit ${r.status}; fired triggers go to harness/autopilot/state/inbox.md\n\n\`\`\`\n${r.out.trim().split(/\r?\n/).slice(-20).join('\n')}\n\`\`\`\n` };
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
function runJob(id, root) {
|
|
181
|
+
const job = JOBS.find((j) => j.id === id);
|
|
182
|
+
if (!job) throw new Error(`unknown job ${id}; one of ${JOBS.map((j) => j.id).join(', ')}`);
|
|
183
|
+
const dir = reportDir(root, id);
|
|
184
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
185
|
+
const at = new Date();
|
|
186
|
+
const s = stamp(at);
|
|
187
|
+
const r = RUNNERS[id](root);
|
|
188
|
+
const report = path.join(dir, `${s}.md`);
|
|
189
|
+
fs.writeFileSync(path.join(dir, `${s}.log`), r.log);
|
|
190
|
+
fs.writeFileSync(report, `# ${id} — ${at.toISOString()}\n\nStatus: **${r.status}**\n\n${r.summary}`);
|
|
191
|
+
const last = { job: id, status: r.status, at: at.toISOString(), report: path.relative(root, report).replace(/\\/g, '/') };
|
|
192
|
+
fs.writeFileSync(path.join(dir, 'last-run.json'), JSON.stringify(last, null, 2) + '\n');
|
|
193
|
+
return last;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function install({ root, node, exec = schtasks, from }) {
|
|
197
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'pan-schedule-'));
|
|
198
|
+
const done = [];
|
|
199
|
+
try {
|
|
200
|
+
for (const step of installPlan({ root, node, from })) {
|
|
201
|
+
const file = path.join(tmp, `${step.name}.xml`);
|
|
202
|
+
// Task Scheduler reads its XML as UTF-16 with a byte-order mark.
|
|
203
|
+
fs.writeFileSync(file, Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(step.xml, 'utf16le')]));
|
|
204
|
+
const r = exec(step.args.map((a) => (a === '<xml file>' ? file : a)));
|
|
205
|
+
if (r.status !== 0) throw new Error(`schtasks ${step.args.slice(0, 3).join(' ')}: ${(r.stderr || r.stdout || '').trim()}`);
|
|
206
|
+
done.push(step.name);
|
|
207
|
+
}
|
|
208
|
+
} finally {
|
|
209
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
210
|
+
}
|
|
211
|
+
return done;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function uninstall({ exec = schtasks } = {}) {
|
|
215
|
+
const removed = [];
|
|
216
|
+
for (const step of uninstallPlan()) {
|
|
217
|
+
if (exec(['/Query', '/TN', step.name]).status !== 0) continue;
|
|
218
|
+
const r = exec(step.args);
|
|
219
|
+
if (r.status !== 0) throw new Error(`schtasks /Delete ${step.name}: ${(r.stderr || r.stdout || '').trim()}`);
|
|
220
|
+
removed.push(step.name);
|
|
221
|
+
}
|
|
222
|
+
return removed;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function list({ root, exec = schtasks }) {
|
|
226
|
+
const latest = latestReports(root);
|
|
227
|
+
const lines = [];
|
|
228
|
+
for (const job of JOBS) {
|
|
229
|
+
const q = exec(['/Query', '/TN', taskName(job), '/FO', 'LIST', '/V']);
|
|
230
|
+
const field = (k) => ((q.stdout || '').match(new RegExp(`^${k}:\\s*(.*)$`, 'm')) || [])[1] || '—';
|
|
231
|
+
// 267011 is SCHED_S_TASK_HAS_NOT_RUN; its Last Run Time is a 1999 placeholder.
|
|
232
|
+
const last = field('Last Result') === '267011' ? 'never run' : `${field('Last Run Time')} (result ${field('Last Result')})`;
|
|
233
|
+
lines.push(q.status === 0
|
|
234
|
+
? `${taskName(job)} ${DAYS[job.day]} ${job.time} next: ${field('Next Run Time')} last: ${last}`
|
|
235
|
+
: `${taskName(job)} not installed`);
|
|
236
|
+
const l = latest[job.id];
|
|
237
|
+
lines.push(` latest report: ${l ? `${l.at.slice(0, 16)} ${l.status} — ${l.report}` : 'none yet'}`);
|
|
238
|
+
}
|
|
239
|
+
return lines.join('\n');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function main(argv) {
|
|
243
|
+
const get = (k) => { const i = argv.indexOf(k); return i >= 0 ? argv[i + 1] : null; };
|
|
244
|
+
const root = path.resolve(get('--root') || mainWorktree(SCRIPT_ROOT));
|
|
245
|
+
const node = get('--node') || process.execPath;
|
|
246
|
+
if (argv.includes('--run')) {
|
|
247
|
+
const last = runJob(get('--run'), root);
|
|
248
|
+
console.log(`${last.job}: ${last.status} — ${last.report}`);
|
|
249
|
+
return last.status === 'fail' ? 1 : 0;
|
|
250
|
+
}
|
|
251
|
+
if (argv.includes('--list')) {
|
|
252
|
+
if (process.platform !== 'win32') { console.log(cronLines({ root, node }).join('\n')); return 0; }
|
|
253
|
+
console.log(list({ root }));
|
|
254
|
+
return 0;
|
|
255
|
+
}
|
|
256
|
+
if (argv.includes('--uninstall')) {
|
|
257
|
+
if (process.platform !== 'win32') { console.log('remove these lines with `crontab -e`:\n' + cronLines({ root, node }).join('\n')); return 0; }
|
|
258
|
+
const removed = uninstall();
|
|
259
|
+
console.log(removed.length ? `removed: ${removed.join(', ')}` : 'nothing to remove');
|
|
260
|
+
return 0;
|
|
261
|
+
}
|
|
262
|
+
if (argv.includes('--install')) {
|
|
263
|
+
if (/fnm_multishells/i.test(node)) { console.error(`local-schedule: ${node} is an fnm session shim that disappears with its shell; pass --node <a stable node.exe>`); return 2; }
|
|
264
|
+
if (!fs.existsSync(path.join(root, 'scripts', 'local-schedule.cjs'))) console.error(`local-schedule: warning — ${root} has no scripts/local-schedule.cjs yet (it lands with this branch); the tasks fail until it does`);
|
|
265
|
+
if (process.platform !== 'win32') { console.log('add these lines with `crontab -e`:\n' + cronLines({ root, node }).join('\n')); return 0; }
|
|
266
|
+
const done = install({ root, node });
|
|
267
|
+
console.log(`installed for ${os.userInfo().username}, running in ${root}: ${done.join(', ')}`);
|
|
268
|
+
console.log(list({ root }));
|
|
269
|
+
return 0;
|
|
270
|
+
}
|
|
271
|
+
console.error('usage: local-schedule.cjs --install [--root DIR] [--node PATH] | --uninstall | --list | --run <codescan|outdated|market-watch>');
|
|
272
|
+
return 2;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
module.exports = { JOBS, TASK_PREFIX, taskName, taskXml, installPlan, uninstallPlan, cronLines, startBoundary, latestReports, reportDir, runJob, install, uninstall, list, main };
|
|
276
|
+
|
|
277
|
+
if (require.main === module) process.exit(main(process.argv.slice(2)));
|
package/scripts/prompt-lint.cjs
CHANGED
|
@@ -13,10 +13,16 @@
|
|
|
13
13
|
* pasted-source an @-include of engine source (`*.cjs` / `*.js`) into a prompt
|
|
14
14
|
* oversized-question an AskUserQuestion block offering more than four options (the
|
|
15
15
|
* host's limit: two to four)
|
|
16
|
+
* oversized-ask an AskUserQuestion call asking more than four questions at once
|
|
17
|
+
* (the host's limit; program L6b, settings asked ten)
|
|
16
18
|
* untyped-spawn a Task( spawn without `model=`, or a general-purpose spawn told
|
|
17
19
|
* to read a role file instead of naming the agent type
|
|
18
20
|
* dev-skill a shipped prompt sending the user to a dev-only skill of this
|
|
19
21
|
* repository (.claude/commands/*.md), which no install carries
|
|
22
|
+
* refused-write a write instruction whose target file is named summary.md,
|
|
23
|
+
* report.md, findings.md or analysis.md: Claude Code refuses a
|
|
24
|
+
* subagent's Write to those names in any directory ("Subagents
|
|
25
|
+
* should return findings as text"; reproduced 2026-10-19, MI-127)
|
|
20
26
|
*
|
|
21
27
|
* node scripts/prompt-lint.cjs # report the current tree, exit 1 on a finding
|
|
22
28
|
*/
|
|
@@ -98,6 +104,20 @@ function lintText(rel, raw, ctx) {
|
|
|
98
104
|
}
|
|
99
105
|
}
|
|
100
106
|
|
|
107
|
+
// oversized-ask: more than four questions in one call.
|
|
108
|
+
const askCallRe = /AskUserQuestion\(\s*\[/g;
|
|
109
|
+
while ((m = askCallRe.exec(text))) {
|
|
110
|
+
let depth = 0;
|
|
111
|
+
let end = -1;
|
|
112
|
+
for (let i = m.index + m[0].length - 1; i < text.length; i++) {
|
|
113
|
+
if (text[i] === '[') depth++;
|
|
114
|
+
else if (text[i] === ']') { depth--; if (depth === 0) { end = i; break; } }
|
|
115
|
+
}
|
|
116
|
+
if (end < 0) continue;
|
|
117
|
+
const questions = (text.slice(m.index, end).match(/\bquestion\s*:/g) || []).length;
|
|
118
|
+
if (questions > 4) hit('oversized-ask', m.index, `${questions} questions in one call`);
|
|
119
|
+
}
|
|
120
|
+
|
|
101
121
|
// oversized-question
|
|
102
122
|
const askRe = /AskUserQuestion/g;
|
|
103
123
|
while ((m = askRe.exec(text))) {
|
|
@@ -133,6 +153,18 @@ function lintText(rel, raw, ctx) {
|
|
|
133
153
|
});
|
|
134
154
|
}
|
|
135
155
|
|
|
156
|
+
// refused-write
|
|
157
|
+
// A line that writes, creates or outputs a path ending in a refused name. Reads of
|
|
158
|
+
// such a path (an older project's file, a template) are not flagged.
|
|
159
|
+
const REFUSED = /(?:^|[\s`'"(])((?:[\w.{}$~-]+\/)+(summary|report|findings|analysis)\.md)\b/i;
|
|
160
|
+
const WRITES = /\b(write|writes|written|writing|create|creates|save|saves)\b|\*\*output:?\*\*|\boutput:/i;
|
|
161
|
+
let offset = 0;
|
|
162
|
+
for (const l of text.split('\n')) {
|
|
163
|
+
const r = REFUSED.exec(l);
|
|
164
|
+
if (r && !/(?:^|\/)templates\//.test(r[1]) && WRITES.test(l)) hit('refused-write', offset, r[1]);
|
|
165
|
+
offset += l.length + 1;
|
|
166
|
+
}
|
|
167
|
+
|
|
136
168
|
// dev-skill
|
|
137
169
|
if (ctx.dev.length) {
|
|
138
170
|
// Every regex metacharacter escaped, backslash included (inline: CodeQL does not follow a helper).
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
/**
|
|
4
|
+
* The prompt-rule registry (program H6).
|
|
5
|
+
*
|
|
6
|
+
* A normative line in a shipped prompt (MUST, NEVER, SHALL, always, never, do not) is a
|
|
7
|
+
* rule. Each gets a stable id, `<file without .md>:<first 8 hex of sha1(text)>`, so it
|
|
8
|
+
* moves with its text and not its line number. The registry, tests/fixtures/prompt-rules.json,
|
|
9
|
+
* lists them. A harness scenario claims the rules it exercises with `covers: [ids]`.
|
|
10
|
+
*
|
|
11
|
+
* --write regenerate the registry. Rules already marked `baseline` keep the mark;
|
|
12
|
+
* with --baseline every rule gets it (how the registry was first created).
|
|
13
|
+
* --check exit 1 when the registry is stale, when a rule that is neither baseline nor
|
|
14
|
+
* claimed has no reason in tests/fixtures/prompt-rules-allowlist.json, or when
|
|
15
|
+
* a scenario claims a rule that does not exist.
|
|
16
|
+
* --list print the rules (id, file, text) as JSON.
|
|
17
|
+
*
|
|
18
|
+
* `harness run --prompt-mutant <id>` deletes one rule's line from the packed package
|
|
19
|
+
* (applyMutant) and runs only the scenarios that cover it: a rule whose removal changes
|
|
20
|
+
* nothing is decorative.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const crypto = require('crypto');
|
|
26
|
+
|
|
27
|
+
const PROMPT_DIRS = ['agents', 'commands', 'pan-wizard-core/workflows', 'pan-wizard-core/references'];
|
|
28
|
+
const REGISTRY = 'tests/fixtures/prompt-rules.json';
|
|
29
|
+
const ALLOWLIST = 'tests/fixtures/prompt-rules-allowlist.json';
|
|
30
|
+
const NORMATIVE_RE = /\b(MUST|NEVER|SHALL|must not|never|always|Always|Never|[Dd]o not|[Dd]on't)\b/;
|
|
31
|
+
|
|
32
|
+
function normalise(line) {
|
|
33
|
+
return line.trim().replace(/^(?:[-*+]|\d+\.)\s+/, '').replace(/^>\s*/, '').replace(/\s+/g, ' ').trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function listFiles(root) {
|
|
37
|
+
const out = [];
|
|
38
|
+
const walk = (rel) => {
|
|
39
|
+
const abs = path.join(root, rel);
|
|
40
|
+
if (!fs.existsSync(abs)) return;
|
|
41
|
+
for (const e of fs.readdirSync(abs, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
42
|
+
const r = `${rel}/${e.name}`;
|
|
43
|
+
if (e.isDirectory()) walk(r); else if (e.name.endsWith('.md')) out.push(r);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
for (const d of PROMPT_DIRS) walk(d);
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The rules of one file's text. */
|
|
51
|
+
function rulesOf(file, text) {
|
|
52
|
+
const rules = [];
|
|
53
|
+
const seen = new Map();
|
|
54
|
+
let fence = null;
|
|
55
|
+
let frontmatter = false;
|
|
56
|
+
text.replace(/\r\n/g, '\n').split('\n').forEach((line, i) => {
|
|
57
|
+
if (i === 0 && line === '---') { frontmatter = true; return; }
|
|
58
|
+
if (frontmatter) { if (line === '---') frontmatter = false; return; }
|
|
59
|
+
const f = line.match(/^\s*(```+|~~~+)/);
|
|
60
|
+
if (f) { if (!fence) fence = f[1]; else if (line.trim().startsWith(fence)) fence = null; return; }
|
|
61
|
+
if (fence || !NORMATIVE_RE.test(line)) return;
|
|
62
|
+
const t = normalise(line);
|
|
63
|
+
if (t.length < 8 || /^\|/.test(t) || /^#/.test(t)) return;
|
|
64
|
+
const base = `${file.replace(/\.md$/, '')}:${crypto.createHash('sha1').update(t).digest('hex').slice(0, 8)}`;
|
|
65
|
+
const n = (seen.get(base) || 0) + 1;
|
|
66
|
+
seen.set(base, n);
|
|
67
|
+
rules.push({ id: n === 1 ? base : `${base}~${n}`, file, line: i + 1, text: t });
|
|
68
|
+
});
|
|
69
|
+
return rules;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function extractRules(root) {
|
|
73
|
+
return listFiles(root).flatMap((f) => rulesOf(f, fs.readFileSync(path.join(root, f), 'utf8')));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readJson(p, fallback) {
|
|
77
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return fallback; }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function loadScenarios(root) {
|
|
81
|
+
const dir = path.join(root, 'harness', 'scenarios');
|
|
82
|
+
if (!fs.existsSync(dir)) return [];
|
|
83
|
+
return fs.readdirSync(dir).filter((n) => n.endsWith('.json')).sort().map((n) => readJson(path.join(dir, n), null)).filter(Boolean);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function coveringScenarios(scenarios, id) {
|
|
87
|
+
return scenarios.filter((s) => Array.isArray(s.covers) && s.covers.includes(id));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function writeRegistry(root, opts = {}) {
|
|
91
|
+
const prev = new Map((readJson(path.join(root, REGISTRY), { rules: [] }).rules || []).map((r) => [r.id, r]));
|
|
92
|
+
const rules = extractRules(root).map(({ id, file, text }) => ({ id, file, text, baseline: !!(opts.baseline || (prev.get(id) && prev.get(id).baseline)) }));
|
|
93
|
+
const p = path.join(root, REGISTRY);
|
|
94
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
95
|
+
fs.writeFileSync(p, JSON.stringify({ rules }, null, 1) + '\n');
|
|
96
|
+
return { rules: rules.length, baseline: rules.filter((r) => r.baseline).length };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function checkRegistry(root) {
|
|
100
|
+
const reg = readJson(path.join(root, REGISTRY), { rules: [] }).rules || [];
|
|
101
|
+
const now = extractRules(root);
|
|
102
|
+
const regIds = new Set(reg.map((r) => r.id));
|
|
103
|
+
const nowIds = new Set(now.map((r) => r.id));
|
|
104
|
+
const stale = reg.length !== now.length || now.some((r) => !regIds.has(r.id));
|
|
105
|
+
const scenarios = loadScenarios(root);
|
|
106
|
+
const claimed = new Set(scenarios.flatMap((s) => (Array.isArray(s.covers) ? s.covers : [])));
|
|
107
|
+
const allow = new Map((readJson(path.join(root, ALLOWLIST), []) || []).filter((e) => e && e.id && e.reason).map((e) => [e.id, e.reason]));
|
|
108
|
+
const unclaimed = reg.filter((r) => !r.baseline && !claimed.has(r.id) && !allow.has(r.id));
|
|
109
|
+
const dangling = [];
|
|
110
|
+
for (const s of scenarios) for (const id of (Array.isArray(s.covers) ? s.covers : [])) if (!nowIds.has(id)) dangling.push({ scenario: s.id, id });
|
|
111
|
+
return { rules: now.length, stale, unclaimed, dangling, claimed: [...claimed].filter((id) => nowIds.has(id)).length };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Delete the first line whose normalised text is the rule. Null when it is not there. */
|
|
115
|
+
function generateMutant(content, ruleText) {
|
|
116
|
+
const eol = /\r\n/.test(content) ? '\r\n' : '\n';
|
|
117
|
+
const lines = content.split(/\r?\n/);
|
|
118
|
+
const i = lines.findIndex((l) => normalise(l) === ruleText);
|
|
119
|
+
if (i < 0) return null;
|
|
120
|
+
lines.splice(i, 1);
|
|
121
|
+
return { content: lines.join(eol), removed: i + 1 };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Apply rule `id`'s mutant to the extracted package at `packageDir`. */
|
|
125
|
+
function applyMutant(root, packageDir, id) {
|
|
126
|
+
const rule = (readJson(path.join(root, REGISTRY), { rules: [] }).rules || []).find((r) => r.id === id) || extractRules(root).find((r) => r.id === id);
|
|
127
|
+
if (!rule) return { error: 'unknown_rule', reason: `no rule ${id} in ${REGISTRY}` };
|
|
128
|
+
const target = path.join(packageDir, rule.file);
|
|
129
|
+
let content;
|
|
130
|
+
try { content = fs.readFileSync(target, 'utf8'); } catch { return { error: 'file_not_in_package', reason: `${rule.file} is not in the package` }; }
|
|
131
|
+
const m = generateMutant(content, rule.text);
|
|
132
|
+
if (!m) return { error: 'rule_not_found', reason: `${rule.file} no longer holds the rule` };
|
|
133
|
+
fs.writeFileSync(target, m.content);
|
|
134
|
+
return { id, file: rule.file, line: m.removed, text: rule.text };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = { extractRules, rulesOf, writeRegistry, checkRegistry, generateMutant, applyMutant, coveringScenarios, normalise, REGISTRY, ALLOWLIST };
|
|
138
|
+
|
|
139
|
+
if (require.main === module) {
|
|
140
|
+
const root = path.join(__dirname, '..');
|
|
141
|
+
const argv = process.argv.slice(2);
|
|
142
|
+
if (argv.includes('--write')) {
|
|
143
|
+
const r = writeRegistry(root, { baseline: argv.includes('--baseline') });
|
|
144
|
+
console.log(`wrote ${REGISTRY}: ${r.rules} rules, ${r.baseline} baseline`);
|
|
145
|
+
} else if (argv.includes('--list')) {
|
|
146
|
+
console.log(JSON.stringify(extractRules(root), null, 1));
|
|
147
|
+
} else {
|
|
148
|
+
const r = checkRegistry(root);
|
|
149
|
+
const problems = [];
|
|
150
|
+
if (r.stale) problems.push(`${REGISTRY} is stale: run node scripts/prompt-rules.cjs --write`);
|
|
151
|
+
for (const u of r.unclaimed) problems.push(`unclaimed rule ${u.id} (${u.file}): "${u.text.slice(0, 80)}" — add it to a scenario's covers, or to ${ALLOWLIST} with a reason`);
|
|
152
|
+
for (const d of r.dangling) problems.push(`scenario ${d.scenario} covers ${d.id}, which is no rule`);
|
|
153
|
+
if (problems.length) { console.log(problems.join('\n')); process.exit(1); }
|
|
154
|
+
console.log(`prompt-rule registry current: ${r.rules} rules, ${r.claimed} claimed by a scenario`);
|
|
155
|
+
}
|
|
156
|
+
}
|
package/scripts/run-tests.cjs
CHANGED
|
@@ -46,6 +46,12 @@ if (files.length === 0) {
|
|
|
46
46
|
process.exit(1);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
// PAN_TEST_CONCURRENCY caps how many test files run at once (scripts/local-ci.cjs sets it).
|
|
50
|
+
// Node's default is one per core but one: on a 40-core machine that is 39 files at a time,
|
|
51
|
+
// and the suite's wall-clock checks were never meant for that. The flag needs Node 20.10+.
|
|
52
|
+
const [major, minor] = process.versions.node.split('.').map(Number);
|
|
53
|
+
const cap = Number(process.env.PAN_TEST_CONCURRENCY);
|
|
54
|
+
const concurrency = cap > 0 && (major > 20 || (major === 20 && minor >= 10)) ? [`--test-concurrency=${cap}`] : [];
|
|
55
|
+
const nodeArgs = watch ? ['--test', '--watch', ...concurrency, ...files] : ['--test', ...concurrency, ...files];
|
|
50
56
|
const result = spawnSync(process.execPath, nodeArgs, { stdio: 'inherit' });
|
|
51
57
|
process.exit(result.status ?? 1);
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Q4 a runPanTools result asserted only by `.output.length`;
|
|
14
14
|
* Q5 a platform conditional that bare-`return`s (counted as a pass) instead of
|
|
15
15
|
* `t.skip(reason)`;
|
|
16
|
-
* Q6 a wall-clock upper bound
|
|
16
|
+
* Q6 a wall-clock upper bound of two seconds or less on a spawned process;
|
|
17
17
|
* Q7 a read of the developer's real HOME / USERPROFILE / os.homedir();
|
|
18
18
|
* Q8 a committed `test.todo` (scaffold stubs must be filled before commit);
|
|
19
19
|
* Q9 an OR of bare property reads inside assert.ok — `assert.ok(a.x || a.y)` asks only
|
|
@@ -143,7 +143,7 @@ const RULES = [
|
|
|
143
143
|
},
|
|
144
144
|
{
|
|
145
145
|
id: 'Q6',
|
|
146
|
-
title: 'wall-clock upper bound
|
|
146
|
+
title: 'wall-clock upper bound of two seconds or less',
|
|
147
147
|
fix: 'assert the behaviour, not the speed; if timing is the contract, bound it generously and allowlist with the reason',
|
|
148
148
|
detect(lines) {
|
|
149
149
|
const out = [];
|
|
@@ -151,7 +151,7 @@ const RULES = [
|
|
|
151
151
|
const line = stripComments(raw);
|
|
152
152
|
if (!/\bassert(?:\.ok)?\s*\(/.test(line) || !TIME_WORDS.test(line)) return;
|
|
153
153
|
const m = /<\s*=?\s*(\d{2,4})\b/.exec(line);
|
|
154
|
-
if (m && Number(m[1])
|
|
154
|
+
if (m && Number(m[1]) <= 2000) out.push({ line: i + 1, text: raw.trim() });
|
|
155
155
|
});
|
|
156
156
|
return out;
|
|
157
157
|
},
|
package/scripts/test-surface.cjs
CHANGED
|
@@ -459,8 +459,15 @@ function main(argv) {
|
|
|
459
459
|
if (argv.includes('--check')) {
|
|
460
460
|
let committed;
|
|
461
461
|
try { committed = readJson(registryPath); } catch { console.error(`no committed registry at ${REGISTRY_REL} — run --write`); return 1; }
|
|
462
|
-
const { drift: d, live, ok } = checkReport(committed, surface, loadLivenessInputs(ROOT), readLivenessAllowlist(ROOT));
|
|
463
|
-
|
|
462
|
+
const { drift: d, live, ok: surfaceOk } = checkReport(committed, surface, loadLivenessInputs(ROOT), readLivenessAllowlist(ROOT));
|
|
463
|
+
// H6: the prompt-rule rows. A rule that is neither grandfathered nor claimed by a
|
|
464
|
+
// scenario's covers (or allowlisted with a reason) fails, as does a stale registry.
|
|
465
|
+
const rules = require('./prompt-rules.cjs').checkRegistry(ROOT);
|
|
466
|
+
const ok = surfaceOk && !rules.stale && !rules.unclaimed.length && !rules.dangling.length;
|
|
467
|
+
if (ok) { console.log('surface registry matches the code; every command, agent, verb and config key is alive; every new prompt rule is claimed'); return 0; }
|
|
468
|
+
if (rules.stale) console.error('prompt-rule registry is stale — run `node scripts/prompt-rules.cjs --write` and commit it');
|
|
469
|
+
for (const u of rules.unclaimed) console.error(` unclaimed prompt rule ${u.id}: "${u.text.slice(0, 80)}"`);
|
|
470
|
+
for (const x of rules.dangling) console.error(` scenario ${x.scenario} covers ${x.id}, which is no rule`);
|
|
464
471
|
if (d.added.length || d.removed.length) {
|
|
465
472
|
console.error(`surface registry is stale — run \`node scripts/test-surface.cjs --write\` and commit it`);
|
|
466
473
|
for (const x of d.added) console.error(` + ${x}`);
|