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
@@ -26,7 +26,7 @@ Before verifying, discover project context:
26
26
  1. List available skills (subdirectories)
27
27
  2. Read `SKILL.md` for each skill (lightweight index ~130 lines)
28
28
  3. Load specific `rules/*.md` files as needed during verification
29
- 4. Do NOT load full `AGENTS.md` files (100KB+ context cost)
29
+ 4. Skip the full `AGENTS.md` inside a skill directory (100KB+ context cost). The project's own `AGENTS.md`, which `./CLAUDE.md` may import, is project instructions — read it.
30
30
  5. Apply skill rules when scanning for anti-patterns and verifying quality
31
31
 
32
32
  This ensures project-specific patterns, conventions, and best practices are applied during verification.
@@ -73,8 +73,10 @@ Set `is_re_verification = false`, proceed with Step 0b.
73
73
  If the current phase number > 1, check that the previous phase was verified:
74
74
 
75
75
  ```bash
76
- PREV_PHASE=$((PHASE_NUM - 1))
77
- ls .planning/phase-${PREV_PHASE}*/*-verification.md 2>/dev/null
76
+ # 10# reads a zero-padded number like 08 as decimal (bash would read it as octal)
77
+ PREV_PHASE=$((10#${PHASE_NUM%%[!0-9]*} - 1))
78
+ PREV_DIR=$(node ~/.claude/pan-wizard-core/bin/pan-tools.cjs find-phase "$PREV_PHASE" --raw 2>/dev/null)
79
+ [ -n "$PREV_DIR" ] && ls "$PREV_DIR"/*-verification.md 2>/dev/null
78
80
  ```
79
81
 
80
82
  **If previous phase has no verification.md:**
@@ -101,12 +103,12 @@ Extract phase goal from roadmap.md — this is the outcome to verify, not the ta
101
103
 
102
104
  ## Step 1c: Repo-Norms-First Verification (P-RES-005)
103
105
 
104
- If `.planning/codebase/CONVENTIONS.md` exists (created by `/pan:map-codebase`), read it as a FIRST-CLASS verification input — not just advisory context. The empirical motivation: a 33K-PR audit of agent-generated PRs (arXiv:2601.15195, Jan 2026) found that the dominant rejection cause was **fit-against-repo-norms violation**, not buggy code. Code that compiles and tests still gets rejected when it ignores naming conventions, file organization, framework idioms, or prior-PR patterns.
106
+ If `.planning/codebase/conventions.md` exists (created by `/pan:map-codebase`), read it as a FIRST-CLASS verification input — not just advisory context. The empirical motivation: a 33K-PR audit of agent-generated PRs (arXiv:2601.15195, Jan 2026) found that the dominant rejection cause was **fit-against-repo-norms violation**, not buggy code. Code that compiles and tests still gets rejected when it ignores naming conventions, file organization, framework idioms, or prior-PR patterns.
105
107
 
106
108
  ```bash
107
109
  # Read codebase conventions and structure if available
108
- [ -f .planning/codebase/CONVENTIONS.md ] && cat .planning/codebase/CONVENTIONS.md
109
- [ -f .planning/codebase/STRUCTURE.md ] && cat .planning/codebase/STRUCTURE.md
110
+ [ -f .planning/codebase/conventions.md ] && cat .planning/codebase/conventions.md
111
+ [ -f .planning/codebase/structure.md ] && cat .planning/codebase/structure.md
110
112
  ```
111
113
 
112
114
  **How to use this in verification:**
@@ -115,11 +117,11 @@ If `.planning/codebase/CONVENTIONS.md` exists (created by `/pan:map-codebase`),
115
117
 
116
118
  2. **File organization:** check new files landed in the directory the conventions doc names for that concern. Auth code in `lib/auth/` not `src/auth/` if conventions said `lib/`.
117
119
 
118
- 3. **Framework idioms:** if CONVENTIONS.md names "we use X over Y because Z", check the executor didn't import Y. Generic correctness isn't enough — local idiom-fit matters.
120
+ 3. **Framework idioms:** if conventions.md names "we use X over Y because Z", check the executor didn't import Y. Generic correctness isn't enough — local idiom-fit matters.
119
121
 
120
- 4. **Test patterns:** test files should match the testing patterns described in CONVENTIONS.md (test file naming, fixture organization, assertion style).
122
+ 4. **Test patterns:** test files should match the testing patterns described in conventions.md (test file naming, fixture organization, assertion style).
121
123
 
122
- If CONVENTIONS.md does not exist, skip this step silently. This is brownfield-only signal. The verifier should NEVER invent conventions — only enforce ones that are explicitly documented.
124
+ If conventions.md does not exist, skip this step silently. This is brownfield-only signal. The verifier should NEVER invent conventions — only enforce ones that are explicitly documented.
123
125
 
124
126
  ## Step 1b: Read the Reasoning Trace (P-RES-003)
125
127
 
