pan-wizard 3.22.0 → 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.
Files changed (134) hide show
  1. package/README.md +38 -24
  2. package/agents/pan-conductor.md +3 -3
  3. package/agents/pan-design-checker.md +83 -0
  4. package/agents/pan-designer.md +67 -0
  5. package/agents/pan-document_code.md +3 -2
  6. package/agents/pan-executor.md +2 -2
  7. package/agents/pan-plan-checker.md +19 -6
  8. package/agents/pan-planner.md +4 -1
  9. package/agents/pan-previewer.md +1 -1
  10. package/agents/pan-roadmapper.md +3 -1
  11. package/agents/pan-verifier.md +10 -10
  12. package/bin/install-lib.cjs +128 -17
  13. package/bin/install.js +480 -74
  14. package/commands/pan/army.md +20 -14
  15. package/commands/pan/audit-deployment.md +17 -14
  16. package/commands/pan/cost.md +2 -2
  17. package/commands/pan/debug.md +1 -1
  18. package/commands/pan/design-phase.md +77 -0
  19. package/commands/pan/exec-phase.md +2 -2
  20. package/commands/pan/experiment.md +18 -14
  21. package/commands/pan/focus-auto.md +5 -5
  22. package/commands/pan/focus-design.md +13 -1
  23. package/commands/pan/focus-exec.md +1 -1
  24. package/commands/pan/learn.md +1 -1
  25. package/commands/pan/links.md +3 -1
  26. package/commands/pan/map-codebase.md +4 -4
  27. package/commands/pan/mcp-bridge.md +1 -1
  28. package/commands/pan/milestone-new.md +3 -1
  29. package/commands/pan/optimize.md +2 -2
  30. package/commands/pan/patches.md +1 -1
  31. package/commands/pan/plan-phase.md +3 -1
  32. package/commands/pan/preview.md +3 -3
  33. package/commands/pan/profile.md +3 -3
  34. package/commands/pan/research-phase.md +1 -1
  35. package/commands/pan/retro.md +4 -1
  36. package/commands/pan/review-deep.md +1 -1
  37. package/commands/pan/settings.md +2 -2
  38. package/commands/pan/what-if.md +1 -1
  39. package/hooks/dist/pan-check-update.js +149 -38
  40. package/hooks/dist/pan-context-monitor.js +155 -81
  41. package/hooks/dist/pan-cost-logger.js +288 -13
  42. package/hooks/dist/pan-statusline.js +24 -8
  43. package/hooks/dist/pan-stop-guard.js +160 -0
  44. package/hooks/dist/pan-trace-logger.js +261 -19
  45. package/package.json +2 -2
  46. package/pan-wizard-core/bin/lib/bridge.cjs +1 -1
  47. package/pan-wizard-core/bin/lib/bus.cjs +19 -4
  48. package/pan-wizard-core/bin/lib/campaign.cjs +8 -3
  49. package/pan-wizard-core/bin/lib/codebase.cjs +15 -5
  50. package/pan-wizard-core/bin/lib/commands.cjs +88 -6
  51. package/pan-wizard-core/bin/lib/config.cjs +27 -2
  52. package/pan-wizard-core/bin/lib/constants.cjs +1 -0
  53. package/pan-wizard-core/bin/lib/core.cjs +113 -8
  54. package/pan-wizard-core/bin/lib/cost.cjs +61 -24
  55. package/pan-wizard-core/bin/lib/distill.cjs +22 -4
  56. package/pan-wizard-core/bin/lib/doc-lint.cjs +25 -7
  57. package/pan-wizard-core/bin/lib/experiment.cjs +25 -1
  58. package/pan-wizard-core/bin/lib/focus.cjs +42 -7
  59. package/pan-wizard-core/bin/lib/frontmatter.cjs +62 -31
  60. package/pan-wizard-core/bin/lib/git.cjs +51 -16
  61. package/pan-wizard-core/bin/lib/hud.cjs +17 -2
  62. package/pan-wizard-core/bin/lib/init.cjs +2 -5
  63. package/pan-wizard-core/bin/lib/knowledge.cjs +20 -1
  64. package/pan-wizard-core/bin/lib/learn-lint.cjs +50 -0
  65. package/pan-wizard-core/bin/lib/links.cjs +9 -6
  66. package/pan-wizard-core/bin/lib/lock.cjs +23 -4
  67. package/pan-wizard-core/bin/lib/memory-optimize.cjs +11 -6
  68. package/pan-wizard-core/bin/lib/milestone.cjs +11 -5
  69. package/pan-wizard-core/bin/lib/optimize.cjs +18 -7
  70. package/pan-wizard-core/bin/lib/phase-remove.cjs +41 -2
  71. package/pan-wizard-core/bin/lib/phase.cjs +82 -11
  72. package/pan-wizard-core/bin/lib/preview.cjs +23 -4
  73. package/pan-wizard-core/bin/lib/review-deep.cjs +14 -6
  74. package/pan-wizard-core/bin/lib/roadmap.cjs +6 -2
  75. package/pan-wizard-core/bin/lib/runner.cjs +17 -7
  76. package/pan-wizard-core/bin/lib/squads.cjs +43 -11
  77. package/pan-wizard-core/bin/lib/state.cjs +32 -11
  78. package/pan-wizard-core/bin/lib/template.cjs +8 -3
  79. package/pan-wizard-core/bin/lib/verify-deploy.cjs +14 -2
  80. package/pan-wizard-core/bin/lib/verify-drift.cjs +15 -1
  81. package/pan-wizard-core/bin/lib/verify-preflight.cjs +9 -3
  82. package/pan-wizard-core/bin/lib/verify.cjs +84 -39
  83. package/pan-wizard-core/bin/lib/whatif.cjs +8 -1
  84. package/pan-wizard-core/bin/pan-tools.cjs +42 -19
  85. package/pan-wizard-core/learnings/index.json +7 -7
  86. package/pan-wizard-core/learnings/universal/autonomous-loop.md +5 -3
  87. package/pan-wizard-core/learnings/universal/concurrency.md +1 -1
  88. package/pan-wizard-core/references/design-methodology.md +94 -0
  89. package/pan-wizard-core/references/git-integration.md +9 -9
  90. package/pan-wizard-core/references/guardrails.md +4 -1
  91. package/pan-wizard-core/references/model-profile-resolution.md +1 -1
  92. package/pan-wizard-core/references/model-profiles.md +44 -34
  93. package/pan-wizard-core/references/planning-config.md +5 -5
  94. package/pan-wizard-core/references/verification-patterns.md +67 -0
  95. package/pan-wizard-core/templates/codebase/architecture.md +1 -1
  96. package/pan-wizard-core/templates/codebase/concerns.md +1 -1
  97. package/pan-wizard-core/templates/codebase/conventions.md +1 -1
  98. package/pan-wizard-core/templates/codebase/integrations.md +1 -1
  99. package/pan-wizard-core/templates/codebase/stack.md +1 -1
  100. package/pan-wizard-core/templates/codebase/structure.md +1 -1
  101. package/pan-wizard-core/templates/codebase/testing.md +1 -1
  102. package/pan-wizard-core/templates/design.md +146 -0
  103. package/pan-wizard-core/templates/discovery.md +3 -3
  104. package/pan-wizard-core/templates/milestone-archive.md +2 -2
  105. package/pan-wizard-core/templates/playbook.md +1 -1
  106. package/pan-wizard-core/templates/uat.md +1 -1
  107. package/pan-wizard-core/workflows/diagnose-issues.md +9 -7
  108. package/pan-wizard-core/workflows/exec-phase.md +28 -8
  109. package/pan-wizard-core/workflows/execute-plan.md +2 -3
  110. package/pan-wizard-core/workflows/health.md +24 -1
  111. package/pan-wizard-core/workflows/help.md +5 -4
  112. package/pan-wizard-core/workflows/learn.md +5 -5
  113. package/pan-wizard-core/workflows/map-codebase.md +1 -1
  114. package/pan-wizard-core/workflows/milestone-audit.md +1 -1
  115. package/pan-wizard-core/workflows/milestone-new.md +4 -4
  116. package/pan-wizard-core/workflows/new-project.md +47 -32
  117. package/pan-wizard-core/workflows/optimize.md +8 -8
  118. package/pan-wizard-core/workflows/pause.md +1 -1
  119. package/pan-wizard-core/workflows/phase-tests.md +2 -2
  120. package/pan-wizard-core/workflows/plan-phase.md +4 -0
  121. package/pan-wizard-core/workflows/profile.md +13 -10
  122. package/pan-wizard-core/workflows/research-phase.md +1 -1
  123. package/pan-wizard-core/workflows/resume-project.md +1 -1
  124. package/pan-wizard-core/workflows/settings.md +9 -9
  125. package/pan-wizard-core/workflows/transition.md +24 -8
  126. package/pan-wizard-core/workflows/update.md +23 -20
  127. package/pan-wizard-core/workflows/verify-phase.md +7 -2
  128. package/pan-zcode/README.md +12 -2
  129. package/pan-zcode/bin/install-zcode.js +1 -1
  130. package/pan-zcode/mcp/native-tools.cjs +1 -1
  131. package/pan-zcode/mcp/server.cjs +76 -14
  132. package/scripts/build-hooks.js +2 -1
  133. package/scripts/generate-skills-docs.py +0 -4
  134. package/scripts/run-tests.cjs +11 -4
