pan-wizard 3.35.0 → 3.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (339) hide show
  1. package/CHANGELOG.md +3645 -0
  2. package/README.md +46 -52
  3. package/agents/pan-conductor.md +2 -3
  4. package/agents/pan-counterfactual.md +1 -2
  5. package/agents/pan-debugger.md +14 -3
  6. package/agents/pan-design-checker.md +1 -2
  7. package/agents/pan-designer.md +2 -3
  8. package/agents/pan-document_code.md +1 -2
  9. package/agents/pan-executor.md +25 -0
  10. package/agents/pan-hardener.md +4 -3
  11. package/agents/pan-integration-checker.md +1 -2
  12. package/agents/pan-knowledge.md +1 -2
  13. package/agents/pan-meta-reviewer.md +3 -3
  14. package/agents/pan-optimizer.md +4 -4
  15. package/agents/pan-phase-researcher.md +80 -475
  16. package/agents/pan-plan-checker.md +40 -3
  17. package/agents/pan-planner.md +21 -7
  18. package/agents/pan-previewer.md +3 -2
  19. package/agents/pan-project-researcher.md +72 -493
  20. package/agents/pan-release.md +1 -1
  21. package/agents/pan-reviewer.md +14 -3
  22. package/agents/pan-roadmapper.md +12 -7
  23. package/agents/pan-verifier.md +9 -4
  24. package/bin/install-lib.cjs +134 -14
  25. package/bin/install.js +59 -12
  26. package/commands/pan/army.md +1 -1
  27. package/commands/pan/autodev.md +76 -0
  28. package/commands/pan/cost.md +21 -1
  29. package/commands/pan/exec-phase.md +3 -4
  30. package/commands/pan/focus-auto.md +14 -7
  31. package/commands/pan/focus-design.md +4 -5
  32. package/commands/pan/focus-docs.md +24 -0
  33. package/commands/pan/learn.md +13 -6
  34. package/commands/pan/milestone-audit.md +8 -1
  35. package/commands/pan/milestone-done.md +10 -5
  36. package/commands/pan/milestone-new.md +2 -2
  37. package/commands/pan/new-project.md +15 -12
  38. package/commands/pan/plan-phase.md +15 -9
  39. package/commands/pan/preview.md +1 -1
  40. package/commands/pan/quick.md +1 -1
  41. package/commands/pan/review-deep.md +11 -5
  42. package/commands/pan/settings.md +6 -0
  43. package/commands/pan/verify-phase.md +6 -2
  44. package/docs/AGENTS.md +917 -0
  45. package/docs/ARCHITECTURE.md +1054 -0
  46. package/docs/CLI-REFERENCE.md +4582 -0
  47. package/docs/COMPARISON.md +116 -0
  48. package/docs/DEVELOPMENT.md +490 -0
  49. package/docs/ECOSYSTEM-REVIEW-2026-06.md +107 -0
  50. package/docs/ECOSYSTEM-REVIEW-2026-08.md +349 -0
  51. package/docs/EXAMPLES.md +584 -0
  52. package/docs/FAQ.md +254 -0
  53. package/docs/FIELD-HARVEST-2026-07.md +88 -0
  54. package/docs/FIELD-REPORT-army-2026-06.md +121 -0
  55. package/docs/HOOKS.md +638 -0
  56. package/docs/IMPROVEMENT-TODO.md +246 -0
  57. package/docs/INTERNALS.md +485 -0
  58. package/docs/MIGRATION-v2-to-v3.md +258 -0
  59. package/docs/SECURITY-HARDENING.md +230 -0
  60. package/docs/SKILLS-ENHANCEMENT-PLAN-V2.md +122 -0
  61. package/docs/SKILLS-FULL-TEXT.md +9685 -0
  62. package/docs/SKILLS-REFERENCE.md +1037 -0
  63. package/docs/TROUBLESHOOTING.md +1084 -0
  64. package/docs/USER-GUIDE.md +1437 -0
  65. package/docs/archive/SKILLS-ENHANCEMENT-PLAN.md +230 -0
  66. package/docs/audits/AUDIT-2026-08-e2e-ROADMAP.md +117 -0
  67. package/docs/audits/AUDIT-2026-08-e2e-VERIFICATION.md +693 -0
  68. package/docs/audits/AUDIT-2026-08-e2e.md +1546 -0
  69. package/docs/audits/OUTSTANDING-2026-08.md +275 -0
  70. package/docs/audits/STABILITY-TEST-2026-08.md +640 -0
  71. package/docs/branding/README-brand-draft.md +119 -0
  72. package/docs/branding/handoff/IMPLEMENTATION.md +205 -0
  73. package/docs/branding/handoff/README-brand-draft.md +119 -0
  74. package/docs/branding/handoff/image-prompts.md +46 -0
  75. package/docs/branding/image-prompts.md +46 -0
  76. package/docs/branding/nano-banana-prompts.txt +60 -0
  77. package/docs/context-monitor.md +13 -0
  78. package/docs/decisions/ADR-0001-strategic-feature-prioritization.md +83 -0
  79. package/docs/decisions/ADR-0002-copilot-cli-runtime.md +71 -0
  80. package/docs/decisions/ADR-0003-smart-execution.md +84 -0
  81. package/docs/decisions/ADR-0004-copilot-cli-interaction.md +89 -0
  82. package/docs/decisions/ADR-0005-command-naming.md +80 -0
  83. package/docs/decisions/ADR-0006-focus-commands.md +101 -0
  84. package/docs/decisions/ADR-0007-ship-v1.md +73 -0
  85. package/docs/decisions/ADR-0008-map-codebase-mermaid-togaf.md +80 -0
  86. package/docs/decisions/ADR-0009-production-deployment-checklist.md +68 -0
  87. package/docs/decisions/ADR-0010-standards-integration.md +79 -0
  88. package/docs/decisions/ADR-0011-help-commands-rewrite.md +64 -0
  89. package/docs/decisions/ADR-0012-contributors-attribution-index.md +42 -0
  90. package/docs/decisions/ADR-0013-strategic-feature-additions.md +53 -0
  91. package/docs/decisions/ADR-0014-internal-cleanup-code-quality.md +53 -0
  92. package/docs/decisions/ADR-0015-focus-auto-runner.md +296 -0
  93. package/docs/decisions/ADR-0016-enhanced-doc-sync.md +54 -0
  94. package/docs/decisions/ADR-0017-enhanced-focus-design.md +69 -0
  95. package/docs/decisions/ADR-0018-user-reality-testing.md +84 -0
  96. package/docs/decisions/ADR-0019-end-to-end-user-acceptance-testing.md +113 -0
  97. package/docs/decisions/ADR-0020-ai-drift-prevention.md +55 -0
  98. package/docs/decisions/ADR-0021-codebase-mapper-v2.md +76 -0
  99. package/docs/decisions/ADR-0022-lifecycle-completeness.md +63 -0
  100. package/docs/decisions/ADR-0023-opus-4-7-adoption.md +89 -0
  101. package/docs/decisions/ADR-0024-spec-b-v2-completion.md +137 -0
  102. package/docs/decisions/ADR-0025-behavioral-guardrails-layer.md +129 -0
  103. package/docs/decisions/ADR-0026-self-improvement-loop.md +171 -0
  104. package/docs/decisions/ADR-0027-doc-code-link-graph.md +164 -0
  105. package/docs/decisions/ADR-0028-unified-agents-skills-target.md +46 -0
  106. package/docs/decisions/ADR-0029-experiment-evaluator.md +76 -0
  107. package/docs/decisions/ADR-0030-planning-concurrency.md +37 -0
  108. package/docs/decisions/ADR-0031-focus-auto-backlog-source.md +47 -0
  109. package/docs/decisions/ADR-0032-squad-model.md +64 -0
  110. package/docs/decisions/ADR-0033-army-campaign.md +40 -0
  111. package/docs/decisions/ADR-0034-scheduled-campaigns.md +57 -0
  112. package/docs/decisions/ADR-0035-army-hud-dashboard.md +40 -0
  113. package/docs/decisions/ADR-0036-agentic-memory-architecture.md +106 -0
  114. package/docs/decisions/ADR-0037-pin-security-agents-to-opus.md +51 -0
  115. package/docs/decisions/ADR-0038-skill-aligned-decomposition.md +41 -0
  116. package/docs/decisions/ADR-0039-worktree-as-project-variant.md +37 -0
  117. package/docs/decisions/ADR-0040-memory-injection-defense.md +55 -0
  118. package/docs/decisions/ADR-0041-mcp-dual-era-bridge.md +47 -0
  119. package/docs/decisions/ADR-0042-design-parity-both-flows.md +45 -0
  120. package/docs/decisions/ADR-0043-addressable-planning-roots.md +56 -0
  121. package/docs/decisions/ADR-0044-bound-the-cached-context.md +104 -0
  122. package/docs/decisions/ADR-0045-agent-plugins-bundle.md +76 -0
  123. package/docs/decisions/ADR-0046-subagent-frontmatter-decisions.md +39 -0
  124. package/docs/decisions/ADR-0047-pan-harness.md +76 -0
  125. package/docs/decisions/ADR-0048-one-core-per-project.md +43 -0
  126. package/docs/decisions/ADR-0049-machine-verdicts-and-findings-ledger.md +116 -0
  127. package/docs/specs/ai_drift_prevention_featureai.md +286 -0
  128. package/docs/specs/codebase_mapper_v2_featureai.md +930 -0
  129. package/docs/specs/command_naming_optimization_featureai.md +930 -0
  130. package/docs/specs/comp_analysis_markdown_v1.md +149 -0
  131. package/docs/specs/continuous_autonomous_execution_featureai.md +304 -0
  132. package/docs/specs/contributors_attribution_index_featureai.md +476 -0
  133. package/docs/specs/copilot_cli_interaction_featureai.md +868 -0
  134. package/docs/specs/copilot_cli_runtime_featureai.md +647 -0
  135. package/docs/specs/custom-skills-assessment.md +461 -0
  136. package/docs/specs/deployment_audit_findings_featureai.md +243 -0
  137. package/docs/specs/doc_code_link_graph_featureai.md +253 -0
  138. package/docs/specs/documentation_alignment_cleanup_featureai.md +69 -0
  139. package/docs/specs/e2e_visual_testing_featureai.md +615 -0
  140. package/docs/specs/end_to_end_user_acceptance_testing_featureai.md +810 -0
  141. package/docs/specs/evidence-loop-plan.md +76 -0
  142. package/docs/specs/evidence_loop_featureai.md +497 -0
  143. package/docs/specs/focus-auto-optimize-category.md +449 -0
  144. package/docs/specs/focus-auto-optimize-superplan.md +188 -0
  145. package/docs/specs/focus_auto_runner_featureai.md +1102 -0
  146. package/docs/specs/focus_design_v2_featureai.md +197 -0
  147. package/docs/specs/focus_sync_v2_featureai.md +202 -0
  148. package/docs/specs/git_integration_overhaul_featureai.md +326 -0
  149. package/docs/specs/glm_zai_provider_featureai.md +200 -0
  150. package/docs/specs/googlecli_adoption_featureai.md +661 -0
  151. package/docs/specs/help_commands_rewrite_featureai.md +860 -0
  152. package/docs/specs/industry_analysis_strategic_additions_featureai.md +231 -0
  153. package/docs/specs/industry_comparison_and_feature_gaps_featureai.md +674 -0
  154. package/docs/specs/internal_cleanup_code_quality_featureai.md +486 -0
  155. package/docs/specs/investigations/README.md +31 -0
  156. package/docs/specs/investigations/digests/2026-09-28.md +47 -0
  157. package/docs/specs/investigations/digests/2026-10-08.md +62 -0
  158. package/docs/specs/investigations/digests/2026-10-10.md +61 -0
  159. package/docs/specs/investigations/features/debugger-red-loop-first.md +55 -0
  160. package/docs/specs/investigations/features/harness-cross-host-behaviour.md +93 -0
  161. package/docs/specs/investigations/features/optimize-apply-regression-gate.md +66 -0
  162. package/docs/specs/investigations/features/reviewer-smell-baseline.md +57 -0
  163. package/docs/specs/investigations/features/trace-failure-capture.md +69 -0
  164. package/docs/specs/lifecycle_completeness_featureai.md +754 -0
  165. package/docs/specs/local-first-ci.md +42 -0
  166. package/docs/specs/map_codebase_mermaid_togaf_featureai.md +805 -0
  167. package/docs/specs/market-delta-2026-09-superplan.md +310 -0
  168. package/docs/specs/market-ideas-2026-09.md +131 -0
  169. package/docs/specs/market-ideas-2026-10.md +428 -0
  170. package/docs/specs/market-ideas-ledger.md +177 -0
  171. package/docs/specs/memory-optimization-2026-10.md +285 -0
  172. package/docs/specs/multi-track-planning-roots.md +143 -0
  173. package/docs/specs/multi_model_routing_featureai.md +687 -0
  174. package/docs/specs/multi_model_routing_superplan.md +197 -0
  175. package/docs/specs/opus_47_existing_enhancements_featureai.md +247 -0
  176. package/docs/specs/opus_47_extended_features_featureai.md +363 -0
  177. package/docs/specs/opus_47_extended_features_v2_featureai.md +397 -0
  178. package/docs/specs/pan_focus_commands_featureai.md +2336 -0
  179. package/docs/specs/pan_zcode_mcp_bridge_featureai.md +120 -0
  180. package/docs/specs/product-excellence-superplan.md +315 -0
  181. package/docs/specs/product_excellence_program_2026_q4_featureai.md +365 -0
  182. package/docs/specs/production_deployment_readiness_featureai.md +433 -0
  183. package/docs/specs/production_readiness_ship_v1_featureai.md +698 -0
  184. package/docs/specs/real_test_enforcement_featureai.md +213 -0
  185. package/docs/specs/reality-check-2026-09-superplan.md +362 -0
  186. package/docs/specs/right-sized-ceremony.md +74 -0
  187. package/docs/specs/roadmap_and_competition_2026_q4_featureai.md +390 -0
  188. package/docs/specs/self_improvement_loop_featureai.md +875 -0
  189. package/docs/specs/skill-aligned-decomposition.md +155 -0
  190. package/docs/specs/standards_integration_featureai.md +833 -0
  191. package/docs/specs/testing-system-redesign-2026-09.md +267 -0
  192. package/docs/specs/user_reality_testing_system_featureai.md +788 -0
  193. package/docs/specs/watch-triggers.json +22 -0
  194. package/docs/specs/workflow-integration-redesign.md +742 -0
  195. package/docs/specs/workflow_state_consistency_featureai.md +183 -0
  196. package/hooks/dist/pan-context-monitor.js +9 -2
  197. package/hooks/dist/pan-cost-logger.js +5 -0
  198. package/hooks/dist/pan-guard.js +28 -5
  199. package/hooks/dist/pan-stop-guard.js +9 -9
  200. package/hooks/dist/pan-trace-logger.js +5 -0
  201. package/package.json +10 -2
  202. package/pan-wizard-core/bin/lib/agents-md.cjs +57 -8
  203. package/pan-wizard-core/bin/lib/autodev-report.cjs +180 -0
  204. package/pan-wizard-core/bin/lib/autodev.cjs +642 -0
  205. package/pan-wizard-core/bin/lib/ci.cjs +143 -0
  206. package/pan-wizard-core/bin/lib/commands.cjs +1 -1
  207. package/pan-wizard-core/bin/lib/config.cjs +41 -5
  208. package/pan-wizard-core/bin/lib/constants.cjs +27 -1
  209. package/pan-wizard-core/bin/lib/core.cjs +31 -14
  210. package/pan-wizard-core/bin/lib/cost.cjs +160 -4
  211. package/pan-wizard-core/bin/lib/cross-family.cjs +164 -0
  212. package/pan-wizard-core/bin/lib/debug-cases.cjs +104 -0
  213. package/pan-wizard-core/bin/lib/doc-lint.cjs +5 -3
  214. package/pan-wizard-core/bin/lib/failure-class.cjs +81 -0
  215. package/pan-wizard-core/bin/lib/findings.cjs +148 -9
  216. package/pan-wizard-core/bin/lib/focus.cjs +1 -1
  217. package/pan-wizard-core/bin/lib/git.cjs +1 -1
  218. package/pan-wizard-core/bin/lib/hosts.cjs +88 -0
  219. package/pan-wizard-core/bin/lib/init.cjs +7 -2
  220. package/pan-wizard-core/bin/lib/knowledge.cjs +2 -0
  221. package/pan-wizard-core/bin/lib/merge.cjs +50 -0
  222. package/pan-wizard-core/bin/lib/mutate.cjs +268 -0
  223. package/pan-wizard-core/bin/lib/next-action.cjs +112 -0
  224. package/pan-wizard-core/bin/lib/optimize.cjs +2 -2
  225. package/pan-wizard-core/bin/lib/phase-gate.cjs +15 -9
  226. package/pan-wizard-core/bin/lib/phase-remove.cjs +91 -11
  227. package/pan-wizard-core/bin/lib/phase.cjs +85 -13
  228. package/pan-wizard-core/bin/lib/plan-lint.cjs +310 -0
  229. package/pan-wizard-core/bin/lib/preview.cjs +26 -4
  230. package/pan-wizard-core/bin/lib/requirements-lint.cjs +130 -0
  231. package/pan-wizard-core/bin/lib/research-lint.cjs +268 -0
  232. package/pan-wizard-core/bin/lib/review-deep.cjs +14 -22
  233. package/pan-wizard-core/bin/lib/review-vote.cjs +105 -0
  234. package/pan-wizard-core/bin/lib/roadmap.cjs +1 -0
  235. package/pan-wizard-core/bin/lib/runner.cjs +2 -16
  236. package/pan-wizard-core/bin/lib/sizing.cjs +356 -0
  237. package/pan-wizard-core/bin/lib/squads.cjs +1 -1
  238. package/pan-wizard-core/bin/lib/state-write.cjs +247 -0
  239. package/pan-wizard-core/bin/lib/test-gate.cjs +433 -0
  240. package/pan-wizard-core/bin/lib/uat.cjs +149 -0
  241. package/pan-wizard-core/bin/lib/usage.cjs +104 -0
  242. package/pan-wizard-core/bin/lib/verdict.cjs +26 -3
  243. package/pan-wizard-core/bin/lib/verify-plan-set.cjs +30 -1
  244. package/pan-wizard-core/bin/lib/verify-preflight.cjs +2 -2
  245. package/pan-wizard-core/bin/lib/verify-red.cjs +174 -0
  246. package/pan-wizard-core/bin/lib/verify-retro.cjs +3 -2
  247. package/pan-wizard-core/bin/lib/verify-scope.cjs +143 -2
  248. package/pan-wizard-core/bin/lib/verify.cjs +86 -31
  249. package/pan-wizard-core/bin/lib/worktree.cjs +70 -0
  250. package/pan-wizard-core/bin/pan-tools.cjs +257 -28
  251. package/pan-wizard-core/learnings/universal/autonomous-loop.md +2 -2
  252. package/pan-wizard-core/mcp/orchestrator.cjs +4 -1
  253. package/pan-wizard-core/references/continuation-format.md +3 -3
  254. package/pan-wizard-core/references/design-methodology.md +1 -1
  255. package/pan-wizard-core/references/guardrails.md +1 -1
  256. package/pan-wizard-core/references/handoff-decisions.md +1 -1
  257. package/pan-wizard-core/references/model-profiles.md +2 -3
  258. package/pan-wizard-core/references/questioning.md +24 -0
  259. package/pan-wizard-core/references/ui-brand.md +4 -6
  260. package/pan-wizard-core/references/verdict-contract.md +2 -1
  261. package/pan-wizard-core/templates/config.json +10 -2
  262. package/pan-wizard-core/templates/debug.md +1 -0
  263. package/pan-wizard-core/templates/design.md +5 -4
  264. package/pan-wizard-core/templates/discovery.md +2 -2
  265. package/pan-wizard-core/templates/phase-prompt.md +7 -0
  266. package/pan-wizard-core/templates/requirements.md +9 -0
  267. package/pan-wizard-core/templates/research.md +10 -0
  268. package/pan-wizard-core/templates/summary-complex.md +5 -1
  269. package/pan-wizard-core/templates/summary-minimal.md +4 -0
  270. package/pan-wizard-core/templates/summary-standard.md +5 -1
  271. package/pan-wizard-core/templates/summary.md +8 -1
  272. package/pan-wizard-core/workflows/add-phase.md +4 -6
  273. package/pan-wizard-core/workflows/assumptions.md +19 -1
  274. package/pan-wizard-core/workflows/diagnose-issues.md +2 -0
  275. package/pan-wizard-core/workflows/discuss-phase.md +4 -2
  276. package/pan-wizard-core/workflows/exec-lean.md +69 -0
  277. package/pan-wizard-core/workflows/exec-phase.md +79 -15
  278. package/pan-wizard-core/workflows/execute-plan.md +3 -1
  279. package/{commands/pan → pan-wizard-core/workflows}/focus-doc-audit.md +13 -29
  280. package/{commands/pan → pan-wizard-core/workflows}/focus-drift-walking.md +3 -18
  281. package/{commands/pan → pan-wizard-core/workflows}/focus-exec.md +8 -23
  282. package/{commands/pan → pan-wizard-core/workflows}/focus-plan.md +6 -20
  283. package/{commands/pan → pan-wizard-core/workflows}/focus-scan.md +3 -16
  284. package/{commands/pan → pan-wizard-core/workflows}/focus-sync.md +7 -21
  285. package/pan-wizard-core/workflows/health.md +6 -0
  286. package/pan-wizard-core/workflows/help.md +114 -77
  287. package/pan-wizard-core/workflows/insert-phase.md +4 -6
  288. package/pan-wizard-core/workflows/learn.md +3 -3
  289. package/pan-wizard-core/workflows/milestone-audit.md +2 -2
  290. package/pan-wizard-core/workflows/milestone-new.md +38 -95
  291. package/pan-wizard-core/workflows/new-project.md +96 -258
  292. package/pan-wizard-core/workflows/optimize.md +107 -5
  293. package/pan-wizard-core/workflows/phase-tests.md +10 -0
  294. package/pan-wizard-core/workflows/plan-phase.md +106 -45
  295. package/pan-wizard-core/workflows/profile.md +50 -0
  296. package/pan-wizard-core/workflows/progress.md +10 -0
  297. package/pan-wizard-core/workflows/quick.md +34 -43
  298. package/pan-wizard-core/workflows/remove-phase.md +11 -1
  299. package/pan-wizard-core/workflows/research-phase.md +118 -40
  300. package/pan-wizard-core/workflows/resume-project.md +18 -2
  301. package/pan-wizard-core/workflows/settings.md +33 -19
  302. package/pan-wizard-core/workflows/todo-check.md +6 -2
  303. package/pan-wizard-core/workflows/transition.md +22 -4
  304. package/pan-wizard-core/workflows/uat-walkthrough.md +90 -0
  305. package/pan-wizard-core/workflows/update.md +1 -1
  306. package/pan-wizard-core/workflows/verify-phase.md +39 -46
  307. package/scripts/build-agent-plugin.js +35 -15
  308. package/scripts/build-help.cjs +106 -0
  309. package/scripts/build-plugin.js +10 -1
  310. package/scripts/deprecate-old-versions.js +70 -3
  311. package/scripts/generate-skills-docs.py +1 -1
  312. package/scripts/git-hooks/pre-push +27 -0
  313. package/scripts/install-git-hooks.js +7 -5
  314. package/scripts/install-pre-push.cjs +38 -0
  315. package/scripts/land.cjs +151 -0
  316. package/scripts/local-ci.cjs +501 -0
  317. package/scripts/local-schedule.cjs +277 -0
  318. package/scripts/mutation-probe.cjs +7 -80
  319. package/scripts/prompt-lint.cjs +32 -0
  320. package/scripts/prompt-rules.cjs +156 -0
  321. package/scripts/run-tests.cjs +7 -1
  322. package/scripts/test-quality-lint.cjs +3 -3
  323. package/scripts/test-surface.cjs +175 -6
  324. package/agents/pan-research-synthesizer.md +0 -246
  325. package/commands/pan/audit-deployment.md +0 -386
  326. package/commands/pan/dashboard.md +0 -22
  327. package/commands/pan/discord.md +0 -19
  328. package/commands/pan/mcp-bridge.md +0 -143
  329. package/commands/pan/milestone-cleanup.md +0 -19
  330. package/commands/pan/milestone-gaps.md +0 -35
  331. package/commands/pan/optimize.md +0 -115
  332. package/commands/pan/profile.md +0 -76
  333. package/commands/pan/research-phase.md +0 -189
  334. package/pan-wizard-core/bin/lib/bridge.cjs +0 -269
  335. package/pan-wizard-core/templates/research-project/architecture.md +0 -204
  336. package/pan-wizard-core/templates/research-project/features.md +0 -147
  337. package/pan-wizard-core/templates/research-project/pitfalls.md +0 -200
  338. package/pan-wizard-core/templates/research-project/stack.md +0 -120
  339. 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
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)));
@@ -51,86 +51,13 @@ const TARGETS = [
51
51
  { file: 'pan-wizard-core/bin/pan-tools.cjs', tests: ['tests/dispatcher.test.cjs', 'tests/dispatcher-arms.test.cjs'] },
52
52
  ];
