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.
Files changed (314) hide show
  1. package/CHANGELOG.md +3645 -0
  2. package/README.md +36 -48
  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 +1 -2
  8. package/agents/pan-document_code.md +1 -2
  9. package/agents/pan-hardener.md +2 -3
  10. package/agents/pan-integration-checker.md +1 -2
  11. package/agents/pan-knowledge.md +1 -2
  12. package/agents/pan-meta-reviewer.md +3 -3
  13. package/agents/pan-optimizer.md +4 -4
  14. package/agents/pan-phase-researcher.md +79 -488
  15. package/agents/pan-plan-checker.md +12 -2
  16. package/agents/pan-planner.md +10 -6
  17. package/agents/pan-previewer.md +1 -2
  18. package/agents/pan-project-researcher.md +70 -499
  19. package/agents/pan-release.md +1 -1
  20. package/agents/pan-reviewer.md +9 -3
  21. package/agents/pan-roadmapper.md +12 -7
  22. package/agents/pan-verifier.md +7 -2
  23. package/bin/install-lib.cjs +134 -14
  24. package/bin/install.js +59 -12
  25. package/commands/pan/army.md +1 -1
  26. package/commands/pan/autodev.md +76 -0
  27. package/commands/pan/exec-phase.md +3 -4
  28. package/commands/pan/focus-auto.md +14 -7
  29. package/commands/pan/focus-design.md +4 -5
  30. package/commands/pan/focus-docs.md +24 -0
  31. package/commands/pan/learn.md +13 -6
  32. package/commands/pan/milestone-audit.md +8 -1
  33. package/commands/pan/milestone-done.md +10 -5
  34. package/commands/pan/milestone-new.md +2 -2
  35. package/commands/pan/new-project.md +15 -12
  36. package/commands/pan/plan-phase.md +15 -9
  37. package/commands/pan/quick.md +1 -1
  38. package/commands/pan/review-deep.md +11 -5
  39. package/commands/pan/settings.md +6 -0
  40. package/commands/pan/verify-phase.md +6 -2
  41. package/docs/AGENTS.md +917 -0
  42. package/docs/ARCHITECTURE.md +1054 -0
  43. package/docs/CLI-REFERENCE.md +4582 -0
  44. package/docs/COMPARISON.md +116 -0
  45. package/docs/DEVELOPMENT.md +490 -0
  46. package/docs/ECOSYSTEM-REVIEW-2026-06.md +107 -0
  47. package/docs/ECOSYSTEM-REVIEW-2026-08.md +349 -0
  48. package/docs/EXAMPLES.md +584 -0
  49. package/docs/FAQ.md +254 -0
  50. package/docs/FIELD-HARVEST-2026-07.md +88 -0
  51. package/docs/FIELD-REPORT-army-2026-06.md +121 -0
  52. package/docs/HOOKS.md +638 -0
  53. package/docs/IMPROVEMENT-TODO.md +246 -0
  54. package/docs/INTERNALS.md +485 -0
  55. package/docs/MIGRATION-v2-to-v3.md +258 -0
  56. package/docs/SECURITY-HARDENING.md +230 -0
  57. package/docs/SKILLS-ENHANCEMENT-PLAN-V2.md +122 -0
  58. package/docs/SKILLS-FULL-TEXT.md +9685 -0
  59. package/docs/SKILLS-REFERENCE.md +1037 -0
  60. package/docs/TROUBLESHOOTING.md +1084 -0
  61. package/docs/USER-GUIDE.md +1437 -0
  62. package/docs/archive/SKILLS-ENHANCEMENT-PLAN.md +230 -0
  63. package/docs/audits/AUDIT-2026-08-e2e-ROADMAP.md +117 -0
  64. package/docs/audits/AUDIT-2026-08-e2e-VERIFICATION.md +693 -0
  65. package/docs/audits/AUDIT-2026-08-e2e.md +1546 -0
  66. package/docs/audits/OUTSTANDING-2026-08.md +275 -0
  67. package/docs/audits/STABILITY-TEST-2026-08.md +640 -0
  68. package/docs/branding/README-brand-draft.md +119 -0
  69. package/docs/branding/handoff/IMPLEMENTATION.md +205 -0
  70. package/docs/branding/handoff/README-brand-draft.md +119 -0
  71. package/docs/branding/handoff/image-prompts.md +46 -0
  72. package/docs/branding/image-prompts.md +46 -0
  73. package/docs/branding/nano-banana-prompts.txt +60 -0
  74. package/docs/context-monitor.md +13 -0
  75. package/docs/decisions/ADR-0001-strategic-feature-prioritization.md +83 -0
  76. package/docs/decisions/ADR-0002-copilot-cli-runtime.md +71 -0
  77. package/docs/decisions/ADR-0003-smart-execution.md +84 -0
  78. package/docs/decisions/ADR-0004-copilot-cli-interaction.md +89 -0
  79. package/docs/decisions/ADR-0005-command-naming.md +80 -0
  80. package/docs/decisions/ADR-0006-focus-commands.md +101 -0
  81. package/docs/decisions/ADR-0007-ship-v1.md +73 -0
  82. package/docs/decisions/ADR-0008-map-codebase-mermaid-togaf.md +80 -0
  83. package/docs/decisions/ADR-0009-production-deployment-checklist.md +68 -0
  84. package/docs/decisions/ADR-0010-standards-integration.md +79 -0
  85. package/docs/decisions/ADR-0011-help-commands-rewrite.md +64 -0
  86. package/docs/decisions/ADR-0012-contributors-attribution-index.md +42 -0
  87. package/docs/decisions/ADR-0013-strategic-feature-additions.md +53 -0
  88. package/docs/decisions/ADR-0014-internal-cleanup-code-quality.md +53 -0
  89. package/docs/decisions/ADR-0015-focus-auto-runner.md +296 -0
  90. package/docs/decisions/ADR-0016-enhanced-doc-sync.md +54 -0
  91. package/docs/decisions/ADR-0017-enhanced-focus-design.md +69 -0
  92. package/docs/decisions/ADR-0018-user-reality-testing.md +84 -0
  93. package/docs/decisions/ADR-0019-end-to-end-user-acceptance-testing.md +113 -0
  94. package/docs/decisions/ADR-0020-ai-drift-prevention.md +55 -0
  95. package/docs/decisions/ADR-0021-codebase-mapper-v2.md +76 -0
  96. package/docs/decisions/ADR-0022-lifecycle-completeness.md +63 -0
  97. package/docs/decisions/ADR-0023-opus-4-7-adoption.md +89 -0
  98. package/docs/decisions/ADR-0024-spec-b-v2-completion.md +137 -0
  99. package/docs/decisions/ADR-0025-behavioral-guardrails-layer.md +129 -0
  100. package/docs/decisions/ADR-0026-self-improvement-loop.md +171 -0
  101. package/docs/decisions/ADR-0027-doc-code-link-graph.md +164 -0
  102. package/docs/decisions/ADR-0028-unified-agents-skills-target.md +46 -0
  103. package/docs/decisions/ADR-0029-experiment-evaluator.md +76 -0
  104. package/docs/decisions/ADR-0030-planning-concurrency.md +37 -0
  105. package/docs/decisions/ADR-0031-focus-auto-backlog-source.md +47 -0
  106. package/docs/decisions/ADR-0032-squad-model.md +64 -0
  107. package/docs/decisions/ADR-0033-army-campaign.md +40 -0
  108. package/docs/decisions/ADR-0034-scheduled-campaigns.md +57 -0
  109. package/docs/decisions/ADR-0035-army-hud-dashboard.md +40 -0
  110. package/docs/decisions/ADR-0036-agentic-memory-architecture.md +106 -0
  111. package/docs/decisions/ADR-0037-pin-security-agents-to-opus.md +51 -0
  112. package/docs/decisions/ADR-0038-skill-aligned-decomposition.md +41 -0
  113. package/docs/decisions/ADR-0039-worktree-as-project-variant.md +37 -0
  114. package/docs/decisions/ADR-0040-memory-injection-defense.md +55 -0
  115. package/docs/decisions/ADR-0041-mcp-dual-era-bridge.md +47 -0
  116. package/docs/decisions/ADR-0042-design-parity-both-flows.md +45 -0
  117. package/docs/decisions/ADR-0043-addressable-planning-roots.md +56 -0
  118. package/docs/decisions/ADR-0044-bound-the-cached-context.md +104 -0
  119. package/docs/decisions/ADR-0045-agent-plugins-bundle.md +76 -0
  120. package/docs/decisions/ADR-0046-subagent-frontmatter-decisions.md +39 -0
  121. package/docs/decisions/ADR-0047-pan-harness.md +76 -0
  122. package/docs/decisions/ADR-0048-one-core-per-project.md +43 -0
  123. package/docs/decisions/ADR-0049-machine-verdicts-and-findings-ledger.md +116 -0
  124. package/docs/specs/ai_drift_prevention_featureai.md +286 -0
  125. package/docs/specs/codebase_mapper_v2_featureai.md +930 -0
  126. package/docs/specs/command_naming_optimization_featureai.md +930 -0
  127. package/docs/specs/comp_analysis_markdown_v1.md +149 -0
  128. package/docs/specs/continuous_autonomous_execution_featureai.md +304 -0
  129. package/docs/specs/contributors_attribution_index_featureai.md +476 -0
  130. package/docs/specs/copilot_cli_interaction_featureai.md +868 -0
  131. package/docs/specs/copilot_cli_runtime_featureai.md +647 -0
  132. package/docs/specs/custom-skills-assessment.md +461 -0
  133. package/docs/specs/deployment_audit_findings_featureai.md +243 -0
  134. package/docs/specs/doc_code_link_graph_featureai.md +253 -0
  135. package/docs/specs/documentation_alignment_cleanup_featureai.md +69 -0
  136. package/docs/specs/e2e_visual_testing_featureai.md +615 -0
  137. package/docs/specs/end_to_end_user_acceptance_testing_featureai.md +810 -0
  138. package/docs/specs/evidence-loop-plan.md +76 -0
  139. package/docs/specs/evidence_loop_featureai.md +497 -0
  140. package/docs/specs/focus-auto-optimize-category.md +449 -0
  141. package/docs/specs/focus-auto-optimize-superplan.md +188 -0
  142. package/docs/specs/focus_auto_runner_featureai.md +1102 -0
  143. package/docs/specs/focus_design_v2_featureai.md +197 -0
  144. package/docs/specs/focus_sync_v2_featureai.md +202 -0
  145. package/docs/specs/git_integration_overhaul_featureai.md +326 -0
  146. package/docs/specs/glm_zai_provider_featureai.md +200 -0
  147. package/docs/specs/googlecli_adoption_featureai.md +661 -0
  148. package/docs/specs/help_commands_rewrite_featureai.md +860 -0
  149. package/docs/specs/industry_analysis_strategic_additions_featureai.md +231 -0
  150. package/docs/specs/industry_comparison_and_feature_gaps_featureai.md +674 -0
  151. package/docs/specs/internal_cleanup_code_quality_featureai.md +486 -0
  152. package/docs/specs/investigations/README.md +31 -0
  153. package/docs/specs/investigations/digests/2026-09-28.md +47 -0
  154. package/docs/specs/investigations/digests/2026-10-08.md +62 -0
  155. package/docs/specs/investigations/digests/2026-10-10.md +61 -0
  156. package/docs/specs/investigations/features/debugger-red-loop-first.md +55 -0
  157. package/docs/specs/investigations/features/harness-cross-host-behaviour.md +93 -0
  158. package/docs/specs/investigations/features/optimize-apply-regression-gate.md +66 -0
  159. package/docs/specs/investigations/features/reviewer-smell-baseline.md +57 -0
  160. package/docs/specs/investigations/features/trace-failure-capture.md +69 -0
  161. package/docs/specs/lifecycle_completeness_featureai.md +754 -0
  162. package/docs/specs/local-first-ci.md +42 -0
  163. package/docs/specs/map_codebase_mermaid_togaf_featureai.md +805 -0
  164. package/docs/specs/market-delta-2026-09-superplan.md +310 -0
  165. package/docs/specs/market-ideas-2026-09.md +131 -0
  166. package/docs/specs/market-ideas-2026-10.md +428 -0
  167. package/docs/specs/market-ideas-ledger.md +177 -0
  168. package/docs/specs/memory-optimization-2026-10.md +285 -0
  169. package/docs/specs/multi-track-planning-roots.md +143 -0
  170. package/docs/specs/multi_model_routing_featureai.md +687 -0
  171. package/docs/specs/multi_model_routing_superplan.md +197 -0
  172. package/docs/specs/opus_47_existing_enhancements_featureai.md +247 -0
  173. package/docs/specs/opus_47_extended_features_featureai.md +363 -0
  174. package/docs/specs/opus_47_extended_features_v2_featureai.md +397 -0
  175. package/docs/specs/pan_focus_commands_featureai.md +2336 -0
  176. package/docs/specs/pan_zcode_mcp_bridge_featureai.md +120 -0
  177. package/docs/specs/product-excellence-superplan.md +315 -0
  178. package/docs/specs/product_excellence_program_2026_q4_featureai.md +365 -0
  179. package/docs/specs/production_deployment_readiness_featureai.md +433 -0
  180. package/docs/specs/production_readiness_ship_v1_featureai.md +698 -0
  181. package/docs/specs/real_test_enforcement_featureai.md +213 -0
  182. package/docs/specs/reality-check-2026-09-superplan.md +362 -0
  183. package/docs/specs/right-sized-ceremony.md +74 -0
  184. package/docs/specs/roadmap_and_competition_2026_q4_featureai.md +390 -0
  185. package/docs/specs/self_improvement_loop_featureai.md +875 -0
  186. package/docs/specs/skill-aligned-decomposition.md +155 -0
  187. package/docs/specs/standards_integration_featureai.md +833 -0
  188. package/docs/specs/testing-system-redesign-2026-09.md +267 -0
  189. package/docs/specs/user_reality_testing_system_featureai.md +788 -0
  190. package/docs/specs/watch-triggers.json +22 -0
  191. package/docs/specs/workflow-integration-redesign.md +742 -0
  192. package/docs/specs/workflow_state_consistency_featureai.md +183 -0
  193. package/hooks/dist/pan-context-monitor.js +9 -2
  194. package/hooks/dist/pan-cost-logger.js +5 -0
  195. package/hooks/dist/pan-guard.js +28 -5
  196. package/hooks/dist/pan-stop-guard.js +9 -9
  197. package/hooks/dist/pan-trace-logger.js +5 -0
  198. package/package.json +10 -2
  199. package/pan-wizard-core/bin/lib/agents-md.cjs +57 -8
  200. package/pan-wizard-core/bin/lib/autodev-report.cjs +180 -0
  201. package/pan-wizard-core/bin/lib/autodev.cjs +642 -0
  202. package/pan-wizard-core/bin/lib/ci.cjs +143 -0
  203. package/pan-wizard-core/bin/lib/commands.cjs +1 -1
  204. package/pan-wizard-core/bin/lib/config.cjs +31 -2
  205. package/pan-wizard-core/bin/lib/constants.cjs +27 -1
  206. package/pan-wizard-core/bin/lib/core.cjs +22 -10
  207. package/pan-wizard-core/bin/lib/cost.cjs +14 -4
  208. package/pan-wizard-core/bin/lib/cross-family.cjs +164 -0
  209. package/pan-wizard-core/bin/lib/debug-cases.cjs +104 -0
  210. package/pan-wizard-core/bin/lib/failure-class.cjs +81 -0
  211. package/pan-wizard-core/bin/lib/findings.cjs +34 -4
  212. package/pan-wizard-core/bin/lib/focus.cjs +1 -1
  213. package/pan-wizard-core/bin/lib/hosts.cjs +88 -0
  214. package/pan-wizard-core/bin/lib/init.cjs +7 -2
  215. package/pan-wizard-core/bin/lib/knowledge.cjs +2 -0
  216. package/pan-wizard-core/bin/lib/merge.cjs +50 -0
  217. package/pan-wizard-core/bin/lib/mutate.cjs +16 -5
  218. package/pan-wizard-core/bin/lib/next-action.cjs +112 -0
  219. package/pan-wizard-core/bin/lib/optimize.cjs +2 -2
  220. package/pan-wizard-core/bin/lib/phase.cjs +48 -4
  221. package/pan-wizard-core/bin/lib/plan-lint.cjs +150 -1
  222. package/pan-wizard-core/bin/lib/preview.cjs +23 -4
  223. package/pan-wizard-core/bin/lib/requirements-lint.cjs +130 -0
  224. package/pan-wizard-core/bin/lib/research-lint.cjs +150 -3
  225. package/pan-wizard-core/bin/lib/review-deep.cjs +14 -22
  226. package/pan-wizard-core/bin/lib/review-vote.cjs +105 -0
  227. package/pan-wizard-core/bin/lib/runner.cjs +2 -16
  228. package/pan-wizard-core/bin/lib/sizing.cjs +356 -0
  229. package/pan-wizard-core/bin/lib/squads.cjs +1 -1
  230. package/pan-wizard-core/bin/lib/state-write.cjs +247 -0
  231. package/pan-wizard-core/bin/lib/uat.cjs +149 -0
  232. package/pan-wizard-core/bin/lib/verdict.cjs +26 -3
  233. package/pan-wizard-core/bin/lib/verify-plan-set.cjs +30 -1
  234. package/pan-wizard-core/bin/lib/verify-preflight.cjs +2 -2
  235. package/pan-wizard-core/bin/lib/verify-red.cjs +37 -1
  236. package/pan-wizard-core/bin/lib/verify-retro.cjs +3 -2
  237. package/pan-wizard-core/bin/lib/verify.cjs +59 -4
  238. package/pan-wizard-core/bin/lib/worktree.cjs +70 -0
  239. package/pan-wizard-core/bin/pan-tools.cjs +168 -27
  240. package/pan-wizard-core/learnings/universal/autonomous-loop.md +2 -2
  241. package/pan-wizard-core/references/continuation-format.md +3 -3
  242. package/pan-wizard-core/references/guardrails.md +1 -1
  243. package/pan-wizard-core/references/model-profiles.md +2 -3
  244. package/pan-wizard-core/references/questioning.md +24 -0
  245. package/pan-wizard-core/references/ui-brand.md +4 -6
  246. package/pan-wizard-core/references/verdict-contract.md +2 -1
  247. package/pan-wizard-core/templates/config.json +5 -1
  248. package/pan-wizard-core/templates/debug.md +1 -0
  249. package/pan-wizard-core/templates/discovery.md +2 -2
  250. package/pan-wizard-core/templates/requirements.md +9 -0
  251. package/pan-wizard-core/workflows/add-phase.md +4 -6
  252. package/pan-wizard-core/workflows/assumptions.md +19 -1
  253. package/pan-wizard-core/workflows/diagnose-issues.md +2 -0
  254. package/pan-wizard-core/workflows/discuss-phase.md +4 -2
  255. package/pan-wizard-core/workflows/exec-lean.md +69 -0
  256. package/pan-wizard-core/workflows/exec-phase.md +36 -8
  257. package/{commands/pan → pan-wizard-core/workflows}/focus-doc-audit.md +13 -29
  258. package/{commands/pan → pan-wizard-core/workflows}/focus-drift-walking.md +3 -18
  259. package/{commands/pan → pan-wizard-core/workflows}/focus-exec.md +8 -23
  260. package/{commands/pan → pan-wizard-core/workflows}/focus-plan.md +6 -20
  261. package/{commands/pan → pan-wizard-core/workflows}/focus-scan.md +3 -16
  262. package/{commands/pan → pan-wizard-core/workflows}/focus-sync.md +7 -21
  263. package/pan-wizard-core/workflows/health.md +6 -0
  264. package/pan-wizard-core/workflows/help.md +31 -53
  265. package/pan-wizard-core/workflows/insert-phase.md +4 -6
  266. package/pan-wizard-core/workflows/learn.md +3 -3
  267. package/pan-wizard-core/workflows/milestone-audit.md +2 -2
  268. package/pan-wizard-core/workflows/milestone-new.md +38 -95
  269. package/pan-wizard-core/workflows/new-project.md +96 -258
  270. package/pan-wizard-core/workflows/optimize.md +107 -5
  271. package/pan-wizard-core/workflows/phase-tests.md +10 -0
  272. package/pan-wizard-core/workflows/plan-phase.md +86 -43
  273. package/pan-wizard-core/workflows/profile.md +50 -0
  274. package/pan-wizard-core/workflows/progress.md +10 -0
  275. package/pan-wizard-core/workflows/quick.md +34 -43
  276. package/pan-wizard-core/workflows/research-phase.md +118 -40
  277. package/pan-wizard-core/workflows/resume-project.md +18 -2
  278. package/pan-wizard-core/workflows/settings.md +21 -24
  279. package/pan-wizard-core/workflows/todo-check.md +6 -2
  280. package/pan-wizard-core/workflows/transition.md +22 -4
  281. package/pan-wizard-core/workflows/uat-walkthrough.md +90 -0
  282. package/pan-wizard-core/workflows/update.md +1 -1
  283. package/pan-wizard-core/workflows/verify-phase.md +2 -2
  284. package/scripts/build-agent-plugin.js +35 -15
  285. package/scripts/build-plugin.js +10 -1
  286. package/scripts/deprecate-old-versions.js +70 -3
  287. package/scripts/generate-skills-docs.py +1 -1
  288. package/scripts/git-hooks/pre-push +27 -0
  289. package/scripts/install-git-hooks.js +7 -5
  290. package/scripts/install-pre-push.cjs +38 -0
  291. package/scripts/land.cjs +151 -0
  292. package/scripts/local-ci.cjs +501 -0
  293. package/scripts/local-schedule.cjs +277 -0
  294. package/scripts/prompt-lint.cjs +32 -0
  295. package/scripts/prompt-rules.cjs +156 -0
  296. package/scripts/run-tests.cjs +7 -1
  297. package/scripts/test-quality-lint.cjs +3 -3
  298. package/scripts/test-surface.cjs +9 -2
  299. package/agents/pan-research-synthesizer.md +0 -246
  300. package/commands/pan/audit-deployment.md +0 -386
  301. package/commands/pan/dashboard.md +0 -22
  302. package/commands/pan/discord.md +0 -19
  303. package/commands/pan/mcp-bridge.md +0 -143
  304. package/commands/pan/milestone-cleanup.md +0 -19
  305. package/commands/pan/milestone-gaps.md +0 -35
  306. package/commands/pan/optimize.md +0 -115
  307. package/commands/pan/profile.md +0 -76
  308. package/commands/pan/research-phase.md +0 -189
  309. package/pan-wizard-core/bin/lib/bridge.cjs +0 -269
  310. package/pan-wizard-core/templates/research-project/architecture.md +0 -210
  311. package/pan-wizard-core/templates/research-project/features.md +0 -153
  312. package/pan-wizard-core/templates/research-project/pitfalls.md +0 -206
  313. package/pan-wizard-core/templates/research-project/stack.md +0 -126
  314. package/pan-wizard-core/templates/research-project/summary.md +0 -170
