pan-wizard 3.30.0 → 3.32.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 (75) hide show
  1. package/README.md +30 -26
  2. package/agents/pan-design-checker.md +18 -0
  3. package/agents/pan-optimizer.md +5 -1
  4. package/agents/pan-plan-checker.md +23 -0
  5. package/agents/pan-reviewer.md +19 -0
  6. package/agents/pan-verifier.md +23 -0
  7. package/bin/install-lib.cjs +129 -33
  8. package/bin/install.js +16 -9
  9. package/commands/pan/army.md +4 -4
  10. package/commands/pan/design-phase.md +8 -0
  11. package/commands/pan/exec-phase.md +1 -1
  12. package/commands/pan/git.md +2 -2
  13. package/commands/pan/learn.md +2 -0
  14. package/commands/pan/optimize.md +14 -2
  15. package/commands/pan/review-deep.md +5 -5
  16. package/commands/pan/verify-phase.md +8 -9
  17. package/hooks/dist/pan-check-update.js +53 -2
  18. package/hooks/dist/pan-context-monitor.js +42 -1
  19. package/hooks/dist/pan-cost-logger.js +132 -11
  20. package/hooks/dist/pan-state-reinject.js +158 -0
  21. package/hooks/dist/pan-stop-guard.js +50 -4
  22. package/hooks/dist/pan-trace-logger.js +324 -18
  23. package/package.json +1 -1
  24. package/pan-wizard-core/bin/lib/commands.cjs +22 -5
  25. package/pan-wizard-core/bin/lib/config.cjs +2 -0
  26. package/pan-wizard-core/bin/lib/constants.cjs +31 -1
  27. package/pan-wizard-core/bin/lib/context-budget.cjs +27 -2
  28. package/pan-wizard-core/bin/lib/core.cjs +232 -23
  29. package/pan-wizard-core/bin/lib/cost-rebuild.cjs +11 -0
  30. package/pan-wizard-core/bin/lib/cost.cjs +135 -46
  31. package/pan-wizard-core/bin/lib/findings.cjs +381 -0
  32. package/pan-wizard-core/bin/lib/focus.cjs +72 -19
  33. package/pan-wizard-core/bin/lib/foreign-planning.cjs +24 -6
  34. package/pan-wizard-core/bin/lib/frontmatter.cjs +5 -2
  35. package/pan-wizard-core/bin/lib/hygiene.cjs +12 -2
  36. package/pan-wizard-core/bin/lib/init.cjs +4 -1
  37. package/pan-wizard-core/bin/lib/memory-optimize.cjs +12 -6
  38. package/pan-wizard-core/bin/lib/memory-rebuild.cjs +9 -2
  39. package/pan-wizard-core/bin/lib/optimize.cjs +349 -21
  40. package/pan-wizard-core/bin/lib/phase-report.cjs +4 -1
  41. package/pan-wizard-core/bin/lib/review-deep.cjs +36 -0
  42. package/pan-wizard-core/bin/lib/roadmap.cjs +87 -18
  43. package/pan-wizard-core/bin/lib/state.cjs +93 -24
  44. package/pan-wizard-core/bin/lib/suggest.cjs +21 -9
  45. package/pan-wizard-core/bin/lib/template.cjs +6 -3
  46. package/pan-wizard-core/bin/lib/verdict.cjs +431 -0
  47. package/pan-wizard-core/bin/lib/verify-deploy.cjs +19 -2
  48. package/pan-wizard-core/bin/lib/verify-preflight.cjs +4 -2
  49. package/pan-wizard-core/bin/lib/verify-scope.cjs +156 -0
  50. package/pan-wizard-core/bin/lib/verify.cjs +25 -2
  51. package/pan-wizard-core/bin/pan-tools.cjs +69 -6
  52. package/pan-wizard-core/learnings/README.md +10 -7
  53. package/pan-wizard-core/mcp/tool-registry.cjs +16 -4
  54. package/pan-wizard-core/references/verdict-contract.md +101 -0
  55. package/pan-wizard-core/templates/verification-report.md +16 -0
  56. package/pan-wizard-core/workflows/exec-phase.md +73 -47
  57. package/pan-wizard-core/workflows/health.md +2 -0
  58. package/pan-wizard-core/workflows/help.md +2 -2
  59. package/pan-wizard-core/workflows/learn.md +4 -2
  60. package/pan-wizard-core/workflows/milestone-audit.md +15 -3
  61. package/pan-wizard-core/workflows/optimize.md +14 -1
  62. package/pan-wizard-core/workflows/phase-tests.md +1 -1
  63. package/pan-wizard-core/workflows/plan-phase.md +20 -15
  64. package/pan-wizard-core/workflows/progress.md +1 -1
  65. package/pan-wizard-core/workflows/todo-add.md +1 -1
  66. package/pan-wizard-core/workflows/todo-check.md +7 -6
  67. package/pan-wizard-core/workflows/verify-phase.md +3 -13
  68. package/pan-zcode/README.md +3 -3
  69. package/scripts/build-agent-plugin.js +4 -0
  70. package/scripts/build-hooks.js +2 -1
  71. package/scripts/build-plugin.js +10 -0
  72. package/scripts/git-hooks/pre-commit +2 -1
  73. package/scripts/npm-dist-tag.js +47 -0
  74. package/scripts/plugin-path.js +6 -1
  75. package/scripts/test-surface.cjs +1 -0
@@ -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, comparePhaseNum, execGit, findPhaseInternal, getMilestoneInfo, toPosix, output, EXIT_OK, error, escapeRegex } = require('./core.cjs');
8
+ const { safeReadFile, normalizePhaseName, comparePhaseNum, execGit, findPhaseInternal, getMilestoneInfo, toPosix, output, EXIT_OK, error, escapeRegex, loadConfig, isGitIgnored, isGitRepo } = require('./core.cjs');
9
9
  const { extractFrontmatter, parseMustHavesBlock } = require('./frontmatter.cjs');
10
10
  const { writeStateMd, readStateSafe } = require('./state.cjs');