@@ -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
- roadmapPhases.set(match[1], match[2].trim());
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
- output({ error: 'No must_haves.artifacts found in frontmatter', path: planFilePath }, raw);
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
- output({ error: 'No must_haves.key_links found in frontmatter', path: planFilePath }, raw);
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
- output(r, raw, r.reconciled ? 'valid' : 'invalid');
494
- process.exit(r.reconciled ? 0 : 1);
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
- let files = Array.isArray(opts.files) ? opts.files : getChangedFiles(cwd);
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
- output(r, raw, r.blocking === 0 ? 'valid' : 'invalid');
542
- if (opts.gate) process.exit(r.blocking > 0 ? 1 : 0);
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
- const phaseRefs = [...stateContent.matchAll(/[Pp]hase\s+(\d+(?:\.\d+)*)/g)].map(match => match[1]);
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 (!diskPhases.has(p) && !diskPhases.has(padded))
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 a fresh config.json with sensible defaults
923
- const defaults = {
924
- model_profile: 'balanced',
925
- commit_docs: true,
926
- search_gitignored: false,
927
- branching_strategy: 'none',
928
- research: true,
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.name || null,
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
  }
@@ -27,13 +27,13 @@
27
27
  * config-get <key> Get a config.json value
28
28
  * history-digest Aggregate all summary.md data
