pan-wizard 3.31.0 → 3.33.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 (87) hide show
  1. package/README.md +28 -24
  2. package/agents/pan-conductor.md +2 -2
  3. package/agents/pan-design-checker.md +18 -0
  4. package/agents/pan-document_code.md +2 -2
  5. package/agents/pan-executor.md +3 -3
  6. package/agents/pan-experiment-runner.md +7 -10
  7. package/agents/pan-hardener.md +15 -15
  8. package/agents/pan-knowledge.md +1 -1
  9. package/agents/pan-meta-reviewer.md +2 -2
  10. package/agents/pan-optimizer.md +7 -3
  11. package/agents/pan-phase-researcher.md +2 -2
  12. package/agents/pan-plan-checker.md +29 -2
  13. package/agents/pan-planner.md +23 -10
  14. package/agents/pan-project-researcher.md +10 -67
  15. package/agents/pan-reviewer.md +19 -0
  16. package/agents/pan-roadmapper.md +5 -48
  17. package/agents/pan-verifier.md +49 -11
  18. package/bin/install-lib.cjs +170 -15
  19. package/bin/install.js +39 -14
  20. package/commands/pan/army.md +2 -2
  21. package/commands/pan/design-phase.md +8 -0
  22. package/commands/pan/exec-phase.md +1 -1
  23. package/commands/pan/experiment.md +2 -2
  24. package/commands/pan/focus-auto.md +12 -8
  25. package/commands/pan/focus-design.md +2 -0
  26. package/commands/pan/focus-exec.md +1 -1
  27. package/commands/pan/focus-scan.md +2 -7
  28. package/commands/pan/git.md +2 -2
  29. package/commands/pan/learn.md +2 -0
  30. package/commands/pan/map-codebase.md +4 -4
  31. package/commands/pan/mcp-bridge.md +1 -1
  32. package/commands/pan/milestone-done.md +1 -2
  33. package/commands/pan/optimize.md +14 -2
  34. package/commands/pan/phase-tests.md +2 -2
  35. package/commands/pan/profile.md +2 -2
  36. package/commands/pan/research-phase.md +2 -4
  37. package/commands/pan/review-deep.md +8 -8
  38. package/commands/pan/todo-add.md +2 -2
  39. package/commands/pan/todo-check.md +2 -2
  40. package/hooks/dist/pan-context-monitor.js +1 -1
  41. package/hooks/dist/pan-cost-logger.js +63 -11
  42. package/hooks/dist/pan-state-reinject.js +142 -17
  43. package/hooks/dist/pan-stop-guard.js +2 -2
  44. package/hooks/dist/pan-trace-logger.js +283 -18
  45. package/package.json +1 -1
  46. package/pan-wizard-core/bin/lib/agents-md.cjs +4 -1
  47. package/pan-wizard-core/bin/lib/commands.cjs +95 -2
  48. package/pan-wizard-core/bin/lib/config.cjs +110 -0
  49. package/pan-wizard-core/bin/lib/constants.cjs +39 -9
  50. package/pan-wizard-core/bin/lib/core.cjs +27 -7
  51. package/pan-wizard-core/bin/lib/cost.cjs +16 -6
  52. package/pan-wizard-core/bin/lib/findings.cjs +385 -0
  53. package/pan-wizard-core/bin/lib/focus.cjs +72 -19
  54. package/pan-wizard-core/bin/lib/foreign-planning.cjs +24 -6
  55. package/pan-wizard-core/bin/lib/hygiene.cjs +12 -2
  56. package/pan-wizard-core/bin/lib/init.cjs +9 -2
  57. package/pan-wizard-core/bin/lib/optimize.cjs +349 -21
  58. package/pan-wizard-core/bin/lib/phase.cjs +52 -2
  59. package/pan-wizard-core/bin/lib/review-deep.cjs +38 -2
  60. package/pan-wizard-core/bin/lib/state.cjs +10 -4
  61. package/pan-wizard-core/bin/lib/verdict.cjs +481 -0
  62. package/pan-wizard-core/bin/lib/verify-drift.cjs +14 -5
  63. package/pan-wizard-core/bin/lib/verify-preflight.cjs +4 -2
  64. package/pan-wizard-core/bin/lib/verify-scope.cjs +223 -0
  65. package/pan-wizard-core/bin/lib/verify.cjs +111 -8
  66. package/pan-wizard-core/bin/pan-tools.cjs +68 -6
  67. package/pan-wizard-core/learnings/README.md +10 -7
  68. package/pan-wizard-core/mcp/server.cjs +18 -9
  69. package/pan-wizard-core/mcp/tool-registry.cjs +2 -0
  70. package/pan-wizard-core/references/checkpoints.md +4 -2
  71. package/pan-wizard-core/references/verdict-contract.md +102 -0
  72. package/pan-wizard-core/templates/discovery.md +1 -1
  73. package/pan-wizard-core/templates/phase-prompt.md +2 -1
  74. package/pan-wizard-core/templates/verification-report.md +21 -0
  75. package/pan-wizard-core/workflows/exec-phase.md +73 -50
  76. package/pan-wizard-core/workflows/health.md +3 -0
  77. package/pan-wizard-core/workflows/learn.md +4 -2
  78. package/pan-wizard-core/workflows/milestone-audit.md +15 -3
  79. package/pan-wizard-core/workflows/new-project.md +1 -1
  80. package/pan-wizard-core/workflows/optimize.md +14 -1
  81. package/pan-wizard-core/workflows/plan-phase.md +20 -15
  82. package/pan-wizard-core/workflows/quick.md +6 -6
  83. package/pan-wizard-core/workflows/verify-phase.md +10 -15
  84. package/pan-zcode/README.md +3 -3
  85. package/scripts/build-agent-plugin.js +19 -5
  86. package/scripts/git-hooks/pre-commit +2 -1
  87. package/scripts/test-surface.cjs +4 -0