53
53
 
54
- /**
55
- * The mutation operators. Each is a plain textual swap with a token that must appear
56
- * OUTSIDE a string or comment to be worth mutating (see mutableLine). They are deliberately
57
- * few and blunt: the point is to find unconstrained logic, not to enumerate every possible
58
- * defect.
59
- */
60
- const OPERATORS = [
61
- { id: 'gte→gt', find: ' >= ', replace: ' > ' },
62
- { id: 'gt→gte', find: ' > ', replace: ' >= ' },
63
- { id: 'lte→lt', find: ' <= ', replace: ' < ' },
64
- { id: 'lt→lte', find: ' < ', replace: ' <= ' },
65
- { id: 'eq→neq', find: ' === ', replace: ' !== ' },
66
- { id: 'neq→eq', find: ' !== ', replace: ' === ' },
67
- { id: 'and→or', find: ' && ', replace: ' || ' },
68
- { id: 'or→and', find: ' || ', replace: ' && ' },
69
- { id: 'true→false', find: 'return true', replace: 'return false' },
70
- { id: 'false→true', find: 'return false', replace: 'return true' },
71
- { id: 'plus→minus', find: ' + 1', replace: ' - 1' },
72
- ];
73
-
74
- /**
75
- * Is this line worth mutating? Skips comments, and skips a line whose only occurrence of
76
- * the token is inside a string literal — mutating a message changes nothing a test should
77
- * be pinning, and a survivor there would be noise.
78
- *
79
- * Crude but conservative: a line containing a quote is only mutated when the token also
80
- * occurs before the first quote.
81
- */
82
- function mutableLine(line, token) {
83
- const trimmed = line.trim();
84
- if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) return false;
85
- const at = line.indexOf(token);
86
- if (at === -1) return false;
87
- const firstQuote = Math.min(
88
- ...["'", '"', '`'].map((q) => { const i = line.indexOf(q); return i === -1 ? Infinity : i; }),
89
- );
90
- return at < firstQuote;
91
- }
92
-
93
- /**
94
- * Every mutation available in a source file: one per (line, operator) pair, first
95
- * occurrence on the line.
96
- *
97
- * @returns {Array<{line: number, op: string, before: string, after: string}>}
98
- */
99
- function mutationsFor(src) {
100
- const lines = src.split(/\r?\n/);
101
- const out = [];
102
- lines.forEach((line, i) => {
103
- for (const op of OPERATORS) {
104
- if (!mutableLine(line, op.find)) continue;
105
- out.push({ line: i + 1, op: op.id, before: line, after: line.replace(op.find, op.replace) });
106
- }
107
- });
108
- return out;
109
- }
110
-
111
- /** Apply one mutation to a source string, by line number. Throws if the line moved. */
112
- function applyMutation(src, mutation) {
113
- const eol = src.includes('\r\n') ? '\r\n' : '\n';
114
- const lines = src.split(/\r?\n/);
115
- const idx = mutation.line - 1;
116
- if (lines[idx] !== mutation.before) {
117
- throw new Error(`line ${mutation.line} is not what the mutation was built from`);
118
- }
119
- lines[idx] = mutation.after;
120
- return lines.join(eol);
121
- }
122
-
123
- /** Deterministic shuffle, so a run is reproducible from its seed. */
124
- function sample(items, n, seed) {
125
- let s = seed >>> 0;
126
- const rand = () => { s = (s * 1664525 + 1013904223) >>> 0; return s / 0x100000000; };
127
- const copy = items.slice();
128
- for (let i = copy.length - 1; i > 0; i--) {
129
- const j = Math.floor(rand() * (i + 1));
130
- [copy[i], copy[j]] = [copy[j], copy[i]];
131
- }
132
- return copy.slice(0, n);
133
- }
54
+ // The operators, the line filter, mutation and the seeded sample live in the shipped
55
+ // mutation report (pan-wizard-core/bin/lib/mutate.cjs, program V5), which a user's phase
56
+ // diff is measured with; this probe applies the JavaScript table to PAN itself.
57
+ const M = require('../pan-wizard-core/bin/lib/mutate.cjs');
58
+ const OPERATORS = M.OPERATORS.js;
59
+ const { mutableLine, applyMutation, sample } = M;
60
+ const mutationsFor = (src) => M.mutationsFor(src, { family: 'js' });
134
61
 
135
62
  function parseArgs(argv) {
136
63
  const a = { max: 20, seed: 1, json: false, targets: [], timeoutMs: 600000, help: false };
@@ -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
+ }
@@ -46,6 +46,12 @@ if (files.length === 0) {
46
46
  process.exit(1);
47
47
  }
48
48
 
49
- const nodeArgs = watch ? ['--test', '--watch', ...files] : ['--test', ...files];
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 under two seconds on a spawned process;
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 under two seconds',
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]) < 2000) out.push({ line: i + 1, text: raw.trim() });
154
+ if (m && Number(m[1]) <= 2000) out.push({ line: i + 1, text: raw.trim() });
155
155
  });
156
156
  return out;
157
157
  },