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
@@ -677,7 +677,7 @@ function buildAgentPluginMcpConfig() {
677
677
  * @param {{updateCheckCommand?:string, contextMonitorCommand?:string, costLoggerCommand?:string, traceLoggerCommand?:string}} commands
678
678
  */
679
679
  function buildCopilotPluginHooksConfig(commands) {
680
- const { updateCheckCommand, contextMonitorCommand, costLoggerCommand, traceLoggerCommand } = commands || {};
680
+ const { updateCheckCommand, contextMonitorCommand, costLoggerCommand, traceLoggerCommand, stopGuardCommand } = commands || {};
681
681
  const hooks = {};
682
682
  if (updateCheckCommand) hooks.SessionStart = [{ type: 'command', command: updateCheckCommand }];
683
683
  if (contextMonitorCommand) hooks.PostToolUse = [{ type: 'command', command: contextMonitorCommand }];
@@ -685,6 +685,8 @@ function buildCopilotPluginHooksConfig(commands) {
685
685
  if (costLoggerCommand) subagentStop.push({ type: 'command', command: costLoggerCommand });
686
686
  if (traceLoggerCommand) subagentStop.push({ type: 'command', command: traceLoggerCommand });
687
687
  if (subagentStop.length > 0) hooks.SubagentStop = subagentStop;
688
+ // Copilot's documented PascalCase alias for agentStop (M14).
689
+ if (stopGuardCommand) hooks.Stop = [{ type: 'command', command: stopGuardCommand }];
688
690
  return { hooks };
689
691
  }
690
692
 
@@ -692,9 +694,15 @@ function buildCopilotPluginHooksConfig(commands) {
692
694
  const COPILOT_PLUGIN_NAMESPACE = 'com.github.copilot';
693
695
 
694
696
  /** The adapter paragraph appended to every bundled skill (ADR-0045 D3). */
695
- function agentPluginSkillAdapterNote() {
697
+ function agentPluginSkillAdapterNote(kind = 'skill') {
698
+ // An agent file (<root>/com.github.copilot/agents/<name>.agent.md) sits as deep as a
699
+ // skill (<root>/skills/<name>/SKILL.md), so the same derivation holds; until
700
+ // 2026-09-26 only the skills carried the note, and the bundled Copilot agents held
701
+ // the token with nothing to say what it meant (3.28.0 review, LOW).
702
+ const what = kind === 'agent' ? 'agent' : 'skill';
703
+ const file = kind === 'agent' ? 'this agent file' : 'this SKILL.md';
696
704
  return `Plugin bundle (Agent Plugins format):
697
- - \`${AGENT_PLUGIN_ROOT_TOKEN}\` in this skill is the directory that holds this plugin's \`plugin.json\` — two levels above this SKILL.md. Your runtime reports this skill's file location when it loads it; derive the root from that path and substitute it wherever \`${AGENT_PLUGIN_ROOT_TOKEN}\` appears before running a command.
705
+ - \`${AGENT_PLUGIN_ROOT_TOKEN}\` in this ${what} is the directory that holds this plugin's \`plugin.json\` — two levels above ${file}. Your runtime reports this ${what}'s file location when it loads it; derive the root from that path and substitute it wherever \`${AGENT_PLUGIN_ROOT_TOKEN}\` appears before running a command.
698
706
  - Prefer the \`pan\` MCP server's tools when your runtime has connected this plugin's \`mcp.json\`, and pass the project's absolute path as each tool's \`cwd\` argument — the server is started in the plugin's directory, which is never the project. Otherwise run \`node ${AGENT_PLUGIN_ROOT_TOKEN}/pan-wizard-core/bin/pan-tools.cjs <verb>\` from the project root.
699
707
  - \`${AGENT_PLUGIN_RUNTIME_HOME_TOKEN}\` is your runtime's user-level configuration directory (for example \`~/.claude\`, \`~/.codex\`, \`~/.gemini\`, \`~/.config/opencode\`, \`~/.copilot\`) and \`${AGENT_PLUGIN_RUNTIME_DIR_TOKEN}\` its project-level directory (\`.claude\`, \`.codex\`, \`.gemini\`, \`.opencode\`, \`.github\`). Substitute the one that applies to the runtime you are.`;
700
708
  }
@@ -742,13 +750,29 @@ function convertClaudeCommandToCopilotSkill(content, skillName) {
742
750
  /**
743
751
  * @param {string} content - Claude agent markdown
744
752
  * @param {object} [opts]
745
- * @param {Record<string,string[]>} [opts.modelLists] - Copilot CLI (>= 1.0.83) accepts a
746
- * `model:` LIST tried in order plus `model-policy`. When a PAN agent pins `model:
747
- * <alias>` and this map has an entry for the alias, the Copilot agent gets that list
748
- * and `model-policy: prefer` (degrade gracefully; `required` would refuse to run).
749
- * NOT wired into the installer yet: the Copilot model ids must be verified on a live
750
- * CLI first (ADR-0028's rule; harness/scenarios/live-gate-copilot.json carries the
751
- * probe). Reality check RC15 / plan item R13, 2026-09-10.
753
+ * Every agent carries `include-custom-instructions: true` (market-ideas M15). Copilot
754
+ * does not give a custom agent spawned as a subagent the repository's instruction
755
+ * files — `copilot-instructions.md`, `AGENTS.md`, `CLAUDE.md` — unless its frontmatter
756
+ * opts in (Copilot CLI reference, "Repository custom instructions for subagents";
757
+ * default false). PAN's rules section lives in AGENTS.md, and PAN's agents run as
758
+ * subagents. Measured 2026-09-26 on Copilot CLI 1.0.88: with an AGENTS.md canary, a
759
+ * subagent with the field answered it and the same agent without the field answered
760
+ * that it had none.
761
+ *
762
+ * @param {Record<string,string[]>} [opts.modelLists] - Copilot CLI accepts a priority
763
+ * list under `models:` ("the runtime uses the first model the user's plan can
764
+ * access; if none resolve, dispatch falls back to the session's model") plus
765
+ * `model-policy`. When a PAN agent pins `model: <alias>` and this map has an entry
766
+ * for the alias, the Copilot agent gets that list and `model-policy: preferred`
767
+ * (degrade gracefully; `required` would refuse to run). Until 2026-09-26 the list
768
+ * was written under the singular `model:` and the policy as `prefer`; the first live
769
+ * probe (Copilot CLI 1.0.88) refused to load that agent — `model-policy: Expected
770
+ * "preferred" or "required"` — and the reference names the list field `models`.
771
+ * Copilot's model ids are dotted (`claude-opus-5.5`; its /models list, same day). A
772
+ * session on `auto` — every Copilot Free session — ignores the list, so the list's
773
+ * fall-through is still unmeasured; NOT wired into the installer until it is, on a
774
+ * paid plan (harness/scenarios/copilot-agent-frontmatter.json). Reality check RC15 /
775
+ * plan item R13, 2026-09-10.
752
776
  */
753
777
  function convertClaudeToCopilotAgent(content, opts = {}) {
754
778
  const converted = convertClaudeToCopilotMarkdown(content);
@@ -806,10 +830,10 @@ function convertClaudeToCopilotAgent(content, opts = {}) {
806
830
  const pinned = extractFrontmatterField(frontmatter, 'model');
807
831
  const list = pinned && Array.isArray(lists[pinned]) ? lists[pinned].filter(Boolean) : null;
808
832
  if (list && list.length) {
809
- modelYaml = `\nmodel:\n${list.map(m => ` - ${yamlQuote(m)}`).join('\n')}\nmodel-policy: prefer`;
833
+ modelYaml = `\nmodels:\n${list.map(m => ` - ${yamlQuote(m)}`).join('\n')}\nmodel-policy: preferred`;
810
834
  }
811
835
  }
812
- return `---\nname: ${yamlQuote(name)}\ndescription: ${yamlQuote(description)}${toolsYaml}${modelYaml}\n---\n${body}`;
836
+ return `---\nname: ${yamlQuote(name)}\ndescription: ${yamlQuote(description)}${toolsYaml}\ninclude-custom-instructions: true${modelYaml}\n---\n${body}`;
813
837
  }
814
838
 
815
839
  // ─── Attribution Processing ─────────────────────────────────────────────────
@@ -1021,7 +1045,7 @@ function stripThinkingFrontmatter(content, runtime) {
1021
1045
  * @returns {Object} A `.github/hooks/pan.json` config object
1022
1046
  */
1023
1047
  function buildCopilotHooksConfig(commands) {
1024
- const { updateCheckCommand, contextMonitorCommand, costLoggerCommand, traceLoggerCommand } = commands || {};
1048
+ const { updateCheckCommand, contextMonitorCommand, costLoggerCommand, traceLoggerCommand, stopGuardCommand } = commands || {};
1025
1049
  const config = { version: 1, hooks: {} };
1026
1050
  if (updateCheckCommand) {
1027
1051
  config.hooks.sessionStart = [{ type: 'command', command: updateCheckCommand }];
@@ -1037,6 +1061,12 @@ function buildCopilotHooksConfig(commands) {
1037
1061
  if (subagentStop.length > 0) {
1038
1062
  config.hooks.subagentStop = subagentStop;
1039
1063
  }
1064
+ // agentStop: the auto-advance stop guard (M14). Copilot blocks a stop only on
1065
+ // the JSON `decision: "block"` output, and ends the turn anyway after eight
1066
+ // consecutive blocks; the guard blocks once per stop chain.
1067
+ if (stopGuardCommand) {
1068
+ config.hooks.agentStop = [{ type: 'command', command: stopGuardCommand }];
1069
+ }
1040
1070
  return config;
1041
1071
  }
1042
1072
 
@@ -1066,20 +1096,38 @@ function buildCopilotHooksConfig(commands) {
1066
1096
  * - copilot: `.github/hooks/pan.json`, camelCase (docs.github.com hooks reference).
1067
1097
  * - opencode: no hook system.
1068
1098
  *
1069
- * `stop` is where the auto-advance stop guard registers (P-1809): Claude's Stop
1070
- * and Gemini's AfterAgent, both of which re-prompt the agent when a hook blocks.
1071
- * Codex and Copilot have a stop event too; PAN does not register the guard there yet.
1099
+ * `stop` is where the auto-advance stop guard registers (P-1809): Claude's Stop,
1100
+ * Gemini's AfterAgent, Codex's Stop and Copilot's agentStop — each re-prompts the
1101
+ * agent when a hook prints `{"decision":"block","reason":…}`, and each hands the
1102
+ * hook `stop_hook_active` (M14, 2026-09-26: codex-rs hooks/schema
1103
+ * stop.command.input.schema.json; Copilot hooks-reference, where the field stays
1104
+ * snake_case in the camelCase agentStop payload). Copilot does not block on exit
1105
+ * code 2 for agentStop — only the JSON form, which is what the guard prints.
1106
+ *
1107
+ * `compact` is where the state re-injection hook registers (M10): SessionStart with
1108
+ * the `compact` matcher (HOOK_SLOT_MATCHERS), the one event after a compaction whose
1109
+ * `hookSpecificOutput.additionalContext` reaches the model on Claude Code and Codex —
1110
+ * both hosts' PostCompact discard it (hooks references, read 2026-09-26). Gemini's
1111
+ * PreCompress fires before the summary and Copilot's preCompact is notification-only,
1112
+ * so neither gets it.
1072
1113
  */
1073
1114
  const HOOK_EVENT_MAP = Object.freeze({
1074
- claude: { surface: 'settings.json', sessionStart: 'SessionStart', postToolUse: 'PostToolUse', subagentStop: 'SubagentStop', stop: 'Stop' },
1075
- gemini: { surface: 'settings.json', sessionStart: 'SessionStart', postToolUse: null, subagentStop: null, stop: 'AfterAgent' },
1076
- codex: { surface: 'hooks.json', sessionStart: 'SessionStart', postToolUse: 'PostToolUse', subagentStop: 'SubagentStop', stop: null },
1077
- copilot: { surface: 'hooks/pan.json', sessionStart: 'sessionStart', postToolUse: 'postToolUse', subagentStop: 'subagentStop', stop: null },
1115
+ claude: { surface: 'settings.json', sessionStart: 'SessionStart', postToolUse: 'PostToolUse', subagentStop: 'SubagentStop', stop: 'Stop', compact: 'SessionStart' },
1116
+ gemini: { surface: 'settings.json', sessionStart: 'SessionStart', postToolUse: null, subagentStop: null, stop: 'AfterAgent', compact: null },
1117
+ codex: { surface: 'hooks.json', sessionStart: 'SessionStart', postToolUse: 'PostToolUse', subagentStop: 'SubagentStop', stop: 'Stop', compact: 'SessionStart' },
1118
+ copilot: { surface: 'hooks/pan.json', sessionStart: 'sessionStart', postToolUse: 'postToolUse', subagentStop: 'subagentStop', stop: 'agentStop', compact: null },
1078
1119
  opencode: null,
1079
1120
  });
1080
1121
 
1122
+ /**
1123
+ * The matcher a slot's registration carries, where the event alone is too broad.
1124
+ * `compact` is SessionStart narrowed to the start that follows a compaction — the
1125
+ * `source: "compact"` matcher value on Claude Code and Codex alike.
1126
+ */
1127
+ const HOOK_SLOT_MATCHERS = Object.freeze({ compact: 'compact' });
1128
+
1081
1129
  /** The hook scripts PAN registers in a Claude-shaped settings.json `hooks` block. */
1082
- const PAN_SETTINGS_HOOKS = Object.freeze(['pan-check-update', 'pan-context-monitor', 'pan-cost-logger', 'pan-trace-logger', 'pan-stop-guard']);
1130
+ const PAN_SETTINGS_HOOKS = Object.freeze(['pan-check-update', 'pan-context-monitor', 'pan-cost-logger', 'pan-trace-logger', 'pan-stop-guard', 'pan-state-reinject']);
1083
1131
 
1084
1132
  /**
1085
1133
  * Remove, from every event array of a Claude-shaped `hooks` object except
@@ -1263,7 +1311,7 @@ function buildCodexMcpSnippet(serverPath, panToolsPath, projectRoot, serverName
1263
1311
  * @returns {object} Merged config object to serialize back to hooks.json
1264
1312
  */
1265
1313
  function mergeCodexHooksConfig(existing, commands) {
1266
- const { updateCheckCommand, contextMonitorCommand, costLoggerCommand, traceLoggerCommand } = commands || {};
1314
+ const { updateCheckCommand, contextMonitorCommand, costLoggerCommand, traceLoggerCommand, stopGuardCommand, stateReinjectCommand } = commands || {};
1267
1315
  const config = (existing && typeof existing === 'object') ? existing : {};
1268
1316
  if (!config.hooks || typeof config.hooks !== 'object') config.hooks = {};
1269
1317
 
@@ -1276,6 +1324,10 @@ function mergeCodexHooksConfig(existing, commands) {
1276
1324
  // - check-update spawns a detached child and prints nothing → async.
1277
1325
  // - context-monitor returns `additionalContext` the model must see THIS
1278
1326
  // turn → stays synchronous.
1327
+ // - stop-guard decides whether the turn may end (`decision: "block"`), which
1328
+ // an async handler cannot do → synchronous (M14).
1329
+ // - state-reinject returns `additionalContext` for the continuation after a
1330
+ // compaction → synchronous, and matched to `source: "compact"` (M10).
1279
1331
  // Codex-only: Claude Code and Copilot hook schemas were not checked for an
1280
1332
  // equivalent flag (plan item 2 gate) — do not copy this column into their
1281
1333
  // builders without reading their docs first.
@@ -1284,9 +1336,11 @@ function mergeCodexHooksConfig(existing, commands) {
1284
1336
  ['PostToolUse', contextMonitorCommand, 'pan-context-monitor', false],
1285
1337
  ['SubagentStop', costLoggerCommand, 'pan-cost-logger', true],
1286
1338
  ['SubagentStop', traceLoggerCommand, 'pan-trace-logger', true],
1339
+ ['Stop', stopGuardCommand, 'pan-stop-guard', false],
1340
+ ['SessionStart', stateReinjectCommand, 'pan-state-reinject', false, HOOK_SLOT_MATCHERS.compact],
1287
1341
  ];
1288
1342
 
1289
- for (const [event, command, marker, async] of wanted) {
1343
+ for (const [event, command, marker, async, matcher] of wanted) {
1290
1344
  if (!command) continue;
1291
1345
  if (!Array.isArray(config.hooks[event])) config.hooks[event] = [];
1292
1346
  let existingHandler = null;
@@ -1306,7 +1360,7 @@ function mergeCodexHooksConfig(existing, commands) {
1306
1360
  }
1307
1361
  const handler = { type: 'command', command };
1308
1362
  if (async) handler.async = true;
1309
- config.hooks[event].push({ hooks: [handler] });
1363
+ config.hooks[event].push({ ...(matcher ? { matcher } : {}), hooks: [handler] });
1310
1364
  }
1311
1365
  return config;
1312
1366
  }
@@ -1322,7 +1376,7 @@ function removeCodexPanHooks(existing) {
1322
1376
  for (const event of Object.keys(existing.hooks)) {
1323
1377
  if (!Array.isArray(existing.hooks[event])) continue;
1324
1378
  existing.hooks[event] = existing.hooks[event].filter(group =>
1325
- !(Array.isArray(group.hooks) && group.hooks.some(h => h.command && /pan-(check-update|context-monitor|cost-logger|trace-logger)/.test(h.command))));
1379
+ !(Array.isArray(group.hooks) && group.hooks.some(h => h.command && /pan-(check-update|context-monitor|cost-logger|trace-logger|stop-guard|state-reinject)/.test(h.command))));
1326
1380
  if (existing.hooks[event].length === 0) delete existing.hooks[event];
1327
1381
  }
1328
1382
  if (Object.keys(existing.hooks).length === 0) delete existing.hooks;
@@ -1395,6 +1449,8 @@ function codexTrustNotice() {
1395
1449
  'installed pan-* agents) only loads once the project is trusted in Codex.',
1396
1450
  'If commands or agents seem missing, approve the project when Codex prompts,',
1397
1451
  'or set trust_level for this path in ~/.codex/config.toml.',
1452
+ 'Codex also skips new or changed hooks until you trust them: after installing',
1453
+ 'or upgrading PAN, run /hooks in Codex and trust the pan-* hooks.',
1398
1454
  ].join('\n');
1399
1455
  }
1400
1456
 
@@ -1688,20 +1744,26 @@ function buildPluginManifest(pkg) {
1688
1744
  }
1689
1745
 
1690
1746
  /**
1691
- * Build the plugin hooks/hooks.json — PAN's four hooks registered with
1692
- * ${CLAUDE_PLUGIN_ROOT}-anchored commands (the documented plugin-relative
1693
- * path convention for hook configs).
1747
+ * Build the plugin hooks/hooks.json — every hook a Claude settings.json install
1748
+ * registers (PAN_SETTINGS_HOOKS), with ${CLAUDE_PLUGIN_ROOT}-anchored commands
1749
+ * (the documented plugin-relative path convention for hook configs). The stop
1750
+ * guard was missing until R43 (2026-09-26), so an autonomous chain run from the
1751
+ * plugin could drop at a phase boundary with nothing to catch it.
1694
1752
  * @returns {object} hooks.json object
1695
1753
  */
1696
1754
  function buildPluginHooksConfig() {
1697
- const hook = (script) => ({
1755
+ const hook = (script, matcher) => ({
1756
+ ...(matcher ? { matcher } : {}),
1698
1757
  hooks: [{ type: 'command', command: `node "\${CLAUDE_PLUGIN_ROOT}/hooks/${script}"` }],
1699
1758
  });
1759
+ const events = HOOK_EVENT_MAP.claude;
1700
1760
  return {
1701
1761
  hooks: {
1702
- SessionStart: [hook('pan-check-update.js')],
1703
- PostToolUse: [hook('pan-context-monitor.js')],
1704
- SubagentStop: [hook('pan-cost-logger.js'), hook('pan-trace-logger.js')],
1762
+ // compact shares SessionStart with the update check, narrowed by its matcher (M10).
1763
+ [events.sessionStart]: [hook('pan-check-update.js'), hook('pan-state-reinject.js', HOOK_SLOT_MATCHERS.compact)],
1764
+ [events.postToolUse]: [hook('pan-context-monitor.js')],
1765
+ [events.subagentStop]: [hook('pan-cost-logger.js'), hook('pan-trace-logger.js')],
1766
+ [events.stop]: [hook('pan-stop-guard.js')],
1705
1767
  },
1706
1768
  };
1707
1769
  }
@@ -2305,6 +2367,7 @@ module.exports = {
2305
2367
  // Copilot CLI hooks config (2026-06)
2306
2368
  buildCopilotHooksConfig,
2307
2369
  HOOK_EVENT_MAP,
2370
+ HOOK_SLOT_MATCHERS,
2308
2371
  PAN_SETTINGS_HOOKS,
2309
2372
  stripPanHookEntries,
2310
2373
  mergeCodexHooksConfig,
@@ -2364,7 +2427,9 @@ function dirDigest(dir) {
2364
2427
  const crypto = require('crypto');
2365
2428
  const lines = [];
2366
2429
  const walk = (abs, rel) => {
2367
- const entries = fs.readdirSync(abs, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
2430
+ // Code-unit order, not localeCompare: the digest compares builds across machines,
2431
+ // and a locale-dependent sort could order the same tree two ways (3.28.0 review, LOW).
2432
+ const entries = fs.readdirSync(abs, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
2368
2433
  for (const e of entries) {
2369
2434
  const childAbs = path.join(abs, e.name);
2370
2435
  const childRel = rel ? `${rel}/${e.name}` : e.name;
@@ -2376,3 +2441,34 @@ function dirDigest(dir) {
2376
2441
  return crypto.createHash('sha256').update(lines.join('\n')).digest('hex');
2377
2442
  }
2378
2443
  module.exports.dirDigest = dirDigest;
2444
+
2445
+ /**
2446
+ * Whether `buf` is the file a manifest hash was taken from, allowing for line
2447
+ * endings. The manifest records sha256 over raw bytes, so a file git re-checked
2448
+ * out with `core.autocrlf=true` — or one an editor saved with the other line
2449
+ * ending — hashed as a user edit, and every upgrade backed it up to
2450
+ * pan-local-patches/ as a "locally modified PAN file" with nothing to merge (the
2451
+ * 3.30.0 upgrade sweep: 8 such files in three projects, 357 in one that commits
2452
+ * `.claude/` to git). A file matches when its raw bytes, its all-LF form or its
2453
+ * all-CRLF form hash to `expectedHash`, so existing manifests keep working
2454
+ * whichever line ending they were written with. A buffer holding a NUL byte is
2455
+ * treated as binary and compared raw only. Pure. `validateRuntimeInstall` in
2456
+ * pan-wizard-core/bin/lib/verify-deploy.cjs carries the same rule (the installed
2457
+ * core cannot require this file); tests/install-eol-hash.test.cjs pins both.
2458
+ * @param {Buffer} buf
2459
+ * @param {string|null|undefined} expectedHash - hex sha256 from the manifest
2460
+ * @returns {boolean}
2461
+ */
2462
+ function bytesMatchHashIgnoringEol(buf, expectedHash) {
2463
+ if (!expectedHash) return false;
2464
+ const crypto = require('crypto');
2465
+ const sha = b => crypto.createHash('sha256').update(b).digest('hex');
2466
+ if (sha(buf) === expectedHash) return true;
2467
+ if (buf.includes(0)) return false;
2468
+ // latin1 maps every byte to one char and back, so the round trip is lossless;
2469
+ // no UTF-8 multi-byte sequence contains 0x0D or 0x0A.
2470
+ const lf = buf.toString('latin1').replace(/\r\n/g, '\n');
2471
+ if (sha(Buffer.from(lf, 'latin1')) === expectedHash) return true;
2472
+ return sha(Buffer.from(lf.replace(/\n/g, '\r\n'), 'latin1')) === expectedHash;
2473
+ }
2474
+ module.exports.bytesMatchHashIgnoringEol = bytesMatchHashIgnoringEol;
package/bin/install.js CHANGED
@@ -1218,7 +1218,7 @@ function uninstall(isGlobal, runtime = 'claude') {
1218
1218
  // 4. Remove PAN hooks (scripts + Copilot CLI hooks config file)
1219
1219
  const hooksDir = path.join(targetDir, 'hooks');
1220
1220
  if (fs.existsSync(hooksDir)) {
1221
- const panHooks = ['pan-statusline.js', 'pan-check-update.js', 'pan-check-update.sh', 'pan-context-monitor.js', 'pan-cost-logger.js', 'pan-trace-logger.js', 'pan-stop-guard.js', 'pan.json'];
1221
+ const panHooks = ['pan-statusline.js', 'pan-check-update.js', 'pan-check-update.sh', 'pan-context-monitor.js', 'pan-cost-logger.js', 'pan-trace-logger.js', 'pan-stop-guard.js', 'pan-state-reinject.js', 'pan.json'];
1222
1222
  let hookCount = 0;
1223
1223
  for (const hook of panHooks) {
1224
1224
  const hookPath = path.join(hooksDir, hook);
@@ -2014,8 +2014,10 @@ function saveLocalPatches(configDir) {
2014
2014
 
2015
2015
  const fullPath = path.join(configDir, relPath);
2016
2016
  if (!fs.existsSync(fullPath)) continue;
2017
- const currentHash = fileHash(fullPath);
2018
- if (currentHash !== originalHash) {
2017
+ // A line-ending-only difference is not a user edit (git autocrlf, editors).
2018
+ let current;
2019
+ try { current = fs.readFileSync(fullPath); } catch { continue; }
2020
+ if (!lib.bytesMatchHashIgnoringEol(current, originalHash)) {
2019
2021
  const backupPath = path.join(patchesDir, safeRel);
2020
2022
  // Defence in depth: never write outside patchesDir, whatever the manifest says.
2021
2023
  const resolvedPatches = path.resolve(patchesDir);
@@ -2739,6 +2741,9 @@ function install(isGlobal, runtime = 'claude') {
2739
2741
  const stopGuardCommand = isGlobal
2740
2742
  ? buildHookCommand(targetDir, 'pan-stop-guard.js')
2741
2743
  : 'node ' + dirName + '/hooks/pan-stop-guard.js';
2744
+ const stateReinjectCommand = isGlobal
2745
+ ? buildHookCommand(targetDir, 'pan-state-reinject.js')
2746
+ : 'node ' + dirName + '/hooks/pan-state-reinject.js';
2742
2747
 
2743
2748
  if (isCodex) {
2744
2749
  // Codex hooks (2026-06): Claude-compatible PascalCase events in the shared
@@ -2750,13 +2755,13 @@ function install(isGlobal, runtime = 'claude') {
2750
2755
  let existing = null;
2751
2756
  try { existing = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8')); } catch { /* absent or invalid — start fresh */ }
2752
2757
  const merged = lib.mergeCodexHooksConfig(existing, {
2753
- updateCheckCommand, contextMonitorCommand, costLoggerCommand, traceLoggerCommand,
2758
+ updateCheckCommand, contextMonitorCommand, costLoggerCommand, traceLoggerCommand, stopGuardCommand, stateReinjectCommand,
2754
2759
  });
2755
2760
  fs.writeFileSync(hooksJsonPath, JSON.stringify(merged, null, 2) + '\n');
2756
2761
  // Print the path we actually wrote. The hardcoded '.codex/hooks.json' was
2757
2762
  // wrong for --global and for --config-dir, telling users to inspect a file
2758
2763
  // that does not exist while the real one sat elsewhere.
2759
- console.log(` ${green}✓${reset} Configured hooks (${displayPath(hooksJsonPath)}: update check, context monitor, cost + trace loggers)`);
2764
+ console.log(` ${green}✓${reset} Configured hooks (${displayPath(hooksJsonPath)}: update check, context monitor, cost + trace loggers, stop guard, state re-injection)`);
2760
2765
  } catch (e) {
2761
2766
  pushInstallWarning('codexHooks', 'hooks.json', e);
2762
2767
  }
@@ -2768,12 +2773,12 @@ function install(isGlobal, runtime = 'claude') {
2768
2773
  // from any legacy config.json registration.
2769
2774
  if (isCopilot) {
2770
2775
  const hooksConfigPath = path.join(targetDir, 'hooks', 'pan.json');
2771
- const hooksConfig = buildCopilotHooksConfig({ updateCheckCommand, contextMonitorCommand, costLoggerCommand, traceLoggerCommand });
2776
+ const hooksConfig = buildCopilotHooksConfig({ updateCheckCommand, contextMonitorCommand, costLoggerCommand, traceLoggerCommand, stopGuardCommand });
2772
2777
  try {
2773
2778
  fs.mkdirSync(path.dirname(hooksConfigPath), { recursive: true });
2774
2779
  fs.writeFileSync(hooksConfigPath, JSON.stringify(hooksConfig, null, 2) + '\n');
2775
2780
  // Same as the Codex case: a global Copilot install writes to ~/.copilot/, not .github/.
2776
- console.log(` ${green}✓${reset} Configured hooks (${displayPath(hooksConfigPath)}: update check, context monitor, cost + trace loggers)`);
2781
+ console.log(` ${green}✓${reset} Configured hooks (${displayPath(hooksConfigPath)}: update check, context monitor, cost + trace loggers, stop guard)`);
2777
2782
  } catch (e) {
2778
2783
  console.error(` ${yellow}✗${reset} Failed to write Copilot hooks config: ${e.message}`);
2779
2784
  }
@@ -2866,6 +2871,7 @@ function install(isGlobal, runtime = 'claude') {
2866
2871
  { slot: 'subagentStop', hook: 'pan-cost-logger', command: costLoggerCommand, label: 'cost logger hook' },
2867
2872
  { slot: 'subagentStop', hook: 'pan-trace-logger', command: traceLoggerCommand, label: 'trace logger hook' },
2868
2873
  { slot: 'stop', hook: 'pan-stop-guard', command: stopGuardCommand, label: 'auto-advance stop guard hook' },
2874
+ { slot: 'compact', hook: 'pan-state-reinject', command: stateReinjectCommand, label: 'state re-injection hook (after compaction)' },
2869
2875
  ];
2870
2876
  const unsupported = [];
2871
2877
  for (const r of registrations) {
@@ -2888,7 +2894,8 @@ function install(isGlobal, runtime = 'claude') {
2888
2894
  entry.hooks && entry.hooks.some(h => h.command && h.command.includes(r.hook))
2889
2895
  );
2890
2896
  if (!registered) {
2891
- settings.hooks[event].push({ hooks: [{ type: 'command', command: r.command }] });
2897
+ const matcher = lib.HOOK_SLOT_MATCHERS[r.slot];
2898
+ settings.hooks[event].push({ ...(matcher ? { matcher } : {}), hooks: [{ type: 'command', command: r.command }] });
2892
2899
  console.log(` ${green}✓${reset} Configured ${r.label}`);
2893
2900
  }
2894
2901
  }
@@ -2896,7 +2903,7 @@ function install(isGlobal, runtime = 'claude') {
2896
2903
  delete settings.hooks;
2897
2904
  }
2898
2905
  if (isGemini && unsupported.length > 0) {
2899
- console.log(` ${dim}ℹ Gemini CLI has no context-window metric for hooks and no subagent-completion event, so ${unsupported.join(', ')} ${unsupported.length === 1 ? 'is' : 'are'} not registered there${reset}`);
2906
+ console.log(` ${dim}ℹ Gemini CLI has no context-window metric for hooks, no subagent-completion event and no post-compaction event that adds context, so ${unsupported.join(', ')} ${unsupported.length === 1 ? 'is' : 'are'} not registered there${reset}`);
2900
2907
  }
2901
2908
  }
2902
2909
 
@@ -85,14 +85,14 @@ Every cap the conductor enforces applies to the campaign, scaled up:
85
85
  | `--source` | `backlog` | Work selection (delegates to focus-auto): `backlog` = ranked roadmap/requirements items; `scan` = category code-scan. |
86
86
  | `--max-cycles` | 5 | Mission items landed before stopping. |
87
87
  | `--total-budget` | 300 | Cumulative point budget. **Advisory by default** — tracked/surfaced but not a hard stop unless `--enforce-budget` / config `budget.enforce: true`. |
88
- | `--enforce-budget` | off | Make `--total-budget` a hard stop again (also settable via config `budget.enforce: true`); `--daily-budget` is enforced only by `enforce_budget: true` in `schedule.json`. |
88
+ | `--enforce-budget` | off | Make `--total-budget` a hard stop again (also settable via config `budget.enforce: true`). With `--schedule`, also arm the campaign with `pan-tools campaign schedule --enforce-budget`, so `--daily-budget` pauses the day's run once spent. |
89
89
  | `--verify-reserve` | 0.15 | Fraction of `--total-budget` (0–0.5) held back for the final Quality re-review so it can't be starved (config `budget.verify_reserve`). Advisory by default (surfaced as `into_verify_reserve`); under `--enforce-budget` it stops taking on new missions early (`budget_reserve_reached`) and the reserved points fund the closing `--clean-seal` re-verification before the last INTEGRATE. |
90
90
  | `--squads` | all | Restrict to a subset, e.g. `--squads architecture,build,quality`. |
91
91
  | `--no-build-worktrees` | off | Build in the main tree instead of branch-per-agent worktrees (small/serial projects). |
92
92
  | `--push` | off | Push approved merges to origin (still human-gated). |
93
93
  | `--clean-seal` | off | One clean build + full verification after the last item (commands from config). |
94
94
  | `--schedule` | off | Arm a self-resuming campaign at this cadence (`hourly`/`daily`/`weekly`/`Nh`/`Nd`) instead of running once — writes the schedule descriptor (ADR-0034). Pair with `--daily-budget`. |
95
- | `--daily-budget` | 300 | Per-day point budget for a scheduled campaign. Advisory by default (an indicator of the day's spend); it only pauses the day's run when `enforce_budget: true` is set by hand in the schedule descriptor (`schedule.json`); no flag or config key sets it. |
95
+ | `--daily-budget` | 300 | Per-day point budget for a scheduled campaign. Advisory by default (an indicator of the day's spend); it pauses the day's run only when the schedule is armed with `--enforce-budget` (`enforce_budget: true` in `schedule.json`). |
96
96
  | `--dry-run` | off | Plan + squad delegation preview only; STOP. |
97
97
  | `--continue` / `--stop` / `--status` | — | Resume / halt / report from `.planning/orchestration/` + focus-auto state. |
98
98
 
@@ -163,10 +163,10 @@ PAN is not a daemon — it cannot wake itself while the session is closed. `--sc
163
163
  - **Arm:** `/pan:army "<goal>" --schedule daily --daily-budget 200` writes `.planning/orchestration/schedule.json` (cadence, daily budget, next-due) instead of running once.
164
164
  - **The trigger (you wire one):** a host scheduler (Claude Code routines / cron / scheduled-tasks) or a `/loop` runs `pan-tools campaign due` and, when it reports due, invokes `/pan:army --continue`. On next session open, a due campaign is surfaced as a nudge.
165
165
  - **Resume (`--continue`):** read the schedule + `.planning/orchestration/` + focus-auto state. If `campaign due` is true and the day's `--daily-budget` isn't spent, run the next mission(s), then `campaign record-run` (advances next-due, accrues the day's spend). If not due or budget-spent, report next-due and STOP.
166
- - **Bounded spend:** point budgets (`--total-budget`, `--daily-budget`) are **advisory indicators by default** — they're tracked and surfaced, not hard stops. `--enforce-budget` (or config `budget.enforce`) makes `--total-budget` a hard stop; a scheduled campaign's `--daily-budget` pauses the day's run only when `enforce_budget: true` is set by hand in `schedule.json`. The real bounds are `--max-cycles`, the conductor caps, the abort file, and the human merge gate at every integrate. A scheduled campaign runs the backlog down to staged, reviewed, green PRs over days.
166
+ - **Bounded spend:** point budgets (`--total-budget`, `--daily-budget`) are **advisory indicators by default** — they're tracked and surfaced, not hard stops. `--enforce-budget` (or config `budget.enforce`) makes `--total-budget` a hard stop; a scheduled campaign's `--daily-budget` pauses the day's run only when the schedule was armed with `--enforce-budget` (`campaign schedule --enforce-budget`; `--advisory-budget` turns it back off). The real bounds are `--max-cycles`, the conductor caps, the abort file, and the human merge gate at every integrate. A scheduled campaign runs the backlog down to staged, reviewed, green PRs over days.
167
167
  - **Verify reserve:** a fraction of `--total-budget` (`--verify-reserve`, default 0.15) is held back so the closing Quality re-review isn't starved. Surfaced always via `campaign status` / `focus auto --status` (`into_verify_reserve`, `new_work_budget_remaining`); under `--enforce-budget` the run stops taking on new missions early (`budget_reserve_reached`) and spends the reserve on the final `--clean-seal` verification before the last INTEGRATE.
168
168
 
169
- Manage it: `pan-tools campaign status` (active/paused, spent today, next-due), `campaign schedule --pause` / `--resume` / `--disable`.
169
+ Manage it: `pan-tools campaign status` (active/paused, spent today, next-due), `campaign schedule --pause` / `--resume` / `--disable`, and `--enforce-budget` / `--advisory-budget` for the daily budget.
170
170
 
171
171
  ---
172
172
 
@@ -58,6 +58,14 @@ ELSE (default):
58
58
  - If it finds gaps (iteration 1) → `pan-designer` revises (address genuine gaps only), re-check.
59
59
  - If it finds gaps (iteration 2) → final revision, record remaining gaps as caveats.
60
60
  - **Max 2 revision iterations** (design → check → revise → check → final) — the same guardrail `plan-phase` uses with `pan-plan-checker`.
61
+ - **Record each check.** Save the checker's returned text verbatim to `{phase_dir}/{padded_phase}-design-check.md` with the Write tool. Record it and branch on the verdict it prints (`PASS` or `GAPS`):
62
+ ```bash
63
+ node ~/.claude/pan-wizard-core/bin/pan-tools.cjs findings record --phase "{phase}" --agent pan-design-checker --file "{phase_dir}/{padded_phase}-design-check.md" --raw
64
+ ```
65
+ A re-check is the next attempt and closes the gaps it no longer reports. When the final iteration still has gaps, record them as the caveats they are before writing the design:
66
+ ```bash
67
+ node ~/.claude/pan-wizard-core/bin/pan-tools.cjs findings dispose --phase "{phase}" --agent pan-design-checker --open --as deferred --reason "design caveat after 2 revision iterations"
68
+ ```
61
69
  5. **Write** `{phase}-design.md` to the phase directory and commit via `pan-tools commit`.
62
70
  6. **Present** results + next step (`/pan:plan-phase` — which will consume this design.md).
63
71
  </process>
@@ -61,7 +61,7 @@ Phase: $ARGUMENTS
61
61
  - `--skip-tests` — Skip automatic test generation after execution completes.
62
62
  - `--skip-review` — Skip automatic code review after execution completes.
63
63
  - `--fast` — Skip both test generation and code review (implies `--skip-tests --skip-review`).
64
- - `--deep-review` (v3.4+) — After the normal reviewer step, also run `/pan:review-deep <phase>` (security audit via pan-hardener + cross-check via pan-meta-reviewer). Produces `.planning/reviews/<N>/deep-review.md`. Recommended for phases touching auth, payment, PII, migrations, or public APIs. Costs roughly 3× a normal review.
64
+ - `--deep-review` — After the normal code review, run the deep review inline: the `/pan:review-deep` process, a security audit by pan-hardener plus a cross-check by pan-meta-reviewer, merged into `.planning/reviews/<N>/deep-review.md`. It builds on the normal review, so it is skipped with `--skip-review` or `--fast`. Recommended for phases touching auth, payment, PII, migrations, or public APIs. Costs roughly 3× a normal review.
65
65
  - `--hierarchical` (v3.4+, Claude Code only — needs native sub-agent spawning) — Spawn `pan-conductor` as a top-level orchestrator that decomposes the phase and spawns executor/reviewer/verifier sub-agents in sequence. `pan-conductor` runs on the reasoning tier under the default profile, which inherits the model you launched with (the `budget` profile drops it to mid). Bounded by safety harness: max 2 nesting levels, 12 spawns per phase, budget ceiling, `.planning/orchestration/abort` kill-switch. On runtimes that cannot spawn nested agents, this flag is a no-op with a warning and falls back to flat exec. Use only for large phases (≥4 autonomous plans) where wall-clock reduction justifies the ~20-30% orchestration tax.
66
66
 
67
67
  Context files are resolved inside the workflow via `pan-tools init execute-phase` and per-subagent `<files_to_read>` blocks.
@@ -13,7 +13,7 @@ allowed-tools:
13
13
  ---
14
14
 
15
15
  <objective>
16
- Phase-aware git workflow with safety guardrails built in. Every subcommand that modifies history runs safety checks. Rollback uses PAN snapshot tags created by exec-phase.
16
+ Phase-aware git workflow with safety guardrails built in. Every subcommand that modifies history runs safety checks. Rollback uses PAN snapshot tags (`pan-rollback-*`), which `pan-tools rollback-snapshot <phase>` creates; no workflow creates them for you.
17
17
 
18
18
  Works with any git repository — PAN installation not required.
19
19
  </objective>
@@ -185,7 +185,7 @@ node ~/.claude/pan-wizard-core/bin/pan-tools.cjs git rollback --tag pan-rollback
185
185
  ```
186
186
 
187
187
  **Rollback workflow:**
188
- 1. Lists all `pan-rollback-*` tags (created by exec-phase before wave execution)
188
+ 1. Lists all `pan-rollback-*` tags (created with `pan-tools rollback-snapshot <phase>`)
189
189
  2. Verifies working tree is clean (blocks on dirty tree unless `--dry-run`)
190
190
  3. Runs `git reset --hard <tag>`
191
191
 
@@ -18,12 +18,14 @@ Analyze the most recent trace session and generate an optimization report.
18
18
  ```
19
19
  /pan:learn
20
20
  /pan:learn --session <session-id>
21
+ /pan:learn --sessions 3
21
22
  /pan:learn --experiment <slug>
22
23
  /pan:learn --apply
23
24
  ```
24
25
 
25
26
  **Flags:**
26
27
  - `--session <id>` — analyze a specific session instead of the most recent
28
+ - `--sessions <n>` — pool the last n sessions into one analysis, so recommendations rest on failures that recur across runs
27
29
  - `--experiment <slug>` *(v3.7.0+, W3)* — analyze a harvested experiment instead of the current project's traces. Reads from `<source-repo>/experiments/<slug>/.planning/optimization/` and writes the report to `<source-repo>/experiments/<slug>/learnings/report-<timestamp>.md`. Used by the self-improvement loop. Run `/pan:experiment harvest <slug>` first.
28
30
  - `--apply` — automatically apply safe optimizations after generating the report (equivalent to running `/pan:optimize apply` immediately after)
29
31
 
@@ -19,6 +19,8 @@ Manage the circular optimization loop: apply recommendations, view stats, list r
19
19
  ```
20
20
  /pan:optimize apply
21
21
  /pan:optimize apply --report <filename>
22
+ /pan:optimize revert <apply_id>
23
+ /pan:optimize revert --last
22
24
  /pan:optimize list
23
25
  /pan:optimize stats
24
26
  /pan:optimize trace init [--description "what you're building"]
@@ -42,7 +44,17 @@ Requires human review (never auto-applied):
42
44
  - Workflow step additions
43
45
  - Structural changes to commands
44
46
 
45
- After applying, the report lists what was applied and what still needs review.
47
+ After applying, the report lists what was applied and what still needs review, plus the `apply_id` that undoes it. Every apply is recorded action by action in `.planning/optimization/applied.jsonl`: the path, whether the file was created or appended to, the exact text, and a hash of the file after the write. Applying the same report a second time writes nothing: each action names the apply that already wrote it.
48
+
49
+ ### revert
50
+ Undo one apply exactly: delete the memory files it created and cut the text it appended (`revert --last` for the newest).
51
+
52
+ Revert never destroys work someone did since:
53
+ - It refuses a file whose content changed after the apply. The comparison ignores line endings, so a CRLF checkout still matches.
54
+ - It refuses a file that a later apply also wrote. Revert that later apply first.
55
+ - It says which files it refused, and why.
56
+
57
+ A CRLF file comes back CRLF. Applies logged before apply records existed cannot be reverted. After a revert, the same report can be applied again.
46
58
 
47
59
  ### list
48
60
  List all optimization reports in `.planning/optimization/reports/`, most recent first.
@@ -52,7 +64,7 @@ Show cumulative optimization statistics:
52
64
  - Total trace sessions run
53
65
  - Total events traced
54
66
  - Total errors/gaps/redundancies seen
55
- - Total optimizations applied across all runs
67
+ - Total optimizations applied across all runs, the apply runs, the reverted runs, and the last `apply_id`
56
68
  - Current active trace session (if any)
57
69
 
58
70
  ### trace init
@@ -38,7 +38,7 @@ Consolidates Spec B v1's X-4 (self-review) + X-12 (harden) into a single command
38
38
  /pan:review-deep 07
39
39
  ```
40
40
 
41
- Run after `/pan:exec-phase 07` completes. Requires `pan-reviewer` to have already written its review to `.planning/phases/07/review.md` (exec-phase does this automatically).
41
+ Run after `/pan:exec-phase 07` completes. It reads the review that exec-phase's code-review step saved in the phase directory (`.planning/phases/07-<slug>/07-review.md`). The reviewer is read-only, so it is exec-phase that writes the review there.
42
42
 
43
43
  ### Integrated with exec-phase
44
44
 
@@ -46,7 +46,7 @@ Run after `/pan:exec-phase 07` completes. Requires `pan-reviewer` to have alread
46
46
  /pan:exec-phase 07 --deep-review
47
47
  ```
48
48
 
49
- Runs the normal exec → reviewer pipeline, then auto-invokes this command. Recommended for phases touching auth, payment, PII, migrations, or public APIs.
49
+ Runs the normal exec → reviewer pipeline, then runs this command's process inline, before the verifier. Recommended for phases touching auth, payment, PII, migrations, or public APIs.
50
50
 
51
51
  ### Integrated with focus-exec
52
52
 
@@ -60,7 +60,7 @@ Per-item deep review during focus campaigns. Useful for high-stakes batches.
60
60
 
61
61
  <process>
62
62
 
63
- 1. **Load reviewer output** — read `.planning/phases/<N>/review.md` written by the earlier `pan-reviewer` step. If missing, warn and offer to run `pan-reviewer` first.
63
+ 1. **Load reviewer output.** Resolve the phase with `pan-tools find-phase <N>`, then read `{directory}/{phase_number}-review.md`, which exec-phase's code-review step wrote. If it is missing, warn and offer to run `pan-reviewer` first. Its findings come from the report's closing `pan-verdict` block.
64
64
 
65
65
  2. **Spawn pan-hardener** (parallel-safe with step 3 isolation below, but recommended sequential for audit clarity):
66
66
  - Prompt includes: `<files_to_read>` with phase plan + diff + reviewer output; `<output_path>` = `.planning/reviews/<N>/hardener.md`; `<framework_scope>` block reminding of OWASP/STRIDE coverage.
@@ -73,7 +73,7 @@ Per-item deep review during focus campaigns. Useful for high-stakes batches.
73
73
  4. **Merge** — call:
74
74
  ```
75
75
  pan-tools review-deep merge <N> \
76
- --reviewer-file .planning/phases/<N>/review.md \
76
+ --reviewer-file {directory}/{phase_number}-review.md \
77
77
  --hardener-file .planning/reviews/<N>/hardener.md \
78
78
  --meta-file .planning/reviews/<N>/meta.md
79
79
  ```
@@ -99,7 +99,7 @@ Verdict is driven by the highest-severity finding across all three sources. Meta
99
99
 
100
100
  <output_files>
101
101
 
102
- - `.planning/phases/<N>/review.md` — pan-reviewer output (written earlier by exec-phase)
102
+ - `{phase_dir}/{phase_number}-review.md` — pan-reviewer output (written by exec-phase's code-review step)
103
103
  - `.planning/reviews/<N>/hardener.md` — pan-hardener output (new)
104
104
  - `.planning/reviews/<N>/meta.md` — pan-meta-reviewer output (new)
105
105
  - `.planning/reviews/<N>/deep-review.md` — merged consolidated report (final deliverable)