pan-wizard 3.32.0 → 3.33.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 (62) hide show
  1. package/agents/pan-conductor.md +2 -2
  2. package/agents/pan-document_code.md +2 -2
  3. package/agents/pan-executor.md +3 -3
  4. package/agents/pan-experiment-runner.md +7 -10
  5. package/agents/pan-hardener.md +15 -15
  6. package/agents/pan-knowledge.md +1 -1
  7. package/agents/pan-meta-reviewer.md +2 -2
  8. package/agents/pan-optimizer.md +2 -2
  9. package/agents/pan-phase-researcher.md +2 -2
  10. package/agents/pan-plan-checker.md +6 -2
  11. package/agents/pan-planner.md +23 -10
  12. package/agents/pan-project-researcher.md +10 -67
  13. package/agents/pan-roadmapper.md +5 -48
  14. package/agents/pan-verifier.md +26 -11
  15. package/bin/install-lib.cjs +170 -15
  16. package/bin/install.js +39 -14
  17. package/commands/pan/army.md +2 -2
  18. package/commands/pan/experiment.md +2 -2
  19. package/commands/pan/focus-auto.md +12 -8
  20. package/commands/pan/focus-design.md +2 -0
  21. package/commands/pan/focus-exec.md +1 -1
  22. package/commands/pan/focus-scan.md +2 -7
  23. package/commands/pan/map-codebase.md +4 -4
  24. package/commands/pan/mcp-bridge.md +1 -1
  25. package/commands/pan/milestone-done.md +1 -2
  26. package/commands/pan/phase-tests.md +2 -2
  27. package/commands/pan/profile.md +2 -2
  28. package/commands/pan/research-phase.md +2 -4
  29. package/commands/pan/review-deep.md +3 -3
  30. package/commands/pan/todo-add.md +2 -2
  31. package/commands/pan/todo-check.md +2 -2
  32. package/hooks/dist/pan-state-reinject.js +140 -15
  33. package/package.json +1 -1
  34. package/pan-wizard-core/bin/lib/agents-md.cjs +4 -1
  35. package/pan-wizard-core/bin/lib/commands.cjs +93 -1
  36. package/pan-wizard-core/bin/lib/config.cjs +110 -0
  37. package/pan-wizard-core/bin/lib/constants.cjs +11 -9
  38. package/pan-wizard-core/bin/lib/core.cjs +27 -7
  39. package/pan-wizard-core/bin/lib/cost.cjs +13 -3
  40. package/pan-wizard-core/bin/lib/findings.cjs +6 -2
  41. package/pan-wizard-core/bin/lib/init.cjs +5 -1
  42. package/pan-wizard-core/bin/lib/phase.cjs +52 -2
  43. package/pan-wizard-core/bin/lib/review-deep.cjs +2 -2
  44. package/pan-wizard-core/bin/lib/state.cjs +3 -2
  45. package/pan-wizard-core/bin/lib/verdict.cjs +54 -4
  46. package/pan-wizard-core/bin/lib/verify-drift.cjs +14 -5
  47. package/pan-wizard-core/bin/lib/verify-scope.cjs +69 -2
  48. package/pan-wizard-core/bin/lib/verify.cjs +87 -7
  49. package/pan-wizard-core/bin/pan-tools.cjs +10 -2
  50. package/pan-wizard-core/mcp/server.cjs +18 -9
  51. package/pan-wizard-core/references/checkpoints.md +4 -2
  52. package/pan-wizard-core/references/verdict-contract.md +2 -1
  53. package/pan-wizard-core/templates/discovery.md +1 -1
  54. package/pan-wizard-core/templates/phase-prompt.md +2 -1
  55. package/pan-wizard-core/templates/verification-report.md +5 -0
  56. package/pan-wizard-core/workflows/exec-phase.md +8 -3
  57. package/pan-wizard-core/workflows/health.md +1 -0
  58. package/pan-wizard-core/workflows/new-project.md +1 -1
  59. package/pan-wizard-core/workflows/quick.md +6 -6
  60. package/pan-wizard-core/workflows/verify-phase.md +7 -2
  61. package/scripts/build-agent-plugin.js +19 -5
  62. package/scripts/test-surface.cjs +4 -0
@@ -16,13 +16,14 @@ const {
16
16
  } = require('./constants.cjs');
17
17
  const { planningPath, phasesPath, filterPlanFiles, filterSummaryFiles, fileAccessible, detectPlanningModel } = require('./utils.cjs');
18
18
  const { detectForeignPlanningTree } = require('./foreign-planning.cjs');
19
+ const { unknownConfigKeys, nearestConfigKey } = require('./config.cjs');
19
20
  // Drift detection lives in verify-drift.cjs; re-exported below so consumers of
20
21
  // verify.cjs are unaffected by the decomposition.
21
22
  const { runDriftCheck, parseConventionRules, checkFileConventions, calculateDriftScore, getChangedFiles, cmdDriftCheck } = require('./verify-drift.cjs');
22
23
  const { collectVerificationStats, countRoadmapPhases, groupGapPatterns, cmdRetro } = require('./verify-retro.cjs');
23
24
  const { detectInstalledRuntimes, validateRuntimeInstall, cmdValidateDeployment } = require('./verify-deploy.cjs');