29
29
  * summary-extract <path> [--fields] Extract structured data from summary.md
30
- * state-snapshot Structured parse of state.md
30
+ * state-snapshot Structured parse of state.md (alias: state snapshot)
31
31
  * phase-plan-index <phase> Index plans with waves and status
32
32
  * websearch <query> Search web via Brave API (if configured)
33
33
  * [--limit N] [--freshness day|week|month]
34
34
  *
35
35
  * Phase Listing:
36
- * phases list [--type plan|summary] List phases with optional type filter
36
+ * phases list [--type plans|summaries] List phases with optional type filter
37
37
  * [--phase N] [--include-archived]
38
38
  *
39
39
  * Phase Operations:
@@ -282,8 +282,20 @@ async function main() {
282
282
 
283
283
  const command = args[0];
284
284
 
285
+ const USAGE = 'Usage: pan-tools <command> [args] [--raw] [--cwd <path>]\nCommands: state, resolve-model, estimate-cost, find-phase, git, distill, experiment, commit, verify-summary, template, frontmatter, verify, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section, config-set, config-get, history-digest, phases, roadmap, requirements, phase, milestone, validate, progress, context-budget, todo, scaffold, init, phase-plan-index, state-snapshot, summary-extract, rollback-snapshot, batch-commit, websearch, focus, preflight, dashboard, hud, report, learnings, deps, drift-check, memory, bridge, whatif, knowledge, skills, hygiene, review-deep, preview, cost, models, squad, worktree, campaign, bus, cache, retro, codebase, standards, optimize, doc-lint, learn, links';
286
+
285
287
  if (!command) {
286
- error('Usage: pan-tools <command> [args] [--raw] [--cwd <path>]\nCommands: state, state-snapshot, resolve-model, find-phase, commit, verify-summary, verify, frontmatter, template, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section, config-set, config-get, history-digest, phases, roadmap, requirements, phase, milestone, validate, progress, context-budget, todo, scaffold, init, phase-plan-index, summary-extract, rollback-snapshot, websearch, focus, preflight, dashboard, learnings, deps, standards');
288
+ error(USAGE);
289
+ }
290
+
291
+ // P-1814 (PanLoop finding 11): `--help` was the single most frequent failed
292
+ // probe in field transcripts — 26 sightings across 10 command docs. It is
293
+ // the most predictable thing any agent or human tries, so it works: print
294
+ // the usage and exit 0. A missing command stays exit 1 (the error contract);
295
+ // an explicit request for help is a success.
296
+ if (command === '--help' || command === '-h' || command === 'help') {
297
+ console.log(USAGE);
298
+ process.exit(0);
287
299
  }
288
300
 
289
301
  switch (command) {
@@ -337,10 +349,16 @@ async function main() {
337
349
  stopped_at: getArgValue(args, '--stopped-at'),
338
350
  resume_file: getArgValue(args, '--resume-file', 'None'),
339
351
  }, raw);
352
+ } else if (subcommand === 'snapshot') {
353
+ // Alias for the top-level `state-snapshot` (P-1813). The hyphenated
354
+ // command reads exactly like a `state` subcommand, and a field agent
355
+ // guessed the spaced form by analogy (PanLoop finding 10). The guess
356
+ // is semantically right, so it lands on the real handler.
357
+ state.cmdStateSnapshot(cwd, raw);
340
358
  } else if (subcommand === 'load' || !subcommand) {
341
359
  state.cmdStateLoad(cwd, raw);
342
360
  } else {
343
- error(`Unknown state subcommand: ${subcommand}. Available: json, update, get, patch, advance-plan, record-metric, update-progress, add-decision, add-blocker, resolve-blocker, record-session, load`);
361
+ error(`Unknown state subcommand: ${subcommand}. Available: json, update, get, patch, advance-plan, record-metric, update-progress, add-decision, add-blocker, resolve-blocker, record-session, load, snapshot`);
344
362
  }