@@ -17,7 +17,7 @@ Analyze existing codebase using parallel pan-document_code agents to produce str
17
17
 
18
18
  Each mapper agent explores a focus area and **writes documents directly** to `.planning/codebase/`. The orchestrator only receives confirmations, keeping context usage minimal.
19
19
 
20
- Output: .planning/codebase/ folder with 7 structured documents about the codebase state.
20
+ Output: .planning/codebase/ folder with structured documents about the codebase state.
21
21
  </objective>
22
22
 
23
23
  <execution_context>
@@ -61,7 +61,7 @@ The CLI returns `{mode, total_tokens, file_count, languages}`:
61
61
 
62
62
  Record the chosen mode + telemetry in the final `.planning/codebase/overview.md` so future runs can reason about drift.
63
63
 
64
- **The mode is decided by repo size alone** — `estimate-size` compares the token estimate to `--threshold` and applies no model check. So single-shot only pays off when the model you launched with actually has a 1M-context window (the default Opus and current Sonnet-class models do; legacy 200K-context models do not). On a 200K-context model, pass a threshold that matches your real window (e.g. `--threshold 150000`) so anything larger resolves to `sharded` instead of overflowing a single agent.
64
+ **The mode is decided by repo size alone** — `estimate-size` compares the token estimate to `--threshold` and applies no model check. So single-shot only pays off when the model you launched with actually has a 1M-context window. On a model with a smaller window, pass a threshold that matches your real window (e.g. `--threshold 150000`) so anything larger resolves to `sharded` instead of overflowing a single agent.
65
65
  </stage_0_ingest_mode>
66
66
 
67
67
  <tool_priority>
@@ -99,14 +99,14 @@ The orchestrator loads context in layers — NOT everything upfront. Mapper agen
99
99
  - Agent 5: relationships focus → writes relationships.md
100
100
  - Agent 6: practices focus → writes best-practices.md
101
101
  4. Wait for agents to complete, collect confirmations (NOT document contents)
102
- 5. Verify all 9 documents exist with line counts
102
+ 5. Verify all documents exist with line counts
103
103
  6. Commit codebase map
104
104
  7. Offer next steps (typically: /pan:new-project or /pan:plan-phase)
105
105
  </process>
106
106
 
107
107
  <success_criteria>
108
108
  - [ ] .planning/codebase/ directory created
109
- - [ ] All 7 codebase documents written by mapper agents
109
+ - [ ] All codebase documents written by mapper agents
110
110
  - [ ] Documents follow template structure
111
111
  - [ ] Parallel agents completed without errors
112
112
  - [ ] User knows next steps
@@ -98,7 +98,7 @@ Normally the host runtime writes this file. The CLI path exists for test fixture
98
98
 
99
99
  <workflow>
100
100
 
101
- **New to a project with MCP tools?** Run `/pan:mcp-bridge list` to see what's available. If empty, check the host runtime's MCP config — each runtime keeps it somewhere different: Claude Code in `settings.json` under its config dir, OpenCode in `opencode.json`, Copilot in `.github/mcp.json`, Codex in its own config, Gemini in its settings file.
101
+ **New to a project with MCP tools?** Run `/pan:mcp-bridge list` to see what's available. If empty, check the host runtime's MCP config — each runtime keeps it somewhere different: Claude Code in `.mcp.json` at the project root (project scope) or `~/.claude.json` (user and local scopes), OpenCode in `opencode.json`, Copilot in `.github/mcp.json`, Codex in its own config, Gemini in its settings file.
102
102
 
103
103
  **Planning a phase that might touch external systems?** Run `/pan:mcp-bridge recommend <phase>` to get a ranked shortlist. Copy relevant tool names into the phase plan's "External tools" section so the executor knows to invoke them.
104
104
 
@@ -34,7 +34,7 @@ The full milestone-done workflow is inlined in <process> below — there is no s
34
34
 
35
35
  **User input:**
36
36
 
37
- - Version: {{version}} (e.g., "1.0", "1.1", "2.0")
37
+ - Version: $ARGUMENTS (e.g., "1.0", "1.1", "2.0"); `{{version}}` below stands for this value
38
38
  </context>
39
39
 
40
40
  <process>
@@ -137,7 +137,6 @@ The full milestone-done workflow is inlined in <process> below — there is no s
137
137
 
138
138
  <critical_rules>
139
139
 
140
- - **Load workflow first:** Read milestone-done.md before executing
141
140
  - **Verify completion:** All phases must have summary.md files
142
141
  - **User confirmation:** Wait for approval at verification gates
143
142
  - **Archive before deleting:** Always create archive files before updating/deleting originals