24
25
  const { cmdPreflight, cmdDepsValidate } = require('./verify-preflight.cjs');
25
- const { scopePhase, cmdVerifyScope } = require('./verify-scope.cjs');
26
+ const { scopePhase, cmdVerifyScope, verificationFreshness, cmdVerifyStale } = require('./verify-scope.cjs');
26
27
  const { planningRootRel } = require('./planning-root.cjs');
27
28
 
28
29
  /**
@@ -116,6 +117,56 @@ function cmdVerifySummary(cwd, summaryPath, checkFileCount, raw) {
116
117
  }, raw, passed ? 'passed' : 'failed');
117
118
  }
118
119
 
120
+ // Longer than a signature, a type or a short config snippet: an implementation body.
121
+ const MAX_ACTION_CODE_LINES = 20;
122
+
123
+ /** The line count of the longest fenced code block in a piece of markdown (0 if none). */
124
+ function longestFencedBlock(text) {
125
+ let longest = 0;
126
+ for (const m of String(text).matchAll(/(^|\n)[ \t]*(`{3,}|~{3,})[^\n]*\n([\s\S]*?)\n[ \t]*\2[ \t]*(?=\n|$)/g)) {
127
+ longest = Math.max(longest, m[3].split('\n').length);
128
+ }
129
+ return longest;
130
+ }
131
+
132
+ // The elements each checkpoint type needs (references/checkpoints.md).
133
+ const CHECKPOINT_REQUIRED = {
134
+ 'checkpoint:human-verify': ['what-built', 'how-to-verify'],
135
+ 'checkpoint:decision': ['decision', 'options'],
136
+ 'checkpoint:human-action': ['action'],
137
+ };
138
+
139
+ /**
140
+ * Check one checkpoint task against its type's shape. A decision checkpoint may
141
+ * carry `auto_select="<option id>"`: the option auto mode takes. Without one, auto
142
+ * mode stops for a human instead of taking whichever option is listed first
143
+ * (market-ideas M22), and an `auto_select` naming no option is an error.
144
+ * @returns {{name: string, type: string, auto_select?: string|null}} the task entry
145
+ */
146
+ function checkCheckpointTask(type, attrs, body, errors, warnings) {
147
+ const keyText = (body.match(/<(?:decision|what-built|action)>([\s\S]*?)<\/(?:decision|what-built|action)>/) || [])[1];
148
+ const label = keyText ? keyText.trim().replace(/\s+/g, ' ').slice(0, 80) : `unnamed ${type}`;
149
+ const required = CHECKPOINT_REQUIRED[type];
150
+ if (!required) {
151
+ errors.push(`Task '${label}' has unknown checkpoint type '${type}'`);
152
+ return { name: label, type };
153
+ }
154
+ for (const el of required) {
155
+ if (!body.includes(`<${el}>`)) errors.push(`Checkpoint '${label}' missing <${el}>`);
156
+ }
157
+ if (type !== 'checkpoint:decision') return { name: label, type };
158
+
159
+ const optionIds = [...body.matchAll(/<option\b[^>]*\bid=["']([^"']+)["']/g)].map(m => m[1]);
160
+ if (optionIds.length === 0) errors.push(`Decision checkpoint '${label}' has no <option id="...">`);
161
+ const autoMatch = attrs.match(/\bauto_select=["']([^"']*)["']/);
162
+ if (!autoMatch) {
163
+ warnings.push(`Decision checkpoint '${label}' has no auto_select: auto mode stops here for a human`);
164
+ } else if (!optionIds.includes(autoMatch[1])) {
165
+ errors.push(`Decision checkpoint '${label}' auto_select="${autoMatch[1]}" names no option (options: ${optionIds.join(', ') || 'none'})`);
166
+ }
167
+ return { name: label, type, auto_select: autoMatch ? autoMatch[1] : null };
168
+ }
169
+
119
170
  /**
120
171
  * Validate a plan.md structure: required frontmatter, task elements, and consistency.
121
172
  * @param {string} cwd - Working directory path
@@ -139,14 +190,24 @@ function cmdVerifyPlanStructure(cwd, filePath, raw) {
139
190
  if (fm[field] === undefined) errors.push(`Missing required frontmatter field: ${field}`);
140
191
  }
141
192
 
142
- // Parse XML <task> elements and validate each has required sub-elements:
143
- // <name> (required), <action> (required), <verify> (recommended),
144
- // <done> (recommended), <files> (recommended)
145
- const taskPattern = /<task[^>]*>([\s\S]*?)<\/task>/g;
193
+ // Parse XML <task> elements. An auto task needs <name> and <action> and should
194
+ // have <verify>, <done> and <files>. A checkpoint task has its own shape
195
+ // (references/checkpoints.md) and is checked against that instead: until
196
+ // 2026-10-03 every checkpoint failed the auto-task checks, and the `<task[^>]*>`
197
+ // pattern also matched the `<tasks>` wrapper, so a decision's first option name
198
+ // was read as the task's name. `\b` keeps the wrapper out.
199
+ const taskPattern = /<task\b([^>]*)>([\s\S]*?)<\/task>/g;
146
200
  const tasks = [];
147
201
  let taskMatch;
148
202
  while ((taskMatch = taskPattern.exec(content)) !== null) {
149
- const taskContent = taskMatch[1];
203
+ const attrs = taskMatch[1];
204
+ const taskContent = taskMatch[2];
205
+ const typeMatch = attrs.match(/\btype=["']?([\w:-]+)/);
206
+ const taskType = typeMatch ? typeMatch[1] : 'auto';
207
+ if (taskType.startsWith('checkpoint:')) {
208
+ tasks.push(checkCheckpointTask(taskType, attrs, taskContent, errors, warnings));
209
+ continue;
210
+ }
150
211
  const nameMatch = taskContent.match(/<name>([\s\S]*?)<\/name>/);
151
212
  const taskName = nameMatch ? nameMatch[1].trim() : 'unnamed';
152
213
  const hasFiles = /<files>/.test(taskContent);
@@ -156,11 +217,19 @@ function cmdVerifyPlanStructure(cwd, filePath, raw) {
156
217
 
157
218
  if (!nameMatch) errors.push('Task missing <name> element');
158
219
  if (!hasAction) errors.push(`Task '${taskName}' missing <action>`);
220
+ // Decisions, not code (market-ideas M32): an <action> names signatures, values,
221
+ // assertions and the verify command. A long fenced block there is an
222
+ // implementation body the planner wrote for the executor.
223
+ const actionText = (taskContent.match(/<action>([\s\S]*?)<\/action>/) || [])[1] || '';
224
+ const longest = longestFencedBlock(actionText);
225
+ if (longest > MAX_ACTION_CODE_LINES) {
226
+ warnings.push(`Task '${taskName}' embeds a ${longest}-line code block in <action>: a plan records decisions (signatures, values, assertions, the verify command), not implementation`);
227
+ }
159
228
  if (!hasVerify) warnings.push(`Task '${taskName}' missing <verify>`);
160
229
  if (!hasDone) warnings.push(`Task '${taskName}' missing <done>`);
161
230
  if (!hasFiles) warnings.push(`Task '${taskName}' missing <files>`);
162
231
 
163
- tasks.push({ name: taskName, hasFiles, hasAction, hasVerify, hasDone });
232
+ tasks.push({ name: taskName, type: taskType, hasFiles, hasAction, hasVerify, hasDone });
164
233
  }
165
234
 
166
235
  if (tasks.length === 0) warnings.push('No <task> elements found');
@@ -856,6 +925,15 @@ function checkConfigFile(cwd, addIssue, repairs) {
856
925
  if (parsed.model_profile && !validProfiles.includes(parsed.model_profile)) {
857
926
  addIssue('warning', 'W004', `${CONFIG_FILE}: invalid model_profile "${parsed.model_profile}"`, `Valid values: ${validProfiles.join(', ')}`);
858
927
  }
928
+ // I006 (market-ideas M26): keys PAN does not read. Info, not a warning: the key
929
+ // may be the user's own note or meant for a newer PAN, but a typo must not pass
930
+ // as a setting that took effect.
931
+ const unknown = unknownConfigKeys(parsed);
932
+ if (unknown.length) {
933
+ const hints = unknown.map((k) => { const n = nearestConfigKey(k); return n ? `${k} → ${n}?` : k; });
934
+ addIssue('info', 'I006', `${CONFIG_FILE}: PAN does not read ${unknown.length === 1 ? 'this key' : 'these keys'}: ${unknown.join(', ')}`,
935
+ `Check the spelling (${hints.join('; ')}), or remove the key. pan-tools config-set names an unknown key when you set it.`);
936
+ }
859
937
  } catch (err) {
860
938
  // JSON parse failed -- config is corrupt and should be reset
861
939
  addIssue('error', 'E005', `${CONFIG_FILE}: JSON parse error - ${err.message}`, 'Run /pan:health --repair to reset to defaults', true);
@@ -1558,6 +1636,8 @@ module.exports = {
1558
1636
  cmdVerifyStubs,
1559
1637
  scopePhase,
1560
1638
  cmdVerifyScope,
1639
+ verificationFreshness,
1640
+ cmdVerifyStale,
1561
1641
  cmdValidateConsistency,
1562
1642
  cmdValidateHealth,
1563
1643
  cmdPreflight,
@@ -611,12 +611,19 @@ async function main() {
611
611
  verify.cmdVerifyStubs(cwd, { gate: args.includes('--gate') }, raw);
612
612
  } else if (subcommand === 'scope') {
613
613
  verify.cmdVerifyScope(cwd, args[2], raw);
614
+ } else if (subcommand === 'stale') {
615
+ verify.cmdVerifyStale(cwd, args[2], raw);
614
616
  } else {
615
- error('Unknown verify subcommand. Available: plan-structure, phase-completeness, references, commits, artifacts, key-links, reconcile, stubs, scope');
617
+ error('Unknown verify subcommand. Available: plan-structure, phase-completeness, references, commits, artifacts, key-links, reconcile, stubs, scope, stale');
616
618
  }
617
619
  break;
618
620
  }
619
621
 
622
+ case 'version': {
623
+ commands.cmdVersion(cwd, { check: args.includes('--check') }, raw);
624
+ break;
625
+ }
626
+
620
627
  case 'generate-slug': {
621
628
  commands.cmdGenerateSlug(args[1], raw);
622
629
  break;
@@ -844,7 +851,8 @@ async function main() {
844
851
  }
845
852
 
846
853
  case 'phase-plan-index': {
847
- phase.cmdPhasePlanIndex(cwd, args[1], raw);
854
+ const failedIdx = args.indexOf('--failed');
855
+ phase.cmdPhasePlanIndex(cwd, args[1], raw, { failed: failedIdx !== -1 ? args[failedIdx + 1] : null });
848
856
  break;
849
857
  }
850
858
 
@@ -50,6 +50,15 @@ const META_SERVER_INFO_KEY = 'io.modelcontextprotocol/serverInfo';
50
50
  // claim to speak a version we don't. Newest first (the `server/discover` order).
51
51
  const SUPPORTED_VERSIONS_LIST = [MODERN_PROTOCOL_VERSION, '2025-06-18', '2025-03-26', '2024-11-05'];
52
52
  const SUPPORTED_PROTOCOL_VERSIONS = new Set(SUPPORTED_VERSIONS_LIST);
53
+ // The `CacheableResult` fields a modern result MUST carry where its type extends
54
+ // that interface: DiscoverResult, ListToolsResult, ListResourcesResult and
55
+ // ReadResourceResult (schema/2026-07-28/schema.ts). Claude Code 2.1.288 drops a
56
+ // list without them, so the server showed as connected with no tools and no
57
+ // resources (live check H2, 2026-10-03). The registry is fixed for the life of the
58
+ // process, so its lists are public and long-lived; a resource is the project's
59
+ // state at the moment it is read, so a read is private and stale at once.
60
+ const REGISTRY_CACHE = Object.freeze({ ttlMs: 3600000, cacheScope: 'public' });
61
+ const READ_CACHE = Object.freeze({ ttlMs: 0, cacheScope: 'private' });
53
62
  /**
54
63
  * The version the server reports in `initialize` / `server/discover`. Read from the
55
64
  * package.json two levels up: the repository root in the source tree, the runtime
@@ -320,12 +329,13 @@ function createServer(opts = {}) {
320
329
  { supported: SUPPORTED_VERSIONS_LIST, requested: requestedVersion });
321
330
  }
322
331
 
323
- // Modern results MUST carry a `resultType`; legacy results MUST NOT change
324
- // shape (clients treat an absent resultType as "complete"). Stamp it only on
332
+ // Modern results MUST carry a `resultType`, a cacheable one its caching fields,
333
+ // and each SHOULD name the server in `_meta`; legacy results MUST NOT change
334
+ // shape (clients treat an absent resultType as "complete"). Stamp them only on
325
335
  // the modern path.
326
- const reply = (result) => rpcResult(id,
336
+ const reply = (result, cache) => rpcResult(id,
327
337
  isModern && result && typeof result === 'object' && result.resultType === undefined
328
- ? { resultType: 'complete', ...result }
338
+ ? { resultType: 'complete', ...cache, ...result, _meta: { ...result._meta, [META_SERVER_INFO_KEY]: SERVER_INFO } }
329
339
  : result);
330
340
 
331
341
  switch (method) {
@@ -339,8 +349,7 @@ function createServer(opts = {}) {
339
349
  supportedVersions: SUPPORTED_VERSIONS_LIST,
340
350
  capabilities: { tools: {}, resources: {} },
341
351
  instructions: 'PAN Wizard engine bridge: planning, verification, and orchestration tools backed by the pan-tools CLI. All tools are read-only except the gated pan_confirm_merge.',
342
- ttlMs: 3600000,
343
- cacheScope: 'public',
352
+ ...REGISTRY_CACHE,
344
353
  _meta: { [META_SERVER_INFO_KEY]: SERVER_INFO },
345
354
  });
346
355
  case 'initialize': {
@@ -354,9 +363,9 @@ function createServer(opts = {}) {
354
363
  case 'ping':
355
364
  return reply({});
356
365
  case 'tools/list':
357
- return reply({ tools: reg.TOOLS.map(toMcpTool) });
366
+ return reply({ tools: reg.TOOLS.map(toMcpTool) }, REGISTRY_CACHE);
358
367
  case 'resources/list':
359
- return reply({ resources: reg.RESOURCES.map(toMcpResource) });
368
+ return reply({ resources: reg.RESOURCES.map(toMcpResource) }, REGISTRY_CACHE);
360
369
  case 'tools/call': {
361
370
  const out = callTool(params && params.name, params && params.arguments);
362
371
  return out.error ? rpcError(id, out.error.code, out.error.message) : reply(out.result);
@@ -364,7 +373,7 @@ function createServer(opts = {}) {
364
373
  case 'resources/read': {
365
374
  const out = readResource(params && params.uri);
366
375
  if (out.unknown) return rpcError(id, -32602, `Unknown resource: ${params && params.uri}`);
367
- return out.error ? rpcError(id, out.error.code, out.error.message) : reply(out.result);
376
+ return out.error ? rpcError(id, out.error.code, out.error.message) : reply(out.result, READ_CACHE);
368
377
  }
369
378
  default:
370
379
  return rpcError(id, -32601, `Method not found: ${method}`);
@@ -8,7 +8,7 @@ Plans execute autonomously. Checkpoints formalize interaction points where human
8
8
  2. **Claude sets up the verification environment** - Start dev servers, seed databases, configure env vars
9
9
  3. **User only does what requires human judgment** - Visual checks, UX evaluation, "does this feel right?"
10
10
  4. **Secrets come from user, automation comes from Claude** - Ask for API keys, then Claude uses them via CLI
11
- 5. **Auto-mode bypasses verification/decision checkpoints** — When `workflow.auto_advance` is true in config: human-verify auto-approves, decision auto-selects first option, human-action still stops (auth gates cannot be automated)
11
+ 5. **Auto-mode bypasses verification checkpoints, and decisions only where the planner named a default** — When `workflow.auto_advance` is true in config: human-verify auto-approves; a decision with `auto_select="<option id>"` takes that option, and a decision without it stops for a human (auto mode never takes an option because it is listed first); human-action still stops (auth gates cannot be automated)
12
12
  </overview>
13
13
 
14
14
  <checkpoint_types>
@@ -105,7 +105,7 @@ Plans execute autonomously. Checkpoints formalize interaction points where human
105
105
 
106
106
  **Structure:**
107
107
  ```xml
108
- <task type="checkpoint:decision" gate="blocking">
108
+ <task type="checkpoint:decision" gate="blocking" auto_select="option-a">
109
109
  <decision>[What's being decided]</decision>
110
110
  <context>[Why this decision matters]</context>
111
111
  <options>
@@ -124,6 +124,8 @@ Plans execute autonomously. Checkpoints formalize interaction points where human
124
124
  </task>
125
125
  ```
126
126
 
127
+ **`auto_select` (optional)** names the option auto mode takes. Set it only when the choice is safe to make without the user: reversible, no new paid service, no credential, no data-model lock-in. It must name an `id` from this checkpoint's options; `verify plan-structure` fails one that names no option. Leave it out when a human must choose, and auto mode stops at this checkpoint. The examples below omit it: picking a vendor or a database is the user's call.
128
+
127
129
  **Example: Auth Provider Selection**
128
130
  ```xml
129
131
  <task type="checkpoint:decision" gate="blocking">
@@ -26,6 +26,7 @@ A judging agent ends its report with one fenced `pan-verdict` block. The block i
26
26
  | `verdict` | yes | your own verdict word (table below) |
27
27
  | `outcome` | yes | `pass`, `fail` or `needs_human` |
28
28
  | `findings` | yes (may be empty) | a list of `{class, severity, where, summary}` |
29
+ | `not_checked` | when a check could not run | a list of `{check, reason}`: a check you were asked to make and could not run, and why (no test script, no browser, a file you could not read). Never fold it into `pass`: a verdict with entries here says what it did not see |
29
30
 
30
31
  | Agent | `verdict` → `outcome` |
31
32
  |---|---|
@@ -33,7 +34,7 @@ A judging agent ends its report with one fenced `pan-verdict` block. The block i
33
34
  | pan-reviewer | `PASS` → `pass` · `PASS_WITH_WARNINGS` → `pass` · `NEEDS_FIXES` → `fail` |
34
35
  | pan-design-checker | `PASS` → `pass` · `GAPS` → `fail` |
35
36
 
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
+ The verifier does not emit this block. Its verification.md frontmatter (`status`, `gaps`, `human_verification`, `unrequested`, `test_gate`, `not_checked`) is its machine contract, and the recorder reads that directly. `test_gate` is `passed`, `failed` or `skipped`. A skipped gate is recorded as a `tests` entry in `not_checked` even when the list leaves it out, and a verification that does not record `test_gate` at all is recorded with a `test_gate_unrecorded` warning.
37
38
 
38
39
  ## `class`: what kind of finding
39
40
 
@@ -30,7 +30,7 @@ Discover [topic] to inform [phase name] implementation.
30
30
 
31
31
  Purpose: [What decision/implementation this enables]
32
32
  Scope: [Boundaries]
33
- Output: DISCOVERY.md with recommendation
33
+ Output: discovery.md with recommendation
34
34
  </discovery_objective>
35
35
 
36
36
  <discovery_scope>
@@ -79,7 +79,8 @@ Output: [What artifacts will be created]
79
79
  <!-- For checkpoint task examples and patterns, see @~/.claude/pan-wizard-core/references/checkpoints.md -->
80
80
  <!-- Key rule: Claude starts dev server BEFORE human-verify checkpoints. User only visits URLs. -->
81
81
 
82
- <task type="checkpoint:decision" gate="blocking">
82
+ <task type="checkpoint:decision" gate="blocking" auto_select="option-a">
83
+ <!-- auto_select: the option auto mode takes. Omit it when a human must choose. -->
83
84
  <decision>[What needs deciding]</decision>
84
85
  <context>[Why this decision matters]</context>
85
86
  <options>
@@ -10,8 +10,13 @@ Template for `.planning/phases/XX-name/{phase_num}-verification.md` — phase go
10
10
  ---
11
11
  phase: XX-name
12
12
  verified: YYYY-MM-DDTHH:MM:SSZ
13
+ verified_commit: <sha> # git rev-parse HEAD before writing this file; `pan-tools verify stale` compares against it
13
14
  status: passed | gaps_found | human_needed
14
15
  score: N/M must-haves verified
16
+ test_gate: passed | failed | skipped
17
+ not_checked: # Only for checks that could not run, with why — a skipped test gate is one
18
+ - check: "tests"
19
+ reason: "no test script in package.json"
15
20
  unrequested: # Only when the scope check (`pan-tools verify scope <phase>`) found work no plan asked for
16
21
  - path: "src/admin/panel.tsx"
17
22
  what: "An admin panel no plan or requirement asked for"
@@ -256,7 +256,11 @@ Execute each wave in sequence. Within a wave: parallel if `PARALLELIZATION=true`
256
256
 
257
257
  **Known Claude Code bug (classifyHandoffIfNeeded):** If an agent reports "failed" with error containing `classifyHandoffIfNeeded is not defined`, this is a Claude Code runtime bug — not a PAN or agent issue. The error fires in the completion handler AFTER all tool calls finish. In this case: run the same spot-checks as step 4 (summary.md exists, git commits present, no Self-Check: FAILED). If spot-checks PASS → treat as **successful**. If spot-checks FAIL → treat as real failure below.
258
258
 
259
- For real failures: report which plan failed → ask "Continue?" or "Stop?" → if continue, dependent plans may also fail. If stop, partial completion report.
259
+ For real failures: report which plan failed → ask "Continue?" or "Stop?". If stop, partial completion report. If continue, never run a plan that depends on a failed one; ask the engine which plans are blocked:
260
+ ```bash
261
+ BLOCKED=$(node ~/.claude/pan-wizard-core/bin/pan-tools.cjs phase-plan-index "${PHASE_NUMBER}" --failed "${FAILED_PLAN_IDS}")
262
+ ```
263
+ `${FAILED_PLAN_IDS}` is the comma-separated ids of every plan that failed so far. `blocked` lists each unfinished plan whose `depends_on` reaches a failed plan, directly or through another blocked plan, with the failed plans behind it. Do not spawn those plans in this or any later wave. Name them in the wave report and the final summary ("Skipped 03-03 and 03-04: they depend on 03-01, which failed"), and run every other plan as planned. The same applies after "Continue with remaining waves?" in step 4.
260
264
 
261
265
  6. **Execute checkpoint plans between waves** — see `<checkpoint_handling>`.
262
266
 
@@ -275,7 +279,7 @@ AUTO_CFG=$(node ~/.claude/pan-wizard-core/bin/pan-tools.cjs config-get workflow.
275
279
 
276
280
  When executor returns a checkpoint AND `AUTO_CFG` is `"true"`:
277
281
  - **human-verify** → Auto-spawn continuation agent with `{user_response}` = `"approved"`. Log `⚡ Auto-approved checkpoint`.
278
- - **decision** → Auto-spawn continuation agent with `{user_response}` = first option from checkpoint details. Log `⚡ Auto-selected: [option]`.
282
+ - **decision** → The executor already took the plan's `auto_select` option when the task had one, so a decision that comes back has none: present it to the user (standard flow below). If a returned decision's task does carry `auto_select` in the plan file, auto-spawn the continuation agent with `{user_response}` = that option id and log `⚡ Auto-selected: [option] (auto_select)`. Never answer with an option because it is listed first.
279
283
  - **human-action** → Present to user (existing behavior below). Auth gates cannot be automated.
280
284
 
281
285
  **Standard flow (not auto-mode, or human-action type):**
@@ -567,6 +571,7 @@ Phase goal: {goal from roadmap.md}
567
571
  Phase requirement IDs: {phase_req_ids}
568
572
  Check must_haves against actual codebase.
569
573
  Cross-reference requirement IDs from PLAN frontmatter against requirements.md — every ID MUST be accounted for.
574
+ Run the project's test suite as the test gate (the run_test_suite step of @~/.claude/pan-wizard-core/workflows/verify-phase.md) and record test_gate in the frontmatter.
570
575
  Create verification.md.",
571
576
  subagent_type="pan-verifier",
572
577
  model="{verifier_model}"
@@ -585,7 +590,7 @@ VERIF_STATUS=$(node ~/.claude/pan-wizard-core/bin/pan-tools.cjs findings record
585
590
 
586
591
  | Status | Action |
587
592
  |--------|--------|
588
- | `passed` | → update_roadmap |
593
+ | `passed` | → update_roadmap. If the verification frontmatter has `test_gate: skipped` or a `not_checked` list, say so when you report the phase: "passed — not checked: tests (no test script)". A pass never hides what it did not check |
589
594
  | `human_needed` | Present items for human testing, get approval or feedback |
590
595
  | `gaps_found` | Present gap summary, offer `/pan:plan-phase {phase} --gaps` |
591
596
 
@@ -165,6 +165,7 @@ Report final status.
165
165
  | I003 | info | Focus-model or campaign tree — the phase-model checks do not apply | No |
166
166
  | I004 | info | planning-with-files shares the tree — PAN leaves its files alone | No |
167
167
  | I005 | info | The planning tree is gitignored while commit_docs is true — planning commits commit nothing | No |
168
+ | I006 | info | config.json holds keys PAN does not read (usually a typo); the fix names the nearest known key | No |
168
169
  | STATE_REQ_DRIFT | warning | state.md complete but REQUIREMENTS.md has unchecked boxes | Yes |
169
170
  | STATE_ROADMAP_DRIFT | warning | state.md complete but roadmap.md has unchecked plan boxes | Yes |
170
171
  | VERIFICATION_GATE_MISSING | warning | Phase has completed plans but no verification record | No |
@@ -985,7 +985,7 @@ Use AskUserQuestion:
985
985
  - .planning/roadmap.md (Current roadmap to revise)
986
986
  </files_to_read>
987
987
 
988
- Update the roadmap based on feedback. Edit files in place.
988
+ Update the roadmap based on feedback: rewrite each changed file with one Write.
989
989
  Return ROADMAP REVISED with changes made.
990
990
  </revision>
991
991
  ", subagent_type="pan-roadmapper", model="{roadmapper_model}", description="Revise roadmap")
@@ -46,7 +46,7 @@ If `$FULL_MODE`:
46
46
  INIT=$(node ~/.claude/pan-wizard-core/bin/pan-tools.cjs init quick "$DESCRIPTION")
47
47
  ```
48
48
 
49
- Parse JSON for: `planner_model`, `executor_model`, `checker_model`, `verifier_model`, `commit_docs`, `next_num`, `slug`, `date`, `timestamp`, `quick_dir`, `task_dir`, `roadmap_exists`, `planning_exists`.
49
+ Parse JSON for: `planner_model`, `executor_model`, `checker_model`, `verifier_model`, `commit_docs`, `next_num`, `slug`, `description_cell`, `date`, `timestamp`, `quick_dir`, `task_dir`, `roadmap_exists`, `planning_exists`.
50
50
 
51
51
  **If `roadmap_exists` is false:** Error — Quick mode requires an active project with roadmap.md. Run `/pan:new-project` first.
52
52
 
@@ -365,23 +365,23 @@ Insert after `### Blockers/Concerns` section:
365
365
 
366
366
  **7c. Append new row to table:**
367
367
 
368
- Use `date` from init:
368
+ Use `date` and `description_cell` from init. Write `description_cell` exactly as init returned it, never `$DESCRIPTION`: init escapes `|` and folds line breaks, and a raw `|` in a description would split the row into an extra column and break the table for every later read.
369
369
 
370
370
  **If `$FULL_MODE` (or table has Status column):**
371
371
  ```markdown
372
- | ${next_num} | ${DESCRIPTION} | ${date} | ${commit_hash} | ${VERIFICATION_STATUS} | [${next_num}-${slug}](./quick/${next_num}-${slug}/) |
372
+ | ${next_num} | ${description_cell} | ${date} | ${commit_hash} | ${VERIFICATION_STATUS} | [${next_num}-${slug}](./quick/${next_num}-${slug}/) |
373
373
  ```
374
374
 
375
375
  **If NOT `$FULL_MODE` (and table has no Status column):**
376
376
  ```markdown
377
- | ${next_num} | ${DESCRIPTION} | ${date} | ${commit_hash} | [${next_num}-${slug}](./quick/${next_num}-${slug}/) |
377
+ | ${next_num} | ${description_cell} | ${date} | ${commit_hash} | [${next_num}-${slug}](./quick/${next_num}-${slug}/) |
378
378
  ```
379
379
 
380
380
  **7d. Update "Last activity" line:**
381
381
 
382
- Use `date` from init:
382
+ Use `date` and `description_cell` from init (one line, whatever the description held):
383
383
  ```
384
- Last activity: ${date} - Completed quick task ${next_num}: ${DESCRIPTION}
384
+ Last activity: ${date} - Completed quick task ${next_num}: ${description_cell}
385
385
  ```
386
386
 
387
387
  Use Edit tool to make these changes atomically
@@ -136,6 +136,10 @@ TEST_TOTAL: ${TEST_COUNT}
136
136
  TEST_PASSED: ${TEST_PASS}
137
137
  TEST_FAILED: ${TEST_FAIL}
138
138
  ```
139
+ Write `test_gate: ${TEST_GATE_STATUS}` into the verification.md frontmatter. When it is
140
+ `skipped`, also add a `not_checked` entry (`check: "tests"`, `reason: "no test script in
141
+ package.json"`). The recorder reads both: a pass whose tests never ran is recorded as a
142
+ pass with tests not checked, never as a plain pass.
139
143
 
140
144
  **If test gate FAILED:** Continue verification (gather full picture) but set overall status to `gaps_found` regardless of goal-backward results. Include test failures as gaps in the report.
141
145
  </step>
@@ -284,7 +288,7 @@ Format each as: Test Name → What to do → Expected result → Why can't verif
284
288
  </step>
285
289
 
286
290
  <step name="determine_status">
287
- **passed:** All truths VERIFIED, all artifacts pass levels 1-3, all key links WIRED, no blocker anti-patterns, AND test gate passed or skipped.
291
+ **passed:** All truths VERIFIED, all artifacts pass levels 1-3, all key links WIRED, no blocker anti-patterns, AND test gate passed or skipped. A skipped gate is listed in `not_checked` with its reason, and the report to the user says the tests did not run.
288
292
 
289
293
  **gaps_found:** Any truth FAILED, artifact MISSING/STUB, key link NOT_WIRED, blocker found, OR test gate FAILED (test regressions detected).
290
294
 
@@ -306,9 +310,10 @@ If gaps_found:
306
310
  <step name="create_report">
307
311
  ```bash
308
312
  REPORT_PATH="$PHASE_DIR/${PHASE_NUM}-verification.md"
313
+ VERIFIED_COMMIT=$(git rev-parse HEAD 2>/dev/null)
309
314
  ```
310
315
 
311
- Fill template sections: frontmatter (phase/timestamp/status/score), **test gate results** (status/total/passed/failed), goal achievement, artifact table, wiring table, requirements coverage, anti-patterns, human verification, gaps summary, fix plans (if gaps_found), metadata.
316
+ Fill template sections: frontmatter (phase/timestamp/`verified_commit: ${VERIFIED_COMMIT}`/status/score/`test_gate`), **test gate results** (status/total/passed/failed), goal achievement, artifact table, wiring table, requirements coverage, anti-patterns, human verification, gaps summary, fix plans (if gaps_found), metadata.
312
317
 
313
318
  See ~/.claude/pan-wizard-core/templates/verification-report.md for complete template.
314
319
  </step>
@@ -5,7 +5,9 @@
5
5
  *
6
6
  * plugin.json closed-schema manifest ($schema + name + metadata)
7
7
  * skills/pan-<name>/SKILL.md every PAN command as an Agent Skill, from the
8
- * ONE unified-skills compiler (ADR-0028)
8
+ * ONE unified-skills compiler (ADR-0028); a skill
9
+ * over Codex's 8,000-byte plugin cut is a pointer
10
+ * page plus references/instructions.md (S8)
9
11
  * mcp.json the bundled bridge, launched as
10
12
  * `node ${PLUGIN_ROOT}/pan-wizard-core/mcp/server.cjs`
11
13
  * pan-wizard-core/ dispatcher + modules + workflows + templates +
@@ -74,9 +76,14 @@ function assertSafeToReplace(dir) {
74
76
  throw new Error(`build-agent-plugin: refusing to replace ${dir} — it is non-empty and does not look like a previous bundle build (no Agent Plugins plugin.json)`);
75
77
  }
76
78
 
77
- /** commands/pan/**.md → skills/pan-<name>/SKILL.md, mirroring the installer's recursion. */
79
+ /**
80
+ * commands/pan/**.md → skills/pan-<name>/SKILL.md, mirroring the installer's recursion.
81
+ * A skill longer than Codex loads from a plugin (8,000 bytes) is split: a pointer
82
+ * SKILL.md plus references/instructions.md (lib.splitOversizedSkill, market-ideas S8).
83
+ */
78
84
  function emitSkills(srcDir, skillsDir, prefix) {
79
85
  let count = 0;
86
+ let split = 0;
80
87
  (function recurse(currentSrc, currentPrefix) {
81
88
  for (const entry of fs.readdirSync(currentSrc, { withFileTypes: true })) {
82
89
  const srcPath = path.join(currentSrc, entry.name);
@@ -88,11 +95,18 @@ function emitSkills(srcDir, skillsDir, prefix) {
88
95
  let content = fs.readFileSync(srcPath, 'utf8');
89
96
  content = lib.rewriteUnifiedSkillCommandContent(content, REWRITE);
90
97
  content = lib.convertClaudeCommandToUnifiedSkill(content, skillName, { adapterNote: lib.agentPluginSkillAdapterNote() });
91
- fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
98
+ const { skill, reference } = lib.splitOversizedSkill(content, skillName);
99
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skill);
100
+ if (reference) {
101
+ const refPath = path.join(skillDir, ...lib.AGENT_PLUGIN_SKILL_REFERENCE.split('/'));
102
+ fs.mkdirSync(path.dirname(refPath), { recursive: true });
103
+ fs.writeFileSync(refPath, reference);
104
+ split++;
105
+ }
92
106
  count++;
93
107
  }
94
108
  })(srcDir, prefix);
95
- return count;
109
+ return { count, split };
96
110
  }
97
111
 
98
112
  /** pan-wizard-core → bundle, markdown rewritten, everything else verbatim. */
@@ -214,7 +228,7 @@ function main() {
214
228
  const copilotAgents = emitCopilotNamespace(path.join(ROOT, 'agents'), path.join(OUT, lib.COPILOT_PLUGIN_NAMESPACE));
215
229
 
216
230
  console.log('PAN Agent Plugins bundle built at', path.relative(ROOT, OUT) || OUT);
217
- console.log(' skills:', skills);
231
+ console.log(' skills:', skills.count, `(${skills.split} as a short SKILL.md + ${lib.AGENT_PLUGIN_SKILL_REFERENCE})`);
218
232
  console.log(' agent reference copies:', agents);
219
233
  console.log(' hook scripts:', hookScripts.length);
220
234
  console.log(` ${lib.COPILOT_PLUGIN_NAMESPACE}/agents:`, copilotAgents);
@@ -69,6 +69,10 @@ const EVENT_HOOKS = Object.freeze({
69
69
  subagentStop: ['pan-cost-logger.js', 'pan-trace-logger.js'],
70
70
  stop: ['pan-stop-guard.js'],
71
71
  compact: ['pan-state-reinject.js'],
72
+ // Gemini and Copilot (market-ideas M33): the marker before a compaction, and the
73
+ // block on the next tool result — the same script in two modes.
74
+ compactMark: ['pan-state-reinject.js'],
75
+ compactInject: ['pan-state-reinject.js'],
72
76
  });
73
77
 
74
78
  // ─── Parsers (pure) ─────────────────────────────────────────────────────────