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.
- package/README.md +28 -24
- package/agents/pan-conductor.md +2 -2
- package/agents/pan-design-checker.md +18 -0
- package/agents/pan-document_code.md +2 -2
- package/agents/pan-executor.md +3 -3
- package/agents/pan-experiment-runner.md +7 -10
- package/agents/pan-hardener.md +15 -15
- package/agents/pan-knowledge.md +1 -1
- package/agents/pan-meta-reviewer.md +2 -2
- package/agents/pan-optimizer.md +7 -3
- package/agents/pan-phase-researcher.md +2 -2
- package/agents/pan-plan-checker.md +29 -2
- package/agents/pan-planner.md +23 -10
- package/agents/pan-project-researcher.md +10 -67
- package/agents/pan-reviewer.md +19 -0
- package/agents/pan-roadmapper.md +5 -48
- package/agents/pan-verifier.md +49 -11
- package/bin/install-lib.cjs +170 -15
- package/bin/install.js +39 -14
- package/commands/pan/army.md +2 -2
- package/commands/pan/design-phase.md +8 -0
- package/commands/pan/exec-phase.md +1 -1
- package/commands/pan/experiment.md +2 -2
- package/commands/pan/focus-auto.md +12 -8
- package/commands/pan/focus-design.md +2 -0
- package/commands/pan/focus-exec.md +1 -1
- package/commands/pan/focus-scan.md +2 -7
- package/commands/pan/git.md +2 -2
- package/commands/pan/learn.md +2 -0
- package/commands/pan/map-codebase.md +4 -4
- package/commands/pan/mcp-bridge.md +1 -1
- package/commands/pan/milestone-done.md +1 -2
- package/commands/pan/optimize.md +14 -2
- package/commands/pan/phase-tests.md +2 -2
- package/commands/pan/profile.md +2 -2
- package/commands/pan/research-phase.md +2 -4
- package/commands/pan/review-deep.md +8 -8
- package/commands/pan/todo-add.md +2 -2
- package/commands/pan/todo-check.md +2 -2
- package/hooks/dist/pan-context-monitor.js +1 -1
- package/hooks/dist/pan-cost-logger.js +63 -11
- package/hooks/dist/pan-state-reinject.js +142 -17
- package/hooks/dist/pan-stop-guard.js +2 -2
- package/hooks/dist/pan-trace-logger.js +283 -18
- package/package.json +1 -1
- package/pan-wizard-core/bin/lib/agents-md.cjs +4 -1
- package/pan-wizard-core/bin/lib/commands.cjs +95 -2
- package/pan-wizard-core/bin/lib/config.cjs +110 -0
- package/pan-wizard-core/bin/lib/constants.cjs +39 -9
- package/pan-wizard-core/bin/lib/core.cjs +27 -7
- package/pan-wizard-core/bin/lib/cost.cjs +16 -6
- package/pan-wizard-core/bin/lib/findings.cjs +385 -0
- package/pan-wizard-core/bin/lib/focus.cjs +72 -19
- package/pan-wizard-core/bin/lib/foreign-planning.cjs +24 -6
- package/pan-wizard-core/bin/lib/hygiene.cjs +12 -2
- package/pan-wizard-core/bin/lib/init.cjs +9 -2
- package/pan-wizard-core/bin/lib/optimize.cjs +349 -21
- package/pan-wizard-core/bin/lib/phase.cjs +52 -2
- package/pan-wizard-core/bin/lib/review-deep.cjs +38 -2
- package/pan-wizard-core/bin/lib/state.cjs +10 -4
- package/pan-wizard-core/bin/lib/verdict.cjs +481 -0
- package/pan-wizard-core/bin/lib/verify-drift.cjs +14 -5
- package/pan-wizard-core/bin/lib/verify-preflight.cjs +4 -2
- package/pan-wizard-core/bin/lib/verify-scope.cjs +223 -0
- package/pan-wizard-core/bin/lib/verify.cjs +111 -8
- package/pan-wizard-core/bin/pan-tools.cjs +68 -6
- package/pan-wizard-core/learnings/README.md +10 -7
- package/pan-wizard-core/mcp/server.cjs +18 -9
- package/pan-wizard-core/mcp/tool-registry.cjs +2 -0
- package/pan-wizard-core/references/checkpoints.md +4 -2
- package/pan-wizard-core/references/verdict-contract.md +102 -0
- package/pan-wizard-core/templates/discovery.md +1 -1
- package/pan-wizard-core/templates/phase-prompt.md +2 -1
- package/pan-wizard-core/templates/verification-report.md +21 -0
- package/pan-wizard-core/workflows/exec-phase.md +73 -50
- package/pan-wizard-core/workflows/health.md +3 -0
- package/pan-wizard-core/workflows/learn.md +4 -2
- package/pan-wizard-core/workflows/milestone-audit.md +15 -3
- package/pan-wizard-core/workflows/new-project.md +1 -1
- package/pan-wizard-core/workflows/optimize.md +14 -1
- package/pan-wizard-core/workflows/plan-phase.md +20 -15
- package/pan-wizard-core/workflows/quick.md +6 -6
- package/pan-wizard-core/workflows/verify-phase.md +10 -15
- package/pan-zcode/README.md +3 -3
- package/scripts/build-agent-plugin.js +19 -5
- package/scripts/git-hooks/pre-commit +2 -1
- 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
|
|
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
|
|
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
|
|
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
|
|
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 `
|
|
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:
|
|
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
|
package/commands/pan/optimize.md
CHANGED
|
@@ -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
|
|
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
|
|
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>
|
package/commands/pan/profile.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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.
|
|
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>
|
package/commands/pan/todo-add.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
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('
|
|
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
|
|
735
|
-
//
|
|
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
|
|
739
|
-
//
|
|
740
|
-
//
|
|
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
|
|
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).
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
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).
|
|
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
|