pan-wizard 3.21.1 → 3.24.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 +38 -24
- package/agents/pan-conductor.md +3 -3
- package/agents/pan-design-checker.md +83 -0
- package/agents/pan-designer.md +67 -0
- package/agents/pan-document_code.md +3 -2
- package/agents/pan-executor.md +2 -2
- package/agents/pan-plan-checker.md +19 -6
- package/agents/pan-planner.md +4 -1
- package/agents/pan-previewer.md +1 -1
- package/agents/pan-roadmapper.md +3 -1
- package/agents/pan-verifier.md +10 -10
- package/bin/install-lib.cjs +128 -17
- package/bin/install.js +480 -74
- package/commands/pan/army.md +20 -14
- package/commands/pan/audit-deployment.md +17 -14
- package/commands/pan/cost.md +2 -2
- package/commands/pan/debug.md +1 -1
- package/commands/pan/design-phase.md +77 -0
- package/commands/pan/exec-phase.md +2 -2
- package/commands/pan/experiment.md +18 -14
- package/commands/pan/focus-auto.md +5 -5
- package/commands/pan/focus-design.md +13 -1
- package/commands/pan/focus-exec.md +1 -1
- package/commands/pan/learn.md +1 -1
- package/commands/pan/links.md +3 -1
- package/commands/pan/map-codebase.md +4 -4
- package/commands/pan/mcp-bridge.md +1 -1
- package/commands/pan/milestone-new.md +3 -1
- package/commands/pan/optimize.md +2 -2
- package/commands/pan/patches.md +1 -1
- package/commands/pan/plan-phase.md +3 -1
- package/commands/pan/preview.md +3 -3
- package/commands/pan/profile.md +3 -3
- package/commands/pan/research-phase.md +1 -1
- package/commands/pan/retro.md +4 -1
- package/commands/pan/review-deep.md +1 -1
- package/commands/pan/settings.md +2 -2
- package/commands/pan/what-if.md +1 -1
- package/hooks/dist/pan-check-update.js +149 -38
- package/hooks/dist/pan-context-monitor.js +155 -81
- package/hooks/dist/pan-cost-logger.js +288 -13
- package/hooks/dist/pan-statusline.js +24 -8
- package/hooks/dist/pan-stop-guard.js +160 -0
- package/hooks/dist/pan-trace-logger.js +261 -19
- package/package.json +2 -2
- package/pan-wizard-core/bin/lib/bridge.cjs +1 -1
- package/pan-wizard-core/bin/lib/bus.cjs +19 -4
- package/pan-wizard-core/bin/lib/campaign.cjs +8 -3
- package/pan-wizard-core/bin/lib/codebase.cjs +15 -5
- package/pan-wizard-core/bin/lib/commands.cjs +88 -6
- package/pan-wizard-core/bin/lib/config.cjs +27 -2
- package/pan-wizard-core/bin/lib/constants.cjs +1 -0
- package/pan-wizard-core/bin/lib/core.cjs +113 -8
- package/pan-wizard-core/bin/lib/cost.cjs +61 -24
- package/pan-wizard-core/bin/lib/distill.cjs +22 -4
- package/pan-wizard-core/bin/lib/doc-lint.cjs +25 -7
- package/pan-wizard-core/bin/lib/experiment.cjs +25 -1
- package/pan-wizard-core/bin/lib/focus.cjs +42 -7
- package/pan-wizard-core/bin/lib/frontmatter.cjs +62 -31
- package/pan-wizard-core/bin/lib/git.cjs +51 -16
- package/pan-wizard-core/bin/lib/hud.cjs +17 -2
- package/pan-wizard-core/bin/lib/init.cjs +2 -5
- package/pan-wizard-core/bin/lib/knowledge.cjs +20 -1
- package/pan-wizard-core/bin/lib/learn-lint.cjs +50 -0
- package/pan-wizard-core/bin/lib/links.cjs +9 -6
- package/pan-wizard-core/bin/lib/lock.cjs +23 -4
- package/pan-wizard-core/bin/lib/memory-optimize.cjs +101 -17
- package/pan-wizard-core/bin/lib/memory-rebuild.cjs +26 -2
- package/pan-wizard-core/bin/lib/milestone.cjs +11 -5
- package/pan-wizard-core/bin/lib/optimize.cjs +18 -7
- package/pan-wizard-core/bin/lib/phase-remove.cjs +41 -2
- package/pan-wizard-core/bin/lib/phase.cjs +82 -11
- package/pan-wizard-core/bin/lib/preview.cjs +23 -4
- package/pan-wizard-core/bin/lib/review-deep.cjs +14 -6
- package/pan-wizard-core/bin/lib/roadmap.cjs +6 -2
- package/pan-wizard-core/bin/lib/runner.cjs +17 -7
- package/pan-wizard-core/bin/lib/squads.cjs +43 -11
- package/pan-wizard-core/bin/lib/state.cjs +32 -11
- package/pan-wizard-core/bin/lib/template.cjs +8 -3
- package/pan-wizard-core/bin/lib/verify-deploy.cjs +14 -2
- package/pan-wizard-core/bin/lib/verify-drift.cjs +15 -1
- package/pan-wizard-core/bin/lib/verify-preflight.cjs +9 -3
- package/pan-wizard-core/bin/lib/verify.cjs +84 -39
- package/pan-wizard-core/bin/lib/whatif.cjs +8 -1
- package/pan-wizard-core/bin/pan-tools.cjs +42 -19
- package/pan-wizard-core/learnings/index.json +7 -7
- package/pan-wizard-core/learnings/universal/autonomous-loop.md +5 -3
- package/pan-wizard-core/learnings/universal/concurrency.md +1 -1
- package/pan-wizard-core/references/design-methodology.md +94 -0
- package/pan-wizard-core/references/git-integration.md +9 -9
- package/pan-wizard-core/references/guardrails.md +4 -1
- package/pan-wizard-core/references/model-profile-resolution.md +1 -1
- package/pan-wizard-core/references/model-profiles.md +44 -34
- package/pan-wizard-core/references/planning-config.md +5 -5
- package/pan-wizard-core/references/verification-patterns.md +67 -0
- package/pan-wizard-core/templates/codebase/architecture.md +1 -1
- package/pan-wizard-core/templates/codebase/concerns.md +1 -1
- package/pan-wizard-core/templates/codebase/conventions.md +1 -1
- package/pan-wizard-core/templates/codebase/integrations.md +1 -1
- package/pan-wizard-core/templates/codebase/stack.md +1 -1
- package/pan-wizard-core/templates/codebase/structure.md +1 -1
- package/pan-wizard-core/templates/codebase/testing.md +1 -1
- package/pan-wizard-core/templates/design.md +146 -0
- package/pan-wizard-core/templates/discovery.md +3 -3
- package/pan-wizard-core/templates/milestone-archive.md +2 -2
- package/pan-wizard-core/templates/playbook.md +1 -1
- package/pan-wizard-core/templates/uat.md +1 -1
- package/pan-wizard-core/workflows/diagnose-issues.md +9 -7
- package/pan-wizard-core/workflows/exec-phase.md +28 -8
- package/pan-wizard-core/workflows/execute-plan.md +2 -3
- package/pan-wizard-core/workflows/health.md +24 -1
- package/pan-wizard-core/workflows/help.md +5 -4
- package/pan-wizard-core/workflows/learn.md +5 -5
- package/pan-wizard-core/workflows/map-codebase.md +1 -1
- package/pan-wizard-core/workflows/milestone-audit.md +1 -1
- package/pan-wizard-core/workflows/milestone-new.md +4 -4
- package/pan-wizard-core/workflows/new-project.md +47 -32
- package/pan-wizard-core/workflows/optimize.md +8 -8
- package/pan-wizard-core/workflows/pause.md +1 -1
- package/pan-wizard-core/workflows/phase-tests.md +2 -2
- package/pan-wizard-core/workflows/plan-phase.md +4 -0
- package/pan-wizard-core/workflows/profile.md +13 -10
- package/pan-wizard-core/workflows/research-phase.md +1 -1
- package/pan-wizard-core/workflows/resume-project.md +1 -1
- package/pan-wizard-core/workflows/settings.md +9 -9
- package/pan-wizard-core/workflows/transition.md +24 -8
- package/pan-wizard-core/workflows/update.md +23 -20
- package/pan-wizard-core/workflows/verify-phase.md +7 -2
- package/pan-zcode/KNOWN-BETA-RISKS.md +8 -0
- package/pan-zcode/README.md +14 -2
- package/pan-zcode/bin/install-zcode.js +1 -1
- package/pan-zcode/mcp/native-tools.cjs +1 -1
- package/pan-zcode/mcp/server.cjs +76 -14
- package/scripts/build-hooks.js +2 -1
- package/scripts/generate-skills-docs.py +0 -4
- package/scripts/run-tests.cjs +11 -4
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// pan-trace-logger — SubagentStop hook (v3.5+).
|
|
3
3
|
//
|
|
4
|
-
// Fires alongside pan-cost-logger on every SubagentStop event.
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// Fires alongside pan-cost-logger on every SubagentStop event. In a PAN project
|
|
5
|
+
// (see isPanProject — M62) it ensures a day-scoped trace session exists and
|
|
6
|
+
// appends a completion event to it. This is the automatic instrumentation layer
|
|
7
|
+
// of the circular optimization loop — no extra user action required. Outside a
|
|
8
|
+
// PAN project the hook no-ops, so a global install does not create .planning/
|
|
9
|
+
// trace artifacts in every repo the user opens.
|
|
9
10
|
//
|
|
10
11
|
// Events logged per subagent:
|
|
11
12
|
// - completion: agent finished, tokens used, exit status
|
|
@@ -16,6 +17,31 @@
|
|
|
16
17
|
|
|
17
18
|
const fs = require('fs');
|
|
18
19
|
const path = require('path');
|
|
20
|
+
const crypto = require('crypto');
|
|
21
|
+
|
|
22
|
+
// Runtime config dirs a local PAN install lands in (mirrors installer getDirName).
|
|
23
|
+
const PAN_RUNTIME_DIRS = ['.claude', '.codex', '.gemini', '.opencode', '.github'];
|
|
24
|
+
|
|
25
|
+
// M62: only instrument actual PAN projects. A global-install hook fires in EVERY
|
|
26
|
+
// repo the user opens; without this gate it silently creates .planning/
|
|
27
|
+
// optimization + trace artifacts in non-PAN repos. A project counts as PAN if it
|
|
28
|
+
// already has a .planning/ tree (a /pan command created it) OR carries a local
|
|
29
|
+
// PAN install (a manifest / core payload under a runtime config dir — covers a
|
|
30
|
+
// fresh local install before any .planning/ exists). Global installs in a plain
|
|
31
|
+
// repo match neither, so the hook no-ops. Best-effort — never throws.
|
|
32
|
+
function isPanProject(cwd) {
|
|
33
|
+
try {
|
|
34
|
+
if (!cwd) return false;
|
|
35
|
+
if (fs.existsSync(path.join(cwd, '.planning'))) return true;
|
|
36
|
+
for (const d of PAN_RUNTIME_DIRS) {
|
|
37
|
+
if (fs.existsSync(path.join(cwd, d, 'pan-file-manifest.json'))) return true;
|
|
38
|
+
if (fs.existsSync(path.join(cwd, d, 'pan-wizard-core'))) return true;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
19
45
|
|
|
20
46
|
const PLANNING_DIR = '.planning';
|
|
21
47
|
const OPTIMIZE_DIR = 'optimization';
|
|
@@ -23,9 +49,10 @@ const TRACES_DIR = 'traces';
|
|
|
23
49
|
const CURRENT_SESSION_FILE = 'current-session';
|
|
24
50
|
const TRACE_EVENT_FILE = 'trace.jsonl';
|
|
25
51
|
|
|
26
|
-
// Trace event schema version — kept in sync by hand with pan-cost-logger.js
|
|
27
|
-
//
|
|
28
|
-
|
|
52
|
+
// Trace event schema version — kept in sync by hand with pan-cost-logger.js
|
|
53
|
+
// (standalone zero-dep hooks can't share a module). v3 added the per-invocation
|
|
54
|
+
// `event_sig` discriminator to the completion event's context. See that file.
|
|
55
|
+
const SCHEMA_V = 3;
|
|
29
56
|
|
|
30
57
|
// YYYYMMDD stamp for a Date (the day-scope of an auto-session id).
|
|
31
58
|
function dayStamp(d) {
|
|
@@ -114,9 +141,145 @@ function readTraceCursor(cwd) {
|
|
|
114
141
|
try { const c = JSON.parse(fs.readFileSync(traceCursorPath(cwd), 'utf-8')); return c && typeof c === 'object' ? c : {}; }
|
|
115
142
|
catch { return {}; }
|
|
116
143
|
}
|
|
144
|
+
|
|
145
|
+
// N17/N25-N27: reserved key in the cursor map recording, per transcript, the
|
|
146
|
+
// SIGNATURES of recently-seen SubagentStop events. Mirrors pan-cost-logger —
|
|
147
|
+
// an empty-slice event is a true re-fire / dual registration ONLY when its
|
|
148
|
+
// full-payload signature was already seen for that transcript; a parallel
|
|
149
|
+
// sibling (any payload difference) or a first-fire (missing/unreadable
|
|
150
|
+
// transcript) carries a new signature and must be recorded, not dropped.
|
|
151
|
+
// Bounded FIFO SET rather than a single slot (N25) so an interleaved sibling
|
|
152
|
+
// can't evict the signature identifying an earlier event's re-fire; bounded by
|
|
153
|
+
// COUNT, not transcript existence, so the L40 prune can't erase the live marker
|
|
154
|
+
// of a missing-transcript first fire (N27). See pan-cost-logger.js for the full
|
|
155
|
+
// design rationale (the two hooks are standalone and must stay in sync by hand).
|
|
156
|
+
const SEEN_EVENTS = '__seenEvents';
|
|
157
|
+
// Pre-N25 single-slot marker — no longer read; dropped on the next write.
|
|
158
|
+
const LEGACY_CONSUME_KEYS = '__consumeKeys';
|
|
159
|
+
// This window is the ONLY layer that recognizes a re-fire which is no longer the
|
|
160
|
+
// LATEST completion in the trace file: the dedup below compares against that one
|
|
161
|
+
// completion and nothing further. A sibling wave wider than this bound on one
|
|
162
|
+
// transcript therefore evicts the earliest sibling's signature, and a late
|
|
163
|
+
// re-fire of THAT sibling is emitted as a phantom completion. That residual is
|
|
164
|
+
// accepted deliberately, not overlooked — see eventSignature below for what
|
|
165
|
+
// widening either window was measured to cost.
|
|
166
|
+
const MAX_SEEN_SIGS = 8;
|
|
167
|
+
const MAX_SEEN_TRANSCRIPTS = 16;
|
|
168
|
+
|
|
169
|
+
// Full-payload hash: byte-identical for a dual-registration re-fire, different
|
|
170
|
+
// for a sibling whose payload differs in ANY field, with no hardcoding of which
|
|
171
|
+
// field a given runtime provides (N26).
|
|
172
|
+
//
|
|
173
|
+
// WHICH fields actually differ on real payloads is only partly established, so
|
|
174
|
+
// the sibling-admission benefit is CONDITIONAL. What this repo has observed:
|
|
175
|
+
// • `agent_type` / `subagent_type` is supplied and does vary between siblings
|
|
176
|
+
// of DIFFERENT type — the trace rows recorded under
|
|
177
|
+
// experiments/*/.planning/optimization/traces/ were written by THIS hook
|
|
178
|
+
// from real payloads and carry real agent names.
|
|
179
|
+
// • `session_id` is SHARED with the parent, and so is the session transcript
|
|
180
|
+
// (docs/FIELD-REPORT-army-2026-06.md, root cause 1).
|
|
181
|
+
// • `model` and `phase` came out null in those recorded rows — the payload
|
|
182
|
+
// carried neither; `usage` is absent entirely in headless mode
|
|
183
|
+
// (docs/HOOKS.md, P-1805).
|
|
184
|
+
// • No recorded payload in this repo carries an `agent_id` or any other
|
|
185
|
+
// per-invocation id; the hook tests inject one as a stand-in.
|
|
186
|
+
// So for two CONCURRENT SAME-TYPE siblings no varying payload field is confirmed
|
|
187
|
+
// on any host: where the host supplies one, both spawns are admitted; where it
|
|
188
|
+
// supplies none the payloads are the same bytes, hence indistinguishable from a
|
|
189
|
+
// re-fire, and the second stays suppressed.
|
|
190
|
+
//
|
|
191
|
+
// What the guards promise, stated as narrowly as they hold: a re-fire is
|
|
192
|
+
// suppressed while it is still RECOGNIZABLE — its signature is in this
|
|
193
|
+
// transcript's seen-event window (MAX_SEEN_SIGS), or it is byte-identical modulo
|
|
194
|
+
// `ts` to the LATEST completion already in the trace file. Nothing further. A
|
|
195
|
+
// re-fire arriving after its signature has been evicted from the marker window,
|
|
196
|
+
// and no longer adjacent to the completion it duplicates, IS emitted as a phantom
|
|
197
|
+
// completion. That is the residual; it is documented rather than engineered away.
|
|
198
|
+
//
|
|
199
|
+
// It was engineered away once, and the cure destroyed data. The dedup was widened
|
|
200
|
+
// to scan a tail of recent completions, with a second prong matching on a
|
|
201
|
+
// repeated signature alone for completions carrying no tokens. Both prongs delete
|
|
202
|
+
// real spawns. Five genuine spawns X,Y,X,Y,X on one shared transcript — the
|
|
203
|
+
// ordinary shared-session topology of docs/FIELD-REPORT-army-2026-06.md, where
|
|
204
|
+
// sequential same-type subagents deliver byte-identical payloads and therefore
|
|
205
|
+
// one signature — collapsed to TWO completions; when their slices carried real
|
|
206
|
+
// usage the collapsed completions' token counts vanished with them. Deleting real
|
|
207
|
+
// telemetry is strictly worse than the occasional phantom completion it prevents.
|
|
208
|
+
// The invariant that justified the contentless prong ("an event that consumed a
|
|
209
|
+
// real transcript slice never looks contentless") is also false: a slice of
|
|
210
|
+
// records that carry no `usage` and no `timestamp` yields zero on every axis and
|
|
211
|
+
// a null duration. So the dedup stays adjacent-only, and this comment states the
|
|
212
|
+
// residual instead of an invariant the code does not hold. Pinned in
|
|
213
|
+
// tests/trace-logger.test.cjs — `grep -n 'no genuine spawn' tests/trace-logger.test.cjs`.
|
|
214
|
+
//
|
|
215
|
+
// The NO-transcript path carries a wider residual: it never consults the marker
|
|
216
|
+
// layer at all (there is no transcript to key it by), so an INTERLEAVED dual
|
|
217
|
+
// registration there — A, B, A′, B′ — puts A′ and B′ out of adjacency reach and
|
|
218
|
+
// both are emitted as phantom completions. Closing that needs a lookback, and a
|
|
219
|
+
// lookback is the window whose failure mode is the data loss above, so it stays
|
|
220
|
+
// open.
|
|
221
|
+
//
|
|
222
|
+
// Where the guards CAN distinguish two events (payloads differ, so signatures
|
|
223
|
+
// differ) both are emitted — that is what the completion's `event_sig` is for.
|
|
224
|
+
// Where they CANNOT (identical bytes), the second is suppressed. The bias falls
|
|
225
|
+
// on the indistinguishable case only, deliberately: an undercount beats a
|
|
226
|
+
// phantom completion (N29).
|
|
227
|
+
//
|
|
228
|
+
// Its cost, stated because no other comment admits it: two SEQUENTIAL genuine
|
|
229
|
+
// spawns whose completions coincide in every field but `ts` collapse to one,
|
|
230
|
+
// losing the second's real counts. Inherent to an adjacent whole-record dedup,
|
|
231
|
+
// identical in the pre-`event_sig` code, and the alternative (no dedup)
|
|
232
|
+
// reinstates the duplicate-record bug. It is the one UNDER-count residual —
|
|
233
|
+
// distinct from the evicted-marker phantom (over-count) and from N29.
|
|
234
|
+
//
|
|
235
|
+
// Null when unserializable → fail open.
|
|
236
|
+
function eventSignature(data) {
|
|
237
|
+
try {
|
|
238
|
+
return crypto.createHash('sha1').update(JSON.stringify(data)).digest('hex');
|
|
239
|
+
} catch { return null; }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function getSeenSigs(cursor, transcriptPath) {
|
|
243
|
+
const se = cursor && cursor[SEEN_EVENTS];
|
|
244
|
+
const arr = se && typeof se === 'object' ? se[transcriptPath] : null;
|
|
245
|
+
return Array.isArray(arr) ? arr : [];
|
|
246
|
+
}
|
|
247
|
+
function addSeenSig(cursor, transcriptPath, sig) {
|
|
248
|
+
if (!sig) return; // unhashable payload → never mark (fail open to recording)
|
|
249
|
+
if (!cursor[SEEN_EVENTS] || typeof cursor[SEEN_EVENTS] !== 'object') cursor[SEEN_EVENTS] = {};
|
|
250
|
+
const se = cursor[SEEN_EVENTS];
|
|
251
|
+
const arr = Array.isArray(se[transcriptPath]) ? se[transcriptPath].filter((s) => s !== sig) : [];
|
|
252
|
+
arr.push(sig);
|
|
253
|
+
while (arr.length > MAX_SEEN_SIGS) arr.shift(); // FIFO — evict the oldest signature
|
|
254
|
+
delete se[transcriptPath]; // re-insert so key order tracks recency for the transcript cap
|
|
255
|
+
se[transcriptPath] = arr;
|
|
256
|
+
const keys = Object.keys(se);
|
|
257
|
+
for (let i = 0; i < keys.length - MAX_SEEN_TRANSCRIPTS; i++) delete se[keys[i]];
|
|
258
|
+
}
|
|
259
|
+
|
|
117
260
|
function writeTraceCursor(cwd, cursor) {
|
|
118
|
-
try {
|
|
119
|
-
|
|
261
|
+
try {
|
|
262
|
+
// Prune dead-transcript keys so the cursor map stays bounded (L40, ADR audit 2026-08).
|
|
263
|
+
const pruned = {};
|
|
264
|
+
for (const [tp, v] of Object.entries(cursor)) {
|
|
265
|
+
if (tp === SEEN_EVENTS || tp === LEGACY_CONSUME_KEYS) continue; // reserved markers — not paths
|
|
266
|
+
if (tp && fs.existsSync(tp)) pruned[tp] = v;
|
|
267
|
+
}
|
|
268
|
+
// Preserve the seen-event marker (N17/N25-N27). Deliberately NOT pruned by
|
|
269
|
+
// transcript existence — a missing-transcript first fire's marker must
|
|
270
|
+
// survive this very write (N27); bounded by count instead (L40).
|
|
271
|
+
const se = cursor[SEEN_EVENTS];
|
|
272
|
+
if (se && typeof se === 'object') {
|
|
273
|
+
const bounded = {};
|
|
274
|
+
for (const tp of Object.keys(se).slice(-MAX_SEEN_TRANSCRIPTS)) {
|
|
275
|
+
const arr = se[tp];
|
|
276
|
+
if (Array.isArray(arr) && arr.length) bounded[tp] = arr.slice(-MAX_SEEN_SIGS);
|
|
277
|
+
}
|
|
278
|
+
if (Object.keys(bounded).length) pruned[SEEN_EVENTS] = bounded;
|
|
279
|
+
}
|
|
280
|
+
fs.mkdirSync(path.dirname(traceCursorPath(cwd)), { recursive: true });
|
|
281
|
+
fs.writeFileSync(traceCursorPath(cwd), JSON.stringify(pruned), 'utf-8');
|
|
282
|
+
} catch { /* best-effort — never block the agent loop */ }
|
|
120
283
|
}
|
|
121
284
|
|
|
122
285
|
/**
|
|
@@ -275,6 +438,11 @@ function buildTraceEvents(data, sessionId, cwd) {
|
|
|
275
438
|
|
|
276
439
|
const ts = new Date().toISOString();
|
|
277
440
|
const agent = data.agent_type || data.subagent_type || 'unknown';
|
|
441
|
+
// This event's per-invocation identity, hashed once and used by BOTH layers:
|
|
442
|
+
// the seen-event marker below and the completion's `event_sig` context field.
|
|
443
|
+
// buildTraceEvents never mutates `data`, so hoisting the hash here yields the
|
|
444
|
+
// same value the marker calls used when they each computed it themselves.
|
|
445
|
+
const eventSig = eventSignature(data);
|
|
278
446
|
|
|
279
447
|
// Per-call tokens come from the transcript SLICE. The SubagentStop `data.usage`,
|
|
280
448
|
// when present, is a CUMULATIVE session counter — not this subagent's delta — so
|
|
@@ -298,7 +466,35 @@ function buildTraceEvents(data, sessionId, cwd) {
|
|
|
298
466
|
durationMs = durationFromSpan(fromTranscript.first_ts, fromTranscript.last_ts);
|
|
299
467
|
if (!model) model = fromTranscript.model;
|
|
300
468
|
if (cwd && fromTranscript.lineCount > since) {
|
|
469
|
+
// A real slice. Advance the cursor and remember this event's signature
|
|
470
|
+
// (N17/N25) so a later empty-slice event can tell its re-fire from a
|
|
471
|
+
// parallel sibling — even when other siblings are recorded in between (N25).
|
|
301
472
|
cursor[data.transcript_path] = fromTranscript.lineCount;
|
|
473
|
+
addSeenSig(cursor, data.transcript_path, eventSig);
|
|
474
|
+
writeTraceCursor(cwd, cursor);
|
|
475
|
+
} else if (cwd && fromTranscript.lineCount <= since) {
|
|
476
|
+
// No transcript records past the cursor: this event consumed NO slice of its
|
|
477
|
+
// own. Two situations land here (N17):
|
|
478
|
+
// • A re-fire / dual global+local hook registration — the SAME event
|
|
479
|
+
// delivered again (byte-identical payload). Its all-zero completion row
|
|
480
|
+
// is a phantom the dedup cannot catch (zeros differ from the real row
|
|
481
|
+
// it follows), so emit nothing (M61). This marker is the ONLY layer
|
|
482
|
+
// that catches that case; there is no signature-matching backstop below
|
|
483
|
+
// it, by design (see eventSignature).
|
|
484
|
+
// • A PARALLEL SIBLING (another subagent — same or different type — whose
|
|
485
|
+
// sibling already consumed the shared transcript to EOF and advanced
|
|
486
|
+
// this shared cursor) or a FIRST FIRE with a missing/unreadable
|
|
487
|
+
// transcript. These are legitimate spawns that must be RECORDED (zero
|
|
488
|
+
// tokens), not dropped.
|
|
489
|
+
// The full-payload signature distinguishes them (N25/N26): emit nothing
|
|
490
|
+
// ONLY when this exact payload was already seen for this transcript;
|
|
491
|
+
// otherwise fall through and emit the completion.
|
|
492
|
+
if (eventSig && getSeenSigs(cursor, data.transcript_path).includes(eventSig)) {
|
|
493
|
+
return []; // already-seen event → re-fire; emit nothing (M61)
|
|
494
|
+
}
|
|
495
|
+
// Sibling / first-fire: remember this event's signature so its own re-fire
|
|
496
|
+
// is subsequently dropped, then fall through to emit the completion.
|
|
497
|
+
addSeenSig(cursor, data.transcript_path, eventSig);
|
|
302
498
|
writeTraceCursor(cwd, cursor);
|
|
303
499
|
}
|
|
304
500
|
} else {
|
|
@@ -341,6 +537,22 @@ function buildTraceEvents(data, sessionId, cwd) {
|
|
|
341
537
|
exit_code: data.exit_code || 0,
|
|
342
538
|
token_source: tokenSource,
|
|
343
539
|
clamped,
|
|
540
|
+
// This spawn's per-invocation discriminator: the event signature,
|
|
541
|
+
// persisted. The signature was already hashed for the seen-event marker
|
|
542
|
+
// but never written into the event, so two parallel same-type siblings
|
|
543
|
+
// produced completions byte-identical modulo `ts` and the dedup dropped the
|
|
544
|
+
// second one even though the marker layer had correctly admitted it (N26).
|
|
545
|
+
// Persisting it gives both layers ONE notion of event identity: siblings
|
|
546
|
+
// differ here even when every other field matches, while a re-fire carries
|
|
547
|
+
// the same signature and still matches the completion it duplicates.
|
|
548
|
+
// It is a field the dedup COMPARES as part of whole-event identity — never
|
|
549
|
+
// a key the dedup searches the trace file by. Two sequential subagents on a
|
|
550
|
+
// shared growing transcript share one payload and therefore one signature
|
|
551
|
+
// while holding different real token counts, so matching on the signature
|
|
552
|
+
// alone deletes genuine completions (see eventSignature). Mirrors
|
|
553
|
+
// pan-cost-logger's ledger-row field of the same name. null when the
|
|
554
|
+
// payload could not be hashed.
|
|
555
|
+
event_sig: eventSig,
|
|
344
556
|
},
|
|
345
557
|
impact: 'trivial',
|
|
346
558
|
correction: null,
|
|
@@ -383,10 +595,12 @@ function appendTraceEvents(cwd, events, sessionId) {
|
|
|
383
595
|
const sessionDir = path.join(getTracesDir(cwd), sessionId);
|
|
384
596
|
fs.mkdirSync(sessionDir, { recursive: true });
|
|
385
597
|
const file = path.join(sessionDir, TRACE_EVENT_FILE);
|
|
386
|
-
// Idempotency guard: a re-fired SubagentStop must not double-log
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
//
|
|
598
|
+
// Idempotency guard: a re-fired SubagentStop must not double-log — the
|
|
599
|
+
// source of the ~57% duplicate completion rows in the field (2026-07). If
|
|
600
|
+
// this batch's completion event duplicates the LAST agent_completion already
|
|
601
|
+
// in the file (every field but `ts`), skip the whole batch — its redundancy
|
|
602
|
+
// event, if any, is derived from the same numbers. Adjacent-only on purpose;
|
|
603
|
+
// see isDuplicateCompletion.
|
|
390
604
|
const completion = events.find(e => e && e.category === 'agent_completion');
|
|
391
605
|
if (completion && isDuplicateCompletion(file, completion)) return false;
|
|
392
606
|
const lines = events.map(e => JSON.stringify(e)).join('\n') + '\n';
|
|
@@ -397,14 +611,37 @@ function appendTraceEvents(cwd, events, sessionId) {
|
|
|
397
611
|
}
|
|
398
612
|
}
|
|
399
613
|
|
|
400
|
-
/**
|
|
614
|
+
/**
|
|
615
|
+
* True when `completion` matches the file's LAST agent_completion, ignoring `ts`.
|
|
616
|
+
*
|
|
617
|
+
* Adjacent-only, and that is the design rather than an oversight. The comparison
|
|
618
|
+
* covers every field including the `event_sig` discriminator in `context`, so a
|
|
619
|
+
* true re-fire (same payload → same signature → same completion) still matches
|
|
620
|
+
* the one it follows, while two siblings the payload can distinguish both survive
|
|
621
|
+
* (N26).
|
|
622
|
+
*
|
|
623
|
+
* It deliberately does NOT scan back over a tail of recent completions, and does
|
|
624
|
+
* not match on a repeated signature. Both of those were tried and both delete
|
|
625
|
+
* real spawns: an identity scan over a window collapses genuine repeat spawns
|
|
626
|
+
* whose completions coincide, and a signature prong collapses genuine spawns that
|
|
627
|
+
* share a payload — the ordinary shared-transcript topology. eventSignature
|
|
628
|
+
* carries the measured reproduction and the residual this leaves standing;
|
|
629
|
+
* tests/trace-logger.test.cjs pins both
|
|
630
|
+
* (`grep -n 'no genuine spawn' tests/trace-logger.test.cjs`).
|
|
631
|
+
*
|
|
632
|
+
* Completions written before the discriminator existed hold no `event_sig` and an
|
|
633
|
+
* older `v`, so one written now never equals them. The bounded consequence: the
|
|
634
|
+
* first append after a schema bump can land beside a pre-bump completion without
|
|
635
|
+
* matching it, and the guard resumes on the following same-shape pair. Those
|
|
636
|
+
* older events still parse and still analyse — optimize.cjs reads a trace event
|
|
637
|
+
* field by field and requires no particular version or field to be present.
|
|
638
|
+
*/
|
|
401
639
|
function isDuplicateCompletion(file, completion) {
|
|
402
640
|
let last;
|
|
403
641
|
try {
|
|
404
|
-
const
|
|
405
|
-
for (const line of raw.split('\n')) {
|
|
642
|
+
for (const line of fs.readFileSync(file, 'utf-8').split('\n')) {
|
|
406
643
|
if (!line) continue;
|
|
407
|
-
let e; try { e = JSON.parse(line); } catch { continue; }
|
|
644
|
+
let e; try { e = JSON.parse(line); } catch { continue; } // skip malformed rows
|
|
408
645
|
if (e && e.category === 'agent_completion') last = e;
|
|
409
646
|
}
|
|
410
647
|
} catch {
|
|
@@ -425,7 +662,11 @@ if (require.main === module) {
|
|
|
425
662
|
try {
|
|
426
663
|
const data = JSON.parse(input);
|
|
427
664
|
const cwd = data.cwd || data.workspace?.current_dir || process.cwd();
|
|
428
|
-
//
|
|
665
|
+
// M62: a global-install hook fires in every repo; skip non-PAN projects so
|
|
666
|
+
// we don't create .planning/ optimization + trace artifacts in them.
|
|
667
|
+
if (!isPanProject(cwd)) return;
|
|
668
|
+
// In a PAN project, ensure a session exists — creates a day-scoped
|
|
669
|
+
// auto-session if needed.
|
|
429
670
|
const sessionId = ensureSessionId(cwd);
|
|
430
671
|
const events = buildTraceEvents(data, sessionId, cwd);
|
|
431
672
|
appendTraceEvents(cwd, events, sessionId);
|
|
@@ -440,6 +681,7 @@ module.exports = {
|
|
|
440
681
|
appendTraceEvents,
|
|
441
682
|
getCurrentSessionId,
|
|
442
683
|
ensureSessionId,
|
|
684
|
+
isPanProject,
|
|
443
685
|
PLANNING_DIR,
|
|
444
686
|
OPTIMIZE_DIR,
|
|
445
687
|
TRACES_DIR,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pan-wizard",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.24.0",
|
|
4
4
|
"description": "Command a bot army for your codebase: an Opus Mission Control delegates whole-project goals to specialist squads and ships behind a human merge gate. Five AI CLIs, zero context rot.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"pan-wizard": "bin/install.js"
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"test:all": "node scripts/run-tests.cjs tests tests/scenarios",
|
|
69
69
|
"test:e2e": "node scripts/run-tests.cjs tests/scenarios",
|
|
70
70
|
"test:vscode": "npx playwright test --config tests/e2e/playwright.config.mjs",
|
|
71
|
-
"test:watch": "node
|
|
71
|
+
"test:watch": "node scripts/run-tests.cjs --watch tests tests/scenarios",
|
|
72
72
|
"build:plugin": "node scripts/build-plugin.js"
|
|
73
73
|
}
|
|
74
74
|
}
|
|
@@ -216,7 +216,7 @@ function recommendForPhase(cwd, phaseNum, opts) {
|
|
|
216
216
|
|
|
217
217
|
return {
|
|
218
218
|
phase: String(phaseNum),
|
|
219
|
-
phase_name: phaseInfo.
|
|
219
|
+
phase_name: phaseInfo.phase_name || null,
|
|
220
220
|
runtime: cache.runtime,
|
|
221
221
|
total_candidates: tools.length,
|
|
222
222
|
recommendations: scored.map(t => ({
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* Each line: {ts, source, payload}
|
|
12
12
|
*
|
|
13
13
|
* Channels are created on first publish. Readers use cursor-based drain
|
|
14
|
-
* (read N lines from an offset) or consume
|
|
14
|
+
* (read N lines from an offset) or partial-consume drain (read the drained
|
|
15
|
+
* window, splice just those lines out, and write the remainder back).
|
|
15
16
|
*
|
|
16
17
|
* Concurrent-write safety: each publish opens the file with append flag
|
|
17
18
|
* (`a`) which the OS treats atomically for writes <PIPE_BUF on POSIX and
|
|
@@ -135,11 +136,15 @@ function readChannel(cwd, channel, opts) {
|
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
/**
|
|
138
|
-
* Drain (read + optionally
|
|
139
|
+
* Drain (read + optionally mutate) messages from a channel.
|
|
139
140
|
*
|
|
140
141
|
* Three drain modes:
|
|
141
142
|
* - `peek` (default): read entries, leave file untouched
|
|
142
|
-
* - `consume`: read entries,
|
|
143
|
+
* - `consume`: read entries, then splice out ONLY the drained window
|
|
144
|
+
* (offset .. offset+consumed) and write the remainder back — messages
|
|
145
|
+
* outside that window (before the offset, or beyond the read limit / the
|
|
146
|
+
* 1000-message default on large channels) survive. The file is emptied only
|
|
147
|
+
* when the drained window covers every line.
|
|
143
148
|
* - `archive`: read entries, rename file to `<channel>-<ts>.archive.jsonl` so
|
|
144
149
|
* historical data is preserved while the channel restarts empty
|
|
145
150
|
*
|
|
@@ -158,7 +163,17 @@ function drain(cwd, channel, opts) {
|
|
|
158
163
|
const file = channelFile(cwd, channel);
|
|
159
164
|
if (mode === 'consume') {
|
|
160
165
|
try {
|
|
161
|
-
|
|
166
|
+
// Consume ONLY the drained window (offset .. offset+limit). Truncating the
|
|
167
|
+
// whole file to empty silently destroyed every message outside that window
|
|
168
|
+
// — anything before the offset, or beyond the read limit / the 1000-message
|
|
169
|
+
// default on large channels (H2, ADR audit 2026-08). Splice out the consumed
|
|
170
|
+
// lines and write the remainder back.
|
|
171
|
+
let allLines = [];
|
|
172
|
+
try { allLines = fs.readFileSync(file, 'utf-8').split('\n').filter(Boolean); } catch { allLines = []; }
|
|
173
|
+
const offset = Math.max(0, Number(opts?.offset) || 0);
|
|
174
|
+
const consumed = read.entries.length; // raw lines actually returned by readChannel
|
|
175
|
+
const remaining = allLines.slice(0, offset).concat(allLines.slice(offset + consumed));
|
|
176
|
+
fs.writeFileSync(file, remaining.length ? remaining.join('\n') + '\n' : '', 'utf-8');
|
|
162
177
|
} catch (e) {
|
|
163
178
|
return { ...read, mode, drain_error: e.message };
|
|
164
179
|
}
|
|
@@ -152,7 +152,11 @@ function isDreamDue(schedule, now) {
|
|
|
152
152
|
if (!schedule.last_run) return false;
|
|
153
153
|
const last = new Date(schedule.last_run);
|
|
154
154
|
if (isNaN(last)) return false;
|
|
155
|
-
|
|
155
|
+
// Due only once the last run rolled over into a NEW calendar day AND there is
|
|
156
|
+
// recorded activity to reflect on. The old `||` made this permanently true
|
|
157
|
+
// after the first run (history is never empty once a run is recorded), which
|
|
158
|
+
// contradicted the "once per calendar day that had activity" contract.
|
|
159
|
+
return !sameUtcDay(last, at) && (schedule.history || []).length > 0;
|
|
156
160
|
}
|
|
157
161
|
|
|
158
162
|
// ─── CLI ─────────────────────────────────────────────────────────────────────
|
|
@@ -190,8 +194,9 @@ function cmdCampaignStatus(cwd, raw) {
|
|
|
190
194
|
function cmdCampaignDue(cwd, raw) {
|
|
191
195
|
const schedule = readSchedule(cwd);
|
|
192
196
|
const d = isRunDue(schedule, new Date());
|
|
193
|
-
// exit-coded so a host scheduler can gate: 0 = due, 1 = not due
|
|
194
|
-
|
|
197
|
+
// exit-coded so a host scheduler can gate: 0 = due, 1 = not due (M9 — output()
|
|
198
|
+
// used to hard-exit 0, so the documented gate always fired the run).
|
|
199
|
+
output({ due: d.due, reason: d.reason, next_due: d.next_due }, raw, d.due ? 'due' : `not due (${d.reason})`, d.due ? 0 : 1);
|
|
195
200
|
}
|
|
196
201
|
|
|
197
202
|
module.exports = {
|
|
@@ -98,8 +98,20 @@ function detectLanguages(cwd) {
|
|
|
98
98
|
} catch { /* manifest not found */ }
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
// TypeScript subsumes javascript
|
|
102
|
-
|
|
101
|
+
// TypeScript subsumes javascript only in a genuine TS project: there must be
|
|
102
|
+
// actual .ts source files AND a tsconfig.json. A bare tsconfig.json (common for
|
|
103
|
+
// editor type-checking of plain JS) or a manifest-promoted empty typescript array
|
|
104
|
+
// must NOT relabel a JS codebase as TypeScript.
|
|
105
|
+
let hasTsconfig = false;
|
|
106
|
+
try {
|
|
107
|
+
fs.accessSync(path.join(cwd, 'tsconfig.json'));
|
|
108
|
+
hasTsconfig = true;
|
|
109
|
+
} catch { /* no tsconfig */ }
|
|
110
|
+
if (
|
|
111
|
+
hasTsconfig &&
|
|
112
|
+
files_by_language.typescript && files_by_language.typescript.length > 0 &&
|
|
113
|
+
files_by_language.javascript && files_by_language.javascript.length > 0
|
|
114
|
+
) {
|
|
103
115
|
// Merge JS files under TypeScript project
|
|
104
116
|
files_by_language.typescript = files_by_language.typescript.concat(files_by_language.javascript);
|
|
105
117
|
delete files_by_language.javascript;
|
|
@@ -546,7 +558,6 @@ function detectErrorHandling(samples) {
|
|
|
546
558
|
}
|
|
547
559
|
|
|
548
560
|
function detectTestingPractices(cwd, sourceFileCount) {
|
|
549
|
-
const patterns = ['*.test.*', '*.spec.*', '__tests__/**'];
|
|
550
561
|
let testFileCount = 0;
|
|
551
562
|
|
|
552
563
|
try {
|
|
@@ -690,9 +701,8 @@ function cmdDetectLanguages(cwd, raw) {
|
|
|
690
701
|
* CLI: Analyze imports and build dependency graph.
|
|
691
702
|
* @param {string} cwd - Project root
|
|
692
703
|
* @param {boolean} raw - Raw output flag
|
|
693
|
-
* @param {string[]} args - Additional arguments (--files f1,f2)
|
|
694
704
|
*/
|
|
695
|
-
function cmdAnalyzeImports(cwd, raw
|
|
705
|
+
function cmdAnalyzeImports(cwd, raw) {
|
|
696
706
|
const graph = buildDependencyGraph(cwd);
|
|
697
707
|
const circularDeps = findCircularDeps(graph);
|
|
698
708
|
const entryPoints = findEntryPoints(graph);
|