pan-wizard 3.4.1 → 3.5.1

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.
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env node
2
+ // pan-trace-logger — SubagentStop hook (v3.5+).
3
+ //
4
+ // Fires alongside pan-cost-logger on every SubagentStop event. If a trace
5
+ // session is active (.planning/optimization/current-session exists), this
6
+ // hook appends a completion event to the trace. This is the automatic
7
+ // instrumentation layer of the circular optimization loop — no extra user
8
+ // action required.
9
+ //
10
+ // Events logged per subagent:
11
+ // - completion: agent finished, tokens used, exit status
12
+ // - redundancy: detected when the same agent type ran twice in this session
13
+ // with similar token counts (rough heuristic for repeated work)
14
+ //
15
+ // Errors are swallowed — this hook must never block the main agent loop.
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+
20
+ const PLANNING_DIR = '.planning';
21
+ const OPTIMIZE_DIR = 'optimization';
22
+ const TRACES_DIR = 'traces';
23
+ const CURRENT_SESSION_FILE = 'current-session';
24
+ const TRACE_EVENT_FILE = 'trace.jsonl';
25
+
26
+ function getOptimizeDir(cwd) {
27
+ return path.join(cwd, PLANNING_DIR, OPTIMIZE_DIR);
28
+ }
29
+
30
+ function getTracesDir(cwd) {
31
+ return path.join(getOptimizeDir(cwd), TRACES_DIR);
32
+ }
33
+
34
+ function getCurrentSessionId(cwd) {
35
+ try {
36
+ return fs.readFileSync(path.join(getOptimizeDir(cwd), CURRENT_SESSION_FILE), 'utf-8').trim() || null;
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Ensure a trace session exists. If none is active, create a day-scoped
44
+ * auto-session so tracing works across the whole flow without manual init.
45
+ *
46
+ * @param {string} cwd
47
+ * @returns {string} The active session ID
48
+ */
49
+ function ensureSessionId(cwd) {
50
+ const existing = getCurrentSessionId(cwd);
51
+ if (existing) return existing;
52
+
53
+ // Create a day-scoped auto session
54
+ const now = new Date();
55
+ const stamp = now.toISOString().replace(/[-:T]/g, '').slice(0, 8); // YYYYMMDD
56
+ const sessionId = `sess_auto_${stamp}`;
57
+ try {
58
+ const sessionDir = path.join(getTracesDir(cwd), sessionId);
59
+ fs.mkdirSync(sessionDir, { recursive: true });
60
+ const meta = {
61
+ session_id: sessionId,
62
+ started_at: now.toISOString(),
63
+ description: 'auto-session (day-scoped)',
64
+ auto: true,
65
+ event_count: 0,
66
+ };
67
+ fs.writeFileSync(path.join(sessionDir, 'session.json'), JSON.stringify(meta, null, 2) + '\n');
68
+ const optimizeDir = getOptimizeDir(cwd);
69
+ fs.mkdirSync(optimizeDir, { recursive: true });
70
+ fs.writeFileSync(path.join(optimizeDir, CURRENT_SESSION_FILE), sessionId + '\n');
71
+ return sessionId;
72
+ } catch {
73
+ return sessionId; // Return the ID even if write fails — best effort
74
+ }
75
+ }
76
+
77
+ function extractNumber(obj, key) {
78
+ if (!obj || typeof obj !== 'object') return 0;
79
+ const v = obj[key];
80
+ return typeof v === 'number' ? v : 0;
81
+ }
82
+
83
+ /**
84
+ * Build trace event(s) from a SubagentStop payload.
85
+ * Pure function — no side effects.
86
+ *
87
+ * @param {Object} data - SubagentStop event payload
88
+ * @returns {Object[]} Array of trace event records
89
+ */
90
+ function buildTraceEvents(data, sessionId) {
91
+ if (!data || typeof data !== 'object') return [];
92
+ if (data.hook_event_name && data.hook_event_name !== 'SubagentStop') return [];
93
+
94
+ const ts = new Date().toISOString();
95
+ const agent = data.agent_type || data.subagent_type || 'unknown';
96
+ const inputTokens = extractNumber(data.usage, 'input_tokens');
97
+ const outputTokens = extractNumber(data.usage, 'output_tokens');
98
+ const cacheRead = extractNumber(data.usage, 'cache_read_input_tokens');
99
+ const totalTokens = inputTokens + outputTokens;
100
+
101
+ const events = [];
102
+
103
+ // Core completion event
104
+ events.push({
105
+ ts,
106
+ session: sessionId,
107
+ agent,
108
+ phase: data.phase || null,
109
+ type: 'decision',
110
+ category: 'agent_completion',
111
+ description: `${agent} completed`,
112
+ context: {
113
+ model: data.model || null,
114
+ input_tokens: inputTokens,
115
+ output_tokens: outputTokens,
116
+ cache_read_tokens: cacheRead,
117
+ total_tokens: totalTokens,
118
+ exit_code: data.exit_code || 0,
119
+ },
120
+ impact: 'trivial',
121
+ correction: null,
122
+ tokens_wasted: null,
123
+ });
124
+
125
+ // Heuristic: if output tokens > 3000 and no cache hits, flag as potential redundancy
126
+ // (expensive agent run that wasn't cached — may be repeated research)
127
+ if (outputTokens > 3000 && cacheRead === 0) {
128
+ events.push({
129
+ ts,
130
+ session: sessionId,
131
+ agent,
132
+ phase: data.phase || null,
133
+ type: 'redundancy',
134
+ category: 'uncached_heavy_run',
135
+ description: `${agent} produced ${outputTokens} output tokens with zero cache hits — possible repeated research`,
136
+ context: { output_tokens: outputTokens, cache_read_tokens: 0 },
137
+ impact: 'minor',
138
+ correction: null,
139
+ tokens_wasted: outputTokens,
140
+ });
141
+ }
142
+
143
+ return events;
144
+ }
145
+
146
+ /**
147
+ * Append trace events to the active session.
148
+ * Returns true if written, false if no session or write failed.
149
+ *
150
+ * @param {string} cwd
151
+ * @param {Object[]} events
152
+ * @param {string} sessionId
153
+ */
154
+ function appendTraceEvents(cwd, events, sessionId) {
155
+ if (!events.length) return false;
156
+ try {
157
+ const sessionDir = path.join(getTracesDir(cwd), sessionId);
158
+ fs.mkdirSync(sessionDir, { recursive: true });
159
+ const lines = events.map(e => JSON.stringify(e)).join('\n') + '\n';
160
+ fs.appendFileSync(path.join(sessionDir, TRACE_EVENT_FILE), lines, 'utf-8');
161
+ return true;
162
+ } catch {
163
+ return false;
164
+ }
165
+ }
166
+
167
+ // ─── Stdin driver ────────────────────────────────────────────────────────────
168
+
169
+ if (require.main === module) {
170
+ let input = '';
171
+ process.stdin.setEncoding('utf8');
172
+ process.stdin.on('data', chunk => (input += chunk));
173
+ process.stdin.on('end', () => {
174
+ try {
175
+ const data = JSON.parse(input);
176
+ const cwd = data.cwd || data.workspace?.current_dir || process.cwd();
177
+ // Always ensure a session exists — creates a day-scoped auto-session if needed
178
+ const sessionId = ensureSessionId(cwd);
179
+ const events = buildTraceEvents(data, sessionId);
180
+ appendTraceEvents(cwd, events, sessionId);
181
+ } catch {
182
+ // Silent fail
183
+ }
184
+ });
185
+ }
186
+
187
+ module.exports = {
188
+ buildTraceEvents,
189
+ appendTraceEvents,
190
+ getCurrentSessionId,
191
+ ensureSessionId,
192
+ PLANNING_DIR,
193
+ OPTIMIZE_DIR,
194
+ TRACES_DIR,
195
+ CURRENT_SESSION_FILE,
196
+ TRACE_EVENT_FILE,
197
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pan-wizard",
3
- "version": "3.4.1",
3
+ "version": "3.5.1",
4
4
  "description": "A lightweight workflow automation and context engineering system for Claude Code, OpenCode, Gemini CLI, Codex, and Copilot CLI.",
5
5
  "bin": {
6
6
  "pan-wizard": "bin/install.js"
@@ -1453,5 +1453,6 @@ module.exports = {
1453
1453
  cmdLearningsExtract,
1454
1454
  cmdLearningsList,
1455
1455
  cmdLearningsPrune,
1456
+ runCommitSafetyChecks,
1456
1457
  VALID_COMMIT_TYPES,
1457
1458
  };
@@ -124,7 +124,7 @@ const FOCUS_DIR = 'focus';
124
124
  const AUTO_RUN_FILE = 'auto-run.json';
125
125
 
126
126
  /** Focus auto-runner categories */
127
- const FOCUS_CATEGORIES = ['cleanup', 'tests', 'stability', 'features', 'docs', 'optimize', 'prompts'];
127
+ const FOCUS_CATEGORIES = ['cleanup', 'tests', 'stability', 'features', 'docs', 'optimize', 'prompts', 'security', 'distill'];
128
128
 
129
129
  /** Category → priority index range (indices into PRIORITY_LEVELS) */
130
130
  const CATEGORY_PRIORITY_RANGE = {
@@ -135,6 +135,8 @@ const CATEGORY_PRIORITY_RANGE = {
135
135
  docs: { min: 5, max: 6 }, // P5-P6
136
136
  optimize: { min: 1, max: 4 }, // P1-P4
137
137
  prompts: { min: 0, max: 6 }, // P0-P6 (all priorities — prompt order is authoritative)
138
+ security: { min: 0, max: 2 }, // P0-P2 (critical/high/medium only — low/info skipped)
139
+ distill: { min: 1, max: 5 }, // P1-P5 (AI bloat: structural quality, not safety-critical)
138
140
  };
139
141
 
140
142
  /** Category → default mode + budget */
@@ -146,6 +148,8 @@ const CATEGORY_DEFAULTS = {
146
148
  docs: { mode: 'balanced', budget: 30 },
147
149
  optimize: { mode: 'balanced', budget: 50 },
148
150
  prompts: { mode: 'balanced', budget: 100 },
151
+ security: { mode: 'bugfix', budget: 40 },
152
+ distill: { mode: 'balanced', budget: 50 },
149
153
  };
150
154
 
151
155
  /** Doc files to scan for staleness (focus sync) */