package/docs/HOOKS.md ADDED
@@ -0,0 +1,638 @@
1
+ # PAN Hook System
2
+
3
+ PAN ships a small set of built-in hooks that enhance the development experience — the ones in the table below, which is the whole set (`ls hooks/*.js`). Hooks are JavaScript files that execute in response to host lifecycle events; they were written for Claude Code and are also registered on Codex, Gemini CLI and Copilot CLI under each host's own event names (see [Hook Runtime Support](#hook-runtime-support)).
4
+
5
+ ## Built-in Hooks
6
+
7
+ | Hook | Event Type | Purpose |
8
+ |------|-----------|---------|
9
+ | `pan-statusline.js` | `statusLine` | Displays context window usage in the status bar and writes metrics to a bridge file |
10
+ | `pan-context-monitor.js` | `PostToolUse` | Reads the bridge file, or the session transcript when there is none, and injects a note into the agent's context when the host will compact it soon |
11
+ | `pan-check-update.js` | `SessionStart` | Checks for PAN updates in the background, caches result |
12
+ | `pan-guard.js` | `PreToolUse` (Gemini CLI: `BeforeTool`; Copilot CLI: `preToolUse`) | Checks each file a tool call is about to write: a credential in the written text, a direct write to PAN's own state (`state.md`, `findings.jsonl`, the token ledger), a judge agent writing outside the planning tree, a test file the phase in flight does not declare. `hooks.guard: report` (the default) logs each match to `.planning/guard.jsonl`; `enforce` also denies a credential write, a state write and a judge write. |
13
+ | `pan-cost-logger.js` | `SubagentStop` | Appends per-spawn cost records to `.planning/metrics/tokens.jsonl` — consumed by `/pan:cost` |
14
+ | `pan-trace-logger.js` | `SubagentStop` | Appends decision/error/redundancy events to `.planning/optimization/traces/<session>/trace.jsonl` — consumed by `/pan:learn` and `/pan:learn --optimize`. Auto-creates a day-scoped session if no explicit `optimize trace init` is active. |
15
+ | `pan-stop-guard.js` | `Stop` (Gemini CLI: `AfterAgent`; Copilot CLI: `agentStop`) | Blocks a session stop **once** when the auto-advance chain dropped at a phase boundary — autonomy armed on disk (`workflow.auto_advance` or `mode: yolo`), no failure/gaps/blocker recorded in `state.md`, roadmap phases unbuilt — and tells the agent to continue the chain (P-1809/P-1810/P-1812). |
16
+ | `pan-state-reinject.js` | `SessionStart`, matcher `compact` (Claude Code, Codex); `PreCompress` + `AfterTool` (Gemini CLI); `preCompact` + `postToolUse` (Copilot CLI) | After a context compaction, hands the model the phase, plan, status and stop point from `.planning/state.md` as `additionalContext`, so a summarised session resumes the work in flight instead of re-planning it. Silent without a current phase and an unbuilt roadmap phase; never writes into the project. |
17
+
18
+ ### pan-statusline.js
19
+
20
+ **Event:** `statusLine` (runs continuously to update the Claude Code status bar; Copilot CLI's experimental statusline too. Gemini CLI has no statusline command, so PAN writes none there.)
21
+
22
+ **What it does:**
23
+ 1. Reads the session's context window metrics from the JSON payload Claude Code pipes to stdin
24
+ 2. Formats a status line showing usage percentage
25
+ 3. Writes metrics to a bridge file at `<os-tmpdir>/pan-hooks-<uid>/claude-ctx-{session_id}.json` — a per-user `0700` subdirectory of the OS temp dir (`os.tmpdir()`), not literally `/tmp` on non-POSIX hosts such as Windows
26
+
27
+ The bridge file enables the context monitor to read metrics without coupling to the statusline's execution cycle.
28
+
29
+ When Claude Code's payload carries `rate_limits` (a subscription's 5-hour and 7-day usage windows), the status line also shows them (`5h 42% · 7d 81%`, red from 90%) and writes them, account-wide, to `<os-tmpdir>/pan-hooks-<uid>/claude-limits.json` (`{five_hour, seven_day, timestamp}`, each window `{used_percentage, resets_at}`). `pan-tools cost limits` reads that file, and `/pan:focus-auto` and `/pan:army` stop at `cost.weekly_limit_stop_pct`.
30
+
31
+ **Bridge file format:**
32
+ ```json
33
+ {
34
+ "session_id": "abc123",
35
+ "remaining_percentage": 28.5,
36
+ "used_pct": 89,
37
+ "total_input_tokens": 143000,
38
+ "context_window_size": 200000,
39
+ "model_id": "<model id>",
40
+ "timestamp": 1708200000
41
+ }
42
+ ```
43
+
44
+ > Note: `used_pct` is rescaled so 80% real usage displays as 100% — it will not equal `100 − remaining_percentage`.
45
+
46
+ ### pan-context-monitor.js
47
+
48
+ **Event:** `PostToolUse` (runs after every tool call)
49
+
50
+ **What it does:**
51
+ 1. Reads the bridge file written by the statusline hook, or, when there is no fresh bridge, the session transcript (below)
52
+ 2. Works out how much room is left before the host compacts the session
53
+ 3. If little is left, injects a note as `additionalContext` that the agent sees
54
+
55
+ **What it measures against.** Claude Code's percentages are against the full model window, but the host compacts earlier. With token counts, from the bridge or the transcript, the room left is measured against the point the host compacts at:
56
+ - **The window:** `CLAUDE_CODE_AUTO_COMPACT_WINDOW`, then `autoCompactWindow` in settings — each settings file in turn, local, then project, then user, gives its per-model value under `modelSettings`, else its value for every model, and the first file that sets one wins — then the model window.
57
+ - **The trigger:** the host compacts about 33K tokens short of that window, whatever its size. That gives the documented ~967K on a native 1M window. On `2026-10-04` a 100K window compacted after a call that measured 67,032 tokens, and not after one that measured 54,371 (harness `context-note-headless`). `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE` lowers the trigger to its percentage of the window when that is lower.
58
+ - **No compaction:** with `DISABLE_AUTO_COMPACT=1` or `autoCompactEnabled: false` the host never compacts, so the model window applies.
59
+
60
+ **What it says — no countdown.** The note carries no figure. Vendor guidance is that a visible remaining-context number makes a model wrap up early and cut corners, and a STOP order after a tool result reads like an injection. So the note:
61
+ - says it comes from the hook, not the user;
62
+ - asks for a checkpoint in `.planning/state.md` (`/pan:pause` writes it), or, in a project with no `.planning/state.md`, "wherever this project records its progress", with no `/pan:pause` hint;
63
+ - says compaction is safe, because PAN restores the planning state afterwards;
64
+ - asks for no shortcuts.
65
+
66
+ **Thresholds** (of the room before compaction):
67
+
68
+ | Level | Room left | The note asks for |
69
+ |-------|-----------|---------------|
70
+ | Normal | > 35% | No note |
71
+ | WARNING | <= 35% | A checkpoint at the next natural stopping point, then carry on at full quality |
72
+ | CRITICAL | <= 25% | A checkpoint before the next step, then finish the current task properly |
73
+
74
+ **Without the status line.** `claude -p` renders no status line (checked `2026-10-04`), and a user may run a status line of their own, so often there is no bridge. Then the monitor reads the transcript the payload names:
75
+ - **The tokens in context:** the newest assistant record's `input_tokens + cache_read_input_tokens + cache_creation_input_tokens`. That record issued the tool call the hook runs after, and the sum is what the status line reports as `context_window.total_input_tokens`. Only the tail of the file is read, and further back only while a large tool result hides the record (at most 4 MB).
76
+ - **After a compaction:** a `compact_boundary` record after the newest call means the context was just rebuilt, so there is no note until the next call measures it.
77
+ - **The window:** from the model id on that record, by the rules of Claude Code's model-config page. On the Anthropic API, the models it lists with a native 1M window get 1M; the hook keeps that list in `modelWindowFor()`. Opus and Sonnet models without a native 1M window have 200K unless their `[1m]` variant is configured (`model` in settings, or `ANTHROPIC_MODEL`), and Haiku has 200K. A context past 200K can only be a 1M window, and that rule comes first. Otherwise `CLAUDE_CODE_DISABLE_1M_CONTEXT=1` means 200K everywhere. On Bedrock, Google Cloud or Foundry a native-1M model's window is unknown, and an unknown window gives no note rather than a guess.
78
+ - **Subagents:** a tool call inside a subagent carries `agent_id`, and its payload still names the main transcript. The monitor measures the subagent's own file and never the main session's bridge: `agent_transcript_path` when the payload carries one, else `<session>/subagents/agent-<id>.jsonl`, or for a subagent the Workflow tool spawned, `<session>/subagents/workflows/<run>/agent-<id>.jsonl`. It debounces each subagent apart.
79
+ - **Codex and Copilot:** PAN does not register the monitor there. Codex has no status-line bridge and its transcript has another shape, and Copilot's documented payload is camelCase (`sessionId`, `transcriptPath`) while the monitor reads only `session_id`, so it could never measure on either; an upgrade removes an older install's entry. In a project that also has the Claude Code install, Copilot runs the `.claude/settings.json` registration, and what Copilot passes that one has not been observed.
80
+
81
+ A fresh bridge still wins for a main-thread call: it is the status line's own reading. The harness scenario `context-note-headless` checks the fallback in a real `claude -p` session.
82
+
83
+ **Debounce logic:**
84
+ - First warning fires immediately
85
+ - After a note, the next one comes no sooner than the fifth tool call after it (the four calls in between are silent)
86
+ - Severity escalation (WARNING → CRITICAL) bypasses debounce
87
+
88
+ **Safety:**
89
+ - Wrapped in try/catch — exits silently on error
90
+ - Never blocks tool execution
91
+ - Stale bridge metrics (>60s old) fall through to the transcript
92
+ - A missing bridge, transcript or model window means no note, never an error
93
+
94
+ ### pan-check-update.js
95
+
96
+ **Event:** `SessionStart` (registered without a matcher, so it runs at every session start the host reports — including the restart after a compaction — not only at startup)
97
+
98
+ **What it does:**
99
+ 1. Reads the installed PAN version from a `VERSION` file: the project's install first, then the `pan-wizard-core/VERSION` beside the hook's own copy (how a plugin install finds its version), then the global install
100
+ 2. Spawns a background process to query npm for the latest version
101
+ 3. Caches the result to `~/.claude/cache/pan-update-check.json` (on other runtimes the installer swaps in that runtime's config directory: `~/.gemini`, `~/.codex`, `~/.copilot`)
102
+ 4. The statusline reads the cached result for its update badge (on Gemini CLI and Codex, which run no PAN statusline, only `pan-tools version --check` reads it); the hook itself re-queries npm on every SessionStart, in a detached child, so the session is never blocked
103
+
104
+ The update check runs at each session start and doesn't block tool execution.
105
+
106
+ ### pan-cost-logger.js
107
+
108
+ **Event:** `SubagentStop` (runs when a Task-spawned sub-agent finishes)
109
+
110
+ **What it does:**
111
+ 1. No-ops unless the project already has a planning tree (`.planning/`, or the tree `PAN_PLANNING_DIR` / `PAN_TRACK` selects). A local install marker alone does not count: a globally-installed hook fires in every repo the user opens, and telemetry fills a planning tree but never creates one
112
+ 2. Parses the SubagentStop event payload on stdin and reads whichever of these the host supplies: `agent_type` / `subagent_type`, `agent_id`, `session_id`, `transcript_path`, `agent_transcript_path`, `cwd`, `usage.*`, `model`, `phase`, `command` (the trace logger also reads `exit_code`). Only the first few are reliably present — real payloads observed by PAN carried no `model` or `phase`, and `usage` is absent entirely in headless mode (see the transcript fallback below); the hook backfills `command`/`phase` from the active trace session (and, when the trace session names none — the day-scoped auto-session never does — `phase` from state.md's current phase and `command` from the latest `/pan:*` or `/pan-*` command turn in the parent transcript — or `exec-lean` when that turn is autodev's lean execute unit, whose prompt names the `exec-lean` workflow and no slash command, `LEAN_UNIT_RE`) and derives `model`/`tier` from the transcript
113
+ 3. Attributes tokens from the **subagent's own transcript** when the host names one — Claude Code's `transcript_path` is the *parent* session's file, and the subagent's conversation sits beside it as `<parent dir>/<session_id>/subagents/agent-<agent_id>.jsonl` — or, for a subagent the Workflow tool spawned (the native `/pan-*` scripts), one level down under its run: `subagents/workflows/<wf_id>/agent-<agent_id>.jsonl` — so `agent_id` (a documented common input field, present when the hook fires inside a subagent) resolves it; an explicit `agent_transcript_path` wins. Without an id it falls back to **this event's slice** of the parent transcript (the records past a per-transcript cursor) — see the per-agent transcripts note below; an id whose file is absent consumes nothing and records an unmeasured spawn (`token_source: "agent-transcript-missing"`, zeros). Within a slice, one API turn counts once: Claude Code writes a turn as one record per content block, each repeating the turn's `message.id` with a usage snapshot, and the last snapshot wins. Either slice's token axes are plausibility-clamped — above 500M cache-read or 10M output (the ceilings `aggregate()` quarantines on) or 20M input / 100M cache-write (hook-only sanity limits) a value is a session's cumulative usage, dropped to `0` with `clamped: true`. The span is written as measured; the reader quarantines a parent-transcript slice that ran longer than six hours (a row from the agent's own transcript is exempt)
114
+ 4. Appends a structured record to `.planning/metrics/tokens.jsonl` with `source: "hook"` to distinguish hook-sourced records from caller-appended ones, unless a [duplicate guard](#duplicate-and-re-fire-guards) drops it. It records nothing for the host's own helper agents (see [Host-internal helper agents](#host-internal-helper-agents))
115
+ 5. Silent on any error — never blocks the agent loop
116
+
117
+ **Record shape** — the row as the hook writes it (values illustrative; the `v: 3` predecessor was a dump against a two-record transcript fixture):
118
+ ```json
119
+ {
120
+ "v": 4,
121
+ "ts": "2026-08-06T10:21:22.465Z",
122
+ "agent": "pan-executor",
123
+ "agent_id": "a15f28b4fab5c68d1",
124
+ "command": null,
125
+ "model": "claude-opus-<version>",
126
+ "tier": "reasoning",
127
+ "input_tokens": 5120,
128
+ "output_tokens": 240,
129
+ "cache_read_tokens": 8000,
130
+ "cache_write_tokens": 500,
131
+ "cache_write_1h_tokens": 0,
132
+ "cache_write_5m_tokens": 500,
133
+ "cost_usd": null,
134
+ "duration_ms": 6789,
135
+ "phase": "07",
136
+ "session": "abc-123",
137
+ "source": "hook",
138
+ "token_source": "agent-transcript",
139
+ "clamped": false,
140
+ "event_sig": "15235bf7d229360296873bf0fadeb5f3f79703d4"
141
+ }
142
+ ```
143
+
144
+ Notes on the fields that are not self-evident:
145
+
146
+ - **`v`** — ledger row schema version, a literal in each hook (`SCHEMA_V`). Rows written before it existed carry no `v`; `cost rebuild` only checks that it is present, as one sign of a hook-written row it may replace. Readers in `pan-wizard-core` take a row field by field rather than switching on the version, so added fields are additive: a mixed-shape ledger aggregates as one. `v: 4` added `agent_id` and the `agent-transcript` token source.
147
+ - **`agent_id`** — the host's per-spawn id, `null` where the host supplies none. It is what makes the agent's own transcript addressable, and it tells two same-type siblings apart without hashing the payload.
148
+ - **`tier`** — derived from `model` (reasoning / mid / fast), `null` for a model the hook can't classify, so `/pan:cost`'s by-tier view is not blind on the hook path.
149
+ - **`cache_write_1h_tokens`** / **`cache_write_5m_tokens`** — the split of `cache_write_tokens` by cache lifetime (one-hour writes bill at a higher rate than five-minute ones), read from the `usage.cache_creation` block of the transcript (or of the payload's `usage` on the fallback path). Present only when the source carried that block; dropped when the write total was clamped.
150
+ - **`long_prompt`** — the tokens (`input_tokens`, `output_tokens`, `cache_read_tokens`, `cache_write_tokens`, `cache_write_1h_tokens`) of the requests in this slice whose prompt, cache reads and writes included, was over `above` (100,000) tokens. A model priced by prompt length bills those requests at its higher rates request by request, and a row sums many requests, so the read side prices this part at the model's long rate and the rest at its base rate. Present only when some request was over the threshold; dropped when any total was clamped. Transcript paths only; `cost rebuild` writes it too.
151
+ - **`duration_ms`** — the span of this event's transcript slice, first record timestamp to last. `null` when either bound is missing, never a fabricated `0`.
152
+ - **`token_source`** — `"agent-transcript"` when the counts came from the subagent's own transcript, `"agent-transcript-missing"` when the host named an agent whose file was not there (an unmeasured spawn: zeros, excluded from `calls` by the reader), `"transcript"` when they came from a slice of the parent session transcript (the fallback when the host names no agent), `"usage-fallback"` when there was no transcript at all and the payload's own `usage` was used instead.
153
+ - **`clamped`** — `true` when a plausibility guard dropped a token value to `0`, on any transcript or fallback path, so a guarded zero is distinguishable from a genuine zero-token run. Until v3.29 only the `usage-fallback` path had a guard, which is why every oversum row in the field carried `clamped: false`. The span is never clamped: `duration_ms` is the measured first-to-last record span, and `cost.cjs` quarantines a parent-slice row whose span exceeds six hours (a row sliced from the agent's own transcript is exempt — it cannot carry anyone else's usage).
154
+ - **`event_sig`** — a SHA-256 of the SubagentStop payload as delivered on stdin, cut to 40 hex characters (the length of the SHA-1 it replaced): this spawn's identity. A dual-registration re-fire is the same bytes and so carries the same signature; a sibling whose payload differs in any field carries a different one. It is what lets the hook count two parallel siblings as two spawns while still suppressing a re-fire of one of them. `null` if the payload could not be serialized. [Duplicate and re-fire guards](#duplicate-and-re-fire-guards) below describes how it is used.
155
+
156
+ **Integration:** records flow into the existing `cost.cjs` aggregator; they appear in `/pan:cost report` without additional configuration. The `source: "hook"` field marks automatic captures apart from `pan-tools cost append` caller-driven records, which carry no `source`: the aggregator counts both alike, and `cost rebuild` uses the field to find the rows it may replace. During a `/pan:army` campaign every squad agent fires `SubagentStop`, so this same per-spawn stream is what the `/pan:hud` dashboard aggregates into its per-squad telemetry — no army-specific instrumentation exists; the dashboard just reads `tokens.jsonl`.
157
+
158
+ **P-1805 transcript fallback:** Claude Code in **headless `claude -p` mode** does NOT include `data.usage` in the SubagentStop payload — it only ships `transcript_path`. When `data.usage` is missing/empty, the hook calls `readUsageFromTranscript(transcriptPath, sessionId)` which parses the JSONL transcript line-by-line, filters entries to the payload's `session_id` (the parent session's, so it does not isolate the subagent), and sums `usage` fields across all assistant messages. Falls back to zeros silently if the transcript is unreadable. Since then the order has reversed: `data.usage` proved to be a cumulative session counter, so the transcript is read whenever the payload names one, and `usage` is only the fallback when it names none (`token_source` `usage-fallback`). Since 2026-06 the same transcript pass also captures the assistant **model id** (`message.model`, last-seen wins) whenever the payload lacks one — `resolveRate()` prefix-matches versioned ids onto rate-table families so `/pan:cost` can price hook records. This was surfaced by the wookie autonomous build where every `tokens.jsonl` record showed zero tokens — per-agent cost attribution is now restored.
159
+
160
+ **Per-agent transcripts:** the transcript fallback above slices the file `transcript_path` names — and on `SubagentStop` that file is the **parent session's** transcript, shared by the main thread and every subagent. Slicing it per event attributed whatever the session had done since the previous stop to whichever subagent happened to stop next; the first stop on a long-lived session (cursor `0`, or a cursor a ledger quarantine had deleted) booked the session's entire history to one row — 7.5 billion cache-read tokens over a ten-day "duration" was observed in a field ledger — and siblings that stopped before the parent grew produced the complementary all-zero rows (63% of that ledger). Claude Code writes each subagent's own conversation beside the parent transcript as `<parent dir>/<session_id>/subagents/agent-<agent_id>.jsonl` (Workflow-tool subagents one level down, under `subagents/workflows/<wf_id>/`), whose first record carries the same `agentId` and `sessionId`; both hooks now derive that path from the payload's `agent_id` (or take an explicit `agent_transcript_path`) and slice *it*, keyed by its own cursor so a resumed agent is charged only its new lines. The parent-slice path remains for hosts that supply no id, now clamped like the fallback. One consequence stated plainly: the parent slice had also been the only place the **main thread's** usage entered the ledger; with per-agent attribution the hooks record subagent spawns and nothing else, which is what `/pan:cost` always claimed to count; `pan-tools cost rebuild` adds one `(main thread)` row per session from its transcript unless run with `--no-main-thread`.
161
+
162
+ **Runtime support:** Claude Code registers it via settings.json `SubagentStop`. Gemini CLI does not: it has no subagent-completion event, and it hands every hook the main session's transcript rather than the subagent's, so there is nothing to measure. (From v3.4 until `2026-09-23` PAN registered it there under `SubagentStop`, a key Gemini skips with an "Invalid hook event name" warning — it never ran.) Since 2026-06: Codex registers it in `.codex/hooks.json` (Claude-compatible `SubagentStop`) and Copilot CLI in `.github/hooks/pan.json` (`subagentStop`). PAN registers no hooks on OpenCode.
163
+
164
+ ### pan-trace-logger.js
165
+
166
+ **Event:** `SubagentStop` (runs alongside pan-cost-logger when a Task-spawned sub-agent finishes)
167
+
168
+ **What it does:**
169
+ 1. No-ops unless the project already has a planning tree — same gate as the cost logger, for the same reason
170
+ 2. Parses the SubagentStop event payload on stdin, reading the same fields with the same caveats (the `exec-lean` attribution of autodev's lean sessions included) — except that it takes `phase` only from the payload or the trace session, never from state.md (so in the day-scoped auto-session it is `null` unless the payload names one), and names an untyped agent from the host's `agent-<id>.meta.json` — and attributes tokens from this event's transcript slice (it keeps its **own** cursor — the cost logger fires on the same event and the two must not consume each other's slice)
171
+ 3. Calls `ensureSessionId()` — creates a day-scoped `sess_auto_YYYYMMDD` trace session if none active, so tracing is always-on with zero setup. A day-scoped auto-session from an earlier day is finalized and rolled over; an explicit session stays sticky while in use but is rolled over the same way once it has ended or gone quiet for a day
172
+ 4. Builds these event types:
173
+ - `decision:agent_completion` — per-agent record with input/output/cache tokens, agent name, phase, and the spawn's tool calls and failed tool results
174
+ - `redundancy:uncached_heavy_run` — fired when output > 3000 tokens with zero cache hits (signals repeated research the optimizer should flag)
175
+ - `error:tool_error` — one per distinct failed tool call in the subagent's own transcript (see [Tool-failure capture](#tool-failure-capture))
176
+ 5. Appends events to `.planning/optimization/traces/<session>/trace.jsonl`, unless a [duplicate guard](#duplicate-and-re-fire-guards) drops the batch
177
+ 6. Silent on error — never blocks the agent loop
178
+
179
+ **Record shape** — the completion event as the hook writes it, for the same spawn as the cost row above (the two hooks fire on one event, so the `event_sig` matches; values illustrative — in this auto-session the trace logger would write `phase: null` unless the payload named one, since it reads no state.md):
180
+ ```json
181
+ {
182
+ "v": 5,
183
+ "ts": "2026-08-06T10:21:22.554Z",
184
+ "session": "sess_auto_20260806",
185
+ "agent": "pan-executor",
186
+ "phase": "07",
187
+ "type": "decision",
188
+ "category": "agent_completion",
189
+ "description": "pan-executor completed",
190
+ "context": {
191
+ "model": "claude-opus-<version>",
192
+ "command": null,
193
+ "agent_id": "a15f28b4fab5c68d1",
194
+ "input_tokens": 5120,
195
+ "output_tokens": 240,
196
+ "cache_read_tokens": 8000,
197
+ "total_tokens": 5360,
198
+ "duration_ms": 6789,
199
+ "exit_code": 0,
200
+ "token_source": "agent-transcript",
201
+ "clamped": false,
202
+ "tool_calls": 14,
203
+ "tool_errors": 2,
204
+ "event_sig": "15235bf7d229360296873bf0fadeb5f3f79703d4"
205
+ },
206
+ "impact": "trivial",
207
+ "correction": null,
208
+ "tokens_wasted": null
209
+ }
210
+ ```
211
+
212
+ `v`, `agent_id`, `duration_ms`, `token_source`, `clamped`, and `event_sig` mean the same as on the cost row (see the field notes there); the trace event nests all of them but `v` under `context` and adds `total_tokens` (input + output). The trace logger resolves the agent transcript, dedupes block records by `message.id` and clamps the token axes exactly as the cost logger does, with its own cursor; a `clamped` completion never fires the `uncached_heavy_run` heuristic, since a guard-produced zero is not a cache miss. The `redundancy:uncached_heavy_run` event in the same batch carries the same `v`/`ts`/`session`/`agent`/`phase` envelope with a smaller `context` (`output_tokens`, `cache_read_tokens`) and `tokens_wasted` set.
213
+
214
+ `tool_calls` and `tool_errors` count the tool calls and the failed tool results in this spawn's own transcript slice. Both are `null` whenever the slice is not the agent's own, holds no new records, or capture is switched off (see the off switch below). The trace schema `v` is 5 since the evidence loop added them. The cost ledger's rows did not change and stay at 4.
215
+
216
+ #### Tool-failure capture
217
+
218
+ The optimiser learns from failures. Until the evidence loop, the only failures it saw were the ones an agent chose to report with `pan-tools optimize trace log`, and across the field projects that was none. Meanwhile about one subagent spawn in four had a failed tool call in its own transcript (measured 2026-09-28). The hook already reads that transcript for tokens, so it records the failed tool results from the same slice:
219
+
220
+ - **One event per distinct failure.** A failure is identified by its tool, its class and its message with the digits normalised. Each event carries a `count`, so a retry loop repeating one failure produces one event, not a flood. At most 10 events are written per spawn; the completion's `tool_errors` keeps the total. An event's `context` holds `tool`, `error_class`, `exit_code`, `message`, `message_sig` (a short hash of the redacted message with digits normalised), `count`, `command` and `agent_id`.
221
+ - **Classes.** `exit_code` (a Bash or PowerShell command exited non-zero, with `exit_code` set), `not_found`, `permission_denied`, `edit_precondition`, `network_or_timeout`, `user_rejected`, `tool_input`, `other`. For a shell failure the message is the first line after `Exit code <n>` that is not npm/yarn/pnpm's `> script` echo.
222
+ - **Redacted before it is written.** Some projects commit their traces, so the message loses bearer tokens, key/value secrets (`token=`, `password:`, `Authorization:`), known token prefixes (`sk-`, `ghp_`, `xox*-`, `AKIA…`, JWTs), long mixed letter-and-digit runs and URL query strings. The project directory becomes `.` and the home directory `~`. The message is capped at 160 characters.
223
+ - **Only where it is attributable.** Capture runs only on the agent-transcript path. A slice of the shared parent transcript would book the session's failures to whichever spawn stopped, so that path records none. A resumed agent is sliced from its cursor, so earlier failures are not counted twice.
224
+ - **Off switch.** `"execution": { "error_pattern_learning": false }` in `.planning/config.json` turns capture off. The key was shipped as "reserved"; this is its meaning.
225
+ - **Not an agent verdict.** A failed call is often a normal step, such as a TDD red run or a grep with no match. `optimize learn` groups failures per agent into `tool_error_patterns`, ranked by how many spawns they recur in (then by occurrences), with the session count alongside, and the optimiser draws a suggestion only from recurring ones (never a memory entry).
226
+
227
+ Judge verdicts (verifier, plan checker, reviewer, design checker) reach the trace another way. `pan-tools findings record`, which the workflows run to read the verdict they branch on, logs `verdict_passed`, `verdict_failed` or `verdict_needs_human`, plus `verdict_retry` when the same judge's previous verdict on that phase was a failure. That path works on every runtime, because it is a verb rather than a hook.
228
+
229
+ **Integration:** events flow into the existing `optimize.cjs` analyzer; they're picked up by `/pan:learn` (one session, or the last n pooled with `--sessions <n>`) and `/pan:learn --optimize` (cumulative stats; `apply` records a report's suggestions for a person to act on). The loop is trace → learn → optimize apply → a person writes the lesson where the agents read it → repeat; nothing in a report reaches an agent on its own.
230
+
231
+ **P-1805 transcript fallback:** Same fix as `pan-cost-logger.js`, with the same later reversal: the transcript is read whenever the payload names one and `data.usage` is only the fallback when it names none. `readUsageFromTranscript()` now reads the slice described above (the subagent's own transcript when one resolves, else the parent transcript at `data.transcript_path`) and sums `usage` once per API turn, the last snapshot per `message.id`, over the records whose `sessionId` (or `session_id`), where present, matches the payload's. Trace events now carry real token counts during autonomous runs instead of zeros. There is no timing fallback: `data.usage` carries no timing, so whenever no transcript slice is read, `duration_ms` stays `null` rather than a measured or fabricated span.
232
+
233
+ **Runtime support:** same surface as the cost logger — Claude via settings.json, Codex via `.codex/hooks.json`, Copilot via `.github/hooks/pan.json` (all on their SubagentStop-equivalent events; no-op on hosts that don't fire it). Not registered on Gemini CLI, for the cost logger's reason, or on OpenCode.
234
+
235
+ Tool-failure capture needs a per-agent transcript, so today it works on Claude Code only. On Copilot CLI both loggers read the camelCase payload the Copilot hooks reference documents: `sessionId`, `transcriptPath`, `agentId`, and `agentName`, which is the agent's configured name and wins over `agentType`, the kind of agent. Before that, every Copilot spawn was booked as `unknown`. Copilot has one session transcript and no per-subagent file, so its rows carry the agent's name, `token_source: agent-transcript-missing`, and no captured failures. This payload shape is documented, not yet observed live; the fixture says so. Codex fires `SubagentStop`, but its per-agent transcript layout is unverified, so no capture is claimed there.
236
+
237
+ **Why the loggers stay synchronous.** Claude Code's `async: true` would run a hook without blocking the session. The cost and trace loggers keep a read-modify-write cursor file shared by every stop, so overlapping asynchronous runs could lose each other's updates. A lost cursor entry makes the next stop re-read that transcript from the start and count its tokens twice. That is the defect class the per-agent attribution work fixed, so do not add `async` to these two (market item M16, declined 2026-09-28). The Codex registration does not follow this: there the installer gives both loggers `async: true` (`mergeCodexHooksConfig` in `bin/install-lib.cjs`).
238
+
239
+ ### Host-internal helper agents
240
+
241
+ Claude Code also fires `SubagentStop` for helper agents of its own. One is the compaction summariser; another runs at almost every turn end of an interactive session. Their payload carries an `agent_id`, an **empty** `agent_type`, and an `agent_transcript_path` that the host never writes. A real `/compact` payload captured on Claude Code 2.1.280 is the fixture `tests/fixtures/hooks/subagent-stop-internal-claude.json`.
242
+
243
+ The host documents `agent_type` as present whenever a hook fires inside a subagent. So a payload that names an agent instance with no type, and whose transcript does not exist, is not a spawn anyone asked for. Both loggers record nothing for it: no cost row, no trace event, and no trace session minted just for it.
244
+
245
+ Before this rule, each such payload was booked as an `unknown` zero-token spawn: 181 rows in three field projects between 2026-09-17 and 2026-09-28, which skewed the ledger's spawn counts and `optimize learn`'s agent stats. Rows written before the rule stay in the ledgers.
246
+
247
+ Two cases are deliberately still recorded. A **named** spawn whose file is late or missing keeps its zero-token `agent-transcript-missing` row (the sibling logic below depends on it). A payload that names no agent instance at all keeps the parent-slice path that other hosts use. The rule is `isHostInternalAgent()`, identical in both hooks.
248
+
249
+ ### Duplicate and re-fire guards
250
+
251
+ One `SubagentStop` can reach the hooks more than once. A project with **both** a global and a local PAN install registers two commands for the event (different paths, so the host runs both) and the two processes share one cursor file — the field's source of ~57% duplicate rows in 2026-07. At the same time PAN's own wave topology spawns **parallel siblings** that share the session transcript, so a legitimate second spawn can look a lot like a re-fire. Both hooks resolve this the same way, with two layers:
252
+
253
+ 1. **The per-transcript seen-event marker** (in the cursor file, bounded by `MAX_SEEN_SIGS` per transcript and `MAX_SEEN_TRANSCRIPTS` overall, FIFO on both axes). An event that consumed no new transcript records is treated as a re-fire only when its `event_sig` was already seen for that transcript; otherwise it is a sibling or a first fire and gets recorded with zero tokens.
254
+ 2. **The adjacent-row duplicate guard.** The new row / completion is compared against the **immediately preceding** ledger row (for the trace logger, the file's last `agent_completion`) across every field but `ts` — `event_sig` included. A true re-fire is the same bytes on stdin, so it produces the same row and matches; two siblings the payload can tell apart differ in their signature and both survive.
255
+
256
+ **What this guarantees, and what it does not.** A re-fire is suppressed while it is still *recognizable*: its signature sits in the marker window, or the row it duplicates sits immediately before it. Nothing further. A re-fire that arrives after its signature has been evicted from the marker window, and that is no longer adjacent to the row it duplicates, **is recorded** — a phantom row. The no-`transcript_path` path is a wider residual still: it has no transcript to key the marker layer by, so an interleaved dual registration there (A, B, A′, B′) leaves both re-fires out of adjacency reach and admits both. Each hook's suite pins the residual rather than asserting it away.
257
+
258
+ **Why the guard is not wider than that.** It was, briefly: the second layer scanned a tail of recent rows for an identical one and additionally treated a repeated `event_sig` as a re-fire whenever the candidate carried no tokens and no measured duration. Both halves delete real spawns. On the shared session transcript that PAN's own wave topology produces, sequential subagents of one type deliver byte-identical payloads — hence one signature — and identical rows whenever their slices happen to match; a run of five genuine spawns collapsed to two, and when those slices carried real usage the tokens went with them. The invariant the second condition rested on ("an event that consumed a real slice never looks contentless") is false as well: a slice of records carrying neither `usage` nor `timestamp` yields zeros and a `null` duration. Deleting real cost data is strictly worse than a phantom row, so the guard stays adjacent-only and the residual above is documented instead of engineered away. Each hook's suite carries the reproduction as a floor (`grep -n 'no genuine spawn' tests/cost-logger-hook.test.cjs tests/trace-logger.test.cjs`).
259
+
260
+ **One conditional worth knowing.** Two *concurrent same-type* siblings are admitted as two spawns only if the host puts some per-invocation field on the payload. PAN has confirmed that `agent_type`/`subagent_type` varies between siblings of different type, and that `session_id` and the transcript path are shared with the parent. Claude Code also sends `agent_id`, its documented per-spawn id (present in the captured `tests/fixtures/hooks/subagent-stop-claude.json`), which tells same-type siblings apart. On a host that sends no such id, no field is confirmed to vary between two same-type siblings: their payloads are the same bytes, a new spawn and a re-fire are indistinguishable, and the second is suppressed. See the `eventSignature` comment in either hook for the full evidence trail.
261
+
262
+ ### pan-state-reinject.js (market-ideas M10)
263
+
264
+ **Runtime support:** Claude Code (`SessionStart` with the `compact` matcher, in `settings.json` and the Claude plugin's `hooks/hooks.json`) and Codex (the same, in `.codex/hooks.json`, synchronous); Gemini CLI (`PreCompress` and `AfterTool` in `settings.json`) and Copilot CLI (`preCompact` and `postToolUse` in `.github/hooks/pan.json`) in two steps, below. Not in the Agent Plugins bundle's Copilot `hooks/hooks.json`. OpenCode runs no hooks; PAN's OpenCode plugin carries the same position into its compaction instead (next section).
265
+
266
+ **Why `SessionStart` and not `PostCompact`:** both hosts document `PostCompact`, and both discard its output — Claude Code lists it under "no decision control", and Codex's `PostCompact` output schema has no `additionalContext`. What each host does after a compaction is start the session again with `source: "compact"` and honour `hookSpecificOutput.additionalContext` from `SessionStart` hooks that match it. The hook also checks `source` itself, so a registration without the matcher stays silent on every other start.
267
+
268
+ **Gemini CLI and Copilot CLI (market-ideas M33):** neither starts the session again after a compaction — Gemini's `SessionStart` sources are `startup`, `resume` and `clear`, Copilot's `startup`, `resume` and `new` — and the event each fires before one cannot add context (Gemini's `PreCompress` is advisory, Copilot's `preCompact` notification-only). So the hook runs in two steps:
269
+
270
+ 1. `--mark` on `PreCompress` / `preCompact`: when a phase is in flight, it leaves a marker for this session and project, `state-reinject-<hash>.json`, named by a hash of the two so neither leaks into a file name. It goes in the per-user `0700` directory the stop guard and the context monitor use, `<os-tmpdir>/pan-hooks-<uid>/`, and is written `0600`. When that directory is not provably the user's (a link; on POSIX, another owner or group or other access) the hook writes nothing.
271
+ 2. `--inject gemini` on `AfterTool` (every tool, no matcher) / `--inject copilot` on `postToolUse`: if the session has a fresh marker, it consumes it, reads the planning tree again (the position after the compaction, not before it) and returns the block once — `hookSpecificOutput.additionalContext` on Gemini, a top-level `additionalContext` on Copilot. Both hosts append it to that tool's result. With no marker it prints nothing; what every other tool call pays is one Node start and a rename that fails.
272
+
273
+ A tool result is the first point after a compaction where both hosts take added context, and it also reaches an autonomous `-p` run that never sees another prompt. The marker is renamed before it is read, so two hook runs racing on one tool result cannot both inject; one no session came back for expires after six hours and is swept by the next `--mark`, which removes only this hook's files from the shared directory. In a project with both a Claude and a Copilot install, Copilot also runs Claude's registration of this script, but that one is the `SessionStart` mode, which a Copilot start never matches, so the hook needs none of the other Copilot hooks' deferral to the Claude copy.
274
+
275
+ Checked live on Copilot CLI 1.0.88 (`2026-10-03`): a manual `/compact` fired `preCompact` and left the marker, and the first tool call after it returned the block, which Copilot appended to the tool result as "Additional guidance from postToolUse hooks" and the model quoted back. Not yet checked live on Gemini CLI, whose test session could not sign in; the Gemini half rests on the v0.61.0 hooks reference and the unit tests.
276
+
277
+ **What it injects:** a short block — current phase and name, plan `N of M`, status, the `Stopped At` line (or the last activity when there is none), the resume file, and the first unticked roadmap phase — followed by an instruction to re-read `state.md` and the current plan before the next step. Each field is truncated on its own and the block stays under 2,000 characters, well inside the host's 10,000-character limit. Template placeholders (`[X]`) and `None` are not treated as values.
278
+
279
+ **When it is silent:** no `state.md`, no `Current Phase`, no `roadmap.md`, or every roadmap phase already ticked. It reads the planning tree the payload's `cwd` names (honouring `PAN_PLANNING_DIR`/`PAN_TRACK` like every PAN hook that reads the planning tree) and writes nothing into the project — the field sweep found hooks that scaffolded `.planning/` in projects that never ran PAN, and this one cannot. The Gemini and Copilot marker is the only file it writes, and it lives in the OS temp directory.
280
+
281
+ **Failure posture:** fail-open — malformed stdin, a payload without a session id (the marker modes), or an unreadable file exits 0 with no output.
282
+
283
+
284
+ ### The OpenCode plugin: pan-wizard.js (memory optimisation O12)
285
+
286
+ **Where:** `<opencode config dir>/plugins/pan-wizard.js`, written by a local or global OpenCode install from `pan-wizard-core/opencode/pan-wizard.js`. It is tracked in the manifest and removed on uninstall, which keeps any plugins of your own. It is CommonJS: the `{"type":"commonjs"}` `package.json` PAN writes in the config directory, one level above `plugins/`, marks it as such for a loader that honours the `type` field. It exports one object both OpenCode loaders accept, `{ id: "pan-wizard", server, setup }`: OpenCode 1 calls `server()`, and OpenCode 2, whose loader refuses a plugin without `setup` or `effect`, calls `setup()`, which registers the same hand-off through `ctx.session.hook("compaction", …)` and pushes the block onto the event's `system` parts (or `context`).
287
+
288
+ **What it does:** OpenCode runs no command hooks, so `pan-state-reinject.js` cannot run there. A plugin's `experimental.session.compacting` hook fires before OpenCode writes a session's continuation summary, and strings pushed onto `output.context` go into that prompt. The plugin pushes the same position the hook gives the other hosts: phase, plan, status, stopping point, and first unbuilt phase. It reads them from the planning tree on disk, honours `PAN_PLANNING_DIR` / `PAN_TRACK`, and asks for `state.md` to be re-read after the compaction.
289
+
290
+ **Safety:** it is inert without a current phase and an unbuilt roadmap phase. It never writes, and fails open: an error adds nothing and never breaks a compaction.
291
+
292
+ **Checked on OpenCode `1.18.32` (`2026-10-04`, no model call):**
293
+ - `opencode debug config` lists the plugin, and `live-gate-opencode` repeats that check.
294
+ - An instrumented copy showed OpenCode importing the module, calling `server()` with the project directory, and receiving the compaction hook.
295
+
296
+ `tests/opencode-plugin.test.cjs` pins what the hook pushes onto `output.context`, calling it directly with a stand-in `output`; no real compaction runs in that test.
297
+
298
+ ### pan-stop-guard.js (P-1809)
299
+
300
+ **Runtime support:** Claude Code (`Stop` in `settings.json`, and in the Claude plugin's `hooks/hooks.json`), Codex (`Stop` in `.codex/hooks.json`, synchronous — an `async` handler cannot block), Copilot CLI (`agentStop` in `.github/hooks/pan.json`) and Gemini CLI (`AfterAgent` in `settings.json` — Gemini's end-of-turn event, which re-prompts the agent with the reason on a block exactly as Claude's `Stop` does). Codex and Copilot hand the hook `stop_hook_active` like Claude Code (Copilot keeps that one field snake_case inside its camelCase payload), accept the same `{"decision":"block","reason":…}` output, and turn a block into a continuation prompt; Copilot blocks only on that JSON form, not on exit code 2, and ends the turn itself after eight consecutive blocks. Gemini sets `stop_hook_active` only for a continuation the block started directly, not after one that used tools, so there the guard keeps its one-shot promise with a marker per session, project and target phase in the per-user `0700` hook directory; a second stop aimed at the same phase is allowed. Until `2026-09-23` PAN registered it on Gemini under `Stop`, which Gemini skips. Codex and Copilot gained it on `2026-09-26`; the Claude plugin build carried every hook but this one until the same date. Not registered for OpenCode.
301
+
302
+ **Event:** `Stop` on Claude Code and Codex, `agentStop` on Copilot CLI, `AfterAgent` on Gemini CLI (runs when the main session tries to end its turn)
303
+
304
+ **What it does:** catches the auto-advance boundary drop mechanically. Field runs showed autonomous chains ending between transition.md's state update and the next-phase Task spawn at a low, nondeterministic rate — a failure prose instructions can reduce but not eliminate. When the session stops, this hook blocks **once**, with a reason instructing the agent to spawn the next phase, if and only if the disk shows the exact drop fingerprint:
305
+
306
+ 1. `.planning/config.json` shows an autonomy signal: `workflow.auto_advance: true` **or** `mode: "yolo"` (P-1810). The guard is **inert** in every other project. The chain's trigger is flag OR config OR yolo; a Stop hook cannot see the `--auto` flag, so every entry hop (discuss/plan/exec-phase) persists the flag into config — arming on config alone left the guard dark on a real flag-driven drop (PanLoop finding 7). transition.md's Route B clears `auto_advance` at the milestone boundary, which disarms that half at the true end of a chain.
307
+ 2. `.planning/state.md` shows **no legitimate-stop marker** (gaps found, failed verification, a recorded blocker). This condition is deliberately inverted (P-1812): the guard originally required the status to read "Ready to plan", and one field batch produced four different phrasings for the same boundary — the only run with unbuilt phases was disarmed by wording alone. An unrecognised phrasing now **arms** the guard (fail-safe: at worst one extra continuation, bounded by the one-shot design) instead of disarming it (fail-open: silent truncation). Genuine gaps/verification/blocker stops still pass through.
308
+ 3. `.planning/roadmap.md` still has unticked `- [ ] **Phase N:` checklist lines.
309
+
310
+ **Loop safety:** the host sets `stop_hook_active` on stop attempts that follow a stop-hook block, and the guard always allows those. It fires at most once per stop chain — a user who genuinely wants to stop is delayed by exactly one continuation, never trapped. The block reason tells an agent that was asked to stop to end its turn again, and never to change `workflow.auto_advance`, `workflow.stop_guard` or `mode` to get past the guard: those are the user's settings and every later session reads them. To stop at each phase as a standing choice, change them yourself: Auto-advance in `/pan:settings`, or the escape hatch below, which also disarms a `mode: "yolo"` project.
311
+
312
+ **Escape hatch:** `workflow.stop_guard: false` in `.planning/config.json` disables the guard without disarming auto-advance.
313
+
314
+ **Failure posture:** fail-open everywhere — missing `.planning/`, unparseable config/state/roadmap, or malformed stdin all allow the stop silently. A missed catch costs a re-run; a wrong block traps a session, so the guard never blocks on uncertainty.
315
+
316
+ ### pan-guard.js (program A7)
317
+
318
+ **Runtime support:** Claude Code and Codex (`PreToolUse`, synchronous on Codex: an `async` handler cannot deny), Gemini CLI (`BeforeTool`) and Copilot CLI (`preToolUse`), plus the Claude plugin's `hooks/hooks.json`. Not OpenCode. The command carries `--host <runtime>`, because each host's payload and its way of denying a call differ; a camelCase Copilot payload (`toolName`, `toolArgs`) gets Copilot's deny whatever the flag says, because Copilot also runs the hooks in `.claude/settings.json`. In a project with both installs the `.github` copy steps aside for the Claude registration, as the other hooks do. Live behaviour on Gemini is unproven without a key.
319
+
320
+ **What it does:** reads the tool call (`tool_name` and `tool_input` on Claude, Codex and Gemini; `toolName` and the JSON string `toolArgs` on Copilot; Codex's `apply_patch` text for the files a patch adds or updates) and checks each file about to be written:
321
+
322
+ - **secret_write**: the written text holds a credential, a known token shape (`ghp_`, `sk-ant-`, `AKIA`, a private key block …) or a quoted key=value secret (`api_key`, `password`, `client_secret` …) of twelve or more characters, mixing letters and digits, that is not a placeholder (`process.env`, `${VAR}`, `your-…`); unquoted `.env`-style values are not matched;
323
+ - **state_write**: a direct write to `state.md`, `findings.jsonl` or `metrics/tokens.jsonl`, which pan-tools owns (state.md through `pan-tools state init|section|quick-task|apply|patch|update`);
324
+ - **judge_write**: a judge (`pan-verifier`, `pan-reviewer`, `pan-plan-checker`, `pan-design-checker`) writing outside the planning tree. It needs the agent's name in the payload (`agent_type`, which Claude Code sends inside a subagent); where the host sends none, the rule never matches;
325
+ - **undeclared_test_edit**: a test file that no unfinished plan of the phase in flight (`**Current Phase:**` in state.md) lists in `files_modified`.
326
+
327
+ **Modes** (`hooks.guard` in `.planning/config.json`): `report` (the default) appends each match to `.planning/guard.jsonl` and lets the call run; `enforce` also denies a secret write, a state write and a judge write, with exit 2 and the reason on stderr on Claude Code, Codex and Gemini, or Copilot's `{"permissionDecision": "deny", "permissionDecisionReason": …}` output; `off` disables it. Under `enforce` the test rule still only reports. The state rule denies since program A7b moved every shipped writer of state.md onto pan-tools verbs; a denied agent is told which verb to use.
328
+
329
+ **Failure posture:** inert outside a PAN project; fail-open on a payload it cannot parse; never fail-open on a matched deny. It writes nothing but its log line.
330
+
331
+ ## Architecture
332
+
333
+ ```text
334
+ Statusline Hook (pan-statusline.js)
335
+ | writes
336
+ v
337
+ <os-tmpdir>/pan-hooks-<uid>/claude-ctx-{session_id}.json (bridge file — not literally /tmp)
338
+ ^ reads (main thread, while fresh)
339
+ |
340
+ Context Monitor (pan-context-monitor.js, PostToolUse) --- reads, without a fresh bridge ---> the session transcript
341
+ | (a subagent's own file for its calls)
342
+ | injects
343
+ v
344
+ additionalContext → Agent sees warning → /pan:pause
345
+
346
+ SessionStart
347
+ |
348
+ v
349
+ Check Update (pan-check-update.js)
350
+ | writes cache
351
+ v
352
+ ~/.claude/cache/pan-update-check.json
353
+
354
+ Tool call about to write a file (PreToolUse; Gemini BeforeTool, Copilot preToolUse)
355
+ |
356
+ v
357
+ Guard (pan-guard.js)
358
+ | appends each match → .planning/guard.jsonl; under hooks.guard: enforce, denies a credential, state or judge write
359
+
360
+ Sub-agent finishes
361
+ |
362
+ +--> Cost Logger (pan-cost-logger.js, SubagentStop)
363
+ | | appends record
364
+ | v
365
+ | .planning/metrics/tokens.jsonl ← consumed by /pan:cost
366
+ |
367
+ +--> Trace Logger (pan-trace-logger.js, SubagentStop)
368
+ | appends decision/redundancy/error events
369
+ v
370
+ .planning/optimization/traces/<session>/trace.jsonl ← consumed by /pan:learn, /pan:learn --optimize
371
+
372
+ Session restarts after a compaction (Claude Code / Codex SessionStart, matcher compact)
373
+ |
374
+ v
375
+ State Re-inject (pan-state-reinject.js, SessionStart)
376
+ | reads .planning/state.md + roadmap.md → additionalContext
377
+
378
+ Before a compaction (Gemini PreCompress, Copilot preCompact)
379
+ |
380
+ v
381
+ State Re-inject --mark → <os-tmpdir>/pan-hooks-<uid>/state-reinject-<session+project hash>.json
382
+ ^ consumes (once)
383
+ |
384
+ State Re-inject --inject (Gemini AfterTool, Copilot postToolUse)
385
+ | reads .planning/state.md + roadmap.md → additionalContext on that tool result
386
+
387
+ Session stop (Claude Code / Codex Stop, Copilot agentStop, Gemini AfterAgent)
388
+ |
389
+ v
390
+ Stop Guard (pan-stop-guard.js, Stop / agentStop / AfterAgent)
391
+ | at an auto-advance boundary: block once with a continue reason; otherwise allow
392
+ ```
393
+
394
+ The hooks communicate through files rather than being directly coupled:
395
+ - Any hook can fail independently without breaking the others
396
+ - Bridge and log files are inspectable for debugging
397
+ - No shared memory or IPC needed
398
+
399
+ ## Installation
400
+
401
+ Hooks are automatically installed and registered during `npx pan-wizard`. The block below is Claude Code's `settings.json` with the command paths shortened: a local install (the default) writes project-relative commands such as `node .claude/hooks/pan-check-update.js`, and a `--global` install writes the config directory's absolute path, quoted:
402
+
403
+ ```json
404
+ {
405
+ "statusLine": {
406
+ "type": "command",
407
+ "command": "node ~/.claude/hooks/pan-statusline.js"
408
+ },
409
+ "hooks": {
410
+ "SessionStart": [
411
+ {
412
+ "hooks": [
413
+ {
414
+ "type": "command",
415
+ "command": "node ~/.claude/hooks/pan-check-update.js"
416
+ }
417
+ ]
418
+ },
419
+ {
420
+ "matcher": "compact",
421
+ "hooks": [
422
+ {
423
+ "type": "command",
424
+ "command": "node ~/.claude/hooks/pan-state-reinject.js"
425
+ }
426
+ ]
427
+ }
428
+ ],
429
+ "PreToolUse": [
430
+ {
431
+ "hooks": [
432
+ {
433
+ "type": "command",
434
+ "command": "node ~/.claude/hooks/pan-guard.js --host claude"
435
+ }
436
+ ]
437
+ }
438
+ ],
439
+ "PostToolUse": [
440
+ {
441
+ "hooks": [
442
+ {
443
+ "type": "command",
444
+ "command": "node ~/.claude/hooks/pan-context-monitor.js"
445
+ }
446
+ ]
447
+ }
448
+ ],
449
+ "SubagentStop": [
450
+ {
451
+ "hooks": [
452
+ {
453
+ "type": "command",
454
+ "command": "node ~/.claude/hooks/pan-cost-logger.js"
455
+ }
456
+ ]
457
+ },
458
+ {
459
+ "hooks": [
460
+ {
461
+ "type": "command",
462
+ "command": "node ~/.claude/hooks/pan-trace-logger.js"
463
+ }
464
+ ]
465
+ }
466
+ ],
467
+ "Stop": [
468
+ {
469
+ "hooks": [
470
+ {
471
+ "type": "command",
472
+ "command": "node ~/.claude/hooks/pan-stop-guard.js"
473
+ }
474
+ ]
475
+ }
476
+ ]
477
+ }
478
+ }
479
+ ```
480
+
481
+ ## Build Pipeline
482
+
483
+ Hook sources live alongside the dist directory:
484
+
485
+ ```text
486
+ hooks/
487
+ pan-statusline.js # Source
488
+ pan-context-monitor.js # Source
489
+ pan-check-update.js # Source
490
+ pan-guard.js # Source
491
+ pan-cost-logger.js # Source
492
+ pan-trace-logger.js # Source
493
+ pan-stop-guard.js # Source
494
+ pan-state-reinject.js # Source
495
+ dist/ # Copied output (installed to user's machine)
496
+ pan-statusline.js
497
+ pan-context-monitor.js
498
+ pan-check-update.js
499
+ pan-guard.js
500
+ pan-cost-logger.js
501
+ pan-trace-logger.js
502
+ pan-stop-guard.js
503
+ pan-state-reinject.js
504
+ ```
505
+
506
+ Build command:
507
+ ```bash
508
+ npm run build:hooks # Copies hooks from hooks/ to hooks/dist/
509
+ ```
510
+
511
+ The build script (`scripts/build-hooks.js`) simply copies the files named in its `HOOKS_TO_COPY` list (a new hook must be added there) — no bundling or transpilation needed since hooks are pure Node.js with zero dependencies.
512
+
513
+ ## Hook Runtime Support
514
+
515
+ | Runtime | Hooks supported | Notes |
516
+ |---------|----------------|-------|
517
+ | Claude Code | Yes | Full support via settings.json hook registration, including the state re-injection on `SessionStart` with the `compact` matcher |
518
+ | Copilot CLI | Yes | `.github/hooks/pan.json` (version 1 schema: sessionStart, preToolUse, postToolUse, preCompact, subagentStop, agentStop — the state re-injection's two steps on `preCompact` and `postToolUse`). No context monitor: Copilot's payload names the session `sessionId`, which the monitor never read, so it measured nothing and is no longer registered. Copilot also runs the hooks in the project's `.claude/settings.json`, so in a project with both installs the Copilot copy of each hook steps aside for the Claude registration and each runs once. The pre-tool guard steps aside the same way, and the copy that runs reads Copilot's camelCase payload and denies with Copilot's JSON decision even though its registration says `--host claude`. The state re-injection is the exception: its Claude registration is the `SessionStart` mode a Copilot start never matches, so its Copilot copy runs. Headless (`copilot -p`) Copilot loads repository hooks only in a trusted folder, or with `COPILOT_ALLOW_ALL=true` or `GITHUB_COPILOT_PROMPT_MODE_REPO_HOOKS=true` — see TROUBLESHOOTING |
519
+ | OpenCode | No (one plugin) | PAN registers no hooks there. Its plugin, `.opencode/plugins/pan-wizard.js`, adds PAN's position to the compaction prompt |
520
+ | Gemini CLI | Partly | settings.json, in Gemini's own event names: `SessionStart` runs the update check, `BeforeTool` the guard, `AfterAgent` the stop guard, and `PreCompress` and `AfterTool` the state re-injection's two steps. No context monitor (no Gemini hook payload or setting carries context-window usage, and Gemini has no statusline command) and no cost or trace logger (no subagent-completion event). Until `2026-09-23` PAN wrote Claude's names here — `PostToolUse`, `SubagentStop`, `Stop` — which Gemini skips, with an "Invalid hook event name" warning, whenever it loads the settings |
521
+ | Codex | Yes | `.codex/hooks.json` since 2026-06 (Claude-compatible PascalCase events; loads once the project is trusted). PAN registers these hooks there — update check, the guard on `PreToolUse` (synchronous), cost and trace loggers, the stop guard on `Stop`, and the state re-injection on `SessionStart` with the `compact` matcher; the observers (update check, cost and trace loggers) carry `async: true` (Codex CLI 0.148+) while the stop guard and the re-injection stay synchronous, because an async handler's output is deferred to a later turn and it cannot block. Codex runs a non-managed hook only after you trust it, and records that trust against a hash of the hook, so a new or changed PAN hook is skipped until you review it in `/hooks` — see TROUBLESHOOTING. No context monitor: Codex has no statusline, and its transcript has another shape than the Claude Code records the monitor reads, so it could never warn there; PAN stopped registering it, and an upgrade removes an older install's entry |
522
+
523
+ PAN registers hooks on Claude Code, Gemini CLI, Codex, and Copilot CLI. It registers none on OpenCode, where one PAN plugin keeps the phase in flight through a compaction.
524
+
525
+ ## Developing Custom Hooks
526
+
527
+ ### Hook Input
528
+
529
+ Hooks receive JSON on stdin with context about the event:
530
+
531
+ **PostToolUse:**
532
+ ```json
533
+ {
534
+ "session_id": "abc123",
535
+ "tool_name": "Bash",
536
+ "tool_input": { "command": "npm test" }
537
+ }
538
+ ```
539
+
540
+ ### Hook Output
541
+
542
+ Hooks can return JSON on stdout. For `PostToolUse`, the payload is a nested envelope:
543
+
544
+ ```json
545
+ {
546
+ "hookSpecificOutput": {
547
+ "hookEventName": "PostToolUse",
548
+ "additionalContext": "PAN context note (from the context-monitor hook, not the user): this session's context is filling up."
549
+ }
550
+ }
551
+ ```
552
+
553
+ The `additionalContext` field (nested under `hookSpecificOutput`) injects text into the agent's conversation — this is the exact shape the context monitor emits to communicate warnings.
554
+
555
+ ### Worked Example: Phase Completion Logger
556
+
557
+ A simple PostToolUse hook that detects phase completions and logs them.
558
+
559
+ **1. Create the hook** (`hooks/my-phase-logger.js`):
560
+
561
+ ```javascript
562
+ #!/usr/bin/env node
563
+ 'use strict';
564
+
565
+ try {
566
+ let input = '';
567
+ process.stdin.on('data', (chunk) => { input += chunk; });
568
+ process.stdin.on('end', () => {
569
+ let data; try { data = JSON.parse(input); } catch { process.exit(0); }
570
+
571
+ // Only care about Bash tool calls that look like phase-complete
572
+ if (data.tool_name !== 'Bash') return;
573
+ const cmd = (data.tool_input && data.tool_input.command) || '';
574
+ if (!cmd.includes('phase complete')) return;
575
+
576
+ // Log to a file (hooks can't write to the agent's output directly)
577
+ const fs = require('fs');
578
+ const os = require('os');
579
+ const path = require('path');
580
+ const timestamp = new Date().toISOString();
581
+ fs.appendFileSync(path.join(os.tmpdir(), 'pan-phase-log.txt'),
582
+ `${timestamp} | Phase completed | ${cmd}\n`
583
+ );
584
+
585
+ // Optionally inject context back to the agent
586
+ const result = { hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: 'Phase completion logged.' } };
587
+ process.stdout.write(JSON.stringify(result));
588
+ });
589
+ } catch (e) {
590
+ // Silent exit — never break the agent
591
+ process.exit(0);
592
+ }
593
+ ```
594
+
595
+ **2. Test locally:**
596
+
597
+ ```bash
598
+ echo '{"tool_name":"Bash","tool_input":{"command":"phase complete 3"}}' | node hooks/my-phase-logger.js
599
+ # Should output: {"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"Phase completion logged."}}
600
+ ```
601
+
602
+ **3. Register in settings.json:**
603
+
604
+ ```json
605
+ {
606
+ "hooks": {
607
+ "PostToolUse": [
608
+ {
609
+ "hooks": [
610
+ {
611
+ "type": "command",
612
+ "command": "node ~/.claude/hooks/my-phase-logger.js"
613
+ }
614
+ ]
615
+ }
616
+ ]
617
+ }
618
+ }
619
+ ```
620
+
621
+ **4. Copy to install location:**
622
+
623
+ ```bash
624
+ cp hooks/my-phase-logger.js ~/.claude/hooks/
625
+ ```
626
+
627
+ **Debugging tips:**
628
+ - Use `process.stderr.write()` for debug logging (stderr doesn't affect hook output)
629
+ - Check `<os-tmpdir>/pan-hooks-<uid>/` for bridge files if your hook writes there
630
+ - Test with `echo '{}' | node your-hook.js` to verify it handles empty input gracefully
631
+
632
+ ### Best Practices
633
+
634
+ 1. **Always wrap in try/catch** — A broken hook should never break the agent's workflow
635
+ 2. **Exit quickly** — Hooks run synchronously before/after tool calls. Keep execution fast.
636
+ 3. **No side effects on failure** — If your hook can't read its data, exit silently
637
+ 4. **Use the bridge file pattern** — For hooks that need to share data, write to a per-user directory under `os.tmpdir()` (PAN uses `pan-hooks-<uid>`, mode 0700) and read from there
638
+ 5. **Test independently** — Hooks are standalone Node.js scripts. Test with `echo '{}' | node your-hook.js`