pan-wizard 3.33.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 +17 -16
- 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 +14 -14
- package/agents/pan-executor.md +12 -8
- package/agents/pan-experiment-runner.md +8 -7
- package/agents/pan-integration-checker.md +2 -2
- package/agents/pan-knowledge.md +1 -1
- package/agents/pan-meta-reviewer.md +3 -3
- package/agents/pan-optimizer.md +39 -43
- package/agents/pan-phase-researcher.md +8 -8
- package/agents/pan-plan-checker.md +30 -28
- package/agents/pan-planner.md +54 -43
- package/agents/pan-previewer.md +1 -1
- package/agents/pan-project-researcher.md +11 -11
- 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 +15 -14
- package/agents/pan-verifier.md +32 -24
- package/bin/install-lib.cjs +58 -23
- package/bin/install.js +47 -1
- package/commands/pan/army.md +15 -11
- 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 +9 -8
- package/commands/pan/focus-auto.md +22 -20
- package/commands/pan/focus-design.md +7 -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 +6 -6
- package/commands/pan/focus-plan.md +12 -11
- package/commands/pan/focus-scan.md +5 -3
- 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 +1 -1
- package/commands/pan/mcp-bridge.md +18 -18
- package/commands/pan/milestone-done.md +6 -3
- 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 +2 -1
- package/commands/pan/plan-phase.md +9 -19
- package/commands/pan/preview.md +1 -1
- package/commands/pan/profile.md +2 -2
- 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 +12 -11
- package/commands/pan/resume.md +9 -11
- package/commands/pan/retro.md +5 -3
- package/commands/pan/review-deep.md +7 -7
- package/commands/pan/settings.md +1 -1
- 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-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 +44 -6
- 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 +33 -4
- package/pan-wizard-core/bin/lib/config.cjs +2 -1
- package/pan-wizard-core/bin/lib/constants.cjs +18 -6
- package/pan-wizard-core/bin/lib/context-budget.cjs +12 -9
- package/pan-wizard-core/bin/lib/core.cjs +53 -19
- package/pan-wizard-core/bin/lib/cost-rebuild.cjs +13 -1
- package/pan-wizard-core/bin/lib/cost.cjs +142 -13
- 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 +9 -2
- 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 +7 -4
- 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 +15 -13
- package/pan-wizard-core/bin/lib/preview.cjs +3 -1
- 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 +43 -28
- package/pan-wizard-core/bin/lib/template.cjs +11 -4
- 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 +5 -2
- package/pan-wizard-core/bin/lib/verify.cjs +30 -28
- package/pan-wizard-core/bin/lib/whatif.cjs +5 -3
- package/pan-wizard-core/bin/pan-tools.cjs +101 -15
- package/pan-wizard-core/learnings/index.json +5 -5
- package/pan-wizard-core/opencode/pan-wizard.js +111 -0
- package/pan-wizard-core/references/checkpoints.md +3 -3
- 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/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/milestone-archive.md +6 -6
- package/pan-wizard-core/templates/milestone.md +1 -1
- package/pan-wizard-core/templates/phase-prompt.md +29 -17
- 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 +53 -8
- 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 +26 -57
- package/pan-wizard-core/workflows/execute-plan.md +8 -7
- package/pan-wizard-core/workflows/health.md +13 -12
- 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 +9 -9
- 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 +5 -3
- 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 +17 -15
- 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-plugin.js +3 -1
- package/scripts/generate-skills-docs.py +13 -4
|
@@ -40,10 +40,12 @@ const {
|
|
|
40
40
|
HYGIENE_TMP_AGE_MS,
|
|
41
41
|
CHARS_PER_TOKEN,
|
|
42
42
|
STATE_FILE,
|
|
43
|
+
ROADMAP_FILE,
|
|
44
|
+
REQUIREMENTS_FILE,
|
|
43
45
|
} = require('./constants.cjs');
|
|
44
46
|
const { planningPath, planningRel, detectPlanningModel } = require('./utils.cjs');
|
|
45
47
|
const { detectForeignPlanningTree } = require('./foreign-planning.cjs');
|
|
46
|
-
const { listMemoryAgents, readMemory, compactMemory } = require('./memory.cjs');
|
|
48
|
+
const { listMemoryAgents, readMemory, compactMemory, pruneMemory, RESERVED_MEMORY_NAMES } = require('./memory.cjs');
|
|
47
49
|
const { readRecords, isSuspectRecord, METRICS_DIR, TOKENS_FILE } = require('./cost.cjs');
|
|
48
50
|
const { assessCacheTtl } = require('./context-budget.cjs');
|
|
49
51
|
const { planningRootRel, planningRoots, withPlanningRoot, describePlanningRoot, TRACKS_DIR } = require('./planning-root.cjs');
|
|
@@ -223,22 +225,131 @@ function checkTmpOrphans(cwd, now = Date.now()) {
|
|
|
223
225
|
return { findings };
|
|
224
226
|
}
|
|
225
227
|
|
|
226
|
-
/**
|
|
228
|
+
/**
|
|
229
|
+
* H-4: per-agent memory logs — past the entry cap (compaction never ran), holding
|
|
230
|
+
* entries `memory select` no longer returns (O4: cited code gone, or unused past the
|
|
231
|
+
* expiry window), and files in the folder that are not read as memory at all.
|
|
232
|
+
*/
|
|
227
233
|
function checkMemoryLogs(cwd) {
|
|
228
234
|
const findings = [];
|
|
229
235
|
const { agents } = listMemoryAgents(cwd);
|
|
230
236
|
for (const a of agents) {
|
|
231
237
|
const mem = readMemory(cwd, a.agent);
|
|
232
238
|
if (!mem || !Array.isArray(mem.entries)) continue;
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
239
|
+
const rel = planningRel('memory', `${a.agent}.md`);
|
|
240
|
+
if (mem.entries.length > MEMORY_ENTRY_CAP) {
|
|
241
|
+
findings.push(mkFinding('memory-bloat', 'warn', rel,
|
|
242
|
+
`${mem.entries.length} entries exceeds cap ${MEMORY_ENTRY_CAP} — compaction keeps the newest`,
|
|
243
|
+
{ action: 'compact-memory', agent: a.agent }));
|
|
244
|
+
}
|
|
245
|
+
const prune = pruneMemory(cwd, a.agent);
|
|
246
|
+
const stale = prune.agents ? prune.agents[0].archive : [];
|
|
247
|
+
if (stale.length) {
|
|
248
|
+
const gone = stale.filter(x => x.reason.startsWith('cited code gone')).length;
|
|
249
|
+
findings.push(mkFinding('memory-stale', 'warn', rel,
|
|
250
|
+
`${stale.length} entr${stale.length === 1 ? 'y is' : 'ies are'} stale or expired `
|
|
251
|
+
+ `(${gone} cite code that is gone, ${stale.length - gone} unused for ${prune.expire_days} days) — `
|
|
252
|
+
+ '`memory prune` archives them',
|
|
253
|
+
{ action: 'prune-memory', agent: a.agent }));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// A file in the folder with no `## Entries` list is never read as memory (PAN's
|
|
257
|
+
// own archives aside): say so rather than let it look like memory.
|
|
258
|
+
for (const n of listMemoryAgents(cwd).not_loaded || []) {
|
|
259
|
+
if (RESERVED_MEMORY_NAMES.includes(n.file.slice(0, -3).toLowerCase())) continue;
|
|
260
|
+
findings.push(mkFinding('memory-format', 'info', planningRel('memory', n.file),
|
|
261
|
+
`${n.reason}, so \`memory select\` and \`/pan:knowledge\` do not read it as memory — move the rules that still hold into an agent log with \`memory append <agent> "<rule>" --cites <path>\``,
|
|
262
|
+
null));
|
|
238
263
|
}
|
|
239
264
|
return { findings };
|
|
240
265
|
}
|
|
241
266
|
|
|
267
|
+
// ─── Host memory (memory optimisation O10) ───────────────────────────────────
|
|
268
|
+
//
|
|
269
|
+
// Cross-session memory accumulates in the host, not in PAN (F7): Claude Code's auto
|
|
270
|
+
// memory keeps an index, MEMORY.md, and loads only its first 200 lines or 25 KB at
|
|
271
|
+
// the start of every session. PAN does not own that store (MI-010) and never writes
|
|
272
|
+
// it; it reads the index and reports what the host will silently cut, and index
|
|
273
|
+
// lines that hold content where a one-line pointer belongs.
|
|
274
|
+
|
|
275
|
+
const HOST_INDEX_MAX_LINES = 200;
|
|
276
|
+
const HOST_INDEX_MAX_BYTES = 25000; // "25KB": the smaller reading, so a warning is never late
|
|
277
|
+
const HOST_INDEX_LINE_MAX_CHARS = 300; // a pointer line is ~100–150 characters
|
|
278
|
+
|
|
279
|
+
/** The directory Claude Code keys a project's auto memory by: the git repository root, the main one for a worktree. */
|
|
280
|
+
function hostMemoryProjectRoot(cwd) {
|
|
281
|
+
let dir = path.resolve(cwd);
|
|
282
|
+
for (;;) {
|
|
283
|
+
const dotGit = path.join(dir, '.git');
|
|
284
|
+
let st = null;
|
|
285
|
+
try { st = fs.statSync(dotGit); } catch { /* keep walking */ }
|
|
286
|
+
if (st && st.isDirectory()) return dir;
|
|
287
|
+
if (st && st.isFile()) {
|
|
288
|
+
// A worktree: `.git` is a file naming <main>/.git/worktrees/<name>.
|
|
289
|
+
const m = (safeReadFile(dotGit) || '').match(/^gitdir:\s*(.+?)\s*$/m);
|
|
290
|
+
if (m) {
|
|
291
|
+
const gitdir = path.resolve(dir, m[1]);
|
|
292
|
+
const i = gitdir.split(path.sep).lastIndexOf('.git');
|
|
293
|
+
if (i > 0 && gitdir.split(path.sep)[i + 1] === 'worktrees') return gitdir.split(path.sep).slice(0, i).join(path.sep);
|
|
294
|
+
}
|
|
295
|
+
return dir;
|
|
296
|
+
}
|
|
297
|
+
const up = path.dirname(dir);
|
|
298
|
+
if (up === dir) return path.resolve(cwd);
|
|
299
|
+
dir = up;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Claude Code's auto-memory directory for a project, and how it was found. */
|
|
304
|
+
function hostMemoryDir(cwd, homeDir) {
|
|
305
|
+
const claudeDir = '.claude';
|
|
306
|
+
for (const f of [path.join(cwd, claudeDir, 'settings.local.json'), path.join(cwd, claudeDir, 'settings.json'), path.join(homeDir, claudeDir, 'settings.json')]) {
|
|
307
|
+
let s = null;
|
|
308
|
+
try { s = JSON.parse(safeReadFile(f) || 'null'); } catch { /* unreadable settings */ }
|
|
309
|
+
if (s && typeof s.autoMemoryDirectory === 'string' && s.autoMemoryDirectory.trim()) {
|
|
310
|
+
const v = s.autoMemoryDirectory.trim();
|
|
311
|
+
return { dir: path.resolve(v.startsWith('~/') ? path.join(homeDir, v.slice(2)) : v), display: v, source: 'autoMemoryDirectory' };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const key = hostMemoryProjectRoot(cwd).replace(/[^A-Za-z0-9]/g, '-');
|
|
315
|
+
return { dir: path.join(homeDir, claudeDir, 'projects', key, 'memory'), display: `~/${claudeDir}/projects/${key}/memory`, source: 'default' };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* H-11: Claude Code's auto-memory index. Read-only; never fixable by PAN.
|
|
320
|
+
* @param {string} cwd
|
|
321
|
+
* @param {{homeDir?: string}} [opts]
|
|
322
|
+
*/
|
|
323
|
+
function checkHostMemory(cwd, opts = {}) {
|
|
324
|
+
const findings = [];
|
|
325
|
+
const home = opts.homeDir || require('os').homedir();
|
|
326
|
+
const loc = hostMemoryDir(cwd, home);
|
|
327
|
+
const text = safeReadFile(path.join(loc.dir, 'MEMORY.md'));
|
|
328
|
+
if (text == null) return { findings, host_memory: { dir: loc.display, index: false } };
|
|
329
|
+
const lines = text.replace(/\r\n/g, '\n').replace(/\n$/, '').split('\n');
|
|
330
|
+
const bytes = Buffer.byteLength(text, 'utf8');
|
|
331
|
+
const rel = `${loc.display}/MEMORY.md`;
|
|
332
|
+
const overLines = lines.length > HOST_INDEX_MAX_LINES;
|
|
333
|
+
const overBytes = bytes > HOST_INDEX_MAX_BYTES;
|
|
334
|
+
if (overLines || overBytes) {
|
|
335
|
+
findings.push(mkFinding('host-memory', 'warn', rel,
|
|
336
|
+
`Claude Code's memory index is ${lines.length} lines / ${(bytes / 1024).toFixed(1)} KB; it loads only the first ${HOST_INDEX_MAX_LINES} lines or 25 KB at session start, so the rest is never seen. `
|
|
337
|
+
+ 'Keep one line per memory there and move detail into topic files (PAN does not edit this store)',
|
|
338
|
+
null));
|
|
339
|
+
} else if (lines.length >= HOST_INDEX_MAX_LINES * 0.9 || bytes >= HOST_INDEX_MAX_BYTES * 0.9) {
|
|
340
|
+
findings.push(mkFinding('host-memory', 'info', rel,
|
|
341
|
+
`Claude Code's memory index is ${lines.length} lines / ${(bytes / 1024).toFixed(1)} KB, near the ${HOST_INDEX_MAX_LINES}-line / 25 KB load limit`,
|
|
342
|
+
null));
|
|
343
|
+
}
|
|
344
|
+
const long = lines.filter(l => l.length > HOST_INDEX_LINE_MAX_CHARS);
|
|
345
|
+
if (long.length) {
|
|
346
|
+
findings.push(mkFinding('host-memory', 'info', rel,
|
|
347
|
+
`${long.length} index line${long.length === 1 ? ' holds' : 's hold'} content instead of a pointer (longest ${Math.max(...long.map(l => l.length))} characters; a pointer is one short line to a topic file)`,
|
|
348
|
+
null));
|
|
349
|
+
}
|
|
350
|
+
return { findings, host_memory: { dir: loc.display, source: loc.source, index: true, lines: lines.length, bytes, long_lines: long.length } };
|
|
351
|
+
}
|
|
352
|
+
|
|
242
353
|
/**
|
|
243
354
|
* Thousands separator that does not depend on the host locale. `toLocaleString()`
|
|
244
355
|
* emits a narrow no-break space in some locales and a comma in others, which
|
|
@@ -407,17 +518,18 @@ function checkCachedContext(cwd) {
|
|
|
407
518
|
try { cached = buildCachedContext(cwd); } catch { return { findings }; }
|
|
408
519
|
if (!cached || !Array.isArray(cached.blocks)) return { findings };
|
|
409
520
|
|
|
410
|
-
// An empty block is not "small" — it means this
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
//
|
|
415
|
-
//
|
|
521
|
+
// An empty block is not "small" — it means this check measures nothing, which
|
|
522
|
+
// is worth saying rather than reporting a healthy zero. (PAN primes no cache,
|
|
523
|
+
// ADR-0023 amended: the host caches each prompt prefix by itself, so this is
|
|
524
|
+
// about measurement, not about caching.) But only for a tree that HAS planning
|
|
525
|
+
// content: a freshly scaffolded `.planning/phases/` has nothing to measure yet,
|
|
526
|
+
// and reporting that is noise on every new project.
|
|
416
527
|
if (cached.blocks.length === 0) {
|
|
417
528
|
if (planningDocCount(cwd) > 0) {
|
|
418
529
|
findings.push(mkFinding('cache-context', 'info', planningRel(),
|
|
419
|
-
'planning docs exist but none
|
|
420
|
-
+ '
|
|
530
|
+
'planning docs exist but none of the stable files this check measures (project.md, requirements.md, '
|
|
531
|
+
+ 'roadmap.md, state.md, standards.md), so it has nothing to read. List stable docs under config.json '
|
|
532
|
+
+ 'cache.extra_files to measure them',
|
|
421
533
|
null));
|
|
422
534
|
}
|
|
423
535
|
return { findings };
|
|
@@ -429,7 +541,8 @@ function checkCachedContext(cwd) {
|
|
|
429
541
|
findings.push(mkFinding('cache-context', severity, planningRel(),
|
|
430
542
|
`cached context block is ~${fmtTokens(blockTokens)} tokens across ${cached.blocks.length} file(s) `
|
|
431
543
|
+ `(warn ${fmtTokens(CACHE_BLOCK_WARN_TOKENS)}, critical ${fmtTokens(CACHE_BLOCK_CRIT_TOKENS)}) — `
|
|
432
|
-
+ 're-read on every agent call
|
|
544
|
+
+ 'state.md is re-read on every agent call; roadmap.md and requirements.md whole by the roadmapper and milestone work '
|
|
545
|
+
+ '(phase agents read `roadmap slice`)',
|
|
433
546
|
null));
|
|
434
547
|
}
|
|
435
548
|
|
|
@@ -437,6 +550,27 @@ function checkCachedContext(cwd) {
|
|
|
437
550
|
const tokens = Math.ceil((b.content || '').length / CHARS_PER_TOKEN);
|
|
438
551
|
if (tokens < CACHE_FILE_WARN_TOKENS) continue;
|
|
439
552
|
const isState = String(b.path).endsWith(STATE_FILE);
|
|
553
|
+
const isRoadmap = String(b.path).endsWith(ROADMAP_FILE);
|
|
554
|
+
const isRequirements = String(b.path).endsWith(REQUIREMENTS_FILE);
|
|
555
|
+
|
|
556
|
+
// The roadmap and requirements are read whole only by the roadmapper and the
|
|
557
|
+
// milestone workflows now; phase agents get `roadmap slice` (O2). A roadmap
|
|
558
|
+
// with shipped phases past the most recent ones can be compacted (O3).
|
|
559
|
+
if (isRoadmap || isRequirements) {
|
|
560
|
+
let fix = null;
|
|
561
|
+
let detail = `~${fmtTokens(tokens)} tokens, read whole by the roadmapper and milestone work (phase agents read \`roadmap slice\`)`;
|
|
562
|
+
if (isRoadmap) {
|
|
563
|
+
const archivable = roadmapCompactionAvailable(cwd);
|
|
564
|
+
if (archivable > 0) {
|
|
565
|
+
fix = { action: 'compact-roadmap' };
|
|
566
|
+
detail += ` — ${archivable} shipped phase section(s) can move to roadmap-history.md (\`roadmap compact\`)`;
|
|
567
|
+
}
|
|
568
|
+
} else {
|
|
569
|
+
detail += ' — `/pan:milestone-done` archives a shipped milestone\'s requirements to `milestones/` and starts the next milestone\'s file fresh';
|
|
570
|
+
}
|
|
571
|
+
findings.push(mkFinding('cache-context', 'warn', b.path, detail, fix));
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
440
574
|
|
|
441
575
|
// A finding may only advertise `auto-fixable` when running the fix would
|
|
442
576
|
// actually change something. state.md stays over the threshold once its
|
|
@@ -491,6 +625,12 @@ function stateCompactionAvailable(cwd) {
|
|
|
491
625
|
}
|
|
492
626
|
}
|
|
493
627
|
|
|
628
|
+
/** Same rule for roadmap.md: offer `compact-roadmap` only when it would archive something. */
|
|
629
|
+
function roadmapCompactionAvailable(cwd) {
|
|
630
|
+
// Lazy for the same reason as state-compact: keep hygiene off a load-time cycle.
|
|
631
|
+
return require('./roadmap-compact.cjs').roadmapCompactionAvailable(cwd);
|
|
632
|
+
}
|
|
633
|
+
|
|
494
634
|
/** H-7: fragment .planning — artifacts present but no project spine. Report-only. */
|
|
495
635
|
function checkPlanningFragment(cwd) {
|
|
496
636
|
const findings = [];
|
|
@@ -572,9 +712,11 @@ function scanHygiene(cwd, opts) {
|
|
|
572
712
|
// matter how many trees we sweep, or a four-track repo reports the same
|
|
573
713
|
// drift four times.
|
|
574
714
|
const version = checkVersionAlignment(cwd);
|
|
715
|
+
// The host's memory index is one per project too, whatever the tracks.
|
|
716
|
+
const host = checkHostMemory(cwd, { homeDir: opts?.homeDir });
|
|
575
717
|
const roots = planningRoots(cwd, { allTracks: !!opts?.allTracks });
|
|
576
718
|
|
|
577
|
-
const findings = [...version.findings];
|
|
719
|
+
const findings = [...version.findings, ...host.findings];
|
|
578
720
|
const scanned = [];
|
|
579
721
|
let planningExists = false;
|
|
580
722
|
|
|
@@ -601,6 +743,7 @@ function scanHygiene(cwd, opts) {
|
|
|
601
743
|
return {
|
|
602
744
|
findings,
|
|
603
745
|
installs: version.installs,
|
|
746
|
+
host_memory: host.host_memory,
|
|
604
747
|
latest_version: version.latest_version,
|
|
605
748
|
planning_exists: planningExists,
|
|
606
749
|
// What was actually looked at. Present in every scan, not just --all-tracks:
|
|
@@ -653,6 +796,12 @@ function applyFix(cwd, finding) {
|
|
|
653
796
|
if (r.error) return { applied: false, detail: r.error };
|
|
654
797
|
return { applied: true, detail: `compacted to ${r.kept ?? r.entries ?? 'cap'} entries` };
|
|
655
798
|
}
|
|
799
|
+
case 'prune-memory': {
|
|
800
|
+
const r = pruneMemory(cwd, fix.agent, { apply: true });
|
|
801
|
+
if (r.error) return { applied: false, detail: r.error };
|
|
802
|
+
if (!r.applied) return { applied: false, detail: 'nothing stale or expired any more' };
|
|
803
|
+
return { applied: true, detail: `archived ${r.archivable} entr${r.archivable === 1 ? 'y' : 'ies'} to ${r.archive_dir}/` };
|
|
804
|
+
}
|
|
656
805
|
case 'compact-state': {
|
|
657
806
|
// Required lazily: state-compact pulls in state.cjs, which pulls in core
|
|
658
807
|
// — importing it at module load would put hygiene on that cycle.
|
|
@@ -665,6 +814,16 @@ function applyFix(cwd, finding) {
|
|
|
665
814
|
detail: `archived ${r.archived.length} section(s) to ${r.history_path} — saves ~${r.tokens_saved_per_call} tokens per agent call`,
|
|
666
815
|
};
|
|
667
816
|
}
|
|
817
|
+
case 'compact-roadmap': {
|
|
818
|
+
const { compactRoadmap } = require('./roadmap-compact.cjs');
|
|
819
|
+
const r = compactRoadmap(cwd, { apply: true });
|
|
820
|
+
if (!r.found) return { applied: false, detail: 'roadmap.md not found' };
|
|
821
|
+
if (!r.applied) return { applied: false, detail: 'no shipped phase past the most recent ones' };
|
|
822
|
+
return {
|
|
823
|
+
applied: true,
|
|
824
|
+
detail: `archived ${r.archived.length} shipped phase section(s) to ${r.history_path} — roadmap.md ~${r.tokens_before - r.tokens_after} tokens smaller`,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
668
827
|
case 'quarantine-ledger': {
|
|
669
828
|
const stamp = new Date().toISOString().slice(0, 10);
|
|
670
829
|
const dest = `${abs}.quarantined-${stamp}`;
|
|
@@ -832,6 +991,8 @@ module.exports = {
|
|
|
832
991
|
checkLegacyUppercase,
|
|
833
992
|
checkTmpOrphans,
|
|
834
993
|
checkMemoryLogs,
|
|
994
|
+
checkHostMemory,
|
|
995
|
+
hostMemoryDir,
|
|
835
996
|
checkCostLedger,
|
|
836
997
|
checkStaleTraces,
|
|
837
998
|
checkStaleReports,
|
|
@@ -31,10 +31,11 @@ function planningRelPath(...segments) {
|
|
|
31
31
|
/**
|
|
32
32
|
* Extract requirement IDs from a ROADMAP phase section.
|
|
33
33
|
* Looks for a **Requirements**: line, strips brackets, splits by comma,
|
|
34
|
-
* and returns null when absent or set to 'TBD'.
|
|
34
|
+
* and returns null when absent or set to 'TBD'. Both bold forms count: the roadmap
|
|
35
|
+
* template writes `**Requirements:**`, which this used to miss (phase_req_ids null).
|
|
35
36
|
*/
|
|
36
37
|
function extractReqIds(roadmapPhase) {
|
|
37
|
-
const reqMatch = roadmapPhase?.section?.match(/^\*\*Requirements
|
|
38
|
+
const reqMatch = roadmapPhase?.section?.match(/^\*\*Requirements(?:\*\*:|:\*\*)[^\S\n]*([^\n]*)$/m);
|
|
38
39
|
const reqExtracted = reqMatch
|
|
39
40
|
? reqMatch[1].replace(/[\[\]]/g, '').split(',').map(segment => segment.trim()).filter(Boolean).join(', ')
|
|
40
41
|
: null;
|
|
@@ -246,7 +247,9 @@ function cmdInitPlanPhase(cwd, phase, raw) {
|
|
|
246
247
|
phase_number: phaseInfo?.phase_number || roadmapPhase?.phase_number || null,
|
|
247
248
|
phase_name: phaseInfo?.phase_name || roadmapPhase?.phase_name || null,
|
|
248
249
|
phase_slug: phaseInfo?.phase_slug || (roadmapPhase?.phase_name ? generateSlugInternal(roadmapPhase.phase_name) : null),
|
|
249
|
-
|
|
250
|
+
// normalizePhaseName pads the integer part, as directories are named (2.1 → 02.1);
|
|
251
|
+
// padStart(2, '0') left a decimal phase unpadded.
|
|
252
|
+
padded_phase: (phaseInfo?.phase_number || roadmapPhase?.phase_number) ? normalizePhaseName(String(phaseInfo?.phase_number || roadmapPhase?.phase_number)) : null,
|
|
250
253
|
phase_req_ids: phaseReqIds,
|
|
251
254
|
|
|
252
255
|
// Existing artifacts
|
|
@@ -592,7 +595,7 @@ function cmdInitPhaseOp(cwd, phase, raw) {
|
|
|
592
595
|
phase_number: phaseInfo?.phase_number || null,
|
|
593
596
|
phase_name: phaseInfo?.phase_name || null,
|
|
594
597
|
phase_slug: phaseInfo?.phase_slug || null,
|
|
595
|
-
padded_phase: phaseInfo?.phase_number
|
|
598
|
+
padded_phase: phaseInfo?.phase_number ? normalizePhaseName(String(phaseInfo.phase_number)) : null,
|
|
596
599
|
|
|
597
600
|
// Existing artifacts
|
|
598
601
|
has_research: phaseInfo?.has_research || false,
|
|
@@ -16,7 +16,21 @@ const fs = require('fs');
|
|
|
16
16
|
const path = require('path');
|
|
17
17
|
const { output, error, safeReadFile, toPosix, escapeRegex } = require('./core.cjs');
|
|
18
18
|
const { planningPath } = require('./utils.cjs');
|
|
19
|
-
const { listMemoryAgents, readMemory } = require('./memory.cjs');
|
|
19
|
+
const { listMemoryAgents, readMemory, RESERVED_MEMORY_NAMES } = require('./memory.cjs');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* PAN's own archives in `.planning/memory/` are never offered as sources: the
|
|
23
|
+
* quarantine holds directives PAN refused to follow (ADR-0040), the state archive
|
|
24
|
+
* is old state, and `archive/` holds the entries `memory prune` retired. Until
|
|
25
|
+
* 2026-10-05 `knowledge ask` ranked them like any other file, and a question about
|
|
26
|
+
* deploying returned the quarantine as its top source for the pan-knowledge agent.
|
|
27
|
+
*/
|
|
28
|
+
function isMemoryArchive(relPosix) {
|
|
29
|
+
const m = String(relPosix).match(/^\.planning\/memory\/(.+)$/);
|
|
30
|
+
if (!m) return false;
|
|
31
|
+
if (m[1] === 'archive' || m[1].startsWith('archive/')) return true;
|
|
32
|
+
return !m[1].includes('/') && RESERVED_MEMORY_NAMES.includes(m[1].replace(/\.md$/i, '').toLowerCase());
|
|
33
|
+
}
|
|
20
34
|
|
|
21
35
|
const CONVERSATIONS_DIR = 'conversations';
|
|
22
36
|
const PLAYBOOK_FILE = 'playbook.md';
|
|
@@ -84,6 +98,7 @@ function gatherCandidates(cwd, question, recallCue) {
|
|
|
84
98
|
try { entries = fs.readdirSync(abs); } catch { continue; }
|
|
85
99
|
for (const entry of entries) {
|
|
86
100
|
const entryAbs = path.join(abs, entry);
|
|
101
|
+
if (isMemoryArchive(toPosix(path.join(rel, entry)))) continue;
|
|
87
102
|
let entryStat;
|
|
88
103
|
try { entryStat = fs.statSync(entryAbs); } catch { continue; }
|
|
89
104
|
if (entryStat.isFile() && entry.endsWith('.md')) {
|
|
@@ -359,6 +374,7 @@ module.exports = {
|
|
|
359
374
|
cmdKnowledgeDiscuss,
|
|
360
375
|
cmdKnowledgePlaybook,
|
|
361
376
|
CITATION_ROOTS,
|
|
377
|
+
isMemoryArchive,
|
|
362
378
|
CONVERSATIONS_DIR,
|
|
363
379
|
PLAYBOOK_FILE,
|
|
364
380
|
PLAYBOOK_CATEGORIES,
|
|
@@ -168,15 +168,102 @@ function readIndex(sourceRoot) {
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
// ─── Cue ranking (memory optimisation O5) ───────────────────────────────────
|
|
172
|
+
//
|
|
173
|
+
// The role ranking loads the same topics whatever the task, filling the budget
|
|
174
|
+
// smallest-first, so a large topic the task needs (migration-safety for a
|
|
175
|
+
// migration) drops out. With a cue — the phase objective and the files it
|
|
176
|
+
// touches — topics are matched to the task: the cue's words against each topic's
|
|
177
|
+
// name, pattern summaries and rules. Lexical, like `memory select`'s scorer, with
|
|
178
|
+
// two changes topic text needs: common words are ignored, and a word meets its
|
|
179
|
+
// own inflections (plurals fold; "corrupting" meets "corrupt", "parsed" "parse").
|
|
180
|
+
|
|
181
|
+
const CUE_STOPWORDS = new Set((
|
|
182
|
+
'the and for with that this from into onto over under when then than them they their there these those '
|
|
183
|
+
+ 'are was were been being have has had not but all any each every per its our your you who what which '
|
|
184
|
+
+ 'can could should would will shall may might must also only just very more most less such same other '
|
|
185
|
+
+ 'add adds added adding make makes made making use uses used using new old get gets set sets run runs '
|
|
186
|
+
+ 'fix fixes fixed one two way ways out off how why where while after before about again still both '
|
|
187
|
+
+ 'file files code thing things project phase plan plans task tasks work works working change changes'
|
|
188
|
+
).split(' '));
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Fold a plural: "retries" → "retry", "classes" → "class", "matches" → "match",
|
|
192
|
+
* "failures" → "failure", "cases" → "case"; "class" and "status" stay.
|
|
193
|
+
*/
|
|
194
|
+
function foldPlural(w) {
|
|
195
|
+
if (w.length > 4 && w.endsWith('ies')) return `${w.slice(0, -3)}y`;
|
|
196
|
+
if (w.length > 5 && w.endsWith('sses')) return w.slice(0, -2);
|
|
197
|
+
if (w.length > 4 && /(x|z|ch|sh)es$/.test(w)) return w.slice(0, -2);
|
|
198
|
+
if (w.length > 3 && w.endsWith('s') && !/(ss|us|is)$/.test(w)) return w.slice(0, -1);
|
|
199
|
+
return w;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** The content words of a text (3+ letters, plurals folded, no stopwords). */
|
|
203
|
+
function cueTerms(text) {
|
|
204
|
+
const out = new Set();
|
|
205
|
+
for (const w of String(text || '').toLowerCase().match(/[a-z0-9]{3,}/g) || []) {
|
|
206
|
+
const f = foldPlural(w);
|
|
207
|
+
if (!CUE_STOPWORDS.has(w) && !CUE_STOPWORDS.has(f)) out.add(f);
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Two words meet when equal, or when the shorter (4+ letters) begins the longer, at most 3 letters apart. */
|
|
213
|
+
function termsMeet(a, b) {
|
|
214
|
+
if (a === b) return true;
|
|
215
|
+
const [s, l] = a.length <= b.length ? [a, b] : [b, a];
|
|
216
|
+
return s.length >= 4 && l.length - s.length <= 3 && l.startsWith(s);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function setHasTerm(set, t) {
|
|
220
|
+
if (set.has(t)) return true;
|
|
221
|
+
for (const u of set) if (termsMeet(t, u)) return true;
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Each topic's terms from the store: its name, and its patterns' summaries and
|
|
227
|
+
* rules (not their evidence, which is long and incidental).
|
|
228
|
+
* @returns {Map<string, {name: Set<string>, text: Set<string>}>}
|
|
229
|
+
*/
|
|
230
|
+
function topicTermsFromStore(sourceRoot) {
|
|
231
|
+
const { extractFieldSection } = require('./learn-lint.cjs');
|
|
232
|
+
const text = new Map();
|
|
233
|
+
for (const p of collectAllPatterns(sourceRoot)) {
|
|
234
|
+
const prev = text.get(p.topic) || '';
|
|
235
|
+
text.set(p.topic, `${prev}\n${p.summary}\n${extractFieldSection(p.body, 'Rule')}`);
|
|
236
|
+
}
|
|
237
|
+
const out = new Map();
|
|
238
|
+
for (const [topic, t] of text) out.set(topic, { name: cueTerms(topic.replace(/-/g, ' ')), text: cueTerms(t) });
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** How well a topic matches the cue: 2 per cue term in its name, 1 per term in its text. */
|
|
243
|
+
function cueScore(cue, terms) {
|
|
244
|
+
if (!terms) return 0;
|
|
245
|
+
let s = 0;
|
|
246
|
+
for (const t of cue) s += setHasTerm(terms.name, t) ? 2 : setHasTerm(terms.text, t) ? 1 : 0;
|
|
247
|
+
return s;
|
|
248
|
+
}
|
|
249
|
+
|
|
171
250
|
/**
|
|
172
251
|
* Query: what topics should agent X load at this point?
|
|
173
252
|
*
|
|
253
|
+
* With `opts.cue` and `opts.topicTerms`, topics are matched to the task (mode
|
|
254
|
+
* `cue`): every topic scoring at least half the best match is a candidate
|
|
255
|
+
* whatever its role relevance, ranked by cue score, then relevance, then size;
|
|
256
|
+
* the rest are not loaded. When nothing matches, or there is no cue, the role
|
|
257
|
+
* ranking applies (mode `role`): relevance, then smallest first.
|
|
258
|
+
*
|
|
174
259
|
* @param {object} index - the index object
|
|
175
260
|
* @param {object} opts
|
|
176
261
|
* @param {string} opts.agent - 'planner' | 'executor' | 'verifier' | 'reviewer'
|
|
177
|
-
* @param {string} [opts.minRelevance] - 'high' | 'medium' | 'low' (default 'medium')
|
|
262
|
+
* @param {string} [opts.minRelevance] - 'high' | 'medium' | 'low' (default 'medium'; role mode)
|
|
178
263
|
* @param {number} [opts.tokenBudget] - max tokens to fit (default 5000)
|
|
179
|
-
* @
|
|
264
|
+
* @param {string} [opts.cue] - the phase objective and the files it touches
|
|
265
|
+
* @param {Map} [opts.topicTerms] - from topicTermsFromStore()
|
|
266
|
+
* @returns {object} { selected: [{name, scope, file, tokens, relevance, cue_score?}], total_tokens, dropped: [...], mode }
|
|
180
267
|
*/
|
|
181
268
|
function topicsForAgent(index, opts) {
|
|
182
269
|
const agent = opts.agent;
|
|
@@ -185,14 +272,22 @@ function topicsForAgent(index, opts) {
|
|
|
185
272
|
const ranks = { high: 3, medium: 2, low: 1 };
|
|
186
273
|
const minRank = ranks[minLevel] || 2;
|
|
187
274
|
|
|
188
|
-
const
|
|
189
|
-
|
|
275
|
+
const cue = cueTerms(opts.cue);
|
|
276
|
+
const scored = index.topics.map(t => {
|
|
190
277
|
const rel = (t.agent_relevance || {})[agent] || 'low';
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
278
|
+
return { ...t, _relevance: rel, _rank: ranks[rel] || 1, _cue: cue.size ? cueScore(cue, opts.topicTerms && opts.topicTerms.get(t.name)) : 0 };
|
|
279
|
+
});
|
|
280
|
+
// A topic must score at least half the best match: a single incidental shared
|
|
281
|
+
// word should not load a topic beside the ones the task is about. On the golden
|
|
282
|
+
// set (tests/fixtures/learn-cue-golden.json) this cut raised precision from
|
|
283
|
+
// 0.22 to 0.37 for a recall of 0.76 instead of 0.81.
|
|
284
|
+
const top = Math.max(0, ...scored.map(t => t._cue));
|
|
285
|
+
const matched = scored.filter(t => t._cue > 0 && t._cue >= Math.ceil(top / 2));
|
|
286
|
+
const mode = matched.length ? 'cue' : 'role';
|
|
287
|
+
|
|
288
|
+
const ranked = mode === 'cue'
|
|
289
|
+
? matched.sort((a, b) => b._cue - a._cue || b._rank - a._rank || a.size_tokens_est - b.size_tokens_est)
|
|
290
|
+
: scored.filter(t => t._rank >= minRank).sort((a, b) => b._rank - a._rank || a.size_tokens_est - b.size_tokens_est);
|
|
196
291
|
|
|
197
292
|
const selected = [];
|
|
198
293
|
const dropped = [];
|
|
@@ -209,6 +304,7 @@ function topicsForAgent(index, opts) {
|
|
|
209
304
|
tokens: t.size_tokens_est,
|
|
210
305
|
relevance: t._relevance,
|
|
211
306
|
patterns: t.patterns,
|
|
307
|
+
...(mode === 'cue' ? { cue_score: t._cue } : {}),
|
|
212
308
|
});
|
|
213
309
|
total += t.size_tokens_est;
|
|
214
310
|
}
|
|
@@ -217,6 +313,8 @@ function topicsForAgent(index, opts) {
|
|
|
217
313
|
agent,
|
|
218
314
|
min_relevance: minLevel,
|
|
219
315
|
token_budget: budget,
|
|
316
|
+
mode,
|
|
317
|
+
...(opts.cue ? { cue: opts.cue } : {}),
|
|
220
318
|
selected,
|
|
221
319
|
dropped,
|
|
222
320
|
total_tokens: total,
|
|
@@ -237,7 +335,8 @@ function cmdBuildIndex(sourceRoot) {
|
|
|
237
335
|
|
|
238
336
|
function cmdTopicsFor(sourceRoot, opts) {
|
|
239
337
|
const index = readIndex(sourceRoot);
|
|
240
|
-
|
|
338
|
+
const topicTerms = opts.cue ? topicTermsFromStore(sourceRoot) : undefined;
|
|
339
|
+
return topicsForAgent(index, { ...opts, topicTerms });
|
|
241
340
|
}
|
|
242
341
|
|
|
243
342
|
module.exports = {
|
|
@@ -245,6 +344,8 @@ module.exports = {
|
|
|
245
344
|
writeIndex,
|
|
246
345
|
readIndex,
|
|
247
346
|
topicsForAgent,
|
|
347
|
+
topicTermsFromStore,
|
|
348
|
+
cueTerms,
|
|
248
349
|
cmdBuildIndex,
|
|
249
350
|
cmdTopicsFor,
|
|
250
351
|
INDEX_PATH_REL,
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* (candidate for internal/ scope rather than universal/)
|
|
12
12
|
* - L-005: Revision marker (rN) appended in body but no supersession field
|
|
13
13
|
* - L-006: Universal-scope pattern cites an internal pattern id (dangles after install)
|
|
14
|
+
* - L-007: A pattern's `cites` no longer holds — the cited file or symbol is gone
|
|
15
|
+
* from the tree that holds the store (memory optimisation O4)
|
|
14
16
|
*
|
|
15
17
|
* These are not patterns themselves — they're integrity checks for the
|
|
16
18
|
* pattern store. Wired to `pan-tools learn lint`.
|
|
@@ -45,7 +47,9 @@ function getLearningsDir(sourceRoot, scope) {
|
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
function readTopicFile(filePath) {
|
|
48
|
-
|
|
50
|
+
// LF: an install committed with core.autocrlf=true holds these files as CRLF,
|
|
51
|
+
// and the LF-only match below then read every topic as having no patterns.
|
|
52
|
+
const content = fs.readFileSync(filePath, 'utf-8').replace(/^/, '').replace(/\r\n/g, '\n');
|
|
49
53
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
50
54
|
if (!fmMatch) return { frontmatter: { patterns: [] }, body: content };
|
|
51
55
|
const fmText = fmMatch[1];
|
|
@@ -113,6 +117,7 @@ function collectAllPatterns(sourceRoot) {
|
|
|
113
117
|
source_experiments: Array.isArray(p.source_experiments) ? p.source_experiments : [],
|
|
114
118
|
superseded_by: p.superseded_by || null,
|
|
115
119
|
superseded_id: p.superseded_id || null,
|
|
120
|
+
cites: Array.isArray(p.cites) ? p.cites : [],
|
|
116
121
|
body: patternBody,
|
|
117
122
|
});
|
|
118
123
|
}
|
|
@@ -170,7 +175,7 @@ const PATTERN_REF_RE = /\bP-(?:[A-Z]+-)?\d+(?:-r\d+)?\b/g;
|
|
|
170
175
|
* pattern_count, file_count, scopes
|
|
171
176
|
* }
|
|
172
177
|
*/
|
|
173
|
-
function lintPatterns(patterns) {
|
|
178
|
+
function lintPatterns(patterns, opts = {}) {
|
|
174
179
|
const violations = [];
|
|
175
180
|
|
|
176
181
|
const idIndex = new Map(); // id -> [{scope, topic, file}, ...]
|
|
@@ -297,6 +302,28 @@ function lintPatterns(patterns) {
|
|
|
297
302
|
}
|
|
298
303
|
}
|
|
299
304
|
|
|
305
|
+
// L-007: a citation that no longer holds. A pattern may name the code it rests
|
|
306
|
+
// on (`cites: [pan-wizard-core/bin/lib/x.cjs#symbol]`, relative to the tree
|
|
307
|
+
// holding the store). When that code is gone the pattern is stale evidence, the
|
|
308
|
+
// way a memory entry is (O4). Checked only when the caller passes the root.
|
|
309
|
+
if (opts.sourceRoot) {
|
|
310
|
+
const { citationProblem } = require('./memory.cjs');
|
|
311
|
+
for (const p of patterns) {
|
|
312
|
+
for (const c of p.cites || []) {
|
|
313
|
+
const problem = citationProblem(opts.sourceRoot, c);
|
|
314
|
+
if (!problem) continue;
|
|
315
|
+
violations.push({
|
|
316
|
+
code: 'L-007',
|
|
317
|
+
severity: 'error',
|
|
318
|
+
pattern_id: p.id,
|
|
319
|
+
file: p.file,
|
|
320
|
+
message: `Pattern "${p.id}" cites ${c}, which no longer holds (${problem}) — update the pattern or retire it`,
|
|
321
|
+
cite: c,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
300
327
|
return {
|
|
301
328
|
violations,
|
|
302
329
|
pattern_count: patterns.length,
|
|
@@ -316,7 +343,7 @@ function cmdLearnLint(sourceRoot, opts = {}) {
|
|
|
316
343
|
const filtered = opts.scope
|
|
317
344
|
? all.filter(p => p.scope === opts.scope)
|
|
318
345
|
: all;
|
|
319
|
-
const result = lintPatterns(filtered);
|
|
346
|
+
const result = lintPatterns(filtered, { sourceRoot });
|
|
320
347
|
|
|
321
348
|
const errors = result.violations.filter(v => v.severity === 'error').length;
|
|
322
349
|
const warnings = result.violations.filter(v => v.severity === 'warning').length;
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const path = require('path');
|
|
20
|
-
const { output, safeReadFile } = require('./core.cjs');
|
|
20
|
+
const { output, safeReadFile, toLf, dominantEol, withEol } = require('./core.cjs');
|
|
21
21
|
const { planningPath } = require('./utils.cjs');
|
|
22
22
|
const { writeStateMd } = require('./state.cjs');
|
|
23
23
|
const { readMemory, parseEntries, listMemoryAgents, compactMemory, DEFAULT_MAX_ENTRIES, MEMORY_DIR } = require('./memory.cjs');
|
|
@@ -167,7 +167,11 @@ function reconcileBullets(lines, keepN, archived, quarantined) {
|
|
|
167
167
|
*/
|
|
168
168
|
function optimizeStateContent(content, opts = {}) {
|
|
169
169
|
const keepN = Number.isFinite(opts.keep) && opts.keep > 0 ? opts.keep : DEFAULT_KEEP;
|
|
170
|
-
|
|
170
|
+
// Work on LF and hand the file back in its own ending. On CRLF input
|
|
171
|
+
// headingText's `(.*)$` could not cross the trailing '\r', so no heading
|
|
172
|
+
// matched APPEND_HEAVY and every Windows project read as already lean.
|
|
173
|
+
const eol = dominantEol(content);
|
|
174
|
+
const sections = parseSections(toLf(content));
|
|
171
175
|
const archived = [];
|
|
172
176
|
const quarantined = [];
|
|
173
177
|
const sectionsTouched = [];
|
|
@@ -184,7 +188,7 @@ function optimizeStateContent(content, opts = {}) {
|
|
|
184
188
|
}
|
|
185
189
|
}
|
|
186
190
|
|
|
187
|
-
return { content: changed ? joinSections(sections) : content, changed, archived, quarantined, sectionsTouched };
|
|
191
|
+
return { content: changed ? withEol(joinSections(sections), eol) : content, changed, archived, quarantined, sectionsTouched };
|
|
188
192
|
}
|
|
189
193
|
|
|
190
194
|
// ─── Command ────────────────────────────────────────────────────────────────
|
|
@@ -339,5 +343,5 @@ function maybeAutoOptimizeMemory(cwd, opts = {}) {
|
|
|
339
343
|
module.exports = {
|
|
340
344
|
optimizeStateContent, reconcileBullets, parseSections, joinSections, cmdMemoryOptimize,
|
|
341
345
|
maybeAutoOptimizeMemory, autoOptimizeEnabled, isSuspiciousDirective,
|
|
342
|
-
APPEND_HEAVY, PLACEHOLDER, DIRECTIVE_PATTERNS, DEFAULT_KEEP, STATE_ARCHIVE_FILE, QUARANTINE_FILE,
|
|
346
|
+
APPEND_HEAVY, PLACEHOLDER, DIRECTIVE_PATTERNS, DEFAULT_KEEP, STATE_ARCHIVE_FILE, QUARANTINE_FILE, QUARANTINE_HEADER,
|
|
343
347
|
};
|