pan-wizard 3.21.1 → 3.24.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 +38 -24
- package/agents/pan-conductor.md +3 -3
- package/agents/pan-design-checker.md +83 -0
- package/agents/pan-designer.md +67 -0
- package/agents/pan-document_code.md +3 -2
- package/agents/pan-executor.md +2 -2
- package/agents/pan-plan-checker.md +19 -6
- package/agents/pan-planner.md +4 -1
- package/agents/pan-previewer.md +1 -1
- package/agents/pan-roadmapper.md +3 -1
- package/agents/pan-verifier.md +10 -10
- package/bin/install-lib.cjs +128 -17
- package/bin/install.js +480 -74
- package/commands/pan/army.md +20 -14
- package/commands/pan/audit-deployment.md +17 -14
- package/commands/pan/cost.md +2 -2
- package/commands/pan/debug.md +1 -1
- package/commands/pan/design-phase.md +77 -0
- package/commands/pan/exec-phase.md +2 -2
- package/commands/pan/experiment.md +18 -14
- package/commands/pan/focus-auto.md +5 -5
- package/commands/pan/focus-design.md +13 -1
- package/commands/pan/focus-exec.md +1 -1
- package/commands/pan/learn.md +1 -1
- package/commands/pan/links.md +3 -1
- package/commands/pan/map-codebase.md +4 -4
- package/commands/pan/mcp-bridge.md +1 -1
- package/commands/pan/milestone-new.md +3 -1
- package/commands/pan/optimize.md +2 -2
- package/commands/pan/patches.md +1 -1
- package/commands/pan/plan-phase.md +3 -1
- package/commands/pan/preview.md +3 -3
- package/commands/pan/profile.md +3 -3
- package/commands/pan/research-phase.md +1 -1
- package/commands/pan/retro.md +4 -1
- package/commands/pan/review-deep.md +1 -1
- package/commands/pan/settings.md +2 -2
- package/commands/pan/what-if.md +1 -1
- package/hooks/dist/pan-check-update.js +149 -38
- package/hooks/dist/pan-context-monitor.js +155 -81
- package/hooks/dist/pan-cost-logger.js +288 -13
- package/hooks/dist/pan-statusline.js +24 -8
- package/hooks/dist/pan-stop-guard.js +160 -0
- package/hooks/dist/pan-trace-logger.js +261 -19
- package/package.json +2 -2
- package/pan-wizard-core/bin/lib/bridge.cjs +1 -1
- package/pan-wizard-core/bin/lib/bus.cjs +19 -4
- package/pan-wizard-core/bin/lib/campaign.cjs +8 -3
- package/pan-wizard-core/bin/lib/codebase.cjs +15 -5
- package/pan-wizard-core/bin/lib/commands.cjs +88 -6
- package/pan-wizard-core/bin/lib/config.cjs +27 -2
- package/pan-wizard-core/bin/lib/constants.cjs +1 -0
- package/pan-wizard-core/bin/lib/core.cjs +113 -8
- package/pan-wizard-core/bin/lib/cost.cjs +61 -24
- package/pan-wizard-core/bin/lib/distill.cjs +22 -4
- package/pan-wizard-core/bin/lib/doc-lint.cjs +25 -7
- package/pan-wizard-core/bin/lib/experiment.cjs +25 -1
- package/pan-wizard-core/bin/lib/focus.cjs +42 -7
- package/pan-wizard-core/bin/lib/frontmatter.cjs +62 -31
- package/pan-wizard-core/bin/lib/git.cjs +51 -16
- package/pan-wizard-core/bin/lib/hud.cjs +17 -2
- package/pan-wizard-core/bin/lib/init.cjs +2 -5
- package/pan-wizard-core/bin/lib/knowledge.cjs +20 -1
- package/pan-wizard-core/bin/lib/learn-lint.cjs +50 -0
- package/pan-wizard-core/bin/lib/links.cjs +9 -6
- package/pan-wizard-core/bin/lib/lock.cjs +23 -4
- package/pan-wizard-core/bin/lib/memory-optimize.cjs +101 -17
- package/pan-wizard-core/bin/lib/memory-rebuild.cjs +26 -2
- package/pan-wizard-core/bin/lib/milestone.cjs +11 -5
- package/pan-wizard-core/bin/lib/optimize.cjs +18 -7
- package/pan-wizard-core/bin/lib/phase-remove.cjs +41 -2
- package/pan-wizard-core/bin/lib/phase.cjs +82 -11
- package/pan-wizard-core/bin/lib/preview.cjs +23 -4
- package/pan-wizard-core/bin/lib/review-deep.cjs +14 -6
- package/pan-wizard-core/bin/lib/roadmap.cjs +6 -2
- package/pan-wizard-core/bin/lib/runner.cjs +17 -7
- package/pan-wizard-core/bin/lib/squads.cjs +43 -11
- package/pan-wizard-core/bin/lib/state.cjs +32 -11
- package/pan-wizard-core/bin/lib/template.cjs +8 -3
- package/pan-wizard-core/bin/lib/verify-deploy.cjs +14 -2
- package/pan-wizard-core/bin/lib/verify-drift.cjs +15 -1
- package/pan-wizard-core/bin/lib/verify-preflight.cjs +9 -3
- package/pan-wizard-core/bin/lib/verify.cjs +84 -39
- package/pan-wizard-core/bin/lib/whatif.cjs +8 -1
- package/pan-wizard-core/bin/pan-tools.cjs +42 -19
- package/pan-wizard-core/learnings/index.json +7 -7
- package/pan-wizard-core/learnings/universal/autonomous-loop.md +5 -3
- package/pan-wizard-core/learnings/universal/concurrency.md +1 -1
- package/pan-wizard-core/references/design-methodology.md +94 -0
- package/pan-wizard-core/references/git-integration.md +9 -9
- package/pan-wizard-core/references/guardrails.md +4 -1
- package/pan-wizard-core/references/model-profile-resolution.md +1 -1
- package/pan-wizard-core/references/model-profiles.md +44 -34
- package/pan-wizard-core/references/planning-config.md +5 -5
- package/pan-wizard-core/references/verification-patterns.md +67 -0
- package/pan-wizard-core/templates/codebase/architecture.md +1 -1
- package/pan-wizard-core/templates/codebase/concerns.md +1 -1
- package/pan-wizard-core/templates/codebase/conventions.md +1 -1
- package/pan-wizard-core/templates/codebase/integrations.md +1 -1
- package/pan-wizard-core/templates/codebase/stack.md +1 -1
- package/pan-wizard-core/templates/codebase/structure.md +1 -1
- package/pan-wizard-core/templates/codebase/testing.md +1 -1
- package/pan-wizard-core/templates/design.md +146 -0
- package/pan-wizard-core/templates/discovery.md +3 -3
- package/pan-wizard-core/templates/milestone-archive.md +2 -2
- package/pan-wizard-core/templates/playbook.md +1 -1
- package/pan-wizard-core/templates/uat.md +1 -1
- package/pan-wizard-core/workflows/diagnose-issues.md +9 -7
- package/pan-wizard-core/workflows/exec-phase.md +28 -8
- package/pan-wizard-core/workflows/execute-plan.md +2 -3
- package/pan-wizard-core/workflows/health.md +24 -1
- package/pan-wizard-core/workflows/help.md +5 -4
- package/pan-wizard-core/workflows/learn.md +5 -5
- package/pan-wizard-core/workflows/map-codebase.md +1 -1
- package/pan-wizard-core/workflows/milestone-audit.md +1 -1
- package/pan-wizard-core/workflows/milestone-new.md +4 -4
- package/pan-wizard-core/workflows/new-project.md +47 -32
- package/pan-wizard-core/workflows/optimize.md +8 -8
- package/pan-wizard-core/workflows/pause.md +1 -1
- package/pan-wizard-core/workflows/phase-tests.md +2 -2
- package/pan-wizard-core/workflows/plan-phase.md +4 -0
- package/pan-wizard-core/workflows/profile.md +13 -10
- package/pan-wizard-core/workflows/research-phase.md +1 -1
- package/pan-wizard-core/workflows/resume-project.md +1 -1
- package/pan-wizard-core/workflows/settings.md +9 -9
- package/pan-wizard-core/workflows/transition.md +24 -8
- package/pan-wizard-core/workflows/update.md +23 -20
- package/pan-wizard-core/workflows/verify-phase.md +7 -2
- package/pan-zcode/KNOWN-BETA-RISKS.md +8 -0
- package/pan-zcode/README.md +14 -2
- package/pan-zcode/bin/install-zcode.js +1 -1
- package/pan-zcode/mcp/native-tools.cjs +1 -1
- package/pan-zcode/mcp/server.cjs +76 -14
- package/scripts/build-hooks.js +2 -1
- package/scripts/generate-skills-docs.py +0 -4
- package/scripts/run-tests.cjs +11 -4
|
@@ -195,11 +195,16 @@ function cmdStateUpdate(cwd, field, value) {
|
|
|
195
195
|
writeStateMd(statePath, content, cwd);
|
|
196
196
|
output({ updated: true });
|
|
197
197
|
} else {
|
|
198
|
-
|
|
198
|
+
// error key ⇒ exit 1: the caller asked for a field update that did not
|
|
199
|
+
// happen, so state.md does not say what the caller now believes it says.
|
|
200
|
+
output({ updated: false, reason: `Field "${field}" not found in state.md`, error: 'field_not_found' });
|
|
199
201
|
}
|
|
200
202
|
} catch {
|
|
201
203
|
// state.md does not exist or is unreadable -- report gracefully
|
|
202
|
-
|
|
204
|
+
// Same condition `state json` reports as {error:'state.md not found'} at exit 1.
|
|
205
|
+
// Reporting it as a bare `updated: false` was the identical defect wearing a
|
|
206
|
+
// different key — one file, one missing file, two exit codes.
|
|
207
|
+
output({ updated: false, reason: 'state.md not found', error: 'state_not_found' });
|
|
203
208
|
}
|
|
204
209
|
}
|
|
205
210
|
|
|
@@ -256,6 +261,9 @@ function cmdStateAdvancePlan(cwd, raw) {
|
|
|
256
261
|
content = stateReplaceField(content, 'Status', 'Phase complete — ready for verification') || content;
|
|
257
262
|
content = stateReplaceField(content, 'Last Activity', today) || content;
|
|
258
263
|
writeStateMd(statePath, content, cwd);
|
|
264
|
+
// No error key, exit 0: `last_plan` is the NORMAL end-of-phase signal — the
|
|
265
|
+
// phase advanced to "ready for verification" and state.md was written. Gating on
|
|
266
|
+
// it would make every completed phase look like a failure.
|
|
259
267
|
output({ advanced: false, reason: 'last_plan', current_plan: currentPlan, total_plans: totalPlans, status: 'ready_for_verification' }, raw, 'false');
|
|
260
268
|
} else {
|
|
261
269
|
const newPlan = currentPlan + 1;
|
|
@@ -303,7 +311,7 @@ function cmdStateRecordMetric(cwd, options, raw) {
|
|
|
303
311
|
writeStateMd(statePath, content, cwd);
|
|
304
312
|
output({ recorded: true, phase, plan, duration }, raw, 'true');
|
|
305
313
|
} else {
|
|
306
|
-
output({ recorded: false, reason: 'Performance Metrics section not found in state.md' }, raw, 'false');
|
|
314
|
+
output({ recorded: false, reason: 'Performance Metrics section not found in state.md', error: 'metrics_section_missing' }, raw, 'false');
|
|
307
315
|
}
|
|
308
316
|
}
|
|
309
317
|
|
|
@@ -345,7 +353,7 @@ function cmdStateUpdateProgress(cwd, raw) {
|
|
|
345
353
|
writeStateMd(statePath, content, cwd);
|
|
346
354
|
output({ updated: true, percent, completed: totalSummaries, total: totalPlans, bar: progressStr }, raw, progressStr);
|
|
347
355
|
} else {
|
|
348
|
-
output({ updated: false, reason: 'Progress field not found in state.md' }, raw, 'false');
|
|
356
|
+
output({ updated: false, reason: 'Progress field not found in state.md', error: 'progress_field_missing' }, raw, 'false');
|
|
349
357
|
}
|
|
350
358
|
}
|
|
351
359
|
|
|
@@ -369,7 +377,7 @@ function cmdStateAddDecision(cwd, options, raw) {
|
|
|
369
377
|
summaryText = readTextArgOrFile(cwd, summary, summary_file, 'summary');
|
|
370
378
|
rationaleText = readTextArgOrFile(cwd, rationale || '', rationale_file, 'rationale');
|
|
371
379
|
} catch (err) {
|
|
372
|
-
output({ added: false, reason: err.message }, raw, 'false');
|
|
380
|
+
output({ added: false, reason: err.message, error: err.message || 'decision_read_failed' }, raw, 'false');
|
|
373
381
|
return;
|
|
374
382
|
}
|
|
375
383
|
|
|
@@ -389,7 +397,7 @@ function cmdStateAddDecision(cwd, options, raw) {
|
|
|
389
397
|
writeStateMd(statePath, content, cwd);
|
|
390
398
|
output({ added: true, decision: entry }, raw, 'true');
|
|
391
399
|
} else {
|
|
392
|
-
output({ added: false, reason: 'Decisions section not found in state.md' }, raw, 'false');
|
|
400
|
+
output({ added: false, reason: 'Decisions section not found in state.md', error: 'decisions_section_missing' }, raw, 'false');
|
|
393
401
|
}
|
|
394
402
|
}
|
|
395
403
|
|
|
@@ -410,7 +418,7 @@ function cmdStateAddBlocker(cwd, text, raw) {
|
|
|
410
418
|
try {
|
|
411
419
|
blockerText = readTextArgOrFile(cwd, blockerOptions.text, blockerOptions.text_file, 'blocker');
|
|
412
420
|
} catch (err) {
|
|
413
|
-
output({ added: false, reason: err.message }, raw, 'false');
|
|
421
|
+
output({ added: false, reason: err.message, error: err.message || 'blocker_read_failed' }, raw, 'false');
|
|
414
422
|
return;
|
|
415
423
|
}
|
|
416
424
|
|
|
@@ -428,7 +436,7 @@ function cmdStateAddBlocker(cwd, text, raw) {
|
|
|
428
436
|
writeStateMd(statePath, content, cwd);
|
|
429
437
|
output({ added: true, blocker: blockerText }, raw, 'true');
|
|
430
438
|
} else {
|
|
431
|
-
output({ added: false, reason: 'Blockers section not found in state.md' }, raw, 'false');
|
|
439
|
+
output({ added: false, reason: 'Blockers section not found in state.md', error: 'blockers_section_missing' }, raw, 'false');
|
|
432
440
|
}
|
|
433
441
|
}
|
|
434
442
|
|
|
@@ -456,6 +464,17 @@ function cmdStateResolveBlocker(cwd, text, raw) {
|
|
|
456
464
|
return !line.toLowerCase().includes(text.toLowerCase());
|
|
457
465
|
});
|
|
458
466
|
|
|
467
|
+
// M27: if nothing was removed, no blocker matched the given text. Report
|
|
468
|
+
// resolved:false rather than falsely claiming a resolution (and skip the
|
|
469
|
+
// write entirely — the file is unchanged).
|
|
470
|
+
if (filtered.length === lines.length) {
|
|
471
|
+
// error key ⇒ exit 1. M27 stopped this from falsely claiming a resolution;
|
|
472
|
+
// the exit code finishes the job. The blocker is STILL OPEN and the caller
|
|
473
|
+
// that asked to clear it would otherwise carry on believing it is gone.
|
|
474
|
+
output({ resolved: false, reason: 'no matching blocker', blocker: text, error: 'blocker_not_matched' }, raw, 'false');
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
459
478
|
let newBody = filtered.join('\n');
|
|
460
479
|
// If section is now empty, add placeholder
|
|
461
480
|
if (!newBody.trim() || !newBody.includes('- ')) {
|
|
@@ -466,7 +485,7 @@ function cmdStateResolveBlocker(cwd, text, raw) {
|
|
|
466
485
|
writeStateMd(statePath, content, cwd);
|
|
467
486
|
output({ resolved: true, blocker: text }, raw, 'true');
|
|
468
487
|
} else {
|
|
469
|
-
output({ resolved: false, reason: 'Blockers section not found in state.md' }, raw, 'false');
|
|
488
|
+
output({ resolved: false, reason: 'Blockers section not found in state.md', error: 'blockers_section_missing' }, raw, 'false');
|
|
470
489
|
}
|
|
471
490
|
}
|
|
472
491
|
|
|
@@ -510,7 +529,7 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
510
529
|
try { require('./memory-optimize.cjs').maybeAutoOptimizeMemory(cwd); } catch { /* best-effort */ }
|
|
511
530
|
output({ recorded: true, updated }, raw, 'true');
|
|
512
531
|
} else {
|
|
513
|
-
output({ recorded: false, reason: 'No session fields found in state.md' }, raw, 'false');
|
|
532
|
+
output({ recorded: false, reason: 'No session fields found in state.md', error: 'session_fields_missing' }, raw, 'false');
|
|
514
533
|
}
|
|
515
534
|
}
|
|
516
535
|
|
|
@@ -1012,7 +1031,9 @@ function cmdDashboard(cwd, raw) {
|
|
|
1012
1031
|
lines.push(`Blockers: ${blockerCount}`);
|
|
1013
1032
|
if (lastActivity) lines.push(`Last Activity: ${lastActivity}${lastActivityDesc ? ' — ' + lastActivityDesc : ''}`);
|
|
1014
1033
|
if (nextPhase) lines.push(`Next Phase: ${nextPhase.number} — ${nextPhase.name}`);
|
|
1015
|
-
output(
|
|
1034
|
+
// raw=true so output() emits the human summary; passing false discarded it
|
|
1035
|
+
// and printed JSON, making the whole lines[] block dead (M28, ADR audit 2026-08).
|
|
1036
|
+
output(result, true, lines.join('\n'));
|
|
1016
1037
|
return;
|
|
1017
1038
|
}
|
|
1018
1039
|
|
|
@@ -9,7 +9,7 @@ const {
|
|
|
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');
|
|
12
|
-
const { normalizePhaseName, findPhaseInternal, generateSlugInternal, toPosix, output, error } = require('./core.cjs');
|
|
12
|
+
const { normalizePhaseName, findPhaseInternal, generateSlugInternal, toPosix, output, EXIT_OK, error } = require('./core.cjs');
|
|
13
13
|
const { reconstructFrontmatter } = require('./frontmatter.cjs');
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -64,8 +64,13 @@ function cmdTemplateSelect(cwd, planPath, raw) {
|
|
|
64
64
|
const result = { template, type, taskCount, fileCount, hasDecisions };
|
|
65
65
|
output(result, raw, template);
|
|
66
66
|
} catch (err) {
|
|
67
|
-
// Fallback to standard template on any read/parse error
|
|
68
|
-
|
|
67
|
+
// Fallback to standard template on any read/parse error.
|
|
68
|
+
// EXIT_OK: this is a RESULT, not a failure — the command answered the question
|
|
69
|
+
// it was asked ("which summary template?") with the documented default, and
|
|
70
|
+
// `error` here is only the reason detection fell back. Callers use it as
|
|
71
|
+
// `TEMPLATE=$(pan-tools template select … --raw)`; exiting non-zero would
|
|
72
|
+
// report a failure for a command that produced a usable answer.
|
|
73
|
+
output({ template: 'templates/summary-standard.md', type: 'standard', error: err.message }, raw, 'templates/summary-standard.md', EXIT_OK);
|
|
69
74
|
}
|
|
70
75
|
}
|
|
71
76
|
|
|
@@ -90,8 +90,20 @@ function validateRuntimeInstall(cwd, configDir, runtime) {
|
|
|
90
90
|
if (hooks && typeof hooks === 'object') {
|
|
91
91
|
for (const hookArr of Object.values(hooks)) {
|
|
92
92
|
if (!Array.isArray(hookArr)) continue;
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
// M29: PAN installs hooks in the nested Claude shape — each event maps to
|
|
94
|
+
// an array of GROUPS, and each group holds the real commands under
|
|
95
|
+
// group.hooks[]: { matcher, hooks: [{ type, command }] }. The old code
|
|
96
|
+
// only read group.command (undefined in that shape), so it validated
|
|
97
|
+
// nothing for PAN's own hooks. Collect both the flat group.command (other
|
|
98
|
+
// runtimes) AND every command nested in group.hooks[].
|
|
99
|
+
for (const group of hookArr) {
|
|
100
|
+
if (!group || typeof group !== 'object') continue;
|
|
101
|
+
if (group.command) hookCommands.push(group.command);
|
|
102
|
+
if (Array.isArray(group.hooks)) {
|
|
103
|
+
for (const h of group.hooks) {
|
|
104
|
+
if (h && h.command) hookCommands.push(h.command);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
95
107
|
}
|
|
96
108
|
}
|
|
97
109
|
}
|
|
@@ -135,7 +135,7 @@ function calculateDriftScore(violations, filesChecked, rulesCount) {
|
|
|
135
135
|
* @param {string} [sinceRef] - Git ref to diff against (default: HEAD)
|
|
136
136
|
* @returns {string[]} Array of relative file paths
|
|
137
137
|
*/
|
|
138
|
-
function getChangedFiles(cwd, sinceRef) {
|
|
138
|
+
function getChangedFiles(cwd, sinceRef, opts = {}) {
|
|
139
139
|
const ref = sinceRef || 'HEAD';
|
|
140
140
|
// Try staged + unstaged first, then diff against ref
|
|
141
141
|
const result = execGit(cwd, ['diff', '--name-only', ref]);
|
|
@@ -150,6 +150,20 @@ function getChangedFiles(cwd, sinceRef) {
|
|
|
150
150
|
if (line.trim()) allFiles.add(line.trim());
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
|
+
// Untracked files are OPT-IN because the two consumers want different things.
|
|
154
|
+
// Drift detection asks "what changed against the baseline", where an untracked
|
|
155
|
+
// scratch file is noise. The stub gate asks "is any code in this handoff a stub",
|
|
156
|
+
// and a brand-new file is precisely the normal shape of PAN execution — a plan
|
|
157
|
+
// creates files, a later step commits them — so during that window `git diff` lists
|
|
158
|
+
// nothing and a fully stubbed new module sailed through the gate.
|
|
159
|
+
if (opts.includeUntracked) {
|
|
160
|
+
const untracked = execGit(cwd, ['ls-files', '--others', '--exclude-standard']);
|
|
161
|
+
if (untracked.exitCode === 0) {
|
|
162
|
+
for (const line of untracked.stdout.split(/\r?\n/)) {
|
|
163
|
+
if (line.trim()) allFiles.add(line.trim());
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
153
167
|
// Filter out binary extensions and limit
|
|
154
168
|
const filtered = [];
|
|
155
169
|
for (const f of allFiles) {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
|
-
const { safeReadFile, execGit, findPhaseInternal, output } = require('./core.cjs');
|
|
9
|
+
const { safeReadFile, execGit, findPhaseInternal, output, normalizePhaseName } = require('./core.cjs');
|
|
10
10
|
const { readStateSafe } = require('./state.cjs');
|
|
11
11
|
const {
|
|
12
12
|
STATE_FILE, ROADMAP_FILE, CONFIG_FILE, PATTERNS_FILE, PHASE_DIR_RE,
|
|
@@ -162,7 +162,13 @@ function cmdDepsValidate(cwd, raw) {
|
|
|
162
162
|
const headerRe = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
|
|
163
163
|
let match;
|
|
164
164
|
while ((match = headerRe.exec(roadmapContent)) !== null) {
|
|
165
|
-
|
|
165
|
+
// Key both maps by the NORMALIZED number. Roadmap headings are unpadded
|
|
166
|
+
// ("Phase 1") and directory names are zero-padded ("01-foundation"), so
|
|
167
|
+
// keying on the raw values made every phase mismatch in BOTH directions:
|
|
168
|
+
// `deps validate` reported "in roadmap but no directory on disk" for phases
|
|
169
|
+
// whose directory was right there, plus an "orphaned directory" warning for
|
|
170
|
+
// the same phase — inventing errors on a roadmap in PAN's own template shape.
|
|
171
|
+
roadmapPhases.set(normalizePhaseName(match[1]), match[2].trim());
|
|
166
172
|
}
|
|
167
173
|
} else {
|
|
168
174
|
issues.push({ type: 'warning', message: 'roadmap.md not found' });
|
|
@@ -176,7 +182,7 @@ function cmdDepsValidate(cwd, raw) {
|
|
|
176
182
|
if (entry.isDirectory()) {
|
|
177
183
|
const dirMatch = entry.name.match(PHASE_DIR_RE);
|
|
178
184
|
if (dirMatch) {
|
|
179
|
-
diskPhases.set(dirMatch[1], entry.name);
|
|
185
|
+
diskPhases.set(normalizePhaseName(dirMatch[1]), entry.name);
|
|
180
186
|
}
|
|
181
187
|
}
|
|
182
188
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const { execFileSync } = require('child_process');
|
|
8
|
-
const { safeReadFile, normalizePhaseName, execGit, findPhaseInternal, getMilestoneInfo, toPosix, output, error, escapeRegex } = require('./core.cjs');
|
|
8
|
+
const { safeReadFile, normalizePhaseName, comparePhaseNum, execGit, findPhaseInternal, getMilestoneInfo, toPosix, output, EXIT_OK, error, escapeRegex } = require('./core.cjs');
|
|
9
9
|
const { extractFrontmatter, parseMustHavesBlock } = require('./frontmatter.cjs');
|
|
10
10
|
const { writeStateMd, readStateSafe } = require('./state.cjs');
|
|
11
11
|
const {
|
|
@@ -362,7 +362,17 @@ function cmdVerifyArtifacts(cwd, planFilePath, raw) {
|
|
|
362
362
|
if (!content) { output({ error: 'File not found', path: planFilePath }, raw); return; }
|
|
363
363
|
const r = checkArtifacts(cwd, content);
|
|
364
364
|
if (r.total === 0) {
|
|
365
|
-
|
|
365
|
+
// EXIT_OK: "nothing declared to check" is a RESULT, not a failure. Two pieces of
|
|
366
|
+
// shipped evidence: (1) the plan template ships `must_haves: { truths: [],
|
|
367
|
+
// artifacts: [], key_links: [] }` (pan-wizard-core/templates/phase-prompt.md,
|
|
368
|
+
// template.cjs generatePlanTemplate), so an empty block is the documented default
|
|
369
|
+
// state of a scaffolded plan; presence of the block is enforced by
|
|
370
|
+
// `verify plan-structure`, not here. (2) parseMustHavesBlock currently matches the
|
|
371
|
+
// block header at 4-space indent while every shipped template and agent emits it at
|
|
372
|
+
// 2 — so this branch is reached for *every* plan authored in PAN's own format, and
|
|
373
|
+
// exiting non-zero would fail every real `verify artifacts` call. Reclassifying
|
|
374
|
+
// this as a failure requires fixing that indentation mismatch first.
|
|
375
|
+
output({ error: 'No must_haves.artifacts found in frontmatter', path: planFilePath }, raw, undefined, EXIT_OK);
|
|
366
376
|
return;
|
|
367
377
|
}
|
|
368
378
|
output(r, raw, r.all_passed ? 'valid' : 'invalid');
|
|
@@ -425,7 +435,10 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
|
|
|
425
435
|
if (!content) { output({ error: 'File not found', path: planFilePath }, raw); return; }
|
|
426
436
|
const r = checkKeyLinks(cwd, content);
|
|
427
437
|
if (r.total === 0) {
|
|
428
|
-
|
|
438
|
+
// EXIT_OK: same reasoning as cmdVerifyArtifacts above — an empty key_links block is
|
|
439
|
+
// the shipped template default, and the parseMustHavesBlock indent mismatch means
|
|
440
|
+
// this branch fires for every plan written in PAN's own format.
|
|
441
|
+
output({ error: 'No must_haves.key_links found in frontmatter', path: planFilePath }, raw, undefined, EXIT_OK);
|
|
429
442
|
return;
|
|
430
443
|
}
|
|
431
444
|
output(r, raw, r.all_verified ? 'valid' : 'invalid');
|
|
@@ -490,8 +503,11 @@ function reconcilePhase(cwd, phaseNum) {
|
|
|
490
503
|
function cmdVerifyReconcile(cwd, phaseNum, raw) {
|
|
491
504
|
if (!phaseNum) { error('Usage: verify reconcile <phase>'); }
|
|
492
505
|
const r = reconcilePhase(cwd, phaseNum);
|
|
493
|
-
|
|
494
|
-
|
|
506
|
+
// Exit non-zero on contradiction so exec-phase's auto-advance gate actually
|
|
507
|
+
// stops on a rubber-stamped verification. output() previously hard-coded
|
|
508
|
+
// exit 0, making the old trailing process.exit dead code (H3, ADR audit
|
|
509
|
+
// 2026-08); the exitCode arg restores the gate.
|
|
510
|
+
output(r, raw, r.reconciled ? 'valid' : 'invalid', r.reconciled ? 0 : 1);
|
|
495
511
|
}
|
|
496
512
|
|
|
497
513
|
// ─── Stub / fake-return scanner (ADR-0036 review — closes the hardcoded
|
|
@@ -516,7 +532,9 @@ const STUB_CODE_EXT = /\.(js|cjs|mjs|jsx|ts|tsx|py|go|rb|java|php|rs|c|cc|cpp|h|
|
|
|
516
532
|
* @returns {{scanned, findings: Array, blocking: number, total: number}}
|
|
517
533
|
*/
|
|
518
534
|
function scanStubs(cwd, opts = {}) {
|
|
519
|
-
|
|
535
|
+
// includeUntracked: a stub gate that cannot see new files is not a gate. See the
|
|
536
|
+
// note in verify-drift.cjs getChangedFiles for why drift does not want the same.
|
|
537
|
+
let files = Array.isArray(opts.files) ? opts.files : getChangedFiles(cwd, null, { includeUntracked: true });
|
|
520
538
|
files = (files || []).filter(f => STUB_CODE_EXT.test(f));
|
|
521
539
|
const findings = [];
|
|
522
540
|
for (const rel of files) {
|
|
@@ -538,8 +556,9 @@ function scanStubs(cwd, opts = {}) {
|
|
|
538
556
|
|
|
539
557
|
function cmdVerifyStubs(cwd, opts = {}, raw) {
|
|
540
558
|
const r = scanStubs(cwd, opts);
|
|
541
|
-
|
|
542
|
-
|
|
559
|
+
// --gate must exit non-zero on blocking findings; output() used to hard-exit 0
|
|
560
|
+
// before the gate check, so the gate was dead (M30, ADR audit 2026-08).
|
|
561
|
+
output(r, raw, r.blocking === 0 ? 'valid' : 'invalid', opts.gate ? (r.blocking > 0 ? 1 : 0) : 0);
|
|
543
562
|
}
|
|
544
563
|
|
|
545
564
|
/**
|
|
@@ -757,8 +776,17 @@ function checkStateFile(cwd, addIssue, repairs) {
|
|
|
757
776
|
if (stateContent === null) {
|
|
758
777
|
// skip further state checks
|
|
759
778
|
} else {
|
|
760
|
-
// Extract phase references (e.g. "Phase 3" or "phase 01") from state.md
|
|
761
|
-
|
|
779
|
+
// Extract phase references (e.g. "Phase 3" or "phase 01") from state.md,
|
|
780
|
+
// deduped by normalized phase number. A real state.md mentions the same phase
|
|
781
|
+
// several times -- a total, a progress line, a decisions note -- and one missing
|
|
782
|
+
// directory must produce one warning, not one per mention. Keying on the
|
|
783
|
+
// normalized form collapses "Phase 3" and "Phase 03"; the first spelling seen
|
|
784
|
+
// is kept so the message quotes what the file actually says.
|
|
785
|
+
const phaseRefs = new Map();
|
|
786
|
+
for (const match of stateContent.matchAll(/[Pp]hase\s+(\d+(?:\.\d+)*)/g)) {
|
|
787
|
+
const key = normalizePhaseName(match[1]);
|
|
788
|
+
if (!phaseRefs.has(key)) phaseRefs.set(key, match[1]);
|
|
789
|
+
}
|
|
762
790
|
// Get disk phases for cross-reference
|
|
763
791
|
const diskPhases = new Set();
|
|
764
792
|
try {
|
|
@@ -773,7 +801,7 @@ function checkStateFile(cwd, addIssue, repairs) {
|
|
|
773
801
|
// phases/ directory may not exist yet
|
|
774
802
|
}
|
|
775
803
|
// Check for invalid references -- only warn if there are phases on disk
|
|
776
|
-
for (const ref of phaseRefs) {
|
|
804
|
+
for (const ref of phaseRefs.values()) {
|
|
777
805
|
const normalizedRef = String(parseInt(ref, 10)).padStart(2, '0');
|
|
778
806
|
if (!diskPhases.has(ref) && !diskPhases.has(normalizedRef) && !diskPhases.has(String(parseInt(ref, 10)))) {
|
|
779
807
|
if (diskPhases.size > 0) {
|
|
@@ -866,10 +894,28 @@ function crossCheckRoadmapDisk(cwd, phasesDirPath, addIssue) {
|
|
|
866
894
|
}
|
|
867
895
|
} catch { /* phases/ may not exist yet */ }
|
|
868
896
|
|
|
897
|
+
// A roadmap phase ahead of the current position has simply not been planned yet.
|
|
898
|
+
// PAN's own workflow creates the condition: /pan:new-project writes a roadmap
|
|
899
|
+
// declaring every phase up front, and /pan:plan-phase creates directories one at a
|
|
900
|
+
// time -- so warning on those would make `degraded` the permanent state of every
|
|
901
|
+
// multi-phase project and drain the verdict of signal. Only a phase at or behind
|
|
902
|
+
// the current position is genuinely suspect. With no readable current phase we
|
|
903
|
+
// cannot tell ahead from behind, so fall back to warning on everything.
|
|
904
|
+
let currentPhase = null;
|
|
905
|
+
try {
|
|
906
|
+
const stateContent = fs.readFileSync(path.join(planningPath(cwd), STATE_FILE), 'utf-8');
|
|
907
|
+
const m = stateContent.match(/\*\*Current Phase:\*\*\s*(\d+[A-Z]?(?:\.\d+)*)/i);
|
|
908
|
+
if (m) currentPhase = m[1];
|
|
909
|
+
} catch { /* no state.md -- checkStateFile reports that; stay conservative here */ }
|
|
910
|
+
|
|
869
911
|
for (const p of roadmapPhases) {
|
|
870
912
|
const padded = String(parseInt(p, 10)).padStart(2, '0');
|
|
871
|
-
if (
|
|
913
|
+
if (diskPhases.has(p) || diskPhases.has(padded)) continue;
|
|
914
|
+
if (currentPhase !== null && comparePhaseNum(p, currentPhase) > 0) {
|
|
915
|
+
addIssue('info', 'I002', `Phase ${p} in ${ROADMAP_FILE} is not planned yet (current phase: ${currentPhase})`, `Run /pan:plan-phase ${p} when you reach it`);
|
|
916
|
+
} else {
|
|
872
917
|
addIssue('warning', 'W006', `Phase ${p} in ${ROADMAP_FILE} but no directory on disk`, 'Create phase directory or remove from roadmap');
|
|
918
|
+
}
|
|
873
919
|
}
|
|
874
920
|
for (const p of diskPhases) {
|
|
875
921
|
const unpadded = String(parseInt(p, 10));
|
|
@@ -919,17 +965,13 @@ function repairIssues(cwd, repairs) {
|
|
|
919
965
|
switch (repair) {
|
|
920
966
|
case 'createConfig':
|
|
921
967
|
case 'resetConfig': {
|
|
922
|
-
// Write
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
plan_checker: true,
|
|
930
|
-
verifier: true,
|
|
931
|
-
parallelization: true,
|
|
932
|
-
};
|
|
968
|
+
// Write the canonical NESTED config via buildConfigDefaults. The old
|
|
969
|
+
// flat literal (research/plan_checker/verifier at top level) did NOT
|
|
970
|
+
// match the schema the gate reads (config.workflow.verifier), so
|
|
971
|
+
// --repair silently disabled the verification gate (M31, ADR audit
|
|
972
|
+
// 2026-08; note the flat form also drifted plan_checker vs plan_check).
|
|
973
|
+
const { buildConfigDefaults } = require('./config.cjs');
|
|
974
|
+
const defaults = buildConfigDefaults(false, {});
|
|
933
975
|
fs.writeFileSync(configFullPath, JSON.stringify(defaults, null, 2), 'utf-8');
|
|
934
976
|
repairActions.push({ action: repair, success: true, path: CONFIG_FILE });
|
|
935
977
|
break;
|
|
@@ -970,7 +1012,7 @@ function repairIssues(cwd, repairs) {
|
|
|
970
1012
|
}
|
|
971
1013
|
} catch (err) {
|
|
972
1014
|
// Repair action failed -- record the error so callers can report it
|
|
973
|
-
repairActions.push({ action: repair, success: false, error: err.message });
|
|
1015
|
+
repairActions.push({ action: repair, success: false, error: err.message || 'repair_failed' });
|
|
974
1016
|
}
|
|
975
1017
|
}
|
|
976
1018
|
|
|
@@ -1111,7 +1153,7 @@ function syncRequirementCheckboxes(cwd) {
|
|
|
1111
1153
|
}
|
|
1112
1154
|
|
|
1113
1155
|
if (fixed > 0) {
|
|
1114
|
-
try { fs.writeFileSync(reqPath, reqContent, 'utf-8'); } catch (e) { return { fixed: 0, error: e.message }; }
|
|
1156
|
+
try { fs.writeFileSync(reqPath, reqContent, 'utf-8'); } catch (e) { return { fixed: 0, error: e.message || 'requirements_write_failed' }; }
|
|
1115
1157
|
}
|
|
1116
1158
|
return { fixed };
|
|
1117
1159
|
}
|
|
@@ -1161,7 +1203,7 @@ function syncRoadmapPlanCheckboxes(cwd) {
|
|
|
1161
1203
|
}
|
|
1162
1204
|
|
|
1163
1205
|
if (fixed > 0) {
|
|
1164
|
-
try { fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8'); } catch (e) { return { fixed: 0, error: e.message }; }
|
|
1206
|
+
try { fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8'); } catch (e) { return { fixed: 0, error: e.message || 'roadmap_write_failed' }; }
|
|
1165
1207
|
}
|
|
1166
1208
|
return { fixed };
|
|
1167
1209
|
}
|
|
@@ -1308,19 +1350,6 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
1308
1350
|
}
|
|
1309
1351
|
}
|
|
1310
1352
|
|
|
1311
|
-
// Determine overall status from error/warning counts
|
|
1312
|
-
let status;
|
|
1313
|
-
if (errors.length > 0) {
|
|
1314
|
-
status = HEALTH_STATUS.BROKEN;
|
|
1315
|
-
} else if (warnings.length > 0) {
|
|
1316
|
-
status = HEALTH_STATUS.DEGRADED;
|
|
1317
|
-
} else {
|
|
1318
|
-
status = HEALTH_STATUS.HEALTHY;
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
const repairableCount = errors.filter(e => e.repairable).length +
|
|
1322
|
-
warnings.filter(w => w.repairable).length;
|
|
1323
|
-
|
|
1324
1353
|
// Check 11 (optional): drift analysis
|
|
1325
1354
|
let driftResult;
|
|
1326
1355
|
if (options.drift) {
|
|
@@ -1352,6 +1381,22 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
1352
1381
|
}
|
|
1353
1382
|
}
|
|
1354
1383
|
|
|
1384
|
+
// Determine overall status from error/warning counts. This must run after every
|
|
1385
|
+
// check that can call addIssue -- computing it earlier left DRIFT_HIGH and
|
|
1386
|
+
// LINKS_ERR sitting in `warnings` while the verdict still read `healthy`, which
|
|
1387
|
+
// contradicts the documented behaviour of --links ("errors degrade health").
|
|
1388
|
+
let status;
|
|
1389
|
+
if (errors.length > 0) {
|
|
1390
|
+
status = HEALTH_STATUS.BROKEN;
|
|
1391
|
+
} else if (warnings.length > 0) {
|
|
1392
|
+
status = HEALTH_STATUS.DEGRADED;
|
|
1393
|
+
} else {
|
|
1394
|
+
status = HEALTH_STATUS.HEALTHY;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
const repairableCount = errors.filter(e => e.repairable).length +
|
|
1398
|
+
warnings.filter(w => w.repairable).length;
|
|
1399
|
+
|
|
1355
1400
|
const result = {
|
|
1356
1401
|
status,
|
|
1357
1402
|
errors,
|
|
@@ -79,7 +79,7 @@ function buildCounterfactualContext(cwd, phaseNum, scenario) {
|
|
|
79
79
|
|
|
80
80
|
return {
|
|
81
81
|
phase: String(phaseNum),
|
|
82
|
-
phase_name: phaseInfo.
|
|
82
|
+
phase_name: phaseInfo.phase_name || null,
|
|
83
83
|
directory: toPosix(phaseInfo.directory),
|
|
84
84
|
scenario,
|
|
85
85
|
slug: scenarioSlug(scenario),
|
|
@@ -256,6 +256,13 @@ function cmdWhatifPrepare(cwd, phaseNum, scenario, raw) {
|
|
|
256
256
|
const ctx = buildCounterfactualContext(cwd, phaseNum, scenario);
|
|
257
257
|
if (ctx.error) { output(ctx, raw); return; }
|
|
258
258
|
const wt = createWorktree(cwd, phaseNum, scenario);
|
|
259
|
+
// `worktree_error` (not `error`) because the payload also carries the context that
|
|
260
|
+
// WAS resolved, and `error` would read as "the context failed". It is still an
|
|
261
|
+
// error-family key, so output() derives exit 1 from it exactly as it does for the
|
|
262
|
+
// `ctx.error` guard one line above — which is the point: the two adjacent failures
|
|
263
|
+
// of the same command must not report different exit codes just because one of
|
|
264
|
+
// them renamed the key. Callers cannot proceed either way: what-if stage 2 spawns
|
|
265
|
+
// an agent INTO worktree_path, and there is no worktree.
|
|
259
266
|
if (wt.error) { output({ ...ctx, worktree_error: wt.error }, raw); return; }
|
|
260
267
|
output({ ...ctx, worktree: wt }, raw);
|
|
261
268
|
}
|