pan-wizard 3.31.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/README.md +28 -24
- package/agents/pan-conductor.md +2 -2
- package/agents/pan-design-checker.md +18 -0
- 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 +7 -3
- package/agents/pan-phase-researcher.md +2 -2
- package/agents/pan-plan-checker.md +29 -2
- package/agents/pan-planner.md +23 -10
- package/agents/pan-project-researcher.md +10 -67
- package/agents/pan-reviewer.md +19 -0
- package/agents/pan-roadmapper.md +5 -48
- package/agents/pan-verifier.md +49 -11
- package/bin/install-lib.cjs +170 -15
- package/bin/install.js +39 -14
- package/commands/pan/army.md +2 -2
- package/commands/pan/design-phase.md +8 -0
- package/commands/pan/exec-phase.md +1 -1
- 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/git.md +2 -2
- package/commands/pan/learn.md +2 -0
- 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/optimize.md +14 -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 +8 -8
- package/commands/pan/todo-add.md +2 -2
- package/commands/pan/todo-check.md +2 -2
- package/hooks/dist/pan-context-monitor.js +1 -1
- package/hooks/dist/pan-cost-logger.js +63 -11
- package/hooks/dist/pan-state-reinject.js +142 -17
- package/hooks/dist/pan-stop-guard.js +2 -2
- package/hooks/dist/pan-trace-logger.js +283 -18
- package/package.json +1 -1
- package/pan-wizard-core/bin/lib/agents-md.cjs +4 -1
- package/pan-wizard-core/bin/lib/commands.cjs +95 -2
- package/pan-wizard-core/bin/lib/config.cjs +110 -0
- package/pan-wizard-core/bin/lib/constants.cjs +39 -9
- package/pan-wizard-core/bin/lib/core.cjs +27 -7
- package/pan-wizard-core/bin/lib/cost.cjs +16 -6
- package/pan-wizard-core/bin/lib/findings.cjs +385 -0
- package/pan-wizard-core/bin/lib/focus.cjs +72 -19
- package/pan-wizard-core/bin/lib/foreign-planning.cjs +24 -6
- package/pan-wizard-core/bin/lib/hygiene.cjs +12 -2
- package/pan-wizard-core/bin/lib/init.cjs +9 -2
- package/pan-wizard-core/bin/lib/optimize.cjs +349 -21
- package/pan-wizard-core/bin/lib/phase.cjs +52 -2
- package/pan-wizard-core/bin/lib/review-deep.cjs +38 -2
- package/pan-wizard-core/bin/lib/state.cjs +10 -4
- package/pan-wizard-core/bin/lib/verdict.cjs +481 -0
- package/pan-wizard-core/bin/lib/verify-drift.cjs +14 -5
- package/pan-wizard-core/bin/lib/verify-preflight.cjs +4 -2
- package/pan-wizard-core/bin/lib/verify-scope.cjs +223 -0
- package/pan-wizard-core/bin/lib/verify.cjs +111 -8
- package/pan-wizard-core/bin/pan-tools.cjs +68 -6
- package/pan-wizard-core/learnings/README.md +10 -7
- package/pan-wizard-core/mcp/server.cjs +18 -9
- package/pan-wizard-core/mcp/tool-registry.cjs +2 -0
- package/pan-wizard-core/references/checkpoints.md +4 -2
- package/pan-wizard-core/references/verdict-contract.md +102 -0
- 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 +21 -0
- package/pan-wizard-core/workflows/exec-phase.md +73 -50
- package/pan-wizard-core/workflows/health.md +3 -0
- package/pan-wizard-core/workflows/learn.md +4 -2
- package/pan-wizard-core/workflows/milestone-audit.md +15 -3
- package/pan-wizard-core/workflows/new-project.md +1 -1
- package/pan-wizard-core/workflows/optimize.md +14 -1
- package/pan-wizard-core/workflows/plan-phase.md +20 -15
- package/pan-wizard-core/workflows/quick.md +6 -6
- package/pan-wizard-core/workflows/verify-phase.md +10 -15
- package/pan-zcode/README.md +3 -3
- package/scripts/build-agent-plugin.js +19 -5
- package/scripts/git-hooks/pre-commit +2 -1
- package/scripts/test-surface.cjs +4 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Verify scope — the mechanical half of the unrequested-work check (market item M11,
|
|
4
|
+
* evidence loop spec D12). Re-exported by verify.cjs, like the other verify-* modules.
|
|
5
|
+
*
|
|
6
|
+
* It answers one question in code: which files did this phase change that none of
|
|
7
|
+
* its plans declared? The verifier judges each candidate. Tests for a declared file
|
|
8
|
+
* or a config change the feature needs are supporting work; anything else it
|
|
9
|
+
* records as `unrequested:` in verification.md. A candidate is a lead, not a
|
|
10
|
+
* finding: code cannot tell scope creep from necessary glue.
|
|
11
|
+
*
|
|
12
|
+
* declared = the union of the phase plans' `files_modified`
|
|
13
|
+
* changed = files touched by the phase's plan commits (subject `{type}({phase}-{plan}):`,
|
|
14
|
+
* the executor's convention), plus the summaries' `key-files`
|
|
15
|
+
* excluded = the planning tree, lockfiles, and PAN's own runtime directories
|
|
16
|
+
*/
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { safeReadFile, execGit, findPhaseInternal, toPosix, output, escapeRegex } = require('./core.cjs');
|
|
20
|
+
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
21
|
+
const { isPlanFile, isSummaryFile, isVerificationFile } = require('./constants.cjs');
|
|
22
|
+
const { planningRel } = require('./utils.cjs');
|
|
23
|
+
|
|
24
|
+
const LOCKFILES = new Set([
|
|
25
|
+
'package-lock.json', 'npm-shrinkwrap.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb', 'bun.lock',
|
|
26
|
+
'Cargo.lock', 'go.sum', 'poetry.lock', 'Pipfile.lock', 'uv.lock', 'composer.lock', 'Gemfile.lock', 'mix.lock', 'pubspec.lock',
|
|
27
|
+
]);
|
|
28
|
+
// PAN's own installed trees: a phase commit that touches them is PAN bookkeeping,
|
|
29
|
+
// never the project's scope (pan-reviewer skips them for the same reason).
|
|
30
|
+
const PAN_OWNED_PREFIXES = ['.claude/', '.codex/', '.gemini/', '.opencode/', '.agents/', '.github/pan-wizard-core/', '.github/agents/', '.github/prompts/', '.github/skills/'];
|
|
31
|
+
const PAN_OWNED_FILES = new Set(['.github/copilot-instructions.md', '.github/hooks/pan.json']);
|
|
32
|
+
const TEST_PATH_RE = /(^|\/)(tests?|__tests__|specs?)\//i;
|
|
33
|
+
const TEST_FILE_RE = /\.(test|spec)\.[a-z0-9]+$/i;
|
|
34
|
+
const COMMIT_SCAN_LIMIT = 2000;
|
|
35
|
+
|
|
36
|
+
function normalisePath(p) {
|
|
37
|
+
return toPosix(String(p || '').trim()).replace(/^\.\/+/, '').replace(/\\/g, '/');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function asList(v) {
|
|
41
|
+
if (Array.isArray(v)) return v.map(String);
|
|
42
|
+
if (typeof v === 'string' && v.trim()) return [v];
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A file's stem without directory, a test/spec infix, or extension: `src/auth.ts` → `auth`. */
|
|
47
|
+
function stem(p) {
|
|
48
|
+
return path.posix.basename(p).replace(/\.(test|spec)(?=\.)/i, '').replace(/\.[^.]+$/, '').toLowerCase();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isDeclared(file, declared) {
|
|
52
|
+
for (const d of declared) {
|
|
53
|
+
if (d === file) return true;
|
|
54
|
+
if (d.endsWith('/') && file.startsWith(d)) return true;
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function exclusionReason(file, planningPrefix) {
|
|
60
|
+
if (file === planningPrefix || file.startsWith(planningPrefix + '/')) return 'planning tree';
|
|
61
|
+
if (LOCKFILES.has(path.posix.basename(file))) return 'lockfile';
|
|
62
|
+
if (PAN_OWNED_FILES.has(file) || PAN_OWNED_PREFIXES.some((p) => file.startsWith(p))) return 'PAN runtime directory';
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Files the phase's plan commits touched, paths relative to `cwd` (git's --relative).
|
|
68
|
+
* @returns {{git: boolean, commits: number, files: Set<string>}}
|
|
69
|
+
*/
|
|
70
|
+
function committedFiles(cwd, phaseNumber) {
|
|
71
|
+
const r = execGit(cwd, ['-c', 'core.quotepath=off', 'log', '--no-merges', '-n', String(COMMIT_SCAN_LIMIT), '--relative', '--name-only', '--format=@@%H%x09%s']);
|
|
72
|
+
if (r.exitCode !== 0) return { git: false, commits: 0, files: new Set() };
|
|
73
|
+
// `{type}({phase}-{plan}):` with the phase's zero padding optional ("feat(03-01):", "fix(3-02)!:").
|
|
74
|
+
const unpadded = String(phaseNumber).replace(/^0+(?=\d)/, '');
|
|
75
|
+
const subjectRe = new RegExp(`^[a-z]+\\(0*${escapeRegex(unpadded)}-[0-9]+\\)!?:`);
|
|
76
|
+
const files = new Set();
|
|
77
|
+
let commits = 0;
|
|
78
|
+
let inPhase = false;
|
|
79
|
+
for (const line of r.stdout.split('\n')) {
|
|
80
|
+
if (line.startsWith('@@')) {
|
|
81
|
+
const subject = line.slice(2).split('\t').slice(1).join('\t');
|
|
82
|
+
inPhase = subjectRe.test(subject);
|
|
83
|
+
if (inPhase) commits++;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (inPhase && line.trim()) files.add(normalisePath(line));
|
|
87
|
+
}
|
|
88
|
+
return { git: true, commits, files };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The scope check for one phase.
|
|
93
|
+
* @returns {Object} `{phase, directory, declared, changed, candidates, excluded, git, commits}` or `{error}`
|
|
94
|
+
*/
|
|
95
|
+
function scopePhase(cwd, phase) {
|
|
96
|
+
const info = findPhaseInternal(cwd, phase);
|
|
97
|
+
if (!info) return { error: 'phase_not_found', reason: `no phase directory for phase ${phase}` };
|
|
98
|
+
const dir = path.join(cwd, info.directory);
|
|
99
|
+
const declared = new Set();
|
|
100
|
+
for (const f of info.plans.filter(isPlanFile)) {
|
|
101
|
+
const fm = extractFrontmatter(safeReadFile(path.join(dir, f)) || '');
|
|
102
|
+
for (const p of asList(fm.files_modified || fm['files-modified'])) {
|
|
103
|
+
const n = normalisePath(p);
|
|
104
|
+
if (n) declared.add(n);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const sources = new Map();
|
|
108
|
+
const note = (file, source) => {
|
|
109
|
+
if (!file) return;
|
|
110
|
+
if (!sources.has(file)) sources.set(file, new Set());
|
|
111
|
+
sources.get(file).add(source);
|
|
112
|
+
};
|
|
113
|
+
for (const f of info.summaries.filter(isSummaryFile)) {
|
|
114
|
+
const fm = extractFrontmatter(safeReadFile(path.join(dir, f)) || '');
|
|
115
|
+
const kf = fm['key-files'] || fm.key_files || {};
|
|
116
|
+
for (const p of [...asList(kf.created), ...asList(kf.modified)]) note(normalisePath(p), 'summary');
|
|
117
|
+
}
|
|
118
|
+
const git = committedFiles(cwd, info.phase_number);
|
|
119
|
+
for (const p of git.files) note(p, 'commit');
|
|
120
|
+
|
|
121
|
+
const planningPrefix = normalisePath(planningRel());
|
|
122
|
+
const declaredList = [...declared].sort();
|
|
123
|
+
const declaredStems = new Set(declaredList.filter((d) => !d.endsWith('/')).map(stem));
|
|
124
|
+
const changed = [...sources.keys()].sort();
|
|
125
|
+
const candidates = [];
|
|
126
|
+
const excluded = [];
|
|
127
|
+
for (const file of changed) {
|
|
128
|
+
const why = exclusionReason(file, planningPrefix);
|
|
129
|
+
if (why) { excluded.push({ path: file, reason: why }); continue; }
|
|
130
|
+
if (isDeclared(file, declared)) continue;
|
|
131
|
+
const isTest = TEST_PATH_RE.test(file) || TEST_FILE_RE.test(file);
|
|
132
|
+
candidates.push({
|
|
133
|
+
path: file,
|
|
134
|
+
hint: isTest && declaredStems.has(stem(file)) ? 'test_for_declared' : null,
|
|
135
|
+
sources: [...sources.get(file)].sort(),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
phase: info.phase_number,
|
|
140
|
+
directory: info.directory,
|
|
141
|
+
declared: declaredList,
|
|
142
|
+
changed,
|
|
143
|
+
candidates,
|
|
144
|
+
excluded,
|
|
145
|
+
git: git.git,
|
|
146
|
+
commits: git.commits,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function cmdVerifyScope(cwd, phase, raw) {
|
|
151
|
+
if (!phase) { output({ error: 'phase_required', reason: 'Usage: verify scope <phase>' }, raw); return; }
|
|
152
|
+
const r = scopePhase(cwd, phase);
|
|
153
|
+
output(r, raw, r.error ? undefined : String(r.candidates.length));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ─── Stale verification (market-ideas M27) ───────────────────────────────────
|
|
157
|
+
// A verification describes the code at one commit. Until 2026-10-03 nothing tied it
|
|
158
|
+
// to that commit, so a phase edited after it passed still read as verified. The
|
|
159
|
+
// verifier now records `verified_commit`; any file the phase covers that changed
|
|
160
|
+
// since then, committed or not, makes the verification stale.
|
|
161
|
+
|
|
162
|
+
const SHA_RE = /^[0-9a-f]{7,40}$/i;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The files a phase's verification covered: the plans' declared `files_modified` and
|
|
166
|
+
* the summaries' `key-files`, minus the planning tree, lockfiles and PAN's runtime
|
|
167
|
+
* directories. No git log scan, so `progress` can afford it for every phase.
|
|
168
|
+
*/
|
|
169
|
+
function coveredFiles(cwd, info) {
|
|
170
|
+
const dir = path.join(cwd, info.directory);
|
|
171
|
+
const files = new Set();
|
|
172
|
+
for (const f of info.plans.filter(isPlanFile)) {
|
|
173
|
+
const fm = extractFrontmatter(safeReadFile(path.join(dir, f)) || '');
|
|
174
|
+
for (const p of asList(fm.files_modified || fm['files-modified'])) files.add(normalisePath(p));
|
|
175
|
+
}
|
|
176
|
+
for (const f of info.summaries.filter(isSummaryFile)) {
|
|
177
|
+
const fm = extractFrontmatter(safeReadFile(path.join(dir, f)) || '');
|
|
178
|
+
const kf = fm['key-files'] || fm.key_files || {};
|
|
179
|
+
for (const p of [...asList(kf.created), ...asList(kf.modified)]) files.add(normalisePath(p));
|
|
180
|
+
}
|
|
181
|
+
const planningPrefix = normalisePath(planningRel());
|
|
182
|
+
return [...files].filter((f) => f && !exclusionReason(f, planningPrefix)).sort();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Whether a phase's verification still describes the code.
|
|
187
|
+
* @param {string} cwd
|
|
188
|
+
* @param {string} phase
|
|
189
|
+
* @param {Object} [info] - findPhaseInternal's result, when the caller already has it
|
|
190
|
+
* @returns {Object} `{phase, state, verified_commit, verification, changed_since, reason?}` or `{error, reason}`.
|
|
191
|
+
* state: `fresh` · `stale` (covered files changed since the verified commit) ·
|
|
192
|
+
* `unverified` (no verification file) · `unknown` (no usable verified_commit, or no git)
|
|
193
|
+
*/
|
|
194
|
+
function verificationFreshness(cwd, phase, info) {
|
|
195
|
+
info = info || findPhaseInternal(cwd, phase);
|
|
196
|
+
if (!info) return { error: 'phase_not_found', reason: `no phase directory for phase ${phase}` };
|
|
197
|
+
const dir = path.join(cwd, info.directory);
|
|
198
|
+
let names = [];
|
|
199
|
+
try { names = fs.readdirSync(dir); } catch { /* unreadable: no verification */ }
|
|
200
|
+
const verification = names.filter(isVerificationFile).sort().pop() || null;
|
|
201
|
+
const base = { phase: info.phase_number, state: 'unverified', verified_commit: null, verification: verification ? toPosix(path.join(info.directory, verification)) : null, changed_since: [] };
|
|
202
|
+
if (!verification) return base;
|
|
203
|
+
const fm = extractFrontmatter(safeReadFile(path.join(dir, verification)) || '');
|
|
204
|
+
const sha = String(fm.verified_commit || '').trim();
|
|
205
|
+
if (!SHA_RE.test(sha)) return { ...base, state: 'unknown', reason: 'the verification records no verified_commit' };
|
|
206
|
+
const known = execGit(cwd, ['cat-file', '-e', `${sha}^{commit}`]);
|
|
207
|
+
if (known.exitCode !== 0) return { ...base, state: 'unknown', verified_commit: sha, reason: 'the verified commit is not in this repository (rebased, squashed, or no git)' };
|
|
208
|
+
const covered = coveredFiles(cwd, info);
|
|
209
|
+
if (!covered.length) return { ...base, state: 'unknown', verified_commit: sha, reason: 'the phase declares no files to compare' };
|
|
210
|
+
// Against the working tree, not HEAD: an uncommitted edit makes the pass stale too.
|
|
211
|
+
const diff = execGit(cwd, ['-c', 'core.quotepath=off', 'diff', '--name-only', '--relative', sha, '--', ...covered]);
|
|
212
|
+
if (diff.exitCode !== 0) return { ...base, state: 'unknown', verified_commit: sha, reason: 'git diff failed' };
|
|
213
|
+
const changed = diff.stdout.split('\n').map(normalisePath).filter(Boolean).sort();
|
|
214
|
+
return { ...base, state: changed.length ? 'stale' : 'fresh', verified_commit: sha, changed_since: changed };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function cmdVerifyStale(cwd, phase, raw) {
|
|
218
|
+
if (!phase) { output({ error: 'phase_required', reason: 'Usage: verify stale <phase>' }, raw); return; }
|
|
219
|
+
const r = verificationFreshness(cwd, phase);
|
|
220
|
+
output(r, raw, r.error ? undefined : r.state);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
module.exports = { scopePhase, cmdVerifyScope, verificationFreshness, cmdVerifyStale, coveredFiles, LOCKFILES };
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const { execFileSync } = require('child_process');
|
|
8
|
-
const { safeReadFile, normalizePhaseName, comparePhaseNum, execGit, findPhaseInternal, getMilestoneInfo, toPosix, output, EXIT_OK, error, escapeRegex } = require('./core.cjs');
|
|
8
|
+
const { safeReadFile, normalizePhaseName, comparePhaseNum, execGit, findPhaseInternal, getMilestoneInfo, toPosix, output, EXIT_OK, error, escapeRegex, loadConfig, isGitIgnored, isGitRepo } = require('./core.cjs');
|
|
9
9
|
const { extractFrontmatter, parseMustHavesBlock } = require('./frontmatter.cjs');
|
|
10
10
|
const { writeStateMd, readStateSafe } = require('./state.cjs');
|
|
11
11
|
const {
|
|
@@ -16,12 +16,14 @@ const {
|
|
|
16
16
|
} = require('./constants.cjs');
|
|
17
17
|
const { planningPath, phasesPath, filterPlanFiles, filterSummaryFiles, fileAccessible, detectPlanningModel } = require('./utils.cjs');
|
|
18
18
|
const { detectForeignPlanningTree } = require('./foreign-planning.cjs');
|
|
19
|
+
const { unknownConfigKeys, nearestConfigKey } = require('./config.cjs');
|
|
19
20
|
// Drift detection lives in verify-drift.cjs; re-exported below so consumers of
|
|
20
21
|
// verify.cjs are unaffected by the decomposition.
|
|
21
22
|
const { runDriftCheck, parseConventionRules, checkFileConventions, calculateDriftScore, getChangedFiles, cmdDriftCheck } = require('./verify-drift.cjs');
|
|
22
23
|
const { collectVerificationStats, countRoadmapPhases, groupGapPatterns, cmdRetro } = require('./verify-retro.cjs');
|
|
23
24
|
const { detectInstalledRuntimes, validateRuntimeInstall, cmdValidateDeployment } = require('./verify-deploy.cjs');
|
|
24
25
|
const { cmdPreflight, cmdDepsValidate } = require('./verify-preflight.cjs');
|
|
26
|
+
const { scopePhase, cmdVerifyScope, verificationFreshness, cmdVerifyStale } = require('./verify-scope.cjs');
|
|
25
27
|
const { planningRootRel } = require('./planning-root.cjs');
|
|
26
28
|
|
|
27
29
|
/**
|
|
@@ -115,6 +117,56 @@ function cmdVerifySummary(cwd, summaryPath, checkFileCount, raw) {
|
|
|
115
117
|
}, raw, passed ? 'passed' : 'failed');
|
|
116
118
|
}
|
|
117
119
|
|
|
120
|
+
// Longer than a signature, a type or a short config snippet: an implementation body.
|
|
121
|
+
const MAX_ACTION_CODE_LINES = 20;
|
|
122
|
+
|
|
123
|
+
/** The line count of the longest fenced code block in a piece of markdown (0 if none). */
|
|
124
|
+
function longestFencedBlock(text) {
|
|
125
|
+
let longest = 0;
|
|
126
|
+
for (const m of String(text).matchAll(/(^|\n)[ \t]*(`{3,}|~{3,})[^\n]*\n([\s\S]*?)\n[ \t]*\2[ \t]*(?=\n|$)/g)) {
|
|
127
|
+
longest = Math.max(longest, m[3].split('\n').length);
|
|
128
|
+
}
|
|
129
|
+
return longest;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// The elements each checkpoint type needs (references/checkpoints.md).
|
|
133
|
+
const CHECKPOINT_REQUIRED = {
|
|
134
|
+
'checkpoint:human-verify': ['what-built', 'how-to-verify'],
|
|
135
|
+
'checkpoint:decision': ['decision', 'options'],
|
|
136
|
+
'checkpoint:human-action': ['action'],
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check one checkpoint task against its type's shape. A decision checkpoint may
|
|
141
|
+
* carry `auto_select="<option id>"`: the option auto mode takes. Without one, auto
|
|
142
|
+
* mode stops for a human instead of taking whichever option is listed first
|
|
143
|
+
* (market-ideas M22), and an `auto_select` naming no option is an error.
|
|
144
|
+
* @returns {{name: string, type: string, auto_select?: string|null}} the task entry
|
|
145
|
+
*/
|
|
146
|
+
function checkCheckpointTask(type, attrs, body, errors, warnings) {
|
|
147
|
+
const keyText = (body.match(/<(?:decision|what-built|action)>([\s\S]*?)<\/(?:decision|what-built|action)>/) || [])[1];
|
|
148
|
+
const label = keyText ? keyText.trim().replace(/\s+/g, ' ').slice(0, 80) : `unnamed ${type}`;
|
|
149
|
+
const required = CHECKPOINT_REQUIRED[type];
|
|
150
|
+
if (!required) {
|
|
151
|
+
errors.push(`Task '${label}' has unknown checkpoint type '${type}'`);
|
|
152
|
+
return { name: label, type };
|
|
153
|
+
}
|
|
154
|
+
for (const el of required) {
|
|
155
|
+
if (!body.includes(`<${el}>`)) errors.push(`Checkpoint '${label}' missing <${el}>`);
|
|
156
|
+
}
|
|
157
|
+
if (type !== 'checkpoint:decision') return { name: label, type };
|
|
158
|
+
|
|
159
|
+
const optionIds = [...body.matchAll(/<option\b[^>]*\bid=["']([^"']+)["']/g)].map(m => m[1]);
|
|
160
|
+
if (optionIds.length === 0) errors.push(`Decision checkpoint '${label}' has no <option id="...">`);
|
|
161
|
+
const autoMatch = attrs.match(/\bauto_select=["']([^"']*)["']/);
|
|
162
|
+
if (!autoMatch) {
|
|
163
|
+
warnings.push(`Decision checkpoint '${label}' has no auto_select: auto mode stops here for a human`);
|
|
164
|
+
} else if (!optionIds.includes(autoMatch[1])) {
|
|
165
|
+
errors.push(`Decision checkpoint '${label}' auto_select="${autoMatch[1]}" names no option (options: ${optionIds.join(', ') || 'none'})`);
|
|
166
|
+
}
|
|
167
|
+
return { name: label, type, auto_select: autoMatch ? autoMatch[1] : null };
|
|
168
|
+
}
|
|
169
|
+
|
|
118
170
|
/**
|
|
119
171
|
* Validate a plan.md structure: required frontmatter, task elements, and consistency.
|
|
120
172
|
* @param {string} cwd - Working directory path
|
|
@@ -138,14 +190,24 @@ function cmdVerifyPlanStructure(cwd, filePath, raw) {
|
|
|
138
190
|
if (fm[field] === undefined) errors.push(`Missing required frontmatter field: ${field}`);
|
|
139
191
|
}
|
|
140
192
|
|
|
141
|
-
// Parse XML <task> elements
|
|
142
|
-
// <
|
|
143
|
-
//
|
|
144
|
-
|
|
193
|
+
// Parse XML <task> elements. An auto task needs <name> and <action> and should
|
|
194
|
+
// have <verify>, <done> and <files>. A checkpoint task has its own shape
|
|
195
|
+
// (references/checkpoints.md) and is checked against that instead: until
|
|
196
|
+
// 2026-10-03 every checkpoint failed the auto-task checks, and the `<task[^>]*>`
|
|
197
|
+
// pattern also matched the `<tasks>` wrapper, so a decision's first option name
|
|
198
|
+
// was read as the task's name. `\b` keeps the wrapper out.
|
|
199
|
+
const taskPattern = /<task\b([^>]*)>([\s\S]*?)<\/task>/g;
|
|
145
200
|
const tasks = [];
|
|
146
201
|
let taskMatch;
|
|
147
202
|
while ((taskMatch = taskPattern.exec(content)) !== null) {
|
|
148
|
-
const
|
|
203
|
+
const attrs = taskMatch[1];
|
|
204
|
+
const taskContent = taskMatch[2];
|
|
205
|
+
const typeMatch = attrs.match(/\btype=["']?([\w:-]+)/);
|
|
206
|
+
const taskType = typeMatch ? typeMatch[1] : 'auto';
|
|
207
|
+
if (taskType.startsWith('checkpoint:')) {
|
|
208
|
+
tasks.push(checkCheckpointTask(taskType, attrs, taskContent, errors, warnings));
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
149
211
|
const nameMatch = taskContent.match(/<name>([\s\S]*?)<\/name>/);
|
|
150
212
|
const taskName = nameMatch ? nameMatch[1].trim() : 'unnamed';
|
|
151
213
|
const hasFiles = /<files>/.test(taskContent);
|
|
@@ -155,11 +217,19 @@ function cmdVerifyPlanStructure(cwd, filePath, raw) {
|
|
|
155
217
|
|
|
156
218
|
if (!nameMatch) errors.push('Task missing <name> element');
|
|
157
219
|
if (!hasAction) errors.push(`Task '${taskName}' missing <action>`);
|
|
220
|
+
// Decisions, not code (market-ideas M32): an <action> names signatures, values,
|
|
221
|
+
// assertions and the verify command. A long fenced block there is an
|
|
222
|
+
// implementation body the planner wrote for the executor.
|
|
223
|
+
const actionText = (taskContent.match(/<action>([\s\S]*?)<\/action>/) || [])[1] || '';
|
|
224
|
+
const longest = longestFencedBlock(actionText);
|
|
225
|
+
if (longest > MAX_ACTION_CODE_LINES) {
|
|
226
|
+
warnings.push(`Task '${taskName}' embeds a ${longest}-line code block in <action>: a plan records decisions (signatures, values, assertions, the verify command), not implementation`);
|
|
227
|
+
}
|
|
158
228
|
if (!hasVerify) warnings.push(`Task '${taskName}' missing <verify>`);
|
|
159
229
|
if (!hasDone) warnings.push(`Task '${taskName}' missing <done>`);
|
|
160
230
|
if (!hasFiles) warnings.push(`Task '${taskName}' missing <files>`);
|
|
161
231
|
|
|
162
|
-
tasks.push({ name: taskName, hasFiles, hasAction, hasVerify, hasDone });
|
|
232
|
+
tasks.push({ name: taskName, type: taskType, hasFiles, hasAction, hasVerify, hasDone });
|
|
163
233
|
}
|
|
164
234
|
|
|
165
235
|
if (tasks.length === 0) warnings.push('No <task> elements found');
|
|
@@ -821,6 +891,23 @@ function checkStateFile(cwd, addIssue, repairs) {
|
|
|
821
891
|
* @param {Function} addIssue - Issue recording callback
|
|
822
892
|
* @param {string[]} repairs - Mutable array of repair actions to schedule
|
|
823
893
|
*/
|
|
894
|
+
/**
|
|
895
|
+
* I005: the planning tree is ignored by git while `commit_docs` is on, so every
|
|
896
|
+
* `pan-tools commit` of planning docs commits nothing, silently. planning-with-files
|
|
897
|
+
* adds `.planning/` to .gitignore by default (its README, read 2026-09-28), which is
|
|
898
|
+
* how a PAN project ends up like this (market item M3). Info, not a warning: keeping
|
|
899
|
+
* the docs out of git is a legitimate choice, and it is one config key away.
|
|
900
|
+
*/
|
|
901
|
+
function checkPlanningIgnored(cwd, addIssue) {
|
|
902
|
+
try {
|
|
903
|
+
if (loadConfig(cwd).commit_docs === false || !isGitRepo(cwd)) return;
|
|
904
|
+
const rel = planningRootRel();
|
|
905
|
+
if (!isGitIgnored(cwd, `${rel}/`)) return;
|
|
906
|
+
addIssue('info', 'I005', `${rel}/ is ignored by git while commit_docs is true, so PAN's planning commits commit nothing`,
|
|
907
|
+
`Remove ${rel}/ from .gitignore (planning-with-files adds it by default), or keep the docs out of git on purpose: pan-tools config-set commit_docs false`);
|
|
908
|
+
} catch { /* best effort: a git failure is not a finding */ }
|
|
909
|
+
}
|
|
910
|
+
|
|
824
911
|
function checkConfigFile(cwd, addIssue, repairs) {
|
|
825
912
|
const configFullPath = path.join(planningPath(cwd), CONFIG_FILE);
|
|
826
913
|
let rawContent;
|
|
@@ -838,6 +925,15 @@ function checkConfigFile(cwd, addIssue, repairs) {
|
|
|
838
925
|
if (parsed.model_profile && !validProfiles.includes(parsed.model_profile)) {
|
|
839
926
|
addIssue('warning', 'W004', `${CONFIG_FILE}: invalid model_profile "${parsed.model_profile}"`, `Valid values: ${validProfiles.join(', ')}`);
|
|
840
927
|
}
|
|
928
|
+
// I006 (market-ideas M26): keys PAN does not read. Info, not a warning: the key
|
|
929
|
+
// may be the user's own note or meant for a newer PAN, but a typo must not pass
|
|
930
|
+
// as a setting that took effect.
|
|
931
|
+
const unknown = unknownConfigKeys(parsed);
|
|
932
|
+
if (unknown.length) {
|
|
933
|
+
const hints = unknown.map((k) => { const n = nearestConfigKey(k); return n ? `${k} → ${n}?` : k; });
|
|
934
|
+
addIssue('info', 'I006', `${CONFIG_FILE}: PAN does not read ${unknown.length === 1 ? 'this key' : 'these keys'}: ${unknown.join(', ')}`,
|
|
935
|
+
`Check the spelling (${hints.join('; ')}), or remove the key. pan-tools config-set names an unknown key when you set it.`);
|
|
936
|
+
}
|
|
841
937
|
} catch (err) {
|
|
842
938
|
// JSON parse failed -- config is corrupt and should be reset
|
|
843
939
|
addIssue('error', 'E005', `${CONFIG_FILE}: JSON parse error - ${err.message}`, 'Run /pan:health --repair to reset to defaults', true);
|
|
@@ -1314,7 +1410,9 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
1314
1410
|
// error and stop: E002-E005 would describe a foreign layout as a broken PAN one,
|
|
1315
1411
|
// and --repair must never write into it. Reality check R15.
|
|
1316
1412
|
const foreign = detectForeignPlanningTree(planningPath(cwd));
|
|
1317
|
-
if (foreign) {
|
|
1413
|
+
if (foreign && foreign.coexists) {
|
|
1414
|
+
addIssue('info', 'I004', `${foreign.tool} also writes into this planning tree: ${foreign.evidence.join(', ')}`, 'None needed: PAN leaves its files alone. Give PAN its own tree with --planning-dir if you prefer them apart (ADR-0043)');
|
|
1415
|
+
} else if (foreign) {
|
|
1318
1416
|
addIssue('error', 'E006', `planning tree belongs to ${foreign.tool}: ${foreign.evidence.join(', ')}`, 'Run PAN with --planning-dir <dir> to use a separate tree (ADR-0043)');
|
|
1319
1417
|
output({ status: HEALTH_STATUS.BROKEN, errors, warnings, info, repairable_count: 0 }, raw, undefined, 1);
|
|
1320
1418
|
return;
|
|
@@ -1337,6 +1435,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
1337
1435
|
addIssue('info', 'I003', `${shape.model}-model project (${shape.evidence.join(', ')}) — the phase-model checks (project.md, roadmap.md, state.md, phases/) do not apply`, null);
|
|
1338
1436
|
}
|
|
1339
1437
|
checkConfigFile(cwd, addIssue, repairs);
|
|
1438
|
+
checkPlanningIgnored(cwd, addIssue);
|
|
1340
1439
|
if (phaseModel) {
|
|
1341
1440
|
checkPhaseDirectories(cwd, addIssue);
|
|
1342
1441
|
checkPhaseContents(cwd, addIssue);
|
|
@@ -1535,6 +1634,10 @@ module.exports = {
|
|
|
1535
1634
|
cmdVerifyReconcile,
|
|
1536
1635
|
scanStubs,
|
|
1537
1636
|
cmdVerifyStubs,
|
|
1637
|
+
scopePhase,
|
|
1638
|
+
cmdVerifyScope,
|
|
1639
|
+
verificationFreshness,
|
|
1640
|
+
cmdVerifyStale,
|
|
1538
1641
|
cmdValidateConsistency,
|
|
1539
1642
|
cmdValidateHealth,
|
|
1540
1643
|
cmdPreflight,
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* history-digest Aggregate all summary.md data
|
|
29
29
|
* summary-extract <path> [--fields] Extract structured data from summary.md
|
|
30
30
|
* state-snapshot Structured parse of state.md (alias: state snapshot)
|
|
31
|
+
* findings record|list|dispose|debt Judges' verdicts and their findings ledger (evidence loop)
|
|
31
32
|
* phase-plan-index <phase> Index plans with waves and status
|
|
32
33
|
* websearch <query> Search web via Brave API (if configured)
|
|
33
34
|
* [--limit N] [--freshness day|week|month]
|
|
@@ -206,6 +207,7 @@ const cost = require('./lib/cost.cjs');
|
|
|
206
207
|
const costRebuild = require('./lib/cost-rebuild.cjs');
|
|
207
208
|
const preview = require('./lib/preview.cjs');
|
|
208
209
|
const reviewDeep = require('./lib/review-deep.cjs');
|
|
210
|
+
const findings = require('./lib/findings.cjs');
|
|
209
211
|
const knowledge = require('./lib/knowledge.cjs');
|
|
210
212
|
const skillAlign = require('./lib/skill-align.cjs');
|
|
211
213
|
const hygiene = require('./lib/hygiene.cjs');
|
|
@@ -331,7 +333,7 @@ async function main() {
|
|
|
331
333
|
+ ' --planning-dir <path> act on an arbitrary project-relative planning tree\n'
|
|
332
334
|
+ ' --all-tracks (hygiene) act on the root tree AND every discovered track\n'
|
|
333
335
|
+ ' env: PAN_TRACK, PAN_PLANNING_DIR (flags win)\n'
|
|
334
|
-
+ '\nCommands: state, resolve-model, estimate-cost, find-phase, git, distill, experiment, commit, verify-summary, template, frontmatter, verify, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section, config-set, config-get, history-digest, phases, roadmap, requirements, phase, milestone, validate, progress, context-budget, todo, scaffold, init, phase-plan-index, state-snapshot, summary-extract, rollback-snapshot, batch-commit, websearch, focus, preflight, dashboard, hud, report, learnings, deps, drift-check, memory, bridge, whatif, knowledge, skills, hygiene, review-deep, preview, cost, models, squad, worktree, campaign, bus, cache, retro, codebase, standards, optimize, doc-lint, learn, links';
|
|
336
|
+
+ '\nCommands: state, resolve-model, estimate-cost, find-phase, git, distill, experiment, commit, verify-summary, template, frontmatter, verify, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section, config-set, config-get, history-digest, phases, roadmap, requirements, phase, milestone, validate, progress, context-budget, todo, scaffold, init, phase-plan-index, state-snapshot, summary-extract, rollback-snapshot, batch-commit, websearch, focus, preflight, dashboard, hud, report, learnings, deps, drift-check, memory, bridge, whatif, knowledge, skills, hygiene, review-deep, findings, preview, cost, models, squad, worktree, campaign, bus, cache, retro, codebase, standards, optimize, doc-lint, learn, links';
|
|
335
337
|
|
|
336
338
|
if (!command) {
|
|
337
339
|
error(USAGE);
|
|
@@ -412,7 +414,7 @@ async function main() {
|
|
|
412
414
|
} else if (subcommand === 'load' || !subcommand) {
|
|
413
415
|
state.cmdStateLoad(cwd, raw);
|
|
414
416
|
} else {
|
|
415
|
-
error(`Unknown state subcommand: ${subcommand}. Available: json, update, get, patch, advance-plan, record-metric, update-progress, add-decision, add-blocker, resolve-blocker, record-session, load, snapshot`);
|
|
417
|
+
error(`Unknown state subcommand: ${subcommand}. Available: json, update, get, patch, compact, advance-plan, record-metric, update-progress, add-decision, add-blocker, resolve-blocker, record-session, load, snapshot`);
|
|
416
418
|
}
|
|
417
419
|
break;
|
|
418
420
|
}
|
|
@@ -607,12 +609,21 @@ async function main() {
|
|
|
607
609
|
verify.cmdVerifyReconcile(cwd, args[2], raw);
|
|
608
610
|
} else if (subcommand === 'stubs') {
|
|
609
611
|
verify.cmdVerifyStubs(cwd, { gate: args.includes('--gate') }, raw);
|
|
612
|
+
} else if (subcommand === 'scope') {
|
|
613
|
+
verify.cmdVerifyScope(cwd, args[2], raw);
|
|
614
|
+
} else if (subcommand === 'stale') {
|
|
615
|
+
verify.cmdVerifyStale(cwd, args[2], raw);
|
|
610
616
|
} else {
|
|
611
|
-
error('Unknown verify subcommand. Available: plan-structure, phase-completeness, references, commits, artifacts, key-links, reconcile, stubs');
|
|
617
|
+
error('Unknown verify subcommand. Available: plan-structure, phase-completeness, references, commits, artifacts, key-links, reconcile, stubs, scope, stale');
|
|
612
618
|
}
|
|
613
619
|
break;
|
|
614
620
|
}
|
|
615
621
|
|
|
622
|
+
case 'version': {
|
|
623
|
+
commands.cmdVersion(cwd, { check: args.includes('--check') }, raw);
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
|
|
616
627
|
case 'generate-slug': {
|
|
617
628
|
commands.cmdGenerateSlug(args[1], raw);
|
|
618
629
|
break;
|
|
@@ -840,7 +851,8 @@ async function main() {
|
|
|
840
851
|
}
|
|
841
852
|
|
|
842
853
|
case 'phase-plan-index': {
|
|
843
|
-
|
|
854
|
+
const failedIdx = args.indexOf('--failed');
|
|
855
|
+
phase.cmdPhasePlanIndex(cwd, args[1], raw, { failed: failedIdx !== -1 ? args[failedIdx + 1] : null });
|
|
844
856
|
break;
|
|
845
857
|
}
|
|
846
858
|
|
|
@@ -914,7 +926,13 @@ async function main() {
|
|
|
914
926
|
output({ error: 'No batch file found. Run focus plan first.' }, raw);
|
|
915
927
|
break;
|
|
916
928
|
}
|
|
917
|
-
|
|
929
|
+
if (batch.error) {
|
|
930
|
+
// The newest batch is the one /pan:focus-exec runs; classifying another would
|
|
931
|
+
// hand it waves for the wrong items, so an unusable newest batch is an error.
|
|
932
|
+
output({ error: batch.error }, raw);
|
|
933
|
+
break;
|
|
934
|
+
}
|
|
935
|
+
items = batch.batch;
|
|
918
936
|
}
|
|
919
937
|
output(focus.classifyStageDependencies(items), raw);
|
|
920
938
|
} else if (subcommand === 'reflection') {
|
|
@@ -1177,6 +1195,44 @@ async function main() {
|
|
|
1177
1195
|
break;
|
|
1178
1196
|
}
|
|
1179
1197
|
|
|
1198
|
+
case 'findings': {
|
|
1199
|
+
// Evidence loop (ADR-0049): judges' verdicts recorded from their reports, and
|
|
1200
|
+
// the dispositions workflows give the findings they continue past.
|
|
1201
|
+
const subcommand = args[1];
|
|
1202
|
+
const filters = {
|
|
1203
|
+
phase: getArgValue(args, '--phase'),
|
|
1204
|
+
agent: getArgValue(args, '--agent'),
|
|
1205
|
+
status: getArgValue(args, '--status'),
|
|
1206
|
+
class: getArgValue(args, '--class'),
|
|
1207
|
+
milestone: getArgValue(args, '--milestone'),
|
|
1208
|
+
};
|
|
1209
|
+
if (subcommand === 'record') {
|
|
1210
|
+
findings.cmdFindingsRecord(cwd, {
|
|
1211
|
+
phase: filters.phase,
|
|
1212
|
+
agent: filters.agent,
|
|
1213
|
+
file: getArgValue(args, '--file'),
|
|
1214
|
+
text: getArgValue(args, '--text'),
|
|
1215
|
+
stdin: args.includes('--stdin'),
|
|
1216
|
+
}, raw);
|
|
1217
|
+
} else if (subcommand === 'list') {
|
|
1218
|
+
findings.cmdFindingsList(cwd, filters, raw);
|
|
1219
|
+
} else if (subcommand === 'dispose') {
|
|
1220
|
+
findings.cmdFindingsDispose(cwd, {
|
|
1221
|
+
ids: findings.findingIdsIn(args.slice(2)),
|
|
1222
|
+
phase: filters.phase,
|
|
1223
|
+
agent: filters.agent,
|
|
1224
|
+
open: args.includes('--open'),
|
|
1225
|
+
as: getArgValue(args, '--as'),
|
|
1226
|
+
reason: getArgValue(args, '--reason'),
|
|
1227
|
+
}, raw);
|
|
1228
|
+
} else if (subcommand === 'debt') {
|
|
1229
|
+
findings.cmdFindingsDebt(cwd, { milestone: filters.milestone }, raw);
|
|
1230
|
+
} else {
|
|
1231
|
+
error('Unknown findings subcommand. Available: record, list, dispose, debt');
|
|
1232
|
+
}
|
|
1233
|
+
break;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1180
1236
|
case 'preview': {
|
|
1181
1237
|
const subcommand = args[1];
|
|
1182
1238
|
if (subcommand === 'phase') {
|
|
@@ -1408,17 +1464,23 @@ async function main() {
|
|
|
1408
1464
|
} else if (subcommand === 'learn') {
|
|
1409
1465
|
optimize.cmdOptimizeLearn(cwd, {
|
|
1410
1466
|
sessionId: getArgValue(args, '--session'),
|
|
1467
|
+
sessions: getArgValue(args, '--sessions'),
|
|
1411
1468
|
}, raw);
|
|
1412
1469
|
} else if (subcommand === 'apply') {
|
|
1413
1470
|
optimize.cmdOptimizeApply(cwd, {
|
|
1414
1471
|
reportPath: getArgValue(args, '--report'),
|
|
1415
1472
|
}, raw);
|
|
1473
|
+
} else if (subcommand === 'revert') {
|
|
1474
|
+
optimize.cmdOptimizeRevert(cwd, {
|
|
1475
|
+
applyId: args[2] && !args[2].startsWith('--') ? args[2] : null,
|
|
1476
|
+
last: args.includes('--last'),
|
|
1477
|
+
}, raw);
|
|
1416
1478
|
} else if (subcommand === 'list') {
|
|
1417
1479
|
optimize.cmdOptimizeList(cwd, raw);
|
|
1418
1480
|
} else if (subcommand === 'stats') {
|
|
1419
1481
|
optimize.cmdOptimizeStats(cwd, raw);
|
|
1420
1482
|
} else {
|
|
1421
|
-
error('Unknown optimize subcommand. Available: trace, learn, apply, list, stats');
|
|
1483
|
+
error('Unknown optimize subcommand. Available: trace, learn, apply, revert, list, stats');
|
|
1422
1484
|
}
|
|
1423
1485
|
break;
|
|
1424
1486
|
}
|
|
@@ -4,7 +4,7 @@ This directory holds AI-derived behavioral patterns extracted from real PAN
|
|
|
4
4
|
Wizard sessions via the **self-improvement loop** (v3.7.0+, see
|
|
5
5
|
[ADR-0026](../../docs/decisions/ADR-0026-self-improvement-loop.md)).
|
|
6
6
|
|
|
7
|
-
Patterns are
|
|
7
|
+
Patterns are promoted by hand with `pan-tools learn promote` (see Lifecycle) from
|
|
8
8
|
harvested experiment data. They are **advisory** — orchestrators weight them
|
|
9
9
|
against current context, not as hard rules.
|
|
10
10
|
|
|
@@ -46,9 +46,10 @@ patterns:
|
|
|
46
46
|
|
|
47
47
|
## Lifecycle
|
|
48
48
|
|
|
49
|
-
1. **Promote** — `pan-tools learn promote --pattern <id> --scope universal --topic <name>` appends a pattern to the topic file (creates the file if absent).
|
|
50
|
-
2. **Unpromote** — `pan-tools learn unpromote --pattern <id> --topic <name>` removes a pattern (for rollback).
|
|
49
|
+
1. **Promote** — `pan-tools learn promote --pattern <id> --scope universal --topic <name> --summary <text> --rule <text>` appends a pattern to the topic file (creates the file if absent); `--evidence`, `--applies-in` and `--source-experiments` fill in the rest of the entry.
|
|
50
|
+
2. **Unpromote** — `pan-tools learn unpromote --pattern <id> --scope <universal|internal> --topic <name>` removes a pattern (for rollback) and deletes the topic file once it holds none.
|
|
51
51
|
3. **List** — `pan-tools learn list-promoted` shows the inventory across both tiers.
|
|
52
|
+
4. **Index** — `pan-tools learn build-index` regenerates `index.json`, the topic index `pan-tools learn topics-for --agent <name>` reads; promote and unpromote do not update it.
|
|
52
53
|
|
|
53
54
|
## Why two tiers
|
|
54
55
|
|
|
@@ -58,13 +59,15 @@ apply when the project *is* PAN. Examples:
|
|
|
58
59
|
- **PAN-internal**: "Always commit individually, never `git add -A`" (because of source repo's pre-commit hooks)
|
|
59
60
|
- **Universal**: "Run the full test suite before marking a phase complete"
|
|
60
61
|
|
|
61
|
-
The promote step
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
The promote step warns when a universal-scope pattern reads like a prompt
|
|
63
|
+
fragment rather than a structural pattern (P-RES-007), and `pan-tools learn lint`
|
|
64
|
+
warns when a universal pattern's heading or Rule names PAN-internal terms such as
|
|
65
|
+
`pan-wizard-core` or `pan-tools`, a candidate for `internal` scope. The human
|
|
66
|
+
running `promote` makes the final call.
|
|
64
67
|
|
|
65
68
|
## Maintenance
|
|
66
69
|
|
|
67
70
|
These files are **AI-managed**. Direct human edits create drift between
|
|
68
|
-
the frontmatter `
|
|
71
|
+
the frontmatter `patterns` list and the body content. For human-authored
|
|
69
72
|
behavioral content, use `references/` instead — that's the canonical
|
|
70
73
|
hand-authored channel (e.g., `references/guardrails.md` shipped in v3.6.0).
|