@@ -19,6 +19,8 @@ Manage the circular optimization loop: apply recommendations, view stats, list r
19
19
  ```
20
20
  /pan:optimize apply
21
21
  /pan:optimize apply --report <filename>
22
+ /pan:optimize revert <apply_id>
23
+ /pan:optimize revert --last
22
24
  /pan:optimize list
23
25
  /pan:optimize stats
24
26
  /pan:optimize trace init [--description "what you're building"]
@@ -42,7 +44,17 @@ Requires human review (never auto-applied):
42
44
  - Workflow step additions
43
45
  - Structural changes to commands
44
46
 
45
- After applying, the report lists what was applied and what still needs review.
47
+ After applying, the report lists what was applied and what still needs review, plus the `apply_id` that undoes it. Every apply is recorded action by action in `.planning/optimization/applied.jsonl`: the path, whether the file was created or appended to, the exact text, and a hash of the file after the write. Applying the same report a second time writes nothing: each action names the apply that already wrote it.
48
+
49
+ ### revert
50
+ Undo one apply exactly: delete the memory files it created and cut the text it appended (`revert --last` for the newest).
51
+
52
+ Revert never destroys work someone did since:
53
+ - It refuses a file whose content changed after the apply. The comparison ignores line endings, so a CRLF checkout still matches.
54
+ - It refuses a file that a later apply also wrote. Revert that later apply first.
55
+ - It says which files it refused, and why.
56
+
57
+ A CRLF file comes back CRLF. Applies logged before apply records existed cannot be reverted. After a revert, the same report can be applied again.
46
58
 
47
59
  ### list
48
60
  List all optimization reports in `.planning/optimization/reports/`, most recent first.
@@ -52,7 +64,7 @@ Show cumulative optimization statistics:
52
64
  - Total trace sessions run
53
65
  - Total events traced
54
66
  - Total errors/gaps/redundancies seen
55
- - Total optimizations applied across all runs
67
+ - Total optimizations applied across all runs, the apply runs, the reverted runs, and the last `apply_id`
56
68
  - Current active trace session (if any)
57
69
 
58
70
  ### trace init
@@ -19,7 +19,7 @@ Generate unit and E2E tests for a completed phase, using its summary.md, context
19
19
 
20
20
  Analyzes implementation files, classifies them into TDD (unit), E2E (browser), or Skip categories, presents a test plan for user approval, then generates tests following RED-GREEN conventions.
21
21
 
22
- Output: Test files committed with message `test(phase-{N}): add unit and E2E tests from add-tests command`
22
+ Output: Test files committed with message `test(phase-{N}): add unit and E2E tests from phase-tests command`
23
23
  </objective>
24
24
 
25
25
  <execution_context>
@@ -34,6 +34,6 @@ Phase: $ARGUMENTS
34
34
  </context>
35
35
 
36
36
  <process>
37
- Execute the add-tests workflow from @~/.claude/pan-wizard-core/workflows/phase-tests.md end-to-end.
37
+ Execute the phase-tests workflow from @~/.claude/pan-wizard-core/workflows/phase-tests.md end-to-end.
38
38
  Preserve all workflow gates (classification approval, test plan approval, RED-GREEN verification, gap reporting).
39
39
  </process>
@@ -12,7 +12,7 @@ allowed-tools:
12
12
  <objective>
13
13
  Switch the model profile used by PAN agents. Controls which Claude model each agent uses, balancing quality vs token spend.
14
14
 
15
- Routes to the set-profile workflow which handles:
15
+ Routes to the profile workflow which handles:
16
16
  - Argument validation (quality/balanced/budget)
17
17
  - Downgrade confirmation (quality → balanced → budget requires 'yes')
18
18
  - Config file creation if missing
@@ -25,7 +25,7 @@ Routes to the set-profile workflow which handles:
25
25
  </execution_context>
26
26
 
27
27
  <process>
28
- **Follow the set-profile workflow** from `@~/.claude/pan-wizard-core/workflows/profile.md`.
28
+ **Follow the profile workflow** from `@~/.claude/pan-wizard-core/workflows/profile.md`.
29
29
 
30
30
  The workflow handles all logic including:
31
31
  1. Profile argument validation
@@ -55,11 +55,9 @@ PHASE_INFO=$(node ~/.claude/pan-wizard-core/bin/pan-tools.cjs roadmap get-phase
55
55
 
56
56
  ## 2. Check Existing Research
57
57
 
58
- ```bash
59
- ls .planning/phases/${PHASE}-*/research.md 2>/dev/null
60
- ```
58
+ Use `has_research` and `research_path` from INIT.
61
59
 
62
- **If exists:** Offer: 1) Update research, 2) View existing, 3) Skip. Wait for response.
60
+ **If exists:** Offer: 1) Update research, 2) View existing (`research_path`), 3) Skip. Wait for response.
63
61
 
64
62
  **If doesn't exist:** Continue.
65
63
 
@@ -38,7 +38,7 @@ Consolidates Spec B v1's X-4 (self-review) + X-12 (harden) into a single command
38
38
  /pan:review-deep 07
39
39
  ```
40
40
 
41
- Run after `/pan:exec-phase 07` completes. Requires `pan-reviewer` to have already written its review to `.planning/phases/07/review.md` (exec-phase does this automatically).
41
+ Run after `/pan:exec-phase 07` completes. It reads the review that exec-phase's code-review step saved in the phase directory (`.planning/phases/07-<slug>/07-review.md`). The reviewer is read-only, so it is exec-phase that writes the review there.
42
42
 
43
43
  ### Integrated with exec-phase
44
44
 
@@ -46,7 +46,7 @@ Run after `/pan:exec-phase 07` completes. Requires `pan-reviewer` to have alread
46
46
  /pan:exec-phase 07 --deep-review
