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.
- package/agents/pan-conductor.md +2 -2
- package/agents/pan-document_code.md +2 -2
- package/agents/pan-executor.md +3 -3
- package/agents/pan-experiment-runner.md +7 -10
- package/agents/pan-hardener.md +15 -15
- package/agents/pan-knowledge.md +1 -1
- package/agents/pan-meta-reviewer.md +2 -2
- package/agents/pan-optimizer.md +2 -2
- package/agents/pan-phase-researcher.md +2 -2
- package/agents/pan-plan-checker.md +6 -2
- package/agents/pan-planner.md +23 -10
- package/agents/pan-project-researcher.md +10 -67
- package/agents/pan-roadmapper.md +5 -48
- package/agents/pan-verifier.md +26 -11
- package/bin/install-lib.cjs +170 -15
- package/bin/install.js +39 -14
- package/commands/pan/army.md +2 -2
- package/commands/pan/experiment.md +2 -2
- package/commands/pan/focus-auto.md +12 -8
- package/commands/pan/focus-design.md +2 -0
- package/commands/pan/focus-exec.md +1 -1
- package/commands/pan/focus-scan.md +2 -7
- package/commands/pan/map-codebase.md +4 -4
- package/commands/pan/mcp-bridge.md +1 -1
- package/commands/pan/milestone-done.md +1 -2
- package/commands/pan/phase-tests.md +2 -2
- package/commands/pan/profile.md +2 -2
- package/commands/pan/research-phase.md +2 -4
- package/commands/pan/review-deep.md +3 -3
- package/commands/pan/todo-add.md +2 -2
- package/commands/pan/todo-check.md +2 -2
- package/hooks/dist/pan-state-reinject.js +140 -15
- package/package.json +1 -1
- package/pan-wizard-core/bin/lib/agents-md.cjs +4 -1
- package/pan-wizard-core/bin/lib/commands.cjs +93 -1
- package/pan-wizard-core/bin/lib/config.cjs +110 -0
- package/pan-wizard-core/bin/lib/constants.cjs +11 -9
- package/pan-wizard-core/bin/lib/core.cjs +27 -7
- package/pan-wizard-core/bin/lib/cost.cjs +13 -3
- package/pan-wizard-core/bin/lib/findings.cjs +6 -2
- package/pan-wizard-core/bin/lib/init.cjs +5 -1
- package/pan-wizard-core/bin/lib/phase.cjs +52 -2
- package/pan-wizard-core/bin/lib/review-deep.cjs +2 -2
- package/pan-wizard-core/bin/lib/state.cjs +3 -2
- package/pan-wizard-core/bin/lib/verdict.cjs +54 -4
- package/pan-wizard-core/bin/lib/verify-drift.cjs +14 -5
- package/pan-wizard-core/bin/lib/verify-scope.cjs +69 -2
- package/pan-wizard-core/bin/lib/verify.cjs +87 -7
- package/pan-wizard-core/bin/pan-tools.cjs +10 -2
- package/pan-wizard-core/mcp/server.cjs +18 -9
- package/pan-wizard-core/references/checkpoints.md +4 -2
- package/pan-wizard-core/references/verdict-contract.md +2 -1
- package/pan-wizard-core/templates/discovery.md +1 -1
- package/pan-wizard-core/templates/phase-prompt.md +2 -1
- package/pan-wizard-core/templates/verification-report.md +5 -0
- package/pan-wizard-core/workflows/exec-phase.md +8 -3
- package/pan-wizard-core/workflows/health.md +1 -0
- package/pan-wizard-core/workflows/new-project.md +1 -1
- package/pan-wizard-core/workflows/quick.md +6 -6
- package/pan-wizard-core/workflows/verify-phase.md +7 -2
- package/scripts/build-agent-plugin.js +19 -5
- package/scripts/test-surface.cjs +4 -0
package/agents/pan-verifier.md
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
77
|
-
|
|
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/
|
|
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/
|
|
109
|
-
[ -f .planning/codebase/
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
package/bin/install-lib.cjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
1054
|
-
|
|
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).
|
|
1111
|
-
*
|
|
1112
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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(', ')}${
|
|
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
|
|
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
|
|
package/commands/pan/army.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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] [--
|
|
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` |
|
|
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
|
-
|
|
|
538
|
-
|
|
|
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
|
-
|
|
|
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
|
-
|
|
|
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
|
|
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,
|
|
4
|
+
description: Automated batch execution pipeline with 6 stages, behavioral rules, 3 execution tiers
|
|
5
5
|
allowed-tools:
|
|
6
6
|
- Read
|
|
7
7
|
- Write
|