345
363
  break;
346
364
  }
@@ -530,7 +548,7 @@ async function main() {
530
548
  } else if (subcommand === 'stubs') {
531
549
  verify.cmdVerifyStubs(cwd, { gate: args.includes('--gate') }, raw);
532
550
  } else {
533
- error('Unknown verify subcommand. Available: plan-structure, phase-completeness, references, commits, artifacts, key-links');
551
+ error('Unknown verify subcommand. Available: plan-structure, phase-completeness, references, commits, artifacts, key-links, reconcile, stubs');
534
552
  }
535
553
  break;
536
554
  }
@@ -786,8 +804,12 @@ async function main() {
786
804
 
787
805
  case 'batch-commit': {
788
806
  const itemsJson = args[1];
789
- let items = [];
790
- try { items = JSON.parse(itemsJson); } catch { /* empty */ }
807
+ // Report a malformed payload rather than silently reporting 'no_items':
808
+ // a caller whose JSON was garbled (e.g. shell-stripped quotes) must not be
809
+ // told there was nothing to commit. Absent arg → genuine empty batch.
810
+ const items = itemsJson === undefined || itemsJson === ''
811
+ ? []
812
+ : parseJsonOrError(itemsJson, 'batch-commit');
791
813
  commands.cmdBatchCommit(cwd, items, raw);
792
814
  break;
793
815
  }
@@ -1295,6 +1317,7 @@ async function main() {
1295
1317
  const subcommand = args[1];
1296
1318
  if (subcommand === 'trace') {
1297
1319
  const traceSub = args[2];
1320
+ const tokensWasted = getArgValue(args, '--tokens-wasted');
1298
1321
  optimize.cmdOptimizeTrace(cwd, traceSub, {
1299
1322
  sessionId: getArgValue(args, '--session'),
1300
1323
  all: args.includes('--all'),
@@ -1306,9 +1329,8 @@ async function main() {
1306
1329
  type: getArgValue(args, '--type'),
1307
1330
  category: getArgValue(args, '--category'),
1308
1331
  impact: getArgValue(args, '--impact'),
1309
- description: getArgValue(args, '--description'),
1310
1332
  correction: getArgValue(args, '--correction'),
1311
- tokens_wasted: getArgValue(args, '--tokens-wasted') ? Number(getArgValue(args, '--tokens-wasted')) : null,
1333
+ tokens_wasted: tokensWasted ? Number(tokensWasted) : null,
1312
1334
  context: (() => { const v = getArgValue(args, '--context'); if (!v) return null; try { return JSON.parse(v); } catch { return null; } })(),
1313
1335
  }, raw);
1314
1336
  } else if (subcommand === 'learn') {
@@ -1382,7 +1404,7 @@ async function main() {
1382
1404
  const sourceExperiments = sourceExpsCsv
1383
1405
  ? sourceExpsCsv.split(',').map(s => s.trim()).filter(Boolean)
1384
1406
  : [];
1385
- const sourceRoot = getArgValue(args, '--source-root') || cwd;
1407
+ const sourceRoot = getArgValue(args, '--source-root') || learnLint.resolveLearningsRoot();
1386
1408
 
1387
1409
  const result = optimize.promotePattern(
1388
1410
  { id: patternId, summary, evidence, rule, applies_in: appliesIn, source_experiments: sourceExperiments },
@@ -1396,7 +1418,7 @@ async function main() {
1396
1418
  const patternId = getArgValue(args, '--pattern');
1397
1419
  const scope = getArgValue(args, '--scope');
1398
1420
  const topic = getArgValue(args, '--topic');
1399
- const sourceRoot = getArgValue(args, '--source-root') || cwd;
1421
+ const sourceRoot = getArgValue(args, '--source-root') || learnLint.resolveLearningsRoot();
1400
1422
  if (!patternId || !scope || !topic) {
1401
1423
  error('learn unpromote requires --pattern <id> --scope <s> --topic <t>');
1402
1424
  }
@@ -1406,14 +1428,14 @@ async function main() {
1406
1428
  }
1407
1429
 
1408
1430
  if (subcommand === 'list-promoted') {
1409
- const sourceRoot = getArgValue(args, '--source-root') || cwd;
1431
+ const sourceRoot = getArgValue(args, '--source-root') || learnLint.resolveLearningsRoot();
1410
1432
  const result = optimize.listPromotedPatterns({ sourceRoot });
1411
1433
  output(result, raw);
1412
1434
  break;
1413
1435
  }
1414
1436
 
1415
1437
  if (subcommand === 'build-index') {
1416
- const sourceRoot = getArgValue(args, '--source-root') || cwd;
1438
+ const sourceRoot = getArgValue(args, '--source-root') || learnLint.resolveLearningsRoot();
1417
1439
  const result = learnIndex.cmdBuildIndex(sourceRoot);
1418
1440
  if (raw) {
1419
1441
  output(result, true,
@@ -1428,7 +1450,7 @@ async function main() {
1428
1450
  }
1429
1451
 
1430
1452
  if (subcommand === 'topics-for') {
1431
- const sourceRoot = getArgValue(args, '--source-root') || cwd;
1453
+ const sourceRoot = getArgValue(args, '--source-root') || learnLint.resolveLearningsRoot();
1432
1454
  const agent = getArgValue(args, '--agent');
1433
1455
  if (!agent) { error('learn topics-for requires --agent <name>'); }
1434
1456
  const minRelevance = getArgValue(args, '--min-relevance', 'medium');
@@ -1451,7 +1473,7 @@ async function main() {
1451
1473
  }
1452
1474
 
1453
1475
  if (subcommand === 'lint') {
1454
- const sourceRoot = getArgValue(args, '--source-root') || cwd;
1476
+ const sourceRoot = getArgValue(args, '--source-root') || learnLint.resolveLearningsRoot();
1455
1477
  const scope = getArgValue(args, '--scope');
1456
1478
  const strict = args.includes('--strict');
1457
1479
  const result = learnLint.cmdLearnLint(sourceRoot, { scope, strict });
@@ -1466,11 +1488,12 @@ async function main() {
1466
1488
  for (const v of result.violations) {
1467
1489
  lines.push(`[${v.severity.toUpperCase()}] ${v.code} ${v.pattern_id}: ${v.message}`);
1468
1490
  }
1469
- output(result, true, lines.join('\n'));
1491
+ output(result, true, lines.join('\n'), result.summary.status === 'fail' ? 1 : 0);
1470
1492
  } else {
1471
- output(result, false);
1493
+ output(result, false, undefined, result.summary.status === 'fail' ? 1 : 0);
1472
1494
  }
1473
- if (result.summary.status === 'fail') process.exit(1);
1495
+ // exit code carried through output() above (it used to hard-exit 0
1496
+ // before this check, so `learn lint` never gated — M32, ADR audit 2026-08).
1474
1497
  break;
1475
1498
  }
1476
1499
 
@@ -1504,7 +1527,7 @@ async function main() {
1504
1527
  }
1505
1528
 
1506
1529
  default:
1507
- error(`Unknown command: ${command}. Run pan-tools without arguments to see available commands.`);
1530
+ error(`Unknown command: ${command}. Run pan-tools --help to see available commands.`);
1508
1531
  }
1509
1532
  }
1510
1533
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema_version": 1,
3
- "generated_at": "2026-07-09T15:40:55.203Z",
3
+ "generated_at": "2026-08-07T13:36:47.532Z",
4
4
  "topics": [
5
5
  {
6
6
  "name": "experiment-runner",
@@ -152,8 +152,8 @@
152
152
  "P-350",
153
153
  "P-360"
154
154
  ],
155
- "size_bytes": 10667,
156
- "size_tokens_est": 2667,
155
+ "size_bytes": 11837,
156
+ "size_tokens_est": 2960,
157
157
  "agent_relevance": {
158
158
  "planner": "low",
159
159
  "executor": "medium",
@@ -218,8 +218,8 @@
218
218
  "P-1204",
219
219
  "P-NPRS-003"
220
220
  ],
221
- "size_bytes": 3400,
222
- "size_tokens_est": 850,
221
+ "size_bytes": 3402,
222
+ "size_tokens_est": 851,
223
223
  "agent_relevance": {
224
224
  "planner": "medium",
225
225
  "executor": "high",
@@ -804,7 +804,7 @@
804
804
  "totals": {
805
805
  "topics": 47,
806
806
  "patterns": 93,
807
- "size_bytes": 148954,
808
- "size_tokens_est": 37255
807
+ "size_bytes": 150126,
808
+ "size_tokens_est": 37549
809
809
  }
810
810
  }
@@ -37,12 +37,14 @@ patterns:
37
37
 
38
38
  ## P-330 — Scale agents into a coordinated army with squads, worktree isolation, and a human-gated ship
39
39
 
40
- **Evidence:** The bot-army model (ADR-0032/0033) showed the durable shape for running a whole-project goal across many agents: a delegation-only coordinator (never codes) fans work to role-scoped *squads* (architecture/build/quality/release), the build squad parallelizes by giving each agent its own branch + git worktree (so concurrent builders never touch the same file), quality is adversarial and read-only, and the path to a protected branch is a human-approved gate, not a bot merge.
40
+ **Evidence:** The bot-army model (ADR-0032/0033) showed the durable shape for running a whole-project goal across many agents: a coordinator that routes rather than implements fans work to role-scoped *squads* (architecture/build/quality/release), the build squad parallelizes by giving each agent its own branch + git worktree (which is why concurrent builders cannot land in the same working file), quality is adversarial and reads more than it writes, and the path to a protected branch is a human-approved gate, not a bot merge.
41
+
42
+ **Where the enforcement actually lives, and where it does not.** Worktree isolation is real isolation — separate directories, separate branches, enforced by git. The *role* labels are not: a coordinator that can spawn agents generally also holds file and shell tools, and a squad's `read-only` label is a contract its prompt is told to honour rather than a sandbox the runtime imposes. Both were written as absolutes here ("delegation-only", "never codes") and both were wrong; the guidance below survived the correction unchanged, because instructing the split is still right — claiming it is enforced is what was not.
41
43
 
42
44
  **Rule:** When scaling beyond a single agent:
43
45
 
44
- 1. **Coordinator delegates, never codes** — its tools are delegation-only; it plans, decomposes, and routes to squads, then aggregates tight summaries.
45
- 2. **Group agents into role-scoped squads with least-privilege tools** — design read-only, build read/write, quality read-only/adversarial, release always-ask. Resolve the roster from data, not hardcoded prompt lists.
46
+ 1. **Instruct the coordinator to delegate rather than implement** — it plans, decomposes, and routes to squads, then aggregates tight summaries. Treat that as prompt discipline and verify the grant instead of assuming it: read the coordinator's own `tools:` frontmatter, and if it holds write or shell tools, the split is instructed, not enforced. Say which of the two your prompt is relying on wherever you describe the design to a user.
47
+ 2. **Group agents into role-scoped squads with a declared least-privilege contract** — design read-only, build read/write, quality read-only/adversarial, release always-ask. Resolve the roster from data, not hardcoded prompt lists. A label is a contract until a per-agent grant matches it; a registry that maps agents to role labels changes no execution path on its own, so publish those labels as advisory rather than as guarantees.
46
48
  3. **Parallelize by isolation, not by hope** — one branch + worktree per concurrent builder; never two agents in one tree. Serialize builds only where the build tree corrupts under concurrency (a per-project opt-in).
47
49
  4. **The mutating boundary is human-gated** — merging to a protected branch is `always-ask`; recovery is revert / previous tag, never force-push or history rewrite.
48
50
  5. **The harness scales with the army, not after it** — depth caps, spawn/budget ceilings, and an abort kill-switch checked before every spawn are mandatory; a longer loop must not relax a single cap. Power and safety are the same investment.
@@ -18,7 +18,7 @@ patterns:
18
18
 
19
19
  ## P-1204 — O_EXCL lockfile + retry with bounded backoff is enough for multi-process file writes in Node — no flock needed
20
20
 
21
- **Evidence:** whoocache lock.js + atomic-write.js: parallel-process tests with two child processes each calling set() 1000 times completed with consistent index, zero lost writes. P-1402 (whoocache 02-02 summary): 'O_EXCL lockfile + Windows rename retry' shipped Phase 2.
21
+ **Evidence:** whoocache lock.js + atomic-write.js: parallel-process tests with two child processes each calling set() 1000 times completed with consistent index, zero lost writes. The whoocache 02-02 summary records 'O_EXCL lockfile + Windows rename retry' shipped Phase 2.
22
22
 
23
23
  **Rule:** For multi-process safe writes (parallel CLI invocations sharing one cache/index/state file), use fs.openSync(lockPath, 'wx') as a 'try-acquire' (EEXIST means held). On failure, retry with random backoff 5-50ms, capped at ~10 attempts. Always wrap acquired work in try/finally and unlink the lockfile in finally. Cross-platform safe (no flock dependency). Combine with the atomic write-tmp-then-rename pattern (P-1201) so even if the lock holder is killed mid-write, recovery is automatic.
24
24
 
@@ -0,0 +1,94 @@
1
+ # Design Methodology (shared)
2
+
3
+ Single source of truth for how PAN designs a change **before** it is planned or
4
+ executed. Cited by `pan-designer` (main flow, via `/pan:design-phase`) and by
5
+ `focus-design` (the `/featureAI` feature pipeline). Both flows use the *same*
6
+ method at different **depth tiers** so design quality can't drift between them.
7
+
8
+ This reference defines: the depth tiers, what each tier must produce, and the
9
+ quality bar every design artifact is held to (the same bar `pan-design-checker`
10
+ verifies against). It does not prescribe a runtime — agents apply it with
11
+ whatever tools they have.
12
+
13
+ ---
14
+
15
+ ## Depth tiers
16
+
17
+ A tier selects which sections of the design artifact (`templates/design.md`) are
18
+ **mandatory**. Deeper tiers are supersets of shallower ones. Pick the shallowest
19
+ tier that fits the altitude of the work.
20
+
21
+ | Tier | Altitude | Mandatory sections | Skips | Typical entry point |
22
+ |------|----------|--------------------|-------|---------------------|
23
+ | `spike` | Throwaway PoC / trivial change | Problem · Scope · Minimal architecture | Everything else — flag as speculative | `/pan:design-phase --spike`, `focus-design --spike` |
24
+ | `phase` | One roadmap phase | Problem · Success criteria (≥2 machine-checkable) · Architecture assessment · Design synthesis · Phase-scoped ADR · Threat-lite · Test hooks | Demand validation, competitive intel, market/strategy | **`/pan:design-phase` (main-flow default)** |
25
+ | `feature` | A whole feature (internal) | `phase` + Demand validation · Error-handling design · Full STRIDE-lite threat model · Test plan · Feature ladder | Competitive intel, strategic (Blue Ocean/Wardley) | `focus-design --internal` |
26
+ | `full` | A market-facing feature | `feature` + Competitive intelligence · Strategic analysis (ERRC) · Adoption analysis | — (nothing) | `focus-design --full` (DEFAULT) |
27
+
28
+ **Altitude rule (why the tiers exist):** product/strategic design — *whether*
29
+ and *what* to build, with demand and competitive evidence — is decided **once**
30
+ at feature/milestone creation (`focus-design`, or the product-design pass in
31
+ `milestone-new`). Per-phase design (`phase` tier) covers only *how* to build an
32
+ already-scoped phase: architecture, ADR, threat, synthesis. This keeps the
33
+ frequently-run per-phase path cheap and stops the main flow re-litigating product
34
+ questions on every phase.
35
+
36
+ ---
37
+
38
+ ## What every tier produces
39
+
40
+ The artifact is `{scope}-design.md` from `templates/design.md`. Sections below are
41
+ ordered; a tier makes a contiguous prefix (plus its named additions) mandatory.
42
+
43
+ ### 1. Problem & scope (all tiers)
44
+ - **Problem statement** — what problem exists, why it matters, cost of inaction. Concrete, not a vague generality.
45
+ - **Scope** — in-scope / out-of-scope, and the fixed boundary this design must not exceed (a phase boundary from `roadmap.md`, or a feature boundary).
46
+
47
+ ### 2. Success criteria (`phase`+)
48
+ - 3–7 criteria, **at least 2 machine-checkable** (verifiable by an automated test, not manual inspection). State the check for each machine-checkable one.
49
+
50
+ ### 3. Architecture assessment & synthesis (`phase`+)
51
+ - **Conventions** — extract the project's actual conventions (module layout, error style, boundaries) from the codebase; do not assume.
52
+ - **Components & boundaries** — what modules are added/touched; confirm the design honors the discovered boundaries (no layer violations).
53
+ - **Interface contract** — exact invocation + input/output schema + status/exit codes, per project convention.
54
+ - **State & filesystem scope** — reads from / writes to (must stay within project root / `.planning/`), state mutations, side effects.
55
+ - **Design decisions** — each significant decision: what was decided, why, alternatives rejected.
56
+
57
+ ### 4. ADR (`phase`+)
58
+ - A right-sized Architecture Decision Record: Status · Date · Context (problem, forces, current state, requirements traceability) · Decision (summary + numbered sub-decisions with rationale + integration) · Consequences (positive **and** negative, every negative with a mitigation or explicit acceptance).
59
+ - `phase` tier: phase-scoped ADR (may be a section in `design.md`). `feature`/`full`: a standalone `docs/decisions/ADR-NNNN-*.md`.
60
+
61
+ ### 5. Threat model (`phase` = lite, `feature`+ = full)
62
+ - STRIDE-lite: for each realistic threat, its vector and its mitigation (or explicit acceptance). `phase` tier covers the change's own surface; `feature`+ covers the full feature.
63
+
64
+ ### 6. Error handling & diagnostics (`feature`+)
65
+ - Every error condition → output shape + handling style (validate args before side effects; safe reads return null; actionable messages).
66
+
67
+ ### 7. Test plan (`phase` = hooks, `feature`+ = full)
68
+ - How each success criterion is verified; which are unit vs scenario; the seed inputs for the machine-checkable ones.
69
+
70
+ ### 8. Demand & strategy (`feature`+ / `full`)
71
+ - `feature`: demand evidence (or an explicit "speculative" flag), feature ladder (v0 MVP → v1 → v2).
72
+ - `full`: competitive intelligence, strategic analysis (ERRC: eliminate/reduce/raise/create), adoption analysis.
73
+
74
+ ---
75
+
76
+ ## Quality bar (what `pan-design-checker` enforces)
77
+
78
+ A design artifact **passes** only when all of these hold at its tier. This is the
79
+ same checklist the independent checker verifies — designers should self-check
80
+ against it before handing off, but passing is decided by the checker, not the
81
+ author.
82
+
83
+ 1. **Requirement coverage** — every stated requirement maps to a design element; nothing is silently dropped.
84
+ 2. **Machine-checkable criteria** — ≥2 success criteria are automatable, and each names its check (`phase`+).
85
+ 3. **Architecture conformance** — the design matches conventions *discovered* from the codebase, and introduces no layer/boundary violation.
86
+ 4. **ADR honesty** — alternatives are real (not strawmen); every negative consequence has a mitigation or is explicitly accepted; no placeholder/skeleton sections (`phase`+).
87
+ 5. **Threat coverage** — each identified threat is mitigated or explicitly accepted; none is left open (`phase` lite / `feature`+ full).
88
+ 6. **Testability** — the test plan/hooks map to the success criteria (`phase`+).
89
+ 7. **Scope discipline** — the design stays within the fixed boundary; scope-expanding ideas are captured as "deferred", never designed in.
90
+
91
+ If any dimension fails, the checker returns structured gaps and the designer
92
+ revises. The reflexion loop is capped at **2 revision iterations** (design →
93
+ check → revise → check → final) to prevent over-revision — the same guardrail
94
+ `plan-phase` uses with `pan-plan-checker`.