47
47
  ```
48
48
 
49
- Runs the normal exec → reviewer pipeline, then auto-invokes this command. Recommended for phases touching auth, payment, PII, migrations, or public APIs.
49
+ Runs the normal exec → reviewer pipeline, then runs this command's process inline, before the verifier. Recommended for phases touching auth, payment, PII, migrations, or public APIs.
50
50
 
51
51
  ### Integrated with focus-exec
52
52
 
@@ -60,7 +60,7 @@ Per-item deep review during focus campaigns. Useful for high-stakes batches.
60
60
 
61
61
  <process>
62
62
 
63
- 1. **Load reviewer output** — read `.planning/phases/<N>/review.md` written by the earlier `pan-reviewer` step. If missing, warn and offer to run `pan-reviewer` first.
63
+ 1. **Load reviewer output.** Resolve the phase with `pan-tools find-phase <N>`, then read `{directory}/{phase_number}-review.md`, which exec-phase's code-review step wrote. If it is missing, warn and offer to run `pan-reviewer` first. Its findings come from the report's closing `pan-verdict` block.
64
64
 
65
65
  2. **Spawn pan-hardener** (parallel-safe with step 3 isolation below, but recommended sequential for audit clarity):
66
66
  - Prompt includes: `<files_to_read>` with phase plan + diff + reviewer output; `<output_path>` = `.planning/reviews/<N>/hardener.md`; `<framework_scope>` block reminding of OWASP/STRIDE coverage.
@@ -73,7 +73,7 @@ Per-item deep review during focus campaigns. Useful for high-stakes batches.
73
73
  4. **Merge** — call:
74
74
  ```
75
75
  pan-tools review-deep merge <N> \
76
- --reviewer-file .planning/phases/<N>/review.md \
76
+ --reviewer-file {directory}/{phase_number}-review.md \
77
77
  --hardener-file .planning/reviews/<N>/hardener.md \
78
78
  --meta-file .planning/reviews/<N>/meta.md