@@ -481,11 +483,17 @@ Unrequested work never changes the status on its own; a phase can pass with it.
481
483
  **Why human:** {Why can't verify programmatically}
482
484
  ```
483
485
 
486
+ ## Step 8b: Run the Test Gate
487
+
488
+ Run the project's test suite exactly as the verify-phase protocol's `run_test_suite` step says (`~/.claude/pan-wizard-core/workflows/verify-phase.md`): detect a test script first, then run it and judge the exit code before any count. Record the result in the frontmatter as `test_gate: passed | failed | skipped`. `skipped` is only for a project with no test script; a suite that exists and did not run is `failed`.
489
+
490
+ A gate you could not run goes in `not_checked` with the reason, e.g. `- check: "tests"` / `reason: "no test script in package.json"`. If the orchestrator already ran the suite and gave you the result, record that result rather than running it again.
491
+
484
492
  ## Step 9: Determine Overall Status
485
493
 
486
- **Status: passed** — All truths VERIFIED, all artifacts pass levels 1-3, all key links WIRED, no blocker anti-patterns.
494
+ **Status: passed** — All truths VERIFIED, all artifacts pass levels 1-3, all key links WIRED, no blocker anti-patterns, and the test gate passed or was skipped. A skipped gate stays visible: it is listed in `not_checked`, so the pass says what it did not check.
487
495
 
488
- **Status: gaps_found** — One or more truths FAILED, artifacts MISSING/STUB, key links NOT_WIRED, or blocker anti-patterns found.
496
+ **Status: gaps_found** — One or more truths FAILED, artifacts MISSING/STUB, key links NOT_WIRED, blocker anti-patterns found, or the test gate FAILED.
489
497
 
490
498
  **Status: human_needed** — All automated checks pass but items flagged for human verification.
491
499
 
@@ -523,14 +531,21 @@ gaps:
523
531
 
524
532
  **ALWAYS use the Write tool to create files** — never use `Bash(cat << 'EOF')` or heredoc commands for file creation.
525
533
 
534
+ Record `verified_commit` from `git rev-parse HEAD` before you write the file. It ties the verdict to the code you checked: `pan-tools verify stale <phase>` and `pan-tools progress` compare the phase's files against it, and a file changed since then marks this verification stale.
535
+
526
536
  Create `.planning/phases/{phase_dir}/{phase_num}-verification.md`:
527
537
 
528
538
  ```markdown
529
539
  ---
530
540
  phase: XX-name
531
541
  verified: YYYY-MM-DDTHH:MM:SSZ
542
+ verified_commit: <sha> # `git rev-parse HEAD` before you write this file: the code you verified
532
543
  status: passed | gaps_found | human_needed
533
544
  score: N/M must-haves verified
545
+ test_gate: passed | failed | skipped # Step 8b
546
+ not_checked: # Only for checks that could not run — never count them as passed
547
+ - check: "tests"
548
+ reason: "no test script in package.json"
534
549
  re_verification: # Only if previous verification.md existed
535
550
  previous_status: gaps_found
536
551
  previous_score: 2/5
@@ -438,7 +438,7 @@ function convertClaudeCommandToCodexSkill(content, skillName) {
438
438
  description = toSingleLine(description);
439
439
  const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
440
440
  const adapter = getCodexSkillAdapterHeader(skillName);
441
- return `---\nname: ${yamlQuote(skillName)}\ndescription: ${yamlQuote(description)}\nmetadata:\n short-description: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
441
+ return `---\nname: ${yamlQuote(skillName)}\ndescription: ${yamlQuote(description)}\nlicense: ${yamlQuote(SKILL_LICENSE)}\nmetadata:\n short-description: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
442
442
  }
443
443
 
444
444
  /**
@@ -474,6 +474,12 @@ ${note ? `\n${note}\n` : ''}</pan_skill_adapter>`;
474
474
  * for environment requirements (max 500 chars). Kept short and factual: these
475
475
  * are the two things a host cannot infer and that every PAN skill depends on.
476
476
  */
477
+ // The Agent Skills `license` field: optional in the spec, and GitHub's validator
478
+ // (`gh skill publish --dry-run`) warns on every skill without it (market-ideas M30).
479
+ // Read from package.json so the skills always carry the package's own licence.
480
+ const SKILL_LICENSE = (() => {
481
+ try { return require('../package.json').license || 'MIT'; } catch { return 'MIT'; }
482
+ })();
477
483
  const SKILL_COMPATIBILITY = 'Requires Node.js (skills invoke the bundled pan-tools CLI) and a project with a .planning/ directory, created by /pan-new-project or /pan-map-codebase.';
478
484
 
479
485
  function convertClaudeCommandToUnifiedSkill(content, skillName, opts = {}) {
@@ -500,7 +506,7 @@ function convertClaudeCommandToUnifiedSkill(content, skillName, opts = {}) {
500
506
  // Deliberately NOT emitting `allowed-tools`: it is marked experimental in the
501
507
  // spec, and ADR-0028's frontmatter rule is that anything unverified stays out
502
508
  // until a live per-runtime check confirms no parser rejects it.
503
- return `---\nname: ${yamlQuote(skillName)}\ndescription: ${yamlQuote(description)}\ncompatibility: ${yamlQuote(SKILL_COMPATIBILITY)}\nmetadata:\n short-description: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
509
+ return `---\nname: ${yamlQuote(skillName)}\ndescription: ${yamlQuote(description)}\nlicense: ${yamlQuote(SKILL_LICENSE)}\ncompatibility: ${yamlQuote(SKILL_COMPATIBILITY)}\nmetadata:\n short-description: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
504
510
  }
505
511
 
506
512
  // ─── Unified-skill content rewrites (extracted from bin/install.js, 2026-09) ──
@@ -707,6 +713,65 @@ function agentPluginSkillAdapterNote(kind = 'skill') {
707
713
  - \`${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.`;
708
714
  }
709
715
 
716
+ // ─── Agent Plugin skills over Codex's prompt cut (market-ideas S8) ──────────
717
+ // Codex injects at most the first 8,000 bytes of a skill that came from an Agent
718
+ // Plugin (codex-rs/ext/skills/src/render.rs `MAX_SKILL_PROMPT_BYTES`, host_prompt.rs).
719
+ // Measured on Codex CLI 0.157.1 on 2026-10-03: a 56,540-byte PAN skill reached the
720
+ // model as its first 8,000 bytes, cut mid-sentence; the same skill from an install's
721
+ // .agents/skills/ arrived whole, so only the bundle is affected. A bundle skill over
722
+ // the cut ships as a short SKILL.md that keeps its frontmatter and adapter block and
723
+ // points at the full instructions in references/instructions.md, the Agent Skills
724
+ // directory for files a skill loads on demand.
725
+ const AGENT_PLUGIN_SKILL_MAX_BYTES = 8000;
726
+ const AGENT_PLUGIN_SKILL_REFERENCE = 'references/instructions.md';
727
+ const SKILL_SUMMARY_MIN_CHARS = 40;
728
+
729
+ /**
730
+ * Split a converted bundle skill that is longer than Codex loads into a pointer
731
+ * SKILL.md and a reference file. Pure. A skill within the limit comes back as is.
732
+ * @param {string} skillMd - the unified converter's output (frontmatter + adapter + body)
733
+ * @param {string} skillName
734
+ * @param {number} [maxBytes]
735
+ * @returns {{skill: string, reference: string|null}}
736
+ */
737
+ function splitOversizedSkill(skillMd, skillName, maxBytes = AGENT_PLUGIN_SKILL_MAX_BYTES) {
738
+ if (Buffer.byteLength(skillMd, 'utf8') <= maxBytes) return { skill: skillMd, reference: null };
739
+ const close = '</pan_skill_adapter>';
740
+ const end = skillMd.indexOf(close);
741
+ if (end === -1) throw new Error(`splitOversizedSkill: ${skillName} has no adapter block to keep`);
742
+ const head = skillMd.slice(0, end + close.length);
743
+ const body = skillMd.slice(end + close.length).replace(/^\s+/, '');
744
+ const pointer = (summary) => [
745
+ head,
746
+ '',
747
+ '## Instructions',
748
+ '',
749
+ `This skill's full instructions are in \`${AGENT_PLUGIN_SKILL_REFERENCE}\`, in the same directory as this SKILL.md: they are longer than some runtimes load into a prompt. Before you do anything else, read that file in full with your file-reading tool, then follow it exactly. It is the complete procedure, and nothing on this page replaces it.`,
750
+ ...(summary ? ['', '## What this skill does', '', summary] : []),
751
+ '',
752
+ ].join('\n');
753
+ // The command's own <objective> is the summary, else its first plain prose
754
+ // paragraph; trimmed at word boundaries until the page fits, and dropped when
755
+ // too little of it would be left.
756
+ let summary = ((body.match(/<objective>\s*([\s\S]*?)\s*<\/objective>/) || [])[1] || '').trim();
757
+ if (!summary) {
758
+ summary = body.split(/\n\s*\n/).map((p) => p.trim())
759
+ .find((p) => p.length >= SKILL_SUMMARY_MIN_CHARS && !/^[#<`|>\-*]/.test(p) && !/^\d+\./.test(p)) || '';
760
+ }
761
+ let skill = pointer(summary);
762
+ while (summary && Buffer.byteLength(skill, 'utf8') > maxBytes) {
763
+ summary = summary.slice(0, Math.floor(summary.length * 0.8)).replace(/\s+\S*$/, '');
764
+ if (summary.length < SKILL_SUMMARY_MIN_CHARS) summary = '';
765
+ else summary += ' …';
766
+ skill = pointer(summary);
767
+ }
768
+ if (Buffer.byteLength(skill, 'utf8') > maxBytes) {
769
+ throw new Error(`splitOversizedSkill: ${skillName}'s frontmatter and adapter alone exceed ${maxBytes} bytes`);
770
+ }
771
+ const reference = `# ${skillName}: full instructions\n\nRead from \`${skillName}/SKILL.md\`, whose adapter block defines \`${AGENT_PLUGIN_ROOT_TOKEN}\`, \`{{PAN_ARGS}}\` and the runtime-directory tokens used below.\n\n${body}`;
772
+ return { skill, reference };
773
+ }
774
+
710
775
  /** Generate Copilot CLI skill adapter header */
711
776
  function getCopilotSkillAdapterHeader(skillName) {
712
777
  const invocation = `/pan-${skillName.replace(/^pan-/, '')}`;
@@ -743,7 +808,7 @@ function convertClaudeCommandToCopilotSkill(content, skillName) {
743
808
  description = toSingleLine(description);
744
809
  const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
745
810
  const adapter = getCopilotSkillAdapterHeader(skillName);
746
- return `---\nname: ${yamlQuote(skillName)}\ndescription: ${yamlQuote(description)}\nmetadata:\n short-description: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
811
+ return `---\nname: ${yamlQuote(skillName)}\ndescription: ${yamlQuote(description)}\nlicense: ${yamlQuote(SKILL_LICENSE)}\nmetadata:\n short-description: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
747
812
  }
748
813
 
749
814
  /** Claude agent → Copilot .agent.md */
@@ -838,6 +903,31 @@ function convertClaudeToCopilotAgent(content, opts = {}) {
838
903
 
839
904
  // ─── Attribution Processing ─────────────────────────────────────────────────
840
905
 
906
+ /**
907
+ * The commit attribution Claude Code's settings ask for, in processAttribution's
908
+ * terms: undefined keeps PAN's default `Co-Authored-By` line, null removes it, a
909
+ * string replaces it. Claude Code's settings reference (read 2026-10-03):
910
+ * `attribution: false` hides all attribution (2.1.281 and later); an empty
911
+ * `attribution.commit` hides the commit trailer and a non-empty one replaces it; once
912
+ * `commit` or `pr` is set the deprecated `includeCoAuthoredBy` is ignored, and before
913
+ * that `includeCoAuthoredBy: false` still hides the trailer. Until 2026-10-03 PAN
914
+ * read only `attribution.commit`, so `false` (an explicit request to hide) kept the
915
+ * default line.
916
+ * @param {object|null} settings - a parsed Claude settings.json
917
+ * @returns {string|null|undefined}
918
+ */
919
+ function commitAttributionFromSettings(settings) {
920
+ const s = settings && typeof settings === 'object' ? settings : {};
921
+ const a = s.attribution;
922
+ if (a === false) return null;
923
+ if (a && typeof a === 'object') {
924
+ if (typeof a.commit === 'string') return a.commit === '' ? null : a.commit;
925
+ if (a.pr !== undefined) return undefined; // attribution set: includeCoAuthoredBy no longer applies
926
+ }
927
+ if (s.includeCoAuthoredBy === false) return null;
928
+ return undefined;
929
+ }
930
+
841
931
  /**
842
932
  * Process Co-Authored-By lines based on attribution setting.
843
933
  * @param {string} content - File content
@@ -1042,16 +1132,25 @@ function stripThinkingFrontmatter(content, runtime) {
1042
1132
  * @param {Object} commands
1043
1133
  * @param {string} commands.updateCheckCommand - node invocation for pan-check-update.js
1044
1134
  * @param {string} commands.contextMonitorCommand - node invocation for pan-context-monitor.js
1135
+ * @param {string} [commands.stateReinjectMarkCommand] - pan-state-reinject.js `--mark` (preCompact)
1136
+ * @param {string} [commands.stateReinjectInjectCommand] - pan-state-reinject.js `--inject copilot` (postToolUse)
1045
1137
  * @returns {Object} A `.github/hooks/pan.json` config object
1046
1138
  */
1047
1139
  function buildCopilotHooksConfig(commands) {
1048
- const { updateCheckCommand, contextMonitorCommand, costLoggerCommand, traceLoggerCommand, stopGuardCommand } = commands || {};
1140
+ const { updateCheckCommand, contextMonitorCommand, costLoggerCommand, traceLoggerCommand, stopGuardCommand,
1141
+ stateReinjectMarkCommand, stateReinjectInjectCommand } = commands || {};
1049
1142
  const config = { version: 1, hooks: {} };
1050
1143
  if (updateCheckCommand) {
1051
1144
  config.hooks.sessionStart = [{ type: 'command', command: updateCheckCommand }];
1052
1145
  }
1053
- if (contextMonitorCommand) {
1054
- config.hooks.postToolUse = [{ type: 'command', command: contextMonitorCommand }];
1146
+ const postToolUse = [];
1147
+ if (contextMonitorCommand) postToolUse.push({ type: 'command', command: contextMonitorCommand });
1148
+ // State re-injection after a compaction (M33): preCompact leaves the marker, the
1149
+ // next postToolUse returns the block once (hooks/pan-state-reinject.js).
1150
+ if (stateReinjectInjectCommand) postToolUse.push({ type: 'command', command: stateReinjectInjectCommand });
1151
+ if (postToolUse.length > 0) config.hooks.postToolUse = postToolUse;
1152
+ if (stateReinjectMarkCommand) {
1153
+ config.hooks.preCompact = [{ type: 'command', command: stateReinjectMarkCommand }];
1055
1154
  }
1056
1155
  // subagentStop is Copilot's SubagentStop equivalent (verified docs.github.com
1057
1156
  // 2026-06) — carries the cost + trace loggers, same as Claude and Codex (Gemini registers neither).
@@ -1107,15 +1206,20 @@ function buildCopilotHooksConfig(commands) {
1107
1206
  * `compact` is where the state re-injection hook registers (M10): SessionStart with
1108
1207
  * the `compact` matcher (HOOK_SLOT_MATCHERS), the one event after a compaction whose
1109
1208
  * `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.
1209
+ * both hosts' PostCompact discard it (hooks references, read 2026-09-26).
1210
+ *
1211
+ * `compactMark` / `compactInject` (market-ideas M33) carry it where no start event
1212
+ * follows a compaction. Gemini's PreCompress and Copilot's preCompact are advisory,
1213
+ * so the hook runs in two steps: a marker before the compaction, the block on the
1214
+ * next tool result, the first point after it where both hosts take added context
1215
+ * (gemini-cli v0.61.0 docs/hooks/reference.md; Copilot hooks reference; read
1216
+ * 2026-10-03, and live on Copilot CLI 1.0.88 the same day).
1113
1217
  */
1114
1218
  const HOOK_EVENT_MAP = Object.freeze({
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 },
1219
+ claude: { surface: 'settings.json', sessionStart: 'SessionStart', postToolUse: 'PostToolUse', subagentStop: 'SubagentStop', stop: 'Stop', compact: 'SessionStart', compactMark: null, compactInject: null },
1220
+ gemini: { surface: 'settings.json', sessionStart: 'SessionStart', postToolUse: null, subagentStop: null, stop: 'AfterAgent', compact: null, compactMark: 'PreCompress', compactInject: 'AfterTool' },
1221
+ codex: { surface: 'hooks.json', sessionStart: 'SessionStart', postToolUse: 'PostToolUse', subagentStop: 'SubagentStop', stop: 'Stop', compact: 'SessionStart', compactMark: null, compactInject: null },
1222
+ copilot: { surface: 'hooks/pan.json', sessionStart: 'sessionStart', postToolUse: 'postToolUse', subagentStop: 'subagentStop', stop: 'agentStop', compact: null, compactMark: 'preCompact', compactInject: 'postToolUse' },
1119
1223
  opencode: null,
1120
1224
  });
1121
1225
 
@@ -1140,14 +1244,16 @@ const PAN_SETTINGS_HOOKS = Object.freeze(['pan-check-update', 'pan-context-monit
1140
1244
  *
1141
1245
  * @param {object} hooks - settings.hooks
1142
1246
  * @param {string[]} hookNames - script basenames without `.js` (e.g. 'pan-stop-guard')
1143
- * @param {string|null} [keepEvent] - the event the hook now belongs to, left alone
1247
+ * @param {string|string[]|null} [keepEvent] - the event (or events: a hook such as
1248
+ * pan-state-reinject registers under two on Gemini) the hook now belongs to, left alone
1144
1249
  * @returns {string[]}
1145
1250
  */
1146
1251
  function stripPanHookEntries(hooks, hookNames, keepEvent = null) {
1147
1252
  const touched = [];
1148
1253
  if (!hooks || typeof hooks !== 'object' || Array.isArray(hooks)) return touched;
1254
+ const keep = new Set([].concat(keepEvent || []));
1149
1255
  for (const event of Object.keys(hooks)) {
1150
- if (event === keepEvent || !Array.isArray(hooks[event])) continue;
1256
+ if (keep.has(event) || !Array.isArray(hooks[event])) continue;
1151
1257
  const before = hooks[event].length;
1152
1258
  hooks[event] = hooks[event].filter(entry => !(entry && Array.isArray(entry.hooks)
1153
1259
  && entry.hooks.some(h => h && typeof h.command === 'string' && hookNames.some(n => h.command.includes(n)))));
@@ -1719,6 +1825,49 @@ function verifyInstall(configDir, manifest) {
1719
1825
  return { ok: missing.length === 0 && empty.length === 0, missing, empty, warnings };
1720
1826
  }
1721
1827
 
1828
+ /**
1829
+ * The script a PAN hook command runs, as written in the command: `node "<path>"`
1830
+ * (global installs, buildHookCommand) or `node <dir>/hooks/<hook>.js` (local
1831
+ * installs, relative to the project root). Null for any other command shape.
1832
+ * @param {string} command
1833
+ * @returns {string|null}
1834
+ */
1835
+ function hookCommandScript(command) {
1836
+ // Plain arguments may follow the script (`--mark`, `--inject gemini`, M33).
1837
+ const m = /^node\s+(?:"([^"]+)"|([^\s"]+))(?:\s+[^\s"]+)*$/.exec(String(command || '').trim());
1838
+ return m ? (m[1] || m[2]) : null;
1839
+ }
1840
+
1841
+ /**
1842
+ * Check that every hook command an install is about to register runs a script
1843
+ * that exists and has content (market-ideas M24). Read-only: nothing is executed
1844
+ * or written. A dev install from a clone without `npm run build:hooks` copied no
1845
+ * hook scripts and registered every hook anyway, so each session event ran a
1846
+ * missing file. A relative script resolves against `projectRoot`, the directory a
1847
+ * local install's hook commands run from.
1848
+ *
1849
+ * @param {string[]} commands - the hook commands to register
1850
+ * @param {string} projectRoot
1851
+ * @returns {{ok: boolean, missing: string[]}} each missing entry names the script and why
1852
+ */
1853
+ function verifyHookEntrypoints(commands, projectRoot) {
1854
+ const missing = [];
1855
+ for (const command of commands) {
1856
+ const script = hookCommandScript(command);
1857
+ if (!script) {
1858
+ missing.push(`${command} (not a "node <script>" command)`);
1859
+ continue;
1860
+ }
1861
+ const abs = path_v.isAbsolute(script) ? script : path_v.join(projectRoot, script);
1862
+ try {
1863
+ if (fs_v.statSync(abs).size === 0) missing.push(`${script} (empty)`);
1864
+ } catch {
1865
+ missing.push(`${script} (missing)`);
1866
+ }
1867
+ }
1868
+ return { ok: missing.length === 0, missing };
1869
+ }
1870
+
1722
1871
  // ─── Claude Code plugin packaging (2026-06) ─────────────────────────────────
1723
1872
 
1724
1873
  /**
@@ -2353,6 +2502,7 @@ module.exports = {
2353
2502
  convertClaudeToCopilotAgent,
2354
2503
  // Attribution
2355
2504
  processAttribution,
2505
+ commitAttributionFromSettings,
2356
2506
  // JSONC
2357
2507
  parseJsonc,
2358
2508
  // Opus 4.7 capabilities
@@ -2392,6 +2542,9 @@ module.exports = {
2392
2542
  buildAgentPluginManifest,
2393
2543
  buildAgentPluginMcpConfig,
2394
2544
  agentPluginSkillAdapterNote,
2545
+ AGENT_PLUGIN_SKILL_MAX_BYTES,
2546
+ AGENT_PLUGIN_SKILL_REFERENCE,
2547
+ splitOversizedSkill,
2395
2548
  buildCopilotPluginHooksConfig,
2396
2549
  COPILOT_PLUGIN_NAMESPACE,
2397
2550
  buildPluginManifest,
@@ -2400,6 +2553,8 @@ module.exports = {
2400
2553
  buildPluginSelfTestCommand,
2401
2554
  // Install verification (v3.7.10)
2402
2555
  verifyInstall,
2556
+ hookCommandScript,
2557
+ verifyHookEntrypoints,
2403
2558
  // AGENTS.md universal rules layer (ADR-0028 Phase 3)
2404
2559
  buildAgentsMdSection,
2405
2560
  upsertAgentsMdSection,
package/bin/install.js CHANGED
@@ -364,15 +364,10 @@ function getCommitAttribution(runtime) {
364
364
  result = settings.attribution.commit;
365
365
  }
366
366
  } else if (runtime === 'claude') {
367
- // Claude Code
367
+ // Claude Code: `attribution: false`, `attribution.commit` and the deprecated
368
+ // `includeCoAuthoredBy`, resolved in the documented order (install-lib).
368
369
  const settings = readSettings(path.join(getGlobalDir('claude', explicitConfigDir), 'settings.json')) || {}; // unusable file = no info (read-only probe)
369
- if (!settings.attribution || settings.attribution.commit === undefined) {
370
- result = undefined;
371
- } else if (settings.attribution.commit === '') {
372
- result = null;
373
- } else {
374
- result = settings.attribution.commit;
375
- }
370
+ result = lib.commitAttributionFromSettings(settings);
376
371
  } else if (runtime === 'copilot') {
377
372
  // Copilot CLI: user-editable settings live in settings.json; config.json is
378
373
  // legacy (auto-migrated by the CLI, now internal state) — fall back for old installs
@@ -2744,6 +2739,28 @@ function install(isGlobal, runtime = 'claude') {
2744
2739
  const stateReinjectCommand = isGlobal
2745
2740
  ? buildHookCommand(targetDir, 'pan-state-reinject.js')
2746
2741
  : 'node ' + dirName + '/hooks/pan-state-reinject.js';
2742
+ // Gemini and Copilot (M33): a marker before the compaction, the block on the next
2743
+ // tool result. The inject mode names the host, whose output shapes differ.
2744
+ const stateReinjectMarkCommand = `${stateReinjectCommand} --mark`;
2745
+ const stateReinjectInjectCommand = `${stateReinjectCommand} --inject ${isCopilot ? 'copilot' : 'gemini'}`;
2746
+
2747
+ // Every hook command about to be registered must run a script that is there
2748
+ // (market-ideas M24). The copy above skips the hooks when hooks/dist is absent (a
2749
+ // source checkout without `npm run build:hooks`), and every hook was registered
2750
+ // anyway, so each session event then ran a missing file. Fail before writing any
2751
+ // runtime config. OpenCode registers no hooks.
2752
+ if (!isOpencode) {
2753
+ const entrypoints = lib.verifyHookEntrypoints([
2754
+ updateCheckCommand, contextMonitorCommand, statuslineCommand, costLoggerCommand,
2755
+ traceLoggerCommand, stopGuardCommand, stateReinjectCommand, stateReinjectMarkCommand, stateReinjectInjectCommand,
2756
+ ], process.cwd());
2757
+ if (!entrypoints.ok) {
2758
+ console.error(`\n ${red}✖ Hook verification FAILED — these hook commands would run a script that is not there:${reset}`);
2759
+ for (const m of entrypoints.missing) console.error(` - ${m}`);
2760
+ console.error(`\n No hooks were registered. In a source checkout, run ${cyan}npm run build:hooks${reset} first, then re-run the installer.\n`);
2761
+ process.exit(1);
2762
+ }
2763
+ }
2747
2764
 
2748
2765
  if (isCodex) {
2749
2766
  // Codex hooks (2026-06): Claude-compatible PascalCase events in the shared
@@ -2773,12 +2790,12 @@ function install(isGlobal, runtime = 'claude') {
2773
2790
  // from any legacy config.json registration.
2774
2791
  if (isCopilot) {
2775
2792
  const hooksConfigPath = path.join(targetDir, 'hooks', 'pan.json');
2776
- const hooksConfig = buildCopilotHooksConfig({ updateCheckCommand, contextMonitorCommand, costLoggerCommand, traceLoggerCommand, stopGuardCommand });
2793
+ const hooksConfig = buildCopilotHooksConfig({ updateCheckCommand, contextMonitorCommand, costLoggerCommand, traceLoggerCommand, stopGuardCommand, stateReinjectMarkCommand, stateReinjectInjectCommand });
2777
2794
  try {
2778
2795
  fs.mkdirSync(path.dirname(hooksConfigPath), { recursive: true });
2779
2796
  fs.writeFileSync(hooksConfigPath, JSON.stringify(hooksConfig, null, 2) + '\n');
2780
2797
  // Same as the Codex case: a global Copilot install writes to ~/.copilot/, not .github/.
2781
- console.log(` ${green}✓${reset} Configured hooks (${displayPath(hooksConfigPath)}: update check, context monitor, cost + trace loggers, stop guard)`);
2798
+ console.log(` ${green}✓${reset} Configured hooks (${displayPath(hooksConfigPath)}: update check, context monitor, cost + trace loggers, stop guard, state re-injection)`);
2782
2799
  } catch (e) {
2783
2800
  console.error(` ${yellow}✗${reset} Failed to write Copilot hooks config: ${e.message}`);
2784
2801
  }
@@ -2872,19 +2889,27 @@ function install(isGlobal, runtime = 'claude') {
2872
2889
  { slot: 'subagentStop', hook: 'pan-trace-logger', command: traceLoggerCommand, label: 'trace logger hook' },
2873
2890
  { slot: 'stop', hook: 'pan-stop-guard', command: stopGuardCommand, label: 'auto-advance stop guard hook' },
2874
2891
  { slot: 'compact', hook: 'pan-state-reinject', command: stateReinjectCommand, label: 'state re-injection hook (after compaction)' },
2892
+ // Gemini (M33): no start event follows a compaction, so a marker before it and
2893
+ // the block on the next tool result.
2894
+ { slot: 'compactMark', hook: 'pan-state-reinject', command: stateReinjectMarkCommand, label: 'state re-injection marker (before compaction)' },
2895
+ { slot: 'compactInject', hook: 'pan-state-reinject', command: stateReinjectInjectCommand, label: 'state re-injection (first tool result after compaction)' },
2875
2896
  ];
2897
+ // Every event a hook registers under on this runtime: a hook may have more than
2898
+ // one (pan-state-reinject on Gemini), and the upgrade sweep must leave all of them.
2899
+ const eventsOf = (hook) => registrations.filter((x) => x.hook === hook).map((x) => events[x.slot]).filter(Boolean);
2876
2900
  const unsupported = [];
2877
2901
  for (const r of registrations) {
2878
2902
  const event = events[r.slot] || null;
2879
2903
  // Upgrade path: an entry for this hook under any other event is dead (a key
2880
2904
  // the runtime does not have) or a stale duplicate. Keyed on the script, so a
2881
2905
  // hook that moved event is cleaned wherever an older install put it.
2882
- const removedFrom = lib.stripPanHookEntries(settings.hooks, [r.hook], event);
2906
+ const keep = eventsOf(r.hook);
2907
+ const removedFrom = lib.stripPanHookEntries(settings.hooks, [r.hook], keep);
2883
2908
  if (removedFrom.length > 0) {
2884
- console.log(` ${green}✓${reset} Removed the ${r.label} from ${removedFrom.join(', ')}${event ? '' : ` (${runtime} has no event for it)`}`);
2909
+ console.log(` ${green}✓${reset} Removed the ${r.label} from ${removedFrom.join(', ')}${keep.length ? '' : ` (${runtime} has no event for it)`}`);
2885
2910
  }
2886
2911
  if (!event) {
2887
- unsupported.push(r.hook);
2912
+ if (keep.length === 0 && !unsupported.includes(r.hook)) unsupported.push(r.hook);
2888
2913
  continue;
2889
2914
  }
2890
2915
  if (!Array.isArray(settings.hooks[event])) {
@@ -2903,7 +2928,7 @@ function install(isGlobal, runtime = 'claude') {
2903
2928
  delete settings.hooks;
2904
2929
  }
2905
2930
  if (isGemini && unsupported.length > 0) {
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}`);
2931
+ 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}`);
2907
2932
  }
2908
2933
  }
2909
2934
 
@@ -36,9 +36,9 @@ The army is the campaign-scale sibling of `/pan:exec-phase --hierarchical` (one
36
36
 
37
37
  **Reading the Model-tier column.** These are PAN *tiers*, not model names. `reasoning` resolves to `inherit` — the model you launched with — while `mid` and `fast` map to the provider's mid/fast models (Sonnet and Haiku on Anthropic).
38
38
 
39
- **Reading the Access column.** The backticked values on the tier-1 rows are the `access` labels `squad list` reports; tier 0 and tier 2 carry PAN's own. They are role contracts the conductor's prompt assigns when it delegates, not a sandbox: `squads.cjs` says of itself that it "modifies no agent and changes no execution path", so a label can differ from what an agent may actually do — expect that, since several `read-only` squad members hold `Write` to emit planning or verification artifacts. The binding grant is each agent's own `tools:` frontmatter (`grep '^tools:' agents/*.md`), and Mission Control's includes `Write` and `Bash`: routing rather than coding is how it is instructed to behave, not something the runtime prevents. The rail those grants do enforce is delegation depth — `grep -l '^tools:.*Task' agents/*.md` names every agent able to spawn another (today, `pan-conductor`), so a squad agent cannot fan out further.
39
+ **Reading the Access column.** The backticked values on the tier-1 rows are the `access` labels `squad list` reports; tier 0 and tier 2 carry PAN's own. They are role contracts the conductor's prompt assigns when it delegates, not a sandbox: `squads.cjs` says of itself that it "modifies no agent and changes no execution path", so a label can differ from what an agent may actually do — expect that, since several `read-only` squad members hold `Write` to emit planning or verification artifacts. The binding grant is each agent's own `tools:` frontmatter (`grep '^tools:' ~/.claude/agents/*.md`), and Mission Control's includes `Write` and `Bash`: routing rather than coding is how it is instructed to behave, not something the runtime prevents. The rail those grants do enforce is delegation depth — `grep -l '^tools:.*Task' ~/.claude/agents/*.md` names every agent able to spawn another (today, `pan-conductor`), so a squad agent cannot fan out further.
40
40
 
41
- **Squad tier is not profile tier.** The tier column above is a `squads.cjs` grouping attribute — what `pan-tools squad list` reports — and it is not what resolves an agent's model; that comes from the active `model_profile` (`quality` and `balanced` are `reasoning` for every agent, and `budget` is the only profile that down-tiers), plus any `model:` pin in an agent's own frontmatter. So the `mid` on the Quality and Release rows does not describe what those agents run under the default profile — under `quality` and `balanced` they resolve `reasoning` like everything else. The tier-1 values above are the squad groupings; the tier-0 and tier-2 values are per-agent profile tiers. **`budget` resolves per agent, not per row:** it sends some workers to `fast` and others to `mid`, so no single value is true of the Tier 2 row — `MODEL_PROFILES` in `pan-wizard-core/bin/lib/core.cjs` is the table, and it is the one to read rather than a tier written into a doc. For the agents that pin a model outright, `grep -l '^model: opus' agents/*.md` lists them — the pin applies on Claude Code only, since the installer strips it for the other runtimes.
41
+ **Squad tier is not profile tier.** The tier column above is a `squads.cjs` grouping attribute — what `pan-tools squad list` reports — and it is not what resolves an agent's model; that comes from the active `model_profile` (`quality` and `balanced` are `reasoning` for every agent, and `budget` is the only profile that down-tiers), plus any `model:` pin in an agent's own frontmatter. So the `mid` on the Quality and Release rows does not describe what those agents run under the default profile — under `quality` and `balanced` they resolve `reasoning` like everything else. The tier-1 values above are the squad groupings; the tier-0 and tier-2 values are per-agent profile tiers. **`budget` resolves per agent, not per row:** it sends some workers to `fast` and others to `mid`, so no single value is true of the Tier 2 row — `MODEL_PROFILES` in `~/.claude/pan-wizard-core/bin/lib/core.cjs` is the table, and it is the one to read rather than a tier written into a doc. For the agents that pin a model outright, `grep -l '^model: opus' ~/.claude/agents/*.md` lists them — the pin applies on Claude Code only, since the installer strips it for the other runtimes.
42
42
 
43
43
  ---
44
44
 
@@ -32,7 +32,7 @@ allowed-tools:
32
32
 
33
33
  - Building production user features. Use `/pan:new-project` and `/pan:exec-phase` directly.
34
34
  - Validating a single-file change. The experiment loop is heavy — use `npm test` and `/pan:quick`.
35
- - Inside the PAN source repo. The command **refuses** to scaffold experiments inside `d:\PanWizard\` (or wherever the source is cloned). The experiment root defaults to `~/pan-experiments/`.
35
+ - Inside the PAN source repo. The command **refuses** to scaffold experiments inside PAN's source repository, wherever it is cloned. The experiment root defaults to `~/pan-experiments/`.
36
36
 
37
37
  ---
38
38
 
@@ -208,7 +208,7 @@ pan-tools experiment manifest md-lint
208
208
 
209
209
  ## Safety guards
210
210
 
211
- - **Never inside source repo.** `newExperiment` refuses to write to `d:\PanWizard\` (or wherever the PAN source is). Mirrors `bin/install.js` `PAN_SOURCE_ROOT` guard.
211
+ - **Never inside source repo.** `newExperiment` refuses to write inside PAN's source repository, wherever it is cloned. Mirrors `bin/install.js` `PAN_SOURCE_ROOT` guard.
212
212
  - **No clobber.** Refuses to scaffold over an existing experiment folder of the same slug.
213
213
  - **Slug validation.** Lowercase + digits + hyphens, max 40 chars. Rejects uppercase, spaces, special characters.
214
214
  - **Idea path validation.** Errors if the `--idea` file doesn't exist.
@@ -88,7 +88,8 @@ Wait for the user's reply before proceeding. Do not guess or pick a default cate
88
88
 
89
89
  ```
90
90
  /pan:focus-auto [--source scan|backlog] [--category CAT] [--mode MODE] [--budget N]
91
- [--max-cycles N] [--total-budget N] [--continue] [--stop] [--status]
91
+ [--max-cycles N] [--total-budget N] [--enforce-budget] [--verify-reserve F]
92
+ [--continue] [--stop] [--status]
92
93
  [--dry-run] [--deep-review]
93
94
  [--parallel-research] [--parallel-verify] [--clean-seal]
94
95
  ```
@@ -124,6 +125,7 @@ Wait for the user's reply before proceeding. Do not guess or pick a default cate
124
125
  | optimize | P1-P4 | balanced | 50 |
125
126
  | prompts | P0-P6 | balanced | 100 |
126
127
  | security | P0-P2 | bugfix | 40 |
128
+ | distill | P1-P5 | balanced | 50 |
127
129
 
128
130
  ## Backlog source (`--source backlog`, ADR-0031)
129
131
 
@@ -162,7 +164,8 @@ The proven shape is **parallel read-only research → exactly ONE serial impleme
162
164
  - Skip to Phase 2 (Main Loop)
163
165
  4. If no `--category` was provided, you already displayed the menu in FIRST ACTION above. Use SELECTED_CATEGORY from the user's reply.
164
166
  5. Initialize new run using the category from step 4:
165
- - Run `pan-tools focus auto --category <SELECTED_CATEGORY> [--mode MODE] [--budget N] [--max-cycles N] [--total-budget N] [--dry-run]`
167
+ - Run `pan-tools focus auto --category <SELECTED_CATEGORY> [--source scan|backlog] [--mode MODE] [--budget N] [--max-cycles N] [--total-budget N] [--enforce-budget] [--verify-reserve F] [--deep-review] [--parallel-research] [--parallel-verify] [--clean-seal] [--dry-run]`,
168
+ passing on every one of these flags the user gave: the tooling reads each of them, and a flag left off here never takes effect
166
169
  - If `--dry-run`: display the plan, STOP
167
170
  - Record the run state
168
171
 
@@ -352,6 +355,7 @@ Check the response for stop conditions:
352
355
  - `diminishing_returns`: Optimize only — cycle efficiency < 30% of previous cycle — go to Phase 3
353
356
  - `prompts_complete`: Prompts only — all prompts in document executed — go to Phase 3
354
357
  - `security_complete`: Security only — scan found no HIGH/CRITICAL items remaining — go to Phase 3
358
+ - `distill_complete`: Distill only — scan found no bloat findings remaining — go to Phase 3
355
359
  - `deep_review_block`: `--deep-review` only — critical pattern detected in changed files — go to Phase 3 with warning
356
360
  - `null`: Continue to next cycle
357
361
 
@@ -418,7 +422,7 @@ Then continue immediately to the next cycle (back to Step 2.1).
418
422
  | Layer | Mechanism | Action |
419
423
  |-------|-----------|--------|
420
424
  | Per-cycle budget | `--budget N` per cycle | Limits single-cycle damage |
421
- | Cumulative budget | `--total-budget N` | Prevents runaway spending |
425
+ | Cumulative budget | `--total-budget N` | Advisory by default (tracked and surfaced); stops the run only when enforced (`--enforce-budget` / config `budget.enforce`) |
422
426
  | Verify reserve | `--verify-reserve F` | Holds back a budget fraction so the final re-verification isn't starved — stops new work early under enforcement (`budget_reserve_reached`) and spends the reserve on the clean re-verify in Phase 3 |
423
427
  | Iteration limit | `--max-cycles N` | Hard stop on loop count |
424
428
  | Regression circuit breaker | tests_after < tests_before | Immediate stop, status=stopped |
@@ -534,12 +538,12 @@ Three passes per cycle:
534
538
 
535
539
  | OWASP | Grep pattern | Priority |
536
540
  |-------|-------------|---------|
537
- | A03 Injection | `eval(`, `execSync(`, `` `SELECT.*\${ ``, `child_process.exec(` | P0 |
538
- | A02 Crypto | `createHash\(['"]md5\|sha1`, `Math\.random\(\)` near auth/token | P0 |
541
+ | A05 Injection | `eval(`, `execSync(`, `` `SELECT.*\${ ``, `child_process.exec(` | P0 |
542
+ | A04 Crypto | `createHash\(['"]md5\|sha1`, `Math\.random\(\)` near auth/token | P0 |
539
543
  | A01 Access | Route without auth middleware, IDOR (raw `req.params.id` to DB) | P1 |
540
- | A05 Misconfig | `origin:\s*['"]?\*`, `Access-Control-Allow-Origin: \*`, stack in response | P1 |
544
+ | A02 Misconfig | `origin:\s*['"]?\*`, `Access-Control-Allow-Origin: \*`, stack in response | P1 |
541
545
  | A07 Auth | No session expiry, credentials in URL params | P1 |
542
- | A04 Design | Missing rate-limit on auth/payment endpoints | P2 |
546
+ | A06 Design | Missing rate-limit on auth/payment endpoints | P2 |
543
547
  | A09 Logging | Security events (`login`, `payment`, `admin`) with no log call nearby | P2 |
544
548
 
545
549
  **Pass 2 — Structural check (always runs):**
@@ -669,7 +673,7 @@ This prevents the campaign from burning budget on items that will predictably fa
669
673
  - Continue after a test regression — a test count decrease means code was broken; continuing compounds the damage
670
674
  - Expand scope beyond what the scan found — scope creep in an autonomous loop compounds unpredictably across cycles
671
675
  - Run more cycles than --max-cycles — the limit exists to cap total cost and prevent runaway loops
672
- - Spend more points than --total-budget — the budget cap is the user's cost control mechanism
676
+ - Spend more points than --total-budget when the budget is enforced (`--enforce-budget` or config `budget.enforce: true`) — the enforced cap is the user's cost control mechanism
673
677
  - Skip recording cycle results via --update — unrecorded cycles break resume, status, and stop-condition checks
674
678
  - Change test expectations to match broken code — this hides bugs instead of fixing them
675
679
  - Use `git add -A` or `git add .` — bulk staging can accidentally commit secrets, build artifacts, or unrelated changes
@@ -1073,6 +1073,8 @@ Output a complete summary with:
1073
1073
 
1074
1074
  ## NEVER DO
1075
1075
 
1076
+ These rules and ALWAYS DO below apply to the phases your mode actually runs. Anything the Mode + Phase Matrix or a modifier skips (a whole phase, or the parts a lite phase leaves out) is not a violation.
1077
+
1076
1078
  - Design without proving the problem exists (Phase 0 demand evidence is mandatory)
1077
1079
  - Skip competitive research — must be best-of-breed
1078
1080
  - Copy a tool's design without understanding WHY they made that choice
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: focus-exec
3
3
  group: Focus
4
- description: Automated batch execution pipeline with 6 stages, 9 behavioral rules, 3 execution tiers
4
+ description: Automated batch execution pipeline with 6 stages, behavioral rules, 3 execution tiers
5
5
  allowed-tools:
6
6
  - Read
7
7
  - Write