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
|
@@ -22,6 +22,7 @@ const {
|
|
|
22
22
|
const { extractFrontmatter, extractPriorityEffort } = require('./frontmatter.cjs');
|
|
23
23
|
const { enumerateRoadmapPhases } = require('./roadmap.cjs');
|
|
24
24
|
const { readErrorPatterns } = require('./commands.cjs');
|
|
25
|
+
const { weeklyLimitStop } = require('./cost.cjs');
|
|
25
26
|
const { planningPath, listPhaseDirs, classifyPhaseStatus, filterPlanFiles, filterSummaryFiles, planningRel } = require('./utils.cjs');
|
|
26
27
|
|
|
27
28
|
// ─── Scan helpers ───────────────────────────────────────────────────────────
|
|
@@ -302,10 +303,13 @@ function allocateBudget(items, budget, mode) {
|
|
|
302
303
|
} else if (mode === 'features') {
|
|
303
304
|
const { picked: mPicked, used: mUsed } = allocatePass(items, budget, { maxPriority: 0, minPriority: 0, track: 'mandatory' });
|
|
304
305
|
addToBatch(mPicked);
|
|
305
|
-
|
|
306
|
+
// The feature/stability split shares what the P0 pass left: splitting the
|
|
307
|
+
// whole budget again let a batch reach nearly twice the budget.
|
|
308
|
+
const rest = budget - mUsed;
|
|
309
|
+
const featureBudget = Math.floor(rest * FEATURE_RATIO);
|
|
306
310
|
const { picked: fPicked, used: fUsed } = allocatePass(items, featureBudget, { minPriority: 3, maxPriority: 5, track: 'feature', exclude: batchIds });
|
|
307
311
|
addToBatch(fPicked);
|
|
308
|
-
const { picked: sPicked, used: sUsed } = allocatePass(items,
|
|
312
|
+
const { picked: sPicked, used: sUsed } = allocatePass(items, rest - featureBudget, { minPriority: 1, maxPriority: 2, track: 'stability', exclude: batchIds });
|
|
309
313
|
addToBatch(sPicked);
|
|
310
314
|
allocated = mUsed + fUsed + sUsed;
|
|
311
315
|
} else {
|
|
@@ -920,9 +924,14 @@ function focusAutoUpdate(cwd, raw, getVal) {
|
|
|
920
924
|
run.totals.tests_current = cycle.tests_after;
|
|
921
925
|
run.status = AUTORUN_STATUSES.IN_PROGRESS;
|
|
922
926
|
|
|
923
|
-
|
|
927
|
+
// The host's 7-day usage window (Claude Code subscriptions, read from what PAN's
|
|
928
|
+
// status line last saw): past cost.weekly_limit_stop_pct the run stops resumable,
|
|
929
|
+
// so the rest of the window stays the user's. No fresh reading never stops it.
|
|
930
|
+
const weekly = weeklyLimitStop(cwd);
|
|
931
|
+
cycle.weekly_limit_used_pct = weekly.seven_day_used_pct;
|
|
932
|
+
const stopReason = determineStopReason(cycle, run) || (weekly.stop ? 'weekly_limit' : null);
|
|
924
933
|
if (stopReason) {
|
|
925
|
-
run.status = stopReason === 'regression' ? AUTORUN_STATUSES.STOPPED : AUTORUN_STATUSES.COMPLETED;
|
|
934
|
+
run.status = stopReason === 'regression' || stopReason === 'weekly_limit' ? AUTORUN_STATUSES.STOPPED : AUTORUN_STATUSES.COMPLETED;
|
|
926
935
|
run.stop_reason = stopReason;
|
|
927
936
|
}
|
|
928
937
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const { safeReadFile, output, error, escapeRegex } = require('./core.cjs');
|
|
7
|
+
const { safeReadFile, output, error, escapeRegex, toLf, dominantEol, withEol } = require('./core.cjs');
|
|
8
8
|
const { FIELD_VALUE_RE, PRIORITY_LEVELS, EFFORT_SIZES } = require('./constants.cjs');
|
|
9
9
|
|
|
10
10
|
// --- Inline array rendering thresholds -------------------------------------------
|
|
@@ -169,7 +169,21 @@ function renderYamlEntry(lines, key, value, depth) {
|
|
|
169
169
|
lines.push(`${indent}${key}:`);
|
|
170
170
|
const itemIndent = ' '.repeat(depth + 1);
|
|
171
171
|
for (const item of value) {
|
|
172
|
-
|
|
172
|
+
if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
173
|
+
// A list of maps (a plan's must_haves artifacts and key_links): the first
|
|
174
|
+
// key on the dash line, the rest under it. Written as a scalar, each item
|
|
175
|
+
// came out "[object Object]".
|
|
176
|
+
const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined);
|
|
177
|
+
entries.forEach(([k, v], idx) => {
|
|
178
|
+
// An array value goes out as a flow sequence of JSON strings, which the
|
|
179
|
+
// parser reads back as an array (`exports: ["GET", "POST"]`).
|
|
180
|
+
const text = Array.isArray(v) ? `[${v.map((x) => JSON.stringify(String(x))).join(', ')}]`
|
|
181
|
+
: quoteIfNeeded(typeof v === 'object' ? JSON.stringify(v) : v);
|
|
182
|
+
lines.push(`${itemIndent}${idx === 0 ? '- ' : ' '}${k}: ${text}`);
|
|
183
|
+
});
|
|
184
|
+
} else {
|
|
185
|
+
lines.push(`${itemIndent}- ${quoteIfNeeded(item)}`);
|
|
186
|
+
}
|
|
173
187
|
}
|
|
174
188
|
}
|
|
175
189
|
} else if (typeof value === 'object') {
|
|
@@ -201,34 +215,76 @@ function quoteIfNeeded(value) {
|
|
|
201
215
|
*/
|
|
202
216
|
function spliceFrontmatter(content, newObj) {
|
|
203
217
|
const yamlStr = reconstructFrontmatter(newObj);
|
|
204
|
-
|
|
218
|
+
// Match on LF and give the file back in its own ending. The LF-only match
|
|
219
|
+
// missed the block in a CRLF file, so `frontmatter set` and `merge` stacked a
|
|
220
|
+
// second frontmatter on top of the first instead of replacing it.
|
|
221
|
+
const eol = dominantEol(content);
|
|
222
|
+
const text = toLf(content);
|
|
223
|
+
const match = text.match(/^---\n[\s\S]+?\n---/);
|
|
205
224
|
if (match) {
|
|
206
|
-
return `---\n${yamlStr}\n---` +
|
|
225
|
+
return withEol(`---\n${yamlStr}\n---` + text.slice(match[0].length), eol);
|
|
226
|
+
}
|
|
227
|
+
return withEol(`---\n${yamlStr}\n---\n\n` + text, eol);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* A YAML scalar as written in a must_haves value: a double-quoted one unescapes `\\`
|
|
232
|
+
* and `\"`, a single-quoted one `''`, a plain one is taken as written. The raw `\\.`
|
|
233
|
+
* of the shipped key-link example reached `new RegExp` in `verify key-links`, so
|
|
234
|
+
* `pattern: "prisma\\.message\\.(find|create)"` never matched.
|
|
235
|
+
* @param {string} raw - The value text after `key:` (or after `- `)
|
|
236
|
+
* @returns {string}
|
|
237
|
+
*/
|
|
238
|
+
function yamlScalar(raw) {
|
|
239
|
+
const s = String(raw).trim();
|
|
240
|
+
// A flow sequence (`exports: ["GET", "POST"]`, as the plan template writes it) is an
|
|
241
|
+
// array of scalars: taken whole it was one string, and `verify artifacts` then
|
|
242
|
+
// reported every export missing.
|
|
243
|
+
if (s.length >= 2 && s.startsWith('[') && s.endsWith(']')) {
|
|
244
|
+
const items = [];
|
|
245
|
+
let cur = '';
|
|
246
|
+
let quote = null;
|
|
247
|
+
const inner = s.slice(1, -1);
|
|
248
|
+
for (let i = 0; i < inner.length; i++) {
|
|
249
|
+
const c = inner[i];
|
|
250
|
+
if (quote) {
|
|
251
|
+
cur += c;
|
|
252
|
+
if (c === '\\' && quote === '"' && i + 1 < inner.length) cur += inner[++i];
|
|
253
|
+
else if (c === quote) quote = null;
|
|
254
|
+
} else if (c === '"' || c === "'") { quote = c; cur += c; }
|
|
255
|
+
else if (c === ',') { items.push(cur); cur = ''; }
|
|
256
|
+
else cur += c;
|
|
257
|
+
}
|
|
258
|
+
items.push(cur);
|
|
259
|
+
return items.map((x) => yamlScalar(x)).filter((x) => x !== '');
|
|
207
260
|
}
|
|
208
|
-
return
|
|
261
|
+
if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) return s.slice(1, -1).replace(/\\(["\\])/g, '$1');
|
|
262
|
+
if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) return s.slice(1, -1).replace(/''/g, "'");
|
|
263
|
+
return s;
|
|
209
264
|
}
|
|
210
265
|
|
|
211
266
|
/**
|
|
212
267
|
* Parse a specific block (artifacts, key_links, truths) from must_haves in raw YAML frontmatter.
|
|
213
268
|
*
|
|
214
|
-
* Block structure being parsed:
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
* provides: bar (level 3+ -- 8-space indent, continuation k-v pairs)
|
|
269
|
+
* Block structure being parsed (indents are relative, never fixed columns):
|
|
270
|
+
* must_haves:
|
|
271
|
+
* artifacts: (the blockName, deeper than must_haves)
|
|
272
|
+
* - path: foo (list items)
|
|
273
|
+
* provides: bar (continuation k-v pairs, deeper than the item's dash)
|
|
220
274
|
*
|
|
221
|
-
* This function finds the named block
|
|
222
|
-
*
|
|
223
|
-
*
|
|
275
|
+
* This function finds the named block under must_haves, then parses its list
|
|
276
|
+
* items and their nested key-value pairs into an array of objects or strings.
|
|
277
|
+
* Values go through yamlScalar.
|
|
224
278
|
*
|
|
225
279
|
* @param {string} content - Full markdown content with frontmatter
|
|
226
280
|
* @param {string} blockName - Block name to extract (e.g., "artifacts", "key_links")
|
|
227
281
|
* @returns {Array} Parsed array of block items (objects or strings)
|
|
228
282
|
*/
|
|
229
283
|
function parseMustHavesBlock(content, blockName) {
|
|
230
|
-
// Extract raw YAML between --- delimiters
|
|
231
|
-
|
|
284
|
+
// Extract raw YAML between --- delimiters. On LF: a CRLF plan (every Windows
|
|
285
|
+
// field project) matched nothing here, so its must_haves read as empty and the
|
|
286
|
+
// reconcile gate below trusted the verdict again, as it did before the indent fix.
|
|
287
|
+
const fmMatch = toLf(content).match(/^---\n([\s\S]+?)\n---/);
|
|
232
288
|
if (!fmMatch) return [];
|
|
233
289
|
|
|
234
290
|
const yaml = fmMatch[1];
|
|
@@ -290,18 +346,18 @@ function parseMustHavesBlock(content, blockName) {
|
|
|
290
346
|
|
|
291
347
|
const body = dashMatch[2];
|
|
292
348
|
// A plain string item — no `key: value` shape.
|
|
293
|
-
const kvMatch = body.match(/^(\w+):\s*
|
|
349
|
+
const kvMatch = body.match(/^(\w+):\s*(.*?)\s*$/);
|
|
294
350
|
if (kvMatch) {
|
|
295
351
|
currentItem = {};
|
|
296
|
-
currentItem[kvMatch[1]] = kvMatch[2];
|
|
352
|
+
currentItem[kvMatch[1]] = yamlScalar(kvMatch[2]);
|
|
297
353
|
} else {
|
|
298
|
-
currentItem = body
|
|
354
|
+
currentItem = yamlScalar(body);
|
|
299
355
|
}
|
|
300
356
|
} else if (currentItem && typeof currentItem === 'object') {
|
|
301
357
|
// Continuation key-value: deeper than the item's dash (properties of it)
|
|
302
|
-
const kvMatch = line.match(/^\s+(\w+):\s*
|
|
358
|
+
const kvMatch = line.match(/^\s+(\w+):\s*(.*?)\s*$/);
|
|
303
359
|
if (kvMatch) {
|
|
304
|
-
const val = kvMatch[2];
|
|
360
|
+
const val = yamlScalar(kvMatch[2]);
|
|
305
361
|
// Coerce pure-integer strings to numbers for convenience
|
|
306
362
|
currentItem[kvMatch[1]] = /^\d+$/.test(val) ? parseInt(val, 10) : val;
|
|
307
363
|
}
|
|
@@ -309,7 +365,7 @@ function parseMustHavesBlock(content, blockName) {
|
|
|
309
365
|
// already deeper than the list-item dash (a shallower one would have been
|
|
310
366
|
// treated as a new item above), so relative depth alone identifies it —
|
|
311
367
|
// no fixed column.
|
|
312
|
-
const arrMatch = line.match(/^\s+-\s+
|
|
368
|
+
const arrMatch = line.match(/^\s+-\s+(.+?)\s*$/);
|
|
313
369
|
if (arrMatch) {
|
|
314
370
|
// Convert the most recently added key's scalar value into an array,
|
|
315
371
|
// then append this item to that array
|
|
@@ -318,7 +374,7 @@ function parseMustHavesBlock(content, blockName) {
|
|
|
318
374
|
if (lastKey && !Array.isArray(currentItem[lastKey])) {
|
|
319
375
|
currentItem[lastKey] = currentItem[lastKey] ? [currentItem[lastKey]] : [];
|
|
320
376
|
}
|
|
321
|
-
if (lastKey) currentItem[lastKey].push(arrMatch[1]);
|
|
377
|
+
if (lastKey) currentItem[lastKey].push(yamlScalar(arrMatch[1]));
|
|
322
378
|
}
|
|
323
379
|
}
|
|
324
380
|
}
|
|
@@ -354,6 +410,27 @@ const FRONTMATTER_SCHEMAS = {
|
|
|
354
410
|
verification: { required: ['phase', 'verified', 'status', 'score'] },
|
|
355
411
|
};
|
|
356
412
|
|
|
413
|
+
/**
|
|
414
|
+
* extractFrontmatter cannot read a list of maps: each must_haves artifact and key
|
|
415
|
+
* link came back as the string of its first line (`path: "src/x.ts`), so the plan
|
|
416
|
+
* checker and verify-phase, which read them through `frontmatter get`, got no
|
|
417
|
+
* objects. parseMustHavesBlock reads both blocks by relative indent.
|
|
418
|
+
* @param {object} fm - extractFrontmatter output
|
|
419
|
+
* @param {string} content - the file it came from
|
|
420
|
+
* @returns {object} fm, with must_haves.artifacts and .key_links as objects
|
|
421
|
+
*/
|
|
422
|
+
function withParsedMustHaves(fm, content) {
|
|
423
|
+
const mh = fm && fm.must_haves;
|
|
424
|
+
if (!mh || typeof mh !== 'object' || Array.isArray(mh)) return fm;
|
|
425
|
+
const fixed = { ...mh };
|
|
426
|
+
for (const block of ['artifacts', 'key_links']) {
|
|
427
|
+
if (!(block in fixed)) continue;
|
|
428
|
+
const parsed = parseMustHavesBlock(content, block);
|
|
429
|
+
if (parsed.length) fixed[block] = parsed;
|
|
430
|
+
}
|
|
431
|
+
return { ...fm, must_haves: fixed };
|
|
432
|
+
}
|
|
433
|
+
|
|
357
434
|
/**
|
|
358
435
|
* Get frontmatter from a file, optionally filtered to a single field.
|
|
359
436
|
* @param {string} cwd - Working directory path
|
|
@@ -367,7 +444,7 @@ function cmdFrontmatterGet(cwd, filePath, field, raw) {
|
|
|
367
444
|
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
|
|
368
445
|
const content = safeReadFile(fullPath);
|
|
369
446
|
if (!content) { output({ error: 'File not found', path: filePath }, raw); return; }
|
|
370
|
-
const fm = extractFrontmatter(content);
|
|
447
|
+
const fm = withParsedMustHaves(extractFrontmatter(content), content);
|
|
371
448
|
if (field) {
|
|
372
449
|
const value = fm[field];
|
|
373
450
|
if (value === undefined) { output({ error: 'Field not found', field }, raw); return; }
|
|
@@ -395,7 +472,9 @@ function cmdFrontmatterSet(cwd, filePath, field, value, raw) {
|
|
|
395
472
|
} catch {
|
|
396
473
|
output({ error: 'File not found', path: filePath }, raw); return;
|
|
397
474
|
}
|
|
398
|
-
|
|
475
|
+
// The block is rewritten whole, so a plan's must_haves lists of maps must be read
|
|
476
|
+
// as maps: extractFrontmatter alone flattened each item to its first line.
|
|
477
|
+
const fm = withParsedMustHaves(extractFrontmatter(content), content);
|
|
399
478
|
let parsedValue;
|
|
400
479
|
// Attempt JSON parse so callers can pass structured values (arrays, objects);
|
|
401
480
|
// fall back to raw string if the value is not valid JSON
|
|
@@ -427,7 +506,7 @@ function cmdFrontmatterMerge(cwd, filePath, data, raw) {
|
|
|
427
506
|
} catch {
|
|
428
507
|
output({ error: 'File not found', path: filePath }, raw); return;
|
|
429
508
|
}
|
|
430
|
-
const fm = extractFrontmatter(content);
|
|
509
|
+
const fm = withParsedMustHaves(extractFrontmatter(content), content);
|
|
431
510
|
let mergeData;
|
|
432
511
|
// Parse the JSON data string; abort with error if the caller passed invalid JSON
|
|
433
512
|
try { mergeData = JSON.parse(data); } catch { error('Invalid JSON for --data'); return; }
|
|
@@ -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,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, normalizePhaseName, toPosix, output, error, scanPendingTodos, isGitRepo, execGit } = require('./core.cjs');
|
|
7
|
+
const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, normalizePhaseName, toPosix, output, error, scanPendingTodos, isGitRepo, execGit, escapeTableCell } = require('./core.cjs');
|
|
8
8
|
const { PHASES_DIR, CODEBASE_DIR, QUICK_DIR, MILESTONES_DIR, STATE_FILE, ROADMAP_FILE, CONFIG_FILE, PROJECT_FILE, REQUIREMENTS_FILE, isPlanFile, isSummaryFile, isResearchFile, isContextFile, isVerificationFile, PLAN_SUFFIX, SUMMARY_SUFFIX, CONTEXT_SUFFIX, RESEARCH_SUFFIX, VERIFICATION_SUFFIX, UAT_SUFFIX, MAX_SLUG_LENGTH } = require('./constants.cjs');
|
|
9
9
|
const { planningPath, phasesPath, filterPlanFiles, filterSummaryFiles, classifyPhaseStatus, hasBraveSearchKey, parsePhaseDir, planningRel } = require('./utils.cjs');
|
|
10
10
|
const { classifyPlanTier } = require('./phase.cjs');
|
|
@@ -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
|
|
@@ -445,6 +448,10 @@ function cmdInitQuick(cwd, description, raw) {
|
|
|
445
448
|
next_num: nextNum,
|
|
446
449
|
slug: slug,
|
|
447
450
|
description: description || null,
|
|
451
|
+
// The description as one markdown table cell (pipes escaped, line breaks
|
|
452
|
+
// folded). The workflow writes this, never the raw text, into state.md's
|
|
453
|
+
// "Quick Tasks Completed" table, where a raw `|` would break the table.
|
|
454
|
+
description_cell: description ? escapeTableCell(description) : null,
|
|
448
455
|
|
|
449
456
|
// Timestamps
|
|
450
457
|
date: now.toISOString().split('T')[0],
|
|
@@ -588,7 +595,7 @@ function cmdInitPhaseOp(cwd, phase, raw) {
|
|
|
588
595
|
phase_number: phaseInfo?.phase_number || null,
|
|
589
596
|
phase_name: phaseInfo?.phase_name || null,
|
|
590
597
|
phase_slug: phaseInfo?.phase_slug || null,
|
|
591
|
-
padded_phase: phaseInfo?.phase_number
|
|
598
|
+
padded_phase: phaseInfo?.phase_number ? normalizePhaseName(String(phaseInfo.phase_number)) : null,
|
|
592
599
|
|
|
593
600
|
// Existing artifacts
|
|
594
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,
|