pan-wizard 3.26.0 → 3.28.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 +48 -48
- package/agents/pan-previewer.md +1 -1
- package/bin/install-lib.cjs +580 -18
- package/bin/install.js +25 -44
- package/commands/pan/army.md +1 -1
- package/commands/pan/hygiene.md +14 -8
- package/commands/pan/milestone-audit.md +10 -4
- package/commands/pan/preview.md +2 -2
- 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 +5 -2
- 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 +49 -0
- package/pan-wizard-core/bin/lib/context-budget.cjs +98 -0
- package/pan-wizard-core/bin/lib/core.cjs +190 -26
- package/pan-wizard-core/bin/lib/cost.cjs +113 -11
- 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/foreign-planning.cjs +56 -0
- package/pan-wizard-core/bin/lib/hud.cjs +1 -1
- package/pan-wizard-core/bin/lib/hygiene.cjs +428 -37
- package/pan-wizard-core/bin/lib/init.cjs +98 -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 +26 -5
- package/pan-wizard-core/bin/lib/whatif.cjs +0 -1
- package/pan-wizard-core/bin/pan-tools.cjs +58 -4
- package/pan-wizard-core/mcp/server.cjs +92 -8
- package/pan-wizard-core/mcp/tool-registry.cjs +50 -3
- package/pan-wizard-core/references/model-profiles.md +2 -2
- package/pan-wizard-core/workflows/health.md +1 -0
- package/pan-wizard-core/workflows/milestone-audit.md +35 -6
- package/pan-zcode/README.md +1 -1
- package/scripts/build-agent-plugin.js +220 -0
- package/scripts/build-plugin.js +48 -3
- package/scripts/generate-skills-docs.py +1 -1
- package/scripts/release-check.js +58 -12
package/bin/install.js
CHANGED
|
@@ -46,7 +46,7 @@ const pkg = require('../package.json');
|
|
|
46
46
|
// phrased by capability, not by name; these are the "switch to this" examples
|
|
47
47
|
// that keep the advice actionable. Nothing in PAN gates on these values.
|
|
48
48
|
const RECOMMENDED_MODELS = {
|
|
49
|
-
flagship: 'claude-fable-5',
|
|
49
|
+
flagship: 'claude-fable-5-1',
|
|
50
50
|
reasoningTier: 'claude-opus-5 / claude-opus-4-8',
|
|
51
51
|
};
|
|
52
52
|
|
|
@@ -565,21 +565,15 @@ function copyCommandsAsUnifiedSkills(srcDir, skillsDir, prefix, pathPrefix, core
|
|
|
565
565
|
fs.mkdirSync(skillDir, { recursive: true });
|
|
566
566
|
|
|
567
567
|
let content = fs.readFileSync(srcPath, 'utf8');
|
|
568
|
-
//
|
|
569
|
-
//
|
|
570
|
-
//
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
578
|
-
content = content.replace(/\.\/\.claude\//g, `./${getDirName(runtime)}/`);
|
|
579
|
-
// Not every runtime puts a `pan-tools` bin on PATH — invoke via node.
|
|
580
|
-
const panToolsPath = `${corePrefix}pan-wizard-core/bin/pan-tools.cjs`;
|
|
581
|
-
content = content.replace(/\bpan-tools\b(?=\s+[a-z])/g, `node ${panToolsPath}`);
|
|
582
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
568
|
+
// The path rewrite lives in install-lib (rewriteUnifiedSkillCommandContent)
|
|
569
|
+
// because the Agent Plugins bundle builder runs the SAME function — one
|
|
570
|
+
// converter, several call sites, never a second copy (ADR-0028, ADR-0045).
|
|
571
|
+
content = lib.rewriteUnifiedSkillCommandContent(content, {
|
|
572
|
+
corePrefix,
|
|
573
|
+
pathPrefix,
|
|
574
|
+
projectDirPrefix: `./${getDirName(runtime)}/`,
|
|
575
|
+
attribution: getCommitAttribution(runtime),
|
|
576
|
+
});
|
|
583
577
|
content = convertClaudeCommandToUnifiedSkill(content, skillName);
|
|
584
578
|
|
|
585
579
|
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
|
|
@@ -670,17 +664,14 @@ function copySharedCore(srcDir, destDir, corePrefix, runtimePathPrefix, runtime)
|
|
|
670
664
|
recurse(srcPath, destPath);
|
|
671
665
|
} else if (entry.name.endsWith('.md')) {
|
|
672
666
|
try {
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
content =
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
|
682
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
683
|
-
content = convertSlashCommandsToCopilotSkillMentions(content);
|
|
667
|
+
// Shared with the Agent Plugins bundle builder (install-lib) — see
|
|
668
|
+
// rewriteSharedCoreMarkdown for the rewrite order and rationale.
|
|
669
|
+
const content = lib.rewriteSharedCoreMarkdown(fs.readFileSync(srcPath, 'utf8'), {
|
|
670
|
+
corePrefix,
|
|
671
|
+
pathPrefix: runtimePathPrefix,
|
|
672
|
+
projectDirPrefix: `./${dirName}/`,
|
|
673
|
+
attribution: getCommitAttribution(runtime),
|
|
674
|
+
});
|
|
684
675
|
fs.writeFileSync(destPath, content);
|
|
685
676
|
} catch (err) {
|
|
686
677
|
pushInstallWarning('copySharedCore(md)', destPath, err);
|
|
@@ -728,21 +719,13 @@ function stripInternalFromLearningsIndex(indexPath) {
|
|
|
728
719
|
if (err.code !== 'ENOENT') pushInstallWarning('stripInternalLearnings', 'learnings/index.json', err);
|
|
729
720
|
return;
|
|
730
721
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
const
|
|
734
|
-
if (
|
|
735
|
-
|
|
736
|
-
parsed.topics = kept;
|
|
737
|
-
if (parsed.totals && typeof parsed.totals === 'object') {
|
|
738
|
-
parsed.totals.topics = kept.length;
|
|
739
|
-
parsed.totals.patterns = kept.reduce((n, t) => n + (Array.isArray(t.patterns) ? t.patterns.length : 0), 0);
|
|
740
|
-
parsed.totals.size_bytes = kept.reduce((n, t) => n + (t.size_bytes || 0), 0);
|
|
741
|
-
parsed.totals.size_tokens_est = kept.reduce((n, t) => n + (t.size_tokens_est || 0), 0);
|
|
742
|
-
}
|
|
722
|
+
// The transform is pure and shared with the bundle builders (install-lib):
|
|
723
|
+
// null means "not an index" or "nothing internal to drop" — both no-ops here.
|
|
724
|
+
const stripped = lib.stripInternalLearningsTopics(parsed);
|
|
725
|
+
if (!stripped) return;
|
|
743
726
|
|
|
744
727
|
try {
|
|
745
|
-
fs.writeFileSync(indexPath, JSON.stringify(
|
|
728
|
+
fs.writeFileSync(indexPath, JSON.stringify(stripped, null, 2) + '\n');
|
|
746
729
|
} catch (err) {
|
|
747
730
|
pushInstallWarning('stripInternalLearnings', 'learnings/index.json', err);
|
|
748
731
|
}
|
|
@@ -2230,10 +2213,8 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
2230
2213
|
fs.mkdirSync(agentsRefDir, { recursive: true });
|
|
2231
2214
|
const agentsSrc = path.join(src, 'agents');
|
|
2232
2215
|
for (const f of fs.readdirSync(agentsSrc).filter(n => n.endsWith('.md'))) {
|
|
2233
|
-
|
|
2234
|
-
content =
|
|
2235
|
-
content = content.replace(/\.\/\.claude\/pan-wizard-core\//g, `${corePrefix}pan-wizard-core/`);
|
|
2236
|
-
content = convertSlashCommandsToCopilotSkillMentions(content);
|
|
2216
|
+
// Shared with the Agent Plugins bundle builder (install-lib).
|
|
2217
|
+
const content = lib.rewriteAgentReferenceCopy(fs.readFileSync(path.join(agentsSrc, f), 'utf8'), corePrefix);
|
|
2237
2218
|
fs.writeFileSync(path.join(agentsRefDir, f), content);
|
|
2238
2219
|
}
|
|
2239
2220
|
} catch (e) {
|
package/commands/pan/army.md
CHANGED
|
@@ -92,7 +92,7 @@ Every cap the conductor enforces applies to the campaign, scaled up:
|
|
|
92
92
|
| `--push` | off | Push approved merges to origin (still human-gated). |
|
|
93
93
|
| `--clean-seal` | off | One clean build + full verification after the last item (commands from config). |
|
|
94
94
|
| `--schedule` | off | Arm a self-resuming campaign at this cadence (`hourly`/`daily`/`weekly`/`Nh`/`Nd`) instead of running once — writes the schedule descriptor (ADR-0034). Pair with `--daily-budget`. |
|
|
95
|
-
| `--daily-budget` | 300 | Per-day point budget for a scheduled campaign. Advisory by default (an indicator of the day's spend); it only pauses the day's run when `
|
|
95
|
+
| `--daily-budget` | 300 | Per-day point budget for a scheduled campaign. Advisory by default (an indicator of the day's spend); it only pauses the day's run when `enforce_budget: true` is set by hand in the schedule descriptor (`schedule.json`); no flag or config key sets it. |
|
|
96
96
|
| `--dry-run` | off | Plan + squad delegation preview only; STOP. |
|
|
97
97
|
| `--continue` / `--stop` / `--status` | — | Resume / halt / report from `.planning/orchestration/` + focus-auto state. |
|
|
98
98
|
|
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>
|
package/commands/pan/preview.md
CHANGED
|
@@ -34,9 +34,9 @@ Consolidates Spec B v1's architect + simulate + predict-milestone into one entry
|
|
|
34
34
|
**What it does:**
|
|
35
35
|
1. `pan-tools preview phase <N>` returns `{files_mentioned, test_files_mentioned, risk_signals, risk_score, plans[], status}`.
|
|
36
36
|
2. Spawn `pan-previewer` with the payload as `<preview_input>`.
|
|
37
|
-
3. Agent writes `.planning/phases/<
|
|
37
|
+
3. Agent writes `.planning/phases/<NN-slug>/preview.md` with files touched / tests at risk / migration steps / risk assessment / bottom line.
|
|
38
38
|
|
|
39
|
-
**Output:** `.planning/phases/<
|
|
39
|
+
**Output:** `.planning/phases/<NN-slug>/preview.md`
|
|
40
40
|
|
|
41
41
|
### `phases` — Cross-phase dependency graph
|
|
42
42
|
|
|
@@ -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.28.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"
|
|
@@ -70,6 +70,9 @@
|
|
|
70
70
|
"test:e2e": "node scripts/run-tests.cjs tests/scenarios",
|
|
71
71
|
"test:vscode": "npx playwright test --config tests/e2e/playwright.config.mjs",
|
|
72
72
|
"test:watch": "node scripts/run-tests.cjs --watch tests tests/scenarios",
|
|
73
|
-
"build:plugin": "node scripts/build-plugin.js"
|
|
73
|
+
"build:plugin": "node scripts/build-plugin.js",
|
|
74
|
+
"build:agent-plugin": "node scripts/build-agent-plugin.js",
|
|
75
|
+
"harness": "node harness/src/run.cjs --tier 0",
|
|
76
|
+
"harness:model": "node harness/src/run.cjs --tier 2"
|
|
74
77
|
}
|
|
75
78
|
}
|
|
@@ -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');
|