11
11
  const {
@@ -22,6 +22,7 @@ const { runDriftCheck, parseConventionRules, checkFileConventions, calculateDrif
22
22
  const { collectVerificationStats, countRoadmapPhases, groupGapPatterns, cmdRetro } = require('./verify-retro.cjs');
23
23
  const { detectInstalledRuntimes, validateRuntimeInstall, cmdValidateDeployment } = require('./verify-deploy.cjs');
24
24
  const { cmdPreflight, cmdDepsValidate } = require('./verify-preflight.cjs');
25
+ const { scopePhase, cmdVerifyScope } = require('./verify-scope.cjs');
25
26
  const { planningRootRel } = require('./planning-root.cjs');
26
27
 
27
28
  /**
@@ -821,6 +822,23 @@ function checkStateFile(cwd, addIssue, repairs) {
821
822
  * @param {Function} addIssue - Issue recording callback
822
823
  * @param {string[]} repairs - Mutable array of repair actions to schedule
823
824
  */
825
+ /**
826
+ * I005: the planning tree is ignored by git while `commit_docs` is on, so every
827
+ * `pan-tools commit` of planning docs commits nothing, silently. planning-with-files
828
+ * adds `.planning/` to .gitignore by default (its README, read 2026-09-28), which is
829
+ * how a PAN project ends up like this (market item M3). Info, not a warning: keeping
830
+ * the docs out of git is a legitimate choice, and it is one config key away.
831
+ */
832
+ function checkPlanningIgnored(cwd, addIssue) {
833
+ try {
834
+ if (loadConfig(cwd).commit_docs === false || !isGitRepo(cwd)) return;
835
+ const rel = planningRootRel();
836
+ if (!isGitIgnored(cwd, `${rel}/`)) return;
837
+ addIssue('info', 'I005', `${rel}/ is ignored by git while commit_docs is true, so PAN's planning commits commit nothing`,
838
+ `Remove ${rel}/ from .gitignore (planning-with-files adds it by default), or keep the docs out of git on purpose: pan-tools config-set commit_docs false`);
839
+ } catch { /* best effort: a git failure is not a finding */ }
840
+ }
841
+
824
842
  function checkConfigFile(cwd, addIssue, repairs) {
825
843
  const configFullPath = path.join(planningPath(cwd), CONFIG_FILE);
826
844
  let rawContent;
@@ -1314,7 +1332,9 @@ function cmdValidateHealth(cwd, options, raw) {
1314
1332
  // error and stop: E002-E005 would describe a foreign layout as a broken PAN one,
1315
1333
  // and --repair must never write into it. Reality check R15.
1316
1334
  const foreign = detectForeignPlanningTree(planningPath(cwd));
1317
- if (foreign) {
1335
+ if (foreign && foreign.coexists) {
1336
+ addIssue('info', 'I004', `${foreign.tool} also writes into this planning tree: ${foreign.evidence.join(', ')}`, 'None needed: PAN leaves its files alone. Give PAN its own tree with --planning-dir if you prefer them apart (ADR-0043)');
1337
+ } else if (foreign) {
1318
1338
  addIssue('error', 'E006', `planning tree belongs to ${foreign.tool}: ${foreign.evidence.join(', ')}`, 'Run PAN with --planning-dir <dir> to use a separate tree (ADR-0043)');
1319
1339
  output({ status: HEALTH_STATUS.BROKEN, errors, warnings, info, repairable_count: 0 }, raw, undefined, 1);
1320
1340
  return;
@@ -1337,6 +1357,7 @@ function cmdValidateHealth(cwd, options, raw) {
1337
1357
  addIssue('info', 'I003', `${shape.model}-model project (${shape.evidence.join(', ')}) — the phase-model checks (project.md, roadmap.md, state.md, phases/) do not apply`, null);
1338
1358
  }
1339
1359
  checkConfigFile(cwd, addIssue, repairs);
1360
+ checkPlanningIgnored(cwd, addIssue);
1340
1361
  if (phaseModel) {
1341
1362
  checkPhaseDirectories(cwd, addIssue);
1342
1363
  checkPhaseContents(cwd, addIssue);
@@ -1535,6 +1556,8 @@ module.exports = {
1535
1556
  cmdVerifyReconcile,
1536
1557
  scanStubs,
1537
1558
  cmdVerifyStubs,
1559
+ scopePhase,
1560
+ cmdVerifyScope,
1538
1561
  cmdValidateConsistency,
1539
1562
  cmdValidateHealth,
1540
1563
  cmdPreflight,
@@ -28,6 +28,7 @@
28
28
  * history-digest Aggregate all summary.md data
29
29
  * summary-extract <path> [--fields] Extract structured data from summary.md
30
30
  * state-snapshot Structured parse of state.md (alias: state snapshot)
31
+ * findings record|list|dispose|debt Judges' verdicts and their findings ledger (evidence loop)
31
32
  * phase-plan-index <phase> Index plans with waves and status
32
33
  * websearch <query> Search web via Brave API (if configured)
33
34
  * [--limit N] [--freshness day|week|month]
@@ -206,6 +207,7 @@ const cost = require('./lib/cost.cjs');
206
207
  const costRebuild = require('./lib/cost-rebuild.cjs');
207
208
  const preview = require('./lib/preview.cjs');
208
209
  const reviewDeep = require('./lib/review-deep.cjs');
210
+ const findings = require('./lib/findings.cjs');
209
211
  const knowledge = require('./lib/knowledge.cjs');
210
212
  const skillAlign = require('./lib/skill-align.cjs');
211
213
  const hygiene = require('./lib/hygiene.cjs');
@@ -331,7 +333,7 @@ async function main() {
331
333
  + ' --planning-dir <path> act on an arbitrary project-relative planning tree\n'
332
334
  + ' --all-tracks (hygiene) act on the root tree AND every discovered track\n'
333
335
  + ' env: PAN_TRACK, PAN_PLANNING_DIR (flags win)\n'
334
- + '\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';
336
+ + '\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, findings, preview, cost, models, squad, worktree, campaign, bus, cache, retro, codebase, standards, optimize, doc-lint, learn, links';
335
337
 
336
338
  if (!command) {
337
339
  error(USAGE);
@@ -412,7 +414,7 @@ async function main() {
412
414
  } else if (subcommand === 'load' || !subcommand) {
413
415
  state.cmdStateLoad(cwd, raw);
414
416
  } else {
415
- 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`);
417
+ error(`Unknown state subcommand: ${subcommand}. Available: json, update, get, patch, compact, advance-plan, record-metric, update-progress, add-decision, add-blocker, resolve-blocker, record-session, load, snapshot`);
416
418
  }
417
419
  break;
418
420
  }
@@ -420,7 +422,13 @@ async function main() {
420
422
  case 'resolve-model': {
421
423
  const metadataIdx = args.indexOf('--metadata');
422
424
  const metadataJson = metadataIdx !== -1 ? args[metadataIdx + 1] : undefined;
423
- commands.cmdResolveModel(cwd, args[1], raw, metadataJson);
425
+ const attemptRaw = getArgValue(args, '--attempt');
426
+ let attempt;
427
+ if (attemptRaw !== null && attemptRaw !== undefined) {
428
+ if (!/^[1-9]\d*$/.test(String(attemptRaw))) error('--attempt must be a positive integer (1 for a first try)');
429
+ attempt = parseInt(attemptRaw, 10);
430
+ }
431
+ commands.cmdResolveModel(cwd, args[1], raw, metadataJson, attempt);
424
432
  break;
425
433
  }
426
434
 
@@ -601,8 +609,10 @@ async function main() {
601
609
  verify.cmdVerifyReconcile(cwd, args[2], raw);
602
610
  } else if (subcommand === 'stubs') {
603
611
  verify.cmdVerifyStubs(cwd, { gate: args.includes('--gate') }, raw);
612
+ } else if (subcommand === 'scope') {
613
+ verify.cmdVerifyScope(cwd, args[2], raw);
604
614
  } else {
605
- error('Unknown verify subcommand. Available: plan-structure, phase-completeness, references, commits, artifacts, key-links, reconcile, stubs');
615
+ error('Unknown verify subcommand. Available: plan-structure, phase-completeness, references, commits, artifacts, key-links, reconcile, stubs, scope');
606
616
  }
607
617
  break;
608
618
  }
@@ -908,7 +918,13 @@ async function main() {
908
918
  output({ error: 'No batch file found. Run focus plan first.' }, raw);
909
919
  break;
910
920
  }
911
- items = batch.batch || [];
921
+ if (batch.error) {
922
+ // The newest batch is the one /pan:focus-exec runs; classifying another would
923
+ // hand it waves for the wrong items, so an unusable newest batch is an error.
924
+ output({ error: batch.error }, raw);
925
+ break;
926
+ }
927
+ items = batch.batch;
912
928
  }
913
929
  output(focus.classifyStageDependencies(items), raw);
914
930
  } else if (subcommand === 'reflection') {
@@ -1171,6 +1187,44 @@ async function main() {
1171
1187
  break;
1172
1188
  }
1173
1189
 
1190
+ case 'findings': {
1191
+ // Evidence loop (ADR-0049): judges' verdicts recorded from their reports, and
1192
+ // the dispositions workflows give the findings they continue past.
1193
+ const subcommand = args[1];
1194
+ const filters = {
1195
+ phase: getArgValue(args, '--phase'),
1196
+ agent: getArgValue(args, '--agent'),
1197
+ status: getArgValue(args, '--status'),
1198
+ class: getArgValue(args, '--class'),
1199
+ milestone: getArgValue(args, '--milestone'),
1200
+ };
1201
+ if (subcommand === 'record') {
1202
+ findings.cmdFindingsRecord(cwd, {
1203
+ phase: filters.phase,
1204
+ agent: filters.agent,
1205
+ file: getArgValue(args, '--file'),
1206
+ text: getArgValue(args, '--text'),
1207
+ stdin: args.includes('--stdin'),
1208
+ }, raw);
1209
+ } else if (subcommand === 'list') {
1210
+ findings.cmdFindingsList(cwd, filters, raw);
1211
+ } else if (subcommand === 'dispose') {
1212
+ findings.cmdFindingsDispose(cwd, {
1213
+ ids: findings.findingIdsIn(args.slice(2)),
1214
+ phase: filters.phase,
1215
+ agent: filters.agent,
1216
+ open: args.includes('--open'),
1217
+ as: getArgValue(args, '--as'),
1218
+ reason: getArgValue(args, '--reason'),
1219
+ }, raw);
1220
+ } else if (subcommand === 'debt') {
1221
+ findings.cmdFindingsDebt(cwd, { milestone: filters.milestone }, raw);
1222
+ } else {
1223
+ error('Unknown findings subcommand. Available: record, list, dispose, debt');
1224
+ }
1225
+ break;
1226
+ }
1227
+
1174
1228
  case 'preview': {
1175
1229
  const subcommand = args[1];
1176
1230
  if (subcommand === 'phase') {
@@ -1272,6 +1326,9 @@ async function main() {
1272
1326
  daily_budget: budget != null ? Number(budget) : undefined,
1273
1327
  enabled: args.includes('--disable') ? false : undefined,
1274
1328
  paused: args.includes('--pause') ? true : (args.includes('--resume') ? false : undefined),
1329
+ // campaign.cjs has always honoured enforce_budget; no flag reached it, so
1330
+ // making the daily budget a hard stop meant editing schedule.json by hand.
1331
+ enforce_budget: args.includes('--enforce-budget') ? true : (args.includes('--advisory-budget') ? false : undefined),
1275
1332
  }, raw);
1276
1333
  } else if (subcommand === 'status' || !subcommand) {
1277
1334
  campaign.cmdCampaignStatus(cwd, raw);
@@ -1399,17 +1456,23 @@ async function main() {
1399
1456
  } else if (subcommand === 'learn') {
1400
1457
  optimize.cmdOptimizeLearn(cwd, {
1401
1458
  sessionId: getArgValue(args, '--session'),
1459
+ sessions: getArgValue(args, '--sessions'),
1402
1460
  }, raw);
1403
1461
  } else if (subcommand === 'apply') {
1404
1462
  optimize.cmdOptimizeApply(cwd, {
1405
1463
  reportPath: getArgValue(args, '--report'),
1406
1464
  }, raw);
1465
+ } else if (subcommand === 'revert') {
1466
+ optimize.cmdOptimizeRevert(cwd, {
1467
+ applyId: args[2] && !args[2].startsWith('--') ? args[2] : null,
1468
+ last: args.includes('--last'),
1469
+ }, raw);
1407
1470
  } else if (subcommand === 'list') {
1408
1471
  optimize.cmdOptimizeList(cwd, raw);
1409
1472
  } else if (subcommand === 'stats') {
1410
1473
  optimize.cmdOptimizeStats(cwd, raw);
1411
1474
  } else {
1412
- error('Unknown optimize subcommand. Available: trace, learn, apply, list, stats');
1475
+ error('Unknown optimize subcommand. Available: trace, learn, apply, revert, list, stats');
1413
1476
  }
1414
1477
  break;
1415
1478
  }
@@ -4,7 +4,7 @@ This directory holds AI-derived behavioral patterns extracted from real PAN
4
4
  Wizard sessions via the **self-improvement loop** (v3.7.0+, see
5
5
  [ADR-0026](../../docs/decisions/ADR-0026-self-improvement-loop.md)).
6
6
 
7
- Patterns are produced by running `pan-tools learn promote --pattern <id>` over
7
+ Patterns are promoted by hand with `pan-tools learn promote` (see Lifecycle) from
8
8
  harvested experiment data. They are **advisory** — orchestrators weight them
9
9
  against current context, not as hard rules.
10
10
 
@@ -46,9 +46,10 @@ patterns:
46
46
 
47
47
  ## Lifecycle
48
48
 
49
- 1. **Promote** — `pan-tools learn promote --pattern <id> --scope universal --topic <name>` appends a pattern to the topic file (creates the file if absent).
50
- 2. **Unpromote** — `pan-tools learn unpromote --pattern <id> --topic <name>` removes a pattern (for rollback).
49
+ 1. **Promote** — `pan-tools learn promote --pattern <id> --scope universal --topic <name> --summary <text> --rule <text>` appends a pattern to the topic file (creates the file if absent); `--evidence`, `--applies-in` and `--source-experiments` fill in the rest of the entry.
50
+ 2. **Unpromote** — `pan-tools learn unpromote --pattern <id> --scope <universal|internal> --topic <name>` removes a pattern (for rollback) and deletes the topic file once it holds none.
51
51
  3. **List** — `pan-tools learn list-promoted` shows the inventory across both tiers.
52
+ 4. **Index** — `pan-tools learn build-index` regenerates `index.json`, the topic index `pan-tools learn topics-for --agent <name>` reads; promote and unpromote do not update it.
52
53
 
53
54
  ## Why two tiers
54
55
 
@@ -58,13 +59,15 @@ apply when the project *is* PAN. Examples:
58
59
  - **PAN-internal**: "Always commit individually, never `git add -A`" (because of source repo's pre-commit hooks)
59
60
  - **Universal**: "Run the full test suite before marking a phase complete"
60
61
 
61
- The promote step uses a heuristic filter on file paths in the pattern's
62
- evidence. References to `pan-wizard-core/`, `bin/install.js`, `commands/pan/*`
63
- suggest `internal` scope. The human running `promote` makes the final call.
62
+ The promote step warns when a universal-scope pattern reads like a prompt
63
+ fragment rather than a structural pattern (P-RES-007), and `pan-tools learn lint`
64
+ warns when a universal pattern's heading or Rule names PAN-internal terms such as
65
+ `pan-wizard-core` or `pan-tools`, a candidate for `internal` scope. The human
66
+ running `promote` makes the final call.
64
67
 
65
68
  ## Maintenance
66
69
 
67
70
  These files are **AI-managed**. Direct human edits create drift between
68
- the frontmatter `pattern_ids` list and the body content. For human-authored
71
+ the frontmatter `patterns` list and the body content. For human-authored
69
72
  behavioral content, use `references/` instead — that's the canonical
70
73
  hand-authored channel (e.g., `references/guardrails.md` shipped in v3.6.0).
@@ -69,19 +69,31 @@ const RESOURCES = [
69
69
  description: 'Doc↔code link graph verdict: forward links, backlink contracts, and anchor targets, with finding codes.' },
70
70
  { uri: 'pan://cost', name: 'Token cost', verb: 'cost', args: ['report'],
71
71
  description: 'Aggregated token spend from the .planning/metrics ledger. Reads as zeros on a project with no recorded calls.' },
72
+ { uri: 'pan://findings', name: 'Findings', verb: 'findings', args: ['list'],
73
+ description: 'Every finding PAN\'s judges recorded (verifier, plan checker, reviewer, design checker), with its folded status — open, fixed, deferred, dismissed or decision — and the reason given. Reads as an empty list on a project with no recorded verdicts.' },
72
74
  ];
73
75
 
74
76
  /** Actionable pan-tools verbs → MCP tools (each spawns `node pan-tools.cjs <verb>`). */
75
77
  const SPAWN_TOOLS = [
76
78
  {
77
79
  name: 'pan_resolve_model', title: 'Resolve model for an agent', verb: 'resolve-model',
78
- description: 'Resolve the model tier/id PAN would use for an agent under the active profile.',
80
+ description: 'Resolve the model tier/id PAN would use for an agent under the active profile. Pass attempt > 1 for a retry after a failure: the tier climbs toward the agent\'s quality tier (routing.max_escalations caps it).',
79
81
  readOnly: true, destructive: false,
80
82
  inputSchema: {
81
83
  type: 'object', additionalProperties: false, required: ['agent'],
82
- properties: { agent: { type: 'string', description: 'Agent type, e.g. pan-planner' } },
84
+ properties: {
85
+ agent: { type: 'string', description: 'Agent type, e.g. pan-planner' },
86
+ attempt: { type: 'integer', minimum: 1, maximum: 99, description: '1 for a first try; 2 for the first retry after a failure' },
87
+ },
88
+ },
89
+ args: (i) => {
90
+ const out = [str('agent', i && i.agent, AGENT_RE, 64)];
91
+ if (i && i.attempt !== undefined) {
92
+ if (!Number.isInteger(i.attempt) || i.attempt < 1 || i.attempt > 99) throw new Error('Invalid "attempt": must be an integer from 1 to 99');
93
+ out.push('--attempt', String(i.attempt));
94
+ }
95
+ return out;
83
96
  },
84
- args: (i) => [str('agent', i && i.agent, AGENT_RE, 64)],
85
97
  },
86
98
  {
87
99
  name: 'pan_find_phase', title: 'Find a phase', verb: 'find-phase',
@@ -155,7 +167,7 @@ const { NATIVE_TOOLS } = require('./native-tools.cjs');
155
167
  // safety argument of ADR-0041 is that no client input reaches it.
156
168
  const PROJECT_CWD_PROPERTY = Object.freeze({
157
169
  type: 'string',
158
- description: 'Absolute path of the PAN project to operate on. Optional: defaults to the directory the server was started in. Pass it when the server was launched from a plugin directory (Agent Plugins clients do this by default), or to address another project.',
170
+ description: 'Absolute path of the PAN project to operate on. Optional: defaults to the directory the server was started in. Pass it when the server was launched from a plugin directory (Agent Plugins clients do this by default), or to address another project. The server acts on any existing directory named here with the permissions of the user who runs it — it trusts its caller as it trusts the session that started it.',
159
171
  });
160
172
 
161
173
  const PROJECT_CWD_MAX = 1024;
@@ -0,0 +1,101 @@
1
+ # The `pan-verdict` block
2
+
3
+ A judging agent ends its report with one fenced `pan-verdict` block. The block is the machine-readable form of the verdict the report already gives in prose. The orchestrator records it with `pan-tools findings record`, and that record is what the workflow branches on. It also feeds the findings ledger that milestone audits read for tech debt. The prose stays for people, and the block states the same verdict for code.
4
+
5
+ ## Rules
6
+
7
+ 1. **Place it last.** The block is the final element of your return. When a report holds more than one block, the recorder reads the last one.
8
+ 2. **Valid JSON only.** No comments and no trailing commas. One line or pretty-printed are both fine.
9
+ 3. **Same verdict as the prose.** `verdict` is the word your report already uses, and `outcome` is what that word means (see the table below). The block never disagrees with the report.
10
+ 4. **One finding per issue.** List the findings the report lists, each as its own entry, most severe first. A clean pass has `"findings": []`.
11
+ 5. **Plain summaries.** State each finding in one sentence of 300 characters or fewer. Put the location in `where`, not in `summary`.
12
+
13
+ ````markdown
14
+ ```pan-verdict
15
+ {"contract":"1.0","agent":"pan-reviewer","phase":"03","verdict":"NEEDS_FIXES","outcome":"fail","findings":[{"class":"defect","severity":"high","where":"src/utils/parser.ts:42","summary":"parse() drops the final token of every line"}]}
16
+ ```
17
+ ````
18
+
19
+ ## Fields
20
+
21
+ | Field | Required | Value |
22
+ |---|---|---|
23
+ | `contract` | yes | `"1.0"` |
24
+ | `agent` | yes | your agent name, e.g. `pan-reviewer` |
25
+ | `phase` | when you know it | the phase number you were asked about, e.g. `"03"` |
26
+ | `verdict` | yes | your own verdict word (table below) |
27
+ | `outcome` | yes | `pass`, `fail` or `needs_human` |
28
+ | `findings` | yes (may be empty) | a list of `{class, severity, where, summary}` |
29
+
30
+ | Agent | `verdict` → `outcome` |
31
+ |---|---|
32
+ | pan-plan-checker | `passed` → `pass` · `issues_found` → `fail` |
33
+ | pan-reviewer | `PASS` → `pass` · `PASS_WITH_WARNINGS` → `pass` · `NEEDS_FIXES` → `fail` |
34
+ | pan-design-checker | `PASS` → `pass` · `GAPS` → `fail` |
35
+
36
+ The verifier does not emit this block. Its verification.md frontmatter (`status`, `gaps`, `human_verification`, `unrequested`) is its machine contract, and the recorder reads that directly.
37
+
38
+ ## `class`: what kind of finding
39
+
40
+ | Class | Use when |
41
+ |---|---|
42
+ | `missing` | Something required is absent: a requirement with no task, a must-have not built, an unplanned key link |
43
+ | `partial` | Present but incomplete: a task missing its verify/done fields, a stub, a half-derived criterion |
44
+ | `contradicts` | It conflicts with a requirement, a locked decision (context.md) or the codebase's established architecture |
45
+ | `unrequested` | Work, a file or a behaviour that no plan or requirement asked for (scope creep) |
46
+ | `defect` | Incorrect behaviour or a code error; a broken dependency order in a plan |
47
+ | `risk` | A security, reliability or scale exposure; a plan whose scope will not fit its budget |
48
+ | `quality` | Convention, naming, style or maintainability, with no correctness impact |
49
+ | `human` | Cannot be settled by a check, so a person must look |
50
+
51
+ ## `severity`: how much it matters
52
+
53
+ `critical` · `high` · `medium` · `low` · `info`. This is the same ladder `/pan:review-deep` uses.
54
+
55
+ ## Mapping for each judge
56
+
57
+ **pan-plan-checker.**
58
+ - Classes by dimension:
59
+ - Requirement coverage, key links, test coverage, Nyquist → `missing`
60
+ - Task completeness, verification derivation, spec sufficiency, decision trace → `partial`
61
+ - Dependency correctness → `defect`
62
+ - Scope sanity → `risk`
63
+ - Context compliance → `contradicts`
64
+ - Standards → `quality`
65
+ - Severity: blocker → `high`, warning → `medium`, info → `info`.
66
+ - `where`: `NN-MM-plan.md` or `NN-MM-plan.md#task-N`.
67
+
68
+ **pan-reviewer.**
69
+ - Classes by category: Convention → `quality`, Security → `risk`, Code Quality → `defect`, work no plan asked for → `unrequested`.
70
+ - Severity: ERROR → `high`, WARNING → `medium`, INFO → `info`.
71
+ - `where`: `path:line`.
72
+
73
+ **pan-design-checker.**
74
+ - Classes by dimension:
75
+ - Requirement coverage → `missing`
76
+ - Architecture conformance → `contradicts`
77
+ - Threat coverage → `risk`
78
+ - Scope discipline → `unrequested`
79
+ - Every other dimension → `partial`
80
+ - Severity: each gap is `high`. A gap you report as a caveat on the final iteration is `medium`.
81
+ - `where`: the design section, e.g. `03-design.md#interfaces`.
82
+
83
+ ## Examples
84
+
85
+ A plan check that passes:
86
+
87
+ ````markdown
88
+ ```pan-verdict
89
+ {"contract":"1.0","agent":"pan-plan-checker","phase":"05","verdict":"passed","outcome":"pass","findings":[]}
90
+ ```
91
+ ````
92
+
93
+ A design check with two gaps:
94
+
95
+ ````markdown
96
+ ```pan-verdict
97
+ {"contract":"1.0","agent":"pan-design-checker","phase":"05","verdict":"GAPS","outcome":"fail","findings":[
98
+ {"class":"missing","severity":"high","where":"05-design.md#requirements","summary":"REQ-12 (export to CSV) maps to no design element"},
99
+ {"class":"risk","severity":"high","where":"05-design.md#threats","summary":"the upload endpoint accepts any file type and no threat entry covers it"}]}
100
+ ```
101
+ ````
@@ -12,6 +12,9 @@ phase: XX-name
12
12
  verified: YYYY-MM-DDTHH:MM:SSZ
13
13
  status: passed | gaps_found | human_needed
14
14
  score: N/M must-haves verified
15
+ unrequested: # Only when the scope check (`pan-tools verify scope <phase>`) found work no plan asked for
16
+ - path: "src/admin/panel.tsx"
17
+ what: "An admin panel no plan or requirement asked for"
15
18
  ---
16
19
 
17
20
  # Phase {X}: {Name} Verification Report
@@ -89,6 +92,19 @@ None — all verifiable items checked programmatically.
89
92
  **Expected:** {What should happen}
90
93
  **Why human:** {Why can't verify programmatically}
91
94
 
95
+ ## Unrequested Work
96
+
97
+ {If the scope check found none:}
98
+ None: every change traces to a plan.
99
+
100
+ {Otherwise, one row per `unrequested:` entry:}
101
+
102
+ | File | What it adds | Why no plan covers it |
103
+ |------|--------------|-----------------------|
104
+ | src/admin/panel.tsx | An admin panel | No requirement mentions admin tooling |
105
+
106
+ Unrequested work is recorded, not blocking: the status does not change because of it.
107
+
92
108
  ## Gaps Summary
93
109
 
94
110
  {If no gaps:}
@@ -117,6 +117,14 @@ Parse JSON for: `phase`, `plans[]` (each with `id`, `wave`, `autonomous`, `objec
117
117
 
118
118
  **Filtering:** Skip plans where `has_summary: true`. If `--gaps-only`: also skip non-gap_closure plans. If all filtered: "No matching incomplete plans" → exit.
119
119
 
120
+ **If `--gaps-only`:** this run is the fix round after a verification found gaps — a second attempt at work that failed once. Resolve the executor for that attempt and use it in place of `executor_model` for every spawn in this run:
121
+
122
+ ```bash
123
+ EXECUTOR_MODEL=$(node ~/.claude/pan-wizard-core/bin/pan-tools.cjs resolve-model pan-executor --attempt 2 --raw)
124
+ ```
125
+
126
+ It equals `executor_model` unless the `budget` profile runs the executor below its quality tier; then it is one tier higher (capped by `routing.max_escalations`), so the gaps are not retried on the tier that left them.
127
+
120
128
  Report:
121
129
  ```
122
130
  ## Execution Plan
@@ -417,13 +425,21 @@ Task(
417
425
  )
418
426
  ```
419
427
 
420
- 3. Handle review results:
428
+ 3. **Save the report.** Use the Write tool to write the reviewer's returned text, verbatim and in full (including its closing `pan-verdict` block), to `{phase_dir}/{phase_number}-review.md`. The reviewer is read-only by design, so the orchestrator saves the report. `/pan:review-deep` and the findings ledger read this file.
429
+
430
+ 4. **Record the verdict, then act on it.** The record is what this step branches on. It also adds the findings to the ledger (`.planning/findings.jsonl`) and logs the verdict to the trace, so no separate trace call is needed:
431
+ ```bash
432
+ REVIEW_FILE="{phase_dir}/{phase_number}-review.md"
433
+ REVIEW_VERDICT=$(node ~/.claude/pan-wizard-core/bin/pan-tools.cjs findings record --phase "${PHASE_NUMBER}" --agent pan-reviewer --file "$REVIEW_FILE" --raw 2>/dev/null) \
434
+ || REVIEW_VERDICT=$(grep -A1 '^### Verdict' "$REVIEW_FILE" | tail -1 | tr -d ' ')
435
+ ```
436
+ The fallback reads the word under `### Verdict` when the report carries no valid `pan-verdict` block.
421
437
 
422
438
  | Verdict | Action |
423
439
  |---------|--------|
424
440
  | `PASS` | Continue to verification |
425
- | `PASS_WITH_WARNINGS` | Report warnings, continue to verification |
426
- | `NEEDS_FIXES` | Present ERROR findings to user: "Fix before verification?" or "Continue anyway?" |
441
+ | `PASS_WITH_WARNINGS` | Report the warnings, then record that the phase continues past them (below), and continue to verification |
442
+ | `NEEDS_FIXES` | Present the ERROR findings to the user: "Fix before verification?" or "Continue anyway?" |
427
443
 
428
444
  ```
429
445
  ## Code Review Results
@@ -434,34 +450,60 @@ Task(
434
450
  {If errors: list top 5 with file:line}
435
451
  ```
436
452
 
437
- **If user chooses "Continue anyway":** Proceed to verification. Review findings are informational, not blocking.
453
+ **Never continue past findings silently.** Whenever the phase moves on with review findings still open, record why. The reason is what a milestone audit later reports as tech debt:
454
+ - `PASS_WITH_WARNINGS`:
455
+ ```bash
456
+ node ~/.claude/pan-wizard-core/bin/pan-tools.cjs findings dispose --phase "${PHASE_NUMBER}" --agent pan-reviewer --open --as deferred --reason "accepted at review: PASS_WITH_WARNINGS"
457
+ ```
458
+ - **The user chooses "Continue anyway"** at `NEEDS_FIXES`: the same command with `--reason "continued past NEEDS_FIXES at the user's choice"`, then proceed to verification.
459
+ - **The user chooses "Fix before verification"**: apply the fixes, then run the review once more (steps 2–4). The new record closes the findings it no longer reports.
438
460
 
439
- 4. **Circular optimization — log reviewer corrections to trace bus (W1 fix):**
461
+ 5. **No separate trace logging.** The record logs `verdict_passed` or `verdict_failed` for the reviewer on every runtime. The `reviewer_correction` and `reviewer_warnings` trace calls that used to sit here read the error count from a shell variable nothing ever set.
440
462
 
441
- When verdict is `NEEDS_FIXES`, write a `reviewer_correction` event so the optimizer can see the primary quality signal:
442
- ```bash
443
- # Extract error count from review output (parse "Errors: N" line)
444
- REVIEW_ERROR_COUNT=$(echo "$REVIEW_OUTPUT" | grep -E "^- Errors: [0-9]+" | grep -oE "[0-9]+" | head -1)
445
- REVIEW_ERROR_COUNT=${REVIEW_ERROR_COUNT:-1}
446
- node ~/.claude/pan-wizard-core/bin/pan-tools.cjs optimize trace log \
447
- --type error --category reviewer_correction \
448
- --description "Phase ${PHASE_NUMBER} reviewer: NEEDS_FIXES — ${REVIEW_ERROR_COUNT} error(s) found" \
449
- --agent pan-reviewer --impact major \
450
- --context "{\"phase\":\"${PHASE_NUMBER}\",\"verdict\":\"NEEDS_FIXES\",\"error_count\":${REVIEW_ERROR_COUNT}}" \
451
- 2>/dev/null || true
463
+ 6. **Deep review — only when `--deep-review` is in $ARGUMENTS.** The security pass and cross-check that `/pan:review-deep` runs, done inline right after the normal review. If the normal review was skipped (`--skip-review` or `--fast`), say that `--deep-review` needs the review it builds on, and continue without it.
464
+
465
+ a. Spawn the hardener. Its output path is `.planning/reviews/${PHASE_NUMBER}/hardener.md`:
466
+ ```
467
+ Task(
468
+ subagent_type="pan-hardener",
469
+ model="$(node ~/.claude/pan-wizard-core/bin/pan-tools.cjs resolve-model pan-hardener --raw)",
470
+ prompt="
471
+ <files_to_read>
472
+ - {phase_dir}/{phase_number}-review.md (the first-pass review)
473
+ - the phase plans: {phase_dir}/*-plan.md
474
+ Changed files to audit:
475
+ ${CHANGED_FILES_LIST}
476
+ </files_to_read>
477
+ <output_path>.planning/reviews/${PHASE_NUMBER}/hardener.md</output_path>
478
+ <framework_scope>OWASP Top 10 (2025) and STRIDE, across every changed file.</framework_scope>
479
+ "
480
+ )
452
481
  ```
453
482
 
454
- When verdict is `PASS_WITH_WARNINGS`, log a softer signal:
455
- ```bash
456
- node ~/.claude/pan-wizard-core/bin/pan-tools.cjs optimize trace log \
457
- --type decision --category reviewer_warnings \
458
- --description "Phase ${PHASE_NUMBER} reviewer: PASS_WITH_WARNINGS — warnings present" \
459
- --agent pan-reviewer --impact trivial \
460
- --context "{\"phase\":\"${PHASE_NUMBER}\",\"verdict\":\"PASS_WITH_WARNINGS\"}" \
461
- 2>/dev/null || true
483
+ b. Spawn the meta-reviewer on both first-pass reports. Its output path is `.planning/reviews/${PHASE_NUMBER}/meta.md`:
484
+ ```
485
+ Task(
486
+ subagent_type="pan-meta-reviewer",
487
+ model="$(node ~/.claude/pan-wizard-core/bin/pan-tools.cjs resolve-model pan-meta-reviewer --raw)",
488
+ prompt="
489
+ <files_to_read>
490
+ - {phase_dir}/{phase_number}-review.md
491
+ - .planning/reviews/${PHASE_NUMBER}/hardener.md
492
+ </files_to_read>
493
+ <output_path>.planning/reviews/${PHASE_NUMBER}/meta.md</output_path>
494
+ "
495
+ )
462
496
  ```
497
+ If either agent returns its report instead of writing the output path, save the returned text there with the Write tool before the merge.
463
498
 
464
- When verdict is `PASS`, no trace event needed — clean passes are the expected baseline.
499
+ c. Merge the three reports:
500
+ ```bash
501
+ node ~/.claude/pan-wizard-core/bin/pan-tools.cjs review-deep merge "${PHASE_NUMBER}" \
502
+ --reviewer-file "{phase_dir}/{phase_number}-review.md" \
503
+ --hardener-file ".planning/reviews/${PHASE_NUMBER}/hardener.md" \
504
+ --meta-file ".planning/reviews/${PHASE_NUMBER}/meta.md"
505
+ ```
506
+ This writes `.planning/reviews/${PHASE_NUMBER}/deep-review.md` and prints its verdict (`ok` · `ok_with_minor` · `fix_before_merge` · `review_required` · `block`). Report the verdict and the finding count. On `review_required` or `block`, stop before verification and present the high and critical findings to the user, as with `NEEDS_FIXES`.
465
507
  </step>
466
508
 
467
509
  <step name="close_parent_artifacts">
@@ -531,31 +573,15 @@ Create verification.md.",
531
573
  )
532
574
  ```
533
575
 
534
- Read status:
576
+ Record the verification and read its status:
535
577
  ```bash
536
- VERIF_STATUS=$(grep "^status:" "$PHASE_DIR"/*-verification.md | cut -d: -f2 | tr -d ' ')
578
+ VERIF_FILE=$(ls "$PHASE_DIR"/*-verification.md 2>/dev/null | head -1)
579
+ VERIF_STATUS=$(node ~/.claude/pan-wizard-core/bin/pan-tools.cjs findings record --phase "${PHASE_NUMBER}" --file "$VERIF_FILE" --raw 2>/dev/null) \
580
+ || VERIF_STATUS=$(grep "^status:" "$VERIF_FILE" | cut -d: -f2 | tr -d ' ')
537
581
  ```
582
+ `findings record` reads the verdict from the verification frontmatter (`status`, `gaps`, `human_verification`, `unrequested`) and adds its findings to the ledger. A re-verification after a fix round is the next attempt: the gaps it no longer reports are closed, and the retry is logged. Recording the same file twice is a no-op, so verify-phase recording it too is safe. The grep fallback keeps the old reading when the record cannot parse the file.
538
583
 
539
- **P-1806 fix (v3.7.8):** the trace event for verify-phase outcome is logged HERE (in exec-phase.md after the verifier returns) rather than only at the end of verify-phase.md. When verification runs inline (orchestrator-as-verifier in auto mode, no separate Task spawn), the trace.log block at the bottom of verify-phase.md never executes — the orchestrator just writes verification.md and continues. By logging here, the event fires regardless of the verification path taken. Surfaced by the wookie autonomous build (v3.7.5–v3.7.7): all 5 phases used inline verification, no `verification_passed` events appeared in the trace despite v3.7.5 adding the trace block to verify-phase.md.
540
-
541
- ```bash
542
- if [ "$VERIF_STATUS" = "passed" ]; then
543
- node ~/.claude/pan-wizard-core/bin/pan-tools.cjs optimize trace log \
544
- --type decision --category verification_passed \
545
- --description "Phase ${PHASE_NUMBER} verification passed (logged from exec-phase, P-1806)" \
546
- --agent pan-verifier --impact minor 2>/dev/null || true
547
- elif [ "$VERIF_STATUS" = "gaps_found" ]; then
548
- node ~/.claude/pan-wizard-core/bin/pan-tools.cjs optimize trace log \
549
- --type error --category verification_gaps \
550
- --description "Phase ${PHASE_NUMBER} verification found gaps" \
551
- --agent pan-verifier --impact major 2>/dev/null || true
552
- elif [ "$VERIF_STATUS" = "human_needed" ]; then
553
- node ~/.claude/pan-wizard-core/bin/pan-tools.cjs optimize trace log \
554
- --type decision --category verification_human_needed \
555
- --description "Phase ${PHASE_NUMBER} verification awaiting human review" \
556
- --agent pan-verifier --impact minor 2>/dev/null || true
557
- fi
558
- ```
584
+ **Trace logging happens in the record.** The record above logs the verification outcome (`verdict_passed`, `verdict_failed` or `verdict_needs_human`) here, after the verifier returns. That covers inline verification too, which is the reason P-1806 moved the trace call here (v3.7.8). No separate `optimize trace log` call is needed.
559
585
 
560
586
  | Status | Action |
561
587
  |--------|--------|
@@ -163,6 +163,8 @@ Report final status.
163
163
  | I001 | info | Plan without SUMMARY (may be in progress) | No |
164
164
  | I002 | info | Phase in ROADMAP ahead of current phase, not planned yet | No |
165
165
  | I003 | info | Focus-model or campaign tree — the phase-model checks do not apply | No |
166
+ | I004 | info | planning-with-files shares the tree — PAN leaves its files alone | No |
167
+ | I005 | info | The planning tree is gitignored while commit_docs is true — planning commits commit nothing | No |
166
168
  | STATE_REQ_DRIFT | warning | state.md complete but REQUIREMENTS.md has unchecked boxes | Yes |
167
169
  | STATE_ROADMAP_DRIFT | warning | state.md complete but roadmap.md has unchecked plan boxes | Yes |
168
170
  | VERIFICATION_GATE_MISSING | warning | Phase has completed plans but no verification record | No |
@@ -224,7 +224,7 @@ The Focus workflow is a **scan → plan → exec → sync** pipeline. Each step
224
224
  | `/pan:plan-phase <N>` | Research + plan a phase with verification |
225
225
  | `/pan:exec-phase <N>` | Execute plans with wave-based parallelization |
226
226
  | `/pan:research-phase <N>` | Deep ecosystem research for specialized domains |
227
- | `/pan:verify-phase <N>` | Validate built features through UAT |
227
+ | `/pan:verify-phase <N>` | Re-run goal-backward verification with a test-suite gate |
228
228
 
229
229
  ### Phase Management
230
230
  | Command | Description |
@@ -348,7 +348,7 @@ The Focus workflow is a **scan → plan → exec → sync** pipeline. Each step
348
348
 
349
349
  **`/pan:exec-phase <N>`** — Execute all plans in a phase. Groups by wave, runs waves sequentially, plans within each wave in parallel.
350
350
 
351
- **`/pan:verify-phase <N>`** — Conversational UAT. Presents tests one at a time, diagnoses failures, creates fix plans.
351
+ **`/pan:verify-phase <N>`** — Re-run goal-backward verification behind a test-suite gate. Writes the phase's verification report (passed, gaps_found or human_needed); fix gaps with `/pan:plan-phase <N> --gaps`.
352
352
 
353
353
  **`/pan:quick <desc>`** — Small tasks with PAN guarantees but shorter path. Skips researcher, checker, verifier. Lives in `.planning/quick/`.
354
354