79
79
  ```
@@ -99,7 +99,7 @@ Verdict is driven by the highest-severity finding across all three sources. Meta
99
99
 
100
100
  <output_files>
101
101
 
102
- - `.planning/phases/<N>/review.md` — pan-reviewer output (written earlier by exec-phase)
102
+ - `{phase_dir}/{phase_number}-review.md` — pan-reviewer output (written by exec-phase's code-review step)
103
103
  - `.planning/reviews/<N>/hardener.md` — pan-hardener output (new)
104
104
  - `.planning/reviews/<N>/meta.md` — pan-meta-reviewer output (new)
105
105
  - `.planning/reviews/<N>/deep-review.md` — merged consolidated report (final deliverable)
@@ -111,18 +111,18 @@ Verdict is driven by the highest-severity finding across all three sources. Meta
111
111
 
112
112
  | Runtime | hardener | meta-reviewer | merge |
113
113
  |---------|----------|---------------|-------|
114
- | Claude Code | Full, thinking enabled (6000/4000 budget) | Full | Full |
114
+ | Claude Code | Full; reasoning depth from the agent's `effort:` frontmatter | Full | Full |
115
115
  | OpenCode | Prose "think step-by-step" preamble substitutes for thinking | Same | Full (runtime-agnostic CLI) |
116
116
  | Gemini | Same | Same | Full |
117
117
  | Codex | Same | Same | Full |
118
118
  | Copilot | Same | Same | Full |
119
119
 
120
- The merger CLI (`pan-tools review-deep merge`) is pure Node.js and works identically across runtimes. Only the *quality* of the hardener and meta-reviewer outputs varies with model capability — an Opus-class model with extended thinking produces the richest findings.
120
+ The merger CLI (`pan-tools review-deep merge`) is pure Node.js and works identically across runtimes. Only the *quality* of the hardener and meta-reviewer outputs varies with model capability.
121
121
 
122
122
  </runtime_compatibility>
123
123
 
124
124
  <calibration_note>
125
125
 
126
- Deep review is opt-in for a reason: it costs roughly 3× a normal review (hardener + meta + merge adds two agent spawns per phase). Use it for high-stakes phases, not every phase. `--deep-review` gating by phase tags is a v3.4 candidate enhancement.
126
+ Deep review is opt-in for a reason: it costs roughly 3× a normal review (hardener + meta + merge adds two agent spawns per phase). Use it for high-stakes phases, not every phase.
127
127
 
128
128
  </calibration_note>
@@ -13,7 +13,7 @@ allowed-tools:
13
13
  <objective>
14
14
  Capture an idea, task, or issue that surfaces during a PAN session as a structured todo for later work.
15
15
 
16
- Routes to the add-todo workflow which handles:
16
+ Routes to the todo-add workflow which handles:
17
17
  - Directory structure creation
18
18
  - Content extraction from arguments or conversation
19
19
  - Area inference from file paths
@@ -34,7 +34,7 @@ State is resolved in-workflow via `init todos` and targeted reads.
34
34
  </context>
35
35
 
36
36
  <process>
37
- **Follow the add-todo workflow** from `@~/.claude/pan-wizard-core/workflows/todo-add.md`.
37
+ **Follow the todo-add workflow** from `@~/.claude/pan-wizard-core/workflows/todo-add.md`.
38
38
 
39
39
  The workflow handles all logic including:
40
40
  1. Directory ensuring
@@ -13,7 +13,7 @@ allowed-tools:
13
13
  <objective>
14
14
  List all pending todos, allow selection, load full context for the selected todo, and route to appropriate action.
15
15
 
16
- Routes to the check-todos workflow which handles:
16
+ Routes to the todo-check workflow which handles:
17
17
  - Todo counting and listing with area filtering
18
18
  - Interactive selection with full context loading
19
19
  - Roadmap correlation checking
@@ -32,7 +32,7 @@ Todo state and roadmap correlation are loaded in-workflow using `init todos` and
32
32
  </context>
33
33
 
34
34
  <process>
35
- **Follow the check-todos workflow** from `@~/.claude/pan-wizard-core/workflows/todo-check.md`.
35
+ **Follow the todo-check workflow** from `@~/.claude/pan-wizard-core/workflows/todo-check.md`.
36
36
 
37
37
  The workflow handles all logic including:
38
38
  1. Todo existence checking
@@ -5,7 +5,7 @@
5
5
  // context limits (the statusline only shows the user).
6
6
  //
7
7
  // How it works:
8
- // 1. The statusline hook writes metrics to /tmp/claude-ctx-{session_id}.json
8
+ // 1. The statusline hook writes metrics to <os tmpdir>/pan-hooks-<uid>/claude-ctx-{session_id}.json
9
9
  // 2. This hook reads those metrics after each tool use
10
10
  // 3. When remaining context drops below thresholds, it injects a warning
11
11
  // as additionalContext, which the agent sees in its conversation
@@ -146,9 +146,11 @@ const CURSOR_FILE = '.cost-cursor.json';
146
146
  // the per-invocation `event_sig` discriminator; v4 added `agent_id` and the
147
147
  // `agent-transcript` token source. Kept as a literal in each hook —
148
148
  // they are standalone zero-dep scripts that can't import from pan-wizard-core, so
149
- // the two hooks must stay in sync by hand. No constant in pan-wizard-core mirrors
150
- // it: the readers there take a row field by field rather than switching on its
151
- // version, so an added field is additive for them.
149
+ // each hook's copy is kept by hand (in lockstep through v4; the trace logger's events
150
+ // have since moved to 5 on their own). cost-rebuild.cjs in
151
+ // pan-wizard-core mirrors it for the rows it rebuilds; the readers there take a row
152
+ // field by field rather than switching on its version, so an added field is additive
153
+ // for them.
152
154
  const SCHEMA_V = 4;
153
155
 
154
156
  // Reverse-map a resolved model id to its cost tier so the "By tier" dashboard
@@ -456,10 +458,13 @@ const MAX_CURSOR_KEYS = 512;
456
458
  // marker phantom (an over-count) and from N29 (concurrent same-type siblings).
457
459
  //
458
460
  // Returns null when the payload cannot be serialized — callers then fail OPEN
459
- // (record, never mark).
461
+ // (record, never mark). SHA-256 cut to the 40 hex characters the SHA-1 it replaced
462
+ // produced, so persisted `event_sig` values keep their shape: a dedup key, not a
463
+ // security boundary, but CodeQL rightly flags SHA-1 over a payload that carries a
464
+ // session id (2026-09-29).
460
465
  function eventSignature(data) {
461
466
  try {
462
- return crypto.createHash('sha1').update(JSON.stringify(data)).digest('hex');
467
+ return crypto.createHash('sha256').update(JSON.stringify(data)).digest('hex').slice(0, 40);
463
468
  } catch { return null; }
464
469
  }
465
470
 
@@ -559,6 +564,27 @@ function resolveAgentTranscript(data) {
559
564
  }
560
565
  }
561
566
 
567
+ // Host-internal agents (measured 2026-09-28). Claude Code runs helper agents of its own:
568
+ // the compaction summariser, and one at almost every turn end of an interactive session.
569
+ // It fires SubagentStop for them with an `agent_id`, an EMPTY `agent_type`, and an
570
+ // `agent_transcript_path` it never writes. That shape was captured from a real `/compact`
571
+ // on Claude Code 2.1.280 (tests/fixtures/hooks/subagent-stop-internal-claude.json). Both
572
+ // loggers booked each as an `unknown` zero-token spawn: 181 rows in three field projects
573
+ // in eleven days, skewing optimize learn's agent stats and the ledger's spawn counts.
574
+ // The host documents `agent_type` as present whenever a hook fires inside a subagent, so
575
+ // a named instance with no type and no transcript is not a spawn anyone asked for, and
576
+ // nothing is recorded. A NAMED spawn whose file is late or missing keeps its zero-token
577
+ // row (N17/N25); a payload with no agent instance at all keeps the parent-slice path.
578
+ // Identical in pan-cost-logger.js and pan-trace-logger.js (tests/trace-logger.test.cjs
579
+ // pins the copies).
580
+ function isHostInternalAgent(data, agentTranscript) {
581
+ if (!data || typeof data !== 'object' || agentTranscript) return false;
582
+ const typed = [data.agent_type, data.subagent_type].some((v) => typeof v === 'string' && v.trim() !== '');
583
+ if (typed) return false;
584
+ return (typeof data.agent_id === 'string' && AGENT_ID_SAFE.test(data.agent_id))
585
+ || (typeof data.agent_transcript_path === 'string' && data.agent_transcript_path !== '');
586
+ }
587
+
562
588
  // A slice is a SUM over one subagent's conversation, so its ceiling sits above a
563
589
  // single call's (PLAUSIBLE_MAX): a long agent legitimately re-reads its cached
564
590
  // context on every turn. The cache_read and output ceilings are the absolute
@@ -576,6 +602,29 @@ function clampSlice(n, max) {
576
602
  return typeof n === 'number' && n >= 0 && n <= max ? n : 0;
577
603
  }
578
604
 
605
+ // Copilot CLI's camelCase hook payload (docs.github.com/en/copilot/reference/hooks-reference,
606
+ // read 2026-09-28; documented, not yet observed live) names every field differently from
607
+ // Claude's snake_case one, so this hook read no session, transcript or agent from it and
608
+ // booked every Copilot spawn as `unknown`. Its `agentType` is the KIND of agent (built-in
609
+ // or custom); `agentName` is the configured name PAN's agents carry, so the name wins.
610
+ // Claude payloads carry none of these keys and pass through unchanged. Identical in
611
+ // pan-cost-logger.js and pan-trace-logger.js (tests/copilot-payload.test.cjs pins both).
612
+ function normalizeHookPayload(data) {
613
+ if (!data || typeof data !== 'object' || Array.isArray(data)) return data;
614
+ const pick = (...keys) => {
615
+ for (const k of keys) if (typeof data[k] === 'string' && data[k]) return data[k];
616
+ return undefined;
617
+ };
618
+ const out = { ...data };
619
+ if (out.session_id === undefined && pick('sessionId')) out.session_id = pick('sessionId');
620
+ if (out.transcript_path === undefined && pick('transcriptPath')) out.transcript_path = pick('transcriptPath');
621
+ if (out.agent_id === undefined && pick('agentId')) out.agent_id = pick('agentId');
622
+ const name = pick('agent_name', 'agentName');
623
+ if (name) out.agent_type = name;
624
+ else if (out.agent_type === undefined && pick('agentType')) out.agent_type = pick('agentType');
625
+ return out;
626
+ }
627
+
579
628
  /**
580
629
  * Extract what we can from the SubagentStop event payload.
581
630
  * Pure function — safe to test without stdin.
@@ -586,6 +635,7 @@ function clampSlice(n, max) {
586
635
  */
587
636
  function buildCostRecord(data, cwd) {
588
637
  if (!data || typeof data !== 'object') return null;
638
+ data = normalizeHookPayload(data);
589
639
 
590
640
  // Only log actual subagent stops; ignore other Stop variants.
591
641
  if (data.hook_event_name && data.hook_event_name !== 'SubagentStop') return null;
@@ -613,6 +663,7 @@ function buildCostRecord(data, cwd) {
613
663
  // a value dropped to 0 by a plausibility guard so a guarded zero is
614
664
  // distinguishable from a genuine zero-token run.
615
665
  const agentTranscript = resolveAgentTranscript(data);
666
+ if (isHostInternalAgent(data, agentTranscript)) return null; // the host's own helper, not a spawn
616
667
  const parentPath = typeof data.transcript_path === 'string' && data.transcript_path ? data.transcript_path : null;
617
668
  // The host NAMED an agent (a bare id, or an explicit agent transcript path) but
618
669
  // its file is not there — not flushed yet, or a host that sends ids without
@@ -731,13 +782,14 @@ function buildCostRecord(data, cwd) {
731
782
  // Backfill command/phase from the active trace session when the payload omits
732
783
  // them (real SubagentStop payloads carry neither); tier is derived from the model.
733
784
  const sessionMeta = readActiveSessionMeta(cwd);
734
- // The trace session only exists while the optimizer runs, so the parent transcript is
735
- // the fallback that makes command attribution work outside focus mode — see
785
+ // The trace logger's day-scoped auto-session names no command (only a session opened
786
+ // by `optimize trace init` or focus mode does), so the parent transcript is the
787
+ // fallback that makes command attribution work in ordinary use — see
736
788
  // readCommandFromTranscript.
737
789
  const command = data.command || sessionMeta.command || readCommandFromTranscript(data.transcript_path) || null;
738
- // The trace session is only present while the optimizer is running (off by
739
- // default), so state.md is the fallback that makes phase attribution work in
740
- // ordinary use instead of only under tracing.
790
+ // The day-scoped auto-session names no phase either, so state.md is the fallback
791
+ // that makes phase attribution work in ordinary use, not only in a session that
792
+ // was opened with one.
741
793
  const phase = data.phase || sessionMeta.phase || readCurrentPhase(cwd) || null;
742
794
 
743
795
  const record = {
@@ -1003,4 +1055,4 @@ if (require.main === module) {
1003
1055
  });
1004
1056
  }
1005
1057
 
1006
- module.exports = { deferToClaudeRegistration, buildCostRecord, appendRecord, readUsageFromTranscript, addCacheTtlSplit, resolveAgentTranscript, readCursor, writeCursor, isPanProject, hasPlanningTree, readCommandFromTranscript, isSessionStale, PAN_RUNTIME_DIRS, METRICS_DIR, TOKENS_FILE, CURSOR_FILE, SLICE_MAX, MAX_CURSOR_KEYS };
1058
+ module.exports = { deferToClaudeRegistration, normalizeHookPayload, isHostInternalAgent, buildCostRecord, appendRecord, readUsageFromTranscript, addCacheTtlSplit, resolveAgentTranscript, readCursor, writeCursor, isPanProject, hasPlanningTree, readCommandFromTranscript, isSessionStale, PAN_RUNTIME_DIRS, METRICS_DIR, TOKENS_FILE, CURSOR_FILE, SLICE_MAX, MAX_CURSOR_KEYS };
@@ -18,11 +18,30 @@
18
18
  // `compact` matcher on both; the source is re-checked here so a registration
19
19
  // without the matcher stays inert on every other start.
20
20
  //
21
+ // Gemini CLI and Copilot CLI (market-ideas M33) restart no session after a
22
+ // compaction (Gemini's SessionStart sources: startup | resume | clear; Copilot's:
23
+ // startup | resume | new), and their pre-compaction events cannot add context
24
+ // (Gemini PreCompress and Copilot preCompact are advisory). So there the block
25
+ // arrives in two steps:
26
+ // --mark on PreCompress / preCompact: when a PAN phase is in flight,
27
+ // leave a per-session marker in the per-user hook directory
28
+ // under the OS temp directory (0700, written 0600);
29
+ // --inject <host> on the next tool result (Gemini AfterTool, Copilot
30
+ // postToolUse): consume the marker and return the block once,
31
+ // in that host's output shape. With no marker it prints nothing.
32
+ // A tool result is the first point after a compaction where both hosts take added
33
+ // context, including an autonomous `-p` run that never sees another prompt (read
34
+ // 2026-10-03: gemini-cli v0.61.0 docs/hooks/reference.md; the Copilot hooks
35
+ // reference on docs.github.com). The marker modes need no R39 deferral: in a
36
+ // project with both installs, Copilot also runs Claude's registration of this
37
+ // script, but that one is the SessionStart mode, which a Copilot start never matches.
38
+ //
21
39
  // Inert unless the project is a PAN project with work in flight: `.planning/`
22
40
  // holds a state.md that names a current phase AND a roadmap.md with an unticked
23
41
  // phase. Everywhere else — no planning tree, a focus-only project, a finished
24
- // milestone — it prints nothing. It never writes a file (field finding: hooks
25
- // that scaffolded `.planning/` in projects that never ran PAN).
42
+ // milestone — it prints nothing. It never writes into the project (field finding:
43
+ // hooks that scaffolded `.planning/` in projects that never ran PAN); the only
44
+ // file it writes is the marker, outside the project.
26
45
  //
27
46
  // Fail-open everywhere: bad stdin, unreadable files — exit 0 silently. A missing
28
47
  // reminder costs a re-read; a crashed hook is a failing hook at every start.
@@ -31,14 +50,16 @@
31
50
  // additionalContext; each field is truncated on its own.
32
51
 
33
52
  const fs = require('fs');
53
+ const os = require('os');
34
54
  const path = require('path');
55
+ const crypto = require('crypto');
35
56
 
36
57
  /**
37
58
  * Which planning tree this hook acts on.
38
59
  *
39
60
  * Mirrors pan-wizard-core/bin/lib/planning-root.cjs, which hooks cannot require
40
- * (they are standalone and run inside the host runtime). All PAN hooks carry an
41
- * identical copy — if the CLI is pointed at a track while a hook still writes to
61
+ * (they are standalone and run inside the host runtime). Every PAN hook that
62
+ * reads the planning tree carries an identical copy — if the CLI is pointed at a track while a hook still writes to
42
63
  * `.planning/`, that tree's telemetry lands in the wrong place.
43
64
  *
44
65
  * Env only — a hook gets no argv. A value that escapes the project root is
@@ -124,7 +145,99 @@ function readIfExists(p) {
124
145
  try { return fs.readFileSync(p, 'utf8'); } catch { return null; }
125
146
  }
126
147
 
148
+ // ─── The marker (Gemini, Copilot) ─────────────────────────────────────────────
149
+
150
+ // A marker older than this belongs to a session that never made another tool call.
151
+ const MARKER_TTL_MS = 6 * 60 * 60 * 1000;
152
+
153
+ /** Gemini sends `session_id`; Copilot's camelCase payloads send `sessionId`. */
154
+ function sessionIdOf(payload) {
155
+ for (const k of ['session_id', 'sessionId']) {
156
+ if (typeof payload[k] === 'string' && payload[k]) return payload[k];
157
+ }
158
+ return '';
159
+ }
160
+
161
+ /** The per-user hook directory's name: the stop guard's and the context monitor's too. */
162
+ function hookDirName() {
163
+ const uid = (typeof process.getuid === 'function' ? process.getuid() : process.env.USERNAME || 'win');
164
+ return `pan-hooks-${uid}`;
165
+ }
166
+
167
+ // Per-user hook state directory inside tmpdir, created 0700 — the same directory
168
+ // and the same checks as hookStateDir() in pan-stop-guard.js (hooks are standalone
169
+ // files and cannot require one another). Null when the directory is not provably
170
+ // ours: on a shared host another user must not be able to plant a marker that
171
+ // injects, or a symlink the marker is written through (M60).
172
+ function hookStateDir() {
173
+ const dir = path.join(os.tmpdir(), hookDirName());
174
+ try {
175
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
176
+ const st = fs.lstatSync(dir);
177
+ if (st.isSymbolicLink()) return null;
178
+ // POSIX-only ownership and mode checks: Windows fakes mode bits (N15).
179
+ if (typeof process.getuid === 'function') {
180
+ if (st.uid !== process.getuid()) return null;
181
+ if ((st.mode & 0o077) !== 0) return null;
182
+ }
183
+ return dir;
184
+ } catch { return null; }
185
+ }
186
+
187
+ /** One marker per (session, project), named by a hash so neither leaks into a file name. Pure. */
188
+ function markerName(sessionId, projectDir) {
189
+ const key = crypto.createHash('sha256').update(`${sessionId}\u0000${path.resolve(projectDir)}`).digest('hex').slice(0, 32);
190
+ return `state-reinject-${key}.json`;
191
+ }
192
+
193
+ // This hook's files in the shared directory: markers, and one a crash left mid-take.
194
+ const MARKER_FILE_RE = /^state-reinject-[0-9a-f]{32}\.json(?:\.\d+\.taken)?$/;
195
+
196
+ function writeMarker(file, now = Date.now()) {
197
+ fs.writeFileSync(file, JSON.stringify({ at: now }), { mode: 0o600 });
198
+ // Sweep markers no session came back for — only this hook's: the directory
199
+ // holds the other hooks' state too. Best effort.
200
+ try {
201
+ for (const name of fs.readdirSync(path.dirname(file))) {
202
+ if (!MARKER_FILE_RE.test(name)) continue;
203
+ const p = path.join(path.dirname(file), name);
204
+ try { if (now - fs.statSync(p).mtimeMs > MARKER_TTL_MS) fs.unlinkSync(p); } catch { /* raced */ }
205
+ }
206
+ } catch { /* unreadable dir */ }
207
+ }
208
+
209
+ /**
210
+ * Take the marker if there is a fresh one: rename it first, so two hooks racing on
211
+ * the same tool result cannot both inject. True when this call took it.
212
+ */
213
+ function takeMarker(file, now = Date.now()) {
214
+ const taken = `${file}.${process.pid}.taken`;
215
+ try { fs.renameSync(file, taken); } catch { return false; }
216
+ let fresh = false;
217
+ try { fresh = now - JSON.parse(fs.readFileSync(taken, 'utf8')).at <= MARKER_TTL_MS; } catch { /* unreadable: stale */ }
218
+ try { fs.unlinkSync(taken); } catch { /* gone */ }
219
+ return fresh;
220
+ }
221
+
222
+ /** The block in the shape each host's tool-result event takes. */
223
+ function injectOutput(host, context) {
224
+ if (host === 'gemini') return { hookSpecificOutput: { hookEventName: 'AfterTool', additionalContext: context } };
225
+ if (host === 'copilot') return { additionalContext: context };
226
+ return null;
227
+ }
228
+
229
+ function contextFor(projectDir) {
230
+ return buildReinjectContext({
231
+ stateContent: readIfExists(planningPath(projectDir, 'state.md')),
232
+ roadmapContent: readIfExists(planningPath(projectDir, 'roadmap.md')),
233
+ planningRel: planningDirName(),
234
+ });
235
+ }
236
+
127
237
  function main() {
238
+ const argv = process.argv.slice(2);
239
+ const mode = argv.includes('--mark') ? 'mark' : argv.includes('--inject') ? 'inject' : 'session-start';
240
+ const host = mode === 'inject' ? argv[argv.indexOf('--inject') + 1] : null;
128
241
  let input = '';
129
242
  process.stdin.setEncoding('utf8');
130
243
  process.stdin.on('data', chunk => { input += chunk; });
@@ -133,20 +246,32 @@ function main() {
133
246
  let payload = {};
134
247
  try { payload = JSON.parse(input); } catch { /* fail open on bad stdin */ }
135
248
  if (!payload || typeof payload !== 'object') payload = {};
136
- if (payload.source !== 'compact') { process.exit(0); return; }
137
-
138
249
  const projectDir = typeof payload.cwd === 'string' && payload.cwd ? payload.cwd : process.cwd();
139
- const context = buildReinjectContext({
140
- stateContent: readIfExists(planningPath(projectDir, 'state.md')),
141
- roadmapContent: readIfExists(planningPath(projectDir, 'roadmap.md')),
142
- planningRel: planningDirName(),
143
- });
144
- if (context) {
145
- process.stdout.write(JSON.stringify({
146
- hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: context },
147
- }));
250
+
251
+ if (mode === 'session-start') {
252
+ if (payload.source !== 'compact') { process.exit(0); return; }
253
+ const context = contextFor(projectDir);
254
+ if (context) {
255
+ process.stdout.write(JSON.stringify({
256
+ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: context },
257
+ }));
258
+ }
259
+ } else {
260
+ const sid = sessionIdOf(payload);
261
+ // An unknown host leaves the marker for a host that can use it.
262
+ if (!sid || (mode === 'inject' && !injectOutput(host, ''))) { process.exit(0); return; }
263
+ const dir = hookStateDir();
264
+ if (!dir) { process.exit(0); return; }
265
+ const file = path.join(dir, markerName(sid, projectDir));
266
+ if (mode === 'mark') {
267
+ if (contextFor(projectDir)) writeMarker(file);
268
+ } else if (takeMarker(file)) {
269
+ // Re-read now: the position after the compaction, not before it.
270
+ const context = contextFor(projectDir);
271
+ if (context) process.stdout.write(JSON.stringify(injectOutput(host, context)));
272
+ }
148
273
  }
149
- } catch { /* fail open — never break a session start */ }
274
+ } catch { /* fail open — never break a session or a tool call */ }
150
275
  process.exit(0);
151
276
  });
152
277
  }
@@ -155,4 +280,4 @@ if (require.main === module) {
155
280
  main();
156
281
  }
157
282
 
158
- module.exports = { buildReinjectContext, UNTICKED_PHASE_RE };
283
+ module.exports = { buildReinjectContext, UNTICKED_PHASE_RE, markerName, hookDirName, sessionIdOf, injectOutput, MARKER_TTL_MS };
@@ -116,8 +116,8 @@ const crypto = require('crypto');
116
116
  * Which planning tree this hook acts on.
117
117
  *
118
118
  * Mirrors pan-wizard-core/bin/lib/planning-root.cjs, which hooks cannot require
119
- * (they are standalone and run inside the host runtime). All PAN hooks carry an
120
- * identical copy — if the CLI is pointed at a track while a hook still writes to
119
+ * (they are standalone and run inside the host runtime). Every PAN hook that
120
+ * reads the planning tree carries an identical copy — if the CLI is pointed at a track while a hook still writes to
121
121
  * `.planning/`, that tree's telemetry lands in the wrong place.
122
122
  *
123
123
  * Env only — a hook gets no argv. A value that escapes the project root is