pan-wizard 3.26.0 → 3.27.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 +1 -1
- package/commands/pan/hygiene.md +14 -8
- package/commands/pan/milestone-audit.md +10 -4
- package/hooks/dist/pan-cost-logger.js +69 -5
- package/hooks/dist/pan-stop-guard.js +32 -1
- package/hooks/dist/pan-trace-logger.js +35 -2
- package/package.json +1 -1
- package/pan-wizard-core/bin/lib/bridge.cjs +0 -1
- package/pan-wizard-core/bin/lib/bus.cjs +0 -1
- package/pan-wizard-core/bin/lib/campaign.cjs +3 -2
- package/pan-wizard-core/bin/lib/commands-learnings.cjs +8 -8
- package/pan-wizard-core/bin/lib/commands.cjs +15 -14
- package/pan-wizard-core/bin/lib/config.cjs +5 -5
- package/pan-wizard-core/bin/lib/constants.cjs +27 -0
- package/pan-wizard-core/bin/lib/context-budget.cjs +28 -0
- package/pan-wizard-core/bin/lib/core.cjs +190 -26
- package/pan-wizard-core/bin/lib/cost.cjs +0 -1
- package/pan-wizard-core/bin/lib/distill.cjs +3 -3
- package/pan-wizard-core/bin/lib/focus.cjs +16 -16
- package/pan-wizard-core/bin/lib/hud.cjs +1 -1
- package/pan-wizard-core/bin/lib/hygiene.cjs +397 -37
- package/pan-wizard-core/bin/lib/init.cjs +90 -13
- package/pan-wizard-core/bin/lib/knowledge.cjs +0 -1
- package/pan-wizard-core/bin/lib/memory.cjs +1 -1
- package/pan-wizard-core/bin/lib/milestone.cjs +3 -3
- package/pan-wizard-core/bin/lib/optimize.cjs +3 -3
- package/pan-wizard-core/bin/lib/phase.cjs +4 -4
- package/pan-wizard-core/bin/lib/planning-root.cjs +327 -0
- package/pan-wizard-core/bin/lib/preview.cjs +0 -1
- package/pan-wizard-core/bin/lib/review-deep.cjs +0 -1
- package/pan-wizard-core/bin/lib/roadmap.cjs +1 -1
- package/pan-wizard-core/bin/lib/state-compact.cjs +339 -0
- package/pan-wizard-core/bin/lib/state.cjs +0 -1
- package/pan-wizard-core/bin/lib/template.cjs +1 -1
- package/pan-wizard-core/bin/lib/utils.cjs +39 -11
- package/pan-wizard-core/bin/lib/verify.cjs +4 -3
- package/pan-wizard-core/bin/lib/whatif.cjs +0 -1
- package/pan-wizard-core/bin/pan-tools.cjs +58 -4
- package/pan-wizard-core/workflows/milestone-audit.md +35 -6
package/README.md
CHANGED
|
@@ -659,7 +659,7 @@ PAN is not a replacement for your IDE or AI agent — it's the orchestration lay
|
|
|
659
659
|
| `/pan:debug [desc]` | Systematic debugging with persistent state |
|
|
660
660
|
| `/pan:quick [--full]` | Execute ad-hoc task with PAN guarantees (`--full` adds plan-checking and verification) |
|
|
661
661
|
| `/pan:health [--repair]` | Validate `.planning/` directory integrity; `--repair` auto-fixes detected issues |
|
|
662
|
-
| `/pan:hygiene [--apply] [--trace-age-days N]` | Scan for PAN version drift and stale project artifacts (legacy filenames, .tmp orphans, memory bloat, poisoned cost ledgers, trace debris, fragment planning dirs); `--apply` executes the safe fixes — ledgers are quarantined by rename, never deleted |
|
|
662
|
+
| `/pan:hygiene [--apply] [--trace-age-days N] [--all-tracks]` | Scan for PAN version drift and stale project artifacts (legacy filenames, .tmp orphans, memory bloat, poisoned cost ledgers, trace and report debris, cached-context bloat, fragment planning dirs); `--apply` executes the safe fixes — ledgers are quarantined by rename, never deleted, and settled `state.md` history is archived rather than dropped |
|
|
663
663
|
| `/pan:links [--strict]` | Validate the doc-code link graph: inline `[[<id>]]` refs, `// @pan:` source anchors, `require-code-mention` contracts (ADR-0027, v3.8.0+) |
|
|
664
664
|
| `/pan:phase-tests [N]` | Generate tests for a completed phase based on UAT criteria |
|
|
665
665
|
| `/pan:milestone-cleanup` | Archive accumulated phase directories from completed milestones |
|
package/commands/pan/hygiene.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: pan:hygiene
|
|
3
3
|
group: System
|
|
4
4
|
description: Scan the project for PAN version drift and stale artifacts (legacy filenames, memory bloat, poisoned ledgers, trace debris) and apply safe cleanups
|
|
5
|
-
argument-hint: "[--apply] [--trace-age-days N]"
|
|
5
|
+
argument-hint: "[--apply] [--trace-age-days N] [--all-tracks] [--track <name>]"
|
|
6
6
|
allowed-tools:
|
|
7
7
|
- Read
|
|
8
8
|
- Bash
|
|
@@ -17,12 +17,18 @@ Keep a PAN-managed project aligned with the latest PAN version and free of accum
|
|
|
17
17
|
## 1. Scan
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
SCAN=$(node ~/.claude/pan-wizard-core/bin/pan-tools.cjs hygiene scan)
|
|
20
|
+
SCAN=$(node ~/.claude/pan-wizard-core/bin/pan-tools.cjs hygiene scan --all-tracks)
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Pass `--all-tracks` by default: a project may hold several planning trees (`.planning/` plus `.planning/tracks/<name>/`), and scanning only the root tree reports "clean" for debris sitting in a sibling track. Use `--track <name>` to scan one specific tree instead.
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Parse JSON: `findings[]` (`check`, `severity`, `path`, `detail`, `fixable`, `track`), `installs[]`, `latest_version`, `roots_scanned[]`, `summary` (including `summary.by_track`).
|
|
26
|
+
|
|
27
|
+
Display the findings grouped by severity (critical → warn → info), labelling each with its `track` when more than one tree was scanned.
|
|
28
|
+
|
|
29
|
+
**Always state which trees were scanned** — read `roots_scanned[]` and name them. If `summary.total` is 0, report "Project is clean and aligned" *together with* the list of trees that were read. A clean verdict without its scope is what let stale debris hide in an unscanned track; never report one without the other.
|
|
30
|
+
|
|
31
|
+
If any root has `planning_root_exists: false`, say so plainly — that is a mistyped `--track`, not a clean tree.
|
|
26
32
|
|
|
27
33
|
## 2. Version drift (manual remediation)
|
|
28
34
|
|
|
@@ -41,7 +47,7 @@ Hygiene never runs the installer itself.
|
|
|
41
47
|
**Without `--apply` in $ARGUMENTS:** run the dry-run and present what WOULD change:
|
|
42
48
|
|
|
43
49
|
```bash
|
|
44
|
-
node ~/.claude/pan-wizard-core/bin/pan-tools.cjs hygiene clean
|
|
50
|
+
node ~/.claude/pan-wizard-core/bin/pan-tools.cjs hygiene clean --all-tracks
|
|
45
51
|
```
|
|
46
52
|
|
|
47
53
|
Then ask the user (AskUserQuestion, header "Apply fixes", options: "Apply safe fixes" / "Skip") unless running headless — in auto/headless contexts, report the dry-run only and stop.
|
|
@@ -49,14 +55,14 @@ Then ask the user (AskUserQuestion, header "Apply fixes", options: "Apply safe f
|
|
|
49
55
|
**With `--apply` (or after user confirmation):**
|
|
50
56
|
|
|
51
57
|
```bash
|
|
52
|
-
node ~/.claude/pan-wizard-core/bin/pan-tools.cjs hygiene clean --apply
|
|
58
|
+
node ~/.claude/pan-wizard-core/bin/pan-tools.cjs hygiene clean --all-tracks --apply
|
|
53
59
|
```
|
|
54
60
|
|
|
55
|
-
Safe fixes are: lowercase renames of legacy planning filenames, deletion of aged .tmp orphans, memory-log compaction, poisoned-ledger quarantine (rename in place — never deleted),
|
|
61
|
+
Safe fixes are: lowercase renames of legacy planning filenames, deletion of aged .tmp orphans, memory-log compaction, poisoned-ledger quarantine (rename in place — never deleted), pruning of trace sessions **and optimization reports** past retention (newest 5 of each always kept), and **state.md compaction** (`compact-state`) — settled history is archived to `state-history.md` so it stops being re-read into every agent call. Nothing is deleted by any of these: the ledger is renamed, and state history is written to the archive before state.md is rewritten. Each fix is applied inside its own tree's scope, so a track's debris is cleaned in that track. Pass through `--trace-age-days N` and any `--track <name>` if provided.
|
|
56
62
|
|
|
57
63
|
## 4. Report
|
|
58
64
|
|
|
59
|
-
Summarize: fixes executed / failed / left manual, plus the installer command if version drift remains. Recommend re-running `/pan:hygiene` after the installer to confirm alignment.
|
|
65
|
+
Summarize: the trees scanned, fixes executed / failed / left manual (attributed per track), plus the installer command if version drift remains. If a `cache-context` finding appeared, state the per-call token cost it represents — that block is re-read on **every** agent call, so it is the project's largest recurring expense. Recommend re-running `/pan:hygiene` after the installer to confirm alignment.
|
|
60
66
|
|
|
61
67
|
</process>
|
|
62
68
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: pan:milestone-audit
|
|
3
3
|
group: Milestone
|
|
4
4
|
description: Audit milestone completion against original intent before archiving
|
|
5
|
-
argument-hint: "[version]"
|
|
5
|
+
argument-hint: "[version] [--track <name>] [--all-tracks]"
|
|
6
6
|
allowed-tools:
|
|
7
7
|
- Read
|
|
8
8
|
- Glob
|
|
@@ -26,9 +26,11 @@ Version: $ARGUMENTS (optional — defaults to current milestone)
|
|
|
26
26
|
|
|
27
27
|
Core planning files are resolved in-workflow (`init milestone-op`) and loaded only as needed.
|
|
28
28
|
|
|
29
|
-
**
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
**Planning tree:** the project may hold several. `init milestone-op` reports the `planning_root` it resolved; use that root for every glob below rather than assuming `.planning/`. Pass `--track <name>` to audit a specific tree, or run `init milestone-op --all-tracks` first to see every tree's milestone state.
|
|
30
|
+
|
|
31
|
+
**Completed Work:** (relative to the resolved `planning_root`)
|
|
32
|
+
Glob: {planning_root}/phases/*/*-summary.md
|
|
33
|
+
Glob: {planning_root}/phases/*/*-verification.md
|
|
32
34
|
</context>
|
|
33
35
|
|
|
34
36
|
<citation_requirement>
|
|
@@ -57,4 +59,8 @@ Do not trust summary files at face value. If a verification.md says "all tests p
|
|
|
57
59
|
<process>
|
|
58
60
|
Execute the audit-milestone workflow from @~/.claude/pan-wizard-core/workflows/milestone-audit.md end-to-end.
|
|
59
61
|
Preserve all workflow gates (scope determination, verification reading, integration check, requirements coverage, routing).
|
|
62
|
+
|
|
63
|
+
Two gates are non-negotiable because they guard against auditing the wrong thing:
|
|
64
|
+
- **State the resolved `planning_root` in the report.** An audit that does not name the tree it read cannot be checked.
|
|
65
|
+
- **Stop if `milestone_ambiguous` is true.** More than one milestone marked current is a roadmap defect for the owner to fix; auditing one of them silently is how a report ends up describing a milestone that does not exist.
|
|
60
66
|
</process>
|
|
@@ -20,6 +20,38 @@ const crypto = require('crypto');
|
|
|
20
20
|
// Runtime config dirs a local PAN install lands in (mirrors installer getDirName).
|
|
21
21
|
const PAN_RUNTIME_DIRS = ['.claude', '.codex', '.gemini', '.opencode', '.github'];
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Which planning tree this hook writes to.
|
|
25
|
+
*
|
|
26
|
+
* Mirrors pan-wizard-core/bin/lib/planning-root.cjs, which the hook cannot
|
|
27
|
+
* require (hooks are standalone and run inside the host runtime). Without this
|
|
28
|
+
* the CLI could be pointed at `--track verify` while the cost hook kept writing
|
|
29
|
+
* to `.planning/`, so a track's telemetry landed in the wrong tree.
|
|
30
|
+
*
|
|
31
|
+
* Env only — a hook gets no argv. Values that escape the project root are
|
|
32
|
+
* ignored rather than honoured; a bad value must degrade to the default, never
|
|
33
|
+
* write outside the project.
|
|
34
|
+
*/
|
|
35
|
+
function planningDirName() {
|
|
36
|
+
const raw = process.env.PAN_PLANNING_DIR || '';
|
|
37
|
+
if (raw.trim()) {
|
|
38
|
+
const rel = raw.trim().replace(/\\/g, '/');
|
|
39
|
+
const bad = rel.startsWith('/') || rel.startsWith('\\') || /^[A-Za-z]:/.test(rel)
|
|
40
|
+
|| rel.split('/').includes('..');
|
|
41
|
+
if (!bad) return rel;
|
|
42
|
+
}
|
|
43
|
+
const track = (process.env.PAN_TRACK || '').trim();
|
|
44
|
+
if (track && /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(track)) {
|
|
45
|
+
return `.planning/tracks/${track}`;
|
|
46
|
+
}
|
|
47
|
+
return '.planning';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Absolute path inside the active planning tree. */
|
|
51
|
+
function planningPath(cwd, ...segments) {
|
|
52
|
+
return path.join(cwd, ...planningDirName().split('/'), ...segments);
|
|
53
|
+
}
|
|
54
|
+
|
|
23
55
|
// M62: only instrument actual PAN projects. A global-install hook fires in EVERY
|
|
24
56
|
// repo the user opens; without this gate it silently creates .planning/ metrics
|
|
25
57
|
// artifacts in non-PAN repos. A project counts as PAN if it already has a
|
|
@@ -30,7 +62,7 @@ const PAN_RUNTIME_DIRS = ['.claude', '.codex', '.gemini', '.opencode', '.github'
|
|
|
30
62
|
function isPanProject(cwd) {
|
|
31
63
|
try {
|
|
32
64
|
if (!cwd) return false;
|
|
33
|
-
if (fs.existsSync(
|
|
65
|
+
if (fs.existsSync(planningPath(cwd))) return true;
|
|
34
66
|
for (const d of PAN_RUNTIME_DIRS) {
|
|
35
67
|
if (fs.existsSync(path.join(cwd, d, 'pan-file-manifest.json'))) return true;
|
|
36
68
|
if (fs.existsSync(path.join(cwd, d, 'pan-wizard-core'))) return true;
|
|
@@ -71,7 +103,7 @@ function tierForModel(model) {
|
|
|
71
103
|
// Never throws — returns {} on any miss.
|
|
72
104
|
function readActiveSessionMeta(cwd) {
|
|
73
105
|
try {
|
|
74
|
-
const optDir =
|
|
106
|
+
const optDir = planningPath(cwd, 'optimization');
|
|
75
107
|
const sid = fs.readFileSync(path.join(optDir, 'current-session'), 'utf-8').trim();
|
|
76
108
|
if (!sid) return {};
|
|
77
109
|
const meta = JSON.parse(fs.readFileSync(path.join(optDir, 'traces', sid, 'session.json'), 'utf-8'));
|
|
@@ -81,6 +113,35 @@ function readActiveSessionMeta(cwd) {
|
|
|
81
113
|
}
|
|
82
114
|
}
|
|
83
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Current phase from state.md — the fallback when no optimizer trace is running.
|
|
118
|
+
*
|
|
119
|
+
* Phase attribution used to come ONLY from the active trace session, and
|
|
120
|
+
* tracing is off by default, so in normal use every ledger row carried
|
|
121
|
+
* `phase: null`. A field ledger had 121 rows and 100% of them were unattributed,
|
|
122
|
+
* which makes "which phase got expensive" unanswerable from PAN's own telemetry
|
|
123
|
+
* — exactly the question a slowdown raises.
|
|
124
|
+
*
|
|
125
|
+
* state.md is authoritative for the current phase and is present whenever the
|
|
126
|
+
* phase model is in use. Frontmatter first (cheap, canonical), then the
|
|
127
|
+
* `**Current Phase:**` body field that `extractFieldsFromState` reads.
|
|
128
|
+
*/
|
|
129
|
+
function readCurrentPhase(cwd) {
|
|
130
|
+
try {
|
|
131
|
+
const content = fs.readFileSync(planningPath(cwd, 'state.md'), 'utf-8');
|
|
132
|
+
const fm = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
133
|
+
if (fm) {
|
|
134
|
+
const m = fm[1].match(/^\s*current_phase\s*:\s*["']?([^"'\r\n]+)["']?\s*$/mi);
|
|
135
|
+
if (m && m[1].trim() && m[1].trim() !== 'null') return m[1].trim();
|
|
136
|
+
}
|
|
137
|
+
const body = content.match(/\*\*Current Phase:\*\*\s*(.+)/i);
|
|
138
|
+
if (body && body[1].trim()) return body[1].trim();
|
|
139
|
+
return null;
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
84
145
|
// Duration of a transcript slice from its first→last record timestamp. Returns
|
|
85
146
|
// null when either bound is absent/unparseable — never a fabricated 0.
|
|
86
147
|
function durationFromSpan(firstTs, lastTs) {
|
|
@@ -97,7 +158,7 @@ function durationFromSpan(firstTs, lastTs) {
|
|
|
97
158
|
// cache-read into the billions/trillions and stamps it onto every subagent record
|
|
98
159
|
// (field report 2026-06). Stored next to tokens.jsonl; best-effort, never blocks.
|
|
99
160
|
function cursorFilePath(cwd) {
|
|
100
|
-
return
|
|
161
|
+
return planningPath(cwd, METRICS_DIR, CURSOR_FILE);
|
|
101
162
|
}
|
|
102
163
|
function readCursor(cwd) {
|
|
103
164
|
try {
|
|
@@ -373,7 +434,10 @@ function buildCostRecord(data, cwd) {
|
|
|
373
434
|
// them (real SubagentStop payloads carry neither); tier is derived from the model.
|
|
374
435
|
const sessionMeta = readActiveSessionMeta(cwd);
|
|
375
436
|
const command = data.command || sessionMeta.command || null;
|
|
376
|
-
|
|
437
|
+
// The trace session is only present while the optimizer is running (off by
|
|
438
|
+
// default), so state.md is the fallback that makes phase attribution work in
|
|
439
|
+
// ordinary use instead of only under tracing.
|
|
440
|
+
const phase = data.phase || sessionMeta.phase || readCurrentPhase(cwd) || null;
|
|
377
441
|
|
|
378
442
|
const record = {
|
|
379
443
|
v: SCHEMA_V,
|
|
@@ -513,7 +577,7 @@ function appendRecord(cwd, record) {
|
|
|
513
577
|
// differs from the real row it follows, so the dedup never fired (M61).
|
|
514
578
|
if (record.__emptySlice) return false;
|
|
515
579
|
try {
|
|
516
|
-
const dir =
|
|
580
|
+
const dir = planningPath(cwd, METRICS_DIR);
|
|
517
581
|
fs.mkdirSync(dir, { recursive: true });
|
|
518
582
|
const file = path.join(dir, TOKENS_FILE);
|
|
519
583
|
// Idempotency guard: a re-fired SubagentStop must not double-log. Skip the
|
|
@@ -52,6 +52,37 @@
|
|
|
52
52
|
|
|
53
53
|
const fs = require('fs');
|
|
54
54
|
const path = require('path');
|
|
55
|
+
/**
|
|
56
|
+
* Which planning tree this hook acts on.
|
|
57
|
+
*
|
|
58
|
+
* Mirrors pan-wizard-core/bin/lib/planning-root.cjs, which hooks cannot require
|
|
59
|
+
* (they are standalone and run inside the host runtime). All PAN hooks carry an
|
|
60
|
+
* identical copy — if the CLI is pointed at a track while a hook still writes to
|
|
61
|
+
* `.planning/`, that tree's telemetry lands in the wrong place.
|
|
62
|
+
*
|
|
63
|
+
* Env only — a hook gets no argv. A value that escapes the project root is
|
|
64
|
+
* ignored rather than honoured: a bad value degrades to the default, never
|
|
65
|
+
* writes outside the project.
|
|
66
|
+
*/
|
|
67
|
+
function planningDirName() {
|
|
68
|
+
const raw = process.env.PAN_PLANNING_DIR || '';
|
|
69
|
+
if (raw.trim()) {
|
|
70
|
+
const rel = raw.trim().replace(/\\/g, '/');
|
|
71
|
+
const bad = rel.startsWith('/') || rel.startsWith('\\') || /^[A-Za-z]:/.test(rel)
|
|
72
|
+
|| rel.split('/').includes('..');
|
|
73
|
+
if (!bad) return rel;
|
|
74
|
+
}
|
|
75
|
+
const track = (process.env.PAN_TRACK || '').trim();
|
|
76
|
+
if (track && /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(track)) {
|
|
77
|
+
return `.planning/tracks/${track}`;
|
|
78
|
+
}
|
|
79
|
+
return '.planning';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Absolute path inside the active planning tree. */
|
|
83
|
+
function planningPath(cwd, ...segments) {
|
|
84
|
+
return path.join(cwd, ...planningDirName().split('/'), ...segments);
|
|
85
|
+
}
|
|
55
86
|
|
|
56
87
|
// Unticked phase line, exactly as templates/roadmap.md and pan-roadmapper.md
|
|
57
88
|
// emit it. Fixture doctrine: this shape is pinned against the shipped template
|
|
@@ -135,7 +166,7 @@ function main() {
|
|
|
135
166
|
if (!payload || typeof payload !== 'object') payload = {};
|
|
136
167
|
|
|
137
168
|
const projectDir = typeof payload.cwd === 'string' && payload.cwd ? payload.cwd : process.cwd();
|
|
138
|
-
const planningDir =
|
|
169
|
+
const planningDir = planningPath(projectDir);
|
|
139
170
|
|
|
140
171
|
let config = null;
|
|
141
172
|
try { config = JSON.parse(fs.readFileSync(path.join(planningDir, 'config.json'), 'utf8')); } catch { /* no project / bad config -> allow */ }
|
|
@@ -18,6 +18,37 @@
|
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const path = require('path');
|
|
20
20
|
const crypto = require('crypto');
|
|
21
|
+
/**
|
|
22
|
+
* Which planning tree this hook acts on.
|
|
23
|
+
*
|
|
24
|
+
* Mirrors pan-wizard-core/bin/lib/planning-root.cjs, which hooks cannot require
|
|
25
|
+
* (they are standalone and run inside the host runtime). All PAN hooks carry an
|
|
26
|
+
* identical copy — if the CLI is pointed at a track while a hook still writes to
|
|
27
|
+
* `.planning/`, that tree's telemetry lands in the wrong place.
|
|
28
|
+
*
|
|
29
|
+
* Env only — a hook gets no argv. A value that escapes the project root is
|
|
30
|
+
* ignored rather than honoured: a bad value degrades to the default, never
|
|
31
|
+
* writes outside the project.
|
|
32
|
+
*/
|
|
33
|
+
function planningDirName() {
|
|
34
|
+
const raw = process.env.PAN_PLANNING_DIR || '';
|
|
35
|
+
if (raw.trim()) {
|
|
36
|
+
const rel = raw.trim().replace(/\\/g, '/');
|
|
37
|
+
const bad = rel.startsWith('/') || rel.startsWith('\\') || /^[A-Za-z]:/.test(rel)
|
|
38
|
+
|| rel.split('/').includes('..');
|
|
39
|
+
if (!bad) return rel;
|
|
40
|
+
}
|
|
41
|
+
const track = (process.env.PAN_TRACK || '').trim();
|
|
42
|
+
if (track && /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(track)) {
|
|
43
|
+
return `.planning/tracks/${track}`;
|
|
44
|
+
}
|
|
45
|
+
return '.planning';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Absolute path inside the active planning tree. */
|
|
49
|
+
function planningPath(cwd, ...segments) {
|
|
50
|
+
return path.join(cwd, ...planningDirName().split('/'), ...segments);
|
|
51
|
+
}
|
|
21
52
|
|
|
22
53
|
// Runtime config dirs a local PAN install lands in (mirrors installer getDirName).
|
|
23
54
|
const PAN_RUNTIME_DIRS = ['.claude', '.codex', '.gemini', '.opencode', '.github'];
|
|
@@ -32,7 +63,7 @@ const PAN_RUNTIME_DIRS = ['.claude', '.codex', '.gemini', '.opencode', '.github'
|
|
|
32
63
|
function isPanProject(cwd) {
|
|
33
64
|
try {
|
|
34
65
|
if (!cwd) return false;
|
|
35
|
-
if (fs.existsSync(
|
|
66
|
+
if (fs.existsSync(planningPath(cwd))) return true;
|
|
36
67
|
for (const d of PAN_RUNTIME_DIRS) {
|
|
37
68
|
if (fs.existsSync(path.join(cwd, d, 'pan-file-manifest.json'))) return true;
|
|
38
69
|
if (fs.existsSync(path.join(cwd, d, 'pan-wizard-core'))) return true;
|
|
@@ -43,7 +74,9 @@ function isPanProject(cwd) {
|
|
|
43
74
|
}
|
|
44
75
|
}
|
|
45
76
|
|
|
46
|
-
|
|
77
|
+
// Resolved per call via planningDirName() so a track-scoped run traces into
|
|
78
|
+
// its own tree; kept as a name for the code paths that only need the label.
|
|
79
|
+
const PLANNING_DIR = planningDirName();
|
|
47
80
|
const OPTIMIZE_DIR = 'optimization';
|
|
48
81
|
const TRACES_DIR = 'traces';
|
|
49
82
|
const CURRENT_SESSION_FILE = 'current-session';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pan-wizard",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.27.0",
|
|
4
4
|
"description": "Command a bot army for your codebase: a reasoning-tier 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"
|
|
@@ -30,7 +30,6 @@
|
|
|
30
30
|
const fs = require('fs');
|
|
31
31
|
const path = require('path');
|
|
32
32
|
const { output, error, safeReadFile, toPosix, findPhaseInternal } = require('./core.cjs');
|
|
33
|
-
const { PLANNING_DIR } = require('./constants.cjs');
|
|
34
33
|
const { planningPath } = require('./utils.cjs');
|
|
35
34
|
|
|
36
35
|
const BRIDGE_DIR = 'bridge';
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
const fs = require('fs');
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const { output, error } = require('./core.cjs');
|
|
16
|
-
const {
|
|
16
|
+
const { VERIFY_RESERVE_FRACTION } = require('./constants.cjs');
|
|
17
|
+
const { planningPath } = require('./utils.cjs');
|
|
17
18
|
|
|
18
19
|
const ORCH_DIR = 'orchestration';
|
|
19
20
|
const SCHEDULE_FILE = 'schedule.json';
|
|
@@ -21,7 +22,7 @@ const HISTORY_CAP = 50;
|
|
|
21
22
|
const DAY_MS = 86400000;
|
|
22
23
|
|
|
23
24
|
function schedulePath(cwd) {
|
|
24
|
-
return
|
|
25
|
+
return planningPath(cwd, ORCH_DIR, SCHEDULE_FILE);
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
/**
|
|
@@ -9,8 +9,8 @@ const fs = require('fs');
|
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const { getArchivedPhaseDirs, output, error } = require('./core.cjs');
|
|
11
11
|
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
12
|
-
const {
|
|
13
|
-
const { phasesPath } = require('./utils.cjs');
|
|
12
|
+
const { PATTERNS_FILE, SESSION_HISTORY_FILE, LEARNINGS_FILE, isSummaryFile } = require('./constants.cjs');
|
|
13
|
+
const { phasesPath, planningPath } = require('./utils.cjs');
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Scan all phase directories (archived + current) and read summary frontmatter.
|
|
@@ -78,7 +78,7 @@ function collectPhaseSummaries(cwd) {
|
|
|
78
78
|
* @returns {Array<{id: string, title: string, wrong: string, right: string, context: string|null, date: string|null}>}
|
|
79
79
|
*/
|
|
80
80
|
function readErrorPatterns(cwd) {
|
|
81
|
-
const filePath =
|
|
81
|
+
const filePath = planningPath(cwd, PATTERNS_FILE);
|
|
82
82
|
let content;
|
|
83
83
|
try {
|
|
84
84
|
content = fs.readFileSync(filePath, 'utf-8');
|
|
@@ -141,7 +141,7 @@ function appendErrorPattern(cwd, pattern) {
|
|
|
141
141
|
return { error: "Pattern requires 'wrong' and 'right' fields" };
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
const filePath =
|
|
144
|
+
const filePath = planningPath(cwd, PATTERNS_FILE);
|
|
145
145
|
const existing = readErrorPatterns(cwd);
|
|
146
146
|
|
|
147
147
|
// Determine next ID
|
|
@@ -198,7 +198,7 @@ function appendSessionSummary(cwd, summary) {
|
|
|
198
198
|
return { error: "Summary requires 'phase' field" };
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
const filePath =
|
|
201
|
+
const filePath = planningPath(cwd, SESSION_HISTORY_FILE);
|
|
202
202
|
const date = summary.date || new Date().toISOString().split('T')[0];
|
|
203
203
|
|
|
204
204
|
const entry = [
|
|
@@ -308,7 +308,7 @@ function formatLearningEntry(learning) {
|
|
|
308
308
|
* @returns {void}
|
|
309
309
|
*/
|
|
310
310
|
function cmdLearningsExtract(cwd, raw) {
|
|
311
|
-
const learningsPath =
|
|
311
|
+
const learningsPath = planningPath(cwd, LEARNINGS_FILE);
|
|
312
312
|
const newLearnings = [];
|
|
313
313
|
const today = new Date().toISOString().split('T')[0];
|
|
314
314
|
|
|
@@ -445,7 +445,7 @@ function cmdLearningsExtract(cwd, raw) {
|
|
|
445
445
|
* @returns {void}
|
|
446
446
|
*/
|
|
447
447
|
function cmdLearningsList(cwd, raw) {
|
|
448
|
-
const learningsPath =
|
|
448
|
+
const learningsPath = planningPath(cwd, LEARNINGS_FILE);
|
|
449
449
|
|
|
450
450
|
let content;
|
|
451
451
|
try {
|
|
@@ -484,7 +484,7 @@ function cmdLearningsList(cwd, raw) {
|
|
|
484
484
|
* @returns {void}
|
|
485
485
|
*/
|
|
486
486
|
function cmdLearningsPrune(cwd, opts, raw) {
|
|
487
|
-
const learningsPath =
|
|
487
|
+
const learningsPath = planningPath(cwd, LEARNINGS_FILE);
|
|
488
488
|
|
|
489
489
|
if (!opts || (opts.days == null && opts.id == null)) {
|
|
490
490
|
error('Prune requires --days N or --id LEARN-NNN');
|
|
@@ -5,10 +5,11 @@ const fs = require('fs');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { safeReadFile, loadConfig, isGitIgnored, isGitRepo, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, resolveModelInternal, resolveEffortInternal, detectProvider, resolveTierToModel, estimateCostMultiplier, MODEL_PROFILES, output, error, findPhaseInternal, scanPendingTodos, toPosix } = require('./core.cjs');
|
|
7
7
|
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
8
|
-
const {
|
|
9
|
-
const { planningPath, phasesPath, filterPlanFiles, filterSummaryFiles } = require('./utils.cjs');
|
|
8
|
+
const { PHASES_DIR, MILESTONES_DIR, QUICK_DIR, STATE_FILE, ROADMAP_FILE, PROJECT_FILE, PATTERNS_FILE, SESSION_HISTORY_FILE, LEARNINGS_FILE, CONTEXT_SUFFIX, UAT_SUFFIX, VERIFICATION_SUFFIX, isPlanFile, isSummaryFile, ARCHIVE_DIR_RE, PHASE_DIR_RE, CONTEXT_WINDOW, WARNING_THRESHOLD, CRITICAL_THRESHOLD, VALID_COMMIT_TYPES, DEFAULT_SENSITIVE_PATTERNS } = require('./constants.cjs');
|
|
9
|
+
const { planningPath, phasesPath, filterPlanFiles, filterSummaryFiles, planningRel } = require('./utils.cjs');
|
|
10
10
|
const { estimateTokens } = require('./context-budget.cjs');
|
|
11
11
|
const { collectPhaseSummaries, readErrorPatterns, appendErrorPattern, appendSessionSummary, parseLearnings, formatLearningEntry, cmdLearningsExtract, cmdLearningsList, cmdLearningsPrune } = require('./commands-learnings.cjs');
|
|
12
|
+
const { planningRootRel } = require('./planning-root.cjs');
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Generate a URL-safe slug from text by lowercasing and replacing non-alphanumeric chars.
|
|
@@ -381,7 +382,7 @@ function cmdCommit(cwd, message, files, raw, amend, opts) {
|
|
|
381
382
|
output({ committed: false, hash: null, reason: 'skipped_commit_docs_false' }, raw, 'skipped');
|
|
382
383
|
return;
|
|
383
384
|
}
|
|
384
|
-
if (isGitIgnored(cwd,
|
|
385
|
+
if (isGitIgnored(cwd, planningRootRel())) {
|
|
385
386
|
// No error key, exit 0: .planning/ is gitignored - also the user's choice.
|
|
386
387
|
output({ committed: false, hash: null, reason: 'skipped_gitignored' }, raw, 'skipped');
|
|
387
388
|
return;
|
|
@@ -395,7 +396,7 @@ function cmdCommit(cwd, message, files, raw, amend, opts) {
|
|
|
395
396
|
const preStaged = new Set(stagedFiles(cwd));
|
|
396
397
|
|
|
397
398
|
// Stage files
|
|
398
|
-
const filesToStage = files && files.length > 0 ? files : [
|
|
399
|
+
const filesToStage = files && files.length > 0 ? files : [planningRel() + '/'];
|
|
399
400
|
for (const file of filesToStage) execGit(cwd, ['add', file]);
|
|
400
401
|
|
|
401
402
|
// Safety checks
|
|
@@ -614,7 +615,7 @@ function renderProgressBar(percent, width) {
|
|
|
614
615
|
*/
|
|
615
616
|
function cmdProgressRender(cwd, format, raw) {
|
|
616
617
|
const phasesDir = phasesPath(cwd);
|
|
617
|
-
const roadmapPath =
|
|
618
|
+
const roadmapPath = planningPath(cwd, ROADMAP_FILE);
|
|
618
619
|
const milestone = getMilestoneInfo(cwd);
|
|
619
620
|
|
|
620
621
|
const phases = [];
|
|
@@ -693,9 +694,9 @@ function cmdProgressRender(cwd, format, raw) {
|
|
|
693
694
|
* Compute and output a composite health score from progress, context budget, and staleness.
|
|
694
695
|
*/
|
|
695
696
|
function renderHealthReport(cwd, { phasesDir, phases, totalPlans, totalSummaries, percent }, raw) {
|
|
696
|
-
const stateContent = safeReadFile(
|
|
697
|
-
const roadmapContent = safeReadFile(
|
|
698
|
-
const projectContent = safeReadFile(
|
|
697
|
+
const stateContent = safeReadFile(planningPath(cwd, STATE_FILE));
|
|
698
|
+
const roadmapContent = safeReadFile(planningPath(cwd, ROADMAP_FILE));
|
|
699
|
+
const projectContent = safeReadFile(planningPath(cwd, PROJECT_FILE));
|
|
699
700
|
|
|
700
701
|
const stateTokens = estimateTokens(stateContent);
|
|
701
702
|
const roadmapTokens = estimateTokens(roadmapContent);
|
|
@@ -742,7 +743,7 @@ function renderHealthReport(cwd, { phasesDir, phases, totalPlans, totalSummaries
|
|
|
742
743
|
// Read session history count
|
|
743
744
|
let sessionCount = 0;
|
|
744
745
|
try {
|
|
745
|
-
const sessionContent = fs.readFileSync(
|
|
746
|
+
const sessionContent = fs.readFileSync(planningPath(cwd, SESSION_HISTORY_FILE), 'utf-8');
|
|
746
747
|
sessionCount = (sessionContent.match(/^### Session — /gm) || []).length;
|
|
747
748
|
} catch { /* file doesn't exist */ }
|
|
748
749
|
|
|
@@ -782,8 +783,8 @@ function cmdTodoComplete(cwd, filename, raw) {
|
|
|
782
783
|
error('filename required for todo complete');
|
|
783
784
|
}
|
|
784
785
|
|
|
785
|
-
const pendingDir =
|
|
786
|
-
const completedDir =
|
|
786
|
+
const pendingDir = planningPath(cwd, 'todos', 'pending');
|
|
787
|
+
const completedDir = planningPath(cwd, 'todos', 'completed');
|
|
787
788
|
const sourcePath = path.join(pendingDir, filename);
|
|
788
789
|
|
|
789
790
|
let content;
|
|
@@ -870,14 +871,14 @@ function cmdScaffold(cwd, type, options, raw) {
|
|
|
870
871
|
}
|
|
871
872
|
const slug = generateSlugInternal(name);
|
|
872
873
|
const dirName = `${padded}-${slug}`;
|
|
873
|
-
const phasesParent =
|
|
874
|
+
const phasesParent = planningPath(cwd, PHASES_DIR);
|
|
874
875
|
try {
|
|
875
876
|
fs.mkdirSync(phasesParent, { recursive: true });
|
|
876
877
|
fs.mkdirSync(path.join(phasesParent, dirName), { recursive: true });
|
|
877
878
|
} catch (e) {
|
|
878
879
|
error(`Failed to create phase directory: ${e.message}`);
|
|
879
880
|
}
|
|
880
|
-
output({ created: true, directory:
|
|
881
|
+
output({ created: true, directory: planningRel(PHASES_DIR, dirName) }, raw, planningRel(PHASES_DIR, dirName));
|
|
881
882
|
return;
|
|
882
883
|
}
|
|
883
884
|
default:
|
|
@@ -972,7 +973,7 @@ function cmdBatchCommit(cwd, items, raw) {
|
|
|
972
973
|
}
|
|
973
974
|
|
|
974
975
|
// Stage .planning/ only
|
|
975
|
-
execGit(cwd, ['add',
|
|
976
|
+
execGit(cwd, ['add', planningRel() + '/']);
|
|
976
977
|
|
|
977
978
|
// Check if there's anything to commit
|
|
978
979
|
const statusResult = execGit(cwd, ['diff', '--cached', '--name-only']);
|
|
@@ -7,11 +7,11 @@ const os = require('os');
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const { output, error, safeReadFile, toPosix, findPhaseInternal } = require('./core.cjs');
|
|
9
9
|
const {
|
|
10
|
-
|
|
10
|
+
CONFIG_FILE, PROJECT_FILE, STANDARDS_FILE,
|
|
11
11
|
STANDARDS_CATALOG, STANDARDS_CATEGORIES, STANDARDS_RECOMMENDATIONS,
|
|
12
12
|
PHASE_KEYWORDS_TO_STANDARDS, STANDARDS_EXTERNAL_TOOLS,
|
|
13
13
|
} = require('./constants.cjs');
|
|
14
|
-
const { readJsonFile, planningPath, fileAccessible, hasBraveSearchKey } = require('./utils.cjs');
|
|
14
|
+
const { readJsonFile, planningPath, fileAccessible, hasBraveSearchKey, planningRel } = require('./utils.cjs');
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Count checked checklist items in a standards section.
|
|
@@ -119,7 +119,7 @@ function cmdConfigEnsureSection(cwd, raw) {
|
|
|
119
119
|
|
|
120
120
|
try {
|
|
121
121
|
fs.writeFileSync(configPath, JSON.stringify(defaults, null, 2), 'utf-8');
|
|
122
|
-
output({ created: true, path:
|
|
122
|
+
output({ created: true, path: planningRel(CONFIG_FILE) }, raw, 'created');
|
|
123
123
|
} catch (err) {
|
|
124
124
|
error('Failed to create config.json: ' + err.message);
|
|
125
125
|
}
|
|
@@ -354,7 +354,7 @@ function cmdStandardsSelect(cwd, standardId, raw) {
|
|
|
354
354
|
output({
|
|
355
355
|
added: standardId,
|
|
356
356
|
project_standards: currentIds,
|
|
357
|
-
standards_file: toPosix(
|
|
357
|
+
standards_file: toPosix(planningRel(STANDARDS_FILE)),
|
|
358
358
|
}, raw, 'Added ' + STANDARDS_CATALOG[standardId].name);
|
|
359
359
|
}
|
|
360
360
|
|
|
@@ -398,7 +398,7 @@ function cmdStandardsRemove(cwd, standardId, raw) {
|
|
|
398
398
|
output({
|
|
399
399
|
removed: standardId,
|
|
400
400
|
project_standards: newIds,
|
|
401
|
-
standards_file: toPosix(
|
|
401
|
+
standards_file: toPosix(planningRel(STANDARDS_FILE)),
|
|
402
402
|
}, raw, 'Removed ' + standardId);
|
|
403
403
|
}
|
|
404
404
|
|
|
@@ -572,11 +572,31 @@ const MEMORY_LOAD_WARN_TOKENS = 4000; // memory-budget telemetry: warn thres
|
|
|
572
572
|
const MEMORY_LOAD_CRIT_TOKENS = 8000; // memory-budget telemetry: critical threshold (absolute tokens)
|
|
573
573
|
const MEMORY_LOAD_MAX_FRACTION = 0.15; // memory-budget telemetry: max fraction of median agent input
|
|
574
574
|
|
|
575
|
+
// Cached prompt context (ADR-0044). The files in CACHEABLE_CONTEXT_FILES are
|
|
576
|
+
// re-read into EVERY agent call, so their combined size is the single largest
|
|
577
|
+
// recurring cost in a PAN project — cache reads dominate token traffic by roughly
|
|
578
|
+
// two orders of magnitude over generation. These thresholds mirror the
|
|
579
|
+
// MEMORY_LOAD_* pattern above: measured, classified, and surfaced.
|
|
580
|
+
const CACHE_BLOCK_WARN_TOKENS = 15000; // cached context block: warn threshold (absolute tokens)
|
|
581
|
+
const CACHE_BLOCK_CRIT_TOKENS = 25000; // cached context block: critical threshold
|
|
582
|
+
const CACHE_FILE_WARN_TOKENS = 6000; // any SINGLE cacheable file past this is the one to fix
|
|
583
|
+
|
|
584
|
+
// state.md compaction (ADR-0044). state.md is the largest cacheable file in
|
|
585
|
+
// practice because its section writers only ever append; closed history keeps
|
|
586
|
+
// being re-read months after it stopped being actionable.
|
|
587
|
+
const STATE_HISTORY_FILE = 'state-history.md';
|
|
588
|
+
const STATE_COMPACT_KEEP_DAYS = 30; // dated sections newer than this stay in state.md
|
|
589
|
+
|
|
575
590
|
// Hygiene — project cleanup + version alignment (docs/FIELD-HARVEST-2026-07.md follow-ups).
|
|
576
591
|
const HYGIENE_TRACE_RETENTION_DAYS = 30; // trace sessions older than this are prunable…
|
|
577
592
|
const HYGIENE_TRACE_KEEP_MIN = 5; // …but always keep this many newest sessions
|
|
593
|
+
const HYGIENE_REPORT_KEEP_MIN = 5; // …same shape for optimization/reports/
|
|
578
594
|
const HYGIENE_LEDGER_SUSPECT_RATIO = 0.5; // ledger "poisoned" when suspect fraction ≥ this…
|
|
579
595
|
const HYGIENE_LEDGER_MIN_RECORDS = 20; // …and it has at least this many records
|
|
596
|
+
// …OR when the suspect rows, however few, carry this share of the token MASS.
|
|
597
|
+
// A count-only gate passes a ledger whose 24% bad rows hold 90% of the tokens —
|
|
598
|
+
// statistically fine, arithmetically useless. Mass is what aggregates read.
|
|
599
|
+
const HYGIENE_LEDGER_SUSPECT_MASS_RATIO = 0.5;
|
|
580
600
|
const HYGIENE_TMP_AGE_MS = 60 * 60 * 1000; // .tmp orphans older than 1h are deletable
|
|
581
601
|
|
|
582
602
|
// Skill-Aligned Decomposition pass (ADR-0038): planner draft ↔ skill-surface alignment.
|
|
@@ -772,8 +792,15 @@ module.exports = {
|
|
|
772
792
|
MEMORY_LOAD_CRIT_TOKENS,
|
|
773
793
|
MEMORY_LOAD_MAX_FRACTION,
|
|
774
794
|
// Hygiene
|
|
795
|
+
CACHE_BLOCK_WARN_TOKENS,
|
|
796
|
+
CACHE_BLOCK_CRIT_TOKENS,
|
|
797
|
+
CACHE_FILE_WARN_TOKENS,
|
|
798
|
+
STATE_HISTORY_FILE,
|
|
799
|
+
STATE_COMPACT_KEEP_DAYS,
|
|
775
800
|
HYGIENE_TRACE_RETENTION_DAYS,
|
|
776
801
|
HYGIENE_TRACE_KEEP_MIN,
|
|
802
|
+
HYGIENE_REPORT_KEEP_MIN,
|
|
803
|
+
HYGIENE_LEDGER_SUSPECT_MASS_RATIO,
|
|
777
804
|
HYGIENE_LEDGER_SUSPECT_RATIO,
|
|
778
805
|
HYGIENE_LEDGER_MIN_RECORDS,
|
|
779
806
|
HYGIENE_TMP_AGE_MS,
|