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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const { loadConfig, getMilestoneInfo, escapeRegex, safeReadFile, output, error } = require('./core.cjs');
|
|
7
|
+
const { loadConfig, getMilestoneInfo, escapeRegex, escapeTableCell, safeReadFile, output, error } = require('./core.cjs');
|
|
8
8
|
const { extractFrontmatter, reconstructFrontmatter } = require('./frontmatter.cjs');
|
|
9
9
|
const { withFileLock, writeFileAtomic } = require('./lock.cjs');
|
|
10
10
|
const {
|
|
@@ -294,21 +294,22 @@ function cmdStateRecordMetric(cwd, options, raw) {
|
|
|
294
294
|
return;
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
-
//
|
|
298
|
-
|
|
297
|
+
// The Performance Metrics section's first table: its rows only, then an optional
|
|
298
|
+
// "None yet" line. Taking the rest of the section as the table body put every row
|
|
299
|
+
// below the template's "Recent Trend" and footer lines, outside the table. The
|
|
300
|
+
// separator is matched on one line: `[-|\s]+` ran on into a `| - | - |` row.
|
|
301
|
+
const metricsPattern = /(##\s*Performance Metrics[\s\S]*?\n\|[^\n]+\n\|[-|: \t]+\r?\n)((?:\|[^\n]*(?:\n|$))*)((?:[^\S\n]*None yet\.?[^\S\n]*(?:\n|$))?)/i;
|
|
299
302
|
const metricsMatch = content.match(metricsPattern);
|
|
300
303
|
|
|
301
304
|
if (metricsMatch) {
|
|
302
|
-
|
|
303
|
-
const newRow = `| Phase ${phase} P${plan} | ${duration} | ${tasks || '-'} tasks | ${files || '-'} files |`;
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
content = content.replace(metricsPattern, (_match, header) => `${header}${tableBody}\n`);
|
|
305
|
+
const cell = escapeTableCell;
|
|
306
|
+
const newRow = `| Phase ${cell(phase)} P${cell(plan)} | ${cell(duration)} | ${cell(tasks || '-')} tasks | ${cell(files || '-')} files |`;
|
|
307
|
+
// A template placeholder row (`| - | - | - | - |`, `| None yet | | | |`) gives way.
|
|
308
|
+
const isPlaceholder = (row) => /None yet/i.test(row) || row.split('|').slice(1, -1).every((c) => /^\s*-?\s*$/.test(c));
|
|
309
|
+
const rows = metricsMatch[2].split('\n').map((r) => r.replace(/\r$/, '')).filter((r) => r.trim() && !isPlaceholder(r));
|
|
310
|
+
rows.push(newRow);
|
|
311
|
+
|
|
312
|
+
content = content.replace(metricsPattern, (_match, header) => `${header}${rows.join('\n')}\n`);
|
|
312
313
|
writeStateMd(statePath, content, cwd);
|
|
313
314
|
output({ recorded: true, phase, plan, duration }, raw, 'true');
|
|
314
315
|
} else {
|
|
@@ -431,7 +432,8 @@ function cmdStateAddBlocker(cwd, text, raw) {
|
|
|
431
432
|
|
|
432
433
|
if (match) {
|
|
433
434
|
let sectionBody = match[2];
|
|
434
|
-
|
|
435
|
+
// "None yet" first: stripping "None" first left a stray "yet." on the template.
|
|
436
|
+
sectionBody = sectionBody.replace(/None yet\.?\s*\n?/gi, '').replace(/None\.?\s*\n?/gi, '');
|
|
435
437
|
sectionBody = sectionBody.trimEnd() + '\n' + entry + '\n';
|
|
436
438
|
content = content.replace(sectionPattern, (_match, header) => `${header}${sectionBody}`);
|
|
437
439
|
writeStateMd(statePath, content, cwd);
|
|
@@ -537,14 +539,23 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
537
539
|
// --- Snapshot Parsers --------------------------------------------------------
|
|
538
540
|
|
|
539
541
|
/**
|
|
540
|
-
* Parse the
|
|
541
|
-
*
|
|
542
|
-
*
|
|
542
|
+
* Parse the decisions from state.md content: the `- [Phase N]: summary — rationale`
|
|
543
|
+
* bullets `state add-decision` appends under `### Decisions` (the template's
|
|
544
|
+
* heading), or else an older "## Decisions Made" table. Reading only the table
|
|
545
|
+
* left `decisions` empty on every state.md PAN itself writes.
|
|
543
546
|
* @param {string} content - Full state.md content
|
|
544
547
|
* @returns {Array<{phase: string, summary: string, rationale: string}>}
|
|
545
548
|
*/
|
|
546
549
|
function parseDecisionsFromState(content) {
|
|
547
550
|
const decisions = [];
|
|
551
|
+
const section = content.match(/###?\s*(?:Decisions|Decisions Made|Accumulated.*Decisions)\s*\n([\s\S]*?)(?=\n###?|\n##[^#]|$)/i);
|
|
552
|
+
for (const line of section ? section[1].split('\n') : []) {
|
|
553
|
+
const m = line.match(/^-\s+\[Phase\s+([^\]]*)\]:\s*(.+?)\s*$/i);
|
|
554
|
+
if (!m) continue;
|
|
555
|
+
const [summary, ...rest] = m[2].split(' — ');
|
|
556
|
+
decisions.push({ phase: m[1].trim(), summary: summary.trim(), rationale: rest.join(' — ').trim() });
|
|
557
|
+
}
|
|
558
|
+
if (decisions.length > 0) return decisions;
|
|
548
559
|
// Match the decisions table body after header row and separator row
|
|
549
560
|
const decisionsMatch = content.match(/##\s*Decisions Made[\s\S]*?\n\|[^\n]+\n\|[-|\s]+\n([\s\S]*?)(?=\n##|\n$|$)/i);
|
|
550
561
|
if (decisionsMatch) {
|
|
@@ -566,14 +577,15 @@ function parseDecisionsFromState(content) {
|
|
|
566
577
|
|
|
567
578
|
/**
|
|
568
579
|
* Parse the Blockers section from state.md content.
|
|
569
|
-
* Extracts bullet-point items (lines starting with "- ") from under
|
|
570
|
-
*
|
|
580
|
+
* Extracts bullet-point items (lines starting with "- ") from under the heading
|
|
581
|
+
* `state add-blocker` writes to: `### Blockers/Concerns` in the template, or
|
|
582
|
+
* `Blockers` / `Concerns`. Matching only "## Blockers" left the list empty.
|
|
571
583
|
* @param {string} content - Full state.md content
|
|
572
584
|
* @returns {string[]} Array of blocker text strings
|
|
573
585
|
*/
|
|
574
586
|
function parseBlockersFromState(content) {
|
|
575
587
|
const blockers = [];
|
|
576
|
-
const blockersMatch = content.match(
|
|
588
|
+
const blockersMatch = content.match(/###?\s*(?:Blockers|Blockers\/Concerns|Concerns)\s*\n([\s\S]*?)(?=\n###?|\n##[^#]|$)/i);
|
|
577
589
|
if (blockersMatch) {
|
|
578
590
|
const blockersSection = blockersMatch[1];
|
|
579
591
|
const items = blockersSection.match(/^-\s+(.+)$/gm) || [];
|
|
@@ -584,6 +596,18 @@ function parseBlockersFromState(content) {
|
|
|
584
596
|
return blockers;
|
|
585
597
|
}
|
|
586
598
|
|
|
599
|
+
/**
|
|
600
|
+
* The blockers that are still open: the section's bullets minus a "None" placeholder.
|
|
601
|
+
* `preflight` and `dashboard` read a `## Blockers` heading of their own, which the
|
|
602
|
+
* state template never writes (it writes `### Blockers/Concerns`, where
|
|
603
|
+
* `state add-blocker` puts its bullets), so both reported no blockers with one open.
|
|
604
|
+
* @param {string} content - Full state.md content
|
|
605
|
+
* @returns {string[]}
|
|
606
|
+
*/
|
|
607
|
+
function activeBlockersFromState(content) {
|
|
608
|
+
return parseBlockersFromState(content).filter((b) => b && !/^none(\s+yet)?\.?$/i.test(b));
|
|
609
|
+
}
|
|
610
|
+
|
|
587
611
|
/**
|
|
588
612
|
* Parse the Session section from state.md content.
|
|
589
613
|
* Extracts **Last Date:**, **Stopped At:**, and **Resume File:** bold-field
|
|
@@ -1028,17 +1052,8 @@ function cmdDashboard(cwd, raw) {
|
|
|
1028
1052
|
lastActivity = stateExtractField(stateContent, 'Last Activity');
|
|
1029
1053
|
lastActivityDesc = stateExtractField(stateContent, 'Last Activity Description');
|
|
1030
1054
|
|
|
1031
|
-
// Parse blockers
|
|
1032
|
-
|
|
1033
|
-
if (blockersMatch) {
|
|
1034
|
-
const items = blockersMatch[1].match(/^-\s+(.+)$/gm) || [];
|
|
1035
|
-
for (const item of items) {
|
|
1036
|
-
const text = item.replace(/^-\s+/, '').trim();
|
|
1037
|
-
if (text && !/^none$/i.test(text)) {
|
|
1038
|
-
activeBlockers.push(text);
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1055
|
+
// Parse blockers (the template's `### Blockers/Concerns`, or a legacy `## Blockers`)
|
|
1056
|
+
activeBlockers.push(...activeBlockersFromState(stateContent));
|
|
1042
1057
|
blockerCount = activeBlockers.length;
|
|
1043
1058
|
}
|
|
1044
1059
|
|
|
@@ -1110,6 +1125,7 @@ function cmdDashboard(cwd, raw) {
|
|
|
1110
1125
|
|
|
1111
1126
|
module.exports = {
|
|
1112
1127
|
readStateSafe: safeReadFile,
|
|
1128
|
+
activeBlockersFromState,
|
|
1113
1129
|
stateExtractField,
|
|
1114
1130
|
stateReplaceField,
|
|
1115
1131
|
writeStateMd,
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const {
|
|
8
|
-
PHASES_DIR, PLAN_SUFFIX, SUMMARY_SUFFIX, VERIFICATION_SUFFIX,
|
|
8
|
+
PHASES_DIR, PLAN_SUFFIX, SUMMARY_SUFFIX, VERIFICATION_SUFFIX, ROADMAP_SLICE_SUFFIX,
|
|
9
9
|
SIMPLE_TASK_THRESHOLD, SIMPLE_FILE_THRESHOLD, COMPLEX_TASK_THRESHOLD, COMPLEX_FILE_THRESHOLD,
|
|
10
10
|
} = require('./constants.cjs');
|
|
11
11
|
const { planningPath, phasesPath } = require('./utils.cjs');
|
|
@@ -86,7 +86,7 @@ function cmdTemplateSelect(cwd, planPath, raw) {
|
|
|
86
86
|
* @param {string} phaseName - Human-readable phase name
|
|
87
87
|
* @param {string} phaseSlug - Phase slug for directory naming
|
|
88
88
|
* @param {string} phaseNum - Raw phase number from options
|
|
89
|
-
* @param {Object} options - Additional options (type, wave, fields)
|
|
89
|
+
* @param {Object} options - Additional options (type, wave, fields, slicePath)
|
|
90
90
|
* @returns {{frontmatter: Object, body: string, fileName: string}}
|
|
91
91
|
*/
|
|
92
92
|
function generatePlanTemplate(phaseId, planId, phaseName, phaseSlug, phaseNum, options) {
|
|
@@ -94,6 +94,10 @@ function generatePlanTemplate(phaseId, planId, phaseName, phaseSlug, phaseNum, o
|
|
|
94
94
|
const wave = parseInt(options.wave, 10) || 1;
|
|
95
95
|
const fields = options.fields || {};
|
|
96
96
|
const padded = phaseId.split('-')[0]; // e.g., "01" from "01-setup-auth"
|
|
97
|
+
// The <context> names the phase's roadmap slice, as templates/phase-prompt.md
|
|
98
|
+
// does (O2): `@.planning/roadmap.md` handed every executor the whole roadmap.
|
|
99
|
+
// cmdTemplateFill passes the phase's real directory.
|
|
100
|
+
const slicePath = options.slicePath || `.planning/phases/${phaseId}/${padded}${ROADMAP_SLICE_SUFFIX}`;
|
|
97
101
|
|
|
98
102
|
const frontmatter = {
|
|
99
103
|
phase: phaseId,
|
|
@@ -121,7 +125,7 @@ function generatePlanTemplate(phaseId, planId, phaseName, phaseSlug, phaseNum, o
|
|
|
121
125
|
'',
|
|
122
126
|
'## Context',
|
|
123
127
|
'@.planning/project.md',
|
|
124
|
-
|
|
128
|
+
`@${slicePath}`,
|
|
125
129
|
'@.planning/state.md',
|
|
126
130
|
'',
|
|
127
131
|
'## Tasks',
|
|
@@ -286,7 +290,10 @@ function cmdTemplateFill(cwd, templateType, options, raw) {
|
|
|
286
290
|
generated = generateSummaryTemplate(phaseId, planNum, phaseName, options.phase, fields);
|
|
287
291
|
break;
|
|
288
292
|
case 'plan':
|
|
289
|
-
generated = generatePlanTemplate(phaseId, planNum, phaseName, phaseSlug, options.phase,
|
|
293
|
+
generated = generatePlanTemplate(phaseId, planNum, phaseName, phaseSlug, options.phase, {
|
|
294
|
+
...options,
|
|
295
|
+
slicePath: toPosix(path.join(phaseInfo.directory, `${padded}${ROADMAP_SLICE_SUFFIX}`)),
|
|
296
|
+
});
|
|
290
297
|
break;
|
|
291
298
|
case 'verification':
|
|
292
299
|
generated = generateVerificationTemplate(phaseId, phaseName, options.phase, fields);
|
|
@@ -40,7 +40,9 @@ const KNOWN_VERDICTS = Object.freeze({
|
|
|
40
40
|
'pan-design-checker': Object.freeze({ PASS: 'pass', GAPS: 'fail' }),
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
-
const LIMITS = Object.freeze({ findings: 100, where: 200, summary: 300, verdict: 40, score: 40 });
|
|
43
|
+
const LIMITS = Object.freeze({ findings: 100, where: 200, summary: 300, verdict: 40, score: 40, not_checked: 20, check: 80 });
|
|
44
|
+
// What the verifier's test gate can report (verify-phase.md, run_test_suite).
|
|
45
|
+
const TEST_GATE_VALUES = Object.freeze(['passed', 'failed', 'skipped']);
|
|
44
46
|
const AGENT_RE = /^[a-z][a-z0-9-]{0,63}$/;
|
|
45
47
|
const OPEN_FENCE_RE = /^[ \t]*(`{3,}|~{3,})[ \t]*pan-verdict[ \t]*$/;
|
|
46
48
|
const CLOSE_FENCE_RE = /^[ \t]*(`{3,}|~{3,})[ \t]*$/;
|
|
@@ -195,7 +197,32 @@ function validateVerdict(obj, opts = {}) {
|
|
|
195
197
|
findings.push({ class: cls, severity, where, summary });
|
|
196
198
|
});
|
|
197
199
|
|
|
198
|
-
|
|
200
|
+
const notChecked = normaliseNotChecked(obj.not_checked, warnings);
|
|
201
|
+
return { ok: true, verdict: { contract, agent, outcome, verdict, phase, score, findings, not_checked: notChecked }, warnings };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The checks a judge could not run, each `{check, reason}` (market-ideas M23).
|
|
206
|
+
* A verdict that carries them says what it did not see: a `pass` with tests that
|
|
207
|
+
* never ran is recorded as exactly that, never as a plain pass. Accepts bare
|
|
208
|
+
* strings as checks with no reason; drops entries with no check.
|
|
209
|
+
*/
|
|
210
|
+
function normaliseNotChecked(value, warnings) {
|
|
211
|
+
if (value == null) return [];
|
|
212
|
+
if (!Array.isArray(value)) {
|
|
213
|
+
warnings.push('not_checked_not_an_array');
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
const out = [];
|
|
217
|
+
for (const entry of value.slice(0, LIMITS.not_checked)) {
|
|
218
|
+
const obj = typeof entry === 'string' ? { check: entry } : (entry && typeof entry === 'object' && !Array.isArray(entry) ? entry : null);
|
|
219
|
+
const check = obj ? truncate(cleanText(obj.check ?? obj.gate ?? ''), LIMITS.check) : '';
|
|
220
|
+
if (!check) continue;
|
|
221
|
+
const reason = obj.reason == null || cleanText(obj.reason) === '' ? null : truncate(cleanText(obj.reason), LIMITS.summary);
|
|
222
|
+
out.push({ check, reason });
|
|
223
|
+
}
|
|
224
|
+
if (value.length > LIMITS.not_checked) warnings.push(`not_checked_truncated: ${value.length} reported, ${LIMITS.not_checked} kept`);
|
|
225
|
+
return out;
|
|
199
226
|
}
|
|
200
227
|
|
|
201
228
|
// ─── verification.md frontmatter adapter ─────────────────────────────────────
|
|
@@ -354,7 +381,23 @@ function verdictFromVerificationFrontmatter(text, agent = 'pan-verifier') {
|
|
|
354
381
|
const summary = what || (path ? `unrequested change to ${path}` : '');
|
|
355
382
|
if (summary) findings.push({ class: 'unrequested', severity: 'low', where: path, summary });
|
|
356
383
|
}
|
|
357
|
-
|
|
384
|
+
// The test gate (M23). `not_checked` lists what the verifier could not run; a
|
|
385
|
+
// `test_gate: skipped` with no tests entry there still counts as one, so a model
|
|
386
|
+
// that records the gate but forgets the list cannot turn it into a plain pass.
|
|
387
|
+
let notChecked = listOfMappings(yaml, 'not_checked').map((n) => ({ check: n.check || n.gate || '', reason: n.reason || null }));
|
|
388
|
+
const testGateRaw = frontmatterScalar(yaml, 'test_gate');
|
|
389
|
+
const testGate = testGateRaw ? String(testGateRaw).toLowerCase() : null;
|
|
390
|
+
const isTests = (n) => /test/i.test(String(n.check));
|
|
391
|
+
let contradicted = false;
|
|
392
|
+
if (testGate === 'skipped' && !notChecked.some(isTests)) {
|
|
393
|
+
notChecked.push({ check: 'tests', reason: frontmatterScalar(yaml, 'test_gate_reason') || 'the test gate was skipped' });
|
|
394
|
+
} else if ((testGate === 'passed' || testGate === 'failed') && notChecked.some(isTests)) {
|
|
395
|
+
// The gate ran, so a `tests` entry is the template's example copied over: the
|
|
396
|
+
// recorded gate is the measurement, and the entry goes.
|
|
397
|
+
notChecked = notChecked.filter((n) => !isTests(n));
|
|
398
|
+
contradicted = true;
|
|
399
|
+
}
|
|
400
|
+
return { contract: VERDICT_CONTRACT, agent, verdict: status, outcome, findings, not_checked: notChecked, test_gate: testGate, not_checked_contradicted: contradicted };
|
|
358
401
|
}
|
|
359
402
|
|
|
360
403
|
/**
|
|
@@ -383,7 +426,14 @@ function parseVerdictText(text, opts = {}) {
|
|
|
383
426
|
return { ok: false, error: 'unknown_status', reason: `verification status "${adapted.verdict}" is not passed, gaps_found or human_needed` };
|
|
384
427
|
}
|
|
385
428
|
const r = validateVerdict(adapted, opts);
|
|
386
|
-
|
|
429
|
+
if (!r.ok) return r;
|
|
430
|
+
// Say when the verification does not record its test gate, or records a failed
|
|
431
|
+
// gate under a pass: both make "passed" claim more than the verifier checked.
|
|
432
|
+
if (!adapted.test_gate) r.warnings.push('test_gate_unrecorded: the verification does not say whether the test suite ran');
|
|
433
|
+
else if (!TEST_GATE_VALUES.includes(adapted.test_gate)) r.warnings.push(`test_gate_unknown: "${adapted.test_gate}" is not passed, failed or skipped`);
|
|
434
|
+
else if (adapted.test_gate === 'failed' && adapted.outcome === 'pass') r.warnings.push('test_gate_failed_under_pass: the test gate failed but the status is passed');
|
|
435
|
+
if (adapted.not_checked_contradicted) r.warnings.push(`not_checked_contradicted: test_gate is ${adapted.test_gate}, so the not_checked tests entry was dropped`);
|
|
436
|
+
return { ...r, source_kind: 'frontmatter' };
|
|
387
437
|
}
|
|
388
438
|
return { ok: false, error: 'no_verdict', reason: 'no pan-verdict block and no verification frontmatter status' };
|
|
389
439
|
}
|
|
@@ -13,14 +13,24 @@ const {
|
|
|
13
13
|
const { planningPath } = require('./utils.cjs');
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* The codebase map's conventions document. map-codebase writes `conventions.md`;
|
|
17
|
+
* a map from an older PAN, or one written by hand, may be `CONVENTIONS.md`. Only a
|
|
18
|
+
* case-sensitive filesystem tells the two apart, which is why reading the uppercase
|
|
19
|
+
* name alone found nothing on Linux (prompt audit, market-ideas M34).
|
|
20
|
+
*/
|
|
21
|
+
function readConventions(cwd) {
|
|
22
|
+
const dir = path.join(planningPath(cwd), 'codebase');
|
|
23
|
+
return safeReadFile(path.join(dir, 'conventions.md')) || safeReadFile(path.join(dir, 'CONVENTIONS.md'));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse convention rules from the conventions document's markdown content.
|
|
17
28
|
* Extracts anti-pattern rules from prose containing "instead of", "not", "never".
|
|
18
29
|
* Run drift check internally and return result object (no output).
|
|
19
30
|
* Used by cmdValidateHealth --drift.
|
|
20
31
|
*/
|
|
21
32
|
function runDriftCheck(cwd) {
|
|
22
|
-
const
|
|
23
|
-
const conventionsContent = safeReadFile(conventionsPath);
|
|
33
|
+
const conventionsContent = readConventions(cwd);
|
|
24
34
|
const claudeMdContent = safeReadFile(path.join(cwd, 'CLAUDE.md'));
|
|
25
35
|
const combined = [conventionsContent, claudeMdContent].filter(Boolean).join('\n');
|
|
26
36
|
const rules = parseConventionRules(combined || null);
|
|
@@ -198,8 +208,7 @@ function cmdDriftCheck(cwd, raw, args) {
|
|
|
198
208
|
}
|
|
199
209
|
|
|
200
210
|
// Load convention rules
|
|
201
|
-
const
|
|
202
|
-
const conventionsContent = safeReadFile(conventionsPath);
|
|
211
|
+
const conventionsContent = readConventions(cwd);
|
|
203
212
|
const claudeMdContent = safeReadFile(path.join(cwd, 'CLAUDE.md'));
|
|
204
213
|
const combined = [conventionsContent, claudeMdContent].filter(Boolean).join('\n');
|
|
205
214
|
const rules = parseConventionRules(combined || null);
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const { safeReadFile, execGit, findPhaseInternal, output, normalizePhaseName } = require('./core.cjs');
|
|
10
|
-
const { readStateSafe } = require('./state.cjs');
|
|
10
|
+
const { readStateSafe, activeBlockersFromState } = require('./state.cjs');
|
|
11
11
|
const {
|
|
12
12
|
STATE_FILE, ROADMAP_FILE, CONFIG_FILE, PATTERNS_FILE, PHASE_DIR_RE,
|
|
13
13
|
} = require('./constants.cjs');
|
|
@@ -44,19 +44,10 @@ function cmdPreflight(cwd, target, raw) {
|
|
|
44
44
|
blockers.push('state.md not found — run /pan:new-project');
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
// Check 3: no unresolved blockers in state.md
|
|
47
|
+
// Check 3: no unresolved blockers in state.md (the template's `### Blockers/Concerns`,
|
|
48
|
+
// where `state add-blocker` writes, or a legacy `## Blockers`)
|
|
48
49
|
if (stateContent) {
|
|
49
|
-
const
|
|
50
|
-
const activeBlockers = [];
|
|
51
|
-
if (blockersMatch) {
|
|
52
|
-
const items = blockersMatch[1].match(/^-\s+(.+)$/gm) || [];
|
|
53
|
-
for (const item of items) {
|
|
54
|
-
const text = item.replace(/^-\s+/, '').trim();
|
|
55
|
-
if (text && !/^none$/i.test(text)) {
|
|
56
|
-
activeBlockers.push(text);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
50
|
+
const activeBlockers = activeBlockersFromState(stateContent);
|
|
60
51
|
if (activeBlockers.length === 0) {
|
|
61
52
|
checks.push({ name: 'no_blockers', passed: true });
|
|
62
53
|
} else {
|
|
@@ -122,9 +122,11 @@ function cmdRetro(cwd, raw, args) {
|
|
|
122
122
|
common_gap_patterns: gapGroups,
|
|
123
123
|
};
|
|
124
124
|
|
|
125
|
-
// E-4: optional memory write. Top gap patterns become lessons
|
|
126
|
-
// (they surface what plans routinely miss)
|
|
127
|
-
// pan-verifier
|
|
125
|
+
// E-4: optional memory write. Top gap patterns become lessons in pan-planner's
|
|
126
|
+
// log (they surface what plans routinely miss), first-try rate deltas in
|
|
127
|
+
// pan-verifier's. Stored for /pan:knowledge: PAN's workflows no longer load agent
|
|
128
|
+
// memory into agents (ADR-0036, amended 2026-10-04), and /pan:army no longer
|
|
129
|
+
// passes the flag.
|
|
128
130
|
const argsList = Array.isArray(args) ? args : [];
|
|
129
131
|
if (argsList.includes('--write-memory')) {
|
|
130
132
|
const { appendMemory } = require('./memory.cjs');
|
|
@@ -18,7 +18,7 @@ const fs = require('fs');
|
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const { safeReadFile, execGit, findPhaseInternal, toPosix, output, escapeRegex } = require('./core.cjs');
|
|
20
20
|
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
21
|
-
const { isPlanFile, isSummaryFile } = require('./constants.cjs');
|
|
21
|
+
const { isPlanFile, isSummaryFile, isVerificationFile } = require('./constants.cjs');
|
|
22
22
|
const { planningRel } = require('./utils.cjs');
|
|
23
23
|
|
|
24
24
|
const LOCKFILES = new Set([
|
|
@@ -27,8 +27,11 @@ const LOCKFILES = new Set([
|
|
|
27
27
|
]);
|
|
28
28
|
// PAN's own installed trees: a phase commit that touches them is PAN bookkeeping,
|
|
29
29
|
// never the project's scope (pan-reviewer skips them for the same reason).
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
// A Copilot install shares `.github/` with the project: only what the installer writes as
|
|
31
|
+
// its own is listed, so the project's workflows, hooks and Copilot instructions stay in scope,
|
|
32
|
+
// and so do the files PAN merges into (`mcp.json`, `copilot/settings.json`).
|
|
33
|
+
const PAN_OWNED_PREFIXES = ['.claude/', '.codex/', '.gemini/', '.opencode/', '.agents/', '.github/pan-wizard-core/', '.github/pan-local-patches/', '.github/agents/pan-', '.github/skills/pan-', '.github/hooks/pan-'];
|
|
34
|
+
const PAN_OWNED_FILES = new Set(['.github/hooks/pan.json', '.github/package.json', '.github/pan-file-manifest.json']);
|
|
32
35
|
const TEST_PATH_RE = /(^|\/)(tests?|__tests__|specs?)\//i;
|
|
33
36
|
const TEST_FILE_RE = /\.(test|spec)\.[a-z0-9]+$/i;
|
|
34
37
|
const COMMIT_SCAN_LIMIT = 2000;
|
|
@@ -153,4 +156,71 @@ function cmdVerifyScope(cwd, phase, raw) {
|
|
|
153
156
|
output(r, raw, r.error ? undefined : String(r.candidates.length));
|
|
154
157
|
}
|
|
155
158
|
|
|
156
|
-
|
|
159
|
+
// ─── Stale verification (market-ideas M27) ───────────────────────────────────
|
|
160
|
+
// A verification describes the code at one commit. Until 2026-10-03 nothing tied it
|
|
161
|
+
// to that commit, so a phase edited after it passed still read as verified. The
|
|
162
|
+
// verifier now records `verified_commit`; any file the phase covers that changed
|
|
163
|
+
// since then, committed or not, makes the verification stale.
|
|
164
|
+
|
|
165
|
+
const SHA_RE = /^[0-9a-f]{7,40}$/i;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The files a phase's verification covered: the plans' declared `files_modified` and
|
|
169
|
+
* the summaries' `key-files`, minus the planning tree, lockfiles and PAN's runtime
|
|
170
|
+
* directories. No git log scan, so `progress` can afford it for every phase.
|
|
171
|
+
*/
|
|
172
|
+
function coveredFiles(cwd, info) {
|
|
173
|
+
const dir = path.join(cwd, info.directory);
|
|
174
|
+
const files = new Set();
|
|
175
|
+
for (const f of info.plans.filter(isPlanFile)) {
|
|
176
|
+
const fm = extractFrontmatter(safeReadFile(path.join(dir, f)) || '');
|
|
177
|
+
for (const p of asList(fm.files_modified || fm['files-modified'])) files.add(normalisePath(p));
|
|
178
|
+
}
|
|
179
|
+
for (const f of info.summaries.filter(isSummaryFile)) {
|
|
180
|
+
const fm = extractFrontmatter(safeReadFile(path.join(dir, f)) || '');
|
|
181
|
+
const kf = fm['key-files'] || fm.key_files || {};
|
|
182
|
+
for (const p of [...asList(kf.created), ...asList(kf.modified)]) files.add(normalisePath(p));
|
|
183
|
+
}
|
|
184
|
+
const planningPrefix = normalisePath(planningRel());
|
|
185
|
+
return [...files].filter((f) => f && !exclusionReason(f, planningPrefix)).sort();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Whether a phase's verification still describes the code.
|
|
190
|
+
* @param {string} cwd
|
|
191
|
+
* @param {string} phase
|
|
192
|
+
* @param {Object} [info] - findPhaseInternal's result, when the caller already has it
|
|
193
|
+
* @returns {Object} `{phase, state, verified_commit, verification, changed_since, reason?}` or `{error, reason}`.
|
|
194
|
+
* state: `fresh` · `stale` (covered files changed since the verified commit) ·
|
|
195
|
+
* `unverified` (no verification file) · `unknown` (no usable verified_commit, or no git)
|
|
196
|
+
*/
|
|
197
|
+
function verificationFreshness(cwd, phase, info) {
|
|
198
|
+
info = info || findPhaseInternal(cwd, phase);
|
|
199
|
+
if (!info) return { error: 'phase_not_found', reason: `no phase directory for phase ${phase}` };
|
|
200
|
+
const dir = path.join(cwd, info.directory);
|
|
201
|
+
let names = [];
|
|
202
|
+
try { names = fs.readdirSync(dir); } catch { /* unreadable: no verification */ }
|
|
203
|
+
const verification = names.filter(isVerificationFile).sort().pop() || null;
|
|
204
|
+
const base = { phase: info.phase_number, state: 'unverified', verified_commit: null, verification: verification ? toPosix(path.join(info.directory, verification)) : null, changed_since: [] };
|
|
205
|
+
if (!verification) return base;
|
|
206
|
+
const fm = extractFrontmatter(safeReadFile(path.join(dir, verification)) || '');
|
|
207
|
+
const sha = String(fm.verified_commit || '').trim();
|
|
208
|
+
if (!SHA_RE.test(sha)) return { ...base, state: 'unknown', reason: 'the verification records no verified_commit' };
|
|
209
|
+
const known = execGit(cwd, ['cat-file', '-e', `${sha}^{commit}`]);
|
|
210
|
+
if (known.exitCode !== 0) return { ...base, state: 'unknown', verified_commit: sha, reason: 'the verified commit is not in this repository (rebased, squashed, or no git)' };
|
|
211
|
+
const covered = coveredFiles(cwd, info);
|
|
212
|
+
if (!covered.length) return { ...base, state: 'unknown', verified_commit: sha, reason: 'the phase declares no files to compare' };
|
|
213
|
+
// Against the working tree, not HEAD: an uncommitted edit makes the pass stale too.
|
|
214
|
+
const diff = execGit(cwd, ['-c', 'core.quotepath=off', 'diff', '--name-only', '--relative', sha, '--', ...covered]);
|
|
215
|
+
if (diff.exitCode !== 0) return { ...base, state: 'unknown', verified_commit: sha, reason: 'git diff failed' };
|
|
216
|
+
const changed = diff.stdout.split('\n').map(normalisePath).filter(Boolean).sort();
|
|
217
|
+
return { ...base, state: changed.length ? 'stale' : 'fresh', verified_commit: sha, changed_since: changed };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function cmdVerifyStale(cwd, phase, raw) {
|
|
221
|
+
if (!phase) { output({ error: 'phase_required', reason: 'Usage: verify stale <phase>' }, raw); return; }
|
|
222
|
+
const r = verificationFreshness(cwd, phase);
|
|
223
|
+
output(r, raw, r.error ? undefined : r.state);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = { scopePhase, cmdVerifyScope, verificationFreshness, cmdVerifyStale, coveredFiles, LOCKFILES };
|