pan-wizard 3.32.0 → 3.34.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 +23 -20
- package/agents/pan-conductor.md +18 -17
- package/agents/pan-counterfactual.md +2 -2
- package/agents/pan-debugger.md +27 -8
- package/agents/pan-design-checker.md +1 -1
- package/agents/pan-designer.md +2 -2
- package/agents/pan-distiller.md +3 -3
- package/agents/pan-document_code.md +16 -16
- package/agents/pan-executor.md +15 -11
- package/agents/pan-experiment-runner.md +15 -17
- package/agents/pan-hardener.md +15 -15
- package/agents/pan-integration-checker.md +2 -2
- package/agents/pan-knowledge.md +2 -2
- package/agents/pan-meta-reviewer.md +5 -5
- package/agents/pan-optimizer.md +40 -44
- package/agents/pan-phase-researcher.md +10 -10
- package/agents/pan-plan-checker.md +36 -30
- package/agents/pan-planner.md +77 -53
- package/agents/pan-previewer.md +1 -1
- package/agents/pan-project-researcher.md +21 -78
- package/agents/pan-release.md +1 -1
- package/agents/pan-research-synthesizer.md +2 -2
- package/agents/pan-reviewer.md +24 -2
- package/agents/pan-roadmapper.md +20 -62
- package/agents/pan-verifier.md +53 -30
- package/bin/install-lib.cjs +228 -38
- package/bin/install.js +86 -15
- package/commands/pan/army.md +16 -12
- package/commands/pan/assumptions.md +2 -2
- package/commands/pan/audit-deployment.md +11 -12
- package/commands/pan/cost.md +8 -4
- package/commands/pan/debug.md +10 -7
- package/commands/pan/design-phase.md +3 -3
- package/commands/pan/discuss-phase.md +2 -0
- package/commands/pan/exec-phase.md +21 -37
- package/commands/pan/experiment.md +11 -10
- package/commands/pan/focus-auto.md +33 -27
- package/commands/pan/focus-design.md +9 -6
- package/commands/pan/focus-doc-audit.md +4 -3
- package/commands/pan/focus-drift-walking.md +4 -3
- package/commands/pan/focus-exec.md +7 -7
- package/commands/pan/focus-plan.md +12 -11
- package/commands/pan/focus-scan.md +7 -10
- package/commands/pan/focus-sync.md +3 -2
- package/commands/pan/git.md +8 -8
- package/commands/pan/health.md +2 -2
- package/commands/pan/hud.md +3 -2
- package/commands/pan/hygiene.md +14 -7
- package/commands/pan/knowledge.md +9 -9
- package/commands/pan/learn.md +10 -7
- package/commands/pan/links.md +4 -1
- package/commands/pan/map-codebase.md +5 -5
- package/commands/pan/mcp-bridge.md +18 -18
- package/commands/pan/milestone-done.md +7 -5
- package/commands/pan/milestone-new.md +1 -1
- package/commands/pan/new-project.md +10 -10
- package/commands/pan/optimize.md +16 -14
- package/commands/pan/patches.md +2 -6
- package/commands/pan/pause.md +1 -1
- package/commands/pan/phase-tests.md +4 -3
- package/commands/pan/plan-phase.md +9 -19
- package/commands/pan/preview.md +1 -1
- package/commands/pan/profile.md +4 -4
- package/commands/pan/quick.md +1 -1
- package/commands/pan/remove-phase.md +1 -1
- package/commands/pan/report.md +2 -2
- package/commands/pan/research-phase.md +14 -15
- package/commands/pan/resume.md +9 -11
- package/commands/pan/retro.md +5 -3
- package/commands/pan/review-deep.md +10 -10
- package/commands/pan/settings.md +1 -1
- package/commands/pan/todo-add.md +2 -2
- package/commands/pan/todo-check.md +2 -2
- package/commands/pan/verify-phase.md +0 -11
- package/commands/pan/what-if.md +2 -2
- package/hooks/dist/pan-context-monitor.js +343 -44
- package/hooks/dist/pan-cost-logger.js +30 -0
- package/hooks/dist/pan-state-reinject.js +140 -15
- package/hooks/dist/pan-statusline.js +68 -22
- package/hooks/dist/pan-stop-guard.js +7 -2
- package/package.json +1 -1
- package/pan-wizard-core/bin/lib/agents-md.cjs +48 -7
- package/pan-wizard-core/bin/lib/bridge.cjs +6 -5
- package/pan-wizard-core/bin/lib/campaign.cjs +4 -2
- package/pan-wizard-core/bin/lib/commands-learnings.cjs +19 -2
- package/pan-wizard-core/bin/lib/commands.cjs +126 -5
- package/pan-wizard-core/bin/lib/config.cjs +112 -1
- package/pan-wizard-core/bin/lib/constants.cjs +29 -15
- package/pan-wizard-core/bin/lib/context-budget.cjs +12 -9
- package/pan-wizard-core/bin/lib/core.cjs +77 -23
- package/pan-wizard-core/bin/lib/cost-rebuild.cjs +13 -1
- package/pan-wizard-core/bin/lib/cost.cjs +149 -10
- package/pan-wizard-core/bin/lib/distill.cjs +6 -3
- package/pan-wizard-core/bin/lib/doc-lint.cjs +30 -11
- package/pan-wizard-core/bin/lib/findings.cjs +15 -4
- package/pan-wizard-core/bin/lib/focus.cjs +13 -4
- package/pan-wizard-core/bin/lib/frontmatter.cjs +105 -26
- package/pan-wizard-core/bin/lib/hygiene.cjs +178 -17
- package/pan-wizard-core/bin/lib/init.cjs +12 -5
- package/pan-wizard-core/bin/lib/knowledge.cjs +17 -1
- package/pan-wizard-core/bin/lib/learn-index.cjs +111 -10
- package/pan-wizard-core/bin/lib/learn-lint.cjs +30 -3
- package/pan-wizard-core/bin/lib/memory-optimize.cjs +8 -4
- package/pan-wizard-core/bin/lib/memory.cjs +508 -54
- package/pan-wizard-core/bin/lib/milestone.cjs +8 -3
- package/pan-wizard-core/bin/lib/optimize.cjs +84 -16
- package/pan-wizard-core/bin/lib/phase-remove.cjs +85 -21
- package/pan-wizard-core/bin/lib/phase-report.cjs +8 -2
- package/pan-wizard-core/bin/lib/phase.cjs +67 -15
- package/pan-wizard-core/bin/lib/preview.cjs +3 -1
- package/pan-wizard-core/bin/lib/review-deep.cjs +2 -2
- package/pan-wizard-core/bin/lib/roadmap-compact.cjs +267 -0
- package/pan-wizard-core/bin/lib/roadmap.cjs +243 -9
- package/pan-wizard-core/bin/lib/runner.cjs +2 -1
- package/pan-wizard-core/bin/lib/state-compact.cjs +11 -6
- package/pan-wizard-core/bin/lib/state.cjs +47 -31
- package/pan-wizard-core/bin/lib/template.cjs +11 -4
- package/pan-wizard-core/bin/lib/verdict.cjs +54 -4
- package/pan-wizard-core/bin/lib/verify-drift.cjs +14 -5
- package/pan-wizard-core/bin/lib/verify-preflight.cjs +4 -13
- package/pan-wizard-core/bin/lib/verify-retro.cjs +5 -3
- package/pan-wizard-core/bin/lib/verify-scope.cjs +74 -4
- package/pan-wizard-core/bin/lib/verify.cjs +117 -35
- package/pan-wizard-core/bin/lib/whatif.cjs +5 -3
- package/pan-wizard-core/bin/pan-tools.cjs +111 -17
- package/pan-wizard-core/learnings/index.json +5 -5
- package/pan-wizard-core/mcp/server.cjs +18 -9
- package/pan-wizard-core/opencode/pan-wizard.js +111 -0
- package/pan-wizard-core/references/checkpoints.md +7 -5
- package/pan-wizard-core/references/continuation-format.md +3 -3
- package/pan-wizard-core/references/decimal-phase-calculation.md +2 -3
- package/pan-wizard-core/references/design-methodology.md +1 -1
- package/pan-wizard-core/references/git-integration.md +4 -6
- package/pan-wizard-core/references/git-planning-commit.md +2 -2
- package/pan-wizard-core/references/guardrails.md +2 -2
- package/pan-wizard-core/references/handoff-decisions.md +3 -3
- package/pan-wizard-core/references/model-profile-resolution.md +5 -5
- package/pan-wizard-core/references/model-profiles.md +14 -9
- package/pan-wizard-core/references/phase-argument-parsing.md +7 -7
- package/pan-wizard-core/references/planning-config.md +4 -4
- package/pan-wizard-core/references/tdd.md +10 -1
- package/pan-wizard-core/references/verdict-contract.md +2 -1
- package/pan-wizard-core/references/verification-patterns.md +5 -5
- package/pan-wizard-core/templates/codebase/architecture.md +2 -2
- package/pan-wizard-core/templates/codebase/best-practices.md +2 -2
- package/pan-wizard-core/templates/codebase/concerns.md +3 -3
- package/pan-wizard-core/templates/codebase/integrations.md +6 -6
- package/pan-wizard-core/templates/codebase/relationships.md +2 -2
- package/pan-wizard-core/templates/codebase/stack.md +2 -2
- package/pan-wizard-core/templates/codebase/structure.md +4 -4
- package/pan-wizard-core/templates/codebase/testing.md +4 -4
- package/pan-wizard-core/templates/continue-here.md +1 -1
- package/pan-wizard-core/templates/debug-subagent-prompt.md +3 -2
- package/pan-wizard-core/templates/debug.md +15 -1
- package/pan-wizard-core/templates/discovery.md +1 -1
- package/pan-wizard-core/templates/milestone-archive.md +6 -6
- package/pan-wizard-core/templates/milestone.md +1 -1
- package/pan-wizard-core/templates/phase-prompt.md +31 -18
- package/pan-wizard-core/templates/planner-subagent-prompt.md +3 -5
- package/pan-wizard-core/templates/research-project/architecture.md +3 -3
- package/pan-wizard-core/templates/research-project/features.md +1 -1
- package/pan-wizard-core/templates/research-project/pitfalls.md +1 -1
- package/pan-wizard-core/templates/research-project/stack.md +3 -3
- package/pan-wizard-core/templates/research.md +5 -5
- package/pan-wizard-core/templates/retrospective.md +1 -1
- package/pan-wizard-core/templates/roadmap.md +15 -15
- package/pan-wizard-core/templates/summary-complex.md +1 -0
- package/pan-wizard-core/templates/summary-minimal.md +1 -0
- package/pan-wizard-core/templates/summary-standard.md +2 -1
- package/pan-wizard-core/templates/summary.md +18 -2
- package/pan-wizard-core/templates/uat.md +3 -3
- package/pan-wizard-core/templates/user-setup.md +8 -8
- package/pan-wizard-core/templates/verification-report.md +54 -4
- package/pan-wizard-core/workflows/add-phase.md +1 -1
- package/pan-wizard-core/workflows/assumptions.md +1 -1
- package/pan-wizard-core/workflows/discuss-phase.md +9 -8
- package/pan-wizard-core/workflows/exec-phase.md +34 -60
- package/pan-wizard-core/workflows/execute-plan.md +8 -7
- package/pan-wizard-core/workflows/health.md +13 -11
- package/pan-wizard-core/workflows/help.md +8 -9
- package/pan-wizard-core/workflows/learn.md +13 -11
- package/pan-wizard-core/workflows/map-codebase.md +10 -10
- package/pan-wizard-core/workflows/milestone-audit.md +10 -10
- package/pan-wizard-core/workflows/milestone-gaps.md +17 -24
- package/pan-wizard-core/workflows/milestone-new.md +7 -6
- package/pan-wizard-core/workflows/new-project.md +10 -10
- package/pan-wizard-core/workflows/optimize.md +11 -9
- package/pan-wizard-core/workflows/pause.md +6 -1
- package/pan-wizard-core/workflows/phase-tests.md +4 -4
- package/pan-wizard-core/workflows/plan-phase.md +31 -34
- package/pan-wizard-core/workflows/profile.md +2 -2
- package/pan-wizard-core/workflows/progress.md +5 -7
- package/pan-wizard-core/workflows/quick.md +10 -8
- package/pan-wizard-core/workflows/remove-phase.md +1 -1
- package/pan-wizard-core/workflows/research-phase.md +9 -5
- package/pan-wizard-core/workflows/resume-project.md +4 -6
- package/pan-wizard-core/workflows/retro.md +3 -1
- package/pan-wizard-core/workflows/settings.md +16 -24
- package/pan-wizard-core/workflows/todo-check.md +1 -1
- package/pan-wizard-core/workflows/transition.md +12 -12
- package/pan-wizard-core/workflows/update.md +25 -17
- package/pan-wizard-core/workflows/verify-phase.md +24 -17
- package/pan-zcode/KNOWN-BETA-RISKS.md +2 -2
- package/pan-zcode/README.md +7 -7
- package/pan-zcode/bin/install-zcode.js +1 -1
- package/scripts/build-agent-plugin.js +19 -5
- package/scripts/build-plugin.js +3 -1
- package/scripts/generate-skills-docs.py +13 -4
- package/scripts/test-surface.cjs +4 -0
|
@@ -18,11 +18,30 @@
|
|
|
18
18
|
// `compact` matcher on both; the source is re-checked here so a registration
|
|
19
19
|
// without the matcher stays inert on every other start.
|
|
20
20
|
//
|
|
21
|
+
// Gemini CLI and Copilot CLI (market-ideas M33) restart no session after a
|
|
22
|
+
// compaction (Gemini's SessionStart sources: startup | resume | clear; Copilot's:
|
|
23
|
+
// startup | resume | new), and their pre-compaction events cannot add context
|
|
24
|
+
// (Gemini PreCompress and Copilot preCompact are advisory). So there the block
|
|
25
|
+
// arrives in two steps:
|
|
26
|
+
// --mark on PreCompress / preCompact: when a PAN phase is in flight,
|
|
27
|
+
// leave a per-session marker in the per-user hook directory
|
|
28
|
+
// under the OS temp directory (0700, written 0600);
|
|
29
|
+
// --inject <host> on the next tool result (Gemini AfterTool, Copilot
|
|
30
|
+
// postToolUse): consume the marker and return the block once,
|
|
31
|
+
// in that host's output shape. With no marker it prints nothing.
|
|
32
|
+
// A tool result is the first point after a compaction where both hosts take added
|
|
33
|
+
// context, including an autonomous `-p` run that never sees another prompt (read
|
|
34
|
+
// 2026-10-03: gemini-cli v0.61.0 docs/hooks/reference.md; the Copilot hooks
|
|
35
|
+
// reference on docs.github.com). The marker modes need no R39 deferral: in a
|
|
36
|
+
// project with both installs, Copilot also runs Claude's registration of this
|
|
37
|
+
// script, but that one is the SessionStart mode, which a Copilot start never matches.
|
|
38
|
+
//
|
|
21
39
|
// Inert unless the project is a PAN project with work in flight: `.planning/`
|
|
22
40
|
// holds a state.md that names a current phase AND a roadmap.md with an unticked
|
|
23
41
|
// phase. Everywhere else — no planning tree, a focus-only project, a finished
|
|
24
|
-
// milestone — it prints nothing. It never writes
|
|
25
|
-
// that scaffolded `.planning/` in projects that never ran PAN)
|
|
42
|
+
// milestone — it prints nothing. It never writes into the project (field finding:
|
|
43
|
+
// hooks that scaffolded `.planning/` in projects that never ran PAN); the only
|
|
44
|
+
// file it writes is the marker, outside the project.
|
|
26
45
|
//
|
|
27
46
|
// Fail-open everywhere: bad stdin, unreadable files — exit 0 silently. A missing
|
|
28
47
|
// reminder costs a re-read; a crashed hook is a failing hook at every start.
|
|
@@ -31,7 +50,9 @@
|
|
|
31
50
|
// additionalContext; each field is truncated on its own.
|
|
32
51
|
|
|
33
52
|
const fs = require('fs');
|
|
53
|
+
const os = require('os');
|
|
34
54
|
const path = require('path');
|
|
55
|
+
const crypto = require('crypto');
|
|
35
56
|
|
|
36
57
|
/**
|
|
37
58
|
* Which planning tree this hook acts on.
|
|
@@ -124,7 +145,99 @@ function readIfExists(p) {
|
|
|
124
145
|
try { return fs.readFileSync(p, 'utf8'); } catch { return null; }
|
|
125
146
|
}
|
|
126
147
|
|
|
148
|
+
// ─── The marker (Gemini, Copilot) ─────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
// A marker older than this belongs to a session that never made another tool call.
|
|
151
|
+
const MARKER_TTL_MS = 6 * 60 * 60 * 1000;
|
|
152
|
+
|
|
153
|
+
/** Gemini sends `session_id`; Copilot's camelCase payloads send `sessionId`. */
|
|
154
|
+
function sessionIdOf(payload) {
|
|
155
|
+
for (const k of ['session_id', 'sessionId']) {
|
|
156
|
+
if (typeof payload[k] === 'string' && payload[k]) return payload[k];
|
|
157
|
+
}
|
|
158
|
+
return '';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The per-user hook directory's name: the stop guard's and the context monitor's too. */
|
|
162
|
+
function hookDirName() {
|
|
163
|
+
const uid = (typeof process.getuid === 'function' ? process.getuid() : process.env.USERNAME || 'win');
|
|
164
|
+
return `pan-hooks-${uid}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Per-user hook state directory inside tmpdir, created 0700 — the same directory
|
|
168
|
+
// and the same checks as hookStateDir() in pan-stop-guard.js (hooks are standalone
|
|
169
|
+
// files and cannot require one another). Null when the directory is not provably
|
|
170
|
+
// ours: on a shared host another user must not be able to plant a marker that
|
|
171
|
+
// injects, or a symlink the marker is written through (M60).
|
|
172
|
+
function hookStateDir() {
|
|
173
|
+
const dir = path.join(os.tmpdir(), hookDirName());
|
|
174
|
+
try {
|
|
175
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
176
|
+
const st = fs.lstatSync(dir);
|
|
177
|
+
if (st.isSymbolicLink()) return null;
|
|
178
|
+
// POSIX-only ownership and mode checks: Windows fakes mode bits (N15).
|
|
179
|
+
if (typeof process.getuid === 'function') {
|
|
180
|
+
if (st.uid !== process.getuid()) return null;
|
|
181
|
+
if ((st.mode & 0o077) !== 0) return null;
|
|
182
|
+
}
|
|
183
|
+
return dir;
|
|
184
|
+
} catch { return null; }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** One marker per (session, project), named by a hash so neither leaks into a file name. Pure. */
|
|
188
|
+
function markerName(sessionId, projectDir) {
|
|
189
|
+
const key = crypto.createHash('sha256').update(`${sessionId}\u0000${path.resolve(projectDir)}`).digest('hex').slice(0, 32);
|
|
190
|
+
return `state-reinject-${key}.json`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// This hook's files in the shared directory: markers, and one a crash left mid-take.
|
|
194
|
+
const MARKER_FILE_RE = /^state-reinject-[0-9a-f]{32}\.json(?:\.\d+\.taken)?$/;
|
|
195
|
+
|
|
196
|
+
function writeMarker(file, now = Date.now()) {
|
|
197
|
+
fs.writeFileSync(file, JSON.stringify({ at: now }), { mode: 0o600 });
|
|
198
|
+
// Sweep markers no session came back for — only this hook's: the directory
|
|
199
|
+
// holds the other hooks' state too. Best effort.
|
|
200
|
+
try {
|
|
201
|
+
for (const name of fs.readdirSync(path.dirname(file))) {
|
|
202
|
+
if (!MARKER_FILE_RE.test(name)) continue;
|
|
203
|
+
const p = path.join(path.dirname(file), name);
|
|
204
|
+
try { if (now - fs.statSync(p).mtimeMs > MARKER_TTL_MS) fs.unlinkSync(p); } catch { /* raced */ }
|
|
205
|
+
}
|
|
206
|
+
} catch { /* unreadable dir */ }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Take the marker if there is a fresh one: rename it first, so two hooks racing on
|
|
211
|
+
* the same tool result cannot both inject. True when this call took it.
|
|
212
|
+
*/
|
|
213
|
+
function takeMarker(file, now = Date.now()) {
|
|
214
|
+
const taken = `${file}.${process.pid}.taken`;
|
|
215
|
+
try { fs.renameSync(file, taken); } catch { return false; }
|
|
216
|
+
let fresh = false;
|
|
217
|
+
try { fresh = now - JSON.parse(fs.readFileSync(taken, 'utf8')).at <= MARKER_TTL_MS; } catch { /* unreadable: stale */ }
|
|
218
|
+
try { fs.unlinkSync(taken); } catch { /* gone */ }
|
|
219
|
+
return fresh;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** The block in the shape each host's tool-result event takes. */
|
|
223
|
+
function injectOutput(host, context) {
|
|
224
|
+
if (host === 'gemini') return { hookSpecificOutput: { hookEventName: 'AfterTool', additionalContext: context } };
|
|
225
|
+
if (host === 'copilot') return { additionalContext: context };
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function contextFor(projectDir) {
|
|
230
|
+
return buildReinjectContext({
|
|
231
|
+
stateContent: readIfExists(planningPath(projectDir, 'state.md')),
|
|
232
|
+
roadmapContent: readIfExists(planningPath(projectDir, 'roadmap.md')),
|
|
233
|
+
planningRel: planningDirName(),
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
127
237
|
function main() {
|
|
238
|
+
const argv = process.argv.slice(2);
|
|
239
|
+
const mode = argv.includes('--mark') ? 'mark' : argv.includes('--inject') ? 'inject' : 'session-start';
|
|
240
|
+
const host = mode === 'inject' ? argv[argv.indexOf('--inject') + 1] : null;
|
|
128
241
|
let input = '';
|
|
129
242
|
process.stdin.setEncoding('utf8');
|
|
130
243
|
process.stdin.on('data', chunk => { input += chunk; });
|
|
@@ -133,20 +246,32 @@ function main() {
|
|
|
133
246
|
let payload = {};
|
|
134
247
|
try { payload = JSON.parse(input); } catch { /* fail open on bad stdin */ }
|
|
135
248
|
if (!payload || typeof payload !== 'object') payload = {};
|
|
136
|
-
if (payload.source !== 'compact') { process.exit(0); return; }
|
|
137
|
-
|
|
138
249
|
const projectDir = typeof payload.cwd === 'string' && payload.cwd ? payload.cwd : process.cwd();
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
250
|
+
|
|
251
|
+
if (mode === 'session-start') {
|
|
252
|
+
if (payload.source !== 'compact') { process.exit(0); return; }
|
|
253
|
+
const context = contextFor(projectDir);
|
|
254
|
+
if (context) {
|
|
255
|
+
process.stdout.write(JSON.stringify({
|
|
256
|
+
hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: context },
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
const sid = sessionIdOf(payload);
|
|
261
|
+
// An unknown host leaves the marker for a host that can use it.
|
|
262
|
+
if (!sid || (mode === 'inject' && !injectOutput(host, ''))) { process.exit(0); return; }
|
|
263
|
+
const dir = hookStateDir();
|
|
264
|
+
if (!dir) { process.exit(0); return; }
|
|
265
|
+
const file = path.join(dir, markerName(sid, projectDir));
|
|
266
|
+
if (mode === 'mark') {
|
|
267
|
+
if (contextFor(projectDir)) writeMarker(file);
|
|
268
|
+
} else if (takeMarker(file)) {
|
|
269
|
+
// Re-read now: the position after the compaction, not before it.
|
|
270
|
+
const context = contextFor(projectDir);
|
|
271
|
+
if (context) process.stdout.write(JSON.stringify(injectOutput(host, context)));
|
|
272
|
+
}
|
|
148
273
|
}
|
|
149
|
-
} catch { /* fail open — never break a session
|
|
274
|
+
} catch { /* fail open — never break a session or a tool call */ }
|
|
150
275
|
process.exit(0);
|
|
151
276
|
});
|
|
152
277
|
}
|
|
@@ -155,4 +280,4 @@ if (require.main === module) {
|
|
|
155
280
|
main();
|
|
156
281
|
}
|
|
157
282
|
|
|
158
|
-
module.exports = { buildReinjectContext, UNTICKED_PHASE_RE };
|
|
283
|
+
module.exports = { buildReinjectContext, UNTICKED_PHASE_RE, markerName, hookDirName, sessionIdOf, injectOutput, MARKER_TTL_MS };
|
|
@@ -30,6 +30,54 @@ function buildStatuslineOutput(data, deps) {
|
|
|
30
30
|
const session = data.session_id || '';
|
|
31
31
|
const remaining = data.context_window?.remaining_percentage;
|
|
32
32
|
|
|
33
|
+
// The per-user 0700 bridge directory, or null when it is not safe to write into.
|
|
34
|
+
// Mirrors bridgeDir() in pan-context-monitor.js (the reader).
|
|
35
|
+
let bridgeSubdirMemo;
|
|
36
|
+
const secureBridgeDir = () => {
|
|
37
|
+
if (bridgeSubdirMemo !== undefined) return bridgeSubdirMemo;
|
|
38
|
+
bridgeSubdirMemo = null;
|
|
39
|
+
const uid = (typeof process.getuid === 'function' ? process.getuid() : process.env.USERNAME || 'win');
|
|
40
|
+
const sub = pathMod.join(tmpDir, `pan-hooks-${uid}`);
|
|
41
|
+
// Fail CLOSED if the per-user dir is pre-planted/symlinked/owned by someone
|
|
42
|
+
// else: mkdirSync{recursive} silently no-ops on an existing dir, so verify
|
|
43
|
+
// ownership+mode before writing (M60).
|
|
44
|
+
try {
|
|
45
|
+
fsMod.mkdirSync(sub, { recursive: true, mode: 0o700 });
|
|
46
|
+
const st = fsMod.lstatSync(sub);
|
|
47
|
+
// Symlink check is cross-platform; POSIX ownership/mode checks apply only
|
|
48
|
+
// where getuid exists (Windows fakes mode bits; gating on them there
|
|
49
|
+
// disabled the bridge entirely, N15).
|
|
50
|
+
let secure = !st.isSymbolicLink();
|
|
51
|
+
if (secure && typeof process.getuid === 'function') {
|
|
52
|
+
secure = st.uid === process.getuid() && (st.mode & 0o077) === 0;
|
|
53
|
+
}
|
|
54
|
+
if (secure) bridgeSubdirMemo = sub;
|
|
55
|
+
} catch { /* not safe */ }
|
|
56
|
+
return bridgeSubdirMemo;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Usage limits (Claude Code subscriptions: `rate_limits.five_hour` / `seven_day`).
|
|
60
|
+
// Account-wide, so one file for every session; `pan-tools cost limits` and the
|
|
61
|
+
// focus-auto weekly stop read it. Written only when the host sent the block.
|
|
62
|
+
const limits = data.rate_limits && typeof data.rate_limits === 'object' ? data.rate_limits : null;
|
|
63
|
+
const limitWin = (w) => (w && Number.isFinite(Number(w.used_percentage))
|
|
64
|
+
? { used_percentage: Number(w.used_percentage), ...(Number.isFinite(Number(w.resets_at)) ? { resets_at: Number(w.resets_at) } : {}) }
|
|
65
|
+
: null);
|
|
66
|
+
const fiveHour = limits ? limitWin(limits.five_hour) : null;
|
|
67
|
+
const sevenDay = limits ? limitWin(limits.seven_day) : null;
|
|
68
|
+
if ((fiveHour || sevenDay) && d.skipBridge !== true) {
|
|
69
|
+
try {
|
|
70
|
+
const sub = secureBridgeDir();
|
|
71
|
+
if (sub) {
|
|
72
|
+
fsMod.writeFileSync(pathMod.join(sub, 'claude-limits.json'), JSON.stringify({
|
|
73
|
+
...(fiveHour ? { five_hour: fiveHour } : {}),
|
|
74
|
+
...(sevenDay ? { seven_day: sevenDay } : {}),
|
|
75
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
76
|
+
}), { mode: 0o600 });
|
|
77
|
+
}
|
|
78
|
+
} catch { /* best-effort */ }
|
|
79
|
+
}
|
|
80
|
+
|
|
33
81
|
// Context window bar — shows USED percentage scaled so 80% real = 100% shown.
|
|
34
82
|
let ctx = '';
|
|
35
83
|
if (remaining != null) {
|
|
@@ -39,34 +87,23 @@ function buildStatuslineOutput(data, deps) {
|
|
|
39
87
|
|
|
40
88
|
if (session && d.skipBridge !== true) {
|
|
41
89
|
try {
|
|
42
|
-
//
|
|
90
|
+
// The session bridge goes into the per-user 0700 subdir so another user on
|
|
43
91
|
// a shared host can't symlink-attack the predictable session path.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const bridgeSubdir = pathMod.join(tmpDir, `pan-hooks-${uid}`);
|
|
47
|
-
// Fail CLOSED if the per-user dir is pre-planted/symlinked/owned by
|
|
48
|
-
// someone else — mkdirSync{recursive} silently no-ops on an existing dir,
|
|
49
|
-
// so verify ownership+mode before writing the session bridge (M60).
|
|
50
|
-
let secure = false;
|
|
51
|
-
try {
|
|
52
|
-
fsMod.mkdirSync(bridgeSubdir, { recursive: true, mode: 0o700 });
|
|
53
|
-
const st = fsMod.lstatSync(bridgeSubdir);
|
|
54
|
-
// Symlink check is cross-platform; POSIX ownership/mode checks apply
|
|
55
|
-
// only where getuid exists (Windows fakes mode bits — gating on them
|
|
56
|
-
// there disabled the bridge entirely, N15).
|
|
57
|
-
secure = !st.isSymbolicLink();
|
|
58
|
-
if (secure && typeof process.getuid === 'function') {
|
|
59
|
-
secure = st.uid === process.getuid() && (st.mode & 0o077) === 0;
|
|
60
|
-
}
|
|
61
|
-
} catch { secure = false; }
|
|
62
|
-
if (secure) {
|
|
92
|
+
const bridgeSubdir = secureBridgeDir();
|
|
93
|
+
if (bridgeSubdir) {
|
|
63
94
|
const bridgePath = pathMod.join(bridgeSubdir, `claude-ctx-${session}.json`);
|
|
95
|
+
// Token counts and the model id let the context monitor measure against
|
|
96
|
+
// the window the host compacts at, not the model window (O7).
|
|
97
|
+
const cw = data.context_window || {};
|
|
64
98
|
fsMod.writeFileSync(bridgePath, JSON.stringify({
|
|
65
99
|
session_id: session,
|
|
66
100
|
remaining_percentage: remaining,
|
|
67
101
|
used_pct: used,
|
|
102
|
+
...(Number.isFinite(cw.total_input_tokens) ? { total_input_tokens: cw.total_input_tokens } : {}),
|
|
103
|
+
...(Number.isFinite(cw.context_window_size) ? { context_window_size: cw.context_window_size } : {}),
|
|
104
|
+
...(data.model && data.model.id ? { model_id: data.model.id } : {}),
|
|
68
105
|
timestamp: Math.floor(Date.now() / 1000),
|
|
69
|
-
}));
|
|
106
|
+
}), { mode: 0o600 });
|
|
70
107
|
}
|
|
71
108
|
} catch { /* bridge is best-effort */ }
|
|
72
109
|
}
|
|
@@ -154,7 +191,16 @@ function buildStatuslineOutput(data, deps) {
|
|
|
154
191
|
const head = `${panUpdate}\x1b[2m${model}\x1b[0m`;
|
|
155
192
|
const taskSegment = task ? ` │ \x1b[1m${task}\x1b[0m` : '';
|
|
156
193
|
const dirSegment = ` │ \x1b[2m${dirname}\x1b[0m`;
|
|
157
|
-
|
|
194
|
+
// Usage-limit badge: the window closer to its cap, green below 70%, yellow below
|
|
195
|
+
// 90%, red from 90% (focus-auto's default weekly stop).
|
|
196
|
+
let limitBadge = '';
|
|
197
|
+
const shown = [fiveHour && ['5h', fiveHour.used_percentage], sevenDay && ['7d', sevenDay.used_percentage]].filter(Boolean);
|
|
198
|
+
if (shown.length) {
|
|
199
|
+
const worst = Math.max(...shown.map(([, v]) => v));
|
|
200
|
+
const color = worst >= 90 ? '[31m' : worst >= 70 ? '[33m' : '[32m';
|
|
201
|
+
limitBadge = ` ${color}${shown.map(([k, v]) => `${k} ${Math.round(v)}%`).join(' · ')}[0m`;
|
|
202
|
+
}
|
|
203
|
+
return `${head}${taskSegment}${dirSegment}${ctx}${cacheBadge}${thinkingBadge}${limitBadge}`;
|
|
158
204
|
}
|
|
159
205
|
|
|
160
206
|
// ─── Stdin driver ───────────────────────────────────────────────────────────
|
|
@@ -73,6 +73,10 @@ const fs = require('fs');
|
|
|
73
73
|
const os = require('os');
|
|
74
74
|
const path = require('path');
|
|
75
75
|
|
|
76
|
+
// The CLI the block reason tells the agent to run. Installs and the plugin put the
|
|
77
|
+
// core beside hooks/; no `pan-tools` bin is on PATH.
|
|
78
|
+
const PAN_TOOLS = `node "${path.join(__dirname, '..', 'pan-wizard-core', 'bin', 'pan-tools.cjs').split(path.sep).join('/')}"`;
|
|
79
|
+
|
|
76
80
|
// ─── R39: one run per hook when Claude and Copilot share a project ───────────
|
|
77
81
|
// Copilot CLI also runs the hooks in a repository's .claude/settings.json and
|
|
78
82
|
// .claude/settings.local.json. Measured 2026-09-26 (Copilot CLI 1.0.88, repository
|
|
@@ -205,8 +209,9 @@ function buildStopDecision({ stopHookActive, config, stateContent, roadmapConten
|
|
|
205
209
|
`Continue the chain now: if the current phase is finished, follow transition.md's ` +
|
|
206
210
|
`offer_next_phase Route A and spawn Phase ${nextPhase} as a Task subagent ` +
|
|
207
211
|
`(plan-phase orchestrator, ARGUMENTS='${nextPhase} --auto'); if the current phase is ` +
|
|
208
|
-
`mid-flight, resume it instead. If the user explicitly asked to stop, first run
|
|
209
|
-
|
|
212
|
+
`mid-flight, resume it instead. If the user explicitly asked to stop, first run ` +
|
|
213
|
+
`\`${PAN_TOOLS} config-set workflow.auto_advance false\` (under mode: yolo, which arms this ` +
|
|
214
|
+
`guard by itself, set \`workflow.stop_guard false\` the same way instead) — then stop. ` +
|
|
210
215
|
`(Disable this guard permanently with workflow.stop_guard: false.)`
|
|
211
216
|
};
|
|
212
217
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pan-wizard",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.34.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"
|
|
@@ -29,7 +29,10 @@ function buildAgentsMdSection() {
|
|
|
29
29
|
'This project uses PAN Wizard for structured, phase-based planning and execution.',
|
|
30
30
|
'',
|
|
31
31
|
'- `.planning/` is PAN\'s state directory (state.md, roadmap.md, phase directories). Treat it as the source of truth for planning state and modify it through PAN commands, not by hand.',
|
|
32
|
-
|
|
32
|
+
// One section serves every runtime in the project, so it names each one's form
|
|
33
|
+
// (the README's list); `/pan-help` alone sent Claude Code users to a command
|
|
34
|
+
// that does not exist there (prompt audit, market-ideas M34).
|
|
35
|
+
'- PAN\'s commands are named per runtime: `/pan:help`, `/pan:new-project`, `/pan:exec-phase` in Claude Code and Gemini CLI; `/pan-help` and so on in OpenCode, Copilot CLI and unified-skills Claude installs; `$pan-help` in Codex. Start with the help command.',
|
|
33
36
|
'- The `pan-tools` dispatcher backs every command; it lives under `pan-wizard-core/` inside the runtime\'s config directory (or `.agents/` for unified installs).',
|
|
34
37
|
PAN_AGENTS_END,
|
|
35
38
|
].join('\n');
|
|
@@ -79,19 +82,56 @@ function removeAgentsMdSection(existing) {
|
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
/**
|
|
82
|
-
*
|
|
83
|
-
* (
|
|
84
|
-
*
|
|
85
|
+
* PAN's block in CLAUDE.md: the @AGENTS.md import, and Claude Code's "Compact
|
|
86
|
+
* instructions" section (memory optimisation O8). Claude Code reads a section of
|
|
87
|
+
* that name in the project-root CLAUDE.md when it summarises a conversation, and
|
|
88
|
+
* re-reads CLAUDE.md from disk after compaction. The section names what the summary
|
|
89
|
+
* must keep for PAN work to resume where it stopped.
|
|
90
|
+
*
|
|
91
|
+
* Codex is deliberately left out: its `compact_prompt` replaces the built-in
|
|
92
|
+
* compaction prompt rather than adding to it, and the default OpenAI provider
|
|
93
|
+
* ignores it (read 2026-10-04), so PAN would be swapping a tuned prompt for one
|
|
94
|
+
* that may never run. PAN's state re-injection after compaction covers Codex.
|
|
95
|
+
*/
|
|
96
|
+
function buildClaudeMdBridge() {
|
|
97
|
+
return [
|
|
98
|
+
PAN_AGENTS_BEGIN,
|
|
99
|
+
'@AGENTS.md',
|
|
100
|
+
'',
|
|
101
|
+
'# Compact instructions',
|
|
102
|
+
'',
|
|
103
|
+
'When this conversation is compacted, keep what PAN work needs to resume where it stopped:',
|
|
104
|
+
'- the current phase and plan, and the task in progress with its stopping point (`.planning/state.md` records them; keep that path);',
|
|
105
|
+
'- decisions made in this session that `.planning/state.md` does not yet hold;',
|
|
106
|
+
'- the files changed since the last commit.',
|
|
107
|
+
'Leave out file contents and command output: they can be read again.',
|
|
108
|
+
PAN_AGENTS_END,
|
|
109
|
+
].join('\n');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Ensure CLAUDE.md carries PAN's block: the marker-fenced @AGENTS.md import
|
|
114
|
+
* (Claude Code's documented pattern for adopting the universal rules file) and
|
|
115
|
+
* the compact instructions. Idempotent; preserves all user content. An older PAN
|
|
116
|
+
* bridge (the import alone) is replaced in place, so a reinstall or `memory
|
|
117
|
+
* rebuild` brings existing projects up to date.
|
|
85
118
|
* @param {string|null} existing - Current CLAUDE.md content, or null if absent
|
|
86
119
|
* @returns {string} New file content
|
|
87
120
|
*/
|
|
88
121
|
function ensureClaudeMdImport(existing) {
|
|
89
|
-
const block =
|
|
122
|
+
const block = buildClaudeMdBridge();
|
|
90
123
|
if (!existing || !existing.trim()) {
|
|
91
124
|
return block + '\n';
|
|
92
125
|
}
|
|
93
|
-
|
|
94
|
-
|
|
126
|
+
const beginIdx = existing.indexOf(PAN_AGENTS_BEGIN);
|
|
127
|
+
const endIdx = existing.indexOf(PAN_AGENTS_END);
|
|
128
|
+
if (beginIdx !== -1) {
|
|
129
|
+
// PAN's own bridge: bring it up to date. Any other PAN block is left alone.
|
|
130
|
+
if (endIdx > beginIdx && /^@AGENTS\.md\s*$/m.test(existing.slice(beginIdx, endIdx))) {
|
|
131
|
+
const eol = existing.includes('\r\n') ? '\r\n' : '\n';
|
|
132
|
+
return existing.slice(0, beginIdx) + block.replace(/\n/g, eol) + existing.slice(endIdx + PAN_AGENTS_END.length);
|
|
133
|
+
}
|
|
134
|
+
return existing;
|
|
95
135
|
}
|
|
96
136
|
if (/^@AGENTS\.md\s*$/m.test(existing)) {
|
|
97
137
|
return existing; // user already imports AGENTS.md themselves
|
|
@@ -115,6 +155,7 @@ module.exports = {
|
|
|
115
155
|
buildAgentsMdSection,
|
|
116
156
|
upsertAgentsMdSection,
|
|
117
157
|
removeAgentsMdSection,
|
|
158
|
+
buildClaudeMdBridge,
|
|
118
159
|
ensureClaudeMdImport,
|
|
119
160
|
removeClaudeMdImport,
|
|
120
161
|
};
|
|
@@ -23,8 +23,9 @@
|
|
|
23
23
|
* ]
|
|
24
24
|
* }
|
|
25
25
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* Nothing populates this file on its own: no hook or host writes it.
|
|
27
|
+
* `bridge cache --servers <json>` (or an external script) seeds it; this module
|
|
28
|
+
* reads the cache and reasons over it.
|
|
28
29
|
*/
|
|
29
30
|
|
|
30
31
|
const fs = require('fs');
|
|
@@ -184,7 +185,7 @@ function recommendForPhase(cwd, phaseNum, opts) {
|
|
|
184
185
|
phase: String(phaseNum),
|
|
185
186
|
runtime: cache.runtime,
|
|
186
187
|
recommendations: [],
|
|
187
|
-
reason: 'no MCP tools cached —
|
|
188
|
+
reason: 'no MCP tools cached — seed .planning/bridge/available-tools.json with `pan-tools bridge cache --servers <json>`; no hook or host writes it',
|
|
188
189
|
};
|
|
189
190
|
}
|
|
190
191
|
|
|
@@ -240,8 +241,8 @@ function cmdBridgeRecommend(cwd, phaseNum, opts, raw) {
|
|
|
240
241
|
}
|
|
241
242
|
|
|
242
243
|
function cmdBridgeCache(cwd, serversJson, runtime, raw) {
|
|
243
|
-
//
|
|
244
|
-
//
|
|
244
|
+
// The cache's only writer: no hook or host runtime populates the file, so this
|
|
245
|
+
// CLI path (or an external script) is how users seed it.
|
|
245
246
|
if (!serversJson) {
|
|
246
247
|
// No payload — just echo the current cache path/state.
|
|
247
248
|
output(listTools(cwd), raw);
|
|
@@ -66,12 +66,14 @@ function writeScheduleFile(cwd, schedule) {
|
|
|
66
66
|
* @returns {object|{error}} the written descriptor
|
|
67
67
|
*/
|
|
68
68
|
function writeSchedule(cwd, opts, now) {
|
|
69
|
-
|
|
69
|
+
// An update keeps the stored cadence like every other field: `--pause` on an
|
|
70
|
+
// hourly campaign used to set it back to daily.
|
|
71
|
+
const existing = readSchedule(cwd) || {};
|
|
72
|
+
const cadence = opts.cadence || existing.cadence || 'daily';
|
|
70
73
|
if (parseCadence(cadence) === null) {
|
|
71
74
|
return { error: `Invalid cadence "${cadence}". Use hourly | daily | weekly | Nh | Nd.` };
|
|
72
75
|
}
|
|
73
76
|
const at = now || new Date();
|
|
74
|
-
const existing = readSchedule(cwd) || {};
|
|
75
77
|
const schedule = {
|
|
76
78
|
goal: opts.goal ?? existing.goal ?? null,
|
|
77
79
|
source: opts.source ?? existing.source ?? 'backlog',
|
|
@@ -299,6 +299,23 @@ function formatLearningEntry(learning) {
|
|
|
299
299
|
return lines.join('\n');
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
+
/**
|
|
303
|
+
* A summary's `key-files` as one list. The summary templates write `{created, modified}`;
|
|
304
|
+
* older summaries carry a flat list. Only the flat list was read, so summaries PAN writes
|
|
305
|
+
* gave no co-change learnings, and one beside a flat-list summary crashed on `.includes`.
|
|
306
|
+
* @param {Object} fm - Summary frontmatter
|
|
307
|
+
* @returns {string[]}
|
|
308
|
+
*/
|
|
309
|
+
function keyFileList(fm) {
|
|
310
|
+
const kf = fm['key-files'] || fm.key_files;
|
|
311
|
+
if (Array.isArray(kf)) return kf.map(String);
|
|
312
|
+
if (kf && typeof kf === 'object') {
|
|
313
|
+
const list = (v) => (Array.isArray(v) ? v : (v ? [v] : []));
|
|
314
|
+
return [...new Set([...list(kf.created), ...list(kf.modified)].map(String))];
|
|
315
|
+
}
|
|
316
|
+
return [];
|
|
317
|
+
}
|
|
318
|
+
|
|
302
319
|
/**
|
|
303
320
|
* Extract learnings from session summaries and error patterns.
|
|
304
321
|
* Reads session history + error patterns, extracts file co-change patterns
|
|
@@ -347,7 +364,7 @@ function cmdLearningsExtract(cwd, raw) {
|
|
|
347
364
|
const fileCoChanges = new Map(); // file -> Set of co-changed files
|
|
348
365
|
|
|
349
366
|
for (const { frontmatter } of summaries) {
|
|
350
|
-
const keyFiles =
|
|
367
|
+
const keyFiles = keyFileList(frontmatter);
|
|
351
368
|
if (keyFiles.length < 2) continue;
|
|
352
369
|
for (const file of keyFiles) {
|
|
353
370
|
if (!fileCoChanges.has(file)) fileCoChanges.set(file, new Set());
|
|
@@ -368,7 +385,7 @@ function cmdLearningsExtract(cwd, raw) {
|
|
|
368
385
|
// Count co-occurrences
|
|
369
386
|
let count = 0;
|
|
370
387
|
for (const { frontmatter } of summaries) {
|
|
371
|
-
const kf = frontmatter
|
|
388
|
+
const kf = keyFileList(frontmatter);
|
|
372
389
|
if (kf.includes(file) && kf.includes(coFile)) count++;
|
|
373
390
|
}
|
|
374
391
|
if (count < 2) continue;
|